From 833fb7110886857524d0be74914f83da504fe716 Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Wed, 30 Jul 2025 21:39:45 +0200 Subject: [PATCH 01/18] implemented FUSE lookup function --- Cargo.lock | 40 ++++++++++++++ fat-bits/Cargo.toml | 1 + fat-bits/src/dir.rs | 112 ++++++++++++++++++++++++++------------ fat-dump/src/main.rs | 2 +- fat-fuse/src/fuse.rs | 92 +++++++++++++++++++++++++------ fat-fuse/src/inode.rs | 64 +++++++++++++++++----- fat-fuse/src/lib.rs | 124 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 369 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3caec75..7a86e7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,15 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.30" @@ -74,6 +83,20 @@ dependencies = [ "windows-link", ] +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -99,6 +122,7 @@ dependencies = [ "anyhow", "bitflags", "chrono", + "compact_str", "enum_dispatch", "static_assertions", "thiserror", @@ -126,6 +150,10 @@ dependencies = [ "thiserror", ] +[[package]] +name = "fat-mount" +version = "0.1.0" + [[package]] name = "fuser" version = "0.15.1" @@ -178,6 +206,12 @@ dependencies = [ "cc", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.77" @@ -297,6 +331,12 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "shlex" version = "1.3.0" diff --git a/fat-bits/Cargo.toml b/fat-bits/Cargo.toml index 1986af5..45ec5d4 100644 --- a/fat-bits/Cargo.toml +++ b/fat-bits/Cargo.toml @@ -10,6 +10,7 @@ chrono = { version = "0.4.41", default-features = false, features = [ "alloc", "std", ] } +compact_str = "0.9.0" enum_dispatch = "0.3.13" static_assertions = "1.1.0" thiserror = "2.0.12" diff --git a/fat-bits/src/dir.rs b/fat-bits/src/dir.rs index bc8e7a4..88cd3e6 100644 --- a/fat-bits/src/dir.rs +++ b/fat-bits/src/dir.rs @@ -3,6 +3,7 @@ use std::io::Read; use bitflags::bitflags; use chrono::{NaiveDate, NaiveDateTime, TimeDelta}; +use compact_str::CompactString; use crate::datetime::{Date, Time}; use crate::utils::{load_u16_le, load_u32_le}; @@ -48,7 +49,7 @@ impl Display for Attr { /// represents an entry in a diectory #[derive(Debug)] pub struct DirEntry { - name: [u8; 11], + name: [u8; 13], attr: Attr, create_time_tenths: u8, @@ -64,12 +65,13 @@ pub struct DirEntry { file_size: u32, - long_name: Option, + checksum: u8, + long_name: Option, } impl Display for DirEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut name = self.name_string().unwrap_or_else(|| "".to_owned()); + let mut name = self.name_string().unwrap_or_else(|| "".into()); if self.attr.contains(Attr::Directory) { name.push('/'); @@ -89,12 +91,63 @@ impl Display for DirEntry { } impl DirEntry { + fn load_name(bytes: [u8; 11], attr: &Attr) -> [u8; 13] { + let mut name = [0; 13]; + + let mut iter = name.iter_mut(); + + if attr.contains(Attr::Hidden) && !attr.contains(Attr::VolumeId) { + // hidden file or folder + *iter.next().unwrap() = b'.'; + } + + fn truncate_trailing_spaces(mut bytes: &[u8]) -> &[u8] { + while bytes.last() == Some(&0x20) { + bytes = &bytes[..bytes.len() - 1]; + } + + bytes + } + + let stem_bytes = truncate_trailing_spaces(&bytes[..8]); + let ext_bytes = truncate_trailing_spaces(&bytes[8..]); + + for &c in stem_bytes { + let c = match c { + // reject non-ascii + // TODO: implement code pages? probably not... + c if c >= 128 => b'?', + c => c, + }; + + *iter.next().unwrap() = c; + } + + if !ext_bytes.is_empty() { + *iter.next().unwrap() = b'.'; + + for &c in ext_bytes { + let c = match c { + // reject non-ascii + // TODO: implement code pages? probably not... + c if c >= 128 => b'?', + c => c, + }; + + *iter.next().unwrap() = c; + } + } + + name + } + pub fn load(bytes: &[u8]) -> anyhow::Result { assert_eq!(bytes.len(), 32); - let name = bytes[..11].try_into().unwrap(); let attr = Attr::from_bits_truncate(bytes[11]); + let name = Self::load_name(bytes[..11].try_into().unwrap(), &attr); + let create_time_tenths = bytes[13]; anyhow::ensure!( create_time_tenths <= 199, @@ -142,6 +195,7 @@ impl DirEntry { write_date, file_size, long_name: None, + checksum: Self::checksum(&bytes[..11]), }) } @@ -173,8 +227,6 @@ impl DirEntry { return false; } - // &self.name[..2] == &[b'.', b' '] - self.name[0] == b'.' && &self.name[1..] == &[b' '; 10] } @@ -183,8 +235,6 @@ impl DirEntry { return false; } - // &self.name[..3] == &[b'.', b'.', b' '] - &self.name[..2] == &[b'.', b'.'] && &self.name[2..] == &[b' '; 9] } @@ -216,15 +266,18 @@ impl DirEntry { std::str::from_utf8(self.extension()).ok() } - pub fn name_string(&self) -> Option { + pub fn name_string(&self) -> Option { + // use a CompactString here to allow inlining of short names + // maybe switch to a Cow instead? has disadvantage that we need to alloc for short names + if let Some(long_filename) = self.long_name() { - return Some(long_filename.to_owned()); + return Some(long_filename.into()); } let name = std::str::from_utf8(&self.name[..8]).ok()?.trim_ascii_end(); let ext = std::str::from_utf8(&self.name[8..]).ok()?.trim_ascii_end(); - let mut s = String::new(); + let mut s = CompactString::const_new(""); if self.attr.contains(Attr::Hidden) { s.push('.'); @@ -245,7 +298,7 @@ impl DirEntry { self.long_name.as_deref() } - pub fn set_long_name(&mut self, long_name: String) { + pub fn set_long_name(&mut self, long_name: CompactString) { self.long_name = Some(long_name); } @@ -283,10 +336,10 @@ impl DirEntry { self.file_size } - pub fn checksum(&self) -> u8 { + pub fn checksum(name: &[u8]) -> u8 { let mut checksum: u8 = 0; - for &x in self.name() { + for &x in name { checksum = checksum.rotate_right(1).wrapping_add(x); } @@ -482,9 +535,6 @@ impl LongFilenameBuf { pub struct DirIter { reader: R, - // long_filename_rev_buf: Vec, - // long_filename_checksum: Option, - // long_filename_last_ordinal: Option, long_filename_buf: LongFilenameBuf, } @@ -492,9 +542,6 @@ impl DirIter { pub fn new(reader: R) -> DirIter { DirIter { reader, - // long_filename_rev_buf: Vec::new(), - // long_filename_checksum: None, - // long_filename_last_ordinal: None, long_filename_buf: Default::default(), } } @@ -503,14 +550,10 @@ impl DirIter { fn next_impl(&mut self) -> anyhow::Result> { let mut chunk = [0; 32]; if self.reader.read_exact(&mut chunk).is_err() { - // reading failed; nothing we can do here + // nothing we can do here since we might be in an invalid state after a partial read return Ok(None); } - // let Ok(dir_entry) = DirEntry::load(&chunk) else { - // return self.next(); - // }; - let dir_entry = DirEntryWrapper::load(&chunk) .map_err(|e| anyhow::anyhow!("failed to load dir entry: {e}"))?; @@ -534,25 +577,23 @@ impl DirIter { return self.next_impl(); } - match self + if let Some(iter) = self .long_filename_buf - .get_buf(dir_entry.checksum()) + .get_buf(dir_entry.checksum) .map_err(|e| { anyhow::anyhow!( "failed to get long filename for {}: {}", dir_entry.name_string().as_deref().unwrap_or(""), e ) - })? { - Some(iter) => { - // attach long filename to dir_entry + })? + { + // attach long filename to dir_entry - let long_filename: String = - char::decode_utf16(iter).filter_map(|x| x.ok()).collect(); + let long_filename: CompactString = + char::decode_utf16(iter).filter_map(|x| x.ok()).collect(); - dir_entry.set_long_name(long_filename); - } - None => {} // no long filename -> do nothing + dir_entry.set_long_name(long_filename); } self.long_filename_buf.reset(); @@ -568,6 +609,7 @@ impl Iterator for DirIter { match self.next_impl() { Ok(x) => x, Err(e) => { + // print error message, try next eprintln!("{}", e); self.next() diff --git a/fat-dump/src/main.rs b/fat-dump/src/main.rs index b2fe7c3..2ddc569 100644 --- a/fat-dump/src/main.rs +++ b/fat-dump/src/main.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::rc::Rc; use fat_bits::FatFs; -use fat_bits::dir::{DirEntry, DirIter}; +use fat_bits::dir::DirEntry; use fat_bits::fat::Fatty as _; pub fn main() -> anyhow::Result<()> { diff --git a/fat-fuse/src/fuse.rs b/fat-fuse/src/fuse.rs index 6168950..512b99d 100644 --- a/fat-fuse/src/fuse.rs +++ b/fat-fuse/src/fuse.rs @@ -1,11 +1,13 @@ use std::ffi::c_int; use std::time::Duration; +use fat_bits::dir::DirEntry; use fuser::Filesystem; -use libc::{EIO, ENOENT, ENOSYS, ENOTDIR, EPERM}; +use libc::{EIO, ENOENT, ENOSYS, EPERM}; use log::{debug, warn}; use crate::FatFuse; +use crate::inode::Inode; impl Filesystem for FatFuse { fn init( @@ -28,35 +30,91 @@ impl Filesystem for FatFuse { // warn!("[Not Implemented] lookup(parent: {:#x?}, name {:?})", parent, name); // reply.error(ENOSYS); - let parent_inode = if let Some(inode) = self.get_inode(parent) { - inode - } else { + let Some(name) = name.to_str() else { + // TODO: add proper handling of non-utf8 strings + reply.error(ENOSYS); + return; + }; + + let Some(parent_inode) = self.get_inode(parent) else { // parent inode does not exist - // TODO: how can we make sure this does not exist? - // panic? + // TODO: how can we make sure this does not happed? + // TODO: panic? reply.error(EIO); return; }; - let Ok(dir_iter) = parent_inode.dir_iter(&self.fat_fs) else { - reply.error(ENOTDIR); - return; + // let Ok(mut dir_iter) = parent_inode.dir_iter(&self.fat_fs) else { + // reply.error(ENOTDIR); + // return; + // }; + + // let Some(dir_entry) = + // dir_iter.find(|dir_entry| dir_entry.name_string().as_deref() == Some(name)) + // else { + // reply.error(ENOENT); + // return; + // }; + + let dir_entry: DirEntry = match parent_inode + .dir_iter(&self.fat_fs) + // .map_err(|_| ENOTDIR) + .and_then(|mut dir_iter| { + dir_iter + .find(|dir_entry| dir_entry.name_string().as_deref() == Some(name)) + .ok_or(ENOENT) + }) { + Ok(dir_entry) => dir_entry, + Err(err) => { + reply.error(err); + + return; + } }; - let Some(dir_entry) = - dir_iter.find(|dir_entry| dir_entry.name_str().as_deref() == Some("")) - else { - reply.error(ENOENT); - return; + let inode = match self.get_inode_by_first_cluster(dir_entry.first_cluster()) { + Some(inode) => inode, + None => { + // no inode found, make a new one + + let ino = self.next_ino(); + + let inode = Inode::new(&self.fat_fs, dir_entry, ino, self.uid, self.gid); + + self.insert_inode(inode) + } }; - reply.entry(&Duration::from_secs(1), attr, generation); + let attr = inode.file_attr(); + let generation = inode.generation(); - todo!(); + reply.entry(&Duration::from_secs(1), &attr, generation as u64); } - fn forget(&mut self, _req: &fuser::Request<'_>, _ino: u64, _nlookup: u64) {} + fn forget(&mut self, _req: &fuser::Request<'_>, ino: u64, nlookup: u64) { + let Some(inode) = self.get_inode_mut(ino) else { + debug!("tried to forget {} refs of inode {}, but was not found", ino, nlookup); + + return; + }; + + let ref_count = inode.ref_count_mut(); + + if *ref_count < nlookup { + debug!( + "tried to forget {} refs of inode {}, but ref_count is only {}", + nlookup, ino, *ref_count + ); + } + + *ref_count = ref_count.saturating_sub(nlookup); + + if *ref_count == 0 { + // no more references, drop inode + self.drop_inode(ino); + } + } fn getattr( &mut self, diff --git a/fat-fuse/src/inode.rs b/fat-fuse/src/inode.rs index d46d143..96c8000 100644 --- a/fat-fuse/src/inode.rs +++ b/fat-fuse/src/inode.rs @@ -3,8 +3,9 @@ use std::time::SystemTime; use chrono::{NaiveDateTime, NaiveTime}; use fat_bits::FatFs; -use fat_bits::dir::{DirEntry, DirIter}; +use fat_bits::dir::DirEntry; use fuser::FileAttr; +use libc::ENOTDIR; use rand::{Rng, SeedableRng as _}; thread_local! { @@ -19,7 +20,10 @@ thread_local! { static RNG: LazyCell> = LazyCell::new(|| RefCell::new(rand::rngs::SmallRng::from_os_rng())); } -fn get_random_u32() -> u32 { +fn get_random() -> T +where + rand::distr::StandardUniform: rand::distr::Distribution, +{ // RNG.with(|x| unsafe { // let rng = &mut (*x.get()); @@ -50,8 +54,12 @@ const ROOT_INO: u64 = 1; #[allow(dead_code)] pub struct Inode { ino: u64, + // FUSE uses a u64 for generation, but the Linux kernel only handles u32s anyway, truncating + // the high bits, so using more is pretty pointless and possibly even detrimental generation: u32, + ref_count: u64, + size: u64, block_size: u32, @@ -72,10 +80,21 @@ pub struct Inode { #[allow(dead_code)] impl Inode { - pub fn new(fat_fs: &FatFs, dir_entry: DirEntry, uid: u32, gid: u32) -> Inode { + fn new_generation() -> u32 { + let rand: u16 = get_random(); + + let secs = SystemTime::UNIX_EPOCH + .elapsed() + .map(|dur| dur.as_secs() as u16) + .unwrap_or(0); + + ((secs as u32) << 16) | rand as u32 + } + + pub fn new(fat_fs: &FatFs, dir_entry: DirEntry, ino: u64, uid: u32, gid: u32) -> Inode { assert!(dir_entry.is_file() || dir_entry.is_dir()); - let generation = get_random_u32(); + let generation = Self::new_generation(); let kind = if dir_entry.is_dir() { Kind::Dir @@ -96,8 +115,9 @@ impl Inode { let crtime = datetime_to_system(dir_entry.create_time()); Inode { - ino: dir_entry.first_cluster() as u64, + ino, generation, + ref_count: 0, size: dir_entry.file_size() as u64, block_size: fat_fs.bpb().bytes_per_sector() as u32, kind, @@ -111,6 +131,26 @@ impl Inode { } } + pub fn ino(&self) -> u64 { + self.ino + } + + pub fn generation(&self) -> u32 { + self.generation + } + + pub fn ref_count(&self) -> u64 { + self.ref_count + } + + pub fn ref_count_mut(&mut self) -> &mut u64 { + &mut self.ref_count + } + + pub fn first_cluster(&self) -> u32 { + self.first_cluster + } + pub fn file_attr(&self) -> FileAttr { let perm = if self.read_only { 0o555 } else { 0o777 }; @@ -133,11 +173,12 @@ impl Inode { } } - pub fn dir_iter(&self, fat_fs: &FatFs) -> anyhow::Result> { - anyhow::ensure!(self.kind == Kind::Dir, "cannot dir_iter on a file"); + pub fn dir_iter(&self, fat_fs: &FatFs) -> Result, i32> { + // anyhow::ensure!(self.kind == Kind::Dir, "cannot dir_iter on a file"); - // TODO: the boxing here is not particularly pretty, but neccessary, since the DirIter for - // the root holds a + if self.kind != Kind::Dir { + return Err(ENOTDIR); + } if self.ino == ROOT_INO { // root dir @@ -145,9 +186,6 @@ impl Inode { return Ok(fat_fs.root_dir_iter()); } - let chain_reader = fat_fs.chain_reader(self.first_cluster); - - // TODO: get rid of this Box if the boxing is removed from root_dir_iter - Ok(DirIter::new(Box::new(chain_reader))) + Ok(fat_fs.dir_iter(self.first_cluster)) } } diff --git a/fat-fuse/src/lib.rs b/fat-fuse/src/lib.rs index b8499d8..e3a77c3 100644 --- a/fat-fuse/src/lib.rs +++ b/fat-fuse/src/lib.rs @@ -6,6 +6,7 @@ use std::collections::BTreeMap; use std::rc::Rc; use fat_bits::{FatFs, SliceLike}; +use log::debug; use crate::inode::Inode; @@ -16,9 +17,12 @@ pub struct FatFuse { uid: u32, gid: u32, + next_ino: u64, next_fd: u32, inode_table: BTreeMap, + + ino_by_first_cluster: BTreeMap, } impl FatFuse { @@ -28,15 +32,105 @@ impl FatFuse { let fat_fs = FatFs::load(data)?; + // TODO: build and insert root dir inode + Ok(FatFuse { fat_fs, uid, gid, + next_ino: 2, // 0 is reserved and 1 is root next_fd: 0, inode_table: BTreeMap::new(), + ino_by_first_cluster: BTreeMap::new(), }) } + fn next_ino(&mut self) -> u64 { + let ino = self.next_ino; + + assert!(!self.inode_table.contains_key(&ino)); + + self.next_ino += 1; + + ino + } + + fn insert_inode(&mut self, inode: Inode) -> &mut Inode { + let ino = inode.ino(); + let generation = inode.generation(); + let first_cluster = inode.first_cluster(); + + // let old_inode = self.inode_table.insert(ino, inode); + + let entry = self.inode_table.entry(ino); + + let (new_inode, old_inode): (&mut Inode, Option) = match entry { + std::collections::btree_map::Entry::Vacant(vacant_entry) => { + let new_inode = vacant_entry.insert(inode); + (new_inode, None) + } + std::collections::btree_map::Entry::Occupied(occupied_entry) => { + let entry_ref = occupied_entry.into_mut(); + + let old_inode = std::mem::replace(entry_ref, inode); + + (entry_ref, Some(old_inode)) + } + }; + + let old_ino = self.ino_by_first_cluster.insert(first_cluster, ino); + + debug!( + "inserted new inode with ino {} and generation {} (first cluster: {})", + ino, generation, first_cluster + ); + + if let Some(old_inode) = old_inode { + debug!("ejected inode {} {}", old_inode.ino(), old_inode.generation()); + } + + if let Some(old_ino) = old_ino { + debug!("ejected old {} -> {} cluster to ino mapping", first_cluster, old_ino); + } + + new_inode + } + + fn drop_inode(&mut self, ino: u64) { + let Some(inode) = self.inode_table.remove(&ino) else { + debug!("tried to drop inode with ino {}, but was not in table", ino); + + return; + }; + + let first_cluster = inode.first_cluster(); + + let entry = self.ino_by_first_cluster.entry(first_cluster); + + match entry { + std::collections::btree_map::Entry::Vacant(_) => debug!( + "removed inode with ino {} from table, but it's first cluster did not point to any ino", + ino + ), + std::collections::btree_map::Entry::Occupied(occupied_entry) => { + let found_ino = *occupied_entry.get(); + + if found_ino == ino { + // matches our inode, remove it + occupied_entry.remove(); + } else { + // does not match removed inode, leave it as is + debug!( + "removed inode with ino {} from table, but it's first cluster pointer to ino {} instead", + ino, found_ino + ); + } + } + } + + todo!() + } + fn get_inode(&self, ino: u64) -> Option<&Inode> { self.inode_table.get(&ino) } @@ -44,4 +138,34 @@ impl FatFuse { fn get_inode_mut(&mut self, ino: u64) -> Option<&mut Inode> { self.inode_table.get_mut(&ino) } + + pub fn get_inode_by_first_cluster(&self, first_cluster: u32) -> Option<&Inode> { + let ino = self.ino_by_first_cluster.get(&first_cluster)?; + + if let Some(inode) = self.inode_table.get(ino) { + Some(inode) + } else { + debug!( + "first cluster {} is mapped to ino {}, but inode is not in table", + first_cluster, ino + ); + + None + } + } + + pub fn get_inode_by_first_cluster_mut(&mut self, first_cluster: u32) -> Option<&mut Inode> { + let ino = self.ino_by_first_cluster.get(&first_cluster)?; + + if let Some(inode) = self.inode_table.get_mut(ino) { + Some(inode) + } else { + debug!( + "first cluster {} is mapped to ino {}, but inode is not in table", + first_cluster, ino + ); + + None + } + } } From 026e145ccbd81e66294b0af922f2428d856cf769 Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Wed, 30 Jul 2025 21:53:18 +0200 Subject: [PATCH 02/18] added fat-mount --- Cargo.lock | 273 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- fat-mount/Cargo.toml | 10 ++ fat-mount/src/main.rs | 29 +++++ 4 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 fat-mount/Cargo.toml create mode 100644 fat-mount/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 7a86e7e..b60f235 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -17,6 +26,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -83,6 +142,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "compact_str" version = "0.9.0" @@ -115,6 +180,29 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "fat-bits" version = "0.1.0" @@ -153,6 +241,12 @@ dependencies = [ [[package]] name = "fat-mount" version = "0.1.0" +dependencies = [ + "anyhow", + "env_logger", + "fat-fuse", + "fuser", +] [[package]] name = "fuser" @@ -206,12 +300,42 @@ dependencies = [ "cc", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -267,6 +391,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "page_size" version = "0.6.0" @@ -283,6 +413,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -325,6 +470,35 @@ dependencies = [ "getrandom", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustversion" version = "1.0.21" @@ -337,6 +511,26 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "shlex" version = "1.3.0" @@ -392,6 +586,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -540,6 +740,79 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index d353d2e..e944d31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["fat-bits", "fat-dump", "fat-fuse"] +members = ["fat-bits", "fat-dump", "fat-fuse", "fat-mount"] diff --git a/fat-mount/Cargo.toml b/fat-mount/Cargo.toml new file mode 100644 index 0000000..e9f82da --- /dev/null +++ b/fat-mount/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "fat-mount" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.98" +env_logger = "0.11.8" +fat-fuse = { version = "0.1.0", path = "../fat-fuse" } +fuser = "0.15.1" diff --git a/fat-mount/src/main.rs b/fat-mount/src/main.rs new file mode 100644 index 0000000..f9ea244 --- /dev/null +++ b/fat-mount/src/main.rs @@ -0,0 +1,29 @@ +use std::cell::RefCell; +use std::fs::File; +use std::rc::Rc; + +use fat_fuse::FatFuse; +use fuser::MountOption; + +fn main() -> anyhow::Result<()> { + env_logger::init(); + + let mut args = std::env::args(); + + let path = args.next().ok_or(anyhow::anyhow!("missing fs path"))?; + let mountpoint = args.next().ok_or(anyhow::anyhow!("missing mount point"))?; + + let file = File::open(path)?; + + let fat_fuse = FatFuse::new(Rc::new(RefCell::new(file)))?; + + let options = vec![ + MountOption::RO, + MountOption::FSName("fat-fuse".to_owned()), + MountOption::AutoUnmount, + ]; + + fuser::mount2(fat_fuse, mountpoint, &options).unwrap(); + + Ok(()) +} From 8b55d8d13c20aa58bf3c4f2df7a096b021500aee Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Thu, 31 Jul 2025 00:22:16 +0200 Subject: [PATCH 03/18] strip 0 terminator from long name --- fat-bits/src/dir.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fat-bits/src/dir.rs b/fat-bits/src/dir.rs index 88cd3e6..253506f 100644 --- a/fat-bits/src/dir.rs +++ b/fat-bits/src/dir.rs @@ -469,6 +469,13 @@ impl LongFilenameBuf { name = &name[..name.len() - 1]; } + if name.last() == Some(&0) { + name = &name[..name.len() - 1]; + } else { + // no null terminator only for names that are multiples of 13, i.e. perfectly fit + assert_eq!(name.len(), 13); + } + assert!(!name.is_empty()); self.extend_name(name); From f708ab0b501183d021d9d0ffaab046b2d953759c Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Thu, 31 Jul 2025 01:07:01 +0200 Subject: [PATCH 04/18] moved the Rc> back into FatFs --- fat-bits/src/bpb.rs | 1 + fat-bits/src/lib.rs | 9 ++++++++- fat-dump/src/main.rs | 2 +- fat-fuse/src/lib.rs | 5 ++++- fat-mount/src/main.rs | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/fat-bits/src/bpb.rs b/fat-bits/src/bpb.rs index 26e4223..362346c 100644 --- a/fat-bits/src/bpb.rs +++ b/fat-bits/src/bpb.rs @@ -236,6 +236,7 @@ impl Bpb { if self.fat_type() == FatType::Fat32 { return None; } + Some(self.fat_offset() + self.sector_to_offset(self.num_fats() as u32 * self.fat_size())) } diff --git a/fat-bits/src/lib.rs b/fat-bits/src/lib.rs index f17275c..5db4130 100644 --- a/fat-bits/src/lib.rs +++ b/fat-bits/src/lib.rs @@ -98,8 +98,15 @@ pub struct FatFs { fat: fat::Fat, } +unsafe impl Send for FatFs {} + impl FatFs { - pub fn load(data: Rc>) -> anyhow::Result { + pub fn load(data: S) -> anyhow::Result + where + S: SliceLike + Send + 'static, + { + let data = Rc::new(RefCell::new(data)); + let mut bpb_bytes = [0; 512]; data.borrow_mut().read_at_offset(0, &mut bpb_bytes)?; diff --git a/fat-dump/src/main.rs b/fat-dump/src/main.rs index 2ddc569..39e16ff 100644 --- a/fat-dump/src/main.rs +++ b/fat-dump/src/main.rs @@ -22,7 +22,7 @@ pub fn main() -> anyhow::Result<()> { // println!("{}", bpb); - let fat_fs = FatFs::load(Rc::new(RefCell::new(file)))?; + let fat_fs = FatFs::load(file)?; println!("{}", fat_fs.bpb()); println!(); diff --git a/fat-fuse/src/lib.rs b/fat-fuse/src/lib.rs index e3a77c3..309e0e0 100644 --- a/fat-fuse/src/lib.rs +++ b/fat-fuse/src/lib.rs @@ -26,7 +26,10 @@ pub struct FatFuse { } impl FatFuse { - pub fn new(data: Rc>) -> anyhow::Result { + pub fn new(data: S) -> anyhow::Result + where + S: SliceLike + Send + 'static, + { let uid = unsafe { libc::getuid() }; let gid = unsafe { libc::getgid() }; diff --git a/fat-mount/src/main.rs b/fat-mount/src/main.rs index f9ea244..d1e5bef 100644 --- a/fat-mount/src/main.rs +++ b/fat-mount/src/main.rs @@ -15,7 +15,7 @@ fn main() -> anyhow::Result<()> { let file = File::open(path)?; - let fat_fuse = FatFuse::new(Rc::new(RefCell::new(file)))?; + let fat_fuse = FatFuse::new(file)?; let options = vec![ MountOption::RO, From df20ae6f0ce4b929de68730cdc1a1c9e4af6e738 Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Thu, 31 Jul 2025 01:07:48 +0200 Subject: [PATCH 05/18] fixed and update fat-mount --- Cargo.lock | 25 ++++++++++++++++++++++++- fat-mount/Cargo.toml | 1 + fat-mount/src/main.rs | 17 ++++++++++++++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b60f235..a9c6e8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,6 +168,16 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "ctrlc" +version = "3.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" +dependencies = [ + "nix 0.30.1", + "windows-sys", +] + [[package]] name = "enum_dispatch" version = "0.3.13" @@ -243,6 +253,7 @@ name = "fat-mount" version = "0.1.0" dependencies = [ "anyhow", + "ctrlc", "env_logger", "fat-fuse", "fuser", @@ -257,7 +268,7 @@ dependencies = [ "libc", "log", "memchr", - "nix", + "nix 0.29.0", "page_size", "pkg-config", "smallvec", @@ -376,6 +387,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-traits" version = "0.2.19" diff --git a/fat-mount/Cargo.toml b/fat-mount/Cargo.toml index e9f82da..ba48f80 100644 --- a/fat-mount/Cargo.toml +++ b/fat-mount/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] anyhow = "1.0.98" +ctrlc = "3.4.7" env_logger = "0.11.8" fat-fuse = { version = "0.1.0", path = "../fat-fuse" } fuser = "0.15.1" diff --git a/fat-mount/src/main.rs b/fat-mount/src/main.rs index d1e5bef..31a5a8f 100644 --- a/fat-mount/src/main.rs +++ b/fat-mount/src/main.rs @@ -1,6 +1,5 @@ -use std::cell::RefCell; use std::fs::File; -use std::rc::Rc; +use std::sync::mpsc::channel; use fat_fuse::FatFuse; use fuser::MountOption; @@ -10,6 +9,7 @@ fn main() -> anyhow::Result<()> { let mut args = std::env::args(); + let _prog_name = args.next().unwrap(); let path = args.next().ok_or(anyhow::anyhow!("missing fs path"))?; let mountpoint = args.next().ok_or(anyhow::anyhow!("missing mount point"))?; @@ -23,7 +23,18 @@ fn main() -> anyhow::Result<()> { MountOption::AutoUnmount, ]; - fuser::mount2(fat_fuse, mountpoint, &options).unwrap(); + let (tx, rx) = channel(); + + ctrlc::set_handler(move || { + tx.send(()).unwrap(); + }) + .unwrap(); + + let handle = fuser::spawn_mount2(fat_fuse, mountpoint, &options)?; + + rx.recv().unwrap(); + + println!("done"); Ok(()) } From 7a4d89a09a4f43885f68ebfed5dee8fdc0be1817 Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Thu, 31 Jul 2025 01:11:27 +0200 Subject: [PATCH 06/18] removed some FUSE ops that will not be implemented (for now) --- fat-fuse/src/fuse.rs | 195 +------------------------------------------ 1 file changed, 4 insertions(+), 191 deletions(-) diff --git a/fat-fuse/src/fuse.rs b/fat-fuse/src/fuse.rs index 512b99d..c2a2e2d 100644 --- a/fat-fuse/src/fuse.rs +++ b/fat-fuse/src/fuse.rs @@ -214,21 +214,6 @@ impl Filesystem for FatFuse { reply.error(ENOSYS); } - fn symlink( - &mut self, - _req: &fuser::Request<'_>, - parent: u64, - link_name: &std::ffi::OsStr, - target: &std::path::Path, - reply: fuser::ReplyEntry, - ) { - debug!( - "[Not Implemented] symlink(parent: {:#x?}, link_name: {:?}, target: {:?})", - parent, link_name, target, - ); - reply.error(EPERM); - } - fn rename( &mut self, _req: &fuser::Request<'_>, @@ -262,6 +247,10 @@ impl Filesystem for FatFuse { reply.error(EPERM); } + if let Some(old_ino) = self.ino_by_fh.insert(fh, ino) { + debug!("fh {} was associated with ino {}, now with ino {}", fh, old_ino, ino); + } + fn open(&mut self, _req: &fuser::Request<'_>, _ino: u64, _flags: i32, reply: fuser::ReplyOpen) { reply.opened(0, 0); } @@ -418,62 +407,6 @@ impl Filesystem for FatFuse { reply.statfs(0, 0, 0, 0, 0, 512, 255, 0); } - fn setxattr( - &mut self, - _req: &fuser::Request<'_>, - ino: u64, - name: &std::ffi::OsStr, - _value: &[u8], - flags: i32, - position: u32, - reply: fuser::ReplyEmpty, - ) { - debug!( - "[Not Implemented] setxattr(ino: {:#x?}, name: {:?}, flags: {:#x?}, position: {})", - ino, name, flags, position - ); - reply.error(ENOSYS); - } - - fn getxattr( - &mut self, - _req: &fuser::Request<'_>, - ino: u64, - name: &std::ffi::OsStr, - size: u32, - reply: fuser::ReplyXattr, - ) { - debug!("[Not Implemented] getxattr(ino: {:#x?}, name: {:?}, size: {})", ino, name, size); - reply.error(ENOSYS); - } - - fn listxattr( - &mut self, - _req: &fuser::Request<'_>, - ino: u64, - size: u32, - reply: fuser::ReplyXattr, - ) { - debug!("[Not Implemented] listxattr(ino: {:#x?}, size: {})", ino, size); - reply.error(ENOSYS); - } - - fn removexattr( - &mut self, - _req: &fuser::Request<'_>, - ino: u64, - name: &std::ffi::OsStr, - reply: fuser::ReplyEmpty, - ) { - debug!("[Not Implemented] removexattr(ino: {:#x?}, name: {:?})", ino, name); - reply.error(ENOSYS); - } - - fn access(&mut self, _req: &fuser::Request<'_>, ino: u64, mask: i32, reply: fuser::ReplyEmpty) { - debug!("[Not Implemented] access(ino: {:#x?}, mask: {})", ino, mask); - reply.error(ENOSYS); - } - fn create( &mut self, _req: &fuser::Request<'_>, @@ -492,104 +425,6 @@ impl Filesystem for FatFuse { reply.error(ENOSYS); } - fn getlk( - &mut self, - _req: &fuser::Request<'_>, - ino: u64, - fh: u64, - lock_owner: u64, - start: u64, - end: u64, - typ: i32, - pid: u32, - reply: fuser::ReplyLock, - ) { - debug!( - "[Not Implemented] getlk(ino: {:#x?}, fh: {}, lock_owner: {}, start: {}, \ - end: {}, typ: {}, pid: {})", - ino, fh, lock_owner, start, end, typ, pid - ); - reply.error(ENOSYS); - } - - fn setlk( - &mut self, - _req: &fuser::Request<'_>, - ino: u64, - fh: u64, - lock_owner: u64, - start: u64, - end: u64, - typ: i32, - pid: u32, - sleep: bool, - reply: fuser::ReplyEmpty, - ) { - debug!( - "[Not Implemented] setlk(ino: {:#x?}, fh: {}, lock_owner: {}, start: {}, \ - end: {}, typ: {}, pid: {}, sleep: {})", - ino, fh, lock_owner, start, end, typ, pid, sleep - ); - reply.error(ENOSYS); - } - - fn bmap( - &mut self, - _req: &fuser::Request<'_>, - ino: u64, - blocksize: u32, - idx: u64, - reply: fuser::ReplyBmap, - ) { - debug!( - "[Not Implemented] bmap(ino: {:#x?}, blocksize: {}, idx: {})", - ino, blocksize, idx, - ); - reply.error(ENOSYS); - } - - fn ioctl( - &mut self, - _req: &fuser::Request<'_>, - ino: u64, - fh: u64, - flags: u32, - cmd: u32, - in_data: &[u8], - out_size: u32, - reply: fuser::ReplyIoctl, - ) { - debug!( - "[Not Implemented] ioctl(ino: {:#x?}, fh: {}, flags: {}, cmd: {}, \ - in_data.len(): {}, out_size: {})", - ino, - fh, - flags, - cmd, - in_data.len(), - out_size, - ); - reply.error(ENOSYS); - } - - fn fallocate( - &mut self, - _req: &fuser::Request<'_>, - ino: u64, - fh: u64, - offset: i64, - length: i64, - mode: i32, - reply: fuser::ReplyEmpty, - ) { - debug!( - "[Not Implemented] fallocate(ino: {:#x?}, fh: {}, offset: {}, \ - length: {}, mode: {})", - ino, fh, offset, length, mode - ); - reply.error(ENOSYS); - } - fn lseek( &mut self, _req: &fuser::Request<'_>, @@ -605,26 +440,4 @@ impl Filesystem for FatFuse { ); reply.error(ENOSYS); } - - fn copy_file_range( - &mut self, - _req: &fuser::Request<'_>, - ino_in: u64, - fh_in: u64, - offset_in: i64, - ino_out: u64, - fh_out: u64, - offset_out: i64, - len: u64, - flags: u32, - reply: fuser::ReplyWrite, - ) { - debug!( - "[Not Implemented] copy_file_range(ino_in: {:#x?}, fh_in: {}, \ - offset_in: {}, ino_out: {:#x?}, fh_out: {}, offset_out: {}, \ - len: {}, flags: {})", - ino_in, fh_in, offset_in, ino_out, fh_out, offset_out, len, flags - ); - reply.error(ENOSYS); - } } From bdcda26d77059b083481844db8113162a08334ab Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Thu, 31 Jul 2025 01:13:32 +0200 Subject: [PATCH 07/18] cluster 0 as 'empty' marker --- fat-bits/src/lib.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/fat-bits/src/lib.rs b/fat-bits/src/lib.rs index 5db4130..bd50960 100644 --- a/fat-bits/src/lib.rs +++ b/fat-bits/src/lib.rs @@ -185,12 +185,28 @@ impl FatFs { } pub fn cluster_as_subslice_mut(&mut self, cluster: u32) -> SubSliceMut<'_> { + if cluster == 0 { + // for cluster 0 simply return empty subslice + // this makes things a bit easier, since cluster 0 is used as a marker that a file/dir + // is empty + + SubSliceMut::new(self, 0, 0); + } + let offset = self.data_cluster_to_offset(cluster); SubSliceMut::new(self, offset, self.bytes_per_cluster) } pub fn cluster_as_subslice(&self, cluster: u32) -> SubSlice<'_> { + if cluster == 0 { + // for cluster 0 simply return empty subslice + // this makes things a bit easier, since cluster 0 is used as a marker that a file/dir + // is empty + + SubSlice::new(self, 0, 0); + } + let offset = self.data_cluster_to_offset(cluster); SubSlice::new(self, offset, self.bytes_per_cluster) From 7f6c304709ecf04c909a6f75a0ee4dcac9a3c667 Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Thu, 31 Jul 2025 01:14:28 +0200 Subject: [PATCH 08/18] ls/ll works now --- fat-dump/src/main.rs | 3 - fat-fuse/src/fuse.rs | 210 ++++++++++++++++++++++++++++++++---------- fat-fuse/src/inode.rs | 60 +++++++++++- fat-fuse/src/lib.rs | 148 ++++++++++++++++++++++------- fat-mount/src/main.rs | 2 +- 5 files changed, 334 insertions(+), 89 deletions(-) diff --git a/fat-dump/src/main.rs b/fat-dump/src/main.rs index 39e16ff..539bfe0 100644 --- a/fat-dump/src/main.rs +++ b/fat-dump/src/main.rs @@ -1,6 +1,3 @@ -use std::cell::RefCell; -use std::rc::Rc; - use fat_bits::FatFs; use fat_bits::dir::DirEntry; use fat_bits::fat::Fatty as _; diff --git a/fat-fuse/src/fuse.rs b/fat-fuse/src/fuse.rs index c2a2e2d..366ccdd 100644 --- a/fat-fuse/src/fuse.rs +++ b/fat-fuse/src/fuse.rs @@ -2,12 +2,13 @@ use std::ffi::c_int; use std::time::Duration; use fat_bits::dir::DirEntry; -use fuser::Filesystem; -use libc::{EIO, ENOENT, ENOSYS, EPERM}; +use fuser::{FileType, Filesystem}; +use libc::{EINVAL, EIO, ENOENT, ENOSYS, ENOTDIR}; use log::{debug, warn}; -use crate::FatFuse; -use crate::inode::Inode; +use crate::{FatFuse, Inode}; + +const TTL: Duration = Duration::from_secs(1); impl Filesystem for FatFuse { fn init( @@ -32,14 +33,18 @@ impl Filesystem for FatFuse { let Some(name) = name.to_str() else { // TODO: add proper handling of non-utf8 strings + debug!("cannot convert OsStr {:?} to str", name); reply.error(ENOSYS); return; }; + debug!("looking up file {} with parent ino {}", name, parent); + let Some(parent_inode) = self.get_inode(parent) else { // parent inode does not exist // TODO: how can we make sure this does not happed? // TODO: panic? + debug!("could not find inode for parent ino {}", parent); reply.error(EIO); return; @@ -67,29 +72,33 @@ impl Filesystem for FatFuse { }) { Ok(dir_entry) => dir_entry, Err(err) => { + debug!("error: {}", err); reply.error(err); return; } }; - let inode = match self.get_inode_by_first_cluster(dir_entry.first_cluster()) { - Some(inode) => inode, - None => { - // no inode found, make a new one + // let inode = match self.get_inode_by_first_cluster(dir_entry.first_cluster()) { + // Some(inode) => inode, + // None => { + // // no inode found, make a new one + // let ino = self.next_ino(); - let ino = self.next_ino(); + // let inode = Inode::new(&self.fat_fs, &dir_entry, ino, self.uid, self.gid); - let inode = Inode::new(&self.fat_fs, dir_entry, ino, self.uid, self.gid); + // self.insert_inode(inode) + // } + // }; - self.insert_inode(inode) - } - }; + let inode = self.get_or_make_inode_by_dir_entry(&dir_entry); let attr = inode.file_attr(); let generation = inode.generation(); - reply.entry(&Duration::from_secs(1), &attr, generation as u64); + reply.entry(&TTL, &attr, generation as u64); + + inode.refcount_inc(); } fn forget(&mut self, _req: &fuser::Request<'_>, ino: u64, nlookup: u64) { @@ -99,18 +108,9 @@ impl Filesystem for FatFuse { return; }; - let ref_count = inode.ref_count_mut(); + // *ref_count = ref_count.saturating_sub(nlookup); - if *ref_count < nlookup { - debug!( - "tried to forget {} refs of inode {}, but ref_count is only {}", - nlookup, ino, *ref_count - ); - } - - *ref_count = ref_count.saturating_sub(nlookup); - - if *ref_count == 0 { + if inode.refcount_dec(nlookup) == 0 { // no more references, drop inode self.drop_inode(ino); } @@ -123,8 +123,28 @@ impl Filesystem for FatFuse { fh: Option, reply: fuser::ReplyAttr, ) { - warn!("[Not Implemented] getattr(ino: {:#x?}, fh: {:#x?})", ino, fh); - reply.error(ENOSYS); + // warn!("[Not Implemented] getattr(ino: {:#x?}, fh: {:#x?})", ino, fh); + // reply.error(ENOSYS); + + let inode = if let Some(fh) = fh { + let Some(inode) = self.get_inode_by_fh(fh) else { + reply.error(EIO); + + return; + }; + + inode + } else if let Some(inode) = self.get_inode(ino) { + inode + } else { + reply.error(EIO); + + return; + }; + + let attr = inode.file_attr(); + + reply.attr(&TTL, &attr); } fn setattr( @@ -232,27 +252,19 @@ impl Filesystem for FatFuse { reply.error(ENOSYS); } - fn link( - &mut self, - _req: &fuser::Request<'_>, - ino: u64, - newparent: u64, - newname: &std::ffi::OsStr, - reply: fuser::ReplyEntry, - ) { - debug!( - "[Not Implemented] link(ino: {:#x?}, newparent: {:#x?}, newname: {:?})", - ino, newparent, newname - ); - reply.error(EPERM); - } + fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, _flags: i32, reply: fuser::ReplyOpen) { + if !self.inode_table.contains_key(&ino) { + reply.error(EINVAL); + return; + } + + let fh = self.next_fh(); if let Some(old_ino) = self.ino_by_fh.insert(fh, ino) { debug!("fh {} was associated with ino {}, now with ino {}", fh, old_ino, ino); } - fn open(&mut self, _req: &fuser::Request<'_>, _ino: u64, _flags: i32, reply: fuser::ReplyOpen) { - reply.opened(0, 0); + reply.opened(fh, 0); } fn read( @@ -343,11 +355,17 @@ impl Filesystem for FatFuse { fn opendir( &mut self, _req: &fuser::Request<'_>, - _ino: u64, + ino: u64, _flags: i32, reply: fuser::ReplyOpen, ) { - reply.opened(0, 0); + let fh = self.next_fh(); + + if let Some(old_ino) = self.ino_by_fh.insert(fh, ino) { + debug!("fh {} was already associated with ino {}, now with ino {}", fh, old_ino, ino); + } + + reply.opened(fh, 0); } fn readdir( @@ -356,10 +374,79 @@ impl Filesystem for FatFuse { ino: u64, fh: u64, offset: i64, - reply: fuser::ReplyDirectory, + mut reply: fuser::ReplyDirectory, ) { - warn!("[Not Implemented] readdir(ino: {:#x?}, fh: {}, offset: {})", ino, fh, offset); - reply.error(ENOSYS); + let Ok(mut offset): Result = offset.try_into() else { + return; + }; + + let Some(inode) = self.get_inode_by_fh(fh) else { + debug!("could not find inode accociated with fh {} (ino: {})", fh, ino); + + reply.error(EINVAL); + return; + }; + + if inode.ino() != ino { + debug!( + "ino {} of inode associated with fh {} does not match given ino {}", + inode.ino(), + fh, + ino + ); + + reply.error(EINVAL); + return; + } + + let mut _next_idx = 1; + let mut next_offset = || { + let next_idx = _next_idx; + _next_idx += 1; + next_idx + }; + + if inode.is_root() { + if offset == 0 { + debug!("adding . to root dir"); + if reply.add(1, next_offset(), FileType::Directory, ".") { + return; + } + } else { + offset -= 1; + } + + if offset == 0 { + debug!("adding .. to root dir"); + if reply.add(1, next_offset(), FileType::Directory, "..") { + return; + } + } else { + offset -= 1; + } + } + + let Ok(dir_iter) = inode.dir_iter(&self.fat_fs) else { + reply.error(ENOTDIR); + return; + }; + + // need to drop dir_iter here so we can borrow self mut again + // also skip over `offset` entries + let dirs: Vec = dir_iter.skip(offset).collect(); + + for dir_entry in dirs { + let name = dir_entry.name_string().unwrap_or("".into()); + + let inode: &Inode = self.get_or_make_inode_by_dir_entry(&dir_entry); + + debug!("adding entry {} (ino: {})", name, inode.ino()); + if reply.add(ino, next_offset(), inode.kind().into(), name) { + return; + } + } + + reply.ok(); } fn readdirplus( @@ -380,11 +467,34 @@ impl Filesystem for FatFuse { fn releasedir( &mut self, _req: &fuser::Request<'_>, - _ino: u64, - _fh: u64, + ino: u64, + fh: u64, _flags: i32, reply: fuser::ReplyEmpty, ) { + let Some(ino) = self.ino_by_fh.remove(&fh) else { + debug!("can't find inode {} by fh {}", ino, fh); + + reply.error(EIO); + return; + }; + + let Some(inode) = self.inode_table.get(&ino) else { + debug!("ino {} not associated with an inode", ino); + + reply.ok(); + return; + }; + + if inode.ino() != ino { + debug!( + "inode with ino {}, associated with fh {}, does not have expected ino {}", + inode.ino(), + fh, + ino + ); + } + reply.ok(); } diff --git a/fat-fuse/src/inode.rs b/fat-fuse/src/inode.rs index 96c8000..ead17ec 100644 --- a/fat-fuse/src/inode.rs +++ b/fat-fuse/src/inode.rs @@ -6,6 +6,7 @@ use fat_bits::FatFs; use fat_bits::dir::DirEntry; use fuser::FileAttr; use libc::ENOTDIR; +use log::debug; use rand::{Rng, SeedableRng as _}; thread_local! { @@ -91,7 +92,7 @@ impl Inode { ((secs as u32) << 16) | rand as u32 } - pub fn new(fat_fs: &FatFs, dir_entry: DirEntry, ino: u64, uid: u32, gid: u32) -> Inode { + pub fn new(fat_fs: &FatFs, dir_entry: &DirEntry, ino: u64, uid: u32, gid: u32) -> Inode { assert!(dir_entry.is_file() || dir_entry.is_dir()); let generation = Self::new_generation(); @@ -114,6 +115,12 @@ impl Inode { let mtime = datetime_to_system(dir_entry.write_time()); let crtime = datetime_to_system(dir_entry.create_time()); + debug!( + "creating new inode: ino: {} name: {}", + ino, + dir_entry.name_string().unwrap_or("".into()) + ); + Inode { ino, generation, @@ -131,6 +138,28 @@ impl Inode { } } + pub fn root_inode(fat_fs: &FatFs, uid: u32, gid: u32) -> Inode { + // let generation = Self::new_generation(); + + let root_cluster = fat_fs.bpb().root_cluster().unwrap_or(0); + + Inode { + ino: 1, + generation: 0, + ref_count: 0, + size: 0, + block_size: fat_fs.bpb().bytes_per_sector() as u32, + kind: Kind::Dir, + read_only: false, + atime: SystemTime::UNIX_EPOCH, + mtime: SystemTime::UNIX_EPOCH, + crtime: SystemTime::UNIX_EPOCH, + uid, + gid, + first_cluster: root_cluster, + } + } + pub fn ino(&self) -> u64 { self.ino } @@ -143,14 +172,37 @@ impl Inode { self.ref_count } - pub fn ref_count_mut(&mut self) -> &mut u64 { - &mut self.ref_count + pub fn refcount_inc(&mut self) { + self.ref_count += 1; + } + + pub fn refcount_dec(&mut self, n: u64) -> u64 { + if self.ref_count < n { + debug!( + "inode {}: tried to decrement refcount by {}, but is only {}", + self.ino(), + n, + self.ref_count + ); + } + + self.ref_count = self.ref_count.saturating_sub(n); + + self.ref_count + } + + pub fn kind(&self) -> Kind { + self.kind } pub fn first_cluster(&self) -> u32 { self.first_cluster } + pub fn is_root(&self) -> bool { + self.ino == ROOT_INO + } + pub fn file_attr(&self) -> FileAttr { let perm = if self.read_only { 0o555 } else { 0o777 }; @@ -180,7 +232,7 @@ impl Inode { return Err(ENOTDIR); } - if self.ino == ROOT_INO { + if self.is_root() { // root dir return Ok(fat_fs.root_dir_iter()); diff --git a/fat-fuse/src/lib.rs b/fat-fuse/src/lib.rs index 309e0e0..8511d55 100644 --- a/fat-fuse/src/lib.rs +++ b/fat-fuse/src/lib.rs @@ -1,10 +1,9 @@ mod fuse; mod inode; -use std::cell::RefCell; use std::collections::BTreeMap; -use std::rc::Rc; +use fat_bits::dir::DirEntry; use fat_bits::{FatFs, SliceLike}; use log::debug; @@ -18,11 +17,25 @@ pub struct FatFuse { gid: u32, next_ino: u64, - next_fd: u32, + next_fh: u64, inode_table: BTreeMap, ino_by_first_cluster: BTreeMap, + ino_by_fh: BTreeMap, +} + +impl Drop for FatFuse { + fn drop(&mut self) { + println!("inode_table: {}", self.inode_table.len()); + + println!("ino_by_first_cluster: {}", self.ino_by_first_cluster.len()); + for (&first_cluster, &ino) in self.ino_by_first_cluster.iter() { + println!("{} -> {}", first_cluster, ino); + } + + println!("ino_by_fh: {}", self.ino_by_fh.len()); + } } impl FatFuse { @@ -35,17 +48,24 @@ impl FatFuse { let fat_fs = FatFs::load(data)?; - // TODO: build and insert root dir inode - - Ok(FatFuse { + let mut fat_fuse = FatFuse { fat_fs, uid, gid, next_ino: 2, // 0 is reserved and 1 is root - next_fd: 0, + next_fh: 0, inode_table: BTreeMap::new(), ino_by_first_cluster: BTreeMap::new(), - }) + ino_by_fh: BTreeMap::new(), + }; + + // TODO: build and insert root dir inode + + let root_inode = Inode::root_inode(&fat_fuse.fat_fs, uid, gid); + + fat_fuse.insert_inode(root_inode); + + Ok(fat_fuse) } fn next_ino(&mut self) -> u64 { @@ -58,6 +78,16 @@ impl FatFuse { ino } + fn next_fh(&mut self) -> u64 { + let fh = self.next_fh; + + assert!(!self.ino_by_fh.contains_key(&fh)); + + self.next_fh += 1; + + fh + } + fn insert_inode(&mut self, inode: Inode) -> &mut Inode { let ino = inode.ino(); let generation = inode.generation(); @@ -81,8 +111,6 @@ impl FatFuse { } }; - let old_ino = self.ino_by_first_cluster.insert(first_cluster, ino); - debug!( "inserted new inode with ino {} and generation {} (first cluster: {})", ino, generation, first_cluster @@ -92,14 +120,18 @@ impl FatFuse { debug!("ejected inode {} {}", old_inode.ino(), old_inode.generation()); } - if let Some(old_ino) = old_ino { - debug!("ejected old {} -> {} cluster to ino mapping", first_cluster, old_ino); + if first_cluster != 0 { + if let Some(old_ino) = self.ino_by_first_cluster.insert(first_cluster, ino) { + debug!("ejected old {} -> {} cluster to ino mapping", first_cluster, old_ino); + } } new_inode } fn drop_inode(&mut self, ino: u64) { + debug!("dropping ino {}", ino); + let Some(inode) = self.inode_table.remove(&ino) else { debug!("tried to drop inode with ino {}, but was not in table", ino); @@ -108,30 +140,30 @@ impl FatFuse { let first_cluster = inode.first_cluster(); - let entry = self.ino_by_first_cluster.entry(first_cluster); + if first_cluster != 0 { + let entry = self.ino_by_first_cluster.entry(first_cluster); - match entry { - std::collections::btree_map::Entry::Vacant(_) => debug!( - "removed inode with ino {} from table, but it's first cluster did not point to any ino", - ino - ), - std::collections::btree_map::Entry::Occupied(occupied_entry) => { - let found_ino = *occupied_entry.get(); + match entry { + std::collections::btree_map::Entry::Vacant(_) => debug!( + "removed inode with ino {} from table, but it's first cluster did not point to any ino", + ino + ), + std::collections::btree_map::Entry::Occupied(occupied_entry) => { + let found_ino = *occupied_entry.get(); - if found_ino == ino { - // matches our inode, remove it - occupied_entry.remove(); - } else { - // does not match removed inode, leave it as is - debug!( - "removed inode with ino {} from table, but it's first cluster pointer to ino {} instead", - ino, found_ino - ); + if found_ino == ino { + // matches our inode, remove it + occupied_entry.remove(); + } else { + // does not match removed inode, leave it as is + debug!( + "removed inode with ino {} from table, but it's first cluster pointer to ino {} instead", + ino, found_ino + ); + } } } } - - todo!() } fn get_inode(&self, ino: u64) -> Option<&Inode> { @@ -142,7 +174,31 @@ impl FatFuse { self.inode_table.get_mut(&ino) } + fn get_or_make_inode_by_dir_entry(&mut self, dir_entry: &DirEntry) -> &mut Inode { + if self + .get_inode_by_first_cluster_mut(dir_entry.first_cluster()) + .is_some() + { + return self + .get_inode_by_first_cluster_mut(dir_entry.first_cluster()) + .unwrap(); + } + + // no inode found, make a new one + let ino = self.next_ino(); + + let inode = Inode::new(&self.fat_fs, dir_entry, ino, self.uid, self.gid); + + self.insert_inode(inode) + } + pub fn get_inode_by_first_cluster(&self, first_cluster: u32) -> Option<&Inode> { + if first_cluster == 0 { + debug!("trying to get inode by first cluster 0"); + + return None; + } + let ino = self.ino_by_first_cluster.get(&first_cluster)?; if let Some(inode) = self.inode_table.get(ino) { @@ -158,6 +214,12 @@ impl FatFuse { } pub fn get_inode_by_first_cluster_mut(&mut self, first_cluster: u32) -> Option<&mut Inode> { + if first_cluster == 0 { + debug!("trying to get inode by first cluster 0"); + + return None; + } + let ino = self.ino_by_first_cluster.get(&first_cluster)?; if let Some(inode) = self.inode_table.get_mut(ino) { @@ -171,4 +233,28 @@ impl FatFuse { None } } + + pub fn get_inode_by_fh(&self, fh: u64) -> Option<&Inode> { + let ino = self.ino_by_fh.get(&fh)?; + + if let Some(inode) = self.inode_table.get(ino) { + Some(inode) + } else { + debug!("fh {} is mapped to ino {}, but inode is not in table", fh, ino); + + None + } + } + + pub fn get_inode_by_fh_mut(&mut self, fh: u64) -> Option<&mut Inode> { + let ino = self.ino_by_fh.get(&fh)?; + + if let Some(inode) = self.inode_table.get_mut(ino) { + Some(inode) + } else { + debug!("fh {} is mapped to ino {}, but inode is not in table", fh, ino); + + None + } + } } diff --git a/fat-mount/src/main.rs b/fat-mount/src/main.rs index 31a5a8f..e75213c 100644 --- a/fat-mount/src/main.rs +++ b/fat-mount/src/main.rs @@ -30,7 +30,7 @@ fn main() -> anyhow::Result<()> { }) .unwrap(); - let handle = fuser::spawn_mount2(fat_fuse, mountpoint, &options)?; + let _handle = fuser::spawn_mount2(fat_fuse, mountpoint, &options)?; rx.recv().unwrap(); From 19340bd4eec30455dee9b5d23748525a1602d279 Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Thu, 31 Jul 2025 23:47:45 +0200 Subject: [PATCH 09/18] look up inodes by path also DirEntry::name now no longer returns option invalid chars get replaced by ? --- Cargo.lock | 53 ++++++++++++++++-- fat-bits/src/dir.rs | 65 ++++++++++++++++++---- fat-fuse/Cargo.toml | 2 + fat-fuse/src/fuse.rs | 40 ++++++++------ fat-fuse/src/inode.rs | 53 +++++++++++++++--- fat-fuse/src/lib.rs | 122 ++++++++++++++++++++++++++++++++++++++---- 6 files changed, 291 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9c6e8a..f0d44bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,6 +100,12 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "castaway" version = "0.2.4" @@ -162,6 +168,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "compact_string" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5255b88d8ea09573f588088dea17fbea682b4442abea6761a15d1da2c3a76c5c" +dependencies = [ + "serde", + "thiserror 1.0.69", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -223,7 +239,7 @@ dependencies = [ "compact_str", "enum_dispatch", "static_assertions", - "thiserror", + "thiserror 2.0.12", ] [[package]] @@ -240,12 +256,14 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", + "compact_string", "fat-bits", "fuser", + "fxhash", "libc", "log", "rand", - "thiserror", + "thiserror 2.0.12", ] [[package]] @@ -275,6 +293,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -583,13 +610,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/fat-bits/src/dir.rs b/fat-bits/src/dir.rs index 253506f..043ffae 100644 --- a/fat-bits/src/dir.rs +++ b/fat-bits/src/dir.rs @@ -71,7 +71,7 @@ pub struct DirEntry { impl Display for DirEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut name = self.name_string().unwrap_or_else(|| "".into()); + let mut name = self.name_string(); if self.attr.contains(Attr::Directory) { name.push('/'); @@ -266,16 +266,19 @@ impl DirEntry { std::str::from_utf8(self.extension()).ok() } - pub fn name_string(&self) -> Option { + pub fn name_string(&self) -> CompactString { // use a CompactString here to allow inlining of short names // maybe switch to a Cow instead? has disadvantage that we need to alloc for short names + // can't be empty + assert!(!self.is_empty()); + if let Some(long_filename) = self.long_name() { - return Some(long_filename.into()); + return long_filename.into(); } - let name = std::str::from_utf8(&self.name[..8]).ok()?.trim_ascii_end(); - let ext = std::str::from_utf8(&self.name[8..]).ok()?.trim_ascii_end(); + let name = &self.name[..8]; + let ext = &self.name[8..]; let mut s = CompactString::const_new(""); @@ -283,15 +286,50 @@ impl DirEntry { s.push('.'); } - s += name; + // s += name; + + // for &c in self.name[..8].trim_ascii_end() { + // // stem + + // if !c.is_ascii() + // || c < 0x20 + // || !(c.is_ascii_alphanumeric() || VALID_SYMBOLS.contains(&c)) + // { + // // replace invalid character + // // characters above 127 are also ignored, even tho allowed + // s.push('?'); + + // continue; + // } + + // s.push(c as char); + // } + + const VALID_SYMBOLS: &[u8] = &[ + b'$', b'%', b'\'', b'-', b'_', b'@', b'~', b'`', b'!', b'(', b')', b'{', b'}', b'^', + b'#', b'&', + ]; + + fn map_chars(c: u8) -> char { + if !c.is_ascii() + || c < 0x20 + || !(c.is_ascii_alphanumeric() || VALID_SYMBOLS.contains(&c)) + { + '?' + } else { + c as char + } + } + + s.extend(name.trim_ascii_end().iter().copied().map(map_chars)); if !ext.is_empty() { s.push('.'); - s += ext; + s.extend(ext.trim_ascii_end().iter().copied().map(map_chars)); } - Some(s) + s } pub fn long_name(&self) -> Option<&str> { @@ -463,6 +501,11 @@ impl LongFilenameBuf { if dir_entry.is_last() { // first/lasts entry + anyhow::ensure!( + dir_entry.ordinal() <= 20, + "can't have more than 20 long filename dir entries" + ); + let mut name = dir_entry.name(); while name.last() == Some(&0xFFFF) { @@ -485,7 +528,7 @@ impl LongFilenameBuf { return Ok(()); } - assert!(self.checksum.is_some()); + assert!(self.checksum.is_some() && self.last_ordinal.is_some()); anyhow::ensure!( self.checksum == Some(dir_entry.checksum()), @@ -535,6 +578,8 @@ impl LongFilenameBuf { self.checksum.unwrap() ); + anyhow::ensure!(self.rev_buf.len() <= 255, "long filename too long"); + Ok(Some(self.rev_buf.iter().copied().rev())) } } @@ -590,7 +635,7 @@ impl DirIter { .map_err(|e| { anyhow::anyhow!( "failed to get long filename for {}: {}", - dir_entry.name_string().as_deref().unwrap_or(""), + dir_entry.name_string(), e ) })? diff --git a/fat-fuse/Cargo.toml b/fat-fuse/Cargo.toml index ceebf15..9a0326c 100644 --- a/fat-fuse/Cargo.toml +++ b/fat-fuse/Cargo.toml @@ -6,8 +6,10 @@ edition = "2024" [dependencies] anyhow = "1.0.98" chrono = { version = "0.4.41", default-features = false, features = ["alloc", "clock", "std"] } +compact_string = "0.1.0" fat-bits = { version = "0.1.0", path = "../fat-bits" } fuser = "0.15.1" +fxhash = "0.2.1" libc = "0.2.174" log = "0.4.27" rand = { version = "0.9.2", default-features = false, features = ["os_rng", "small_rng"] } diff --git a/fat-fuse/src/fuse.rs b/fat-fuse/src/fuse.rs index 366ccdd..6a77fb4 100644 --- a/fat-fuse/src/fuse.rs +++ b/fat-fuse/src/fuse.rs @@ -1,4 +1,5 @@ use std::ffi::c_int; +use std::rc::Rc; use std::time::Duration; use fat_bits::dir::DirEntry; @@ -28,9 +29,6 @@ impl Filesystem for FatFuse { name: &std::ffi::OsStr, reply: fuser::ReplyEntry, ) { - // warn!("[Not Implemented] lookup(parent: {:#x?}, name {:?})", parent, name); - // reply.error(ENOSYS); - let Some(name) = name.to_str() else { // TODO: add proper handling of non-utf8 strings debug!("cannot convert OsStr {:?} to str", name); @@ -67,7 +65,7 @@ impl Filesystem for FatFuse { // .map_err(|_| ENOTDIR) .and_then(|mut dir_iter| { dir_iter - .find(|dir_entry| dir_entry.name_string().as_deref() == Some(name)) + .find(|dir_entry| &dir_entry.name_string() == name) .ok_or(ENOENT) }) { Ok(dir_entry) => dir_entry, @@ -91,17 +89,23 @@ impl Filesystem for FatFuse { // } // }; - let inode = self.get_or_make_inode_by_dir_entry(&dir_entry); + let inode = self.get_or_make_inode_by_dir_entry( + &dir_entry, + parent_inode.ino(), + parent_inode.path(), + ); let attr = inode.file_attr(); let generation = inode.generation(); reply.entry(&TTL, &attr, generation as u64); - inode.refcount_inc(); + inode.inc_ref_count(); } fn forget(&mut self, _req: &fuser::Request<'_>, ino: u64, nlookup: u64) { + debug!("forgetting ino {} ({} times)", ino, nlookup); + let Some(inode) = self.get_inode_mut(ino) else { debug!("tried to forget {} refs of inode {}, but was not found", ino, nlookup); @@ -110,7 +114,9 @@ impl Filesystem for FatFuse { // *ref_count = ref_count.saturating_sub(nlookup); - if inode.refcount_dec(nlookup) == 0 { + if inode.dec_ref_count(nlookup) == 0 { + debug!("dropping inode with ino {}", inode.ino()); + // no more references, drop inode self.drop_inode(ino); } @@ -380,17 +386,17 @@ impl Filesystem for FatFuse { return; }; - let Some(inode) = self.get_inode_by_fh(fh) else { + let Some(dir_inode) = self.get_inode_by_fh(fh) else { debug!("could not find inode accociated with fh {} (ino: {})", fh, ino); reply.error(EINVAL); return; }; - if inode.ino() != ino { + if dir_inode.ino() != ino { debug!( "ino {} of inode associated with fh {} does not match given ino {}", - inode.ino(), + dir_inode.ino(), fh, ino ); @@ -406,7 +412,7 @@ impl Filesystem for FatFuse { next_idx }; - if inode.is_root() { + if dir_inode.is_root() { if offset == 0 { debug!("adding . to root dir"); if reply.add(1, next_offset(), FileType::Directory, ".") { @@ -426,7 +432,7 @@ impl Filesystem for FatFuse { } } - let Ok(dir_iter) = inode.dir_iter(&self.fat_fs) else { + let Ok(dir_iter) = dir_inode.dir_iter(&self.fat_fs) else { reply.error(ENOTDIR); return; }; @@ -435,10 +441,14 @@ impl Filesystem for FatFuse { // also skip over `offset` entries let dirs: Vec = dir_iter.skip(offset).collect(); - for dir_entry in dirs { - let name = dir_entry.name_string().unwrap_or("".into()); + let dir_ino = dir_inode.ino(); + let dir_path = dir_inode.path(); - let inode: &Inode = self.get_or_make_inode_by_dir_entry(&dir_entry); + for dir_entry in dirs { + let name = dir_entry.name_string(); + + let inode: &Inode = + self.get_or_make_inode_by_dir_entry(&dir_entry, dir_ino, Rc::clone(&dir_path)); debug!("adding entry {} (ino: {})", name, inode.ino()); if reply.add(ino, next_offset(), inode.kind().into(), name) { diff --git a/fat-fuse/src/inode.rs b/fat-fuse/src/inode.rs index ead17ec..1ad6c23 100644 --- a/fat-fuse/src/inode.rs +++ b/fat-fuse/src/inode.rs @@ -1,4 +1,5 @@ use std::cell::{LazyCell, RefCell}; +use std::rc::Rc; use std::time::SystemTime; use chrono::{NaiveDateTime, NaiveTime}; @@ -49,7 +50,7 @@ impl From for fuser::FileType { } } -const ROOT_INO: u64 = 1; +pub const ROOT_INO: u64 = 1; #[derive(Debug)] #[allow(dead_code)] @@ -61,6 +62,8 @@ pub struct Inode { ref_count: u64, + parent_ino: u64, + size: u64, block_size: u32, @@ -77,6 +80,8 @@ pub struct Inode { gid: u32, first_cluster: u32, + + path: Rc, } #[allow(dead_code)] @@ -92,7 +97,15 @@ impl Inode { ((secs as u32) << 16) | rand as u32 } - pub fn new(fat_fs: &FatFs, dir_entry: &DirEntry, ino: u64, uid: u32, gid: u32) -> Inode { + pub fn new( + fat_fs: &FatFs, + dir_entry: &DirEntry, + ino: u64, + uid: u32, + gid: u32, + path: impl Into>, + parent_ino: u64, + ) -> Inode { assert!(dir_entry.is_file() || dir_entry.is_dir()); let generation = Self::new_generation(); @@ -115,16 +128,20 @@ impl Inode { let mtime = datetime_to_system(dir_entry.write_time()); let crtime = datetime_to_system(dir_entry.create_time()); + let path = path.into(); + debug!( - "creating new inode: ino: {} name: {}", + "creating new inode: ino: {} name: {} path: {}", ino, - dir_entry.name_string().unwrap_or("".into()) + dir_entry.name_string(), + path ); Inode { ino, generation, ref_count: 0, + parent_ino, size: dir_entry.file_size() as u64, block_size: fat_fs.bpb().bytes_per_sector() as u32, kind, @@ -135,6 +152,7 @@ impl Inode { uid, gid, first_cluster: dir_entry.first_cluster(), + path, } } @@ -147,6 +165,7 @@ impl Inode { ino: 1, generation: 0, ref_count: 0, + parent_ino: ROOT_INO, // parent is self size: 0, block_size: fat_fs.bpb().bytes_per_sector() as u32, kind: Kind::Dir, @@ -157,6 +176,7 @@ impl Inode { uid, gid, first_cluster: root_cluster, + path: "/".into(), } } @@ -172,11 +192,24 @@ impl Inode { self.ref_count } - pub fn refcount_inc(&mut self) { + pub fn inc_ref_count(&mut self) { + debug!( + "increasing ref_count of ino {} by 1 (new ref_count: {})", + self.ino(), + self.ref_count() + 1 + ); + self.ref_count += 1; } - pub fn refcount_dec(&mut self, n: u64) -> u64 { + pub fn dec_ref_count(&mut self, n: u64) -> u64 { + debug!( + "decreasing ref_count of ino {} by {} (new ref_count: {})", + self.ino(), + n, + self.ref_count().saturating_sub(n), + ); + if self.ref_count < n { debug!( "inode {}: tried to decrement refcount by {}, but is only {}", @@ -191,6 +224,10 @@ impl Inode { self.ref_count } + pub fn parent_ino(&self) -> u64 { + self.parent_ino + } + pub fn kind(&self) -> Kind { self.kind } @@ -199,6 +236,10 @@ impl Inode { self.first_cluster } + pub fn path(&self) -> Rc { + Rc::clone(&self.path) + } + pub fn is_root(&self) -> bool { self.ino == ROOT_INO } diff --git a/fat-fuse/src/lib.rs b/fat-fuse/src/lib.rs index 8511d55..0605514 100644 --- a/fat-fuse/src/lib.rs +++ b/fat-fuse/src/lib.rs @@ -2,9 +2,11 @@ mod fuse; mod inode; use std::collections::BTreeMap; +use std::rc::Rc; use fat_bits::dir::DirEntry; use fat_bits::{FatFs, SliceLike}; +use fxhash::FxHashMap; use log::debug; use crate::inode::Inode; @@ -23,6 +25,7 @@ pub struct FatFuse { ino_by_first_cluster: BTreeMap, ino_by_fh: BTreeMap, + ino_by_path: FxHashMap, u64>, } impl Drop for FatFuse { @@ -35,9 +38,16 @@ impl Drop for FatFuse { } println!("ino_by_fh: {}", self.ino_by_fh.len()); + + println!("ino_by_path: {}", self.ino_by_path.len()); } } +/// SAFETY +/// +/// do NOT leak Rc from this type +unsafe impl Send for FatFuse {} + impl FatFuse { pub fn new(data: S) -> anyhow::Result where @@ -57,6 +67,7 @@ impl FatFuse { inode_table: BTreeMap::new(), ino_by_first_cluster: BTreeMap::new(), ino_by_fh: BTreeMap::new(), + ino_by_path: FxHashMap::default(), }; // TODO: build and insert root dir inode @@ -126,6 +137,10 @@ impl FatFuse { } } + if let Some(old_ino) = self.ino_by_path.insert(new_inode.path(), ino) { + debug!("ejected old {} -> {} path to ino mapping", new_inode.path(), old_ino); + } + new_inode } @@ -157,13 +172,45 @@ impl FatFuse { } else { // does not match removed inode, leave it as is debug!( - "removed inode with ino {} from table, but it's first cluster pointer to ino {} instead", + "removed inode with ino {} from table, but its first cluster pointed to ino {} instead", ino, found_ino ); } } } } + + { + let entry = self.ino_by_path.entry(inode.path()); + + match entry { + std::collections::hash_map::Entry::Vacant(_) => debug!( + "removed inode with ino {} from table, but it's path did not point to any ino", + ino + ), + std::collections::hash_map::Entry::Occupied(occupied_entry) => { + let found_ino = *occupied_entry.get(); + + if found_ino == ino { + // matches our inode, remove it + occupied_entry.remove(); + } else { + // does not match removed inode, leave it as is + debug!( + "removed inode with ino {} from table, but its path pointed to ino {} instead", + ino, found_ino + ); + } + } + } + } + + let Some(parent_inode) = self.get_inode_mut(inode.parent_ino()) else { + panic!("parent inode {} does not exists anymore", inode.parent_ino()); + }; + + // dec refcount + parent_inode.dec_ref_count(1); } fn get_inode(&self, ino: u64) -> Option<&Inode> { @@ -174,20 +221,51 @@ impl FatFuse { self.inode_table.get_mut(&ino) } - fn get_or_make_inode_by_dir_entry(&mut self, dir_entry: &DirEntry) -> &mut Inode { - if self - .get_inode_by_first_cluster_mut(dir_entry.first_cluster()) - .is_some() + fn get_or_make_inode_by_dir_entry( + &mut self, + dir_entry: &DirEntry, + parent_ino: u64, + parent_path: Rc, + ) -> &mut Inode { + if dir_entry.first_cluster() != 0 + && self + .get_inode_by_first_cluster_mut(dir_entry.first_cluster()) + .is_some() { return self .get_inode_by_first_cluster_mut(dir_entry.first_cluster()) .unwrap(); } + let path = { + let mut path = parent_path.as_ref().to_owned(); + + if parent_ino != inode::ROOT_INO { + // root inode already has trailing slash + path.push('/'); + } + + path += &dir_entry.name_string(); + + path + }; + + if self.get_inode_by_path_mut(&path).is_some() { + return self.get_inode_by_path_mut(&path).unwrap(); + } + // no inode found, make a new one let ino = self.next_ino(); - let inode = Inode::new(&self.fat_fs, dir_entry, ino, self.uid, self.gid); + let Some(parent_inode) = self.get_inode_mut(parent_ino) else { + // TODO: what do we do here? should not happen + panic!("parent_ino {} does not lead to inode", parent_ino); + }; + + // inc ref of parent + parent_inode.inc_ref_count(); + + let inode = Inode::new(&self.fat_fs, dir_entry, ino, self.uid, self.gid, path, parent_ino); self.insert_inode(inode) } @@ -235,9 +313,9 @@ impl FatFuse { } pub fn get_inode_by_fh(&self, fh: u64) -> Option<&Inode> { - let ino = self.ino_by_fh.get(&fh)?; + let ino = *self.ino_by_fh.get(&fh)?; - if let Some(inode) = self.inode_table.get(ino) { + if let Some(inode) = self.get_inode(ino) { Some(inode) } else { debug!("fh {} is mapped to ino {}, but inode is not in table", fh, ino); @@ -247,9 +325,9 @@ impl FatFuse { } pub fn get_inode_by_fh_mut(&mut self, fh: u64) -> Option<&mut Inode> { - let ino = self.ino_by_fh.get(&fh)?; + let ino = *self.ino_by_fh.get(&fh)?; - if let Some(inode) = self.inode_table.get_mut(ino) { + if let Some(inode) = self.get_inode_mut(ino) { Some(inode) } else { debug!("fh {} is mapped to ino {}, but inode is not in table", fh, ino); @@ -257,4 +335,28 @@ impl FatFuse { None } } + + pub fn get_inode_by_path(&self, path: &str) -> Option<&Inode> { + let ino = *self.ino_by_path.get(path)?; + + if let Some(inode) = self.get_inode(ino) { + Some(inode) + } else { + debug!("path {} is mapped to ino {}, but inode is not in table", path, ino); + + None + } + } + + pub fn get_inode_by_path_mut(&mut self, path: &str) -> Option<&mut Inode> { + let ino = *self.ino_by_path.get(path)?; + + if let Some(inode) = self.get_inode_mut(ino) { + Some(inode) + } else { + debug!("path {} is mapped to ino {}, but inode is not in table", path, ino); + + None + } + } } From e1d458a38421d08c4a92e5b47feef9ca0d51514f Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Thu, 31 Jul 2025 23:52:06 +0200 Subject: [PATCH 10/18] FatFuse: moved drop -> destroy --- fat-fuse/src/fuse.rs | 13 ++++++++++++- fat-fuse/src/lib.rs | 15 --------------- fat-mount/src/main.rs | 2 -- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/fat-fuse/src/fuse.rs b/fat-fuse/src/fuse.rs index 6a77fb4..adb0cb5 100644 --- a/fat-fuse/src/fuse.rs +++ b/fat-fuse/src/fuse.rs @@ -20,7 +20,18 @@ impl Filesystem for FatFuse { Ok(()) } - fn destroy(&mut self) {} + fn destroy(&mut self) { + debug!("inode_table: {}", self.inode_table.len()); + + debug!("ino_by_first_cluster: {}", self.ino_by_first_cluster.len()); + for (&first_cluster, &ino) in self.ino_by_first_cluster.iter() { + debug!("{} -> {}", first_cluster, ino); + } + + debug!("ino_by_fh: {}", self.ino_by_fh.len()); + + debug!("ino_by_path: {}", self.ino_by_path.len()); + } fn lookup( &mut self, diff --git a/fat-fuse/src/lib.rs b/fat-fuse/src/lib.rs index 0605514..5291746 100644 --- a/fat-fuse/src/lib.rs +++ b/fat-fuse/src/lib.rs @@ -28,21 +28,6 @@ pub struct FatFuse { ino_by_path: FxHashMap, u64>, } -impl Drop for FatFuse { - fn drop(&mut self) { - println!("inode_table: {}", self.inode_table.len()); - - println!("ino_by_first_cluster: {}", self.ino_by_first_cluster.len()); - for (&first_cluster, &ino) in self.ino_by_first_cluster.iter() { - println!("{} -> {}", first_cluster, ino); - } - - println!("ino_by_fh: {}", self.ino_by_fh.len()); - - println!("ino_by_path: {}", self.ino_by_path.len()); - } -} - /// SAFETY /// /// do NOT leak Rc from this type diff --git a/fat-mount/src/main.rs b/fat-mount/src/main.rs index e75213c..c63f238 100644 --- a/fat-mount/src/main.rs +++ b/fat-mount/src/main.rs @@ -34,7 +34,5 @@ fn main() -> anyhow::Result<()> { rx.recv().unwrap(); - println!("done"); - Ok(()) } From 2b01b9ff0e8f56698fb989c2a9a3d6c529a5592c Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Fri, 1 Aug 2025 01:08:48 +0200 Subject: [PATCH 11/18] implemented read --- fat-bits/src/iter.rs | 27 +++++++++-- fat-bits/src/lib.rs | 10 +++- fat-bits/src/subslice.rs | 13 ++++-- fat-fuse/src/fuse.rs | 99 +++++++++++++++++++++++++++++++++++----- fat-fuse/src/inode.rs | 19 +++++++- fat-mount/src/main.rs | 4 +- 6 files changed, 149 insertions(+), 23 deletions(-) diff --git a/fat-bits/src/iter.rs b/fat-bits/src/iter.rs index 8a6f775..e5f6eb4 100644 --- a/fat-bits/src/iter.rs +++ b/fat-bits/src/iter.rs @@ -22,7 +22,7 @@ impl<'a> ClusterChainReader<'a> { } } - fn next_cluster(&mut self) -> bool { + fn move_to_next_cluster(&mut self) -> bool { let Some(next_cluster) = self.next_cluster else { return false; }; @@ -37,12 +37,33 @@ impl<'a> ClusterChainReader<'a> { true } + + pub fn skip(&mut self, n: u64) -> u64 { + let mut bytes_to_skip = n; + + while bytes_to_skip > self.sub_slice.len() as u64 { + bytes_to_skip -= self.sub_slice.len() as u64; + if !self.move_to_next_cluster() { + // ran out of bytes to seek + return n - bytes_to_skip; + } + } + + if bytes_to_skip != 0 { + bytes_to_skip -= self.sub_slice.skip(bytes_to_skip as usize) as u64; + } + + // n should absolutely be zero here + assert_eq!(bytes_to_skip, 0); + + n + } } -impl<'a> Read for ClusterChainReader<'a> { +impl Read for ClusterChainReader<'_> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { if self.sub_slice.is_empty() { - if !self.next_cluster() { + if !self.move_to_next_cluster() { return Ok(0); } } diff --git a/fat-bits/src/lib.rs b/fat-bits/src/lib.rs index bd50960..3c121cd 100644 --- a/fat-bits/src/lib.rs +++ b/fat-bits/src/lib.rs @@ -11,7 +11,7 @@ mod datetime; pub mod dir; pub mod fat; pub mod fs_info; -mod iter; +pub mod iter; mod subslice; mod utils; @@ -240,7 +240,7 @@ impl FatFs { Ok(data) } - fn chain_reader(&self, first_cluster: u32) -> impl Read { + fn chain_reader(&'_ self, first_cluster: u32) -> iter::ClusterChainReader<'_> { iter::ClusterChainReader::new(self, first_cluster) } @@ -274,4 +274,10 @@ impl FatFs { DirIter::new(Box::new(cluster_iter)) } + + pub fn file_reader(&self, first_cluster: u32) -> iter::ClusterChainReader<'_> { + assert!(first_cluster >= 2); + + self.chain_reader(first_cluster) + } } diff --git a/fat-bits/src/subslice.rs b/fat-bits/src/subslice.rs index a710974..3f388f4 100644 --- a/fat-bits/src/subslice.rs +++ b/fat-bits/src/subslice.rs @@ -104,16 +104,21 @@ impl SubSlice<'_> { self.fat_fs } - pub fn fat_fs_mut(&self) -> &FatFs { - self.fat_fs - } - pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn len(&self) -> usize { self.len } + + pub fn skip(&mut self, n: usize) -> usize { + let n = n.min(self.len()); + + self.offset += n as u64; + self.len -= n; + + n + } } impl<'a> SubSlice<'a> { diff --git a/fat-fuse/src/fuse.rs b/fat-fuse/src/fuse.rs index adb0cb5..ed48352 100644 --- a/fat-fuse/src/fuse.rs +++ b/fat-fuse/src/fuse.rs @@ -1,11 +1,12 @@ use std::ffi::c_int; +use std::io::Read; use std::rc::Rc; use std::time::Duration; use fat_bits::dir::DirEntry; use fuser::{FileType, Filesystem}; -use libc::{EINVAL, EIO, ENOENT, ENOSYS, ENOTDIR}; -use log::{debug, warn}; +use libc::{EBADF, EINVAL, EIO, EISDIR, ENOENT, ENOSYS, ENOTDIR}; +use log::{debug, error}; use crate::{FatFuse, Inode}; @@ -281,6 +282,8 @@ impl Filesystem for FatFuse { debug!("fh {} was associated with ino {}, now with ino {}", fh, old_ino, ino); } + debug!("opened inode {}: fh {}", ino, fh); + reply.opened(fh, 0); } @@ -291,16 +294,74 @@ impl Filesystem for FatFuse { fh: u64, offset: i64, size: u32, - flags: i32, - lock_owner: Option, + _flags: i32, + _lock_owner: Option, reply: fuser::ReplyData, ) { - warn!( - "[Not Implemented] read(ino: {:#x?}, fh: {}, offset: {}, size: {}, \ - flags: {:#x?}, lock_owner: {:?})", - ino, fh, offset, size, flags, lock_owner - ); - reply.error(ENOSYS); + // warn!( + // "[Not Implemented] read(ino: {:#x?}, fh: {}, offset: {}, size: {}, \ + // flags: {:#x?}, lock_owner: {:?})", + // ino, fh, offset, size, flags, lock_owner + // ); + // reply.error(ENOSYS); + + if offset < 0 { + debug!("tried to read with negative offset {offset}"); + + reply.error(EINVAL); + return; + } + + let offset = offset as u64; + + let Some(inode) = self.get_inode_by_fh(fh) else { + debug!("fh {fh} is not associated by any inode"); + + reply.error(EBADF); + return; + }; + + if inode.ino() != ino { + debug!("fh {fh} is associated with inode {} instead of {ino}", inode.ino()); + + reply.error(EIO); + return; + } + + if !inode.is_file() { + debug!("tried to use read on directory {ino}"); + + reply.error(EISDIR); + return; + } + + let mut reader = match inode.file_reader(&self.fat_fs) { + Ok(reader) => reader, + Err(err) => { + reply.error(err); + return; + } + }; + + if reader.skip(offset) != offset { + // reader is exhausted, bail + reply.data(&[]); + return; + } + + let mut buf = vec![0; size as usize]; + + let n = match reader.read(&mut buf) { + Ok(n) => n, + Err(err) => { + error!("error while reading: {err}"); + + reply.error(EIO); + return; + } + }; + + reply.data(&buf[..n]); } fn write( @@ -347,13 +408,27 @@ impl Filesystem for FatFuse { fn release( &mut self, _req: &fuser::Request<'_>, - _ino: u64, - _fh: u64, + ino: u64, + fh: u64, _flags: i32, _lock_owner: Option, _flush: bool, reply: fuser::ReplyEmpty, ) { + let Some(found_ino) = self.ino_by_fh.remove(&fh) else { + debug!("tried to release fh {fh} with ino {ino}, but no ino was found in mapping"); + + reply.error(EINVAL); + return; + }; + + if found_ino != ino { + debug!("tried to release fh {fh} with ino {ino}, but found ino is {found_ino} instead"); + + reply.error(EIO); + return; + } + reply.ok(); } diff --git a/fat-fuse/src/inode.rs b/fat-fuse/src/inode.rs index 1ad6c23..c8da0b1 100644 --- a/fat-fuse/src/inode.rs +++ b/fat-fuse/src/inode.rs @@ -5,8 +5,9 @@ use std::time::SystemTime; use chrono::{NaiveDateTime, NaiveTime}; use fat_bits::FatFs; use fat_bits::dir::DirEntry; +use fat_bits::iter::ClusterChainReader; use fuser::FileAttr; -use libc::ENOTDIR; +use libc::{EISDIR, ENOTDIR}; use log::debug; use rand::{Rng, SeedableRng as _}; @@ -232,6 +233,14 @@ impl Inode { self.kind } + pub fn is_file(&self) -> bool { + self.kind == Kind::File + } + + pub fn is_dir(&self) -> bool { + self.kind == Kind::Dir + } + pub fn first_cluster(&self) -> u32 { self.first_cluster } @@ -281,4 +290,12 @@ impl Inode { Ok(fat_fs.dir_iter(self.first_cluster)) } + + pub fn file_reader<'a>(&'a self, fat_fs: &'a FatFs) -> Result, i32> { + if self.is_dir() { + return Err(EISDIR); + } + + Ok(fat_fs.file_reader(self.first_cluster())) + } } diff --git a/fat-mount/src/main.rs b/fat-mount/src/main.rs index c63f238..c7ad1c9 100644 --- a/fat-mount/src/main.rs +++ b/fat-mount/src/main.rs @@ -30,9 +30,11 @@ fn main() -> anyhow::Result<()> { }) .unwrap(); - let _handle = fuser::spawn_mount2(fat_fuse, mountpoint, &options)?; + let handle = fuser::spawn_mount2(fat_fuse, mountpoint, &options)?; rx.recv().unwrap(); + handle.join(); + Ok(()) } From ea3e2a76c4d284d31435e2a55c551d509b852319 Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Fri, 1 Aug 2025 18:09:45 +0200 Subject: [PATCH 12/18] enter the Rc-RefCell madness --- fat-bits/src/datetime.rs | 8 ++ fat-bits/src/dir.rs | 62 ++++++++- fat-bits/src/fat.rs | 138 +++++++++++++++----- fat-bits/src/iter.rs | 90 +++++++++++-- fat-bits/src/lib.rs | 99 ++++++++------- fat-bits/src/subslice.rs | 74 +++++------ fat-bits/src/utils.rs | 17 +-- fat-dump/src/main.rs | 17 +-- fat-fuse/src/fuse.rs | 264 +++++++++++++++++++++++++++------------ fat-fuse/src/inode.rs | 59 +++++---- fat-fuse/src/lib.rs | 137 +++++++------------- fat-mount/src/main.rs | 7 +- 12 files changed, 619 insertions(+), 353 deletions(-) diff --git a/fat-bits/src/datetime.rs b/fat-bits/src/datetime.rs index b9672f2..ecc5309 100644 --- a/fat-bits/src/datetime.rs +++ b/fat-bits/src/datetime.rs @@ -46,6 +46,10 @@ impl Date { ) } + pub fn repr(&self) -> u16 { + self.repr + } + pub fn day(&self) -> u8 { (self.repr & 0x1F) as u8 } @@ -97,6 +101,10 @@ impl Time { Time::from_seconds_minutes_hours(seconds, time.minute() as u8, time.hour() as u8) } + pub fn repr(&self) -> u16 { + self.repr + } + pub fn second(&self) -> u8 { 2 * (self.repr & 0x1F) as u8 } diff --git a/fat-bits/src/dir.rs b/fat-bits/src/dir.rs index 043ffae..eb7460b 100644 --- a/fat-bits/src/dir.rs +++ b/fat-bits/src/dir.rs @@ -1,5 +1,5 @@ use std::fmt::Display; -use std::io::Read; +use std::io::{Read, Write}; use bitflags::bitflags; use chrono::{NaiveDate, NaiveDateTime, TimeDelta}; @@ -199,6 +199,58 @@ impl DirEntry { }) } + pub fn write(&self, mut writer: impl Write) -> std::io::Result<()> { + let mut buf = [0; 32]; + + let mut name = self.name(); + + if name[0] == b'.' && self.is_hidden() { + name = &name[1..]; + } + + if let Some((idx, _)) = name + .iter() + .copied() + .enumerate() + .rev() + .find(|&(_n, u)| u == b'.') + { + let (stem, ext) = name.split_at(idx); + + buf[..8][..stem.len()].copy_from_slice(stem); + buf[8..][..ext.len()].copy_from_slice(ext); + + // (stem, Some(ext)) + } else { + // all stem, no ext + buf[..8][..name.len()].copy_from_slice(name); + } + + buf[11] = self.attr().bits(); + + buf[12] = 0; + + buf[13] = self.create_time_tenths; + buf[13..15].copy_from_slice(&self.create_time.repr().to_le_bytes()); + + buf[16..18].copy_from_slice(&self.create_date.repr().to_le_bytes()); + + buf[18..20].copy_from_slice(&self.last_access_date.repr().to_le_bytes()); + + buf[20..22].copy_from_slice(&((self.first_cluster() >> 16) as u16).to_le_bytes()); + + buf[22..24].copy_from_slice(&self.write_time.repr().to_le_bytes()); + buf[24..26].copy_from_slice(&self.write_date.repr().to_le_bytes()); + + buf[26..28].copy_from_slice(&(self.first_cluster as u16).to_le_bytes()); + + buf[28..].copy_from_slice(&self.file_size.to_le_bytes()); + + writer.write_all(&buf)?; + + Ok(()) + } + /// indicates this DirEntry is empty /// /// can be either simply empty (0xe5) or the sentinel (0x00) that indicates that all following @@ -247,7 +299,13 @@ impl DirEntry { } pub fn name(&self) -> &[u8] { - &self.name + let mut name: &[u8] = &self.name; + + while let Some(&0) = name.last() { + name = &name[..name.len() - 1]; + } + + name } pub fn stem(&self) -> &[u8] { diff --git a/fat-bits/src/fat.rs b/fat-bits/src/fat.rs index bc475dd..8cdc5d0 100644 --- a/fat-bits/src/fat.rs +++ b/fat-bits/src/fat.rs @@ -1,10 +1,12 @@ use std::fmt::Display; +use std::io::Write as _; use std::mem::MaybeUninit; use std::ops::RangeInclusive; use enum_dispatch::enum_dispatch; use crate::FatType; +use crate::subslice::SubSliceMut; #[derive(Debug, thiserror::Error)] pub enum FatError { @@ -19,26 +21,29 @@ pub enum FatError { } #[enum_dispatch] -pub trait Fatty { +pub trait FatOps { // get the next cluster // assumes the cluster is valid, i.e. allocated fn get_entry(&self, cluster: u32) -> u32; + fn set_entry(&mut self, cluster: u32, entry: u32); - fn get_valid_clusters(&self) -> RangeInclusive; - fn get_reserved_clusters(&self) -> RangeInclusive; - fn get_defective_cluster(&self) -> u32; - fn get_reserved_eof_clusters(&self) -> RangeInclusive; - fn get_eof_cluster(&self) -> u32; + fn valid_clusters(&self) -> RangeInclusive; + fn reserved_clusters(&self) -> RangeInclusive; + fn defective_cluster(&self) -> u32; + fn reserved_eof_clusters(&self) -> RangeInclusive; + fn eof_cluster(&self) -> u32; fn count_free_clusters(&self) -> usize { - self.get_valid_clusters() + self.valid_clusters() .map(|cluster| self.get_entry(cluster)) .filter(|&entry| entry == 0) .count() } + + fn write_to_disk(&self, sub_slice: SubSliceMut) -> std::io::Result<()>; } -#[enum_dispatch(Fatty)] +#[enum_dispatch(FatOps)] pub enum Fat { Fat12(Fat12), Fat16(Fat16), @@ -70,18 +75,18 @@ impl Fat { return Err(FatError::FreeCluster); } - if self.get_reserved_clusters().contains(&cluster) { + if self.reserved_clusters().contains(&cluster) { // can't get next cluster for reserved cluster return Err(FatError::ReservedCluster(cluster)); } // defective cluster - if cluster == self.get_defective_cluster() { + if cluster == self.defective_cluster() { // can't get next cluster for defective cluster return Err(FatError::DefectiveCluster); } - if self.get_reserved_eof_clusters().contains(&cluster) { + if self.reserved_eof_clusters().contains(&cluster) { // Reserved and should not be used. May be interpreted as an allocated cluster and the // final cluster in the file (indicating end-of-file condition). // @@ -94,12 +99,12 @@ impl Fat { let entry = self.get_entry(cluster); // interpret second reserved block as EOF here - if entry == self.get_eof_cluster() || self.get_reserved_eof_clusters().contains(&entry) { + if entry == self.eof_cluster() || self.reserved_eof_clusters().contains(&entry) { return Ok(None); } // entry should be in the valid cluster range here; otherwise something went wrong - if !self.get_valid_clusters().contains(&entry) { + if !self.valid_clusters().contains(&entry) { return Err(FatError::InvalidEntry(entry)); } @@ -178,7 +183,7 @@ impl Fat12 { } } -impl Fatty for Fat12 { +impl FatOps for Fat12 { fn get_entry(&self, cluster: u32) -> u32 { let cluster = cluster as usize; assert!(cluster < self.next_sectors.len()); @@ -186,25 +191,68 @@ impl Fatty for Fat12 { self.next_sectors[cluster] as u32 } - fn get_valid_clusters(&self) -> RangeInclusive { + fn set_entry(&mut self, cluster: u32, entry: u32) { + self.next_sectors[cluster as usize] = entry as u16; + } + + fn valid_clusters(&self) -> RangeInclusive { 2..=self.max } - fn get_reserved_clusters(&self) -> RangeInclusive { + fn reserved_clusters(&self) -> RangeInclusive { (self.max as u32 + 1)..=0xFF6 } - fn get_defective_cluster(&self) -> u32 { + fn defective_cluster(&self) -> u32 { 0xFF7 } - fn get_reserved_eof_clusters(&self) -> RangeInclusive { + fn reserved_eof_clusters(&self) -> RangeInclusive { 0xFF8..=0xFFE } - fn get_eof_cluster(&self) -> u32 { + fn eof_cluster(&self) -> u32 { 0xFFF } + + fn write_to_disk(&self, mut sub_slice: SubSliceMut) -> std::io::Result<()> { + // TODO: currently assumed FAT has even number of entries + + assert_eq!(3 * sub_slice.len(), self.next_sectors.len()); + + let mut iter = self.next_sectors.chunks_exact(3); + + let mut buf: [u8; 3]; + + for chunk in &mut iter { + // first (even) entry gets truncated + // let first = u16::from_le_bytes(triple[..2].try_into().unwrap()) & 0xFFF; + // second (odd) entry gets shifted + // let second = u16::from_le_bytes(triple[1..].try_into().unwrap()) >> 4; + + // assert!(idx + 1 < next_sectors.len()); + + // next_sectors[2 * idx] = first; + // next_sectors[2 * idx + 1] = second; + + // sub_slice.write_all(&entry.to_le_bytes())?; + + let first = chunk[0]; + let second = chunk[1]; + + buf = [0; 3]; + + // buf[..2] |= &first.to_le_bytes(); + buf[0] = first.to_le_bytes()[0]; + buf[1] = first.to_le_bytes()[1] | (second << 4).to_le_bytes()[0]; + buf[2] = (second << 4).to_le_bytes()[1]; + sub_slice.write_all(&buf)?; + } + + assert_eq!(iter.remainder().len(), 0); + + Ok(()) + } } pub struct Fat16 { @@ -262,7 +310,7 @@ impl Fat16 { } } -impl Fatty for Fat16 { +impl FatOps for Fat16 { fn get_entry(&self, cluster: u32) -> u32 { let cluster = cluster as usize; assert!(cluster < self.next_sectors.len()); @@ -270,25 +318,39 @@ impl Fatty for Fat16 { self.next_sectors[cluster] as u32 } - fn get_valid_clusters(&self) -> RangeInclusive { + fn set_entry(&mut self, cluster: u32, entry: u32) { + self.next_sectors[cluster as usize] = entry as u16; + } + + fn valid_clusters(&self) -> RangeInclusive { 2..=self.max } - fn get_reserved_clusters(&self) -> RangeInclusive { + fn reserved_clusters(&self) -> RangeInclusive { (self.max as u32 + 1)..=0xFFF6 } - fn get_defective_cluster(&self) -> u32 { + fn defective_cluster(&self) -> u32 { 0xFFF7 } - fn get_reserved_eof_clusters(&self) -> RangeInclusive { + fn reserved_eof_clusters(&self) -> RangeInclusive { 0xFFF8..=0xFFFE } - fn get_eof_cluster(&self) -> u32 { + fn eof_cluster(&self) -> u32 { 0xFFFF } + + fn write_to_disk(&self, mut sub_slice: SubSliceMut) -> std::io::Result<()> { + assert_eq!(2 * sub_slice.len(), self.next_sectors.len()); + + for &entry in self.next_sectors.iter() { + sub_slice.write_all(&entry.to_le_bytes())?; + } + + Ok(()) + } } pub struct Fat32 { @@ -346,7 +408,7 @@ impl Fat32 { } } -impl Fatty for Fat32 { +impl FatOps for Fat32 { fn get_entry(&self, cluster: u32) -> u32 { let cluster = cluster as usize; assert!(cluster < self.next_sectors.len()); @@ -354,23 +416,37 @@ impl Fatty for Fat32 { self.next_sectors[cluster] as u32 } - fn get_valid_clusters(&self) -> RangeInclusive { + fn set_entry(&mut self, cluster: u32, entry: u32) { + self.next_sectors[cluster as usize] = entry; + } + + fn valid_clusters(&self) -> RangeInclusive { 2..=self.max } - fn get_reserved_clusters(&self) -> RangeInclusive { + fn reserved_clusters(&self) -> RangeInclusive { (self.max + 1)..=0xFFFFFFF6 } - fn get_defective_cluster(&self) -> u32 { + fn defective_cluster(&self) -> u32 { 0xFFFFFFF7 } - fn get_reserved_eof_clusters(&self) -> RangeInclusive { + fn reserved_eof_clusters(&self) -> RangeInclusive { 0xFFFFFFF8..=0xFFFFFFFE } - fn get_eof_cluster(&self) -> u32 { + fn eof_cluster(&self) -> u32 { 0xFFFFFFFF } + + fn write_to_disk(&self, mut sub_slice: SubSliceMut) -> std::io::Result<()> { + assert_eq!(4 * sub_slice.len(), self.next_sectors.len()); + + for &entry in self.next_sectors.iter() { + sub_slice.write_all(&entry.to_le_bytes())?; + } + + Ok(()) + } } diff --git a/fat-bits/src/iter.rs b/fat-bits/src/iter.rs index e5f6eb4..726198e 100644 --- a/fat-bits/src/iter.rs +++ b/fat-bits/src/iter.rs @@ -1,11 +1,12 @@ -use std::io::Read; +use std::io::{Read, Write}; use crate::FatFs; -use crate::subslice::SubSlice; -use crate::utils::replace; +use crate::subslice::{SubSlice, SubSliceMut}; pub struct ClusterChainReader<'a> { - sub_slice: SubSlice<'a>, + fat_fs: &'a FatFs, + + sub_slice: SubSlice, next_cluster: Option, } @@ -17,6 +18,7 @@ impl<'a> ClusterChainReader<'a> { let sub_slice = fat_fs.cluster_as_subslice(first_cluster); ClusterChainReader { + fat_fs, sub_slice, next_cluster, } @@ -27,13 +29,8 @@ impl<'a> ClusterChainReader<'a> { return false; }; - replace(&mut self.sub_slice, |sub_slice| { - let fat_fs = sub_slice.release(); - - self.next_cluster = fat_fs.next_cluster(next_cluster).unwrap_or(None); - - fat_fs.cluster_as_subslice(next_cluster) - }); + self.next_cluster = self.fat_fs.next_cluster(next_cluster).unwrap_or(None); + self.sub_slice = self.fat_fs.cluster_as_subslice(next_cluster); true } @@ -71,3 +68,74 @@ impl Read for ClusterChainReader<'_> { self.sub_slice.read(buf) } } + +pub struct ClusterChainWriter<'a> { + fat_fs: &'a FatFs, + + sub_slice: SubSliceMut, + + next_cluster: Option, +} + +impl<'a> ClusterChainWriter<'a> { + pub fn new(fat_fs: &'a FatFs, first_cluster: u32) -> ClusterChainWriter<'a> { + let next_cluster = fat_fs.next_cluster(first_cluster).unwrap_or(None); + + let sub_slice = fat_fs.cluster_as_subslice_mut(first_cluster); + + ClusterChainWriter { + fat_fs, + sub_slice, + next_cluster, + } + } + + fn move_to_next_cluster(&mut self) -> bool { + // TODO: should allocate a new cluster here! + let Some(next_cluster) = self.next_cluster else { + return false; + }; + + self.next_cluster = self.fat_fs.next_cluster(next_cluster).unwrap_or(None); + self.fat_fs.cluster_as_subslice_mut(next_cluster); + + true + } + + pub fn skip(&mut self, n: u64) -> u64 { + let mut bytes_to_skip = n; + + while bytes_to_skip > self.sub_slice.len() as u64 { + bytes_to_skip -= self.sub_slice.len() as u64; + if !self.move_to_next_cluster() { + // ran out of bytes to seek + return n - bytes_to_skip; + } + } + + if bytes_to_skip != 0 { + bytes_to_skip -= self.sub_slice.skip(bytes_to_skip as usize) as u64; + } + + // n should absolutely be zero here + assert_eq!(bytes_to_skip, 0); + + n + } +} + +impl Write for ClusterChainWriter<'_> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if self.sub_slice.is_empty() { + if !(self.move_to_next_cluster()) { + return Ok(0); + } + } + + self.sub_slice.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} diff --git a/fat-bits/src/lib.rs b/fat-bits/src/lib.rs index 3c121cd..a2f0d7f 100644 --- a/fat-bits/src/lib.rs +++ b/fat-bits/src/lib.rs @@ -1,9 +1,10 @@ use std::cell::RefCell; +use std::fmt::Display; use std::io::{Read, Seek, SeekFrom, Write}; use std::rc::Rc; use crate::dir::DirIter; -use crate::fat::{FatError, Fatty}; +use crate::fat::{FatError, FatOps}; use crate::subslice::{SubSlice, SubSliceMut}; pub mod bpb; @@ -98,6 +99,16 @@ pub struct FatFs { fat: fat::Fat, } +impl Display for FatFs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", self.bpb)?; + writeln!(f, "")?; + writeln!(f, "{}", self.fat)?; + + Ok(()) + } +} + unsafe impl Send for FatFs {} impl FatFs { @@ -160,90 +171,74 @@ impl FatFs { }) } - pub fn bpb(&self) -> &bpb::Bpb { - &self.bpb - } - - pub fn fat(&self) -> &fat::Fat { - &self.fat - } - /// byte offset of data cluster - pub fn data_cluster_to_offset(&self, cluster: u32) -> u64 { + fn data_cluster_to_offset(&self, cluster: u32) -> u64 { // assert!(cluster >= 2); - assert!(self.fat().get_valid_clusters().contains(&cluster)); + assert!(self.fat.valid_clusters().contains(&cluster)); self.data_offset + (cluster - 2) as u64 * self.bytes_per_cluster as u64 } + pub fn free_clusters(&self) -> usize { + self.fat.count_free_clusters() + } + + pub fn bytes_per_sector(&self) -> u16 { + self.bpb.bytes_per_sector() + } + + pub fn sectors_per_cluster(&self) -> u8 { + self.bpb.sectors_per_cluster() + } + + pub fn root_cluster(&self) -> Option { + self.bpb.root_cluster() + } + /// next data cluster or None is cluster is EOF /// /// giving an invalid cluster (free, reserved, or defective) returns an appropriate error pub fn next_cluster(&self, cluster: u32) -> Result, FatError> { - self.fat().get_next_cluster(cluster) + self.fat.get_next_cluster(cluster) } - pub fn cluster_as_subslice_mut(&mut self, cluster: u32) -> SubSliceMut<'_> { + pub fn cluster_as_subslice_mut(&self, cluster: u32) -> SubSliceMut { if cluster == 0 { // for cluster 0 simply return empty subslice // this makes things a bit easier, since cluster 0 is used as a marker that a file/dir // is empty - SubSliceMut::new(self, 0, 0); + SubSliceMut::new(self.inner.clone(), 0, 0); } let offset = self.data_cluster_to_offset(cluster); - SubSliceMut::new(self, offset, self.bytes_per_cluster) + SubSliceMut::new(self.inner.clone(), offset, self.bytes_per_cluster) } - pub fn cluster_as_subslice(&self, cluster: u32) -> SubSlice<'_> { + pub fn cluster_as_subslice(&self, cluster: u32) -> SubSlice { if cluster == 0 { // for cluster 0 simply return empty subslice // this makes things a bit easier, since cluster 0 is used as a marker that a file/dir // is empty - SubSlice::new(self, 0, 0); + SubSlice::new(self.inner.clone(), 0, 0); } let offset = self.data_cluster_to_offset(cluster); - SubSlice::new(self, offset, self.bytes_per_cluster) - } - - pub fn root_dir_bytes(&mut self) -> std::io::Result> { - if let Some(root_dir_offset) = self.root_dir_offset { - let mut data = Vec::new(); - - let mut subslice = SubSliceMut::new(self, root_dir_offset, self.root_dir_size); - - subslice.read_to_end(&mut data)?; - - return Ok(data); - } - - let mut cluster = self.bpb().root_cluster().unwrap(); - - let mut data = vec![0; self.bytes_per_cluster]; - - let mut inner = self.inner.borrow_mut(); - - inner.read_at_offset(self.data_cluster_to_offset(cluster), &mut data)?; - - while let Ok(Some(next_cluster)) = self.next_cluster(cluster) { - cluster = next_cluster; - - inner.read_at_offset(self.data_cluster_to_offset(cluster), &mut data)?; - } - - Ok(data) + SubSlice::new(self.inner.clone(), offset, self.bytes_per_cluster) } fn chain_reader(&'_ self, first_cluster: u32) -> iter::ClusterChainReader<'_> { iter::ClusterChainReader::new(self, first_cluster) } + fn chain_writer(&'_ self, first_cluster: u32) -> iter::ClusterChainWriter<'_> { + iter::ClusterChainWriter::new(self, first_cluster) + } + pub fn root_dir_iter<'a>(&'a self) -> DirIter> { // Box + '_> // TODO: maybe wrap this in another RootDirIter enum, so we don't have to Box @@ -251,7 +246,7 @@ impl FatFs { if let Some(root_dir_offset) = self.root_dir_offset { // FAT12/FAT16 - let sub_slice = SubSlice::new(self, root_dir_offset, self.root_dir_size); + let sub_slice = SubSlice::new(self.inner.clone(), root_dir_offset, self.root_dir_size); return DirIter::new(Box::new(sub_slice)); } @@ -259,7 +254,7 @@ impl FatFs { // FAT32 // can't fail; we're in the FAT32 case - let root_cluster = self.bpb().root_cluster().unwrap(); + let root_cluster = self.bpb.root_cluster().unwrap(); let cluster_iter = iter::ClusterChainReader::new(self, root_cluster); @@ -276,8 +271,16 @@ impl FatFs { } pub fn file_reader(&self, first_cluster: u32) -> iter::ClusterChainReader<'_> { + // TODO: needs to take file size into account assert!(first_cluster >= 2); self.chain_reader(first_cluster) } + + pub fn file_writer(&self, first_cluster: u32) -> iter::ClusterChainWriter<'_> { + // TODO: needs to take file size into account + assert!(first_cluster >= 2); + + self.chain_writer(first_cluster) + } } diff --git a/fat-bits/src/subslice.rs b/fat-bits/src/subslice.rs index 3f388f4..798fc74 100644 --- a/fat-bits/src/subslice.rs +++ b/fat-bits/src/subslice.rs @@ -1,16 +1,19 @@ +use std::cell::RefCell; use std::fmt::Debug; use std::io::{Read, Write}; +use std::rc::Rc; -use crate::FatFs; +use crate::SliceLike; -pub struct SubSliceMut<'a> { - fat_fs: &'a mut FatFs, +pub struct SubSliceMut { + // fat_fs: &'a FatFs, + data: Rc>, offset: u64, len: usize, } -impl Debug for SubSliceMut<'_> { +impl Debug for SubSliceMut { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SubSliceMut") .field("offset", &self.offset) @@ -19,17 +22,13 @@ impl Debug for SubSliceMut<'_> { } } -impl SubSliceMut<'_> { - pub fn new(fat_fs: &mut FatFs, offset: u64, len: usize) -> SubSliceMut<'_> { - SubSliceMut { - fat_fs, - offset, - len, - } +impl SubSliceMut { + pub fn new(data: Rc>, offset: u64, len: usize) -> SubSliceMut { + SubSliceMut { data, offset, len } } } -impl SubSliceMut<'_> { +impl<'a> SubSliceMut { pub fn len(&self) -> usize { self.len } @@ -37,14 +36,22 @@ impl SubSliceMut<'_> { pub fn is_empty(&self) -> bool { self.len() == 0 } + + pub fn skip(&mut self, n: usize) -> usize { + let n = n.min(self.len()); + + self.offset += n as u64; + self.len -= n; + + n + } } -impl Read for SubSliceMut<'_> { +impl Read for SubSliceMut { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let bytes_to_read = self.len.min(buf.len()); - self.fat_fs - .inner + self.data .borrow_mut() .read_at_offset(self.offset, &mut buf[..bytes_to_read])?; @@ -55,12 +62,11 @@ impl Read for SubSliceMut<'_> { } } -impl Write for SubSliceMut<'_> { +impl Write for SubSliceMut { fn write(&mut self, buf: &[u8]) -> std::io::Result { let bytes_to_write = self.len.min(buf.len()); - self.fat_fs - .inner + self.data .borrow_mut() .write_at_offset(self.offset, &buf[..bytes_to_write])?; @@ -75,14 +81,14 @@ impl Write for SubSliceMut<'_> { } } -pub struct SubSlice<'a> { - fat_fs: &'a FatFs, +pub struct SubSlice { + data: Rc>, offset: u64, len: usize, } -impl Debug for SubSlice<'_> { +impl Debug for SubSlice { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SubSliceMut") .field("offset", &self.offset) @@ -91,17 +97,9 @@ impl Debug for SubSlice<'_> { } } -impl SubSlice<'_> { - pub fn new(fat_fs: &FatFs, offset: u64, len: usize) -> SubSlice<'_> { - SubSlice { - fat_fs, - offset, - len, - } - } - - pub fn fat_fs(&self) -> &FatFs { - self.fat_fs +impl<'a> SubSlice { + pub fn new(data: Rc>, offset: u64, len: usize) -> SubSlice { + SubSlice { data, offset, len } } pub fn is_empty(&self) -> bool { @@ -121,19 +119,11 @@ impl SubSlice<'_> { } } -impl<'a> SubSlice<'a> { - /// releases the inner &FatFs, consuming self in the process - pub fn release(self) -> &'a FatFs { - self.fat_fs - } -} - -impl Read for SubSlice<'_> { +impl Read for SubSlice { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let bytes_to_read = self.len.min(buf.len()); - self.fat_fs - .inner + self.data .borrow_mut() .read_at_offset(self.offset, &mut buf[..bytes_to_read])?; diff --git a/fat-bits/src/utils.rs b/fat-bits/src/utils.rs index 0fe9834..1e20e9d 100644 --- a/fat-bits/src/utils.rs +++ b/fat-bits/src/utils.rs @@ -3,24 +3,9 @@ pub fn load_u16_le(bytes: &[u8]) -> u16 { u16::from_le_bytes(bytes.try_into().unwrap()) } + pub fn load_u32_le(bytes: &[u8]) -> u32 { assert_eq!(bytes.len(), 4); u32::from_le_bytes(bytes.try_into().unwrap()) } - -/// replace the value at x with f(x) -/// -/// SAFETY: -/// should be safe, I guess? MIRI didn't complain about it -pub fn replace(x: &mut T, f: impl FnOnce(T) -> T) { - unsafe { - let x_ptr = x as *mut T; - - let old_x = std::ptr::read(x_ptr); - - let new_x = f(old_x); - - std::ptr::write(x_ptr, new_x); - } -} diff --git a/fat-dump/src/main.rs b/fat-dump/src/main.rs index 539bfe0..e266352 100644 --- a/fat-dump/src/main.rs +++ b/fat-dump/src/main.rs @@ -1,6 +1,5 @@ use fat_bits::FatFs; use fat_bits::dir::DirEntry; -use fat_bits::fat::Fatty as _; pub fn main() -> anyhow::Result<()> { let args = std::env::args(); @@ -21,16 +20,18 @@ pub fn main() -> anyhow::Result<()> { let fat_fs = FatFs::load(file)?; - println!("{}", fat_fs.bpb()); - println!(); - println!("{}", fat_fs.fat()); + // println!("{}", fat_fs.bpb()); + // println!(); + // println!("{}", fat_fs.fat()); + + println!("{}", fat_fs); println!(); println!( "free clusters: {} ({} bytes)", - fat_fs.fat().count_free_clusters(), - fat_fs.fat().count_free_clusters() - * fat_fs.bpb().bytes_per_sector() as usize - * fat_fs.bpb().sectors_per_cluster() as usize + fat_fs.free_clusters(), + fat_fs.free_clusters() + * fat_fs.bytes_per_sector() as usize + * fat_fs.sectors_per_cluster() as usize ); println!(); diff --git a/fat-fuse/src/fuse.rs b/fat-fuse/src/fuse.rs index ed48352..938fe7c 100644 --- a/fat-fuse/src/fuse.rs +++ b/fat-fuse/src/fuse.rs @@ -1,5 +1,5 @@ use std::ffi::c_int; -use std::io::Read; +use std::io::{Read, Write}; use std::rc::Rc; use std::time::Duration; @@ -8,7 +8,7 @@ use fuser::{FileType, Filesystem}; use libc::{EBADF, EINVAL, EIO, EISDIR, ENOENT, ENOSYS, ENOTDIR}; use log::{debug, error}; -use crate::{FatFuse, Inode}; +use crate::FatFuse; const TTL: Duration = Duration::from_secs(1); @@ -50,56 +50,34 @@ impl Filesystem for FatFuse { debug!("looking up file {} with parent ino {}", name, parent); - let Some(parent_inode) = self.get_inode(parent) else { + let Some(parent_inode) = self.get_inode(parent).cloned() else { // parent inode does not exist // TODO: how can we make sure this does not happed? // TODO: panic? debug!("could not find inode for parent ino {}", parent); - reply.error(EIO); + reply.error(ENOENT); return; }; - // let Ok(mut dir_iter) = parent_inode.dir_iter(&self.fat_fs) else { - // reply.error(ENOTDIR); - // return; - // }; + let parent_inode = parent_inode.borrow(); - // let Some(dir_entry) = - // dir_iter.find(|dir_entry| dir_entry.name_string().as_deref() == Some(name)) - // else { - // reply.error(ENOENT); - // return; - // }; + let dir_entry: DirEntry = + match parent_inode + .dir_iter(&self.fat_fs) + .and_then(|mut dir_iter| { + dir_iter + .find(|dir_entry| &dir_entry.name_string() == name) + .ok_or(ENOENT) + }) { + Ok(dir_entry) => dir_entry, + Err(err) => { + debug!("error: {}", err); + reply.error(err); - let dir_entry: DirEntry = match parent_inode - .dir_iter(&self.fat_fs) - // .map_err(|_| ENOTDIR) - .and_then(|mut dir_iter| { - dir_iter - .find(|dir_entry| &dir_entry.name_string() == name) - .ok_or(ENOENT) - }) { - Ok(dir_entry) => dir_entry, - Err(err) => { - debug!("error: {}", err); - reply.error(err); - - return; - } - }; - - // let inode = match self.get_inode_by_first_cluster(dir_entry.first_cluster()) { - // Some(inode) => inode, - // None => { - // // no inode found, make a new one - // let ino = self.next_ino(); - - // let inode = Inode::new(&self.fat_fs, &dir_entry, ino, self.uid, self.gid); - - // self.insert_inode(inode) - // } - // }; + return; + } + }; let inode = self.get_or_make_inode_by_dir_entry( &dir_entry, @@ -107,6 +85,8 @@ impl Filesystem for FatFuse { parent_inode.path(), ); + let mut inode = inode.borrow_mut(); + let attr = inode.file_attr(); let generation = inode.generation(); @@ -118,19 +98,21 @@ impl Filesystem for FatFuse { fn forget(&mut self, _req: &fuser::Request<'_>, ino: u64, nlookup: u64) { debug!("forgetting ino {} ({} times)", ino, nlookup); - let Some(inode) = self.get_inode_mut(ino) else { + let Some(inode) = self.get_inode(ino).cloned() else { debug!("tried to forget {} refs of inode {}, but was not found", ino, nlookup); return; }; - // *ref_count = ref_count.saturating_sub(nlookup); + let mut inode_ref = inode.borrow_mut(); - if inode.dec_ref_count(nlookup) == 0 { - debug!("dropping inode with ino {}", inode.ino()); + if inode_ref.dec_ref_count(nlookup) == 0 { + debug!("dropping inode {}", inode_ref.ino()); + + drop(inode_ref); // no more references, drop inode - self.drop_inode(ino); + self.drop_inode(inode); } } @@ -146,20 +128,20 @@ impl Filesystem for FatFuse { let inode = if let Some(fh) = fh { let Some(inode) = self.get_inode_by_fh(fh) else { - reply.error(EIO); - + reply.error(EBADF); return; }; inode - } else if let Some(inode) = self.get_inode(ino) { + } else if let Some(inode) = self.get_inode(ino).cloned() { inode } else { - reply.error(EIO); - + reply.error(ENOENT); return; }; + let inode = inode.borrow(); + let attr = inode.file_attr(); reply.attr(&TTL, &attr); @@ -189,6 +171,17 @@ impl Filesystem for FatFuse { ino, mode, uid, gid, size, fh, flags ); reply.error(ENOSYS); + return; + + // TODO: implement this properly + // let Some(inode) = self.get_inode(ino) else { + // debug!("tried to get inode {ino}, but not found"); + // + // reply.error(ENOENT); + // return; + // }; + // + // reply.attr(&TTL, &inode.file_attr()); } fn readlink(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyData) { @@ -272,7 +265,7 @@ impl Filesystem for FatFuse { fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, _flags: i32, reply: fuser::ReplyOpen) { if !self.inode_table.contains_key(&ino) { - reply.error(EINVAL); + reply.error(ENOENT); return; } @@ -321,10 +314,12 @@ impl Filesystem for FatFuse { return; }; + let inode = inode.borrow(); + if inode.ino() != ino { debug!("fh {fh} is associated with inode {} instead of {ino}", inode.ino()); - reply.error(EIO); + reply.error(EINVAL); return; } @@ -361,6 +356,8 @@ impl Filesystem for FatFuse { } }; + debug!("read {n} bytes"); + reply.data(&buf[..n]); } @@ -371,23 +368,102 @@ impl Filesystem for FatFuse { fh: u64, offset: i64, data: &[u8], - write_flags: u32, - flags: i32, - lock_owner: Option, + _write_flags: u32, + _flags: i32, + _lock_owner: Option, reply: fuser::ReplyWrite, ) { - debug!( - "[Not Implemented] write(ino: {:#x?}, fh: {}, offset: {}, data.len(): {}, \ - write_flags: {:#x?}, flags: {:#x?}, lock_owner: {:?})", - ino, - fh, - offset, - data.len(), - write_flags, - flags, - lock_owner - ); - reply.error(ENOSYS); + debug!("new write request: ino={ino} fh={fh} offset={offset} data={data:?}"); + + if offset < 0 { + debug!("tried to write with negative offset {offset}"); + + reply.error(EINVAL); + return; + } + + let offset = offset as u64; + + let Some(inode) = self.get_inode_by_fh(fh) else { + debug!("no inode associated with fh {fh} (given ino: {ino}"); + + reply.error(EBADF); + return; + }; + + let inode = inode.borrow(); + + if inode.is_read_only() { + reply.error(EBADF); + return; + } + + if inode.ino() != ino { + debug!("fh {fh} points to ino {}, but ino {ino} was given", inode.ino()); + + reply.error(EINVAL); + return; + } + + if !inode.is_file() { + debug!("tried to use read on directory {ino}"); + + reply.error(EISDIR); + return; + } + + let mut writer = match inode.file_writer(&self.fat_fs) { + Ok(writer) => writer, + Err(err) => { + reply.error(err); + return; + } + }; + + // if writer.skip(offset) != offset { + // // writer is at EOF, bail + + // } + + let cur_offset = writer.skip(offset); + + // can't seek more than we requested + assert!(cur_offset <= offset); + + let mut bytes_written = 0; + + if cur_offset < offset { + // tried to set offset beyond EOF + // fill with zeros + let zeros = vec![0; (offset - cur_offset) as usize]; + + debug!("writing {} zeros", zeros.len()); + + if let Err(err) = writer.write_all(&zeros) { + debug!("writing zeros returned error: {err}"); + + reply.error(EIO); + return; + } + + bytes_written += zeros.len(); + } + + if let Err(err) = writer.write_all(&data) { + debug!("writing data returned error: {err}"); + + reply.error(EIO); + return; + } + + bytes_written += data.len(); + + reply.written(bytes_written as u32); + + // TODO: update file size + if offset + bytes_written as u64 > inode.size() { + todo!() + } } fn flush( @@ -395,14 +471,34 @@ impl Filesystem for FatFuse { _req: &fuser::Request<'_>, ino: u64, fh: u64, - lock_owner: u64, + _lock_owner: u64, reply: fuser::ReplyEmpty, ) { - debug!( - "[Not Implemented] flush(ino: {:#x?}, fh: {}, lock_owner: {:?})", - ino, fh, lock_owner - ); - reply.error(ENOSYS); + // debug!( + // "[Not Implemented] flush(ino: {:#x?}, fh: {}, lock_owner: {:?})", + // ino, fh, lock_owner + // ); + // reply.error(ENOSYS); + + debug!("flushing ino={ino} fh={fh}"); + + let Some(&found_ino) = self.ino_by_fh.get(&fh) else { + debug!("expected fh {fh} to be mapped to ino {ino}, but not found instead"); + + reply.error(EBADF); + return; + }; + + if found_ino != ino { + debug!( + "expected fh {fh} to be mapped to ino {ino}, but was mapped to {found_ino} instead" + ); + + reply.error(EBADF); + return; + } + + reply.ok(); } fn release( @@ -418,14 +514,14 @@ impl Filesystem for FatFuse { let Some(found_ino) = self.ino_by_fh.remove(&fh) else { debug!("tried to release fh {fh} with ino {ino}, but no ino was found in mapping"); - reply.error(EINVAL); + reply.error(EBADF); return; }; if found_ino != ino { debug!("tried to release fh {fh} with ino {ino}, but found ino is {found_ino} instead"); - reply.error(EIO); + reply.error(EINVAL); return; } @@ -475,10 +571,12 @@ impl Filesystem for FatFuse { let Some(dir_inode) = self.get_inode_by_fh(fh) else { debug!("could not find inode accociated with fh {} (ino: {})", fh, ino); - reply.error(EINVAL); + reply.error(EBADF); return; }; + let dir_inode = dir_inode.borrow(); + if dir_inode.ino() != ino { debug!( "ino {} of inode associated with fh {} does not match given ino {}", @@ -533,9 +631,11 @@ impl Filesystem for FatFuse { for dir_entry in dirs { let name = dir_entry.name_string(); - let inode: &Inode = + let inode = self.get_or_make_inode_by_dir_entry(&dir_entry, dir_ino, Rc::clone(&dir_path)); + let inode = inode.borrow(); + debug!("adding entry {} (ino: {})", name, inode.ino()); if reply.add(ino, next_offset(), inode.kind().into(), name) { return; @@ -571,17 +671,19 @@ impl Filesystem for FatFuse { let Some(ino) = self.ino_by_fh.remove(&fh) else { debug!("can't find inode {} by fh {}", ino, fh); - reply.error(EIO); + reply.error(EBADF); return; }; - let Some(inode) = self.inode_table.get(&ino) else { + let Some(inode) = self.get_inode(ino) else { debug!("ino {} not associated with an inode", ino); reply.ok(); return; }; + let inode = inode.borrow(); + if inode.ino() != ino { debug!( "inode with ino {}, associated with fh {}, does not have expected ino {}", diff --git a/fat-fuse/src/inode.rs b/fat-fuse/src/inode.rs index c8da0b1..6cce26f 100644 --- a/fat-fuse/src/inode.rs +++ b/fat-fuse/src/inode.rs @@ -1,11 +1,11 @@ use std::cell::{LazyCell, RefCell}; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use std::time::SystemTime; use chrono::{NaiveDateTime, NaiveTime}; use fat_bits::FatFs; use fat_bits::dir::DirEntry; -use fat_bits::iter::ClusterChainReader; +use fat_bits::iter::{ClusterChainReader, ClusterChainWriter}; use fuser::FileAttr; use libc::{EISDIR, ENOTDIR}; use log::debug; @@ -27,12 +27,6 @@ fn get_random() -> T where rand::distr::StandardUniform: rand::distr::Distribution, { - // RNG.with(|x| unsafe { - // let rng = &mut (*x.get()); - - // rng.random::() - // }) - RNG.with(|rng| rng.borrow_mut().random()) } @@ -53,6 +47,9 @@ impl From for fuser::FileType { pub const ROOT_INO: u64 = 1; +pub type InodeRef = Rc>; +pub type InodeWeak = Weak>; + #[derive(Debug)] #[allow(dead_code)] pub struct Inode { @@ -63,7 +60,7 @@ pub struct Inode { ref_count: u64, - parent_ino: u64, + parent: Option, size: u64, block_size: u32, @@ -105,7 +102,7 @@ impl Inode { uid: u32, gid: u32, path: impl Into>, - parent_ino: u64, + parent: InodeRef, ) -> Inode { assert!(dir_entry.is_file() || dir_entry.is_dir()); @@ -142,9 +139,9 @@ impl Inode { ino, generation, ref_count: 0, - parent_ino, + parent: Some(parent), size: dir_entry.file_size() as u64, - block_size: fat_fs.bpb().bytes_per_sector() as u32, + block_size: fat_fs.bytes_per_sector() as u32, kind, read_only: dir_entry.is_readonly(), atime, @@ -158,17 +155,15 @@ impl Inode { } pub fn root_inode(fat_fs: &FatFs, uid: u32, gid: u32) -> Inode { - // let generation = Self::new_generation(); - - let root_cluster = fat_fs.bpb().root_cluster().unwrap_or(0); + let root_cluster = fat_fs.root_cluster().unwrap_or(0); Inode { - ino: 1, - generation: 0, + ino: ROOT_INO, + generation: 0, // root cluster always has constant generation of 0 ref_count: 0, - parent_ino: ROOT_INO, // parent is self + parent: None, // parent is self size: 0, - block_size: fat_fs.bpb().bytes_per_sector() as u32, + block_size: fat_fs.bytes_per_sector() as u32, kind: Kind::Dir, read_only: false, atime: SystemTime::UNIX_EPOCH, @@ -225,8 +220,18 @@ impl Inode { self.ref_count } - pub fn parent_ino(&self) -> u64 { - self.parent_ino + pub fn parent(&self) -> Option { + self.parent.clone() + } + + pub fn size(&self) -> u64 { + self.size + } + + pub fn update_size(&mut self, new_size: u64) { + self.size = new_size; + + todo!("update dir entry") } pub fn kind(&self) -> Kind { @@ -241,6 +246,10 @@ impl Inode { self.kind == Kind::Dir } + pub fn is_read_only(&self) -> bool { + self.read_only + } + pub fn first_cluster(&self) -> u32 { self.first_cluster } @@ -298,4 +307,12 @@ impl Inode { Ok(fat_fs.file_reader(self.first_cluster())) } + + pub fn file_writer<'a>(&'a self, fat_fs: &'a FatFs) -> Result, i32> { + if self.is_dir() { + return Err(EISDIR); + } + + Ok(fat_fs.file_writer(self.first_cluster())) + } } diff --git a/fat-fuse/src/lib.rs b/fat-fuse/src/lib.rs index 5291746..4aa53eb 100644 --- a/fat-fuse/src/lib.rs +++ b/fat-fuse/src/lib.rs @@ -1,15 +1,16 @@ mod fuse; mod inode; +use std::cell::RefCell; use std::collections::BTreeMap; use std::rc::Rc; use fat_bits::dir::DirEntry; use fat_bits::{FatFs, SliceLike}; use fxhash::FxHashMap; -use log::debug; +use log::{debug, error}; -use crate::inode::Inode; +use crate::inode::{Inode, InodeRef}; #[allow(dead_code)] pub struct FatFuse { @@ -21,7 +22,7 @@ pub struct FatFuse { next_ino: u64, next_fh: u64, - inode_table: BTreeMap, + inode_table: BTreeMap, ino_by_first_cluster: BTreeMap, ino_by_fh: BTreeMap, @@ -84,26 +85,28 @@ impl FatFuse { fh } - fn insert_inode(&mut self, inode: Inode) -> &mut Inode { + fn insert_inode(&mut self, inode: Inode) -> InodeRef { let ino = inode.ino(); let generation = inode.generation(); let first_cluster = inode.first_cluster(); // let old_inode = self.inode_table.insert(ino, inode); + let inode = Rc::new(RefCell::new(inode)); + let entry = self.inode_table.entry(ino); - let (new_inode, old_inode): (&mut Inode, Option) = match entry { + let (new_inode, old_inode) = match entry { std::collections::btree_map::Entry::Vacant(vacant_entry) => { let new_inode = vacant_entry.insert(inode); - (new_inode, None) + (Rc::clone(new_inode), None) } std::collections::btree_map::Entry::Occupied(occupied_entry) => { let entry_ref = occupied_entry.into_mut(); let old_inode = std::mem::replace(entry_ref, inode); - (entry_ref, Some(old_inode)) + (Rc::clone(entry_ref), Some(old_inode)) } }; @@ -113,6 +116,8 @@ impl FatFuse { ); if let Some(old_inode) = old_inode { + let old_inode = old_inode.borrow(); + debug!("ejected inode {} {}", old_inode.ino(), old_inode.generation()); } @@ -122,22 +127,32 @@ impl FatFuse { } } - if let Some(old_ino) = self.ino_by_path.insert(new_inode.path(), ino) { - debug!("ejected old {} -> {} path to ino mapping", new_inode.path(), old_ino); + let path = new_inode.borrow().path(); + + if let Some(old_ino) = self.ino_by_path.insert(Rc::clone(&path), ino) { + debug!("ejected old {} -> {} path to ino mapping", path, old_ino); } new_inode } - fn drop_inode(&mut self, ino: u64) { - debug!("dropping ino {}", ino); + fn drop_inode(&mut self, inode: InodeRef) { + let inode = inode.borrow(); - let Some(inode) = self.inode_table.remove(&ino) else { - debug!("tried to drop inode with ino {}, but was not in table", ino); + let ino = inode.ino(); + + debug!("dropping inode {}", ino); + + let Some(removed_inode) = self.inode_table.remove(&ino) else { + error!("tried to drop inode with ino {}, but was not in table", ino); return; }; + if removed_inode.borrow().ino() != ino { + error!("removed inode was not expected inode"); + } + let first_cluster = inode.first_cluster(); if first_cluster != 0 { @@ -189,39 +204,28 @@ impl FatFuse { } } } - - let Some(parent_inode) = self.get_inode_mut(inode.parent_ino()) else { - panic!("parent inode {} does not exists anymore", inode.parent_ino()); - }; - - // dec refcount - parent_inode.dec_ref_count(1); } - fn get_inode(&self, ino: u64) -> Option<&Inode> { + fn get_inode(&self, ino: u64) -> Option<&InodeRef> { self.inode_table.get(&ino) } - fn get_inode_mut(&mut self, ino: u64) -> Option<&mut Inode> { - self.inode_table.get_mut(&ino) - } - fn get_or_make_inode_by_dir_entry( &mut self, dir_entry: &DirEntry, parent_ino: u64, parent_path: Rc, - ) -> &mut Inode { + ) -> InodeRef { + // try to find inode by first cluster first if dir_entry.first_cluster() != 0 - && self - .get_inode_by_first_cluster_mut(dir_entry.first_cluster()) - .is_some() + && let Some(inode) = self.get_inode_by_first_cluster(dir_entry.first_cluster()) { - return self - .get_inode_by_first_cluster_mut(dir_entry.first_cluster()) - .unwrap(); + return inode; } + // try to find inode by path + // mostly for empty files/directories which have a first cluster of 0 + let path = { let mut path = parent_path.as_ref().to_owned(); @@ -235,27 +239,25 @@ impl FatFuse { path }; - if self.get_inode_by_path_mut(&path).is_some() { - return self.get_inode_by_path_mut(&path).unwrap(); + if let Some(inode) = self.get_inode_by_path(&path) { + return inode; } // no inode found, make a new one let ino = self.next_ino(); - let Some(parent_inode) = self.get_inode_mut(parent_ino) else { + let Some(parent_inode) = self.get_inode(parent_ino).cloned() else { // TODO: what do we do here? should not happen panic!("parent_ino {} does not lead to inode", parent_ino); }; - // inc ref of parent - parent_inode.inc_ref_count(); - - let inode = Inode::new(&self.fat_fs, dir_entry, ino, self.uid, self.gid, path, parent_ino); + let inode = + Inode::new(&self.fat_fs, dir_entry, ino, self.uid, self.gid, path, parent_inode); self.insert_inode(inode) } - pub fn get_inode_by_first_cluster(&self, first_cluster: u32) -> Option<&Inode> { + pub fn get_inode_by_first_cluster(&self, first_cluster: u32) -> Option { if first_cluster == 0 { debug!("trying to get inode by first cluster 0"); @@ -265,7 +267,7 @@ impl FatFuse { let ino = self.ino_by_first_cluster.get(&first_cluster)?; if let Some(inode) = self.inode_table.get(ino) { - Some(inode) + Some(Rc::clone(inode)) } else { debug!( "first cluster {} is mapped to ino {}, but inode is not in table", @@ -276,31 +278,10 @@ impl FatFuse { } } - pub fn get_inode_by_first_cluster_mut(&mut self, first_cluster: u32) -> Option<&mut Inode> { - if first_cluster == 0 { - debug!("trying to get inode by first cluster 0"); - - return None; - } - - let ino = self.ino_by_first_cluster.get(&first_cluster)?; - - if let Some(inode) = self.inode_table.get_mut(ino) { - Some(inode) - } else { - debug!( - "first cluster {} is mapped to ino {}, but inode is not in table", - first_cluster, ino - ); - - None - } - } - - pub fn get_inode_by_fh(&self, fh: u64) -> Option<&Inode> { + pub fn get_inode_by_fh(&self, fh: u64) -> Option { let ino = *self.ino_by_fh.get(&fh)?; - if let Some(inode) = self.get_inode(ino) { + if let Some(inode) = self.get_inode(ino).cloned() { Some(inode) } else { debug!("fh {} is mapped to ino {}, but inode is not in table", fh, ino); @@ -309,34 +290,10 @@ impl FatFuse { } } - pub fn get_inode_by_fh_mut(&mut self, fh: u64) -> Option<&mut Inode> { - let ino = *self.ino_by_fh.get(&fh)?; - - if let Some(inode) = self.get_inode_mut(ino) { - Some(inode) - } else { - debug!("fh {} is mapped to ino {}, but inode is not in table", fh, ino); - - None - } - } - - pub fn get_inode_by_path(&self, path: &str) -> Option<&Inode> { + pub fn get_inode_by_path(&self, path: &str) -> Option { let ino = *self.ino_by_path.get(path)?; - if let Some(inode) = self.get_inode(ino) { - Some(inode) - } else { - debug!("path {} is mapped to ino {}, but inode is not in table", path, ino); - - None - } - } - - pub fn get_inode_by_path_mut(&mut self, path: &str) -> Option<&mut Inode> { - let ino = *self.ino_by_path.get(path)?; - - if let Some(inode) = self.get_inode_mut(ino) { + if let Some(inode) = self.get_inode(ino).cloned() { Some(inode) } else { debug!("path {} is mapped to ino {}, but inode is not in table", path, ino); diff --git a/fat-mount/src/main.rs b/fat-mount/src/main.rs index c7ad1c9..5a85ce6 100644 --- a/fat-mount/src/main.rs +++ b/fat-mount/src/main.rs @@ -1,4 +1,4 @@ -use std::fs::File; +use std::fs::OpenOptions; use std::sync::mpsc::channel; use fat_fuse::FatFuse; @@ -13,12 +13,13 @@ fn main() -> anyhow::Result<()> { let path = args.next().ok_or(anyhow::anyhow!("missing fs path"))?; let mountpoint = args.next().ok_or(anyhow::anyhow!("missing mount point"))?; - let file = File::open(path)?; + // let file = File::open(path)?; + let file = OpenOptions::new().read(true).write(true).open(path)?; let fat_fuse = FatFuse::new(file)?; let options = vec![ - MountOption::RO, + // MountOption::RO, MountOption::FSName("fat-fuse".to_owned()), MountOption::AutoUnmount, ]; From 2ed107478eafab2ab16a56a4db31d5a9f5f45281 Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Fri, 1 Aug 2025 22:42:13 +0200 Subject: [PATCH 13/18] correctly update file size after write --- fat-bits/src/dir.rs | 67 ++++++------ fat-bits/src/fat.rs | 62 +++++++----- fat-bits/src/fs_info.rs | 8 ++ fat-bits/src/iter.rs | 58 ++++++++++- fat-bits/src/lib.rs | 202 +++++++++++++++++-------------------- fat-bits/src/slice_like.rs | 58 +++++++++++ fat-bits/src/subslice.rs | 115 +++++++++++---------- fat-dump/src/main.rs | 2 +- fat-fuse/src/fuse.rs | 98 +++++++++++------- fat-fuse/src/inode.rs | 47 +++++++-- fat-fuse/src/lib.rs | 6 +- 11 files changed, 451 insertions(+), 272 deletions(-) create mode 100644 fat-bits/src/slice_like.rs diff --git a/fat-bits/src/dir.rs b/fat-bits/src/dir.rs index eb7460b..cf4b240 100644 --- a/fat-bits/src/dir.rs +++ b/fat-bits/src/dir.rs @@ -5,7 +5,10 @@ use bitflags::bitflags; use chrono::{NaiveDate, NaiveDateTime, TimeDelta}; use compact_str::CompactString; +use crate::FatFs; use crate::datetime::{Date, Time}; +use crate::iter::ClusterChainReader; +use crate::subslice::SubSliceMut; use crate::utils::{load_u16_le, load_u32_le}; bitflags! { @@ -67,6 +70,8 @@ pub struct DirEntry { checksum: u8, long_name: Option, + + offset: u64, } impl Display for DirEntry { @@ -141,7 +146,7 @@ impl DirEntry { name } - pub fn load(bytes: &[u8]) -> anyhow::Result { + pub fn load(bytes: &[u8], offset: u64) -> anyhow::Result { assert_eq!(bytes.len(), 32); let attr = Attr::from_bits_truncate(bytes[11]); @@ -196,12 +201,16 @@ impl DirEntry { file_size, long_name: None, checksum: Self::checksum(&bytes[..11]), + offset, }) } - pub fn write(&self, mut writer: impl Write) -> std::io::Result<()> { + fn write(&self, mut writer: impl Write) -> std::io::Result<()> { let mut buf = [0; 32]; + // fill name with 0x20 + buf[..11].copy_from_slice(&[0x20; 11]); + let mut name = self.name(); if name[0] == b'.' && self.is_hidden() { @@ -246,11 +255,21 @@ impl DirEntry { buf[28..].copy_from_slice(&self.file_size.to_le_bytes()); + eprintln!("writing new dir entry: {:?}", buf); + writer.write_all(&buf)?; Ok(()) } + /// write this DisEntry back to the underlying data + pub fn update(&self, fat_fs: &FatFs) -> std::io::Result<()> { + eprintln!("making new SubSliceMut at offset {:#X}", self.offset); + let sub_slice = SubSliceMut::new(fat_fs.inner.clone(), self.offset, 32); + + self.write(sub_slice) + } + /// indicates this DirEntry is empty /// /// can be either simply empty (0xe5) or the sentinel (0x00) that indicates that all following @@ -344,25 +363,6 @@ impl DirEntry { s.push('.'); } - // s += name; - - // for &c in self.name[..8].trim_ascii_end() { - // // stem - - // if !c.is_ascii() - // || c < 0x20 - // || !(c.is_ascii_alphanumeric() || VALID_SYMBOLS.contains(&c)) - // { - // // replace invalid character - // // characters above 127 are also ignored, even tho allowed - // s.push('?'); - - // continue; - // } - - // s.push(c as char); - // } - const VALID_SYMBOLS: &[u8] = &[ b'$', b'%', b'\'', b'-', b'_', b'@', b'~', b'`', b'!', b'(', b')', b'{', b'}', b'^', b'#', b'&', @@ -432,6 +432,10 @@ impl DirEntry { self.file_size } + pub fn update_file_size(&mut self, file_size: u32) { + self.file_size = file_size + } + pub fn checksum(name: &[u8]) -> u8 { let mut checksum: u8 = 0; @@ -526,7 +530,7 @@ enum DirEntryWrapper { } impl DirEntryWrapper { - pub fn load(bytes: &[u8]) -> anyhow::Result { + pub fn load(bytes: &[u8], offset: u64) -> anyhow::Result { assert_eq!(bytes.len(), 32); let attr = Attr::from_bits_truncate(bytes[11]); @@ -534,7 +538,7 @@ impl DirEntryWrapper { let dir_entry = if attr == Attr::LongName { DirEntryWrapper::LongName(LongNameDirEntry::load(bytes)?) } else { - DirEntryWrapper::Regular(DirEntry::load(bytes)?) + DirEntryWrapper::Regular(DirEntry::load(bytes, offset)?) }; Ok(dir_entry) @@ -642,14 +646,14 @@ impl LongFilenameBuf { } } -pub struct DirIter { - reader: R, +pub struct DirIter<'a> { + reader: ClusterChainReader<'a>, long_filename_buf: LongFilenameBuf, } -impl DirIter { - pub fn new(reader: R) -> DirIter { +impl<'a> DirIter<'a> { + pub fn new(reader: ClusterChainReader<'a>) -> Self { DirIter { reader, long_filename_buf: Default::default(), @@ -658,13 +662,16 @@ impl DirIter { /// inner function for iterator fn next_impl(&mut self) -> anyhow::Result> { + let offset = self.reader.current_offset(); + let mut chunk = [0; 32]; + if self.reader.read_exact(&mut chunk).is_err() { // nothing we can do here since we might be in an invalid state after a partial read - return Ok(None); + anyhow::bail!("read failed"); } - let dir_entry = DirEntryWrapper::load(&chunk) + let dir_entry = DirEntryWrapper::load(&chunk, offset) .map_err(|e| anyhow::anyhow!("failed to load dir entry: {e}"))?; let mut dir_entry = match dir_entry { @@ -712,7 +719,7 @@ impl DirIter { } } -impl Iterator for DirIter { +impl Iterator for DirIter<'_> { type Item = DirEntry; fn next(&mut self) -> Option { diff --git a/fat-bits/src/fat.rs b/fat-bits/src/fat.rs index 8cdc5d0..9391363 100644 --- a/fat-bits/src/fat.rs +++ b/fat-bits/src/fat.rs @@ -27,17 +27,23 @@ pub trait FatOps { fn get_entry(&self, cluster: u32) -> u32; fn set_entry(&mut self, cluster: u32, entry: u32); - fn valid_clusters(&self) -> RangeInclusive; - fn reserved_clusters(&self) -> RangeInclusive; - fn defective_cluster(&self) -> u32; - fn reserved_eof_clusters(&self) -> RangeInclusive; - fn eof_cluster(&self) -> u32; + fn valid_entries(&self) -> RangeInclusive; + fn reserved_entries(&self) -> RangeInclusive; + fn defective_entry(&self) -> u32; + fn reserved_eof_entries(&self) -> RangeInclusive; + fn eof_entry(&self) -> u32; - fn count_free_clusters(&self) -> usize { - self.valid_clusters() + fn count_free_clusters(&self) -> u32 { + self.valid_entries() .map(|cluster| self.get_entry(cluster)) .filter(|&entry| entry == 0) - .count() + .count() as u32 + } + + fn first_free_cluster(&self) -> Option { + self.valid_entries() + .map(|cluster| self.get_entry(cluster)) + .find(|&entry| entry == 0) } fn write_to_disk(&self, sub_slice: SubSliceMut) -> std::io::Result<()>; @@ -75,18 +81,18 @@ impl Fat { return Err(FatError::FreeCluster); } - if self.reserved_clusters().contains(&cluster) { + if self.reserved_entries().contains(&cluster) { // can't get next cluster for reserved cluster return Err(FatError::ReservedCluster(cluster)); } // defective cluster - if cluster == self.defective_cluster() { + if cluster == self.defective_entry() { // can't get next cluster for defective cluster return Err(FatError::DefectiveCluster); } - if self.reserved_eof_clusters().contains(&cluster) { + if self.reserved_eof_entries().contains(&cluster) { // Reserved and should not be used. May be interpreted as an allocated cluster and the // final cluster in the file (indicating end-of-file condition). // @@ -99,12 +105,12 @@ impl Fat { let entry = self.get_entry(cluster); // interpret second reserved block as EOF here - if entry == self.eof_cluster() || self.reserved_eof_clusters().contains(&entry) { + if entry == self.eof_entry() || self.reserved_eof_entries().contains(&entry) { return Ok(None); } // entry should be in the valid cluster range here; otherwise something went wrong - if !self.valid_clusters().contains(&entry) { + if !self.valid_entries().contains(&entry) { return Err(FatError::InvalidEntry(entry)); } @@ -195,23 +201,23 @@ impl FatOps for Fat12 { self.next_sectors[cluster as usize] = entry as u16; } - fn valid_clusters(&self) -> RangeInclusive { + fn valid_entries(&self) -> RangeInclusive { 2..=self.max } - fn reserved_clusters(&self) -> RangeInclusive { + fn reserved_entries(&self) -> RangeInclusive { (self.max as u32 + 1)..=0xFF6 } - fn defective_cluster(&self) -> u32 { + fn defective_entry(&self) -> u32 { 0xFF7 } - fn reserved_eof_clusters(&self) -> RangeInclusive { + fn reserved_eof_entries(&self) -> RangeInclusive { 0xFF8..=0xFFE } - fn eof_cluster(&self) -> u32 { + fn eof_entry(&self) -> u32 { 0xFFF } @@ -322,23 +328,23 @@ impl FatOps for Fat16 { self.next_sectors[cluster as usize] = entry as u16; } - fn valid_clusters(&self) -> RangeInclusive { + fn valid_entries(&self) -> RangeInclusive { 2..=self.max } - fn reserved_clusters(&self) -> RangeInclusive { + fn reserved_entries(&self) -> RangeInclusive { (self.max as u32 + 1)..=0xFFF6 } - fn defective_cluster(&self) -> u32 { + fn defective_entry(&self) -> u32 { 0xFFF7 } - fn reserved_eof_clusters(&self) -> RangeInclusive { + fn reserved_eof_entries(&self) -> RangeInclusive { 0xFFF8..=0xFFFE } - fn eof_cluster(&self) -> u32 { + fn eof_entry(&self) -> u32 { 0xFFFF } @@ -420,23 +426,23 @@ impl FatOps for Fat32 { self.next_sectors[cluster as usize] = entry; } - fn valid_clusters(&self) -> RangeInclusive { + fn valid_entries(&self) -> RangeInclusive { 2..=self.max } - fn reserved_clusters(&self) -> RangeInclusive { + fn reserved_entries(&self) -> RangeInclusive { (self.max + 1)..=0xFFFFFFF6 } - fn defective_cluster(&self) -> u32 { + fn defective_entry(&self) -> u32 { 0xFFFFFFF7 } - fn reserved_eof_clusters(&self) -> RangeInclusive { + fn reserved_eof_entries(&self) -> RangeInclusive { 0xFFFFFFF8..=0xFFFFFFFE } - fn eof_cluster(&self) -> u32 { + fn eof_entry(&self) -> u32 { 0xFFFFFFFF } diff --git a/fat-bits/src/fs_info.rs b/fat-bits/src/fs_info.rs index f381652..b8ba50f 100644 --- a/fat-bits/src/fs_info.rs +++ b/fat-bits/src/fs_info.rs @@ -40,4 +40,12 @@ impl FsInfo { next_free, }) } + + pub fn free_count(&self) -> u32 { + self.free_count + } + + pub fn next_free(&self) -> Option { + Some(self.next_free) + } } diff --git a/fat-bits/src/iter.rs b/fat-bits/src/iter.rs index 726198e..aaf7eaf 100644 --- a/fat-bits/src/iter.rs +++ b/fat-bits/src/iter.rs @@ -1,7 +1,7 @@ use std::io::{Read, Write}; -use crate::FatFs; use crate::subslice::{SubSlice, SubSliceMut}; +use crate::{FatFs, FatType}; pub struct ClusterChainReader<'a> { fat_fs: &'a FatFs, @@ -12,7 +12,7 @@ pub struct ClusterChainReader<'a> { } impl<'a> ClusterChainReader<'a> { - pub fn new(fat_fs: &'a FatFs, first_cluster: u32) -> ClusterChainReader<'a> { + pub fn new(fat_fs: &'a FatFs, first_cluster: u32) -> Self { let next_cluster = fat_fs.next_cluster(first_cluster).unwrap_or(None); let sub_slice = fat_fs.cluster_as_subslice(first_cluster); @@ -24,6 +24,28 @@ impl<'a> ClusterChainReader<'a> { } } + pub fn root_dir_reader(fat_fs: &'a FatFs) -> Self { + match fat_fs.fat_type() { + FatType::Fat12 | FatType::Fat16 => { + // fixed root dir, so no need to chain + // get a single SubSlice for it and next_cluster is None + + let sub_slice = fat_fs.root_dir_as_subslice(); + + ClusterChainReader { + fat_fs, + sub_slice, + next_cluster: None, + } + } + FatType::Fat32 => { + // FAT is directory_like, so get a real chain reader + + Self::new(fat_fs, fat_fs.bpb.root_cluster().unwrap()) + } + } + } + fn move_to_next_cluster(&mut self) -> bool { let Some(next_cluster) = self.next_cluster else { return false; @@ -55,6 +77,10 @@ impl<'a> ClusterChainReader<'a> { n } + + pub fn current_offset(&self) -> u64 { + self.sub_slice.offset() + } } impl Read for ClusterChainReader<'_> { @@ -78,7 +104,7 @@ pub struct ClusterChainWriter<'a> { } impl<'a> ClusterChainWriter<'a> { - pub fn new(fat_fs: &'a FatFs, first_cluster: u32) -> ClusterChainWriter<'a> { + pub fn new(fat_fs: &'a FatFs, first_cluster: u32) -> Self { let next_cluster = fat_fs.next_cluster(first_cluster).unwrap_or(None); let sub_slice = fat_fs.cluster_as_subslice_mut(first_cluster); @@ -90,6 +116,28 @@ impl<'a> ClusterChainWriter<'a> { } } + pub fn root_dir_writer(fat_fs: &'a FatFs) -> Self { + match fat_fs.fat_type() { + FatType::Fat12 | FatType::Fat16 => { + // fixed root dir, so no need to chain + // get a single SubSliceMut for it and next_cluster is None + + let sub_slice = fat_fs.root_dir_as_subslice_mut(); + + ClusterChainWriter { + fat_fs, + sub_slice, + next_cluster: None, + } + } + FatType::Fat32 => { + // FAT is directory_like, so get a real chain writer + + Self::new(fat_fs, fat_fs.bpb.root_cluster().unwrap()) + } + } + } + fn move_to_next_cluster(&mut self) -> bool { // TODO: should allocate a new cluster here! let Some(next_cluster) = self.next_cluster else { @@ -122,6 +170,10 @@ impl<'a> ClusterChainWriter<'a> { n } + + pub fn current_offset(&self) -> u64 { + self.sub_slice.offset() + } } impl Write for ClusterChainWriter<'_> { diff --git a/fat-bits/src/lib.rs b/fat-bits/src/lib.rs index a2f0d7f..139bb23 100644 --- a/fat-bits/src/lib.rs +++ b/fat-bits/src/lib.rs @@ -1,10 +1,11 @@ use std::cell::RefCell; use std::fmt::Display; -use std::io::{Read, Seek, SeekFrom, Write}; use std::rc::Rc; use crate::dir::DirIter; use crate::fat::{FatError, FatOps}; +use crate::iter::ClusterChainReader; +pub use crate::slice_like::SliceLike; use crate::subslice::{SubSlice, SubSliceMut}; pub mod bpb; @@ -13,6 +14,7 @@ pub mod dir; pub mod fat; pub mod fs_info; pub mod iter; +mod slice_like; mod subslice; mod utils; @@ -23,80 +25,24 @@ pub enum FatType { Fat32, } -pub trait SliceLike { - fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()>; - - fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()>; -} - -impl SliceLike for &mut [u8] { - fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> { - if offset as usize + buf.len() > self.len() { - return Err(std::io::Error::other(anyhow::anyhow!( - "reading {} bytes at offset {} is out of bounds for slice of len {}", - buf.len(), - offset, - self.len() - ))); - } - - buf.copy_from_slice(&self[offset as usize..][..buf.len()]); - - Ok(()) - } - - fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()> { - if offset as usize + bytes.len() > self.len() { - return Err(std::io::Error::other(anyhow::anyhow!( - "writing {} bytes at offset {} is out of bounds for slice of len {}", - bytes.len(), - offset, - self.len() - ))); - } - - self[offset as usize..][..bytes.len()].copy_from_slice(bytes); - - Ok(()) - } -} - -impl SliceLike for std::fs::File { - fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> { - self.seek(SeekFrom::Start(offset))?; - - self.read_exact(buf)?; - - Ok(()) - } - - fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()> { - self.seek(SeekFrom::Start(offset))?; - - self.write_all(bytes)?; - - Ok(()) - } -} - -#[allow(dead_code)] pub struct FatFs { inner: Rc>, - fat_offset: u64, - fat_size: usize, - + // fat_offset: u64, + // fat_size: usize, root_dir_offset: Option, root_dir_size: usize, pub data_offset: u64, - data_size: usize, - + // data_size: usize, bytes_per_cluster: usize, bpb: bpb::Bpb, fat: fat::Fat, + + next_free: Option, + free_count: u32, } impl Display for FatFs { @@ -146,42 +92,93 @@ impl FatFs { // } // } - let fat_offset = bpb.fat_offset(); - let fat_size = bpb.fat_len_bytes(); + // let fat_offset = bpb.fat_offset(); + // let fat_size = bpb.fat_len_bytes(); let root_dir_offset = bpb.root_directory_offset(); let root_dir_size = bpb.root_dir_len_bytes(); let data_offset = bpb.data_offset(); - let data_size = bpb.data_len_bytes(); + // let data_size = bpb.data_len_bytes(); let bytes_per_cluster = bpb.bytes_per_cluster(); + let next_free = fat.first_free_cluster(); + let free_count = fat.count_free_clusters(); + Ok(FatFs { inner: data, - fat_offset, - fat_size, + // fat_offset, + // fat_size, root_dir_offset, root_dir_size, data_offset, - data_size, + // data_size, bytes_per_cluster, bpb, fat, + next_free, + free_count, }) } + pub fn fat_type(&self) -> FatType { + match &self.fat { + fat::Fat::Fat12(_) => FatType::Fat12, + fat::Fat::Fat16(_) => FatType::Fat16, + fat::Fat::Fat32(_) => FatType::Fat32, + } + } + /// byte offset of data cluster fn data_cluster_to_offset(&self, cluster: u32) -> u64 { // assert!(cluster >= 2); - assert!(self.fat.valid_clusters().contains(&cluster)); + assert!(self.fat.valid_entries().contains(&cluster)); self.data_offset + (cluster - 2) as u64 * self.bytes_per_cluster as u64 } - pub fn free_clusters(&self) -> usize { - self.fat.count_free_clusters() + pub fn free_clusters(&self) -> u32 { + // self.fat.count_free_clusters() + self.free_count + } + + pub fn alloc_cluster(&mut self) -> Option { + let Some(cluster) = self.next_free else { + // no free cluster + return None; + }; + + // set cluster as taken + self.fat.set_entry(cluster, self.fat.eof_entry()); + + // something went terribly wrong + assert_ne!(self.free_count, 0); + + self.free_count -= 1; + + // find next free cluster + self.next_free = self.fat.first_free_cluster(); + + Some(cluster) + } + + pub fn dealloc_cluster(&mut self, cluster: u32) { + // assert cluster is actually valid + assert!( + self.fat + .valid_entries() + .contains(&self.fat.get_entry(cluster)) + ); + + self.fat.set_entry(cluster, 0); + + if self.next_free.is_none() || self.next_free.unwrap() > cluster { + self.next_free = Some(cluster); + } + + self.free_count += 1; } pub fn bytes_per_sector(&self) -> u16 { @@ -203,13 +200,27 @@ impl FatFs { self.fat.get_next_cluster(cluster) } + pub fn cluster_as_subslice(&self, cluster: u32) -> SubSlice { + if cluster == 0 { + // for cluster 0 simply return empty subslice + // this makes things a bit easier, since cluster 0 is used as a marker that a file/dir + // is empty + + return SubSlice::new(self.inner.clone(), 0, 0); + } + + let offset = self.data_cluster_to_offset(cluster); + + SubSlice::new(self.inner.clone(), offset, self.bytes_per_cluster) + } + pub fn cluster_as_subslice_mut(&self, cluster: u32) -> SubSliceMut { if cluster == 0 { // for cluster 0 simply return empty subslice // this makes things a bit easier, since cluster 0 is used as a marker that a file/dir // is empty - SubSliceMut::new(self.inner.clone(), 0, 0); + return SubSliceMut::new(self.inner.clone(), 0, 0); } let offset = self.data_cluster_to_offset(cluster); @@ -217,18 +228,12 @@ impl FatFs { SubSliceMut::new(self.inner.clone(), offset, self.bytes_per_cluster) } - pub fn cluster_as_subslice(&self, cluster: u32) -> SubSlice { - if cluster == 0 { - // for cluster 0 simply return empty subslice - // this makes things a bit easier, since cluster 0 is used as a marker that a file/dir - // is empty + fn root_dir_as_subslice(&self) -> SubSlice { + SubSlice::new(self.inner.clone(), self.root_dir_offset.unwrap(), self.root_dir_size) + } - SubSlice::new(self.inner.clone(), 0, 0); - } - - let offset = self.data_cluster_to_offset(cluster); - - SubSlice::new(self.inner.clone(), offset, self.bytes_per_cluster) + fn root_dir_as_subslice_mut(&self) -> SubSliceMut { + SubSliceMut::new(self.inner.clone(), self.root_dir_offset.unwrap(), self.root_dir_size) } fn chain_reader(&'_ self, first_cluster: u32) -> iter::ClusterChainReader<'_> { @@ -239,35 +244,16 @@ impl FatFs { iter::ClusterChainWriter::new(self, first_cluster) } - pub fn root_dir_iter<'a>(&'a self) -> DirIter> { - // Box + '_> - // TODO: maybe wrap this in another RootDirIter enum, so we don't have to Box + pub fn root_dir_iter<'a>(&self) -> DirIter<'_> { + let reader = ClusterChainReader::root_dir_reader(self); - if let Some(root_dir_offset) = self.root_dir_offset { - // FAT12/FAT16 - - let sub_slice = SubSlice::new(self.inner.clone(), root_dir_offset, self.root_dir_size); - - return DirIter::new(Box::new(sub_slice)); - } - - // FAT32 - - // can't fail; we're in the FAT32 case - let root_cluster = self.bpb.root_cluster().unwrap(); - - let cluster_iter = iter::ClusterChainReader::new(self, root_cluster); - - DirIter::new(Box::new(cluster_iter)) + DirIter::new(reader) } - pub fn dir_iter<'a>(&'a self, first_cluster: u32) -> DirIter> { - // TODO: return type must match root_dir_iter - // if the Box is changed there, update here as well - + pub fn dir_iter<'a>(&self, first_cluster: u32) -> DirIter<'_> { let cluster_iter = self.chain_reader(first_cluster); - DirIter::new(Box::new(cluster_iter)) + DirIter::new(cluster_iter) } pub fn file_reader(&self, first_cluster: u32) -> iter::ClusterChainReader<'_> { diff --git a/fat-bits/src/slice_like.rs b/fat-bits/src/slice_like.rs new file mode 100644 index 0000000..8f72df8 --- /dev/null +++ b/fat-bits/src/slice_like.rs @@ -0,0 +1,58 @@ +use std::fs::File; +use std::io::{Read as _, Seek as _, SeekFrom, Write as _}; + +pub trait SliceLike { + fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()>; + + fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()>; +} + +impl SliceLike for &mut [u8] { + fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> { + if offset as usize + buf.len() > self.len() { + return Err(std::io::Error::other(anyhow::anyhow!( + "reading {} bytes at offset {} is out of bounds for slice of len {}", + buf.len(), + offset, + self.len() + ))); + } + + buf.copy_from_slice(&self[offset as usize..][..buf.len()]); + + Ok(()) + } + + fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()> { + if offset as usize + bytes.len() > self.len() { + return Err(std::io::Error::other(anyhow::anyhow!( + "writing {} bytes at offset {} is out of bounds for slice of len {}", + bytes.len(), + offset, + self.len() + ))); + } + + self[offset as usize..][..bytes.len()].copy_from_slice(bytes); + + Ok(()) + } +} + +impl SliceLike for File { + fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> { + self.seek(SeekFrom::Start(offset))?; + + self.read_exact(buf)?; + + Ok(()) + } + + fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()> { + self.seek(SeekFrom::Start(offset))?; + + self.write_all(bytes)?; + + Ok(()) + } +} diff --git a/fat-bits/src/subslice.rs b/fat-bits/src/subslice.rs index 798fc74..3875d7b 100644 --- a/fat-bits/src/subslice.rs +++ b/fat-bits/src/subslice.rs @@ -5,6 +5,64 @@ use std::rc::Rc; use crate::SliceLike; +pub struct SubSlice { + data: Rc>, + + offset: u64, + len: usize, +} + +impl Debug for SubSlice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SubSliceMut") + .field("offset", &self.offset) + .field("len", &self.len) + .finish() + } +} + +impl<'a> SubSlice { + pub fn new(data: Rc>, offset: u64, len: usize) -> SubSlice { + SubSlice { data, offset, len } + } + + pub fn offset(&self) -> u64 { + self.offset + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn skip(&mut self, n: usize) -> usize { + let n = n.min(self.len()); + + self.offset += n as u64; + self.len -= n; + + n + } +} + +impl Read for SubSlice { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let bytes_to_read = self.len.min(buf.len()); + + self.data + .borrow_mut() + .read_at_offset(self.offset, &mut buf[..bytes_to_read])?; + + self.offset += bytes_to_read as u64; + self.len -= bytes_to_read; + + Ok(bytes_to_read) + } +} + pub struct SubSliceMut { // fat_fs: &'a FatFs, data: Rc>, @@ -29,6 +87,10 @@ impl SubSliceMut { } impl<'a> SubSliceMut { + pub fn offset(&self) -> u64 { + self.offset + } + pub fn len(&self) -> usize { self.len } @@ -80,56 +142,3 @@ impl Write for SubSliceMut { Ok(()) } } - -pub struct SubSlice { - data: Rc>, - - offset: u64, - len: usize, -} - -impl Debug for SubSlice { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SubSliceMut") - .field("offset", &self.offset) - .field("len", &self.len) - .finish() - } -} - -impl<'a> SubSlice { - pub fn new(data: Rc>, offset: u64, len: usize) -> SubSlice { - SubSlice { data, offset, len } - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - pub fn len(&self) -> usize { - self.len - } - - pub fn skip(&mut self, n: usize) -> usize { - let n = n.min(self.len()); - - self.offset += n as u64; - self.len -= n; - - n - } -} - -impl Read for SubSlice { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let bytes_to_read = self.len.min(buf.len()); - - self.data - .borrow_mut() - .read_at_offset(self.offset, &mut buf[..bytes_to_read])?; - - self.offset += bytes_to_read as u64; - self.len -= bytes_to_read; - - Ok(bytes_to_read) - } -} diff --git a/fat-dump/src/main.rs b/fat-dump/src/main.rs index e266352..90b5cb3 100644 --- a/fat-dump/src/main.rs +++ b/fat-dump/src/main.rs @@ -29,7 +29,7 @@ pub fn main() -> anyhow::Result<()> { println!( "free clusters: {} ({} bytes)", fat_fs.free_clusters(), - fat_fs.free_clusters() + fat_fs.free_clusters() as usize * fat_fs.bytes_per_sector() as usize * fat_fs.sectors_per_cluster() as usize ); diff --git a/fat-fuse/src/fuse.rs b/fat-fuse/src/fuse.rs index 938fe7c..b54b4f9 100644 --- a/fat-fuse/src/fuse.rs +++ b/fat-fuse/src/fuse.rs @@ -123,9 +123,6 @@ impl Filesystem for FatFuse { fh: Option, reply: fuser::ReplyAttr, ) { - // warn!("[Not Implemented] getattr(ino: {:#x?}, fh: {:#x?})", ino, fh); - // reply.error(ENOSYS); - let inode = if let Some(fh) = fh { let Some(inode) = self.get_inode_by_fh(fh) else { reply.error(EBADF); @@ -147,6 +144,7 @@ impl Filesystem for FatFuse { reply.attr(&TTL, &attr); } + #[allow(unused_variables)] fn setattr( &mut self, _req: &fuser::Request<'_>, @@ -165,23 +163,23 @@ impl Filesystem for FatFuse { flags: Option, reply: fuser::ReplyAttr, ) { - debug!( - "[Not Implemented] setattr(ino: {:#x?}, mode: {:?}, uid: {:?}, \ - gid: {:?}, size: {:?}, fh: {:?}, flags: {:?})", - ino, mode, uid, gid, size, fh, flags - ); - reply.error(ENOSYS); - return; + // debug!( + // "[Not Implemented] setattr(ino: {:#x?}, mode: {:?}, uid: {:?}, \ + // gid: {:?}, size: {:?}, fh: {:?}, flags: {:?})", + // ino, mode, uid, gid, size, fh, flags + // ); + // reply.error(ENOSYS); + // return; // TODO: implement this properly - // let Some(inode) = self.get_inode(ino) else { - // debug!("tried to get inode {ino}, but not found"); - // - // reply.error(ENOENT); - // return; - // }; - // - // reply.attr(&TTL, &inode.file_attr()); + let Some(inode) = self.get_inode(ino) else { + debug!("tried to get inode {ino}, but not found"); + + reply.error(ENOENT); + return; + }; + + reply.attr(&TTL, &inode.borrow().file_attr()); } fn readlink(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyData) { @@ -286,17 +284,12 @@ impl Filesystem for FatFuse { ino: u64, fh: u64, offset: i64, - size: u32, + mut size: u32, _flags: i32, _lock_owner: Option, reply: fuser::ReplyData, ) { - // warn!( - // "[Not Implemented] read(ino: {:#x?}, fh: {}, offset: {}, size: {}, \ - // flags: {:#x?}, lock_owner: {:?})", - // ino, fh, offset, size, flags, lock_owner - // ); - // reply.error(ENOSYS); + debug!("trying to read {size} bytes at offset {offset} from inode {ino} (fh: {fh})"); if offset < 0 { debug!("tried to read with negative offset {offset}"); @@ -330,6 +323,29 @@ impl Filesystem for FatFuse { return; } + let file_size = inode.size(); + + debug!("file_size: {}", file_size); + + if offset > file_size { + debug!("tried to read after EOF"); + + // offset is beyond file size, nothing to do here, just bail + reply.data(&[]); + return; + } + + if offset + size as u64 > file_size { + // tried to read beyond end of file, truncate size so we don't overflow + debug!( + "tried to read {size} bytes at offset {offset} from inode {ino}, but size is only {file_size}" + ); + + size = (file_size - offset) as u32; + + debug!("truncated read request size to {size}"); + } + let mut reader = match inode.file_reader(&self.fat_fs) { Ok(reader) => reader, Err(err) => { @@ -339,14 +355,14 @@ impl Filesystem for FatFuse { }; if reader.skip(offset) != offset { - // reader is exhausted, bail - reply.data(&[]); + // this should not happen as we checked for valid bounds earlier + reply.error(EIO); return; } let mut buf = vec![0; size as usize]; - let n = match reader.read(&mut buf) { + let bytes_read = match reader.read(&mut buf) { Ok(n) => n, Err(err) => { error!("error while reading: {err}"); @@ -355,10 +371,11 @@ impl Filesystem for FatFuse { return; } }; + if bytes_read != size as usize { + debug!("expected to read {size} bytes, but only read {bytes_read}"); + } - debug!("read {n} bytes"); - - reply.data(&buf[..n]); + reply.data(&buf[..bytes_read]); } fn write( @@ -391,7 +408,8 @@ impl Filesystem for FatFuse { return; }; - let inode = inode.borrow(); + // borrow mut so we can potentially update the file size later + let mut inode = inode.borrow_mut(); if inode.is_read_only() { reply.error(EBADF); @@ -458,12 +476,20 @@ impl Filesystem for FatFuse { bytes_written += data.len(); - reply.written(bytes_written as u32); - - // TODO: update file size if offset + bytes_written as u64 > inode.size() { - todo!() + debug!("write increased file size, updating..."); + + let new_file_size = offset + bytes_written as u64; + + if let Err(err) = inode.update_size(&self.fat_fs, new_file_size) { + debug!("error while updating size: {err}"); + + reply.error(EIO); + return; + } } + + reply.written(bytes_written as u32); } fn flush( diff --git a/fat-fuse/src/inode.rs b/fat-fuse/src/inode.rs index 6cce26f..35ed6e2 100644 --- a/fat-fuse/src/inode.rs +++ b/fat-fuse/src/inode.rs @@ -1,5 +1,5 @@ use std::cell::{LazyCell, RefCell}; -use std::rc::{Rc, Weak}; +use std::rc::Rc; use std::time::SystemTime; use chrono::{NaiveDateTime, NaiveTime}; @@ -48,7 +48,7 @@ impl From for fuser::FileType { pub const ROOT_INO: u64 = 1; pub type InodeRef = Rc>; -pub type InodeWeak = Weak>; +// pub type InodeWeak = Weak>; #[derive(Debug)] #[allow(dead_code)] @@ -220,18 +220,45 @@ impl Inode { self.ref_count } - pub fn parent(&self) -> Option { - self.parent.clone() + pub fn parent(&self) -> Option<&InodeRef> { + self.parent.as_ref() } pub fn size(&self) -> u64 { self.size } - pub fn update_size(&mut self, new_size: u64) { + pub fn update_size(&mut self, fat_fs: &FatFs, new_size: u64) -> anyhow::Result<()> { + let Some(parent_inode) = self.parent() else { + anyhow::bail!("parent inode of {} does not exist", self.ino); + }; + + let parent_inode = parent_inode.borrow(); + + // since we just wrote to the file with this inode, first cluster should not be zero + let Some(mut dir_entry) = parent_inode + .dir_iter(fat_fs) + .unwrap() + .find(|dir_entry| dir_entry.first_cluster() == self.first_cluster()) + else { + anyhow::bail!("could not find dir_entry corresponding to self in parent inode"); + }; + + debug!("new file size: {new_size}"); + + assert!(new_size <= u32::MAX as u64); + + dir_entry.update_file_size(new_size as u32); + + if dir_entry.update(fat_fs).is_err() { + anyhow::bail!("failed to update dir_entry for inode {}", self.ino); + } + + drop(parent_inode); + self.size = new_size; - todo!("update dir entry") + Ok(()) } pub fn kind(&self) -> Kind { @@ -285,8 +312,6 @@ impl Inode { } pub fn dir_iter(&self, fat_fs: &FatFs) -> Result, i32> { - // anyhow::ensure!(self.kind == Kind::Dir, "cannot dir_iter on a file"); - if self.kind != Kind::Dir { return Err(ENOTDIR); } @@ -315,4 +340,10 @@ impl Inode { Ok(fat_fs.file_writer(self.first_cluster())) } + + // pub fn write_back(&self, fat_fs: &FatFs) { + // // let + + // todo!() + // } } diff --git a/fat-fuse/src/lib.rs b/fat-fuse/src/lib.rs index 4aa53eb..7a13c46 100644 --- a/fat-fuse/src/lib.rs +++ b/fat-fuse/src/lib.rs @@ -143,16 +143,12 @@ impl FatFuse { debug!("dropping inode {}", ino); - let Some(removed_inode) = self.inode_table.remove(&ino) else { + if self.inode_table.remove(&ino).is_none() { error!("tried to drop inode with ino {}, but was not in table", ino); return; }; - if removed_inode.borrow().ino() != ino { - error!("removed inode was not expected inode"); - } - let first_cluster = inode.first_cluster(); if first_cluster != 0 { From c9e8833ac681369ea16b328959f7e0a90a8cf71f Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Sat, 2 Aug 2025 00:36:35 +0200 Subject: [PATCH 14/18] implemented rmdir --- fat-bits/src/dir.rs | 128 +++++++++++++++++++++++------------------- fat-fuse/src/fuse.rs | 74 ++++++++++++++---------- fat-fuse/src/inode.rs | 11 +++- fat-fuse/src/lib.rs | 17 +++--- 4 files changed, 131 insertions(+), 99 deletions(-) diff --git a/fat-bits/src/dir.rs b/fat-bits/src/dir.rs index cf4b240..4f7e40d 100644 --- a/fat-bits/src/dir.rs +++ b/fat-bits/src/dir.rs @@ -270,6 +270,17 @@ impl DirEntry { self.write(sub_slice) } + /// erase this DirEntry + pub fn erase(self, fat_fs: &FatFs) -> std::io::Result<()> { + let mut sub_slice = SubSliceMut::new(fat_fs.inner.clone(), self.offset, 32); + + // set first byte to 0xE5 (free) + sub_slice.write_all(&[0xe5])?; + + // paste over with zeros + sub_slice.write_all(&[0; 31]) + } + /// indicates this DirEntry is empty /// /// can be either simply empty (0xe5) or the sentinel (0x00) that indicates that all following @@ -660,62 +671,8 @@ impl<'a> DirIter<'a> { } } - /// inner function for iterator - fn next_impl(&mut self) -> anyhow::Result> { - let offset = self.reader.current_offset(); - - let mut chunk = [0; 32]; - - if self.reader.read_exact(&mut chunk).is_err() { - // nothing we can do here since we might be in an invalid state after a partial read - anyhow::bail!("read failed"); - } - - let dir_entry = DirEntryWrapper::load(&chunk, offset) - .map_err(|e| anyhow::anyhow!("failed to load dir entry: {e}"))?; - - let mut dir_entry = match dir_entry { - DirEntryWrapper::Regular(dir_entry) => dir_entry, - DirEntryWrapper::LongName(long_name) => { - self.long_filename_buf.next(long_name).map_err(|e| { - self.long_filename_buf.reset(); - anyhow::anyhow!("invalid long filename entry: {e}") - })?; - - return self.next_impl(); - } - }; - - if dir_entry.is_sentinel() { - return Ok(None); - } - - if dir_entry.is_empty() { - return self.next_impl(); - } - - if let Some(iter) = self - .long_filename_buf - .get_buf(dir_entry.checksum) - .map_err(|e| { - anyhow::anyhow!( - "failed to get long filename for {}: {}", - dir_entry.name_string(), - e - ) - })? - { - // attach long filename to dir_entry - - let long_filename: CompactString = - char::decode_utf16(iter).filter_map(|x| x.ok()).collect(); - - dir_entry.set_long_name(long_filename); - } - - self.long_filename_buf.reset(); - - Ok(Some(dir_entry)) + pub fn find_by_name(&mut self, name: &str) -> Option { + self.find(|dir_entry| &dir_entry.name_string() == name) } } @@ -723,7 +680,64 @@ impl Iterator for DirIter<'_> { type Item = DirEntry; fn next(&mut self) -> Option { - match self.next_impl() { + fn next_impl(me: &mut DirIter<'_>) -> anyhow::Result> { + let offset = me.reader.current_offset(); + + let mut chunk = [0; 32]; + + if me.reader.read_exact(&mut chunk).is_err() { + // nothing we can do here since we might be in an invalid state after a partial read + anyhow::bail!("read failed"); + } + + let dir_entry = DirEntryWrapper::load(&chunk, offset) + .map_err(|e| anyhow::anyhow!("failed to load dir entry: {e}"))?; + + let mut dir_entry = match dir_entry { + DirEntryWrapper::Regular(dir_entry) => dir_entry, + DirEntryWrapper::LongName(long_name) => { + me.long_filename_buf.next(long_name).map_err(|e| { + me.long_filename_buf.reset(); + anyhow::anyhow!("invalid long filename entry: {e}") + })?; + + return next_impl(me); + } + }; + + if dir_entry.is_sentinel() { + return Ok(None); + } + + if dir_entry.is_empty() { + return next_impl(me); + } + + if let Some(iter) = me + .long_filename_buf + .get_buf(dir_entry.checksum) + .map_err(|e| { + anyhow::anyhow!( + "failed to get long filename for {}: {}", + dir_entry.name_string(), + e + ) + })? + { + // attach long filename to dir_entry + + let long_filename: CompactString = + char::decode_utf16(iter).filter_map(|x| x.ok()).collect(); + + dir_entry.set_long_name(long_filename); + } + + me.long_filename_buf.reset(); + + Ok(Some(dir_entry)) + } + + match next_impl(self) { Ok(x) => x, Err(e) => { // print error message, try next diff --git a/fat-fuse/src/fuse.rs b/fat-fuse/src/fuse.rs index b54b4f9..52cfdd4 100644 --- a/fat-fuse/src/fuse.rs +++ b/fat-fuse/src/fuse.rs @@ -1,6 +1,5 @@ use std::ffi::c_int; use std::io::{Read, Write}; -use std::rc::Rc; use std::time::Duration; use fat_bits::dir::DirEntry; @@ -44,7 +43,8 @@ impl Filesystem for FatFuse { let Some(name) = name.to_str() else { // TODO: add proper handling of non-utf8 strings debug!("cannot convert OsStr {:?} to str", name); - reply.error(ENOSYS); + + reply.error(EINVAL); return; }; @@ -62,28 +62,17 @@ impl Filesystem for FatFuse { let parent_inode = parent_inode.borrow(); - let dir_entry: DirEntry = - match parent_inode - .dir_iter(&self.fat_fs) - .and_then(|mut dir_iter| { - dir_iter - .find(|dir_entry| &dir_entry.name_string() == name) - .ok_or(ENOENT) - }) { - Ok(dir_entry) => dir_entry, - Err(err) => { - debug!("error: {}", err); - reply.error(err); + let dir_entry: DirEntry = match parent_inode.find_child_by_name(&self.fat_fs, name) { + Ok(dir_entry) => dir_entry, + Err(err) => { + debug!("error: {}", err); + reply.error(err); - return; - } - }; + return; + } + }; - let inode = self.get_or_make_inode_by_dir_entry( - &dir_entry, - parent_inode.ino(), - parent_inode.path(), - ); + let inode = self.get_or_make_inode(&dir_entry, &parent_inode); let mut inode = inode.borrow_mut(); @@ -239,8 +228,39 @@ impl Filesystem for FatFuse { name: &std::ffi::OsStr, reply: fuser::ReplyEmpty, ) { - debug!("[Not Implemented] rmdir(parent: {:#x?}, name: {:?})", parent, name,); - reply.error(ENOSYS); + let Some(name) = name.to_str() else { + // TODO: add proper handling of non-utf8 strings + debug!("cannot convert OsStr {:?} to str", name); + + reply.error(EINVAL); + return; + }; + + let Some(parent_inode) = self.get_inode(parent) else { + debug!("parent inode {parent} does not exist"); + + reply.error(ENOENT); + return; + }; + + let dir_entry = match parent_inode.borrow().find_child_by_name(&self.fat_fs, name) { + Ok(dir_entry) => dir_entry, + Err(err) => { + debug!("parent inode {parent} has no child {name}"); + + reply.error(err); + return; + } + }; + + if let Err(err) = dir_entry.erase(&self.fat_fs) { + debug!("error while erasing DirEntry: {err}"); + + reply.error(EIO); + return; + } + + reply.ok(); } fn rename( @@ -651,14 +671,10 @@ impl Filesystem for FatFuse { // also skip over `offset` entries let dirs: Vec = dir_iter.skip(offset).collect(); - let dir_ino = dir_inode.ino(); - let dir_path = dir_inode.path(); - for dir_entry in dirs { let name = dir_entry.name_string(); - let inode = - self.get_or_make_inode_by_dir_entry(&dir_entry, dir_ino, Rc::clone(&dir_path)); + let inode = self.get_or_make_inode(&dir_entry, &dir_inode); let inode = inode.borrow(); diff --git a/fat-fuse/src/inode.rs b/fat-fuse/src/inode.rs index 35ed6e2..59352a4 100644 --- a/fat-fuse/src/inode.rs +++ b/fat-fuse/src/inode.rs @@ -4,10 +4,10 @@ use std::time::SystemTime; use chrono::{NaiveDateTime, NaiveTime}; use fat_bits::FatFs; -use fat_bits::dir::DirEntry; +use fat_bits::dir::{DirEntry, DirIter}; use fat_bits::iter::{ClusterChainReader, ClusterChainWriter}; use fuser::FileAttr; -use libc::{EISDIR, ENOTDIR}; +use libc::{EISDIR, ENOENT, ENOTDIR}; use log::debug; use rand::{Rng, SeedableRng as _}; @@ -311,7 +311,7 @@ impl Inode { } } - pub fn dir_iter(&self, fat_fs: &FatFs) -> Result, i32> { + pub fn dir_iter<'a>(&'a self, fat_fs: &'a FatFs) -> Result, i32> { if self.kind != Kind::Dir { return Err(ENOTDIR); } @@ -325,6 +325,11 @@ impl Inode { Ok(fat_fs.dir_iter(self.first_cluster)) } + pub fn find_child_by_name(&self, fat_fs: &FatFs, name: &str) -> Result { + self.dir_iter(fat_fs) + .and_then(|mut dir_iter| dir_iter.find_by_name(name).ok_or(ENOENT)) + } + pub fn file_reader<'a>(&'a self, fat_fs: &'a FatFs) -> Result, i32> { if self.is_dir() { return Err(EISDIR); diff --git a/fat-fuse/src/lib.rs b/fat-fuse/src/lib.rs index 7a13c46..4ef5dae 100644 --- a/fat-fuse/src/lib.rs +++ b/fat-fuse/src/lib.rs @@ -206,12 +206,9 @@ impl FatFuse { self.inode_table.get(&ino) } - fn get_or_make_inode_by_dir_entry( - &mut self, - dir_entry: &DirEntry, - parent_ino: u64, - parent_path: Rc, - ) -> InodeRef { + fn get_or_make_inode(&mut self, dir_entry: &DirEntry, parent: &Inode) -> InodeRef { + // let parent = parent.borrow(); + // try to find inode by first cluster first if dir_entry.first_cluster() != 0 && let Some(inode) = self.get_inode_by_first_cluster(dir_entry.first_cluster()) @@ -223,9 +220,9 @@ impl FatFuse { // mostly for empty files/directories which have a first cluster of 0 let path = { - let mut path = parent_path.as_ref().to_owned(); + let mut path = parent.path().as_ref().to_owned(); - if parent_ino != inode::ROOT_INO { + if parent.ino() != inode::ROOT_INO { // root inode already has trailing slash path.push('/'); } @@ -242,9 +239,9 @@ impl FatFuse { // no inode found, make a new one let ino = self.next_ino(); - let Some(parent_inode) = self.get_inode(parent_ino).cloned() else { + let Some(parent_inode) = self.get_inode(parent.ino()).cloned() else { // TODO: what do we do here? should not happen - panic!("parent_ino {} does not lead to inode", parent_ino); + panic!("parent_ino {} does not lead to inode", parent.ino()); }; let inode = From bdd01bd70e031accfd4acda1220e1f3d01a80924 Mon Sep 17 00:00:00 2001 From: Moritz Gmeiner Date: Sat, 2 Aug 2025 17:22:32 +0200 Subject: [PATCH 15/18] implemented setattr (properly) --- Cargo.lock | 2 + fat-bits/Cargo.toml | 1 + fat-bits/src/datetime.rs | 22 ++-- fat-bits/src/dir.rs | 142 +++++++++++----------- fat-fuse/Cargo.toml | 1 + fat-fuse/src/fuse.rs | 253 ++++++++++++++++++++++++++++----------- fat-fuse/src/inode.rs | 130 +++++++++++++------- fat-fuse/src/lib.rs | 4 +- 8 files changed, 356 insertions(+), 199 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0d44bf..83fc35b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,6 +238,7 @@ dependencies = [ "chrono", "compact_str", "enum_dispatch", + "log", "static_assertions", "thiserror 2.0.12", ] @@ -255,6 +256,7 @@ name = "fat-fuse" version = "0.1.0" dependencies = [ "anyhow", + "bitflags", "chrono", "compact_string", "fat-bits", diff --git a/fat-bits/Cargo.toml b/fat-bits/Cargo.toml index 45ec5d4..6c53e1f 100644 --- a/fat-bits/Cargo.toml +++ b/fat-bits/Cargo.toml @@ -12,5 +12,6 @@ chrono = { version = "0.4.41", default-features = false, features = [ ] } compact_str = "0.9.0" enum_dispatch = "0.3.13" +log = "0.4.27" static_assertions = "1.1.0" thiserror = "2.0.12" diff --git a/fat-bits/src/datetime.rs b/fat-bits/src/datetime.rs index ecc5309..74f43ea 100644 --- a/fat-bits/src/datetime.rs +++ b/fat-bits/src/datetime.rs @@ -1,8 +1,8 @@ use std::time::SystemTime; -use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, Timelike, Utc}; +use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveTime, Timelike}; -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub struct Date { repr: u16, } @@ -22,8 +22,7 @@ impl Date { Ok(date) } - #[allow(dead_code)] - pub fn from_day_month_year(day: u8, month: u8, year: u16) -> anyhow::Result { + fn from_day_month_year(day: u8, month: u8, year: u16) -> anyhow::Result { anyhow::ensure!(day <= 31, "invalid day: {}", day); anyhow::ensure!(month <= 12, "invalid month: {}", month); anyhow::ensure!(1980 <= year && year <= 2107, "invalid year: {}", year); @@ -33,10 +32,7 @@ impl Date { Ok(Date { repr }) } - #[allow(dead_code)] - pub fn from_system_time(time: SystemTime) -> anyhow::Result { - let datetime: DateTime = time.into(); - + pub fn from_datetime(datetime: DateTime) -> anyhow::Result { let date = datetime.date_naive(); Date::from_day_month_year( @@ -67,7 +63,7 @@ impl Date { } } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub struct Time { repr: u16, } @@ -79,8 +75,7 @@ impl Time { Ok(time) } - #[allow(dead_code)] - pub fn from_seconds_minutes_hours(seconds: u8, minutes: u8, hours: u8) -> anyhow::Result