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 io,
33 path::{Path, PathBuf},
34 sync::{
35 Arc,
36 atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering},
37 mpsc::{Receiver, SyncSender, sync_channel},
38 },
39 thread,
40};
41
42#[cfg(any(not(any(windows, target_os = "macos")), test))]
43use std::{ffi::OsString, fs};
44
45#[cfg(not(any(windows, target_os = "macos")))]
46pub use std::fs::{FileType, Metadata};
47
48#[cfg(target_os = "macos")]
49#[allow(unsafe_code)]
50mod macos;
51
52#[cfg(windows)]
53#[allow(unsafe_code)]
54mod windows;
55
56#[cfg(target_os = "macos")]
57pub use macos::{Entry, FileType, Metadata};
58
59#[cfg(target_os = "macos")]
60use macos::ReadDir as NativeReadDir;
61
62#[cfg(windows)]
63pub use windows::{Entry, FileType, Metadata};
64
65#[cfg(windows)]
66use windows::ReadDir as NativeReadDir;
67
68type Descend = dyn Fn(usize, &Entry) -> bool + Send + Sync;
71type Batch = io::Result<Vec<io::Result<Entry>>>;
75const ENTRY_CHUNK_SIZE: usize = 4;
78
79#[derive(Clone, Copy)]
81pub enum Order {
82 Completion,
84 ParentFirst,
86}
87
88#[derive(Clone, Copy, Debug, Default)]
90pub struct Options {
91 #[cfg(target_os = "macos")]
93 pub apfs_clone_metadata: bool,
94}
95
96#[cfg(not(any(windows, target_os = "macos")))]
98pub struct Entry {
99 pub depth: usize,
101 pub file_name: OsString,
103 pub file_type: FileType,
105 pub metadata: io::Result<Metadata>,
107 pub parent_path: Arc<Path>,
109}
110
111enum Job {
112 ReadDir {
114 root_idx: usize,
115 path: Arc<Path>,
116 entry_depth: usize,
119 },
120 #[cfg(not(any(windows, target_os = "macos")))]
122 StatCompletion {
123 root_idx: usize,
124 path: Arc<Path>,
125 entry_depth: usize,
127 entries: Vec<fs::DirEntry>,
128 },
129}
130
131impl Job {
132 fn root_idx(&self) -> usize {
134 match self {
135 Job::ReadDir { root_idx, .. } => *root_idx,
136 #[cfg(not(any(windows, target_os = "macos")))]
137 Job::StatCompletion { root_idx, .. } => *root_idx,
138 }
139 }
140}
141
142enum Event {
144 Batch {
145 root_idx: usize,
146 batch: Batch,
147 },
148 RootFinished {
151 root_idx: usize,
152 },
153 Finished,
155}
156
157pub enum RootEvent {
163 Entry(io::Result<Entry>),
165 Finished,
167}
168
169struct PoolShared {
170 injector: Injector<Job>,
172 stealers: Vec<Stealer<Job>>,
173 stop: AtomicBool,
174 descend: Arc<Descend>,
175 events: SyncSender<Event>,
176 active_roots: AtomicUsize,
178 jobs_per_root: HashMap<usize, AtomicUsize>,
181 order: Order,
182 #[cfg(any(windows, target_os = "macos"))]
183 options: Options,
184 unparkers: Vec<Unparker>,
186 idle: Vec<AtomicBool>,
189 next_wake: AtomicUsize,
191}
192
193struct Pool {
194 shared: Arc<PoolShared>,
195 events: Receiver<Event>,
196 handles: Vec<thread::JoinHandle<()>>,
197}
198
199pub struct RootWalk {
202 next: Vec<(usize, RootEvent)>,
204 pool: Option<Pool>,
206}
207
208pub struct Walk {
211 next: Vec<io::Result<Entry>>,
218 pool: Option<Pool>,
222}
223
224#[cfg(any(windows, target_os = "macos"))]
230pub fn read_dir(
231 path: &Path,
232 options: Options,
233) -> io::Result<impl Iterator<Item = io::Result<Entry>>> {
234 NativeReadDir::open(Arc::from(path), 0, options)
235}
236
237pub fn walk(
241 root: &Path,
242 threads: usize,
243 order: Order,
244 options: Options,
245 descend: impl Fn(&Entry) -> bool + Send + Sync + 'static,
246) -> Walk {
247 let root = Entry::from_path(root, options);
248 let pool = match &root {
249 Ok(entry) if entry.file_type.is_dir() && descend(entry) => {
250 let path = Arc::from(entry.path());
251 let pool = start_pool(
252 threads.max(1),
253 HashMap::from([(0, AtomicUsize::new(0))]),
254 order,
255 Arc::new(move |_, entry| descend(entry)),
256 options,
257 );
258 start_jobs(
259 &pool,
260 vec![Job::ReadDir {
261 root_idx: 0,
262 path,
263 entry_depth: 1,
264 }],
265 );
266 Some(pool)
267 }
268 _ => None,
269 };
270 Walk {
271 next: vec![root],
272 pool,
273 }
274}
275
276impl Iterator for Walk {
277 type Item = io::Result<Entry>;
278
279 fn next(&mut self) -> Option<Self::Item> {
280 loop {
281 if let Some(entry) = self.next.pop() {
282 return Some(entry);
283 }
284
285 match self.pool.as_ref()?.events.recv() {
286 Ok(Event::Batch {
287 batch: Ok(entries), ..
288 }) => {
289 self.next.extend(entries.into_iter().rev());
290 }
291 Ok(Event::Batch {
292 batch: Err(err), ..
293 }) => return Some(Err(err)),
294 Ok(Event::RootFinished { .. }) => {}
295 Ok(Event::Finished) => {
296 self.pool = None;
297 return None;
298 }
299 Err(_) => return Some(Err(io::Error::other("directory worker stopped"))),
300 }
301 }
302 }
303}
304
305pub fn walk_roots(
315 roots: impl IntoIterator<Item = (usize, PathBuf)>,
316 threads: usize,
317 order: Order,
318 options: Options,
319 descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
320) -> RootWalk {
321 start_root_walk(
322 roots.into_iter().collect(),
323 threads,
324 order,
325 descend,
326 |path: PathBuf| Entry::from_path(&path, options),
327 options,
328 )
329}
330
331pub fn walk_root_entries(
342 roots: impl IntoIterator<Item = (usize, io::Result<Entry>)>,
343 threads: usize,
344 order: Order,
345 options: Options,
346 descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
347) -> RootWalk {
348 start_root_walk(
349 roots.into_iter().collect(),
350 threads,
351 order,
352 descend,
353 std::convert::identity,
354 options,
355 )
356}
357
358fn start_root_walk<Root>(
359 roots: Vec<(usize, Root)>,
360 threads: usize,
361 order: Order,
362 descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
363 prepare: impl Fn(Root) -> io::Result<Entry>,
364 options: Options,
365) -> RootWalk {
366 let jobs_per_root = roots
367 .iter()
368 .map(|(root_idx, _)| (*root_idx, AtomicUsize::new(0)))
369 .collect::<HashMap<_, _>>();
370 assert_eq!(
371 jobs_per_root.len(),
372 roots.len(),
373 "root indices must be unique"
374 );
375 let descend = Arc::new(descend);
376 let (next, root_jobs) = begin_walks(
377 roots
378 .into_iter()
379 .map(|(root_idx, root)| (root_idx, prepare(root))),
380 descend.as_ref(),
381 );
382 let pool = if root_jobs.is_empty() {
383 None
384 } else {
385 let pool = start_pool(threads.max(1), jobs_per_root, order, descend, options);
386 start_jobs(&pool, root_jobs);
387 Some(pool)
388 };
389 RootWalk { next, pool }
390}
391
392impl Iterator for RootWalk {
393 type Item = (usize, RootEvent);
394
395 fn next(&mut self) -> Option<Self::Item> {
396 loop {
397 if let Some(entry) = self.next.pop() {
398 return Some(entry);
399 }
400 match self.pool.as_ref()?.events.recv() {
401 Ok(Event::Batch {
402 root_idx,
403 batch: Ok(entries),
404 }) => self.next.extend(
405 entries
406 .into_iter()
407 .rev()
408 .map(|entry| (root_idx, RootEvent::Entry(entry))),
409 ),
410 Ok(Event::Batch {
411 root_idx,
412 batch: Err(err),
413 }) => return Some((root_idx, RootEvent::Entry(Err(err)))),
414 Ok(Event::RootFinished { root_idx }) => {
415 return Some((root_idx, RootEvent::Finished));
416 }
417 Ok(Event::Finished) => {
418 self.pool = None;
419 return None;
420 }
421 Err(_) => {
422 return Some((
423 0,
424 RootEvent::Entry(Err(io::Error::other("directory worker stopped"))),
425 ));
426 }
427 }
428 }
429 }
430}
431
432impl PoolShared {
433 fn wake_worker(&self) {
435 let len = self.idle.len();
436 let start = self.next_wake.fetch_add(1, AtomicOrdering::Relaxed) % len;
439 for offset in 0..len {
440 let idx = (start + offset) % len;
441 if self.idle[idx]
442 .compare_exchange(true, false, AtomicOrdering::AcqRel, AtomicOrdering::Relaxed)
443 .is_ok()
444 {
445 self.unparkers[idx].unpark();
446 break;
447 }
448 }
449 }
450
451 fn wake_workers(&self) {
453 for unparker in &self.unparkers {
454 unparker.unpark();
455 }
456 }
457}
458
459#[cfg(not(any(windows, target_os = "macos")))]
460impl Entry {
461 #[must_use]
463 pub fn path(&self) -> PathBuf {
464 self.parent_path.join(&self.file_name)
465 }
466
467 pub fn from_path(path: &Path, _options: Options) -> io::Result<Self> {
469 let metadata = fs::symlink_metadata(path)?;
470 Ok(Self {
471 depth: 0,
472 file_name: path.file_name().unwrap_or(path.as_os_str()).to_owned(),
473 file_type: metadata.file_type(),
474 metadata: Ok(metadata),
475 parent_path: Arc::from(path.parent().unwrap_or(Path::new(""))),
476 })
477 }
478
479 fn from_dir_entry(
480 depth: usize,
481 parent_path: Arc<Path>,
482 entry: fs::DirEntry,
483 ) -> io::Result<Self> {
484 Ok(Self {
485 depth,
486 file_name: entry.file_name(),
487 file_type: entry.file_type()?,
488 metadata: entry.metadata(),
489 parent_path,
490 })
491 }
492}
493
494fn start_pool(
495 threads: usize,
496 jobs_per_root: HashMap<usize, AtomicUsize>,
497 order: Order,
498 descend: Arc<Descend>,
499 options: Options,
500) -> Pool {
501 #[cfg(not(any(windows, target_os = "macos")))]
502 let _ = options;
503 let workers: Vec<_> = (0..threads).map(|_| Worker::new_lifo()).collect();
504 let parkers: Vec<_> = (0..threads).map(|_| Parker::new()).collect();
505 let (event_tx, event_rx) = sync_channel(threads * 2);
506 let shared = Arc::new(PoolShared {
507 injector: Injector::new(),
508 stealers: workers.iter().map(Worker::stealer).collect(),
509 stop: AtomicBool::new(false),
510 descend,
511 events: event_tx,
512 active_roots: AtomicUsize::new(0),
513 jobs_per_root,
514 order,
515 #[cfg(any(windows, target_os = "macos"))]
516 options,
517 unparkers: parkers
518 .iter()
519 .map(|parker| parker.unparker().clone())
520 .collect(),
521 idle: (0..threads).map(|_| AtomicBool::new(false)).collect(),
522 next_wake: AtomicUsize::new(0),
523 });
524 let handles: Vec<_> = workers
525 .into_iter()
526 .zip(parkers)
527 .enumerate()
528 .map(|(idx, (worker, parker))| {
529 let shared = Arc::clone(&shared);
530 thread::Builder::new()
531 .name(format!("dua-fs-walk-{idx}"))
532 .spawn(move || worker_loop(idx, worker, parker, shared))
533 .expect("filesystem worker thread can be spawned")
534 })
535 .collect();
536
537 Pool {
538 shared,
539 events: event_rx,
540 handles,
541 }
542}
543
544fn begin_walks(
547 roots: impl IntoIterator<Item = (usize, io::Result<Entry>)>,
548 descend: &Descend,
549) -> (Vec<(usize, RootEvent)>, Vec<Job>) {
550 let mut next = Vec::new();
551 let mut jobs = Vec::new();
552 for (root_idx, mut entry) in roots {
553 if let Ok(entry) = &mut entry {
554 entry.depth = 0;
555 }
556 let has_job = if let Ok(entry) = &entry
557 && entry.metadata.is_ok()
558 && entry.file_type.is_dir()
559 && descend(root_idx, entry)
560 {
561 jobs.push(Job::ReadDir {
562 root_idx,
563 path: Arc::from(entry.path()),
564 entry_depth: 1,
565 });
566 true
567 } else {
568 false
569 };
570 next.push((root_idx, RootEvent::Entry(entry)));
571 if !has_job {
572 next.push((root_idx, RootEvent::Finished));
573 }
574 }
575 next.reverse();
576 (next, jobs)
577}
578
579fn start_jobs(pool: &Pool, root_jobs: Vec<Job>) {
582 let wake_all = root_jobs.len() > 1;
583 debug_assert_eq!(
584 pool.shared.active_roots.load(AtomicOrdering::Relaxed),
585 0,
586 "initial jobs must be started on an idle pool"
587 );
588 debug_assert!(
589 root_jobs.iter().all(|j| match j {
590 Job::ReadDir { entry_depth, .. } => *entry_depth,
591 #[cfg(not(any(windows, target_os = "macos")))]
592 Job::StatCompletion { entry_depth, .. } => *entry_depth,
593 } == 1),
594 "the first jobs should be root jobs, so active_root counts match"
595 );
596 pool.shared
597 .active_roots
598 .store(root_jobs.len(), AtomicOrdering::Relaxed);
599 for job in &root_jobs {
600 add_pending(job.root_idx(), 1, &pool.shared);
601 }
602 for job in root_jobs {
603 pool.shared.injector.push(job);
604 }
605 if wake_all {
606 pool.shared.wake_workers();
607 } else {
608 pool.shared.wake_worker();
609 }
610}
611
612fn worker_loop(idx: usize, worker: Worker<Job>, parker: Parker, shared: Arc<PoolShared>) {
613 while !shared.stop.load(AtomicOrdering::Relaxed) {
614 let found = if let Some(found) = find_job(&worker, &shared) {
615 found
616 } else {
617 shared.idle[idx].store(true, AtomicOrdering::Release);
618 let Some(found) = find_job(&worker, &shared) else {
619 parker.park();
620 shared.idle[idx].store(false, AtomicOrdering::Release);
621 continue;
622 };
623 shared.idle[idx].store(false, AtomicOrdering::Release);
624 found
625 };
626 let (job, stolen) = found;
627 if stolen {
628 shared.wake_worker();
631 }
632 run_job(job, &worker, &shared);
633 }
634}
635
636impl Drop for Pool {
637 fn drop(&mut self) {
638 self.shared.stop.store(true, AtomicOrdering::Relaxed);
639 self.shared.wake_workers();
640 for handle in self.handles.drain(..) {
641 handle.join().ok();
642 }
643 }
644}
645
646fn find_job(worker: &Worker<Job>, shared: &PoolShared) -> Option<(Job, bool)> {
657 loop {
658 if let Some(job) = worker.pop() {
659 return Some((job, false));
660 }
661
662 match shared.injector.steal_batch_and_pop(worker) {
663 Steal::Success(job) => return Some((job, false)),
664 Steal::Retry => continue,
665 Steal::Empty => {}
666 }
667
668 let mut retry = false;
669 for stealer in &shared.stealers {
670 match stealer.steal() {
671 Steal::Success(job) => return Some((job, true)),
672 Steal::Retry => retry = true,
673 Steal::Empty => {}
674 }
675 }
676 if !retry {
677 return None;
678 }
679 }
680}
681
682fn run_job(job: Job, worker: &Worker<Job>, shared: &PoolShared) {
683 match job {
684 Job::ReadDir {
685 root_idx: root,
686 path,
687 entry_depth,
688 } => {
689 if matches!(shared.order, Order::Completion) {
690 read_dir_completion(root, path, entry_depth, worker, shared);
691 } else {
692 read_dir_parent_first(root, path, entry_depth, worker, shared);
693 }
694 }
695 #[cfg(not(any(windows, target_os = "macos")))]
696 Job::StatCompletion {
697 root_idx: root,
698 path,
699 entry_depth,
700 entries,
701 } => stat_entries_completion(root, path, entry_depth, entries, worker, shared),
702 }
703}
704
705#[cfg(not(any(windows, target_os = "macos")))]
711fn read_dir_completion(
712 root_idx: usize,
713 path: Arc<Path>,
714 entry_depth: usize,
715 worker: &Worker<Job>,
716 shared: &PoolShared,
717) {
718 let dir_entries = match fs::read_dir(&path) {
719 Ok(entries) => entries,
720 Err(err) => {
721 if shared
722 .events
723 .send(Event::Batch {
724 root_idx,
725 batch: Err(err),
726 })
727 .is_err()
728 {
729 shared.stop.store(true, AtomicOrdering::Relaxed);
730 }
731 finish_pending(root_idx, shared);
732 return;
733 }
734 };
735 let mut chunk = Vec::with_capacity(ENTRY_CHUNK_SIZE);
736 let mut errors = Vec::new();
737 let mut has_jobs = false;
738 for entry in dir_entries {
739 match entry {
740 Ok(entry) => {
741 chunk.push(entry);
742 if chunk.len() == ENTRY_CHUNK_SIZE {
743 add_pending(root_idx, 1, shared);
744 worker.push(Job::StatCompletion {
745 root_idx,
746 path: Arc::clone(&path),
747 entry_depth,
748 entries: std::mem::replace(
749 &mut chunk,
750 Vec::with_capacity(ENTRY_CHUNK_SIZE),
751 ),
752 });
753 has_jobs = true;
754 }
755 }
756 Err(err) => errors.push(Err(err)),
757 }
758 }
759 if !chunk.is_empty() {
760 add_pending(root_idx, 1, shared);
761 worker.push(Job::StatCompletion {
762 root_idx,
763 path,
764 entry_depth,
765 entries: chunk,
766 });
767 has_jobs = true;
768 }
769 if has_jobs {
770 shared.wake_worker();
771 }
772 if !errors.is_empty()
773 && shared
774 .events
775 .send(Event::Batch {
776 root_idx,
777 batch: Ok(errors),
778 })
779 .is_err()
780 {
781 shared.stop.store(true, AtomicOrdering::Relaxed);
782 }
783 finish_pending(root_idx, shared);
784}
785
786#[cfg(any(windows, target_os = "macos"))]
788fn native_read_dir(
789 path: Arc<Path>,
790 depth: usize,
791 shared: &PoolShared,
792) -> io::Result<NativeReadDir> {
793 NativeReadDir::open(path, depth, shared.options)
794}
795
796#[cfg(any(windows, target_os = "macos"))]
802fn read_dir_completion(
803 root_idx: usize,
804 path: Arc<Path>,
805 depth: usize,
806 worker: &Worker<Job>,
807 shared: &PoolShared,
808) {
809 let dir_entries = match native_read_dir(path, depth, shared) {
810 Ok(entries) => entries,
811 Err(err) => {
812 if shared
813 .events
814 .send(Event::Batch {
815 root_idx,
816 batch: Err(err),
817 })
818 .is_err()
819 {
820 shared.stop.store(true, AtomicOrdering::Relaxed);
821 }
822 finish_pending(root_idx, shared);
823 return;
824 }
825 };
826 let mut entries = Vec::with_capacity(ENTRY_CHUNK_SIZE);
827 let mut jobs = Vec::new();
828 for entry in dir_entries {
829 if let Ok(entry) = &entry
830 && entry.file_type.is_dir()
831 && (shared.descend)(root_idx, entry)
832 {
833 jobs.push(Job::ReadDir {
834 root_idx,
835 path: Arc::from(entry.path()),
836 entry_depth: depth + 1,
837 });
838 }
839 entries.push(entry);
840 if entries.len() == ENTRY_CHUNK_SIZE
841 && !publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared)
842 {
843 finish_pending(root_idx, shared);
844 return;
845 }
846 }
847 if !entries.is_empty() {
848 publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared);
849 }
850 finish_pending(root_idx, shared);
851}
852
853#[cfg(any(windows, target_os = "macos"))]
854fn publish_completion_batch(
855 root_idx: usize,
856 entries: &mut Vec<io::Result<Entry>>,
857 jobs: &mut Vec<Job>,
858 worker: &Worker<Job>,
859 shared: &PoolShared,
860) -> bool {
861 add_pending(root_idx, jobs.len(), shared);
862 schedule_jobs(std::mem::take(jobs), worker, shared);
863 if shared
864 .events
865 .send(Event::Batch {
866 root_idx,
867 batch: Ok(std::mem::replace(
868 entries,
869 Vec::with_capacity(ENTRY_CHUNK_SIZE),
870 )),
871 })
872 .is_err()
873 {
874 shared.stop.store(true, AtomicOrdering::Relaxed);
875 false
876 } else {
877 true
878 }
879}
880
881#[cfg(any(windows, target_os = "macos"))]
882fn read_dir_parent_first(
883 root_idx: usize,
884 path: Arc<Path>,
885 depth: usize,
886 worker: &Worker<Job>,
887 shared: &PoolShared,
888) {
889 let dir_entries = match native_read_dir(path, depth, shared) {
890 Ok(entries) => entries,
891 Err(err) => {
892 finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
893 return;
894 }
895 };
896 let mut jobs = Vec::new();
897 let entries = dir_entries
898 .map(|entry| {
899 entry.inspect(|entry| {
900 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
901 jobs.push(Job::ReadDir {
902 root_idx,
903 path: Arc::from(entry.path()),
904 entry_depth: depth + 1,
905 });
906 }
907 })
908 })
909 .collect();
910 finish_directory(root_idx, Ok(entries), jobs, worker, shared);
911}
912
913#[cfg(not(any(windows, target_os = "macos")))]
914fn stat_entries_completion(
915 root_idx: usize,
916 path: Arc<Path>,
917 depth: usize,
918 entries: Vec<fs::DirEntry>,
919 worker: &Worker<Job>,
920 shared: &PoolShared,
921) {
922 let mut jobs = Vec::new();
923 let entries = entries
924 .into_iter()
925 .map(|entry| {
926 Entry::from_dir_entry(depth, Arc::clone(&path), entry).inspect(|entry| {
927 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
928 jobs.push(Job::ReadDir {
929 root_idx,
930 path: Arc::from(entry.path()),
931 entry_depth: entry.depth + 1,
932 });
933 }
934 })
935 })
936 .collect();
937 add_pending(root_idx, jobs.len(), shared);
938 schedule_jobs(jobs, worker, shared);
939 if shared
940 .events
941 .send(Event::Batch {
942 root_idx,
943 batch: Ok(entries),
944 })
945 .is_err()
946 {
947 shared.stop.store(true, AtomicOrdering::Relaxed);
948 }
949 finish_pending(root_idx, shared);
950}
951
952#[cfg(not(any(windows, target_os = "macos")))]
960fn read_dir_parent_first(
961 root_idx: usize,
962 path: Arc<Path>,
963 depth: usize,
964 worker: &Worker<Job>,
965 shared: &PoolShared,
966) {
967 read_dir_inline(root_idx, path, depth, worker, shared);
968}
969
970#[cfg(not(any(windows, target_os = "macos")))]
974fn read_dir_inline(
975 root_idx: usize,
976 path: Arc<Path>,
977 depth: usize,
978 worker: &Worker<Job>,
979 shared: &PoolShared,
980) {
981 let dir_entries = match fs::read_dir(&path) {
982 Ok(entries) => entries,
983 Err(err) => {
984 finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
985 return;
986 }
987 };
988 let mut jobs = Vec::new();
989 let entries = dir_entries
990 .map(|entry| {
991 entry
992 .and_then(|entry| Entry::from_dir_entry(depth, Arc::clone(&path), entry))
993 .inspect(|entry| {
994 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
995 jobs.push(Job::ReadDir {
996 root_idx,
997 path: Arc::from(entry.path()),
998 entry_depth: depth + 1,
999 });
1000 }
1001 })
1002 })
1003 .collect();
1004 finish_directory(root_idx, Ok(entries), jobs, worker, shared);
1005}
1006
1007fn finish_directory(
1011 root_idx: usize,
1012 batch: Batch,
1013 jobs: Vec<Job>,
1014 worker: &Worker<Job>,
1015 shared: &PoolShared,
1016) {
1017 add_pending(root_idx, jobs.len(), shared);
1018
1019 match shared.order {
1020 Order::ParentFirst => {
1021 if shared
1022 .events
1023 .send(Event::Batch { root_idx, batch })
1024 .is_err()
1025 {
1026 shared.stop.store(true, AtomicOrdering::Relaxed);
1027 return;
1028 }
1029 schedule_jobs(jobs, worker, shared);
1030 }
1031 Order::Completion => {
1032 schedule_jobs(jobs, worker, shared);
1033 if shared
1034 .events
1035 .send(Event::Batch { root_idx, batch })
1036 .is_err()
1037 {
1038 shared.stop.store(true, AtomicOrdering::Relaxed);
1039 return;
1040 }
1041 }
1042 }
1043
1044 finish_pending(root_idx, shared);
1045}
1046
1047fn add_pending(root: usize, count: usize, shared: &PoolShared) {
1048 shared.jobs_per_root[&root].fetch_add(count, AtomicOrdering::Relaxed);
1049}
1050
1051fn finish_pending(root_idx: usize, shared: &PoolShared) {
1054 if shared.jobs_per_root[&root_idx].fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
1055 shared.events.send(Event::RootFinished { root_idx }).ok();
1056 if shared.active_roots.fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
1057 shared.events.send(Event::Finished).ok();
1058 }
1059 }
1060}
1061
1062fn schedule_jobs(jobs: Vec<Job>, worker: &Worker<Job>, shared: &PoolShared) {
1063 let has_jobs = !jobs.is_empty();
1064 for job in jobs {
1065 worker.push(job);
1066 }
1067 if has_jobs {
1068 shared.wake_worker();
1069 }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use super::*;
1075
1076 #[test]
1077 fn parallel_walk_is_parent_first_and_does_not_follow_symlinks() {
1078 let dir = tempfile::tempdir().unwrap();
1079 fs::create_dir_all(dir.path().join("b/child")).unwrap();
1080 fs::create_dir(dir.path().join("a")).unwrap();
1081 fs::write(dir.path().join("b/child/file"), b"x").unwrap();
1082
1083 #[cfg(unix)]
1084 std::os::unix::fs::symlink(dir.path().join("b"), dir.path().join("link")).unwrap();
1085
1086 #[cfg(unix)]
1087 let expected = ["", "a", "b", "b/child", "b/child/file", "link"];
1088 #[cfg(not(unix))]
1089 let expected = ["", "a", "b", "b/child", "b/child/file"];
1090 let expected = expected.into_iter().map(PathBuf::from).collect::<Vec<_>>();
1091
1092 for threads in [1, 4] {
1093 let paths = walk(
1094 dir.path(),
1095 threads,
1096 Order::ParentFirst,
1097 Options::default(),
1098 |_| true,
1099 )
1100 .map(|entry| {
1101 entry
1102 .unwrap()
1103 .path()
1104 .strip_prefix(dir.path())
1105 .unwrap()
1106 .to_owned()
1107 })
1108 .collect::<Vec<_>>();
1109 let mut sorted_paths = paths.clone();
1110 sorted_paths.sort();
1111 assert_eq!(
1112 sorted_paths, expected,
1113 "walk with {threads} threads should visit every expected path exactly once"
1114 );
1115
1116 for path in paths.iter().filter(|path| path.components().count() > 1) {
1117 let parent = path.parent().unwrap();
1118 assert!(
1119 paths.iter().position(|path| path == parent)
1120 < paths.iter().position(|candidate| candidate == path),
1121 "parent {parent:?} should precede child {path:?} with {threads} threads; \
1122 traversal order: {paths:?}"
1123 );
1124 }
1125 }
1126 }
1127
1128 #[test]
1129 fn pruning_keeps_the_directory_and_missing_roots_are_errors() {
1130 let dir = tempfile::tempdir().unwrap();
1131 fs::create_dir_all(dir.path().join("skip/child")).unwrap();
1132
1133 let paths = walk(
1134 dir.path(),
1135 2,
1136 Order::Completion,
1137 Options::default(),
1138 |entry| entry.file_name != "skip",
1139 )
1140 .map(|entry| entry.unwrap().file_name)
1141 .collect::<Vec<_>>();
1142 assert_eq!(
1143 paths,
1144 vec![
1145 dir.path().file_name().unwrap().to_owned(),
1146 OsString::from("skip")
1147 ],
1148 "a pruned directory should be yielded without traversing its children"
1149 );
1150
1151 assert!(
1152 walk(
1153 &dir.path().join("missing"),
1154 2,
1155 Order::Completion,
1156 Options::default(),
1157 |_| true,
1158 )
1159 .next()
1160 .unwrap()
1161 .is_err(),
1162 "a missing root should be yielded as an I/O error"
1163 );
1164 }
1165
1166 #[test]
1167 fn concurrent_roots_keep_their_identity() {
1168 let dir = tempfile::tempdir().unwrap();
1169 let roots = [dir.path().join("a"), dir.path().join("b")];
1170 for root in &roots {
1171 fs::create_dir_all(root.join("child")).unwrap();
1172 }
1173
1174 let events = walk_roots(
1175 roots.iter().cloned().enumerate(),
1176 2,
1177 Order::Completion,
1178 Options::default(),
1179 |_, _| true,
1180 )
1181 .collect::<Vec<_>>();
1182 let mut paths = Vec::new();
1183 let mut last_entry = [0; 2];
1184 let mut finished = [None; 2];
1185 for (position, (root_idx, event)) in events.into_iter().enumerate() {
1186 match event {
1187 RootEvent::Entry(entry) => {
1188 last_entry[root_idx] = position;
1189 paths.push((
1190 root_idx,
1191 entry
1192 .unwrap()
1193 .path()
1194 .strip_prefix(&roots[root_idx])
1195 .unwrap()
1196 .to_owned(),
1197 ));
1198 }
1199 RootEvent::Finished => finished[root_idx] = Some(position),
1200 }
1201 }
1202 paths.sort();
1203 assert_eq!(
1204 paths,
1205 [
1206 (0, PathBuf::new()),
1207 (0, PathBuf::from("child")),
1208 (1, PathBuf::new()),
1209 (1, PathBuf::from("child")),
1210 ]
1211 );
1212 for root_idx in 0..roots.len() {
1213 assert!(
1214 last_entry[root_idx] < finished[root_idx].unwrap(),
1215 "root {root_idx} must finish after its last entry",
1216 );
1217 }
1218 }
1219
1220 #[test]
1221 fn prepared_roots_rebase_existing_descendants() {
1222 let directory = tempfile::tempdir().unwrap();
1223 let descendant = directory.path().join("descendant");
1224 fs::create_dir(&descendant).unwrap();
1225 let child = descendant.join("child");
1226 fs::write(&child, b"nested file").unwrap();
1227
1228 let descendant_entry = walk(
1229 directory.path(),
1230 2,
1231 Order::ParentFirst,
1232 Options::default(),
1233 |_| true,
1234 )
1235 .find_map(|entry| {
1236 let entry = entry.unwrap();
1237 (entry.path() == descendant).then_some(entry)
1238 })
1239 .expect("the initial walk should yield the descendant directory");
1240 assert_eq!(descendant_entry.depth, 1);
1241
1242 let mut events = walk_root_entries(
1243 [(7, Ok(descendant_entry))],
1244 2,
1245 Order::ParentFirst,
1246 Options::default(),
1247 |root_idx, entry| {
1248 assert_eq!(root_idx, 7);
1249 assert_eq!(entry.depth, 0, "the predicate should see a re-rooted entry");
1250 true
1251 },
1252 );
1253
1254 let Some((7, RootEvent::Entry(Ok(mut root)))) = events.next() else {
1255 panic!("the prepared descendant should be emitted as the new root");
1256 };
1257 assert_eq!(root.path(), descendant);
1258 assert_eq!(
1259 root.depth, 0,
1260 "the entry originally found at depth 1 must become the new traversal root"
1261 );
1262
1263 let Some((7, RootEvent::Entry(Ok(entry)))) = events.next() else {
1264 panic!("the re-rooted directory should emit its child");
1265 };
1266 assert_eq!(entry.path(), child);
1267 assert_eq!(
1268 entry.depth, 1,
1269 "the child depth must be relative to the prepared entry used as the new root"
1270 );
1271 assert_eq!(
1272 events
1273 .next()
1274 .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1275 Some((7, true))
1276 );
1277 assert_eq!(
1278 events.next().map(|(root_idx, _)| root_idx),
1279 None,
1280 "nothing left after the Finished event"
1281 );
1282
1283 root.metadata = Err(io::Error::from(io::ErrorKind::PermissionDenied));
1284 let mut events = walk_root_entries(
1285 [(7, Ok(root))],
1286 2,
1287 Order::ParentFirst,
1288 Options::default(),
1289 |_, _| panic!("a directory with inaccessible metadata must not be descended"),
1290 );
1291 let Some((7, RootEvent::Entry(Ok(root)))) = events.next() else {
1292 panic!("the prepared directory must retain its metadata error");
1293 };
1294 let error = root
1295 .metadata
1296 .err()
1297 .expect("the inaccessible root must retain its metadata error");
1298 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
1299 assert_eq!(
1300 events
1301 .next()
1302 .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1303 Some((7, true))
1304 );
1305 assert_eq!(events.next().map(|(root_idx, _)| root_idx), None);
1306 }
1307
1308 #[cfg(any(windows, target_os = "macos"))]
1309 #[test]
1310 fn prepared_roots_reuse_native_directory_metadata() {
1311 let directory = tempfile::tempdir().unwrap();
1312 let path = directory.path().join("file");
1313 fs::write(&path, b"cached metadata").unwrap();
1314 let expected_len = fs::metadata(&path).unwrap().len();
1315
1316 let entry = read_dir(directory.path(), Options::default())
1317 .unwrap()
1318 .next()
1319 .unwrap()
1320 .unwrap();
1321 assert_eq!(entry.depth, 0);
1322 assert_eq!(entry.path(), path);
1323 fs::remove_file(&path).unwrap();
1324
1325 let mut events = walk_root_entries(
1326 [(7, Ok(entry))],
1327 1,
1328 Order::Completion,
1329 Options::default(),
1330 |_, _| true,
1331 );
1332 let Some((7, RootEvent::Entry(Ok(entry)))) = events.next() else {
1333 panic!(
1334 "prepared root must be yielded without querying its removed path which would fail"
1335 );
1336 };
1337 assert_eq!(entry.path(), path);
1338 assert_eq!(entry.metadata.unwrap().len(), expected_len);
1339 assert_eq!(
1340 events
1341 .next()
1342 .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1343 Some((7, true))
1344 );
1345 assert_eq!(events.next().map(|(root_idx, _)| root_idx), None);
1346 }
1347
1348 #[test]
1349 fn wide_walk_wakes_multiple_idle_workers() {
1350 let dir = tempfile::tempdir().unwrap();
1351 for idx in 0..32 {
1352 fs::create_dir_all(dir.path().join(format!("{idx}/child"))).unwrap();
1353 }
1354
1355 let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1356 let seen_threads = Arc::clone(&worker_threads);
1357 walk(
1358 dir.path(),
1359 8,
1360 Order::Completion,
1361 Options::default(),
1362 move |entry| {
1363 if entry.depth == 1 {
1364 thread::sleep(std::time::Duration::from_millis(1));
1365 } else if entry.depth == 2 {
1366 seen_threads.lock().unwrap().insert(thread::current().id());
1367 thread::sleep(std::time::Duration::from_millis(10));
1368 }
1369 true
1370 },
1371 )
1372 .for_each(drop);
1373
1374 assert!(
1375 worker_threads.lock().unwrap().len() >= 4,
1376 "a wide directory should engage more than the producer and one thief"
1377 );
1378 }
1379
1380 #[cfg(any(windows, target_os = "macos"))]
1381 #[test]
1382 fn native_metadata_is_collected_by_the_directory_worker() {
1383 let dir = tempfile::tempdir().unwrap();
1384 for idx in 0..32 {
1385 fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1386 }
1387
1388 let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1389 let seen_threads = Arc::clone(&worker_threads);
1390 walk(
1391 dir.path(),
1392 8,
1393 Order::Completion,
1394 Options::default(),
1395 move |entry| {
1396 if entry.depth == 1 {
1397 seen_threads.lock().unwrap().insert(thread::current().id());
1398 thread::sleep(std::time::Duration::from_millis(2));
1399 }
1400 true
1401 },
1402 )
1403 .for_each(drop);
1404
1405 assert_eq!(
1406 worker_threads.lock().unwrap().len(),
1407 1,
1408 "native directory-entry metadata should stay on the enumerating worker"
1409 );
1410 }
1411
1412 #[cfg(any(windows, target_os = "macos"))]
1413 #[test]
1414 fn native_completion_streams_metadata_before_enumeration_finishes() {
1415 let dir = tempfile::tempdir().unwrap();
1416 for idx in 0..=ENTRY_CHUNK_SIZE {
1417 fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1418 }
1419
1420 let (continue_tx, continue_rx) = std::sync::mpsc::sync_channel(0);
1421 let continue_rx = Arc::new(std::sync::Mutex::new(continue_rx));
1422 let seen = Arc::new(AtomicUsize::new(0));
1423 let seen_in_worker = Arc::clone(&seen);
1424 let mut entries =
1425 walk(
1426 dir.path(),
1427 2,
1428 Order::Completion,
1429 Options::default(),
1430 move |entry| {
1431 if entry.depth == 1
1432 && seen_in_worker.fetch_add(1, AtomicOrdering::Relaxed) == ENTRY_CHUNK_SIZE
1433 {
1434 continue_rx
1435 .lock()
1436 .unwrap()
1437 .recv_timeout(std::time::Duration::from_secs(2))
1438 .expect("the first metadata batch should arrive before enumeration finishes");
1439 }
1440 true
1441 },
1442 );
1443
1444 assert_eq!(
1445 entries.next().unwrap().unwrap().depth,
1446 0,
1447 "the root entry should be yielded first"
1448 );
1449 assert_eq!(
1450 entries.next().unwrap().unwrap().depth,
1451 1,
1452 "the first metadata batch should be yielded before enumeration resumes"
1453 );
1454 continue_tx.send(()).unwrap();
1455 entries.for_each(drop);
1456 assert_eq!(
1457 seen.load(AtomicOrdering::Relaxed),
1458 ENTRY_CHUNK_SIZE + 1,
1459 "all directory entries should be inspected"
1460 );
1461 }
1462}