tugger_file_manifest/
lib.rs

1// Copyright 2022 Gregory Szorc.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use 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
19/// File mode indicating execute bit for other.
20pub const S_IXOTH: u32 = 0o1;
21/// File mode indicating write bit for other.
22pub const S_IWOTH: u32 = 0o2;
23/// File mode indicating read bit for other.
24pub const S_IROTH: u32 = 0o4;
25/// File mode indicating execute bit for group.
26pub const S_IXGRP: u32 = 0o10;
27/// File mode indicating write bit for group.
28pub const S_IWGRP: u32 = 0o20;
29/// File mode indicating read bit for group.
30pub const S_IRGRP: u32 = 0o40;
31/// File mode indicating execute bit for owner.
32pub const S_IXUSR: u32 = 0o100;
33/// File mode indicating write bit for owner.
34pub const S_IWUSR: u32 = 0o200;
35/// File mode indicating read bit for owner.
36pub const S_IRUSR: u32 = 0o400;
37/// Sticky bit.
38pub const S_ISVTX: u32 = 0o1000;
39/// Set GID bit.
40pub const S_ISGID: u32 = 0o2000;
41/// Set UID bit.
42pub const S_ISUID: u32 = 0o4000;
43/// File mode is a fifo / named pipe.
44pub const S_IFIFO: u32 = 0o10000;
45/// File mode is a character device.
46pub const S_IFCHR: u32 = 0o20000;
47/// File mode indicating a directory.
48pub const S_IFDIR: u32 = 0o40000;
49/// File mode indicating a block device.
50pub const S_IFBLK: u32 = 0o60000;
51/// File mode indicating a regular file.
52pub const S_IFREG: u32 = 0o100000;
53/// File mode indicating a symbolic link.
54pub const S_IFLNK: u32 = 0o120000;
55/// File mode indicating a socket.
56pub 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    // The function to call depends on the type of the target.
98    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/// Represents an abstract location for binary data.
108///
109/// Data can be backed by the filesystem or in memory.
110#[derive(Clone, Debug, PartialEq)]
111pub enum FileData {
112    Path(PathBuf),
113    Memory(Vec<u8>),
114}
115
116impl FileData {
117    /// Resolve the data for this instance.
118    ///
119    /// If backed by a file, the file will be read.
120    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    /// Convert this instance to a memory variant.
132    ///
133    /// This ensures any file-backed data is present in memory.
134    pub fn to_memory(&self) -> Result<Self, std::io::Error> {
135        Ok(Self::Memory(self.resolve_content()?))
136    }
137
138    /// Obtain a filesystem path backing this content.
139    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/// Represents a virtual file, without an associated path.
172#[derive(Clone, Debug, PartialEq)]
173pub struct FileEntry {
174    /// The content of the file.
175    data: FileData,
176
177    /// Whether the file is executable.
178    executable: bool,
179
180    /// Indicates that this file is a link pointing to the specified path.
181    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    /// Construct a new instance given data and an executable bit.
235    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    /// Construct a new instance referencing a path.
244    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    /// Obtain the [FileData] backing this instance.
253    pub fn file_data(&self) -> &FileData {
254        &self.data
255    }
256
257    /// Whether the file is executable.
258    pub fn is_executable(&self) -> bool {
259        self.executable
260    }
261
262    /// Set whether the file is executable.
263    pub fn set_executable(&mut self, v: bool) {
264        self.executable = v;
265    }
266
267    /// Resolve the data constituting this file.
268    pub fn resolve_content(&self) -> Result<Vec<u8>, std::io::Error> {
269        self.data.resolve_content()
270    }
271
272    /// Obtain the target of a link, if this is a link entry.
273    pub fn link_target(&self) -> Option<&Path> {
274        self.link.as_deref()
275    }
276
277    /// Set the target of a link.
278    pub fn set_link_target(&mut self, target: PathBuf) {
279        self.link = Some(target);
280    }
281
282    /// Obtain a new instance guaranteed to have file data stored in memory.
283    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    /// Write this file entry to the given destination path.
292    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/// Represents a virtual file, with an associated path.
315#[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    /// Create a new instance from a path and `FileEntry`.
342    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    /// The path of this instance.
350    pub fn path(&self) -> &Path {
351        &self.path
352    }
353
354    /// The [FileEntry] holding details about this file.
355    pub fn entry(&self) -> &FileEntry {
356        &self.entry
357    }
358
359    /// Obtain an instance that is guaranteed to be backed by memory.
360    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    /// Obtain the path to this file as a String.
368    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
420/// Normalize a path or error on validation failure.
421///
422/// This is called before inserting paths into a [FileManifest].
423pub fn normalize_path(path: &Path) -> Result<PathBuf, FileManifestError> {
424    // Always store UNIX style directory separators.
425    let path_s = format!("{}", path.display()).replace('\\', "/");
426
427    if path_s.contains("..") {
428        return Err(FileManifestError::IllegalRelativePath(path_s));
429    }
430
431    // is_absolute() on Windows doesn't check for leading /.
432    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/// Represents a collection of files.
440///
441/// Files are keyed by their path. The file content is abstract and can be
442/// backed by multiple sources.
443#[derive(Clone, Debug, Default, PartialEq)]
444pub struct FileManifest {
445    files: BTreeMap<PathBuf, FileEntry>,
446    /// Whether to allow storage of links.
447    allow_links: bool,
448}
449
450impl FileManifest {
451    /// Create a new instance that allows the storage of links.
452    pub fn new_with_links() -> Self {
453        Self {
454            files: BTreeMap::new(),
455            allow_links: true,
456        }
457    }
458
459    /// Whether the instance has any files entries.
460    pub fn is_empty(&self) -> bool {
461        self.files.is_empty()
462    }
463
464    /// Add a file on the filesystem to the manifest.
465    ///
466    /// The filesystem path must have a prefix specified which will be stripped
467    /// from the manifest path. This prefix must appear in the passed path.
468    ///
469    /// The stored file data is a reference to the file path. So that file must
470    /// outlive this manifest instance.
471    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    /// Add a file on the filesystem to this manifest, reading file data into memory.
488    ///
489    /// This is like `add_path()` except the file is read and its contents stored in
490    /// memory. This ensures that the file can be materialized even if the source file
491    /// is deleted.
492    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    /// Add a `FileEntry` to this manifest under the given path.
509    ///
510    /// The path cannot contain relative paths and must not be absolute.
511    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    /// Add an iterable of `File` to this manifest.
528    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    /// Add a symlink to the manifest.
540    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    /// Merge the content of another manifest into this one.
555    ///
556    /// All entries from the other manifest are overlayed into this manifest while
557    /// preserving paths exactly. If this manifest already has an entry for a given
558    /// path, it will be overwritten by an entry in the other manifest.
559    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    /// Obtain all relative directories contained within files in this manifest.
568    ///
569    /// The root directory is not represented in the return value.
570    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    /// Resolve all required directories relative to another directory.
588    ///
589    /// The root directory itself is included.
590    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    /// Whether this manifest contains the specified file path.
603    pub fn has_path(&self, path: impl AsRef<Path>) -> bool {
604        self.files.contains_key(path.as_ref())
605    }
606
607    /// Obtain the entry for a given path.
608    pub fn get(&self, path: impl AsRef<Path>) -> Option<&FileEntry> {
609        self.files.get(path.as_ref())
610    }
611
612    /// Obtain an iterator over paths and file entries in this manifest.
613    pub fn iter_entries(&self) -> std::collections::btree_map::Iter<PathBuf, FileEntry> {
614        self.files.iter()
615    }
616
617    /// Obtain an iterator of entries as `File` instances.
618    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    /// Remove an entry from this manifest.
623    pub fn remove(&mut self, path: impl AsRef<Path>) -> Option<FileEntry> {
624        self.files.remove(path.as_ref())
625    }
626
627    /// Obtain entries in this manifest grouped by directory.
628    ///
629    /// The returned map has keys corresponding to the relative directory and
630    /// values of files in that directory.
631    ///
632    /// The root directory is modeled by the `None` key.
633    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            // Ensure there are keys for all parents.
655            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    /// Write files in this manifest to the specified path.
670    ///
671    /// Existing files will be replaced if they exist.
672    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    /// Calls `materialize_files()` but removes the destination directory if it exists.
690    ///
691    /// This ensures the content of the destination reflects exactly what's defined
692    /// in this manifest.
693    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    /// Ensure the content of all entries is backed by memory.
706    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}