use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;
use rayon::prelude::*;
use walkdir::WalkDir;
use crate::entry::Entry;
use crate::error::{Error, Result};
use crate::filter::filter_entries;
use crate::ignore::{apply_ignore_set, load_ignore_set, IgnoreSet};
use crate::options::{CliSymlinkMode, ListOptions};
use crate::sort::sort_entries;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ListTiming {
pub readdir_ms: u128,
pub stat_ms: u128,
pub sort_ms: u128,
}
impl ListTiming {
pub fn add_assign(&mut self, other: &ListTiming) {
self.readdir_ms = self.readdir_ms.saturating_add(other.readdir_ms);
self.stat_ms = self.stat_ms.saturating_add(other.stat_ms);
self.sort_ms = self.sort_ms.saturating_add(other.sort_ms);
}
}
#[derive(Debug, Clone)]
pub struct Listing {
pub root: PathBuf,
pub root_is_dir: bool,
pub entries: Vec<Entry>,
pub minor_errors: usize,
pub timing: Option<ListTiming>,
}
impl Listing {
fn new(root: PathBuf, root_is_dir: bool, entries: Vec<Entry>) -> Self {
Self {
root,
root_is_dir,
entries,
minor_errors: 0,
timing: None,
}
}
fn with_timing(mut self, timing: Option<ListTiming>) -> Self {
self.timing = timing;
self
}
}
fn follow_cli_path(path: &Path, opts: &ListOptions) -> bool {
if opts.follow_links {
return true;
}
match opts.cli_symlink {
CliSymlinkMode::Never => false,
CliSymlinkMode::Always => {
fs::symlink_metadata(path)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
}
CliSymlinkMode::DirOnly => {
let is_link = fs::symlink_metadata(path)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false);
if !is_link {
return false;
}
fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
}
}
}
pub fn list_path(path: &Path, opts: &ListOptions) -> Result<Listing> {
let path = if path.as_os_str().is_empty() {
Path::new(".")
} else {
path
};
let follow = follow_cli_path(path, opts);
let meta = if follow {
fs::metadata(path)
} else {
fs::symlink_metadata(path)
}
.map_err(|source| {
if source.kind() == std::io::ErrorKind::NotFound {
Error::NotFound(path.to_path_buf())
} else {
Error::Metadata {
path: path.to_path_buf(),
source,
}
}
})?;
if opts.directory || !meta.is_dir() {
let fill = meta_fill_from(opts);
let entry = if follow {
Entry::from_path_follow_with(path, 0, fill)?
} else {
Entry::from_path_and_meta_with(path, &meta, 0, fill)?
};
return Ok(Listing::new(path.to_path_buf(), meta.is_dir(), vec![entry]));
}
if opts.recursive {
list_recursive(path, opts)
} else {
list_directory(path, opts)
}
}
fn meta_fill_from(opts: &ListOptions) -> crate::entry::MetaFill {
crate::entry::MetaFill {
resolve_names: opts.resolve_owner_group,
read_context: opts.read_selinux,
}
}
fn entry_from_dir_entry(
item: &fs::DirEntry,
follow_links: bool,
fill: crate::entry::MetaFill,
prefer_statx: bool,
) -> Option<Entry> {
let entry_path = item.path();
if follow_links {
return Entry::from_path_follow_with(&entry_path, 0, fill).ok();
}
#[cfg(target_os = "linux")]
{
if prefer_statx {
if let Ok(e) = crate::linux_statx::entry_from_statx(&entry_path, 0, fill) {
return Some(e);
}
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = prefer_statx;
}
let meta = item
.metadata()
.or_else(|_| fs::symlink_metadata(&entry_path));
match meta {
Ok(m) => Entry::from_path_and_meta_with(&entry_path, &m, 0, fill).ok(),
Err(_) => None,
}
}
fn stat_dir_entries(dir_entries: &[fs::DirEntry], opts: &ListOptions) -> Vec<Entry> {
let follow = opts.follow_links;
let fill = meta_fill_from(opts);
let prefer_statx = opts.linux_statx;
let map_one = |item: &fs::DirEntry| entry_from_dir_entry(item, follow, fill, prefer_statx);
#[cfg(all(target_os = "linux", feature = "io-uring"))]
{
if opts.io_uring
&& !follow
&& dir_entries.len() >= crate::io_uring_stat::IO_URING_THRESHOLD
&& !fill.resolve_names
&& !fill.read_context
{
let paths: Vec<_> = dir_entries.iter().map(|d| d.path()).collect();
if let Some(entries) = crate::io_uring_stat::entries_from_paths_uring(&paths, fill) {
if entries.len() * 10 >= paths.len() * 8 {
return entries;
}
}
}
}
if !opts.use_parallel_stat(dir_entries.len()) {
return dir_entries.iter().filter_map(map_one).collect();
}
let collect = || dir_entries.par_iter().filter_map(map_one).collect();
if opts.threads > 1 {
match rayon::ThreadPoolBuilder::new()
.num_threads(opts.threads)
.build()
{
Ok(pool) => pool.install(collect),
Err(_) => collect(),
}
} else {
collect()
}
}
pub fn list_directory(path: &Path, opts: &ListOptions) -> Result<Listing> {
let collect_timing = opts.collect_timing;
let mut timing = ListTiming::default();
let mut entries = Vec::new();
let fill = meta_fill_from(opts);
if opts.all {
if let Ok(dot) = Entry::from_path_with(path, 0, fill) {
let mut e = dot;
e.name = ".".to_string();
entries.push(e);
}
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
if let Ok(mut e) = Entry::from_path_with(parent, 0, fill) {
e.name = "..".to_string();
e.path = parent.to_path_buf();
entries.push(e);
}
} else if let Ok(mut e) = Entry::from_path_with(path, 0, fill) {
e.name = "..".to_string();
entries.push(e);
}
}
let t_readdir = if collect_timing {
Some(Instant::now())
} else {
None
};
let read = fs::read_dir(path).map_err(|source| Error::ReadDir {
path: path.to_path_buf(),
source,
})?;
let mut dir_entries = Vec::new();
for item in read {
let item = item.map_err(|source| Error::ReadDir {
path: path.to_path_buf(),
source,
})?;
dir_entries.push(item);
}
if let Some(t0) = t_readdir {
timing.readdir_ms = t0.elapsed().as_millis();
}
let t_stat = if collect_timing {
Some(Instant::now())
} else {
None
};
let children = stat_dir_entries(&dir_entries, opts);
entries.extend(children);
if let Some(t0) = t_stat {
timing.stat_ms = t0.elapsed().as_millis();
}
let t_sort = if collect_timing {
Some(Instant::now())
} else {
None
};
filter_entries(&mut entries, opts);
if opts.use_ignore_files {
let set = load_ignore_set(path);
apply_ignore_set(&mut entries, &set);
}
sort_entries(&mut entries, opts);
if let Some(t0) = t_sort {
timing.sort_ms = t0.elapsed().as_millis();
}
Ok(
Listing::new(path.to_path_buf(), true, entries).with_timing(if collect_timing {
Some(timing)
} else {
None
}),
)
}
struct Walked {
path: PathBuf,
depth: usize,
is_dir: bool,
}
pub fn list_recursive(path: &Path, opts: &ListOptions) -> Result<Listing> {
let collect_timing = opts.collect_timing;
let mut timing = ListTiming::default();
let mut minor_errors = 0usize;
let max_depth = opts.max_depth.unwrap_or(usize::MAX);
let root_ignore = if opts.use_ignore_files {
Some(load_ignore_set(path))
} else {
None
};
let t_walk = if collect_timing {
Some(Instant::now())
} else {
None
};
let walker = WalkDir::new(path)
.follow_links(opts.follow_links)
.max_depth(max_depth)
.sort_by_file_name();
let mut walked: Vec<Walked> = Vec::new();
for item in walker {
let item = match item {
Ok(i) => i,
Err(_) => {
minor_errors += 1;
continue;
}
};
let depth = item.depth();
if depth == 0 {
continue; }
walked.push(Walked {
path: item.path().to_path_buf(),
depth,
is_dir: item.file_type().is_dir(),
});
}
if let Some(t0) = t_walk {
timing.readdir_ms = t0.elapsed().as_millis();
}
let fill = meta_fill_from(opts);
let t_stat = if collect_timing {
Some(Instant::now())
} else {
None
};
let use_parallel = opts.use_parallel_stat(walked.len().max(1));
let built: Vec<Option<Entry>> = if use_parallel {
let map = |w: &Walked| {
Entry::from_path_with(&w.path, w.depth, fill)
.ok()
.filter(|e| crate::filter::should_show(e, opts))
};
if opts.threads > 1 {
match rayon::ThreadPoolBuilder::new()
.num_threads(opts.threads)
.build()
{
Ok(pool) => pool.install(|| walked.par_iter().map(map).collect()),
Err(_) => walked.par_iter().map(map).collect(),
}
} else {
walked.par_iter().map(map).collect()
}
} else {
walked
.iter()
.map(|w| {
Entry::from_path_with(&w.path, w.depth, fill)
.ok()
.filter(|e| crate::filter::should_show(e, opts))
})
.collect()
};
if let Some(t0) = t_stat {
timing.stat_ms = t0.elapsed().as_millis();
}
let mut ignore_by_dir: Vec<(PathBuf, IgnoreSet)> = Vec::new();
let mut entries: Vec<Entry> = Vec::with_capacity(built.len().saturating_add(16));
if opts.emit_dir_headers {
entries.push(Entry::dir_header(path, 0));
}
for (w, maybe) in walked.iter().zip(built) {
let Some(e) = maybe else {
continue;
};
if ignored_by_sets(&e, opts, root_ignore.as_ref(), &mut ignore_by_dir) {
continue;
}
if opts.emit_dir_headers && w.is_dir {
entries.push(e);
entries.push(Entry::dir_header(&w.path, w.depth));
} else {
entries.push(e);
}
}
let t_sort = if collect_timing {
Some(Instant::now())
} else {
None
};
if opts.emit_dir_headers {
sort_recursive_sections(&mut entries, opts);
} else {
if !matches!(opts.sort_by, crate::options::SortBy::Name) || opts.reverse {
sort_tree_preorder(&mut entries, opts);
}
}
if let Some(t0) = t_sort {
timing.sort_ms = t0.elapsed().as_millis();
}
let mut listing = Listing::new(path.to_path_buf(), true, entries)
.with_timing(if collect_timing { Some(timing) } else { None });
listing.minor_errors = minor_errors;
Ok(listing)
}
fn sort_tree_preorder(entries: &mut [Entry], opts: &ListOptions) {
if entries.is_empty() {
return;
}
let mut i = 0;
while i < entries.len() {
let depth = entries[i].depth;
let parent = entries[i].path.parent().map(Path::to_path_buf);
let mut j = i + 1;
while j < entries.len() {
if entries[j].depth < depth {
break;
}
if entries[j].depth == depth {
let p = entries[j].path.parent().map(Path::to_path_buf);
if p != parent {
break;
}
}
j += 1;
}
let sib: Vec<usize> = (i..j).filter(|&k| entries[k].depth == depth).collect();
if sib.len() > 1 {
let mut blocks: Vec<Vec<Entry>> = Vec::with_capacity(sib.len());
for (bi, &start) in sib.iter().enumerate() {
let end = sib.get(bi + 1).copied().unwrap_or(j);
blocks.push(entries[start..end].to_vec());
}
blocks.sort_by(|a, b| crate::sort::cmp_entry(&a[0], &b[0], opts));
let mut out = Vec::with_capacity(j - i);
for b in blocks {
out.extend(b);
}
entries[i..j].clone_from_slice(&out);
}
i = if j > i { j } else { i + 1 };
}
}
fn ignored_by_sets(
entry: &Entry,
opts: &ListOptions,
root_ignore: Option<&IgnoreSet>,
cache: &mut Vec<(PathBuf, IgnoreSet)>,
) -> bool {
if !opts.use_ignore_files {
return false;
}
if let Some(root) = root_ignore {
if root.ignores(entry) {
return true;
}
}
let Some(parent) = entry.path.parent() else {
return false;
};
if let Some(root) = root_ignore {
if parent == root.base {
return false;
}
}
if let Some((_, set)) = cache.iter().find(|(p, _)| p == parent) {
return set.ignores(entry);
}
let set = load_ignore_set(parent);
let ignored = set.ignores(entry);
cache.push((parent.to_path_buf(), set));
ignored
}
fn sort_recursive_sections(entries: &mut [Entry], opts: &ListOptions) {
let mut start = 0;
while start < entries.len() {
if entries[start].is_dir_header {
let section_start = start + 1;
let mut end = section_start;
while end < entries.len() && !entries[end].is_dir_header {
end += 1;
}
sort_entries(&mut entries[section_start..end], opts);
start = end;
} else {
let mut end = start;
while end < entries.len() && !entries[end].is_dir_header {
end += 1;
}
sort_entries(&mut entries[start..end], opts);
start = end;
}
}
}
#[derive(Debug)]
pub struct ListOutcome {
pub listings: Vec<Listing>,
pub path_errors: Vec<Error>,
pub minor_errors: usize,
}
impl ListOutcome {
pub fn exit_code(&self) -> i32 {
if !self.path_errors.is_empty() {
2
} else if self.minor_errors > 0 || self.listings.iter().any(|l| l.minor_errors > 0) {
1
} else {
0
}
}
pub fn total_timing(&self) -> ListTiming {
let mut total = ListTiming::default();
for l in &self.listings {
if let Some(ref t) = l.timing {
total.add_assign(t);
}
}
total
}
}
pub fn list_paths(paths: &[PathBuf], opts: &ListOptions) -> Result<Vec<Listing>> {
let outcome = list_paths_with_errors(paths, opts);
if let Some(err) = outcome.path_errors.into_iter().next() {
return Err(err);
}
Ok(outcome.listings)
}
pub fn list_paths_with_errors(paths: &[PathBuf], opts: &ListOptions) -> ListOutcome {
let owned: Vec<PathBuf>;
let paths: &[PathBuf] = if paths.is_empty() {
owned = vec![PathBuf::from(".")];
&owned
} else {
paths
};
let mut listings = Vec::with_capacity(paths.len());
let mut path_errors = Vec::new();
let mut minor_errors = 0usize;
for p in paths {
match list_path(p, opts) {
Ok(l) => {
minor_errors += l.minor_errors;
listings.push(l);
}
Err(e) => path_errors.push(e),
}
}
ListOutcome {
listings,
path_errors,
minor_errors,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn temp_dir() -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
let base = std::env::temp_dir().join(format!(
"f00-core-list-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0),
N.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&base).unwrap();
base
}
#[test]
fn list_paths_with_errors_partial_success() {
let dir = temp_dir();
fs::write(dir.join("a.txt"), b"x").unwrap();
let missing = dir.join("gone");
let opts = ListOptions::default();
let outcome = list_paths_with_errors(&[dir.clone(), missing], &opts);
assert_eq!(outcome.listings.len(), 1);
assert_eq!(outcome.path_errors.len(), 1);
assert_eq!(outcome.exit_code(), 2);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn list_paths_ok_exit_0() {
let dir = temp_dir();
fs::write(dir.join("a.txt"), b"x").unwrap();
let opts = ListOptions::default();
let outcome = list_paths_with_errors(std::slice::from_ref(&dir), &opts);
assert!(outcome.path_errors.is_empty());
assert_eq!(outcome.exit_code(), 0);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn list_directory_honors_ignore_files() {
let dir = temp_dir();
fs::write(dir.join("keep.txt"), b"k").unwrap();
fs::write(dir.join("skip.o"), b"o").unwrap();
fs::write(dir.join(".gitignore"), "*.o\n").unwrap();
let opts = ListOptions {
use_ignore_files: true,
..Default::default()
};
let listing = list_directory(&dir, &opts).unwrap();
let names: Vec<_> = listing.entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"keep.txt"));
assert!(!names.contains(&"skip.o"));
let opts_off = ListOptions {
use_ignore_files: false,
..Default::default()
};
let listing = list_directory(&dir, &opts_off).unwrap();
let names: Vec<_> = listing.entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"skip.o"));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn parallel_list_same_names_as_sequential() {
let dir = temp_dir();
let mut expected = Vec::new();
for i in 0..64 {
let name = format!("file_{i:03}.txt");
fs::write(dir.join(&name), b"x").unwrap();
expected.push(name);
}
fs::create_dir(dir.join("subdir")).unwrap();
expected.push("subdir".into());
expected.sort();
let sequential = ListOptions {
parallel: false,
threads: 1,
..Default::default()
};
let seq = list_directory(&dir, &sequential).unwrap();
let mut seq_names: Vec<_> = seq.entries.iter().map(|e| e.name.clone()).collect();
seq_names.sort();
assert_eq!(
seq_names, expected,
"listing must include every created entry"
);
if !cfg!(target_os = "freebsd") {
let parallel = ListOptions {
parallel: true,
threads: 0,
..Default::default()
};
let par = list_directory(&dir, ¶llel).unwrap();
let mut par_names: Vec<_> = par.entries.iter().map(|e| e.name.clone()).collect();
par_names.sort();
assert_eq!(
seq_names, par_names,
"parallel and sequential must produce identical ordered names"
);
}
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn collect_timing_fills_phases() {
let dir = temp_dir();
for i in 0..8 {
fs::write(dir.join(format!("f{i}")), b"x").unwrap();
}
let opts = ListOptions {
collect_timing: true,
parallel: false,
threads: 1,
..Default::default()
};
let listing = list_directory(&dir, &opts).unwrap();
let t = listing.timing.expect("timing present");
let _ = (t.readdir_ms, t.stat_ms, t.sort_ms);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn threads_one_forces_serial_path() {
let dir = temp_dir();
for i in 0..40 {
fs::write(dir.join(format!("n{i}")), b"x").unwrap();
}
let opts = ListOptions {
parallel: true,
threads: 1,
..Default::default()
};
assert!(!opts.use_parallel_stat(40));
let listing = list_directory(&dir, &opts).unwrap();
assert_eq!(listing.entries.len(), 40);
let _ = fs::remove_dir_all(&dir);
}
}