use std::{ collections::BTreeMap, fs, hash::Hasher, path::{Path, PathBuf}, }; use fuser::INodeNo; use twox_hash::XxHash64; use crate::db::entities::Model; use crate::item::{FileType, Item}; pub fn virtual_inode(path: &str) -> INodeNo { let mut hasher = XxHash64::with_seed(5678); hasher.write(path.as_bytes()); return INodeNo(hasher.finish() | (1u64 << 63)); } pub fn parent_inode_from_path(local_path: &Path) -> INodeNo { let components: Vec<_> = local_path .components() .filter(|c| matches!(c, std::path::Component::Normal(_))) .collect(); if components.len() <= 1 { return INodeNo::ROOT; } let mut parent_path = String::new(); for (i, comp) in components[..components.len() - 1].iter().enumerate() { if i > 0 { parent_path.push('/'); } parent_path.push_str(comp.as_os_str().to_str().unwrap_or_default()); } return virtual_inode(&parent_path); } pub fn ensure_virtual_dirs( local_path: &Path, source: &Path, map: &mut BTreeMap, ) -> INodeNo { let components: Vec<_> = local_path .components() .filter(|c| matches!(c, std::path::Component::Normal(_))) .collect(); if components.len() <= 1 { return INodeNo::ROOT; } let mut current_parent = INodeNo::ROOT; let mut current_path = String::new(); for component in &components[..components.len() - 1] { let comp_str = component.as_os_str().to_str().unwrap_or_default(); if !current_path.is_empty() { current_path.push('/'); } current_path.push_str(comp_str); let virt_ino = virtual_inode(¤t_path); if !map.contains_key(&virt_ino) { let virt_item = Item::new( virt_ino, current_parent, comp_str.to_string(), source.to_path_buf(), PathBuf::from(¤t_path), FileType::Directory, fs::metadata(source).unwrap(), None, ); map.insert(virt_ino, virt_item); } current_parent = virt_ino; } return current_parent; } pub fn restore_virtual_paths( snapshot: &mut BTreeMap, db_items: &std::collections::HashMap, source: &Path, ) { let restorations: Vec<(INodeNo, String, PathBuf)> = db_items .values() .filter_map(|db_item| { let ino = INodeNo(db_item.inode as u64); if ino == INodeNo::ROOT { return None; } snapshot .get(&ino) .filter(|item| item.hash as i64 == db_item.hash) .map(|_| { ( ino, db_item.name.clone(), PathBuf::from(&db_item.local_path), ) }) }) .collect(); for (_, _, local_path) in &restorations { ensure_virtual_dirs(local_path, source, snapshot); } for (ino, name, local_path) in restorations { if let Some(item) = snapshot.get_mut(&ino) { item.name = name; item.parent_inode = parent_inode_from_path(&local_path); item.local_path = local_path; } } } #[cfg(test)] mod tests { use super::*; use std::collections::HashMap; #[test] fn virtual_inode_deterministic() { let ino1 = virtual_inode("foo"); let ino2 = virtual_inode("foo"); assert_eq!(ino1, ino2); } #[test] fn virtual_inode_high_bit_set() { let ino = virtual_inode("test"); assert_eq!(ino.0 & (1u64 << 63), 1u64 << 63); } #[test] fn virtual_inode_different_inputs() { let ino1 = virtual_inode("foo"); let ino2 = virtual_inode("bar"); assert_ne!(ino1, ino2); } #[test] fn parent_inode_single_component() { let ino = parent_inode_from_path(Path::new("file.txt")); assert_eq!(ino, INodeNo::ROOT); } #[test] fn parent_inode_multi_component() { let ino = parent_inode_from_path(Path::new("a/b/c")); let expected = virtual_inode("a/b"); assert_eq!(ino, expected); } #[test] fn parent_inode_absolute_path_filtered() { let ino = parent_inode_from_path(Path::new("/file")); assert_eq!(ino, INodeNo::ROOT); } #[test] fn ensure_virtual_dirs_single_component() { let tmp = tempfile::tempdir().unwrap(); let source = tmp.path(); let mut map = BTreeMap::new(); let ino = ensure_virtual_dirs(Path::new("file.txt"), source, &mut map); assert_eq!(ino, INodeNo::ROOT); assert!(map.is_empty()); } #[test] fn ensure_virtual_dirs_creates_hierarchy() { let tmp = tempfile::tempdir().unwrap(); let source = tmp.path(); let mut map = BTreeMap::new(); let ino = ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map); let a_ino = virtual_inode("a"); let ab_ino = virtual_inode("a/b"); assert_eq!(ino, ab_ino); assert_eq!(map.len(), 2); assert!(map.contains_key(&a_ino)); assert!(map.contains_key(&ab_ino)); let a_item = &map[&a_ino]; assert_eq!(a_item.parent_inode, INodeNo::ROOT); let ab_item = &map[&ab_ino]; assert_eq!(ab_item.parent_inode, a_ino); } #[test] fn ensure_virtual_dirs_idempotent() { let tmp = tempfile::tempdir().unwrap(); let source = tmp.path(); let mut map = BTreeMap::new(); ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map); let size_after_first = map.len(); ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map); let size_after_second = map.len(); assert_eq!(size_after_first, size_after_second); } #[test] fn restore_virtual_paths_skips_root() { let tmp = tempfile::tempdir().unwrap(); let source = tmp.path(); let mut snapshot = BTreeMap::new(); let real_ino = INodeNo(42); let real_item = Item::new( real_ino, INodeNo::ROOT, "real_file".to_string(), source.to_path_buf(), PathBuf::from("real_file"), FileType::File, fs::metadata(source).unwrap(), None, ); snapshot.insert(real_ino, real_item); let mut db_items = HashMap::new(); db_items.insert( 1i64, Model { inode: 1, hash: 0, name: "root".to_string(), original_path: "/tmp/test".to_string(), local_path: "/tmp/test".to_string(), file_type: "directory".to_string(), }, ); db_items.insert( 42i64, Model { inode: 42, hash: 0, name: "real_file".to_string(), original_path: "real_file".to_string(), local_path: "real_file".to_string(), file_type: "file".to_string(), }, ); restore_virtual_paths(&mut snapshot, &db_items, source); for item in snapshot.values() { assert_ne!(item.name, "/"); assert_ne!(item.name, "tmp"); } } }