1use crate::{crossdev, walk};
2use byte_unit::{Byte, Unit, UnitType};
3use serde::Deserialize;
4use std::collections::BTreeSet;
5use std::path::PathBuf;
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::time::Duration;
9use std::{fmt, path::Path};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
13pub enum ByteFormat {
14 #[serde(rename = "metric")]
16 Metric,
17 #[serde(rename = "binary")]
19 Binary,
20 #[serde(rename = "bytes")]
22 Bytes,
23 #[serde(rename = "gb")]
25 GB,
26 #[serde(rename = "gib")]
28 GiB,
29 #[serde(rename = "mb")]
31 MB,
32 #[serde(rename = "mib")]
34 MiB,
35}
36
37impl ByteFormat {
38 #[must_use]
40 pub fn width(self) -> usize {
41 use ByteFormat::{Binary, Bytes, MB, MiB};
42 match self {
43 Binary => 11,
44 Bytes | MB | MiB => 12,
45 _ => 10,
46 }
47 }
48 #[must_use]
50 pub fn total_width(self) -> usize {
51 use ByteFormat::{Binary, Bytes, GB, GiB, MB, Metric, MiB};
52 const THE_SPACE_BETWEEN_UNIT_AND_NUMBER: usize = 1;
53
54 self.width()
55 + match self {
56 Binary | MiB | GiB => 3,
57 Metric | MB | GB => 2,
58 Bytes => 1,
59 }
60 + THE_SPACE_BETWEEN_UNIT_AND_NUMBER
61 }
62 #[must_use]
64 pub fn display(self, bytes: u128) -> impl fmt::Display {
65 ByteFormatDisplay {
66 format: self,
67 bytes,
68 }
69 }
70}
71
72struct ByteFormatDisplay {
74 format: ByteFormat,
75 bytes: u128,
76}
77
78impl fmt::Display for ByteFormatDisplay {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
80 use ByteFormat::{Binary, Bytes, GB, GiB, MB, Metric, MiB};
81
82 let bytes = Byte::from_u128(self.bytes).expect("supported byte count");
83 let adjusted = match self.format {
84 Bytes => return write!(f, "{} b", self.bytes),
85 Binary => bytes.get_appropriate_unit(UnitType::Binary),
86 Metric => bytes.get_appropriate_unit(UnitType::Decimal),
87 GB => bytes.get_adjusted_unit(Unit::GB),
88 GiB => bytes.get_adjusted_unit(Unit::GiB),
89 MB => bytes.get_adjusted_unit(Unit::MB),
90 MiB => bytes.get_adjusted_unit(Unit::MiB),
91 };
92 let b = format!("{adjusted:.2}");
93 let mut splits = b.split(' ');
94 match (splits.next(), splits.next()) {
95 (Some(bytes), Some(unit)) => write!(
96 f,
97 "{} {:>unit_width$}",
98 bytes,
99 unit,
100 unit_width = match self.format {
101 Binary => 3,
102 _ => 2,
103 }
104 ),
105 _ => f.write_str(&b),
106 }
107 }
108}
109
110#[derive(Debug)]
112pub(crate) struct Throttle {
113 trigger: Arc<AtomicBool>,
114}
115
116impl Throttle {
117 pub(crate) fn new(duration: Duration, initial_sleep: Option<Duration>) -> Self {
121 let instance = Self {
122 trigger: Arc::default(),
123 };
124
125 let trigger = Arc::downgrade(&instance.trigger);
126 std::thread::spawn(move || {
127 if let Some(duration) = initial_sleep {
128 std::thread::sleep(duration);
129 }
130 while let Some(t) = trigger.upgrade() {
131 t.store(true, Ordering::Relaxed);
132 std::thread::sleep(duration);
133 }
134 });
135
136 instance
137 }
138
139 pub(crate) fn throttled<F>(&self, f: F)
141 where
142 F: FnOnce(),
143 {
144 if self.can_update() {
145 f();
146 }
147 }
148
149 pub(crate) fn can_update(&self) -> bool {
151 self.trigger.swap(false, Ordering::Relaxed)
152 }
153}
154
155#[derive(Clone)]
157pub struct WalkOptions {
158 pub threads: usize,
160 pub count_hard_links: bool,
162 pub apparent_size: bool,
164 pub cross_filesystems: bool,
166 pub ignore_dirs: BTreeSet<PathBuf>,
168}
169
170pub(crate) struct WalkRoot {
172 pub index: usize,
174 pub path: PathBuf,
176 pub device_id: u64,
179}
180
181impl WalkOptions {
182 pub(crate) fn iter_from_paths(
183 &self,
184 roots: Vec<WalkRoot>,
185 skip_root: bool,
186 order: walk::Order,
187 ) -> impl Iterator<Item = (usize, walk::RootEvent)> + use<> {
188 let num_roots = roots
189 .iter()
190 .map(|root| root.index)
191 .max()
192 .map_or(0, |idx| idx + 1);
193 let path_count = roots.len();
194 let (device_ids, paths_with_idx) = roots.into_iter().fold(
195 (vec![0; num_roots], Vec::with_capacity(path_count)),
196 |(mut device_ids, mut paths), root| {
197 device_ids[root.index] = root.device_id;
198 paths.push((root.index, root.path));
199 (device_ids, paths)
200 },
201 );
202 let ignore_dirs = self.ignore_dirs.clone();
203 let cwd = std::env::current_dir().unwrap_or_default();
204 let cross_filesystems = self.cross_filesystems;
205 walk::walk_roots(
206 paths_with_idx,
207 self.threads,
208 order,
209 move |root_idx, entry| {
210 (cross_filesystems
211 || entry.metadata.as_ref().map_or(true, |metadata| {
212 crossdev::is_same_device(device_ids[root_idx], metadata)
213 }))
214 && (entry.depth == 0 || !ignore_directory(&entry.path(), &ignore_dirs, &cwd))
215 },
216 )
217 .filter(move |(_, event)| {
218 !skip_root
219 || match event {
220 walk::RootEvent::Entry(entry) => {
221 entry.as_ref().map_or(true, |entry| entry.depth > 0)
222 }
223 walk::RootEvent::Finished => true,
224 }
225 })
226 }
227}
228
229#[derive(Default)]
231pub struct WalkResult {
232 pub num_errors: u64,
234}
235
236impl WalkResult {
237 #[must_use]
241 pub fn to_exit_code(&self) -> i32 {
242 i32::from(self.num_errors > 0)
243 }
244}
245
246pub fn canonicalize_ignore_dirs(ignore_dirs: &[PathBuf]) -> BTreeSet<PathBuf> {
250 let dirs = ignore_dirs
251 .iter()
252 .map(gix::path::realpath)
253 .filter_map(Result::ok)
254 .collect();
255 log::info!("Ignoring canonicalized {dirs:?}");
256 dirs
257}
258
259fn ignore_directory(path: &Path, ignore_dirs: &BTreeSet<PathBuf>, cwd: &Path) -> bool {
260 if ignore_dirs.is_empty() {
261 return false;
262 }
263 let path = gix::path::realpath_opts(path, cwd, 32);
264 path.is_ok_and(|path| {
265 let ignored = ignore_dirs.contains(&path);
266 if ignored {
267 log::debug!("Ignored {}", path.display());
268 }
269 ignored
270 })
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_ignore_directories() {
279 let cwd = std::env::current_dir().unwrap();
280 #[cfg(unix)]
281 let mut parameters = vec![
282 ("/usr", vec!["/usr"], true),
283 ("/usr/local", vec!["/usr"], false),
284 ("/smth", vec!["/usr"], false),
285 ("/usr/local/..", vec!["/usr/local/.."], true),
286 ("/usr", vec!["/usr/local/.."], true),
287 ("/usr/local/share/../..", vec!["/usr"], true),
288 ];
289
290 #[cfg(windows)]
291 let mut parameters = vec![
292 ("C:\\Windows", vec!["C:\\Windows"], true),
293 ("C:\\Windows\\System", vec!["C:\\Windows"], false),
294 ("C:\\Smth", vec!["C:\\Windows"], false),
295 (
296 "C:\\Windows\\System\\..",
297 vec!["C:\\Windows\\System\\.."],
298 true,
299 ),
300 ("C:\\Windows", vec!["C:\\Windows\\System\\.."], true),
301 (
302 "C:\\Windows\\System\\Speech\\..\\..",
303 vec!["C:\\Windows"],
304 true,
305 ),
306 ];
307
308 parameters.extend([
309 ("src", vec!["src"], true),
310 ("src/interactive", vec!["src"], false),
311 ("src/interactive/..", vec!["src"], true),
312 ]);
313
314 for (path, ignore_dirs, expected_result) in parameters {
315 let ignore_dirs = canonicalize_ignore_dirs(
316 &ignore_dirs.into_iter().map(Into::into).collect::<Vec<_>>(),
317 );
318 assert_eq!(
319 ignore_directory(path.as_ref(), &ignore_dirs, &cwd),
320 expected_result,
321 "result='{expected_result}' for path='{path}' and ignore_dir='{ignore_dirs:?}' "
322 );
323 }
324 }
325
326 #[test]
327 fn explicitly_selected_ignored_root_is_traversed() {
328 let root = tempfile::tempdir().unwrap();
329 let child = root.path().join("child");
330 std::fs::create_dir(&child).unwrap();
331 let options = WalkOptions {
332 threads: 2,
333 count_hard_links: false,
334 apparent_size: false,
335 cross_filesystems: true,
336 ignore_dirs: canonicalize_ignore_dirs(&[root.path().to_owned()]),
337 };
338
339 let paths = options
340 .iter_from_paths(
341 vec![WalkRoot {
342 index: 0,
343 path: root.path().to_owned(),
344 device_id: crossdev::init(root.path()).unwrap(),
345 }],
346 false,
347 walk::Order::Completion,
348 )
349 .filter_map(|(_, event)| match event {
350 walk::RootEvent::Entry(entry) => Some(entry.unwrap().path()),
351 walk::RootEvent::Finished => None,
352 })
353 .collect::<Vec<_>>();
354
355 assert!(paths.contains(&child));
356 }
357}