Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
2 changes: 2 additions & 0 deletions src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
228 changes: 208 additions & 20 deletions src/core/inode_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{
ffi::{OsStr, OsString},
fmt::Debug,
hash::Hash,
path::PathBuf,
path::{Path, PathBuf},
sync::{Arc, atomic::Ordering},
};

Expand Down Expand Up @@ -70,6 +70,7 @@ pub trait FileIdResolver: Send + Sync + 'static {
id: <Self::ResolvedType as FileIdType>::_Id,
increment: bool,
) -> u64;
fn lookup_root(&self, id: <Self::ResolvedType as FileIdType>::_Id) -> ();
fn add_children(
&self,
parent: u64,
Expand Down Expand Up @@ -98,6 +99,8 @@ impl FileIdResolver for InodeResolver {
id.into()
}

fn lookup_root(&self, _id: <Self::ResolvedType as FileIdType>::_Id) -> () {}

// Do nothing, user should provide its own inode
fn add_children(
&self,
Expand Down Expand Up @@ -166,6 +169,8 @@ impl FileIdResolver for ComponentsResolver {
)
}

fn lookup_root(&self, _id: <Self::ResolvedType as FileIdType>::_Id) -> () {}

fn add_children(
&self,
parent: u64,
Expand Down Expand Up @@ -212,7 +217,10 @@ impl FileIdResolver for ComponentsResolver {
}

fn prune(&self, keep: &HashSet<Self::ResolvedType>) {
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) {
Expand Down Expand Up @@ -262,6 +270,8 @@ impl FileIdResolver for PathResolver {
self.resolver.lookup(parent, child, id, increment)
}

fn lookup_root(&self, _id: <Self::ResolvedType as FileIdType>::_Id) -> () {}

fn add_children(
&self,
parent: u64,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -355,6 +365,13 @@ where
)
}

fn lookup_root(&self, id: <Self::ResolvedType as FileIdType>::_Id) -> () {
self.mapper
.write()
.expect("Failed to acquire write lock")
.set_root_inode_backing_id(id);
}

fn add_children(
&self,
parent: u64,
Expand Down Expand Up @@ -405,7 +422,11 @@ where
}

fn prune(&self, _keep: &HashSet<Self::ResolvedType>) {
// TODO
let resolver_keep: HashSet<Inode> = _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) {
Expand All @@ -424,6 +445,125 @@ where
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HybridIdNotFound {}

/// Specialized methods for hybrid resolver to deal with file paths and backing IDs.
impl<BackingId> HybridResolver<BackingId>
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.
///
/// # 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<BackingId>,
) -> Result<Option<PathBuf>, HybridIdNotFound> {
let mapper = self
.mapper
.read()
.expect("failed to acquire read lock on mapper");
let path = mapper
.resolve(id.inode())
.map_err(|_| HybridIdNotFound {})?
.map(|components| {
components
.into_iter()
.map(|component| component.name.as_ref())
.rev()
.collect::<PathBuf>()
});
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<BackingId>,
limit: Option<usize>,
) -> Result<Vec<PathBuf>, HybridIdNotFound> {
let mapper = self
.mapper
.read()
.expect("failed to acquire read lock on mapper");
let resolved = mapper
.resolve_all(id.inode(), limit)
.map_err(|_| HybridIdNotFound {})?;
let paths = resolved
.iter()
.map(|components| {
components
.iter()
.rev()
.map(|component| component.name.as_ref())
.collect::<PathBuf>()
})
.collect();
Ok(paths)
}

/// 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<BackingId>,
) -> Result<Option<BackingId>, HybridIdNotFound> {
let mapper = self
.mapper
.read()
.expect("failed to acquire read lock on mapper");
Ok(mapper
.get_backing_id(id.inode())
.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<BackingId>,
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::<Vec<_>>(),
)
.map_err(|_| HybridIdNotFound {})?;
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -467,11 +607,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.
}
Expand Down Expand Up @@ -565,16 +705,31 @@ 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)
.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!(dir1_id.first_path(), 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!(dir2_id.first_path(), Some(PathBuf::from("dir1/dir2")));
assert_eq!(
resolver
.first_path(&dir2_id)
.expect("dir2 inode should be found"),
Some(PathBuf::from("dir1/dir2"))
);

// Test add_children
let grandchildren = vec![
Expand All @@ -584,9 +739,11 @@ 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)
.expect("child inode of dir1/dir2 should be found"),
Some(PathBuf::from("dir1/dir2").join(name))
);
}
Expand All @@ -601,9 +758,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!(
renamed_grandchild_path.first_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"))
);

Expand All @@ -615,9 +774,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!(
renamed_grandchild_path.first_path(),
resolver
.first_path(&renamed_grandchild_id)
.expect("renamed grandchild inode (dir3/grandchild2_renamed) should exist"),
Some(PathBuf::from("dir3/grandchild2_renamed"))
);

Expand All @@ -627,24 +788,37 @@ 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)
.expect("ghost inode explicitly inserted with refcount = 0 should exist"),
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)
.expect("hard link inode should be found"),
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);
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 = hard_link_id_2.all_paths(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")));
Expand All @@ -658,13 +832,27 @@ 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))
.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"
);
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]
Expand Down
Loading