Skip to main content

dua_core/
lib.rs

1//! Parallel filesystem traversal backed by a work-stealing worker pool.
2//!
3//! [`walk`] yields the root first, then workers read directories and distribute newly discovered
4//! subdirectories among themselves. [`Order::ParentFirst`] publishes each directory's entries
5//! before scheduling its children, while [`Order::Completion`] allows descendant batches to arrive
6//! first when their reads finish sooner. Sibling order is unspecified in both modes.
7//!
8//! The `descend` predicate controls which directories are traversed; rejected directories are
9//! still yielded (but not traversed).
10//! Symbolic links are reported but never followed, and filesystem errors are
11//! returned as iterator items. Dropping the iterator stops and joins its workers.
12//!
13//! # Scheduling
14//!
15//! The root directory starts in a shared injector queue. On platforms where directory-entry
16//! metadata may require another syscall, directory reads enqueue small metadata batches, and
17//! metadata batches enqueue accepted child directories. Windows and macOS workers instead consume
18//! native metadata returned by directory enumeration and enqueue child directories immediately.
19//! Every worker can run available jobs from its local LIFO queue or steal from a peer. Each
20//! successful thief wakes another idle worker, ramping up only while work remains stealable. A
21//! worker parks when no queue has work and is unparked when new work arrives or the walk stops. The
22//! last completed job emits the finished event; dropping the iterator stops and joins all workers.
23#![deny(unsafe_code)]
24#![deny(missing_docs)]
25
26use crossbeam::{
27    deque::{Injector, Steal, Stealer, Worker},
28    sync::{Parker, Unparker},
29};
30use std::{
31    collections::HashMap,
32    fs, io,
33    num::NonZeroU32,
34    path::{Path, PathBuf},
35    sync::{
36        Arc,
37        atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering},
38        mpsc::{Receiver, SyncSender, sync_channel},
39    },
40    thread,
41};
42
43#[cfg(any(not(any(windows, target_os = "macos")), test))]
44use std::ffi::OsString;
45
46#[cfg(not(any(windows, target_os = "macos")))]
47pub use std::fs::{FileType, Metadata};
48
49#[cfg(target_os = "macos")]
50#[allow(unsafe_code)]
51mod macos;
52
53#[cfg(windows)]
54#[allow(unsafe_code)]
55mod windows;
56
57#[cfg(target_os = "macos")]
58pub use macos::{Entry, FileType, Metadata};
59
60#[cfg(target_os = "macos")]
61use macos::ReadDir as NativeReadDir;
62
63#[cfg(target_os = "macos")]
64use std::fs::read_dir as read_dir_types;
65
66#[cfg(windows)]
67pub use windows::{Entry, FileType, Metadata};
68
69#[cfg(windows)]
70use windows::{ReadDir as NativeReadDir, read_dir_types};
71
72#[cfg(any(windows, target_os = "macos"))]
73enum ReadDir {
74    Metadata(NativeReadDir),
75    FileTypes {
76        entries: fs::ReadDir,
77        parent_path: Arc<Path>,
78        depth: usize,
79    },
80}
81
82#[cfg(any(windows, target_os = "macos"))]
83impl ReadDir {
84    fn open(path: Arc<Path>, depth: usize, options: Options) -> io::Result<Self> {
85        if options.skip_metadata {
86            Ok(Self::FileTypes {
87                entries: read_dir_types(&path)?,
88                parent_path: path,
89                depth,
90            })
91        } else {
92            NativeReadDir::open(path, depth, options).map(Self::Metadata)
93        }
94    }
95}
96
97#[cfg(any(windows, target_os = "macos"))]
98impl Iterator for ReadDir {
99    type Item = io::Result<Entry>;
100
101    fn next(&mut self) -> Option<Self::Item> {
102        match self {
103            Self::Metadata(reader) => reader.next(),
104            Self::FileTypes {
105                entries,
106                parent_path,
107                depth,
108            } => entries.next().map(|entry| {
109                let entry = entry?;
110                Ok(Entry {
111                    depth: *depth,
112                    file_name: entry.file_name(),
113                    file_type: FileType::from_std(entry.file_type()?),
114                    metadata: None,
115                    parent_path: Arc::clone(parent_path),
116                    directory_id: None,
117                    parent_directory_id: None,
118                })
119            }),
120        }
121    }
122}
123
124/// Decides whether to traverse an entry's children for a given root index.
125/// Returning `false` prunes descendants but still emits the entry itself.
126type Descend = dyn Fn(usize, &Entry) -> bool + Send + Sync;
127/// Entries obtained from one directory read.
128/// An outer error means the directory could not be opened; inner errors come from reading or
129/// converting individual directory entries.
130type Batch = io::Result<Vec<io::Result<Entry>>>;
131/// Number of directory entries grouped into each metadata job or result batch.
132/// Small chunks expose parallel work and stream wide directories while amortizing queue overhead.
133const ENTRY_CHUNK_SIZE: usize = 4;
134
135/// Controls when entries are yielded relative to their descendants.
136#[derive(Clone, Copy)]
137pub enum Order {
138    /// Yield entries as their parent-directory reads complete.
139    Completion,
140    /// Yield every parent before its descendants.
141    ParentFirst,
142}
143
144/// Filesystem metadata requested during traversal.
145#[derive(Clone, Copy, Debug, Default)]
146pub struct Options {
147    /// Collect only entry types, leaving [`Entry::metadata`] as `None`.
148    ///
149    /// Directory enumeration supplies types when available. Explicit roots and filesystems
150    /// without directory-entry types may still require a metadata lookup. This also disables
151    /// APFS clone metadata collection.
152    pub skip_metadata: bool,
153    /// Collect APFS clone identity and data-fork allocation metadata.
154    #[cfg(target_os = "macos")]
155    pub apfs_clone_metadata: bool,
156}
157
158impl Options {
159    /// Collect only entry types, leaving [`Entry::metadata`] as `None`.
160    #[must_use]
161    pub fn skip_metadata(mut self) -> Self {
162        self.skip_metadata = true;
163        self
164    }
165}
166
167/// Dense identifier of a directory within one filesystem walk.
168#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
169pub struct DirectoryId(NonZeroU32);
170
171impl DirectoryId {
172    fn new(index: usize) -> Self {
173        let stored = u32::try_from(index)
174            .ok()
175            .and_then(|index| index.checked_add(1))
176            .and_then(NonZeroU32::new)
177            .expect("directory identifier overflow");
178        Self(stored)
179    }
180
181    /// Return the zero-based identifier for indexing compact side tables.
182    #[must_use]
183    pub fn index(self) -> usize {
184        (self.0.get() - 1) as usize
185    }
186}
187
188/// A filesystem entry produced by [`walk`].
189#[cfg(not(any(windows, target_os = "macos")))]
190pub struct Entry {
191    /// Distance from the walk root: `0` for the root, `1` for its children, and so on.
192    pub depth: usize,
193    /// File name relative to `parent_path`.
194    pub file_name: OsString,
195    /// Filesystem entry type without following symbolic links.
196    pub file_type: FileType,
197    /// Requested metadata or its read error; `None` when [`Options::skip_metadata`] is set.
198    pub metadata: Option<io::Result<Metadata>>,
199    /// Path containing this entry.
200    pub parent_path: Arc<Path>,
201    /// Dense identifier of this directory within the current walk, or `None` for non-directories.
202    pub directory_id: Option<DirectoryId>,
203    /// Dense identifier of the directory containing this entry, or `None` for a walk root.
204    pub parent_directory_id: Option<DirectoryId>,
205}
206
207enum Job {
208    /// Read a directory and schedule processing of its entries.
209    ReadDir {
210        root_idx: usize,
211        path: Arc<Path>,
212        /// Dense identifier of the directory being read.
213        directory_id: usize,
214        /// Depth to be assigned to entries read from `path`; always at least `1`.
215        /// The directory at `path` is one level shallower.
216        entry_depth: usize,
217    },
218    /// Fetch metadata for a chunk of entries from a completed directory read.
219    #[cfg(not(any(windows, target_os = "macos")))]
220    StatCompletion {
221        root_idx: usize,
222        path: Arc<Path>,
223        /// Dense identifier of the directory containing these entries.
224        directory_id: usize,
225        /// Depth assigned to every entry in this chunk; always at least `1`, i.e. a file in a directory.
226        entry_depth: usize,
227        entries: Vec<fs::DirEntry>,
228    },
229}
230
231impl Job {
232    /// Return the index of the root path that this job belongs to.
233    fn root_idx(&self) -> usize {
234        match self {
235            Job::ReadDir { root_idx, .. } => *root_idx,
236            #[cfg(not(any(windows, target_os = "macos")))]
237            Job::StatCompletion { root_idx, .. } => *root_idx,
238        }
239    }
240}
241
242/// Internal worker-channel events, including batches, per-root completion, and pool completion.
243enum Event {
244    Batch {
245        root_idx: usize,
246        batch: Batch,
247    },
248    /// All work for this root is complete; emitted after all of its batches.
249    /// Completion events for different roots may occur in any order.
250    RootFinished {
251        root_idx: usize,
252    },
253    /// Emitted once after all roots have emitted `RootFinished`; this is the final event.
254    Finished,
255}
256
257/// Per-root events exposed by [`RootWalk`].
258/// Unlike [`Event`], batches are flattened into entries and pool-wide completion ends the iterator
259/// instead of being yielded; `Finished` therefore means only that the associated root completed.
260/// [`RootWalk`] yields `(root_idx, event)`, separating root routing from event meaning. [`Event`]
261/// cannot do this uniformly because its `Finished` variant is pool-wide and has no root index.
262pub enum RootEvent {
263    /// An entry or filesystem error produced while walking the root.
264    Entry(io::Result<Entry>),
265    /// All entries for the root have been emitted.
266    Finished,
267}
268
269struct PoolShared {
270    /// Global queue that makes the initial root job available to whichever worker starts first.
271    injector: Injector<Job>,
272    stealers: Vec<Stealer<Job>>,
273    stop: AtomicBool,
274    descend: Arc<Descend>,
275    events: SyncSender<Event>,
276    /// Number of roots with queued or running jobs.
277    active_roots: AtomicUsize,
278    /// Number of queued or running jobs for each root index.
279    /// A counter reaching zero emits that root's [`Event::RootFinished`].
280    jobs_per_root: HashMap<usize, AtomicUsize>,
281    order: Order,
282    options: Options,
283    /// Handles used to wake workers, indexed by worker number.
284    unparkers: Vec<Unparker>,
285    /// Whether each worker has announced that it is idle, indexed like `unparkers`.
286    /// `wake_worker` atomically claims one idle worker before unparking it.
287    idle: Vec<AtomicBool>,
288    /// A round-robin cursor for the first idle worker to inspect.
289    next_wake: AtomicUsize,
290    /// Allocates dense identifiers to directories as they are discovered.
291    next_directory_id: AtomicUsize,
292}
293
294struct Pool {
295    shared: Arc<PoolShared>,
296    events: Receiver<Event>,
297    handles: Vec<thread::JoinHandle<()>>,
298}
299
300/// A multi-root iterator yielding each root index with entry and per-root completion events.
301/// Unlike [`Walk`], it preserves root identity and exposes when each root finishes.
302pub struct RootWalk {
303    /// Entries buffered for delivery, by root index.
304    next: Vec<(usize, RootEvent)>,
305    /// See [`Walk::pool`].
306    pool: Option<Pool>,
307}
308
309/// A single-root directory iterator whose directory reads happen in parallel.
310/// Unlike `RootWalk`, it yields entries directly and hides root identity and completion events.
311pub struct Walk {
312    /// Entries buffered for delivery.
313    ///
314    /// This vector is used as a stack: it starts with the root, and received batches are inserted
315    /// in reverse so popping preserves their original order.
316    ///
317    /// If consumption isn't as fast as its production, threads will block.
318    next: Vec<io::Result<Entry>>,
319    /// Owns the worker threads for as long as traversal is active.
320    ///
321    /// Clearing or dropping it requests shutdown, unparks every worker, and joins their threads.
322    pool: Option<Pool>,
323    root: PathBuf,
324    options: Options,
325    /// Whether the worker pool has finished the current traversal.
326    finished: bool,
327}
328
329/// Read a directory using native enumeration, collecting metadata unless
330/// [`Options::skip_metadata`] is set.
331///
332/// Entries have depth zero so they can be passed directly to [`walk_root_entries`] without
333/// querying their paths again. Directory-open errors are returned immediately; later enumeration
334/// errors are yielded by the iterator.
335#[cfg(any(windows, target_os = "macos"))]
336pub fn read_dir(
337    path: &Path,
338    options: Options,
339) -> io::Result<impl Iterator<Item = io::Result<Entry>>> {
340    ReadDir::open(Arc::from(path), 0, options)
341}
342
343/// Walk `root` without following symlinks.
344/// Unlike `walk_roots`, this yields entries directly for a single root and hides
345/// completion events.
346pub fn walk(
347    root: &Path,
348    threads: usize,
349    order: Order,
350    options: Options,
351    descend: impl Fn(&Entry) -> bool + Send + Sync + 'static,
352) -> Walk {
353    let root_path = root.to_owned();
354    let mut root = Entry::from_path(root, options);
355    if let Ok(entry) = &mut root
356        && entry.file_type.is_dir()
357    {
358        entry.directory_id = Some(DirectoryId::new(0));
359    }
360    let pool = match &root {
361        Ok(entry) if entry.file_type.is_dir() && descend(entry) => {
362            let path = Arc::from(entry.path());
363            let pool = start_pool(
364                threads.max(1),
365                HashMap::from([(0, AtomicUsize::new(0))]),
366                order,
367                Arc::new(move |_, entry| descend(entry)),
368                options,
369                1,
370            );
371            start_jobs(
372                &pool,
373                vec![Job::ReadDir {
374                    root_idx: 0,
375                    path,
376                    directory_id: 0,
377                    entry_depth: 1,
378                }],
379            );
380            Some(pool)
381        }
382        _ => None,
383    };
384    Walk {
385        next: vec![root],
386        pool,
387        root: root_path,
388        options,
389        finished: false,
390    }
391}
392
393impl Walk {
394    /// Restart an exhausted directory walk while retaining its worker threads.
395    ///
396    /// Returns `false` if the walk is still active or did not start a worker pool.
397    #[must_use]
398    pub fn restart(&mut self) -> bool {
399        if !self.finished || !self.next.is_empty() {
400            return false;
401        }
402        let Some(pool) = self.pool.as_ref() else {
403            return false;
404        };
405        let mut root = Entry::from_path(&self.root, self.options);
406        if let Ok(entry) = &mut root
407            && entry.file_type.is_dir()
408        {
409            entry.directory_id = Some(DirectoryId::new(0));
410        }
411        let job = match &root {
412            Ok(entry) if entry.file_type.is_dir() && (pool.shared.descend)(0, entry) => {
413                Some(Job::ReadDir {
414                    root_idx: 0,
415                    path: Arc::from(entry.path()),
416                    directory_id: 0,
417                    entry_depth: 1,
418                })
419            }
420            _ => None,
421        };
422        self.next.push(root);
423        if let Some(job) = job {
424            pool.shared
425                .next_directory_id
426                .store(1, AtomicOrdering::Relaxed);
427            self.finished = false;
428            start_jobs(pool, vec![job]);
429        }
430        true
431    }
432}
433
434impl Iterator for Walk {
435    type Item = io::Result<Entry>;
436
437    fn next(&mut self) -> Option<Self::Item> {
438        loop {
439            if let Some(entry) = self.next.pop() {
440                return Some(entry);
441            }
442            if self.finished {
443                return None;
444            }
445
446            match self.pool.as_ref()?.events.recv() {
447                Ok(Event::Batch {
448                    batch: Ok(entries), ..
449                }) => {
450                    self.next.extend(entries.into_iter().rev());
451                }
452                Ok(Event::Batch {
453                    batch: Err(err), ..
454                }) => return Some(Err(err)),
455                Ok(Event::RootFinished { .. }) => {}
456                Ok(Event::Finished) => {
457                    self.finished = true;
458                    return None;
459                }
460                Err(_) => return Some(Err(io::Error::other("directory worker stopped"))),
461            }
462        }
463    }
464}
465
466/// Walk multiple indexed roots without following symlinks.
467/// Unlike [`walk`], this preserves each root index and yields its completion as a [`RootEvent`].
468///
469/// Each item in `roots` is `(root_index, path)`. `root_index` is a caller-chosen identifier passed
470/// to `descend` and returned with every [`RootEvent`] for that root, unique per root path.
471///
472/// # Panics
473///
474/// Panics if two roots have the same index.
475pub fn walk_roots(
476    roots: impl IntoIterator<Item = (usize, PathBuf)>,
477    threads: usize,
478    order: Order,
479    options: Options,
480    descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
481) -> RootWalk {
482    start_root_walk(
483        roots.into_iter().collect(),
484        threads,
485        order,
486        descend,
487        |path: PathBuf| Entry::from_path(&path, options),
488        options,
489    )
490}
491
492/// Walk multiple indexed roots whose entries and metadata have already been collected.
493///
494/// Unlike [`walk_roots`], this reuses each supplied entry without querying its path again. Entry
495/// errors are yielded for their corresponding root, and each root retains its index and completion
496/// event just as it does with [`walk_roots`]. Supplied entries are re-rooted at depth zero before
497/// the predicate runs, and their descendants start at depth one.
498/// The supplied entries retain their metadata; `options` controls metadata for descendants.
499///
500/// # Panics
501///
502/// Panics if two roots have the same index.
503pub fn walk_root_entries(
504    roots: impl IntoIterator<Item = (usize, io::Result<Entry>)>,
505    threads: usize,
506    order: Order,
507    options: Options,
508    descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
509) -> RootWalk {
510    start_root_walk(
511        roots.into_iter().collect(),
512        threads,
513        order,
514        descend,
515        std::convert::identity,
516        options,
517    )
518}
519
520fn start_root_walk<Root>(
521    roots: Vec<(usize, Root)>,
522    threads: usize,
523    order: Order,
524    descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
525    prepare: impl Fn(Root) -> io::Result<Entry>,
526    options: Options,
527) -> RootWalk {
528    let jobs_per_root = roots
529        .iter()
530        .map(|(root_idx, _)| (*root_idx, AtomicUsize::new(0)))
531        .collect::<HashMap<_, _>>();
532    assert_eq!(
533        jobs_per_root.len(),
534        roots.len(),
535        "root indices must be unique"
536    );
537    let descend = Arc::new(descend);
538    let (next, root_jobs, next_directory_id) = begin_walks(
539        roots
540            .into_iter()
541            .map(|(root_idx, root)| (root_idx, prepare(root))),
542        descend.as_ref(),
543    );
544    let pool = if root_jobs.is_empty() {
545        None
546    } else {
547        let pool = start_pool(
548            threads.max(1),
549            jobs_per_root,
550            order,
551            descend,
552            options,
553            next_directory_id,
554        );
555        start_jobs(&pool, root_jobs);
556        Some(pool)
557    };
558    RootWalk { next, pool }
559}
560
561impl Iterator for RootWalk {
562    type Item = (usize, RootEvent);
563
564    fn next(&mut self) -> Option<Self::Item> {
565        loop {
566            if let Some(entry) = self.next.pop() {
567                return Some(entry);
568            }
569            match self.pool.as_ref()?.events.recv() {
570                Ok(Event::Batch {
571                    root_idx,
572                    batch: Ok(entries),
573                }) => self.next.extend(
574                    entries
575                        .into_iter()
576                        .rev()
577                        .map(|entry| (root_idx, RootEvent::Entry(entry))),
578                ),
579                Ok(Event::Batch {
580                    root_idx,
581                    batch: Err(err),
582                }) => return Some((root_idx, RootEvent::Entry(Err(err)))),
583                Ok(Event::RootFinished { root_idx }) => {
584                    return Some((root_idx, RootEvent::Finished));
585                }
586                Ok(Event::Finished) => {
587                    self.pool = None;
588                    return None;
589                }
590                Err(_) => {
591                    return Some((
592                        0,
593                        RootEvent::Entry(Err(io::Error::other("directory worker stopped"))),
594                    ));
595                }
596            }
597        }
598    }
599}
600
601impl PoolShared {
602    /// Wake one worker that has announced it is idle.
603    fn wake_worker(&self) {
604        let len = self.idle.len();
605        // This cursor only distributes scan starting points, so relaxed races affect fairness, not
606        // correctness; the compare-exchange below exclusively claims the worker to wake.
607        let start = self.next_wake.fetch_add(1, AtomicOrdering::Relaxed) % len;
608        for offset in 0..len {
609            let idx = (start + offset) % len;
610            if self.idle[idx]
611                .compare_exchange(true, false, AtomicOrdering::AcqRel, AtomicOrdering::Relaxed)
612                .is_ok()
613            {
614                self.unparkers[idx].unpark();
615                break;
616            }
617        }
618    }
619
620    /// Wake all threads unconditionally.
621    fn wake_workers(&self) {
622        for unparker in &self.unparkers {
623            unparker.unpark();
624        }
625    }
626}
627
628#[cfg(not(any(windows, target_os = "macos")))]
629impl Entry {
630    /// Return the full path to this entry.
631    #[must_use]
632    pub fn path(&self) -> PathBuf {
633        self.parent_path.join(&self.file_name)
634    }
635
636    /// Create an entry from a filesystem path.
637    pub fn from_path(path: &Path, options: Options) -> io::Result<Self> {
638        let metadata = fs::symlink_metadata(path)?;
639        Ok(Self {
640            depth: 0,
641            file_name: path.file_name().unwrap_or(path.as_os_str()).to_owned(),
642            file_type: metadata.file_type(),
643            metadata: (!options.skip_metadata).then_some(Ok(metadata)),
644            parent_path: Arc::from(path.parent().unwrap_or(Path::new(""))),
645            directory_id: None,
646            parent_directory_id: None,
647        })
648    }
649
650    fn from_dir_entry(
651        depth: usize,
652        parent_path: Arc<Path>,
653        entry: fs::DirEntry,
654        options: Options,
655    ) -> io::Result<Self> {
656        Ok(Self {
657            depth,
658            file_name: entry.file_name(),
659            file_type: entry.file_type()?,
660            metadata: (!options.skip_metadata).then(|| entry.metadata()),
661            parent_path,
662            directory_id: None,
663            parent_directory_id: None,
664        })
665    }
666}
667
668fn start_pool(
669    threads: usize,
670    jobs_per_root: HashMap<usize, AtomicUsize>,
671    order: Order,
672    descend: Arc<Descend>,
673    options: Options,
674    next_directory_id: usize,
675) -> Pool {
676    let workers: Vec<_> = (0..threads).map(|_| Worker::new_lifo()).collect();
677    let parkers: Vec<_> = (0..threads).map(|_| Parker::new()).collect();
678    let (event_tx, event_rx) = sync_channel(threads * 2);
679    let shared = Arc::new(PoolShared {
680        injector: Injector::new(),
681        stealers: workers.iter().map(Worker::stealer).collect(),
682        stop: AtomicBool::new(false),
683        descend,
684        events: event_tx,
685        active_roots: AtomicUsize::new(0),
686        jobs_per_root,
687        order,
688        options,
689        unparkers: parkers
690            .iter()
691            .map(|parker| parker.unparker().clone())
692            .collect(),
693        idle: (0..threads).map(|_| AtomicBool::new(false)).collect(),
694        next_wake: AtomicUsize::new(0),
695        next_directory_id: AtomicUsize::new(next_directory_id),
696    });
697    let handles: Vec<_> = workers
698        .into_iter()
699        .zip(parkers)
700        .enumerate()
701        .map(|(idx, (worker, parker))| {
702            let shared = Arc::clone(&shared);
703            thread::Builder::new()
704                .name(format!("dua-fs-walk-{idx}"))
705                .spawn(move || worker_loop(idx, worker, parker, shared))
706                .expect("filesystem worker thread can be spawned")
707        })
708        .collect();
709
710    Pool {
711        shared,
712        events: event_rx,
713        handles,
714    }
715}
716
717/// Prepare initial root events and directory jobs.
718/// Returns events in stack order for [`RootWalk::next`] to pop, plus jobs requiring a worker pool.
719fn begin_walks(
720    roots: impl IntoIterator<Item = (usize, io::Result<Entry>)>,
721    descend: &Descend,
722) -> (Vec<(usize, RootEvent)>, Vec<Job>, usize) {
723    let mut next = Vec::new();
724    let mut jobs = Vec::new();
725    let mut next_directory_id = 0;
726    for (root_idx, mut entry) in roots {
727        if let Ok(entry) = &mut entry {
728            entry.depth = 0;
729            entry.parent_directory_id = None;
730            if entry.file_type.is_dir() {
731                entry.directory_id = Some(DirectoryId::new(next_directory_id));
732                next_directory_id += 1;
733            } else {
734                entry.directory_id = None;
735            }
736        }
737        let has_job = if let Ok(entry) = &entry
738            && entry.metadata.as_ref().is_none_or(Result::is_ok)
739            && entry.file_type.is_dir()
740            && descend(root_idx, entry)
741        {
742            let directory_id = entry
743                .directory_id
744                .expect("directory roots receive an identifier");
745            jobs.push(Job::ReadDir {
746                root_idx,
747                path: Arc::from(entry.path()),
748                directory_id: directory_id.index(),
749                entry_depth: 1,
750            });
751            true
752        } else {
753            false
754        };
755        next.push((root_idx, RootEvent::Entry(entry)));
756        if !has_job {
757            next.push((root_idx, RootEvent::Finished));
758        }
759    }
760    next.reverse();
761    (next, jobs, next_directory_id)
762}
763
764/// Seed an idle pool with one initial job per active root.
765/// Initializes per-root completion accounting, queues the jobs, and wakes workers to process them.
766fn start_jobs(pool: &Pool, root_jobs: Vec<Job>) {
767    let wake_all = root_jobs.len() > 1;
768    debug_assert_eq!(
769        pool.shared.active_roots.load(AtomicOrdering::Relaxed),
770        0,
771        "initial jobs must be started on an idle pool"
772    );
773    debug_assert!(
774        root_jobs.iter().all(|j| match j {
775            Job::ReadDir { entry_depth, .. } => *entry_depth,
776            #[cfg(not(any(windows, target_os = "macos")))]
777            Job::StatCompletion { entry_depth, .. } => *entry_depth,
778        } == 1),
779        "the first jobs should be root jobs, so active_root counts match"
780    );
781    pool.shared
782        .active_roots
783        .store(root_jobs.len(), AtomicOrdering::Relaxed);
784    for job in &root_jobs {
785        add_pending(job.root_idx(), 1, &pool.shared);
786    }
787    for job in root_jobs {
788        pool.shared.injector.push(job);
789    }
790    if wake_all {
791        pool.shared.wake_workers();
792    } else {
793        pool.shared.wake_worker();
794    }
795}
796
797fn worker_loop(idx: usize, worker: Worker<Job>, parker: Parker, shared: Arc<PoolShared>) {
798    while !shared.stop.load(AtomicOrdering::Relaxed) {
799        let found = if let Some(found) = find_job(&worker, &shared) {
800            found
801        } else {
802            shared.idle[idx].store(true, AtomicOrdering::Release);
803            let Some(found) = find_job(&worker, &shared) else {
804                parker.park();
805                shared.idle[idx].store(false, AtomicOrdering::Release);
806                continue;
807            };
808            shared.idle[idx].store(false, AtomicOrdering::Release);
809            found
810        };
811        let (job, stolen) = found;
812        if stolen {
813            // A successful steal proves peer work is available; wake one more worker so
814            // concurrency ramps up only while work remains stealable.
815            shared.wake_worker();
816        }
817        run_job(job, &worker, &shared);
818    }
819}
820
821impl Drop for Pool {
822    fn drop(&mut self) {
823        self.shared.stop.store(true, AtomicOrdering::Relaxed);
824        self.shared.wake_workers();
825        for handle in self.handles.drain(..) {
826            handle.join().ok();
827        }
828    }
829}
830
831/// Find work in order of increasing synchronization cost.
832///
833/// The worker checks its own LIFO queue first, favoring locality and avoiding
834/// shared-queue contention. It next takes a batch from the injector, keeping one job and moving
835/// the rest into its local queue. Only then does it inspect other workers, because stealing from a
836/// peer is the most contentious path. Consequently, a worker with local jobs keeps processing
837/// them before helping elsewhere, and injector jobs take priority over peer jobs.
838///
839/// Returns the selected job and whether it was stolen from another worker; the caller uses a
840/// successful steal to wake another idle worker. Returns `None` when a full scan finds no work.
841fn find_job(worker: &Worker<Job>, shared: &PoolShared) -> Option<(Job, bool)> {
842    loop {
843        if let Some(job) = worker.pop() {
844            return Some((job, false));
845        }
846
847        match shared.injector.steal_batch_and_pop(worker) {
848            Steal::Success(job) => return Some((job, false)),
849            Steal::Retry => continue,
850            Steal::Empty => {}
851        }
852
853        let mut retry = false;
854        for stealer in &shared.stealers {
855            match stealer.steal() {
856                Steal::Success(job) => return Some((job, true)),
857                Steal::Retry => retry = true,
858                Steal::Empty => {}
859            }
860        }
861        if !retry {
862            return None;
863        }
864    }
865}
866
867fn run_job(job: Job, worker: &Worker<Job>, shared: &PoolShared) {
868    match job {
869        Job::ReadDir {
870            root_idx: root,
871            path,
872            directory_id,
873            entry_depth,
874        } => {
875            if matches!(shared.order, Order::Completion) {
876                read_dir_completion(root, path, directory_id, entry_depth, worker, shared);
877            } else {
878                read_dir_parent_first(root, path, directory_id, entry_depth, worker, shared);
879            }
880        }
881        #[cfg(not(any(windows, target_os = "macos")))]
882        Job::StatCompletion {
883            root_idx: root,
884            path,
885            directory_id,
886            entry_depth,
887            entries,
888        } => stat_entries_completion(
889            root,
890            path,
891            directory_id,
892            entry_depth,
893            entries,
894            worker,
895            shared,
896        ),
897    }
898}
899
900/// Read a directory for completion-order traversal.
901/// Successful directory entries are split into stealable metadata jobs, while enumeration errors
902/// are emitted directly; the directory-read job completes after all chunks are queued.
903/// This adds parallelism within wide directories when metadata calls dominate. Both traversal
904/// orders already process separate directories concurrently, so typical trees may see no speedup.
905/// Type-only walks convert entries inline instead, avoiding metadata-job overhead.
906#[cfg(not(any(windows, target_os = "macos")))]
907fn read_dir_completion(
908    root_idx: usize,
909    path: Arc<Path>,
910    directory_id: usize,
911    entry_depth: usize,
912    worker: &Worker<Job>,
913    shared: &PoolShared,
914) {
915    if shared.options.skip_metadata {
916        read_dir_inline(root_idx, path, directory_id, entry_depth, worker, shared);
917        return;
918    }
919    let dir_entries = match fs::read_dir(&path) {
920        Ok(entries) => entries,
921        Err(err) => {
922            if shared
923                .events
924                .send(Event::Batch {
925                    root_idx,
926                    batch: Err(err),
927                })
928                .is_err()
929            {
930                shared.stop.store(true, AtomicOrdering::Relaxed);
931            }
932            finish_pending(root_idx, shared);
933            return;
934        }
935    };
936    let mut chunk = Vec::with_capacity(ENTRY_CHUNK_SIZE);
937    let mut errors = Vec::new();
938    let mut has_jobs = false;
939    for entry in dir_entries {
940        match entry {
941            Ok(entry) => {
942                chunk.push(entry);
943                if chunk.len() == ENTRY_CHUNK_SIZE {
944                    add_pending(root_idx, 1, shared);
945                    worker.push(Job::StatCompletion {
946                        root_idx,
947                        path: Arc::clone(&path),
948                        directory_id,
949                        entry_depth,
950                        entries: std::mem::replace(
951                            &mut chunk,
952                            Vec::with_capacity(ENTRY_CHUNK_SIZE),
953                        ),
954                    });
955                    has_jobs = true;
956                }
957            }
958            Err(err) => errors.push(Err(err)),
959        }
960    }
961    if !chunk.is_empty() {
962        add_pending(root_idx, 1, shared);
963        worker.push(Job::StatCompletion {
964            root_idx,
965            path,
966            directory_id,
967            entry_depth,
968            entries: chunk,
969        });
970        has_jobs = true;
971    }
972    if has_jobs {
973        shared.wake_worker();
974    }
975    if !errors.is_empty()
976        && shared
977            .events
978            .send(Event::Batch {
979                root_idx,
980                batch: Ok(errors),
981            })
982            .is_err()
983    {
984        shared.stop.store(true, AtomicOrdering::Relaxed);
985    }
986    finish_pending(root_idx, shared);
987}
988
989/// Read a directory for completion-order traversal.
990///
991/// Unlike the generic implementation, native readers collect metadata while enumerating,
992/// so complete entries are published directly in chunks instead of being split into stealable
993/// metadata jobs. This streams wide directories but keeps their metadata work on one worker.
994#[cfg(any(windows, target_os = "macos"))]
995fn read_dir_completion(
996    root_idx: usize,
997    path: Arc<Path>,
998    directory_id: usize,
999    depth: usize,
1000    worker: &Worker<Job>,
1001    shared: &PoolShared,
1002) {
1003    let dir_entries = match ReadDir::open(path, depth, shared.options) {
1004        Ok(entries) => entries,
1005        Err(err) => {
1006            if shared
1007                .events
1008                .send(Event::Batch {
1009                    root_idx,
1010                    batch: Err(err),
1011                })
1012                .is_err()
1013            {
1014                shared.stop.store(true, AtomicOrdering::Relaxed);
1015            }
1016            finish_pending(root_idx, shared);
1017            return;
1018        }
1019    };
1020    let mut entries = Vec::with_capacity(ENTRY_CHUNK_SIZE);
1021    let mut jobs = Vec::new();
1022    for mut entry in dir_entries {
1023        if let Ok(entry) = &mut entry {
1024            assign_directory_ids(entry, directory_id, shared);
1025        }
1026        if let Ok(entry) = &entry
1027            && entry.file_type.is_dir()
1028            && (shared.descend)(root_idx, entry)
1029        {
1030            jobs.push(Job::ReadDir {
1031                root_idx,
1032                path: Arc::from(entry.path()),
1033                directory_id: entry
1034                    .directory_id
1035                    .expect("directories receive an identifier")
1036                    .index(),
1037                entry_depth: depth + 1,
1038            });
1039        }
1040        entries.push(entry);
1041        if entries.len() == ENTRY_CHUNK_SIZE
1042            && !publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared)
1043        {
1044            finish_pending(root_idx, shared);
1045            return;
1046        }
1047    }
1048    if !entries.is_empty() {
1049        publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared);
1050    }
1051    finish_pending(root_idx, shared);
1052}
1053
1054#[cfg(any(windows, target_os = "macos"))]
1055fn publish_completion_batch(
1056    root_idx: usize,
1057    entries: &mut Vec<io::Result<Entry>>,
1058    jobs: &mut Vec<Job>,
1059    worker: &Worker<Job>,
1060    shared: &PoolShared,
1061) -> bool {
1062    add_pending(root_idx, jobs.len(), shared);
1063    schedule_jobs(std::mem::take(jobs), worker, shared);
1064    if shared
1065        .events
1066        .send(Event::Batch {
1067            root_idx,
1068            batch: Ok(std::mem::replace(
1069                entries,
1070                Vec::with_capacity(ENTRY_CHUNK_SIZE),
1071            )),
1072        })
1073        .is_err()
1074    {
1075        shared.stop.store(true, AtomicOrdering::Relaxed);
1076        false
1077    } else {
1078        true
1079    }
1080}
1081
1082#[cfg(any(windows, target_os = "macos"))]
1083fn read_dir_parent_first(
1084    root_idx: usize,
1085    path: Arc<Path>,
1086    directory_id: usize,
1087    depth: usize,
1088    worker: &Worker<Job>,
1089    shared: &PoolShared,
1090) {
1091    let dir_entries = match ReadDir::open(path, depth, shared.options) {
1092        Ok(entries) => entries,
1093        Err(err) => {
1094            finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
1095            return;
1096        }
1097    };
1098    let mut jobs = Vec::new();
1099    let entries = dir_entries
1100        .map(|entry| {
1101            entry.map(|mut entry| {
1102                assign_directory_ids(&mut entry, directory_id, shared);
1103                if entry.file_type.is_dir() && (shared.descend)(root_idx, &entry) {
1104                    jobs.push(Job::ReadDir {
1105                        root_idx,
1106                        path: Arc::from(entry.path()),
1107                        directory_id: entry
1108                            .directory_id
1109                            .expect("directories receive an identifier")
1110                            .index(),
1111                        entry_depth: depth + 1,
1112                    });
1113                }
1114                entry
1115            })
1116        })
1117        .collect();
1118    finish_directory(root_idx, Ok(entries), jobs, worker, shared);
1119}
1120
1121#[cfg(not(any(windows, target_os = "macos")))]
1122fn stat_entries_completion(
1123    root_idx: usize,
1124    path: Arc<Path>,
1125    directory_id: usize,
1126    depth: usize,
1127    entries: Vec<fs::DirEntry>,
1128    worker: &Worker<Job>,
1129    shared: &PoolShared,
1130) {
1131    let mut jobs = Vec::new();
1132    let entries = entries
1133        .into_iter()
1134        .map(|entry| {
1135            Entry::from_dir_entry(depth, Arc::clone(&path), entry, shared.options).map(
1136                |mut entry| {
1137                    assign_directory_ids(&mut entry, directory_id, shared);
1138                    if entry.file_type.is_dir() && (shared.descend)(root_idx, &entry) {
1139                        jobs.push(Job::ReadDir {
1140                            root_idx,
1141                            path: Arc::from(entry.path()),
1142                            directory_id: entry
1143                                .directory_id
1144                                .expect("directories receive an identifier")
1145                                .index(),
1146                            entry_depth: entry.depth + 1,
1147                        });
1148                    }
1149                    entry
1150                },
1151            )
1152        })
1153        .collect();
1154    add_pending(root_idx, jobs.len(), shared);
1155    schedule_jobs(jobs, worker, shared);
1156    if shared
1157        .events
1158        .send(Event::Batch {
1159            root_idx,
1160            batch: Ok(entries),
1161        })
1162        .is_err()
1163    {
1164        shared.stop.store(true, AtomicOrdering::Relaxed);
1165    }
1166    finish_pending(root_idx, shared);
1167}
1168
1169/// Read a directory for parent-first traversal.
1170/// Entries are converted inline rather than scheduled as `StatCompletion` jobs, producing the
1171/// complete parent batch and its child-directory jobs together. This lets `finish_directory` send
1172/// the parent batch before making any child job available, preserving parent-before-descendant
1173/// order. Metadata within one directory is serial, although separate directories still run in
1174/// parallel; this often matches completion-order performance unless wide-directory metadata is the
1175/// bottleneck.
1176#[cfg(not(any(windows, target_os = "macos")))]
1177fn read_dir_parent_first(
1178    root_idx: usize,
1179    path: Arc<Path>,
1180    directory_id: usize,
1181    depth: usize,
1182    worker: &Worker<Job>,
1183    shared: &PoolShared,
1184) {
1185    read_dir_inline(root_idx, path, directory_id, depth, worker, shared);
1186}
1187
1188/// Convert a directory's entries on the worker that enumerates it, then schedule its children.
1189///
1190/// Parent-first traversal converts each entry inline to preserve ordering.
1191#[cfg(not(any(windows, target_os = "macos")))]
1192fn read_dir_inline(
1193    root_idx: usize,
1194    path: Arc<Path>,
1195    directory_id: usize,
1196    depth: usize,
1197    worker: &Worker<Job>,
1198    shared: &PoolShared,
1199) {
1200    let dir_entries = match fs::read_dir(&path) {
1201        Ok(entries) => entries,
1202        Err(err) => {
1203            finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
1204            return;
1205        }
1206    };
1207    let mut jobs = Vec::new();
1208    let entries = dir_entries
1209        .map(|entry| {
1210            entry
1211                .and_then(|entry| {
1212                    Entry::from_dir_entry(depth, Arc::clone(&path), entry, shared.options)
1213                })
1214                .map(|mut entry| {
1215                    assign_directory_ids(&mut entry, directory_id, shared);
1216                    if entry.file_type.is_dir() && (shared.descend)(root_idx, &entry) {
1217                        jobs.push(Job::ReadDir {
1218                            root_idx,
1219                            path: Arc::from(entry.path()),
1220                            directory_id: entry
1221                                .directory_id
1222                                .expect("directories receive an identifier")
1223                                .index(),
1224                            entry_depth: depth + 1,
1225                        });
1226                    }
1227                    entry
1228                })
1229        })
1230        .collect();
1231    finish_directory(root_idx, Ok(entries), jobs, worker, shared);
1232}
1233
1234fn assign_directory_ids(entry: &mut Entry, parent_directory_id: usize, shared: &PoolShared) {
1235    entry.parent_directory_id = Some(DirectoryId::new(parent_directory_id));
1236    entry.directory_id = entry.file_type.is_dir().then(|| {
1237        DirectoryId::new(
1238            shared
1239                .next_directory_id
1240                .fetch_update(AtomicOrdering::Relaxed, AtomicOrdering::Relaxed, |id| {
1241                    id.checked_add(1)
1242                })
1243                .expect("directory identifier overflow"),
1244        )
1245    });
1246}
1247
1248/// Publish a completed directory read and schedule its accepted child-directory jobs.
1249/// `ParentFirst` sends the batch before exposing child jobs; `Completion` exposes child jobs first.
1250/// Child jobs are counted before either action, and the current job is marked complete afterward.
1251fn finish_directory(
1252    root_idx: usize,
1253    batch: Batch,
1254    jobs: Vec<Job>,
1255    worker: &Worker<Job>,
1256    shared: &PoolShared,
1257) {
1258    add_pending(root_idx, jobs.len(), shared);
1259
1260    match shared.order {
1261        Order::ParentFirst => {
1262            if shared
1263                .events
1264                .send(Event::Batch { root_idx, batch })
1265                .is_err()
1266            {
1267                shared.stop.store(true, AtomicOrdering::Relaxed);
1268                return;
1269            }
1270            schedule_jobs(jobs, worker, shared);
1271        }
1272        Order::Completion => {
1273            schedule_jobs(jobs, worker, shared);
1274            if shared
1275                .events
1276                .send(Event::Batch { root_idx, batch })
1277                .is_err()
1278            {
1279                shared.stop.store(true, AtomicOrdering::Relaxed);
1280                return;
1281            }
1282        }
1283    }
1284
1285    finish_pending(root_idx, shared);
1286}
1287
1288fn add_pending(root: usize, count: usize, shared: &PoolShared) {
1289    shared.jobs_per_root[&root].fetch_add(count, AtomicOrdering::Relaxed);
1290}
1291
1292/// Mark one job complete for `root`.
1293/// The last job emits `RootFinished`; if this was also the last active root, `Finished` follows.
1294fn finish_pending(root_idx: usize, shared: &PoolShared) {
1295    if shared.jobs_per_root[&root_idx].fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
1296        shared.events.send(Event::RootFinished { root_idx }).ok();
1297        if shared.active_roots.fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
1298            shared.events.send(Event::Finished).ok();
1299        }
1300    }
1301}
1302
1303fn schedule_jobs(jobs: Vec<Job>, worker: &Worker<Job>, shared: &PoolShared) {
1304    let has_jobs = !jobs.is_empty();
1305    for job in jobs {
1306        worker.push(job);
1307    }
1308    if has_jobs {
1309        shared.wake_worker();
1310    }
1311}
1312
1313#[cfg(test)]
1314mod tests {
1315    use super::*;
1316
1317    #[test]
1318    fn directory_ids_are_compact_dense_and_match_parents() {
1319        assert_eq!(size_of::<Option<DirectoryId>>(), 4);
1320
1321        let dir = tempfile::tempdir().unwrap();
1322        fs::create_dir_all(dir.path().join("a/child")).unwrap();
1323        fs::create_dir(dir.path().join("b")).unwrap();
1324        fs::write(dir.path().join("a/file"), b"x").unwrap();
1325        fs::write(dir.path().join("file"), b"x").unwrap();
1326
1327        let entries = walk(
1328            dir.path(),
1329            4,
1330            Order::ParentFirst,
1331            Options::default(),
1332            |_| true,
1333        )
1334        .map(Result::unwrap)
1335        .collect::<Vec<_>>();
1336        let directories = entries
1337            .iter()
1338            .filter_map(|entry| entry.directory_id.map(|id| (entry.path(), id)))
1339            .collect::<HashMap<_, _>>();
1340        let mut ids = directories
1341            .values()
1342            .map(|id| id.index())
1343            .collect::<Vec<_>>();
1344        ids.sort_unstable();
1345        assert_eq!(ids, (0..directories.len()).collect::<Vec<_>>());
1346
1347        for entry in entries {
1348            assert_eq!(entry.directory_id.is_some(), entry.file_type.is_dir());
1349            if entry.depth == 0 {
1350                assert_eq!(entry.parent_directory_id, None);
1351            } else {
1352                assert_eq!(
1353                    entry.parent_directory_id,
1354                    directories.get(entry.path().parent().unwrap()).copied()
1355                );
1356            }
1357        }
1358    }
1359
1360    #[test]
1361    fn parallel_walk_is_parent_first_and_does_not_follow_symlinks() {
1362        let dir = tempfile::tempdir().unwrap();
1363        fs::create_dir_all(dir.path().join("b/child")).unwrap();
1364        fs::create_dir(dir.path().join("a")).unwrap();
1365        fs::write(dir.path().join("b/child/file"), b"x").unwrap();
1366
1367        #[cfg(unix)]
1368        std::os::unix::fs::symlink(dir.path().join("b"), dir.path().join("link")).unwrap();
1369
1370        #[cfg(unix)]
1371        let expected = ["", "a", "b", "b/child", "b/child/file", "link"];
1372        #[cfg(not(unix))]
1373        let expected = ["", "a", "b", "b/child", "b/child/file"];
1374        let expected = expected.into_iter().map(PathBuf::from).collect::<Vec<_>>();
1375
1376        for threads in [1, 4] {
1377            let paths = walk(
1378                dir.path(),
1379                threads,
1380                Order::ParentFirst,
1381                Options::default(),
1382                |_| true,
1383            )
1384            .map(|entry| {
1385                entry
1386                    .unwrap()
1387                    .path()
1388                    .strip_prefix(dir.path())
1389                    .unwrap()
1390                    .to_owned()
1391            })
1392            .collect::<Vec<_>>();
1393            let mut sorted_paths = paths.clone();
1394            sorted_paths.sort();
1395            assert_eq!(
1396                sorted_paths, expected,
1397                "walk with {threads} threads should visit every expected path exactly once"
1398            );
1399
1400            for path in paths.iter().filter(|path| path.components().count() > 1) {
1401                let parent = path.parent().unwrap();
1402                assert!(
1403                    paths.iter().position(|path| path == parent)
1404                        < paths.iter().position(|candidate| candidate == path),
1405                    "parent {parent:?} should precede child {path:?} with {threads} threads; \
1406                     traversal order: {paths:?}"
1407                );
1408            }
1409        }
1410    }
1411
1412    #[test]
1413    fn a_completed_walk_can_reuse_its_workers() {
1414        let dir = tempfile::tempdir().unwrap();
1415        fs::create_dir_all(dir.path().join("first/child")).unwrap();
1416
1417        let mut walk = walk(dir.path(), 2, Order::Completion, Options::default(), |_| {
1418            true
1419        });
1420        assert!(!walk.restart(), "an active walk cannot be restarted");
1421        let worker_ids = walk
1422            .pool
1423            .as_ref()
1424            .unwrap()
1425            .handles
1426            .iter()
1427            .map(|handle| handle.thread().id())
1428            .collect::<Vec<_>>();
1429        walk.by_ref().for_each(drop);
1430        assert!(walk.next().is_none(), "a completed walk stays exhausted");
1431
1432        fs::create_dir_all(dir.path().join("second/child")).unwrap();
1433        assert!(walk.restart());
1434        let paths = walk
1435            .by_ref()
1436            .map(|entry| {
1437                entry
1438                    .unwrap()
1439                    .path()
1440                    .strip_prefix(dir.path())
1441                    .unwrap()
1442                    .into()
1443            })
1444            .collect::<Vec<PathBuf>>();
1445
1446        assert!(paths.contains(&PathBuf::from("second/child")));
1447        assert_eq!(
1448            walk.pool
1449                .as_ref()
1450                .unwrap()
1451                .handles
1452                .iter()
1453                .map(|handle| handle.thread().id())
1454                .collect::<Vec<_>>(),
1455            worker_ids,
1456            "a restarted walk keeps its original worker threads"
1457        );
1458    }
1459
1460    #[test]
1461    fn pruning_keeps_the_directory_and_missing_roots_are_errors() {
1462        let dir = tempfile::tempdir().unwrap();
1463        fs::create_dir_all(dir.path().join("skip/child")).unwrap();
1464
1465        let paths = walk(
1466            dir.path(),
1467            2,
1468            Order::Completion,
1469            Options::default(),
1470            |entry| entry.file_name != "skip",
1471        )
1472        .map(|entry| entry.unwrap().file_name)
1473        .collect::<Vec<_>>();
1474        assert_eq!(
1475            paths,
1476            vec![
1477                dir.path().file_name().unwrap().to_owned(),
1478                OsString::from("skip")
1479            ],
1480            "a pruned directory should be yielded without traversing its children"
1481        );
1482
1483        assert!(
1484            walk(
1485                &dir.path().join("missing"),
1486                2,
1487                Order::Completion,
1488                Options::default(),
1489                |_| true,
1490            )
1491            .next()
1492            .unwrap()
1493            .is_err(),
1494            "a missing root should be yielded as an I/O error"
1495        );
1496    }
1497
1498    #[test]
1499    fn concurrent_roots_keep_their_identity() {
1500        let dir = tempfile::tempdir().unwrap();
1501        let roots = [dir.path().join("a"), dir.path().join("b")];
1502        for root in &roots {
1503            fs::create_dir_all(root.join("child")).unwrap();
1504        }
1505
1506        let events = walk_roots(
1507            roots.iter().cloned().enumerate(),
1508            2,
1509            Order::Completion,
1510            Options::default(),
1511            |_, _| true,
1512        )
1513        .collect::<Vec<_>>();
1514        let mut paths = Vec::new();
1515        let mut last_entry = [0; 2];
1516        let mut finished = [None; 2];
1517        for (position, (root_idx, event)) in events.into_iter().enumerate() {
1518            match event {
1519                RootEvent::Entry(entry) => {
1520                    last_entry[root_idx] = position;
1521                    paths.push((
1522                        root_idx,
1523                        entry
1524                            .unwrap()
1525                            .path()
1526                            .strip_prefix(&roots[root_idx])
1527                            .unwrap()
1528                            .to_owned(),
1529                    ));
1530                }
1531                RootEvent::Finished => finished[root_idx] = Some(position),
1532            }
1533        }
1534        paths.sort();
1535        assert_eq!(
1536            paths,
1537            [
1538                (0, PathBuf::new()),
1539                (0, PathBuf::from("child")),
1540                (1, PathBuf::new()),
1541                (1, PathBuf::from("child")),
1542            ]
1543        );
1544        for root_idx in 0..roots.len() {
1545            assert!(
1546                last_entry[root_idx] < finished[root_idx].unwrap(),
1547                "root {root_idx} must finish after its last entry",
1548            );
1549        }
1550    }
1551
1552    #[test]
1553    fn prepared_roots_rebase_existing_descendants() {
1554        let directory = tempfile::tempdir().unwrap();
1555        let descendant = directory.path().join("descendant");
1556        fs::create_dir(&descendant).unwrap();
1557        let child = descendant.join("child");
1558        fs::write(&child, b"nested file").unwrap();
1559
1560        let descendant_entry = walk(
1561            directory.path(),
1562            2,
1563            Order::ParentFirst,
1564            Options::default(),
1565            |_| true,
1566        )
1567        .find_map(|entry| {
1568            let entry = entry.unwrap();
1569            (entry.path() == descendant).then_some(entry)
1570        })
1571        .expect("the initial walk should yield the descendant directory");
1572        assert_eq!(descendant_entry.depth, 1);
1573
1574        let mut events = walk_root_entries(
1575            [(7, Ok(descendant_entry))],
1576            2,
1577            Order::ParentFirst,
1578            Options::default(),
1579            |root_idx, entry| {
1580                assert_eq!(root_idx, 7);
1581                assert_eq!(entry.depth, 0, "the predicate should see a re-rooted entry");
1582                true
1583            },
1584        );
1585
1586        let Some((7, RootEvent::Entry(Ok(mut root)))) = events.next() else {
1587            panic!("the prepared descendant should be emitted as the new root");
1588        };
1589        assert_eq!(root.path(), descendant);
1590        assert_eq!(
1591            root.depth, 0,
1592            "the entry originally found at depth 1 must become the new traversal root"
1593        );
1594
1595        let Some((7, RootEvent::Entry(Ok(entry)))) = events.next() else {
1596            panic!("the re-rooted directory should emit its child");
1597        };
1598        assert_eq!(entry.path(), child);
1599        assert_eq!(
1600            entry.depth, 1,
1601            "the child depth must be relative to the prepared entry used as the new root"
1602        );
1603        assert_eq!(
1604            events
1605                .next()
1606                .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1607            Some((7, true))
1608        );
1609        assert_eq!(
1610            events.next().map(|(root_idx, _)| root_idx),
1611            None,
1612            "nothing left after the Finished event"
1613        );
1614
1615        root.metadata = Some(Err(io::Error::from(io::ErrorKind::PermissionDenied)));
1616        let mut events = walk_root_entries(
1617            [(7, Ok(root))],
1618            2,
1619            Order::ParentFirst,
1620            Options::default(),
1621            |_, _| panic!("a directory with inaccessible metadata must not be descended"),
1622        );
1623        let Some((7, RootEvent::Entry(Ok(root)))) = events.next() else {
1624            panic!("the prepared directory must retain its metadata error");
1625        };
1626        let error = root
1627            .metadata
1628            .unwrap()
1629            .err()
1630            .expect("the inaccessible root must retain its metadata error");
1631        assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
1632        assert_eq!(
1633            events
1634                .next()
1635                .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1636            Some((7, true))
1637        );
1638        assert_eq!(events.next().map(|(root_idx, _)| root_idx), None);
1639    }
1640
1641    #[cfg(any(windows, target_os = "macos"))]
1642    #[test]
1643    fn prepared_roots_reuse_native_directory_metadata() {
1644        let directory = tempfile::tempdir().unwrap();
1645        let path = directory.path().join("file");
1646        fs::write(&path, b"cached metadata").unwrap();
1647        let expected_len = fs::metadata(&path).unwrap().len();
1648
1649        let entry = read_dir(directory.path(), Options::default())
1650            .unwrap()
1651            .next()
1652            .unwrap()
1653            .unwrap();
1654        assert_eq!(entry.depth, 0);
1655        assert_eq!(entry.path(), path);
1656        fs::remove_file(&path).unwrap();
1657
1658        let mut events = walk_root_entries(
1659            [(7, Ok(entry))],
1660            1,
1661            Order::Completion,
1662            Options::default(),
1663            |_, _| true,
1664        );
1665        let Some((7, RootEvent::Entry(Ok(entry)))) = events.next() else {
1666            panic!(
1667                "prepared root must be yielded without querying its removed path which would fail"
1668            );
1669        };
1670        assert_eq!(entry.path(), path);
1671        assert_eq!(entry.metadata.unwrap().unwrap().len(), expected_len);
1672        assert_eq!(
1673            events
1674                .next()
1675                .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1676            Some((7, true))
1677        );
1678        assert_eq!(events.next().map(|(root_idx, _)| root_idx), None);
1679    }
1680
1681    #[test]
1682    fn wide_walk_wakes_multiple_idle_workers() {
1683        let dir = tempfile::tempdir().unwrap();
1684        for idx in 0..32 {
1685            fs::create_dir_all(dir.path().join(format!("{idx}/child"))).unwrap();
1686        }
1687
1688        let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1689        let seen_threads = Arc::clone(&worker_threads);
1690        walk(
1691            dir.path(),
1692            8,
1693            Order::Completion,
1694            Options::default(),
1695            move |entry| {
1696                if entry.depth == 1 {
1697                    thread::sleep(std::time::Duration::from_millis(1));
1698                } else if entry.depth == 2 {
1699                    seen_threads.lock().unwrap().insert(thread::current().id());
1700                    thread::sleep(std::time::Duration::from_millis(10));
1701                }
1702                true
1703            },
1704        )
1705        .for_each(drop);
1706
1707        assert!(
1708            worker_threads.lock().unwrap().len() >= 4,
1709            "a wide directory should engage more than the producer and one thief"
1710        );
1711    }
1712
1713    #[cfg(any(windows, target_os = "macos"))]
1714    #[test]
1715    fn native_metadata_is_collected_by_the_directory_worker() {
1716        let dir = tempfile::tempdir().unwrap();
1717        for idx in 0..32 {
1718            fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1719        }
1720
1721        let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1722        let seen_threads = Arc::clone(&worker_threads);
1723        walk(
1724            dir.path(),
1725            8,
1726            Order::Completion,
1727            Options::default(),
1728            move |entry| {
1729                if entry.depth == 1 {
1730                    seen_threads.lock().unwrap().insert(thread::current().id());
1731                    thread::sleep(std::time::Duration::from_millis(2));
1732                }
1733                true
1734            },
1735        )
1736        .for_each(drop);
1737
1738        assert_eq!(
1739            worker_threads.lock().unwrap().len(),
1740            1,
1741            "native directory-entry metadata should stay on the enumerating worker"
1742        );
1743    }
1744
1745    #[cfg(any(windows, target_os = "macos"))]
1746    #[test]
1747    fn native_completion_streams_metadata_before_enumeration_finishes() {
1748        let dir = tempfile::tempdir().unwrap();
1749        for idx in 0..=ENTRY_CHUNK_SIZE {
1750            fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1751        }
1752
1753        let (continue_tx, continue_rx) = std::sync::mpsc::sync_channel(0);
1754        let continue_rx = Arc::new(std::sync::Mutex::new(continue_rx));
1755        let seen = Arc::new(AtomicUsize::new(0));
1756        let seen_in_worker = Arc::clone(&seen);
1757        let mut entries =
1758            walk(
1759                dir.path(),
1760                2,
1761                Order::Completion,
1762                Options::default(),
1763                move |entry| {
1764                    if entry.depth == 1
1765                        && seen_in_worker.fetch_add(1, AtomicOrdering::Relaxed) == ENTRY_CHUNK_SIZE
1766                    {
1767                        continue_rx
1768                    .lock()
1769                    .unwrap()
1770                    .recv_timeout(std::time::Duration::from_secs(2))
1771                    .expect("the first metadata batch should arrive before enumeration finishes");
1772                    }
1773                    true
1774                },
1775            );
1776
1777        assert_eq!(
1778            entries.next().unwrap().unwrap().depth,
1779            0,
1780            "the root entry should be yielded first"
1781        );
1782        assert_eq!(
1783            entries.next().unwrap().unwrap().depth,
1784            1,
1785            "the first metadata batch should be yielded before enumeration resumes"
1786        );
1787        continue_tx.send(()).unwrap();
1788        entries.for_each(drop);
1789        assert_eq!(
1790            seen.load(AtomicOrdering::Relaxed),
1791            ENTRY_CHUNK_SIZE + 1,
1792            "all directory entries should be inspected"
1793        );
1794    }
1795}