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)]
166pub enum FileKind {
167 Directory,
169 RegularFile,
171 Symlink,
173}
174
175#[cfg(feature = "fuse")]
176impl From<FileKind> for fuser::FileType {
177 fn from(kind: FileKind) -> Self {
178 match kind {
179 FileKind::Directory => fuser::FileType::Directory,
180 FileKind::RegularFile => fuser::FileType::RegularFile,
181 FileKind::Symlink => fuser::FileType::Symlink,
182 }
183 }
184}
185
186#[derive(Clone, Debug)]
188pub struct DirEntry {
189 pub ino: Ino,
191 pub name: String,
193 pub kind: FileKind,
195}
196
197#[derive(Clone)]
199pub struct CachedFile {
200 pub data: Vec<u8>,
202 pub attr: FileAttr,
204}
205
206pub struct EngramFS {
212 inodes: Arc<RwLock<HashMap<Ino, FileAttr>>>,
214
215 inode_paths: Arc<RwLock<HashMap<Ino, String>>>,
217
218 path_inodes: Arc<RwLock<HashMap<String, Ino>>>,
220
221 directories: Arc<RwLock<HashMap<Ino, Vec<DirEntry>>>>,
223
224 file_cache: Arc<RwLock<HashMap<Ino, CachedFile>>>,
226
227 next_ino: Arc<RwLock<Ino>>,
229
230 read_only: bool,
232
233 attr_ttl: Duration,
235
236 entry_ttl: Duration,
238}
239
240impl EngramFS {
241 pub fn new(read_only: bool) -> Self {
247 let mut fs = EngramFS {
248 inodes: Arc::new(RwLock::new(HashMap::new())),
249 inode_paths: Arc::new(RwLock::new(HashMap::new())),
250 path_inodes: Arc::new(RwLock::new(HashMap::new())),
251 directories: Arc::new(RwLock::new(HashMap::new())),
252 file_cache: Arc::new(RwLock::new(HashMap::new())),
253 next_ino: Arc::new(RwLock::new(2)), read_only,
255 attr_ttl: Duration::from_secs(1),
256 entry_ttl: Duration::from_secs(1),
257 };
258
259 fs.init_root();
261 fs
262 }
263
264 fn init_root(&mut self) {
266 let root_attr = FileAttr {
267 ino: ROOT_INO,
268 size: 0,
269 blocks: 0,
270 kind: FileKind::Directory,
271 perm: 0o755,
272 nlink: 2,
273 ..Default::default()
274 };
275
276 self.inodes
279 .write()
280 .expect("Lock poisoned during init")
281 .insert(ROOT_INO, root_attr);
282 self.inode_paths
283 .write()
284 .expect("Lock poisoned during init")
285 .insert(ROOT_INO, "/".to_string());
286 self.path_inodes
287 .write()
288 .expect("Lock poisoned during init")
289 .insert("/".to_string(), ROOT_INO);
290 self.directories
291 .write()
292 .expect("Lock poisoned during init")
293 .insert(ROOT_INO, Vec::new());
294 }
295
296 fn alloc_ino(&self) -> Result<Ino, &'static str> {
298 let mut next = self
299 .next_ino
300 .write()
301 .map_err(|_| "Inode allocator lock poisoned")?;
302 let ino = *next;
303 *next += 1;
304 Ok(ino)
305 }
306
307 pub fn add_file(&self, path: &str, data: Vec<u8>) -> Result<Ino, &'static str> {
318 let path = normalize_path(path);
319
320 let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
322 eprintln!("WARNING: path_inodes lock poisoned, recovering...");
323 poisoned.into_inner()
324 });
325 if path_inodes.contains_key(&path) {
326 return Err("File already exists");
327 }
328 drop(path_inodes);
329
330 let parent_path = parent_path(&path).ok_or("Invalid path")?;
332 let parent_ino = self.ensure_directory(&parent_path)?;
333
334 let ino = self.alloc_ino()?;
336 let size = data.len() as u64;
337
338 let attr = FileAttr {
339 ino,
340 size,
341 blocks: size.div_ceil(512),
342 kind: FileKind::RegularFile,
343 perm: 0o644,
344 nlink: 1,
345 ..Default::default()
346 };
347
348 self.inodes
350 .write()
351 .map_err(|_| "Inodes lock poisoned")?
352 .insert(ino, attr.clone());
353 self.inode_paths
354 .write()
355 .map_err(|_| "Inode paths lock poisoned")?
356 .insert(ino, path.clone());
357 self.path_inodes
358 .write()
359 .map_err(|_| "Path inodes lock poisoned")?
360 .insert(path.clone(), ino);
361 self.file_cache
362 .write()
363 .map_err(|_| "File cache lock poisoned")?
364 .insert(ino, CachedFile { data, attr });
365
366 let filename = filename(&path).ok_or("Invalid filename")?;
368 self.directories
369 .write()
370 .map_err(|_| "Directories lock poisoned")?
371 .get_mut(&parent_ino)
372 .ok_or("Parent directory not found")?
373 .push(DirEntry {
374 ino,
375 name: filename.to_string(),
376 kind: FileKind::RegularFile,
377 });
378
379 Ok(ino)
380 }
381
382 fn ensure_directory(&self, path: &str) -> Result<Ino, &'static str> {
384 let path = normalize_path(path);
385
386 if path == "/" {
388 return Ok(ROOT_INO);
389 }
390
391 let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
393 eprintln!("WARNING: path_inodes lock poisoned in ensure_directory, recovering...");
394 poisoned.into_inner()
395 });
396 if let Some(&ino) = path_inodes.get(&path) {
397 return Ok(ino);
398 }
399 drop(path_inodes);
400
401 let parent_path = parent_path(&path).ok_or("Invalid path")?;
403 let parent_ino = self.ensure_directory(&parent_path)?;
404
405 let ino = self.alloc_ino()?;
407 let attr = FileAttr {
408 ino,
409 size: 0,
410 blocks: 0,
411 kind: FileKind::Directory,
412 perm: 0o755,
413 nlink: 2,
414 ..Default::default()
415 };
416
417 self.inodes
418 .write()
419 .map_err(|_| "Inodes lock poisoned")?
420 .insert(ino, attr);
421 self.inode_paths
422 .write()
423 .map_err(|_| "Inode paths lock poisoned")?
424 .insert(ino, path.clone());
425 self.path_inodes
426 .write()
427 .map_err(|_| "Path inodes lock poisoned")?
428 .insert(path.clone(), ino);
429 self.directories
430 .write()
431 .map_err(|_| "Directories lock poisoned")?
432 .insert(ino, Vec::new());
433
434 let dirname = filename(&path).ok_or("Invalid dirname")?;
436 self.directories
437 .write()
438 .map_err(|_| "Directories lock poisoned")?
439 .get_mut(&parent_ino)
440 .ok_or("Parent not found")?
441 .push(DirEntry {
442 ino,
443 name: dirname.to_string(),
444 kind: FileKind::Directory,
445 });
446
447 if let Some(parent_attr) = self
449 .inodes
450 .write()
451 .map_err(|_| "Inodes lock poisoned")?
452 .get_mut(&parent_ino)
453 {
454 parent_attr.nlink += 1;
455 }
456
457 Ok(ino)
458 }
459
460 pub fn lookup_path(&self, path: &str) -> Option<Ino> {
462 let path = normalize_path(path);
463 let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
464 eprintln!("WARNING: path_inodes lock poisoned in lookup_path, recovering...");
465 poisoned.into_inner()
466 });
467 path_inodes.get(&path).copied()
468 }
469
470 pub fn get_attr(&self, ino: Ino) -> Option<FileAttr> {
472 let inodes = self.inodes.read().unwrap_or_else(|poisoned| {
473 eprintln!("WARNING: inodes lock poisoned in get_attr, recovering...");
474 poisoned.into_inner()
475 });
476 inodes.get(&ino).cloned()
477 }
478
479 pub fn read_data(&self, ino: Ino, offset: u64, size: u32) -> Option<Vec<u8>> {
481 let cache = self.file_cache.read().unwrap_or_else(|poisoned| {
482 eprintln!("WARNING: file_cache lock poisoned in read_data, recovering...");
483 poisoned.into_inner()
484 });
485 let cached = cache.get(&ino)?;
486
487 let start = offset as usize;
488 let end = std::cmp::min(start + size as usize, cached.data.len());
489
490 if start >= cached.data.len() {
491 return Some(Vec::new());
492 }
493
494 Some(cached.data[start..end].to_vec())
495 }
496
497 pub fn read_dir(&self, ino: Ino) -> Option<Vec<DirEntry>> {
499 let directories = self.directories.read().unwrap_or_else(|poisoned| {
500 eprintln!("WARNING: directories lock poisoned in read_dir, recovering...");
501 poisoned.into_inner()
502 });
503 directories.get(&ino).cloned()
504 }
505
506 pub fn lookup_entry(&self, parent_ino: Ino, name: &str) -> Option<Ino> {
508 let dirs = self.directories.read().unwrap_or_else(|poisoned| {
509 eprintln!("WARNING: directories lock poisoned in lookup_entry, recovering...");
510 poisoned.into_inner()
511 });
512 let entries = dirs.get(&parent_ino)?;
513 entries.iter().find(|e| e.name == name).map(|e| e.ino)
514 }
515
516 pub fn get_parent(&self, ino: Ino) -> Option<Ino> {
518 if ino == ROOT_INO {
519 return Some(ROOT_INO); }
521
522 let paths = self.inode_paths.read().unwrap_or_else(|poisoned| {
523 eprintln!("WARNING: inode_paths lock poisoned in get_parent, recovering...");
524 poisoned.into_inner()
525 });
526 let path = paths.get(&ino)?;
527 let parent = parent_path(path)?;
528
529 let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
530 eprintln!("WARNING: path_inodes lock poisoned in get_parent, recovering...");
531 poisoned.into_inner()
532 });
533 path_inodes.get(&parent).copied()
534 }
535
536 pub fn file_count(&self) -> usize {
538 let cache = self.file_cache.read().unwrap_or_else(|poisoned| {
539 eprintln!("WARNING: file_cache lock poisoned in file_count, recovering...");
540 poisoned.into_inner()
541 });
542 cache.len()
543 }
544
545 pub fn total_size(&self) -> u64 {
547 let cache = self.file_cache.read().unwrap_or_else(|poisoned| {
548 eprintln!("WARNING: file_cache lock poisoned in total_size, recovering...");
549 poisoned.into_inner()
550 });
551 cache.values().map(|f| f.attr.size).sum()
552 }
553
554 pub fn is_read_only(&self) -> bool {
556 self.read_only
557 }
558
559 pub fn attr_ttl(&self) -> Duration {
561 self.attr_ttl
562 }
563
564 pub fn entry_ttl(&self) -> Duration {
566 self.entry_ttl
567 }
568}
569
570#[cfg(feature = "fuse")]
575impl fuser::Filesystem for EngramFS {
576 fn init(
578 &mut self,
579 _req: &fuser::Request<'_>,
580 _config: &mut fuser::KernelConfig,
581 ) -> Result<(), libc::c_int> {
582 eprintln!(
583 "EngramFS initialized: {} files, {} bytes total",
584 self.file_count(),
585 self.total_size()
586 );
587 Ok(())
588 }
589
590 fn destroy(&mut self) {
592 eprintln!("EngramFS unmounted");
593 }
594
595 fn lookup(
597 &mut self,
598 _req: &fuser::Request<'_>,
599 parent: u64,
600 name: &OsStr,
601 reply: fuser::ReplyEntry,
602 ) {
603 let name = match name.to_str() {
604 Some(n) => n,
605 None => {
606 reply.error(libc::ENOENT);
607 return;
608 }
609 };
610
611 match self.lookup_entry(parent, name) {
612 Some(ino) => {
613 if let Some(attr) = self.get_attr(ino) {
614 let fuser_attr: fuser::FileAttr = attr.into();
615 reply.entry(&self.entry_ttl, &fuser_attr, 0);
616 } else {
617 reply.error(libc::ENOENT);
618 }
619 }
620 None => {
621 reply.error(libc::ENOENT);
622 }
623 }
624 }
625
626 fn getattr(
628 &mut self,
629 _req: &fuser::Request<'_>,
630 ino: u64,
631 _fh: Option<u64>,
632 reply: fuser::ReplyAttr,
633 ) {
634 match self.get_attr(ino) {
635 Some(attr) => {
636 let fuser_attr: fuser::FileAttr = attr.into();
637 reply.attr(&self.attr_ttl, &fuser_attr);
638 }
639 None => {
640 reply.error(libc::ENOENT);
641 }
642 }
643 }
644
645 fn read(
647 &mut self,
648 _req: &fuser::Request<'_>,
649 ino: u64,
650 _fh: u64,
651 offset: i64,
652 size: u32,
653 _flags: i32,
654 _lock_owner: Option<u64>,
655 reply: fuser::ReplyData,
656 ) {
657 match self.read_data(ino, offset as u64, size) {
658 Some(data) => {
659 reply.data(&data);
660 }
661 None => {
662 reply.error(libc::ENOENT);
663 }
664 }
665 }
666
667 fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) {
669 if self.get_attr(ino).is_none() {
671 reply.error(libc::ENOENT);
672 return;
673 }
674
675 if self.read_only {
677 let write_flags = libc::O_WRONLY | libc::O_RDWR | libc::O_APPEND | libc::O_TRUNC;
678 if flags & write_flags != 0 {
679 reply.error(libc::EROFS);
680 return;
681 }
682 }
683
684 reply.opened(0, 0);
686 }
687
688 fn release(
690 &mut self,
691 _req: &fuser::Request<'_>,
692 _ino: u64,
693 _fh: u64,
694 _flags: i32,
695 _lock_owner: Option<u64>,
696 _flush: bool,
697 reply: fuser::ReplyEmpty,
698 ) {
699 reply.ok();
700 }
701
702 fn opendir(
704 &mut self,
705 _req: &fuser::Request<'_>,
706 ino: u64,
707 _flags: i32,
708 reply: fuser::ReplyOpen,
709 ) {
710 match self.get_attr(ino) {
711 Some(attr) if attr.kind == FileKind::Directory => {
712 reply.opened(0, 0);
713 }
714 Some(_) => {
715 reply.error(libc::ENOTDIR);
716 }
717 None => {
718 reply.error(libc::ENOENT);
719 }
720 }
721 }
722
723 fn readdir(
725 &mut self,
726 _req: &fuser::Request<'_>,
727 ino: u64,
728 _fh: u64,
729 offset: i64,
730 mut reply: fuser::ReplyDirectory,
731 ) {
732 let mut entries: Vec<(u64, fuser::FileType, String)> = Vec::new();
733
734 entries.push((ino, fuser::FileType::Directory, ".".to_string()));
736 let parent_ino = self.get_parent(ino).unwrap_or(ino);
737 entries.push((parent_ino, fuser::FileType::Directory, "..".to_string()));
738
739 if let Some(dir_entries) = self.read_dir(ino) {
741 for entry in dir_entries {
742 entries.push((entry.ino, entry.kind.into(), entry.name));
743 }
744 }
745
746 for (i, (ino, kind, name)) in entries.into_iter().enumerate().skip(offset as usize) {
748 if reply.add(ino, (i + 1) as i64, kind, &name) {
750 break;
751 }
752 }
753
754 reply.ok();
755 }
756
757 fn releasedir(
759 &mut self,
760 _req: &fuser::Request<'_>,
761 _ino: u64,
762 _fh: u64,
763 _flags: i32,
764 reply: fuser::ReplyEmpty,
765 ) {
766 reply.ok();
767 }
768
769 fn statfs(&mut self, _req: &fuser::Request<'_>, _ino: u64, reply: fuser::ReplyStatfs) {
771 let total_files = self.file_count() as u64;
772 let total_size = self.total_size();
773 let block_size = 4096u64;
774 let total_blocks = (total_size + block_size - 1) / block_size;
775
776 reply.statfs(
777 total_blocks, 0, 0, total_files, 0, block_size as u32, 255, block_size as u32, );
786 }
787
788 fn access(&mut self, _req: &fuser::Request<'_>, ino: u64, mask: i32, reply: fuser::ReplyEmpty) {
790 if self.get_attr(ino).is_none() {
792 reply.error(libc::ENOENT);
793 return;
794 }
795
796 if self.read_only && (mask & libc::W_OK != 0) {
798 reply.error(libc::EROFS);
799 return;
800 }
801
802 reply.ok();
804 }
805
806 fn readlink(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyData) {
808 match self.get_attr(ino) {
810 Some(attr) if attr.kind == FileKind::Symlink => {
811 reply.error(libc::ENOSYS); }
813 Some(_) => {
814 reply.error(libc::EINVAL); }
816 None => {
817 reply.error(libc::ENOENT);
818 }
819 }
820 }
821}
822
823#[cfg(feature = "fuse")]
829#[derive(Clone, Debug)]
830pub struct MountOptions {
831 pub read_only: bool,
833 pub allow_other: bool,
835 pub allow_root: bool,
837 pub fsname: String,
839}
840
841#[cfg(feature = "fuse")]
842impl Default for MountOptions {
843 fn default() -> Self {
844 MountOptions {
845 read_only: true,
846 allow_other: false,
847 allow_root: true,
848 fsname: "engram".to_string(),
849 }
850 }
851}
852
853#[cfg(feature = "fuse")]
875pub fn mount<P: AsRef<Path>>(
876 fs: EngramFS,
877 mountpoint: P,
878 options: MountOptions,
879) -> Result<(), std::io::Error> {
880 use fuser::MountOption;
881
882 let mut mount_options = vec![
883 MountOption::FSName(options.fsname),
884 MountOption::AutoUnmount,
885 MountOption::DefaultPermissions,
886 ];
887
888 if options.read_only {
889 mount_options.push(MountOption::RO);
890 }
891
892 if options.allow_other {
893 mount_options.push(MountOption::AllowOther);
894 } else if options.allow_root {
895 mount_options.push(MountOption::AllowRoot);
896 }
897
898 fuser::mount2(fs, mountpoint.as_ref(), &mount_options)
899}
900
901#[cfg(feature = "fuse")]
925pub fn spawn_mount<P: AsRef<Path>>(
926 fs: EngramFS,
927 mountpoint: P,
928 options: MountOptions,
929) -> Result<fuser::BackgroundSession, std::io::Error> {
930 use fuser::MountOption;
931
932 let mut mount_options = vec![
933 MountOption::FSName(options.fsname),
934 MountOption::AutoUnmount,
935 MountOption::DefaultPermissions,
936 ];
937
938 if options.read_only {
939 mount_options.push(MountOption::RO);
940 }
941
942 if options.allow_other {
943 mount_options.push(MountOption::AllowOther);
944 } else if options.allow_root {
945 mount_options.push(MountOption::AllowRoot);
946 }
947
948 fuser::spawn_mount2(fs, mountpoint.as_ref(), &mount_options)
949}
950
951pub struct EngramFSBuilder {
970 fs: EngramFS,
971}
972
973impl EngramFSBuilder {
974 pub fn new() -> Self {
976 EngramFSBuilder {
977 fs: EngramFS::new(true), }
979 }
980
981 pub fn add_file(self, path: &str, data: Vec<u8>) -> Self {
983 let _ = self.fs.add_file(path, data);
984 self
985 }
986
987 pub fn read_only(mut self, read_only: bool) -> Self {
989 self.fs.read_only = read_only;
990 self
991 }
992
993 pub fn build(self) -> EngramFS {
995 self.fs
996 }
997}
998
999impl Default for EngramFSBuilder {
1000 fn default() -> Self {
1001 Self::new()
1002 }
1003}
1004
1005fn normalize_path(path: &str) -> String {
1011 let path = if path.starts_with('/') {
1012 path.to_string()
1013 } else {
1014 format!("/{}", path)
1015 };
1016
1017 if path.len() > 1 && path.ends_with('/') {
1018 path[..path.len() - 1].to_string()
1019 } else {
1020 path
1021 }
1022}
1023
1024fn parent_path(path: &str) -> Option<String> {
1026 let path = normalize_path(path);
1027 if path == "/" {
1028 return None;
1029 }
1030
1031 match path.rfind('/') {
1032 Some(0) => Some("/".to_string()),
1033 Some(pos) => Some(path[..pos].to_string()),
1034 None => None,
1035 }
1036}
1037
1038fn filename(path: &str) -> Option<&str> {
1040 let path = path.trim_end_matches('/');
1041 path.rsplit('/').next()
1042}
1043
1044#[allow(dead_code)]
1046fn system_time_to_unix(time: SystemTime) -> u64 {
1047 time.duration_since(UNIX_EPOCH)
1048 .map(|d| d.as_secs())
1049 .unwrap_or(0)
1050}
1051
1052#[derive(Clone, Debug, Default)]
1058pub struct MountStats {
1059 pub reads: u64,
1061 pub read_bytes: u64,
1063 pub lookups: u64,
1065 pub readdirs: u64,
1067 pub cache_hits: u64,
1069 pub cache_misses: u64,
1071 pub decode_time_us: u64,
1073}
1074
1075#[cfg(test)]
1080mod tests {
1081 use super::*;
1082
1083 #[test]
1084 fn test_normalize_path() {
1085 assert_eq!(normalize_path("foo"), "/foo");
1086 assert_eq!(normalize_path("/foo"), "/foo");
1087 assert_eq!(normalize_path("/foo/"), "/foo");
1088 assert_eq!(normalize_path("/"), "/");
1089 }
1090
1091 #[test]
1092 fn test_parent_path() {
1093 assert_eq!(parent_path("/foo/bar"), Some("/foo".to_string()));
1094 assert_eq!(parent_path("/foo"), Some("/".to_string()));
1095 assert_eq!(parent_path("/"), None);
1096 }
1097
1098 #[test]
1099 fn test_filename() {
1100 assert_eq!(filename("/foo/bar"), Some("bar"));
1101 assert_eq!(filename("/foo"), Some("foo"));
1102 assert_eq!(filename("/foo/bar/"), Some("bar"));
1103 }
1104
1105 #[test]
1106 fn test_add_file() {
1107 let fs = EngramFS::new(true);
1108
1109 let ino = fs.add_file("/test.txt", b"hello world".to_vec()).unwrap();
1110 assert!(ino > ROOT_INO);
1111
1112 let data = fs.read_data(ino, 0, 100).unwrap();
1113 assert_eq!(data, b"hello world");
1114 }
1115
1116 #[test]
1117 fn test_nested_directories() {
1118 let fs = EngramFS::new(true);
1119
1120 fs.add_file("/a/b/c/file.txt", b"deep".to_vec()).unwrap();
1121
1122 assert!(fs.lookup_path("/a").is_some());
1124 assert!(fs.lookup_path("/a/b").is_some());
1125 assert!(fs.lookup_path("/a/b/c").is_some());
1126 assert!(fs.lookup_path("/a/b/c/file.txt").is_some());
1127 }
1128
1129 #[test]
1130 fn test_readdir() {
1131 let fs = EngramFS::new(true);
1132
1133 fs.add_file("/foo.txt", b"foo".to_vec()).unwrap();
1134 fs.add_file("/bar.txt", b"bar".to_vec()).unwrap();
1135 fs.add_file("/subdir/baz.txt", b"baz".to_vec()).unwrap();
1136
1137 let root_entries = fs.read_dir(ROOT_INO).unwrap();
1138 assert_eq!(root_entries.len(), 3); let names: Vec<_> = root_entries.iter().map(|e| e.name.as_str()).collect();
1141 assert!(names.contains(&"foo.txt"));
1142 assert!(names.contains(&"bar.txt"));
1143 assert!(names.contains(&"subdir"));
1144 }
1145
1146 #[test]
1147 fn test_read_partial() {
1148 let fs = EngramFS::new(true);
1149 let data = b"0123456789";
1150
1151 let ino = fs.add_file("/test.txt", data.to_vec()).unwrap();
1152
1153 let partial = fs.read_data(ino, 3, 4).unwrap();
1155 assert_eq!(partial, b"3456");
1156
1157 let past_end = fs.read_data(ino, 20, 10).unwrap();
1159 assert!(past_end.is_empty());
1160 }
1161
1162 #[test]
1163 fn test_builder() {
1164 let fs = EngramFSBuilder::new()
1165 .add_file("/a.txt", b"a".to_vec())
1166 .add_file("/b.txt", b"b".to_vec())
1167 .build();
1168
1169 assert_eq!(fs.file_count(), 2);
1170 }
1171
1172 #[test]
1173 fn test_get_parent() {
1174 let fs = EngramFS::new(true);
1175
1176 fs.add_file("/a/b/c.txt", b"test".to_vec()).unwrap();
1177
1178 let c_ino = fs.lookup_path("/a/b/c.txt").unwrap();
1179 let b_ino = fs.lookup_path("/a/b").unwrap();
1180 let a_ino = fs.lookup_path("/a").unwrap();
1181
1182 assert_eq!(fs.get_parent(c_ino), Some(b_ino));
1183 assert_eq!(fs.get_parent(b_ino), Some(a_ino));
1184 assert_eq!(fs.get_parent(a_ino), Some(ROOT_INO));
1185 assert_eq!(fs.get_parent(ROOT_INO), Some(ROOT_INO));
1186 }
1187
1188 #[test]
1189 fn test_default_attrs() {
1190 let attr = FileAttr::default();
1191 assert_eq!(attr.perm, 0o644);
1192 assert_eq!(attr.nlink, 1);
1193 assert_eq!(attr.blksize, 4096);
1194 }
1195
1196 #[test]
1197 fn test_file_kind_conversion() {
1198 #[cfg(feature = "fuse")]
1200 {
1201 let dir: fuser::FileType = FileKind::Directory.into();
1202 assert_eq!(dir, fuser::FileType::Directory);
1203
1204 let file: fuser::FileType = FileKind::RegularFile.into();
1205 assert_eq!(file, fuser::FileType::RegularFile);
1206 }
1207 }
1208
1209 #[test]
1210 fn test_lock_poisoning_recovery() {
1211 use std::sync::Arc;
1212 use std::thread;
1213
1214 let fs = Arc::new(EngramFS::new(true));
1219
1220 fs.add_file("/test.txt", b"hello".to_vec()).unwrap();
1222 let ino = fs.lookup_path("/test.txt").unwrap();
1223
1224 let data = fs.read_data(ino, 0, 5);
1230 assert!(data.is_some());
1231 assert_eq!(data.unwrap(), b"hello");
1232
1233 let found_ino = fs.lookup_path("/test.txt");
1235 assert_eq!(found_ino, Some(ino));
1236
1237 let attr = fs.get_attr(ino);
1239 assert!(attr.is_some());
1240 assert_eq!(attr.unwrap().size, 5);
1241
1242 let fs_clone = Arc::clone(&fs);
1244 let handle = thread::spawn(move || {
1245 for _ in 0..10 {
1247 let _ = fs_clone.read_data(ino, 0, 5);
1248 let _ = fs_clone.lookup_path("/test.txt");
1249 }
1250 });
1251
1252 for _ in 0..10 {
1254 let _ = fs.read_data(ino, 0, 5);
1255 let _ = fs.get_attr(ino);
1256 }
1257
1258 handle.join().unwrap();
1259
1260 assert_eq!(fs.file_count(), 1);
1262 assert_eq!(fs.total_size(), 5);
1263 }
1264
1265 #[test]
1266 fn test_write_lock_error_propagation() {
1267 let fs = EngramFS::new(false);
1269
1270 let result = fs.add_file("/test.txt", b"content".to_vec());
1272 assert!(result.is_ok());
1273
1274 assert!(fs.lookup_path("/test.txt").is_some());
1276 assert_eq!(fs.file_count(), 1);
1277 }
1278}