1use crate::aggregate::TraversalProgress;
2use crate::traverse::{
3 BackgroundTraversal, EntryData, Traversal, TraversalEntry, TraversalEvent, Tree, TreeIndex,
4};
5use crate::{WalkOptions, WalkResult};
6use anyhow::Result;
7use bstr::ByteSlice;
8use petgraph::Direction;
9use std::ffi::OsStr;
10use std::io;
11use std::path::PathBuf;
12
13pub fn stacks(
27 mut out: impl io::Write,
28 err: Option<impl io::Write>,
29 walk_options: WalkOptions,
30 paths: Vec<PathBuf>,
31 max_depth: Option<usize>,
32) -> Result<WalkResult> {
33 let mut traversal = Traversal::new();
34 let stream = max_depth.is_none();
35 let pattern_roots = walk_options.ignore_patterns.as_ref().map(|_| paths.clone());
37 let mut background = BackgroundTraversal::start(
38 traversal.root_index,
39 &walk_options,
40 paths,
41 pattern_roots.as_deref(),
42 false,
43 true,
44 )?
45 .retain_depth(max_depth.or(Some(0)));
46 let mut progress = TraversalProgress::new(err);
47
48 while let Ok(event) = background.event_rx.recv() {
49 let stack = stream.then(|| stack_path(&event)).flatten();
50 let size_before = traversal.tree[traversal.root_index].size;
51 let finished = background.integrate_traversal_event(&mut traversal, event) == Some(true);
52 let own_size = traversal.tree[traversal.root_index].size - size_before;
53 if let Some(stack) = stack
54 && own_size > 0
55 {
56 progress.clear();
57 writeln!(out, "{stack} {own_size}")?;
58 }
59 progress.update(background.stats.entries_traversed);
60 if finished {
61 break;
62 }
63 }
64 progress.clear();
65
66 if !stream {
67 write_stacks(&mut out, &traversal.tree, traversal.root_index)?;
68 }
69
70 Ok(WalkResult {
71 num_errors: background.stats.io_errors,
72 })
73}
74
75fn stack_path(event: &TraversalEvent) -> Option<String> {
76 let TraversalEvent::Entry(Ok(TraversalEntry(entry)), root, _, _) = event else {
77 return None;
78 };
79 let mut stack = frame_name(root.as_os_str());
80 if entry.depth > 0 {
81 for component in entry
82 .path()
83 .strip_prefix(root.as_path())
84 .expect("walk entries remain below their root")
85 .components()
86 {
87 stack.push(';');
88 stack.push_str(&frame_name(component.as_os_str()));
89 }
90 }
91 Some(stack)
92}
93
94fn write_stacks(mut out: impl io::Write, tree: &Tree, root: TreeIndex) -> io::Result<()> {
96 let mut stack: Vec<(TreeIndex, String)> = tree
98 .neighbors_directed(root, Direction::Outgoing)
99 .map(|child| (child, frame(&tree[child])))
100 .collect();
101
102 while let Some((index, prefix)) = stack.pop() {
103 let mut children_size = 0u128;
104 for child in tree.neighbors_directed(index, Direction::Outgoing) {
105 children_size += tree[child].size;
106 stack.push((child, format!("{prefix};{}", frame(&tree[child]))));
107 }
108 let own_size = tree[index].size.saturating_sub(children_size);
112 if own_size > 0 {
113 writeln!(out, "{prefix} {own_size}")?;
114 }
115 }
116 Ok(())
117}
118
119fn frame(entry: &EntryData) -> String {
122 frame_name(entry.name.as_os_str())
123}
124
125fn frame_name(name: &OsStr) -> String {
126 let mut encoded = String::new();
127 for chunk in name.as_encoded_bytes().utf8_chunks() {
128 for character in chunk.valid().chars() {
129 match character {
130 '\\' => encoded.push_str(r"\\"),
131 ';' => encoded.push_str(r"\x3b"),
132 character if character.is_control() => encoded.extend(character.escape_default()),
133 character => encoded.push(character),
134 }
135 }
136 encoded.extend(chunk.invalid().escape_bytes());
137 }
138 encoded
139}
140
141#[cfg(test)]
142mod tests {
143 use super::{frame, stacks};
144 use crate::traverse::EntryData;
145 use crate::{TraversalOptions, WalkOptions};
146 use std::collections::BTreeMap;
147
148 fn walk_options() -> WalkOptions {
149 WalkOptions {
150 threads: 1,
151 count_hard_links: true,
152 apparent_size: true,
153 cross_filesystems: true,
154 ignore_dirs: std::collections::BTreeSet::default(),
155 ignore_patterns: None,
156 metadata_options: TraversalOptions::default(),
157 }
158 }
159
160 fn folded(out: &[u8]) -> BTreeMap<String, u128> {
163 std::str::from_utf8(out)
164 .unwrap()
165 .lines()
166 .map(|line| {
167 let (stack, size) = line.rsplit_once(' ').expect("a size follows each stack");
168 (stack.to_owned(), size.parse().expect("a numeric size"))
169 })
170 .collect()
171 }
172
173 fn folded_frame(path: impl Into<std::path::PathBuf>) -> String {
174 frame(&EntryData {
175 name: path.into(),
176 ..EntryData::default()
177 })
178 }
179
180 #[test]
181 fn every_file_appears_with_its_size_below_its_directories() {
182 let dir = tempfile::tempdir().unwrap();
183 std::fs::create_dir(dir.path().join("nested")).unwrap();
184 std::fs::write(dir.path().join("nested/file"), b"content").unwrap();
185 std::fs::write(dir.path().join("top"), b"hi").unwrap();
186
187 let root = dir.path().to_owned();
188 let mut out = Vec::new();
189 let result = stacks(
190 &mut out,
191 None::<Vec<u8>>,
192 walk_options(),
193 vec![root.clone()],
194 None,
195 )
196 .unwrap();
197 assert_eq!(result.num_errors, 0);
198
199 let folded = folded(&out);
200 let base = folded_frame(root);
201 assert_eq!(
202 folded.get(&format!("{base};nested;file")),
203 Some(&7),
204 "the nested file is folded under its two directories with its own size"
205 );
206 assert_eq!(
207 folded.get(&format!("{base};top")),
208 Some(&2),
209 "the top-level file appears directly under the root"
210 );
211 }
212
213 #[test]
214 fn folded_sizes_sum_to_the_reported_total() {
215 use crate::traverse::{BackgroundTraversal, Traversal};
216
217 let dir = tempfile::tempdir().unwrap();
218 std::fs::create_dir(dir.path().join("a")).unwrap();
219 std::fs::write(dir.path().join("a/one"), b"12345").unwrap();
220 std::fs::write(dir.path().join("two"), b"678").unwrap();
221
222 let mut out = Vec::new();
223 stacks(
224 &mut out,
225 None::<Vec<u8>>,
226 walk_options(),
227 vec![dir.path().to_owned()],
228 None,
229 )
230 .unwrap();
231 let folded_total: u128 = folded(&out).values().sum();
232
233 let mut traversal = Traversal::new();
235 let mut background = BackgroundTraversal::start(
236 traversal.root_index,
237 &walk_options(),
238 vec![dir.path().to_owned()],
239 None,
240 false,
241 true,
242 )
243 .unwrap();
244 while background
245 .integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
246 != Some(true)
247 {}
248
249 assert_eq!(
250 folded_total, traversal.tree[traversal.root_index].size,
251 "the folded lines account for every byte the traversal totals up"
252 );
253 }
254
255 #[test]
256 fn a_single_file_input_is_folded_as_one_line() {
257 let dir = tempfile::tempdir().unwrap();
258 let file = dir.path().join("solo");
259 std::fs::write(&file, b"solo!").unwrap();
260
261 let mut out = Vec::new();
262 stacks(
263 &mut out,
264 None::<Vec<u8>>,
265 walk_options(),
266 vec![file.clone()],
267 None,
268 )
269 .unwrap();
270
271 let folded = folded(&out);
272 assert_eq!(folded.len(), 1);
273 assert_eq!(folded.get(&folded_frame(file)), Some(&5));
274 }
275
276 #[cfg(unix)]
277 #[test]
278 fn output_is_written_while_the_walk_is_still_running() {
279 struct CreateFileOnFirstWrite {
280 out: Vec<u8>,
281 path: std::path::PathBuf,
282 }
283
284 impl std::io::Write for CreateFileOnFirstWrite {
285 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
286 if self.out.is_empty() {
287 std::fs::write(&self.path, b"x")?;
288 }
289 self.out.extend_from_slice(buf);
290 Ok(buf.len())
291 }
292
293 fn flush(&mut self) -> std::io::Result<()> {
294 Ok(())
295 }
296 }
297
298 let dir = tempfile::tempdir().unwrap();
299 let mut deepest = dir.path().to_owned();
300 for _ in 0..200 {
301 deepest.push("d");
302 }
303 std::fs::create_dir_all(&deepest).unwrap();
304 std::fs::write(dir.path().join("trigger"), b"x").unwrap();
305 let mut out = CreateFileOnFirstWrite {
306 out: Vec::new(),
307 path: deepest.join("late"),
308 };
309
310 stacks(
311 &mut out,
312 None::<Vec<u8>>,
313 walk_options(),
314 vec![dir.path().to_owned()],
315 None,
316 )
317 .unwrap();
318
319 let text = String::from_utf8(out.out).unwrap();
320 assert!(
321 text.lines().any(|line| line.ends_with(";late 1")),
322 "the first stack line should be written before the deepest directory is read: {text:?}"
323 );
324 }
325
326 #[test]
327 fn depth_rolls_hidden_descendants_into_the_last_frame() {
328 let dir = tempfile::tempdir().unwrap();
329 std::fs::create_dir(dir.path().join("nested")).unwrap();
330 std::fs::write(dir.path().join("nested/file"), b"content").unwrap();
331
332 let mut out = Vec::new();
333 stacks(
334 &mut out,
335 None::<Vec<u8>>,
336 walk_options(),
337 vec![dir.path().to_owned()],
338 Some(1),
339 )
340 .unwrap();
341
342 let folded = folded(&out);
343 let nested = format!("{};nested", folded_frame(dir.path()));
344 assert!(folded.contains_key(&nested));
345 assert!(!folded.keys().any(|stack| stack.ends_with(";file")));
346 }
347
348 #[test]
349 fn frame_names_are_encoded_without_collisions() {
350 let encode = |name: &str| {
351 frame(&EntryData {
352 name: name.into(),
353 ..EntryData::default()
354 })
355 };
356
357 assert_eq!(encode("a;b"), r"a\x3bb");
358 assert_eq!(encode("a_b"), "a_b");
359 assert_eq!(encode(r"a\x3bb"), r"a\\x3bb");
360 assert_eq!(encode("a\nb"), r"a\nb");
361 }
362}