compact_path_tree/
lib.rs

1use std::fs::DirEntry;
2use std::io;
3use std::path::{Component, Components, Path, PathBuf};
4
5/// A visitor that can determine which paths should be included and how errors
6/// are handled, or just view paths as the tree is constructed.
7pub trait PathVisitor {
8    /// Determine whether the given entry should be included in the tree.
9    ///
10    /// When `Ok(true)` is returned, the entry is included. When `Ok(false)` is
11    /// returned, the entry is omitted, including any children for directories.
12    /// When `Err(..)` is returned, the item is omitted and
13    /// `PathVisitor::handle_error` is used to determine whether the operation
14    /// should fail or not.
15    fn filter(&mut self, _entry: &DirEntry) -> io::Result<bool> {
16        Ok(true)
17    }
18
19    /// A general-purpose function for any logic involving included entries.
20    /// This is called after `filter` and only for entries for which `filter`
21    /// returned `Ok(true)`.
22    fn visit(&mut self, _entry: &DirEntry) -> io::Result<()> {
23        Ok(())
24    }
25
26    /// Handle a given IO error, determining whether it should be fatal or
27    /// not.
28    ///
29    /// `Some(..)` indicates that the error is fatal and should stop traversal,
30    /// and `None` indicates that the error is non-fatal and should be ignored.
31    /// When an error is ignored, the
32    ///
33    /// This function will be called for any error that occurs, including errors
34    /// from `PathVisitor::filter` or `PathVisitor::visit`.
35    ///
36    /// The default implementation logs `PermissionDenied` errors to stderr but
37    /// doesn't stop.
38    fn handle_error(
39        &mut self,
40        error: io::Error,
41        directory: &Path,
42        entry: Option<&DirEntry>,
43    ) -> Option<io::Error> {
44        match error.kind() {
45            io::ErrorKind::PermissionDenied => {
46                let description = match entry {
47                    None => format!("item in `{}`", directory.display()),
48                    Some(i) => format!("`{}`", i.path().display().to_string()),
49                };
50                eprintln!("Permission denied reading {}: {}", description, error);
51                None
52            }
53            _ => Some(error),
54        }
55    }
56}
57
58/// A compact immutable representation of the paths within a directory.
59#[derive(Clone, PartialEq, Eq)]
60pub struct CompactPathTree {
61    root: PathBuf,
62    path: PathBuf,
63}
64
65impl CompactPathTree {
66    fn add_item(
67        path: &mut PathBuf,
68        item: &DirEntry,
69        visitor: &mut impl PathVisitor,
70    ) -> io::Result<()> {
71        if !visitor.filter(&item)? {
72            return Ok(());
73        }
74
75        visitor.visit(item)?;
76
77        // very important! try to get type before adding anything to the tree:
78        // if an error occurs and the visitor opts to ignore it, we don't want
79        // to leave the tree in a partially modified state
80        let typ = item.file_type()?;
81
82        path.push(item.file_name());
83        if typ.is_dir() {
84            // as above, make sure we never leave the path in an illegal state
85            let r = Self::build_tree(path, &item.path(), visitor);
86            path.push(Component::ParentDir.as_os_str());
87            r?;
88        } else {
89            path.push(Component::ParentDir.as_os_str());
90        }
91
92        Ok(())
93    }
94
95    fn build_tree(
96        path: &mut PathBuf,
97        dir: &Path,
98        visitor: &mut impl PathVisitor,
99    ) -> io::Result<()> {
100        for item in dir.read_dir()? {
101            let item = match item.map_err(|e| visitor.handle_error(e, dir, None)) {
102                Ok(i) => i,
103                Err(None) => continue,
104                Err(Some(e)) => return Err(e),
105            };
106
107            if let Err(Some(e)) = Self::add_item(path, &item, visitor)
108                .map_err(|e| visitor.handle_error(e, dir, Some(&item)))
109            {
110                return Err(e);
111            }
112        }
113
114        Ok(())
115    }
116
117    /// Construct a new `CompactPathTree` by doing a depth-first traversal of
118    /// the given directory.
119    ///
120    /// The given visitor is used to determine which items should be included
121    /// and what errors are fatal.
122    ///
123    /// Symbolic links will be stored in the tree, but not followed.
124    pub fn new(root: PathBuf, visitor: &mut impl PathVisitor) -> io::Result<Self> {
125        let mut path = PathBuf::new();
126        Self::build_tree(&mut path, &root, visitor)?;
127        path.shrink_to_fit();
128
129        Ok(Self { root, path })
130    }
131
132    /// Get the underlying path this tree is represented as.
133    pub fn inner(&self) -> &Path {
134        &self.path
135    }
136
137    /// Get the root path this tree was constructed from.
138    pub fn root(&self) -> &Path {
139        &self.root
140    }
141
142    /// Get an iterator over the paths stored in this tree.
143    ///
144    /// The root path isn't included in the output of this iterator, only
145    /// its contents are. The paths are iterated in a depth-first traversal of
146    /// the tree, with parents being emitted before children. No other
147    /// guarantees are made with regards to ordering.
148    pub fn iter(&self) -> CompactPathTreeIter {
149        self.into_iter()
150    }
151}
152
153impl<'a> IntoIterator for &'a CompactPathTree {
154    type Item = PathBuf;
155    type IntoIter = CompactPathTreeIter<'a>;
156
157    fn into_iter(self) -> Self::IntoIter {
158        CompactPathTreeIter {
159            current: self.root.clone(),
160            components: self.path.components(),
161        }
162    }
163}
164
165pub struct CompactPathTreeIter<'a> {
166    current: PathBuf,
167    components: Components<'a>,
168}
169
170impl<'a> Iterator for CompactPathTreeIter<'a> {
171    type Item = PathBuf;
172
173    fn next(&mut self) -> Option<Self::Item> {
174        for c in &mut self.components {
175            match c {
176                Component::ParentDir => {
177                    self.current.pop();
178                }
179                Component::Normal(p) => {
180                    self.current.push(p);
181                    return Some(self.current.clone());
182                }
183                c => unreachable!("illegal component {:?} in path tree", c),
184            }
185        }
186
187        None
188    }
189}