1use std::fs;
2use std::path::{Path, PathBuf};
3use std::time::Instant;
4
5use rayon::prelude::*;
6use walkdir::WalkDir;
7
8use crate::entry::Entry;
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_from_dir_entry(
149 item: &fs::DirEntry,
150 follow_links: bool,
151 fill: crate::entry::MetaFill,
152 prefer_statx: bool,
153) -> Option<Entry> {
154 let entry_path = item.path();
155 if follow_links {
156 return Entry::from_path_follow_with(&entry_path, 0, fill).ok();
157 }
158 #[cfg(target_os = "linux")]
159 {
160 if prefer_statx {
161 if let Ok(e) = crate::linux_statx::entry_from_statx(&entry_path, 0, fill) {
162 return Some(e);
163 }
164 }
165 }
166 #[cfg(not(target_os = "linux"))]
167 {
168 let _ = prefer_statx;
169 }
170 let meta = item
171 .metadata()
172 .or_else(|_| fs::symlink_metadata(&entry_path));
173 match meta {
174 Ok(m) => Entry::from_path_and_meta_with(&entry_path, &m, 0, fill).ok(),
175 Err(_) => None,
176 }
177}
178
179fn stat_dir_entries(dir_entries: &[fs::DirEntry], opts: &ListOptions) -> Vec<Entry> {
181 let follow = opts.follow_links;
182 let fill = meta_fill_from(opts);
183 let prefer_statx = opts.linux_statx;
184 let map_one = |item: &fs::DirEntry| entry_from_dir_entry(item, follow, fill, prefer_statx);
185
186 #[cfg(all(target_os = "linux", feature = "io-uring"))]
188 {
189 if opts.io_uring
190 && !follow
191 && dir_entries.len() >= crate::io_uring_stat::IO_URING_THRESHOLD
192 && !fill.resolve_names
193 && !fill.read_context
194 {
195 let paths: Vec<_> = dir_entries.iter().map(|d| d.path()).collect();
196 if let Some(entries) = crate::io_uring_stat::entries_from_paths_uring(&paths, fill) {
197 if entries.len() * 10 >= paths.len() * 8 {
199 return entries;
200 }
201 }
202 }
203 }
204
205 if !opts.use_parallel_stat(dir_entries.len()) {
206 return dir_entries.iter().filter_map(map_one).collect();
207 }
208
209 let collect = || dir_entries.par_iter().filter_map(map_one).collect();
210
211 if opts.threads > 1 {
212 match rayon::ThreadPoolBuilder::new()
213 .num_threads(opts.threads)
214 .build()
215 {
216 Ok(pool) => pool.install(collect),
217 Err(_) => collect(),
218 }
219 } else {
220 collect()
221 }
222}
223
224pub fn list_directory(path: &Path, opts: &ListOptions) -> Result<Listing> {
226 let collect_timing = opts.collect_timing;
227 let mut timing = ListTiming::default();
228
229 let mut entries = Vec::new();
230
231 let fill = meta_fill_from(opts);
232 if opts.all {
234 if let Ok(dot) = Entry::from_path_with(path, 0, fill) {
235 let mut e = dot;
236 e.name = ".".to_string();
237 entries.push(e);
238 }
239 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
240 if let Ok(mut e) = Entry::from_path_with(parent, 0, fill) {
241 e.name = "..".to_string();
242 e.path = parent.to_path_buf();
243 entries.push(e);
244 }
245 } else if let Ok(mut e) = Entry::from_path_with(path, 0, fill) {
246 e.name = "..".to_string();
248 entries.push(e);
249 }
250 }
251
252 let t_readdir = if collect_timing {
253 Some(Instant::now())
254 } else {
255 None
256 };
257
258 let read = fs::read_dir(path).map_err(|source| Error::ReadDir {
259 path: path.to_path_buf(),
260 source,
261 })?;
262
263 let mut dir_entries = Vec::new();
264 for item in read {
265 let item = item.map_err(|source| Error::ReadDir {
266 path: path.to_path_buf(),
267 source,
268 })?;
269 dir_entries.push(item);
270 }
271
272 if let Some(t0) = t_readdir {
273 timing.readdir_ms = t0.elapsed().as_millis();
274 }
275
276 let t_stat = if collect_timing {
277 Some(Instant::now())
278 } else {
279 None
280 };
281
282 let children = stat_dir_entries(&dir_entries, opts);
283 entries.extend(children);
284
285 if let Some(t0) = t_stat {
286 timing.stat_ms = t0.elapsed().as_millis();
287 }
288
289 let t_sort = if collect_timing {
290 Some(Instant::now())
291 } else {
292 None
293 };
294
295 filter_entries(&mut entries, opts);
296 if opts.use_ignore_files {
297 let set = load_ignore_set(path);
298 apply_ignore_set(&mut entries, &set);
299 }
300 sort_entries(&mut entries, opts);
302
303 if let Some(t0) = t_sort {
304 timing.sort_ms = t0.elapsed().as_millis();
305 }
306
307 Ok(
308 Listing::new(path.to_path_buf(), true, entries).with_timing(if collect_timing {
309 Some(timing)
310 } else {
311 None
312 }),
313 )
314}
315
316pub fn list_recursive(path: &Path, opts: &ListOptions) -> Result<Listing> {
318 let collect_timing = opts.collect_timing;
319 let mut timing = ListTiming::default();
320 let t_all = if collect_timing {
321 Some(Instant::now())
322 } else {
323 None
324 };
325
326 let mut entries = Vec::new();
327 let mut minor_errors = 0usize;
328 let max_depth = opts.max_depth.unwrap_or(usize::MAX);
329
330 let root_ignore = if opts.use_ignore_files {
331 Some(load_ignore_set(path))
332 } else {
333 None
334 };
335 let mut ignore_by_dir: Vec<(PathBuf, IgnoreSet)> = Vec::new();
336
337 let fill = meta_fill_from(opts);
338 let walker = WalkDir::new(path)
343 .follow_links(opts.follow_links)
344 .max_depth(max_depth)
345 .sort_by_file_name();
346
347 let mut current_dir: Option<PathBuf> = None;
349
350 for item in walker {
351 let item = match item {
352 Ok(i) => i,
353 Err(_) => {
354 minor_errors += 1;
355 continue;
356 }
357 };
358
359 let depth = item.depth();
360 let entry_path = item.path();
361
362 if depth == 0 {
364 current_dir = Some(entry_path.to_path_buf());
365 entries.push(Entry::dir_header(entry_path, 0));
367 continue;
368 }
369
370 let parent = entry_path.parent().map(|p| p.to_path_buf());
371 if let Some(ref p) = parent {
372 if current_dir.as_ref() != Some(p) {
373 current_dir = Some(p.clone());
375 }
376 }
377
378 if item.file_type().is_dir() && depth > 0 {
381 if let Ok(meta) = item.metadata() {
382 if let Ok(e) = Entry::from_path_and_meta_with(entry_path, &meta, depth, fill) {
383 let show = crate::filter::should_show(&e, opts)
384 && !ignored_by_sets(&e, opts, root_ignore.as_ref(), &mut ignore_by_dir);
385 if show {
386 entries.push(e);
387 }
388 }
389 }
390 entries.push(Entry::dir_header(entry_path, depth));
391 continue;
392 }
393
394 let meta = match item.metadata() {
395 Ok(m) => m,
396 Err(_) => continue,
397 };
398 if let Ok(e) = Entry::from_path_and_meta_with(entry_path, &meta, depth, fill) {
399 if crate::filter::should_show(&e, opts)
400 && !ignored_by_sets(&e, opts, root_ignore.as_ref(), &mut ignore_by_dir)
401 {
402 entries.push(e);
403 }
404 }
405 }
406
407 if let Some(t0) = t_all {
408 let ms = t0.elapsed().as_millis();
410 timing.readdir_ms = ms / 2;
411 timing.stat_ms = ms.saturating_sub(timing.readdir_ms);
412 }
413
414 let t_sort = if collect_timing {
415 Some(Instant::now())
416 } else {
417 None
418 };
419
420 sort_recursive_sections(&mut entries, opts);
422
423 if let Some(t0) = t_sort {
424 timing.sort_ms = t0.elapsed().as_millis();
425 }
426
427 let mut listing = Listing::new(path.to_path_buf(), true, entries)
428 .with_timing(if collect_timing { Some(timing) } else { None });
429 listing.minor_errors = minor_errors;
430 Ok(listing)
431}
432
433fn ignored_by_sets(
435 entry: &Entry,
436 opts: &ListOptions,
437 root_ignore: Option<&IgnoreSet>,
438 cache: &mut Vec<(PathBuf, IgnoreSet)>,
439) -> bool {
440 if !opts.use_ignore_files {
441 return false;
442 }
443 if let Some(root) = root_ignore {
444 if root.ignores(entry) {
445 return true;
446 }
447 }
448 let Some(parent) = entry.path.parent() else {
449 return false;
450 };
451 if let Some(root) = root_ignore {
452 if parent == root.base {
453 return false;
454 }
455 }
456 if let Some((_, set)) = cache.iter().find(|(p, _)| p == parent) {
457 return set.ignores(entry);
458 }
459 let set = load_ignore_set(parent);
460 let ignored = set.ignores(entry);
461 cache.push((parent.to_path_buf(), set));
462 ignored
463}
464
465fn sort_recursive_sections(entries: &mut [Entry], opts: &ListOptions) {
466 let mut start = 0;
467 while start < entries.len() {
468 if entries[start].is_dir_header {
470 let section_start = start + 1;
471 let mut end = section_start;
472 while end < entries.len() && !entries[end].is_dir_header {
473 end += 1;
474 }
475 sort_entries(&mut entries[section_start..end], opts);
476 start = end;
477 } else {
478 let mut end = start;
480 while end < entries.len() && !entries[end].is_dir_header {
481 end += 1;
482 }
483 sort_entries(&mut entries[start..end], opts);
484 start = end;
485 }
486 }
487}
488
489#[derive(Debug)]
491pub struct ListOutcome {
492 pub listings: Vec<Listing>,
493 pub path_errors: Vec<Error>,
495 pub minor_errors: usize,
497}
498
499impl ListOutcome {
500 pub fn exit_code(&self) -> i32 {
502 if !self.path_errors.is_empty() {
503 2
504 } else if self.minor_errors > 0 || self.listings.iter().any(|l| l.minor_errors > 0) {
505 1
506 } else {
507 0
508 }
509 }
510
511 pub fn total_timing(&self) -> ListTiming {
513 let mut total = ListTiming::default();
514 for l in &self.listings {
515 if let Some(ref t) = l.timing {
516 total.add_assign(t);
517 }
518 }
519 total
520 }
521}
522
523pub fn list_paths(paths: &[PathBuf], opts: &ListOptions) -> Result<Vec<Listing>> {
528 let outcome = list_paths_with_errors(paths, opts);
529 if let Some(err) = outcome.path_errors.into_iter().next() {
530 return Err(err);
532 }
533 Ok(outcome.listings)
534}
535
536pub fn list_paths_with_errors(paths: &[PathBuf], opts: &ListOptions) -> ListOutcome {
538 let owned: Vec<PathBuf>;
539 let paths: &[PathBuf] = if paths.is_empty() {
540 owned = vec![PathBuf::from(".")];
541 &owned
542 } else {
543 paths
544 };
545
546 let mut listings = Vec::with_capacity(paths.len());
547 let mut path_errors = Vec::new();
548 let mut minor_errors = 0usize;
549
550 for p in paths {
551 match list_path(p, opts) {
552 Ok(l) => {
553 minor_errors += l.minor_errors;
554 listings.push(l);
555 }
556 Err(e) => path_errors.push(e),
557 }
558 }
559
560 ListOutcome {
561 listings,
562 path_errors,
563 minor_errors,
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use std::fs;
571
572 fn temp_dir() -> PathBuf {
573 use std::sync::atomic::{AtomicU64, Ordering};
574 static N: AtomicU64 = AtomicU64::new(0);
575 let base = std::env::temp_dir().join(format!(
576 "f00-core-list-{}-{}-{}",
577 std::process::id(),
578 std::time::SystemTime::now()
579 .duration_since(std::time::UNIX_EPOCH)
580 .map(|d| d.as_nanos())
581 .unwrap_or(0),
582 N.fetch_add(1, Ordering::Relaxed)
583 ));
584 fs::create_dir_all(&base).unwrap();
585 base
586 }
587
588 #[test]
589 fn list_paths_with_errors_partial_success() {
590 let dir = temp_dir();
591 fs::write(dir.join("a.txt"), b"x").unwrap();
592 let missing = dir.join("gone");
593 let opts = ListOptions::default();
594 let outcome = list_paths_with_errors(&[dir.clone(), missing], &opts);
595 assert_eq!(outcome.listings.len(), 1);
596 assert_eq!(outcome.path_errors.len(), 1);
597 assert_eq!(outcome.exit_code(), 2);
598 let _ = fs::remove_dir_all(&dir);
599 }
600
601 #[test]
602 fn list_paths_ok_exit_0() {
603 let dir = temp_dir();
604 fs::write(dir.join("a.txt"), b"x").unwrap();
605 let opts = ListOptions::default();
606 let outcome = list_paths_with_errors(std::slice::from_ref(&dir), &opts);
607 assert!(outcome.path_errors.is_empty());
608 assert_eq!(outcome.exit_code(), 0);
609 let _ = fs::remove_dir_all(&dir);
610 }
611
612 #[test]
613 fn list_directory_honors_ignore_files() {
614 let dir = temp_dir();
615 fs::write(dir.join("keep.txt"), b"k").unwrap();
616 fs::write(dir.join("skip.o"), b"o").unwrap();
617 fs::write(dir.join(".gitignore"), "*.o\n").unwrap();
618
619 let opts = ListOptions {
620 use_ignore_files: true,
621 ..Default::default()
622 };
623 let listing = list_directory(&dir, &opts).unwrap();
624 let names: Vec<_> = listing.entries.iter().map(|e| e.name.as_str()).collect();
625 assert!(names.contains(&"keep.txt"));
626 assert!(!names.contains(&"skip.o"));
627
628 let opts_off = ListOptions {
629 use_ignore_files: false,
630 ..Default::default()
631 };
632 let listing = list_directory(&dir, &opts_off).unwrap();
633 let names: Vec<_> = listing.entries.iter().map(|e| e.name.as_str()).collect();
634 assert!(names.contains(&"skip.o"));
635
636 let _ = fs::remove_dir_all(&dir);
637 }
638
639 #[test]
640 fn parallel_list_same_names_as_sequential() {
641 let dir = temp_dir();
642 let mut expected = Vec::new();
644 for i in 0..64 {
645 let name = format!("file_{i:03}.txt");
646 fs::write(dir.join(&name), b"x").unwrap();
647 expected.push(name);
648 }
649 fs::create_dir(dir.join("subdir")).unwrap();
650 expected.push("subdir".into());
651 expected.sort();
652
653 let sequential = ListOptions {
654 parallel: false,
655 threads: 1,
656 ..Default::default()
657 };
658 let seq = list_directory(&dir, &sequential).unwrap();
659 let mut seq_names: Vec<_> = seq.entries.iter().map(|e| e.name.clone()).collect();
660 seq_names.sort();
661 assert_eq!(
662 seq_names, expected,
663 "listing must include every created entry"
664 );
665
666 if !cfg!(target_os = "freebsd") {
668 let parallel = ListOptions {
669 parallel: true,
670 threads: 0,
671 ..Default::default()
672 };
673 let par = list_directory(&dir, ¶llel).unwrap();
674 let mut par_names: Vec<_> = par.entries.iter().map(|e| e.name.clone()).collect();
675 par_names.sort();
676 assert_eq!(
677 seq_names, par_names,
678 "parallel and sequential must produce identical ordered names"
679 );
680 }
681
682 let _ = fs::remove_dir_all(&dir);
683 }
684
685 #[test]
686 fn collect_timing_fills_phases() {
687 let dir = temp_dir();
688 for i in 0..8 {
689 fs::write(dir.join(format!("f{i}")), b"x").unwrap();
690 }
691 let opts = ListOptions {
692 collect_timing: true,
693 parallel: false,
694 threads: 1,
695 ..Default::default()
696 };
697 let listing = list_directory(&dir, &opts).unwrap();
698 let t = listing.timing.expect("timing present");
699 let _ = (t.readdir_ms, t.stat_ms, t.sort_ms);
701 let _ = fs::remove_dir_all(&dir);
702 }
703
704 #[test]
705 fn threads_one_forces_serial_path() {
706 let dir = temp_dir();
707 for i in 0..40 {
708 fs::write(dir.join(format!("n{i}")), b"x").unwrap();
709 }
710 let opts = ListOptions {
711 parallel: true,
712 threads: 1,
713 ..Default::default()
714 };
715 assert!(!opts.use_parallel_stat(40));
716 let listing = list_directory(&dir, &opts).unwrap();
717 assert_eq!(listing.entries.len(), 40);
718 let _ = fs::remove_dir_all(&dir);
719 }
720}