Skip to main content

dua/
traverse.rs

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/// Stable index of an entry in a [`Tree`].
23#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
24pub struct TreeIndex(NonZeroU32);
25
26impl TreeIndex {
27    /// Construct an index from its zero-based slot number.
28    #[must_use]
29    pub fn new(index: usize) -> Self {
30        Self::from(index)
31    }
32
33    /// Return this index as a `usize` for compact side tables.
34    #[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/// Metadata stored for a filesystem entry, excluding its arena-backed name.
73#[derive(Clone, Copy, Eq, PartialEq)]
74pub struct EntryData {
75    /// The entry's size in bytes. If it's a directory, the size is the aggregated file size of all children
76    /// plus the  size of the directory entry itself
77    pub size: u128,
78    /// Last modification time if available.
79    pub mtime: SystemTime,
80    /// Recursive entry count for directories, or `None` for files.
81    pub entry_count: Option<u64>,
82    /// If set, the item meta-data could not be obtained
83    pub metadata_io_error: bool,
84    /// `true` if the entry is a directory.
85    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            // Skip mtime
106            .field("metadata_io_error", &self.metadata_io_error)
107            .finish()
108    }
109}
110
111/// Borrowed view of an entry in a [`Tree`].
112#[derive(Debug, Eq, PartialEq)]
113pub struct Entry<'a> {
114    /// The entry name relative to its parent.
115    pub name: Cow<'a, Path>,
116    /// The entry metadata.
117    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/// Compact arena record for one filesystem entry.
129///
130/// Nodes live contiguously in [`Tree::nodes`], while names live once in the
131/// shared `Tree::names` byte arena and are referenced by `name_start` and
132/// `name_len`. Tree links are 32-bit slot indices, and `flags` packs occupancy,
133/// directory, metadata-error, and optional-entry-count state. This keeps a node
134/// at 64 bytes on supported 64-bit targets and avoids both an owned `PathBuf`
135/// and separate `petgraph` edge storage per entry—the main sources of the
136/// traversal's reduced memory footprint. Removed slots reuse `next_sibling` as
137/// a free-list link so their node storage can be recycled.
138#[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/// Failure to grow or modify a traversal tree.
195#[derive(Debug)]
196pub enum TreeError {
197    /// A backing allocation failed.
198    Allocation(std::collections::TryReserveError),
199    /// A name or node index exceeded the tree's `u32` storage limit.
200    Capacity,
201    /// A supplied node index does not exist.
202    InvalidIndex,
203    /// The child already belongs to another parent.
204    AlreadyAttached,
205    /// Attaching the child would create a cycle.
206    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/// Arena-backed filesystem tree with stable 32-bit node indices.
230#[derive(Clone)]
231pub struct Tree {
232    /// Contiguous arena slots addressed by [`TreeIndex`]; vacant slots form the free list.
233    nodes: Vec<TreeNode>,
234    /// Append-only platform-native name bytes referenced by each node's offset and length.
235    names: Vec<u8>,
236    /// First vacant node slot, chained through `TreeNode::next_sibling`, or [`NONE`].
237    free_head: u32,
238    /// Number of occupied nodes, which may be smaller than `nodes.len()` after removals.
239    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    /// Create an empty tree.
260    #[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    /// Add a parentless node.
271    ///
272    /// # Panics
273    ///
274    /// Panics if the tree exceeds its storage limits or allocation fails.
275    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    /// Add a node that is not yet attached to the tree.
281    pub fn add_detached(&mut self, name: impl AsRef<Path>, data: EntryData) -> TreeIndex {
282        self.add_root(name, data)
283    }
284
285    /// Add a child to `parent`, placing it before previously attached children.
286    ///
287    /// # Panics
288    ///
289    /// Panics if `parent` is missing or the tree cannot grow.
290    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    /// Try to add a parentless node.
301    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    /// Try to add a child to `parent`.
311    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    /// Attach a parentless node to `parent`, before its existing children.
341    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    /// Return a node's parent, or `None` for a parentless or missing node.
363    #[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    /// Iterate a node's children in reverse insertion order.
370    #[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    /// Iterate all currently occupied node indices.
379    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    /// Return an entry view for `index`.
388    #[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    /// Return a copy of an entry's metadata.
399    #[must_use]
400    pub fn data(&self, index: TreeIndex) -> Option<EntryData> {
401        self.node(index).map(TreeNode::data)
402    }
403
404    /// Replace an entry's metadata, returning `false` if the index is missing.
405    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    /// Mutate an entry's metadata, returning `false` if the index is missing.
414    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    /// Return an entry's arena-backed name.
423    #[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    /// Replace an entry's name. Old arena bytes remain reserved until the tree is dropped.
445    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    /// Remove `index` and all descendants, returning the number removed.
457    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    /// Return whether `index` refers to a live node.
483    #[must_use]
484    pub fn contains(&self, index: TreeIndex) -> bool {
485        self.node(index).is_some()
486    }
487
488    /// Return the number of live nodes.
489    #[must_use]
490    pub fn len(&self) -> usize {
491        self.len
492    }
493
494    /// Return whether this tree has no live nodes.
495    #[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
603/// Iterator over a node's children.
604pub 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/// The result of the previous filesystem traversal
623#[derive(Debug)]
624pub struct Traversal {
625    /// A tree representing the entire filestem traversal
626    pub tree: Tree,
627    /// The top-level node of the tree.
628    pub root_index: TreeIndex,
629    /// The time at which the instance was created, typically the start of the traversal.
630    pub start_time: Instant,
631    /// The time it cost to compute the traversal, when done.
632    pub cost: Option<Duration>,
633}
634
635impl Default for Traversal {
636    fn default() -> Self {
637        Self::new()
638    }
639}
640
641impl Traversal {
642    /// Create a new empty traversal with a synthetic root node.
643    #[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    /// Return `true` if this traversal is considered expensive to recompute.
656    #[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/// Runtime statistics gathered while traversal is running.
663#[derive(Clone, Copy)]
664pub struct TraversalStats {
665    /// Amount of files or directories we have seen during the filesystem traversal
666    pub entries_traversed: u64,
667    /// The time at which the traversal started.
668    pub start: std::time::Instant,
669    /// The amount of time it took to finish the traversal. Set only once done.
670    pub elapsed: Option<std::time::Duration>,
671    /// Total amount of IO errors encountered when traversing the filesystem
672    pub io_errors: u64,
673    /// Total amount of bytes seen during the traversal
674    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
689/// A filesystem entry waiting to be integrated into a traversal.
690pub struct TraversalEntry(pub(crate) crate::walk::Entry);
691
692/// Events emitted by a background filesystem traversal.
693pub enum TraversalEvent {
694    /// A discovered entry and its traversal context:
695    ///
696    /// 0. The discovered entry, or the I/O error encountered while reading it.
697    /// 1. The path of the input root being traversed.
698    /// 2. The input root's device ID.
699    /// 3. The input root's index in the original input list, used to place its tree node in the
700    ///    per-root side table so callers can recover input order, including failed roots.
701    Entry(io::Result<TraversalEntry>, Arc<PathBuf>, u64, usize),
702    /// A root that could not be initialized, with its input index.
703    RootError(Arc<PathBuf>, usize),
704    /// Traversal completed.
705    Finished,
706}
707
708/// An in-progress traversal which exposes newly obtained entries
709pub struct BackgroundTraversal {
710    walk_options: WalkOptions,
711    /// Tree node index that acts as root for this traversal integration.
712    pub root_idx: TreeIndex,
713    /// Running traversal statistics.
714    pub stats: TraversalStats,
715    /// Root nodes in input order; populated as root traversal events are integrated.
716    pub(crate) root_nodes: Vec<Option<TreeIndex>>,
717    /// Retained tree node for each dense directory identifier emitted by the walker.
718    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    /// Receiver used to obtain traversal events from the worker thread.
726    pub event_rx: Receiver<TraversalEvent>,
727}
728
729impl BackgroundTraversal {
730    /// Start a background thread to perform the actual tree walk, and dispatch the results
731    /// as events to be received on [`BackgroundTraversal::event_rx`].
732    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    /// Start an incremental traversal that preserves subtrees listed in `preexisting_nodes`.
752    ///
753    /// This is used when extending a traversal to a parent directory: rescanning an existing
754    /// subtree would waste work and duplicate its nodes and totals. Each tuple is
755    /// `(path, node, needs_metadata)`. `needs_metadata` is true when the node was a synthetic
756    /// traversal root that represented the directory's contents but not the directory entry
757    /// itself; once that root becomes a child, its own metadata must be integrated.
758    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                            // The channel is closed, this means the user has
865                            // requested to quit the app. Abort the walking.
866                            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    /// Return the top-level nodes in the same order as the traversal inputs once all roots exist.
892    #[must_use]
893    pub fn root_nodes(&self) -> Option<Vec<TreeIndex>> {
894        self.root_nodes.iter().copied().collect()
895    }
896
897    /// Keep tree nodes through `depth`, while still aggregating all sizes, or retain all nodes when
898    /// it is `None`. For example, 0 retains roots only, 1 also retains their immediate children,
899    /// and 2 also retains grandchildren.
900    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        // Entry errors carry no descendant path, so report them on the corresponding root.
915        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    /// Integrate `event` into traversal `t` so its information is represented by it.
952    /// This builds the traversal tree from a directory-walk.
953    ///
954    /// Returns
955    /// * `Some(true)` if the traversal is finished
956    /// * `Some(false)` if the caller may update its state after throttling kicked in
957    /// * `None` - the event was written into the traversal, but there is nothing else to do
958    ///
959    /// # Panics
960    ///
961    /// Panics if a child entry arrives before its parent, violating the parent-first traversal
962    /// invariant.
963    #[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")))]
1146/// Return disk usage for `name` on Unix-like platforms.
1147fn 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/// Return disk usage from metadata already collected by the macOS filesystem walker.
1160#[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/// Return disk usage for `name` on Windows platforms.
1178#[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 uses fclonefileat(2) first on Apple platforms, producing an APFS clone.
1421        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}