1#![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
124type Descend = dyn Fn(usize, &Entry) -> bool + Send + Sync;
127type Batch = io::Result<Vec<io::Result<Entry>>>;
131const ENTRY_CHUNK_SIZE: usize = 4;
134
135#[derive(Clone, Copy)]
137pub enum Order {
138 Completion,
140 ParentFirst,
142}
143
144#[derive(Clone, Copy, Debug, Default)]
146pub struct Options {
147 pub skip_metadata: bool,
153 #[cfg(target_os = "macos")]
155 pub apfs_clone_metadata: bool,
156}
157
158impl Options {
159 #[must_use]
161 pub fn skip_metadata(mut self) -> Self {
162 self.skip_metadata = true;
163 self
164 }
165}
166
167#[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 #[must_use]
183 pub fn index(self) -> usize {
184 (self.0.get() - 1) as usize
185 }
186}
187
188#[cfg(not(any(windows, target_os = "macos")))]
190pub struct Entry {
191 pub depth: usize,
193 pub file_name: OsString,
195 pub file_type: FileType,
197 pub metadata: Option<io::Result<Metadata>>,
199 pub parent_path: Arc<Path>,
201 pub directory_id: Option<DirectoryId>,
203 pub parent_directory_id: Option<DirectoryId>,
205}
206
207enum Job {
208 ReadDir {
210 root_idx: usize,
211 path: Arc<Path>,
212 directory_id: usize,
214 entry_depth: usize,
217 },
218 #[cfg(not(any(windows, target_os = "macos")))]
220 StatCompletion {
221 root_idx: usize,
222 path: Arc<Path>,
223 directory_id: usize,
225 entry_depth: usize,
227 entries: Vec<fs::DirEntry>,
228 },
229}
230
231impl Job {
232 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
242enum Event {
244 Batch {
245 root_idx: usize,
246 batch: Batch,
247 },
248 RootFinished {
251 root_idx: usize,
252 },
253 Finished,
255}
256
257pub enum RootEvent {
263 Entry(io::Result<Entry>),
265 Finished,
267}
268
269struct PoolShared {
270 injector: Injector<Job>,
272 stealers: Vec<Stealer<Job>>,
273 stop: AtomicBool,
274 descend: Arc<Descend>,
275 events: SyncSender<Event>,
276 active_roots: AtomicUsize,
278 jobs_per_root: HashMap<usize, AtomicUsize>,
281 order: Order,
282 options: Options,
283 unparkers: Vec<Unparker>,
285 idle: Vec<AtomicBool>,
288 next_wake: AtomicUsize,
290 next_directory_id: AtomicUsize,
292}
293
294struct Pool {
295 shared: Arc<PoolShared>,
296 events: Receiver<Event>,
297 handles: Vec<thread::JoinHandle<()>>,
298}
299
300pub struct RootWalk {
303 next: Vec<(usize, RootEvent)>,
305 pool: Option<Pool>,
307}
308
309pub struct Walk {
312 next: Vec<io::Result<Entry>>,
319 pool: Option<Pool>,
323 root: PathBuf,
324 options: Options,
325 finished: bool,
327}
328
329#[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
343pub 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 #[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
466pub 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
492pub 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 fn wake_worker(&self) {
604 let len = self.idle.len();
605 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 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 #[must_use]
632 pub fn path(&self) -> PathBuf {
633 self.parent_path.join(&self.file_name)
634 }
635
636 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
717fn 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
764fn 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 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
831fn 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#[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#[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#[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#[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
1248fn 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
1292fn 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}