Skip to content

Commit 948e042

Browse files
committed
fix(ls): replace recursion with DFS
Changes ls to use a Depth-First Search (DFS) algorithm instead of recursion. Fixes #8725 and should help towards fixing #11215; this also opens the door for greater optimizations that fully fix the latter.
1 parent 7609966 commit 948e042

1 file changed

Lines changed: 114 additions & 95 deletions

File tree

src/uu/ls/src/ls.rs

Lines changed: 114 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2189,6 +2189,8 @@ fn push_basic_escape(buf: &mut String, byte: u8) {
21892189
}
21902190
}
21912191

2192+
type DirData = (PathBuf, bool);
2193+
21922194
// A struct to encapsulate state that is passed around from `list` functions.
21932195
struct ListState<'a> {
21942196
out: BufWriter<Stdout>,
@@ -2203,6 +2205,9 @@ struct ListState<'a> {
22032205
#[cfg(unix)]
22042206
gid_cache: FxHashMap<u32, String>,
22052207
recent_time_range: RangeInclusive<SystemTime>,
2208+
stack: Vec<DirData>,
2209+
listed_ancestors: FxHashSet<FileInformation>,
2210+
initial_locs_len: usize,
22062211
}
22072212

22082213
#[allow(clippy::cognitive_complexity)]
@@ -2224,6 +2229,9 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
22242229
// According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
22252230
recent_time_range: (SystemTime::now() - Duration::new(31_556_952 / 2, 0))
22262231
..=SystemTime::now(),
2232+
stack: Vec::new(),
2233+
listed_ancestors: FxHashSet::default(),
2234+
initial_locs_len,
22272235
};
22282236

22292237
for loc in locs {
@@ -2267,7 +2275,10 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
22672275

22682276
display_items(&files, config, &mut state, &mut dired)?;
22692277

2278+
let mut buf: Vec<PathData> = Vec::new();
2279+
22702280
for (pos, path_data) in dirs.iter().enumerate() {
2281+
let needs_blank_line = pos != 0 || !files.is_empty();
22712282
// Do read_dir call here to match GNU semantics by printing
22722283
// read_dir errors before directory headings, names and totals
22732284
let read_dir = match fs::read_dir(path_data.path()) {
@@ -2284,41 +2295,44 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
22842295
Ok(rd) => rd,
22852296
};
22862297

2287-
// Print dir heading - name... 'total' comes after error display
2288-
if initial_locs_len > 1 || config.recursive {
2289-
let needs_blank_line = !(pos.eq(&0usize) && files.is_empty());
2290-
if needs_blank_line {
2291-
writeln!(state.out)?;
2292-
if config.dired {
2293-
dired.padding += 1;
2294-
}
2295-
}
2296-
if config.dired {
2297-
dired::indent(&mut state.out)?;
2298-
}
2299-
show_dir_name(path_data, &mut state.out, config)?;
2300-
writeln!(state.out)?;
2301-
if config.dired {
2302-
let dir_len = path_data.display_name().len();
2303-
// add the //SUBDIRED// coordinates
2304-
dired::calculate_subdired(&mut dired, dir_len);
2305-
// Add the padding for the dir name
2306-
dired::add_dir_name(&mut dired, dir_len);
2307-
}
2308-
}
2309-
let mut listed_ancestors = FxHashSet::default();
2310-
listed_ancestors.insert(FileInformation::from_path(
2298+
state.listed_ancestors.insert(FileInformation::from_path(
23112299
path_data.path(),
23122300
path_data.must_dereference,
23132301
)?);
2314-
enter_directory(
2315-
path_data,
2302+
2303+
// List each of the arguments to ls first.
2304+
depth_first_list(
2305+
(path_data.path().to_path_buf(), needs_blank_line),
23162306
read_dir,
23172307
config,
23182308
&mut state,
2319-
&mut listed_ancestors,
2309+
&mut buf,
23202310
&mut dired,
2311+
true,
23212312
)?;
2313+
2314+
// Only runs if it must list recursively.
2315+
while let Some(dir_data) = state.stack.pop() {
2316+
let read_dir = match fs::read_dir(&dir_data.0) {
2317+
Err(err) => {
2318+
// flush stdout buffer before the error to preserve formatting and order
2319+
state.out.flush()?;
2320+
show!(LsError::IOErrorContext(
2321+
path_data.path().to_path_buf(),
2322+
err,
2323+
path_data.command_line
2324+
));
2325+
continue;
2326+
}
2327+
Ok(rd) => rd,
2328+
};
2329+
depth_first_list(
2330+
dir_data, read_dir, config, &mut state, &mut buf, &mut dired, false,
2331+
)?;
2332+
}
2333+
2334+
// No need to clear state.buf since [`enter_directory`] drains it.
2335+
state.listed_ancestors.clear();
23222336
}
23232337
if config.dired && !config.hyperlink {
23242338
dired::print_dired_output(config, &dired, &mut state.out)?;
@@ -2435,18 +2449,57 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool {
24352449
.any(|p| p.matches_with(&file_name, options))
24362450
}
24372451

2438-
#[allow(clippy::cognitive_complexity)]
2439-
fn enter_directory(
2440-
path_data: &PathData,
2452+
fn depth_first_list(
2453+
(dir_path, needs_blank_line): DirData,
24412454
mut read_dir: ReadDir,
24422455
config: &Config,
24432456
state: &mut ListState,
2444-
listed_ancestors: &mut FxHashSet<FileInformation>,
2457+
buf: &mut Vec<PathData>,
24452458
dired: &mut DiredOutput,
2459+
is_top_level: bool,
24462460
) -> UResult<()> {
2447-
// Create vec of entries with initial dot files
2448-
let mut entries: Vec<PathData> = if config.files == Files::All {
2449-
vec![
2461+
let path_data = PathData::new(dir_path, None, None, config, false);
2462+
2463+
// Print dir heading - name... 'total' comes after error display
2464+
if state.initial_locs_len > 1 || config.recursive {
2465+
if is_top_level {
2466+
if needs_blank_line {
2467+
writeln!(state.out)?;
2468+
if config.dired {
2469+
dired.padding += 1;
2470+
}
2471+
}
2472+
if config.dired {
2473+
dired::indent(&mut state.out)?;
2474+
}
2475+
show_dir_name(&path_data, &mut state.out, config)?;
2476+
writeln!(state.out)?;
2477+
if config.dired {
2478+
let dir_len = path_data.path().as_os_str().len();
2479+
// add the //SUBDIRED// coordinates
2480+
dired::calculate_subdired(dired, dir_len);
2481+
// Add the padding for the dir name
2482+
dired::add_dir_name(dired, dir_len);
2483+
}
2484+
} else {
2485+
writeln!(state.out)?;
2486+
if config.dired {
2487+
dired.padding += 1;
2488+
dired::indent(&mut state.out)?;
2489+
let dir_name_size = path_data.path().as_os_str().len();
2490+
dired::calculate_subdired(dired, dir_name_size);
2491+
dired::add_dir_name(dired, dir_name_size);
2492+
}
2493+
show_dir_name(&path_data, &mut state.out, config)?;
2494+
writeln!(state.out)?;
2495+
}
2496+
}
2497+
2498+
buf.clear();
2499+
// Append entries with initial dot files and record their existence
2500+
let trim = if config.files == Files::All {
2501+
const DOT_DIRECTORIES: usize = 2;
2502+
buf.extend::<[_; DOT_DIRECTORIES]>([
24502503
PathData::new(
24512504
path_data.path().to_path_buf(),
24522505
None,
@@ -2461,95 +2514,61 @@ fn enter_directory(
24612514
config,
24622515
false,
24632516
),
2464-
]
2517+
]);
2518+
DOT_DIRECTORIES
24652519
} else {
2466-
vec![]
2520+
0
24672521
};
24682522

24692523
// Convert those entries to the PathData struct
24702524
for raw_entry in read_dir.by_ref() {
2471-
let dir_entry = match raw_entry {
2472-
Ok(path) => path,
2525+
match raw_entry {
2526+
Ok(dir_entry) => {
2527+
if should_display(&dir_entry, config) {
2528+
buf.push(PathData::new(
2529+
dir_entry.path(),
2530+
Some(dir_entry),
2531+
None,
2532+
config,
2533+
false,
2534+
));
2535+
}
2536+
}
24732537
Err(err) => {
24742538
state.out.flush()?;
24752539
show!(LsError::IOError(err));
2476-
continue;
24772540
}
2478-
};
2479-
2480-
if should_display(&dir_entry, config) {
2481-
let entry_path_data =
2482-
PathData::new(dir_entry.path(), Some(dir_entry), None, config, false);
2483-
entries.push(entry_path_data);
24842541
}
24852542
}
24862543

2487-
sort_entries(&mut entries, config);
2544+
sort_entries(buf, config);
24882545

2489-
// Print total after any error display
24902546
if config.format == Format::Long || config.alloc_size {
2491-
let total = return_total(&entries, config, &mut state.out)?;
2547+
let total = return_total(buf, config, &mut state.out)?;
24922548
write!(state.out, "{}", total.as_str())?;
24932549
if config.dired {
24942550
dired::add_total(dired, total.len());
24952551
}
24962552
}
24972553

2498-
display_items(&entries, config, state, dired)?;
2554+
display_items(buf, config, state, dired)?;
24992555

25002556
if config.recursive {
2501-
// release the open fd before recursing to not run out of resources
2502-
for entry in &entries {
2503-
entry.de.take();
2504-
}
2505-
drop(read_dir);
2506-
for e in entries
2557+
for e in buf
25072558
.iter()
2508-
.skip(if config.files == Files::All { 2 } else { 0 })
2559+
.skip(trim)
25092560
.filter(|p| p.file_type().is_some_and(FileType::is_dir))
2561+
.rev()
25102562
{
2511-
match fs::read_dir(e.path()) {
2512-
Err(err) => {
2513-
state.out.flush()?;
2514-
show!(LsError::IOErrorContext(
2515-
e.path().to_path_buf(),
2516-
err,
2517-
e.command_line
2518-
));
2519-
}
2520-
Ok(rd) => {
2521-
if listed_ancestors
2522-
.insert(FileInformation::from_path(e.path(), e.must_dereference)?)
2523-
{
2524-
// when listing several directories in recursive mode, we show
2525-
// "dirname:" at the beginning of the file list
2526-
writeln!(state.out)?;
2527-
if config.dired {
2528-
// We already injected the first dir
2529-
// Continue with the others
2530-
// blank line between directory sections
2531-
dired.padding += 1;
2532-
dired::indent(&mut state.out)?;
2533-
let dir_name_size = e.path().as_os_str().len();
2534-
dired::calculate_subdired(dired, dir_name_size);
2535-
// inject dir name
2536-
dired::add_dir_name(dired, dir_name_size);
2537-
}
2538-
2539-
show_dir_name(e, &mut state.out, config)?;
2540-
writeln!(state.out)?;
2541-
enter_directory(e, rd, config, state, listed_ancestors, dired)?;
2542-
listed_ancestors
2543-
.remove(&FileInformation::from_path(e.path(), e.must_dereference)?);
2544-
} else {
2545-
state.out.flush()?;
2546-
show!(LsError::AlreadyListedError(e.path().to_path_buf()));
2547-
}
2548-
}
2563+
let fi = FileInformation::from_path(e.path(), e.must_dereference)?;
2564+
if state.listed_ancestors.insert(fi) {
2565+
state.stack.push((e.path().to_path_buf(), true));
2566+
} else {
2567+
state.out.flush()?;
2568+
show!(LsError::AlreadyListedError(e.path().to_path_buf()));
25492569
}
25502570
}
25512571
}
2552-
25532572
Ok(())
25542573
}
25552574

0 commit comments

Comments
 (0)