Skip to content

Commit c31cb0d

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 sh. Fixes #8725 and should help towards fixing #11215; this also opens the door for greater optimizations that fully fix the latter.
1 parent 5dcde30 commit c31cb0d

1 file changed

Lines changed: 117 additions & 82 deletions

File tree

src/uu/ls/src/ls.rs

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

2192+
#[derive(Debug)]
2193+
struct DirData {
2194+
dir_path: PathBuf,
2195+
read_dir: ReadDir,
2196+
needs_blank_line: bool,
2197+
}
2198+
2199+
impl DirData {
2200+
fn new(dir_path: PathBuf, read_dir: ReadDir, needs_blank_line: bool) -> Self {
2201+
Self {
2202+
dir_path,
2203+
read_dir,
2204+
needs_blank_line,
2205+
}
2206+
}
2207+
}
2208+
21922209
// A struct to encapsulate state that is passed around from `list` functions.
21932210
struct ListState<'a> {
21942211
out: BufWriter<Stdout>,
@@ -2203,6 +2220,9 @@ struct ListState<'a> {
22032220
#[cfg(unix)]
22042221
gid_cache: FxHashMap<u32, String>,
22052222
recent_time_range: RangeInclusive<SystemTime>,
2223+
stack: Vec<DirData>,
2224+
listed_ancestors: FxHashSet<FileInformation>,
2225+
initial_locs_len: usize,
22062226
}
22072227

22082228
#[allow(clippy::cognitive_complexity)]
@@ -2224,6 +2244,9 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
22242244
// According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
22252245
recent_time_range: (SystemTime::now() - Duration::new(31_556_952 / 2, 0))
22262246
..=SystemTime::now(),
2247+
stack: Vec::with_capacity(initial_locs_len),
2248+
listed_ancestors: FxHashSet::default(),
2249+
initial_locs_len,
22272250
};
22282251

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

22682291
display_items(&files, config, &mut state, &mut dired)?;
22692292

2293+
let mut buf: Vec<PathData> = Vec::new();
2294+
22702295
for (pos, path_data) in dirs.iter().enumerate() {
2296+
let needs_blank_line = pos != 0 || !files.is_empty();
22712297
// Do read_dir call here to match GNU semantics by printing
22722298
// read_dir errors before directory headings, names and totals
22732299
let read_dir = match fs::read_dir(path_data.path()) {
@@ -2284,41 +2310,28 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
22842310
Ok(rd) => rd,
22852311
};
22862312

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(
2313+
state.listed_ancestors.insert(FileInformation::from_path(
23112314
path_data.path(),
23122315
path_data.must_dereference,
23132316
)?);
2314-
enter_directory(
2315-
path_data,
2316-
read_dir,
2317+
2318+
// List each of the arguments to ls first.
2319+
depth_first_list(
2320+
DirData::new(path_data.path().to_path_buf(), read_dir, needs_blank_line),
23172321
config,
23182322
&mut state,
2319-
&mut listed_ancestors,
2323+
&mut buf,
23202324
&mut dired,
2325+
true,
23212326
)?;
2327+
2328+
// Only runs if it must list recursively
2329+
while let Some(dir_data) = state.stack.pop() {
2330+
depth_first_list(dir_data, config, &mut state, &mut buf, &mut dired, false)?;
2331+
}
2332+
2333+
// No need to clear state.buf since [`enter_directory`] drains it.
2334+
state.listed_ancestors.clear();
23222335
}
23232336
if config.dired && !config.hyperlink {
23242337
dired::print_dired_output(config, &dired, &mut state.out)?;
@@ -2435,18 +2448,60 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool {
24352448
.any(|p| p.matches_with(&file_name, options))
24362449
}
24372450

2438-
#[allow(clippy::cognitive_complexity)]
2439-
fn enter_directory(
2440-
path_data: &PathData,
2441-
mut read_dir: ReadDir,
2451+
fn depth_first_list(
2452+
DirData {
2453+
dir_path,
2454+
mut read_dir,
2455+
needs_blank_line,
2456+
}: DirData,
24422457
config: &Config,
24432458
state: &mut ListState,
2444-
listed_ancestors: &mut FxHashSet<FileInformation>,
2459+
buf: &mut Vec<PathData>,
24452460
dired: &mut DiredOutput,
2461+
is_top_level: bool,
24462462
) -> UResult<()> {
2447-
// Create vec of entries with initial dot files
2448-
let mut entries: Vec<PathData> = if config.files == Files::All {
2449-
vec![
2463+
let path_data = PathData::new(dir_path, None, None, config, false);
2464+
2465+
// Print dir heading - name... 'total' comes after error display
2466+
if state.initial_locs_len > 1 || config.recursive {
2467+
if is_top_level {
2468+
if needs_blank_line {
2469+
writeln!(state.out)?;
2470+
if config.dired {
2471+
dired.padding += 1;
2472+
}
2473+
}
2474+
if config.dired {
2475+
dired::indent(&mut state.out)?;
2476+
}
2477+
show_dir_name(&path_data, &mut state.out, config)?;
2478+
writeln!(state.out)?;
2479+
if config.dired {
2480+
let dir_len = path_data.path().as_os_str().len();
2481+
// add the //SUBDIRED// coordinates
2482+
dired::calculate_subdired(dired, dir_len);
2483+
// Add the padding for the dir name
2484+
dired::add_dir_name(dired, dir_len);
2485+
}
2486+
} else {
2487+
writeln!(state.out)?;
2488+
if config.dired {
2489+
dired.padding += 1;
2490+
dired::indent(&mut state.out)?;
2491+
let dir_name_size = path_data.path().as_os_str().len();
2492+
dired::calculate_subdired(dired, dir_name_size);
2493+
dired::add_dir_name(dired, dir_name_size);
2494+
}
2495+
show_dir_name(&path_data, &mut state.out, config)?;
2496+
writeln!(state.out)?;
2497+
}
2498+
}
2499+
2500+
buf.clear();
2501+
// Append entries with initial dot files and record their existence
2502+
let trim = if config.files == Files::All {
2503+
const DOT_DIRECTORIES: usize = 2;
2504+
buf.extend::<[_; DOT_DIRECTORIES]>([
24502505
PathData::new(
24512506
path_data.path().to_path_buf(),
24522507
None,
@@ -2461,52 +2516,51 @@ fn enter_directory(
24612516
config,
24622517
false,
24632518
),
2464-
]
2519+
]);
2520+
DOT_DIRECTORIES
24652521
} else {
2466-
vec![]
2522+
0
24672523
};
24682524

24692525
// Convert those entries to the PathData struct
24702526
for raw_entry in read_dir.by_ref() {
2471-
let dir_entry = match raw_entry {
2472-
Ok(path) => path,
2527+
match raw_entry {
2528+
Ok(dir_entry) => {
2529+
if should_display(&dir_entry, config) {
2530+
buf.push(PathData::new(
2531+
dir_entry.path(),
2532+
Some(dir_entry),
2533+
None,
2534+
config,
2535+
false,
2536+
));
2537+
}
2538+
}
24732539
Err(err) => {
24742540
state.out.flush()?;
24752541
show!(LsError::IOError(err));
2476-
continue;
24772542
}
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);
24842543
}
24852544
}
24862545

2487-
sort_entries(&mut entries, config);
2546+
sort_entries(buf, config);
24882547

2489-
// Print total after any error display
24902548
if config.format == Format::Long || config.alloc_size {
2491-
let total = return_total(&entries, config, &mut state.out)?;
2549+
let total = return_total(buf, config, &mut state.out)?;
24922550
write!(state.out, "{}", total.as_str())?;
24932551
if config.dired {
24942552
dired::add_total(dired, total.len());
24952553
}
24962554
}
24972555

2498-
display_items(&entries, config, state, dired)?;
2556+
display_items(buf, config, state, dired)?;
24992557

25002558
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
2559+
for e in buf
25072560
.iter()
2508-
.skip(if config.files == Files::All { 2 } else { 0 })
2561+
.skip(trim)
25092562
.filter(|p| p.file_type().is_some_and(FileType::is_dir))
2563+
.rev()
25102564
{
25112565
match fs::read_dir(e.path()) {
25122566
Err(err) => {
@@ -2518,29 +2572,11 @@ fn enter_directory(
25182572
));
25192573
}
25202574
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)?);
2575+
let fi = FileInformation::from_path(e.path(), e.must_dereference)?;
2576+
if state.listed_ancestors.insert(fi) {
2577+
state
2578+
.stack
2579+
.push(DirData::new(e.path().to_path_buf(), rd, true));
25442580
} else {
25452581
state.out.flush()?;
25462582
show!(LsError::AlreadyListedError(e.path().to_path_buf()));
@@ -2549,7 +2585,6 @@ fn enter_directory(
25492585
}
25502586
}
25512587
}
2552-
25532588
Ok(())
25542589
}
25552590

0 commit comments

Comments
 (0)