1use crate::{Throttle, WalkOptions, WalkRoot, crossdev, inodefilter::InodeFilter};
2
3use crossbeam::channel::Receiver;
4#[cfg(not(any(windows, target_os = "macos")))]
5use filesize::PathExt;
6use std::{
7 borrow::Cow,
8 collections::HashMap,
9 fmt, io,
10 num::NonZeroU32,
11 path::{Path, PathBuf},
12 sync::Arc,
13 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
14};
15
16const NONE: u32 = u32::MAX;
17const FLAG_OCCUPIED: u32 = 1 << 0;
18const FLAG_DIRECTORY: u32 = 1 << 1;
19const FLAG_METADATA_IO_ERROR: u32 = 1 << 2;
20const FLAG_ENTRY_COUNT: u32 = 1 << 3;
21
22#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
24pub struct TreeIndex(NonZeroU32);
25
26impl TreeIndex {
27 #[must_use]
29 pub fn new(index: usize) -> Self {
30 Self::from(index)
31 }
32
33 #[must_use]
35 pub fn index(self) -> usize {
36 (self.0.get() - 1) as usize
37 }
38
39 fn from_raw(index: u32) -> Self {
40 debug_assert_ne!(index, NONE);
41 Self(NonZeroU32::new(index + 1).expect("tree index excludes u32::MAX"))
42 }
43}
44
45impl Default for TreeIndex {
46 fn default() -> Self {
47 Self::from_raw(0)
48 }
49}
50
51impl fmt::Debug for TreeIndex {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "TreeIndex({})", self.index())
54 }
55}
56
57impl From<u32> for TreeIndex {
58 fn from(index: u32) -> Self {
59 assert!(index != NONE, "u32::MAX is reserved for missing tree links");
60 Self::from_raw(index)
61 }
62}
63
64impl From<usize> for TreeIndex {
65 fn from(index: usize) -> Self {
66 let index = u32::try_from(index).expect("tree index exceeds u32::MAX - 1");
67 assert!(index != NONE, "u32::MAX is reserved for missing tree links");
68 Self::from_raw(index)
69 }
70}
71
72#[derive(Clone, Copy, Eq, PartialEq)]
74pub struct EntryData {
75 pub size: u128,
78 pub mtime: SystemTime,
80 pub entry_count: Option<u64>,
82 pub metadata_io_error: bool,
84 pub is_dir: bool,
86}
87
88impl Default for EntryData {
89 fn default() -> EntryData {
90 EntryData {
91 size: u128::default(),
92 mtime: UNIX_EPOCH,
93 entry_count: None,
94 metadata_io_error: bool::default(),
95 is_dir: false,
96 }
97 }
98}
99
100impl fmt::Debug for EntryData {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.debug_struct("EntryData")
103 .field("size", &self.size)
104 .field("entry_count", &self.entry_count)
105 .field("metadata_io_error", &self.metadata_io_error)
107 .finish()
108 }
109}
110
111#[derive(Debug, Eq, PartialEq)]
113pub struct Entry<'a> {
114 pub name: Cow<'a, Path>,
116 pub data: EntryData,
118}
119
120impl std::ops::Deref for Entry<'_> {
121 type Target = EntryData;
122
123 fn deref(&self) -> &Self::Target {
124 &self.data
125 }
126}
127
128#[derive(Clone)]
139struct TreeNode {
140 size: u128,
141 mtime: SystemTime,
142 entry_count: u64,
143 name_start: u32,
144 name_len: u32,
145 parent: u32,
146 first_child: u32,
147 next_sibling: u32,
148 flags: u32,
149}
150
151impl TreeNode {
152 fn new(name_start: u32, name_len: u32, data: EntryData) -> Self {
153 Self {
154 size: data.size,
155 mtime: data.mtime,
156 entry_count: data.entry_count.unwrap_or_default(),
157 name_start,
158 name_len,
159 parent: NONE,
160 first_child: NONE,
161 next_sibling: NONE,
162 flags: FLAG_OCCUPIED
163 | (u32::from(data.is_dir) * FLAG_DIRECTORY)
164 | (u32::from(data.metadata_io_error) * FLAG_METADATA_IO_ERROR)
165 | (u32::from(data.entry_count.is_some()) * FLAG_ENTRY_COUNT),
166 }
167 }
168
169 fn is_occupied(&self) -> bool {
170 self.flags & FLAG_OCCUPIED != 0
171 }
172
173 fn data(&self) -> EntryData {
174 EntryData {
175 size: self.size,
176 mtime: self.mtime,
177 entry_count: (self.flags & FLAG_ENTRY_COUNT != 0).then_some(self.entry_count),
178 metadata_io_error: self.flags & FLAG_METADATA_IO_ERROR != 0,
179 is_dir: self.flags & FLAG_DIRECTORY != 0,
180 }
181 }
182
183 fn set_data(&mut self, data: EntryData) {
184 self.size = data.size;
185 self.mtime = data.mtime;
186 self.entry_count = data.entry_count.unwrap_or_default();
187 self.flags = FLAG_OCCUPIED
188 | (u32::from(data.is_dir) * FLAG_DIRECTORY)
189 | (u32::from(data.metadata_io_error) * FLAG_METADATA_IO_ERROR)
190 | (u32::from(data.entry_count.is_some()) * FLAG_ENTRY_COUNT);
191 }
192}
193
194#[derive(Debug)]
196pub enum TreeError {
197 Allocation(std::collections::TryReserveError),
199 Capacity,
201 InvalidIndex,
203 AlreadyAttached,
205 Cycle,
207}
208
209impl fmt::Display for TreeError {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 Self::Allocation(err) => err.fmt(f),
213 Self::Capacity => f.write_str("tree exceeds its u32 storage limit"),
214 Self::InvalidIndex => f.write_str("tree index does not exist"),
215 Self::AlreadyAttached => f.write_str("tree node already has a parent"),
216 Self::Cycle => f.write_str("tree attachment would create a cycle"),
217 }
218 }
219}
220
221impl std::error::Error for TreeError {}
222
223impl From<std::collections::TryReserveError> for TreeError {
224 fn from(err: std::collections::TryReserveError) -> Self {
225 Self::Allocation(err)
226 }
227}
228
229#[derive(Clone)]
231pub struct Tree {
232 nodes: Vec<TreeNode>,
234 names: Vec<u8>,
236 free_head: u32,
238 len: usize,
240}
241
242impl Default for Tree {
243 fn default() -> Self {
244 Self::new()
245 }
246}
247
248impl fmt::Debug for Tree {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 f.debug_struct("Tree")
251 .field("nodes", &self.nodes.len())
252 .field("names", &self.names.len())
253 .field("len", &self.len)
254 .finish()
255 }
256}
257
258impl Tree {
259 #[must_use]
261 pub const fn new() -> Self {
262 Self {
263 nodes: Vec::new(),
264 names: Vec::new(),
265 free_head: NONE,
266 len: 0,
267 }
268 }
269
270 pub fn add_root(&mut self, name: impl AsRef<Path>, data: EntryData) -> TreeIndex {
276 self.try_add_root(name, data)
277 .expect("tree storage can be allocated")
278 }
279
280 pub fn add_detached(&mut self, name: impl AsRef<Path>, data: EntryData) -> TreeIndex {
282 self.add_root(name, data)
283 }
284
285 pub fn add_child(
291 &mut self,
292 parent: TreeIndex,
293 name: impl AsRef<Path>,
294 data: EntryData,
295 ) -> TreeIndex {
296 self.try_add_child(parent, name, data)
297 .expect("tree storage can be allocated and parent is valid")
298 }
299
300 pub fn try_add_root(
302 &mut self,
303 name: impl AsRef<Path>,
304 data: EntryData,
305 ) -> Result<TreeIndex, TreeError> {
306 let (name_start, name_len) = self.try_append_name(name.as_ref())?;
307 self.try_allocate(TreeNode::new(name_start, name_len, data))
308 }
309
310 pub fn try_add_child(
312 &mut self,
313 parent: TreeIndex,
314 name: impl AsRef<Path>,
315 data: EntryData,
316 ) -> Result<TreeIndex, TreeError> {
317 if !self.contains(parent) {
318 return Err(TreeError::InvalidIndex);
319 }
320 let child = self.try_add_root(name, data)?;
321 self.attach(parent, child)?;
322 Ok(child)
323 }
324
325 pub(crate) fn try_add_child_native(
326 &mut self,
327 parent: TreeIndex,
328 name: &[u8],
329 data: EntryData,
330 ) -> Result<TreeIndex, TreeError> {
331 if !self.contains(parent) {
332 return Err(TreeError::InvalidIndex);
333 }
334 let (name_start, name_len) = self.try_append_native_name(name)?;
335 let child = self.try_allocate(TreeNode::new(name_start, name_len, data))?;
336 self.attach(parent, child)?;
337 Ok(child)
338 }
339
340 pub fn attach(&mut self, parent: TreeIndex, child: TreeIndex) -> Result<(), TreeError> {
342 if !self.contains(parent) || !self.contains(child) {
343 return Err(TreeError::InvalidIndex);
344 }
345 if self.nodes[child.index()].parent != NONE {
346 return Err(TreeError::AlreadyAttached);
347 }
348 let mut ancestor = Some(parent);
349 while let Some(index) = ancestor {
350 if index == child {
351 return Err(TreeError::Cycle);
352 }
353 ancestor = self.parent(index);
354 }
355 let first_child = self.nodes[parent.index()].first_child;
356 self.nodes[child.index()].parent = parent.index() as u32;
357 self.nodes[child.index()].next_sibling = first_child;
358 self.nodes[parent.index()].first_child = child.index() as u32;
359 Ok(())
360 }
361
362 #[must_use]
364 pub fn parent(&self, index: TreeIndex) -> Option<TreeIndex> {
365 let node = self.node(index)?;
366 (node.parent != NONE).then(|| TreeIndex::from_raw(node.parent))
367 }
368
369 #[must_use]
371 pub fn children(&self, index: TreeIndex) -> Children<'_> {
372 Children {
373 nodes: &self.nodes,
374 next: self.node(index).map_or(NONE, |node| node.first_child),
375 }
376 }
377
378 pub fn indices(&self) -> impl Iterator<Item = TreeIndex> + '_ {
380 self.nodes
381 .iter()
382 .enumerate()
383 .filter(|(_, node)| node.is_occupied())
384 .map(|(index, _)| TreeIndex::from(index))
385 }
386
387 #[must_use]
389 pub fn entry(&self, index: TreeIndex) -> Option<Entry<'_>> {
390 let node = self.node(index)?;
391 let name = self.name(index)?;
392 Some(Entry {
393 name,
394 data: node.data(),
395 })
396 }
397
398 #[must_use]
400 pub fn data(&self, index: TreeIndex) -> Option<EntryData> {
401 self.node(index).map(TreeNode::data)
402 }
403
404 pub fn set_data(&mut self, index: TreeIndex, data: EntryData) -> bool {
406 let Some(node) = self.node_mut(index) else {
407 return false;
408 };
409 node.set_data(data);
410 true
411 }
412
413 pub fn update(&mut self, index: TreeIndex, edit: impl FnOnce(&mut EntryData)) -> bool {
415 let Some(mut data) = self.data(index) else {
416 return false;
417 };
418 edit(&mut data);
419 self.set_data(index, data)
420 }
421
422 #[must_use]
424 pub fn name(&self, index: TreeIndex) -> Option<Cow<'_, Path>> {
425 let bytes = self.native_name(index)?;
426 #[cfg(unix)]
427 {
428 use std::os::unix::ffi::OsStrExt as _;
429 Some(Cow::Borrowed(Path::new(std::ffi::OsStr::from_bytes(bytes))))
430 }
431 #[cfg(windows)]
432 {
433 use std::os::windows::ffi::OsStringExt as _;
434 let wide = bytes
435 .chunks_exact(2)
436 .map(|bytes| u16::from_le_bytes([bytes[0], bytes[1]]))
437 .collect::<Vec<_>>();
438 Some(Cow::Owned(PathBuf::from(std::ffi::OsString::from_wide(
439 &wide,
440 ))))
441 }
442 }
443
444 pub fn rename(&mut self, index: TreeIndex, name: impl AsRef<Path>) -> Result<(), TreeError> {
446 if !self.contains(index) {
447 return Err(TreeError::InvalidIndex);
448 }
449 let (start, len) = self.try_append_name(name.as_ref())?;
450 let node = &mut self.nodes[index.index()];
451 node.name_start = start;
452 node.name_len = len;
453 Ok(())
454 }
455
456 pub fn remove_subtree(&mut self, index: TreeIndex) -> usize {
458 if !self.contains(index) {
459 return 0;
460 }
461 self.detach(index);
462 let mut pending = vec![index];
463 let mut removed = 0;
464 while let Some(index) = pending.pop() {
465 let mut child = self.nodes[index.index()].first_child;
466 while child != NONE {
467 pending.push(TreeIndex::from_raw(child));
468 child = self.nodes[child as usize].next_sibling;
469 }
470 let node = &mut self.nodes[index.index()];
471 node.flags = 0;
472 node.parent = NONE;
473 node.first_child = NONE;
474 node.next_sibling = self.free_head;
475 self.free_head = index.index() as u32;
476 self.len -= 1;
477 removed += 1;
478 }
479 removed
480 }
481
482 #[must_use]
484 pub fn contains(&self, index: TreeIndex) -> bool {
485 self.node(index).is_some()
486 }
487
488 #[must_use]
490 pub fn len(&self) -> usize {
491 self.len
492 }
493
494 #[must_use]
496 pub fn is_empty(&self) -> bool {
497 self.len == 0
498 }
499
500 pub(crate) fn native_name(&self, index: TreeIndex) -> Option<&[u8]> {
501 let node = self.node(index)?;
502 let start = node.name_start as usize;
503 Some(&self.names[start..][..node.name_len as usize])
504 }
505
506 fn node(&self, index: TreeIndex) -> Option<&TreeNode> {
507 self.nodes
508 .get(index.index())
509 .filter(|node| node.is_occupied())
510 }
511
512 fn node_mut(&mut self, index: TreeIndex) -> Option<&mut TreeNode> {
513 self.nodes
514 .get_mut(index.index())
515 .filter(|node| node.is_occupied())
516 }
517
518 fn try_allocate(&mut self, node: TreeNode) -> Result<TreeIndex, TreeError> {
519 let index = if self.free_head == NONE {
520 let index = u32::try_from(self.nodes.len()).map_err(|_| TreeError::Capacity)?;
521 if index == NONE {
522 return Err(TreeError::Capacity);
523 }
524 self.nodes.try_reserve(1)?;
525 self.nodes.push(node);
526 index
527 } else {
528 let index = self.free_head;
529 self.free_head = self.nodes[index as usize].next_sibling;
530 self.nodes[index as usize] = node;
531 index
532 };
533 self.len += 1;
534 Ok(TreeIndex::from_raw(index))
535 }
536
537 fn try_append_name(&mut self, name: &Path) -> Result<(u32, u32), TreeError> {
538 #[cfg(unix)]
539 {
540 use std::os::unix::ffi::OsStrExt as _;
541 self.try_append_native_name(name.as_os_str().as_bytes())
542 }
543 #[cfg(windows)]
544 {
545 use std::os::windows::ffi::OsStrExt as _;
546 let units = name.as_os_str().encode_wide();
547 let byte_len = units
548 .clone()
549 .count()
550 .checked_mul(2)
551 .ok_or(TreeError::Capacity)?;
552 let start = self.names.len();
553 let end = start.checked_add(byte_len).ok_or(TreeError::Capacity)?;
554 let (start, len) = (
555 u32::try_from(start).map_err(|_| TreeError::Capacity)?,
556 u32::try_from(byte_len).map_err(|_| TreeError::Capacity)?,
557 );
558 u32::try_from(end).map_err(|_| TreeError::Capacity)?;
559 self.names.try_reserve(byte_len)?;
560 self.names.extend(units.flat_map(u16::to_le_bytes));
561 Ok((start, len))
562 }
563 }
564
565 fn try_append_native_name(&mut self, name: &[u8]) -> Result<(u32, u32), TreeError> {
566 let start = self.names.len();
567 let end = start.checked_add(name.len()).ok_or(TreeError::Capacity)?;
568 let (start, len) = (
569 u32::try_from(start).map_err(|_| TreeError::Capacity)?,
570 u32::try_from(name.len()).map_err(|_| TreeError::Capacity)?,
571 );
572 u32::try_from(end).map_err(|_| TreeError::Capacity)?;
573 self.names.try_reserve(name.len())?;
574 self.names.extend_from_slice(name);
575 Ok((start, len))
576 }
577
578 fn detach(&mut self, index: TreeIndex) {
579 let parent = self.nodes[index.index()].parent;
580 if parent == NONE {
581 return;
582 }
583 let mut link = self.nodes[parent as usize].first_child;
584 let mut previous = NONE;
585 while link != NONE {
586 if link == index.index() as u32 {
587 let next = self.nodes[link as usize].next_sibling;
588 if previous == NONE {
589 self.nodes[parent as usize].first_child = next;
590 } else {
591 self.nodes[previous as usize].next_sibling = next;
592 }
593 self.nodes[index.index()].parent = NONE;
594 self.nodes[index.index()].next_sibling = NONE;
595 return;
596 }
597 previous = link;
598 link = self.nodes[link as usize].next_sibling;
599 }
600 }
601}
602
603pub struct Children<'a> {
605 nodes: &'a [TreeNode],
606 next: u32,
607}
608
609impl Iterator for Children<'_> {
610 type Item = TreeIndex;
611
612 fn next(&mut self) -> Option<Self::Item> {
613 if self.next == NONE {
614 return None;
615 }
616 let index = self.next;
617 self.next = self.nodes[index as usize].next_sibling;
618 Some(TreeIndex::from_raw(index))
619 }
620}
621
622#[derive(Debug)]
624pub struct Traversal {
625 pub tree: Tree,
627 pub root_index: TreeIndex,
629 pub start_time: Instant,
631 pub cost: Option<Duration>,
633}
634
635impl Default for Traversal {
636 fn default() -> Self {
637 Self::new()
638 }
639}
640
641impl Traversal {
642 #[must_use]
644 pub fn new() -> Self {
645 let mut tree = Tree::new();
646 let root_index = tree.add_root("", EntryData::default());
647 Self {
648 tree,
649 root_index,
650 start_time: Instant::now(),
651 cost: None,
652 }
653 }
654
655 #[must_use]
657 pub fn is_costly(&self) -> bool {
658 self.cost.is_none_or(|d| d.as_secs_f32() > 10.0)
659 }
660}
661
662#[derive(Clone, Copy)]
664pub struct TraversalStats {
665 pub entries_traversed: u64,
667 pub start: std::time::Instant,
669 pub elapsed: Option<std::time::Duration>,
671 pub io_errors: u64,
673 pub total_bytes: Option<u128>,
675}
676
677impl Default for TraversalStats {
678 fn default() -> Self {
679 Self {
680 entries_traversed: 0,
681 start: std::time::Instant::now(),
682 elapsed: None,
683 io_errors: 0,
684 total_bytes: None,
685 }
686 }
687}
688
689pub struct TraversalEntry(pub(crate) crate::walk::Entry);
691
692pub enum TraversalEvent {
694 Entry(io::Result<TraversalEntry>, Arc<PathBuf>, u64, usize),
702 RootError(Arc<PathBuf>, usize),
704 Finished,
706}
707
708pub struct BackgroundTraversal {
710 walk_options: WalkOptions,
711 pub root_idx: TreeIndex,
713 pub stats: TraversalStats,
715 pub(crate) root_nodes: Vec<Option<TreeIndex>>,
717 nodes_by_directory: Vec<Option<TreeIndex>>,
719 inodes: InodeFilter,
720 throttle: Option<Throttle>,
721 skip_root: bool,
722 use_root_path: bool,
723 retained_depth: Option<usize>,
724 preexisting_nodes: HashMap<PathBuf, (TreeIndex, bool)>,
725 pub event_rx: Receiver<TraversalEvent>,
727}
728
729impl BackgroundTraversal {
730 pub fn start(
733 root_idx: TreeIndex,
734 walk_options: &WalkOptions,
735 input: Vec<PathBuf>,
736 pattern_roots: Option<&[PathBuf]>,
737 skip_root: bool,
738 use_root_path: bool,
739 ) -> anyhow::Result<BackgroundTraversal> {
740 Self::start_inner(
741 root_idx,
742 walk_options,
743 input,
744 pattern_roots,
745 skip_root,
746 use_root_path,
747 HashMap::new(),
748 )
749 }
750
751 pub fn start_incremental(
759 root_idx: TreeIndex,
760 walk_options: &WalkOptions,
761 input: Vec<PathBuf>,
762 pattern_roots: Option<&[PathBuf]>,
763 skip_root: bool,
764 use_root_path: bool,
765 preexisting_nodes: Vec<(PathBuf, TreeIndex, bool)>,
766 ) -> anyhow::Result<BackgroundTraversal> {
767 let mut walk_options = walk_options.clone();
768 walk_options.ignore_dirs.extend(
769 preexisting_nodes
770 .iter()
771 .filter_map(|(path, _, _)| gix::path::realpath(path).ok()),
772 );
773 Self::start_inner(
774 root_idx,
775 &walk_options,
776 input,
777 pattern_roots,
778 skip_root,
779 use_root_path,
780 preexisting_nodes
781 .into_iter()
782 .map(|(path, index, needs_metadata)| (path, (index, needs_metadata)))
783 .collect(),
784 )
785 }
786
787 fn start_inner(
788 root_idx: TreeIndex,
789 walk_options: &WalkOptions,
790 input: Vec<PathBuf>,
791 pattern_roots: Option<&[PathBuf]>,
792 skip_root: bool,
793 use_root_path: bool,
794 preexisting_nodes: HashMap<PathBuf, (TreeIndex, bool)>,
795 ) -> anyhow::Result<BackgroundTraversal> {
796 let num_roots = input.len();
797 let (entry_tx, entry_rx) = crossbeam::channel::bounded(100);
798 let pattern_roots = pattern_roots.map(<[PathBuf]>::to_owned);
799 std::thread::Builder::new()
800 .name("dua-fs-walk-dispatcher".to_string())
801 .spawn({
802 let walk_options = walk_options.clone();
803 move || {
804 let (mut root_paths, mut root_indices, mut device_ids, mut walk_roots) = (
805 Vec::with_capacity(input.len()),
806 Vec::with_capacity(input.len()),
807 Vec::with_capacity(input.len()),
808 Vec::with_capacity(input.len()),
809 );
810 for (root_idx, root_path) in input.into_iter().enumerate() {
811 log::info!("Walking {}", root_path.display());
812 let device_id = if walk_options.cross_filesystems {
813 0
814 } else {
815 let Ok(device_id) = crossdev::init(&root_path) else {
816 if entry_tx
817 .send(TraversalEvent::RootError(Arc::new(root_path), root_idx))
818 .is_err()
819 {
820 return;
821 }
822 continue;
823 };
824 device_id
825 };
826 let pattern_root = pattern_roots.as_deref().map(|pattern_roots| {
827 pattern_roots
828 .iter()
829 .filter(|candidate| root_path.starts_with(candidate))
830 .max_by_key(|candidate| candidate.components().count())
831 .cloned()
832 .unwrap_or_else(|| root_path.clone())
833 });
834 walk_roots.push(WalkRoot {
835 index: walk_roots.len(),
836 pattern_root,
837 path: root_path.clone(),
838 #[cfg(any(windows, target_os = "macos"))]
839 entry: None,
840 device_id,
841 });
842 root_indices.push(root_idx);
843 device_ids.push(device_id);
844 root_paths.push(Arc::new(root_path));
845 }
846
847 for (root, event) in walk_options.iter_from_paths(
848 walk_roots,
849 skip_root,
850 crate::walk::Order::ParentFirst,
851 ) {
852 let crate::walk::RootEvent::Entry(entry) = event else {
853 continue;
854 };
855 if entry_tx
856 .send(TraversalEvent::Entry(
857 entry.map(TraversalEntry),
858 Arc::clone(&root_paths[root]),
859 device_ids[root],
860 root_indices[root],
861 ))
862 .is_err()
863 {
864 return;
867 }
868 }
869 if entry_tx.send(TraversalEvent::Finished).is_err() {
870 log::error!("Failed to send TraversalEvents::Finished event");
871 }
872 }
873 })?;
874
875 Ok(Self {
876 walk_options: walk_options.clone(),
877 root_idx,
878 stats: TraversalStats::default(),
879 root_nodes: vec![None; num_roots],
880 nodes_by_directory: Vec::new(),
881 inodes: InodeFilter::default(),
882 throttle: Some(Throttle::new(Duration::from_millis(250), None)),
883 skip_root,
884 use_root_path,
885 retained_depth: None,
886 preexisting_nodes,
887 event_rx: entry_rx,
888 })
889 }
890
891 #[must_use]
893 pub fn root_nodes(&self) -> Option<Vec<TreeIndex>> {
894 self.root_nodes.iter().copied().collect()
895 }
896
897 pub(crate) fn retain_depth(mut self, depth: Option<usize>) -> Self {
901 self.retained_depth = depth;
902 self
903 }
904
905 fn record_error_on_root(
906 &mut self,
907 traversal: &mut Traversal,
908 root_idx: usize,
909 root_path: &Path,
910 ) {
911 if self.skip_root {
912 return;
913 }
914 if let Some(root) = self.root_nodes[root_idx] {
916 traversal
917 .tree
918 .update(root, |entry| entry.metadata_io_error = true);
919 return;
920 }
921 let name = if self.use_root_path {
922 root_path.to_owned()
923 } else {
924 root_path
925 .file_name()
926 .unwrap_or(root_path.as_os_str())
927 .into()
928 };
929 let node = traversal.tree.add_child(
930 self.root_idx,
931 name,
932 EntryData {
933 metadata_io_error: true,
934 is_dir: true,
935 ..EntryData::default()
936 },
937 );
938 traversal.tree.update(self.root_idx, |entry| {
939 *entry.entry_count.get_or_insert(0) += 1;
940 });
941 self.root_nodes[root_idx] = Some(node);
942 }
943
944 fn set_directory_node(&mut self, directory_id: usize, node: TreeIndex) {
945 if self.nodes_by_directory.len() <= directory_id {
946 self.nodes_by_directory.resize(directory_id + 1, None);
947 }
948 self.nodes_by_directory[directory_id] = Some(node);
949 }
950
951 #[expect(
964 clippy::too_many_lines,
965 reason = "event integration keeps tree updates atomic"
966 )]
967 pub fn integrate_traversal_event(
968 &mut self,
969 traversal: &mut Traversal,
970 event: TraversalEvent,
971 ) -> Option<bool> {
972 match event {
973 TraversalEvent::Entry(entry, root_path, device_id, root_idx) => {
974 self.stats.entries_traversed += 1;
975 let mut data = EntryData::default();
976 let Ok(TraversalEntry(entry)) = entry else {
977 self.stats.io_errors += 1;
978 self.record_error_on_root(traversal, root_idx, &root_path);
979 return self
980 .throttle
981 .as_ref()
982 .is_some_and(|t| t.can_update())
983 .then_some(false);
984 };
985 let walk_depth = entry.depth;
986 let name = if !self.skip_root && walk_depth == 0 && self.use_root_path {
987 root_path.as_path()
988 } else {
989 Path::new(&entry.file_name)
990 };
991
992 let mut file_size = 0u128;
993 let mut mtime: SystemTime = UNIX_EPOCH;
994 let mut has_mtime = false;
995 data.is_dir = entry.file_type.is_dir();
996 if let Ok(m) = &entry.metadata {
997 if self.walk_options.count_hard_links
998 || self.inodes.add(&entry, m)
999 && (self.walk_options.cross_filesystems
1000 || crossdev::is_same_device(device_id, m))
1001 {
1002 if self.walk_options.apparent_size {
1003 file_size = u128::from(m.len());
1004 } else {
1005 file_size = u128::from(
1006 size_on_disk(
1007 &entry.parent_path,
1008 name,
1009 m,
1010 data.is_dir,
1011 &self.walk_options,
1012 &mut self.inodes,
1013 )
1014 .unwrap_or_else(|_| {
1015 self.stats.io_errors += 1;
1016 data.metadata_io_error = true;
1017 0
1018 }),
1019 );
1020 }
1021 } else {
1022 data.entry_count = Some(0);
1023 }
1024
1025 if let Ok(modified) = m.modified() {
1026 mtime = modified;
1027 has_mtime = true;
1028 } else {
1029 self.stats.io_errors += 1;
1030 data.metadata_io_error = true;
1031 }
1032 } else {
1033 self.stats.io_errors += 1;
1034 data.metadata_io_error = true;
1035 }
1036
1037 data.mtime = mtime;
1038 data.size = file_size;
1039 if data.is_dir {
1040 data.entry_count = Some(1);
1041 }
1042 let entry_count = u64::from(data.is_dir || data.entry_count != Some(0));
1043 let preexisting = if self.preexisting_nodes.is_empty() {
1044 None
1045 } else {
1046 self.preexisting_nodes.remove(&entry.path())
1047 };
1048 if let Some((index, needs_metadata)) = preexisting {
1049 if let Some(directory_id) = entry.directory_id {
1050 self.set_directory_node(directory_id.index(), index);
1051 }
1052 if needs_metadata {
1053 traversal.tree.update(index, |existing| {
1054 existing.size += file_size;
1055 *existing.entry_count.get_or_insert(0) += entry_count;
1056 if has_mtime {
1057 existing.mtime = data.mtime;
1058 }
1059 existing.metadata_io_error |= data.metadata_io_error;
1060 existing.is_dir = data.is_dir;
1061 });
1062
1063 let mut ancestor = traversal.tree.parent(index);
1064 while let Some(ancestor_index) = ancestor {
1065 ancestor = traversal.tree.parent(ancestor_index);
1066 traversal.tree.update(ancestor_index, |entry| {
1067 entry.size += file_size;
1068 *entry.entry_count.get_or_insert(0) += entry_count;
1069 });
1070 }
1071 }
1072 return self
1073 .throttle
1074 .as_ref()
1075 .is_some_and(|t| t.can_update())
1076 .then_some(false);
1077 }
1078 let retain_entry = self.retained_depth.is_none_or(|depth| walk_depth <= depth);
1079
1080 let parent_index = if walk_depth == 0 {
1081 self.root_idx
1082 } else {
1083 let parent_id = entry
1084 .parent_directory_id
1085 .expect("non-root entries have a parent directory identifier");
1086 if self.skip_root && walk_depth == 1 {
1087 self.set_directory_node(parent_id.index(), self.root_idx);
1088 }
1089 self.nodes_by_directory
1090 .get(parent_id.index())
1091 .copied()
1092 .flatten()
1093 .expect("parent entries are emitted before their children")
1094 };
1095 let mut retained_node = None;
1096 if retain_entry {
1097 let entry_index = traversal.tree.add_child(parent_index, name, data);
1098 retained_node = Some(entry_index);
1099 if walk_depth == 0 {
1100 self.root_nodes[root_idx] = Some(entry_index);
1101 }
1102 }
1103 if let Some(directory_id) = entry.directory_id {
1104 self.set_directory_node(
1105 directory_id.index(),
1106 retained_node.unwrap_or(parent_index),
1107 );
1108 }
1109
1110 let mut ancestor = Some(parent_index);
1111 while let Some(index) = ancestor {
1112 ancestor = traversal.tree.parent(index);
1113 traversal.tree.update(index, |entry| {
1114 entry.size += file_size;
1115 *entry.entry_count.get_or_insert(0) += entry_count;
1116 });
1117 }
1118
1119 if self.throttle.as_ref().is_some_and(|t| t.can_update()) {
1120 return Some(false);
1121 }
1122 }
1123 TraversalEvent::RootError(root_path, root_idx) => {
1124 self.stats.io_errors += 1;
1125 self.record_error_on_root(traversal, root_idx, &root_path);
1126 }
1127 TraversalEvent::Finished => {
1128 self.throttle = None;
1129 let root_size = traversal
1130 .tree
1131 .data(self.root_idx)
1132 .expect("traversal root exists")
1133 .size;
1134 self.nodes_by_directory.clear();
1135 self.stats.total_bytes = Some(root_size);
1136 self.stats.elapsed = Some(self.stats.start.elapsed());
1137
1138 return Some(true);
1139 }
1140 }
1141 None
1142 }
1143}
1144
1145#[cfg(not(any(windows, target_os = "macos")))]
1146fn size_on_disk(
1148 _parent: &Path,
1149 name: &Path,
1150 meta: &crate::walk::Metadata,
1151 _is_dir: bool,
1152 _options: &WalkOptions,
1153 _inodes: &mut InodeFilter,
1154) -> io::Result<u64> {
1155 name.size_on_disk_fast(meta)
1156}
1157
1158#[cfg(target_os = "macos")]
1159#[allow(clippy::unnecessary_wraps)]
1161fn size_on_disk(
1162 _parent: &Path,
1163 _name: &Path,
1164 meta: &crate::walk::Metadata,
1165 _is_dir: bool,
1166 options: &WalkOptions,
1167 inodes: &mut InodeFilter,
1168) -> io::Result<u64> {
1169 Ok(if options.metadata_options.apfs_clone_metadata {
1170 inodes.allocated_size(meta)
1171 } else {
1172 meta.allocated_size()
1173 })
1174}
1175
1176#[cfg(windows)]
1177#[allow(clippy::unnecessary_wraps)]
1179fn size_on_disk(
1180 _parent: &Path,
1181 _name: &Path,
1182 meta: &crate::walk::Metadata,
1183 is_dir: bool,
1184 _options: &WalkOptions,
1185 _inodes: &mut InodeFilter,
1186) -> io::Result<u64> {
1187 Ok(if is_dir { 0 } else { meta.allocated_size() })
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192 use super::*;
1193
1194 #[test]
1195 fn ancestor_sizes_update_before_traversal_finishes() {
1196 let dir = tempfile::tempdir().unwrap();
1197 std::fs::create_dir(dir.path().join("nested")).unwrap();
1198 std::fs::write(dir.path().join("nested/file"), b"content").unwrap();
1199
1200 let mut traversal = Traversal::new();
1201 let mut background = BackgroundTraversal::start(
1202 traversal.root_index,
1203 &WalkOptions {
1204 threads: 2,
1205 count_hard_links: true,
1206 apparent_size: true,
1207 cross_filesystems: true,
1208 ignore_dirs: std::collections::BTreeSet::default(),
1209 ignore_patterns: None,
1210 metadata_options: crate::TraversalOptions::default(),
1211 },
1212 vec![dir.path().to_owned()],
1213 None,
1214 false,
1215 false,
1216 )
1217 .unwrap();
1218
1219 loop {
1220 let event = background.event_rx.recv().unwrap();
1221 let is_file = matches!(
1222 &event,
1223 TraversalEvent::Entry(Ok(TraversalEntry(entry)), _, _, _)
1224 if entry.file_name == "file"
1225 );
1226 background.integrate_traversal_event(&mut traversal, event);
1227 if is_file {
1228 let root_size = traversal.tree.data(traversal.root_index).unwrap().size;
1229 assert!(
1230 root_size >= 7,
1231 "root size should include the 7-byte nested file, got {root_size}"
1232 );
1233 let nested_size = traversal
1234 .tree
1235 .indices()
1236 .find_map(|index| {
1237 (traversal.tree.name(index).as_deref() == Some(Path::new("nested")))
1238 .then(|| traversal.tree.data(index).unwrap().size)
1239 })
1240 .unwrap();
1241 assert!(
1242 nested_size >= 7,
1243 "nested directory size should include its 7-byte file, got {nested_size}"
1244 );
1245 break;
1246 }
1247 }
1248 }
1249
1250 #[test]
1251 fn duplicate_roots_keep_their_own_children() {
1252 let dir = tempfile::tempdir().unwrap();
1253 std::fs::write(dir.path().join("file"), b"content").unwrap();
1254 let mut traversal = Traversal::new();
1255 let mut background = BackgroundTraversal::start(
1256 traversal.root_index,
1257 &WalkOptions {
1258 threads: 1,
1259 count_hard_links: true,
1260 apparent_size: true,
1261 cross_filesystems: true,
1262 ignore_dirs: std::collections::BTreeSet::default(),
1263 ignore_patterns: None,
1264 metadata_options: crate::TraversalOptions::default(),
1265 },
1266 vec![dir.path().to_owned(), dir.path().to_owned()],
1267 None,
1268 false,
1269 false,
1270 )
1271 .unwrap();
1272
1273 while !background
1274 .integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
1275 .unwrap_or(false)
1276 {}
1277
1278 let roots = traversal
1279 .tree
1280 .children(traversal.root_index)
1281 .collect::<Vec<_>>();
1282 assert_eq!(roots.len(), 2);
1283 for root in roots {
1284 assert_eq!(traversal.tree.children(root).count(), 1);
1285 }
1286 }
1287
1288 #[test]
1289 fn retained_depth_rolls_deeper_sizes_into_the_last_kept_node() {
1290 let dir = tempfile::tempdir().unwrap();
1291 std::fs::create_dir_all(dir.path().join("one/two")).unwrap();
1292 std::fs::write(dir.path().join("one/two/file"), b"content").unwrap();
1293 for (depth, expected_nodes) in [(0, 2), (1, 3)] {
1294 let mut traversal = Traversal::new();
1295 let mut background = BackgroundTraversal::start(
1296 traversal.root_index,
1297 &WalkOptions {
1298 threads: 1,
1299 count_hard_links: true,
1300 apparent_size: true,
1301 cross_filesystems: true,
1302 ignore_dirs: std::collections::BTreeSet::default(),
1303 ignore_patterns: None,
1304 metadata_options: crate::TraversalOptions::default(),
1305 },
1306 vec![dir.path().to_owned()],
1307 None,
1308 false,
1309 true,
1310 )
1311 .unwrap()
1312 .retain_depth(Some(depth));
1313
1314 while !background
1315 .integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
1316 .unwrap_or(false)
1317 {}
1318
1319 assert_eq!(traversal.tree.len(), expected_nodes);
1320 assert!(traversal.tree.data(traversal.root_index).unwrap().size >= 7);
1321 let root = traversal
1322 .tree
1323 .children(traversal.root_index)
1324 .next()
1325 .unwrap();
1326 let last_retained = if depth == 0 {
1327 root
1328 } else {
1329 traversal.tree.children(root).next().unwrap()
1330 };
1331 assert!(traversal.tree.data(last_retained).unwrap().size >= 7);
1332 }
1333 }
1334
1335 #[test]
1336 fn descendant_entry_errors_mark_the_retained_root() {
1337 let dir = tempfile::tempdir().unwrap();
1338 let root_path = dir.path().to_owned();
1339 let mut traversal = Traversal::new();
1340 let mut background = BackgroundTraversal::start(
1341 traversal.root_index,
1342 &WalkOptions {
1343 threads: 1,
1344 count_hard_links: true,
1345 apparent_size: true,
1346 cross_filesystems: true,
1347 ignore_dirs: std::collections::BTreeSet::default(),
1348 ignore_patterns: None,
1349 metadata_options: crate::TraversalOptions::default(),
1350 },
1351 vec![root_path.clone()],
1352 None,
1353 false,
1354 true,
1355 )
1356 .unwrap()
1357 .retain_depth(Some(0));
1358
1359 while background.root_nodes[0].is_none() {
1360 let event = background.event_rx.recv().unwrap();
1361 background.integrate_traversal_event(&mut traversal, event);
1362 }
1363 let root = background.root_nodes[0].unwrap();
1364 background.integrate_traversal_event(
1365 &mut traversal,
1366 TraversalEvent::Entry(
1367 Err(io::Error::other("unreadable descendant")),
1368 Arc::new(root_path),
1369 0,
1370 0,
1371 ),
1372 );
1373
1374 assert_eq!(background.stats.io_errors, 1);
1375 assert!(
1376 traversal.tree.data(root).unwrap().metadata_io_error,
1377 "a path-less descendant error is reported on its retained root: {:?}",
1378 traversal.tree.entry(root).unwrap()
1379 );
1380 }
1381
1382 #[cfg(target_os = "macos")]
1383 #[test]
1384 fn interactive_traversal_deduplicates_apfs_clones() {
1385 use std::os::unix::fs::MetadataExt as _;
1386
1387 fn total(path: &Path, deduplicate: bool) -> u128 {
1388 let mut traversal = Traversal::new();
1389 let mut background = BackgroundTraversal::start(
1390 traversal.root_index,
1391 &WalkOptions {
1392 threads: 2,
1393 count_hard_links: false,
1394 apparent_size: false,
1395 cross_filesystems: true,
1396 ignore_dirs: std::collections::BTreeSet::default(),
1397 ignore_patterns: None,
1398 metadata_options: crate::TraversalOptions {
1399 apfs_clone_metadata: deduplicate,
1400 },
1401 },
1402 vec![path.to_owned()],
1403 None,
1404 false,
1405 false,
1406 )
1407 .unwrap();
1408
1409 while !background
1410 .integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
1411 .unwrap_or(false)
1412 {}
1413 traversal.tree.data(traversal.root_index).unwrap().size
1414 }
1415
1416 let directory = tempfile::tempdir().unwrap();
1417 let original = directory.path().join("original");
1418 let clone = directory.path().join("clone");
1419 std::fs::write(&original, vec![1; 8192]).unwrap();
1420 std::fs::copy(&original, clone).unwrap();
1422 let data_fork_size = u128::from(std::fs::metadata(original).unwrap().blocks()) * 512;
1423
1424 assert_eq!(
1425 total(directory.path(), false) - total(directory.path(), true),
1426 data_fork_size
1427 );
1428 }
1429
1430 #[cfg(unix)]
1431 #[test]
1432 fn root_device_error_is_reported() {
1433 use std::os::unix::fs::symlink;
1434
1435 let dir = tempfile::tempdir().unwrap();
1436 let root = dir.path().join("dangling");
1437 let valid = dir.path().join("valid");
1438 symlink(dir.path().join("missing"), &root).unwrap();
1439 std::fs::write(&valid, b"content").unwrap();
1440 let mut traversal = Traversal::new();
1441 let mut background = BackgroundTraversal::start(
1442 traversal.root_index,
1443 &WalkOptions {
1444 threads: 1,
1445 count_hard_links: true,
1446 apparent_size: true,
1447 cross_filesystems: false,
1448 ignore_dirs: std::collections::BTreeSet::default(),
1449 ignore_patterns: None,
1450 metadata_options: crate::TraversalOptions::default(),
1451 },
1452 vec![root.clone(), valid.clone()],
1453 None,
1454 false,
1455 false,
1456 )
1457 .unwrap();
1458
1459 while !background
1460 .integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
1461 .unwrap_or(false)
1462 {}
1463
1464 assert_eq!(background.stats.io_errors, 1);
1465 let roots = background
1466 .root_nodes
1467 .iter()
1468 .copied()
1469 .collect::<Option<Vec<_>>>()
1470 .unwrap();
1471 assert_eq!(roots.len(), 2, "one node per input root: {roots:?}");
1472 assert_eq!(
1473 traversal
1474 .tree
1475 .data(traversal.root_index)
1476 .unwrap()
1477 .entry_count,
1478 Some(2),
1479 "the synthetic root counts both input roots"
1480 );
1481 assert!(
1482 traversal.tree.data(roots[0]).unwrap().metadata_io_error,
1483 "the failed root records its I/O error: {:?}",
1484 traversal.tree.entry(roots[0]).unwrap()
1485 );
1486 assert_eq!(
1487 traversal.tree.name(roots[0]).unwrap(),
1488 Path::new("dangling"),
1489 "the failed root retains its display name"
1490 );
1491 assert!(
1492 roots
1493 .iter()
1494 .all(|root| traversal.tree.parent(*root) == Some(traversal.root_index)),
1495 "all input roots are children of the synthetic root: {roots:?}"
1496 );
1497 }
1498
1499 #[test]
1500 fn tree_tracks_parents_and_reverse_insertion_order() {
1501 let mut tree = Tree::new();
1502 let root = tree.add_root("root", EntryData::default());
1503 let first = tree.add_child(root, "first", EntryData::default());
1504 let second = tree.add_child(root, "second", EntryData::default());
1505
1506 assert_eq!(tree.parent(first), Some(root));
1507 assert_eq!(tree.parent(second), Some(root));
1508 assert_eq!(tree.children(root).collect::<Vec<_>>(), [second, first]);
1509 }
1510
1511 #[test]
1512 fn tree_removal_is_stable_and_reuses_slots() {
1513 let mut tree = Tree::new();
1514 let root = tree.add_root("root", EntryData::default());
1515 let kept = tree.add_child(root, "kept", EntryData::default());
1516 let removed = tree.add_child(root, "removed", EntryData::default());
1517 let nested = tree.add_child(removed, "nested", EntryData::default());
1518
1519 assert_eq!(tree.remove_subtree(removed), 2);
1520 assert!(tree.contains(kept));
1521 assert_eq!(tree.children(root).collect::<Vec<_>>(), [kept]);
1522
1523 let reused = tree.add_child(root, "reused", EntryData::default());
1524 assert!(
1525 reused == removed || reused == nested,
1526 "a deleted slot is reused"
1527 );
1528 assert_eq!(tree.name(reused).as_deref(), Some(Path::new("reused")));
1529 assert_eq!(tree.children(root).collect::<Vec<_>>(), [reused, kept]);
1530 }
1531
1532 #[test]
1533 fn detached_nodes_can_be_renamed_and_attached() {
1534 let mut tree = Tree::new();
1535 let root = tree.add_root("root", EntryData::default());
1536 let child = tree.add_detached("before", EntryData::default());
1537
1538 tree.rename(child, "after").unwrap();
1539 tree.attach(root, child).unwrap();
1540
1541 assert_eq!(tree.name(child).as_deref(), Some(Path::new("after")));
1542 assert_eq!(tree.parent(child), Some(root));
1543 assert!(matches!(tree.attach(child, root), Err(TreeError::Cycle)));
1544 }
1545
1546 #[cfg(target_pointer_width = "64")]
1547 #[test]
1548 fn tree_nodes_and_optional_indices_are_compact() {
1549 assert_eq!(std::mem::size_of::<TreeNode>(), 64);
1550 assert_eq!(std::mem::size_of::<Option<TreeIndex>>(), 4);
1551 }
1552}