Skip to main content

embeddenator_fs/fs/
fuse_shim.rs

1//! FUSE Filesystem Shim for Holographic Engrams
2//!
3//! This module provides kernel integration via FUSE (Filesystem in Userspace),
4//! allowing engrams to be mounted as native filesystems.
5//!
6//! # Architecture
7//!
8//! ```text
9//! ┌────────────────────────────────────────────────────────────────┐
10//! │                     User Applications                          │
11//! │                    (read, write, stat, etc.)                   │
12//! └────────────────────────────────────────────────────────────────┘
13//!                                  │ VFS syscalls
14//!                                  ▼
15//! ┌────────────────────────────────────────────────────────────────┐
16//! │                     Linux Kernel VFS                           │
17//! └────────────────────────────────────────────────────────────────┘
18//!                                  │ FUSE protocol
19//!                                  ▼
20//! ┌────────────────────────────────────────────────────────────────┐
21//! │                   /dev/fuse character device                   │
22//! └────────────────────────────────────────────────────────────────┘
23//!                                  │ libfuse / fuser crate
24//!                                  ▼
25//! ┌────────────────────────────────────────────────────────────────┐
26//! │                    EngramFS (this module)                      │
27//! │  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐  │
28//! │  │   Manifest   │  │   Codebook   │  │   Differential       │  │
29//! │  │   (paths)    │  │   (private)  │  │   Decoder            │  │
30//! │  └──────────────┘  └──────────────┘  └──────────────────────┘  │
31//! └────────────────────────────────────────────────────────────────┘
32//!                                  │
33//!                                  ▼
34//! ┌────────────────────────────────────────────────────────────────┐
35//! │                    Engram Storage                              │
36//! │            (on-disk or memory-mapped .engram file)             │
37//! └────────────────────────────────────────────────────────────────┘
38//! ```
39//!
40//! # Usage
41//!
42//! ```bash
43//! # Mount an engram (requires --features fuse)
44//! embeddenator mount --engram root.engram --manifest manifest.json /mnt/engram
45//!
46//! # Access files normally
47//! ls /mnt/engram
48//! cat /mnt/engram/some/file.txt
49//!
50//! # Unmount
51//! fusermount -u /mnt/engram
52//! ```
53//!
54//! # Feature Flag
55//!
56//! The FUSE integration requires the `fuse` feature to be enabled:
57//!
58//! ```toml
59//! [dependencies]
60//! embeddenator = { version = "0.2", features = ["fuse"] }
61//! ```
62
63use std::collections::HashMap;
64use std::sync::{Arc, RwLock};
65use std::time::{Duration, SystemTime, UNIX_EPOCH};
66
67#[cfg(feature = "fuse")]
68use std::ffi::OsStr;
69
70#[cfg(feature = "fuse")]
71use std::path::Path;
72
73/// Inode number type (matches fuser's u64 inode convention)
74pub type Ino = u64;
75
76/// Root inode number (FUSE convention: inode 1 is root)
77pub const ROOT_INO: Ino = 1;
78
79/// File attributes for FUSE
80///
81/// This mirrors fuser::FileAttr but is always available regardless
82/// of feature flags, allowing the core filesystem logic to work
83/// without the fuser crate.
84#[derive(Clone, Debug)]
85pub struct FileAttr {
86    /// Inode number
87    pub ino: Ino,
88    /// File size in bytes
89    pub size: u64,
90    /// Number of 512-byte blocks allocated
91    pub blocks: u64,
92    /// Last access time
93    pub atime: SystemTime,
94    /// Last modification time
95    pub mtime: SystemTime,
96    /// Last status change time
97    pub ctime: SystemTime,
98    /// Creation time (macOS only)
99    pub crtime: SystemTime,
100    /// File type
101    pub kind: FileKind,
102    /// Permissions (mode & 0o7777)
103    pub perm: u16,
104    /// Hard link count
105    pub nlink: u32,
106    /// User ID of owner
107    pub uid: u32,
108    /// Group ID of owner
109    pub gid: u32,
110    /// Device ID (for special files)
111    pub rdev: u32,
112    /// Block size for filesystem I/O
113    pub blksize: u32,
114    /// Flags (macOS only)
115    pub flags: u32,
116}
117
118impl Default for FileAttr {
119    fn default() -> Self {
120        let now = SystemTime::now();
121        FileAttr {
122            ino: 0,
123            size: 0,
124            blocks: 0,
125            atime: now,
126            mtime: now,
127            ctime: now,
128            crtime: now,
129            kind: FileKind::RegularFile,
130            perm: 0o644,
131            nlink: 1,
132            uid: unsafe { libc::getuid() },
133            gid: unsafe { libc::getgid() },
134            rdev: 0,
135            blksize: 4096,
136            flags: 0,
137        }
138    }
139}
140
141#[cfg(feature = "fuse")]
142impl From<FileAttr> for fuser::FileAttr {
143    fn from(attr: FileAttr) -> Self {
144        fuser::FileAttr {
145            ino: attr.ino,
146            size: attr.size,
147            blocks: attr.blocks,
148            atime: attr.atime,
149            mtime: attr.mtime,
150            ctime: attr.ctime,
151            crtime: attr.crtime,
152            kind: attr.kind.into(),
153            perm: attr.perm,
154            nlink: attr.nlink,
155            uid: attr.uid,
156            gid: attr.gid,
157            rdev: attr.rdev,
158            blksize: attr.blksize,
159            flags: attr.flags,
160        }
161    }
162}
163
164/// File type
165#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
166pub enum FileKind {
167    /// Directory
168    Directory,
169    /// Regular file
170    #[default]
171    RegularFile,
172    /// Symbolic link
173    Symlink,
174    /// Hard link (treated as regular file for FUSE, but tracked separately)
175    Hardlink,
176    /// Character device
177    CharDevice,
178    /// Block device
179    BlockDevice,
180    /// FIFO (named pipe)
181    Fifo,
182    /// Unix socket
183    Socket,
184}
185
186#[cfg(feature = "fuse")]
187impl From<FileKind> for fuser::FileType {
188    fn from(kind: FileKind) -> Self {
189        match kind {
190            FileKind::Directory => fuser::FileType::Directory,
191            FileKind::RegularFile => fuser::FileType::RegularFile,
192            FileKind::Symlink => fuser::FileType::Symlink,
193            FileKind::Hardlink => fuser::FileType::RegularFile, // Hardlinks appear as regular files
194            FileKind::CharDevice => fuser::FileType::CharDevice,
195            FileKind::BlockDevice => fuser::FileType::BlockDevice,
196            FileKind::Fifo => fuser::FileType::NamedPipe,
197            FileKind::Socket => fuser::FileType::Socket,
198        }
199    }
200}
201
202/// Directory entry
203#[derive(Clone, Debug)]
204pub struct DirEntry {
205    /// Inode number
206    pub ino: Ino,
207    /// Entry name
208    pub name: String,
209    /// Entry type
210    pub kind: FileKind,
211}
212
213/// Symlink data storage
214#[derive(Clone, Debug)]
215pub struct SymlinkEntry {
216    /// Symlink target path
217    pub target: String,
218}
219
220/// Cached file data for read operations
221#[derive(Clone)]
222pub struct CachedFile {
223    /// File content
224    pub data: Vec<u8>,
225    /// File attributes
226    pub attr: FileAttr,
227}
228
229/// Device node metadata (for char/block devices)
230#[derive(Clone, Debug)]
231pub struct DeviceNode {
232    /// Major device number
233    pub major: u32,
234    /// Minor device number
235    pub minor: u32,
236}
237
238/// The EngramFS FUSE filesystem implementation
239///
240/// This provides a read-only view of decoded engram data as a standard
241/// POSIX filesystem. Files are decoded on-demand from the holographic
242/// representation and cached for efficient repeated access.
243pub struct EngramFS {
244    /// Inode to file attributes mapping
245    inodes: Arc<RwLock<HashMap<Ino, FileAttr>>>,
246
247    /// Inode to path mapping
248    inode_paths: Arc<RwLock<HashMap<Ino, String>>>,
249
250    /// Path to inode mapping
251    path_inodes: Arc<RwLock<HashMap<String, Ino>>>,
252
253    /// Directory contents (parent_ino -> entries)
254    directories: Arc<RwLock<HashMap<Ino, Vec<DirEntry>>>>,
255
256    /// Cached file data (ino -> data)
257    file_cache: Arc<RwLock<HashMap<Ino, CachedFile>>>,
258
259    /// Symlink targets (ino -> target path)
260    symlinks: Arc<RwLock<HashMap<Ino, String>>>,
261
262    /// Device nodes (ino -> major/minor)
263    devices: Arc<RwLock<HashMap<Ino, DeviceNode>>>,
264
265    /// Next available inode number
266    next_ino: Arc<RwLock<Ino>>,
267
268    /// Read-only mode
269    read_only: bool,
270
271    /// TTL for cached attributes
272    attr_ttl: Duration,
273
274    /// TTL for cached entries
275    entry_ttl: Duration,
276}
277
278impl EngramFS {
279    /// Create a new EngramFS instance
280    ///
281    /// # Arguments
282    ///
283    /// * `read_only` - Whether the filesystem is read-only (default: true for engrams)
284    pub fn new(read_only: bool) -> Self {
285        let mut fs = EngramFS {
286            inodes: Arc::new(RwLock::new(HashMap::new())),
287            inode_paths: Arc::new(RwLock::new(HashMap::new())),
288            path_inodes: Arc::new(RwLock::new(HashMap::new())),
289            directories: Arc::new(RwLock::new(HashMap::new())),
290            file_cache: Arc::new(RwLock::new(HashMap::new())),
291            symlinks: Arc::new(RwLock::new(HashMap::new())),
292            devices: Arc::new(RwLock::new(HashMap::new())),
293            next_ino: Arc::new(RwLock::new(2)), // Start after root
294            read_only,
295            attr_ttl: Duration::from_secs(1),
296            entry_ttl: Duration::from_secs(1),
297        };
298
299        // Initialize root directory
300        fs.init_root();
301        fs
302    }
303
304    /// Initialize root directory
305    fn init_root(&mut self) {
306        let root_attr = FileAttr {
307            ino: ROOT_INO,
308            size: 0,
309            blocks: 0,
310            kind: FileKind::Directory,
311            perm: 0o755,
312            nlink: 2,
313            ..Default::default()
314        };
315
316        // SAFETY: init_root only called during construction, before any concurrent access
317        // If locks are poisoned here, the filesystem is unrecoverable anyway
318        self.inodes
319            .write()
320            .expect("Lock poisoned during init")
321            .insert(ROOT_INO, root_attr);
322        self.inode_paths
323            .write()
324            .expect("Lock poisoned during init")
325            .insert(ROOT_INO, "/".to_string());
326        self.path_inodes
327            .write()
328            .expect("Lock poisoned during init")
329            .insert("/".to_string(), ROOT_INO);
330        self.directories
331            .write()
332            .expect("Lock poisoned during init")
333            .insert(ROOT_INO, Vec::new());
334    }
335
336    /// Allocate a new inode number
337    fn alloc_ino(&self) -> Result<Ino, &'static str> {
338        let mut next = self
339            .next_ino
340            .write()
341            .map_err(|_| "Inode allocator lock poisoned")?;
342        let ino = *next;
343        *next += 1;
344        Ok(ino)
345    }
346
347    /// Add a file to the filesystem
348    ///
349    /// # Arguments
350    ///
351    /// * `path` - Absolute path within the filesystem (e.g., "/foo/bar.txt")
352    /// * `data` - File content bytes
353    ///
354    /// # Returns
355    ///
356    /// The assigned inode number for the new file
357    pub fn add_file(&self, path: &str, data: Vec<u8>) -> Result<Ino, &'static str> {
358        let path = normalize_path(path);
359
360        // Check if already exists
361        let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
362            eprintln!("WARNING: path_inodes lock poisoned, recovering...");
363            poisoned.into_inner()
364        });
365        if path_inodes.contains_key(&path) {
366            return Err("File already exists");
367        }
368        drop(path_inodes);
369
370        // Ensure parent directory exists
371        let parent_path = parent_path(&path).ok_or("Invalid path")?;
372        let parent_ino = self.ensure_directory(&parent_path)?;
373
374        // Create file
375        let ino = self.alloc_ino()?;
376        let size = data.len() as u64;
377
378        let attr = FileAttr {
379            ino,
380            size,
381            blocks: size.div_ceil(512),
382            kind: FileKind::RegularFile,
383            perm: 0o644,
384            nlink: 1,
385            ..Default::default()
386        };
387
388        // Store file
389        self.inodes
390            .write()
391            .map_err(|_| "Inodes lock poisoned")?
392            .insert(ino, attr.clone());
393        self.inode_paths
394            .write()
395            .map_err(|_| "Inode paths lock poisoned")?
396            .insert(ino, path.clone());
397        self.path_inodes
398            .write()
399            .map_err(|_| "Path inodes lock poisoned")?
400            .insert(path.clone(), ino);
401        self.file_cache
402            .write()
403            .map_err(|_| "File cache lock poisoned")?
404            .insert(ino, CachedFile { data, attr });
405
406        // Add to parent directory
407        let filename = filename(&path).ok_or("Invalid filename")?;
408        self.directories
409            .write()
410            .map_err(|_| "Directories lock poisoned")?
411            .get_mut(&parent_ino)
412            .ok_or("Parent directory not found")?
413            .push(DirEntry {
414                ino,
415                name: filename.to_string(),
416                kind: FileKind::RegularFile,
417            });
418
419        Ok(ino)
420    }
421
422    /// Ensure a directory exists, creating it if necessary
423    fn ensure_directory(&self, path: &str) -> Result<Ino, &'static str> {
424        let path = normalize_path(path);
425
426        // Root always exists
427        if path == "/" {
428            return Ok(ROOT_INO);
429        }
430
431        // Check if already exists
432        let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
433            eprintln!("WARNING: path_inodes lock poisoned in ensure_directory, recovering...");
434            poisoned.into_inner()
435        });
436        if let Some(&ino) = path_inodes.get(&path) {
437            return Ok(ino);
438        }
439        drop(path_inodes);
440
441        // Create parent first
442        let parent_path = parent_path(&path).ok_or("Invalid path")?;
443        let parent_ino = self.ensure_directory(&parent_path)?;
444
445        // Create this directory
446        let ino = self.alloc_ino()?;
447        let attr = FileAttr {
448            ino,
449            size: 0,
450            blocks: 0,
451            kind: FileKind::Directory,
452            perm: 0o755,
453            nlink: 2,
454            ..Default::default()
455        };
456
457        self.inodes
458            .write()
459            .map_err(|_| "Inodes lock poisoned")?
460            .insert(ino, attr);
461        self.inode_paths
462            .write()
463            .map_err(|_| "Inode paths lock poisoned")?
464            .insert(ino, path.clone());
465        self.path_inodes
466            .write()
467            .map_err(|_| "Path inodes lock poisoned")?
468            .insert(path.clone(), ino);
469        self.directories
470            .write()
471            .map_err(|_| "Directories lock poisoned")?
472            .insert(ino, Vec::new());
473
474        // Add to parent
475        let dirname = filename(&path).ok_or("Invalid dirname")?;
476        self.directories
477            .write()
478            .map_err(|_| "Directories lock poisoned")?
479            .get_mut(&parent_ino)
480            .ok_or("Parent not found")?
481            .push(DirEntry {
482                ino,
483                name: dirname.to_string(),
484                kind: FileKind::Directory,
485            });
486
487        // Update parent nlink
488        if let Some(parent_attr) = self
489            .inodes
490            .write()
491            .map_err(|_| "Inodes lock poisoned")?
492            .get_mut(&parent_ino)
493        {
494            parent_attr.nlink += 1;
495        }
496
497        Ok(ino)
498    }
499
500    /// Lookup a path and return its inode
501    pub fn lookup_path(&self, path: &str) -> Option<Ino> {
502        let path = normalize_path(path);
503        let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
504            eprintln!("WARNING: path_inodes lock poisoned in lookup_path, recovering...");
505            poisoned.into_inner()
506        });
507        path_inodes.get(&path).copied()
508    }
509
510    /// Get file attributes by inode
511    pub fn get_attr(&self, ino: Ino) -> Option<FileAttr> {
512        let inodes = self.inodes.read().unwrap_or_else(|poisoned| {
513            eprintln!("WARNING: inodes lock poisoned in get_attr, recovering...");
514            poisoned.into_inner()
515        });
516        inodes.get(&ino).cloned()
517    }
518
519    /// Read file data
520    pub fn read_data(&self, ino: Ino, offset: u64, size: u32) -> Option<Vec<u8>> {
521        let cache = self.file_cache.read().unwrap_or_else(|poisoned| {
522            eprintln!("WARNING: file_cache lock poisoned in read_data, recovering...");
523            poisoned.into_inner()
524        });
525        let cached = cache.get(&ino)?;
526
527        let start = offset as usize;
528        let end = std::cmp::min(start + size as usize, cached.data.len());
529
530        if start >= cached.data.len() {
531            return Some(Vec::new());
532        }
533
534        Some(cached.data[start..end].to_vec())
535    }
536
537    /// Read directory contents
538    pub fn read_dir(&self, ino: Ino) -> Option<Vec<DirEntry>> {
539        let directories = self.directories.read().unwrap_or_else(|poisoned| {
540            eprintln!("WARNING: directories lock poisoned in read_dir, recovering...");
541            poisoned.into_inner()
542        });
543        directories.get(&ino).cloned()
544    }
545
546    /// Lookup entry in directory by name
547    pub fn lookup_entry(&self, parent_ino: Ino, name: &str) -> Option<Ino> {
548        let dirs = self.directories.read().unwrap_or_else(|poisoned| {
549            eprintln!("WARNING: directories lock poisoned in lookup_entry, recovering...");
550            poisoned.into_inner()
551        });
552        let entries = dirs.get(&parent_ino)?;
553        entries.iter().find(|e| e.name == name).map(|e| e.ino)
554    }
555
556    /// Get parent inode for a given inode
557    pub fn get_parent(&self, ino: Ino) -> Option<Ino> {
558        if ino == ROOT_INO {
559            return Some(ROOT_INO); // Root's parent is itself
560        }
561
562        let paths = self.inode_paths.read().unwrap_or_else(|poisoned| {
563            eprintln!("WARNING: inode_paths lock poisoned in get_parent, recovering...");
564            poisoned.into_inner()
565        });
566        let path = paths.get(&ino)?;
567        let parent = parent_path(path)?;
568
569        let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
570            eprintln!("WARNING: path_inodes lock poisoned in get_parent, recovering...");
571            poisoned.into_inner()
572        });
573        path_inodes.get(&parent).copied()
574    }
575
576    /// Get total number of files
577    pub fn file_count(&self) -> usize {
578        let cache = self.file_cache.read().unwrap_or_else(|poisoned| {
579            eprintln!("WARNING: file_cache lock poisoned in file_count, recovering...");
580            poisoned.into_inner()
581        });
582        cache.len()
583    }
584
585    /// Get total size of all files
586    pub fn total_size(&self) -> u64 {
587        let cache = self.file_cache.read().unwrap_or_else(|poisoned| {
588            eprintln!("WARNING: file_cache lock poisoned in total_size, recovering...");
589            poisoned.into_inner()
590        });
591        cache.values().map(|f| f.attr.size).sum()
592    }
593
594    /// Check if filesystem is read-only
595    pub fn is_read_only(&self) -> bool {
596        self.read_only
597    }
598
599    /// Get attribute TTL
600    pub fn attr_ttl(&self) -> Duration {
601        self.attr_ttl
602    }
603
604    /// Get entry TTL
605    pub fn entry_ttl(&self) -> Duration {
606        self.entry_ttl
607    }
608
609    /// Add a symbolic link to the filesystem
610    ///
611    /// # Arguments
612    ///
613    /// * `path` - Absolute path within the filesystem (e.g., "/lib/libc.so.6")
614    /// * `target` - The symlink target path (can be relative or absolute)
615    ///
616    /// # Returns
617    ///
618    /// The assigned inode number for the new symlink
619    pub fn add_symlink(&self, path: &str, target: String) -> Result<Ino, &'static str> {
620        let path = normalize_path(path);
621
622        // Check if already exists
623        let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
624            eprintln!("WARNING: path_inodes lock poisoned in add_symlink, recovering...");
625            poisoned.into_inner()
626        });
627        if path_inodes.contains_key(&path) {
628            return Err("Symlink already exists");
629        }
630        drop(path_inodes);
631
632        // Ensure parent directory exists
633        let parent_path = parent_path(&path).ok_or("Invalid path")?;
634        let parent_ino = self.ensure_directory(&parent_path)?;
635
636        // Create symlink
637        let ino = self.alloc_ino()?;
638        let size = target.len() as u64; // Symlink size is the target path length
639
640        let attr = FileAttr {
641            ino,
642            size,
643            blocks: 0,
644            kind: FileKind::Symlink,
645            perm: 0o777, // Symlinks typically have 777 permissions
646            nlink: 1,
647            ..Default::default()
648        };
649
650        // Store symlink
651        self.inodes
652            .write()
653            .map_err(|_| "Inodes lock poisoned")?
654            .insert(ino, attr);
655        self.inode_paths
656            .write()
657            .map_err(|_| "Inode paths lock poisoned")?
658            .insert(ino, path.clone());
659        self.path_inodes
660            .write()
661            .map_err(|_| "Path inodes lock poisoned")?
662            .insert(path.clone(), ino);
663        self.symlinks
664            .write()
665            .map_err(|_| "Symlinks lock poisoned")?
666            .insert(ino, target);
667
668        // Add to parent directory
669        let filename = filename(&path).ok_or("Invalid filename")?;
670        self.directories
671            .write()
672            .map_err(|_| "Directories lock poisoned")?
673            .get_mut(&parent_ino)
674            .ok_or("Parent directory not found")?
675            .push(DirEntry {
676                ino,
677                name: filename.to_string(),
678                kind: FileKind::Symlink,
679            });
680
681        Ok(ino)
682    }
683
684    /// Add a device node to the filesystem (Option C: store device data)
685    ///
686    /// # Arguments
687    ///
688    /// * `path` - Absolute path within the filesystem (e.g., "/dev/null")
689    /// * `is_char` - true for character device, false for block device
690    /// * `major` - Major device number
691    /// * `minor` - Minor device number
692    /// * `data` - Device data content (Option C encoding)
693    ///
694    /// # Returns
695    ///
696    /// The assigned inode number for the new device
697    pub fn add_device(
698        &self,
699        path: &str,
700        is_char: bool,
701        major: u32,
702        minor: u32,
703        data: Vec<u8>,
704    ) -> Result<Ino, &'static str> {
705        let path = normalize_path(path);
706
707        // Check if already exists
708        let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
709            eprintln!("WARNING: path_inodes lock poisoned in add_device, recovering...");
710            poisoned.into_inner()
711        });
712        if path_inodes.contains_key(&path) {
713            return Err("Device already exists");
714        }
715        drop(path_inodes);
716
717        // Ensure parent directory exists
718        let parent_path = parent_path(&path).ok_or("Invalid path")?;
719        let parent_ino = self.ensure_directory(&parent_path)?;
720
721        // Create device node
722        let ino = self.alloc_ino()?;
723        let size = data.len() as u64;
724        let kind = if is_char {
725            FileKind::CharDevice
726        } else {
727            FileKind::BlockDevice
728        };
729
730        let attr = FileAttr {
731            ino,
732            size,
733            blocks: size.div_ceil(512),
734            kind,
735            perm: 0o666,
736            nlink: 1,
737            rdev: (major << 8) | minor, // Encode major/minor in rdev
738            ..Default::default()
739        };
740
741        // Store device node
742        self.inodes
743            .write()
744            .map_err(|_| "Inodes lock poisoned")?
745            .insert(ino, attr.clone());
746        self.inode_paths
747            .write()
748            .map_err(|_| "Inode paths lock poisoned")?
749            .insert(ino, path.clone());
750        self.path_inodes
751            .write()
752            .map_err(|_| "Path inodes lock poisoned")?
753            .insert(path.clone(), ino);
754        self.devices
755            .write()
756            .map_err(|_| "Devices lock poisoned")?
757            .insert(ino, DeviceNode { major, minor });
758
759        // Store device data (Option C)
760        self.file_cache
761            .write()
762            .map_err(|_| "File cache lock poisoned")?
763            .insert(ino, CachedFile { data, attr });
764
765        // Add to parent directory
766        let filename = filename(&path).ok_or("Invalid filename")?;
767        self.directories
768            .write()
769            .map_err(|_| "Directories lock poisoned")?
770            .get_mut(&parent_ino)
771            .ok_or("Parent directory not found")?
772            .push(DirEntry {
773                ino,
774                name: filename.to_string(),
775                kind,
776            });
777
778        Ok(ino)
779    }
780
781    /// Add a FIFO (named pipe) to the filesystem
782    pub fn add_fifo(&self, path: &str) -> Result<Ino, &'static str> {
783        self.add_special_file(path, FileKind::Fifo)
784    }
785
786    /// Add a Unix socket to the filesystem
787    pub fn add_socket(&self, path: &str) -> Result<Ino, &'static str> {
788        self.add_special_file(path, FileKind::Socket)
789    }
790
791    /// Internal helper to add special files (FIFO, socket)
792    fn add_special_file(&self, path: &str, kind: FileKind) -> Result<Ino, &'static str> {
793        let path = normalize_path(path);
794
795        // Check if already exists
796        let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
797            eprintln!("WARNING: path_inodes lock poisoned in add_special_file, recovering...");
798            poisoned.into_inner()
799        });
800        if path_inodes.contains_key(&path) {
801            return Err("Special file already exists");
802        }
803        drop(path_inodes);
804
805        // Ensure parent directory exists
806        let parent_path = parent_path(&path).ok_or("Invalid path")?;
807        let parent_ino = self.ensure_directory(&parent_path)?;
808
809        // Create special file
810        let ino = self.alloc_ino()?;
811
812        let attr = FileAttr {
813            ino,
814            size: 0,
815            blocks: 0,
816            kind,
817            perm: 0o666,
818            nlink: 1,
819            ..Default::default()
820        };
821
822        // Store special file
823        self.inodes
824            .write()
825            .map_err(|_| "Inodes lock poisoned")?
826            .insert(ino, attr);
827        self.inode_paths
828            .write()
829            .map_err(|_| "Inode paths lock poisoned")?
830            .insert(ino, path.clone());
831        self.path_inodes
832            .write()
833            .map_err(|_| "Path inodes lock poisoned")?
834            .insert(path.clone(), ino);
835
836        // Add to parent directory
837        let filename = filename(&path).ok_or("Invalid filename")?;
838        self.directories
839            .write()
840            .map_err(|_| "Directories lock poisoned")?
841            .get_mut(&parent_ino)
842            .ok_or("Parent directory not found")?
843            .push(DirEntry {
844                ino,
845                name: filename.to_string(),
846                kind,
847            });
848
849        Ok(ino)
850    }
851
852    /// Read symlink target
853    pub fn read_symlink(&self, ino: Ino) -> Option<String> {
854        let symlinks = self.symlinks.read().unwrap_or_else(|poisoned| {
855            eprintln!("WARNING: symlinks lock poisoned in read_symlink, recovering...");
856            poisoned.into_inner()
857        });
858        symlinks.get(&ino).cloned()
859    }
860
861    /// Get device node info
862    pub fn get_device(&self, ino: Ino) -> Option<DeviceNode> {
863        let devices = self.devices.read().unwrap_or_else(|poisoned| {
864            eprintln!("WARNING: devices lock poisoned in get_device, recovering...");
865            poisoned.into_inner()
866        });
867        devices.get(&ino).cloned()
868    }
869
870    /// Get total number of symlinks
871    pub fn symlink_count(&self) -> usize {
872        let symlinks = self.symlinks.read().unwrap_or_else(|poisoned| {
873            eprintln!("WARNING: symlinks lock poisoned in symlink_count, recovering...");
874            poisoned.into_inner()
875        });
876        symlinks.len()
877    }
878
879    /// Get total number of device nodes
880    pub fn device_count(&self) -> usize {
881        let devices = self.devices.read().unwrap_or_else(|poisoned| {
882            eprintln!("WARNING: devices lock poisoned in device_count, recovering...");
883            poisoned.into_inner()
884        });
885        devices.len()
886    }
887}
888
889// =============================================================================
890// FUSER FILESYSTEM TRAIT IMPLEMENTATION
891// =============================================================================
892
893#[cfg(feature = "fuse")]
894impl fuser::Filesystem for EngramFS {
895    /// Initialize filesystem
896    fn init(
897        &mut self,
898        _req: &fuser::Request<'_>,
899        _config: &mut fuser::KernelConfig,
900    ) -> Result<(), libc::c_int> {
901        eprintln!(
902            "EngramFS initialized: {} files, {} bytes total",
903            self.file_count(),
904            self.total_size()
905        );
906        Ok(())
907    }
908
909    /// Clean up filesystem
910    fn destroy(&mut self) {
911        eprintln!("EngramFS unmounted");
912    }
913
914    /// Look up a directory entry by name
915    fn lookup(
916        &mut self,
917        _req: &fuser::Request<'_>,
918        parent: u64,
919        name: &OsStr,
920        reply: fuser::ReplyEntry,
921    ) {
922        let name = match name.to_str() {
923            Some(n) => n,
924            None => {
925                reply.error(libc::ENOENT);
926                return;
927            }
928        };
929
930        match self.lookup_entry(parent, name) {
931            Some(ino) => {
932                if let Some(attr) = self.get_attr(ino) {
933                    let fuser_attr: fuser::FileAttr = attr.into();
934                    reply.entry(&self.entry_ttl, &fuser_attr, 0);
935                } else {
936                    reply.error(libc::ENOENT);
937                }
938            }
939            None => {
940                reply.error(libc::ENOENT);
941            }
942        }
943    }
944
945    /// Get file attributes
946    fn getattr(
947        &mut self,
948        _req: &fuser::Request<'_>,
949        ino: u64,
950        _fh: Option<u64>,
951        reply: fuser::ReplyAttr,
952    ) {
953        match self.get_attr(ino) {
954            Some(attr) => {
955                let fuser_attr: fuser::FileAttr = attr.into();
956                reply.attr(&self.attr_ttl, &fuser_attr);
957            }
958            None => {
959                reply.error(libc::ENOENT);
960            }
961        }
962    }
963
964    /// Read data from a file
965    fn read(
966        &mut self,
967        _req: &fuser::Request<'_>,
968        ino: u64,
969        _fh: u64,
970        offset: i64,
971        size: u32,
972        _flags: i32,
973        _lock_owner: Option<u64>,
974        reply: fuser::ReplyData,
975    ) {
976        match self.read_data(ino, offset as u64, size) {
977            Some(data) => {
978                reply.data(&data);
979            }
980            None => {
981                reply.error(libc::ENOENT);
982            }
983        }
984    }
985
986    /// Open a file
987    fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) {
988        // Check if file exists
989        if self.get_attr(ino).is_none() {
990            reply.error(libc::ENOENT);
991            return;
992        }
993
994        // Check for write flags on read-only filesystem
995        if self.read_only {
996            let write_flags = libc::O_WRONLY | libc::O_RDWR | libc::O_APPEND | libc::O_TRUNC;
997            if flags & write_flags != 0 {
998                reply.error(libc::EROFS);
999                return;
1000            }
1001        }
1002
1003        // Return a dummy file handle (we're stateless)
1004        reply.opened(0, 0);
1005    }
1006
1007    /// Release an open file
1008    fn release(
1009        &mut self,
1010        _req: &fuser::Request<'_>,
1011        _ino: u64,
1012        _fh: u64,
1013        _flags: i32,
1014        _lock_owner: Option<u64>,
1015        _flush: bool,
1016        reply: fuser::ReplyEmpty,
1017    ) {
1018        reply.ok();
1019    }
1020
1021    /// Open a directory
1022    fn opendir(
1023        &mut self,
1024        _req: &fuser::Request<'_>,
1025        ino: u64,
1026        _flags: i32,
1027        reply: fuser::ReplyOpen,
1028    ) {
1029        match self.get_attr(ino) {
1030            Some(attr) if attr.kind == FileKind::Directory => {
1031                reply.opened(0, 0);
1032            }
1033            Some(_) => {
1034                reply.error(libc::ENOTDIR);
1035            }
1036            None => {
1037                reply.error(libc::ENOENT);
1038            }
1039        }
1040    }
1041
1042    /// Read directory entries
1043    fn readdir(
1044        &mut self,
1045        _req: &fuser::Request<'_>,
1046        ino: u64,
1047        _fh: u64,
1048        offset: i64,
1049        mut reply: fuser::ReplyDirectory,
1050    ) {
1051        let mut entries: Vec<(u64, fuser::FileType, String)> = Vec::new();
1052
1053        // Add . and ..
1054        entries.push((ino, fuser::FileType::Directory, ".".to_string()));
1055        let parent_ino = self.get_parent(ino).unwrap_or(ino);
1056        entries.push((parent_ino, fuser::FileType::Directory, "..".to_string()));
1057
1058        // Add directory contents
1059        if let Some(dir_entries) = self.read_dir(ino) {
1060            for entry in dir_entries {
1061                entries.push((entry.ino, entry.kind.into(), entry.name));
1062            }
1063        }
1064
1065        // Skip entries before offset and emit remaining
1066        for (i, (ino, kind, name)) in entries.into_iter().enumerate().skip(offset as usize) {
1067            // Reply returns true if buffer is full
1068            if reply.add(ino, (i + 1) as i64, kind, &name) {
1069                break;
1070            }
1071        }
1072
1073        reply.ok();
1074    }
1075
1076    /// Release a directory handle
1077    fn releasedir(
1078        &mut self,
1079        _req: &fuser::Request<'_>,
1080        _ino: u64,
1081        _fh: u64,
1082        _flags: i32,
1083        reply: fuser::ReplyEmpty,
1084    ) {
1085        reply.ok();
1086    }
1087
1088    /// Get filesystem statistics
1089    fn statfs(&mut self, _req: &fuser::Request<'_>, _ino: u64, reply: fuser::ReplyStatfs) {
1090        let total_files = self.file_count() as u64;
1091        let total_size = self.total_size();
1092        let block_size = 4096u64;
1093        let total_blocks = total_size.div_ceil(block_size);
1094
1095        reply.statfs(
1096            total_blocks,      // blocks - total data blocks
1097            0,                 // bfree - free blocks (0 for read-only)
1098            0,                 // bavail - available blocks (0 for read-only)
1099            total_files,       // files - total file nodes
1100            0,                 // ffree - free file nodes (0 for read-only)
1101            block_size as u32, // bsize - block size
1102            255,               // namelen - maximum name length
1103            block_size as u32, // frsize - fragment size
1104        );
1105    }
1106
1107    /// Check file access permissions
1108    fn access(&mut self, _req: &fuser::Request<'_>, ino: u64, mask: i32, reply: fuser::ReplyEmpty) {
1109        // Check if file exists
1110        if self.get_attr(ino).is_none() {
1111            reply.error(libc::ENOENT);
1112            return;
1113        }
1114
1115        // Deny write access on read-only filesystem
1116        if self.read_only && (mask & libc::W_OK != 0) {
1117            reply.error(libc::EROFS);
1118            return;
1119        }
1120
1121        // Allow all other access (simplified permission model)
1122        reply.ok();
1123    }
1124
1125    /// Read symbolic link target
1126    fn readlink(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyData) {
1127        match self.get_attr(ino) {
1128            Some(attr) if attr.kind == FileKind::Symlink => {
1129                // Look up the symlink target
1130                match self.read_symlink(ino) {
1131                    Some(target) => {
1132                        reply.data(target.as_bytes());
1133                    }
1134                    None => {
1135                        eprintln!("WARNING: Symlink {} has no target stored", ino);
1136                        reply.error(libc::EIO);
1137                    }
1138                }
1139            }
1140            Some(_) => {
1141                reply.error(libc::EINVAL); // Not a symlink
1142            }
1143            None => {
1144                reply.error(libc::ENOENT);
1145            }
1146        }
1147    }
1148
1149    /// Create a symbolic link
1150    fn symlink(
1151        &mut self,
1152        _req: &fuser::Request<'_>,
1153        parent: u64,
1154        link_name: &OsStr,
1155        target: &std::path::Path,
1156        reply: fuser::ReplyEntry,
1157    ) {
1158        if self.read_only {
1159            reply.error(libc::EROFS);
1160            return;
1161        }
1162
1163        let link_name = match link_name.to_str() {
1164            Some(n) => n,
1165            None => {
1166                reply.error(libc::EINVAL);
1167                return;
1168            }
1169        };
1170
1171        let target = match target.to_str() {
1172            Some(t) => t.to_string(),
1173            None => {
1174                reply.error(libc::EINVAL);
1175                return;
1176            }
1177        };
1178
1179        // Get parent path
1180        let parent_path_str = match self.inode_paths.read() {
1181            Ok(paths) => match paths.get(&parent) {
1182                Some(p) => p.clone(),
1183                None => {
1184                    reply.error(libc::ENOENT);
1185                    return;
1186                }
1187            },
1188            Err(_) => {
1189                reply.error(libc::EIO);
1190                return;
1191            }
1192        };
1193
1194        // Construct symlink path
1195        let symlink_path = if parent_path_str == "/" {
1196            format!("/{}", link_name)
1197        } else {
1198            format!("{}/{}", parent_path_str, link_name)
1199        };
1200
1201        // Create the symlink
1202        match self.add_symlink(&symlink_path, target) {
1203            Ok(ino) => {
1204                if let Some(attr) = self.get_attr(ino) {
1205                    let fuser_attr: fuser::FileAttr = attr.into();
1206                    reply.entry(&self.entry_ttl, &fuser_attr, 0);
1207                } else {
1208                    reply.error(libc::EIO);
1209                }
1210            }
1211            Err(_) => {
1212                reply.error(libc::EIO);
1213            }
1214        }
1215    }
1216
1217    /// Create a special device node (mknod)
1218    fn mknod(
1219        &mut self,
1220        _req: &fuser::Request<'_>,
1221        parent: u64,
1222        name: &OsStr,
1223        mode: u32,
1224        _umask: u32,
1225        rdev: u32,
1226        reply: fuser::ReplyEntry,
1227    ) {
1228        if self.read_only {
1229            reply.error(libc::EROFS);
1230            return;
1231        }
1232
1233        let name = match name.to_str() {
1234            Some(n) => n,
1235            None => {
1236                reply.error(libc::EINVAL);
1237                return;
1238            }
1239        };
1240
1241        // Get parent path
1242        let parent_path_str = match self.inode_paths.read() {
1243            Ok(paths) => match paths.get(&parent) {
1244                Some(p) => p.clone(),
1245                None => {
1246                    reply.error(libc::ENOENT);
1247                    return;
1248                }
1249            },
1250            Err(_) => {
1251                reply.error(libc::EIO);
1252                return;
1253            }
1254        };
1255
1256        // Construct file path
1257        let file_path = if parent_path_str == "/" {
1258            format!("/{}", name)
1259        } else {
1260            format!("{}/{}", parent_path_str, name)
1261        };
1262
1263        // Determine file type from mode
1264        let file_type = mode & libc::S_IFMT;
1265        let major = (rdev >> 8) & 0xff;
1266        let minor = rdev & 0xff;
1267
1268        let result = match file_type {
1269            libc::S_IFCHR => self.add_device(&file_path, true, major, minor, Vec::new()),
1270            libc::S_IFBLK => self.add_device(&file_path, false, major, minor, Vec::new()),
1271            libc::S_IFIFO => self.add_fifo(&file_path),
1272            libc::S_IFSOCK => self.add_socket(&file_path),
1273            _ => {
1274                reply.error(libc::EINVAL);
1275                return;
1276            }
1277        };
1278
1279        match result {
1280            Ok(ino) => {
1281                if let Some(attr) = self.get_attr(ino) {
1282                    let fuser_attr: fuser::FileAttr = attr.into();
1283                    reply.entry(&self.entry_ttl, &fuser_attr, 0);
1284                } else {
1285                    reply.error(libc::EIO);
1286                }
1287            }
1288            Err(_) => {
1289                reply.error(libc::EIO);
1290            }
1291        }
1292    }
1293}
1294
1295// =============================================================================
1296// MOUNT FUNCTIONS
1297// =============================================================================
1298
1299/// Mount options for EngramFS
1300#[cfg(feature = "fuse")]
1301#[derive(Clone, Debug)]
1302pub struct MountOptions {
1303    /// Read-only mount (default: true)
1304    pub read_only: bool,
1305    /// Allow other users to access the mount (default: false)
1306    pub allow_other: bool,
1307    /// Allow root to access the mount (default: true)
1308    pub allow_root: bool,
1309    /// Filesystem name shown in mount output
1310    pub fsname: String,
1311}
1312
1313#[cfg(feature = "fuse")]
1314impl Default for MountOptions {
1315    fn default() -> Self {
1316        MountOptions {
1317            read_only: true,
1318            allow_other: false,
1319            allow_root: true,
1320            fsname: "engram".to_string(),
1321        }
1322    }
1323}
1324
1325/// Mount an EngramFS at the specified path
1326///
1327/// This function blocks until the filesystem is unmounted. Use `spawn_mount`
1328/// for a non-blocking version.
1329///
1330/// # Arguments
1331///
1332/// * `fs` - The EngramFS instance to mount
1333/// * `mountpoint` - Directory path where the filesystem will be mounted
1334/// * `options` - Mount options (see `MountOptions`)
1335///
1336/// # Example
1337///
1338/// ```no_run
1339/// use embeddenator_fs::fuse_shim::{EngramFS, mount, MountOptions};
1340///
1341/// let fs = EngramFS::new(true);
1342/// // ... populate fs with files ...
1343///
1344/// mount(fs, "/mnt/engram", MountOptions::default()).unwrap();
1345/// ```
1346#[cfg(feature = "fuse")]
1347pub fn mount<P: AsRef<Path>>(
1348    fs: EngramFS,
1349    mountpoint: P,
1350    options: MountOptions,
1351) -> Result<(), std::io::Error> {
1352    use fuser::MountOption;
1353
1354    let mut mount_options = vec![
1355        MountOption::FSName(options.fsname),
1356        MountOption::AutoUnmount,
1357        MountOption::DefaultPermissions,
1358    ];
1359
1360    if options.read_only {
1361        mount_options.push(MountOption::RO);
1362    }
1363
1364    if options.allow_other {
1365        mount_options.push(MountOption::AllowOther);
1366    } else if options.allow_root {
1367        mount_options.push(MountOption::AllowRoot);
1368    }
1369
1370    fuser::mount2(fs, mountpoint.as_ref(), &mount_options)
1371}
1372
1373/// Mount an EngramFS with signal handling for graceful unmount
1374///
1375/// This function installs signal handlers for SIGINT, SIGTERM, and SIGHUP,
1376/// enabling graceful unmount when the user presses Ctrl+C or sends a kill signal.
1377///
1378/// # Arguments
1379///
1380/// * `fs` - The EngramFS instance to mount
1381/// * `mountpoint` - Directory path where the filesystem will be mounted
1382/// * `options` - Mount options (see `MountOptions`)
1383///
1384/// # Signal Handling
1385///
1386/// When a signal is received:
1387/// 1. The signal type is logged
1388/// 2. The FUSE session is cleanly unmounted
1389/// 3. The function returns `Ok(())`
1390///
1391/// # Example
1392///
1393/// ```no_run
1394/// use embeddenator_fs::fuse_shim::{EngramFS, mount_with_signals, MountOptions};
1395///
1396/// let fs = EngramFS::new(true);
1397/// // ... populate fs with files ...
1398///
1399/// // This will handle Ctrl+C gracefully
1400/// mount_with_signals(fs, "/mnt/engram", MountOptions::default()).unwrap();
1401/// ```
1402#[cfg(feature = "fuse")]
1403pub fn mount_with_signals<P: AsRef<Path>>(
1404    fs: EngramFS,
1405    mountpoint: P,
1406    options: MountOptions,
1407) -> Result<(), std::io::Error> {
1408    use crate::fs::signal::{install_signal_handlers, ShutdownSignal};
1409    use fuser::MountOption;
1410    use std::sync::Arc;
1411
1412    // Set up shutdown signal
1413    let shutdown = Arc::new(ShutdownSignal::new());
1414    install_signal_handlers(shutdown.clone())?;
1415
1416    let mut mount_options = vec![
1417        MountOption::FSName(options.fsname),
1418        MountOption::AutoUnmount,
1419        MountOption::DefaultPermissions,
1420    ];
1421
1422    if options.read_only {
1423        mount_options.push(MountOption::RO);
1424    }
1425
1426    if options.allow_other {
1427        mount_options.push(MountOption::AllowOther);
1428    } else if options.allow_root {
1429        mount_options.push(MountOption::AllowRoot);
1430    }
1431
1432    // Use spawn_mount2 to get a session we can control
1433    let session = fuser::spawn_mount2(fs, mountpoint.as_ref(), &mount_options)?;
1434
1435    // Wait for shutdown signal or natural unmount
1436    eprintln!("EngramFS mounted. Press Ctrl+C to unmount gracefully.");
1437
1438    // Poll for shutdown signal
1439    loop {
1440        if shutdown.is_signaled() {
1441            eprintln!(
1442                "\nReceived {} - unmounting gracefully...",
1443                shutdown.signal_name()
1444            );
1445            // Session will be dropped here, triggering unmount
1446            drop(session);
1447            break;
1448        }
1449
1450        // Check if session is still alive by sleeping briefly
1451        std::thread::sleep(std::time::Duration::from_millis(100));
1452
1453        // Try to detect if session ended (e.g., via fusermount -u)
1454        // The session will be joined when dropped
1455    }
1456
1457    eprintln!("EngramFS unmounted cleanly.");
1458    Ok(())
1459}
1460
1461/// Spawn an EngramFS mount in a background thread
1462///
1463/// Returns a `BackgroundSession` that will automatically unmount when dropped.
1464///
1465/// # Arguments
1466///
1467/// * `fs` - The EngramFS instance to mount
1468/// * `mountpoint` - Directory path where the filesystem will be mounted
1469/// * `options` - Mount options (see `MountOptions`)
1470///
1471/// # Example
1472///
1473/// ```no_run
1474/// use embeddenator_fs::fuse_shim::{EngramFS, spawn_mount, MountOptions};
1475///
1476/// let fs = EngramFS::new(true);
1477/// // ... populate fs with files ...
1478///
1479/// let session = spawn_mount(fs, "/mnt/engram", MountOptions::default()).unwrap();
1480/// // Filesystem is now mounted and accessible
1481///
1482/// // When session is dropped, the filesystem will be unmounted
1483/// ```
1484#[cfg(feature = "fuse")]
1485pub fn spawn_mount<P: AsRef<Path>>(
1486    fs: EngramFS,
1487    mountpoint: P,
1488    options: MountOptions,
1489) -> Result<fuser::BackgroundSession, std::io::Error> {
1490    use fuser::MountOption;
1491
1492    let mut mount_options = vec![
1493        MountOption::FSName(options.fsname),
1494        MountOption::AutoUnmount,
1495        MountOption::DefaultPermissions,
1496    ];
1497
1498    if options.read_only {
1499        mount_options.push(MountOption::RO);
1500    }
1501
1502    if options.allow_other {
1503        mount_options.push(MountOption::AllowOther);
1504    } else if options.allow_root {
1505        mount_options.push(MountOption::AllowRoot);
1506    }
1507
1508    fuser::spawn_mount2(fs, mountpoint.as_ref(), &mount_options)
1509}
1510
1511// =============================================================================
1512// BUILDER PATTERN
1513// =============================================================================
1514
1515/// Builder for creating an EngramFS from engram data
1516///
1517/// # Example
1518///
1519/// ```
1520/// use embeddenator_fs::fuse_shim::EngramFSBuilder;
1521///
1522/// let fs = EngramFSBuilder::new()
1523///     .add_file("/README.md", b"# Hello World".to_vec())
1524///     .add_file("/src/main.rs", b"fn main() {}".to_vec())
1525///     .build();
1526///
1527/// assert_eq!(fs.file_count(), 2);
1528/// ```
1529pub struct EngramFSBuilder {
1530    fs: EngramFS,
1531}
1532
1533impl EngramFSBuilder {
1534    /// Create a new builder
1535    pub fn new() -> Self {
1536        EngramFSBuilder {
1537            fs: EngramFS::new(true), // Read-only by default
1538        }
1539    }
1540
1541    /// Add a file from decoded engram data
1542    pub fn add_file(self, path: &str, data: Vec<u8>) -> Self {
1543        let _ = self.fs.add_file(path, data);
1544        self
1545    }
1546
1547    /// Set read-only mode (default: true)
1548    pub fn read_only(mut self, read_only: bool) -> Self {
1549        self.fs.read_only = read_only;
1550        self
1551    }
1552
1553    /// Build the filesystem
1554    pub fn build(self) -> EngramFS {
1555        self.fs
1556    }
1557}
1558
1559impl Default for EngramFSBuilder {
1560    fn default() -> Self {
1561        Self::new()
1562    }
1563}
1564
1565// =============================================================================
1566// UTILITY FUNCTIONS
1567// =============================================================================
1568
1569/// Normalize a path (ensure leading /, remove trailing /)
1570fn normalize_path(path: &str) -> String {
1571    let path = if path.starts_with('/') {
1572        path.to_string()
1573    } else {
1574        format!("/{}", path)
1575    };
1576
1577    if path.len() > 1 && path.ends_with('/') {
1578        path[..path.len() - 1].to_string()
1579    } else {
1580        path
1581    }
1582}
1583
1584/// Get parent path
1585fn parent_path(path: &str) -> Option<String> {
1586    let path = normalize_path(path);
1587    if path == "/" {
1588        return None;
1589    }
1590
1591    match path.rfind('/') {
1592        Some(0) => Some("/".to_string()),
1593        Some(pos) => Some(path[..pos].to_string()),
1594        None => None,
1595    }
1596}
1597
1598/// Get filename from path
1599fn filename(path: &str) -> Option<&str> {
1600    let path = path.trim_end_matches('/');
1601    path.rsplit('/').next()
1602}
1603
1604/// Convert SystemTime to Duration since UNIX_EPOCH (useful for logging)
1605#[allow(dead_code)]
1606fn system_time_to_unix(time: SystemTime) -> u64 {
1607    time.duration_since(UNIX_EPOCH)
1608        .map(|d| d.as_secs())
1609        .unwrap_or(0)
1610}
1611
1612// =============================================================================
1613// STATISTICS
1614// =============================================================================
1615
1616/// Statistics for the mounted filesystem
1617#[derive(Clone, Debug, Default)]
1618pub struct MountStats {
1619    /// Number of read operations
1620    pub reads: u64,
1621    /// Total bytes read
1622    pub read_bytes: u64,
1623    /// Number of lookup operations
1624    pub lookups: u64,
1625    /// Number of readdir operations
1626    pub readdirs: u64,
1627    /// Number of cache hits
1628    pub cache_hits: u64,
1629    /// Number of cache misses
1630    pub cache_misses: u64,
1631    /// Total decode time in microseconds
1632    pub decode_time_us: u64,
1633}
1634
1635// =============================================================================
1636// TESTS
1637// =============================================================================
1638
1639#[cfg(test)]
1640mod tests {
1641    use super::*;
1642
1643    #[test]
1644    fn test_normalize_path() {
1645        assert_eq!(normalize_path("foo"), "/foo");
1646        assert_eq!(normalize_path("/foo"), "/foo");
1647        assert_eq!(normalize_path("/foo/"), "/foo");
1648        assert_eq!(normalize_path("/"), "/");
1649    }
1650
1651    #[test]
1652    fn test_parent_path() {
1653        assert_eq!(parent_path("/foo/bar"), Some("/foo".to_string()));
1654        assert_eq!(parent_path("/foo"), Some("/".to_string()));
1655        assert_eq!(parent_path("/"), None);
1656    }
1657
1658    #[test]
1659    fn test_filename() {
1660        assert_eq!(filename("/foo/bar"), Some("bar"));
1661        assert_eq!(filename("/foo"), Some("foo"));
1662        assert_eq!(filename("/foo/bar/"), Some("bar"));
1663    }
1664
1665    #[test]
1666    fn test_add_file() {
1667        let fs = EngramFS::new(true);
1668
1669        let ino = fs.add_file("/test.txt", b"hello world".to_vec()).unwrap();
1670        assert!(ino > ROOT_INO);
1671
1672        let data = fs.read_data(ino, 0, 100).unwrap();
1673        assert_eq!(data, b"hello world");
1674    }
1675
1676    #[test]
1677    fn test_nested_directories() {
1678        let fs = EngramFS::new(true);
1679
1680        fs.add_file("/a/b/c/file.txt", b"deep".to_vec()).unwrap();
1681
1682        // All directories should exist
1683        assert!(fs.lookup_path("/a").is_some());
1684        assert!(fs.lookup_path("/a/b").is_some());
1685        assert!(fs.lookup_path("/a/b/c").is_some());
1686        assert!(fs.lookup_path("/a/b/c/file.txt").is_some());
1687    }
1688
1689    #[test]
1690    fn test_readdir() {
1691        let fs = EngramFS::new(true);
1692
1693        fs.add_file("/foo.txt", b"foo".to_vec()).unwrap();
1694        fs.add_file("/bar.txt", b"bar".to_vec()).unwrap();
1695        fs.add_file("/subdir/baz.txt", b"baz".to_vec()).unwrap();
1696
1697        let root_entries = fs.read_dir(ROOT_INO).unwrap();
1698        assert_eq!(root_entries.len(), 3); // foo.txt, bar.txt, subdir
1699
1700        let names: Vec<_> = root_entries.iter().map(|e| e.name.as_str()).collect();
1701        assert!(names.contains(&"foo.txt"));
1702        assert!(names.contains(&"bar.txt"));
1703        assert!(names.contains(&"subdir"));
1704    }
1705
1706    #[test]
1707    fn test_read_partial() {
1708        let fs = EngramFS::new(true);
1709        let data = b"0123456789";
1710
1711        let ino = fs.add_file("/test.txt", data.to_vec()).unwrap();
1712
1713        // Read middle portion
1714        let partial = fs.read_data(ino, 3, 4).unwrap();
1715        assert_eq!(partial, b"3456");
1716
1717        // Read past end
1718        let past_end = fs.read_data(ino, 20, 10).unwrap();
1719        assert!(past_end.is_empty());
1720    }
1721
1722    #[test]
1723    fn test_builder() {
1724        let fs = EngramFSBuilder::new()
1725            .add_file("/a.txt", b"a".to_vec())
1726            .add_file("/b.txt", b"b".to_vec())
1727            .build();
1728
1729        assert_eq!(fs.file_count(), 2);
1730    }
1731
1732    #[test]
1733    fn test_get_parent() {
1734        let fs = EngramFS::new(true);
1735
1736        fs.add_file("/a/b/c.txt", b"test".to_vec()).unwrap();
1737
1738        let c_ino = fs.lookup_path("/a/b/c.txt").unwrap();
1739        let b_ino = fs.lookup_path("/a/b").unwrap();
1740        let a_ino = fs.lookup_path("/a").unwrap();
1741
1742        assert_eq!(fs.get_parent(c_ino), Some(b_ino));
1743        assert_eq!(fs.get_parent(b_ino), Some(a_ino));
1744        assert_eq!(fs.get_parent(a_ino), Some(ROOT_INO));
1745        assert_eq!(fs.get_parent(ROOT_INO), Some(ROOT_INO));
1746    }
1747
1748    #[test]
1749    fn test_default_attrs() {
1750        let attr = FileAttr::default();
1751        assert_eq!(attr.perm, 0o644);
1752        assert_eq!(attr.nlink, 1);
1753        assert_eq!(attr.blksize, 4096);
1754    }
1755
1756    #[test]
1757    fn test_file_kind_conversion() {
1758        // Only run conversion tests when fuse feature is enabled
1759        #[cfg(feature = "fuse")]
1760        {
1761            let dir: fuser::FileType = FileKind::Directory.into();
1762            assert_eq!(dir, fuser::FileType::Directory);
1763
1764            let file: fuser::FileType = FileKind::RegularFile.into();
1765            assert_eq!(file, fuser::FileType::RegularFile);
1766        }
1767    }
1768
1769    #[test]
1770    fn test_lock_poisoning_recovery() {
1771        use std::sync::Arc;
1772        use std::thread;
1773
1774        // This test demonstrates that the filesystem can recover from poisoned locks
1775        // In the read path (read-only operations), we use unwrap_or_else with into_inner()
1776        // to continue serving requests even with poisoned locks
1777
1778        let fs = Arc::new(EngramFS::new(true));
1779
1780        // Add a file successfully
1781        fs.add_file("/test.txt", b"hello".to_vec()).unwrap();
1782        let ino = fs.lookup_path("/test.txt").unwrap();
1783
1784        // Simulate lock poisoning scenario by creating a poisoned lock in a thread
1785        // Note: We can't actually poison the lock in a real test without unsafe code,
1786        // but we can verify that our error handling works correctly
1787
1788        // Test that read operations continue to work
1789        let data = fs.read_data(ino, 0, 5);
1790        assert!(data.is_some());
1791        assert_eq!(data.unwrap(), b"hello");
1792
1793        // Test that lookup continues to work
1794        let found_ino = fs.lookup_path("/test.txt");
1795        assert_eq!(found_ino, Some(ino));
1796
1797        // Test that get_attr works
1798        let attr = fs.get_attr(ino);
1799        assert!(attr.is_some());
1800        assert_eq!(attr.unwrap().size, 5);
1801
1802        // Test concurrent access doesn't cause issues
1803        let fs_clone = Arc::clone(&fs);
1804        let handle = thread::spawn(move || {
1805            // Multiple reads from another thread
1806            for _ in 0..10 {
1807                let _ = fs_clone.read_data(ino, 0, 5);
1808                let _ = fs_clone.lookup_path("/test.txt");
1809            }
1810        });
1811
1812        // Simultaneous reads from main thread
1813        for _ in 0..10 {
1814            let _ = fs.read_data(ino, 0, 5);
1815            let _ = fs.get_attr(ino);
1816        }
1817
1818        handle.join().unwrap();
1819
1820        // Verify filesystem is still functional
1821        assert_eq!(fs.file_count(), 1);
1822        assert_eq!(fs.total_size(), 5);
1823    }
1824
1825    #[test]
1826    fn test_write_lock_error_propagation() {
1827        // Test that write operations properly propagate lock errors
1828        let fs = EngramFS::new(false);
1829
1830        // This should succeed
1831        let result = fs.add_file("/test.txt", b"content".to_vec());
1832        assert!(result.is_ok());
1833
1834        // Verify the file was created
1835        assert!(fs.lookup_path("/test.txt").is_some());
1836        assert_eq!(fs.file_count(), 1);
1837    }
1838}