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 + } + } }