1use crate::api::{DirEntry, ListDirOptions, TraversalCompletion};
3use crate::{VfError, VfResult, internal::TraversalBudget};
4use std::path::Path;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum WalkControl {
9 Continue,
10 SkipSubtree,
11 Stop,
12}
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum WalkEventKind {
16 Enter,
17 Entry,
18 Leave,
19}
20#[derive(Debug, Clone)]
22pub struct WalkEvent {
23 pub kind: WalkEventKind,
24 pub entry: DirEntry,
25 pub depth: usize,
26}
27
28#[doc(hidden)]
32pub fn walk_events(
33 root: DirEntry,
34 options: ListDirOptions,
35 sort_by_name: bool,
36 mut read_dir: impl FnMut(&Path, ListDirOptions) -> VfResult<Vec<DirEntry>>,
37 mut callback: impl FnMut(&WalkEvent) -> VfResult<WalkControl>,
38) -> VfResult<TraversalCompletion> {
39 let mut budget = TraversalBudget::new(
40 options.max_entries_or(crate::api::DEFAULT_DIRECTORY_MAX_ENTRIES),
41 options.max_path_bytes_or(crate::api::DEFAULT_DIRECTORY_MAX_PATH_BYTES),
42 );
43 budget.charge(root.path())?;
44 let kind = if root.attrs().is_dir() {
45 WalkEventKind::Enter
46 } else {
47 WalkEventKind::Entry
48 };
49 let mut pending = vec![WalkEvent {
50 kind,
51 entry: root,
52 depth: 0,
53 }];
54 while let Some(mut event) = pending.pop() {
55 match callback(&event)? {
56 WalkControl::Stop => return Ok(TraversalCompletion::Stopped),
57 decision if event.kind == WalkEventKind::Enter => {
58 event.kind = WalkEventKind::Leave;
59 if decision == WalkControl::SkipSubtree {
60 pending.push(event);
61 continue;
62 }
63 if event.depth > options.max_depth_or(Default::default()).get() {
64 if options.truncate_at_max_depth() {
65 pending.push(event);
66 continue;
67 }
68 return Err(VfError::client(0, libc::EFBIG as u32)
69 .with_context("walk_events", event.entry.path()));
70 }
71 let remaining = budget.remaining_options(ListDirOptions::new());
72 let mut entries = read_dir(event.entry.path(), remaining)?;
73 for entry in &entries {
74 budget
75 .charge(entry.path())
76 .map_err(|error| error.with_context("walk_events", entry.path()))?;
77 }
78 if sort_by_name {
79 entries.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
80 }
81 let child_depth = event.depth + 1;
82 pending.push(event);
83 for entry in entries.into_iter().rev() {
84 let kind = if entry.attrs().is_dir() {
85 WalkEventKind::Enter
86 } else {
87 WalkEventKind::Entry
88 };
89 pending.push(WalkEvent {
90 kind,
91 entry,
92 depth: child_depth,
93 });
94 }
95 }
96 _ => {}
97 }
98 }
99 Ok(TraversalCompletion::Complete)
100}
101
102#[cfg(test)]
103mod tests {
104 use std::path::PathBuf;
105
106 use super::*;
107 use crate::{AttrMask, FileOperand, VfAttrs, VfType};
108 fn entry(path: &str, dir: bool) -> DirEntry {
109 DirEntry::new(
110 path.into(),
111 crate::metadata_from_attrs(VfAttrs {
112 file: FileOperand::from_path(path),
113 masks: AttrMask::MODE,
114 ftype: if dir {
115 VfType::Directory
116 } else {
117 VfType::Regular
118 },
119 ..Default::default()
120 }),
121 )
122 }
123 #[test]
124 fn prune_prevents_io_and_events_have_depth_first_order() {
125 let mut events = Vec::new();
126 walk_events(
127 entry("/", true),
128 ListDirOptions::default().with_recursive(true),
129 true,
130 |path, _| {
131 assert_eq!(path, Path::new("/"), "pruned directory must not be read");
132 Ok(vec![entry("/z", false), entry("/a", true)])
133 },
134 |event| {
135 events.push((event.kind, event.entry.path().to_path_buf(), event.depth));
136 Ok(if event.entry.path() == Path::new("/a") {
137 WalkControl::SkipSubtree
138 } else {
139 WalkControl::Continue
140 })
141 },
142 )
143 .unwrap();
144 assert_eq!(
145 events
146 .iter()
147 .map(|e| (e.0, e.1.as_path(), e.2))
148 .collect::<Vec<_>>(),
149 vec![
150 (WalkEventKind::Enter, Path::new("/"), 0),
151 (WalkEventKind::Enter, Path::new("/a"), 1),
152 (WalkEventKind::Leave, Path::new("/a"), 1),
153 (WalkEventKind::Entry, Path::new("/z"), 1),
154 (WalkEventKind::Leave, Path::new("/"), 0)
155 ]
156 );
157 }
158 #[test]
159 fn depth_limit_allows_boundary_entries_and_only_blocks_directory_descent() {
160 let options = ListDirOptions::new()
161 .with_recursive(true)
162 .with_max_depth(crate::api::DepthLimit::new(0).expect("finite depth"));
163 let mut events = Vec::new();
164 assert_eq!(
165 walk_events(
166 entry("/", true),
167 options,
168 false,
169 |path, _| {
170 assert_eq!(path, Path::new("/"));
171 Ok(vec![entry("/file", false)])
172 },
173 |event| {
174 events.push((event.kind, event.entry.path().to_path_buf(), event.depth));
175 Ok(WalkControl::Continue)
176 },
177 )
178 .unwrap(),
179 TraversalCompletion::Complete
180 );
181 assert_eq!(
182 events,
183 vec![
184 (WalkEventKind::Enter, PathBuf::from("/"), 0),
185 (WalkEventKind::Entry, PathBuf::from("/file"), 1),
186 (WalkEventKind::Leave, PathBuf::from("/"), 0),
187 ]
188 );
189
190 let mut strict_entries = Vec::new();
191 let error = walk_events(
192 entry("/", true),
193 options,
194 false,
195 |path, _| {
196 assert_eq!(path, Path::new("/"));
197 Ok(vec![entry("/file", false), entry("/child", true)])
198 },
199 |event| {
200 strict_entries.push(event.entry.path().to_path_buf());
201 Ok(WalkControl::Continue)
202 },
203 )
204 .unwrap_err();
205 assert_eq!(error.raw_code(), libc::EFBIG as u32);
206 assert!(strict_entries.contains(&PathBuf::from("/file")));
207
208 let mut truncated_events = Vec::new();
209 let mut listed = Vec::new();
210 assert_eq!(
211 walk_events(
212 entry("/", true),
213 options.with_truncate_at_max_depth(true),
214 false,
215 |path, _| {
216 listed.push(path.to_path_buf());
217 assert_eq!(path, Path::new("/"));
218 Ok(vec![entry("/file", false), entry("/child", true)])
219 },
220 |event| {
221 truncated_events.push((event.kind, event.entry.path().to_path_buf()));
222 Ok(WalkControl::Continue)
223 },
224 )
225 .unwrap(),
226 TraversalCompletion::Complete
227 );
228 assert_eq!(listed, [PathBuf::from("/")]);
229 assert!(truncated_events.contains(&(WalkEventKind::Entry, PathBuf::from("/file"))));
230 assert!(truncated_events.contains(&(WalkEventKind::Enter, PathBuf::from("/child"))));
231 }
232
233 #[test]
234 fn stop_error_and_limits_do_not_read_descendants() {
235 assert_eq!(
236 walk_events(
237 entry("/", true),
238 ListDirOptions::default().with_recursive(true),
239 false,
240 |_, _| panic!("stopped"),
241 |_| Ok(WalkControl::Stop)
242 )
243 .unwrap(),
244 TraversalCompletion::Stopped
245 );
246 assert!(
247 walk_events(
248 entry("/", true),
249 ListDirOptions::default().with_recursive(true),
250 false,
251 |_, _| panic!("callback failed"),
252 |_| Err(VfError::client(0, libc::EIO as u32))
253 )
254 .is_err()
255 );
256 for options in [
257 ListDirOptions::new()
258 .with_recursive(true)
259 .with_max_entries(1),
260 ListDirOptions::new()
261 .with_recursive(true)
262 .with_max_path_bytes(1),
263 ListDirOptions::new()
264 .with_recursive(true)
265 .with_max_depth(crate::api::DepthLimit::new(0).expect("finite depth")),
266 ] {
267 assert_eq!(
268 walk_events(
269 entry("/", true),
270 options,
271 false,
272 |path, _| {
273 assert_eq!(path, Path::new("/"));
274 Ok(vec![entry("/a", true)])
275 },
276 |_| Ok(WalkControl::Continue)
277 )
278 .unwrap_err()
279 .raw_code(),
280 libc::EFBIG as u32
281 );
282 }
283 }
284}