From dcc5dbe8944da76cd2a5ea0e662653dc97dbd736 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Mon, 9 Feb 2026 17:27:09 +0700 Subject: [PATCH 1/8] implement setup and lookup_root, add inode pruning to hybrid ID --- build.rs | 4 +- src/core/inode_mapping.rs | 126 +++++++++++++++++--- src/inode_multi_mapper.rs | 41 ++++++- src/lib.rs | 4 +- src/templates/default_fuse_handler.rs | 14 +++ src/templates/mirror_fs.rs | 5 + src/types/file_id_type.rs | 100 +++------------- templates.rs | 4 +- templates/fuse_driver.rs.j2 | 60 ++++++++-- templates/fuse_handler.rs.j2 | 31 ++++- templates/{mouting.rs.j2 => mounting.rs.j2} | 6 +- 11 files changed, 271 insertions(+), 124 deletions(-) rename templates/{mouting.rs.j2 => mounting.rs.j2} (98%) diff --git a/build.rs b/build.rs index 8f7bb56..67350c6 100644 --- a/build.rs +++ b/build.rs @@ -47,8 +47,8 @@ fn main() -> std::io::Result<()> { let content = FuseHandlerTemplate { mode }.render()?; fs::write(mode_dir.join("fuse_handler.rs"), content)?; - let content = MoutingTemplate { mode }.render()?; - fs::write(mode_dir.join("mouting.rs"), content)?; + let content = MountingTemplate { mode }.render()?; + fs::write(mode_dir.join("mounting.rs"), content)?; } Ok(()) diff --git a/src/core/inode_mapping.rs b/src/core/inode_mapping.rs index 35bd3bd..836a523 100644 --- a/src/core/inode_mapping.rs +++ b/src/core/inode_mapping.rs @@ -70,6 +70,7 @@ pub trait FileIdResolver: Send + Sync + 'static { id: ::_Id, increment: bool, ) -> u64; + fn lookup_root(&self, id: ::_Id) -> (); fn add_children( &self, parent: u64, @@ -98,6 +99,8 @@ impl FileIdResolver for InodeResolver { id.into() } + fn lookup_root(&self, _id: ::_Id) -> () {} + // Do nothing, user should provide its own inode fn add_children( &self, @@ -166,6 +169,8 @@ impl FileIdResolver for ComponentsResolver { ) } + fn lookup_root(&self, _id: ::_Id) -> () {} + fn add_children( &self, parent: u64, @@ -212,7 +217,10 @@ impl FileIdResolver for ComponentsResolver { } fn prune(&self, keep: &HashSet) { - self.mapper.write().expect("Failed to acquire write lock").prune(keep); + self.mapper + .write() + .expect("Failed to acquire write lock") + .prune(keep); } fn rename(&self, parent: u64, name: &OsStr, newparent: u64, newname: &OsStr) { @@ -262,6 +270,8 @@ impl FileIdResolver for PathResolver { self.resolver.lookup(parent, child, id, increment) } + fn lookup_root(&self, _id: ::_Id) -> () {} + fn add_children( &self, parent: u64, @@ -307,7 +317,7 @@ where } fn resolve_id(&self, ino: u64) -> Self::ResolvedType { - HybridId::new(Inode::from(ino), self.mapper.clone()) + HybridId::new(Inode::from(ino)) } fn lookup( @@ -355,6 +365,13 @@ where ) } + fn lookup_root(&self, id: ::_Id) -> () { + self.mapper + .write() + .expect("Failed to acquire write lock") + .set_root_inode_backing_id(id); + } + fn add_children( &self, parent: u64, @@ -405,7 +422,14 @@ where } fn prune(&self, _keep: &HashSet) { - // TODO + let resolver_keep: HashSet = _keep + .iter() + .map(|id| id.inode().clone()) + .collect(); + self.mapper + .write() + .expect("Failed to acquire write lock") + .prune(&resolver_keep); } fn rename(&self, parent: u64, name: &OsStr, newparent: u64, newname: &OsStr) { @@ -424,6 +448,70 @@ where } } +/// Specialized methods for hybrid resolver to deal with file paths and backing IDs. +impl HybridResolver +where + BackingId: Clone + Eq + Hash + Send + Sync + std::fmt::Debug + 'static, +{ + /// Retrieves the first path to the hybrid ID's inode. This is a convenience method + /// for users who do not need a more exhaustive list of paths that might occupy an inode. + /// + /// # 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. + pub fn first_path(&self, id: &HybridId) -> Option { + let mapper = self + .mapper + .read() + .expect("failed to acquire read lock on mapper"); + let path = mapper.resolve(id.inode()).map(|components| { + components + .iter() + .map(|component| component.name.as_ref()) + .rev() + .collect::() + }); + path + } + + /// Retrieves all paths to the hybrid ID's inode, up to a given limit. + /// + /// Inodes that are not found will result in an empty vector. + pub fn all_paths(&self, id: &HybridId, limit: Option) -> Vec { + let mapper = self + .mapper + .read() + .expect("failed to acquire read lock on mapper"); + let resolved = mapper.resolve_all(id.inode(), limit); + resolved + .iter() + .map(|components| { + components + .iter() + .rev() + .map(|component| component.name.as_ref()) + .collect::() + }) + .collect() + } + + /// Retrieves the backing ID of the inode. + /// + /// This is useful for comparing to the backing ID of the actual underlying + /// file that a filesystem handler opened, which mitigates the risk of a race + /// condition, in which case another backing path could be tried, or an error + /// could be returned. The stable backing ID can also be used as key for the + /// inode's data as defined by the user. + pub fn backing_id(&self, id: &HybridId) -> Option { + let mapper = self + .mapper + .read() + .expect("failed to acquire read lock on mapper"); + mapper.get_backing_id(id.inode()).cloned() + } +} + #[cfg(test)] mod tests { use super::*; @@ -467,11 +555,11 @@ mod tests { // Test prune let keep = HashSet::new(); resolver.prune(&keep); - + // child_ino should be gone now because refcount was 0 (decremented by earlier forget) and we pruned it. // We can verify it's gone by trying to resolve it and expecting panic (as per other test) or just by knowing prune works. // But calling forget again is definitely wrong if it's gone. - + // If we want to test that prune actually removed it, we should check existence. // But since we can't easily check existence without internal access, we rely on the fact that subsequent operations might fail or the other test. } @@ -565,16 +653,19 @@ 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(), Some(PathBuf::from(""))); + assert_eq!(resolver.first_path(&root_id), Some(PathBuf::from(""))); // 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(), Some(PathBuf::from("dir1"))); + assert_eq!(resolver.first_path(&dir1_id), Some(PathBuf::from("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(), Some(PathBuf::from("dir1/dir2"))); + assert_eq!( + resolver.first_path(&dir2_id), + Some(PathBuf::from("dir1/dir2")) + ); // Test add_children let grandchildren = vec![ @@ -584,9 +675,9 @@ mod tests { 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); + let child_id = resolver.resolve_id(*ino); assert_eq!( - child_path.first_path(), + resolver.first_path(&child_id), Some(PathBuf::from("dir1/dir2").join(name)) ); } @@ -603,7 +694,7 @@ mod tests { ); let renamed_grandchild_path = resolver.resolve_id(added_grandchildren[1].1); assert_eq!( - renamed_grandchild_path.first_path(), + resolver.first_path(&renamed_grandchild_path), Some(PathBuf::from("dir1/dir2/grandchild2_renamed")) ); @@ -617,7 +708,7 @@ mod tests { ); let renamed_grandchild_path = resolver.resolve_id(added_grandchildren[1].1); assert_eq!( - renamed_grandchild_path.first_path(), + resolver.first_path(&renamed_grandchild_path), Some(PathBuf::from("dir3/grandchild2_renamed")) ); @@ -627,14 +718,17 @@ mod tests { assert_ne!(non_existent_ino, 0); let non_existent_path = resolver.resolve_id(non_existent_ino); assert_eq!( - non_existent_path.first_path(), + resolver.first_path(&non_existent_path), Some(PathBuf::from("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(), Some(PathBuf::from("hard_link"))); + assert_eq!( + resolver.first_path(&hard_link_id), + Some(PathBuf::from("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); @@ -644,7 +738,7 @@ mod tests { ); resolver.lookup(dir1_ino, OsStr::new("hard_linked_2"), Some(7), true); - let paths = hard_link_id_2.all_paths(Some(100)); + let paths = resolver.all_paths(&hard_link_id_2, Some(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"))); @@ -658,7 +752,7 @@ mod tests { ); // Test path resolution after overriding a location with a new backing ID - let paths = hard_link_id_2.all_paths(Some(100)); + let paths = resolver.all_paths(&hard_link_id_2, Some(100)); assert!( !paths.contains(&PathBuf::from("dir1/dir2/hard_linked")), "the path list should no longer contain the overridden location" diff --git a/src/inode_multi_mapper.rs b/src/inode_multi_mapper.rs index ba97340..c0bb7c9 100644 --- a/src/inode_multi_mapper.rs +++ b/src/inode_multi_mapper.rs @@ -1,3 +1,4 @@ +use super::inode_mapper::HasLookupCount; use crate::types::{Inode, ROOT_INODE}; use bimap::BiHashMap; use std::{ @@ -7,7 +8,7 @@ use std::{ fmt::Debug, hash::{Hash, Hasher}, ops::Deref, - sync::Arc, + sync::{Arc, atomic::Ordering}, }; #[derive(Debug)] @@ -276,6 +277,16 @@ where } } + /// Overrides the backing ID of the root inode. + pub fn set_root_inode_backing_id(&mut self, backing_id: Option) -> () { + if let Some(id) = backing_id { + self.data.backing.insert(self.root_inode.clone(), id); + } else { + self.data.backing.remove_by_left(&self.root_inode); + } + } + + /// Retrieves the root inode. pub fn get_root_inode(&self) -> Inode { self.root_inode.clone() } @@ -955,6 +966,34 @@ where } } +impl InodeMultiMapper +where + BackingId: Clone + Eq + Hash + Debug, + Data: HasLookupCount + Send + Sync + 'static, +{ + pub fn prune(&mut self, keep: &HashSet) { + let mut to_remove = Vec::new(); + + for (inode, value) in &self.data.inodes { + if *inode == ROOT_INODE { + continue; + } + if value.data.lookup_count().load(Ordering::SeqCst) == 0 { + // Check if inode is in keep list + if let Some(_resolve_info) = self.resolve(inode) { + if !keep.contains(&inode) { + to_remove.push(inode.clone()); + } + } + } + } + + for inode in to_remove { + self.remove(&inode); + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 2d5510b..83db96b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,6 @@ pub mod prelude { #[cfg(feature = "serial")] -include!(concat!(env!("OUT_DIR"), "/serial/mouting.rs")); +include!(concat!(env!("OUT_DIR"), "/serial/mounting.rs")); #[cfg(not(feature = "serial"))] -include!(concat!(env!("OUT_DIR"), "/parallel/mouting.rs")); \ No newline at end of file +include!(concat!(env!("OUT_DIR"), "/parallel/mounting.rs")); \ No newline at end of file diff --git a/src/templates/default_fuse_handler.rs b/src/templates/default_fuse_handler.rs index a0fc377..bf33887 100644 --- a/src/templates/default_fuse_handler.rs +++ b/src/templates/default_fuse_handler.rs @@ -1,6 +1,7 @@ use std::{ ffi::{OsStr, OsString}, path::Path, + sync::Arc, time::Duration, }; @@ -94,6 +95,10 @@ impl FuseHandler for DefaultFuseHandler { Duration::from_secs(1) } + fn setup(&self, _resolver: Arc) -> FuseResult<()> { + Ok(()) + } + fn init(&self, _req: &RequestInfo, _config: &mut KernelConfig) -> FuseResult<()> { Ok(()) } @@ -531,6 +536,15 @@ impl FuseHandler for DefaultFuseHandler { } } + fn lookup_root(&self) -> FuseResult { + match self.handling { + HandlingMethod::Error(kind) => { + Err(PosixError::new(kind, "[Not Implemented] lookup_root")) + } + HandlingMethod::Panic => panic!("[Not Implemented] lookup_root"), + } + } + fn lseek( &self, _req: &RequestInfo, diff --git a/src/templates/mirror_fs.rs b/src/templates/mirror_fs.rs index 74a1c02..2a66e12 100644 --- a/src/templates/mirror_fs.rs +++ b/src/templates/mirror_fs.rs @@ -127,6 +127,11 @@ macro_rules! mirror_fs_readonly_methods { unix_fs::lookup(&file_path) } + fn lookup_root(&self) -> FuseResult { + let file_path = self.source_path.join(Path::new("")); + unix_fs::lookup(&file_path) + } + fn open( &self, _req: &RequestInfo, diff --git a/src/types/file_id_type.rs b/src/types/file_id_type.rs index faab8d1..83f408c 100644 --- a/src/types/file_id_type.rs +++ b/src/types/file_id_type.rs @@ -11,13 +11,13 @@ use std::{ ffi::OsString, fmt::{Debug, Display}, hash::Hasher, + marker::PhantomData, path::{Path, PathBuf}, - sync::{Arc, RwLock, atomic::AtomicU64}, }; use super::arguments::FileAttribute; use super::inode::*; -use crate::{core::InodeResolvable, inode_multi_mapper::InodeMultiMapper}; +use crate::core::InodeResolvable; use fuser::FileType as FileKind; /// Represents the type used to identify files in the file system. @@ -56,9 +56,13 @@ use fuser::FileType as FileKind; /// comparison method. /// - Root: Represented by the constant ROOT_INODE with a value of 1 and an empty string. /// - Usage: (see https://github.com/Alogani/easy_fuser/pull/77#issuecomment-3830951142) -/// - If two paths represents hardlinks, the user will return the same inode to the fuse filesystem -/// - The user can use the hardlinks of the current filesystem by using `libc::fstat(...).f_fsid` (Persistent) or libc::fstatfs(...).f_dev` (Ephemeral) -/// - When a Fuse operation provides an inode, the user can use `BackingId::all_paths()` to retrieve all the paths associated to that inode +/// - During setup(), the user must store the `HybridResolver` object supplied by this method. +/// - The user can use the hardlinks of the current filesystem by using `libc::fstat(...).f_fsid` (Persistent) +/// or libc::fstatfs(...).f_dev` (Ephemeral) at opportunities to provide a stable file ID (there are currently +/// 7 methods: `mkdir`, `mknod`, `create`, `lookup`, `symlink`, `link`, `rename`, and `lookup_root`) +/// - If two paths represents hardlinks, the user will return the same inode to the fuse filesystem +/// - When a Fuse operation provides an inode, the user can use `HybridResolver::all_paths()` to retrieve +/// all the paths associated to that inode pub trait FileIdType: 'static + Debug + Clone + PartialEq + Eq + std::hash::Hash + InodeResolvable { @@ -176,7 +180,7 @@ where BackingId: Clone + Eq + std::hash::Hash + Debug, { inode: Inode, - mapper: Arc>>, + _static: PhantomData, } impl HybridId @@ -184,69 +188,17 @@ where BackingId: Clone + Eq + std::hash::Hash + Debug, { /// Creates a new hybrid ID. - pub fn new(inode: Inode, mapper: Arc>>) -> Self { - Self { inode, mapper } - } - - /// Retrieves the first path to the inode. - /// - /// # 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. - pub fn first_path(&self) -> Option { - let mapper = self - .mapper - .read() - .expect("failed to acquire read lock on mapper"); - let path = mapper.resolve(&self.inode).map(|components| { - components - .iter() - .map(|component| component.name.as_ref()) - .rev() - .collect::() - }); - path + pub fn new(inode: Inode) -> Self { + Self { + inode, + _static: PhantomData, + } } /// Retrieves the inode of the hybrid ID. pub fn inode(&self) -> &Inode { &self.inode } - - /// Retrieves all paths to the inode, up to a given limit. - pub fn all_paths(&self, limit: Option) -> 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() - } - - /// Retrieves the backing ID of the inode. - /// - /// This is useful for comparing to the backing ID of the actual underlying - /// file that a filesystem handler opened, which mitigates the risk of a race - /// condition, in which case another backing path could be tried, or an error - /// could be returned. - #[deprecated = "Unstable: need sanity check to resist TOCTOU bugs. _marked temporarly as deprecated_"] - pub fn backing_id(&self) -> Option { - let mapper = self - .mapper - .read() - .expect("failed to acquire read lock on mapper"); - mapper.get_backing_id(&self.inode).cloned() - } } impl PartialEq for HybridId @@ -254,7 +206,7 @@ where BackingId: Clone + Eq + std::hash::Hash + Debug, { fn eq(&self, other: &Self) -> bool { - self.inode == other.inode && Arc::ptr_eq(&self.mapper, &other.mapper) + self.inode == other.inode } } @@ -266,7 +218,6 @@ where { fn hash(&self, state: &mut H) { self.inode.hash(state); - Arc::as_ptr(&self.mapper).hash(state); } } @@ -275,15 +226,7 @@ where BackingId: Clone + Eq + std::hash::Hash + Debug, { 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(), - } - ) + write!(f, "HybridId({:?})", self.inode) } } @@ -296,14 +239,7 @@ where type MinimalMetadata = (Option, FileKind); fn display(&self) -> impl Display { - format!( - "HybridId({:?}, {})", - self.inode, - match &self.first_path() { - Some(path) => path.display().to_string(), - None => "".to_string(), - } - ) + format!("HybridId({:?})", self.inode) } fn is_filesystem_root(&self) -> bool { diff --git a/templates.rs b/templates.rs index 53d95c5..6fb340a 100644 --- a/templates.rs +++ b/templates.rs @@ -13,7 +13,7 @@ pub struct FuseHandlerTemplate<'a> { } #[derive(Template)] -#[template(path = "mouting.rs.j2")] -pub struct MoutingTemplate<'a> { +#[template(path = "mounting.rs.j2")] +pub struct MountingTemplate<'a> { pub mode: &'a str, } diff --git a/templates/fuse_driver.rs.j2 b/templates/fuse_driver.rs.j2 index bc811bb..ddcea9b 100644 --- a/templates/fuse_driver.rs.j2 +++ b/templates/fuse_driver.rs.j2 @@ -1,6 +1,7 @@ {% let send_sync = mode != "serial" %} use std::{ + io, sync::Arc, collections::{HashMap, VecDeque}, ffi::{OsStr, OsString}, @@ -41,6 +42,9 @@ type DirIter = HashMap<(u64, i64), VecDeque<(OsString, u64, TAttr)>>; use tokio::sync::Mutex; {% endif %} +{% macro async_keyword() -%}{% if mode == "async" %}async{% endif %}{%- endmacro %} +{% macro await_keyword() -%}{% if mode == "async" %}.await{% endif %}{%- endmacro %} + pub(crate) struct FuseDriver where TId: FileIdType, @@ -49,7 +53,7 @@ where pub resolver: Arc, {% if !send_sync %} - handler: THandler, + handler: Arc, dirmap_iter: RefCell>, dirmapplus_iter: RefCell>, {% else %} @@ -84,23 +88,37 @@ where TId: FileIdType, THandler: FuseHandler, { - pub fn new( + pub {{ async_keyword() }} fn new( handler: THandler, {% if send_sync %} num_threads: usize {% else %} _num_threads: usize {% endif %} - ) -> FuseDriver { + ) -> io::Result> { {% if mode == "parallel" %} #[cfg(feature = "deadlock_detection")] spawn_deadlock_checker(); {% endif %} - FuseDriver { - resolver: Arc::new(TId::create_resolver()), + let resolver = Arc::new(TId::create_resolver()); + {% if mode == "async" %} + let runtime = Arc::new(Runtime::new().unwrap()); + runtime.block_on(async { + Self::bootstrap_handler(&handler, resolver.clone()).await.map_err(|e| { + io::Error::from_raw_os_error(e.raw_error()) + }) + })?; + {% else %} + Self::bootstrap_handler(&handler, resolver.clone()).map_err(|e| { + io::Error::from_raw_os_error(e.raw_error()) + })?; + {% endif %} + + Ok(FuseDriver { + resolver: resolver, {% if mode == "serial" %} - handler, + handler: Arc::new(handler), dirmap_iter: RefCell::new(HashMap::new()), dirmapplus_iter: RefCell::new(HashMap::new()), {% else %} @@ -113,14 +131,14 @@ where threadpool: ThreadPool::new(num_threads), reply_threadpool: ThreadPool::new(num_threads), {% elif mode == "async" %} - runtime: Arc::new(Runtime::new().unwrap()), + runtime: runtime, {% endif %} - } + }) } {% if !send_sync %} - pub fn get_handler(&self) -> &THandler { - &self.handler + pub {{ async_keyword() }} fn get_handler(&self) -> Arc { + self.handler.clone() } pub fn get_dirmap_iter(&self) -> &RefCell> { @@ -143,9 +161,26 @@ where self.dirmapplus_iter.clone() } {% endif %} -} - + {{ async_keyword() }} pub fn bootstrap_handler( + handler: &THandler, + resolver: Arc, + ) -> FuseResult<()> { + handler.setup(resolver.clone()){{ await_keyword() }}?; + let lookup_root_result = handler.lookup_root(){{ await_keyword() }}; + match lookup_root_result { + Ok(metadata) => { + let (id, _file_attr) = TId::extract_metadata(metadata); + resolver.lookup_root(id); + }, + Err(e) => { + // This is normal if the filesystem is not using HybridId, we'll just put a warning instead + warn!("[{}] lookup_root", e); + } + } + Ok(()) + } +} impl fuser::Filesystem for FuseDriver where @@ -214,7 +249,6 @@ where }, {%- endmacro %} - fn init(&mut self, req: &Request, config: &mut KernelConfig) -> Result<(), c_int> { let req = RequestInfo::from(req); match self.get_handler().init(&req, config) { diff --git a/templates/fuse_handler.rs.j2 b/templates/fuse_handler.rs.j2 index 6c5c60b..4086c05 100644 --- a/templates/fuse_handler.rs.j2 +++ b/templates/fuse_handler.rs.j2 @@ -90,7 +90,7 @@ {% let send_sync = mode != "serial" %} - +use std::sync::Arc; use std::ffi::{OsStr, OsString}; use std::path::Path; use std::time::Duration; @@ -102,7 +102,7 @@ use async_trait::async_trait; #[async_trait] {% endif %} -pub trait FuseHandler: 'static {% if send_sync %}+ Sync + Send {% endif %} { +pub trait FuseHandler: 'static + Sync + Send { {% macro async_keyword() -%}{% if mode == "async" %}async{% endif %}{%- endmacro %} {% macro await_keyword() -%}{% if mode == "async" %}.await{% endif %}{%- endmacro %} @@ -117,6 +117,17 @@ pub trait FuseHandler: 'static {% if send_sync %}+ Sync + Send Duration::from_secs(1) } + /// Perform pre-mount setup for the filesystem, which allows the filesystem to use advanced + /// functionalities of the library such as [`HybridId`](crate::types::HybridId). + /// + /// # Parameters + /// - `resolver`: The path resolver for the filesystem. If using [`HybridId`](crate::types::HybridId), + /// the user is expected to store the resolver for later use. + {{ async_keyword() }} fn setup(&self, resolver: Arc) -> FuseResult<()> { + self.get_inner().setup(resolver) + {{ await_keyword() }} + } + /// Initialize the filesystem and configure kernel connection fn init(&self, req: &RequestInfo, config: &mut KernelConfig) -> FuseResult<()> { self.get_inner().init(req, config) @@ -126,7 +137,7 @@ pub trait FuseHandler: 'static {% if send_sync %}+ Sync + Send fn destroy(&self) { self.get_inner().destroy(); } - + /// Check file access permissions /// /// This method is called for the access() system call. If the 'default_permissions' @@ -337,6 +348,20 @@ pub trait FuseHandler: 'static {% if send_sync %}+ Sync + Send {{ await_keyword() }} } + /// Perform a pre-mount lookup of the root inode. + /// + /// This method acts similarly to the [`lookup`](Self::lookup) + /// method, but is only called once for the root inode, requires no parameters, + /// and is called before mounting succeeds so that the subsequent + /// lookup operations are performed with the root inode being valid. + /// + /// This method must be implemented if the filesystem requires the root + /// inode to have a stable backing ID. + {{ async_keyword() }} fn lookup_root(&self) -> FuseResult { + self.get_inner().lookup_root() + {{ await_keyword() }} + } + /// Reposition read/write file offset {{ async_keyword() }} fn lseek( &self, diff --git a/templates/mouting.rs.j2 b/templates/mounting.rs.j2 similarity index 98% rename from templates/mouting.rs.j2 rename to templates/mounting.rs.j2 index 7ae1c82..d346a79 100644 --- a/templates/mouting.rs.j2 +++ b/templates/mounting.rs.j2 @@ -49,7 +49,7 @@ where {% else %} 1, {% endif %} - ); + )?; mount2(driver, mountpoint, options) } @@ -94,7 +94,7 @@ pub fn spawn_mount( ) -> io::Result> where T: FileIdType, - FS: FuseHandler + Send, + FS: FuseHandler, P: AsRef, { let driver = FuseDriver::new( @@ -104,7 +104,7 @@ where {% else %} 1, {% endif %} - ); + )?; let resolver = driver.resolver.clone(); let session = spawn_mount2(driver, mountpoint, options)?; Ok(FuseSession::new(session, resolver)) From d0feb3d895aa4a1d7a638937e5e31221346c326f Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Mon, 9 Feb 2026 17:37:52 +0700 Subject: [PATCH 2/8] fix: undo faulty trait bounds --- templates/fuse_driver.rs.j2 | 8 ++++---- templates/fuse_handler.rs.j2 | 2 +- templates/mounting.rs.j2 | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/fuse_driver.rs.j2 b/templates/fuse_driver.rs.j2 index ddcea9b..4c4930b 100644 --- a/templates/fuse_driver.rs.j2 +++ b/templates/fuse_driver.rs.j2 @@ -53,7 +53,7 @@ where pub resolver: Arc, {% if !send_sync %} - handler: Arc, + handler: THandler, dirmap_iter: RefCell>, dirmapplus_iter: RefCell>, {% else %} @@ -118,7 +118,7 @@ where Ok(FuseDriver { resolver: resolver, {% if mode == "serial" %} - handler: Arc::new(handler), + handler: handler, dirmap_iter: RefCell::new(HashMap::new()), dirmapplus_iter: RefCell::new(HashMap::new()), {% else %} @@ -137,8 +137,8 @@ where } {% if !send_sync %} - pub {{ async_keyword() }} fn get_handler(&self) -> Arc { - self.handler.clone() + pub fn get_handler(&self) -> &THandler { + &self.handler } pub fn get_dirmap_iter(&self) -> &RefCell> { diff --git a/templates/fuse_handler.rs.j2 b/templates/fuse_handler.rs.j2 index 4086c05..072b6d8 100644 --- a/templates/fuse_handler.rs.j2 +++ b/templates/fuse_handler.rs.j2 @@ -102,7 +102,7 @@ use async_trait::async_trait; #[async_trait] {% endif %} -pub trait FuseHandler: 'static + Sync + Send { +pub trait FuseHandler: 'static {% if send_sync %} + Sync + Send {% endif %} { {% macro async_keyword() -%}{% if mode == "async" %}async{% endif %}{%- endmacro %} {% macro await_keyword() -%}{% if mode == "async" %}.await{% endif %}{%- endmacro %} diff --git a/templates/mounting.rs.j2 b/templates/mounting.rs.j2 index d346a79..8d91fec 100644 --- a/templates/mounting.rs.j2 +++ b/templates/mounting.rs.j2 @@ -94,7 +94,7 @@ pub fn spawn_mount( ) -> io::Result> where T: FileIdType, - FS: FuseHandler, + FS: FuseHandler + Send, P: AsRef, { let driver = FuseDriver::new( From a6f77ff965bf1bcb8cdcb2dcd85ab116a0ec2b0e Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Mon, 9 Feb 2026 17:49:12 +0700 Subject: [PATCH 3/8] fix: only ignore FunctionNotImplemented when calling lookup_root --- templates/fuse_driver.rs.j2 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/templates/fuse_driver.rs.j2 b/templates/fuse_driver.rs.j2 index 4c4930b..05a950d 100644 --- a/templates/fuse_driver.rs.j2 +++ b/templates/fuse_driver.rs.j2 @@ -174,8 +174,13 @@ where resolver.lookup_root(id); }, Err(e) => { - // This is normal if the filesystem is not using HybridId, we'll just put a warning instead - warn!("[{}] lookup_root", e); + if e.kind() == ErrorKind::FunctionNotImplemented { + // This is normal if the filesystem is not using HybridId, we'll just put a warning here but + // return the error to the caller + warn!("[{}] lookup_root", e); + } else { + return Err(e); + } } } Ok(()) From 2f8993a0923ced2c63c5f7a60561e9fd27526011 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 10 Feb 2026 09:48:09 +0700 Subject: [PATCH 4/8] fix: make ID resolver fallible --- src/core/inode_mapping.rs | 110 +++++++++++++++++++++++++++----------- src/inode_multi_mapper.rs | 98 ++++++++++++++++----------------- 2 files changed, 125 insertions(+), 83 deletions(-) diff --git a/src/core/inode_mapping.rs b/src/core/inode_mapping.rs index 836a523..55ddecf 100644 --- a/src/core/inode_mapping.rs +++ b/src/core/inode_mapping.rs @@ -422,10 +422,7 @@ where } fn prune(&self, _keep: &HashSet) { - let resolver_keep: HashSet = _keep - .iter() - .map(|id| id.inode().clone()) - .collect(); + let resolver_keep: HashSet = _keep.iter().map(|id| id.inode().clone()).collect(); self.mapper .write() .expect("Failed to acquire write lock") @@ -448,6 +445,9 @@ where } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HybridIdNotFound {} + /// Specialized methods for hybrid resolver to deal with file paths and backing IDs. impl HybridResolver where @@ -456,35 +456,54 @@ where /// Retrieves the first path to the hybrid ID's inode. This is a convenience method /// for users who do not need a more exhaustive list of paths that might occupy an inode. /// + /// # Returns + /// - `Ok(Some(path))` if the inode is found and the path is resolved. + /// - `Ok(None)` if the inode is found, but it belongs to an orphaned tree. + /// - `Err(HybridIdNotFound)` if the inode is not found at all. This usually happens + /// when you hold an inode past its lifetime, which ends at the last forget() call that + /// sets its lookup count to 0. + /// /// # 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. - pub fn first_path(&self, id: &HybridId) -> Option { + pub fn first_path( + &self, + id: &HybridId, + ) -> Result, HybridIdNotFound> { let mapper = self .mapper .read() .expect("failed to acquire read lock on mapper"); - let path = mapper.resolve(id.inode()).map(|components| { - components - .iter() - .map(|component| component.name.as_ref()) - .rev() - .collect::() - }); - path + let path = mapper + .resolve(id.inode()) + .map_err(|_| HybridIdNotFound {})? + .map(|components| { + components + .into_iter() + .map(|component| component.name.as_ref()) + .rev() + .collect::() + }); + Ok(path) } /// Retrieves all paths to the hybrid ID's inode, up to a given limit. /// /// Inodes that are not found will result in an empty vector. - pub fn all_paths(&self, id: &HybridId, limit: Option) -> Vec { + pub fn all_paths( + &self, + id: &HybridId, + limit: Option, + ) -> Result, HybridIdNotFound> { let mapper = self .mapper .read() .expect("failed to acquire read lock on mapper"); - let resolved = mapper.resolve_all(id.inode(), limit); - resolved + let resolved = mapper + .resolve_all(id.inode(), limit) + .map_err(|_| HybridIdNotFound {})?; + let paths = resolved .iter() .map(|components| { components @@ -493,7 +512,8 @@ where .map(|component| component.name.as_ref()) .collect::() }) - .collect() + .collect(); + Ok(paths) } /// Retrieves the backing ID of the inode. @@ -653,17 +673,29 @@ 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!(resolver.first_path(&root_id), Some(PathBuf::from(""))); + assert_eq!( + resolver + .first_path(&root_id) + .expect("root inode should be found"), + Some(PathBuf::from("")) + ); // 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!(resolver.first_path(&dir1_id), Some(PathBuf::from("dir1"))); + assert_eq!( + resolver + .first_path(&dir1_id) + .expect("dir1 inode should be found"), + Some(PathBuf::from("dir1")) + ); let dir2_ino = resolver.lookup(dir1_ino, OsStr::new("dir2"), Some(2), true); let dir2_id = resolver.resolve_id(dir2_ino); assert_eq!( - resolver.first_path(&dir2_id), + resolver + .first_path(&dir2_id) + .expect("dir2 inode should be found"), Some(PathBuf::from("dir1/dir2")) ); @@ -677,7 +709,9 @@ mod tests { for (name, ino) in added_grandchildren.iter() { let child_id = resolver.resolve_id(*ino); assert_eq!( - resolver.first_path(&child_id), + resolver + .first_path(&child_id) + .expect("child inode of dir1/dir2 should be found"), Some(PathBuf::from("dir1/dir2").join(name)) ); } @@ -692,9 +726,11 @@ mod tests { dir2_ino, OsStr::new("grandchild2_renamed"), ); - let renamed_grandchild_path = resolver.resolve_id(added_grandchildren[1].1); + let renamed_grandchild_id = resolver.resolve_id(added_grandchildren[1].1); assert_eq!( - resolver.first_path(&renamed_grandchild_path), + resolver + .first_path(&renamed_grandchild_id) + .expect("renamed grandchild inode (dir1/dir2/grandchild2_renamed) should be found"), Some(PathBuf::from("dir1/dir2/grandchild2_renamed")) ); @@ -706,9 +742,11 @@ mod tests { dir3_ino, OsStr::new("grandchild2_renamed"), ); - let renamed_grandchild_path = resolver.resolve_id(added_grandchildren[1].1); + let renamed_grandchild_id = resolver.resolve_id(added_grandchildren[1].1); assert_eq!( - resolver.first_path(&renamed_grandchild_path), + resolver + .first_path(&renamed_grandchild_id) + .expect("renamed grandchild inode (dir3/grandchild2_renamed) should exist"), Some(PathBuf::from("dir3/grandchild2_renamed")) ); @@ -718,7 +756,9 @@ mod tests { assert_ne!(non_existent_ino, 0); let non_existent_path = resolver.resolve_id(non_existent_ino); assert_eq!( - resolver.first_path(&non_existent_path), + resolver + .first_path(&non_existent_path) + .expect("ghost inode explicitly inserted with refcount = 0 should exist"), Some(PathBuf::from("non_existent")) ); @@ -726,7 +766,9 @@ mod tests { 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!( - resolver.first_path(&hard_link_id), + resolver + .first_path(&hard_link_id) + .expect("hard link inode should be found"), Some(PathBuf::from("hard_link")) ); @@ -734,11 +776,17 @@ mod tests { 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" + "hard link inodes should be the same if callers supply the same backing ID" + ); + assert_eq!( + hard_link_id_2, hard_link_id, + "hard link IDs should be the same if callers supply the same backing ID" ); resolver.lookup(dir1_ino, OsStr::new("hard_linked_2"), Some(7), true); - let paths = resolver.all_paths(&hard_link_id_2, Some(100)); + let paths = resolver + .all_paths(&hard_link_id_2, Some(100)) + .expect("hard_link_id_2 should wrap an existing inode"); 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"))); @@ -752,7 +800,9 @@ mod tests { ); // Test path resolution after overriding a location with a new backing ID - let paths = resolver.all_paths(&hard_link_id_2, Some(100)); + let paths = resolver + .all_paths(&hard_link_id_2, Some(100)) + .expect("hard_link_id_2 should wrap an existing inode"); assert!( !paths.contains(&PathBuf::from("dir1/dir2/hard_linked")), "the path list should no longer contain the overridden location" diff --git a/src/inode_multi_mapper.rs b/src/inode_multi_mapper.rs index c0bb7c9..874ff96 100644 --- a/src/inode_multi_mapper.rs +++ b/src/inode_multi_mapper.rs @@ -37,6 +37,9 @@ where backing: BiHashMap, } +#[derive(Debug, PartialEq, Eq)] +pub struct InodeNotFound {} + #[derive(Debug)] struct InodeValue where @@ -670,55 +673,40 @@ where /// Resolves an inode to one combination of its full path components /// + /// # Returns + /// - `Ok(Some(result))` if the inode is found and the path components are resolved. + /// - `Ok(None)` if there is no way to trace back to the root inode, indicating an orphaned + /// inode or orphaned tree, 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). + /// - `Err(InodeNotFound)` if the inode is not found at all. + /// /// # 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). + /// - 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. /// - 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) + pub fn resolve( + &self, + inode: &Inode, + ) -> Result>>, InodeNotFound> { + let all_result = self.resolve_all(inode, Some(1))?; + Ok(all_result.into_iter().next()) } - /// Recursively resolve all possible combinations of path components that resolve to the given inode, up to a given limit. + /// Recursively resolve all possible combinations of path components that resolve to the given + /// inode, up to a given limit. + /// + /// # Returns + /// - `Ok(result)` if the inode is found. Any paths that connect the inode to the root inode + /// are returned, up to a given limit. The components are in reverse order. + /// - `Err(InodeNotFound)` if the inode is not found at all. pub fn resolve_all<'a>( &'a self, inode: &Inode, limit: Option, - ) -> Vec>> { + ) -> Result>>, InodeNotFound> { + // Check if the inode exists + let _ = self.get(inode).ok_or_else(|| InodeNotFound {})?; let mut result = vec![]; fn scoped_resolve<'a, Data, BackingId>( @@ -788,7 +776,7 @@ where &mut Vec::new(), &mut HashSet::new(), ); - result + Ok(result) } pub fn get(&self, inode: &Inode) -> Option> { @@ -980,7 +968,7 @@ where } if value.data.lookup_count().load(Ordering::SeqCst) == 0 { // Check if inode is in keep list - if let Some(_resolve_info) = self.resolve(inode) { + if let Ok(_resolve_info) = self.resolve(inode) { if !keep.contains(&inode) { to_remove.push(inode.clone()); } @@ -1148,7 +1136,10 @@ mod tests { .unwrap(); // Resolve the file inode - let path = mapper.resolve(&file_inode).unwrap(); + let path = mapper + .resolve(&file_inode) + .expect("inode should exist") + .expect("inode should not be orphaned"); // Check the resolved path (it should be in reverse order) assert_eq!(path.len(), 2); @@ -1182,11 +1173,16 @@ mod tests { ); // Resolve the root inode (should be empty) - let root_path = mapper.resolve(&ROOT_INODE).unwrap(); + let root_path = mapper + .resolve(&ROOT_INODE) + .expect("root inode should exist") + .expect("root inode should not be orphaned"); assert!(root_path.is_empty()); // Try to resolve a non-existent inode - assert!(mapper.resolve(&Inode::from(999)).is_none()); + mapper + .resolve(&Inode::from(999)) + .expect_err("inode should not be found"); } #[test] @@ -1195,13 +1191,9 @@ mod tests { 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" - ); + mapper + .resolve(&invalid_inode) + .expect_err("inode should not be found"); } #[test] From 5fd26fcbe50cc587c2dfbded3b7a070450fcc21c Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 10 Feb 2026 09:51:52 +0700 Subject: [PATCH 5/8] fix: FuseDriver::new() should not be async --- templates/fuse_driver.rs.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/fuse_driver.rs.j2 b/templates/fuse_driver.rs.j2 index 05a950d..5de0ad7 100644 --- a/templates/fuse_driver.rs.j2 +++ b/templates/fuse_driver.rs.j2 @@ -88,7 +88,7 @@ where TId: FileIdType, THandler: FuseHandler, { - pub {{ async_keyword() }} fn new( + pub fn new( handler: THandler, {% if send_sync %} num_threads: usize From 7fdfee0206e49c6404c8bfa98e9769476d9314a2 Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 24 Feb 2026 14:35:23 +0700 Subject: [PATCH 6/8] fix: expose resolver structures for use in setup() --- src/core.rs | 2 ++ src/lib.rs | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core.rs b/src/core.rs index cfe026d..30f6519 100644 --- a/src/core.rs +++ b/src/core.rs @@ -5,3 +5,5 @@ mod thread_mode; pub(crate) use fuse_driver::FuseDriver; pub(crate) use inode_mapping::{FileIdResolver, InodeResolvable, ROOT_INO}; +// Expose these structs for usage of any public methods +pub use inode_mapping::{ComponentsResolver, HybridResolver, InodeResolver, PathResolver}; diff --git a/src/lib.rs b/src/lib.rs index 83db96b..04d2ead 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,8 +23,8 @@ mod core; mod fuse_handler; pub mod inode_mapper; -pub mod session; pub mod inode_multi_mapper; +pub mod session; pub mod templates; pub mod types; pub mod unix_fs; @@ -39,11 +39,11 @@ pub mod prelude { pub use super::types::*; pub use super::{mount, spawn_mount}; + pub use super::core::{ComponentsResolver, HybridResolver, InodeResolver, PathResolver}; pub use fuser::{BackgroundSession, MountOption, Session, SessionUnmounter}; } - #[cfg(feature = "serial")] include!(concat!(env!("OUT_DIR"), "/serial/mounting.rs")); #[cfg(not(feature = "serial"))] -include!(concat!(env!("OUT_DIR"), "/parallel/mounting.rs")); \ No newline at end of file +include!(concat!(env!("OUT_DIR"), "/parallel/mounting.rs")); From 035f7a88e5f9c0cdba8f6fcdcbd33ae45fa07a5c Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 24 Feb 2026 14:49:05 +0700 Subject: [PATCH 7/8] fix: backing_id() should be fallible --- src/core/inode_mapping.rs | 10 ++++++++-- src/inode_multi_mapper.rs | 7 +++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/core/inode_mapping.rs b/src/core/inode_mapping.rs index 55ddecf..fe834db 100644 --- a/src/core/inode_mapping.rs +++ b/src/core/inode_mapping.rs @@ -523,12 +523,18 @@ where /// condition, in which case another backing path could be tried, or an error /// could be returned. The stable backing ID can also be used as key for the /// inode's data as defined by the user. - pub fn backing_id(&self, id: &HybridId) -> Option { + pub fn backing_id( + &self, + id: &HybridId, + ) -> Result, HybridIdNotFound> { let mapper = self .mapper .read() .expect("failed to acquire read lock on mapper"); - mapper.get_backing_id(id.inode()).cloned() + Ok(mapper + .get_backing_id(id.inode()) + .map_err(|_| HybridIdNotFound {})? + .cloned()) } } diff --git a/src/inode_multi_mapper.rs b/src/inode_multi_mapper.rs index 874ff96..d240537 100644 --- a/src/inode_multi_mapper.rs +++ b/src/inode_multi_mapper.rs @@ -797,8 +797,11 @@ where } /// Retrieves the backing ID of a given inode - pub fn get_backing_id(&self, inode: &Inode) -> Option<&BackingId> { - self.data.backing.get_by_left(inode) + pub fn get_backing_id(&self, inode: &Inode) -> Result, InodeNotFound> { + if self.data.inodes.get(inode).is_none() { + return Err(InodeNotFound {}); + } + Ok(self.data.backing.get_by_left(inode)) } /// Retrieves all children of a given parent inode. From b63fd58c02d3bd8c4603127b2cd4ee85f6bc33fe Mon Sep 17 00:00:00 2001 From: Khanh Tran Date: Tue, 24 Feb 2026 16:01:26 +0700 Subject: [PATCH 8/8] feat: add path invalidation --- src/core/inode_mapping.rs | 40 ++++++++++++++++++++++++- src/inode_multi_mapper.rs | 61 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/core/inode_mapping.rs b/src/core/inode_mapping.rs index fe834db..991d639 100644 --- a/src/core/inode_mapping.rs +++ b/src/core/inode_mapping.rs @@ -3,7 +3,7 @@ use std::{ ffi::{OsStr, OsString}, fmt::Debug, hash::Hash, - path::PathBuf, + path::{Path, PathBuf}, sync::{Arc, atomic::Ordering}, }; @@ -536,6 +536,32 @@ where .map_err(|_| HybridIdNotFound {})? .cloned()) } + + /// Invalidate a hybrid ID's path, which is useful when a path from [`Self::all_paths`] + /// no longer represents the hybrid ID due to upstream changes. A full path that does not + /// start with a forward slash is expected. + /// + /// The method ignores paths that do not point to the inode. + pub fn invalidate_path_of_id( + &self, + id: &HybridId, + path: &Path, + ) -> Result<(), HybridIdNotFound> { + let mut mapper = self + .mapper + .write() + .expect("failed to acquire write lock on mapper"); + mapper + .invalidate_inode_path( + id.inode(), + &path + .components() + .map(|c| c.as_os_str().to_os_string()) + .collect::>(), + ) + .map_err(|_| HybridIdNotFound {})?; + Ok(()) + } } #[cfg(test)] @@ -815,6 +841,18 @@ mod tests { ); assert!(paths.contains(&PathBuf::from("hard_link"))); assert!(paths.contains(&PathBuf::from("dir1/hard_linked_2"))); + + // Test invalidate_path_of_id + resolver + .invalidate_path_of_id(&hard_link_id_2, &PathBuf::from("dir1/hard_linked_2")) + .expect("hard_link_id_2 should wrap an existing inode"); + let paths = resolver + .all_paths(&hard_link_id_2, Some(100)) + .expect("hard_link_id_2 should wrap an existing inode"); + assert!( + !paths.contains(&PathBuf::from("dir1/hard_linked_2")), + "the path list should no longer contain the invalidated location" + ); } #[test] diff --git a/src/inode_multi_mapper.rs b/src/inode_multi_mapper.rs index d240537..c4678be 100644 --- a/src/inode_multi_mapper.rs +++ b/src/inode_multi_mapper.rs @@ -1,5 +1,7 @@ use super::inode_mapper::HasLookupCount; -use crate::types::{Inode, ROOT_INODE}; +use crate::{ + types::{Inode, ROOT_INODE}, +}; use bimap::BiHashMap; use std::{ borrow::Borrow, @@ -955,6 +957,63 @@ where None } } + + /// Invalidates an inode's path alias without deferencing or + /// removing the inode from the mapper. + pub fn invalidate_inode_path( + &mut self, + inode: &Inode, + path: &[OsString], + ) -> Result<(), InodeNotFound> { + // Check if the inode exists, and bail immediately if it doesn't. + self.get(inode).ok_or(InodeNotFound {})?; + // Empty path means that the inode has no parent inode, and therefore has nothing to invalidate. + if path.is_empty() { + return Ok(()); + } + let mut current_inode = self.root_inode.clone(); + let mut parent_inode = None; + for component in path.iter() { + let inode_children = match self.data.children.get(¤t_inode) { + Some(children) => children, + None => return Ok(()), + }; + let child_inode = match inode_children.get(component.as_os_str()) { + Some(child_inode) => child_inode, + None => return Ok(()), + }; + parent_inode = Some(current_inode.clone()); + current_inode = child_inode.clone(); + } + // The path is not occupied by the inode, so it is ignored. + if current_inode != *inode { + return Ok(()); + } + let parent_inode = + parent_inode.expect("parent inode should be present because the loop runs once"); + let last_component = path.last().expect("path should be non-empty"); + // Unassociate the last component, which is a name linking to the target inode, from the parent inode's children map. + let parent_children = self.data.children.get_mut(&parent_inode); + if let Some(parent_children) = parent_children { + parent_children.remove(last_component.as_os_str()); + if parent_children.is_empty() { + self.data.children.remove(&parent_inode); + } + } + // Unassociate the last component from the current inode's association map. + let current_inode_info = self + .data + .inodes + .get_mut(¤t_inode) + .expect("current inode should exist"); + if let Some(links_to_parent) = current_inode_info.links.get_mut(&parent_inode) { + links_to_parent.remove(last_component.as_os_str()); + if links_to_parent.is_empty() { + current_inode_info.links.remove(&parent_inode); + } + } + Ok(()) + } } impl InodeMultiMapper