1use std::{
10 collections::{BTreeMap, BTreeSet},
11 ffi::OsStr,
12 io::Write,
13 path::{Path, PathBuf},
14};
15
16#[cfg(unix)]
17use std::os::unix::fs::PermissionsExt;
18
19pub const S_IXOTH: u32 = 0o1;
21pub const S_IWOTH: u32 = 0o2;
23pub const S_IROTH: u32 = 0o4;
25pub const S_IXGRP: u32 = 0o10;
27pub const S_IWGRP: u32 = 0o20;
29pub const S_IRGRP: u32 = 0o40;
31pub const S_IXUSR: u32 = 0o100;
33pub const S_IWUSR: u32 = 0o200;
35pub const S_IRUSR: u32 = 0o400;
37pub const S_ISVTX: u32 = 0o1000;
39pub const S_ISGID: u32 = 0o2000;
41pub const S_ISUID: u32 = 0o4000;
43pub const S_IFIFO: u32 = 0o10000;
45pub const S_IFCHR: u32 = 0o20000;
47pub const S_IFDIR: u32 = 0o40000;
49pub const S_IFBLK: u32 = 0o60000;
51pub const S_IFREG: u32 = 0o100000;
53pub const S_IFLNK: u32 = 0o120000;
55pub const S_IFSOCK: u32 = 0o140000;
57
58#[cfg(unix)]
59pub fn is_executable(metadata: &std::fs::Metadata) -> bool {
60 let permissions = metadata.permissions();
61 permissions.mode() & 0o111 != 0
62}
63
64#[cfg(windows)]
65pub fn is_executable(_metadata: &std::fs::Metadata) -> bool {
66 false
67}
68
69#[cfg(unix)]
70pub fn set_executable(file: &mut std::fs::File) -> Result<(), std::io::Error> {
71 let mut permissions = file.metadata()?.permissions();
72 permissions.set_mode(0o770);
73 file.set_permissions(permissions)?;
74 Ok(())
75}
76
77#[cfg(windows)]
78pub fn set_executable(_file: &mut std::fs::File) -> Result<(), std::io::Error> {
79 Ok(())
80}
81
82#[cfg(unix)]
83pub fn create_symlink(
84 path: impl AsRef<Path>,
85 target: impl AsRef<Path>,
86) -> Result<(), std::io::Error> {
87 std::os::unix::fs::symlink(target, path)
88}
89
90#[cfg(windows)]
91pub fn create_symlink(
92 path: impl AsRef<Path>,
93 target: impl AsRef<Path>,
94) -> Result<(), std::io::Error> {
95 let target = target.as_ref();
96
97 let metadata = std::fs::metadata(target)?;
99
100 if metadata.is_dir() {
101 std::os::windows::fs::symlink_dir(target, path)
102 } else {
103 std::os::windows::fs::symlink_file(target, path)
104 }
105}
106
107#[derive(Clone, Debug, PartialEq)]
111pub enum FileData {
112 Path(PathBuf),
113 Memory(Vec<u8>),
114}
115
116impl FileData {
117 pub fn resolve_content(&self) -> Result<Vec<u8>, std::io::Error> {
121 match self {
122 Self::Path(p) => {
123 let data = std::fs::read(p)?;
124
125 Ok(data)
126 }
127 Self::Memory(data) => Ok(data.clone()),
128 }
129 }
130
131 pub fn to_memory(&self) -> Result<Self, std::io::Error> {
135 Ok(Self::Memory(self.resolve_content()?))
136 }
137
138 pub fn backing_path(&self) -> Option<&Path> {
140 match self {
141 Self::Path(p) => Some(p.as_path()),
142 Self::Memory(_) => None,
143 }
144 }
145}
146
147impl From<&Path> for FileData {
148 fn from(path: &Path) -> Self {
149 Self::Path(path.to_path_buf())
150 }
151}
152
153impl From<PathBuf> for FileData {
154 fn from(path: PathBuf) -> Self {
155 Self::Path(path)
156 }
157}
158
159impl From<Vec<u8>> for FileData {
160 fn from(data: Vec<u8>) -> Self {
161 Self::Memory(data)
162 }
163}
164
165impl From<&[u8]> for FileData {
166 fn from(data: &[u8]) -> Self {
167 Self::Memory(data.into())
168 }
169}
170
171#[derive(Clone, Debug, PartialEq)]
173pub struct FileEntry {
174 data: FileData,
176
177 executable: bool,
179
180 link: Option<PathBuf>,
182}
183
184impl TryFrom<&Path> for FileEntry {
185 type Error = std::io::Error;
186
187 fn try_from(path: &Path) -> Result<Self, Self::Error> {
188 let metadata = std::fs::metadata(path)?;
189 let executable = is_executable(&metadata);
190
191 Ok(Self {
192 data: FileData::from(path),
193 executable,
194 link: None,
195 })
196 }
197}
198
199impl TryFrom<PathBuf> for FileEntry {
200 type Error = std::io::Error;
201
202 fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
203 Self::try_from(path.as_path())
204 }
205}
206
207impl From<&FileEntry> for FileEntry {
208 fn from(entry: &FileEntry) -> Self {
209 entry.clone()
210 }
211}
212
213impl From<Vec<u8>> for FileEntry {
214 fn from(data: Vec<u8>) -> Self {
215 Self {
216 data: data.into(),
217 executable: false,
218 link: None,
219 }
220 }
221}
222
223impl From<&[u8]> for FileEntry {
224 fn from(data: &[u8]) -> Self {
225 Self {
226 data: data.into(),
227 executable: false,
228 link: None,
229 }
230 }
231}
232
233impl FileEntry {
234 pub fn new_from_data(data: impl Into<FileData>, executable: bool) -> Self {
236 Self {
237 data: data.into(),
238 executable,
239 link: None,
240 }
241 }
242
243 pub fn new_from_path(path: impl AsRef<Path>, executable: bool) -> Self {
245 Self {
246 data: path.as_ref().into(),
247 executable,
248 link: None,
249 }
250 }
251
252 pub fn file_data(&self) -> &FileData {
254 &self.data
255 }
256
257 pub fn is_executable(&self) -> bool {
259 self.executable
260 }
261
262 pub fn set_executable(&mut self, v: bool) {
264 self.executable = v;
265 }
266
267 pub fn resolve_content(&self) -> Result<Vec<u8>, std::io::Error> {
269 self.data.resolve_content()
270 }
271
272 pub fn link_target(&self) -> Option<&Path> {
274 self.link.as_deref()
275 }
276
277 pub fn set_link_target(&mut self, target: PathBuf) {
279 self.link = Some(target);
280 }
281
282 pub fn to_memory(&self) -> Result<Self, std::io::Error> {
284 Ok(Self {
285 data: self.data.to_memory()?,
286 executable: self.executable,
287 link: self.link.clone(),
288 })
289 }
290
291 pub fn write_to_path(&self, dest_path: impl AsRef<Path>) -> Result<(), FileManifestError> {
293 let dest_path = dest_path.as_ref();
294 let parent = dest_path
295 .parent()
296 .ok_or(FileManifestError::NoParentDirectory)?;
297
298 std::fs::create_dir_all(parent)?;
299
300 if let Some(link) = &self.link {
301 create_symlink(dest_path, link)?;
302 } else {
303 let mut fh = std::fs::File::create(&dest_path)?;
304 fh.write_all(&self.resolve_content()?)?;
305 if self.executable {
306 set_executable(&mut fh)?;
307 }
308 }
309
310 Ok(())
311 }
312}
313
314#[derive(Clone, Debug, PartialEq)]
316pub struct File {
317 path: PathBuf,
318 entry: FileEntry,
319}
320
321impl TryFrom<&Path> for File {
322 type Error = std::io::Error;
323
324 fn try_from(path: &Path) -> Result<Self, Self::Error> {
325 let entry = FileEntry::try_from(path)?;
326
327 Ok(Self {
328 path: path.to_path_buf(),
329 entry,
330 })
331 }
332}
333
334impl From<File> for FileEntry {
335 fn from(f: File) -> Self {
336 f.entry
337 }
338}
339
340impl File {
341 pub fn new(path: impl AsRef<Path>, entry: impl Into<FileEntry>) -> Self {
343 Self {
344 path: path.as_ref().to_path_buf(),
345 entry: entry.into(),
346 }
347 }
348
349 pub fn path(&self) -> &Path {
351 &self.path
352 }
353
354 pub fn entry(&self) -> &FileEntry {
356 &self.entry
357 }
358
359 pub fn to_memory(&self) -> Result<Self, std::io::Error> {
361 Ok(Self {
362 path: self.path.clone(),
363 entry: self.entry.to_memory()?,
364 })
365 }
366
367 pub fn path_string(&self) -> String {
369 self.path.display().to_string()
370 }
371}
372
373impl AsRef<Path> for File {
374 fn as_ref(&self) -> &Path {
375 &self.path
376 }
377}
378
379#[derive(Debug)]
380pub enum FileManifestError {
381 IllegalRelativePath(String),
382 IllegalAbsolutePath(String),
383 NoParentDirectory,
384 IoError(std::io::Error),
385 StripPrefix(std::path::StripPrefixError),
386 LinkNotAllowed,
387}
388
389impl std::fmt::Display for FileManifestError {
390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 match self {
392 Self::IllegalRelativePath(path) => {
393 f.write_str(&format!("path cannot contain '..': {}", path))
394 }
395 Self::IllegalAbsolutePath(path) => {
396 f.write_str(&format!("path cannot be absolute: {}", path))
397 }
398 Self::NoParentDirectory => f.write_str("could not resolve parent directory"),
399 Self::IoError(inner) => inner.fmt(f),
400 Self::StripPrefix(inner) => inner.fmt(f),
401 Self::LinkNotAllowed => f.write_str("links are not allowed on this FileManifest"),
402 }
403 }
404}
405
406impl std::error::Error for FileManifestError {}
407
408impl From<std::io::Error> for FileManifestError {
409 fn from(err: std::io::Error) -> Self {
410 Self::IoError(err)
411 }
412}
413
414impl From<std::path::StripPrefixError> for FileManifestError {
415 fn from(err: std::path::StripPrefixError) -> Self {
416 Self::StripPrefix(err)
417 }
418}
419
420pub fn normalize_path(path: &Path) -> Result<PathBuf, FileManifestError> {
424 let path_s = format!("{}", path.display()).replace('\\', "/");
426
427 if path_s.contains("..") {
428 return Err(FileManifestError::IllegalRelativePath(path_s));
429 }
430
431 if path_s.starts_with('/') || path.is_absolute() {
433 return Err(FileManifestError::IllegalAbsolutePath(path_s));
434 }
435
436 Ok(PathBuf::from(path_s))
437}
438
439#[derive(Clone, Debug, Default, PartialEq)]
444pub struct FileManifest {
445 files: BTreeMap<PathBuf, FileEntry>,
446 allow_links: bool,
448}
449
450impl FileManifest {
451 pub fn new_with_links() -> Self {
453 Self {
454 files: BTreeMap::new(),
455 allow_links: true,
456 }
457 }
458
459 pub fn is_empty(&self) -> bool {
461 self.files.is_empty()
462 }
463
464 pub fn add_path(
472 &mut self,
473 path: impl AsRef<Path>,
474 strip_prefix: impl AsRef<Path>,
475 ) -> Result<(), FileManifestError> {
476 let path = path.as_ref();
477 let strip_prefix = strip_prefix.as_ref();
478
479 let add_path = path.strip_prefix(strip_prefix)?;
480
481 self.files
482 .insert(normalize_path(add_path)?, FileEntry::try_from(path)?);
483
484 Ok(())
485 }
486
487 pub fn add_path_memory(
493 &mut self,
494 path: impl AsRef<Path>,
495 strip_prefix: impl AsRef<Path>,
496 ) -> Result<(), FileManifestError> {
497 let path = path.as_ref();
498 let strip_prefix = strip_prefix.as_ref();
499
500 let add_path = path.strip_prefix(strip_prefix)?;
501
502 let entry = FileEntry::try_from(path)?.to_memory()?;
503 self.files.insert(normalize_path(add_path)?, entry);
504
505 Ok(())
506 }
507
508 pub fn add_file_entry(
512 &mut self,
513 path: impl AsRef<Path>,
514 entry: impl Into<FileEntry>,
515 ) -> Result<(), FileManifestError> {
516 let entry = entry.into();
517
518 if entry.link.is_some() && !self.allow_links {
519 return Err(FileManifestError::LinkNotAllowed);
520 }
521
522 self.files.insert(normalize_path(path.as_ref())?, entry);
523
524 Ok(())
525 }
526
527 pub fn add_files(
529 &mut self,
530 files: impl Iterator<Item = File>,
531 ) -> Result<(), FileManifestError> {
532 for file in files {
533 self.add_file_entry(file.path, file.entry)?;
534 }
535
536 Ok(())
537 }
538
539 pub fn add_symlink(
541 &mut self,
542 manifest_path: impl AsRef<Path>,
543 link_target: impl AsRef<Path>,
544 ) -> Result<(), FileManifestError> {
545 let entry = FileEntry {
546 data: vec![].into(),
547 executable: false,
548 link: Some(link_target.as_ref().to_path_buf()),
549 };
550
551 self.add_file_entry(manifest_path, entry)
552 }
553
554 pub fn add_manifest(&mut self, other: &Self) -> Result<(), FileManifestError> {
560 for (key, value) in &other.files {
561 self.add_file_entry(key, value.clone())?;
562 }
563
564 Ok(())
565 }
566
567 pub fn relative_directories(&self) -> Vec<PathBuf> {
571 let mut dirs = BTreeSet::new();
572
573 for p in self.files.keys() {
574 let mut ans = p.ancestors();
575 ans.next();
576
577 for a in ans {
578 if a.display().to_string() != "" {
579 dirs.insert(a.to_path_buf());
580 }
581 }
582 }
583
584 dirs.iter().map(|x| x.to_path_buf()).collect()
585 }
586
587 pub fn resolve_directories(&self, relative_to: impl AsRef<Path>) -> Vec<PathBuf> {
591 let relative_to = relative_to.as_ref();
592
593 let mut dirs = vec![relative_to.to_path_buf()];
594
595 for p in self.relative_directories() {
596 dirs.push(relative_to.join(p));
597 }
598
599 dirs
600 }
601
602 pub fn has_path(&self, path: impl AsRef<Path>) -> bool {
604 self.files.contains_key(path.as_ref())
605 }
606
607 pub fn get(&self, path: impl AsRef<Path>) -> Option<&FileEntry> {
609 self.files.get(path.as_ref())
610 }
611
612 pub fn iter_entries(&self) -> std::collections::btree_map::Iter<PathBuf, FileEntry> {
614 self.files.iter()
615 }
616
617 pub fn iter_files(&self) -> impl std::iter::Iterator<Item = File> + '_ {
619 self.files.iter().map(|(k, v)| File::new(k, v.clone()))
620 }
621
622 pub fn remove(&mut self, path: impl AsRef<Path>) -> Option<FileEntry> {
624 self.files.remove(path.as_ref())
625 }
626
627 pub fn entries_by_directory(
634 &self,
635 ) -> BTreeMap<Option<&Path>, BTreeMap<&OsStr, (&Path, &FileEntry)>> {
636 let mut res = BTreeMap::new();
637
638 for (path, content) in &self.files {
639 let parent = match path.parent() {
640 Some(p) => {
641 if p == Path::new("") {
642 None
643 } else {
644 Some(p)
645 }
646 }
647 None => None,
648 };
649 let filename = path.file_name().unwrap();
650
651 let entry = res.entry(parent).or_insert_with(BTreeMap::new);
652 entry.insert(filename, (path.as_path(), content));
653
654 if let Some(parent) = parent {
656 let mut parent = parent.parent();
657 while parent.is_some() && parent != Some(Path::new("")) {
658 res.entry(parent).or_insert_with(BTreeMap::new);
659 parent = parent.unwrap().parent();
660 }
661 }
662 }
663
664 res.entry(None).or_insert_with(BTreeMap::new);
665
666 res
667 }
668
669 pub fn materialize_files(
673 &self,
674 dest: impl AsRef<Path>,
675 ) -> Result<Vec<PathBuf>, FileManifestError> {
676 let mut dest_paths = vec![];
677
678 let dest = dest.as_ref();
679
680 for (k, v) in self.iter_entries() {
681 let dest_path = dest.join(k);
682 v.write_to_path(&dest_path)?;
683 dest_paths.push(dest_path)
684 }
685
686 Ok(dest_paths)
687 }
688
689 pub fn materialize_files_with_replace(
694 &self,
695 dest: impl AsRef<Path>,
696 ) -> Result<Vec<PathBuf>, FileManifestError> {
697 let dest = dest.as_ref();
698 if dest.exists() {
699 std::fs::remove_dir_all(dest)?;
700 }
701
702 self.materialize_files(dest)
703 }
704
705 pub fn ensure_in_memory(&mut self) -> Result<(), std::io::Error> {
707 for entry in self.files.values_mut() {
708 entry.data = entry.data.to_memory()?;
709 }
710
711 Ok(())
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718
719 #[cfg(unix)]
720 use tempfile::TempDir;
721
722 #[cfg(unix)]
723 fn temp_dir() -> std::io::Result<TempDir> {
724 tempfile::Builder::new()
725 .prefix("tugger-file-manifest-test-")
726 .tempdir()
727 }
728
729 #[test]
730 fn test_add_file_entry() -> Result<(), FileManifestError> {
731 let mut m = FileManifest::default();
732 let f = FileEntry::new_from_data(vec![42], false);
733
734 m.add_file_entry(Path::new("foo"), f.clone())?;
735
736 let entries = m.iter_entries().collect::<Vec<_>>();
737
738 assert_eq!(entries.len(), 1);
739 assert_eq!(entries[0].0, &PathBuf::from("foo"));
740 assert_eq!(entries[0].1, &f);
741
742 Ok(())
743 }
744
745 #[test]
746 fn test_add_files() -> Result<(), FileManifestError> {
747 let mut m = FileManifest::default();
748
749 let files = vec![
750 File::new("foo", vec![42]),
751 File::new("dir0/file0", vec![42]),
752 ];
753
754 m.add_files(files.into_iter())?;
755
756 assert_eq!(m.files.len(), 2);
757
758 Ok(())
759 }
760
761 #[test]
762 fn test_add_bad_path() -> Result<(), FileManifestError> {
763 let mut m = FileManifest::default();
764 let f = FileEntry::new_from_data(vec![], false);
765
766 let res = m.add_file_entry(Path::new("../etc/passwd"), f.clone());
767 let err = res.err().unwrap();
768 match err {
769 FileManifestError::IllegalRelativePath(_) => (),
770 _ => panic!("error does not match expected"),
771 }
772
773 let res = m.add_file_entry(Path::new("/foo"), f);
774 let err = res.err().unwrap();
775 match err {
776 FileManifestError::IllegalAbsolutePath(_) => (),
777 _ => panic!("error does not match expected"),
778 }
779
780 Ok(())
781 }
782
783 #[test]
784 fn add_link_rejected() -> Result<(), FileManifestError> {
785 let mut m = FileManifest::default();
786 let mut f = FileEntry::from(vec![42]);
787 f.link = Some("/etc/passwd".into());
788
789 let res = m.add_file_entry("foo", f);
790 let err = res.err().unwrap();
791 assert!(matches!(err, FileManifestError::LinkNotAllowed));
792
793 Ok(())
794 }
795
796 #[cfg(unix)]
797 #[test]
798 fn symlink_unix() -> Result<(), FileManifestError> {
799 let mut m = FileManifest::new_with_links();
800 m.add_symlink("etc", "/etc")?;
801
802 let td = temp_dir()?;
803
804 m.materialize_files(td.path())?;
805
806 let p = td.path().join("etc");
807 let metadata = std::fs::symlink_metadata(&p)?;
808
809 assert_ne!(metadata.permissions().mode() & S_IFLNK, 0);
810 assert_eq!(std::fs::read_link(&p)?, PathBuf::from("/etc"));
811
812 Ok(())
813 }
814
815 #[test]
816 fn test_relative_directories() -> Result<(), FileManifestError> {
817 let mut m = FileManifest::default();
818 let f = FileEntry::new_from_data(vec![], false);
819
820 m.add_file_entry(Path::new("foo"), f.clone())?;
821 let dirs = m.relative_directories();
822 assert_eq!(dirs.len(), 0);
823
824 m.add_file_entry(Path::new("dir1/dir2/foo"), f)?;
825 let dirs = m.relative_directories();
826 assert_eq!(
827 dirs,
828 vec![PathBuf::from("dir1"), PathBuf::from("dir1/dir2")]
829 );
830
831 Ok(())
832 }
833
834 #[test]
835 fn test_resolve_directories() -> Result<(), FileManifestError> {
836 let mut m = FileManifest::default();
837 let f = FileEntry::new_from_data(vec![], false);
838
839 m.add_file_entry(Path::new("foo"), f.clone())?;
840 m.add_file_entry(Path::new("dir1/dir2/foo"), f)?;
841
842 let dirs = m.resolve_directories(Path::new("/tmp"));
843 assert_eq!(
844 dirs,
845 vec![
846 PathBuf::from("/tmp"),
847 PathBuf::from("/tmp/dir1"),
848 PathBuf::from("/tmp/dir1/dir2")
849 ]
850 );
851
852 Ok(())
853 }
854
855 #[test]
856 fn test_entries_by_directory() -> Result<(), FileManifestError> {
857 let c = FileEntry::new_from_data(vec![42], false);
858
859 let mut m = FileManifest::default();
860 m.add_file_entry(Path::new("root.txt"), c.clone())?;
861 m.add_file_entry(Path::new("dir0/dir0_file0.txt"), c.clone())?;
862 m.add_file_entry(Path::new("dir0/child0/dir0_child0_file0.txt"), c.clone())?;
863 m.add_file_entry(Path::new("dir0/child0/dir0_child0_file1.txt"), c.clone())?;
864 m.add_file_entry(Path::new("dir0/child1/dir0_child1_file0.txt"), c.clone())?;
865 m.add_file_entry(Path::new("dir1/child0/dir1_child0_file0.txt"), c.clone())?;
866
867 let entries = m.entries_by_directory();
868
869 assert_eq!(entries.keys().count(), 6);
870 assert_eq!(
871 entries.keys().collect::<Vec<_>>(),
872 vec![
873 &None,
874 &Some(Path::new("dir0")),
875 &Some(Path::new("dir0/child0")),
876 &Some(Path::new("dir0/child1")),
877 &Some(Path::new("dir1")),
878 &Some(Path::new("dir1/child0")),
879 ]
880 );
881
882 assert_eq!(
883 entries.get(&None).unwrap(),
884 &[(
885 OsStr::new("root.txt"),
886 (PathBuf::from("root.txt").as_path(), &c)
887 ),]
888 .iter()
889 .cloned()
890 .collect()
891 );
892 assert_eq!(
893 entries.get(&Some(Path::new("dir0"))).unwrap(),
894 &[(
895 OsStr::new("dir0_file0.txt"),
896 (PathBuf::from("dir0/dir0_file0.txt").as_path(), &c)
897 )]
898 .iter()
899 .cloned()
900 .collect()
901 );
902 assert_eq!(
903 entries.get(&Some(Path::new("dir0/child0"))).unwrap(),
904 &[
905 (
906 OsStr::new("dir0_child0_file0.txt"),
907 (
908 PathBuf::from("dir0/child0/dir0_child0_file0.txt").as_path(),
909 &c
910 )
911 ),
912 (
913 OsStr::new("dir0_child0_file1.txt"),
914 (
915 PathBuf::from("dir0/child0/dir0_child0_file1.txt").as_path(),
916 &c
917 )
918 )
919 ]
920 .iter()
921 .cloned()
922 .collect()
923 );
924 assert_eq!(
925 entries.get(&Some(Path::new("dir0/child1"))).unwrap(),
926 &[(
927 OsStr::new("dir0_child1_file0.txt"),
928 (
929 PathBuf::from("dir0/child1/dir0_child1_file0.txt").as_path(),
930 &c
931 )
932 )]
933 .iter()
934 .cloned()
935 .collect()
936 );
937 assert_eq!(
938 entries.get(&Some(Path::new("dir1/child0"))).unwrap(),
939 &[(
940 OsStr::new("dir1_child0_file0.txt"),
941 (
942 PathBuf::from("dir1/child0/dir1_child0_file0.txt").as_path(),
943 &c
944 )
945 )]
946 .iter()
947 .cloned()
948 .collect()
949 );
950
951 Ok(())
952 }
953
954 #[test]
955 fn test_entries_by_directory_windows() -> Result<(), FileManifestError> {
956 let c = FileEntry::new_from_data(vec![42], false);
957
958 let mut m = FileManifest::default();
959 m.add_file_entry(Path::new("root.txt"), c.clone())?;
960 m.add_file_entry(Path::new("dir0\\dir0_file0.txt"), c.clone())?;
961 m.add_file_entry(Path::new("dir0\\child0\\dir0_child0_file0.txt"), c.clone())?;
962 m.add_file_entry(Path::new("dir0\\child0\\dir0_child0_file1.txt"), c.clone())?;
963 m.add_file_entry(Path::new("dir0\\child1\\dir0_child1_file0.txt"), c.clone())?;
964 m.add_file_entry(Path::new("dir1\\child0\\dir1_child0_file0.txt"), c.clone())?;
965
966 let entries = m.entries_by_directory();
967
968 assert_eq!(entries.keys().count(), 6);
969 assert_eq!(
970 entries.keys().collect::<Vec<_>>(),
971 vec![
972 &None,
973 &Some(Path::new("dir0")),
974 &Some(Path::new("dir0/child0")),
975 &Some(Path::new("dir0/child1")),
976 &Some(Path::new("dir1")),
977 &Some(Path::new("dir1/child0")),
978 ]
979 );
980
981 assert_eq!(
982 entries.get(&None).unwrap(),
983 &[(
984 OsStr::new("root.txt"),
985 (PathBuf::from("root.txt").as_path(), &c)
986 ),]
987 .iter()
988 .cloned()
989 .collect()
990 );
991 assert_eq!(
992 entries.get(&Some(Path::new("dir0"))).unwrap(),
993 &[(
994 OsStr::new("dir0_file0.txt"),
995 (PathBuf::from("dir0/dir0_file0.txt").as_path(), &c)
996 )]
997 .iter()
998 .cloned()
999 .collect()
1000 );
1001 assert_eq!(
1002 entries.get(&Some(Path::new("dir0/child0"))).unwrap(),
1003 &[
1004 (
1005 OsStr::new("dir0_child0_file0.txt"),
1006 (
1007 PathBuf::from("dir0/child0/dir0_child0_file0.txt").as_path(),
1008 &c
1009 )
1010 ),
1011 (
1012 OsStr::new("dir0_child0_file1.txt"),
1013 (
1014 PathBuf::from("dir0/child0/dir0_child0_file1.txt").as_path(),
1015 &c
1016 )
1017 )
1018 ]
1019 .iter()
1020 .cloned()
1021 .collect()
1022 );
1023 assert_eq!(
1024 entries.get(&Some(Path::new("dir0/child1"))).unwrap(),
1025 &[(
1026 OsStr::new("dir0_child1_file0.txt"),
1027 (
1028 PathBuf::from("dir0/child1/dir0_child1_file0.txt").as_path(),
1029 &c
1030 )
1031 )]
1032 .iter()
1033 .cloned()
1034 .collect()
1035 );
1036 assert_eq!(
1037 entries.get(&Some(Path::new("dir1/child0"))).unwrap(),
1038 &[(
1039 OsStr::new("dir1_child0_file0.txt"),
1040 (
1041 PathBuf::from("dir1/child0/dir1_child0_file0.txt").as_path(),
1042 &c
1043 )
1044 )]
1045 .iter()
1046 .cloned()
1047 .collect()
1048 );
1049
1050 Ok(())
1051 }
1052}