diff --git a/Cargo.lock b/Cargo.lock index 3caec75..db9c49a 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", @@ -178,6 +202,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 +327,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 + } + } } 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(()) +}