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
29impl Stat {
30 pub fn uninitialized() -> Self {
39 Self {
40 st_mode: 0,
41 st_uid: 0,
42 st_gid: 0,
43 st_mtim_sec: 0,
44 xattrs: RefCell::new(BTreeMap::new()),
45 }
46 }
47}
48
49#[derive(Debug)]
51pub enum LeafContent<T> {
52 Regular(T),
54 BlockDevice(u64),
56 CharacterDevice(u64),
58 Fifo,
60 Socket,
62 Symlink(Box<OsStr>),
64}
65
66#[derive(Debug)]
68pub struct Leaf<T> {
69 pub stat: Stat,
71 pub content: LeafContent<T>,
73}
74
75#[derive(Debug)]
77pub struct Directory<T> {
78 pub stat: Stat,
80 pub(crate) entries: BTreeMap<Box<OsStr>, Inode<T>>,
82}
83
84#[derive(Debug)]
86pub enum Inode<T> {
87 Directory(Box<Directory<T>>),
89 Leaf(Rc<Leaf<T>>),
91}
92
93#[derive(Error, Debug)]
95pub enum ImageError {
96 #[error("Invalid filename {0:?}")]
98 InvalidFilename(Box<OsStr>),
99 #[error("Directory entry {0:?} does not exist")]
101 NotFound(Box<OsStr>),
102 #[error("Directory entry {0:?} is not a subdirectory")]
104 NotADirectory(Box<OsStr>),
105 #[error("Directory entry {0:?} is a directory")]
107 IsADirectory(Box<OsStr>),
108 #[error("Directory entry {0:?} is not a regular file")]
110 IsNotRegular(Box<OsStr>),
111}
112
113impl<T> Inode<T> {
114 pub fn stat(&self) -> &Stat {
116 match self {
117 Inode::Directory(dir) => &dir.stat,
118 Inode::Leaf(leaf) => &leaf.stat,
119 }
120 }
121}
122
123impl<T> Directory<T> {
124 pub fn new(stat: Stat) -> Self {
126 Self {
127 stat,
128 entries: BTreeMap::new(),
129 }
130 }
131
132 pub fn inodes(&self) -> impl Iterator<Item = &Inode<T>> + use<'_, T> {
134 self.entries.values()
135 }
136
137 pub fn entries(&self) -> impl Iterator<Item = (&OsStr, &Inode<T>)> + use<'_, T> {
154 self.entries.iter().map(|(k, v)| (k.as_ref(), v))
155 }
156
157 pub fn sorted_entries(&self) -> impl Iterator<Item = (&OsStr, &Inode<T>)> + use<'_, T> {
160 self.entries.iter().map(|(k, v)| (k.as_ref(), v))
161 }
162
163 pub fn get_directory(&self, pathname: &OsStr) -> Result<&Directory<T>, ImageError> {
182 match self.get_directory_opt(pathname)? {
183 Some(r) => Ok(r),
184 None => Err(ImageError::NotFound(Box::from(pathname))),
185 }
186 }
187
188 pub fn get_directory_opt(&self, pathname: &OsStr) -> Result<Option<&Directory<T>>, ImageError> {
190 let path = Path::new(pathname);
191 let mut dir = self;
192
193 for component in path.components() {
194 dir = match component {
195 Component::RootDir => dir,
196 Component::Prefix(..) | Component::CurDir | Component::ParentDir => {
197 return Err(ImageError::InvalidFilename(pathname.into()));
198 }
199 Component::Normal(filename) => match dir.entries.get(filename) {
200 Some(Inode::Directory(subdir)) => subdir,
201 Some(_) => return Err(ImageError::NotADirectory(filename.into())),
202 None => return Ok(None),
203 },
204 }
205 }
206
207 Ok(Some(dir))
208 }
209
210 pub fn get_directory_mut(&mut self, pathname: &OsStr) -> Result<&mut Directory<T>, ImageError> {
214 let path = Path::new(pathname);
215 let mut dir = self;
216
217 for component in path.components() {
218 dir = match component {
219 Component::RootDir => dir,
220 Component::Prefix(..) | Component::CurDir | Component::ParentDir => {
221 return Err(ImageError::InvalidFilename(pathname.into()));
222 }
223 Component::Normal(filename) => match dir.entries.get_mut(filename) {
224 Some(Inode::Directory(subdir)) => subdir,
225 Some(_) => return Err(ImageError::NotADirectory(filename.into())),
226 None => return Err(ImageError::NotFound(filename.into())),
227 },
228 };
229 }
230
231 Ok(dir)
232 }
233
234 pub fn split<'d, 'n>(
255 &'d self,
256 pathname: &'n OsStr,
257 ) -> Result<(&'d Directory<T>, &'n OsStr), ImageError> {
258 let path = Path::new(pathname);
259
260 let Some(filename) = path.file_name() else {
261 return Err(ImageError::InvalidFilename(Box::from(pathname)));
262 };
263
264 let dir = match path.parent() {
265 Some(parent) => self.get_directory(parent.as_os_str())?,
266 None => self,
267 };
268
269 Ok((dir, filename))
270 }
271
272 pub fn split_mut<'d, 'n>(
277 &'d mut self,
278 pathname: &'n OsStr,
279 ) -> Result<(&'d mut Directory<T>, &'n OsStr), ImageError> {
280 let path = Path::new(pathname);
281
282 let Some(filename) = path.file_name() else {
283 return Err(ImageError::InvalidFilename(Box::from(pathname)));
284 };
285
286 let dir = match path.parent() {
287 Some(parent) => self.get_directory_mut(parent.as_os_str())?,
288 None => self,
289 };
290
291 Ok((dir, filename))
292 }
293
294 pub fn ref_leaf(&self, filename: &OsStr) -> Result<Rc<Leaf<T>>, ImageError> {
310 match self.entries.get(filename) {
311 Some(Inode::Leaf(leaf)) => Ok(Rc::clone(leaf)),
312 Some(Inode::Directory(..)) => Err(ImageError::IsADirectory(Box::from(filename))),
313 None => Err(ImageError::NotFound(Box::from(filename))),
314 }
315 }
316
317 pub fn get_file<'a>(&'a self, filename: &OsStr) -> Result<&'a T, ImageError> {
333 self.get_file_opt(filename)?
334 .ok_or_else(|| ImageError::NotFound(Box::from(filename)))
335 }
336
337 pub fn get_file_opt<'a>(&'a self, filename: &OsStr) -> Result<Option<&'a T>, ImageError> {
339 match self.entries.get(filename) {
340 Some(Inode::Leaf(leaf)) => match &leaf.content {
341 LeafContent::Regular(file) => Ok(Some(file)),
342 _ => Err(ImageError::IsNotRegular(filename.into())),
343 },
344 Some(Inode::Directory(..)) => Err(ImageError::IsADirectory(filename.into())),
345 None => Ok(None),
346 }
347 }
348
349 pub fn merge(&mut self, filename: &OsStr, inode: Inode<T>) {
365 if let Inode::Directory(new_dir) = inode {
368 if let Some(Inode::Directory(old_dir)) = self.entries.get_mut(filename) {
369 old_dir.stat = new_dir.stat;
370 } else {
371 self.insert(filename, Inode::Directory(new_dir));
375 }
376 } else {
377 self.insert(filename, inode);
378 }
379 }
380
381 pub fn insert(&mut self, filename: &OsStr, inode: Inode<T>) {
392 self.entries.insert(Box::from(filename), inode);
393 }
394
395 pub fn remove(&mut self, filename: &OsStr) {
403 self.entries.remove(filename);
404 }
405
406 pub fn lookup(&self, filename: &OsStr) -> Option<&Inode<T>> {
413 self.entries.get(filename)
414 }
415
416 pub fn pop(&mut self, filename: &OsStr) -> Option<Inode<T>> {
423 self.entries.remove(filename)
424 }
425
426 pub fn clear(&mut self) {
429 self.entries.clear();
430 }
431
432 pub fn newest_file(&self) -> i64 {
437 let mut newest = self.stat.st_mtim_sec;
438 for inode in self.entries.values() {
439 let mtime = match inode {
440 Inode::Leaf(ref leaf) => leaf.stat.st_mtim_sec,
441 Inode::Directory(ref dir) => dir.newest_file(),
442 };
443 if mtime > newest {
444 newest = mtime;
445 }
446 }
447 newest
448 }
449}
450
451#[derive(Debug)]
453pub struct FileSystem<T> {
454 pub root: Directory<T>,
456}
457
458impl<T> FileSystem<T> {
459 pub fn new(root_stat: Stat) -> Self {
461 Self {
462 root: Directory::new(root_stat),
463 }
464 }
465
466 pub fn set_root_stat(&mut self, stat: Stat) {
468 self.root.stat = stat;
469 }
470
471 pub fn copy_root_metadata_from_usr(&mut self) -> Result<(), ImageError> {
492 let usr = self.root.get_directory(OsStr::new("usr"))?;
493
494 let st_mode = usr.stat.st_mode;
496 let st_uid = usr.stat.st_uid;
497 let st_gid = usr.stat.st_gid;
498 let st_mtim_sec = usr.stat.st_mtim_sec;
499 let xattrs = usr.stat.xattrs.clone();
500
501 self.root.stat.st_mode = st_mode;
503 self.root.stat.st_uid = st_uid;
504 self.root.stat.st_gid = st_gid;
505 self.root.stat.st_mtim_sec = st_mtim_sec;
506 self.root.stat.xattrs = xattrs;
507
508 Ok(())
509 }
510
511 pub fn for_each_stat<F>(&self, f: F)
516 where
517 F: Fn(&Stat),
518 {
519 fn visit_inode<T, F: Fn(&Stat)>(inode: &Inode<T>, f: &F) {
520 match inode {
521 Inode::Directory(ref dir) => visit_dir(dir, f),
522 Inode::Leaf(ref leaf) => f(&leaf.stat),
523 }
524 }
525
526 fn visit_dir<T, F: Fn(&Stat)>(dir: &Directory<T>, f: &F) {
527 f(&dir.stat);
528 for (_name, inode) in dir.entries.iter() {
529 visit_inode(inode, f);
530 }
531 }
532
533 visit_dir(&self.root, &f);
534 }
535
536 pub fn filter_xattrs<F>(&self, predicate: F)
543 where
544 F: Fn(&OsStr) -> bool,
545 {
546 self.for_each_stat(|stat| {
547 stat.xattrs.borrow_mut().retain(|k, _| predicate(k));
548 });
549 }
550
551 pub fn canonicalize_run(&mut self) -> Result<(), ImageError> {
566 if self.root.get_directory_opt(OsStr::new("run"))?.is_some() {
567 let usr_mtime = self.root.get_directory(OsStr::new("usr"))?.stat.st_mtim_sec;
568 let run_dir = self.root.get_directory_mut(OsStr::new("run"))?;
569 run_dir.stat.st_mtim_sec = usr_mtime;
570 run_dir.clear();
571 }
572 Ok(())
573 }
574
575 pub fn transform_for_oci(&mut self) -> Result<(), ImageError> {
592 self.copy_root_metadata_from_usr()?;
593 self.canonicalize_run()?;
594 Ok(())
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use std::cell::RefCell;
602 use std::collections::BTreeMap;
603 use std::ffi::{OsStr, OsString};
604 use std::rc::Rc;
605
606 #[derive(Debug, Default)]
608 struct FileContents {}
609
610 fn default_stat() -> Stat {
612 Stat {
613 st_mode: 0o755,
614 st_uid: 0,
615 st_gid: 0,
616 st_mtim_sec: 0,
617 xattrs: RefCell::new(BTreeMap::new()),
618 }
619 }
620
621 fn stat_with_mtime(mtime: i64) -> Stat {
623 Stat {
624 st_mode: 0o755,
625 st_uid: 1000,
626 st_gid: 1000,
627 st_mtim_sec: mtime,
628 xattrs: RefCell::new(BTreeMap::new()),
629 }
630 }
631
632 fn new_leaf_file(mtime: i64) -> Rc<Leaf<FileContents>> {
634 Rc::new(Leaf {
635 stat: stat_with_mtime(mtime),
636 content: LeafContent::Regular(FileContents::default()),
637 })
638 }
639
640 fn new_leaf_symlink(target: &str, mtime: i64) -> Rc<Leaf<FileContents>> {
642 Rc::new(Leaf {
643 stat: stat_with_mtime(mtime),
644 content: LeafContent::Symlink(OsString::from(target).into_boxed_os_str()),
645 })
646 }
647
648 fn new_dir_inode<T>(mtime: i64) -> Inode<T> {
650 Inode::Directory(Box::new(Directory {
651 stat: stat_with_mtime(mtime),
652 entries: BTreeMap::new(),
653 }))
654 }
655
656 fn new_dir_inode_with_stat<T>(stat: Stat) -> Inode<T> {
658 Inode::Directory(Box::new(Directory {
659 stat,
660 entries: BTreeMap::new(),
661 }))
662 }
663
664 #[test]
665 fn test_directory_new() {
666 let stat = stat_with_mtime(123);
667 let dir = Directory::<()>::new(stat);
668 assert_eq!(dir.stat.st_mtim_sec, 123);
669 assert!(dir.entries.is_empty());
670 }
671
672 #[test]
673 fn test_insert_and_get_leaf() {
674 let mut dir = Directory::<FileContents>::new(default_stat());
675 let leaf = new_leaf_file(10);
676 dir.insert(OsStr::new("file.txt"), Inode::Leaf(Rc::clone(&leaf)));
677 assert_eq!(dir.entries.len(), 1);
678
679 let retrieved_leaf_rc = dir.ref_leaf(OsStr::new("file.txt")).unwrap();
680 assert!(Rc::ptr_eq(&retrieved_leaf_rc, &leaf));
681
682 let regular_file_content = dir.get_file(OsStr::new("file.txt")).unwrap();
683 assert!(matches!(regular_file_content, FileContents {}));
684 }
685
686 #[test]
687 fn test_insert_and_get_directory() {
688 let mut dir = Directory::<()>::new(default_stat());
689 let sub_dir_inode = new_dir_inode(20);
690 dir.insert(OsStr::new("subdir"), sub_dir_inode);
691 assert_eq!(dir.entries.len(), 1);
692
693 let retrieved_subdir = dir.get_directory(OsStr::new("subdir")).unwrap();
694 assert_eq!(retrieved_subdir.stat.st_mtim_sec, 20);
695
696 let retrieved_subdir_opt = dir
697 .get_directory_opt(OsStr::new("subdir"))
698 .unwrap()
699 .unwrap();
700 assert_eq!(retrieved_subdir_opt.stat.st_mtim_sec, 20);
701 }
702
703 #[test]
704 fn test_get_directory_errors() {
705 let mut root = Directory::new(default_stat());
706 root.insert(OsStr::new("dir1"), new_dir_inode(10));
707 root.insert(OsStr::new("file1"), Inode::Leaf(new_leaf_file(30)));
708
709 match root.get_directory(OsStr::new("nonexistent")) {
710 Err(ImageError::NotFound(name)) => assert_eq!(name.to_str().unwrap(), "nonexistent"),
711 _ => panic!("Expected NotFound"),
712 }
713 assert!(root
714 .get_directory_opt(OsStr::new("nonexistent"))
715 .unwrap()
716 .is_none());
717
718 match root.get_directory(OsStr::new("file1")) {
719 Err(ImageError::NotADirectory(name)) => assert_eq!(name.to_str().unwrap(), "file1"),
720 _ => panic!("Expected NotADirectory"),
721 }
722 }
723
724 #[test]
725 fn test_get_file_errors() {
726 let mut dir = Directory::new(default_stat());
727 dir.insert(OsStr::new("subdir"), new_dir_inode(10));
728 dir.insert(
729 OsStr::new("link.txt"),
730 Inode::Leaf(new_leaf_symlink("target", 20)),
731 );
732
733 match dir.get_file(OsStr::new("nonexistent.txt")) {
734 Err(ImageError::NotFound(name)) => {
735 assert_eq!(name.to_str().unwrap(), "nonexistent.txt")
736 }
737 _ => panic!("Expected NotFound"),
738 }
739 assert!(dir
740 .get_file_opt(OsStr::new("nonexistent.txt"))
741 .unwrap()
742 .is_none());
743
744 match dir.get_file(OsStr::new("subdir")) {
745 Err(ImageError::IsADirectory(name)) => assert_eq!(name.to_str().unwrap(), "subdir"),
746 _ => panic!("Expected IsADirectory"),
747 }
748 match dir.get_file(OsStr::new("link.txt")) {
749 Err(ImageError::IsNotRegular(name)) => assert_eq!(name.to_str().unwrap(), "link.txt"),
750 res => panic!("Expected IsNotRegular, got {res:?}"),
751 }
752 }
753
754 #[test]
755 fn test_remove() {
756 let mut dir = Directory::new(default_stat());
757 dir.insert(OsStr::new("file1.txt"), Inode::Leaf(new_leaf_file(10)));
758 dir.insert(OsStr::new("subdir"), new_dir_inode(20));
759 assert_eq!(dir.entries.len(), 2);
760
761 dir.remove(OsStr::new("file1.txt"));
762 assert_eq!(dir.entries.len(), 1);
763 assert!(!dir.entries.contains_key(OsStr::new("file1.txt")));
764
765 dir.remove(OsStr::new("nonexistent")); assert_eq!(dir.entries.len(), 1);
767 }
768
769 #[test]
770 fn test_merge() {
771 let mut dir = Directory::new(default_stat());
772
773 dir.merge(OsStr::new("item"), Inode::Leaf(new_leaf_file(10)));
775 assert_eq!(
776 dir.entries
777 .get(OsStr::new("item"))
778 .unwrap()
779 .stat()
780 .st_mtim_sec,
781 10
782 );
783
784 let mut existing_dir_inode = new_dir_inode_with_stat(stat_with_mtime(80));
786 if let Inode::Directory(ref mut ed_box) = existing_dir_inode {
787 ed_box.insert(OsStr::new("inner_file"), Inode::Leaf(new_leaf_file(85)));
788 }
789 dir.insert(OsStr::new("merged_dir"), existing_dir_inode);
790
791 let new_merging_dir_inode = new_dir_inode_with_stat(stat_with_mtime(90));
792 dir.merge(OsStr::new("merged_dir"), new_merging_dir_inode);
793
794 match dir.entries.get(OsStr::new("merged_dir")) {
795 Some(Inode::Directory(d)) => {
796 assert_eq!(d.stat.st_mtim_sec, 90); assert_eq!(d.entries.len(), 1); assert!(d.entries.contains_key(OsStr::new("inner_file")));
799 }
800 _ => panic!("Expected directory after merge"),
801 }
802
803 dir.merge(OsStr::new("merged_dir"), Inode::Leaf(new_leaf_file(100)));
805 assert!(matches!(
806 dir.entries.get(OsStr::new("merged_dir")),
807 Some(Inode::Leaf(_))
808 ));
809 assert_eq!(
810 dir.entries
811 .get(OsStr::new("merged_dir"))
812 .unwrap()
813 .stat()
814 .st_mtim_sec,
815 100
816 );
817 }
818
819 #[test]
820 fn test_clear() {
821 let mut dir = Directory::new(default_stat());
822 dir.insert(OsStr::new("file1"), Inode::Leaf(new_leaf_file(10)));
823 dir.stat.st_mtim_sec = 100;
824
825 dir.clear();
826 assert!(dir.entries.is_empty());
827 assert_eq!(dir.stat.st_mtim_sec, 100); }
829
830 #[test]
831 fn test_newest_file() {
832 let mut root = Directory::new(stat_with_mtime(5));
833 assert_eq!(root.newest_file(), 5);
834
835 root.insert(OsStr::new("file1"), Inode::Leaf(new_leaf_file(10)));
836 assert_eq!(root.newest_file(), 10);
837
838 let subdir_stat = stat_with_mtime(15);
839 let mut subdir = Box::new(Directory::new(subdir_stat));
840 subdir.insert(OsStr::new("subfile1"), Inode::Leaf(new_leaf_file(12)));
841 root.insert(OsStr::new("subdir"), Inode::Directory(subdir));
842 assert_eq!(root.newest_file(), 15);
843
844 if let Some(Inode::Directory(sd)) = root.entries.get_mut(OsStr::new("subdir")) {
845 sd.insert(OsStr::new("subfile2"), Inode::Leaf(new_leaf_file(20)));
846 }
847 assert_eq!(root.newest_file(), 20);
848
849 root.stat.st_mtim_sec = 25;
850 assert_eq!(root.newest_file(), 25);
851 }
852
853 #[test]
854 fn test_iteration_entries_sorted_inodes() {
855 let mut dir = Directory::new(default_stat());
856 dir.insert(OsStr::new("b_file"), Inode::Leaf(new_leaf_file(10)));
857 dir.insert(OsStr::new("a_dir"), new_dir_inode(20));
858 dir.insert(
859 OsStr::new("c_link"),
860 Inode::Leaf(new_leaf_symlink("target", 30)),
861 );
862
863 let names_from_entries: Vec<&OsStr> = dir.entries().map(|(name, _)| name).collect();
864 assert_eq!(names_from_entries.len(), 3); assert!(names_from_entries.contains(&OsStr::new("a_dir")));
866 assert!(names_from_entries.contains(&OsStr::new("b_file")));
867 assert!(names_from_entries.contains(&OsStr::new("c_link")));
868
869 let sorted_names: Vec<&OsStr> = dir.sorted_entries().map(|(name, _)| name).collect();
870 assert_eq!(
871 sorted_names,
872 vec![
873 OsStr::new("a_dir"),
874 OsStr::new("b_file"),
875 OsStr::new("c_link")
876 ]
877 );
878
879 let mut inode_types = vec![];
880 for inode in dir.inodes() {
881 match inode {
882 Inode::Directory(_) => inode_types.push("dir"),
883 Inode::Leaf(_) => inode_types.push("leaf"),
884 }
885 }
886 assert_eq!(inode_types.len(), 3);
887 assert_eq!(inode_types.iter().filter(|&&t| t == "dir").count(), 1);
888 assert_eq!(inode_types.iter().filter(|&&t| t == "leaf").count(), 2);
889 }
890
891 #[test]
892 fn test_copy_root_metadata_from_usr() {
893 let mut fs = FileSystem::<FileContents>::new(default_stat());
894
895 let usr_stat = Stat {
897 st_mode: 0o755,
898 st_uid: 42,
899 st_gid: 43,
900 st_mtim_sec: 1234567890,
901 xattrs: RefCell::new(BTreeMap::from([(
902 Box::from(OsStr::new("security.selinux")),
903 Box::from(b"system_u:object_r:usr_t:s0".as_slice()),
904 )])),
905 };
906 let usr_dir = Directory {
907 stat: usr_stat,
908 entries: BTreeMap::new(),
909 };
910 fs.root.entries.insert(
911 Box::from(OsStr::new("usr")),
912 Inode::Directory(Box::new(usr_dir)),
913 );
914
915 fs.copy_root_metadata_from_usr().unwrap();
916
917 assert_eq!(fs.root.stat.st_mode, 0o755);
918 assert_eq!(fs.root.stat.st_uid, 42);
919 assert_eq!(fs.root.stat.st_gid, 43);
920 assert_eq!(fs.root.stat.st_mtim_sec, 1234567890);
921 assert!(fs
922 .root
923 .stat
924 .xattrs
925 .borrow()
926 .contains_key(OsStr::new("security.selinux")));
927 }
928
929 #[test]
930 fn test_copy_root_metadata_from_usr_missing() {
931 let mut fs = FileSystem::<FileContents>::new(default_stat());
932
933 match fs.copy_root_metadata_from_usr() {
934 Err(ImageError::NotFound(name)) => assert_eq!(name.to_str().unwrap(), "usr"),
935 other => panic!("Expected NotFound error, got {:?}", other),
936 }
937 }
938
939 #[test]
940 fn test_filter_xattrs() {
941 let root_stat = Stat {
942 st_mode: 0o755,
943 st_uid: 0,
944 st_gid: 0,
945 st_mtim_sec: 0,
946 xattrs: RefCell::new(BTreeMap::from([
947 (
948 Box::from(OsStr::new("security.selinux")),
949 Box::from(b"label".as_slice()),
950 ),
951 (
952 Box::from(OsStr::new("security.capability")),
953 Box::from(b"cap".as_slice()),
954 ),
955 (
956 Box::from(OsStr::new("user.custom")),
957 Box::from(b"value".as_slice()),
958 ),
959 ])),
960 };
961 let fs = FileSystem::<FileContents>::new(root_stat);
962
963 fs.filter_xattrs(|name| name.as_encoded_bytes().starts_with(b"user."));
965
966 let root_xattrs = fs.root.stat.xattrs.borrow();
967 assert_eq!(root_xattrs.len(), 1);
968 assert!(root_xattrs.contains_key(OsStr::new("user.custom")));
969 }
970
971 #[test]
972 fn test_canonicalize_run() {
973 let mut fs = FileSystem::<FileContents>::new(default_stat());
974
975 let usr_dir = Directory::new(stat_with_mtime(12345));
977 fs.root
978 .insert(OsStr::new("usr"), Inode::Directory(Box::new(usr_dir)));
979
980 let mut run_dir = Directory::new(stat_with_mtime(99999));
982 run_dir.insert(OsStr::new("somefile"), Inode::Leaf(new_leaf_file(11111)));
983 let mut subdir = Directory::new(stat_with_mtime(22222));
984 subdir.insert(OsStr::new("nested"), Inode::Leaf(new_leaf_file(33333)));
985 run_dir.insert(OsStr::new("subdir"), Inode::Directory(Box::new(subdir)));
986 fs.root
987 .insert(OsStr::new("run"), Inode::Directory(Box::new(run_dir)));
988
989 assert_eq!(
991 fs.root
992 .get_directory(OsStr::new("run"))
993 .unwrap()
994 .entries
995 .len(),
996 2
997 );
998
999 fs.canonicalize_run().unwrap();
1001
1002 let run = fs.root.get_directory(OsStr::new("run")).unwrap();
1004 assert!(run.entries.is_empty());
1005 assert_eq!(run.stat.st_mtim_sec, 12345);
1006 }
1007
1008 #[test]
1009 fn test_canonicalize_run_no_run_dir() {
1010 let mut fs = FileSystem::<FileContents>::new(default_stat());
1011
1012 let usr_dir = Directory::new(stat_with_mtime(12345));
1014 fs.root
1015 .insert(OsStr::new("usr"), Inode::Directory(Box::new(usr_dir)));
1016
1017 fs.canonicalize_run().unwrap();
1019 }
1020
1021 #[test]
1022 fn test_transform_for_oci() {
1023 let mut fs = FileSystem::<FileContents>::new(default_stat());
1024
1025 let usr_stat = Stat {
1027 st_mode: 0o750,
1028 st_uid: 100,
1029 st_gid: 200,
1030 st_mtim_sec: 54321,
1031 xattrs: RefCell::new(BTreeMap::from([(
1032 Box::from(OsStr::new("user.test")),
1033 Box::from(b"val".as_slice()),
1034 )])),
1035 };
1036 fs.root
1037 .insert(OsStr::new("usr"), new_dir_inode_with_stat(usr_stat));
1038
1039 let mut run_dir = Directory::new(stat_with_mtime(99999));
1041 run_dir.insert(OsStr::new("file"), Inode::Leaf(new_leaf_file(11111)));
1042 fs.root
1043 .insert(OsStr::new("run"), Inode::Directory(Box::new(run_dir)));
1044
1045 fs.transform_for_oci().unwrap();
1047
1048 assert_eq!(fs.root.stat.st_mode, 0o750);
1050 assert_eq!(fs.root.stat.st_uid, 100);
1051 assert_eq!(fs.root.stat.st_gid, 200);
1052 assert_eq!(fs.root.stat.st_mtim_sec, 54321);
1053
1054 let run = fs.root.get_directory(OsStr::new("run")).unwrap();
1056 assert!(run.entries.is_empty());
1057 assert_eq!(run.stat.st_mtim_sec, 54321);
1058 }
1059}