1use crate::aggregate::TraversalProgress;
2use crate::snapshot::Replay;
3use crate::traverse::{
4 BackgroundTraversal, Traversal, TraversalEntry, TraversalEvent, Tree, TreeIndex,
5};
6use crate::tree::metadata_io_error_count;
7use crate::{WalkOptions, WalkResult};
8use anyhow::{Context, Result};
9use bstr::ByteSlice;
10use std::ffi::OsStr;
11use std::io;
12use std::path::PathBuf;
13
14pub fn stacks(
28 mut out: impl io::Write,
29 err: Option<impl io::Write>,
30 walk_options: WalkOptions,
31 paths: Vec<PathBuf>,
32 max_depth: Option<usize>,
33) -> Result<WalkResult> {
34 let mut traversal = Traversal::new();
35 let stream = max_depth.is_none();
36 let pattern_roots = walk_options.ignore_patterns.as_ref().map(|_| paths.clone());
38 let mut background = BackgroundTraversal::start(
39 traversal.root_index,
40 &walk_options,
41 paths,
42 pattern_roots.as_deref(),
43 false,
44 true,
45 )?
46 .retain_depth(max_depth.or(Some(0)));
47 let mut progress = TraversalProgress::new(err);
48
49 while let Ok(event) = background.event_rx.recv() {
50 let stack = stream.then(|| stack_path(&event)).flatten();
51 let size_before = traversal
52 .tree
53 .data(traversal.root_index)
54 .context("traversal root is missing")?
55 .size;
56 let finished = background.integrate_traversal_event(&mut traversal, event) == Some(true);
57 let own_size = traversal
58 .tree
59 .data(traversal.root_index)
60 .context("traversal root is missing")?
61 .size
62 .checked_sub(size_before)
63 .context("traversal size decreased")?;
64 if let Some(stack) = stack
65 && own_size > 0
66 {
67 progress.clear();
68 writeln!(out, "{stack} {own_size}")?;
69 }
70 progress.update(background.stats.entries_traversed);
71 if finished {
72 break;
73 }
74 }
75 progress.clear();
76
77 if !stream {
78 let roots = background
79 .root_nodes()
80 .context("traversal did not produce a node for every root")?;
81 write_stacks(&mut out, &traversal.tree, &roots, max_depth)?;
82 }
83
84 Ok(WalkResult {
85 num_errors: background.stats.io_errors,
86 })
87}
88
89pub fn stacks_from_traversal(
95 mut out: impl io::Write,
96 traversal: &Traversal,
97 roots: &[TreeIndex],
98 max_depth: Option<usize>,
99) -> Result<WalkResult> {
100 write_stacks(&mut out, &traversal.tree, roots, max_depth)?;
101 Ok(WalkResult {
102 num_errors: metadata_io_error_count(&traversal.tree, roots),
103 })
104}
105
106struct ReplayStackEntry {
107 size: u128,
108 children_size: u128,
109 prefix_len: usize,
110}
111
112pub fn stacks_from_replay<R: io::Read + io::Seek>(
114 mut out: impl io::Write,
115 replay: &mut Replay<R>,
116 max_depth: Option<usize>,
117) -> Result<WalkResult> {
118 let mut num_errors = 0u64;
119 let mut open = Vec::new();
120 let mut prefix = String::new();
121 replay.for_each_entry(|entry| {
122 num_errors = num_errors.saturating_add(u64::from(entry.data.metadata_io_error));
123 while open.len() > entry.depth {
124 write_replay_stack(&mut out, &mut open, &mut prefix)?;
125 }
126 if max_depth.is_some_and(|max_depth| entry.depth > max_depth) {
127 return Ok(());
128 }
129
130 if let Some(parent) = open.last_mut() {
131 parent.children_size = parent
132 .children_size
133 .checked_add(entry.data.size)
134 .context("stack child sizes overflowed")?;
135 }
136 if !open.is_empty() {
137 prefix.push(';');
138 }
139 push_frame(&mut prefix, entry.name().as_os_str());
140 open.push(ReplayStackEntry {
141 size: entry.data.size,
142 children_size: 0,
143 prefix_len: prefix.len(),
144 });
145 Ok(())
146 })?;
147 while !open.is_empty() {
148 write_replay_stack(&mut out, &mut open, &mut prefix)?;
149 }
150 Ok(WalkResult { num_errors })
151}
152
153fn write_replay_stack(
154 out: &mut impl io::Write,
155 open: &mut Vec<ReplayStackEntry>,
156 prefix: &mut String,
157) -> Result<()> {
158 let entry = open.pop().expect("called with an open stack entry");
159 let own_size = entry
160 .size
161 .checked_sub(entry.children_size)
162 .context("stack children exceed their parent's size")?;
163 debug_assert_eq!(prefix.len(), entry.prefix_len);
164 if own_size > 0 {
165 writeln!(out, "{prefix} {own_size}")?;
166 }
167 prefix.truncate(open.last().map_or(0, |entry| entry.prefix_len));
168 Ok(())
169}
170
171fn stack_path(event: &TraversalEvent) -> Option<String> {
172 let TraversalEvent::Entry(Ok(TraversalEntry(entry)), root, _, _) = event else {
173 return None;
174 };
175 let mut stack = frame_name(root.as_os_str());
176 if entry.depth > 0 {
177 for component in entry
178 .path()
179 .strip_prefix(root.as_path())
180 .expect("walk entries remain below their root")
181 .components()
182 {
183 stack.push(';');
184 stack.push_str(&frame_name(component.as_os_str()));
185 }
186 }
187 Some(stack)
188}
189
190fn write_stacks(
192 mut out: impl io::Write,
193 tree: &Tree,
194 roots: &[TreeIndex],
195 max_depth: Option<usize>,
196) -> Result<()> {
197 let mut stack: Vec<(TreeIndex, usize, String)> = roots
199 .iter()
200 .rev()
201 .map(|&root| (root, 0, frame(tree, root)))
202 .collect();
203
204 while let Some((index, depth, prefix)) = stack.pop() {
205 let mut children_size = 0u128;
206 if max_depth.is_none_or(|max_depth| depth < max_depth) {
207 for child in tree.children(index) {
208 children_size = children_size
209 .checked_add(tree.data(child).expect("tree child exists").size)
210 .context("stack child sizes overflowed")?;
211 stack.push((child, depth + 1, format!("{prefix};{}", frame(tree, child))));
212 }
213 }
214 let own_size = tree
218 .data(index)
219 .expect("tree entry exists")
220 .size
221 .checked_sub(children_size)
222 .context("stack children exceed their parent's size")?;
223 if own_size > 0 {
224 writeln!(out, "{prefix} {own_size}")?;
225 }
226 }
227 Ok(())
228}
229
230fn frame(tree: &Tree, index: TreeIndex) -> String {
233 frame_name(tree.name(index).expect("tree entry exists").as_os_str())
234}
235
236fn frame_name(name: &OsStr) -> String {
237 let mut encoded = String::new();
238 push_frame(&mut encoded, name);
239 encoded
240}
241
242fn push_frame(encoded: &mut String, name: &OsStr) {
243 for chunk in name.as_encoded_bytes().utf8_chunks() {
244 for character in chunk.valid().chars() {
245 match character {
246 '\\' => encoded.push_str(r"\\"),
247 ';' => encoded.push_str(r"\x3b"),
248 character if character.is_control() => encoded.extend(character.escape_default()),
249 character => encoded.push(character),
250 }
251 }
252 encoded.extend(chunk.invalid().escape_bytes());
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::{Replay, frame_name, stacks, stacks_from_replay, stacks_from_traversal};
259 use crate::traverse::{EntryData, Traversal};
260 use crate::{TraversalOptions, WalkOptions};
261 use bstr::ByteSlice;
262 use std::{collections::BTreeMap, ffi::OsStr};
263
264 fn walk_options() -> WalkOptions {
265 WalkOptions {
266 threads: 1,
267 count_hard_links: true,
268 apparent_size: true,
269 cross_filesystems: true,
270 ignore_dirs: std::collections::BTreeSet::default(),
271 ignore_patterns: None,
272 metadata_options: TraversalOptions::default(),
273 }
274 }
275
276 fn folded(out: &[u8]) -> BTreeMap<String, u128> {
279 std::str::from_utf8(out)
280 .unwrap()
281 .lines()
282 .map(|line| {
283 let (stack, size) = line.rsplit_once(' ').expect("a size follows each stack");
284 (stack.to_owned(), size.parse().expect("a numeric size"))
285 })
286 .collect()
287 }
288
289 fn folded_frame(path: impl Into<std::path::PathBuf>) -> String {
290 frame_name(path.into().as_os_str())
291 }
292
293 #[test]
294 fn every_file_appears_with_its_size_below_its_directories() {
295 let dir = tempfile::tempdir().unwrap();
296 std::fs::create_dir(dir.path().join("nested")).unwrap();
297 std::fs::write(dir.path().join("nested/file"), b"content").unwrap();
298 std::fs::write(dir.path().join("top"), b"hi").unwrap();
299
300 let root = dir.path().to_owned();
301 let mut out = Vec::new();
302 let result = stacks(
303 &mut out,
304 None::<Vec<u8>>,
305 walk_options(),
306 vec![root.clone()],
307 None,
308 )
309 .unwrap();
310 assert_eq!(result.num_errors, 0);
311
312 let folded = folded(&out);
313 let base = folded_frame(root);
314 assert_eq!(
315 folded.get(&format!("{base};nested;file")),
316 Some(&7),
317 "the nested file is folded under its two directories with its own size"
318 );
319 assert_eq!(
320 folded.get(&format!("{base};top")),
321 Some(&2),
322 "the top-level file appears directly under the root"
323 );
324 }
325
326 #[test]
327 fn folded_sizes_sum_to_the_reported_total() {
328 use crate::traverse::{BackgroundTraversal, Traversal};
329
330 let dir = tempfile::tempdir().unwrap();
331 std::fs::create_dir(dir.path().join("a")).unwrap();
332 std::fs::write(dir.path().join("a/one"), b"12345").unwrap();
333 std::fs::write(dir.path().join("two"), b"678").unwrap();
334
335 let mut out = Vec::new();
336 stacks(
337 &mut out,
338 None::<Vec<u8>>,
339 walk_options(),
340 vec![dir.path().to_owned()],
341 None,
342 )
343 .unwrap();
344 let folded_total: u128 = folded(&out).values().sum();
345
346 let mut traversal = Traversal::new();
348 let mut background = BackgroundTraversal::start(
349 traversal.root_index,
350 &walk_options(),
351 vec![dir.path().to_owned()],
352 None,
353 false,
354 true,
355 )
356 .unwrap();
357 while background
358 .integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
359 != Some(true)
360 {}
361
362 assert_eq!(
363 folded_total,
364 traversal
365 .tree
366 .data(traversal.root_index)
367 .expect("traversal root exists")
368 .size,
369 "the folded lines account for every byte the traversal totals up"
370 );
371 }
372
373 #[test]
374 fn a_single_file_input_is_folded_as_one_line() {
375 let dir = tempfile::tempdir().unwrap();
376 let file = dir.path().join("solo");
377 std::fs::write(&file, b"solo!").unwrap();
378
379 let mut out = Vec::new();
380 stacks(
381 &mut out,
382 None::<Vec<u8>>,
383 walk_options(),
384 vec![file.clone()],
385 None,
386 )
387 .unwrap();
388
389 let folded = folded(&out);
390 assert_eq!(folded.len(), 1);
391 assert_eq!(folded.get(&folded_frame(file)), Some(&5));
392 }
393
394 #[cfg(unix)]
395 #[test]
396 fn output_is_written_while_the_walk_is_still_running() {
397 struct CreateFileOnFirstWrite {
398 out: Vec<u8>,
399 path: std::path::PathBuf,
400 }
401
402 impl std::io::Write for CreateFileOnFirstWrite {
403 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
404 if self.out.is_empty() {
405 std::fs::write(&self.path, b"x")?;
406 }
407 self.out.extend_from_slice(buf);
408 Ok(buf.len())
409 }
410
411 fn flush(&mut self) -> std::io::Result<()> {
412 Ok(())
413 }
414 }
415
416 let dir = tempfile::tempdir().unwrap();
417 let mut deepest = dir.path().to_owned();
418 for _ in 0..200 {
419 deepest.push("d");
420 }
421 std::fs::create_dir_all(&deepest).unwrap();
422 std::fs::write(dir.path().join("trigger"), b"x").unwrap();
423 let mut out = CreateFileOnFirstWrite {
424 out: Vec::new(),
425 path: deepest.join("late"),
426 };
427
428 stacks(
429 &mut out,
430 None::<Vec<u8>>,
431 walk_options(),
432 vec![dir.path().to_owned()],
433 None,
434 )
435 .unwrap();
436
437 let text = String::from_utf8(out.out).unwrap();
438 assert!(
439 text.lines().any(|line| line.ends_with(";late 1")),
440 "the first stack line should be written before the deepest directory is read: {text:?}"
441 );
442 }
443
444 #[test]
445 fn depth_rolls_hidden_descendants_into_the_last_frame() {
446 let dir = tempfile::tempdir().unwrap();
447 std::fs::create_dir(dir.path().join("nested")).unwrap();
448 std::fs::write(dir.path().join("nested/file"), b"content").unwrap();
449
450 let mut out = Vec::new();
451 stacks(
452 &mut out,
453 None::<Vec<u8>>,
454 walk_options(),
455 vec![dir.path().to_owned()],
456 Some(1),
457 )
458 .unwrap();
459
460 let folded = folded(&out);
461 let nested = format!("{};nested", folded_frame(dir.path()));
462 assert!(folded.contains_key(&nested));
463 assert!(!folded.keys().any(|stack| stack.ends_with(";file")));
464 }
465
466 #[test]
467 fn completed_traversal_rolls_hidden_entries_into_the_cutoff() {
468 let mut traversal = Traversal::new();
469 let root = traversal.tree.add_child(
470 traversal.root_index,
471 "first",
472 EntryData {
473 size: 9,
474 is_dir: true,
475 ..EntryData::default()
476 },
477 );
478 let cutoff = traversal.tree.add_child(
479 root,
480 "cutoff",
481 EntryData {
482 size: 9,
483 is_dir: true,
484 ..EntryData::default()
485 },
486 );
487 traversal.tree.add_child(
488 cutoff,
489 "hidden",
490 EntryData {
491 size: 7,
492 metadata_io_error: true,
493 ..EntryData::default()
494 },
495 );
496 let second = traversal.tree.add_child(
497 traversal.root_index,
498 "second",
499 EntryData {
500 size: 2,
501 ..EntryData::default()
502 },
503 );
504
505 let mut out = Vec::new();
506 let result = stacks_from_traversal(&mut out, &traversal, &[root, second], Some(1)).unwrap();
507 let mut snapshot = Vec::new();
508 crate::snapshot::write(&mut snapshot, &traversal, &[root, second], None).unwrap();
509 let mut replay = Replay::new(std::io::Cursor::new(snapshot)).unwrap();
510 let mut replayed = Vec::new();
511 let replayed_result = stacks_from_replay(&mut replayed, &mut replay, Some(1)).unwrap();
512 assert_eq!(folded(&replayed), folded(&out));
513 assert_eq!(replayed_result.num_errors, result.num_errors);
514 insta::assert_snapshot!(out.as_bstr(), "depth cutoff rolls hidden descendants into parent", @r"
515 first;cutoff 9
516 second 2
517 ");
518 assert_eq!(result.num_errors, 1);
519 }
520
521 #[test]
522 fn completed_traversal_rejects_invalid_aggregate_sizes() {
523 for (parent_size, child_sizes, expected) in [
524 (1, vec![2], "stack children exceed their parent's size"),
525 (
526 u128::MAX,
527 vec![u128::MAX, 1],
528 "stack child sizes overflowed",
529 ),
530 ] {
531 let mut traversal = Traversal::new();
532 let root = traversal.tree.add_child(
533 traversal.root_index,
534 "root",
535 EntryData {
536 size: parent_size,
537 is_dir: true,
538 ..EntryData::default()
539 },
540 );
541 for size in child_sizes {
542 traversal.tree.add_child(
543 root,
544 "child",
545 EntryData {
546 size,
547 ..EntryData::default()
548 },
549 );
550 }
551
552 let Err(error) = stacks_from_traversal(Vec::new(), &traversal, &[root], None) else {
553 panic!("invalid aggregate sizes must fail");
554 };
555 assert_eq!(error.to_string(), expected);
556 }
557 }
558
559 #[test]
560 fn frame_names_are_encoded_without_collisions() {
561 let encode = |name: &str| frame_name(OsStr::new(name));
562
563 assert_eq!(encode("a;b"), r"a\x3bb");
564 assert_eq!(encode("a_b"), "a_b");
565 assert_eq!(encode(r"a\x3bb"), r"a\\x3bb");
566 assert_eq!(encode("a\nb"), r"a\nb");
567 }
568}