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 root: PathBuf,
223 options: Options,
224 finished: bool,
226}
227
228#[cfg(any(windows, target_os = "macos"))]
234pub fn read_dir(
235 path: &Path,
236 options: Options,
237) -> io::Result<impl Iterator<Item = io::Result<Entry>>> {
238 NativeReadDir::open(Arc::from(path), 0, options)
239}
240
241pub fn walk(
245 root: &Path,
246 threads: usize,
247 order: Order,
248 options: Options,
249 descend: impl Fn(&Entry) -> bool + Send + Sync + 'static,
250) -> Walk {
251 let root_path = root.to_owned();
252 let root = Entry::from_path(root, options);
253 let pool = match &root {
254 Ok(entry) if entry.file_type.is_dir() && descend(entry) => {
255 let path = Arc::from(entry.path());
256 let pool = start_pool(
257 threads.max(1),
258 HashMap::from([(0, AtomicUsize::new(0))]),
259 order,
260 Arc::new(move |_, entry| descend(entry)),
261 options,
262 );
263 start_jobs(
264 &pool,
265 vec![Job::ReadDir {
266 root_idx: 0,
267 path,
268 entry_depth: 1,
269 }],
270 );
271 Some(pool)
272 }
273 _ => None,
274 };
275 Walk {
276 next: vec![root],
277 pool,
278 root: root_path,
279 options,
280 finished: false,
281 }
282}
283
284impl Walk {
285 #[must_use]
289 pub fn restart(&mut self) -> bool {
290 if !self.finished || !self.next.is_empty() {
291 return false;
292 }
293 let Some(pool) = self.pool.as_ref() else {
294 return false;
295 };
296 let root = Entry::from_path(&self.root, self.options);
297 let job = match &root {
298 Ok(entry) if entry.file_type.is_dir() && (pool.shared.descend)(0, entry) => {
299 Some(Job::ReadDir {
300 root_idx: 0,
301 path: Arc::from(entry.path()),
302 entry_depth: 1,
303 })
304 }
305 _ => None,
306 };
307 self.next.push(root);
308 if let Some(job) = job {
309 self.finished = false;
310 start_jobs(pool, vec![job]);
311 }
312 true
313 }
314}
315
316impl Iterator for Walk {
317 type Item = io::Result<Entry>;
318
319 fn next(&mut self) -> Option<Self::Item> {
320 loop {
321 if let Some(entry) = self.next.pop() {
322 return Some(entry);
323 }
324 if self.finished {
325 return None;
326 }
327
328 match self.pool.as_ref()?.events.recv() {
329 Ok(Event::Batch {
330 batch: Ok(entries), ..
331 }) => {
332 self.next.extend(entries.into_iter().rev());
333 }
334 Ok(Event::Batch {
335 batch: Err(err), ..
336 }) => return Some(Err(err)),
337 Ok(Event::RootFinished { .. }) => {}
338 Ok(Event::Finished) => {
339 self.finished = true;
340 return None;
341 }
342 Err(_) => return Some(Err(io::Error::other("directory worker stopped"))),
343 }
344 }
345 }
346}
347
348pub fn walk_roots(
358 roots: impl IntoIterator<Item = (usize, PathBuf)>,
359 threads: usize,
360 order: Order,
361 options: Options,
362 descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
363) -> RootWalk {
364 start_root_walk(
365 roots.into_iter().collect(),
366 threads,
367 order,
368 descend,
369 |path: PathBuf| Entry::from_path(&path, options),
370 options,
371 )
372}
373
374pub fn walk_root_entries(
385 roots: impl IntoIterator<Item = (usize, io::Result<Entry>)>,
386 threads: usize,
387 order: Order,
388 options: Options,
389 descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
390) -> RootWalk {
391 start_root_walk(
392 roots.into_iter().collect(),
393 threads,
394 order,
395 descend,
396 std::convert::identity,
397 options,
398 )
399}
400
401fn start_root_walk<Root>(
402 roots: Vec<(usize, Root)>,
403 threads: usize,
404 order: Order,
405 descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
406 prepare: impl Fn(Root) -> io::Result<Entry>,
407 options: Options,
408) -> RootWalk {
409 let jobs_per_root = roots
410 .iter()
411 .map(|(root_idx, _)| (*root_idx, AtomicUsize::new(0)))
412 .collect::<HashMap<_, _>>();
413 assert_eq!(
414 jobs_per_root.len(),
415 roots.len(),
416 "root indices must be unique"
417 );
418 let descend = Arc::new(descend);
419 let (next, root_jobs) = begin_walks(
420 roots
421 .into_iter()
422 .map(|(root_idx, root)| (root_idx, prepare(root))),
423 descend.as_ref(),
424 );
425 let pool = if root_jobs.is_empty() {
426 None
427 } else {
428 let pool = start_pool(threads.max(1), jobs_per_root, order, descend, options);
429 start_jobs(&pool, root_jobs);
430 Some(pool)
431 };
432 RootWalk { next, pool }
433}
434
435impl Iterator for RootWalk {
436 type Item = (usize, RootEvent);
437
438 fn next(&mut self) -> Option<Self::Item> {
439 loop {
440 if let Some(entry) = self.next.pop() {
441 return Some(entry);
442 }
443 match self.pool.as_ref()?.events.recv() {
444 Ok(Event::Batch {
445 root_idx,
446 batch: Ok(entries),
447 }) => self.next.extend(
448 entries
449 .into_iter()
450 .rev()
451 .map(|entry| (root_idx, RootEvent::Entry(entry))),
452 ),
453 Ok(Event::Batch {
454 root_idx,
455 batch: Err(err),
456 }) => return Some((root_idx, RootEvent::Entry(Err(err)))),
457 Ok(Event::RootFinished { root_idx }) => {
458 return Some((root_idx, RootEvent::Finished));
459 }
460 Ok(Event::Finished) => {
461 self.pool = None;
462 return None;
463 }
464 Err(_) => {
465 return Some((
466 0,
467 RootEvent::Entry(Err(io::Error::other("directory worker stopped"))),
468 ));
469 }
470 }
471 }
472 }
473}
474
475impl PoolShared {
476 fn wake_worker(&self) {
478 let len = self.idle.len();
479 let start = self.next_wake.fetch_add(1, AtomicOrdering::Relaxed) % len;
482 for offset in 0..len {
483 let idx = (start + offset) % len;
484 if self.idle[idx]
485 .compare_exchange(true, false, AtomicOrdering::AcqRel, AtomicOrdering::Relaxed)
486 .is_ok()
487 {
488 self.unparkers[idx].unpark();
489 break;
490 }
491 }
492 }
493
494 fn wake_workers(&self) {
496 for unparker in &self.unparkers {
497 unparker.unpark();
498 }
499 }
500}
501
502#[cfg(not(any(windows, target_os = "macos")))]
503impl Entry {
504 #[must_use]
506 pub fn path(&self) -> PathBuf {
507 self.parent_path.join(&self.file_name)
508 }
509
510 pub fn from_path(path: &Path, _options: Options) -> io::Result<Self> {
512 let metadata = fs::symlink_metadata(path)?;
513 Ok(Self {
514 depth: 0,
515 file_name: path.file_name().unwrap_or(path.as_os_str()).to_owned(),
516 file_type: metadata.file_type(),
517 metadata: Ok(metadata),
518 parent_path: Arc::from(path.parent().unwrap_or(Path::new(""))),
519 })
520 }
521
522 fn from_dir_entry(
523 depth: usize,
524 parent_path: Arc<Path>,
525 entry: fs::DirEntry,
526 ) -> io::Result<Self> {
527 Ok(Self {
528 depth,
529 file_name: entry.file_name(),
530 file_type: entry.file_type()?,
531 metadata: entry.metadata(),
532 parent_path,
533 })
534 }
535}
536
537fn start_pool(
538 threads: usize,
539 jobs_per_root: HashMap<usize, AtomicUsize>,
540 order: Order,
541 descend: Arc<Descend>,
542 options: Options,
543) -> Pool {
544 #[cfg(not(any(windows, target_os = "macos")))]
545 let _ = options;
546 let workers: Vec<_> = (0..threads).map(|_| Worker::new_lifo()).collect();
547 let parkers: Vec<_> = (0..threads).map(|_| Parker::new()).collect();
548 let (event_tx, event_rx) = sync_channel(threads * 2);
549 let shared = Arc::new(PoolShared {
550 injector: Injector::new(),
551 stealers: workers.iter().map(Worker::stealer).collect(),
552 stop: AtomicBool::new(false),
553 descend,
554 events: event_tx,
555 active_roots: AtomicUsize::new(0),
556 jobs_per_root,
557 order,
558 #[cfg(any(windows, target_os = "macos"))]
559 options,
560 unparkers: parkers
561 .iter()
562 .map(|parker| parker.unparker().clone())
563 .collect(),
564 idle: (0..threads).map(|_| AtomicBool::new(false)).collect(),
565 next_wake: AtomicUsize::new(0),
566 });
567 let handles: Vec<_> = workers
568 .into_iter()
569 .zip(parkers)
570 .enumerate()
571 .map(|(idx, (worker, parker))| {
572 let shared = Arc::clone(&shared);
573 thread::Builder::new()
574 .name(format!("dua-fs-walk-{idx}"))
575 .spawn(move || worker_loop(idx, worker, parker, shared))
576 .expect("filesystem worker thread can be spawned")
577 })
578 .collect();
579
580 Pool {
581 shared,
582 events: event_rx,
583 handles,
584 }
585}
586
587fn begin_walks(
590 roots: impl IntoIterator<Item = (usize, io::Result<Entry>)>,
591 descend: &Descend,
592) -> (Vec<(usize, RootEvent)>, Vec<Job>) {
593 let mut next = Vec::new();
594 let mut jobs = Vec::new();
595 for (root_idx, mut entry) in roots {
596 if let Ok(entry) = &mut entry {
597 entry.depth = 0;
598 }
599 let has_job = if let Ok(entry) = &entry
600 && entry.metadata.is_ok()
601 && entry.file_type.is_dir()
602 && descend(root_idx, entry)
603 {
604 jobs.push(Job::ReadDir {
605 root_idx,
606 path: Arc::from(entry.path()),
607 entry_depth: 1,
608 });
609 true
610 } else {
611 false
612 };
613 next.push((root_idx, RootEvent::Entry(entry)));
614 if !has_job {
615 next.push((root_idx, RootEvent::Finished));
616 }
617 }
618 next.reverse();
619 (next, jobs)
620}
621
622fn start_jobs(pool: &Pool, root_jobs: Vec<Job>) {
625 let wake_all = root_jobs.len() > 1;
626 debug_assert_eq!(
627 pool.shared.active_roots.load(AtomicOrdering::Relaxed),
628 0,
629 "initial jobs must be started on an idle pool"
630 );
631 debug_assert!(
632 root_jobs.iter().all(|j| match j {
633 Job::ReadDir { entry_depth, .. } => *entry_depth,
634 #[cfg(not(any(windows, target_os = "macos")))]
635 Job::StatCompletion { entry_depth, .. } => *entry_depth,
636 } == 1),
637 "the first jobs should be root jobs, so active_root counts match"
638 );
639 pool.shared
640 .active_roots
641 .store(root_jobs.len(), AtomicOrdering::Relaxed);
642 for job in &root_jobs {
643 add_pending(job.root_idx(), 1, &pool.shared);
644 }
645 for job in root_jobs {
646 pool.shared.injector.push(job);
647 }
648 if wake_all {
649 pool.shared.wake_workers();
650 } else {
651 pool.shared.wake_worker();
652 }
653}
654
655fn worker_loop(idx: usize, worker: Worker<Job>, parker: Parker, shared: Arc<PoolShared>) {
656 while !shared.stop.load(AtomicOrdering::Relaxed) {
657 let found = if let Some(found) = find_job(&worker, &shared) {
658 found
659 } else {
660 shared.idle[idx].store(true, AtomicOrdering::Release);
661 let Some(found) = find_job(&worker, &shared) else {
662 parker.park();
663 shared.idle[idx].store(false, AtomicOrdering::Release);
664 continue;
665 };
666 shared.idle[idx].store(false, AtomicOrdering::Release);
667 found
668 };
669 let (job, stolen) = found;
670 if stolen {
671 shared.wake_worker();
674 }
675 run_job(job, &worker, &shared);
676 }
677}
678
679impl Drop for Pool {
680 fn drop(&mut self) {
681 self.shared.stop.store(true, AtomicOrdering::Relaxed);
682 self.shared.wake_workers();
683 for handle in self.handles.drain(..) {
684 handle.join().ok();
685 }
686 }
687}
688
689fn find_job(worker: &Worker<Job>, shared: &PoolShared) -> Option<(Job, bool)> {
700 loop {
701 if let Some(job) = worker.pop() {
702 return Some((job, false));
703 }
704
705 match shared.injector.steal_batch_and_pop(worker) {
706 Steal::Success(job) => return Some((job, false)),
707 Steal::Retry => continue,
708 Steal::Empty => {}
709 }
710
711 let mut retry = false;
712 for stealer in &shared.stealers {
713 match stealer.steal() {
714 Steal::Success(job) => return Some((job, true)),
715 Steal::Retry => retry = true,
716 Steal::Empty => {}
717 }
718 }
719 if !retry {
720 return None;
721 }
722 }
723}
724
725fn run_job(job: Job, worker: &Worker<Job>, shared: &PoolShared) {
726 match job {
727 Job::ReadDir {
728 root_idx: root,
729 path,
730 entry_depth,
731 } => {
732 if matches!(shared.order, Order::Completion) {
733 read_dir_completion(root, path, entry_depth, worker, shared);
734 } else {
735 read_dir_parent_first(root, path, entry_depth, worker, shared);
736 }
737 }
738 #[cfg(not(any(windows, target_os = "macos")))]
739 Job::StatCompletion {
740 root_idx: root,
741 path,
742 entry_depth,
743 entries,
744 } => stat_entries_completion(root, path, entry_depth, entries, worker, shared),
745 }
746}
747
748#[cfg(not(any(windows, target_os = "macos")))]
754fn read_dir_completion(
755 root_idx: usize,
756 path: Arc<Path>,
757 entry_depth: usize,
758 worker: &Worker<Job>,
759 shared: &PoolShared,
760) {
761 let dir_entries = match fs::read_dir(&path) {
762 Ok(entries) => entries,
763 Err(err) => {
764 if shared
765 .events
766 .send(Event::Batch {
767 root_idx,
768 batch: Err(err),
769 })
770 .is_err()
771 {
772 shared.stop.store(true, AtomicOrdering::Relaxed);
773 }
774 finish_pending(root_idx, shared);
775 return;
776 }
777 };
778 let mut chunk = Vec::with_capacity(ENTRY_CHUNK_SIZE);
779 let mut errors = Vec::new();
780 let mut has_jobs = false;
781 for entry in dir_entries {
782 match entry {
783 Ok(entry) => {
784 chunk.push(entry);
785 if chunk.len() == ENTRY_CHUNK_SIZE {
786 add_pending(root_idx, 1, shared);
787 worker.push(Job::StatCompletion {
788 root_idx,
789 path: Arc::clone(&path),
790 entry_depth,
791 entries: std::mem::replace(
792 &mut chunk,
793 Vec::with_capacity(ENTRY_CHUNK_SIZE),
794 ),
795 });
796 has_jobs = true;
797 }
798 }
799 Err(err) => errors.push(Err(err)),
800 }
801 }
802 if !chunk.is_empty() {
803 add_pending(root_idx, 1, shared);
804 worker.push(Job::StatCompletion {
805 root_idx,
806 path,
807 entry_depth,
808 entries: chunk,
809 });
810 has_jobs = true;
811 }
812 if has_jobs {
813 shared.wake_worker();
814 }
815 if !errors.is_empty()
816 && shared
817 .events
818 .send(Event::Batch {
819 root_idx,
820 batch: Ok(errors),
821 })
822 .is_err()
823 {
824 shared.stop.store(true, AtomicOrdering::Relaxed);
825 }
826 finish_pending(root_idx, shared);
827}
828
829#[cfg(any(windows, target_os = "macos"))]
831fn native_read_dir(
832 path: Arc<Path>,
833 depth: usize,
834 shared: &PoolShared,
835) -> io::Result<NativeReadDir> {
836 NativeReadDir::open(path, depth, shared.options)
837}
838
839#[cfg(any(windows, target_os = "macos"))]
845fn read_dir_completion(
846 root_idx: usize,
847 path: Arc<Path>,
848 depth: usize,
849 worker: &Worker<Job>,
850 shared: &PoolShared,
851) {
852 let dir_entries = match native_read_dir(path, depth, shared) {
853 Ok(entries) => entries,
854 Err(err) => {
855 if shared
856 .events
857 .send(Event::Batch {
858 root_idx,
859 batch: Err(err),
860 })
861 .is_err()
862 {
863 shared.stop.store(true, AtomicOrdering::Relaxed);
864 }
865 finish_pending(root_idx, shared);
866 return;
867 }
868 };
869 let mut entries = Vec::with_capacity(ENTRY_CHUNK_SIZE);
870 let mut jobs = Vec::new();
871 for entry in dir_entries {
872 if let Ok(entry) = &entry
873 && entry.file_type.is_dir()
874 && (shared.descend)(root_idx, entry)
875 {
876 jobs.push(Job::ReadDir {
877 root_idx,
878 path: Arc::from(entry.path()),
879 entry_depth: depth + 1,
880 });
881 }
882 entries.push(entry);
883 if entries.len() == ENTRY_CHUNK_SIZE
884 && !publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared)
885 {
886 finish_pending(root_idx, shared);
887 return;
888 }
889 }
890 if !entries.is_empty() {
891 publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared);
892 }
893 finish_pending(root_idx, shared);
894}
895
896#[cfg(any(windows, target_os = "macos"))]
897fn publish_completion_batch(
898 root_idx: usize,
899 entries: &mut Vec<io::Result<Entry>>,
900 jobs: &mut Vec<Job>,
901 worker: &Worker<Job>,
902 shared: &PoolShared,
903) -> bool {
904 add_pending(root_idx, jobs.len(), shared);
905 schedule_jobs(std::mem::take(jobs), worker, shared);
906 if shared
907 .events
908 .send(Event::Batch {
909 root_idx,
910 batch: Ok(std::mem::replace(
911 entries,
912 Vec::with_capacity(ENTRY_CHUNK_SIZE),
913 )),
914 })
915 .is_err()
916 {
917 shared.stop.store(true, AtomicOrdering::Relaxed);
918 false
919 } else {
920 true
921 }
922}
923
924#[cfg(any(windows, target_os = "macos"))]
925fn read_dir_parent_first(
926 root_idx: usize,
927 path: Arc<Path>,
928 depth: usize,
929 worker: &Worker<Job>,
930 shared: &PoolShared,
931) {
932 let dir_entries = match native_read_dir(path, depth, shared) {
933 Ok(entries) => entries,
934 Err(err) => {
935 finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
936 return;
937 }
938 };
939 let mut jobs = Vec::new();
940 let entries = dir_entries
941 .map(|entry| {
942 entry.inspect(|entry| {
943 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
944 jobs.push(Job::ReadDir {
945 root_idx,
946 path: Arc::from(entry.path()),
947 entry_depth: depth + 1,
948 });
949 }
950 })
951 })
952 .collect();
953 finish_directory(root_idx, Ok(entries), jobs, worker, shared);
954}
955
956#[cfg(not(any(windows, target_os = "macos")))]
957fn stat_entries_completion(
958 root_idx: usize,
959 path: Arc<Path>,
960 depth: usize,
961 entries: Vec<fs::DirEntry>,
962 worker: &Worker<Job>,
963 shared: &PoolShared,
964) {
965 let mut jobs = Vec::new();
966 let entries = entries
967 .into_iter()
968 .map(|entry| {
969 Entry::from_dir_entry(depth, Arc::clone(&path), entry).inspect(|entry| {
970 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
971 jobs.push(Job::ReadDir {
972 root_idx,
973 path: Arc::from(entry.path()),
974 entry_depth: entry.depth + 1,
975 });
976 }
977 })
978 })
979 .collect();
980 add_pending(root_idx, jobs.len(), shared);
981 schedule_jobs(jobs, worker, shared);
982 if shared
983 .events
984 .send(Event::Batch {
985 root_idx,
986 batch: Ok(entries),
987 })
988 .is_err()
989 {
990 shared.stop.store(true, AtomicOrdering::Relaxed);
991 }
992 finish_pending(root_idx, shared);
993}
994
995#[cfg(not(any(windows, target_os = "macos")))]
1003fn read_dir_parent_first(
1004 root_idx: usize,
1005 path: Arc<Path>,
1006 depth: usize,
1007 worker: &Worker<Job>,
1008 shared: &PoolShared,
1009) {
1010 read_dir_inline(root_idx, path, depth, worker, shared);
1011}
1012
1013#[cfg(not(any(windows, target_os = "macos")))]
1017fn read_dir_inline(
1018 root_idx: usize,
1019 path: Arc<Path>,
1020 depth: usize,
1021 worker: &Worker<Job>,
1022 shared: &PoolShared,
1023) {
1024 let dir_entries = match fs::read_dir(&path) {
1025 Ok(entries) => entries,
1026 Err(err) => {
1027 finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
1028 return;
1029 }
1030 };
1031 let mut jobs = Vec::new();
1032 let entries = dir_entries
1033 .map(|entry| {
1034 entry
1035 .and_then(|entry| Entry::from_dir_entry(depth, Arc::clone(&path), entry))
1036 .inspect(|entry| {
1037 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
1038 jobs.push(Job::ReadDir {
1039 root_idx,
1040 path: Arc::from(entry.path()),
1041 entry_depth: depth + 1,
1042 });
1043 }
1044 })
1045 })
1046 .collect();
1047 finish_directory(root_idx, Ok(entries), jobs, worker, shared);
1048}
1049
1050fn finish_directory(
1054 root_idx: usize,
1055 batch: Batch,
1056 jobs: Vec<Job>,
1057 worker: &Worker<Job>,
1058 shared: &PoolShared,
1059) {
1060 add_pending(root_idx, jobs.len(), shared);
1061
1062 match shared.order {
1063 Order::ParentFirst => {
1064 if shared
1065 .events
1066 .send(Event::Batch { root_idx, batch })
1067 .is_err()
1068 {
1069 shared.stop.store(true, AtomicOrdering::Relaxed);
1070 return;
1071 }
1072 schedule_jobs(jobs, worker, shared);
1073 }
1074 Order::Completion => {
1075 schedule_jobs(jobs, worker, shared);
1076 if shared
1077 .events
1078 .send(Event::Batch { root_idx, batch })
1079 .is_err()
1080 {
1081 shared.stop.store(true, AtomicOrdering::Relaxed);
1082 return;
1083 }
1084 }
1085 }
1086
1087 finish_pending(root_idx, shared);
1088}
1089
1090fn add_pending(root: usize, count: usize, shared: &PoolShared) {
1091 shared.jobs_per_root[&root].fetch_add(count, AtomicOrdering::Relaxed);
1092}
1093
1094fn finish_pending(root_idx: usize, shared: &PoolShared) {
1097 if shared.jobs_per_root[&root_idx].fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
1098 shared.events.send(Event::RootFinished { root_idx }).ok();
1099 if shared.active_roots.fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
1100 shared.events.send(Event::Finished).ok();
1101 }
1102 }
1103}
1104
1105fn schedule_jobs(jobs: Vec<Job>, worker: &Worker<Job>, shared: &PoolShared) {
1106 let has_jobs = !jobs.is_empty();
1107 for job in jobs {
1108 worker.push(job);
1109 }
1110 if has_jobs {
1111 shared.wake_worker();
1112 }
1113}
1114
1115#[cfg(test)]
1116mod tests {
1117 use super::*;
1118
1119 #[test]
1120 fn parallel_walk_is_parent_first_and_does_not_follow_symlinks() {
1121 let dir = tempfile::tempdir().unwrap();
1122 fs::create_dir_all(dir.path().join("b/child")).unwrap();
1123 fs::create_dir(dir.path().join("a")).unwrap();
1124 fs::write(dir.path().join("b/child/file"), b"x").unwrap();
1125
1126 #[cfg(unix)]
1127 std::os::unix::fs::symlink(dir.path().join("b"), dir.path().join("link")).unwrap();
1128
1129 #[cfg(unix)]
1130 let expected = ["", "a", "b", "b/child", "b/child/file", "link"];
1131 #[cfg(not(unix))]
1132 let expected = ["", "a", "b", "b/child", "b/child/file"];
1133 let expected = expected.into_iter().map(PathBuf::from).collect::<Vec<_>>();
1134
1135 for threads in [1, 4] {
1136 let paths = walk(
1137 dir.path(),
1138 threads,
1139 Order::ParentFirst,
1140 Options::default(),
1141 |_| true,
1142 )
1143 .map(|entry| {
1144 entry
1145 .unwrap()
1146 .path()
1147 .strip_prefix(dir.path())
1148 .unwrap()
1149 .to_owned()
1150 })
1151 .collect::<Vec<_>>();
1152 let mut sorted_paths = paths.clone();
1153 sorted_paths.sort();
1154 assert_eq!(
1155 sorted_paths, expected,
1156 "walk with {threads} threads should visit every expected path exactly once"
1157 );
1158
1159 for path in paths.iter().filter(|path| path.components().count() > 1) {
1160 let parent = path.parent().unwrap();
1161 assert!(
1162 paths.iter().position(|path| path == parent)
1163 < paths.iter().position(|candidate| candidate == path),
1164 "parent {parent:?} should precede child {path:?} with {threads} threads; \
1165 traversal order: {paths:?}"
1166 );
1167 }
1168 }
1169 }
1170
1171 #[test]
1172 fn a_completed_walk_can_reuse_its_workers() {
1173 let dir = tempfile::tempdir().unwrap();
1174 fs::create_dir_all(dir.path().join("first/child")).unwrap();
1175
1176 let mut walk = walk(dir.path(), 2, Order::Completion, Options::default(), |_| {
1177 true
1178 });
1179 assert!(!walk.restart(), "an active walk cannot be restarted");
1180 let worker_ids = walk
1181 .pool
1182 .as_ref()
1183 .unwrap()
1184 .handles
1185 .iter()
1186 .map(|handle| handle.thread().id())
1187 .collect::<Vec<_>>();
1188 walk.by_ref().for_each(drop);
1189 assert!(walk.next().is_none(), "a completed walk stays exhausted");
1190
1191 fs::create_dir_all(dir.path().join("second/child")).unwrap();
1192 assert!(walk.restart());
1193 let paths = walk
1194 .by_ref()
1195 .map(|entry| {
1196 entry
1197 .unwrap()
1198 .path()
1199 .strip_prefix(dir.path())
1200 .unwrap()
1201 .into()
1202 })
1203 .collect::<Vec<PathBuf>>();
1204
1205 assert!(paths.contains(&PathBuf::from("second/child")));
1206 assert_eq!(
1207 walk.pool
1208 .as_ref()
1209 .unwrap()
1210 .handles
1211 .iter()
1212 .map(|handle| handle.thread().id())
1213 .collect::<Vec<_>>(),
1214 worker_ids,
1215 "a restarted walk keeps its original worker threads"
1216 );
1217 }
1218
1219 #[test]
1220 fn pruning_keeps_the_directory_and_missing_roots_are_errors() {
1221 let dir = tempfile::tempdir().unwrap();
1222 fs::create_dir_all(dir.path().join("skip/child")).unwrap();
1223
1224 let paths = walk(
1225 dir.path(),
1226 2,
1227 Order::Completion,
1228 Options::default(),
1229 |entry| entry.file_name != "skip",
1230 )
1231 .map(|entry| entry.unwrap().file_name)
1232 .collect::<Vec<_>>();
1233 assert_eq!(
1234 paths,
1235 vec![
1236 dir.path().file_name().unwrap().to_owned(),
1237 OsString::from("skip")
1238 ],
1239 "a pruned directory should be yielded without traversing its children"
1240 );
1241
1242 assert!(
1243 walk(
1244 &dir.path().join("missing"),
1245 2,
1246 Order::Completion,
1247 Options::default(),
1248 |_| true,
1249 )
1250 .next()
1251 .unwrap()
1252 .is_err(),
1253 "a missing root should be yielded as an I/O error"
1254 );
1255 }
1256
1257 #[test]
1258 fn concurrent_roots_keep_their_identity() {
1259 let dir = tempfile::tempdir().unwrap();
1260 let roots = [dir.path().join("a"), dir.path().join("b")];
1261 for root in &roots {
1262 fs::create_dir_all(root.join("child")).unwrap();
1263 }
1264
1265 let events = walk_roots(
1266 roots.iter().cloned().enumerate(),
1267 2,
1268 Order::Completion,
1269 Options::default(),
1270 |_, _| true,
1271 )
1272 .collect::<Vec<_>>();
1273 let mut paths = Vec::new();
1274 let mut last_entry = [0; 2];
1275 let mut finished = [None; 2];
1276 for (position, (root_idx, event)) in events.into_iter().enumerate() {
1277 match event {
1278 RootEvent::Entry(entry) => {
1279 last_entry[root_idx] = position;
1280 paths.push((
1281 root_idx,
1282 entry
1283 .unwrap()
1284 .path()
1285 .strip_prefix(&roots[root_idx])
1286 .unwrap()
1287 .to_owned(),
1288 ));
1289 }
1290 RootEvent::Finished => finished[root_idx] = Some(position),
1291 }
1292 }
1293 paths.sort();
1294 assert_eq!(
1295 paths,
1296 [
1297 (0, PathBuf::new()),
1298 (0, PathBuf::from("child")),
1299 (1, PathBuf::new()),
1300 (1, PathBuf::from("child")),
1301 ]
1302 );
1303 for root_idx in 0..roots.len() {
1304 assert!(
1305 last_entry[root_idx] < finished[root_idx].unwrap(),
1306 "root {root_idx} must finish after its last entry",
1307 );
1308 }
1309 }
1310
1311 #[test]
1312 fn prepared_roots_rebase_existing_descendants() {
1313 let directory = tempfile::tempdir().unwrap();
1314 let descendant = directory.path().join("descendant");
1315 fs::create_dir(&descendant).unwrap();
1316 let child = descendant.join("child");
1317 fs::write(&child, b"nested file").unwrap();
1318
1319 let descendant_entry = walk(
1320 directory.path(),
1321 2,
1322 Order::ParentFirst,
1323 Options::default(),
1324 |_| true,
1325 )
1326 .find_map(|entry| {
1327 let entry = entry.unwrap();
1328 (entry.path() == descendant).then_some(entry)
1329 })
1330 .expect("the initial walk should yield the descendant directory");
1331 assert_eq!(descendant_entry.depth, 1);
1332
1333 let mut events = walk_root_entries(
1334 [(7, Ok(descendant_entry))],
1335 2,
1336 Order::ParentFirst,
1337 Options::default(),
1338 |root_idx, entry| {
1339 assert_eq!(root_idx, 7);
1340 assert_eq!(entry.depth, 0, "the predicate should see a re-rooted entry");
1341 true
1342 },
1343 );
1344
1345 let Some((7, RootEvent::Entry(Ok(mut root)))) = events.next() else {
1346 panic!("the prepared descendant should be emitted as the new root");
1347 };
1348 assert_eq!(root.path(), descendant);
1349 assert_eq!(
1350 root.depth, 0,
1351 "the entry originally found at depth 1 must become the new traversal root"
1352 );
1353
1354 let Some((7, RootEvent::Entry(Ok(entry)))) = events.next() else {
1355 panic!("the re-rooted directory should emit its child");
1356 };
1357 assert_eq!(entry.path(), child);
1358 assert_eq!(
1359 entry.depth, 1,
1360 "the child depth must be relative to the prepared entry used as the new root"
1361 );
1362 assert_eq!(
1363 events
1364 .next()
1365 .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1366 Some((7, true))
1367 );
1368 assert_eq!(
1369 events.next().map(|(root_idx, _)| root_idx),
1370 None,
1371 "nothing left after the Finished event"
1372 );
1373
1374 root.metadata = Err(io::Error::from(io::ErrorKind::PermissionDenied));
1375 let mut events = walk_root_entries(
1376 [(7, Ok(root))],
1377 2,
1378 Order::ParentFirst,
1379 Options::default(),
1380 |_, _| panic!("a directory with inaccessible metadata must not be descended"),
1381 );
1382 let Some((7, RootEvent::Entry(Ok(root)))) = events.next() else {
1383 panic!("the prepared directory must retain its metadata error");
1384 };
1385 let error = root
1386 .metadata
1387 .err()
1388 .expect("the inaccessible root must retain its metadata error");
1389 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
1390 assert_eq!(
1391 events
1392 .next()
1393 .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1394 Some((7, true))
1395 );
1396 assert_eq!(events.next().map(|(root_idx, _)| root_idx), None);
1397 }
1398
1399 #[cfg(any(windows, target_os = "macos"))]
1400 #[test]
1401 fn prepared_roots_reuse_native_directory_metadata() {
1402 let directory = tempfile::tempdir().unwrap();
1403 let path = directory.path().join("file");
1404 fs::write(&path, b"cached metadata").unwrap();
1405 let expected_len = fs::metadata(&path).unwrap().len();
1406
1407 let entry = read_dir(directory.path(), Options::default())
1408 .unwrap()
1409 .next()
1410 .unwrap()
1411 .unwrap();
1412 assert_eq!(entry.depth, 0);
1413 assert_eq!(entry.path(), path);
1414 fs::remove_file(&path).unwrap();
1415
1416 let mut events = walk_root_entries(
1417 [(7, Ok(entry))],
1418 1,
1419 Order::Completion,
1420 Options::default(),
1421 |_, _| true,
1422 );
1423 let Some((7, RootEvent::Entry(Ok(entry)))) = events.next() else {
1424 panic!(
1425 "prepared root must be yielded without querying its removed path which would fail"
1426 );
1427 };
1428 assert_eq!(entry.path(), path);
1429 assert_eq!(entry.metadata.unwrap().len(), expected_len);
1430 assert_eq!(
1431 events
1432 .next()
1433 .map(|(root_idx, event)| (root_idx, matches!(event, RootEvent::Finished))),
1434 Some((7, true))
1435 );
1436 assert_eq!(events.next().map(|(root_idx, _)| root_idx), None);
1437 }
1438
1439 #[test]
1440 fn wide_walk_wakes_multiple_idle_workers() {
1441 let dir = tempfile::tempdir().unwrap();
1442 for idx in 0..32 {
1443 fs::create_dir_all(dir.path().join(format!("{idx}/child"))).unwrap();
1444 }
1445
1446 let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1447 let seen_threads = Arc::clone(&worker_threads);
1448 walk(
1449 dir.path(),
1450 8,
1451 Order::Completion,
1452 Options::default(),
1453 move |entry| {
1454 if entry.depth == 1 {
1455 thread::sleep(std::time::Duration::from_millis(1));
1456 } else if entry.depth == 2 {
1457 seen_threads.lock().unwrap().insert(thread::current().id());
1458 thread::sleep(std::time::Duration::from_millis(10));
1459 }
1460 true
1461 },
1462 )
1463 .for_each(drop);
1464
1465 assert!(
1466 worker_threads.lock().unwrap().len() >= 4,
1467 "a wide directory should engage more than the producer and one thief"
1468 );
1469 }
1470
1471 #[cfg(any(windows, target_os = "macos"))]
1472 #[test]
1473 fn native_metadata_is_collected_by_the_directory_worker() {
1474 let dir = tempfile::tempdir().unwrap();
1475 for idx in 0..32 {
1476 fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1477 }
1478
1479 let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1480 let seen_threads = Arc::clone(&worker_threads);
1481 walk(
1482 dir.path(),
1483 8,
1484 Order::Completion,
1485 Options::default(),
1486 move |entry| {
1487 if entry.depth == 1 {
1488 seen_threads.lock().unwrap().insert(thread::current().id());
1489 thread::sleep(std::time::Duration::from_millis(2));
1490 }
1491 true
1492 },
1493 )
1494 .for_each(drop);
1495
1496 assert_eq!(
1497 worker_threads.lock().unwrap().len(),
1498 1,
1499 "native directory-entry metadata should stay on the enumerating worker"
1500 );
1501 }
1502
1503 #[cfg(any(windows, target_os = "macos"))]
1504 #[test]
1505 fn native_completion_streams_metadata_before_enumeration_finishes() {
1506 let dir = tempfile::tempdir().unwrap();
1507 for idx in 0..=ENTRY_CHUNK_SIZE {
1508 fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1509 }
1510
1511 let (continue_tx, continue_rx) = std::sync::mpsc::sync_channel(0);
1512 let continue_rx = Arc::new(std::sync::Mutex::new(continue_rx));
1513 let seen = Arc::new(AtomicUsize::new(0));
1514 let seen_in_worker = Arc::clone(&seen);
1515 let mut entries =
1516 walk(
1517 dir.path(),
1518 2,
1519 Order::Completion,
1520 Options::default(),
1521 move |entry| {
1522 if entry.depth == 1
1523 && seen_in_worker.fetch_add(1, AtomicOrdering::Relaxed) == ENTRY_CHUNK_SIZE
1524 {
1525 continue_rx
1526 .lock()
1527 .unwrap()
1528 .recv_timeout(std::time::Duration::from_secs(2))
1529 .expect("the first metadata batch should arrive before enumeration finishes");
1530 }
1531 true
1532 },
1533 );
1534
1535 assert_eq!(
1536 entries.next().unwrap().unwrap().depth,
1537 0,
1538 "the root entry should be yielded first"
1539 );
1540 assert_eq!(
1541 entries.next().unwrap().unwrap().depth,
1542 1,
1543 "the first metadata batch should be yielded before enumeration resumes"
1544 );
1545 continue_tx.send(()).unwrap();
1546 entries.for_each(drop);
1547 assert_eq!(
1548 seen.load(AtomicOrdering::Relaxed),
1549 ENTRY_CHUNK_SIZE + 1,
1550 "all directory entries should be inspected"
1551 );
1552 }
1553}