From ef45e769b871ff794c3d4d1e63bbde5e4003f5c2 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Thu, 4 Dec 2025 17:06:21 +0700 Subject: [PATCH 01/25] fix: do not remove inode until forgotten --- src/inode_mapper.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/inode_mapper.rs b/src/inode_mapper.rs index 8e39e8b..a794cf6 100644 --- a/src/inode_mapper.rs +++ b/src/inode_mapper.rs @@ -472,18 +472,24 @@ impl InodeMapper { }); // Insert the child into the new parent's children map - if let Some(old_inode) = self + if let Some(_) = self .data .children .get_mut(newparent) .and_then(|children| children.insert(newname, child_inode)) { - let InodeValue { - parent: _, - name: _, - data, - } = self.data.inodes.remove(&old_inode).unwrap(); - Ok(Some((old_inode, data))) + // The FUSE file system owns the old inode until it issues enough forget calls + // to reduce the inode's reference count to 0. Therefore, inodes may not be removed from + // this list outside of the remove() abstraction, which is only called when refcount + // is 0. This corresponds to behavior where files continue to write to an old inode even + // if the inode has already been unlinked by either rename, unlink, or rmdir syscalls. + // let InodeValue { + // parent: _, + // name: _, + // data, + // } = self.data.inodes.remove(&old_inode).unwrap(); + // Ok(Some((old_inode, data))) + Ok(None) } else { Ok(None) } From 06ea2c8591c05bad6fec453dc0ddf61d30d9080d Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Thu, 4 Dec 2025 18:33:45 +0700 Subject: [PATCH 02/25] fix: child inode should be present when moved inside an empty inode --- src/core/inode_mapping.rs | 35 ++++++++++++++++++++ src/inode_mapper.rs | 70 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/core/inode_mapping.rs b/src/core/inode_mapping.rs index 89e7956..b7c697e 100644 --- a/src/core/inode_mapping.rs +++ b/src/core/inode_mapping.rs @@ -374,4 +374,39 @@ mod tests { let non_existent_path = resolver.resolve_id(non_existent_ino); assert_eq!(non_existent_path, PathBuf::from("non_existent")); } + + #[test] + fn test_path_resolver_back_and_forth_rename() { + let resolver = PathResolver::new(); + + // Test lookup and resolve_id for root + let root_ino = ROOT_INODE.into(); + let root_path = resolver.resolve_id(root_ino); + assert_eq!(root_path, PathBuf::from("")); + + // Add directories + let dir1_ino = resolver.lookup(root_ino, OsStr::new("dir1"), (), true); + let dir2_ino = resolver.lookup(dir1_ino, OsStr::new("dir2"), (), true); + let file_ino = resolver.lookup(root_ino, OsStr::new("file.txt"), (), true); + + // Rename file to a different directory + resolver.rename( + root_ino, + OsStr::new("file.txt"), + dir2_ino, + OsStr::new("file.txt"), + ); + let renamed_file_path = resolver.resolve_id(file_ino); + assert_eq!(renamed_file_path, PathBuf::from("dir1/dir2/file.txt")); + + // Rename file back to original directory + resolver.rename( + dir2_ino, + OsStr::new("file.txt"), + root_ino, + OsStr::new("file.txt"), + ); + let renamed_file_path = resolver.resolve_id(file_ino); + assert_eq!(renamed_file_path, PathBuf::from("file.txt")); + } } diff --git a/src/inode_mapper.rs b/src/inode_mapper.rs index a794cf6..ce792e3 100644 --- a/src/inode_mapper.rs +++ b/src/inode_mapper.rs @@ -475,8 +475,9 @@ impl InodeMapper { if let Some(_) = self .data .children - .get_mut(newparent) - .and_then(|children| children.insert(newname, child_inode)) + .entry(newparent.clone()) + .or_insert_with(HashMap::new) + .insert(newname, child_inode) { // The FUSE file system owns the old inode until it issues enough forget calls // to reduce the inode's reference count to 0. Therefore, inodes may not be removed from @@ -752,6 +753,71 @@ mod tests { assert_eq!(inode_value.name.as_os_str(), OsStr::new("new_name")); } + #[test] + fn test_rename_child_inode_into_empty_dir_inode() { + let mut mapper = InodeMapper::new(()); + let root = mapper.get_root_inode(); + + // Insert initial structure + let parent1 = mapper + .insert_child(&root, OsString::from("parent1"), |_| ()) + .unwrap(); + let parent2 = mapper + .insert_child(&parent1, OsString::from("parent2"), |_| ()) + .unwrap(); + let child = mapper + .insert_child(&root, OsString::from("test_name"), |_| ()) + .unwrap(); + + // Perform rename + let result = mapper.rename( + &root, + OsStr::new("test_name"), + &parent2, + OsString::from("test_name"), + ); + + // Assert successful rename + assert!(result.is_ok()); + assert_eq!(result.unwrap(), None); + + // Verify new location + let renamed_child = mapper.lookup(&parent2, OsStr::new("test_name")); + assert!(renamed_child.is_some()); + assert_eq!(renamed_child.unwrap().inode, &child); + + // Verify old location is empty + assert!(mapper.lookup(&root, OsStr::new("test_name")).is_none()); + + // Verify inode data is updated + let inode_value = mapper.get(&child).unwrap(); + assert_eq!(inode_value.parent, &parent2); + assert_eq!(inode_value.name.as_os_str(), OsStr::new("test_name")); + + // Perform rename back to original path + let result = mapper.rename( + &parent2, + OsStr::new("test_name"), + &root, + OsString::from("test_name"), + ); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), None); + + // Verify new location + let renamed_child = mapper.lookup(&root, OsStr::new("test_name")); + assert!(renamed_child.is_some()); + assert_eq!(renamed_child.unwrap().inode, &child); + + // Verify old location is empty + assert!(mapper.lookup(&parent2, OsStr::new("test_name")).is_none()); + + // Verify inode data is updated + let inode_value = mapper.get(&child).unwrap(); + assert_eq!(inode_value.parent, &root); + assert_eq!(inode_value.name.as_os_str(), OsStr::new("test_name")); + } + #[test] fn test_rename_non_existent_child() { let mut mapper = InodeMapper::new(0); From 2f57612d800a7680c26b3f60572f1015d0898540 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Thu, 18 Dec 2025 09:32:21 +0700 Subject: [PATCH 03/25] test: add test for preventing premature inode removals --- src/inode_mapper.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/inode_mapper.rs b/src/inode_mapper.rs index ce792e3..3f0fdd7 100644 --- a/src/inode_mapper.rs +++ b/src/inode_mapper.rs @@ -545,8 +545,8 @@ mod tests { use std::collections::HashSet; use std::ffi::OsString; - use crate::types::Inode; use crate::ROOT_INODE; + use crate::types::Inode; #[test] fn test_insert_child_returns_old_inode() { @@ -753,6 +753,54 @@ mod tests { assert_eq!(inode_value.name.as_os_str(), OsStr::new("new_name")); } + #[test] + fn test_should_not_prematurely_purge_old_inode_after_renaming() { + // Data fields of all inodes in this test are 1 to simulate reflection of the FUSE inode refcount + let mut mapper = InodeMapper::new(1u64); + let root = mapper.get_root_inode(); + + let parent1 = mapper + .insert_child(&root, OsString::from("parent1"), |_| 1) + .unwrap(); + let parent2 = mapper + .insert_child(&root, OsString::from("parent2"), |_| 1) + .unwrap(); + let child1 = mapper + .insert_child(&parent1, OsString::from("child1"), |_| 1) + .unwrap(); + let child2 = mapper + .insert_child(&parent2, OsString::from("child2"), |_| 1) + .unwrap(); + + // Rename child1 to child2 + mapper + .rename( + &parent1, + OsStr::new("child1"), + &parent2, + OsString::from("child2"), + ) + .expect("should be able to insert inode"); + assert!( + mapper.get(&child1).is_some(), + "first inode should be present" + ); + assert!( + mapper.get(&child1).unwrap().parent == &parent2, + "first inode should point to parent2 as parent" + ); + assert!( + mapper + .get_children(&parent2) + .contains(&(&Arc::new(OsString::from("child2")), &child1)), + "first inode should be in parent2's child node list" + ); + assert!( + mapper.get(&child2).is_some(), + "second inode must be present as an orphaned inode but not removed immediately" + ); + } + #[test] fn test_rename_child_inode_into_empty_dir_inode() { let mut mapper = InodeMapper::new(()); From 4573fa08a39150221900caf939bebcdf060a831b Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 13 Jan 2026 11:13:58 +0700 Subject: [PATCH 04/25] feat: add resolver to support persistent hard links --- Cargo.toml | 1 + src/core/inode_mapping.rs | 279 +++++++- src/inode_mapper.rs | 2 +- src/inode_multi_mapper.rs | 1284 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/types/file_id_type.rs | 121 +++- 6 files changed, 1661 insertions(+), 27 deletions(-) create mode 100644 src/inode_multi_mapper.rs diff --git a/Cargo.toml b/Cargo.toml index 158cb99..9a66422 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ parking_lot = { version = "0.12", features = ["deadlock_detection"], optional = # easy_fuser_async_macro = { path = "./easy_fuser_async_macro", optional = true } tokio = { version = "1.48", features = ["full"], optional = true } async-trait = { version = "0.1.89", optional = true } +bimap = { version = "0.6.3", features = ["std"] } [dev-dependencies] tempfile = "3.23" diff --git a/src/core/inode_mapping.rs b/src/core/inode_mapping.rs index b7c697e..948cc2e 100644 --- a/src/core/inode_mapping.rs +++ b/src/core/inode_mapping.rs @@ -1,12 +1,14 @@ use std::{ ffi::{OsStr, OsString}, + fmt::Debug, + hash::Hash, path::PathBuf, - sync::atomic::Ordering, + sync::{Arc, atomic::Ordering}, }; -use std::sync::{atomic::AtomicU64, RwLock}; +use std::sync::{RwLock, atomic::AtomicU64}; -use crate::inode_mapper::*; +use crate::inode_multi_mapper::*; use crate::types::*; pub(crate) const ROOT_INO: u64 = 1; @@ -42,6 +44,17 @@ impl InodeResolvable for Vec { } } +impl InodeResolvable for HybridId +where + BackingId: Clone + Eq + Hash + Send + Sync + Debug + 'static, +{ + type Resolver = HybridResolver; + + fn create_resolver() -> Self::Resolver { + HybridResolver::new() + } +} + /// FileIdResolver /// FileIdResolver handles its data behind Locks if needed and should not be nested inside a Mutex pub trait FileIdResolver: Send + Sync + 'static { @@ -102,7 +115,7 @@ impl FileIdResolver for InodeResolver { } pub struct ComponentsResolver { - mapper: RwLock>, + mapper: RwLock>, } impl FileIdResolver for ComponentsResolver { @@ -110,7 +123,7 @@ impl FileIdResolver for ComponentsResolver { fn new() -> Self { ComponentsResolver { - mapper: RwLock::new(InodeMapper::new(AtomicU64::new(0))), + mapper: RwLock::new(InodeMultiMapper::new(AtomicU64::new(0))), } } @@ -140,7 +153,7 @@ impl FileIdResolver for ComponentsResolver { self.mapper .write() .expect("Failed to acquire write lock") - .insert_child(&parent, child.to_os_string(), |_| { + .insert_child(&parent, child.to_os_string(), None, |_| { AtomicU64::new(if increment { 1 } else { 0 }) }) .expect("Failed to insert child"), @@ -153,23 +166,18 @@ impl FileIdResolver for ComponentsResolver { children: Vec<(OsString, ())>, increment: bool, ) -> Vec<(OsString, u64)> { + let value_creator = + |value_creator: ValueCreatorParams| match value_creator.existing_data { + Some(nlookup) => { + let count = nlookup.load(Ordering::Relaxed); + AtomicU64::new(if increment { count + 1 } else { count }) + } + None => AtomicU64::new(if increment { 1 } else { 0 }), + }; let children_with_creator: Vec<_> = children .iter() - .map(|(name, _)| { - ( - name.clone(), - |value_creator: ValueCreatorParams| match value_creator.existing_data - { - Some(nlookup) => { - let count = nlookup.load(Ordering::Relaxed); - AtomicU64::new(if increment { count + 1 } else { count }) - } - None => AtomicU64::new(if increment { 1 } else { 0 }), - }, - ) - }) + .map(|(name, _)| (name.clone(), None, value_creator)) .collect(); - let parent_inode = Inode::from(parent); let inserted_children = self .mapper @@ -177,7 +185,6 @@ impl FileIdResolver for ComponentsResolver { .expect("Failed to acquire write lock") .insert_children(&parent_inode, children_with_creator) .expect("Failed to insert children"); - inserted_children .into_iter() .zip(children) @@ -263,11 +270,152 @@ impl FileIdResolver for PathResolver { } } +pub struct HybridResolver +where + BackingId: Clone + Eq + Hash, +{ + mapper: Arc>>, +} + +impl FileIdResolver for HybridResolver +where + BackingId: Clone + Eq + Hash + Send + Sync + std::fmt::Debug + 'static, +{ + type ResolvedType = HybridId; + + fn new() -> Self { + let instance = Arc::new(RwLock::new(InodeMultiMapper::new(AtomicU64::new(0)))); + HybridResolver { mapper: instance } + } + + fn resolve_id(&self, ino: u64) -> Self::ResolvedType { + let first_path = self + .mapper + .read() + .unwrap() + .resolve(&Inode::from(ino)) + .expect("Failed to resolve inode") + .iter() + .map(|inode_info| (**inode_info.name).clone()) + .rev() + .collect::(); + HybridId::new(Inode::from(ino), first_path, self.mapper.clone()) + } + + fn lookup( + &self, + parent: u64, + child: &OsStr, + id: ::_Id, + increment: bool, + ) -> u64 { + let parent = Inode::from(parent); + { + // Optimistically assume the child exists + if let Some(lookup_result) = self + .mapper + .read() + .expect("cannot acquire read lock") + .lookup(&parent, child) && + // Backing ID must match to use the hot path + lookup_result.backing_id.cloned() == id + { + if increment { + lookup_result.data.fetch_add(1, Ordering::SeqCst); + } + return u64::from(lookup_result.inode.clone()); + } + } + // This scenario happens if the child node does not exist or the backing ID does not match + u64::from( + self.mapper + .write() + .expect("Failed to acquire write lock") + .insert_child(&parent, child.to_os_string(), id, |params| { + // If the child node already exists, use the existing reference count + let mut new_value = params + .existing_data + .map(|d| d.load(Ordering::SeqCst)) + .unwrap_or(0); + if increment { + new_value += 1; + } + AtomicU64::new(new_value) + }) + .expect("Failed to insert child"), + ) + } + + fn forget(&self, ino: u64, nlookup: u64) { + let inode = Inode::from(ino); + { + // Optimistically assume we don't have to remove yet + let guard = self.mapper.read().expect("Failed to acquire read lock"); + let inode_info = guard.get(&inode).expect("Failed to find inode"); + if inode_info.data.fetch_sub(nlookup, Ordering::SeqCst) > 0 { + return; + } + } + self.mapper + .write() + .expect("Failed to acquire write lock") + .remove(&inode) + .unwrap(); + } + + fn add_children( + &self, + parent: u64, + children: Vec<(OsString, ::_Id)>, + increment: bool, + ) -> Vec<(OsString, u64)> { + let value_creator = + |value_creator: ValueCreatorParams| match value_creator.existing_data { + Some(nlookup) => { + let count = nlookup.load(Ordering::Relaxed); + AtomicU64::new(if increment { count + 1 } else { count }) + } + None => AtomicU64::new(if increment { 1 } else { 0 }), + }; + let children_with_creator: Vec<_> = children + .iter() + .map(|(name, id)| (name.clone(), id.clone(), value_creator)) + .collect(); + let parent_inode = Inode::from(parent); + let inserted_children = self + .mapper + .write() + .expect("Failed to acquire write lock") + .insert_children(&parent_inode, children_with_creator) + .expect("Failed to insert children"); + inserted_children + .into_iter() + .zip(children) + .map(|(inode, (name, _))| (name, u64::from(inode))) + .collect() + } + + fn rename(&self, parent: u64, name: &OsStr, newparent: u64, newname: &OsStr) { + let parent_inode = Inode::from(parent); + let newparent_inode = Inode::from(newparent); + self.mapper + .write() + .expect("Failed to acquire write lock") + .rename( + &parent_inode, + name, + &newparent_inode, + newname.to_os_string(), + ) + .expect("Failed to rename inode"); + } +} + #[cfg(test)] mod tests { use super::*; use std::ffi::OsStr; - use std::path::PathBuf; + use std::path::{Path, PathBuf}; #[test] fn test_components_resolver() { @@ -375,6 +523,93 @@ mod tests { assert_eq!(non_existent_path, PathBuf::from("non_existent")); } + #[test] + fn test_hybrid_resolver() { + let resolver = HybridResolver::::new(); + + // Test lookup and resolve_id for root + let root_ino = ROOT_INODE.into(); + let root_id = resolver.resolve_id(root_ino); + assert_eq!(root_id.first_path(), ""); + + // Test lookup and resolve_id for child and create nested structures + let dir1_ino = resolver.lookup(root_ino, OsStr::new("dir1"), Some(1), true); + let dir1_id = resolver.resolve_id(dir1_ino); + assert_eq!(dir1_id.first_path(), "dir1"); + + let dir2_ino = resolver.lookup(dir1_ino, OsStr::new("dir2"), Some(2), true); + let dir2_id = resolver.resolve_id(dir2_ino); + assert_eq!(dir2_id.first_path(), "dir1/dir2"); + + // Test add_children + let grandchildren = vec![ + (OsString::from("grandchild1"), Some(3)), + (OsString::from("grandchild2"), Some(4)), + ]; + let added_grandchildren = resolver.add_children(dir2_ino, grandchildren, true); + assert_eq!(added_grandchildren.len(), 2); + for (name, ino) in added_grandchildren.iter() { + let child_path = resolver.resolve_id(*ino); + assert_eq!(child_path.first_path(), Path::new("dir1/dir2").join(name)); + } + + // Test forget + resolver.forget(added_grandchildren[0].1, 1); + + // Test rename within the same directory + resolver.rename( + dir2_ino, + OsStr::new("grandchild2"), + dir2_ino, + OsStr::new("grandchild2_renamed"), + ); + let renamed_grandchild_path = resolver.resolve_id(added_grandchildren[1].1); + assert_eq!( + renamed_grandchild_path.first_path(), + Path::new("dir1/dir2/grandchild2_renamed") + ); + + // Test rename to a different directory + let dir3_ino = resolver.lookup(root_ino, OsStr::new("dir3"), Some(5), true); + resolver.rename( + dir2_ino, + OsStr::new("grandchild2_renamed"), + dir3_ino, + OsStr::new("grandchild2_renamed"), + ); + let renamed_grandchild_path = resolver.resolve_id(added_grandchildren[1].1); + assert_eq!( + renamed_grandchild_path.first_path(), + Path::new("dir3/grandchild2_renamed") + ); + + // Test lookup for non-existent file + let non_existent_ino = + resolver.lookup(root_ino, OsStr::new("non_existent"), Some(6), false); + assert_ne!(non_existent_ino, 0); + let non_existent_path = resolver.resolve_id(non_existent_ino); + assert_eq!(non_existent_path.first_path(), Path::new("non_existent")); + + // Test lookup for a file with existing backing ID + let hard_link_ino = resolver.lookup(root_ino, OsStr::new("hard_link"), Some(7), true); + let hard_link_id = resolver.resolve_id(hard_link_ino); + assert_eq!(hard_link_id.first_path(), Path::new("hard_link")); + + let hard_link_ino_2 = resolver.lookup(dir2_ino, OsStr::new("hard_linked"), Some(7), true); + // TODO: Expand and get the full path + let hard_link_id_2 = resolver.resolve_id(hard_link_ino_2); + assert_eq!( + hard_link_ino_2, hard_link_ino, + "hard link should be the same if callers supply the same ID" + ); + + resolver.lookup(dir1_ino, OsStr::new("hard_linked_2"), Some(7), true); + let paths = hard_link_id_2.paths(100); + assert!(paths.contains(&PathBuf::from("dir1/dir2/hard_linked"))); + assert!(paths.contains(&PathBuf::from("hard_link"))); + assert!(paths.contains(&PathBuf::from("dir1/hard_linked_2"))); + } + #[test] fn test_path_resolver_back_and_forth_rename() { let resolver = PathResolver::new(); diff --git a/src/inode_mapper.rs b/src/inode_mapper.rs index 3f0fdd7..119798e 100644 --- a/src/inode_mapper.rs +++ b/src/inode_mapper.rs @@ -358,7 +358,7 @@ impl InodeMapper { /// # Notes /// - Returns `None` if any inode in the path is not found, indicating an incomplete or invalid path. /// - The root inode is identified when its parent is equal to itself and is never returned - pub fn resolve(&self, inode: &Inode) -> Option>> { + pub fn resolve(&self, inode: &Inode) -> Option>> { let mut result: Vec> = Vec::new(); let mut current_info = self.get(inode)?; let mut current_inode = inode.clone(); diff --git a/src/inode_multi_mapper.rs b/src/inode_multi_mapper.rs new file mode 100644 index 0000000..0925a29 --- /dev/null +++ b/src/inode_multi_mapper.rs @@ -0,0 +1,1284 @@ +use crate::types::{Inode, ROOT_INODE}; +use bimap::BiHashMap; +use std::{ + borrow::Borrow, + collections::{HashMap, HashSet}, + ffi::{OsStr, OsString}, + fmt::Debug, + hash::Hash, + ops::Deref, + sync::Arc, +}; + +#[derive(Debug)] +pub struct InodeMultiMapper +where + BackingId: Clone + Eq + Hash, + Data: Send + Sync + 'static, +{ + data: InodeData, + root_inode: Inode, + next_inode: Inode, +} + +#[derive(Debug)] +struct InodeData +where + BackingId: Clone + Eq + Hash, + Data: Send + Sync + 'static, +{ + /// A map of inodes' internal data + inodes: HashMap>, + /// A map of inodes' child nodes. + children: HashMap>, + /// Bidirectional hash map to allow lookups and upserts of inodes by + /// user-supplied backing IDs (e.g. using a stat and statfs call). + backing: BiHashMap, +} + +#[derive(Debug)] +struct InodeValue +where + Data: Send + Sync + 'static, +{ + links: HashMap>, + data: Data, +} + +#[derive(Debug)] +pub struct ValueCreatorParams<'a, Data> +where + Data: Send + Sync + 'static, +{ + pub new_inode: &'a Inode, + pub parent: &'a Inode, + pub child_name: &'a OsStr, + pub existing_data: Option<&'a Data>, +} + +#[derive(Debug)] + +pub struct LookupResult<'a, Data, BackingId> +where + Data: Send + Sync + 'static, + BackingId: Clone + Eq + Hash, +{ + pub inode: &'a Inode, + pub backing_id: Option<&'a BackingId>, + pub links: &'a HashMap>, + pub data: &'a Data, +} + +#[derive(Debug)] +pub struct InodeInfo<'a, Data> +where + Data: Send + Sync + 'static, +{ + pub links: &'a HashMap>, + #[allow(dead_code)] + pub data: &'a Data, +} + +impl<'a, Data> Clone for InodeInfo<'a, Data> +where + Data: Send + Sync + 'static, +{ + fn clone(&self) -> Self { + InodeInfo { + links: &self.links, + data: &self.data, + } + } +} + +#[derive(Debug)] +pub struct InodeResolveItem<'a, Data> +where + Data: Send + Sync + 'static, +{ + pub parent: &'a Inode, + pub name: &'a Arc, + pub inode: InodeInfo<'a, Data>, +} + +impl<'a, Data> Clone for InodeResolveItem<'a, Data> +where + Data: Send + Sync + 'static, +{ + fn clone(&self) -> Self { + InodeResolveItem { + parent: &self.parent, + name: &self.name, + inode: self.inode.clone(), + } + } +} + +impl<'a, Data> Deref for InodeResolveItem<'a, Data> +where + Data: Send + Sync + 'static, +{ + type Target = InodeInfo<'a, Data>; + + fn deref(&self) -> &Self::Target { + &self.inode + } +} + +#[derive(Debug)] +pub struct InodeInfoMut<'a, Data> +where + Data: Send + Sync + 'static, +{ + #[allow(dead_code)] + links: &'a mut HashMap>, + #[allow(dead_code)] + data: &'a mut Data, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum InsertError { + ParentNotFound, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum RenameError { + NotFound, + ParentNotFound, + NewParentNotFound, +} + +/// A wrapper around `Arc` for efficient storage and comparison in hash maps. +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct OsStringWrapper(Arc); + +impl AsRef> for OsStringWrapper { + fn as_ref(&self) -> &Arc { + &self.0 + } +} + +impl AsMut> for OsStringWrapper { + fn as_mut(&mut self) -> &mut Arc { + &mut self.0 + } +} + +impl Borrow for OsStringWrapper { + fn borrow(&self) -> &OsStr { + self.0.as_os_str() + } +} + +impl InodeMultiMapper +where + BackingId: Clone + Eq + Hash, + Data: Send + Sync + 'static, +{ + /// Creates a new `InodeMultiMapper` instance with the root inode initialized. + /// + /// This function initializes the `InodeMultiMapper` with an empty structure and sets up the root inode + /// with the provided data. The root inode is assigned an empty name and its parent is set to itself. + pub fn new(data: Data) -> Self { + let mut result = InodeMultiMapper { + data: InodeData { + inodes: HashMap::new(), + children: HashMap::new(), + backing: BiHashMap::new(), + }, + root_inode: ROOT_INODE.clone(), + next_inode: ROOT_INODE.add_one(), + }; + result.data.inodes.insert( + ROOT_INODE.clone(), + InodeValue { + links: HashMap::from([( + ROOT_INODE.clone(), + HashSet::from([OsStringWrapper(Arc::new(OsString::from("")))]), + )]), + data, + }, + ); + result + } + + fn reserve_inode_space(&mut self, entries_count: usize) { + if self.data.inodes.is_empty() { + self.data.inodes.reserve(entries_count); + } else if self.data.inodes.capacity() < self.data.inodes.len() + entries_count { + self.data + .inodes + .reserve(entries_count + self.data.inodes.len() - self.data.inodes.capacity()); + } + } + + fn reserve_children_space(&mut self, entries_count: usize) { + if self.data.children.is_empty() { + self.data.children.reserve(entries_count); + } else if self.data.children.capacity() < self.data.children.len() + entries_count { + self.data + .children + .reserve(entries_count + self.data.children.len() - self.data.children.capacity()); + } + } + + fn reserve_inode_children_space(&mut self, parent: &Inode, entries_count: usize) { + if let Some(parent_children) = self.data.children.get_mut(parent) { + if parent_children.is_empty() { + parent_children.reserve(entries_count); + } else if parent_children.capacity() < parent_children.len() + entries_count { + parent_children + .reserve(entries_count + parent_children.len() - parent_children.capacity()); + } + } else { + self.data + .children + .insert(parent.clone(), HashMap::with_capacity(entries_count)); + } + } + + fn reserve_backing_space(&mut self, entries_count: usize) { + if self.data.backing.is_empty() { + self.data.backing.reserve(entries_count); + } else if self.data.backing.capacity() < self.data.backing.len() + entries_count { + self.data + .backing + .reserve(entries_count + self.data.backing.len() - self.data.backing.capacity()); + } + } + + pub fn get_root_inode(&self) -> Inode { + self.root_inode.clone() + } + + /// A private method that inserts a child inode into the InodeMultiMapper, + /// even if the parent doesn't exist. + /// + /// This function creates a new inode or updates an existing one, + /// associating it with the given parent and child name. It uses a + /// value_creator function to generate or update the data associated with + /// the inode. + /// + /// Note: This method doesn't check if the parent exists, which can lead to + /// inconsistencies if used incorrectly. It's primarily intended for internal use or in scenarios where the parent's existence is guaranteed. + /// + /// # Behavior: + /// - If the child doesn't exist: + /// - If the backing ID is not specified, or if the backing ID cannot be resolved to a valid inode: + /// - A new inode is created with a unique ID. + /// - The new inode is bi-directionally associated with the parent as well as to the backing ID. + /// - The data is created using the value_creator function. + /// - If the backing ID is specified and points to a valid existing inode, + /// - That existing inode will be associated with the parent instead. + /// - If the child already exists: + /// - If the backing ID is specified and points to a second existing inode, the child will be + /// unassociated from the parent and be replaced with the second inode. + /// - If the backing ID is specified and does not point to any inode, the backing ID will be + /// associated with the new inode. + /// - The data is updated using the value_creator function. + /// - The value_creator function is called with the inode, parent, child name, and existing data (if any) as arguments. + /// + /// # Caveats + /// - This method may create orphaned inodes if used with non-existent parents. Use with caution. + fn insert_child_unchecked( + &mut self, + parent: &Inode, + child: OsString, + backing_id: Option, + value_creator: impl Fn(ValueCreatorParams) -> Data, + ) -> Inode { + let child_name = OsStringWrapper(Arc::new(child)); + let parent_children = self + .data + .children + .entry(parent.clone()) + .or_insert_with(HashMap::new); + let backing_inode = backing_id + .clone() + .map(|backing_id| { + self.data + .backing + .get_by_right(&backing_id) + .map(|inode| inode.clone()) + }) + .flatten(); + let target_child_inode = parent_children.get(&child_name).map(|inode| inode.clone()); + match (backing_inode, target_child_inode) { + (Some(backing_inode), Some(target_child_inode)) => { + if backing_inode != target_child_inode { + let target_child_inode_data = self + .data + .inodes + .get_mut(&target_child_inode) + .expect("target child inode not found"); + // Deassociate parent from old child inode if possible + let links = target_child_inode_data + .links + .entry(parent.clone()) + .or_insert_with(HashSet::new); + links.remove(&child_name.clone()); + if links.is_empty() { + target_child_inode_data.links.remove(&parent.clone()); + } + } + let existing_inode_data = self + .data + .inodes + .get_mut(&backing_inode) + .expect("backing inode not found"); + // Associate parent to new child + existing_inode_data + .links + .entry(parent.clone()) + .or_insert_with(HashSet::new) + .insert(child_name.clone()); + existing_inode_data.data = value_creator(ValueCreatorParams { + parent: &parent, + new_inode: &backing_inode, + child_name: &child_name.as_ref(), + existing_data: Some(&existing_inode_data.data), + }); + // Associate new child to parent + parent_children.insert(child_name.clone(), backing_inode.clone()); + backing_inode + } + (None, Some(target_child_inode)) => { + let target_child_inode_data = self + .data + .inodes + .get_mut(&target_child_inode) + .expect("target child inode not found"); + // Associate parent to target child + target_child_inode_data + .links + .entry(parent.clone()) + .or_insert_with(HashSet::new) + .insert(child_name.clone()); + // Associate target child to parent + parent_children.insert(child_name.clone(), target_child_inode.clone()); + // Update data + target_child_inode_data.data = value_creator(ValueCreatorParams { + parent: &parent, + new_inode: &target_child_inode, + child_name: &child_name.as_ref(), + existing_data: Some(&target_child_inode_data.data), + }); + // Associate target child to backing ID + if let Some(backing_id) = backing_id { + self.data + .backing + .insert(target_child_inode.clone(), backing_id); + } + target_child_inode + } + (Some(backing_inode), None) => { + let backing_inode_data = self + .data + .inodes + .get_mut(&backing_inode) + .expect("backing inode not found"); + // Associate parent to child + backing_inode_data + .links + .entry(parent.clone()) + .or_insert_with(HashSet::new) + .insert(child_name.clone()); + // Associate child to parent + parent_children.insert(child_name.clone(), backing_inode.clone()); + // Update data + backing_inode_data.data = value_creator(ValueCreatorParams { + parent: &parent, + new_inode: &backing_inode, + child_name: &child_name.as_ref(), + existing_data: Some(&backing_inode_data.data), + }); + // Backing inode is already associated with the backing ID, so this step is skipped + backing_inode + } + (None, None) => { + let new_inode = loop { + let new_inode = self.next_inode.clone(); + self.next_inode = new_inode.add_one(); + if self.data.inodes.get(&new_inode).is_none() { + break new_inode; + } + }; + // Associate parent to child and initialize data + self.data.inodes.insert( + new_inode.clone(), + InodeValue { + links: HashMap::from([( + parent.clone(), + HashSet::from([child_name.clone()]), + )]), + data: value_creator(ValueCreatorParams { + parent: &parent, + new_inode: &new_inode, + child_name: &child_name.as_ref(), + existing_data: None, + }), + }, + ); + // Associate child to parent + parent_children.insert(child_name.clone(), new_inode.clone()); + if let Some(backing_id) = backing_id { + self.data.backing.insert(new_inode.clone(), backing_id); + } + new_inode + } + } + } + + /// Safely inserts a child inode into the InodeMultiMapper. + /// + /// This method checks if the parent exists before inserting the child. It uses a value_creator + /// function to generate the data associated with the new inode. If the backing ID is specified, + /// the new child will be associated with the backing ID. + /// + /// # Behavior + /// - Returns Err(InsertError::ParentNotFound) if the parent doesn't exist. + /// - If successful, returns Ok(Inode) with the newly created or existing child inode. + /// + /// The value_creator function is called with the new inode, parent inode, child name, and existing data (if any) as arguments. + pub fn insert_child( + &mut self, + parent: &Inode, + child: OsString, + backing_id: Option, + value_creator: impl Fn(ValueCreatorParams) -> Data, + ) -> Result { + if !self.data.inodes.contains_key(parent) { + return Err(InsertError::ParentNotFound); + } + Ok(self.insert_child_unchecked(parent, child, backing_id, value_creator)) + } + + /// Inserts multiple children into the InodeMultiMapper for a given parent inode. + /// + /// This method efficiently inserts multiple children at once, optimizing memory allocation + /// for the parent's children HashMap. It checks if the parent exists before insertion. + /// + /// # Behavior + /// - Returns Err(InsertError::ParentNotFound) if the parent doesn't exist. + /// - If successful, returns Ok(Vec) with the newly created or existing child inodes. + /// + /// The value_creator function is called with the new inode, parent inode, child name, and + /// existing data (if any) as arguments. + pub fn insert_children( + &mut self, + parent: &Inode, + children: Vec<( + OsString, + Option, + impl Fn(ValueCreatorParams) -> Data, + )>, + ) -> Result, InsertError> { + if !self.data.inodes.contains_key(parent) { + return Err(InsertError::ParentNotFound); + } + + self.reserve_inode_space(children.len()); + self.reserve_inode_children_space(parent, children.len()); + self.reserve_backing_space(children.len()); + + Ok(children + .into_iter() + .map(|(child, backing_id, value_creator)| { + self.insert_child_unchecked(parent, child, backing_id, value_creator) + }) + .collect()) + } + + /// Batch inserts multiple entries into the InodeMultiMapper, creating missing parent directories as needed. + /// + /// This method efficiently handles the insertion of multiple entries, potentially with nested paths. + /// It sorts entries by path length to ensure parent directories are created before their children. + /// + /// # Behavior + /// - Creates missing parent directories using the default_parent_creator function. (data field will always be null) + /// - Inserts entries using the provided value_creator function. + /// - Returns Err(InsertError::ParentNotFound) if the initial parent inode doesn't exist. + /// + /// # Note + /// Expects each entry's path to include the entry name as the last element. + /// + /// # Caveats + /// If the closures are not defined in same scope, there might be a compiler error concerning lifetimes (eg: implementation of `Fn` is not general enough) + /// To resolve this problem, always fully qualify the argumentsof the closure (eg: `|my_data: ValueCreatorParams| {}` and not `|my_data| {}`) + pub fn batch_insert( + &mut self, + parent: &Inode, + entries: Vec<( + Vec, + Option, + impl Fn(ValueCreatorParams) -> Data, + )>, + default_parent_creator: impl Fn(ValueCreatorParams) -> Data, + ) -> Result<(), InsertError> { + if !self.data.inodes.contains_key(parent) { + return Err(InsertError::ParentNotFound); + } + + // Sort entries by path length to ensure parents are created first + let mut sorted_entries = entries; + sorted_entries.sort_by_key(|f| f.0.len()); + + let mut path_cache: HashMap, Inode> = HashMap::new(); + path_cache.insert(vec![], parent.clone()); + + self.reserve_inode_space(sorted_entries.len()); + self.reserve_children_space(sorted_entries.len()); + self.reserve_inode_children_space(parent, sorted_entries.len()); + self.reserve_backing_space(sorted_entries.len()); + + for (mut path, backing_id, value_creator) in sorted_entries { + let name = path.pop().expect("Name should be provided"); + let parent_inode = + self.ensure_path_exists(&mut path_cache, &path, &default_parent_creator); + self.insert_child_unchecked(&parent_inode, name, backing_id, value_creator); + } + Ok(()) + } + + fn ensure_path_exists( + &mut self, + path_cache: &mut HashMap, Inode>, + path: &[OsString], + default_parent_creator: &impl Fn(ValueCreatorParams) -> Data, + ) -> Inode { + let mut current_inode = path_cache[&vec![]].clone(); + for (i, component) in path.iter().enumerate() { + let current_path = &path[..=i]; + if let Some(inode) = path_cache.get(current_path) { + current_inode = inode.clone(); + } else { + let child_inode = self + .data + .children + .get_mut(¤t_inode) + .and_then(|children| children.get(component.as_os_str())); + let new_inode = if let Some(child_inode) = child_inode { + child_inode.clone() + } else { + // Since backing_id and child_inode is both None, there will always be a new inode + self.insert_child_unchecked( + ¤t_inode, + component.clone(), + None, + |mut value_creator_params| { + value_creator_params.existing_data = None; + default_parent_creator(value_creator_params) + }, + ) + }; + path_cache.insert(current_path.to_vec(), new_inode.clone()); + current_inode = new_inode; + } + } + current_inode + } + + /// Resolves an inode to one combination of its full path components + /// + /// # Notes + /// - Due to the nature of an inode being able to have multiple links, there can be multiple combinations of path components + /// that resolve to the same inode. This method only returns the first combination of path components that + /// resolves to the inode. + /// - Returns `None` if any inode in the path is not found, indicating an incomplete or invalid path, or + /// there is an infinite loop (eg: if the inode is linked to itself and there is no way to trace back to the + /// root inode). + /// - The root inode is identified when its parent is equal to itself and is never returned + pub fn resolve(&self, inode: &Inode) -> Option>> { + let mut visited = HashSet::new(); + let mut result: Vec> = Vec::new(); + let mut current_info = self.get(inode)?; + let mut current_inode = inode.clone(); + + 'resolution_loop: loop { + let is_root_inode = current_inode == ROOT_INODE; + if is_root_inode { + break 'resolution_loop; + } + for (parent, names) in current_info.links.iter() { + if visited.contains(parent) { + // The parent inode has already been visited, do not follow, try another link + continue; + } + // There must be at least one name, orphaned inodes cannot be resolved + if names.is_empty() { + continue; + } + visited.insert(current_inode.clone()); + current_inode = parent.clone(); + result.push(InodeResolveItem { + parent, + name: names.iter().next().unwrap().as_ref(), + inode: current_info, + }); + current_info = self.get(¤t_inode)?; + continue 'resolution_loop; + } + return None; + } + Some(result) + } + + /// Recursively resolve all possible combinations of path components that resolve to the given inode, up to a given limit. + pub fn resolve_all<'a>( + &'a self, + inode: &Inode, + limit: usize, + ) -> Vec>> { + let mut result = vec![]; + + fn scoped_resolve<'a, Data, BackingId>( + mapper: &'a InodeMultiMapper, + result: &mut Vec>>, + limit: usize, + current_inode: &Inode, + resolve_item_stack: &mut Vec>, + visited_stack: &mut HashSet, + ) -> () + where + BackingId: Clone + Eq + Hash, + Data: Send + Sync, + { + let is_root_inode = *current_inode == ROOT_INODE; + if is_root_inode { + if result.len() < limit { + // Freeze the result + result.push(resolve_item_stack.to_vec()); + } + // All resolved paths must not go beyond the root inode + return; + } + let current_info = match mapper.get(current_inode) { + Some(info) => info, + None => return, + }; + visited_stack.insert(current_inode.clone()); + 'scan_loop: for (parent, names) in current_info.links.iter() { + if result.len() >= limit { + break 'scan_loop; + } + if visited_stack.contains(parent) { + continue; + } + if names.is_empty() { + continue; + } + for name in names.iter() { + if result.len() >= limit { + break 'scan_loop; + } + resolve_item_stack.push(InodeResolveItem { + parent, + name: name.as_ref(), + inode: current_info.clone(), + }); + scoped_resolve( + mapper, + result, + limit, + parent, + resolve_item_stack, + visited_stack, + ); + resolve_item_stack.pop(); + } + } + // Pop the inserted item from the visited stack + visited_stack.remove(current_inode); + } + scoped_resolve( + self, + &mut result, + limit, + &inode, + &mut Vec::new(), + &mut HashSet::new(), + ); + result + } + + pub fn get(&self, inode: &Inode) -> Option> { + self.data.inodes.get(inode).map(|inode_value| InodeInfo { + links: &inode_value.links, + data: &inode_value.data, + }) + } + + pub fn get_mut(&mut self, inode: &Inode) -> Option> { + self.data + .inodes + .get_mut(inode) + .map(|inode_value| InodeInfoMut { + links: &mut inode_value.links, + data: &mut inode_value.data, + }) + } + + /// Retrieves all children of a given parent inode. + /// + /// # Note + /// - Does not check if the parent inode exists. + /// - Returns an empty vector if the parent has no children or doesn't exist. + pub fn get_children(&self, parent: &Inode) -> Vec<(&Arc, &Inode)> { + self.data + .children + .get(parent) + .map(|children| { + children + .iter() + .map(|(name, inode)| (name.as_ref(), inode)) + .collect() + }) + .unwrap_or(vec![]) + } + + /// Looks up a child inode by its parent inode and name + pub fn lookup( + &self, + parent: &Inode, + name: &OsStr, + ) -> Option> { + self.data + .children + .get(parent) + .and_then(|children| children.get(name)) + .map(|inode| { + let inode_value = self.data.inodes.get(inode).unwrap(); + LookupResult { + inode: inode, + backing_id: self.data.backing.get_by_left(inode), + links: &inode_value.links, + data: &inode_value.data, + } + }) + } + + /// Renames a child inode from one parent to another + pub fn rename( + &mut self, + parent: &Inode, + oldname: &OsStr, + newparent: &Inode, + newname: OsString, + ) -> Result, RenameError> { + let newname = OsStringWrapper(Arc::new(newname)); + + // Check if the new parent exists + if !self.data.inodes.contains_key(parent) { + return Err(RenameError::ParentNotFound); + } + if !self.data.inodes.contains_key(newparent) { + return Err(RenameError::NewParentNotFound); + } + + // Remove the child from the old parent + let mut is_parent_empty = false; + let child_inode = self + .data + .children + .get_mut(parent) + .ok_or(RenameError::NotFound) + .and_then(|parent_children| { + let child_inode = parent_children + .remove(oldname) + .ok_or(RenameError::NotFound)?; + if parent_children.is_empty() { + is_parent_empty = true; + } + Ok(child_inode) + })?; + + // Remove the old parent if it's now empty + if is_parent_empty { + self.data.children.remove(parent); + } + + // Update the inode value, remove an association to old parent and add association to new parent + self.data.inodes.get_mut(&child_inode).map(|inode_value| { + // Remove an association to old parent, and remove the set + let old_parent_associations = inode_value + .links + .entry(parent.clone()) + .or_insert_with(HashSet::new); + old_parent_associations.remove(oldname); + if old_parent_associations.is_empty() { + inode_value.links.remove(&parent); + } + + // Add an association to new parent + inode_value + .links + .entry(newparent.clone()) + .or_insert_with(HashSet::new) + .insert(newname.clone()); + }); + + // Insert the child into the new parent's children map + self.data + .children + .entry(newparent.clone()) + .or_insert_with(HashMap::new) + .insert(newname, child_inode); + + Ok(None) + } + + /// Removes an inode and its associated data from the `InodeMapper`. + /// + /// This function removes the specified inode from the `inodes`, `children` and `backing` maps. + /// It also cleans up empty parent entries in the `children` map. + /// + /// # Note + /// This operation will no longer cascade to child inodes since an inode may be + /// owned by multiple parents, and any inode can now have ROOT_INODE as a child. + /// + /// # Behavior + /// - Panics if we intend to remove ROOT in debug build + /// - If the inode doesn't exist, the function does nothing. + /// - If the parent's children map becomes empty after removal, the parent entry + /// is also removed from the `children` map to conserve memory. + pub fn remove(&mut self, inode: &Inode) -> Option { + #[cfg(debug_assertions)] + if *inode == ROOT_INODE { + panic!("Cannot remove ROOT"); + } + if let Some(inode_value) = self.data.inodes.remove(inode) { + // Remove this inode from its parent's children + for (parent, names) in inode_value.links.iter() { + if let Some(parent_children) = self.data.children.get_mut(parent) { + for name in names.iter() { + parent_children.remove(name); + } + if parent_children.is_empty() { + self.data.children.remove(parent); + } + } + } + + // Remove links to children, but don't cascade + self.data.children.remove(inode); + + // Remove links to backing ID + self.data.backing.remove_by_left(inode); + Some(inode_value.data) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::collections::HashSet; + use std::ffi::OsString; + + use crate::ROOT_INODE; + use crate::types::Inode; + + #[test] + fn test_insert_child_returns_old_inode() { + let mut mapper = InodeMultiMapper::::new(0); + let root = mapper.get_root_inode(); + let child_name = OsString::from("child"); + + // Insert the first child + let first_child_inode = Inode::from(2); + assert_eq!( + mapper.insert_child(&root, child_name.clone(), None, |value_creator_params| { + assert!(value_creator_params.existing_data.is_none()); + 42 + }), + Ok(first_child_inode.clone()) + ); + + // Insert a child with the same name + assert_eq!( + mapper.insert_child(&root, child_name.clone(), None, |value_creator_params| { + assert_eq!(value_creator_params.existing_data, Some(&42)); + 84 + }), + Ok(first_child_inode.clone()) + ); + + // Verify that the child was indeed replaced + let lookup_result = mapper.lookup(&root, child_name.as_os_str()); + assert!(lookup_result.is_some()); + assert_eq!(*lookup_result.unwrap().data, 84); + } + + #[test] + fn test_insert_multiple_children() { + let mut mapper = InodeMultiMapper::::new(0); + let children: Vec<( + OsString, + Option, + Box) -> u64>, + )> = vec![ + (OsString::from("child1"), None, Box::new(|_| 10)), + (OsString::from("child2"), None, Box::new(|_| 20)), + (OsString::from("child3"), None, Box::new(|_| 30)), + ]; + + let result = mapper.insert_children(&ROOT_INODE, children); + + assert!(result.is_ok()); + let inserted_inodes = result.unwrap(); + assert_eq!(inserted_inodes.len(), 3); + + for (i, inode) in inserted_inodes.iter().enumerate() { + let child_name = OsString::from(format!("child{}", i + 1)); + let child_value = mapper.lookup(&ROOT_INODE, &child_name).unwrap(); + assert_eq!(child_value.inode, inode); + assert_eq!( + child_value.links.get(&ROOT_INODE), + Some(&HashSet::from([OsStringWrapper(Arc::new( + child_name.clone() + ))])) + ); + assert_eq!(*child_value.data, (i as u64 + 1) * 10); + } + } + + #[test] + fn test_batch_insert_large_entries_varying_depths() { + let mut mapper = InodeMultiMapper::::new(0); + let mut entries = Vec::new(); + let mut expected_inodes = HashSet::new(); + + const FILE_COUNT: usize = 50; + // Create a large number of entries with varying depths + for i in 0..FILE_COUNT as u64 { + let depth = i % 5; // Vary depth from 0 to 4 + let mut path = Vec::new(); + for j in 0..depth { + path.push(OsString::from(format!("dir_{}", j))); + } + path.push(OsString::from(format!("file_{}", i))); + entries.push((path, None, move |_: ValueCreatorParams| i)); + expected_inodes.insert(Inode::from(i + 2)); // Start from 2 to avoid conflict with root_inode + } + + // Perform batch insert + let result = mapper.batch_insert(&ROOT_INODE, entries, |_: ValueCreatorParams| 0); + + // Verify results + assert!(result.is_ok(), "Batch insert should succeed"); + + // Check if all inserted inodes exist + for i in 2..=(FILE_COUNT as u64 + 1) { + let inode = Inode::from(i); + assert!(mapper.get(&inode).is_some(), "{:?} should exist", inode); + } + + // Verify the structure for a few sample paths + let sample_paths = vec![ + vec!["file_0"], + vec!["dir_0", "file_1"], + vec!["dir_0", "dir_1", "file_2"], + vec!["dir_0", "dir_1", "dir_2", "file_3"], + vec!["dir_0", "dir_1", "dir_2", "dir_3", "file_4"], + ]; + + for (i, path) in sample_paths.iter().enumerate() { + let mut current_inode = ROOT_INODE.clone(); + for (j, component) in path.iter().enumerate() { + let lookup_result = mapper.lookup(¤t_inode, OsStr::new(component)); + assert!( + lookup_result.is_some(), + "Failed to find {} in path {:?}", + component, + path + ); + let lookup_result_unwraped = lookup_result.unwrap(); + if j == path.len() - 1 { + assert_eq!( + *lookup_result_unwraped.data, i as u64, + "Incorrect data for file {}", + i + ); + } + current_inode = lookup_result_unwraped.inode.clone(); + } + } + } + + #[test] + fn test_resolve_inode_to_full_path() { + let mut mapper = InodeMultiMapper::<(), u64>::new(()); + + let dir_inode = mapper + .insert_child( + &mapper.get_root_inode(), + OsString::from("dir"), + None, + |_| (), + ) + .unwrap(); + let file_inode = mapper + .insert_child(&dir_inode, OsString::from("file.txt"), None, |_| ()) + .unwrap(); + + // Resolve the file inode + let path = mapper.resolve(&file_inode).unwrap(); + + // Check the resolved path (it should be in reverse order) + assert_eq!(path.len(), 2); + assert_eq!( + path[0] + .links + .values() + .next() + .unwrap() + .iter() + .next() + .unwrap() + .as_ref() + .to_str() + .unwrap(), + "file.txt" + ); + assert_eq!( + path[1] + .links + .values() + .next() + .unwrap() + .iter() + .next() + .unwrap() + .as_ref() + .to_str() + .unwrap(), + "dir" + ); + + // Resolve the root inode (should be empty) + let root_path = mapper.resolve(&ROOT_INODE).unwrap(); + assert!(root_path.is_empty()); + + // Try to resolve a non-existent inode + assert!(mapper.resolve(&Inode::from(999)).is_none()); + } + + #[test] + fn test_resolve_invalid_inode() { + let mapper = InodeMultiMapper::::new(0); + let invalid_inode = Inode::from(999); + + // Attempt to resolve an invalid inode + let result = mapper.resolve(&invalid_inode); + + // Assert that the result is None + assert!( + result.is_none(), + "Resolving an invalid inode should return None" + ); + } + + #[test] + fn test_rename_child_inode() { + let mut mapper = InodeMultiMapper::<(), u64>::new(()); + let root = mapper.get_root_inode(); + + // Insert initial structure + let parent1 = mapper + .insert_child(&root, OsString::from("parent1"), None, |_| ()) + .unwrap(); + let parent2 = mapper + .insert_child(&root, OsString::from("parent2"), None, |_| ()) + .unwrap(); + let child = mapper + .insert_child(&parent1, OsString::from("old_name"), None, |_| ()) + .unwrap(); + mapper + .insert_child(&parent2, OsString::from("dummy"), None, |_| ()) + .unwrap(); + + // Perform rename + let result = mapper.rename( + &parent1, + OsStr::new("old_name"), + &parent2, + OsString::from("new_name"), + ); + + // Assert successful rename + assert!(result.is_ok()); + assert_eq!(result.unwrap(), None); + + // Verify new location + let renamed_child = mapper.lookup(&parent2, OsStr::new("new_name")); + assert!(renamed_child.is_some()); + assert_eq!(renamed_child.unwrap().inode, &child); + + // Verify old location is empty + assert!(mapper.lookup(&parent1, OsStr::new("old_name")).is_none()); + + // Verify inode data is updated + let inode_value = mapper.get(&child).unwrap(); + assert_eq!(inode_value.links.get(&parent1), None); + assert_eq!( + inode_value.links.get(&parent2), + Some(&HashSet::from([OsStringWrapper(Arc::new(OsString::from( + "new_name" + )))])) + ); + } + + #[test] + fn test_should_not_prematurely_purge_old_inode_after_renaming() { + // Data fields of all inodes in this test are 1 to simulate reflection of the FUSE inode refcount + let mut mapper = InodeMultiMapper::::new(1u64); + let root = mapper.get_root_inode(); + + let parent1 = mapper + .insert_child(&root, OsString::from("parent1"), None, |_| 1) + .unwrap(); + let parent2 = mapper + .insert_child(&root, OsString::from("parent2"), None, |_| 1) + .unwrap(); + let child1 = mapper + .insert_child(&parent1, OsString::from("child1"), None, |_| 1) + .unwrap(); + let child2 = mapper + .insert_child(&parent2, OsString::from("child2"), None, |_| 1) + .unwrap(); + + // Rename child1 to child2 + mapper + .rename( + &parent1, + OsStr::new("child1"), + &parent2, + OsString::from("child2"), + ) + .expect("should be able to insert inode"); + assert!( + mapper.get(&child1).is_some(), + "first inode should be present" + ); + assert!( + mapper.get(&child1).unwrap().links.contains_key(&parent2), + "first inode should point to parent2 as parent" + ); + assert!( + mapper + .get_children(&parent2) + .contains(&(&Arc::new(OsString::from("child2")), &child1)), + "first inode should be in parent2's child node list" + ); + assert!( + !mapper + .get_children(&parent2) + .contains(&(&Arc::new(OsString::from("child2")), &child2)), + "second inode should no longer be in parent2's child node list" + ); + assert!( + mapper.get(&child2).is_some(), + "second inode must be present as an orphaned inode but not removed immediately" + ); + } + + #[test] + fn test_rename_child_inode_into_empty_dir_inode() { + let mut mapper = InodeMultiMapper::<(), u64>::new(()); + let root = mapper.get_root_inode(); + + // Insert initial structure + let parent1 = mapper + .insert_child(&root, OsString::from("parent1"), None, |_| ()) + .unwrap(); + let parent2 = mapper + .insert_child(&parent1, OsString::from("parent2"), None, |_| ()) + .unwrap(); + let child = mapper + .insert_child(&root, OsString::from("test_name"), None, |_| ()) + .unwrap(); + + // Perform rename + let result = mapper.rename( + &root, + OsStr::new("test_name"), + &parent2, + OsString::from("test_name"), + ); + + // Assert successful rename + assert!(result.is_ok()); + assert_eq!(result.unwrap(), None); + + // Verify new location + let renamed_child = mapper.lookup(&parent2, OsStr::new("test_name")); + assert!(renamed_child.is_some()); + assert_eq!(renamed_child.unwrap().inode, &child); + + // Verify old location is empty + assert!(mapper.lookup(&root, OsStr::new("test_name")).is_none()); + + // Verify inode data is updated + let inode_value = mapper.get(&child).unwrap(); + assert_eq!( + inode_value.links.get(&parent2), + Some(&HashSet::from([OsStringWrapper(Arc::new(OsString::from( + "test_name" + )))])) + ); + + // Perform rename back to original path + let result = mapper.rename( + &parent2, + OsStr::new("test_name"), + &root, + OsString::from("test_name"), + ); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), None); + + // Verify new location + let renamed_child = mapper.lookup(&root, OsStr::new("test_name")); + assert!(renamed_child.is_some()); + assert_eq!(renamed_child.unwrap().inode, &child); + + // Verify old location is empty + assert!(mapper.lookup(&parent2, OsStr::new("test_name")).is_none()); + + // Verify inode data is updated + let inode_value = mapper.get(&child).unwrap(); + assert_eq!( + inode_value.links.get(&root), + Some(&HashSet::from([OsStringWrapper(Arc::new(OsString::from( + "test_name" + )))])) + ); + } + + #[test] + fn test_rename_non_existent_child() { + let mut mapper = InodeMultiMapper::::new(0); + + // Insert parent inodes + let root = mapper.get_root_inode(); + let parent = mapper + .insert_child(&root, OsString::from("parent"), None, |_| 1) + .unwrap(); + let newparent = mapper + .insert_child(&root, OsString::from("newparent"), None, |_| 2) + .unwrap(); + + // Attempt to rename a non-existent child + let result = mapper.rename( + &parent, + OsStr::new("non_existent"), + &newparent, + OsString::from("new_name"), + ); + + assert!(matches!(result, Err(RenameError::NotFound))); + } +} diff --git a/src/lib.rs b/src/lib.rs index c23ea55..fd034e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ mod core; mod fuse_handler; pub mod inode_mapper; +pub mod inode_multi_mapper; pub mod templates; pub mod types; pub mod unix_fs; diff --git a/src/types/file_id_type.rs b/src/types/file_id_type.rs index 718e50e..8c986ad 100644 --- a/src/types/file_id_type.rs +++ b/src/types/file_id_type.rs @@ -10,15 +10,15 @@ use std::{ ffi::OsString, fmt::{Debug, Display}, + hash::Hasher, path::{Path, PathBuf}, + sync::{Arc, RwLock, atomic::AtomicU64}, }; -use fuser::FileType as FileKind; - -use crate::core::InodeResolvable; - use super::arguments::FileAttribute; use super::inode::*; +use crate::{core::InodeResolvable, inode_multi_mapper::InodeMultiMapper}; +use fuser::FileType as FileKind; /// Represents the type used to identify files in the file system. /// @@ -38,6 +38,20 @@ use super::inode::*; /// - Pros: Slightly lower overhead than PathBuf, allows path to be divided into parts. /// - Cons: Path components are stored in reverse order, which may require additional handling. /// - Root: Represented by an empty vector. +/// +/// 4. `HybridId`: Uses inode for identification; however, file paths are also provided for use. +/// - Pros: +/// - Supports automatic inode-to-path mapping, similar to PathBuf. +/// - User can supply an optional backing ID to accurately reuse an existing inode and model a hard link +/// if the underlying file system uses hard links, and allows for retrieving multiple paths to the same inode. +/// - Hard links persist after unmounting and remounting the file system. +/// - Cons: +/// - May have more overhead compared to PathBuf. +/// - May lead to performance degradation or service denial if the user tries to exhaustively search all paths +/// to an inode, and hard links were extensively used. +/// - The pre-supplied PathBuf can change over multiple requests to the same inode, so it should not be used as a +/// comparison method. +/// - Root: Represented by the constant ROOT_INODE with a value of 1 and an empty string. pub trait FileIdType: 'static + Debug + Clone + PartialEq + Eq + std::hash::Hash + InodeResolvable { @@ -148,3 +162,102 @@ impl FileIdType for Vec { ((), minimal_metadata) } } + +#[derive(Debug, Clone)] +pub struct HybridId +where + BackingId: Clone + Eq + std::hash::Hash, +{ + inode: Inode, + first_path: PathBuf, + mapper: Arc>>, +} + +impl HybridId +where + BackingId: Clone + Eq + std::hash::Hash, +{ + pub fn new( + inode: Inode, + first_path: PathBuf, + mapper: Arc>>, + ) -> Self { + Self { + inode, + first_path, + mapper, + } + } + + pub fn first_path(&self) -> &Path { + self.first_path.as_ref() + } + + pub fn inode(&self) -> &Inode { + &self.inode + } + + pub fn paths(&self, limit: usize) -> Vec { + let mapper = self + .mapper + .read() + .expect("failed to acquire read lock on mapper"); + let resolved = mapper.resolve_all(&self.inode, limit); + resolved + .iter() + .map(|components| { + components + .iter() + .rev() + .map(|component| component.name.as_ref()) + .collect::() + }) + .collect() + } +} + +impl PartialEq for HybridId +where + BackingId: Clone + Eq + std::hash::Hash, +{ + fn eq(&self, other: &Self) -> bool { + self.inode == other.inode && Arc::ptr_eq(&self.mapper, &other.mapper) + } +} + +impl Eq for HybridId where BackingId: Clone + Eq + std::hash::Hash {} + +impl std::hash::Hash for HybridId +where + BackingId: Clone + Eq + std::hash::Hash, +{ + fn hash(&self, state: &mut H) { + self.inode.hash(state); + // Resolver is not immutable (it can expire and be regenerated) + } +} + +impl FileIdType for HybridId +where + BackingId: Clone + Eq + std::hash::Hash + Send + Sync + Debug + 'static, +{ + type _Id = Option; + type Metadata = (Option, FileAttribute); + type MinimalMetadata = (Option, FileKind); + + fn display(&self) -> impl Display { + format!("HybridId({:?}, {})", self.inode, self.first_path.display()) + } + + fn is_filesystem_root(&self) -> bool { + self.inode == ROOT_INODE + } + + fn extract_metadata(metadata: Self::Metadata) -> (Self::_Id, FileAttribute) { + metadata + } + + fn extract_minimal_metadata(minimal_metadata: Self::MinimalMetadata) -> (Self::_Id, FileKind) { + minimal_metadata + } +} From 469c9ebd7669cf5fcda5d94b3ea08d8c8a82727f Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 13 Jan 2026 11:18:36 +0700 Subject: [PATCH 05/25] misc: remove extraneous todo --- src/core/inode_mapping.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/inode_mapping.rs b/src/core/inode_mapping.rs index 948cc2e..0c926ce 100644 --- a/src/core/inode_mapping.rs +++ b/src/core/inode_mapping.rs @@ -596,7 +596,6 @@ mod tests { assert_eq!(hard_link_id.first_path(), Path::new("hard_link")); let hard_link_ino_2 = resolver.lookup(dir2_ino, OsStr::new("hard_linked"), Some(7), true); - // TODO: Expand and get the full path let hard_link_id_2 = resolver.resolve_id(hard_link_ino_2); assert_eq!( hard_link_ino_2, hard_link_ino, From 90b7ec4a35ae9f4670d5e9d5e56e2eecf890885f Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 13 Jan 2026 18:12:56 +0700 Subject: [PATCH 06/25] fix: orphaned inodes should be acknowledged --- src/core/inode_mapping.rs | 34 +++++++++++++++++++++------------- src/types/file_id_type.rs | 17 ++++++++++++----- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/core/inode_mapping.rs b/src/core/inode_mapping.rs index 0c926ce..03edfe4 100644 --- a/src/core/inode_mapping.rs +++ b/src/core/inode_mapping.rs @@ -294,11 +294,13 @@ where .read() .unwrap() .resolve(&Inode::from(ino)) - .expect("Failed to resolve inode") - .iter() - .map(|inode_info| (**inode_info.name).clone()) - .rev() - .collect::(); + .map(|components| { + components + .iter() + .map(|inode_info| (**inode_info.name).clone()) + .rev() + .collect::() + }); HybridId::new(Inode::from(ino), first_path, self.mapper.clone()) } @@ -530,16 +532,16 @@ mod tests { // Test lookup and resolve_id for root let root_ino = ROOT_INODE.into(); let root_id = resolver.resolve_id(root_ino); - assert_eq!(root_id.first_path(), ""); + assert_eq!(root_id.first_path(), Some(Path::new(""))); // Test lookup and resolve_id for child and create nested structures let dir1_ino = resolver.lookup(root_ino, OsStr::new("dir1"), Some(1), true); let dir1_id = resolver.resolve_id(dir1_ino); - assert_eq!(dir1_id.first_path(), "dir1"); + assert_eq!(dir1_id.first_path(), Some(Path::new("dir1"))); let dir2_ino = resolver.lookup(dir1_ino, OsStr::new("dir2"), Some(2), true); let dir2_id = resolver.resolve_id(dir2_ino); - assert_eq!(dir2_id.first_path(), "dir1/dir2"); + assert_eq!(dir2_id.first_path(), Some(Path::new("dir1/dir2"))); // Test add_children let grandchildren = vec![ @@ -550,7 +552,10 @@ mod tests { assert_eq!(added_grandchildren.len(), 2); for (name, ino) in added_grandchildren.iter() { let child_path = resolver.resolve_id(*ino); - assert_eq!(child_path.first_path(), Path::new("dir1/dir2").join(name)); + assert_eq!( + child_path.first_path(), + Some(Path::new("dir1/dir2").join(name).as_path()) + ); } // Test forget @@ -566,7 +571,7 @@ mod tests { let renamed_grandchild_path = resolver.resolve_id(added_grandchildren[1].1); assert_eq!( renamed_grandchild_path.first_path(), - Path::new("dir1/dir2/grandchild2_renamed") + Some(Path::new("dir1/dir2/grandchild2_renamed")) ); // Test rename to a different directory @@ -580,7 +585,7 @@ mod tests { let renamed_grandchild_path = resolver.resolve_id(added_grandchildren[1].1); assert_eq!( renamed_grandchild_path.first_path(), - Path::new("dir3/grandchild2_renamed") + Some(Path::new("dir3/grandchild2_renamed")) ); // Test lookup for non-existent file @@ -588,12 +593,15 @@ mod tests { resolver.lookup(root_ino, OsStr::new("non_existent"), Some(6), false); assert_ne!(non_existent_ino, 0); let non_existent_path = resolver.resolve_id(non_existent_ino); - assert_eq!(non_existent_path.first_path(), Path::new("non_existent")); + assert_eq!( + non_existent_path.first_path(), + Some(Path::new("non_existent")) + ); // Test lookup for a file with existing backing ID let hard_link_ino = resolver.lookup(root_ino, OsStr::new("hard_link"), Some(7), true); let hard_link_id = resolver.resolve_id(hard_link_ino); - assert_eq!(hard_link_id.first_path(), Path::new("hard_link")); + assert_eq!(hard_link_id.first_path(), Some(Path::new("hard_link"))); let hard_link_ino_2 = resolver.lookup(dir2_ino, OsStr::new("hard_linked"), Some(7), true); let hard_link_id_2 = resolver.resolve_id(hard_link_ino_2); diff --git a/src/types/file_id_type.rs b/src/types/file_id_type.rs index 8c986ad..1e8b0e4 100644 --- a/src/types/file_id_type.rs +++ b/src/types/file_id_type.rs @@ -169,7 +169,7 @@ where BackingId: Clone + Eq + std::hash::Hash, { inode: Inode, - first_path: PathBuf, + first_path: Option, mapper: Arc>>, } @@ -179,7 +179,7 @@ where { pub fn new( inode: Inode, - first_path: PathBuf, + first_path: Option, mapper: Arc>>, ) -> Self { Self { @@ -189,8 +189,8 @@ where } } - pub fn first_path(&self) -> &Path { - self.first_path.as_ref() + pub fn first_path(&self) -> Option<&Path> { + self.first_path.as_deref() } pub fn inode(&self) -> &Inode { @@ -246,7 +246,14 @@ where type MinimalMetadata = (Option, FileKind); fn display(&self) -> impl Display { - format!("HybridId({:?}, {})", self.inode, self.first_path.display()) + format!( + "HybridId({:?}, {})", + self.inode, + match &self.first_path { + Some(path) => path.display().to_string(), + None => "".to_string(), + } + ) } fn is_filesystem_root(&self) -> bool { From e7393e80b7c9634aec0c9d68c39ca0bc44130165 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 13 Jan 2026 18:15:25 +0700 Subject: [PATCH 07/25] fix: revert to old behavior in path and component resolver --- src/core/inode_mapping.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/core/inode_mapping.rs b/src/core/inode_mapping.rs index 03edfe4..23aa4d3 100644 --- a/src/core/inode_mapping.rs +++ b/src/core/inode_mapping.rs @@ -8,8 +8,8 @@ use std::{ use std::sync::{RwLock, atomic::AtomicU64}; -use crate::inode_multi_mapper::*; -use crate::types::*; +use crate::{inode_mapper, types::*}; +use crate::{inode_mapper::InodeMapper, inode_multi_mapper::*}; pub(crate) const ROOT_INO: u64 = 1; @@ -115,7 +115,7 @@ impl FileIdResolver for InodeResolver { } pub struct ComponentsResolver { - mapper: RwLock>, + mapper: RwLock>, } impl FileIdResolver for ComponentsResolver { @@ -123,7 +123,7 @@ impl FileIdResolver for ComponentsResolver { fn new() -> Self { ComponentsResolver { - mapper: RwLock::new(InodeMultiMapper::new(AtomicU64::new(0))), + mapper: RwLock::new(InodeMapper::new(AtomicU64::new(0))), } } @@ -153,7 +153,7 @@ impl FileIdResolver for ComponentsResolver { self.mapper .write() .expect("Failed to acquire write lock") - .insert_child(&parent, child.to_os_string(), None, |_| { + .insert_child(&parent, child.to_os_string(), |_| { AtomicU64::new(if increment { 1 } else { 0 }) }) .expect("Failed to insert child"), @@ -167,7 +167,9 @@ impl FileIdResolver for ComponentsResolver { increment: bool, ) -> Vec<(OsString, u64)> { let value_creator = - |value_creator: ValueCreatorParams| match value_creator.existing_data { + |value_creator: inode_mapper::ValueCreatorParams| match value_creator + .existing_data + { Some(nlookup) => { let count = nlookup.load(Ordering::Relaxed); AtomicU64::new(if increment { count + 1 } else { count }) @@ -176,7 +178,7 @@ impl FileIdResolver for ComponentsResolver { }; let children_with_creator: Vec<_> = children .iter() - .map(|(name, _)| (name.clone(), None, value_creator)) + .map(|(name, _)| (name.clone(), value_creator)) .collect(); let parent_inode = Inode::from(parent); let inserted_children = self From 82e57d45dc0fea673e677a929ea506d0d90c8c0d Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Wed, 14 Jan 2026 11:11:58 +0700 Subject: [PATCH 08/25] fix: reduce output when using debug trait on hybrid id --- src/types/file_id_type.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/types/file_id_type.rs b/src/types/file_id_type.rs index 1e8b0e4..256ec19 100644 --- a/src/types/file_id_type.rs +++ b/src/types/file_id_type.rs @@ -163,7 +163,7 @@ impl FileIdType for Vec { } } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct HybridId where BackingId: Clone + Eq + std::hash::Hash, @@ -237,10 +237,26 @@ where } } +impl Debug for HybridId +where + BackingId: Clone + Eq + std::hash::Hash, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "HybridId({:?}, {})", + self.inode, + match &self.first_path { + Some(path) => path.display().to_string(), + None => "".to_string(), + } + ) + } +} + impl FileIdType for HybridId where BackingId: Clone + Eq + std::hash::Hash + Send + Sync + Debug + 'static, -{ +{ type _Id = Option; type Metadata = (Option, FileAttribute); type MinimalMetadata = (Option, FileKind); From 8d6f838cdb11cc39d6df43709637b1a52686b72a Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Wed, 14 Jan 2026 15:17:23 +0700 Subject: [PATCH 09/25] fix: specifying a new backing ID should always resolve to a new inode --- src/core/inode_mapping.rs | 14 +++++ src/inode_multi_mapper.rs | 108 +++++++++++++++++++++++++++++--------- src/types/file_id_type.rs | 17 +++--- 3 files changed, 105 insertions(+), 34 deletions(-) diff --git a/src/core/inode_mapping.rs b/src/core/inode_mapping.rs index 23aa4d3..6ed4c92 100644 --- a/src/core/inode_mapping.rs +++ b/src/core/inode_mapping.rs @@ -617,6 +617,20 @@ mod tests { assert!(paths.contains(&PathBuf::from("dir1/dir2/hard_linked"))); assert!(paths.contains(&PathBuf::from("hard_link"))); assert!(paths.contains(&PathBuf::from("dir1/hard_linked_2"))); + + // Overriding a location with a new backing ID should always create a new inode + let overridden_hard_link_ino = + resolver.lookup(dir2_ino, OsStr::new("hard_linked"), Some(8), true); + assert_ne!( + overridden_hard_link_ino, hard_link_ino, + "overridden location's inode should change upon encountering a new ID" + ); + + // Test path resolution after overriding a location with a new backing ID + let paths = hard_link_id_2.paths(100); + assert!(!paths.contains(&PathBuf::from("dir1/dir2/hard_linked")), "the path list should no longer contain the overridden location"); + assert!(paths.contains(&PathBuf::from("hard_link"))); + assert!(paths.contains(&PathBuf::from("dir1/hard_linked_2"))); } #[test] diff --git a/src/inode_multi_mapper.rs b/src/inode_multi_mapper.rs index 0925a29..0a9640a 100644 --- a/src/inode_multi_mapper.rs +++ b/src/inode_multi_mapper.rs @@ -172,7 +172,7 @@ impl Borrow for OsStringWrapper { impl InodeMultiMapper where - BackingId: Clone + Eq + Hash, + BackingId: Clone + Eq + Hash + Debug, Data: Send + Sync + 'static, { /// Creates a new `InodeMultiMapper` instance with the root inode initialized. @@ -271,11 +271,15 @@ where /// - If the backing ID is specified and points to a valid existing inode, /// - That existing inode will be associated with the parent instead. /// - If the child already exists: - /// - If the backing ID is specified and points to a second existing inode, the child will be - /// unassociated from the parent and be replaced with the second inode. - /// - If the backing ID is specified and does not point to any inode, the backing ID will be - /// associated with the new inode. - /// - The data is updated using the value_creator function. + /// - If the backing ID is specified and points to an existing inode: + /// - If the inode (A) pointed to by the backing ID is different, the old child inode (B) will be + /// unassociated from the parent and be replaced with inode (A). + /// - The data is updated using the value_creator function. + /// - If the backing ID (I1) is specified and does not point to any existing inode: + /// - If the child inode (A) does not have a backing ID, the backing ID (I1) will be associated with the child inode (A). + /// The data is then updated using the value_creator function. + /// - If the child inode (A) has a backing ID (I2) therefore (I2 != I1), the child inode (A) will be unassociated from the + /// parent. A new inode is then created and the value_creator function is then called. /// - The value_creator function is called with the inode, parent, child name, and existing data (if any) as arguments. /// /// # Caveats @@ -343,33 +347,85 @@ where backing_inode } (None, Some(target_child_inode)) => { + let target_child_inode_backing_id = self + .data + .backing + .get_by_left(&target_child_inode) + .map(|inode| inode.clone()); let target_child_inode_data = self .data .inodes .get_mut(&target_child_inode) .expect("target child inode not found"); - // Associate parent to target child - target_child_inode_data - .links - .entry(parent.clone()) - .or_insert_with(HashSet::new) - .insert(child_name.clone()); - // Associate target child to parent - parent_children.insert(child_name.clone(), target_child_inode.clone()); - // Update data - target_child_inode_data.data = value_creator(ValueCreatorParams { - parent: &parent, - new_inode: &target_child_inode, - child_name: &child_name.as_ref(), - existing_data: Some(&target_child_inode_data.data), - }); - // Associate target child to backing ID - if let Some(backing_id) = backing_id { + if let Some(desired_backing_id) = backing_id.clone() + && let Some(target_child_inode_backing_id) = target_child_inode_backing_id + { + assert_ne!( + desired_backing_id, target_child_inode_backing_id, + "the desired backing ID should not match because it is not yet recognized" + ); + // Deassociate parent from target child + let links = target_child_inode_data + .links + .entry(parent.clone()) + .or_insert_with(HashSet::new); + links.remove(&child_name.clone()); + if links.is_empty() { + target_child_inode_data.links.remove(&parent.clone()); + } + // Create new inode + let new_inode = loop { + let new_inode = self.next_inode.clone(); + self.next_inode = new_inode.add_one(); + if self.data.inodes.get(&new_inode).is_none() { + break new_inode; + } + }; + // Associate parent to new child and initialize data + self.data.inodes.insert( + new_inode.clone(), + InodeValue { + links: HashMap::from([( + parent.clone(), + HashSet::from([child_name.clone()]), + )]), + data: value_creator(ValueCreatorParams { + parent: &parent, + new_inode: &new_inode, + child_name: &child_name.as_ref(), + existing_data: None, + }), + }, + ); + // Associate new child to parent self.data .backing - .insert(target_child_inode.clone(), backing_id); + .insert(new_inode.clone(), desired_backing_id); + new_inode + } else { + // Associate parent to target child + target_child_inode_data + .links + .entry(parent.clone()) + .or_insert_with(HashSet::new) + .insert(child_name.clone()); + // Associate target child to parent + parent_children.insert(child_name.clone(), target_child_inode.clone()); + // Update data + target_child_inode_data.data = value_creator(ValueCreatorParams { + parent: &parent, + new_inode: &target_child_inode, + child_name: &child_name.as_ref(), + existing_data: Some(&target_child_inode_data.data), + }); + // Associate target child to backing ID + if let Some(backing_id) = backing_id { + self.data + .backing + .insert(target_child_inode.clone(), backing_id); + } + target_child_inode } - target_child_inode } (Some(backing_inode), None) => { let backing_inode_data = self @@ -640,7 +696,7 @@ where visited_stack: &mut HashSet, ) -> () where - BackingId: Clone + Eq + Hash, + BackingId: Clone + Eq + Hash + Debug, Data: Send + Sync, { let is_root_inode = *current_inode == ROOT_INODE; diff --git a/src/types/file_id_type.rs b/src/types/file_id_type.rs index 256ec19..0a02766 100644 --- a/src/types/file_id_type.rs +++ b/src/types/file_id_type.rs @@ -166,7 +166,7 @@ impl FileIdType for Vec { #[derive(Clone)] pub struct HybridId where - BackingId: Clone + Eq + std::hash::Hash, + BackingId: Clone + Eq + std::hash::Hash + Debug, { inode: Inode, first_path: Option, @@ -175,7 +175,7 @@ where impl HybridId where - BackingId: Clone + Eq + std::hash::Hash, + BackingId: Clone + Eq + std::hash::Hash + Debug, { pub fn new( inode: Inode, @@ -218,18 +218,18 @@ where impl PartialEq for HybridId where - BackingId: Clone + Eq + std::hash::Hash, + BackingId: Clone + Eq + std::hash::Hash + Debug, { fn eq(&self, other: &Self) -> bool { self.inode == other.inode && Arc::ptr_eq(&self.mapper, &other.mapper) } } -impl Eq for HybridId where BackingId: Clone + Eq + std::hash::Hash {} +impl Eq for HybridId where BackingId: Clone + Eq + std::hash::Hash + Debug {} impl std::hash::Hash for HybridId where - BackingId: Clone + Eq + std::hash::Hash, + BackingId: Clone + Eq + std::hash::Hash + Debug, { fn hash(&self, state: &mut H) { self.inode.hash(state); @@ -239,10 +239,11 @@ where impl Debug for HybridId where - BackingId: Clone + Eq + std::hash::Hash, + BackingId: Clone + Eq + std::hash::Hash + Debug, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, + write!( + f, "HybridId({:?}, {})", self.inode, match &self.first_path { @@ -256,7 +257,7 @@ where impl FileIdType for HybridId where BackingId: Clone + Eq + std::hash::Hash + Send + Sync + Debug + 'static, -{ +{ type _Id = Option; type Metadata = (Option, FileAttribute); type MinimalMetadata = (Option, FileKind); From a85505c8e0a916b065894ce8b4b0f4e7cb73df93 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Wed, 14 Jan 2026 15:19:52 +0700 Subject: [PATCH 10/25] misc: remove debug assertions --- src/inode_multi_mapper.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/inode_multi_mapper.rs b/src/inode_multi_mapper.rs index 0a9640a..d8cf6d9 100644 --- a/src/inode_multi_mapper.rs +++ b/src/inode_multi_mapper.rs @@ -360,6 +360,7 @@ where if let Some(desired_backing_id) = backing_id.clone() && let Some(target_child_inode_backing_id) = target_child_inode_backing_id { + #[cfg(debug_assertions)] assert_ne!( desired_backing_id, target_child_inode_backing_id, "the desired backing ID should not match because it is not yet recognized" From 26712e728e17cb3306a52496fa4ecdd1706c585c Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Thu, 15 Jan 2026 09:49:49 +0700 Subject: [PATCH 11/25] feat: deterministically compute inode depending on backing ID --- src/inode_multi_mapper.rs | 52 ++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/src/inode_multi_mapper.rs b/src/inode_multi_mapper.rs index d8cf6d9..6532fa3 100644 --- a/src/inode_multi_mapper.rs +++ b/src/inode_multi_mapper.rs @@ -5,7 +5,7 @@ use std::{ collections::{HashMap, HashSet}, ffi::{OsStr, OsString}, fmt::Debug, - hash::Hash, + hash::{Hash, Hasher}, ops::Deref, sync::Arc, }; @@ -247,6 +247,35 @@ where } } + /// Compute a deterministic inode value based on the backing ID. + /// If the inode already exists, the algorithm tries to find the next available inode + /// starting from the hash value. + /// If the backing ID is not provided, allocate a new inode. + fn compute_or_allocate_inode(&mut self, backing_id: Option<&BackingId>) -> Inode { + // Deterministically hash the backing ID to get a stable inode value if possible + match backing_id { + Some(backing_id) => { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + backing_id.hash(&mut hasher); + let hash = hasher.finish(); + let mut preferred_inode = Inode::from(hash); + loop { + if self.data.inodes.get(&preferred_inode).is_none() { + break preferred_inode; + } + preferred_inode = preferred_inode.add_one(); + } + } + None => loop { + let new_inode = self.next_inode.clone(); + self.next_inode = new_inode.add_one(); + if self.data.inodes.get(&new_inode).is_none() { + break new_inode; + } + }, + } + } + pub fn get_root_inode(&self) -> Inode { self.root_inode.clone() } @@ -375,13 +404,7 @@ where target_child_inode_data.links.remove(&parent.clone()); } // Create new inode - let new_inode = loop { - let new_inode = self.next_inode.clone(); - self.next_inode = new_inode.add_one(); - if self.data.inodes.get(&new_inode).is_none() { - break new_inode; - } - }; + let new_inode = self.compute_or_allocate_inode(Some(&desired_backing_id)); // Associate parent to new child and initialize data self.data.inodes.insert( new_inode.clone(), @@ -453,13 +476,7 @@ where backing_inode } (None, None) => { - let new_inode = loop { - let new_inode = self.next_inode.clone(); - self.next_inode = new_inode.add_one(); - if self.data.inodes.get(&new_inode).is_none() { - break new_inode; - } - }; + let new_inode = self.compute_or_allocate_inode(backing_id.as_ref()); // Associate parent to child and initialize data self.data.inodes.insert( new_inode.clone(), @@ -477,6 +494,11 @@ where }, ); // Associate child to parent + let parent_children = self + .data + .children + .entry(parent.clone()) + .or_insert_with(HashMap::new); parent_children.insert(child_name.clone(), new_inode.clone()); if let Some(backing_id) = backing_id { self.data.backing.insert(new_inode.clone(), backing_id); From e3cbab18093ce903169b0e052814d3e343375114 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Fri, 16 Jan 2026 16:04:26 +0700 Subject: [PATCH 12/25] feat: separate thread pool for processing FUSE replies --- src/core/fuse_driver.rs | 305 ++++++++++++++++++++++++---------- src/core/fuse_driver_types.rs | 103 ++++++++++-- src/core/macros.rs | 136 +++++++++------ src/inode_multi_mapper.rs | 4 +- src/types/file_id_type.rs | 4 +- 5 files changed, 390 insertions(+), 162 deletions(-) diff --git a/src/core/fuse_driver.rs b/src/core/fuse_driver.rs index 24a8b18..9ad981c 100644 --- a/src/core/fuse_driver.rs +++ b/src/core/fuse_driver.rs @@ -14,7 +14,7 @@ use fuser::{ }; use super::{ - fuse_driver_types::{execute_task, FuseDriver}, + fuse_driver_types::{FuseDriver, execute_reply_task, execute_task, reply_executor}, inode_mapping::FileIdResolver, macros::*, thread_mode::*, @@ -49,16 +49,20 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.access( &req, resolver.resolve_id(ino), AccessMask::from_bits_retain(mask), ) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("access: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -68,12 +72,16 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.bmap(&req, resolver.resolve_id(ino), blocksize, idx) { - Ok(block) => reply.bmap(block), + Ok(block) => { + execute_reply_task!(reply_executor, { reply.bmap(block) }); + } Err(e) => { warn!("bmap: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -95,6 +103,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.copy_file_range( &req, @@ -107,10 +117,12 @@ where len, flags, ) { - Ok(bytes_written) => reply.written(bytes_written), + Ok(bytes_written) => { + execute_reply_task!(reply_executor, { reply.written(bytes_written) }); + } Err(e) => { warn!("copy_file_range: ino {:x?}, [{}], {:?}", ino_in, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -130,6 +142,8 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); let name = name.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.create( &req, @@ -144,17 +158,19 @@ where let (id, file_attr) = TId::extract_metadata(metadata); let ino = resolver.lookup(parent, &name, id, true); let (fuse_attr, ttl, generation) = file_attr.to_fuse(ino); - reply.created( - &ttl.unwrap_or(default_ttl), - &fuse_attr, - generation.unwrap_or(get_random_generation()), - file_handle.as_raw(), - response_flags.bits(), - ); + execute_reply_task!(reply_executor, { + reply.created( + &ttl.unwrap_or(default_ttl), + &fuse_attr, + generation.unwrap_or(get_random_generation()), + file_handle.as_raw(), + response_flags.bits(), + ); + }); } Err(e) => { warn!("create: {:?}, parent_ino: {:x?}, {:?}", parent, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -173,6 +189,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.fallocate( &req, @@ -182,10 +200,12 @@ where length, FallocateFlags::from_bits_retain(mode), ) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("fallocate: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -195,6 +215,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.flush( &req, @@ -202,10 +224,12 @@ where unsafe { BorrowedFileHandle::from_raw(fh) }, lock_owner, ) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("flush: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -223,6 +247,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.fsync( &req, @@ -230,10 +256,12 @@ where unsafe { BorrowedFileHandle::from_raw(fh) }, datasync, ) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("fsync: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -243,6 +271,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.fsyncdir( &req, @@ -250,10 +280,12 @@ where unsafe { BorrowedFileHandle::from_raw(fh) }, datasync, ) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("fsyncdir: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -263,8 +295,11 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { handle_fuse_reply_attr!( + reply_executor, handler, resolver, &req, @@ -295,6 +330,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { let lock_info = LockInfo { start, @@ -309,15 +346,19 @@ where lock_owner, lock_info, ) { - Ok(lock_info) => reply.locked( - lock_info.start, - lock_info.end, - lock_info.lock_type.bits(), - lock_info.pid, - ), + Ok(lock_info) => { + execute_reply_task!(reply_executor, { + reply.locked( + lock_info.start, + lock_info.end, + lock_info.lock_type.bits(), + lock_info.pid, + ) + }); + } Err(e) => { warn!("getlk: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -328,20 +369,24 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); let name = name.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.getxattr(&req, resolver.resolve_id(ino), &name, size) { Ok(xattr_data) => { - if size == 0 { - reply.size(xattr_data.len() as u32); - } else if size >= xattr_data.len() as u32 { - reply.data(&xattr_data); - } else { - reply.error(ErrorKind::ResultTooLarge.into()); - } + execute_reply_task!(reply_executor, { + if size == 0 { + reply.size(xattr_data.len() as u32); + } else if size >= xattr_data.len() as u32 { + reply.data(&xattr_data); + } else { + reply.error(ErrorKind::ResultTooLarge.into()); + } + }); } Err(e) => { warn!("getxattr: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -362,6 +407,8 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); let in_data = in_data.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.ioctl( &req, @@ -372,10 +419,12 @@ where in_data, out_size, ) { - Ok((result, data)) => reply.ioctl(result, &data), + Ok((result, data)) => { + execute_reply_task!(reply_executor, { reply.ioctl(result, &data) }); + } Err(e) => { warn!("ioctl: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -393,8 +442,11 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); let newname = newname.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { handle_fuse_reply_entry!( + reply_executor, handler, resolver, &req, @@ -416,20 +468,24 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.listxattr(&req, resolver.resolve_id(ino), size) { Ok(xattr_data) => { - if size == 0 { - reply.size(xattr_data.len() as u32); - } else if size >= xattr_data.len() as u32 { - reply.data(&xattr_data); - } else { - reply.error(ErrorKind::ResultTooLarge.into()); - } + execute_reply_task!(reply_executor, { + if size == 0 { + reply.size(xattr_data.len() as u32); + } else if size >= xattr_data.len() as u32 { + reply.data(&xattr_data); + } else { + reply.error(ErrorKind::ResultTooLarge.into()); + } + }); } Err(e) => { warn!("listxattr: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -440,8 +496,11 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); let name = name.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { handle_fuse_reply_entry!( + reply_executor, handler, resolver, &req, @@ -466,6 +525,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.lseek( &req, @@ -473,10 +534,12 @@ where unsafe { BorrowedFileHandle::from_raw(fh) }, seek_from_raw(Some(whence), offset), ) { - Ok(new_offset) => reply.offset(new_offset), + Ok(new_offset) => { + execute_reply_task!(reply_executor, { reply.offset(new_offset) }); + } Err(e) => { warn!("lseek: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -495,8 +558,11 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); let name = name.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { handle_fuse_reply_entry!( + reply_executor, handler, resolver, &req, @@ -523,8 +589,11 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); let name = name.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { handle_fuse_reply_entry!( + reply_executor, handler, resolver, &req, @@ -548,6 +617,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.open( &req, @@ -555,11 +626,13 @@ where OpenFlags::from_bits_retain(_flags), ) { Ok((file_handle, response_flags)) => { - reply.opened(file_handle.as_raw(), response_flags.bits()) + execute_reply_task!(reply_executor, { + reply.opened(file_handle.as_raw(), response_flags.bits()) + }); } Err(e) => { warn!("open: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -569,6 +642,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.opendir( &req, @@ -576,11 +651,13 @@ where OpenFlags::from_bits_retain(_flags), ) { Ok((file_handle, response_flags)) => { - reply.opened(file_handle.as_raw(), response_flags.bits()) + execute_reply_task!(reply_executor, { + reply.opened(file_handle.as_raw(), response_flags.bits()) + }); } Err(e) => { warn!("opendir: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -600,6 +677,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.read( &req, @@ -610,10 +689,12 @@ where FUSEOpenFlags::from_bits_retain(flags), lock_owner, ) { - Ok(data_reply) => reply.data(&data_reply), + Ok(data_reply) => { + execute_reply_task!(reply_executor, { reply.data(&data_reply) }); + } Err(e) => { warn!("read: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -665,12 +746,16 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.readlink(&req, resolver.resolve_id(ino)) { - Ok(link) => reply.data(&link), + Ok(link) => { + execute_reply_task!(reply_executor, { reply.data(&link) }); + } Err(e) => { warn!("[{}] readlink, ino: {:x?}, {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -689,6 +774,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.release( &req, @@ -698,10 +785,12 @@ where _lock_owner, _flush, ) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("release: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -711,6 +800,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.releasedir( &req, @@ -718,10 +809,12 @@ where unsafe { OwnedFileHandle::from_raw(fh) }, OpenFlags::from_bits_retain(flags), ) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("releasedir: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -732,12 +825,16 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); let name = name.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.removexattr(&req, resolver.resolve_id(ino), &name) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("removexattr: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -758,6 +855,8 @@ where let resolver = self.get_resolver(); let name = name.to_owned(); let newname = newname.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.rename( &req, @@ -769,11 +868,11 @@ where ) { Ok(()) => { resolver.rename(parent, &name, newparent, &newname); - reply.ok() + execute_reply_task!(reply_executor, { reply.ok() }); } Err(e) => { warn!("[{}] rename: parent_ino: {:x?}, {:?}", parent, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } } }); @@ -784,12 +883,16 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); let name = name.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.rmdir(&req, resolver.resolve_id(parent), &name) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("[{}] rmdir: parent_ino: {:x?}, {:?}", parent, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -830,8 +933,11 @@ where flags: None, file_handle: fh.map(|fh| unsafe { BorrowedFileHandle::from_raw(fh) }), }; + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { handle_fuse_reply_attr!( + reply_executor, handler, resolver, &req, @@ -859,6 +965,8 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { let lock_info = LockInfo { start, @@ -874,10 +982,12 @@ where lock_info, sleep, ) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("setlk: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -898,6 +1008,8 @@ where let resolver = self.get_resolver(); let name = name.to_owned(); let value = value.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.setxattr( &req, @@ -907,10 +1019,12 @@ where FUSESetXAttrFlags::from_bits_retain(flags), position, ) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("setxattr: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -920,21 +1034,27 @@ where let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.statfs(&req, resolver.resolve_id(ino)) { - Ok(statfs) => reply.statfs( - statfs.total_blocks, - statfs.free_blocks, - statfs.available_blocks, - statfs.total_files, - statfs.free_files, - statfs.block_size, - statfs.max_filename_length, - statfs.fragment_size, - ), + Ok(statfs) => { + execute_reply_task!(reply_executor, { + reply.statfs( + statfs.total_blocks, + statfs.free_blocks, + statfs.available_blocks, + statfs.total_files, + statfs.free_files, + statfs.block_size, + statfs.max_filename_length, + statfs.fragment_size, + ) + }); + } Err(e) => { warn!("statfs: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -953,8 +1073,11 @@ where let resolver = self.get_resolver(); let link_name = link_name.to_owned(); let target = target.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { handle_fuse_reply_entry!( + reply_executor, handler, resolver, &req, @@ -983,6 +1106,8 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); let data = data.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.write( &req, @@ -994,10 +1119,12 @@ where OpenFlags::from_bits_retain(flags), lock_owner, ) { - Ok(bytes_written) => reply.written(bytes_written), + Ok(bytes_written) => { + execute_reply_task!(reply_executor, { reply.written(bytes_written) }); + } Err(e) => { warn!("write: ino {:x?}, [{}], {:?}", ino, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); @@ -1008,12 +1135,16 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); let name = name.to_owned(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!(self); execute_task!(self, { match handler.unlink(&req, resolver.resolve_id(parent), &name) { - Ok(()) => reply.ok(), + Ok(()) => { + execute_reply_task!(reply_executor, { reply.ok() }); + } Err(e) => { warn!("[{}] unlink: parent_ino: {:x?}, {:?}", parent, e, req); - reply.error(e.raw_error()) + execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); } }; }); diff --git a/src/core/fuse_driver_types.rs b/src/core/fuse_driver_types.rs index d550d7e..4ea1746 100644 --- a/src/core/fuse_driver_types.rs +++ b/src/core/fuse_driver_types.rs @@ -66,14 +66,31 @@ mod serial { }; } + macro_rules! reply_executor { + ($self:expr) => { + () + }; + } + + macro_rules! execute_reply_task { + ($reply_executor:expr, $block:block) => { + $block + }; + } + + pub(crate) use execute_reply_task; pub(crate) use execute_task; + pub(crate) use reply_executor; } #[cfg(feature = "parallel")] mod parallel { use super::*; - use std::sync::Arc; + use std::{ + sync::Arc, + thread::{self, available_parallelism}, + }; use threadpool::ThreadPool; @@ -92,6 +109,18 @@ mod parallel { dirmap_iter: Arc>>, dirmapplus_iter: Arc>>, pub threadpool: ThreadPool, + pub reply_threadpool: ThreadPool, + } + + impl Drop for FuseDriver + where + TId: FileIdType, + THandler: FuseHandler, + { + fn drop(&mut self) { + self.threadpool.join(); + self.reply_threadpool.join(); + } } impl FuseDriver @@ -102,12 +131,20 @@ mod parallel { pub fn new(handler: THandler, num_threads: usize) -> FuseDriver { #[cfg(feature = "deadlock_detection")] spawn_deadlock_checker(); + // FIXME: This is a general estimation, we should allow user to set the number of reply threads through an API, + // possibly through the spawn_mount method, or make a breaking change that modifies this method + let parallelism = thread::available_parallelism(); + let estimated_num_reply_threads = num_threads * 3; + let adjusted_num_reply_threads = parallelism + .map(|p: std::num::NonZero| (p.get() * 2).min(estimated_num_reply_threads)) + .unwrap_or(estimated_num_reply_threads); FuseDriver { handler: Arc::new(handler), resolver: Arc::new(TId::create_resolver()), dirmap_iter: Arc::new(Mutex::new(HashMap::new())), dirmapplus_iter: Arc::new(Mutex::new(HashMap::new())), threadpool: ThreadPool::new(num_threads), + reply_threadpool: ThreadPool::new(adjusted_num_reply_threads), } } @@ -130,11 +167,25 @@ mod parallel { macro_rules! execute_task { ($self:expr, $block:block) => { - $self.threadpool.execute(move || $block); + $self.threadpool.execute(move || $block) }; } + macro_rules! reply_executor { + ($self:expr) => { + $self.reply_threadpool.clone() + }; + } + + macro_rules! execute_reply_task { + ($reply_executor:expr, $block:block) => { + $reply_executor.execute(move || $block); + }; + } + + pub(crate) use execute_reply_task; pub(crate) use execute_task; + pub(crate) use reply_executor; } #[cfg(feature = "async")] @@ -154,7 +205,7 @@ mod async_task { resolver: Arc, dirmap_iter: Arc>>, dirmapplus_iter: Arc>>, - pub runtime: Runtime, + pub runtime: Arc, } impl FuseDriver @@ -170,7 +221,7 @@ mod async_task { resolver: Arc::new(TId::create_resolver()), dirmap_iter: Arc::new(Mutex::new(HashMap::new())), dirmapplus_iter: Arc::new(Mutex::new(HashMap::new())), - runtime: Runtime::new().unwrap(), + runtime: Arc::new(Runtime::new().unwrap()), } } @@ -193,11 +244,25 @@ mod async_task { macro_rules! execute_task { ($self:expr, $block:block) => { - $self.runtime.spawn(async move { $block }); + $self.runtime.spawn(async move { $block }) + }; + } + + macro_rules! reply_executor { + ($self:expr) => { + $self.runtime.clone() }; } + macro_rules! execute_reply_task { + ($reply_executor:expr, $block:block) => { + $reply_executor.spawn(async move { $block }) + }; + } + + pub(crate) use execute_reply_task; pub(crate) use execute_task; + pub(crate) use reply_executor; } #[cfg(feature = "deadlock_detection")] @@ -208,19 +273,21 @@ fn spawn_deadlock_checker() { use std::time::Duration; // Create a background thread which checks for deadlocks every 10s - thread::spawn(move || loop { - thread::sleep(Duration::from_secs(10)); - let deadlocks = deadlock::check_deadlock(); - if deadlocks.is_empty() { - info!("# No deadlock"); - continue; - } - - eprintln!("# {} deadlocks detected", deadlocks.len()); - for (i, threads) in deadlocks.iter().enumerate() { - error!("Deadlock #{}", i); - for t in threads { - error!("Thread Id {:#?}\n, {:#?}", t.thread_id(), t.backtrace()); + thread::spawn(move || { + loop { + thread::sleep(Duration::from_secs(10)); + let deadlocks = deadlock::check_deadlock(); + if deadlocks.is_empty() { + info!("# No deadlock"); + continue; + } + + eprintln!("# {} deadlocks detected", deadlocks.len()); + for (i, threads) in deadlocks.iter().enumerate() { + error!("Deadlock #{}", i); + for t in threads { + error!("Thread Id {:#?}\n, {:#?}", t.thread_id(), t.backtrace()); + } } } }); diff --git a/src/core/macros.rs b/src/core/macros.rs index 6f4fd09..dd3443d 100644 --- a/src/core/macros.rs +++ b/src/core/macros.rs @@ -1,5 +1,5 @@ macro_rules! handle_fuse_reply_entry { - ($handler:expr, $resolver:expr, $req:expr, $parent:expr, $name:expr, $reply:expr, + ($reply_executor:expr, $handler:expr, $resolver:expr, $req:expr, $parent:expr, $name:expr, $reply:expr, $function:ident, ($($args:expr),*)) => { macro_rules! if_lookup { (lookup, $choice1:tt, $choice2:tt) => { @@ -14,13 +14,18 @@ macro_rules! handle_fuse_reply_entry { match handler.$function($($args),*) { Ok(metadata) => { let default_ttl = handler.get_default_ttl(); - let (id, file_attr) = TId::extract_metadata(metadata); - let ino = $resolver.lookup($parent, $name, id, true); - let (fuse_attr, ttl, generation) = file_attr.to_fuse(ino); - $reply.entry( - &ttl.unwrap_or(default_ttl), - &fuse_attr, - generation.unwrap_or(get_random_generation()), + execute_reply_task!( + $reply_executor, + { + let (id, file_attr) = TId::extract_metadata(metadata); + let ino = $resolver.lookup($parent, $name, id, true); + let (fuse_attr, ttl, generation) = file_attr.to_fuse(ino); + $reply.entry( + &ttl.unwrap_or(default_ttl), + &fuse_attr, + generation.unwrap_or(get_random_generation()), + ); + } ); } Err(e) => { @@ -35,24 +40,39 @@ macro_rules! handle_fuse_reply_entry { }, { warn!("{}: parent_ino {:x?}, [{}], {:?}", stringify!($function), $parent, e, $req); }); - $reply.error(e.raw_error()) + execute_reply_task!( + $reply_executor, + { + $reply.error(e.raw_error()) + } + ); } } }; } macro_rules! handle_fuse_reply_attr { - ($handler:expr, $resolve:expr, $req:expr, $ino:expr, $reply:expr, + ($reply_executor:expr, $handler:expr, $resolve:expr, $req:expr, $ino:expr, $reply:expr, $function:ident, ($($args:expr),*)) => { match $handler.$function($($args),*) { Ok(file_attr) => { let default_ttl = $handler.get_default_ttl(); - let (fuse_attr, ttl, _) = file_attr.to_fuse($ino); - $reply.attr(&ttl.unwrap_or(default_ttl), &fuse_attr); + execute_reply_task!( + $reply_executor, + { + let (fuse_attr, ttl, _) = file_attr.to_fuse($ino); + $reply.attr(&ttl.unwrap_or(default_ttl), &fuse_attr); + } + ); } Err(e) => { warn!("{}: ino {:x?}, [{}], {:?}", stringify!($function), $ino, e, $req); - $reply.error(e.raw_error()) + execute_reply_task!( + $reply_executor, + { + $reply.error(e.raw_error()) + } + ); } } }; @@ -97,12 +117,16 @@ macro_rules! handle_dir_read { let handler = $self.get_handler(); let resolver = $self.get_resolver(); let dirmap_iter = $self.$get_iter_method(); + #[cfg_attr(feature = "serial", allow(unused_variables))] + let reply_executor = reply_executor!($self); execute_task!($self, { // Validate offset if $offset < 0 { error!("readdir called with a negative offset"); - $reply.error(ErrorKind::InvalidArgument.into()); + execute_reply_task!(reply_executor, { + $reply.error(ErrorKind::InvalidArgument.into()); + }); return; } @@ -142,7 +166,9 @@ macro_rules! handle_dir_read { } Err(e) => { warn!("readdir {:?}: {:?}", req_info, e); - $reply.error(e.raw_error()); + execute_reply_task!(reply_executor, { + $reply.error(e.raw_error()); + }); return; } }, @@ -151,7 +177,9 @@ macro_rules! handle_dir_read { Some(dirmap_iter) => dirmap_iter, None => { // Case when fuse tries to read again after the final item - $reply.ok(); + execute_reply_task!(reply_executor, { + $reply.ok(); + }); return; } }, @@ -160,46 +188,48 @@ macro_rules! handle_dir_read { let mut new_offset = $offset; // ### Process directory entries - if_readdir!( - $handler_method, - { - // readdir: Add entries until buffer is full - while let Some((name, ino, kind)) = dir_iter.pop_front() { - if $reply.add(ino, new_offset, kind, &name) { - dir_iter.push_front((name, ino, kind)); - dirmap_iter - .safe_borrow_mut() - .insert(($ino, new_offset - 1), dir_iter); - break; + execute_reply_task!(reply_executor, { + if_readdir!( + $handler_method, + { + // readdir: Add entries until buffer is full + while let Some((name, ino, kind)) = dir_iter.pop_front() { + if $reply.add(ino, new_offset, kind, &name) { + dir_iter.push_front((name, ino, kind)); + dirmap_iter + .safe_borrow_mut() + .insert(($ino, new_offset - 1), dir_iter); + break; + } + new_offset += 1; } - new_offset += 1; - } - $reply.ok(); - }, - { - // readdirplus: Add entries with extended attributes - let default_ttl = handler.get_default_ttl(); - while let Some((name, ino, file_attr)) = dir_iter.pop_front() { - let (fuse_attr, ttl, generation) = file_attr.clone().to_fuse(ino); - if $reply.add( - ino, - new_offset, - &name, - &ttl.unwrap_or(default_ttl), - &fuse_attr, - generation.unwrap_or(get_random_generation()), - ) { - dir_iter.push_front((name, ino, file_attr.clone())); - dirmap_iter - .safe_borrow_mut() - .insert((ino, new_offset - 1), dir_iter); - break; + $reply.ok(); + }, + { + // readdirplus: Add entries with extended attributes + let default_ttl = handler.get_default_ttl(); + while let Some((name, ino, file_attr)) = dir_iter.pop_front() { + let (fuse_attr, ttl, generation) = file_attr.clone().to_fuse(ino); + if $reply.add( + ino, + new_offset, + &name, + &ttl.unwrap_or(default_ttl), + &fuse_attr, + generation.unwrap_or(get_random_generation()), + ) { + dir_iter.push_front((name, ino, file_attr.clone())); + dirmap_iter + .safe_borrow_mut() + .insert((ino, new_offset - 1), dir_iter); + break; + } + new_offset += 1; } - new_offset += 1; + $reply.ok(); } - $reply.ok(); - } - ); + ); + }); }); }}; } diff --git a/src/inode_multi_mapper.rs b/src/inode_multi_mapper.rs index 6532fa3..fd4cf08 100644 --- a/src/inode_multi_mapper.rs +++ b/src/inode_multi_mapper.rs @@ -387,11 +387,11 @@ where .get_mut(&target_child_inode) .expect("target child inode not found"); if let Some(desired_backing_id) = backing_id.clone() - && let Some(target_child_inode_backing_id) = target_child_inode_backing_id + && let Some(_target_child_inode_backing_id) = target_child_inode_backing_id { #[cfg(debug_assertions)] assert_ne!( - desired_backing_id, target_child_inode_backing_id, + desired_backing_id, _target_child_inode_backing_id, "the desired backing ID should not match because it is not yet recognized" ); // Deassociate parent from target child diff --git a/src/types/file_id_type.rs b/src/types/file_id_type.rs index 0a02766..3fa601f 100644 --- a/src/types/file_id_type.rs +++ b/src/types/file_id_type.rs @@ -62,7 +62,7 @@ pub trait FileIdType: /// /// For PathBuf-based: FileAttribute /// - User only needs to provide FileAttribute; Inode is managed internally. - type Metadata; + type Metadata: Send; /// Minimal metadata type for the file system. /// @@ -71,7 +71,7 @@ pub trait FileIdType: /// /// For PathBuf-based: FileKind /// - User only needs to provide FileKind; Inode is managed internally. - type MinimalMetadata; + type MinimalMetadata: Send; #[doc(hidden)] type _Id; From 933f5ac6d6bbfd3bbdfef154496ccc9410d89f99 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Fri, 16 Jan 2026 17:02:12 +0700 Subject: [PATCH 13/25] breaking-feat: passthrough API --- .vscode/settings.json | 2 +- Cargo.toml | 4 +-- src/core/fuse_driver.rs | 40 ++++++++++++++++++++++----- src/fuse_handler.rs | 11 +++++--- src/templates/default_fuse_handler.rs | 7 +++-- src/templates/mirror_fs.rs | 9 ++++-- src/types.rs | 3 +- src/types/opens.rs | 39 ++++++++++++++++++++++++++ 8 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 src/types/opens.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a34a00..51eb545 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.cargo.features": ["serial"] + "rust-analyzer.cargo.features": ["serial", "passthrough"] } diff --git a/Cargo.toml b/Cargo.toml index 158cb99..630c9db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,13 @@ serial = [] parallel = ["dep:threadpool"] async = ["dep:async-trait", "dep:tokio"] deadlock_detection = ["parallel", "dep:parking_lot"] - +passthrough = ["fuser/abi-7-40"] [dependencies] # Core dependencies log = "0.4" libc = "0.2" -fuser = "0.16" +fuser = { version = "0.16", features = ["abi-7-40"] } bitflags = "2.10" # Parallel dependencies diff --git a/src/core/fuse_driver.rs b/src/core/fuse_driver.rs index 24a8b18..bf3115a 100644 --- a/src/core/fuse_driver.rs +++ b/src/core/fuse_driver.rs @@ -14,7 +14,7 @@ use fuser::{ }; use super::{ - fuse_driver_types::{execute_task, FuseDriver}, + fuse_driver_types::{FuseDriver, execute_task}, inode_mapping::FileIdResolver, macros::*, thread_mode::*, @@ -549,14 +549,27 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); execute_task!(self, { + let open_helper = OpenHelper::new(&reply); match handler.open( &req, resolver.resolve_id(ino), OpenFlags::from_bits_retain(_flags), + open_helper, ) { - Ok((file_handle, response_flags)) => { - reply.opened(file_handle.as_raw(), response_flags.bits()) - } + Ok((file_handle, response_flags, backing_id)) => match backing_id { + #[cfg(feature = "passthrough")] + Some(backing_id) => { + reply.opened_passthrough( + file_handle.as_raw(), + response_flags.bits(), + backing_id.as_ref(), + ); + } + _ => { + let response_flags = response_flags & !FUSEOpenResponseFlags::PASSTHROUGH; + reply.opened(file_handle.as_raw(), response_flags.bits()) + } + }, Err(e) => { warn!("open: ino {:x?}, [{}], {:?}", ino, e, req); reply.error(e.raw_error()) @@ -570,14 +583,27 @@ where let handler = self.get_handler(); let resolver = self.get_resolver(); execute_task!(self, { + let open_helper = OpenHelper::new(&reply); match handler.opendir( &req, resolver.resolve_id(ino), OpenFlags::from_bits_retain(_flags), + open_helper, ) { - Ok((file_handle, response_flags)) => { - reply.opened(file_handle.as_raw(), response_flags.bits()) - } + Ok((file_handle, response_flags, backing_id)) => match backing_id { + #[cfg(feature = "passthrough")] + Some(backing_id) => { + reply.opened_passthrough( + file_handle.as_raw(), + response_flags.bits(), + backing_id.as_ref(), + ); + } + _ => { + let response_flags = response_flags & !FUSEOpenResponseFlags::PASSTHROUGH; + reply.opened(file_handle.as_raw(), response_flags.bits()) + } + }, Err(e) => { warn!("opendir: ino {:x?}, [{}], {:?}", ino, e, req); reply.error(e.raw_error()) diff --git a/src/fuse_handler.rs b/src/fuse_handler.rs index 5a76a9a..39db937 100644 --- a/src/fuse_handler.rs +++ b/src/fuse_handler.rs @@ -103,6 +103,7 @@ mod private { #[cfg(feature = "serial")] impl OptionalSendSync for T {} } +use fuser::BackingId; use private::OptionalSendSync; pub trait FuseHandler: OptionalSendSync + 'static { @@ -367,8 +368,9 @@ pub trait FuseHandler: OptionalSendSync + 'static { req: &RequestInfo, file_id: TId, flags: OpenFlags, - ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags)> { - self.get_inner().open(req, file_id, flags) + helper: OpenHelper, + ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags, Option)> { + self.get_inner().open(req, file_id, flags, helper) } /// Open a directory @@ -379,8 +381,9 @@ pub trait FuseHandler: OptionalSendSync + 'static { req: &RequestInfo, file_id: TId, flags: OpenFlags, - ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags)> { - self.get_inner().opendir(req, file_id, flags) + helper: OpenHelper, + ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags, Option)> { + self.get_inner().opendir(req, file_id, flags, helper) } /// Read data from a file diff --git a/src/templates/default_fuse_handler.rs b/src/templates/default_fuse_handler.rs index 164f2fb..df7b758 100644 --- a/src/templates/default_fuse_handler.rs +++ b/src/templates/default_fuse_handler.rs @@ -631,7 +631,8 @@ impl FuseHandler for DefaultFuseHandler { _req: &RequestInfo, file_id: TId, flags: OpenFlags, - ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags)> { + _helper: OpenHelper, + ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags, Option)> { match self.handling { HandlingMethod::Error(kind) => Err(PosixError::new( kind, @@ -654,11 +655,13 @@ impl FuseHandler for DefaultFuseHandler { _req: &RequestInfo, _file_id: TId, _flags: OpenFlags, - ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags)> { + _helper: OpenHelper, + ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags, Option)> { // Safe because in releasedir we don't use it Ok(( unsafe { OwnedFileHandle::from_raw(0) }, FUSEOpenResponseFlags::empty(), + None, )) } diff --git a/src/templates/mirror_fs.rs b/src/templates/mirror_fs.rs index 36fc585..d37ca74 100644 --- a/src/templates/mirror_fs.rs +++ b/src/templates/mirror_fs.rs @@ -134,12 +134,17 @@ macro_rules! mirror_fs_readonly_methods { _req: &RequestInfo, file_id: PathBuf, flags: OpenFlags, - ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags)> { + _helper: OpenHelper, + ) -> FuseResult<( + OwnedFileHandle, + FUSEOpenResponseFlags, + Option, + )> { let file_path = self.source_path.join(file_id); let fd = unix_fs::open(file_path.as_ref(), flags)?; // Open by definition returns positive Fd or error let file_handle = OwnedFileHandle::from_owned_fd(fd).unwrap(); - Ok((file_handle, FUSEOpenResponseFlags::empty())) + Ok((file_handle, FUSEOpenResponseFlags::empty(), None)) } fn readdir( diff --git a/src/types.rs b/src/types.rs index fb058a5..d058fcd 100644 --- a/src/types.rs +++ b/src/types.rs @@ -23,7 +23,8 @@ pub mod file_handle; mod file_id_type; pub mod flags; mod inode; +pub mod opens; -pub use self::{arguments::*, errors::*, file_handle::*, file_id_type::*, flags::*, inode::*}; +pub use self::{arguments::*, errors::*, file_handle::*, file_id_type::*, flags::*, inode::*, opens::*}; pub use fuser::{FileType as FileKind, KernelConfig, TimeOrNow}; diff --git a/src/types/opens.rs b/src/types/opens.rs new file mode 100644 index 0000000..275fd00 --- /dev/null +++ b/src/types/opens.rs @@ -0,0 +1,39 @@ +#[cfg(feature = "passthrough")] +use fuser::BackingId; +use fuser::ReplyOpen; +#[cfg(feature = "passthrough")] +use std::sync::Arc; + +pub struct OpenHelper<'a> { + reply_open: &'a ReplyOpen, +} + +impl<'a> OpenHelper<'a> { + pub(crate) fn new(reply_open: &'a ReplyOpen) -> Self { + Self { reply_open } + } + + #[cfg(feature = "passthrough")] + pub fn open_backing( + self, + fd: impl std::os::fd::AsFd, + ) -> Result { + self.reply_open + .open_backing(fd) + .map(|id| PassthroughBackingId { + backing_id: Arc::new(id), + }) + } +} + +#[derive(Debug, Clone)] +pub struct PassthroughBackingId { + #[cfg(feature = "passthrough")] + pub(crate) backing_id: Arc, +} + +impl AsRef for PassthroughBackingId { + fn as_ref(&self) -> &BackingId { + self.backing_id.as_ref() + } +} From abdece61b4a4cc98e942549ecad5a9d830b82fb7 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Fri, 16 Jan 2026 17:04:44 +0700 Subject: [PATCH 14/25] fix: remove extra incompatible backing ID import --- src/fuse_handler.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fuse_handler.rs b/src/fuse_handler.rs index 39db937..8a5392e 100644 --- a/src/fuse_handler.rs +++ b/src/fuse_handler.rs @@ -103,7 +103,6 @@ mod private { #[cfg(feature = "serial")] impl OptionalSendSync for T {} } -use fuser::BackingId; use private::OptionalSendSync; pub trait FuseHandler: OptionalSendSync + 'static { From 08f6d235a7eb7074f922a82bfd9dee7b8d194850 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Fri, 16 Jan 2026 17:10:17 +0700 Subject: [PATCH 15/25] fix: repair gated features --- src/types/opens.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/opens.rs b/src/types/opens.rs index 275fd00..9b09a83 100644 --- a/src/types/opens.rs +++ b/src/types/opens.rs @@ -32,6 +32,7 @@ pub struct PassthroughBackingId { pub(crate) backing_id: Arc, } +#[cfg(feature = "passthrough")] impl AsRef for PassthroughBackingId { fn as_ref(&self) -> &BackingId { self.backing_id.as_ref() From 20d98c03d436b4c052fb0140e819ca49cf97be10 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Fri, 16 Jan 2026 17:18:42 +0700 Subject: [PATCH 16/25] fix: minor dead code suppressions --- Cargo.toml | 2 +- src/types/opens.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 630c9db..7b187de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ passthrough = ["fuser/abi-7-40"] # Core dependencies log = "0.4" libc = "0.2" -fuser = { version = "0.16", features = ["abi-7-40"] } +fuser = { version = "0.16" } bitflags = "2.10" # Parallel dependencies diff --git a/src/types/opens.rs b/src/types/opens.rs index 9b09a83..e893cae 100644 --- a/src/types/opens.rs +++ b/src/types/opens.rs @@ -5,6 +5,7 @@ use fuser::ReplyOpen; use std::sync::Arc; pub struct OpenHelper<'a> { + #[allow(dead_code)] reply_open: &'a ReplyOpen, } From 1454b2f9a20526150f9f505dcaf62f5f837c8143 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 20 Jan 2026 10:21:25 +0700 Subject: [PATCH 17/25] feat: enable passthrough for file creates --- Cargo.toml | 4 + src/core/fuse_driver.rs | 37 +++- src/fuse_handler.rs | 22 ++- src/templates/default_fuse_handler.rs | 245 ++++++++++++++------------ src/templates/mirror_fs.rs | 10 +- src/types.rs | 4 +- src/types/{opens.rs => helpers.rs} | 25 ++- 7 files changed, 219 insertions(+), 128 deletions(-) rename src/types/{opens.rs => helpers.rs} (61%) diff --git a/Cargo.toml b/Cargo.toml index 7b187de..befa77f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,7 @@ env_logger = "0.11" [package.metadata.docs.rs] features = ["parallel"] + +[patch.crates-io] +# Bleeding edge patch for FUSE create() passthrough, will be removed when patch is merged +fuser = { git = "https://github.com/khanhtranngoccva/fuser", branch = "passthrough" } \ No newline at end of file diff --git a/src/core/fuse_driver.rs b/src/core/fuse_driver.rs index bf3115a..209b68c 100644 --- a/src/core/fuse_driver.rs +++ b/src/core/fuse_driver.rs @@ -131,6 +131,7 @@ where let resolver = self.get_resolver(); let name = name.to_owned(); execute_task!(self, { + let helper = CreateHelper::new(&reply); match handler.create( &req, resolver.resolve_id(parent), @@ -138,19 +139,39 @@ where mode, umask, OpenFlags::from_bits_retain(flags), + helper, ) { - Ok((file_handle, metadata, response_flags)) => { + Ok((file_handle, metadata, response_flags, passthrough_backing_id)) => { let default_ttl = handler.get_default_ttl(); let (id, file_attr) = TId::extract_metadata(metadata); let ino = resolver.lookup(parent, &name, id, true); let (fuse_attr, ttl, generation) = file_attr.to_fuse(ino); - reply.created( - &ttl.unwrap_or(default_ttl), - &fuse_attr, - generation.unwrap_or(get_random_generation()), - file_handle.as_raw(), - response_flags.bits(), - ); + match passthrough_backing_id { + #[cfg(feature = "passthrough")] + Some(passthrough_backing_id) => { + let response_flags = + response_flags | FUSEOpenResponseFlags::PASSTHROUGH; + reply.created_passthrough( + &ttl.unwrap_or(default_ttl), + &fuse_attr, + generation.unwrap_or(get_random_generation()), + file_handle.as_raw(), + response_flags.bits(), + passthrough_backing_id.as_ref(), + ); + } + _ => { + let response_flags = + response_flags & !FUSEOpenResponseFlags::PASSTHROUGH; + reply.created( + &ttl.unwrap_or(default_ttl), + &fuse_attr, + generation.unwrap_or(get_random_generation()), + file_handle.as_raw(), + response_flags.bits(), + ); + } + } } Err(e) => { warn!("create: {:?}, parent_ino: {:x?}, {:?}", parent, e, req); diff --git a/src/fuse_handler.rs b/src/fuse_handler.rs index 8a5392e..8331ee5 100644 --- a/src/fuse_handler.rs +++ b/src/fuse_handler.rs @@ -183,9 +183,15 @@ pub trait FuseHandler: OptionalSendSync + 'static { mode: u32, umask: u32, flags: OpenFlags, - ) -> FuseResult<(OwnedFileHandle, TId::Metadata, FUSEOpenResponseFlags)> { + helper: CreateHelper, + ) -> FuseResult<( + OwnedFileHandle, + TId::Metadata, + FUSEOpenResponseFlags, + Option, + )> { self.get_inner() - .create(req, parent_id, name, mode, umask, flags) + .create(req, parent_id, name, mode, umask, flags, helper) } /// Preallocate or deallocate space to a file @@ -368,7 +374,11 @@ pub trait FuseHandler: OptionalSendSync + 'static { file_id: TId, flags: OpenFlags, helper: OpenHelper, - ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags, Option)> { + ) -> FuseResult<( + OwnedFileHandle, + FUSEOpenResponseFlags, + Option, + )> { self.get_inner().open(req, file_id, flags, helper) } @@ -381,7 +391,11 @@ pub trait FuseHandler: OptionalSendSync + 'static { file_id: TId, flags: OpenFlags, helper: OpenHelper, - ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags, Option)> { + ) -> FuseResult<( + OwnedFileHandle, + FUSEOpenResponseFlags, + Option, + )> { self.get_inner().opendir(req, file_id, flags, helper) } diff --git a/src/templates/default_fuse_handler.rs b/src/templates/default_fuse_handler.rs index df7b758..25ba51e 100644 --- a/src/templates/default_fuse_handler.rs +++ b/src/templates/default_fuse_handler.rs @@ -155,8 +155,9 @@ impl FuseHandler for DefaultFuseHandler { flags: u32, // Not implemented yet in standard ) -> FuseResult { match self.handling { - HandlingMethod::Error(kind) => Err( - PosixError::new(kind, if cfg!(debug_assertions) { + HandlingMethod::Error(kind) => Err(PosixError::new( + kind, + if cfg!(debug_assertions) { format!( "copy_file_range(file_in: {}, file_handle_in: {:?}, offset_in: {}, file_out: {}, file_handle_out: {:?}, offset_out: {}, len: {}, flags: {})", file_in.display(), @@ -170,8 +171,8 @@ impl FuseHandler for DefaultFuseHandler { ) } else { String::new() - }) - ), + }, + )), HandlingMethod::Panic => panic!( "[Not Implemented] copy_file_range(file_in: {}, file_handle_in: {:?}, offset_in: {}, file_out: {}, file_handle_out: {:?}, offset_out: {}, len: {}, flags: {})", file_in.display(), @@ -194,7 +195,13 @@ impl FuseHandler for DefaultFuseHandler { mode: u32, umask: u32, flags: OpenFlags, - ) -> FuseResult<(OwnedFileHandle, TId::Metadata, FUSEOpenResponseFlags)> { + _helper: CreateHelper, + ) -> FuseResult<( + OwnedFileHandle, + TId::Metadata, + FUSEOpenResponseFlags, + Option, + )> { match self.handling { HandlingMethod::Error(kind) => Err(PosixError::new( kind, @@ -232,8 +239,9 @@ impl FuseHandler for DefaultFuseHandler { mode: FallocateFlags, ) -> FuseResult<()> { match self.handling { - HandlingMethod::Error(kind) => Err( - PosixError::new(kind, if cfg!(debug_assertions) { + HandlingMethod::Error(kind) => Err(PosixError::new( + kind, + if cfg!(debug_assertions) { format!( "fallocate(file_id: {}, file_handle: {:?}, offset: {}, length: {}, mode: {:?})", file_id.display(), @@ -244,8 +252,8 @@ impl FuseHandler for DefaultFuseHandler { ) } else { String::new() - }) - ), + }, + )), HandlingMethod::Panic => panic!( "[Not Implemented] fallocate(file_id: {}, file_handle: {:?}, offset: {}, length: {}, mode: {:?})", file_id.display(), @@ -365,20 +373,21 @@ impl FuseHandler for DefaultFuseHandler { lock_info: LockInfo, ) -> FuseResult { match self.handling { - HandlingMethod::Error(kind) => Err( - PosixError::new(kind, if cfg!(debug_assertions) { + HandlingMethod::Error(kind) => Err(PosixError::new( + kind, + if cfg!(debug_assertions) { format!( - "getlk(file_id: {}, file_handle: {:?}, lock_owner: {}, lock_info: {:?})", - file_id.display(), - file_handle, - lock_owner, - lock_info - ) - } else { - String::new() - }) - ), - HandlingMethod::Panic => panic!( + "getlk(file_id: {}, file_handle: {:?}, lock_owner: {}, lock_info: {:?})", + file_id.display(), + file_handle, + lock_owner, + lock_info + ) + } else { + String::new() + }, + )), + HandlingMethod::Panic => panic!( "[Not Implemented] getlk(file_id: {}, file_handle: {:?}, lock_owner: {}, lock_info: {:?})", file_id.display(), file_handle, @@ -601,21 +610,22 @@ impl FuseHandler for DefaultFuseHandler { rdev: DeviceType, ) -> FuseResult { match self.handling { - HandlingMethod::Error(kind) => Err( - PosixError::new(kind, if cfg!(debug_assertions) { + HandlingMethod::Error(kind) => Err(PosixError::new( + kind, + if cfg!(debug_assertions) { format!( - "mknod(parent_id: {}, name: {:?}, mode: {}, umask: {}, rdev: {:?})", - parent_id.display(), - Path::new(name), - mode, - umask, - rdev - ) - } else { - String::new() - }) - ), - HandlingMethod::Panic => panic!( + "mknod(parent_id: {}, name: {:?}, mode: {}, umask: {}, rdev: {:?})", + parent_id.display(), + Path::new(name), + mode, + umask, + rdev + ) + } else { + String::new() + }, + )), + HandlingMethod::Panic => panic!( "[Not Implemented] mknod(parent_id: {}, name: {:?}, mode: {}, umask: {}, rdev: {:?})", parent_id.display(), Path::new(name), @@ -632,7 +642,11 @@ impl FuseHandler for DefaultFuseHandler { file_id: TId, flags: OpenFlags, _helper: OpenHelper, - ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags, Option)> { + ) -> FuseResult<( + OwnedFileHandle, + FUSEOpenResponseFlags, + Option, + )> { match self.handling { HandlingMethod::Error(kind) => Err(PosixError::new( kind, @@ -656,7 +670,11 @@ impl FuseHandler for DefaultFuseHandler { _file_id: TId, _flags: OpenFlags, _helper: OpenHelper, - ) -> FuseResult<(OwnedFileHandle, FUSEOpenResponseFlags, Option)> { + ) -> FuseResult<( + OwnedFileHandle, + FUSEOpenResponseFlags, + Option, + )> { // Safe because in releasedir we don't use it Ok(( unsafe { OwnedFileHandle::from_raw(0) }, @@ -676,22 +694,23 @@ impl FuseHandler for DefaultFuseHandler { lock_owner: Option, ) -> FuseResult> { match self.handling { - HandlingMethod::Error(kind) => Err( - PosixError::new(kind, if cfg!(debug_assertions) { + HandlingMethod::Error(kind) => Err(PosixError::new( + kind, + if cfg!(debug_assertions) { format!( - "read(file_id: {}, file_handle: {:?}, seek: {:?}, size: {}, flags: {:?}, lock_owner: {:?})", - file_id.display(), - file_handle, - seek, - size, - flags, - lock_owner - ) - } else { - String::new() - }) - ), - HandlingMethod::Panic => panic!( + "read(file_id: {}, file_handle: {:?}, seek: {:?}, size: {}, flags: {:?}, lock_owner: {:?})", + file_id.display(), + file_handle, + seek, + size, + flags, + lock_owner + ) + } else { + String::new() + }, + )), + HandlingMethod::Panic => panic!( "[Not Implemented] read(file_id: {}, file_handle: {:?}, seek: {:?}, size: {}, flags: {:?}, lock_owner: {:?})", file_id.display(), file_handle, @@ -783,21 +802,22 @@ impl FuseHandler for DefaultFuseHandler { flush: bool, ) -> FuseResult<()> { match self.handling { - HandlingMethod::Error(kind) => Err( - PosixError::new(kind, if cfg!(debug_assertions) { + HandlingMethod::Error(kind) => Err(PosixError::new( + kind, + if cfg!(debug_assertions) { format!( - "release(file_id: {}, file_handle: {:?}, flags: {:?}, lock_owner: {:?}, flush: {})", - file_id.display(), - file_handle, - flags, - lock_owner, - flush - ) - } else { - String::new() - }) - ), - HandlingMethod::Panic => panic!( + "release(file_id: {}, file_handle: {:?}, flags: {:?}, lock_owner: {:?}, flush: {})", + file_id.display(), + file_handle, + flags, + lock_owner, + flush + ) + } else { + String::new() + }, + )), + HandlingMethod::Panic => panic!( "[Not Implemented] release(file_id: {}, file_handle: {:?}, flags: {:?}, lock_owner: {:?}, flush: {})", file_id.display(), file_handle, @@ -850,21 +870,22 @@ impl FuseHandler for DefaultFuseHandler { flags: RenameFlags, ) -> FuseResult<()> { match self.handling { - HandlingMethod::Error(kind) => Err( - PosixError::new(kind, if cfg!(debug_assertions) { + HandlingMethod::Error(kind) => Err(PosixError::new( + kind, + if cfg!(debug_assertions) { format!( - "rename(parent_id: {}, name: {:?}, newparent: {}, newname: {:?}, flags: {:?})", - parent_id.display(), - Path::new(name), - newparent.display(), - Path::new(newname), - flags - ) - } else { - String::new() - }) - ), - HandlingMethod::Panic => panic!( + "rename(parent_id: {}, name: {:?}, newparent: {}, newname: {:?}, flags: {:?})", + parent_id.display(), + Path::new(name), + newparent.display(), + Path::new(newname), + flags + ) + } else { + String::new() + }, + )), + HandlingMethod::Panic => panic!( "[Not Implemented] rename(parent_id: {}, name: {:?}, newparent: {}, newname: {:?}, flags: {:?})", parent_id.display(), Path::new(name), @@ -934,21 +955,22 @@ impl FuseHandler for DefaultFuseHandler { sleep: bool, ) -> FuseResult<()> { match self.handling { - HandlingMethod::Error(kind) => Err( - PosixError::new(kind, if cfg!(debug_assertions) { + HandlingMethod::Error(kind) => Err(PosixError::new( + kind, + if cfg!(debug_assertions) { format!( - "setlk(file_id: {}, file_handle: {:?}, lock_owner: {}, lock_info: {:?}, sleep: {})", - file_id.display(), - file_handle, - lock_owner, - lock_info, - sleep - ) - } else { - String::new() - }) - ), - HandlingMethod::Panic => panic!( + "setlk(file_id: {}, file_handle: {:?}, lock_owner: {}, lock_info: {:?}, sleep: {})", + file_id.display(), + file_handle, + lock_owner, + lock_info, + sleep + ) + } else { + String::new() + }, + )), + HandlingMethod::Panic => panic!( "[Not Implemented] setlk(file_id: {}, file_handle: {:?}, lock_owner: {}, lock_info: {:?}, sleep: {})", file_id.display(), file_handle, @@ -1061,23 +1083,24 @@ impl FuseHandler for DefaultFuseHandler { lock_owner: Option, ) -> FuseResult { match self.handling { - HandlingMethod::Error(kind) => Err( - PosixError::new(kind, if cfg!(debug_assertions) { + HandlingMethod::Error(kind) => Err(PosixError::new( + kind, + if cfg!(debug_assertions) { format!( - "write(file_id: {}, file_handle: {:?}, seek: {:?}, data_len: {}, write_flags: {:?}, flags: {:?}, lock_owner: {:?})", - file_id.display(), - file_handle, - seek, - data.len(), - write_flags, - flags, - lock_owner - ) - } else { - String::new() - }) - ), - HandlingMethod::Panic => panic!( + "write(file_id: {}, file_handle: {:?}, seek: {:?}, data_len: {}, write_flags: {:?}, flags: {:?}, lock_owner: {:?})", + file_id.display(), + file_handle, + seek, + data.len(), + write_flags, + flags, + lock_owner + ) + } else { + String::new() + }, + )), + HandlingMethod::Panic => panic!( "[Not Implemented] write(file_id: {}, file_handle: {:?}, seek: {:?}, data_len: {}, write_flags: {:?}, flags: {:?}, lock_owner: {:?})", file_id.display(), file_handle, diff --git a/src/templates/mirror_fs.rs b/src/templates/mirror_fs.rs index d37ca74..d392628 100644 --- a/src/templates/mirror_fs.rs +++ b/src/templates/mirror_fs.rs @@ -186,12 +186,18 @@ macro_rules! mirror_fs_readwrite_methods { mode: u32, umask: u32, flags: OpenFlags, - ) -> FuseResult<(OwnedFileHandle, FileAttribute, FUSEOpenResponseFlags)> { + _helper: CreateHelper, + ) -> FuseResult<( + OwnedFileHandle, + FileAttribute, + FUSEOpenResponseFlags, + Option, + )> { let file_path = self.source_path.join(parent_id).join(name); let (fd, file_attr) = unix_fs::create(&file_path, mode, umask, flags)?; // Open by definition returns positive Fd or error let file_handle = OwnedFileHandle::from_owned_fd(fd).unwrap(); - Ok((file_handle, file_attr, FUSEOpenResponseFlags::empty())) + Ok((file_handle, file_attr, FUSEOpenResponseFlags::empty(), None)) } fn mkdir( diff --git a/src/types.rs b/src/types.rs index d058fcd..e1502b1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -23,8 +23,8 @@ pub mod file_handle; mod file_id_type; pub mod flags; mod inode; -pub mod opens; +pub mod helpers; -pub use self::{arguments::*, errors::*, file_handle::*, file_id_type::*, flags::*, inode::*, opens::*}; +pub use self::{arguments::*, errors::*, file_handle::*, file_id_type::*, flags::*, inode::*, helpers::*}; pub use fuser::{FileType as FileKind, KernelConfig, TimeOrNow}; diff --git a/src/types/opens.rs b/src/types/helpers.rs similarity index 61% rename from src/types/opens.rs rename to src/types/helpers.rs index e893cae..e16cdd3 100644 --- a/src/types/opens.rs +++ b/src/types/helpers.rs @@ -1,6 +1,6 @@ #[cfg(feature = "passthrough")] use fuser::BackingId; -use fuser::ReplyOpen; +use fuser::{ReplyCreate, ReplyOpen}; #[cfg(feature = "passthrough")] use std::sync::Arc; @@ -9,6 +9,29 @@ pub struct OpenHelper<'a> { reply_open: &'a ReplyOpen, } +pub struct CreateHelper<'a> { + #[allow(dead_code)] + reply_create: &'a ReplyCreate, +} + +impl<'a> CreateHelper<'a> { + pub(crate) fn new(reply_create: &'a ReplyCreate) -> Self { + Self { reply_create } + } + + #[cfg(feature = "passthrough")] + pub fn open_backing( + self, + fd: impl std::os::fd::AsFd, + ) -> Result { + self.reply_create + .open_backing(fd) + .map(|id| PassthroughBackingId { + backing_id: Arc::new(id), + }) + } +} + impl<'a> OpenHelper<'a> { pub(crate) fn new(reply_open: &'a ReplyOpen) -> Self { Self { reply_open } From 52d5590fa00f4eda83ee6cbf5cc2fb7725df7c7f Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 20 Jan 2026 13:36:42 +0700 Subject: [PATCH 18/25] feat: add weak variant for passthrough backing IDs to allow reuse --- src/types/helpers.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/types/helpers.rs b/src/types/helpers.rs index e16cdd3..9286167 100644 --- a/src/types/helpers.rs +++ b/src/types/helpers.rs @@ -2,7 +2,7 @@ use fuser::BackingId; use fuser::{ReplyCreate, ReplyOpen}; #[cfg(feature = "passthrough")] -use std::sync::Arc; +use std::sync::{Arc, Weak}; pub struct OpenHelper<'a> { #[allow(dead_code)] @@ -62,3 +62,29 @@ impl AsRef for PassthroughBackingId { self.backing_id.as_ref() } } + +impl PassthroughBackingId { + #[cfg(feature = "passthrough")] + pub fn downgrade(&self) -> WeakPassthroughBackingId { + WeakPassthroughBackingId { + backing_id: Arc::downgrade(&self.backing_id), + } + } +} + +#[derive(Debug, Clone)] +pub struct WeakPassthroughBackingId { + #[cfg(feature = "passthrough")] + pub(crate) backing_id: Weak, +} + +impl WeakPassthroughBackingId { + #[cfg(feature = "passthrough")] + pub fn upgrade(&self) -> Option { + self.backing_id + .upgrade() + .map(|backing_id| PassthroughBackingId { + backing_id: backing_id, + }) + } +} From dbaf2e2a9b7d2e985583e152fbb4affc86161eb4 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 20 Jan 2026 14:03:24 +0700 Subject: [PATCH 19/25] feat: Add comparison traits to flags --- src/types/flags.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/types/flags.rs b/src/types/flags.rs index 71a6eec..2a2b846 100644 --- a/src/types/flags.rs +++ b/src/types/flags.rs @@ -8,7 +8,7 @@ use bitflags::bitflags; bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] /// Flags used to check file accessibility. pub struct AccessMask: i32 { /// Check if the file exists. @@ -24,7 +24,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] /// Flags used in fallocate calls. pub struct FallocateFlags: i32 { /// Retain file size; don't extend even if offset + len is greater @@ -50,7 +50,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSEAttrFlags: u32 { const SUBMOUNT = 1 << 0; const DAX = 1 << 1; @@ -59,7 +59,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSEGetAttrFlags: i32 { const GETATTR_FH = 1 << 0; const _ = !0; @@ -67,7 +67,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSEOpenFlags: i32 { const KILL_SUIDGID = 1 << 0; const _ = !0; @@ -75,7 +75,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] /// Flags used in the response to a FUSE open operation. pub struct FUSEOpenResponseFlags: u32 { /// Bypass page cache for this file. @@ -99,7 +99,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSEIoctlFlags: u32 { const COMPAT = 1 << 0; const UNRESTRICTED = 1 << 1; @@ -112,7 +112,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSEReadFlags: i32 { const LOCKOWNER = 1 << 0; const _ = !0; @@ -120,7 +120,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSEReleaseFlags: i32 { const FLUSH = 1 << 0; const FLOCK_UNLOCK = 1 << 1; @@ -129,7 +129,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSEFsyncFlags: u32 { const FDATASYNC = 1 << 0; const _ = !0; @@ -137,7 +137,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSESetXAttrFlags: i32 { const ACL_KILL_SGID = 1 << 0; const _ = !0; @@ -145,7 +145,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSEWriteFlags: u32 { const CACHE = 1 << 0; const LOCKOWNER = 1 << 1; @@ -155,7 +155,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct IOCtlFlags: u32 { // Placeholder for future flags const _ = !0; @@ -163,7 +163,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] // c_short in BSD, c_int in linux /// Flags representing different types of file locks. pub struct LockType: i32 { @@ -178,7 +178,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] /// Flags used when opening files. pub struct OpenFlags: i32 { /// Open for reading only. @@ -220,7 +220,7 @@ bitflags! { } bitflags! { - #[derive(Debug, Copy, Clone)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] /// Flags used in rename operations. pub struct RenameFlags: u32 { /// Atomically exchange the old and new pathnames. (Linux only) From ff2d063a7dd779c3798eca00c1df5c8e3dcddae1 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 20 Jan 2026 14:53:46 +0700 Subject: [PATCH 20/25] feat: add stub weak pointer for parity with weak api --- src/types/helpers.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/types/helpers.rs b/src/types/helpers.rs index 9286167..4b2db7a 100644 --- a/src/types/helpers.rs +++ b/src/types/helpers.rs @@ -79,6 +79,13 @@ pub struct WeakPassthroughBackingId { } impl WeakPassthroughBackingId { + #[cfg(feature = "passthrough")] + pub fn new() -> Self { + Self { + backing_id: Weak::new(), + } + } + #[cfg(feature = "passthrough")] pub fn upgrade(&self) -> Option { self.backing_id From 8cd1605a43fb024e9d011224cd5711124b892365 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Wed, 21 Jan 2026 10:10:53 +0700 Subject: [PATCH 21/25] fix: add API parity --- src/core/fuse_driver.rs | 383 ++++++++++++++------------ src/core/fuse_driver_types.rs | 3 +- src/core/inode_mapping.rs | 91 +++--- src/core/macros.rs | 19 +- src/fuse_handler.rs | 32 ++- src/inode_mapper.rs | 20 +- src/inode_multi_mapper.rs | 12 +- src/templates/default_fuse_handler.rs | 41 +-- src/templates/fd_handler_helper.rs | 13 +- src/types.rs | 10 +- src/types/arguments.rs | 19 +- src/types/errors.rs | 8 + src/types/file_handle.rs | 24 ++ src/types/flags.rs | 105 ++++++- src/types/inode.rs | 25 +- 15 files changed, 503 insertions(+), 302 deletions(-) diff --git a/src/core/fuse_driver.rs b/src/core/fuse_driver.rs index 7ad4652..2b6ef66 100644 --- a/src/core/fuse_driver.rs +++ b/src/core/fuse_driver.rs @@ -1,16 +1,15 @@ +use log::{info, warn}; use std::{ ffi::OsStr, path::Path, time::{Instant, SystemTime}, }; -use libc::c_int; -use log::{error, info, warn}; - use fuser::{ - self, KernelConfig, ReplyAttr, ReplyBmap, ReplyCreate, ReplyData, ReplyDirectory, - ReplyDirectoryPlus, ReplyEmpty, ReplyEntry, ReplyIoctl, ReplyLock, ReplyLseek, ReplyOpen, - ReplyStatfs, ReplyWrite, ReplyXattr, Request, TimeOrNow, + self, AccessFlags, Errno, FileHandle, FopenFlags, Generation, INodeNo, IoctlFlags, + KernelConfig, LockOwner, ReadFlags, ReplyAttr, ReplyBmap, ReplyCreate, ReplyData, + ReplyDirectory, ReplyDirectoryPlus, ReplyEmpty, ReplyEntry, ReplyIoctl, ReplyLock, ReplyLseek, + ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request, TimeOrNow, WriteFlags, }; use super::{ @@ -21,8 +20,8 @@ use super::{ }; use crate::{fuse_handler::FuseHandler, types::*}; -fn get_random_generation() -> u64 { - Instant::now().elapsed().as_nanos() as u64 +fn get_random_generation() -> Generation { + Generation(Instant::now().elapsed().as_nanos() as u64) } impl fuser::Filesystem for FuseDriver @@ -30,13 +29,13 @@ where TId: FileIdType, THandler: FuseHandler, { - fn init(&mut self, req: &Request, config: &mut KernelConfig) -> Result<(), c_int> { + fn init(&mut self, req: &Request, config: &mut KernelConfig) -> Result<(), Errno> { let req = RequestInfo::from(req); match self.get_handler().init(&req, config) { Ok(()) => Ok(()), Err(e) => { warn!("[{}] init {:?}", e, req); - Err(e.raw_error()) + Err(e.into()) } } } @@ -45,30 +44,26 @@ where self.get_handler().destroy(); } - fn access(&mut self, req: &Request, ino: u64, mask: i32, reply: ReplyEmpty) { + fn access(&self, req: &Request, ino: INodeNo, mask: AccessFlags, reply: ReplyEmpty) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); #[cfg_attr(feature = "serial", allow(unused_variables))] let reply_executor = reply_executor!(self); execute_task!(self, { - match handler.access( - &req, - resolver.resolve_id(ino), - AccessMask::from_bits_retain(mask), - ) { + match handler.access(&req, resolver.resolve_id(ino), AccessMask::from(mask)) { Ok(()) => { execute_reply_task!(reply_executor, { reply.ok() }); } Err(e) => { warn!("access: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn bmap(&mut self, req: &Request<'_>, ino: u64, blocksize: u32, idx: u64, reply: ReplyBmap) { + fn bmap(&self, req: &Request, ino: INodeNo, blocksize: u32, idx: u64, reply: ReplyBmap) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -81,23 +76,23 @@ where } Err(e) => { warn!("bmap: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn copy_file_range( - &mut self, + &self, req: &Request, - ino_in: u64, - fh_in: u64, + ino_in: INodeNo, + fh_in: FileHandle, offset_in: i64, - ino_out: u64, - fh_out: u64, + ino_out: INodeNo, + fh_out: FileHandle, offset_out: i64, len: u64, - flags: u32, + flags: fuser::CopyFileRangeFlags, reply: ReplyWrite, ) { let req = RequestInfo::from(req); @@ -109,29 +104,29 @@ where match handler.copy_file_range( &req, resolver.resolve_id(ino_in), - unsafe { BorrowedFileHandle::from_raw(fh_in) }, + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh_in) }, offset_in, resolver.resolve_id(ino_out), - unsafe { BorrowedFileHandle::from_raw(fh_out) }, + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh_out) }, offset_out, len, - flags, + CopyFileRangeFlags::from(flags), ) { Ok(bytes_written) => { execute_reply_task!(reply_executor, { reply.written(bytes_written) }); } Err(e) => { warn!("copy_file_range: ino {:x?}, [{}], {:?}", ino_in, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn create( - &mut self, + &self, req: &Request, - parent: u64, + parent: INodeNo, name: &OsStr, mode: u32, umask: u32, @@ -169,9 +164,9 @@ where reply.created_passthrough( &ttl.unwrap_or(default_ttl), &fuse_attr, - generation.unwrap_or(get_random_generation()), - file_handle.as_raw(), - response_flags.bits(), + generation.unwrap_or(get_random_generation().into()), + file_handle.as_fuser_file_handle(), + FopenFlags::from(response_flags), passthrough_backing_id.as_ref(), ); } @@ -182,8 +177,8 @@ where &ttl.unwrap_or(default_ttl), &fuse_attr, generation.unwrap_or(get_random_generation()), - file_handle.as_raw(), - response_flags.bits(), + file_handle.as_fuser_file_handle(), + FopenFlags::from(response_flags), ); } } @@ -191,17 +186,17 @@ where } Err(e) => { warn!("create: {:?}, parent_ino: {:x?}, {:?}", parent, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn fallocate( - &mut self, + &self, req: &Request, - ino: u64, - fh: u64, + ino: INodeNo, + fh: FileHandle, offset: i64, length: i64, mode: i32, @@ -216,7 +211,7 @@ where match handler.fallocate( &req, resolver.resolve_id(ino), - unsafe { BorrowedFileHandle::from_raw(fh) }, + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }, offset, length, FallocateFlags::from_bits_retain(mode), @@ -226,13 +221,20 @@ where } Err(e) => { warn!("fallocate: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn flush(&mut self, req: &Request, ino: u64, fh: u64, lock_owner: u64, reply: ReplyEmpty) { + fn flush( + &self, + req: &Request, + ino: INodeNo, + fh: FileHandle, + lock_owner: LockOwner, + reply: ReplyEmpty, + ) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -242,7 +244,7 @@ where match handler.flush( &req, resolver.resolve_id(ino), - unsafe { BorrowedFileHandle::from_raw(fh) }, + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }, lock_owner, ) { Ok(()) => { @@ -250,13 +252,13 @@ where } Err(e) => { warn!("flush: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn forget(&mut self, req: &Request, ino: u64, nlookup: u64) { + fn forget(&self, req: &Request, ino: INodeNo, nlookup: u64) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -264,7 +266,14 @@ where resolver.forget(ino, nlookup); } - fn fsync(&mut self, req: &Request, ino: u64, fh: u64, datasync: bool, reply: ReplyEmpty) { + fn fsync( + &self, + req: &Request, + ino: INodeNo, + fh: FileHandle, + datasync: bool, + reply: ReplyEmpty, + ) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -274,7 +283,7 @@ where match handler.fsync( &req, resolver.resolve_id(ino), - unsafe { BorrowedFileHandle::from_raw(fh) }, + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }, datasync, ) { Ok(()) => { @@ -282,13 +291,20 @@ where } Err(e) => { warn!("fsync: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn fsyncdir(&mut self, req: &Request, ino: u64, fh: u64, datasync: bool, reply: ReplyEmpty) { + fn fsyncdir( + &self, + req: &Request, + ino: INodeNo, + fh: FileHandle, + datasync: bool, + reply: ReplyEmpty, + ) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -298,7 +314,7 @@ where match handler.fsyncdir( &req, resolver.resolve_id(ino), - unsafe { BorrowedFileHandle::from_raw(fh) }, + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }, datasync, ) { Ok(()) => { @@ -306,13 +322,13 @@ where } Err(e) => { warn!("fsyncdir: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn getattr(&mut self, req: &Request, ino: u64, fh: Option, reply: ReplyAttr) { + fn getattr(&self, req: &Request, ino: INodeNo, fh: Option, reply: ReplyAttr) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -330,18 +346,18 @@ where ( &req, resolver.resolve_id(ino), - fh.map(|fh| unsafe { BorrowedFileHandle::from_raw(fh) }) + fh.map(|fh| unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }) ) ); }); } fn getlk( - &mut self, - req: &Request<'_>, - ino: u64, - fh: u64, - lock_owner: u64, + &self, + req: &Request, + ino: INodeNo, + fh: FileHandle, + lock_owner: LockOwner, start: u64, end: u64, typ: i32, @@ -363,7 +379,7 @@ where match handler.getlk( &req, resolver.resolve_id(ino), - unsafe { BorrowedFileHandle::from_raw(fh) }, + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }, lock_owner, lock_info, ) { @@ -379,13 +395,13 @@ where } Err(e) => { warn!("getlk: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn getxattr(&mut self, req: &Request, ino: u64, name: &OsStr, size: u32, reply: ReplyXattr) { + fn getxattr(&self, req: &Request, ino: INodeNo, name: &OsStr, size: u32, reply: ReplyXattr) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -401,24 +417,30 @@ where } else if size >= xattr_data.len() as u32 { reply.data(&xattr_data); } else { - reply.error(ErrorKind::ResultTooLarge.into()); + reply.error( + PosixError::new( + ErrorKind::ResultTooLarge, + "returned result is too large", + ) + .into(), + ); } }); } Err(e) => { warn!("getxattr: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn ioctl( - &mut self, - req: &Request<'_>, - ino: u64, - fh: u64, - flags: u32, + &self, + req: &Request, + ino: INodeNo, + fh: FileHandle, + flags: IoctlFlags, cmd: u32, in_data: &[u8], out_size: u32, @@ -434,8 +456,8 @@ where match handler.ioctl( &req, resolver.resolve_id(ino), - unsafe { BorrowedFileHandle::from_raw(fh) }, - IOCtlFlags::from_bits_retain(flags), + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }, + FUSEIoctlFlags::from(flags), cmd, in_data, out_size, @@ -445,17 +467,17 @@ where } Err(e) => { warn!("ioctl: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn link( - &mut self, + &self, req: &Request, - ino: u64, - newparent: u64, + ino: INodeNo, + newparent: INodeNo, newname: &OsStr, reply: ReplyEntry, ) { @@ -485,7 +507,7 @@ where }); } - fn listxattr(&mut self, req: &Request, ino: u64, size: u32, reply: ReplyXattr) { + fn listxattr(&self, req: &Request, ino: INodeNo, size: u32, reply: ReplyXattr) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -500,19 +522,25 @@ where } else if size >= xattr_data.len() as u32 { reply.data(&xattr_data); } else { - reply.error(ErrorKind::ResultTooLarge.into()); + reply.error( + PosixError::new( + ErrorKind::ResultTooLarge, + "returned result is too large than allowed size", + ) + .into(), + ); } }); } Err(e) => { warn!("listxattr: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn lookup(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { + fn lookup(&self, req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEntry) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -535,10 +563,10 @@ where } fn lseek( - &mut self, + &self, req: &Request, - ino: u64, - fh: u64, + ino: INodeNo, + fh: FileHandle, offset: i64, whence: i32, reply: ReplyLseek, @@ -552,7 +580,7 @@ where match handler.lseek( &req, resolver.resolve_id(ino), - unsafe { BorrowedFileHandle::from_raw(fh) }, + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }, seek_from_raw(Some(whence), offset), ) { Ok(new_offset) => { @@ -560,16 +588,16 @@ where } Err(e) => { warn!("lseek: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn mkdir( - &mut self, + &self, req: &Request, - parent: u64, + parent: INodeNo, name: &OsStr, mode: u32, umask: u32, @@ -597,9 +625,9 @@ where } fn mknod( - &mut self, + &self, req: &Request, - parent: u64, + parent: INodeNo, name: &OsStr, mode: u32, umask: u32, @@ -634,7 +662,7 @@ where }); } - fn open(&mut self, req: &Request, ino: u64, _flags: i32, reply: ReplyOpen) { + fn open(&self, req: &Request, ino: INodeNo, flags: fuser::OpenFlags, reply: ReplyOpen) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -645,7 +673,7 @@ where match handler.open( &req, resolver.resolve_id(ino), - OpenFlags::from_bits_retain(_flags), + OpenFlags::from(flags), open_helper, ) { Ok((file_handle, response_flags, backing_id)) => { @@ -654,28 +682,31 @@ where #[cfg(feature = "passthrough")] Some(backing_id) => { reply.opened_passthrough( - file_handle.as_raw(), - response_flags.bits(), + file_handle.as_fuser_file_handle(), + FopenFlags::from(response_flags), backing_id.as_ref(), ); } _ => { let response_flags = response_flags & !FUSEOpenResponseFlags::PASSTHROUGH; - reply.opened(file_handle.as_raw(), response_flags.bits()) + reply.opened( + file_handle.as_fuser_file_handle(), + FopenFlags::from(response_flags), + ) } } }); } Err(e) => { warn!("open: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn opendir(&mut self, req: &Request, ino: u64, _flags: i32, reply: ReplyOpen) { + fn opendir(&self, req: &Request, ino: INodeNo, flags: fuser::OpenFlags, reply: ReplyOpen) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -686,7 +717,7 @@ where match handler.opendir( &req, resolver.resolve_id(ino), - OpenFlags::from_bits_retain(_flags), + OpenFlags::from(flags), open_helper, ) { Ok((file_handle, response_flags, backing_id)) => { @@ -695,36 +726,40 @@ where #[cfg(feature = "passthrough")] Some(backing_id) => { reply.opened_passthrough( - file_handle.as_raw(), - response_flags.bits(), + file_handle.as_fuser_file_handle(), + FopenFlags::from(response_flags), backing_id.as_ref(), ); } _ => { let response_flags = response_flags & !FUSEOpenResponseFlags::PASSTHROUGH; - reply.opened(file_handle.as_raw(), response_flags.bits()) + reply.opened( + file_handle.as_fuser_file_handle(), + FopenFlags::from(response_flags), + ) } } }); } Err(e) => { warn!("opendir: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn read( - &mut self, + &self, req: &Request, - ino: u64, - fh: u64, - offset: i64, + ino: INodeNo, + fh: FileHandle, + offset: u64, size: u32, - flags: i32, - lock_owner: Option, + read_flags: ReadFlags, + flags: u32, + lock_owner: Option, reply: ReplyData, ) { let req = RequestInfo::from(req); @@ -736,10 +771,11 @@ where match handler.read( &req, resolver.resolve_id(ino), - unsafe { BorrowedFileHandle::from_raw(fh) }, - seek_from_raw(None, offset), + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }, + SeekFrom::Start(offset), size, - FUSEOpenFlags::from_bits_retain(flags), + FUSEReadFlags::from(read_flags), + OpenFlags::from_bits_retain(flags as i32), lock_owner, ) { Ok(data_reply) => { @@ -747,18 +783,18 @@ where } Err(e) => { warn!("read: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn readdir( - &mut self, + &self, req: &Request, - ino: u64, - fh: u64, - offset: i64, + ino: INodeNo, + fh: FileHandle, + offset: u64, mut reply: ReplyDirectory, ) { handle_dir_read!( @@ -775,11 +811,11 @@ where } fn readdirplus( - &mut self, + &self, req: &Request, - ino: u64, - fh: u64, - offset: i64, + ino: INodeNo, + fh: FileHandle, + offset: u64, mut reply: ReplyDirectoryPlus, ) { handle_dir_read!( @@ -795,7 +831,7 @@ where ); } - fn readlink(&mut self, req: &Request, ino: u64, reply: ReplyData) { + fn readlink(&self, req: &Request, ino: INodeNo, reply: ReplyData) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -808,19 +844,19 @@ where } Err(e) => { warn!("[{}] readlink, ino: {:x?}, {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn release( - &mut self, + &self, req: &Request, - ino: u64, - fh: u64, - _flags: i32, - _lock_owner: Option, + ino: INodeNo, + fh: FileHandle, + flags: i32, + lock_owner: Option, _flush: bool, reply: ReplyEmpty, ) { @@ -833,9 +869,9 @@ where match handler.release( &req, resolver.resolve_id(ino), - unsafe { OwnedFileHandle::from_raw(fh) }, - OpenFlags::from_bits_retain(_flags), - _lock_owner, + unsafe { OwnedFileHandle::from_fuser_file_handle(fh) }, + OpenFlags::from_bits_retain(flags), + lock_owner, _flush, ) { Ok(()) => { @@ -843,13 +879,20 @@ where } Err(e) => { warn!("release: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn releasedir(&mut self, req: &Request, ino: u64, fh: u64, flags: i32, reply: ReplyEmpty) { + fn releasedir( + &self, + req: &Request, + ino: INodeNo, + fh: FileHandle, + flags: i32, + reply: ReplyEmpty, + ) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -859,7 +902,7 @@ where match handler.releasedir( &req, resolver.resolve_id(ino), - unsafe { OwnedFileHandle::from_raw(fh) }, + unsafe { OwnedFileHandle::from_fuser_file_handle(fh) }, OpenFlags::from_bits_retain(flags), ) { Ok(()) => { @@ -867,13 +910,13 @@ where } Err(e) => { warn!("releasedir: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn removexattr(&mut self, req: &Request, ino: u64, name: &OsStr, reply: ReplyEmpty) { + fn removexattr(&self, req: &Request, ino: INodeNo, name: &OsStr, reply: ReplyEmpty) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -887,20 +930,20 @@ where } Err(e) => { warn!("removexattr: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn rename( - &mut self, + &self, req: &Request, - parent: u64, + parent: INodeNo, name: &OsStr, - newparent: u64, + newparent: INodeNo, newname: &OsStr, - flags: u32, + flags: fuser::RenameFlags, reply: ReplyEmpty, ) { let req = RequestInfo::from(req); @@ -917,7 +960,7 @@ where &name, resolver.resolve_id(newparent), &newname, - RenameFlags::from_bits_retain(flags), + RenameFlags::from(flags), ) { Ok(()) => { resolver.rename(parent, &name, newparent, &newname); @@ -925,13 +968,13 @@ where } Err(e) => { warn!("[{}] rename: parent_ino: {:x?}, {:?}", parent, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } } }); } - fn rmdir(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { + fn rmdir(&self, req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -945,16 +988,16 @@ where } Err(e) => { warn!("[{}] rmdir: parent_ino: {:x?}, {:?}", parent, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn setattr( - &mut self, + &self, req: &Request, - ino: u64, + ino: INodeNo, mode: Option, uid: Option, gid: Option, @@ -962,7 +1005,7 @@ where atime: Option, mtime: Option, ctime: Option, - fh: Option, + fh: Option, crtime: Option, chgtime: Option, bkuptime: Option, @@ -984,7 +1027,7 @@ where chgtime: chgtime, bkuptime: bkuptime, flags: None, - file_handle: fh.map(|fh| unsafe { BorrowedFileHandle::from_raw(fh) }), + file_handle: fh.map(|fh| unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }), }; #[cfg_attr(feature = "serial", allow(unused_variables))] let reply_executor = reply_executor!(self); @@ -1003,11 +1046,11 @@ where } fn setlk( - &mut self, - req: &Request<'_>, - ino: u64, - fh: u64, - lock_owner: u64, + &self, + req: &Request, + ino: INodeNo, + fh: FileHandle, + lock_owner: LockOwner, start: u64, end: u64, typ: i32, @@ -1030,7 +1073,7 @@ where match handler.setlk( &req, resolver.resolve_id(ino), - unsafe { BorrowedFileHandle::from_raw(fh) }, + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }, lock_owner, lock_info, sleep, @@ -1040,16 +1083,16 @@ where } Err(e) => { warn!("setlk: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn setxattr( - &mut self, + &self, req: &Request, - ino: u64, + ino: INodeNo, name: &OsStr, value: &[u8], flags: i32, @@ -1077,13 +1120,13 @@ where } Err(e) => { warn!("setxattr: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn statfs(&mut self, req: &Request, ino: u64, reply: ReplyStatfs) { + fn statfs(&self, req: &Request, ino: INodeNo, reply: ReplyStatfs) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -1107,16 +1150,16 @@ where } Err(e) => { warn!("statfs: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } fn symlink( - &mut self, + &self, req: &Request, - parent: u64, + parent: INodeNo, link_name: &OsStr, target: &Path, reply: ReplyEntry, @@ -1144,15 +1187,15 @@ where } fn write( - &mut self, + &self, req: &Request, - ino: u64, - fh: u64, + ino: INodeNo, + fh: FileHandle, offset: i64, data: &[u8], - write_flags: u32, + write_flags: WriteFlags, flags: i32, - lock_owner: Option, + lock_owner: Option, reply: ReplyWrite, ) { let req = RequestInfo::from(req); @@ -1165,10 +1208,10 @@ where match handler.write( &req, resolver.resolve_id(ino), - unsafe { BorrowedFileHandle::from_raw(fh) }, + unsafe { BorrowedFileHandle::from_fuser_file_handle(fh) }, seek_from_raw(None, offset), data, - FUSEWriteFlags::from_bits_retain(write_flags), + FUSEWriteFlags::from(write_flags), OpenFlags::from_bits_retain(flags), lock_owner, ) { @@ -1177,13 +1220,13 @@ where } Err(e) => { warn!("write: ino {:x?}, [{}], {:?}", ino, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); } - fn unlink(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { + fn unlink(&self, req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) { let req = RequestInfo::from(req); let handler = self.get_handler(); let resolver = self.get_resolver(); @@ -1197,7 +1240,7 @@ where } Err(e) => { warn!("[{}] unlink: parent_ino: {:x?}, {:?}", parent, e, req); - execute_reply_task!(reply_executor, { reply.error(e.raw_error()) }); + execute_reply_task!(reply_executor, { reply.error(e.into()) }); } }; }); diff --git a/src/core/fuse_driver_types.rs b/src/core/fuse_driver_types.rs index 4ea1746..3731b77 100644 --- a/src/core/fuse_driver_types.rs +++ b/src/core/fuse_driver_types.rs @@ -9,7 +9,7 @@ use super::inode_mapping::FileIdResolver; use crate::fuse_handler::FuseHandler; use crate::types::*; -type DirIter = HashMap<(u64, i64), VecDeque<(OsString, u64, TAttr)>>; +type DirIter = HashMap<(INodeNo, u64), VecDeque<(OsString, INodeNo, TAttr)>>; #[cfg(feature = "serial")] mod serial { @@ -293,6 +293,7 @@ fn spawn_deadlock_checker() { }); } +use fuser::INodeNo; #[cfg(feature = "serial")] pub use serial::*; diff --git a/src/core/inode_mapping.rs b/src/core/inode_mapping.rs index 6ed4c92..5be3790 100644 --- a/src/core/inode_mapping.rs +++ b/src/core/inode_mapping.rs @@ -8,6 +8,8 @@ use std::{ use std::sync::{RwLock, atomic::AtomicU64}; +use fuser::INodeNo; + use crate::{inode_mapper, types::*}; use crate::{inode_mapper::InodeMapper, inode_multi_mapper::*}; @@ -61,22 +63,22 @@ pub trait FileIdResolver: Send + Sync + 'static { type ResolvedType: FileIdType; fn new() -> Self; - fn resolve_id(&self, ino: u64) -> Self::ResolvedType; + fn resolve_id(&self, ino: INodeNo) -> Self::ResolvedType; fn lookup( &self, - parent: u64, + parent: INodeNo, child: &OsStr, id: ::_Id, increment: bool, - ) -> u64; + ) -> INodeNo; fn add_children( &self, - parent: u64, + parent: INodeNo, children: Vec<(OsString, ::_Id)>, increment: bool, - ) -> Vec<(OsString, u64)>; - fn forget(&self, ino: u64, nlookup: u64); - fn rename(&self, parent: u64, name: &OsStr, newparent: u64, newname: &OsStr); + ) -> Vec<(OsString, INodeNo)>; + fn forget(&self, ino: INodeNo, nlookup: u64); + fn rename(&self, parent: INodeNo, name: &OsStr, newparent: INodeNo, newname: &OsStr); } pub struct InodeResolver {} @@ -88,30 +90,30 @@ impl FileIdResolver for InodeResolver { Self {} } - fn resolve_id(&self, ino: u64) -> Self::ResolvedType { + fn resolve_id(&self, ino: INodeNo) -> Self::ResolvedType { Inode::from(ino) } - fn lookup(&self, _parent: u64, _child: &OsStr, id: Inode, _increment: bool) -> u64 { + fn lookup(&self, _parent: INodeNo, _child: &OsStr, id: Inode, _increment: bool) -> INodeNo { id.into() } // Do nothing, user should provide its own inode fn add_children( &self, - _parent: u64, + _parent: INodeNo, children: Vec<(OsString, Inode)>, _increment: bool, - ) -> Vec<(OsString, u64)> { + ) -> Vec<(OsString, INodeNo)> { children .into_iter() - .map(|(name, inode)| (name, u64::from(inode))) + .map(|(name, inode)| (name, INodeNo::from(inode))) .collect() } - fn forget(&self, _ino: u64, _nlookup: u64) {} + fn forget(&self, _ino: INodeNo, _nlookup: u64) {} - fn rename(&self, _parent: u64, _name: &OsStr, _newparent: u64, _newname: &OsStr) {} + fn rename(&self, _parent: INodeNo, _name: &OsStr, _newparent: INodeNo, _newname: &OsStr) {} } pub struct ComponentsResolver { @@ -127,7 +129,7 @@ impl FileIdResolver for ComponentsResolver { } } - fn resolve_id(&self, ino: u64) -> Self::ResolvedType { + fn resolve_id(&self, ino: INodeNo) -> Self::ResolvedType { self.mapper .read() .unwrap() @@ -138,7 +140,7 @@ impl FileIdResolver for ComponentsResolver { .collect() } - fn lookup(&self, parent: u64, child: &OsStr, _id: (), increment: bool) -> u64 { + fn lookup(&self, parent: INodeNo, child: &OsStr, _id: (), increment: bool) -> INodeNo { let parent = Inode::from(parent); { // Optimistically assume the child exists @@ -146,10 +148,10 @@ impl FileIdResolver for ComponentsResolver { if increment { lookup_result.data.fetch_add(1, Ordering::SeqCst); } - return u64::from(lookup_result.inode.clone()); + return INodeNo::from(lookup_result.inode.clone()); } } - u64::from( + INodeNo::from( self.mapper .write() .expect("Failed to acquire write lock") @@ -162,10 +164,10 @@ impl FileIdResolver for ComponentsResolver { fn add_children( &self, - parent: u64, + parent: INodeNo, children: Vec<(OsString, ())>, increment: bool, - ) -> Vec<(OsString, u64)> { + ) -> Vec<(OsString, INodeNo)> { let value_creator = |value_creator: inode_mapper::ValueCreatorParams| match value_creator .existing_data @@ -190,11 +192,11 @@ impl FileIdResolver for ComponentsResolver { inserted_children .into_iter() .zip(children) - .map(|(inode, (name, _))| (name, u64::from(inode))) + .map(|(inode, (name, _))| (name, INodeNo::from(inode))) .collect() } - fn forget(&self, ino: u64, nlookup: u64) { + fn forget(&self, ino: INodeNo, nlookup: u64) { let inode = Inode::from(ino); { // Optimistically assume we don't have to remove yet @@ -207,7 +209,7 @@ impl FileIdResolver for ComponentsResolver { self.mapper.write().unwrap().remove(&inode).unwrap(); } - fn rename(&self, parent: u64, name: &OsStr, newparent: u64, newname: &OsStr) { + fn rename(&self, parent: INodeNo, name: &OsStr, newparent: INodeNo, newname: &OsStr) { let parent_inode = Inode::from(parent); let newparent_inode = Inode::from(newparent); self.mapper @@ -236,7 +238,7 @@ impl FileIdResolver for PathResolver { } } - fn resolve_id(&self, ino: u64) -> Self::ResolvedType { + fn resolve_id(&self, ino: INodeNo) -> Self::ResolvedType { self.resolver .resolve_id(ino) .iter() @@ -246,28 +248,28 @@ impl FileIdResolver for PathResolver { fn lookup( &self, - parent: u64, + parent: INodeNo, child: &OsStr, id: ::_Id, increment: bool, - ) -> u64 { + ) -> INodeNo { self.resolver.lookup(parent, child, id, increment) } fn add_children( &self, - parent: u64, + parent: INodeNo, children: Vec<(OsString, ::_Id)>, increment: bool, - ) -> Vec<(OsString, u64)> { + ) -> Vec<(OsString, INodeNo)> { self.resolver.add_children(parent, children, increment) } - fn forget(&self, ino: u64, nlookup: u64) { + fn forget(&self, ino: INodeNo, nlookup: u64) { self.resolver.forget(ino, nlookup); } - fn rename(&self, parent: u64, name: &OsStr, newparent: u64, newname: &OsStr) { + fn rename(&self, parent: INodeNo, name: &OsStr, newparent: INodeNo, newname: &OsStr) { self.resolver.rename(parent, name, newparent, newname); } } @@ -290,7 +292,7 @@ where HybridResolver { mapper: instance } } - fn resolve_id(&self, ino: u64) -> Self::ResolvedType { + fn resolve_id(&self, ino: INodeNo) -> Self::ResolvedType { let first_path = self .mapper .read() @@ -308,11 +310,11 @@ where fn lookup( &self, - parent: u64, + parent: INodeNo, child: &OsStr, id: ::_Id, increment: bool, - ) -> u64 { + ) -> INodeNo { let parent = Inode::from(parent); { // Optimistically assume the child exists @@ -327,11 +329,11 @@ where if increment { lookup_result.data.fetch_add(1, Ordering::SeqCst); } - return u64::from(lookup_result.inode.clone()); + return INodeNo::from(lookup_result.inode.clone()); } } // This scenario happens if the child node does not exist or the backing ID does not match - u64::from( + INodeNo::from( self.mapper .write() .expect("Failed to acquire write lock") @@ -350,7 +352,7 @@ where ) } - fn forget(&self, ino: u64, nlookup: u64) { + fn forget(&self, ino: INodeNo, nlookup: u64) { let inode = Inode::from(ino); { // Optimistically assume we don't have to remove yet @@ -369,10 +371,10 @@ where fn add_children( &self, - parent: u64, + parent: INodeNo, children: Vec<(OsString, ::_Id)>, increment: bool, - ) -> Vec<(OsString, u64)> { + ) -> Vec<(OsString, INodeNo)> { let value_creator = |value_creator: ValueCreatorParams| match value_creator.existing_data { Some(nlookup) => { @@ -395,11 +397,11 @@ where inserted_children .into_iter() .zip(children) - .map(|(inode, (name, _))| (name, u64::from(inode))) + .map(|(inode, (name, _))| (name, INodeNo::from(inode))) .collect() } - fn rename(&self, parent: u64, name: &OsStr, newparent: u64, newname: &OsStr) { + fn rename(&self, parent: INodeNo, name: &OsStr, newparent: INodeNo, newname: &OsStr) { let parent_inode = Inode::from(parent); let newparent_inode = Inode::from(newparent); self.mapper @@ -522,7 +524,7 @@ mod tests { // Test lookup for non-existent file let non_existent_ino = resolver.lookup(root_ino, OsStr::new("non_existent"), (), false); - assert_ne!(non_existent_ino, 0); + assert_ne!(non_existent_ino, INodeNo(0)); let non_existent_path = resolver.resolve_id(non_existent_ino); assert_eq!(non_existent_path, PathBuf::from("non_existent")); } @@ -593,7 +595,7 @@ mod tests { // Test lookup for non-existent file let non_existent_ino = resolver.lookup(root_ino, OsStr::new("non_existent"), Some(6), false); - assert_ne!(non_existent_ino, 0); + assert_ne!(non_existent_ino, INodeNo(0)); let non_existent_path = resolver.resolve_id(non_existent_ino); assert_eq!( non_existent_path.first_path(), @@ -628,7 +630,10 @@ mod tests { // Test path resolution after overriding a location with a new backing ID let paths = hard_link_id_2.paths(100); - assert!(!paths.contains(&PathBuf::from("dir1/dir2/hard_linked")), "the path list should no longer contain the overridden location"); + assert!( + !paths.contains(&PathBuf::from("dir1/dir2/hard_linked")), + "the path list should no longer contain the overridden location" + ); assert!(paths.contains(&PathBuf::from("hard_link"))); assert!(paths.contains(&PathBuf::from("dir1/hard_linked_2"))); } diff --git a/src/core/macros.rs b/src/core/macros.rs index dd3443d..5e2c9ce 100644 --- a/src/core/macros.rs +++ b/src/core/macros.rs @@ -43,7 +43,7 @@ macro_rules! handle_fuse_reply_entry { execute_reply_task!( $reply_executor, { - $reply.error(e.raw_error()) + $reply.error(Errno::from_i32(e.raw_error())) } ); } @@ -70,7 +70,7 @@ macro_rules! handle_fuse_reply_attr { execute_reply_task!( $reply_executor, { - $reply.error(e.raw_error()) + $reply.error(Errno::from_i32(e.raw_error())) } ); } @@ -121,20 +121,11 @@ macro_rules! handle_dir_read { let reply_executor = reply_executor!($self); execute_task!($self, { - // Validate offset - if $offset < 0 { - error!("readdir called with a negative offset"); - execute_reply_task!(reply_executor, { - $reply.error(ErrorKind::InvalidArgument.into()); - }); - return; - } - // ### Initialize directory iterator let mut dir_iter = match $offset { // First read: fetch children from handler 0 => match handler.$handler_method(&req_info, resolver.resolve_id($ino), unsafe { - BorrowedFileHandle::from_raw($fh) + BorrowedFileHandle::from_fuser_file_handle($fh) }) { Ok(children) => { // Unpack and process children @@ -167,7 +158,7 @@ macro_rules! handle_dir_read { Err(e) => { warn!("readdir {:?}: {:?}", req_info, e); execute_reply_task!(reply_executor, { - $reply.error(e.raw_error()); + $reply.error(Errno::from_i32(e.raw_error())); }); return; } @@ -221,7 +212,7 @@ macro_rules! handle_dir_read { dir_iter.push_front((name, ino, file_attr.clone())); dirmap_iter .safe_borrow_mut() - .insert((ino, new_offset - 1), dir_iter); + .insert(($ino, new_offset - 1), dir_iter); break; } new_offset += 1; diff --git a/src/fuse_handler.rs b/src/fuse_handler.rs index 8331ee5..8673e51 100644 --- a/src/fuse_handler.rs +++ b/src/fuse_handler.rs @@ -103,6 +103,7 @@ mod private { #[cfg(feature = "serial")] impl OptionalSendSync for T {} } +use fuser::LockOwner; use private::OptionalSendSync; pub trait FuseHandler: OptionalSendSync + 'static { @@ -154,7 +155,7 @@ pub trait FuseHandler: OptionalSendSync + 'static { file_handle_out: BorrowedFileHandle, offset_out: i64, len: u64, - flags: u32, // Not implemented yet in standard + flags: CopyFileRangeFlags, // Not implemented yet in standard ) -> FuseResult { self.get_inner().copy_file_range( req, @@ -217,7 +218,7 @@ pub trait FuseHandler: OptionalSendSync + 'static { req: &RequestInfo, file_id: TId, file_handle: BorrowedFileHandle, - lock_owner: u64, + lock_owner: LockOwner, ) -> FuseResult<()> { self.get_inner() .flush(req, file_id, file_handle, lock_owner) @@ -274,7 +275,7 @@ pub trait FuseHandler: OptionalSendSync + 'static { req: &RequestInfo, file_id: TId, file_handle: BorrowedFileHandle, - lock_owner: u64, + lock_owner: LockOwner, lock_info: LockInfo, ) -> FuseResult { self.get_inner() @@ -298,7 +299,7 @@ pub trait FuseHandler: OptionalSendSync + 'static { req: &RequestInfo, file_id: TId, file_handle: BorrowedFileHandle, - flags: IOCtlFlags, + flags: FUSEIoctlFlags, cmd: u32, in_data: Vec, out_size: u32, @@ -411,11 +412,20 @@ pub trait FuseHandler: OptionalSendSync + 'static { file_handle: BorrowedFileHandle, seek: SeekFrom, size: u32, - flags: FUSEOpenFlags, - lock_owner: Option, + read_flags: FUSEReadFlags, + flags: OpenFlags, + lock_owner: Option, ) -> FuseResult> { - self.get_inner() - .read(req, file_id, file_handle, seek, size, flags, lock_owner) + self.get_inner().read( + req, + file_id, + file_handle, + seek, + size, + read_flags, + flags, + lock_owner, + ) } /// Read directory contents @@ -469,7 +479,7 @@ pub trait FuseHandler: OptionalSendSync + 'static { file_id: TId, file_handle: OwnedFileHandle, flags: OpenFlags, - lock_owner: Option, + lock_owner: Option, flush: bool, ) -> FuseResult<()> { self.get_inner() @@ -537,7 +547,7 @@ pub trait FuseHandler: OptionalSendSync + 'static { req: &RequestInfo, file_id: TId, file_handle: BorrowedFileHandle, - lock_owner: u64, + lock_owner: LockOwner, lock_info: LockInfo, sleep: bool, ) -> FuseResult<()> { @@ -589,7 +599,7 @@ pub trait FuseHandler: OptionalSendSync + 'static { data: Vec, write_flags: FUSEWriteFlags, flags: OpenFlags, - lock_owner: Option, + lock_owner: Option, ) -> FuseResult { self.get_inner().write( req, diff --git a/src/inode_mapper.rs b/src/inode_mapper.rs index 119798e..de98924 100644 --- a/src/inode_mapper.rs +++ b/src/inode_mapper.rs @@ -555,7 +555,7 @@ mod tests { let child_name = OsString::from("child"); // Insert the first child - let first_child_inode = Inode::from(2); + let first_child_inode = Inode::new(2); assert_eq!( mapper.insert_child(&root, child_name.clone(), |value_creator_params| { assert!(value_creator_params.existing_data.is_none()); @@ -619,7 +619,7 @@ mod tests { } path.push(OsString::from(format!("file_{}", i))); entries.push((path, move |_: ValueCreatorParams| i)); - expected_inodes.insert(Inode::from(i + 2)); // Start from 2 to avoid conflict with root_inode + expected_inodes.insert(Inode::new(i + 2)); // Start from 2 to avoid conflict with root_inode } // Perform batch insert @@ -630,7 +630,7 @@ mod tests { // Check if all inserted inodes exist for i in 2..=(FILE_COUNT as u64 + 1) { - let inode = Inode::from(i); + let inode = Inode::new(i); assert!(mapper.get(&inode).is_some(), "{:?} should exist", inode); } @@ -690,13 +690,13 @@ mod tests { assert!(root_path.is_empty()); // Try to resolve a non-existent inode - assert!(mapper.resolve(&Inode::from(999)).is_none()); + assert!(mapper.resolve(&Inode::new(999)).is_none()); } #[test] fn test_resolve_invalid_inode() { let mapper = InodeMapper::new(0); - let invalid_inode = Inode::from(999); + let invalid_inode = Inode::new(999); // Attempt to resolve an invalid inode let result = mapper.resolve(&invalid_inode); @@ -893,11 +893,11 @@ mod tests { #[test] fn test_remove_cascading() { let mut mapper = InodeMapper::new(()); - let child1 = Inode::from(2); - let child2 = Inode::from(3); - let grandchild1 = Inode::from(4); - let grandchild2 = Inode::from(5); - let great_grandchild = Inode::from(6); + let child1 = Inode::new(2); + let child2 = Inode::new(3); + let grandchild1 = Inode::new(4); + let grandchild2 = Inode::new(5); + let great_grandchild = Inode::new(6); // Create a deeper nested structure mapper diff --git a/src/inode_multi_mapper.rs b/src/inode_multi_mapper.rs index fd4cf08..b80f4dd 100644 --- a/src/inode_multi_mapper.rs +++ b/src/inode_multi_mapper.rs @@ -258,7 +258,7 @@ where let mut hasher = std::collections::hash_map::DefaultHasher::new(); backing_id.hash(&mut hasher); let hash = hasher.finish(); - let mut preferred_inode = Inode::from(hash); + let mut preferred_inode = Inode::new(hash); loop { if self.data.inodes.get(&preferred_inode).is_none() { break preferred_inode; @@ -967,7 +967,7 @@ mod tests { let child_name = OsString::from("child"); // Insert the first child - let first_child_inode = Inode::from(2); + let first_child_inode = Inode::new(2); assert_eq!( mapper.insert_child(&root, child_name.clone(), None, |value_creator_params| { assert!(value_creator_params.existing_data.is_none()); @@ -1040,7 +1040,7 @@ mod tests { } path.push(OsString::from(format!("file_{}", i))); entries.push((path, None, move |_: ValueCreatorParams| i)); - expected_inodes.insert(Inode::from(i + 2)); // Start from 2 to avoid conflict with root_inode + expected_inodes.insert(Inode::new(i + 2)); // Start from 2 to avoid conflict with root_inode } // Perform batch insert @@ -1051,7 +1051,7 @@ mod tests { // Check if all inserted inodes exist for i in 2..=(FILE_COUNT as u64 + 1) { - let inode = Inode::from(i); + let inode = Inode::new(i); assert!(mapper.get(&inode).is_some(), "{:?} should exist", inode); } @@ -1142,13 +1142,13 @@ mod tests { assert!(root_path.is_empty()); // Try to resolve a non-existent inode - assert!(mapper.resolve(&Inode::from(999)).is_none()); + assert!(mapper.resolve(&Inode::new(999)).is_none()); } #[test] fn test_resolve_invalid_inode() { let mapper = InodeMultiMapper::::new(0); - let invalid_inode = Inode::from(999); + let invalid_inode = Inode::new(999); // Attempt to resolve an invalid inode let result = mapper.resolve(&invalid_inode); diff --git a/src/templates/default_fuse_handler.rs b/src/templates/default_fuse_handler.rs index 25ba51e..ff86e02 100644 --- a/src/templates/default_fuse_handler.rs +++ b/src/templates/default_fuse_handler.rs @@ -152,14 +152,14 @@ impl FuseHandler for DefaultFuseHandler { file_handle_out: BorrowedFileHandle, offset_out: i64, len: u64, - flags: u32, // Not implemented yet in standard + flags: CopyFileRangeFlags, // Not implemented yet in standard ) -> FuseResult { match self.handling { HandlingMethod::Error(kind) => Err(PosixError::new( kind, if cfg!(debug_assertions) { format!( - "copy_file_range(file_in: {}, file_handle_in: {:?}, offset_in: {}, file_out: {}, file_handle_out: {:?}, offset_out: {}, len: {}, flags: {})", + "copy_file_range(file_in: {}, file_handle_in: {:?}, offset_in: {}, file_out: {}, file_handle_out: {:?}, offset_out: {}, len: {}, flags: {:?})", file_in.display(), file_handle_in, offset_in, @@ -174,7 +174,7 @@ impl FuseHandler for DefaultFuseHandler { }, )), HandlingMethod::Panic => panic!( - "[Not Implemented] copy_file_range(file_in: {}, file_handle_in: {:?}, offset_in: {}, file_out: {}, file_handle_out: {:?}, offset_out: {}, len: {}, flags: {})", + "[Not Implemented] copy_file_range(file_in: {}, file_handle_in: {:?}, offset_in: {}, file_out: {}, file_handle_out: {:?}, offset_out: {}, len: {}, flags: {:?})", file_in.display(), file_handle_in, offset_in, @@ -270,14 +270,14 @@ impl FuseHandler for DefaultFuseHandler { _req: &RequestInfo, file_id: TId, file_handle: BorrowedFileHandle, - lock_owner: u64, + lock_owner: LockOwner, ) -> FuseResult<()> { match self.handling { HandlingMethod::Error(kind) => Err(PosixError::new( kind, if cfg!(debug_assertions) { format!( - "flush(file_id: {}, file_handle: {:?}, lock_owner: {})", + "flush(file_id: {}, file_handle: {:?}, lock_owner: {:?})", file_id.display(), file_handle, lock_owner @@ -287,7 +287,7 @@ impl FuseHandler for DefaultFuseHandler { }, )), HandlingMethod::Panic => panic!( - "[Not Implemented] flush(file_id: {}, file_handle: {:?}, lock_owner: {})", + "[Not Implemented] flush(file_id: {}, file_handle: {:?}, lock_owner: {:?})", file_id.display(), file_handle, lock_owner @@ -369,7 +369,7 @@ impl FuseHandler for DefaultFuseHandler { _req: &RequestInfo, file_id: TId, file_handle: BorrowedFileHandle, - lock_owner: u64, + lock_owner: LockOwner, lock_info: LockInfo, ) -> FuseResult { match self.handling { @@ -377,7 +377,7 @@ impl FuseHandler for DefaultFuseHandler { kind, if cfg!(debug_assertions) { format!( - "getlk(file_id: {}, file_handle: {:?}, lock_owner: {}, lock_info: {:?})", + "getlk(file_id: {}, file_handle: {:?}, lock_owner: {:?}, lock_info: {:?})", file_id.display(), file_handle, lock_owner, @@ -388,7 +388,7 @@ impl FuseHandler for DefaultFuseHandler { }, )), HandlingMethod::Panic => panic!( - "[Not Implemented] getlk(file_id: {}, file_handle: {:?}, lock_owner: {}, lock_info: {:?})", + "[Not Implemented] getlk(file_id: {}, file_handle: {:?}, lock_owner: {:?}, lock_info: {:?})", file_id.display(), file_handle, lock_owner, @@ -432,7 +432,7 @@ impl FuseHandler for DefaultFuseHandler { _req: &RequestInfo, file_id: TId, file_handle: BorrowedFileHandle, - flags: IOCtlFlags, + flags: FUSEIoctlFlags, cmd: u32, in_data: Vec, out_size: u32, @@ -690,19 +690,21 @@ impl FuseHandler for DefaultFuseHandler { file_handle: BorrowedFileHandle, seek: SeekFrom, size: u32, - flags: FUSEOpenFlags, - lock_owner: Option, + read_flags: FUSEReadFlags, + flags: OpenFlags, + lock_owner: Option, ) -> FuseResult> { match self.handling { HandlingMethod::Error(kind) => Err(PosixError::new( kind, if cfg!(debug_assertions) { format!( - "read(file_id: {}, file_handle: {:?}, seek: {:?}, size: {}, flags: {:?}, lock_owner: {:?})", + "read(file_id: {}, file_handle: {:?}, seek: {:?}, size: {}, read_flags: {:?}, flags: {:?}, lock_owner: {:?})", file_id.display(), file_handle, seek, size, + read_flags, flags, lock_owner ) @@ -711,11 +713,12 @@ impl FuseHandler for DefaultFuseHandler { }, )), HandlingMethod::Panic => panic!( - "[Not Implemented] read(file_id: {}, file_handle: {:?}, seek: {:?}, size: {}, flags: {:?}, lock_owner: {:?})", + "[Not Implemented] read(file_id: {}, file_handle: {:?}, seek: {:?}, size: {}, read_flags: {:?}, flags: {:?}, lock_owner: {:?})", file_id.display(), file_handle, seek, size, + read_flags, flags, lock_owner ), @@ -798,7 +801,7 @@ impl FuseHandler for DefaultFuseHandler { file_id: TId, file_handle: OwnedFileHandle, flags: OpenFlags, - lock_owner: Option, + lock_owner: Option, flush: bool, ) -> FuseResult<()> { match self.handling { @@ -950,7 +953,7 @@ impl FuseHandler for DefaultFuseHandler { _req: &RequestInfo, file_id: TId, file_handle: BorrowedFileHandle, - lock_owner: u64, + lock_owner: LockOwner, lock_info: LockInfo, sleep: bool, ) -> FuseResult<()> { @@ -959,7 +962,7 @@ impl FuseHandler for DefaultFuseHandler { kind, if cfg!(debug_assertions) { format!( - "setlk(file_id: {}, file_handle: {:?}, lock_owner: {}, lock_info: {:?}, sleep: {})", + "setlk(file_id: {}, file_handle: {:?}, lock_owner: {:?}, lock_info: {:?}, sleep: {})", file_id.display(), file_handle, lock_owner, @@ -971,7 +974,7 @@ impl FuseHandler for DefaultFuseHandler { }, )), HandlingMethod::Panic => panic!( - "[Not Implemented] setlk(file_id: {}, file_handle: {:?}, lock_owner: {}, lock_info: {:?}, sleep: {})", + "[Not Implemented] setlk(file_id: {}, file_handle: {:?}, lock_owner: {:?}, lock_info: {:?}, sleep: {})", file_id.display(), file_handle, lock_owner, @@ -1080,7 +1083,7 @@ impl FuseHandler for DefaultFuseHandler { data: Vec, write_flags: FUSEWriteFlags, flags: OpenFlags, - lock_owner: Option, + lock_owner: Option, ) -> FuseResult { match self.handling { HandlingMethod::Error(kind) => Err(PosixError::new( diff --git a/src/templates/fd_handler_helper.rs b/src/templates/fd_handler_helper.rs index fd238da..05a8696 100644 --- a/src/templates/fd_handler_helper.rs +++ b/src/templates/fd_handler_helper.rs @@ -79,7 +79,7 @@ macro_rules! fd_handler_readonly_methods { _req: &RequestInfo, _file_id: TId, file_handle: BorrowedFileHandle, - _lock_owner: u64, + _lock_owner: LockOwner, ) -> FuseResult<()> { unix_fs::flush(file_handle.as_borrowed_fd()) } @@ -111,8 +111,9 @@ macro_rules! fd_handler_readonly_methods { file_handle: BorrowedFileHandle, seek: SeekFrom, size: u32, - _flags: FUSEOpenFlags, - _lock_owner: Option, + _read_flags: FUSEReadFlags, + _flags: OpenFlags, + _lock_owner: Option, ) -> FuseResult> { unix_fs::read(file_handle.as_borrowed_fd(), seek, size as usize) } @@ -123,7 +124,7 @@ macro_rules! fd_handler_readonly_methods { _file_id: TId, file_handle: OwnedFileHandle, _flags: OpenFlags, - _lock_owner: Option, + _lock_owner: Option, _flush: bool, ) -> FuseResult<()> { unix_fs::release(file_handle.into_owned_fd()) @@ -143,7 +144,7 @@ macro_rules! fd_handler_readwrite_methods { file_handle_out: BorrowedFileHandle, offset_out: i64, len: u64, - _flags: u32, + _flags: CopyFileRangeFlags, ) -> FuseResult { unix_fs::copy_file_range( file_handle_in.as_borrowed_fd(), @@ -175,7 +176,7 @@ macro_rules! fd_handler_readwrite_methods { data: Vec, _write_flags: FUSEWriteFlags, _flags: OpenFlags, - _lock_owner: Option, + _lock_owner: Option, ) -> FuseResult { unix_fs::write(file_handle.as_borrowed_fd(), seek, &data).map(|res| res as u32) } diff --git a/src/types.rs b/src/types.rs index e1502b1..6bc1d76 100644 --- a/src/types.rs +++ b/src/types.rs @@ -22,9 +22,9 @@ pub mod errors; pub mod file_handle; mod file_id_type; pub mod flags; -mod inode; pub mod helpers; - -pub use self::{arguments::*, errors::*, file_handle::*, file_id_type::*, flags::*, inode::*, helpers::*}; - -pub use fuser::{FileType as FileKind, KernelConfig, TimeOrNow}; +mod inode; +pub use self::{ + arguments::*, errors::*, file_handle::*, file_id_type::*, flags::*, helpers::*, inode::*, +}; +pub use fuser::{FileType as FileKind, Generation, KernelConfig, LockOwner, TimeOrNow}; diff --git a/src/types/arguments.rs b/src/types/arguments.rs index 8357c03..3a864fb 100644 --- a/src/types/arguments.rs +++ b/src/types/arguments.rs @@ -17,12 +17,13 @@ use std::time::{Duration, SystemTime}; -use fuser::FileAttr as FuseFileAttr; +use fuser::{FileAttr as FuseFileAttr, Generation, INodeNo, RequestId}; use fuser::{FileType, Request, TimeOrNow}; use libc::mode_t; +use crate::types::LockType; + use super::BorrowedFileHandle; -use super::LockType; pub use std::io::SeekFrom; @@ -161,13 +162,14 @@ impl StatFs { /// - `pid`: Process ID of the process that initiated the request #[derive(Debug, Clone)] pub struct RequestInfo { - pub id: u64, + pub id: RequestId, pub uid: u32, pub gid: u32, pub pid: u32, } -impl<'a> From<&Request<'a>> for RequestInfo { - fn from(req: &Request<'a>) -> Self { + +impl From<&Request> for RequestInfo { + fn from(req: &Request) -> Self { Self { id: req.unique(), uid: req.uid(), @@ -215,12 +217,15 @@ pub struct FileAttribute { /// - Must be non-zero (FUSE treats zero as an error) /// - Should be unique over the file system's lifetime if exported over NFS /// - Should be a new, previously unused number if an inode is reused after deletion - pub generation: Option, + pub generation: Option, } /// `FuseFileAttr`, `Option`, `Option` impl FileAttribute { - pub(crate) fn to_fuse(self, ino: u64) -> (FuseFileAttr, Option, Option) { + pub(crate) fn to_fuse( + self, + ino: INodeNo, + ) -> (FuseFileAttr, Option, Option) { ( FuseFileAttr { ino, diff --git a/src/types/errors.rs b/src/types/errors.rs index 897a870..5b7e531 100644 --- a/src/types/errors.rs +++ b/src/types/errors.rs @@ -16,6 +16,8 @@ //! - [`ErrorKind::to_error`]: Converts an ErrorKind to a PosixError with a custom message. //! +use fuser::Errno; + use crate::unix_fs::get_errno; use std::any::Any; @@ -90,6 +92,12 @@ where } } +impl From for Errno { + fn from(error: PosixError) -> Self { + Errno::from_i32(error.raw_error()) + } +} + impl Debug for PosixError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PosixError") diff --git a/src/types/file_handle.rs b/src/types/file_handle.rs index f4f7c2c..0a461b0 100644 --- a/src/types/file_handle.rs +++ b/src/types/file_handle.rs @@ -58,6 +58,18 @@ impl OwnedFileHandle { Self(handle) } + /// Creates an OwnedFileHandle from a [`fuser::FileHandle`]. + /// + /// Unsafe because it assumes the provided value is a valid, open file handle. + pub unsafe fn from_fuser_file_handle(handle: fuser::FileHandle) -> Self { + Self(handle.0) + } + + /// Converts the OwnedFileHandle into a [`fuser::FileHandle`]. + pub fn as_fuser_file_handle(&self) -> fuser::FileHandle { + fuser::FileHandle(self.0) + } + /// Borrows the file handle, creating a BorrowedFileHandle with a lifetime tied to self. pub fn borrow(&self) -> BorrowedFileHandle<'_> { BorrowedFileHandle(self.0, PhantomData) @@ -115,6 +127,18 @@ impl<'a> BorrowedFileHandle<'a> { Self(handle, PhantomData) } + /// Creates a BorrowedFileHandle from a [`fuser::FileHandle`]. + /// + /// Unsafe because it assumes the provided value is a valid, open file handle. + pub unsafe fn from_fuser_file_handle(handle: fuser::FileHandle) -> Self { + Self(handle.0, PhantomData) + } + + /// Converts the BorrowedFileHandle into a [`fuser::FileHandle`]. + pub fn as_fuser_file_handle(&self) -> fuser::FileHandle { + fuser::FileHandle(self.0) + } + /// Converts the BorrowedFileHandle into a BorrowedFd. /// /// Note: This method performs an unchecked cast from `u64` to `i32`, which may lead to undefined behavior if the file handle value doesn't fit within an `i32`. diff --git a/src/types/flags.rs b/src/types/flags.rs index 2a2b846..bc31985 100644 --- a/src/types/flags.rs +++ b/src/types/flags.rs @@ -23,6 +23,37 @@ bitflags! { } } +impl From for AccessMask { + fn from(flags: fuser::AccessFlags) -> Self { + AccessMask::from_bits_retain(flags.bits()) + } +} + +impl From for fuser::AccessFlags { + fn from(flags: AccessMask) -> Self { + fuser::AccessFlags::from_bits_retain(flags.bits()) + } +} + +bitflags! { + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] + pub struct CopyFileRangeFlags: u64 { + const _ = !0; + } +} + +impl From for CopyFileRangeFlags { + fn from(flags: fuser::CopyFileRangeFlags) -> Self { + CopyFileRangeFlags::from_bits_retain(flags.bits()) + } +} + +impl From for fuser::CopyFileRangeFlags { + fn from(flags: CopyFileRangeFlags) -> Self { + fuser::CopyFileRangeFlags::from_bits_retain(flags.bits()) + } +} + bitflags! { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] /// Flags used in fallocate calls. @@ -98,6 +129,18 @@ bitflags! { } } +impl From for FUSEOpenResponseFlags { + fn from(flags: fuser::FopenFlags) -> Self { + FUSEOpenResponseFlags::from_bits_retain(flags.bits()) + } +} + +impl From for fuser::FopenFlags { + fn from(flags: FUSEOpenResponseFlags) -> Self { + fuser::FopenFlags::from_bits_retain(flags.bits()) + } +} + bitflags! { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSEIoctlFlags: u32 { @@ -111,6 +154,18 @@ bitflags! { } } +impl From for FUSEIoctlFlags { + fn from(flags: fuser::IoctlFlags) -> Self { + FUSEIoctlFlags::from_bits_retain(flags.bits()) + } +} + +impl From for fuser::IoctlFlags { + fn from(flags: FUSEIoctlFlags) -> Self { + fuser::IoctlFlags::from_bits_retain(flags.bits()) + } +} + bitflags! { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSEReadFlags: i32 { @@ -119,6 +174,18 @@ bitflags! { } } +impl From for FUSEReadFlags { + fn from(flags: fuser::ReadFlags) -> Self { + FUSEReadFlags::from_bits_retain(flags.bits() as i32) + } +} + +impl From for fuser::ReadFlags { + fn from(flags: FUSEReadFlags) -> Self { + fuser::ReadFlags::from_bits_retain(flags.bits() as u32) + } +} + bitflags! { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct FUSEReleaseFlags: i32 { @@ -154,11 +221,15 @@ bitflags! { } } -bitflags! { - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] - pub struct IOCtlFlags: u32 { - // Placeholder for future flags - const _ = !0; +impl From for FUSEWriteFlags { + fn from(flags: fuser::WriteFlags) -> Self { + FUSEWriteFlags::from_bits_retain(flags.bits()) + } +} + +impl From for fuser::WriteFlags { + fn from(flags: FUSEWriteFlags) -> Self { + fuser::WriteFlags::from_bits_retain(flags.bits()) } } @@ -219,6 +290,18 @@ bitflags! { } } +impl From for OpenFlags { + fn from(flags: fuser::OpenFlags) -> Self { + OpenFlags::from_bits_retain(flags.0) + } +} + +impl From for fuser::OpenFlags { + fn from(flags: OpenFlags) -> Self { + fuser::OpenFlags(flags.bits()) + } +} + bitflags! { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] /// Flags used in rename operations. @@ -232,3 +315,15 @@ bitflags! { const _ = !0; } } + +impl From for RenameFlags { + fn from(flags: fuser::RenameFlags) -> Self { + RenameFlags::from_bits_retain(flags.bits()) + } +} + +impl From for fuser::RenameFlags { + fn from(flags: RenameFlags) -> Self { + fuser::RenameFlags::from_bits_retain(flags.bits()) + } +} \ No newline at end of file diff --git a/src/types/inode.rs b/src/types/inode.rs index b0638a1..b2bdfcf 100644 --- a/src/types/inode.rs +++ b/src/types/inode.rs @@ -1,10 +1,10 @@ //! Inode number in a FUSE (Filesystem in Userspace) filesystem. - use crate::core::ROOT_INO; +use fuser::INodeNo; /// Represents the mountpoint folder in a FuseFilesystem /// Its value is 1 and should not be modified. -pub const ROOT_INODE: Inode = Inode::from(ROOT_INO); +pub const ROOT_INODE: Inode = Inode::new(ROOT_INO); /// Represents an inode number in a FUSE (Filesystem in Userspace) filesystem. /// @@ -32,22 +32,30 @@ pub const ROOT_INODE: Inode = Inode::from(ROOT_INO); /// using `PathBuf` as `FileIdType`. This alternative approach allows for file /// identification based on paths rather than inode numbers. #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[repr(transparent)] pub struct Inode(u64); impl Inode { /// Allow const creation of Inode. - pub const fn from(value: u64) -> Self { + pub const fn new(value: u64) -> Self { Inode(value) } /// Convenience for creating a new Inode pub fn add_one(&self) -> Self { - Inode::from(u64::from(self.clone()) + 1) + Inode::new(u64::from(self.clone()) + 1) + } +} + +impl From for INodeNo { + /// Converts an Inode into a raw [`fuser::INodeNo`]. + fn from(value: Inode) -> Self { + INodeNo(value.0) } } impl From for u64 { - // Converts a u64 into an Inode. + /// Converts a u64 into an Inode. /// /// This allows for easy creation of Inode instances from raw inode numbers. fn from(value: Inode) -> Self { @@ -55,6 +63,13 @@ impl From for u64 { } } +impl From for Inode { + /// Converts a raw [`fuser::INodeNo`] into an Inode. + fn from(value: INodeNo) -> Self { + Inode(value.0) + } +} + impl From for Inode { /// Converts a number into an Inode. /// From 9b22171ac39481f673d087ecac861efb08c3f4b2 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Thu, 22 Jan 2026 12:13:49 +0700 Subject: [PATCH 22/25] example: example fixes for new bleeding edge API --- examples/ftp_fs/Cargo.toml | 6 ++- examples/ftp_fs/src/filesystem.rs | 5 ++- examples/hello_fs/Cargo.toml | 6 ++- examples/hello_fs/src/main.rs | 7 ++-- examples/in_memory_fs/Cargo.toml | 6 ++- examples/in_memory_fs/src/filesystem.rs | 12 ++++-- examples/in_memory_fs/src/main.rs | 49 ++++++++----------------- examples/passthrough_fs/Cargo.toml | 6 ++- examples/random_fs/Cargo.toml | 4 ++ examples/random_fs/src/main.rs | 10 +++-- examples/zip_fs/Cargo.toml | 4 ++ examples/zip_fs/src/filesystem.rs | 5 ++- 12 files changed, 68 insertions(+), 52 deletions(-) diff --git a/examples/ftp_fs/Cargo.toml b/examples/ftp_fs/Cargo.toml index 6364e06..3d578ba 100644 --- a/examples/ftp_fs/Cargo.toml +++ b/examples/ftp_fs/Cargo.toml @@ -21,4 +21,8 @@ tempfile = "3.2" libunftp = "0.20" unftp-sbe-fs = "0.2" async-trait = "0.1.68" -tokio = { version = "1.42", features = ["full"] } \ No newline at end of file +tokio = { version = "1.42", features = ["full"] } + +[patch.crates-io] +# Bleeding edge patch for FUSE create() passthrough, will be removed when patch is merged +fuser = { git = "https://github.com/khanhtranngoccva/fuser", branch = "passthrough" } \ No newline at end of file diff --git a/examples/ftp_fs/src/filesystem.rs b/examples/ftp_fs/src/filesystem.rs index eb357fb..078f993 100644 --- a/examples/ftp_fs/src/filesystem.rs +++ b/examples/ftp_fs/src/filesystem.rs @@ -83,8 +83,9 @@ impl FuseHandler for FtpFs { _file_handle: BorrowedFileHandle, offset: SeekFrom, size: u32, - _flags: FUSEOpenFlags, - _lock_owner: Option, + _read_flags: FUSEReadFlags, + _flags: OpenFlags, + _lock_owner: Option, ) -> FuseResult> { self.with_ftp(|ftp| { let mut cursor = ftp.retr_as_buffer(file_id.to_str().unwrap())?; diff --git a/examples/hello_fs/Cargo.toml b/examples/hello_fs/Cargo.toml index d05ff5c..63359bb 100644 --- a/examples/hello_fs/Cargo.toml +++ b/examples/hello_fs/Cargo.toml @@ -9,4 +9,8 @@ logging = ["dep:env_logger", "dep:log"] [dependencies] easy_fuser = { path = "../..", features = ["serial"] } log = { version = "0.4", optional = true } -env_logger = { version = "0.11", optional = true } \ No newline at end of file +env_logger = { version = "0.11", optional = true } + +[patch.crates-io] +# Bleeding edge patch for FUSE create() passthrough, will be removed when patch is merged +fuser = { git = "https://github.com/khanhtranngoccva/fuser", branch = "passthrough" } \ No newline at end of file diff --git a/examples/hello_fs/src/main.rs b/examples/hello_fs/src/main.rs index 2e8d31a..a38ae37 100644 --- a/examples/hello_fs/src/main.rs +++ b/examples/hello_fs/src/main.rs @@ -33,7 +33,7 @@ const HELLO_DIR_ATTR: (Inode, FileAttribute) = ( const HELLO_TXT_CONTENT: &str = "Hello World!\n"; const HELLO_TXT_ATTR: (Inode, FileAttribute) = ( - Inode::from(2), + Inode::new(2), FileAttribute { size: 13, blocks: 1, @@ -110,8 +110,9 @@ impl FuseHandler for HelloFS { _file_handle: BorrowedFileHandle, seek: SeekFrom, size: u32, - _flags: FUSEOpenFlags, - _lock_owner: Option, + _read_flags: FUSEReadFlags, + _flags: OpenFlags, + _lock_owner: Option, ) -> FuseResult> { if file_id == HELLO_TXT_ATTR.0 { let offset = match seek { diff --git a/examples/in_memory_fs/Cargo.toml b/examples/in_memory_fs/Cargo.toml index 8c23f42..7f186c7 100644 --- a/examples/in_memory_fs/Cargo.toml +++ b/examples/in_memory_fs/Cargo.toml @@ -11,4 +11,8 @@ logging = ["dep:env_logger", "dep:log"] [dependencies] easy_fuser = { path = "../..", features = ["parallel"] } log = { version = "0.4", optional = true } -env_logger = { version = "0.11", optional = true } \ No newline at end of file +env_logger = { version = "0.11", optional = true } + +[patch.crates-io] +# Bleeding edge patch for FUSE create() passthrough, will be removed when patch is merged +fuser = { git = "https://github.com/khanhtranngoccva/fuser", branch = "passthrough" } \ No newline at end of file diff --git a/examples/in_memory_fs/src/filesystem.rs b/examples/in_memory_fs/src/filesystem.rs index 7ebdb03..d56f565 100644 --- a/examples/in_memory_fs/src/filesystem.rs +++ b/examples/in_memory_fs/src/filesystem.rs @@ -155,11 +155,13 @@ impl FuseHandler for InMemoryFS { mode: u32, _umask: u32, _flags: OpenFlags, + _helper: CreateHelper<'_>, ) -> Result< ( OwnedFileHandle, (Inode, FileAttribute), FUSEOpenResponseFlags, + Option, ), PosixError, > { @@ -204,6 +206,7 @@ impl FuseHandler for InMemoryFS { unsafe { OwnedFileHandle::from_raw(0) }, (new_inode.clone(), attr), FUSEOpenResponseFlags::empty(), + None, )) } else { Err(ErrorKind::FileNotFound.to_error("")) @@ -260,7 +263,7 @@ impl FuseHandler for InMemoryFS { _req: &RequestInfo, _file_id: Inode, _file_handle: BorrowedFileHandle, - _lock_owner: u64, + _lock_owner: LockOwner, ) -> FuseResult<()> { Ok(()) } @@ -363,8 +366,9 @@ impl FuseHandler for InMemoryFS { _fh: BorrowedFileHandle, offset: SeekFrom, size: u32, - _flags: FUSEOpenFlags, - _lock_owner: Option, + _read_flags: FUSEReadFlags, + _flags: OpenFlags, + _lock_owner: Option, ) -> FuseResult> { self.access(req, ino.clone(), AccessMask::CAN_READ)?; let fs = self.fs.lock().unwrap(); @@ -579,7 +583,7 @@ impl FuseHandler for InMemoryFS { data: Vec, _write_flags: FUSEWriteFlags, _flags: OpenFlags, - _lock_owner: Option, + _lock_owner: Option, ) -> FuseResult { self.access(req, ino.clone(), AccessMask::CAN_WRITE)?; let mut fs = self.fs.lock().unwrap(); diff --git a/examples/in_memory_fs/src/main.rs b/examples/in_memory_fs/src/main.rs index 0724665..37f2e2d 100644 --- a/examples/in_memory_fs/src/main.rs +++ b/examples/in_memory_fs/src/main.rs @@ -1,7 +1,8 @@ #![doc = include_str!("../README.md")] use easy_fuser::prelude::*; -use std::ffi::OsStr; +use std::fs::File; +use std::io::Write; use std::path::Path; const README_CONTENT: &[u8] = include_bytes!("../README.md") as &[u8]; @@ -11,38 +12,8 @@ pub use filesystem::InMemoryFS; fn create_memory_fs() -> InMemoryFS { let memoryfs = InMemoryFS::new(); - #[cfg(feature = "readme")] - { - // An example of interacting directly with the filesystem - let request_info = RequestInfo { - id: 0, - uid: 0, - gid: 0, - pid: 0, - }; // dummy RequestInfo - let (fd, (inode, _), _) = memoryfs - .create( - &request_info, - ROOT_INODE, - OsStr::new("README.md"), - 0o755, - 0, - OpenFlags::empty(), - ) - .unwrap(); - let _ = memoryfs - .write( - &request_info, - inode, - fd.borrow(), - SeekFrom::Start(0), - README_CONTENT.to_vec(), - FUSEWriteFlags::empty(), - OpenFlags::empty(), - None, - ) - .unwrap(); - } + // NOTE: manual call example here is removed because the [`CreateHelper`] + // parameter is not supported memoryfs } @@ -66,5 +37,15 @@ fn main() { let memoryfs = create_memory_fs(); println!("Mounting filesystem..."); - easy_fuser::mount(memoryfs, Path::new(&mountpoint), &options, 1).unwrap(); + let session = easy_fuser::spawn_mount(memoryfs, Path::new(&mountpoint), &options, 1).unwrap(); + // Insert the readme here + #[cfg(feature = "readme")] + File::create(Path::new(&mountpoint).join("README.md")) + .unwrap() + .write_all(README_CONTENT) + .unwrap(); + let mut wait_string = String::new(); + println!("Press Enter to unmount..."); + std::io::stdin().read_line(&mut wait_string).unwrap(); + session.unmount_and_join().unwrap(); } diff --git a/examples/passthrough_fs/Cargo.toml b/examples/passthrough_fs/Cargo.toml index 7dbd525..f5d1e99 100644 --- a/examples/passthrough_fs/Cargo.toml +++ b/examples/passthrough_fs/Cargo.toml @@ -7,4 +7,8 @@ description = "A FUSE passthrough filesystem example using easy_fuser" [dependencies] easy_fuser = { path = "../..", features = ["parallel"] } clap = { version = "4.5", features = ["derive"] } -ctrlc = "3.4" \ No newline at end of file +ctrlc = "3.4" + +[patch.crates-io] +# Bleeding edge patch for FUSE create() passthrough, will be removed when patch is merged +fuser = { git = "https://github.com/khanhtranngoccva/fuser", branch = "passthrough" } \ No newline at end of file diff --git a/examples/random_fs/Cargo.toml b/examples/random_fs/Cargo.toml index 5b3e8d5..2d6ee5d 100644 --- a/examples/random_fs/Cargo.toml +++ b/examples/random_fs/Cargo.toml @@ -6,3 +6,7 @@ edition = "2021" [dependencies] easy_fuser = { path = "../..", features = ["parallel"] } rand = "0.8.5" + +[patch.crates-io] +# Bleeding edge patch for FUSE create() passthrough, will be removed when patch is merged +fuser = { git = "https://github.com/khanhtranngoccva/fuser", branch = "passthrough" } \ No newline at end of file diff --git a/examples/random_fs/src/main.rs b/examples/random_fs/src/main.rs index 5c4c8c3..7323f4b 100644 --- a/examples/random_fs/src/main.rs +++ b/examples/random_fs/src/main.rs @@ -77,11 +77,13 @@ impl FuseHandler for RandomFS { _mode: u32, _umask: u32, _flags: OpenFlags, + _helper: CreateHelper<'_>, ) -> Result< ( OwnedFileHandle, (Inode, FileAttribute), FUSEOpenResponseFlags, + Option, ), PosixError, > { @@ -93,6 +95,7 @@ impl FuseHandler for RandomFS { unsafe { OwnedFileHandle::from_raw(0) }, (ino, attr), FUSEOpenResponseFlags::empty(), + None, )) } @@ -171,8 +174,9 @@ impl FuseHandler for RandomFS { _fh: BorrowedFileHandle, offset: SeekFrom, size: u32, - _flags: FUSEOpenFlags, - _lock_owner: Option, + _read_flags: FUSEReadFlags, + _flags: OpenFlags, + _lock_owner: Option, ) -> FuseResult> { let mut rng = rand::thread_rng(); let lines = rng.gen_range(0..81); @@ -238,7 +242,7 @@ impl FuseHandler for RandomFS { data: Vec, _write_flags: FUSEWriteFlags, _flags: OpenFlags, - _lock_owner: Option, + _lock_owner: Option, ) -> FuseResult { Ok(data.len() as u32) } diff --git a/examples/zip_fs/Cargo.toml b/examples/zip_fs/Cargo.toml index dbb265f..ea75ff1 100644 --- a/examples/zip_fs/Cargo.toml +++ b/examples/zip_fs/Cargo.toml @@ -12,3 +12,7 @@ ctrlc = "3.4" [dev-dependencies] tempfile = "3.2" + +[patch.crates-io] +# Bleeding edge patch for FUSE create() passthrough, will be removed when patch is merged +fuser = { git = "https://github.com/khanhtranngoccva/fuser", branch = "passthrough" } \ No newline at end of file diff --git a/examples/zip_fs/src/filesystem.rs b/examples/zip_fs/src/filesystem.rs index 6cdde27..130ab49 100644 --- a/examples/zip_fs/src/filesystem.rs +++ b/examples/zip_fs/src/filesystem.rs @@ -135,8 +135,9 @@ impl FuseHandler for ZipFs { _file_handle: BorrowedFileHandle, seek: SeekFrom, size: u32, - _flags: FUSEOpenFlags, - _lock_owner: Option, + _read_flags: FUSEReadFlags, + _flags: OpenFlags, + _lock_owner: Option, ) -> FuseResult> { let InodeInfo { parent: _, From a5b09dd588a5f1f7708965595921d4cdaeea175e Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Mon, 26 Jan 2026 12:48:03 +0700 Subject: [PATCH 23/25] doc: add kernel caveat to passthrough ID --- .vscode/settings.json | 2 +- src/types/helpers.rs | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 51eb545..1765111 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.cargo.features": ["serial", "passthrough"] + "rust-analyzer.cargo.features": ["parallel", "passthrough"] } diff --git a/src/types/helpers.rs b/src/types/helpers.rs index 4b2db7a..9609ff0 100644 --- a/src/types/helpers.rs +++ b/src/types/helpers.rs @@ -4,21 +4,29 @@ use fuser::{ReplyCreate, ReplyOpen}; #[cfg(feature = "passthrough")] use std::sync::{Arc, Weak}; +/// Helper object for the [`open`](crate::FuseHandler::open) method. pub struct OpenHelper<'a> { #[allow(dead_code)] reply_open: &'a ReplyOpen, } +/// Helper object for the [`create`](crate::FuseHandler::create) method. pub struct CreateHelper<'a> { #[allow(dead_code)] reply_create: &'a ReplyCreate, } impl<'a> CreateHelper<'a> { + /// Initializes a new [`CreateHelper`] object. pub(crate) fn new(reply_create: &'a ReplyCreate) -> Self { Self { reply_create } } + /// Registers a backing ID for FUSE passthrough operations and returns + /// a [`PassthroughBackingId`], which can be used as the fourth return value + /// of the [`create`](crate::FuseHandler::create) method. + /// + /// Check [`PassthroughBackingId`] for information on correct usage. #[cfg(feature = "passthrough")] pub fn open_backing( self, @@ -33,10 +41,16 @@ impl<'a> CreateHelper<'a> { } impl<'a> OpenHelper<'a> { + /// Initializes a new [`OpenHelper`] object. pub(crate) fn new(reply_open: &'a ReplyOpen) -> Self { Self { reply_open } } + /// Registers a backing ID for FUSE passthrough operations and returns + /// a [`PassthroughBackingId`], which can be used as the third return value + /// of the [`open`](crate::FuseHandler::open) method. + /// + /// Check [`PassthroughBackingId`] for information on correct usage. #[cfg(feature = "passthrough")] pub fn open_backing( self, @@ -50,6 +64,33 @@ impl<'a> OpenHelper<'a> { } } +/// Passthrough backing ID for FUSE passthrough operations. +/// +/// This structure is created by the [`OpenHelper::open_backing`] +/// and [`CreateHelper::open_backing`] methods, and allows the FUSE +/// kernel module to bypass the FUSE daemon to increase performance. +/// +/// This structure is a no-op if the `passthrough` feature is not +/// enabled. +/// +/// In the scope of a single file ID (e.g. [`Inode`](crate::types::inode::Inode), +/// [`PathBuf`](std::path::PathBuf), [`Vec`](std::ffi::OsString) or +/// [`HybridId`](crate::types::file_id_type::HybridId)), +/// - All active file handle objects ([`FileHandle`](crate::types::file_handle::OwnedFileHandle)) +/// must be opened in the same mode (passthrough or non-passthrough). +/// - If the file handles are opened in passthrough mode, they must share the same +/// [`PassthroughBackingId`] value. Identical [`PassthroughBackingId`] values can be +/// obtained by cloning an existing value. +/// - If either or both of these rules is not met, the FUSE kernel module will return +/// an [`EIO`](libc::EIO) error code that is very difficult to troubleshoot. It is best +/// to return [`EBUSY`](libc::EBUSY) when you detect this situation, which usually happens +/// when trying to open a file twice may result in two completely different file views. +/// +/// Backing IDs can be downgraded to a [`WeakPassthroughBackingId`] value, similar to +/// the underlying [`Arc`] type. This allows the passthrough ID references to be stored in +/// a file ID to passthrough ID hash table (required to fulfill these above two rules) +/// without interfering with the ability of the backing ID to be released when the last +/// file handle to an inode is closed. #[derive(Debug, Clone)] pub struct PassthroughBackingId { #[cfg(feature = "passthrough")] @@ -64,6 +105,9 @@ impl AsRef for PassthroughBackingId { } impl PassthroughBackingId { + /// Downgrades a [`PassthroughBackingId`] to a weak reference, which allows it + /// to be stored in a long-lived table without preventing the backing ID from being + /// released when the last file handle to an inode is closed. #[cfg(feature = "passthrough")] pub fn downgrade(&self) -> WeakPassthroughBackingId { WeakPassthroughBackingId { @@ -86,6 +130,7 @@ impl WeakPassthroughBackingId { } } + /// Upgrades a [`WeakPassthroughBackingId`] to a [`PassthroughBackingId`]. #[cfg(feature = "passthrough")] pub fn upgrade(&self) -> Option { self.backing_id From 6be8204552386394e9a9b0ed811c751c242d1633 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Mon, 26 Jan 2026 12:50:43 +0700 Subject: [PATCH 24/25] fix: minor doc and ide flag fixes --- .vscode/settings.json | 2 +- src/types/helpers.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1765111..51eb545 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.cargo.features": ["parallel", "passthrough"] + "rust-analyzer.cargo.features": ["serial", "passthrough"] } diff --git a/src/types/helpers.rs b/src/types/helpers.rs index 9609ff0..04213f7 100644 --- a/src/types/helpers.rs +++ b/src/types/helpers.rs @@ -76,7 +76,7 @@ impl<'a> OpenHelper<'a> { /// In the scope of a single file ID (e.g. [`Inode`](crate::types::inode::Inode), /// [`PathBuf`](std::path::PathBuf), [`Vec`](std::ffi::OsString) or /// [`HybridId`](crate::types::file_id_type::HybridId)), -/// - All active file handle objects ([`FileHandle`](crate::types::file_handle::OwnedFileHandle)) +/// - All active file handle objects ([`OwnedFileHandle`](crate::types::file_handle::OwnedFileHandle)) /// must be opened in the same mode (passthrough or non-passthrough). /// - If the file handles are opened in passthrough mode, they must share the same /// [`PassthroughBackingId`] value. Identical [`PassthroughBackingId`] values can be From 8ca3ccfc7a4fab83f99d27c4a83f9de262847619 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Mon, 26 Jan 2026 16:30:48 +0700 Subject: [PATCH 25/25] fix: open_backing should not require object ownership --- src/types/helpers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/helpers.rs b/src/types/helpers.rs index 04213f7..42ed997 100644 --- a/src/types/helpers.rs +++ b/src/types/helpers.rs @@ -29,7 +29,7 @@ impl<'a> CreateHelper<'a> { /// Check [`PassthroughBackingId`] for information on correct usage. #[cfg(feature = "passthrough")] pub fn open_backing( - self, + &self, fd: impl std::os::fd::AsFd, ) -> Result { self.reply_create @@ -53,7 +53,7 @@ impl<'a> OpenHelper<'a> { /// Check [`PassthroughBackingId`] for information on correct usage. #[cfg(feature = "passthrough")] pub fn open_backing( - self, + &self, fd: impl std::os::fd::AsFd, ) -> Result { self.reply_open