1use std::{
5 cell::RefCell,
6 collections::BTreeMap,
7 ffi::OsStr,
8 path::{Component, Path},
9 rc::Rc,
10};
11
12use thiserror::Error;
13
14#[derive(Debug)]
16pub struct Stat {
17 pub st_mode: u32,
19 pub st_uid: u32,
21 pub st_gid: u32,
23 pub st_mtim_sec: i64,
25 pub xattrs: RefCell<BTreeMap<Box<OsStr>, Box<[u8]>>>,
27}
28
29#[derive(Debug)]
31pub enum LeafContent<T> {
32 Regular(T),
34 BlockDevice(u64),
36 CharacterDevice(u64),
38 Fifo,
40 Socket,
42 Symlink(Box<OsStr>),
44}
45
46#[derive(Debug)]
48pub struct Leaf<T> {
49 pub stat: Stat,
51 pub content: LeafContent<T>,
53}
54
55#[derive(Debug)]
57pub struct Directory<T> {
58 pub stat: Stat,
60 pub(crate) entries: BTreeMap<Box<OsStr>, Inode<T>>,
62}
63
64#[derive(Debug)]
66pub enum Inode<T> {
67 Directory(Box<Directory<T>>),
69 Leaf(Rc<Leaf<T>>),
71}
72
73#[derive(Error, Debug)]
75pub enum ImageError {
76 #[error("Invalid filename {0:?}")]
78 InvalidFilename(Box<OsStr>),
79 #[error("Directory entry {0:?} does not exist")]
81 NotFound(Box<OsStr>),
82 #[error("Directory entry {0:?} is not a subdirectory")]
84 NotADirectory(Box<OsStr>),
85 #[error("Directory entry {0:?} is a directory")]
87 IsADirectory(Box<OsStr>),
88 #[error("Directory entry {0:?} is not a regular file")]
90 IsNotRegular(Box<OsStr>),
91}
92
93impl<T> Inode<T> {
94 pub fn stat(&self) -> &Stat {
96 match self {
97 Inode::Directory(dir) => &dir.stat,
98 Inode::Leaf(leaf) => &leaf.stat,
99 }
100 }
101}
102
103impl<T> Default for Directory<T> {
105 fn default() -> Self {
106 Self {
107 stat: Stat {
108 st_uid: 0,
109 st_gid: 0,
110 st_mode: 0o555,
111 st_mtim_sec: 0,
112 xattrs: Default::default(),
113 },
114 entries: BTreeMap::default(),
115 }
116 }
117}
118
119impl<T> Directory<T> {
120 pub fn new(stat: Stat) -> Self {
122 Self {
123 stat,
124 entries: BTreeMap::new(),
125 }
126 }
127
128 pub fn inodes(&self) -> impl Iterator<Item = &Inode<T>> + use<'_, T> {
130 self.entries.values()
131 }
132
133 pub fn entries(&self) -> impl Iterator<Item = (&OsStr, &Inode<T>)> + use<'_, T> {
150 self.entries.iter().map(|(k, v)| (k.as_ref(), v))
151 }
152
153 pub fn sorted_entries(&self) -> impl Iterator<Item = (&OsStr, &Inode<T>)> + use<'_, T> {
156 self.entries.iter().map(|(k, v)| (k.as_ref(), v))
157 }
158
159 pub fn get_directory(&self, pathname: &OsStr) -> Result<&Directory<T>, ImageError> {
178 match self.get_directory_opt(pathname)? {
179 Some(r) => Ok(r),
180 None => Err(ImageError::NotFound(Box::from(pathname))),
181 }
182 }
183
184 pub fn get_directory_opt(&self, pathname: &OsStr) -> Result<Option<&Directory<T>>, ImageError> {
186 let path = Path::new(pathname);
187 let mut dir = self;
188
189 for component in path.components() {
190 dir = match component {
191 Component::RootDir => dir,
192 Component::Prefix(..) | Component::CurDir | Component::ParentDir => {
193 return Err(ImageError::InvalidFilename(pathname.into()))
194 }
195 Component::Normal(filename) => match dir.entries.get(filename) {
196 Some(Inode::Directory(subdir)) => subdir,
197 Some(_) => return Err(ImageError::NotADirectory(filename.into())),
198 None => return Ok(None),
199 },
200 }
201 }
202
203 Ok(Some(dir))
204 }
205
206 pub fn get_directory_mut(&mut self, pathname: &OsStr) -> Result<&mut Directory<T>, ImageError> {
210 let path = Path::new(pathname);
211 let mut dir = self;
212
213 for component in path.components() {
214 dir = match component {
215 Component::RootDir => dir,
216 Component::Prefix(..) | Component::CurDir | Component::ParentDir => {
217 return Err(ImageError::InvalidFilename(pathname.into()))
218 }
219 Component::Normal(filename) => match dir.entries.get_mut(filename) {
220 Some(Inode::Directory(subdir)) => subdir,
221 Some(_) => return Err(ImageError::NotADirectory(filename.into())),
222 None => return Err(ImageError::NotFound(filename.into())),
223 },
224 };
225 }
226
227 Ok(dir)
228 }
229
230 pub fn split<'d, 'n>(
251 &'d self,
252 pathname: &'n OsStr,
253 ) -> Result<(&'d Directory<T>, &'n OsStr), ImageError> {
254 let path = Path::new(pathname);
255
256 let Some(filename) = path.file_name() else {
257 return Err(ImageError::InvalidFilename(Box::from(pathname)));
258 };
259
260 let dir = match path.parent() {
261 Some(parent) => self.get_directory(parent.as_os_str())?,
262 None => self,
263 };
264
265 Ok((dir, filename))
266 }
267
268 pub fn split_mut<'d, 'n>(
273 &'d mut self,
274 pathname: &'n OsStr,
275 ) -> Result<(&'d mut Directory<T>, &'n OsStr), ImageError> {
276 let path = Path::new(pathname);
277
278 let Some(filename) = path.file_name() else {
279 return Err(ImageError::InvalidFilename(Box::from(pathname)));
280 };
281
282 let dir = match path.parent() {
283 Some(parent) => self.get_directory_mut(parent.as_os_str())?,
284 None => self,
285 };
286
287 Ok((dir, filename))
288 }
289
290 pub fn ref_leaf(&self, filename: &OsStr) -> Result<Rc<Leaf<T>>, ImageError> {
306 match self.entries.get(filename) {
307 Some(Inode::Leaf(leaf)) => Ok(Rc::clone(leaf)),
308 Some(Inode::Directory(..)) => Err(ImageError::IsADirectory(Box::from(filename))),
309 None => Err(ImageError::NotFound(Box::from(filename))),
310 }
311 }
312
313 pub fn get_file<'a>(&'a self, filename: &OsStr) -> Result<&'a T, ImageError> {
329 self.get_file_opt(filename)?
330 .ok_or_else(|| ImageError::NotFound(Box::from(filename)))
331 }
332
333 pub fn get_file_opt<'a>(&'a self, filename: &OsStr) -> Result<Option<&'a T>, ImageError> {
335 match self.entries.get(filename) {
336 Some(Inode::Leaf(leaf)) => match &leaf.content {
337 LeafContent::Regular(file) => Ok(Some(file)),
338 _ => Err(ImageError::IsNotRegular(filename.into())),
339 },
340 Some(Inode::Directory(..)) => Err(ImageError::IsADirectory(filename.into())),
341 None => Ok(None),
342 }
343 }
344
345 pub fn merge(&mut self, filename: &OsStr, inode: Inode<T>) {
361 if let Inode::Directory(new_dir) = inode {
364 if let Some(Inode::Directory(old_dir)) = self.entries.get_mut(filename) {
365 old_dir.stat = new_dir.stat;
366 } else {
367 self.insert(filename, Inode::Directory(new_dir));
371 }
372 } else {
373 self.insert(filename, inode);
374 }
375 }
376
377 pub fn insert(&mut self, filename: &OsStr, inode: Inode<T>) {
388 self.entries.insert(Box::from(filename), inode);
389 }
390
391 pub fn remove(&mut self, filename: &OsStr) {
399 self.entries.remove(filename);
400 }
401
402 pub fn lookup(&self, filename: &OsStr) -> Option<&Inode<T>> {
409 self.entries.get(filename)
410 }
411
412 pub fn pop(&mut self, filename: &OsStr) -> Option<Inode<T>> {
419 self.entries.remove(filename)
420 }
421
422 pub fn clear(&mut self) {
425 self.entries.clear();
426 }
427
428 pub fn newest_file(&self) -> i64 {
433 let mut newest = self.stat.st_mtim_sec;
434 for inode in self.entries.values() {
435 let mtime = match inode {
436 Inode::Leaf(ref leaf) => leaf.stat.st_mtim_sec,
437 Inode::Directory(ref dir) => dir.newest_file(),
438 };
439 if mtime > newest {
440 newest = mtime;
441 }
442 }
443 newest
444 }
445}
446
447#[derive(Debug)]
449pub struct FileSystem<T> {
450 pub root: Directory<T>,
452 pub have_root_stat: bool,
454}
455
456impl<T> Default for FileSystem<T> {
457 fn default() -> Self {
458 Self {
459 root: Directory::default(),
460 have_root_stat: false,
461 }
462 }
463}
464
465impl<T> FileSystem<T> {
466 pub fn set_root_stat(&mut self, stat: Stat) {
470 self.have_root_stat = true;
471 self.root.stat = stat;
472 }
473
474 pub fn ensure_root_stat(&mut self) {
479 if !self.have_root_stat {
480 self.root.stat.st_mtim_sec = self.root.newest_file();
481 self.have_root_stat = true;
482 }
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use std::cell::RefCell;
490 use std::collections::BTreeMap;
491 use std::ffi::{OsStr, OsString};
492 use std::rc::Rc;
493
494 #[derive(Debug, Default)]
496 struct FileContents {}
497
498 fn stat_with_mtime(mtime: i64) -> Stat {
500 Stat {
501 st_mode: 0o755,
502 st_uid: 1000,
503 st_gid: 1000,
504 st_mtim_sec: mtime,
505 xattrs: RefCell::new(BTreeMap::new()),
506 }
507 }
508
509 fn new_leaf_file(mtime: i64) -> Rc<Leaf<FileContents>> {
511 Rc::new(Leaf {
512 stat: stat_with_mtime(mtime),
513 content: LeafContent::Regular(FileContents::default()),
514 })
515 }
516
517 fn new_leaf_symlink(target: &str, mtime: i64) -> Rc<Leaf<FileContents>> {
519 Rc::new(Leaf {
520 stat: stat_with_mtime(mtime),
521 content: LeafContent::Symlink(OsString::from(target).into_boxed_os_str()),
522 })
523 }
524
525 fn new_dir_inode<T>(mtime: i64) -> Inode<T> {
527 Inode::Directory(Box::new(Directory {
528 stat: stat_with_mtime(mtime),
529 entries: BTreeMap::new(),
530 }))
531 }
532
533 fn new_dir_inode_with_stat<T>(stat: Stat) -> Inode<T> {
535 Inode::Directory(Box::new(Directory {
536 stat,
537 entries: BTreeMap::new(),
538 }))
539 }
540
541 #[test]
542 fn test_directory_default() {
543 let dir = Directory::<()>::default();
544 assert_eq!(dir.stat.st_uid, 0);
545 assert_eq!(dir.stat.st_gid, 0);
546 assert_eq!(dir.stat.st_mode, 0o555);
547 assert_eq!(dir.stat.st_mtim_sec, 0);
548 assert!(dir.stat.xattrs.borrow().is_empty());
549 assert!(dir.entries.is_empty());
550 }
551
552 #[test]
553 fn test_directory_new() {
554 let stat = stat_with_mtime(123);
555 let dir = Directory::<()>::new(stat);
556 assert_eq!(dir.stat.st_mtim_sec, 123);
557 assert!(dir.entries.is_empty());
558 }
559
560 #[test]
561 fn test_insert_and_get_leaf() {
562 let mut dir = Directory::<FileContents>::default();
563 let leaf = new_leaf_file(10);
564 dir.insert(OsStr::new("file.txt"), Inode::Leaf(Rc::clone(&leaf)));
565 assert_eq!(dir.entries.len(), 1);
566
567 let retrieved_leaf_rc = dir.ref_leaf(OsStr::new("file.txt")).unwrap();
568 assert!(Rc::ptr_eq(&retrieved_leaf_rc, &leaf));
569
570 let regular_file_content = dir.get_file(OsStr::new("file.txt")).unwrap();
571 assert!(matches!(regular_file_content, FileContents {}));
572 }
573
574 #[test]
575 fn test_insert_and_get_directory() {
576 let mut dir = Directory::<()>::default();
577 let sub_dir_inode = new_dir_inode(20);
578 dir.insert(OsStr::new("subdir"), sub_dir_inode);
579 assert_eq!(dir.entries.len(), 1);
580
581 let retrieved_subdir = dir.get_directory(OsStr::new("subdir")).unwrap();
582 assert_eq!(retrieved_subdir.stat.st_mtim_sec, 20);
583
584 let retrieved_subdir_opt = dir
585 .get_directory_opt(OsStr::new("subdir"))
586 .unwrap()
587 .unwrap();
588 assert_eq!(retrieved_subdir_opt.stat.st_mtim_sec, 20);
589 }
590
591 #[test]
592 fn test_get_directory_errors() {
593 let mut root = Directory::default();
594 root.insert(OsStr::new("dir1"), new_dir_inode(10));
595 root.insert(OsStr::new("file1"), Inode::Leaf(new_leaf_file(30)));
596
597 match root.get_directory(OsStr::new("nonexistent")) {
598 Err(ImageError::NotFound(name)) => assert_eq!(name.to_str().unwrap(), "nonexistent"),
599 _ => panic!("Expected NotFound"),
600 }
601 assert!(root
602 .get_directory_opt(OsStr::new("nonexistent"))
603 .unwrap()
604 .is_none());
605
606 match root.get_directory(OsStr::new("file1")) {
607 Err(ImageError::NotADirectory(name)) => assert_eq!(name.to_str().unwrap(), "file1"),
608 _ => panic!("Expected NotADirectory"),
609 }
610 }
611
612 #[test]
613 fn test_get_file_errors() {
614 let mut dir = Directory::default();
615 dir.insert(OsStr::new("subdir"), new_dir_inode(10));
616 dir.insert(
617 OsStr::new("link.txt"),
618 Inode::Leaf(new_leaf_symlink("target", 20)),
619 );
620
621 match dir.get_file(OsStr::new("nonexistent.txt")) {
622 Err(ImageError::NotFound(name)) => {
623 assert_eq!(name.to_str().unwrap(), "nonexistent.txt")
624 }
625 _ => panic!("Expected NotFound"),
626 }
627 assert!(dir
628 .get_file_opt(OsStr::new("nonexistent.txt"))
629 .unwrap()
630 .is_none());
631
632 match dir.get_file(OsStr::new("subdir")) {
633 Err(ImageError::IsADirectory(name)) => assert_eq!(name.to_str().unwrap(), "subdir"),
634 _ => panic!("Expected IsADirectory"),
635 }
636 match dir.get_file(OsStr::new("link.txt")) {
637 Err(ImageError::IsNotRegular(name)) => assert_eq!(name.to_str().unwrap(), "link.txt"),
638 res => panic!("Expected IsNotRegular, got {res:?}"),
639 }
640 }
641
642 #[test]
643 fn test_remove() {
644 let mut dir = Directory::default();
645 dir.insert(OsStr::new("file1.txt"), Inode::Leaf(new_leaf_file(10)));
646 dir.insert(OsStr::new("subdir"), new_dir_inode(20));
647 assert_eq!(dir.entries.len(), 2);
648
649 dir.remove(OsStr::new("file1.txt"));
650 assert_eq!(dir.entries.len(), 1);
651 assert!(!dir.entries.contains_key(OsStr::new("file1.txt")));
652
653 dir.remove(OsStr::new("nonexistent")); assert_eq!(dir.entries.len(), 1);
655 }
656
657 #[test]
658 fn test_merge() {
659 let mut dir = Directory::default();
660
661 dir.merge(OsStr::new("item"), Inode::Leaf(new_leaf_file(10)));
663 assert_eq!(
664 dir.entries
665 .get(OsStr::new("item"))
666 .unwrap()
667 .stat()
668 .st_mtim_sec,
669 10
670 );
671
672 let mut existing_dir_inode = new_dir_inode_with_stat(stat_with_mtime(80));
674 if let Inode::Directory(ref mut ed_box) = existing_dir_inode {
675 ed_box.insert(OsStr::new("inner_file"), Inode::Leaf(new_leaf_file(85)));
676 }
677 dir.insert(OsStr::new("merged_dir"), existing_dir_inode);
678
679 let new_merging_dir_inode = new_dir_inode_with_stat(stat_with_mtime(90));
680 dir.merge(OsStr::new("merged_dir"), new_merging_dir_inode);
681
682 match dir.entries.get(OsStr::new("merged_dir")) {
683 Some(Inode::Directory(d)) => {
684 assert_eq!(d.stat.st_mtim_sec, 90); assert_eq!(d.entries.len(), 1); assert!(d.entries.contains_key(OsStr::new("inner_file")));
687 }
688 _ => panic!("Expected directory after merge"),
689 }
690
691 dir.merge(OsStr::new("merged_dir"), Inode::Leaf(new_leaf_file(100)));
693 assert!(matches!(
694 dir.entries.get(OsStr::new("merged_dir")),
695 Some(Inode::Leaf(_))
696 ));
697 assert_eq!(
698 dir.entries
699 .get(OsStr::new("merged_dir"))
700 .unwrap()
701 .stat()
702 .st_mtim_sec,
703 100
704 );
705 }
706
707 #[test]
708 fn test_clear() {
709 let mut dir = Directory::default();
710 dir.insert(OsStr::new("file1"), Inode::Leaf(new_leaf_file(10)));
711 dir.stat.st_mtim_sec = 100;
712
713 dir.clear();
714 assert!(dir.entries.is_empty());
715 assert_eq!(dir.stat.st_mtim_sec, 100); }
717
718 #[test]
719 fn test_newest_file() {
720 let mut root = Directory::new(stat_with_mtime(5));
721 assert_eq!(root.newest_file(), 5);
722
723 root.insert(OsStr::new("file1"), Inode::Leaf(new_leaf_file(10)));
724 assert_eq!(root.newest_file(), 10);
725
726 let subdir_stat = stat_with_mtime(15);
727 let mut subdir = Box::new(Directory::new(subdir_stat));
728 subdir.insert(OsStr::new("subfile1"), Inode::Leaf(new_leaf_file(12)));
729 root.insert(OsStr::new("subdir"), Inode::Directory(subdir));
730 assert_eq!(root.newest_file(), 15);
731
732 if let Some(Inode::Directory(sd)) = root.entries.get_mut(OsStr::new("subdir")) {
733 sd.insert(OsStr::new("subfile2"), Inode::Leaf(new_leaf_file(20)));
734 }
735 assert_eq!(root.newest_file(), 20);
736
737 root.stat.st_mtim_sec = 25;
738 assert_eq!(root.newest_file(), 25);
739 }
740
741 #[test]
742 fn test_iteration_entries_sorted_inodes() {
743 let mut dir = Directory::default();
744 dir.insert(OsStr::new("b_file"), Inode::Leaf(new_leaf_file(10)));
745 dir.insert(OsStr::new("a_dir"), new_dir_inode(20));
746 dir.insert(
747 OsStr::new("c_link"),
748 Inode::Leaf(new_leaf_symlink("target", 30)),
749 );
750
751 let names_from_entries: Vec<&OsStr> = dir.entries().map(|(name, _)| name).collect();
752 assert_eq!(names_from_entries.len(), 3); assert!(names_from_entries.contains(&OsStr::new("a_dir")));
754 assert!(names_from_entries.contains(&OsStr::new("b_file")));
755 assert!(names_from_entries.contains(&OsStr::new("c_link")));
756
757 let sorted_names: Vec<&OsStr> = dir.sorted_entries().map(|(name, _)| name).collect();
758 assert_eq!(
759 sorted_names,
760 vec![
761 OsStr::new("a_dir"),
762 OsStr::new("b_file"),
763 OsStr::new("c_link")
764 ]
765 );
766
767 let mut inode_types = vec![];
768 for inode in dir.inodes() {
769 match inode {
770 Inode::Directory(_) => inode_types.push("dir"),
771 Inode::Leaf(_) => inode_types.push("leaf"),
772 }
773 }
774 assert_eq!(inode_types.len(), 3);
775 assert_eq!(inode_types.iter().filter(|&&t| t == "dir").count(), 1);
776 assert_eq!(inode_types.iter().filter(|&&t| t == "leaf").count(), 2);
777 }
778}