1use std::fs;
2use std::path::{Path, PathBuf};
3use std::time::Instant;
4
5use rayon::prelude::*;
6use crate::entry::{Entry, EntryKind};
9use crate::error::{Error, Result};
10use crate::filter::filter_entries;
11use crate::ignore::{apply_ignore_set, load_ignore_set, IgnoreSet};
12use crate::options::{CliSymlinkMode, ListOptions};
13use crate::sort::sort_entries;
14
15#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct ListTiming {
18 pub readdir_ms: u128,
20 pub stat_ms: u128,
22 pub sort_ms: u128,
24}
25
26impl ListTiming {
27 pub fn add_assign(&mut self, other: &ListTiming) {
29 self.readdir_ms = self.readdir_ms.saturating_add(other.readdir_ms);
30 self.stat_ms = self.stat_ms.saturating_add(other.stat_ms);
31 self.sort_ms = self.sort_ms.saturating_add(other.sort_ms);
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct Listing {
38 pub root: PathBuf,
40 pub root_is_dir: bool,
42 pub entries: Vec<Entry>,
44 pub minor_errors: usize,
47 pub timing: Option<ListTiming>,
49}
50
51impl Listing {
52 fn new(root: PathBuf, root_is_dir: bool, entries: Vec<Entry>) -> Self {
53 Self {
54 root,
55 root_is_dir,
56 entries,
57 minor_errors: 0,
58 timing: None,
59 }
60 }
61
62 fn with_timing(mut self, timing: Option<ListTiming>) -> Self {
63 self.timing = timing;
64 self
65 }
66}
67
68fn follow_cli_path(path: &Path, opts: &ListOptions) -> bool {
70 if opts.follow_links {
71 return true;
72 }
73 match opts.cli_symlink {
74 CliSymlinkMode::Never => false,
75 CliSymlinkMode::Always => {
76 fs::symlink_metadata(path)
78 .map(|m| m.file_type().is_symlink())
79 .unwrap_or(false)
80 }
81 CliSymlinkMode::DirOnly => {
82 let is_link = fs::symlink_metadata(path)
84 .map(|m| m.file_type().is_symlink())
85 .unwrap_or(false);
86 if !is_link {
87 return false;
88 }
89 fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
90 }
91 }
92}
93
94pub fn list_path(path: &Path, opts: &ListOptions) -> Result<Listing> {
98 let path = if path.as_os_str().is_empty() {
99 Path::new(".")
100 } else {
101 path
102 };
103
104 let follow = follow_cli_path(path, opts);
105
106 let meta = if follow {
107 fs::metadata(path)
108 } else {
109 fs::symlink_metadata(path)
110 }
111 .map_err(|source| {
112 if source.kind() == std::io::ErrorKind::NotFound {
113 Error::NotFound(path.to_path_buf())
114 } else {
115 Error::Metadata {
116 path: path.to_path_buf(),
117 source,
118 }
119 }
120 })?;
121
122 if opts.directory || !meta.is_dir() {
124 let fill = meta_fill_from(opts);
125 let entry = if follow {
126 Entry::from_path_follow_with(path, 0, fill)?
127 } else {
128 Entry::from_path_and_meta_with(path, &meta, 0, fill)?
129 };
130 return Ok(Listing::new(path.to_path_buf(), meta.is_dir(), vec![entry]));
131 }
132
133 if opts.recursive {
134 list_recursive(path, opts)
135 } else {
136 list_directory(path, opts)
137 }
138}
139
140fn meta_fill_from(opts: &ListOptions) -> crate::entry::MetaFill {
141 crate::entry::MetaFill {
142 resolve_names: opts.resolve_owner_group,
143 read_context: opts.read_selinux,
144 }
145}
146
147fn entry_kind_only(item: &fs::DirEntry) -> Option<Entry> {
150 let file_type = item.file_type().ok()?;
151 let kind = EntryKind::from_file_type(file_type);
152 let name = item.file_name().to_string_lossy().into_owned();
153 let path = item.path();
154 Some(Entry {
155 path,
156 name,
157 kind,
158 size: 0,
159 modified: None,
160 created: None,
161 accessed: None,
162 changed: None,
163 mode: 0,
164 readonly: false,
165 symlink_target: None,
166 depth: 0,
167 git_status: crate::entry::GitStatus::Clean,
168 is_dir_header: false,
169 nlink: 0,
170 uid: 0,
171 gid: 0,
172 inode: 0,
173 blocks: 0,
174 dev: 0,
175 rdev: 0,
176 blksize: 0,
177 owner: String::new(),
178 group: String::new(),
179 author: String::new(),
180 context: String::new(),
181 })
182}
183
184fn entry_from_dir_entry(
186 item: &fs::DirEntry,
187 follow_links: bool,
188 fill: crate::entry::MetaFill,
189 prefer_statx: bool,
190) -> Option<Entry> {
191 let entry_path = item.path();
192 if follow_links {
193 return Entry::from_path_follow_with(&entry_path, 0, fill).ok();
194 }
195 #[cfg(target_os = "linux")]
196 {
197 if prefer_statx {
198 if let Ok(e) = crate::linux_statx::entry_from_statx(&entry_path, 0, fill) {
199 return Some(e);
200 }
201 }
202 }
203 #[cfg(not(target_os = "linux"))]
204 {
205 let _ = prefer_statx;
206 }
207 let meta = item
208 .metadata()
209 .or_else(|_| fs::symlink_metadata(&entry_path));
210 match meta {
211 Ok(m) => Entry::from_path_and_meta_with(&entry_path, &m, 0, fill).ok(),
212 Err(_) => None,
213 }
214}
215
216fn apply_expensive_fill(entries: &mut [Entry], fill: crate::entry::MetaFill) {
221 if !fill.resolve_names && !fill.read_context {
222 return;
223 }
224 for e in entries.iter_mut() {
225 e.fill_expensive(fill);
226 }
227}
228
229fn stat_dir_entries(dir_entries: &[fs::DirEntry], opts: &ListOptions) -> Vec<Entry> {
237 let follow = opts.follow_links;
238 let fill = meta_fill_from(opts);
239 let prefer_statx = opts.linux_statx;
240
241 if opts.use_kind_only() && !follow {
243 if !opts.use_parallel_stat(dir_entries.len()) {
244 return dir_entries.iter().filter_map(entry_kind_only).collect();
245 }
246 let collect = || dir_entries.par_iter().filter_map(entry_kind_only).collect();
247 return if opts.threads > 1 {
248 match rayon::ThreadPoolBuilder::new()
249 .num_threads(opts.threads)
250 .build()
251 {
252 Ok(pool) => pool.install(collect),
253 Err(_) => collect(),
254 }
255 } else {
256 collect()
257 };
258 }
259
260 #[cfg(target_os = "linux")]
262 {
263 if !follow && prefer_statx && !dir_entries.is_empty() {
264 if let Some(parent) = dir_entries[0].path().parent() {
265 if let Ok(dir_file) = fs::File::open(parent) {
266 use std::os::fd::AsFd;
267
268 let names: Vec<std::ffi::OsString> =
269 dir_entries.iter().map(|d| d.file_name()).collect();
270 let full_paths: Vec<PathBuf> = dir_entries.iter().map(|d| d.path()).collect();
271 let use_par = opts.use_parallel_stat(dir_entries.len());
272
273 #[cfg(feature = "io-uring")]
276 {
277 if opts.io_uring
278 && !use_par
279 && dir_entries.len() >= crate::io_uring_stat::IO_URING_THRESHOLD
280 {
281 if let Some(mut entries) = crate::io_uring_stat::entries_from_dir_uring(
282 dir_file.as_fd(),
283 &names,
284 &full_paths,
285 fill,
286 ) {
287 if entries.len() * 10 >= dir_entries.len() * 8 {
288 apply_expensive_fill(&mut entries, fill);
289 return entries;
290 }
291 }
292 }
293 }
294
295 let dir_fd = dir_file.as_fd();
297 let map_rel = |i: usize| {
298 let name = &names[i];
299 let path = &full_paths[i];
300 crate::linux_statx::entry_from_statx_at(
301 dir_fd,
302 name,
303 path,
304 0,
305 crate::entry::MetaFill {
306 resolve_names: false,
307 read_context: false,
308 },
309 )
310 .ok()
311 };
312
313 let mut entries: Vec<Entry> = if use_par {
314 let collect = || {
315 (0..dir_entries.len())
316 .into_par_iter()
317 .filter_map(map_rel)
318 .collect()
319 };
320 if opts.threads > 1 {
321 match rayon::ThreadPoolBuilder::new()
322 .num_threads(opts.threads)
323 .build()
324 {
325 Ok(pool) => pool.install(collect),
326 Err(_) => collect(),
327 }
328 } else {
329 collect()
330 }
331 } else {
332 (0..dir_entries.len()).filter_map(map_rel).collect()
333 };
334
335 if entries.len() * 10 >= dir_entries.len() * 8 {
336 apply_expensive_fill(&mut entries, fill);
337 return entries;
338 }
339 }
341 }
342 }
343 }
344
345 let cheap = crate::entry::MetaFill {
347 resolve_names: false,
348 read_context: false,
349 };
350 let map_one = |item: &fs::DirEntry| entry_from_dir_entry(item, follow, cheap, prefer_statx);
351
352 #[cfg(all(target_os = "linux", feature = "io-uring"))]
354 {
355 if opts.io_uring
356 && !follow
357 && !opts.use_parallel_stat(dir_entries.len())
358 && dir_entries.len() >= crate::io_uring_stat::IO_URING_THRESHOLD
359 {
360 let paths: Vec<_> = dir_entries.iter().map(|d| d.path()).collect();
361 if let Some(mut entries) = crate::io_uring_stat::entries_from_paths_uring(&paths, fill)
362 {
363 if entries.len() * 10 >= paths.len() * 8 {
364 apply_expensive_fill(&mut entries, fill);
365 return entries;
366 }
367 }
368 }
369 }
370
371 let mut entries: Vec<Entry> = if !opts.use_parallel_stat(dir_entries.len()) {
372 dir_entries.iter().filter_map(map_one).collect()
373 } else {
374 let collect = || dir_entries.par_iter().filter_map(map_one).collect();
375 if opts.threads > 1 {
376 match rayon::ThreadPoolBuilder::new()
377 .num_threads(opts.threads)
378 .build()
379 {
380 Ok(pool) => pool.install(collect),
381 Err(_) => collect(),
382 }
383 } else {
384 collect()
385 }
386 };
387 apply_expensive_fill(&mut entries, fill);
389 entries
390}
391
392pub fn list_directory(path: &Path, opts: &ListOptions) -> Result<Listing> {
394 let collect_timing = opts.collect_timing;
395 let mut timing = ListTiming::default();
396
397 let mut entries = Vec::new();
398
399 let fill = meta_fill_from(opts);
400 if opts.all {
402 if let Ok(dot) = Entry::from_path_with(path, 0, fill) {
403 let mut e = dot;
404 e.name = ".".to_string();
405 entries.push(e);
406 }
407 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
408 if let Ok(mut e) = Entry::from_path_with(parent, 0, fill) {
409 e.name = "..".to_string();
410 e.path = parent.to_path_buf();
411 entries.push(e);
412 }
413 } else if let Ok(mut e) = Entry::from_path_with(path, 0, fill) {
414 e.name = "..".to_string();
416 entries.push(e);
417 }
418 }
419
420 let t_readdir = if collect_timing {
421 Some(Instant::now())
422 } else {
423 None
424 };
425
426 let read = fs::read_dir(path).map_err(|source| Error::ReadDir {
427 path: path.to_path_buf(),
428 source,
429 })?;
430
431 let mut dir_entries = Vec::with_capacity(256);
433 for item in read {
434 let item = item.map_err(|source| Error::ReadDir {
435 path: path.to_path_buf(),
436 source,
437 })?;
438 dir_entries.push(item);
439 }
440 dir_entries.shrink_to_fit();
441
442 if let Some(t0) = t_readdir {
443 timing.readdir_ms = t0.elapsed().as_millis();
444 }
445
446 let t_stat = if collect_timing {
447 Some(Instant::now())
448 } else {
449 None
450 };
451
452 let children = stat_dir_entries(&dir_entries, opts);
453 entries.extend(children);
454
455 if let Some(t0) = t_stat {
456 timing.stat_ms = t0.elapsed().as_millis();
457 }
458
459 let t_sort = if collect_timing {
460 Some(Instant::now())
461 } else {
462 None
463 };
464
465 filter_entries(&mut entries, opts);
466 if opts.use_ignore_files {
467 let set = load_ignore_set(path);
468 apply_ignore_set(&mut entries, &set);
469 }
470 sort_entries(&mut entries, opts);
472
473 if let Some(t0) = t_sort {
474 timing.sort_ms = t0.elapsed().as_millis();
475 }
476
477 Ok(
478 Listing::new(path.to_path_buf(), true, entries).with_timing(if collect_timing {
479 Some(timing)
480 } else {
481 None
482 }),
483 )
484}
485
486struct Walked {
488 path: PathBuf,
489 depth: usize,
490 is_dir: bool,
491}
492
493pub fn list_recursive(path: &Path, opts: &ListOptions) -> Result<Listing> {
495 let collect_timing = opts.collect_timing;
496 let mut timing = ListTiming::default();
497
498 let mut minor_errors = 0usize;
499 let max_depth = opts.max_depth.unwrap_or(usize::MAX);
500
501 let root_ignore = if opts.use_ignore_files {
502 Some(load_ignore_set(path))
503 } else {
504 None
505 };
506
507 let t_walk = if collect_timing {
508 Some(Instant::now())
509 } else {
510 None
511 };
512
513 let mut walked: Vec<Walked> = Vec::new();
516 let mut jwalker = jwalk::WalkDir::new(path)
518 .follow_links(opts.follow_links)
519 .skip_hidden(false)
520 .sort(true);
521 if max_depth != usize::MAX {
522 jwalker = jwalker.max_depth(max_depth);
523 }
524
525 for item in jwalker {
526 let item = match item {
527 Ok(i) => i,
528 Err(_) => {
529 minor_errors += 1;
530 continue;
531 }
532 };
533 let depth = item.depth();
534 if depth == 0 {
535 continue; }
537 let is_dir = item.file_type().is_dir();
538 walked.push(Walked {
539 path: item.path(),
540 depth,
541 is_dir,
542 });
543 }
544
545 for w in &walked {
548 if w.is_dir {
549 if let Err(err) = fs::read_dir(&w.path) {
550 let kind = err.kind();
551 if matches!(
552 kind,
553 std::io::ErrorKind::PermissionDenied
554 | std::io::ErrorKind::NotFound
555 | std::io::ErrorKind::Other
556 ) {
557 minor_errors += 1;
558 }
559 }
560 }
561 }
562
563 if let Some(t0) = t_walk {
564 timing.readdir_ms = t0.elapsed().as_millis();
565 }
566
567 let fill = meta_fill_from(opts);
568 let t_stat = if collect_timing {
569 Some(Instant::now())
570 } else {
571 None
572 };
573
574 let built: Vec<Option<Entry>> = {
577 #[cfg(all(target_os = "linux", feature = "io-uring"))]
578 {
579 if opts.io_uring
580 && !opts.follow_links
581 && walked.len() >= crate::io_uring_stat::IO_URING_THRESHOLD
582 && !fill.resolve_names
583 && !fill.read_context
584 {
585 let paths: Vec<_> = walked.iter().map(|w| w.path.clone()).collect();
586 if let Some(mut ents) = crate::io_uring_stat::entries_from_paths_uring(&paths, fill)
587 {
588 if ents.len() * 10 >= paths.len() * 8 {
589 use std::collections::HashMap;
591 let depth_by: HashMap<&Path, usize> =
592 walked.iter().map(|w| (w.path.as_path(), w.depth)).collect();
593 for e in &mut ents {
594 if let Some(&d) = depth_by.get(e.path.as_path()) {
595 e.depth = d;
596 }
597 }
598 let mut by_path: HashMap<PathBuf, Entry> =
600 ents.into_iter().map(|e| (e.path.clone(), e)).collect();
601 let ordered: Vec<Option<Entry>> = walked
602 .iter()
603 .map(|w| {
604 by_path
605 .remove(&w.path)
606 .filter(|e| crate::filter::should_show(e, opts))
607 })
608 .collect();
609 if let Some(t0) = t_stat {
610 timing.stat_ms = t0.elapsed().as_millis();
611 }
612 return finish_recursive(
614 path,
615 opts,
616 walked,
617 ordered,
618 root_ignore,
619 minor_errors,
620 timing,
621 collect_timing,
622 );
623 }
624 }
625 }
626 }
627
628 let use_parallel = opts.use_parallel_stat(walked.len().max(1));
629 let map = |w: &Walked| {
630 Entry::from_path_with(&w.path, w.depth, fill)
631 .ok()
632 .filter(|e| crate::filter::should_show(e, opts))
633 };
634 if use_parallel {
635 if opts.threads > 1 {
636 match rayon::ThreadPoolBuilder::new()
637 .num_threads(opts.threads)
638 .build()
639 {
640 Ok(pool) => pool.install(|| walked.par_iter().map(map).collect()),
641 Err(_) => walked.par_iter().map(map).collect(),
642 }
643 } else {
644 walked.par_iter().map(map).collect()
645 }
646 } else {
647 walked.iter().map(map).collect()
648 }
649 };
650
651 if let Some(t0) = t_stat {
652 timing.stat_ms = t0.elapsed().as_millis();
653 }
654
655 finish_recursive(
656 path,
657 opts,
658 walked,
659 built,
660 root_ignore,
661 minor_errors,
662 timing,
663 collect_timing,
664 )
665}
666
667#[allow(clippy::too_many_arguments)]
668fn finish_recursive(
669 path: &Path,
670 opts: &ListOptions,
671 walked: Vec<Walked>,
672 built: Vec<Option<Entry>>,
673 root_ignore: Option<IgnoreSet>,
674 minor_errors: usize,
675 mut timing: ListTiming,
676 collect_timing: bool,
677) -> Result<Listing> {
678 let mut ignore_by_dir: Vec<(PathBuf, IgnoreSet)> = Vec::new();
682 let mut plain: Vec<Entry> = Vec::with_capacity(built.len());
683
684 for (_w, maybe) in walked.iter().zip(built) {
685 let Some(e) = maybe else {
686 continue;
687 };
688 if ignored_by_sets(&e, opts, root_ignore.as_ref(), &mut ignore_by_dir) {
689 continue;
690 }
691 plain.push(e);
692 }
693
694 let t_sort = if collect_timing {
695 Some(Instant::now())
696 } else {
697 None
698 };
699
700 let entries = if opts.emit_dir_headers {
701 rebuild_gnu_recursive_sections(path, plain, opts)
702 } else {
703 let mut entries = plain;
706 if !matches!(opts.sort_by, crate::options::SortBy::Name) || opts.reverse {
707 sort_tree_preorder(&mut entries, opts);
708 }
709 entries
710 };
711
712 if let Some(t0) = t_sort {
713 timing.sort_ms = t0.elapsed().as_millis();
714 }
715
716 let mut listing = Listing::new(path.to_path_buf(), true, entries)
717 .with_timing(if collect_timing { Some(timing) } else { None });
718 listing.minor_errors = minor_errors;
719 Ok(listing)
720}
721
722fn sort_tree_preorder(entries: &mut [Entry], opts: &ListOptions) {
726 if entries.is_empty() {
727 return;
728 }
729 let mut i = 0;
734 while i < entries.len() {
735 let depth = entries[i].depth;
736 let parent = entries[i].path.parent().map(Path::to_path_buf);
739 let mut j = i + 1;
740 while j < entries.len() {
741 if entries[j].depth < depth {
742 break;
743 }
744 if entries[j].depth == depth {
745 let p = entries[j].path.parent().map(Path::to_path_buf);
746 if p != parent {
747 break;
748 }
749 }
750 j += 1;
751 }
752 let sib: Vec<usize> = (i..j).filter(|&k| entries[k].depth == depth).collect();
754 if sib.len() > 1 {
755 let mut blocks: Vec<Vec<Entry>> = Vec::with_capacity(sib.len());
758 for (bi, &start) in sib.iter().enumerate() {
759 let end = sib.get(bi + 1).copied().unwrap_or(j);
760 blocks.push(entries[start..end].to_vec());
761 }
762 blocks.sort_by(|a, b| crate::sort::cmp_entry(&a[0], &b[0], opts));
764 let mut out = Vec::with_capacity(j - i);
765 for b in blocks {
766 out.extend(b);
767 }
768 entries[i..j].clone_from_slice(&out);
769 }
770 i = if j > i { j } else { i + 1 };
771 }
772}
773
774fn ignored_by_sets(
776 entry: &Entry,
777 opts: &ListOptions,
778 root_ignore: Option<&IgnoreSet>,
779 cache: &mut Vec<(PathBuf, IgnoreSet)>,
780) -> bool {
781 if !opts.use_ignore_files {
782 return false;
783 }
784 if let Some(root) = root_ignore {
785 if root.ignores(entry) {
786 return true;
787 }
788 }
789 let Some(parent) = entry.path.parent() else {
790 return false;
791 };
792 if let Some(root) = root_ignore {
793 if parent == root.base {
794 return false;
795 }
796 }
797 if let Some((_, set)) = cache.iter().find(|(p, _)| p == parent) {
798 return set.ignores(entry);
799 }
800 let set = load_ignore_set(parent);
801 let ignored = set.ignores(entry);
802 cache.push((parent.to_path_buf(), set));
803 ignored
804}
805
806fn rebuild_gnu_recursive_sections(
809 root: &Path,
810 plain: Vec<Entry>,
811 opts: &ListOptions,
812) -> Vec<Entry> {
813 use std::collections::HashMap;
814
815 let mut by_parent: HashMap<PathBuf, Vec<Entry>> = HashMap::new();
816 for e in plain {
817 let parent = e
818 .path
819 .parent()
820 .map(Path::to_path_buf)
821 .unwrap_or_else(|| root.to_path_buf());
822 by_parent.entry(parent).or_default().push(e);
823 }
824
825 let mut out = Vec::new();
826 fn emit(
827 dir: &Path,
828 depth: usize,
829 by_parent: &mut HashMap<PathBuf, Vec<Entry>>,
830 opts: &ListOptions,
831 out: &mut Vec<Entry>,
832 ) {
833 out.push(Entry::dir_header(dir, depth));
834 let mut kids = by_parent.remove(dir).unwrap_or_default();
835 sort_entries(&mut kids, opts);
836 let subdirs: Vec<PathBuf> = kids
837 .iter()
838 .filter(|e| e.kind == EntryKind::Directory)
839 .map(|e| e.path.clone())
840 .collect();
841 out.extend(kids);
842 for sub in subdirs {
843 emit(&sub, depth.saturating_add(1), by_parent, opts, out);
844 }
845 }
846 emit(root, 0, &mut by_parent, opts, &mut out);
847 out
848}
849
850#[derive(Debug)]
852pub struct ListOutcome {
853 pub listings: Vec<Listing>,
854 pub path_errors: Vec<Error>,
856 pub minor_errors: usize,
858}
859
860impl ListOutcome {
861 pub fn exit_code(&self) -> i32 {
863 if !self.path_errors.is_empty() {
864 2
865 } else if self.minor_errors > 0 || self.listings.iter().any(|l| l.minor_errors > 0) {
866 1
867 } else {
868 0
869 }
870 }
871
872 pub fn total_timing(&self) -> ListTiming {
874 let mut total = ListTiming::default();
875 for l in &self.listings {
876 if let Some(ref t) = l.timing {
877 total.add_assign(t);
878 }
879 }
880 total
881 }
882}
883
884pub fn list_paths(paths: &[PathBuf], opts: &ListOptions) -> Result<Vec<Listing>> {
889 let outcome = list_paths_with_errors(paths, opts);
890 if let Some(err) = outcome.path_errors.into_iter().next() {
891 return Err(err);
893 }
894 Ok(outcome.listings)
895}
896
897pub fn list_paths_with_errors(paths: &[PathBuf], opts: &ListOptions) -> ListOutcome {
899 let owned: Vec<PathBuf>;
900 let paths: &[PathBuf] = if paths.is_empty() {
901 owned = vec![PathBuf::from(".")];
902 &owned
903 } else {
904 paths
905 };
906
907 let mut listings = Vec::with_capacity(paths.len());
908 let mut path_errors = Vec::new();
909 let mut minor_errors = 0usize;
910
911 for p in paths {
912 match list_path(p, opts) {
913 Ok(l) => {
914 minor_errors += l.minor_errors;
915 listings.push(l);
916 }
917 Err(e) => path_errors.push(e),
918 }
919 }
920
921 ListOutcome {
922 listings,
923 path_errors,
924 minor_errors,
925 }
926}
927
928#[cfg(test)]
929mod tests {
930 use super::*;
931 use std::fs;
932
933 fn temp_dir() -> PathBuf {
934 use std::sync::atomic::{AtomicU64, Ordering};
935 static N: AtomicU64 = AtomicU64::new(0);
936 let base = std::env::temp_dir().join(format!(
937 "f00-core-list-{}-{}-{}",
938 std::process::id(),
939 std::time::SystemTime::now()
940 .duration_since(std::time::UNIX_EPOCH)
941 .map(|d| d.as_nanos())
942 .unwrap_or(0),
943 N.fetch_add(1, Ordering::Relaxed)
944 ));
945 fs::create_dir_all(&base).unwrap();
946 base
947 }
948
949 #[test]
950 fn list_paths_with_errors_partial_success() {
951 let dir = temp_dir();
952 fs::write(dir.join("a.txt"), b"x").unwrap();
953 let missing = dir.join("gone");
954 let opts = ListOptions::default();
955 let outcome = list_paths_with_errors(&[dir.clone(), missing], &opts);
956 assert_eq!(outcome.listings.len(), 1);
957 assert_eq!(outcome.path_errors.len(), 1);
958 assert_eq!(outcome.exit_code(), 2);
959 let _ = fs::remove_dir_all(&dir);
960 }
961
962 #[test]
963 fn list_paths_ok_exit_0() {
964 let dir = temp_dir();
965 fs::write(dir.join("a.txt"), b"x").unwrap();
966 let opts = ListOptions::default();
967 let outcome = list_paths_with_errors(std::slice::from_ref(&dir), &opts);
968 assert!(outcome.path_errors.is_empty());
969 assert_eq!(outcome.exit_code(), 0);
970 let _ = fs::remove_dir_all(&dir);
971 }
972
973 #[test]
974 fn list_directory_honors_ignore_files() {
975 let dir = temp_dir();
976 fs::write(dir.join("keep.txt"), b"k").unwrap();
977 fs::write(dir.join("skip.o"), b"o").unwrap();
978 fs::write(dir.join(".gitignore"), "*.o\n").unwrap();
979
980 let opts = ListOptions {
981 use_ignore_files: true,
982 ..Default::default()
983 };
984 let listing = list_directory(&dir, &opts).unwrap();
985 let names: Vec<_> = listing.entries.iter().map(|e| e.name.as_str()).collect();
986 assert!(names.contains(&"keep.txt"));
987 assert!(!names.contains(&"skip.o"));
988
989 let opts_off = ListOptions {
990 use_ignore_files: false,
991 ..Default::default()
992 };
993 let listing = list_directory(&dir, &opts_off).unwrap();
994 let names: Vec<_> = listing.entries.iter().map(|e| e.name.as_str()).collect();
995 assert!(names.contains(&"skip.o"));
996
997 let _ = fs::remove_dir_all(&dir);
998 }
999
1000 #[test]
1001 fn parallel_list_same_names_as_sequential() {
1002 let dir = temp_dir();
1003 let mut expected = Vec::new();
1005 for i in 0..64 {
1006 let name = format!("file_{i:03}.txt");
1007 fs::write(dir.join(&name), b"x").unwrap();
1008 expected.push(name);
1009 }
1010 fs::create_dir(dir.join("subdir")).unwrap();
1011 expected.push("subdir".into());
1012 expected.sort();
1013
1014 let sequential = ListOptions {
1015 parallel: false,
1016 threads: 1,
1017 ..Default::default()
1018 };
1019 let seq = list_directory(&dir, &sequential).unwrap();
1020 let mut seq_names: Vec<_> = seq.entries.iter().map(|e| e.name.clone()).collect();
1021 seq_names.sort();
1022 assert_eq!(
1023 seq_names, expected,
1024 "listing must include every created entry"
1025 );
1026
1027 if !cfg!(target_os = "freebsd") {
1029 let parallel = ListOptions {
1030 parallel: true,
1031 threads: 0,
1032 ..Default::default()
1033 };
1034 let par = list_directory(&dir, ¶llel).unwrap();
1035 let mut par_names: Vec<_> = par.entries.iter().map(|e| e.name.clone()).collect();
1036 par_names.sort();
1037 assert_eq!(
1038 seq_names, par_names,
1039 "parallel and sequential must produce identical ordered names"
1040 );
1041 }
1042
1043 let _ = fs::remove_dir_all(&dir);
1044 }
1045
1046 #[test]
1047 fn collect_timing_fills_phases() {
1048 let dir = temp_dir();
1049 for i in 0..8 {
1050 fs::write(dir.join(format!("f{i}")), b"x").unwrap();
1051 }
1052 let opts = ListOptions {
1053 collect_timing: true,
1054 parallel: false,
1055 threads: 1,
1056 ..Default::default()
1057 };
1058 let listing = list_directory(&dir, &opts).unwrap();
1059 let t = listing.timing.expect("timing present");
1060 let _ = (t.readdir_ms, t.stat_ms, t.sort_ms);
1062 let _ = fs::remove_dir_all(&dir);
1063 }
1064
1065 #[test]
1066 fn threads_one_forces_serial_path() {
1067 let dir = temp_dir();
1068 for i in 0..40 {
1069 fs::write(dir.join(format!("n{i}")), b"x").unwrap();
1070 }
1071 let opts = ListOptions {
1072 parallel: true,
1073 threads: 1,
1074 ..Default::default()
1075 };
1076 assert!(!opts.use_parallel_stat(40));
1077 let listing = list_directory(&dir, &opts).unwrap();
1078 assert_eq!(listing.entries.len(), 40);
1079 let _ = fs::remove_dir_all(&dir);
1080 }
1081}