Skip to main content

arcbox_fs/
passthrough.rs

1//! Passthrough filesystem implementation.
2//!
3//! Maps guest filesystem operations directly to host filesystem.
4//! This is the core filesystem backend that provides actual file I/O.
5
6// Allow casts that are necessary for system calls (libc interop)
7#![allow(
8    clippy::cast_sign_loss,
9    clippy::cast_possible_wrap,
10    clippy::cast_possible_truncation
11)]
12
13use crate::cache::{NegativeCache, NegativeCacheConfig};
14use crate::error::{FsError, Result};
15use std::collections::HashMap;
16use std::ffi::{OsStr, OsString};
17use std::fs::{File, OpenOptions};
18use std::io::{Read, Seek, SeekFrom, Write};
19use std::os::unix::ffi::OsStrExt;
20use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
21use std::path::{Path, PathBuf};
22use std::sync::RwLock;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::time::Duration;
25
26// ============================================================================
27// Inode Management
28// ============================================================================
29
30/// Inode data stored for each known file/directory.
31#[derive(Debug)]
32#[allow(dead_code)]
33struct InodeData {
34    /// Path relative to root.
35    path: PathBuf,
36    /// Reference count (for FUSE forget).
37    refcount: AtomicU64,
38    /// File type from stat mode.
39    file_type: FileType,
40}
41
42impl InodeData {
43    fn new(path: PathBuf, file_type: FileType) -> Self {
44        Self {
45            path,
46            refcount: AtomicU64::new(1),
47            file_type,
48        }
49    }
50
51    fn inc_ref(&self) {
52        self.refcount.fetch_add(1, Ordering::Relaxed);
53    }
54
55    fn dec_ref(&self) -> u64 {
56        self.refcount.fetch_sub(1, Ordering::Relaxed)
57    }
58}
59
60/// File type enumeration.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum FileType {
63    Regular,
64    Directory,
65    Symlink,
66    BlockDevice,
67    CharDevice,
68    Fifo,
69    Socket,
70    Unknown,
71}
72
73impl FileType {
74    fn from_mode(mode: u32) -> Self {
75        let file_type = mode & u32::from(libc::S_IFMT);
76        if file_type == u32::from(libc::S_IFREG) {
77            Self::Regular
78        } else if file_type == u32::from(libc::S_IFDIR) {
79            Self::Directory
80        } else if file_type == u32::from(libc::S_IFLNK) {
81            Self::Symlink
82        } else if file_type == u32::from(libc::S_IFBLK) {
83            Self::BlockDevice
84        } else if file_type == u32::from(libc::S_IFCHR) {
85            Self::CharDevice
86        } else if file_type == u32::from(libc::S_IFIFO) {
87            Self::Fifo
88        } else if file_type == u32::from(libc::S_IFSOCK) {
89            Self::Socket
90        } else {
91            Self::Unknown
92        }
93    }
94
95    #[allow(dead_code)]
96    fn is_dir(self) -> bool {
97        self == Self::Directory
98    }
99
100    /// Converts to dirent type (DT_*).
101    #[must_use]
102    pub fn to_dirent_type(self) -> u32 {
103        match self {
104            Self::Regular => libc::DT_REG as u32,
105            Self::Directory => libc::DT_DIR as u32,
106            Self::Symlink => libc::DT_LNK as u32,
107            Self::BlockDevice => libc::DT_BLK as u32,
108            Self::CharDevice => libc::DT_CHR as u32,
109            Self::Fifo => libc::DT_FIFO as u32,
110            Self::Socket => libc::DT_SOCK as u32,
111            Self::Unknown => libc::DT_UNKNOWN as u32,
112        }
113    }
114}
115
116// ============================================================================
117// File Handle Management
118// ============================================================================
119
120/// Handle data for an open file.
121#[derive(Debug)]
122#[allow(dead_code)]
123struct HandleData {
124    /// The open file.
125    file: File,
126    /// Inode this handle refers to.
127    inode: u64,
128    /// Open flags.
129    flags: u32,
130}
131
132/// Handle data for an open directory.
133#[derive(Debug)]
134struct DirHandleData {
135    /// Inode this handle refers to.
136    inode: u64,
137    /// Cached directory entries.
138    entries: Vec<DirEntry>,
139}
140
141/// A directory entry.
142#[derive(Debug, Clone)]
143pub struct DirEntry {
144    /// Entry name.
145    pub name: OsString,
146    /// Inode number.
147    pub ino: u64,
148    /// File type.
149    pub file_type: FileType,
150}
151
152// ============================================================================
153// Configuration
154// ============================================================================
155
156/// Configuration for the passthrough filesystem.
157#[derive(Debug, Clone)]
158pub struct PassthroughConfig {
159    /// Enable negative cache for non-existent file lookups.
160    pub negative_cache_enabled: bool,
161    /// Maximum entries in the negative cache.
162    pub negative_cache_max_entries: usize,
163    /// Timeout for negative cache entries.
164    pub negative_cache_timeout: Duration,
165}
166
167impl Default for PassthroughConfig {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl PassthroughConfig {
174    /// Creates a new configuration with default values.
175    #[must_use]
176    pub const fn new() -> Self {
177        Self {
178            negative_cache_enabled: true,
179            negative_cache_max_entries: 10_000,
180            negative_cache_timeout: Duration::from_secs(1),
181        }
182    }
183}
184
185// ============================================================================
186// Passthrough Filesystem
187// ============================================================================
188
189/// Passthrough filesystem.
190///
191/// Implements a passthrough filesystem that maps all operations
192/// to the underlying host filesystem. Includes negative caching
193/// to optimize lookups for non-existent files.
194pub struct PassthroughFs {
195    /// Root directory path on host.
196    root: PathBuf,
197    /// Inode to data mapping.
198    inodes: RwLock<HashMap<u64, InodeData>>,
199    /// Next inode number.
200    next_inode: AtomicU64,
201    /// File handle to data mapping.
202    handles: RwLock<HashMap<u64, HandleData>>,
203    /// Directory handle to data mapping.
204    dir_handles: RwLock<HashMap<u64, DirHandleData>>,
205    /// Next handle number.
206    next_handle: AtomicU64,
207    /// Negative cache for non-existent paths.
208    negative_cache: Option<NegativeCache>,
209    /// Configuration.
210    #[allow(dead_code)]
211    config: PassthroughConfig,
212}
213
214impl PassthroughFs {
215    /// Root inode number.
216    pub const ROOT_INODE: u64 = 1;
217
218    /// Creates a new passthrough filesystem with default configuration.
219    ///
220    /// # Errors
221    ///
222    /// Returns [`FsError::InvalidPath`] if the root path is not a directory.
223    pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
224        Self::with_config(root, PassthroughConfig::default())
225    }
226
227    /// Creates a new passthrough filesystem with custom configuration.
228    ///
229    /// # Errors
230    ///
231    /// Returns [`FsError::InvalidPath`] if the root path is not a directory.
232    pub fn with_config(root: impl Into<PathBuf>, config: PassthroughConfig) -> Result<Self> {
233        let root = root.into();
234        if !root.is_dir() {
235            return Err(FsError::InvalidPath(format!(
236                "root path is not a directory: {}",
237                root.display()
238            )));
239        }
240
241        let negative_cache = if config.negative_cache_enabled {
242            Some(NegativeCache::new(NegativeCacheConfig {
243                max_entries: config.negative_cache_max_entries,
244                timeout: config.negative_cache_timeout,
245            }))
246        } else {
247            None
248        };
249
250        // Initialize root inode
251        let mut inodes = HashMap::new();
252        inodes.insert(
253            Self::ROOT_INODE,
254            InodeData::new(PathBuf::new(), FileType::Directory),
255        );
256
257        Ok(Self {
258            root,
259            inodes: RwLock::new(inodes),
260            next_inode: AtomicU64::new(Self::ROOT_INODE + 1),
261            handles: RwLock::new(HashMap::new()),
262            dir_handles: RwLock::new(HashMap::new()),
263            next_handle: AtomicU64::new(1),
264            negative_cache,
265            config,
266        })
267    }
268
269    /// Returns the root directory path.
270    #[must_use]
271    pub fn root(&self) -> &Path {
272        &self.root
273    }
274
275    /// Returns a reference to the negative cache, if enabled.
276    #[must_use]
277    pub fn negative_cache(&self) -> Option<&NegativeCache> {
278        self.negative_cache.as_ref()
279    }
280
281    // ========================================================================
282    // Internal Helpers
283    // ========================================================================
284
285    /// Allocates a new inode number.
286    fn alloc_inode(&self) -> u64 {
287        self.next_inode.fetch_add(1, Ordering::Relaxed)
288    }
289
290    /// Allocates a new handle number.
291    fn alloc_handle(&self) -> u64 {
292        self.next_handle.fetch_add(1, Ordering::Relaxed)
293    }
294
295    /// Gets the full host path for an inode.
296    fn inode_path(&self, inode: u64) -> Result<PathBuf> {
297        if inode == Self::ROOT_INODE {
298            return Ok(self.root.clone());
299        }
300
301        let inodes = self
302            .inodes
303            .read()
304            .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
305
306        let data = inodes.get(&inode).ok_or(FsError::InvalidHandle(inode))?;
307        Ok(self.root.join(&data.path))
308    }
309
310    /// Constructs the full host path for a given parent inode and name.
311    #[allow(clippy::significant_drop_tightening)]
312    fn get_path(&self, parent: u64, name: &OsStr) -> Result<PathBuf> {
313        if parent == Self::ROOT_INODE {
314            return Ok(self.root.join(name));
315        }
316
317        let inodes = self
318            .inodes
319            .read()
320            .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
321
322        let parent_data = inodes.get(&parent).ok_or(FsError::InvalidHandle(parent))?;
323        Ok(self.root.join(&parent_data.path).join(name))
324    }
325
326    /// Gets the relative path from full path.
327    fn relative_path(&self, path: &Path) -> PathBuf {
328        path.strip_prefix(&self.root)
329            .map_or_else(|_| path.to_path_buf(), Path::to_path_buf)
330    }
331
332    /// Creates `FuseAttr` from metadata.
333    #[allow(clippy::cast_possible_truncation)]
334    fn metadata_to_attr(ino: u64, metadata: &std::fs::Metadata) -> crate::fuse::FuseAttr {
335        crate::fuse::FuseAttr {
336            ino,
337            size: metadata.len(),
338            blocks: metadata.blocks(),
339            atime: metadata.atime() as u64,
340            mtime: metadata.mtime() as u64,
341            ctime: metadata.ctime() as u64,
342            atimensec: metadata.atime_nsec() as u32,
343            mtimensec: metadata.mtime_nsec() as u32,
344            ctimensec: metadata.ctime_nsec() as u32,
345            mode: metadata.mode(),
346            nlink: metadata.nlink() as u32,
347            uid: metadata.uid(),
348            gid: metadata.gid(),
349            rdev: metadata.rdev() as u32,
350            blksize: metadata.blksize() as u32,
351            padding: 0,
352        }
353    }
354
355    /// Invalidates negative cache for a path.
356    fn invalidate_negative_cache(&self, path: &Path) {
357        if let Some(ref cache) = self.negative_cache {
358            tracing::trace!(path = %path.display(), "invalidating negative cache");
359            cache.invalidate(path);
360        }
361    }
362
363    // ========================================================================
364    // Inode Operations
365    // ========================================================================
366
367    /// Looks up a name in a directory.
368    ///
369    /// # Errors
370    ///
371    /// - [`FsError::NotFound`] if the file doesn't exist
372    /// - [`FsError::InvalidHandle`] if the parent inode is invalid
373    /// - [`FsError::Io`] for other I/O errors
374    pub fn lookup(&self, parent: u64, name: &OsStr) -> Result<(u64, crate::fuse::FuseAttr)> {
375        let path = self.get_path(parent, name)?;
376
377        // Fast path: check negative cache first
378        if let Some(ref cache) = self.negative_cache {
379            if cache.contains(&path) {
380                tracing::trace!(path = %path.display(), "negative cache hit");
381                return Err(FsError::not_found(path.display().to_string()));
382            }
383        }
384
385        // Perform actual lookup
386        let metadata = match std::fs::symlink_metadata(&path) {
387            Ok(m) => m,
388            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
389                // Add to negative cache
390                if let Some(ref cache) = self.negative_cache {
391                    tracing::trace!(path = %path.display(), "adding to negative cache");
392                    cache.insert(path.clone());
393                }
394                return Err(FsError::not_found(path.display().to_string()));
395            }
396            Err(e) => return Err(FsError::io(e)),
397        };
398
399        let file_type = FileType::from_mode(metadata.mode());
400        let relative = self.relative_path(&path);
401
402        // Check if we already have this inode
403        {
404            let inodes = self
405                .inodes
406                .read()
407                .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
408            for (&ino, data) in inodes.iter() {
409                if data.path == relative {
410                    data.inc_ref();
411                    return Ok((ino, Self::metadata_to_attr(ino, &metadata)));
412                }
413            }
414        }
415
416        // Create new inode
417        let inode = self.alloc_inode();
418        {
419            let mut inodes = self
420                .inodes
421                .write()
422                .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
423            inodes.insert(inode, InodeData::new(relative, file_type));
424        }
425
426        Ok((inode, Self::metadata_to_attr(inode, &metadata)))
427    }
428
429    /// Decrements the reference count of an inode.
430    ///
431    /// When the count reaches zero, the inode is removed from the table.
432    pub fn forget(&self, inode: u64, nlookup: u64) {
433        if inode == Self::ROOT_INODE {
434            return;
435        }
436
437        let should_remove = {
438            let inodes = match self.inodes.read() {
439                Ok(i) => i,
440                Err(_) => return,
441            };
442            if let Some(data) = inodes.get(&inode) {
443                for _ in 0..nlookup {
444                    if data.dec_ref() == 1 {
445                        return;
446                    }
447                }
448                data.refcount.load(Ordering::Relaxed) == 0
449            } else {
450                false
451            }
452        };
453
454        if should_remove {
455            if let Ok(mut inodes) = self.inodes.write() {
456                inodes.remove(&inode);
457            }
458        }
459    }
460
461    /// Gets file attributes.
462    ///
463    /// # Errors
464    ///
465    /// - [`FsError::Io`] if the attributes cannot be retrieved
466    /// - [`FsError::InvalidHandle`] if the inode is invalid
467    pub fn getattr(&self, inode: u64) -> Result<crate::fuse::FuseAttr> {
468        let path = self.inode_path(inode)?;
469        let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
470        Ok(Self::metadata_to_attr(inode, &metadata))
471    }
472
473    /// Sets file attributes.
474    ///
475    /// # Errors
476    ///
477    /// - [`FsError::Io`] if the attributes cannot be set
478    /// - [`FsError::InvalidHandle`] if the inode is invalid
479    #[allow(clippy::too_many_arguments)]
480    pub fn setattr(
481        &self,
482        inode: u64,
483        mode: Option<u32>,
484        uid: Option<u32>,
485        gid: Option<u32>,
486        size: Option<u64>,
487        atime: Option<(i64, u32)>,
488        mtime: Option<(i64, u32)>,
489    ) -> Result<crate::fuse::FuseAttr> {
490        let path = self.inode_path(inode)?;
491
492        // Set mode
493        if let Some(mode) = mode {
494            std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode))
495                .map_err(FsError::io)?;
496        }
497
498        // Set owner
499        if uid.is_some() || gid.is_some() {
500            let uid = uid.map_or(-1_i32 as libc::uid_t, |u| u);
501            let gid = gid.map_or(-1_i32 as libc::gid_t, |g| g);
502            let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
503                .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
504            let ret = unsafe { libc::chown(path_cstr.as_ptr(), uid, gid) };
505            if ret != 0 {
506                return Err(FsError::io(std::io::Error::last_os_error()));
507            }
508        }
509
510        // Set size (truncate)
511        if let Some(size) = size {
512            let file = OpenOptions::new()
513                .write(true)
514                .open(&path)
515                .map_err(FsError::io)?;
516            file.set_len(size).map_err(FsError::io)?;
517        }
518
519        // Set times
520        if atime.is_some() || mtime.is_some() {
521            let atime_spec = atime.map_or(
522                libc::timespec {
523                    tv_sec: 0,
524                    tv_nsec: libc::UTIME_OMIT,
525                },
526                |(sec, nsec)| libc::timespec {
527                    tv_sec: sec,
528                    tv_nsec: i64::from(nsec),
529                },
530            );
531            let mtime_spec = mtime.map_or(
532                libc::timespec {
533                    tv_sec: 0,
534                    tv_nsec: libc::UTIME_OMIT,
535                },
536                |(sec, nsec)| libc::timespec {
537                    tv_sec: sec,
538                    tv_nsec: i64::from(nsec),
539                },
540            );
541            let times = [atime_spec, mtime_spec];
542            let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
543                .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
544            let ret =
545                unsafe { libc::utimensat(libc::AT_FDCWD, path_cstr.as_ptr(), times.as_ptr(), 0) };
546            if ret != 0 {
547                return Err(FsError::io(std::io::Error::last_os_error()));
548            }
549        }
550
551        self.getattr(inode)
552    }
553
554    /// Reads a symbolic link.
555    ///
556    /// # Errors
557    ///
558    /// - [`FsError::Io`] if the link cannot be read
559    /// - [`FsError::InvalidHandle`] if the inode is invalid
560    pub fn readlink(&self, inode: u64) -> Result<PathBuf> {
561        let path = self.inode_path(inode)?;
562        std::fs::read_link(&path).map_err(FsError::io)
563    }
564
565    // ========================================================================
566    // File Creation Operations
567    // ========================================================================
568
569    /// Creates a file.
570    ///
571    /// # Errors
572    ///
573    /// - [`FsError::Io`] if the file cannot be created
574    /// - [`FsError::InvalidHandle`] if the parent inode is invalid
575    pub fn create(
576        &self,
577        parent: u64,
578        name: &OsStr,
579        mode: u32,
580        flags: u32,
581    ) -> Result<(u64, crate::fuse::FuseAttr, u64)> {
582        let path = self.get_path(parent, name)?;
583
584        // Build open options from flags
585        let mut opts = OpenOptions::new();
586        Self::apply_flags(&mut opts, flags);
587        opts.create(true);
588        opts.mode(mode & 0o7777);
589
590        let file = opts.open(&path).map_err(FsError::io)?;
591        let metadata = file.metadata().map_err(FsError::io)?;
592
593        // Invalidate negative cache
594        self.invalidate_negative_cache(&path);
595
596        // Create inode
597        let relative = self.relative_path(&path);
598        let inode = self.alloc_inode();
599        {
600            let mut inodes = self
601                .inodes
602                .write()
603                .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
604            inodes.insert(inode, InodeData::new(relative, FileType::Regular));
605        }
606
607        // Create file handle
608        let handle = self.alloc_handle();
609        {
610            let mut handles = self
611                .handles
612                .write()
613                .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
614            handles.insert(handle, HandleData { file, inode, flags });
615        }
616
617        Ok((inode, Self::metadata_to_attr(inode, &metadata), handle))
618    }
619
620    /// Creates a directory.
621    ///
622    /// # Errors
623    ///
624    /// - [`FsError::Io`] if the directory cannot be created
625    /// - [`FsError::InvalidHandle`] if the parent inode is invalid
626    pub fn mkdir(
627        &self,
628        parent: u64,
629        name: &OsStr,
630        mode: u32,
631    ) -> Result<(u64, crate::fuse::FuseAttr)> {
632        let path = self.get_path(parent, name)?;
633
634        std::fs::create_dir(&path).map_err(FsError::io)?;
635        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode & 0o7777))
636            .map_err(FsError::io)?;
637
638        self.invalidate_negative_cache(&path);
639
640        let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
641        let relative = self.relative_path(&path);
642        let inode = self.alloc_inode();
643
644        {
645            let mut inodes = self
646                .inodes
647                .write()
648                .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
649            inodes.insert(inode, InodeData::new(relative, FileType::Directory));
650        }
651
652        Ok((inode, Self::metadata_to_attr(inode, &metadata)))
653    }
654
655    /// Creates a symbolic link.
656    ///
657    /// # Errors
658    ///
659    /// - [`FsError::Io`] if the symlink cannot be created
660    /// - [`FsError::InvalidHandle`] if the parent inode is invalid
661    pub fn symlink(
662        &self,
663        parent: u64,
664        name: &OsStr,
665        target: &Path,
666    ) -> Result<(u64, crate::fuse::FuseAttr)> {
667        let path = self.get_path(parent, name)?;
668
669        std::os::unix::fs::symlink(target, &path).map_err(FsError::io)?;
670
671        self.invalidate_negative_cache(&path);
672
673        let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
674        let relative = self.relative_path(&path);
675        let inode = self.alloc_inode();
676
677        {
678            let mut inodes = self
679                .inodes
680                .write()
681                .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
682            inodes.insert(inode, InodeData::new(relative, FileType::Symlink));
683        }
684
685        Ok((inode, Self::metadata_to_attr(inode, &metadata)))
686    }
687
688    /// Creates a hard link.
689    ///
690    /// # Errors
691    ///
692    /// - [`FsError::Io`] if the link cannot be created
693    /// - [`FsError::InvalidHandle`] if the source or parent inode is invalid
694    pub fn link(
695        &self,
696        inode: u64,
697        new_parent: u64,
698        new_name: &OsStr,
699    ) -> Result<(u64, crate::fuse::FuseAttr)> {
700        let source_path = self.inode_path(inode)?;
701        let new_path = self.get_path(new_parent, new_name)?;
702
703        std::fs::hard_link(&source_path, &new_path).map_err(FsError::io)?;
704
705        self.invalidate_negative_cache(&new_path);
706
707        // Hard link shares the same inode
708        let metadata = std::fs::symlink_metadata(&new_path).map_err(FsError::io)?;
709
710        // Increment reference count
711        {
712            let inodes = self
713                .inodes
714                .read()
715                .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
716            if let Some(data) = inodes.get(&inode) {
717                data.inc_ref();
718            }
719        }
720
721        Ok((inode, Self::metadata_to_attr(inode, &metadata)))
722    }
723
724    /// Creates a special file node.
725    ///
726    /// # Errors
727    ///
728    /// - [`FsError::Io`] if the node cannot be created
729    /// - [`FsError::InvalidHandle`] if the parent inode is invalid
730    #[allow(clippy::cast_possible_truncation)]
731    pub fn mknod(
732        &self,
733        parent: u64,
734        name: &OsStr,
735        mode: u32,
736        rdev: u64,
737    ) -> Result<(u64, crate::fuse::FuseAttr)> {
738        let path = self.get_path(parent, name)?;
739        let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
740            .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
741
742        let ret = unsafe {
743            libc::mknod(
744                path_cstr.as_ptr(),
745                mode as libc::mode_t,
746                rdev as libc::dev_t,
747            )
748        };
749        if ret != 0 {
750            return Err(FsError::io(std::io::Error::last_os_error()));
751        }
752
753        self.invalidate_negative_cache(&path);
754
755        let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
756        let file_type = FileType::from_mode(metadata.mode());
757        let relative = self.relative_path(&path);
758        let inode = self.alloc_inode();
759
760        {
761            let mut inodes = self
762                .inodes
763                .write()
764                .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
765            inodes.insert(inode, InodeData::new(relative, file_type));
766        }
767
768        Ok((inode, Self::metadata_to_attr(inode, &metadata)))
769    }
770
771    // ========================================================================
772    // File Deletion Operations
773    // ========================================================================
774
775    /// Removes a file.
776    ///
777    /// # Errors
778    ///
779    /// - [`FsError::Io`] if the file cannot be removed
780    /// - [`FsError::InvalidHandle`] if the parent inode is invalid
781    pub fn unlink(&self, parent: u64, name: &OsStr) -> Result<()> {
782        let path = self.get_path(parent, name)?;
783        std::fs::remove_file(&path).map_err(FsError::io)?;
784
785        // The old path should now return ENOENT, so we could add it to
786        // negative cache, but since the file is gone, it's cleaner to
787        // just let it be discovered naturally.
788        Ok(())
789    }
790
791    /// Removes a directory.
792    ///
793    /// # Errors
794    ///
795    /// - [`FsError::Io`] if the directory cannot be removed
796    /// - [`FsError::InvalidHandle`] if the parent inode is invalid
797    pub fn rmdir(&self, parent: u64, name: &OsStr) -> Result<()> {
798        let path = self.get_path(parent, name)?;
799        std::fs::remove_dir(&path).map_err(FsError::io)
800    }
801
802    /// Renames a file or directory.
803    ///
804    /// # Errors
805    ///
806    /// - [`FsError::Io`] if the rename fails
807    /// - [`FsError::InvalidHandle`] if the parent inode is invalid
808    pub fn rename(
809        &self,
810        parent: u64,
811        name: &OsStr,
812        new_parent: u64,
813        new_name: &OsStr,
814        _flags: u32,
815    ) -> Result<()> {
816        let old_path = self.get_path(parent, name)?;
817        let new_path = self.get_path(new_parent, new_name)?;
818
819        std::fs::rename(&old_path, &new_path).map_err(FsError::io)?;
820
821        // Invalidate both paths
822        self.invalidate_negative_cache(&old_path);
823        self.invalidate_negative_cache(&new_path);
824
825        // Update inode path if we have it cached
826        let old_relative = self.relative_path(&old_path);
827        let new_relative = self.relative_path(&new_path);
828
829        {
830            let mut inodes = self
831                .inodes
832                .write()
833                .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
834            for data in inodes.values_mut() {
835                if data.path == old_relative {
836                    data.path = new_relative;
837                    break;
838                }
839            }
840        }
841
842        Ok(())
843    }
844
845    // ========================================================================
846    // File Handle Operations
847    // ========================================================================
848
849    /// Applies POSIX open flags to `OpenOptions`.
850    fn apply_flags(opts: &mut OpenOptions, flags: u32) {
851        let access_mode = flags & libc::O_ACCMODE as u32;
852        match access_mode {
853            x if x == libc::O_RDONLY as u32 => {
854                opts.read(true);
855            }
856            x if x == libc::O_WRONLY as u32 => {
857                opts.write(true);
858            }
859            x if x == libc::O_RDWR as u32 => {
860                opts.read(true).write(true);
861            }
862            _ => {
863                opts.read(true);
864            }
865        }
866
867        if flags & libc::O_APPEND as u32 != 0 {
868            opts.append(true);
869        }
870        if flags & libc::O_TRUNC as u32 != 0 {
871            opts.truncate(true);
872        }
873    }
874
875    /// Opens a file.
876    ///
877    /// # Errors
878    ///
879    /// - [`FsError::Io`] if the file cannot be opened
880    /// - [`FsError::InvalidHandle`] if the inode is invalid
881    pub fn open(&self, inode: u64, flags: u32) -> Result<u64> {
882        let path = self.inode_path(inode)?;
883
884        let mut opts = OpenOptions::new();
885        Self::apply_flags(&mut opts, flags);
886
887        let file = opts.open(&path).map_err(FsError::io)?;
888        let handle = self.alloc_handle();
889
890        {
891            let mut handles = self
892                .handles
893                .write()
894                .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
895            handles.insert(handle, HandleData { file, inode, flags });
896        }
897
898        Ok(handle)
899    }
900
901    /// Reads from a file.
902    ///
903    /// # Errors
904    ///
905    /// - [`FsError::Io`] if read fails
906    /// - [`FsError::InvalidHandle`] if the handle is invalid
907    pub fn read(&self, handle: u64, offset: u64, size: u32) -> Result<Vec<u8>> {
908        let mut handles = self
909            .handles
910            .write()
911            .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
912
913        let data = handles
914            .get_mut(&handle)
915            .ok_or(FsError::InvalidHandle(handle))?;
916
917        data.file
918            .seek(SeekFrom::Start(offset))
919            .map_err(FsError::io)?;
920
921        let mut buf = vec![0u8; size as usize];
922        let n = data.file.read(&mut buf).map_err(FsError::io)?;
923        buf.truncate(n);
924
925        Ok(buf)
926    }
927
928    /// Writes to a file.
929    ///
930    /// # Errors
931    ///
932    /// - [`FsError::Io`] if write fails
933    /// - [`FsError::InvalidHandle`] if the handle is invalid
934    pub fn write(&self, handle: u64, offset: u64, data: &[u8], _flags: u32) -> Result<u32> {
935        let mut handles = self
936            .handles
937            .write()
938            .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
939
940        let handle_data = handles
941            .get_mut(&handle)
942            .ok_or(FsError::InvalidHandle(handle))?;
943
944        handle_data
945            .file
946            .seek(SeekFrom::Start(offset))
947            .map_err(FsError::io)?;
948        let n = handle_data.file.write(data).map_err(FsError::io)?;
949
950        #[allow(clippy::cast_possible_truncation)]
951        Ok(n as u32)
952    }
953
954    /// Flushes a file.
955    ///
956    /// # Errors
957    ///
958    /// - [`FsError::Io`] if flush fails
959    /// - [`FsError::InvalidHandle`] if the handle is invalid
960    pub fn flush(&self, handle: u64) -> Result<()> {
961        let mut handles = self
962            .handles
963            .write()
964            .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
965
966        let data = handles
967            .get_mut(&handle)
968            .ok_or(FsError::InvalidHandle(handle))?;
969        data.file.flush().map_err(FsError::io)
970    }
971
972    /// Syncs a file to disk.
973    ///
974    /// # Errors
975    ///
976    /// - [`FsError::Io`] if sync fails
977    /// - [`FsError::InvalidHandle`] if the handle is invalid
978    pub fn fsync(&self, handle: u64, datasync: bool) -> Result<()> {
979        let handles = self
980            .handles
981            .read()
982            .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
983
984        let data = handles.get(&handle).ok_or(FsError::InvalidHandle(handle))?;
985
986        if datasync {
987            data.file.sync_data().map_err(FsError::io)
988        } else {
989            data.file.sync_all().map_err(FsError::io)
990        }
991    }
992
993    /// Releases (closes) a file handle.
994    pub fn release(&self, handle: u64) -> Result<()> {
995        let mut handles = self
996            .handles
997            .write()
998            .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
999
1000        handles.remove(&handle);
1001        Ok(())
1002    }
1003
1004    /// Seeks in a file (lseek).
1005    ///
1006    /// # Errors
1007    ///
1008    /// - [`FsError::Io`] if seek fails
1009    /// - [`FsError::InvalidHandle`] if the handle is invalid
1010    pub fn lseek(&self, handle: u64, offset: i64, whence: u32) -> Result<u64> {
1011        let mut handles = self
1012            .handles
1013            .write()
1014            .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1015
1016        let data = handles
1017            .get_mut(&handle)
1018            .ok_or(FsError::InvalidHandle(handle))?;
1019
1020        let seek_from = match whence {
1021            0 => SeekFrom::Start(offset as u64), // SEEK_SET
1022            1 => SeekFrom::Current(offset),      // SEEK_CUR
1023            2 => SeekFrom::End(offset),          // SEEK_END
1024            _ => return Err(FsError::InvalidPath("invalid whence".to_string())),
1025        };
1026
1027        data.file.seek(seek_from).map_err(FsError::io)
1028    }
1029
1030    /// Allocates space for a file.
1031    ///
1032    /// # Errors
1033    ///
1034    /// - [`FsError::Io`] if allocation fails
1035    /// - [`FsError::InvalidHandle`] if the handle is invalid
1036    #[cfg(target_os = "linux")]
1037    pub fn fallocate(&self, handle: u64, mode: u32, offset: u64, length: u64) -> Result<()> {
1038        let handles = self
1039            .handles
1040            .read()
1041            .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1042
1043        let data = handles.get(&handle).ok_or(FsError::InvalidHandle(handle))?;
1044        let fd = data.file.as_raw_fd();
1045
1046        #[allow(clippy::cast_possible_wrap)]
1047        let ret = unsafe { libc::fallocate(fd, mode as i32, offset as i64, length as i64) };
1048
1049        if ret != 0 {
1050            Err(FsError::io(std::io::Error::last_os_error()))
1051        } else {
1052            Ok(())
1053        }
1054    }
1055
1056    #[cfg(target_os = "macos")]
1057    pub fn fallocate(&self, handle: u64, _mode: u32, offset: u64, length: u64) -> Result<()> {
1058        // macOS doesn't have fallocate, use ftruncate as fallback for simple cases
1059        let mut handles = self
1060            .handles
1061            .write()
1062            .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1063
1064        let data = handles
1065            .get_mut(&handle)
1066            .ok_or(FsError::InvalidHandle(handle))?;
1067        let new_size = offset + length;
1068        data.file.set_len(new_size).map_err(FsError::io)
1069    }
1070
1071    // ========================================================================
1072    // Directory Operations
1073    // ========================================================================
1074
1075    /// Opens a directory for reading.
1076    ///
1077    /// # Errors
1078    ///
1079    /// - [`FsError::Io`] if the directory cannot be opened
1080    /// - [`FsError::InvalidHandle`] if the inode is invalid
1081    pub fn opendir(&self, inode: u64) -> Result<u64> {
1082        let path = self.inode_path(inode)?;
1083
1084        // Read directory entries
1085        let mut entries = Vec::new();
1086
1087        // Add . and ..
1088        entries.push(DirEntry {
1089            name: OsString::from("."),
1090            ino: inode,
1091            file_type: FileType::Directory,
1092        });
1093
1094        // For .., we need the parent inode
1095        let parent_ino = if inode == Self::ROOT_INODE {
1096            Self::ROOT_INODE
1097        } else {
1098            // Try to find parent, default to root
1099            Self::ROOT_INODE
1100        };
1101        entries.push(DirEntry {
1102            name: OsString::from(".."),
1103            ino: parent_ino,
1104            file_type: FileType::Directory,
1105        });
1106
1107        // Read actual entries
1108        for entry in std::fs::read_dir(&path).map_err(FsError::io)? {
1109            let entry = entry.map_err(FsError::io)?;
1110            let metadata = entry.metadata().map_err(FsError::io)?;
1111            let file_type = FileType::from_mode(metadata.mode());
1112
1113            // Look up or create inode for this entry
1114            let entry_path = entry.path();
1115            let relative = self.relative_path(&entry_path);
1116            let entry_ino = {
1117                let inodes = self
1118                    .inodes
1119                    .read()
1120                    .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
1121                let mut found_ino = None;
1122                for (&ino, data) in inodes.iter() {
1123                    if data.path == relative {
1124                        found_ino = Some(ino);
1125                        break;
1126                    }
1127                }
1128                found_ino
1129            };
1130
1131            let ino = if let Some(ino) = entry_ino {
1132                ino
1133            } else {
1134                // Create new inode
1135                let new_ino = self.alloc_inode();
1136                let mut inodes = self
1137                    .inodes
1138                    .write()
1139                    .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
1140                inodes.insert(new_ino, InodeData::new(relative, file_type));
1141                new_ino
1142            };
1143
1144            entries.push(DirEntry {
1145                name: entry.file_name(),
1146                ino,
1147                file_type,
1148            });
1149        }
1150
1151        let handle = self.alloc_handle();
1152        {
1153            let mut dir_handles = self
1154                .dir_handles
1155                .write()
1156                .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1157            dir_handles.insert(handle, DirHandleData { inode, entries });
1158        }
1159
1160        Ok(handle)
1161    }
1162
1163    /// Reads directory entries.
1164    ///
1165    /// Returns entries starting from `offset`.
1166    ///
1167    /// # Errors
1168    ///
1169    /// - [`FsError::InvalidHandle`] if the handle is invalid
1170    pub fn readdir(&self, handle: u64, offset: u64) -> Result<Vec<DirEntry>> {
1171        let dir_handles = self
1172            .dir_handles
1173            .read()
1174            .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1175
1176        let data = dir_handles
1177            .get(&handle)
1178            .ok_or(FsError::InvalidHandle(handle))?;
1179
1180        let entries: Vec<DirEntry> = data.entries.iter().skip(offset as usize).cloned().collect();
1181
1182        Ok(entries)
1183    }
1184
1185    /// Releases (closes) a directory handle.
1186    pub fn releasedir(&self, handle: u64) -> Result<()> {
1187        let mut dir_handles = self
1188            .dir_handles
1189            .write()
1190            .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1191
1192        dir_handles.remove(&handle);
1193        Ok(())
1194    }
1195
1196    /// Syncs a directory.
1197    ///
1198    /// # Errors
1199    ///
1200    /// - [`FsError::Io`] if sync fails
1201    /// - [`FsError::InvalidHandle`] if the handle is invalid
1202    pub fn fsyncdir(&self, handle: u64, _datasync: bool) -> Result<()> {
1203        let dir_handles = self
1204            .dir_handles
1205            .read()
1206            .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1207
1208        let data = dir_handles
1209            .get(&handle)
1210            .ok_or(FsError::InvalidHandle(handle))?;
1211        let path = self.inode_path(data.inode)?;
1212
1213        // Open directory and sync
1214        let dir = File::open(&path).map_err(FsError::io)?;
1215        dir.sync_all().map_err(FsError::io)
1216    }
1217
1218    // ========================================================================
1219    // Extended Attributes (xattr)
1220    // ========================================================================
1221
1222    /// Gets an extended attribute.
1223    ///
1224    /// # Errors
1225    ///
1226    /// - [`FsError::Io`] if the operation fails
1227    /// - [`FsError::InvalidHandle`] if the inode is invalid
1228    #[cfg(target_os = "linux")]
1229    pub fn getxattr(&self, inode: u64, name: &OsStr, size: u32) -> Result<Vec<u8>> {
1230        use std::os::unix::ffi::OsStrExt;
1231
1232        let path = self.inode_path(inode)?;
1233        let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1234            .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1235        let name_cstr = std::ffi::CString::new(name.as_bytes())
1236            .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1237
1238        if size == 0 {
1239            // Query size
1240            let ret = unsafe {
1241                libc::getxattr(
1242                    path_cstr.as_ptr(),
1243                    name_cstr.as_ptr(),
1244                    std::ptr::null_mut(),
1245                    0,
1246                )
1247            };
1248            if ret < 0 {
1249                return Err(FsError::io(std::io::Error::last_os_error()));
1250            }
1251            Ok(vec![0u8; ret as usize])
1252        } else {
1253            let mut buf = vec![0u8; size as usize];
1254            let ret = unsafe {
1255                libc::getxattr(
1256                    path_cstr.as_ptr(),
1257                    name_cstr.as_ptr(),
1258                    buf.as_mut_ptr().cast(),
1259                    size as usize,
1260                )
1261            };
1262            if ret < 0 {
1263                return Err(FsError::io(std::io::Error::last_os_error()));
1264            }
1265            buf.truncate(ret as usize);
1266            Ok(buf)
1267        }
1268    }
1269
1270    #[cfg(target_os = "macos")]
1271    pub fn getxattr(&self, inode: u64, name: &OsStr, size: u32) -> Result<Vec<u8>> {
1272        let path = self.inode_path(inode)?;
1273        let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1274            .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1275        let name_cstr = std::ffi::CString::new(name.as_bytes())
1276            .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1277
1278        if size == 0 {
1279            let ret = unsafe {
1280                libc::getxattr(
1281                    path_cstr.as_ptr(),
1282                    name_cstr.as_ptr(),
1283                    std::ptr::null_mut(),
1284                    0,
1285                    0,
1286                    0,
1287                )
1288            };
1289            if ret < 0 {
1290                return Err(FsError::io(std::io::Error::last_os_error()));
1291            }
1292            Ok(vec![0u8; ret as usize])
1293        } else {
1294            let mut buf = vec![0u8; size as usize];
1295            let ret = unsafe {
1296                libc::getxattr(
1297                    path_cstr.as_ptr(),
1298                    name_cstr.as_ptr(),
1299                    buf.as_mut_ptr().cast(),
1300                    size as usize,
1301                    0,
1302                    0,
1303                )
1304            };
1305            if ret < 0 {
1306                return Err(FsError::io(std::io::Error::last_os_error()));
1307            }
1308            buf.truncate(ret as usize);
1309            Ok(buf)
1310        }
1311    }
1312
1313    /// Sets an extended attribute.
1314    ///
1315    /// # Errors
1316    ///
1317    /// - [`FsError::Io`] if the operation fails
1318    /// - [`FsError::InvalidHandle`] if the inode is invalid
1319    #[cfg(target_os = "linux")]
1320    pub fn setxattr(&self, inode: u64, name: &OsStr, value: &[u8], flags: u32) -> Result<()> {
1321        let path = self.inode_path(inode)?;
1322        let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1323            .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1324        let name_cstr = std::ffi::CString::new(name.as_bytes())
1325            .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1326
1327        let ret = unsafe {
1328            libc::setxattr(
1329                path_cstr.as_ptr(),
1330                name_cstr.as_ptr(),
1331                value.as_ptr().cast(),
1332                value.len(),
1333                flags as i32,
1334            )
1335        };
1336        if ret != 0 {
1337            Err(FsError::io(std::io::Error::last_os_error()))
1338        } else {
1339            Ok(())
1340        }
1341    }
1342
1343    #[cfg(target_os = "macos")]
1344    pub fn setxattr(&self, inode: u64, name: &OsStr, value: &[u8], flags: u32) -> Result<()> {
1345        let path = self.inode_path(inode)?;
1346        let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1347            .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1348        let name_cstr = std::ffi::CString::new(name.as_bytes())
1349            .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1350
1351        let ret = unsafe {
1352            libc::setxattr(
1353                path_cstr.as_ptr(),
1354                name_cstr.as_ptr(),
1355                value.as_ptr().cast(),
1356                value.len(),
1357                0,
1358                flags as i32,
1359            )
1360        };
1361        if ret != 0 {
1362            Err(FsError::io(std::io::Error::last_os_error()))
1363        } else {
1364            Ok(())
1365        }
1366    }
1367
1368    /// Removes an extended attribute.
1369    ///
1370    /// # Errors
1371    ///
1372    /// - [`FsError::Io`] if the operation fails
1373    /// - [`FsError::InvalidHandle`] if the inode is invalid
1374    #[cfg(target_os = "linux")]
1375    pub fn removexattr(&self, inode: u64, name: &OsStr) -> Result<()> {
1376        let path = self.inode_path(inode)?;
1377        let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1378            .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1379        let name_cstr = std::ffi::CString::new(name.as_bytes())
1380            .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1381
1382        let ret = unsafe { libc::removexattr(path_cstr.as_ptr(), name_cstr.as_ptr()) };
1383        if ret != 0 {
1384            Err(FsError::io(std::io::Error::last_os_error()))
1385        } else {
1386            Ok(())
1387        }
1388    }
1389
1390    #[cfg(target_os = "macos")]
1391    pub fn removexattr(&self, inode: u64, name: &OsStr) -> Result<()> {
1392        let path = self.inode_path(inode)?;
1393        let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1394            .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1395        let name_cstr = std::ffi::CString::new(name.as_bytes())
1396            .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1397
1398        let ret = unsafe { libc::removexattr(path_cstr.as_ptr(), name_cstr.as_ptr(), 0) };
1399        if ret != 0 {
1400            Err(FsError::io(std::io::Error::last_os_error()))
1401        } else {
1402            Ok(())
1403        }
1404    }
1405
1406    // ========================================================================
1407    // Filesystem Information
1408    // ========================================================================
1409
1410    /// Gets filesystem statistics.
1411    ///
1412    /// # Errors
1413    ///
1414    /// - [`FsError::Io`] if statfs fails
1415    pub fn statfs(&self) -> Result<crate::fuse::StatFs> {
1416        let path_cstr = std::ffi::CString::new(self.root.as_os_str().as_bytes())
1417            .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1418
1419        let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
1420        let ret = unsafe { libc::statfs(path_cstr.as_ptr(), &raw mut stat) };
1421        if ret != 0 {
1422            return Err(FsError::io(std::io::Error::last_os_error()));
1423        }
1424
1425        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1426        Ok(crate::fuse::StatFs {
1427            blocks: stat.f_blocks as u64,
1428            bfree: stat.f_bfree as u64,
1429            bavail: stat.f_bavail as u64,
1430            files: stat.f_files as u64,
1431            ffree: stat.f_ffree as u64,
1432            bsize: stat.f_bsize as u32,
1433            namelen: 255, // Common limit
1434            frsize: stat.f_bsize as u32,
1435        })
1436    }
1437
1438    /// Checks file access permissions.
1439    ///
1440    /// # Errors
1441    ///
1442    /// - [`FsError::PermissionDenied`] if access is denied
1443    /// - [`FsError::InvalidHandle`] if the inode is invalid
1444    pub fn access(&self, inode: u64, mask: u32) -> Result<()> {
1445        let path = self.inode_path(inode)?;
1446        let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1447            .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1448
1449        #[allow(clippy::cast_possible_wrap)]
1450        let ret = unsafe { libc::access(path_cstr.as_ptr(), mask as i32) };
1451        if ret != 0 {
1452            let err = std::io::Error::last_os_error();
1453            if err.raw_os_error() == Some(libc::EACCES) {
1454                Err(FsError::permission_denied(path.display().to_string()))
1455            } else {
1456                Err(FsError::io(err))
1457            }
1458        } else {
1459            Ok(())
1460        }
1461    }
1462}
1463
1464impl std::fmt::Debug for PassthroughFs {
1465    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1466        f.debug_struct("PassthroughFs")
1467            .field("root", &self.root)
1468            .field("inodes", &self.inodes.read().map(|i| i.len()).unwrap_or(0))
1469            .field(
1470                "handles",
1471                &self.handles.read().map(|h| h.len()).unwrap_or(0),
1472            )
1473            .field(
1474                "dir_handles",
1475                &self.dir_handles.read().map(|h| h.len()).unwrap_or(0),
1476            )
1477            .finish()
1478    }
1479}
1480
1481// ============================================================================
1482// Tests
1483// ============================================================================
1484
1485#[cfg(test)]
1486mod tests {
1487    use super::*;
1488    use tempfile::TempDir;
1489
1490    const S_IFDIR: u32 = libc::S_IFDIR as u32;
1491    const S_IFREG: u32 = libc::S_IFREG as u32;
1492
1493    fn setup_test_fs() -> (TempDir, PassthroughFs) {
1494        let temp = TempDir::new().expect("failed to create temp dir");
1495        let fs = PassthroughFs::new(temp.path()).expect("failed to create fs");
1496        (temp, fs)
1497    }
1498
1499    #[test]
1500    fn test_new_filesystem() {
1501        let temp = TempDir::new().unwrap();
1502        let fs = PassthroughFs::new(temp.path()).unwrap();
1503        assert_eq!(fs.root(), temp.path());
1504    }
1505
1506    #[test]
1507    fn test_new_invalid_path() {
1508        let result = PassthroughFs::new("/nonexistent/path/12345");
1509        assert!(result.is_err());
1510    }
1511
1512    #[test]
1513    fn test_lookup_existing_file() {
1514        let (temp, fs) = setup_test_fs();
1515
1516        // Create a test file
1517        let file_path = temp.path().join("test.txt");
1518        std::fs::write(&file_path, "hello").unwrap();
1519
1520        // Lookup should succeed
1521        let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("test.txt"));
1522        assert!(result.is_ok());
1523        let (inode, attr) = result.unwrap();
1524        assert!(inode > PassthroughFs::ROOT_INODE);
1525        assert_eq!(attr.size, 5);
1526    }
1527
1528    #[test]
1529    fn test_lookup_nonexistent_file() {
1530        let (_temp, fs) = setup_test_fs();
1531
1532        let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("nonexistent.txt"));
1533        assert!(result.is_err());
1534        assert!(result.unwrap_err().is_not_found());
1535    }
1536
1537    #[test]
1538    fn test_lookup_negative_cache() {
1539        let (_temp, fs) = setup_test_fs();
1540
1541        // First lookup - should miss
1542        let _ = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("missing.txt"));
1543
1544        // Check stats
1545        if let Some(cache) = fs.negative_cache() {
1546            let stats = cache.stats();
1547            assert!(stats.entries > 0 || stats.misses > 0);
1548        }
1549
1550        // Second lookup - should hit cache
1551        let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("missing.txt"));
1552        assert!(result.is_err());
1553        assert!(result.unwrap_err().is_not_found());
1554    }
1555
1556    #[test]
1557    fn test_getattr_root() {
1558        let (_temp, fs) = setup_test_fs();
1559
1560        let result = fs.getattr(PassthroughFs::ROOT_INODE);
1561        assert!(result.is_ok());
1562        let attr = result.unwrap();
1563        assert_eq!(attr.ino, PassthroughFs::ROOT_INODE);
1564        assert!(attr.mode & S_IFDIR != 0);
1565    }
1566
1567    #[test]
1568    fn test_create_and_read_file() {
1569        let (_temp, fs) = setup_test_fs();
1570
1571        // Create file
1572        let (inode, _attr, handle) = fs
1573            .create(
1574                PassthroughFs::ROOT_INODE,
1575                OsStr::new("newfile.txt"),
1576                0o644,
1577                libc::O_RDWR as u32,
1578            )
1579            .unwrap();
1580
1581        assert!(inode > PassthroughFs::ROOT_INODE);
1582
1583        // Write to file
1584        let data = b"hello world";
1585        let written = fs.write(handle, 0, data, 0).unwrap();
1586        assert_eq!(written, data.len() as u32);
1587
1588        // Read back
1589        let read_data = fs.read(handle, 0, 100).unwrap();
1590        assert_eq!(read_data, data);
1591
1592        // Release handle
1593        fs.release(handle).unwrap();
1594    }
1595
1596    #[test]
1597    fn test_mkdir_and_rmdir() {
1598        let (_temp, fs) = setup_test_fs();
1599
1600        // Create directory
1601        let (inode, attr) = fs
1602            .mkdir(PassthroughFs::ROOT_INODE, OsStr::new("testdir"), 0o755)
1603            .unwrap();
1604
1605        assert!(inode > PassthroughFs::ROOT_INODE);
1606        assert!(attr.mode & S_IFDIR != 0);
1607
1608        // Remove directory
1609        fs.rmdir(PassthroughFs::ROOT_INODE, OsStr::new("testdir"))
1610            .unwrap();
1611
1612        // Lookup should fail
1613        let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("testdir"));
1614        assert!(result.is_err());
1615        assert!(result.unwrap_err().is_not_found());
1616    }
1617
1618    #[test]
1619    fn test_symlink_and_readlink() {
1620        let (temp, fs) = setup_test_fs();
1621
1622        // Create a target file
1623        let target = temp.path().join("target.txt");
1624        std::fs::write(&target, "target content").unwrap();
1625
1626        // Create symlink
1627        let (inode, _attr) = fs
1628            .symlink(
1629                PassthroughFs::ROOT_INODE,
1630                OsStr::new("link"),
1631                Path::new("target.txt"),
1632            )
1633            .unwrap();
1634
1635        // Read link
1636        let link_target = fs.readlink(inode).unwrap();
1637        assert_eq!(link_target, Path::new("target.txt"));
1638    }
1639
1640    #[test]
1641    fn test_hard_link() {
1642        let (temp, fs) = setup_test_fs();
1643
1644        // Create original file
1645        let original = temp.path().join("original.txt");
1646        std::fs::write(&original, "content").unwrap();
1647
1648        // Lookup original
1649        let (orig_inode, _) = fs
1650            .lookup(PassthroughFs::ROOT_INODE, OsStr::new("original.txt"))
1651            .unwrap();
1652
1653        // Create hard link
1654        let (link_inode, attr) = fs
1655            .link(
1656                orig_inode,
1657                PassthroughFs::ROOT_INODE,
1658                OsStr::new("hardlink.txt"),
1659            )
1660            .unwrap();
1661
1662        // Hard links share the same inode
1663        assert_eq!(link_inode, orig_inode);
1664        assert!(attr.nlink >= 2);
1665    }
1666
1667    #[test]
1668    fn test_unlink() {
1669        let (temp, fs) = setup_test_fs();
1670
1671        // Create file
1672        let file_path = temp.path().join("todelete.txt");
1673        std::fs::write(&file_path, "delete me").unwrap();
1674
1675        // Unlink
1676        fs.unlink(PassthroughFs::ROOT_INODE, OsStr::new("todelete.txt"))
1677            .unwrap();
1678
1679        // File should be gone
1680        assert!(!file_path.exists());
1681    }
1682
1683    #[test]
1684    fn test_rename() {
1685        let (temp, fs) = setup_test_fs();
1686
1687        // Create file
1688        let old_path = temp.path().join("old.txt");
1689        std::fs::write(&old_path, "content").unwrap();
1690
1691        // Rename
1692        fs.rename(
1693            PassthroughFs::ROOT_INODE,
1694            OsStr::new("old.txt"),
1695            PassthroughFs::ROOT_INODE,
1696            OsStr::new("new.txt"),
1697            0,
1698        )
1699        .unwrap();
1700
1701        // Old path should not exist
1702        assert!(!old_path.exists());
1703        // New path should exist
1704        assert!(temp.path().join("new.txt").exists());
1705    }
1706
1707    #[test]
1708    fn test_opendir_readdir() {
1709        let (temp, fs) = setup_test_fs();
1710
1711        // Create some files
1712        std::fs::write(temp.path().join("file1.txt"), "1").unwrap();
1713        std::fs::write(temp.path().join("file2.txt"), "2").unwrap();
1714        std::fs::create_dir(temp.path().join("subdir")).unwrap();
1715
1716        // Open directory
1717        let handle = fs.opendir(PassthroughFs::ROOT_INODE).unwrap();
1718
1719        // Read entries
1720        let entries = fs.readdir(handle, 0).unwrap();
1721
1722        // Should have at least . .. and our 3 entries
1723        assert!(entries.len() >= 5);
1724
1725        // Check for expected names
1726        let names: Vec<_> = entries
1727            .iter()
1728            .map(|e| e.name.to_string_lossy().to_string())
1729            .collect();
1730        assert!(names.contains(&".".to_string()));
1731        assert!(names.contains(&"..".to_string()));
1732        assert!(names.contains(&"file1.txt".to_string()));
1733        assert!(names.contains(&"file2.txt".to_string()));
1734        assert!(names.contains(&"subdir".to_string()));
1735
1736        // Release
1737        fs.releasedir(handle).unwrap();
1738    }
1739
1740    #[test]
1741    fn test_setattr_size() {
1742        let (temp, fs) = setup_test_fs();
1743
1744        // Create file with content
1745        let file_path = temp.path().join("truncate.txt");
1746        std::fs::write(&file_path, "hello world").unwrap();
1747
1748        let (inode, _) = fs
1749            .lookup(PassthroughFs::ROOT_INODE, OsStr::new("truncate.txt"))
1750            .unwrap();
1751
1752        // Truncate to 5 bytes
1753        let attr = fs
1754            .setattr(inode, None, None, None, Some(5), None, None)
1755            .unwrap();
1756        assert_eq!(attr.size, 5);
1757
1758        // Verify content
1759        let content = std::fs::read_to_string(&file_path).unwrap();
1760        assert_eq!(content, "hello");
1761    }
1762
1763    #[test]
1764    fn test_setattr_mode() {
1765        let (temp, fs) = setup_test_fs();
1766
1767        let file_path = temp.path().join("chmod.txt");
1768        std::fs::write(&file_path, "test").unwrap();
1769
1770        let (inode, _) = fs
1771            .lookup(PassthroughFs::ROOT_INODE, OsStr::new("chmod.txt"))
1772            .unwrap();
1773
1774        // Change mode
1775        let attr = fs
1776            .setattr(inode, Some(0o600), None, None, None, None, None)
1777            .unwrap();
1778        assert_eq!(attr.mode & 0o777, 0o600);
1779    }
1780
1781    #[test]
1782    fn test_open_read_write() {
1783        let (temp, fs) = setup_test_fs();
1784
1785        // Create file
1786        let file_path = temp.path().join("rw.txt");
1787        std::fs::write(&file_path, "initial").unwrap();
1788
1789        let (inode, _) = fs
1790            .lookup(PassthroughFs::ROOT_INODE, OsStr::new("rw.txt"))
1791            .unwrap();
1792
1793        // Open for read/write
1794        let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
1795
1796        // Read
1797        let data = fs.read(handle, 0, 100).unwrap();
1798        assert_eq!(data, b"initial");
1799
1800        // Write at offset
1801        fs.write(handle, 0, b"INITIAL", 0).unwrap();
1802
1803        // Read again
1804        let data = fs.read(handle, 0, 100).unwrap();
1805        assert_eq!(data, b"INITIAL");
1806
1807        fs.release(handle).unwrap();
1808    }
1809
1810    #[test]
1811    fn test_fsync() {
1812        let (temp, fs) = setup_test_fs();
1813
1814        let file_path = temp.path().join("sync.txt");
1815        std::fs::write(&file_path, "test").unwrap();
1816
1817        let (inode, _) = fs
1818            .lookup(PassthroughFs::ROOT_INODE, OsStr::new("sync.txt"))
1819            .unwrap();
1820
1821        let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
1822        fs.write(handle, 0, b"updated", 0).unwrap();
1823
1824        // Sync should succeed
1825        fs.fsync(handle, false).unwrap();
1826        fs.fsync(handle, true).unwrap();
1827
1828        fs.release(handle).unwrap();
1829    }
1830
1831    #[test]
1832    fn test_flush() {
1833        let (temp, fs) = setup_test_fs();
1834
1835        let file_path = temp.path().join("flush.txt");
1836        std::fs::write(&file_path, "test").unwrap();
1837
1838        let (inode, _) = fs
1839            .lookup(PassthroughFs::ROOT_INODE, OsStr::new("flush.txt"))
1840            .unwrap();
1841
1842        let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
1843        fs.write(handle, 0, b"updated", 0).unwrap();
1844        fs.flush(handle).unwrap();
1845        fs.release(handle).unwrap();
1846    }
1847
1848    #[test]
1849    fn test_lseek() {
1850        let (temp, fs) = setup_test_fs();
1851
1852        let file_path = temp.path().join("seek.txt");
1853        std::fs::write(&file_path, "0123456789").unwrap();
1854
1855        let (inode, _) = fs
1856            .lookup(PassthroughFs::ROOT_INODE, OsStr::new("seek.txt"))
1857            .unwrap();
1858
1859        let handle = fs.open(inode, libc::O_RDONLY as u32).unwrap();
1860
1861        // Seek to offset 5
1862        let pos = fs.lseek(handle, 5, 0).unwrap(); // SEEK_SET
1863        assert_eq!(pos, 5);
1864
1865        // Read from there
1866        let data = fs.read(handle, pos, 5).unwrap();
1867        assert_eq!(data, b"56789");
1868
1869        fs.release(handle).unwrap();
1870    }
1871
1872    #[test]
1873    fn test_statfs() {
1874        let (_temp, fs) = setup_test_fs();
1875
1876        let stat = fs.statfs().unwrap();
1877        assert!(stat.blocks > 0);
1878        assert!(stat.bsize > 0);
1879    }
1880
1881    #[test]
1882    fn test_access() {
1883        let (temp, fs) = setup_test_fs();
1884
1885        let file_path = temp.path().join("access.txt");
1886        std::fs::write(&file_path, "test").unwrap();
1887
1888        let (inode, _) = fs
1889            .lookup(PassthroughFs::ROOT_INODE, OsStr::new("access.txt"))
1890            .unwrap();
1891
1892        // Should be readable
1893        fs.access(inode, libc::R_OK as u32).unwrap();
1894
1895        // Should be writable
1896        fs.access(inode, libc::W_OK as u32).unwrap();
1897    }
1898
1899    #[test]
1900    fn test_forget() {
1901        let (temp, fs) = setup_test_fs();
1902
1903        let file_path = temp.path().join("forget.txt");
1904        std::fs::write(&file_path, "test").unwrap();
1905
1906        let (inode, _) = fs
1907            .lookup(PassthroughFs::ROOT_INODE, OsStr::new("forget.txt"))
1908            .unwrap();
1909
1910        // Lookup again to increase refcount
1911        let (inode2, _) = fs
1912            .lookup(PassthroughFs::ROOT_INODE, OsStr::new("forget.txt"))
1913            .unwrap();
1914        assert_eq!(inode, inode2);
1915
1916        // Forget once
1917        fs.forget(inode, 1);
1918
1919        // Should still be in table
1920        assert!(fs.getattr(inode).is_ok());
1921
1922        // Forget again
1923        fs.forget(inode, 1);
1924
1925        // May or may not be removed depending on timing
1926    }
1927
1928    #[test]
1929    #[ignore = "mknod requires elevated permissions on macOS"]
1930    fn test_mknod_regular_file() {
1931        let (_temp, fs) = setup_test_fs();
1932
1933        // Create regular file via mknod
1934        let (inode, attr) = fs
1935            .mknod(
1936                PassthroughFs::ROOT_INODE,
1937                OsStr::new("mknod_file"),
1938                S_IFREG | 0o644,
1939                0,
1940            )
1941            .unwrap();
1942
1943        assert!(inode > PassthroughFs::ROOT_INODE);
1944        assert!(attr.mode & S_IFREG != 0);
1945    }
1946
1947    #[test]
1948    fn test_concurrent_operations() {
1949        use std::sync::Arc;
1950        use std::thread;
1951
1952        let (temp, fs) = setup_test_fs();
1953        let fs = Arc::new(fs);
1954
1955        // Create some initial files
1956        for i in 0..10 {
1957            std::fs::write(
1958                temp.path().join(format!("file{i}.txt")),
1959                format!("content{i}"),
1960            )
1961            .unwrap();
1962        }
1963
1964        let mut handles = vec![];
1965
1966        // Spawn threads doing lookups
1967        for i in 0..4 {
1968            let fs = Arc::clone(&fs);
1969            handles.push(thread::spawn(move || {
1970                for j in 0..100 {
1971                    let name = format!("file{}.txt", (i + j) % 10);
1972                    let _ = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new(&name));
1973                }
1974            }));
1975        }
1976
1977        // Spawn threads doing reads
1978        for i in 0..4 {
1979            let fs = Arc::clone(&fs);
1980            handles.push(thread::spawn(move || {
1981                for j in 0..50 {
1982                    let name = format!("file{}.txt", (i + j) % 10);
1983                    if let Ok((inode, _)) = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new(&name))
1984                    {
1985                        if let Ok(handle) = fs.open(inode, libc::O_RDONLY as u32) {
1986                            let _ = fs.read(handle, 0, 100);
1987                            let _ = fs.release(handle);
1988                        }
1989                    }
1990                }
1991            }));
1992        }
1993
1994        for handle in handles {
1995            handle.join().expect("Thread panicked");
1996        }
1997    }
1998}