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}