1use 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
73pub type Ino = u64;
75
76pub const ROOT_INO: Ino = 1;
78
79#[derive(Clone, Debug)]
85pub struct FileAttr {
86 pub ino: Ino,
88 pub size: u64,
90 pub blocks: u64,
92 pub atime: SystemTime,
94 pub mtime: SystemTime,
96 pub ctime: SystemTime,
98 pub crtime: SystemTime,
100 pub kind: FileKind,
102 pub perm: u16,
104 pub nlink: u32,
106 pub uid: u32,
108 pub gid: u32,
110 pub rdev: u32,
112 pub blksize: u32,
114 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
166pub enum FileKind {
167 Directory,
169 #[default]
171 RegularFile,
172 Symlink,
174 Hardlink,
176 CharDevice,
178 BlockDevice,
180 Fifo,
182 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, 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#[derive(Clone, Debug)]
204pub struct DirEntry {
205 pub ino: Ino,
207 pub name: String,
209 pub kind: FileKind,
211}
212
213#[derive(Clone, Debug)]
215pub struct SymlinkEntry {
216 pub target: String,
218}
219
220#[derive(Clone)]
222pub struct CachedFile {
223 pub data: Vec<u8>,
225 pub attr: FileAttr,
227}
228
229#[derive(Clone, Debug)]
231pub struct DeviceNode {
232 pub major: u32,
234 pub minor: u32,
236}
237
238pub struct EngramFS {
244 inodes: Arc<RwLock<HashMap<Ino, FileAttr>>>,
246
247 inode_paths: Arc<RwLock<HashMap<Ino, String>>>,
249
250 path_inodes: Arc<RwLock<HashMap<String, Ino>>>,
252
253 directories: Arc<RwLock<HashMap<Ino, Vec<DirEntry>>>>,
255
256 file_cache: Arc<RwLock<HashMap<Ino, CachedFile>>>,
258
259 symlinks: Arc<RwLock<HashMap<Ino, String>>>,
261
262 devices: Arc<RwLock<HashMap<Ino, DeviceNode>>>,
264
265 next_ino: Arc<RwLock<Ino>>,
267
268 read_only: bool,
270
271 attr_ttl: Duration,
273
274 entry_ttl: Duration,
276}
277
278impl EngramFS {
279 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)), read_only,
295 attr_ttl: Duration::from_secs(1),
296 entry_ttl: Duration::from_secs(1),
297 };
298
299 fs.init_root();
301 fs
302 }
303
304 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 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 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 pub fn add_file(&self, path: &str, data: Vec<u8>) -> Result<Ino, &'static str> {
358 let path = normalize_path(path);
359
360 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 let parent_path = parent_path(&path).ok_or("Invalid path")?;
372 let parent_ino = self.ensure_directory(&parent_path)?;
373
374 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 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 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 fn ensure_directory(&self, path: &str) -> Result<Ino, &'static str> {
424 let path = normalize_path(path);
425
426 if path == "/" {
428 return Ok(ROOT_INO);
429 }
430
431 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 let parent_path = parent_path(&path).ok_or("Invalid path")?;
443 let parent_ino = self.ensure_directory(&parent_path)?;
444
445 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 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 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 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 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 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 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 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 pub fn get_parent(&self, ino: Ino) -> Option<Ino> {
558 if ino == ROOT_INO {
559 return Some(ROOT_INO); }
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 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 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 pub fn is_read_only(&self) -> bool {
596 self.read_only
597 }
598
599 pub fn attr_ttl(&self) -> Duration {
601 self.attr_ttl
602 }
603
604 pub fn entry_ttl(&self) -> Duration {
606 self.entry_ttl
607 }
608
609 pub fn add_symlink(&self, path: &str, target: String) -> Result<Ino, &'static str> {
620 let path = normalize_path(path);
621
622 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 let parent_path = parent_path(&path).ok_or("Invalid path")?;
634 let parent_ino = self.ensure_directory(&parent_path)?;
635
636 let ino = self.alloc_ino()?;
638 let size = target.len() as u64; let attr = FileAttr {
641 ino,
642 size,
643 blocks: 0,
644 kind: FileKind::Symlink,
645 perm: 0o777, nlink: 1,
647 ..Default::default()
648 };
649
650 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 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 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 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 let parent_path = parent_path(&path).ok_or("Invalid path")?;
719 let parent_ino = self.ensure_directory(&parent_path)?;
720
721 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, ..Default::default()
739 };
740
741 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 self.file_cache
761 .write()
762 .map_err(|_| "File cache lock poisoned")?
763 .insert(ino, CachedFile { data, attr });
764
765 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 pub fn add_fifo(&self, path: &str) -> Result<Ino, &'static str> {
783 self.add_special_file(path, FileKind::Fifo)
784 }
785
786 pub fn add_socket(&self, path: &str) -> Result<Ino, &'static str> {
788 self.add_special_file(path, FileKind::Socket)
789 }
790
791 fn add_special_file(&self, path: &str, kind: FileKind) -> Result<Ino, &'static str> {
793 let path = normalize_path(path);
794
795 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 let parent_path = parent_path(&path).ok_or("Invalid path")?;
807 let parent_ino = self.ensure_directory(&parent_path)?;
808
809 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 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 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 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 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 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 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#[cfg(feature = "fuse")]
894impl fuser::Filesystem for EngramFS {
895 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 fn destroy(&mut self) {
911 eprintln!("EngramFS unmounted");
912 }
913
914 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 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 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 fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) {
988 if self.get_attr(ino).is_none() {
990 reply.error(libc::ENOENT);
991 return;
992 }
993
994 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 reply.opened(0, 0);
1005 }
1006
1007 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 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 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 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 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 for (i, (ino, kind, name)) in entries.into_iter().enumerate().skip(offset as usize) {
1067 if reply.add(ino, (i + 1) as i64, kind, &name) {
1069 break;
1070 }
1071 }
1072
1073 reply.ok();
1074 }
1075
1076 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 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, 0, 0, total_files, 0, block_size as u32, 255, block_size as u32, );
1105 }
1106
1107 fn access(&mut self, _req: &fuser::Request<'_>, ino: u64, mask: i32, reply: fuser::ReplyEmpty) {
1109 if self.get_attr(ino).is_none() {
1111 reply.error(libc::ENOENT);
1112 return;
1113 }
1114
1115 if self.read_only && (mask & libc::W_OK != 0) {
1117 reply.error(libc::EROFS);
1118 return;
1119 }
1120
1121 reply.ok();
1123 }
1124
1125 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 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); }
1143 None => {
1144 reply.error(libc::ENOENT);
1145 }
1146 }
1147 }
1148
1149 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 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 let symlink_path = if parent_path_str == "/" {
1196 format!("/{}", link_name)
1197 } else {
1198 format!("{}/{}", parent_path_str, link_name)
1199 };
1200
1201 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 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 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 let file_path = if parent_path_str == "/" {
1258 format!("/{}", name)
1259 } else {
1260 format!("{}/{}", parent_path_str, name)
1261 };
1262
1263 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#[cfg(feature = "fuse")]
1301#[derive(Clone, Debug)]
1302pub struct MountOptions {
1303 pub read_only: bool,
1305 pub allow_other: bool,
1307 pub allow_root: bool,
1309 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#[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#[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 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 let session = fuser::spawn_mount2(fs, mountpoint.as_ref(), &mount_options)?;
1434
1435 eprintln!("EngramFS mounted. Press Ctrl+C to unmount gracefully.");
1437
1438 loop {
1440 if shutdown.is_signaled() {
1441 eprintln!(
1442 "\nReceived {} - unmounting gracefully...",
1443 shutdown.signal_name()
1444 );
1445 drop(session);
1447 break;
1448 }
1449
1450 std::thread::sleep(std::time::Duration::from_millis(100));
1452
1453 }
1456
1457 eprintln!("EngramFS unmounted cleanly.");
1458 Ok(())
1459}
1460
1461#[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
1511pub struct EngramFSBuilder {
1530 fs: EngramFS,
1531}
1532
1533impl EngramFSBuilder {
1534 pub fn new() -> Self {
1536 EngramFSBuilder {
1537 fs: EngramFS::new(true), }
1539 }
1540
1541 pub fn add_file(self, path: &str, data: Vec<u8>) -> Self {
1543 let _ = self.fs.add_file(path, data);
1544 self
1545 }
1546
1547 pub fn read_only(mut self, read_only: bool) -> Self {
1549 self.fs.read_only = read_only;
1550 self
1551 }
1552
1553 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
1565fn 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
1584fn 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
1598fn filename(path: &str) -> Option<&str> {
1600 let path = path.trim_end_matches('/');
1601 path.rsplit('/').next()
1602}
1603
1604#[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#[derive(Clone, Debug, Default)]
1618pub struct MountStats {
1619 pub reads: u64,
1621 pub read_bytes: u64,
1623 pub lookups: u64,
1625 pub readdirs: u64,
1627 pub cache_hits: u64,
1629 pub cache_misses: u64,
1631 pub decode_time_us: u64,
1633}
1634
1635#[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 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); 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 let partial = fs.read_data(ino, 3, 4).unwrap();
1715 assert_eq!(partial, b"3456");
1716
1717 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 #[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 let fs = Arc::new(EngramFS::new(true));
1779
1780 fs.add_file("/test.txt", b"hello".to_vec()).unwrap();
1782 let ino = fs.lookup_path("/test.txt").unwrap();
1783
1784 let data = fs.read_data(ino, 0, 5);
1790 assert!(data.is_some());
1791 assert_eq!(data.unwrap(), b"hello");
1792
1793 let found_ino = fs.lookup_path("/test.txt");
1795 assert_eq!(found_ino, Some(ino));
1796
1797 let attr = fs.get_attr(ino);
1799 assert!(attr.is_some());
1800 assert_eq!(attr.unwrap().size, 5);
1801
1802 let fs_clone = Arc::clone(&fs);
1804 let handle = thread::spawn(move || {
1805 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 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 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 let fs = EngramFS::new(false);
1829
1830 let result = fs.add_file("/test.txt", b"content".to_vec());
1832 assert!(result.is_ok());
1833
1834 assert!(fs.lookup_path("/test.txt").is_some());
1836 assert_eq!(fs.file_count(), 1);
1837 }
1838}