1use crate::aggregate::TraversalProgress;
2use crate::traverse::{BackgroundTraversal, EntryData, Traversal, Tree, TreeIndex};
3use crate::{WalkOptions, WalkResult};
4use anyhow::Result;
5use bstr::ByteSlice;
6use petgraph::Direction;
7use std::io;
8use std::path::PathBuf;
9
10pub fn stacks(
18 mut out: impl io::Write,
19 err: Option<impl io::Write>,
20 walk_options: WalkOptions,
21 paths: Vec<PathBuf>,
22 max_depth: Option<usize>,
23) -> Result<WalkResult> {
24 let mut traversal = Traversal::new();
25 let pattern_roots = walk_options.ignore_patterns.as_ref().map(|_| paths.clone());
27 let mut background = BackgroundTraversal::start(
28 traversal.root_index,
29 &walk_options,
30 paths,
31 pattern_roots.as_deref(),
32 false,
33 true,
34 )?
35 .retain_depth(max_depth);
36 let mut progress = TraversalProgress::new(err);
37
38 while let Ok(event) = background.event_rx.recv() {
39 let finished = background.integrate_traversal_event(&mut traversal, event) == Some(true);
40 progress.update(background.stats.entries_traversed);
41 if finished {
42 break;
43 }
44 }
45 progress.clear();
46
47 write_stacks(&mut out, &traversal.tree, traversal.root_index)?;
48
49 Ok(WalkResult {
50 num_errors: background.stats.io_errors,
51 })
52}
53
54fn write_stacks(mut out: impl io::Write, tree: &Tree, root: TreeIndex) -> io::Result<()> {
56 let mut stack: Vec<(TreeIndex, String)> = tree
58 .neighbors_directed(root, Direction::Outgoing)
59 .map(|child| (child, frame(&tree[child])))
60 .collect();
61
62 while let Some((index, prefix)) = stack.pop() {
63 let mut children_size = 0u128;
64 for child in tree.neighbors_directed(index, Direction::Outgoing) {
65 children_size += tree[child].size;
66 stack.push((child, format!("{prefix};{}", frame(&tree[child]))));
67 }
68 let own_size = tree[index].size.saturating_sub(children_size);
72 if own_size > 0 {
73 writeln!(out, "{prefix} {own_size}")?;
74 }
75 }
76 Ok(())
77}
78
79fn frame(entry: &EntryData) -> String {
82 let mut encoded = String::new();
83 for chunk in entry.name.as_os_str().as_encoded_bytes().utf8_chunks() {
84 for character in chunk.valid().chars() {
85 match character {
86 '\\' => encoded.push_str(r"\\"),
87 ';' => encoded.push_str(r"\x3b"),
88 character if character.is_control() => encoded.extend(character.escape_default()),
89 character => encoded.push(character),
90 }
91 }
92 encoded.extend(chunk.invalid().escape_bytes());
93 }
94 encoded
95}
96
97#[cfg(test)]
98mod tests {
99 use super::{frame, stacks};
100 use crate::traverse::EntryData;
101 use crate::{TraversalOptions, WalkOptions};
102 use std::collections::BTreeMap;
103
104 fn walk_options() -> WalkOptions {
105 WalkOptions {
106 threads: 1,
107 count_hard_links: true,
108 apparent_size: true,
109 cross_filesystems: true,
110 ignore_dirs: std::collections::BTreeSet::default(),
111 ignore_patterns: None,
112 metadata_options: TraversalOptions::default(),
113 }
114 }
115
116 fn folded(out: &[u8]) -> BTreeMap<String, u128> {
119 std::str::from_utf8(out)
120 .unwrap()
121 .lines()
122 .map(|line| {
123 let (stack, size) = line.rsplit_once(' ').expect("a size follows each stack");
124 (stack.to_owned(), size.parse().expect("a numeric size"))
125 })
126 .collect()
127 }
128
129 fn folded_frame(path: impl Into<std::path::PathBuf>) -> String {
130 frame(&EntryData {
131 name: path.into(),
132 ..EntryData::default()
133 })
134 }
135
136 #[test]
137 fn every_file_appears_with_its_size_below_its_directories() {
138 let dir = tempfile::tempdir().unwrap();
139 std::fs::create_dir(dir.path().join("nested")).unwrap();
140 std::fs::write(dir.path().join("nested/file"), b"content").unwrap();
141 std::fs::write(dir.path().join("top"), b"hi").unwrap();
142
143 let root = dir.path().to_owned();
144 let mut out = Vec::new();
145 let result = stacks(
146 &mut out,
147 None::<Vec<u8>>,
148 walk_options(),
149 vec![root.clone()],
150 None,
151 )
152 .unwrap();
153 assert_eq!(result.num_errors, 0);
154
155 let folded = folded(&out);
156 let base = folded_frame(root);
157 assert_eq!(
158 folded.get(&format!("{base};nested;file")),
159 Some(&7),
160 "the nested file is folded under its two directories with its own size"
161 );
162 assert_eq!(
163 folded.get(&format!("{base};top")),
164 Some(&2),
165 "the top-level file appears directly under the root"
166 );
167 }
168
169 #[test]
170 fn folded_sizes_sum_to_the_reported_total() {
171 use crate::traverse::{BackgroundTraversal, Traversal};
172
173 let dir = tempfile::tempdir().unwrap();
174 std::fs::create_dir(dir.path().join("a")).unwrap();
175 std::fs::write(dir.path().join("a/one"), b"12345").unwrap();
176 std::fs::write(dir.path().join("two"), b"678").unwrap();
177
178 let mut out = Vec::new();
179 stacks(
180 &mut out,
181 None::<Vec<u8>>,
182 walk_options(),
183 vec![dir.path().to_owned()],
184 None,
185 )
186 .unwrap();
187 let folded_total: u128 = folded(&out).values().sum();
188
189 let mut traversal = Traversal::new();
191 let mut background = BackgroundTraversal::start(
192 traversal.root_index,
193 &walk_options(),
194 vec![dir.path().to_owned()],
195 None,
196 false,
197 true,
198 )
199 .unwrap();
200 while background
201 .integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
202 != Some(true)
203 {}
204
205 assert_eq!(
206 folded_total, traversal.tree[traversal.root_index].size,
207 "the folded lines account for every byte the traversal totals up"
208 );
209 }
210
211 #[test]
212 fn a_single_file_input_is_folded_as_one_line() {
213 let dir = tempfile::tempdir().unwrap();
214 let file = dir.path().join("solo");
215 std::fs::write(&file, b"solo!").unwrap();
216
217 let mut out = Vec::new();
218 stacks(
219 &mut out,
220 None::<Vec<u8>>,
221 walk_options(),
222 vec![file.clone()],
223 None,
224 )
225 .unwrap();
226
227 let folded = folded(&out);
228 assert_eq!(folded.len(), 1);
229 assert_eq!(folded.get(&folded_frame(file)), Some(&5));
230 }
231
232 #[test]
233 fn depth_rolls_hidden_descendants_into_the_last_frame() {
234 let dir = tempfile::tempdir().unwrap();
235 std::fs::create_dir(dir.path().join("nested")).unwrap();
236 std::fs::write(dir.path().join("nested/file"), b"content").unwrap();
237
238 let mut out = Vec::new();
239 stacks(
240 &mut out,
241 None::<Vec<u8>>,
242 walk_options(),
243 vec![dir.path().to_owned()],
244 Some(1),
245 )
246 .unwrap();
247
248 let folded = folded(&out);
249 let nested = format!("{};nested", folded_frame(dir.path()));
250 assert!(folded.contains_key(&nested));
251 assert!(!folded.keys().any(|stack| stack.ends_with(";file")));
252 }
253
254 #[test]
255 fn frame_names_are_encoded_without_collisions() {
256 let encode = |name: &str| {
257 frame(&EntryData {
258 name: name.into(),
259 ..EntryData::default()
260 })
261 };
262
263 assert_eq!(encode("a;b"), r"a\x3bb");
264 assert_eq!(encode("a_b"), "a_b");
265 assert_eq!(encode(r"a\x3bb"), r"a\\x3bb");
266 assert_eq!(encode("a\nb"), r"a\nb");
267 }
268}