Compute depth for broken symlinks so --min-depth keeps them

When following links, a broken symlink is surfaced by the walker as an
error that carries no depth, so fd stored it with an unknown depth and
the --min-depth filter dropped it for any minimum value. Record the
depth when the entry is created by counting the path components relative
to the matching search root, which is what the issue discussion
suggested. Broken symlinks are now filtered by depth like any other
entry.

Fixes #1017
This commit is contained in:
hexbinoct
2026-08-10 14:30:46 +05:00
parent ee20f426dd
commit 350b33acda
4 changed files with 73 additions and 13 deletions
+1
View File
@@ -9,6 +9,7 @@
when output goes to a terminal, to prevent terminal escape-sequence injection.
Also reject a placeholder as the executable for `--exec-batch`, while still
allowing it for `--exec`.
- Fix broken symlinks being incorrectly filtered out by `--min-depth` when following links (`--follow`), because their depth was not computed; see #1017 (@hexbinoct).
- Handle invalid working directories gracefully when using `--full-path`, see #1900 (@Xavrir).
- Fire the "search pattern contains a path separator" diagnostic for any pattern containing `/`, not just patterns that happen to name an existing directory. Preserves the legacy Windows behaviour that also flags native `\` separators when the pattern resolves to a real directory. See #1873.
- Also fire the "search pattern contains a path separator" diagnostic for `--and` patterns, not only the primary positional pattern. `--and` patterns are matched against the file name just like the primary pattern, so a path separator in them silently returned zero results. See #1873.
+11 -9
View File
@@ -11,7 +11,9 @@ use crate::filesystem::strip_current_dir;
#[derive(Debug)]
enum DirEntryInner {
Normal(ignore::DirEntry),
BrokenSymlink(PathBuf),
// Broken symlinks are surfaced by the walker as errors that carry no depth,
// so we record the depth (relative to the search root) at creation time.
BrokenSymlink(PathBuf, usize),
}
#[derive(Debug)]
@@ -31,9 +33,9 @@ impl DirEntry {
}
}
pub fn broken_symlink(path: PathBuf) -> Self {
pub fn broken_symlink(path: PathBuf, depth: usize) -> Self {
Self {
inner: DirEntryInner::BrokenSymlink(path),
inner: DirEntryInner::BrokenSymlink(path, depth),
metadata: OnceCell::new(),
style: OnceCell::new(),
}
@@ -42,14 +44,14 @@ impl DirEntry {
pub fn path(&self) -> &Path {
match &self.inner {
DirEntryInner::Normal(e) => e.path(),
DirEntryInner::BrokenSymlink(pathbuf) => pathbuf.as_path(),
DirEntryInner::BrokenSymlink(pathbuf, _) => pathbuf.as_path(),
}
}
pub fn into_path(self) -> PathBuf {
match self.inner {
DirEntryInner::Normal(e) => e.into_path(),
DirEntryInner::BrokenSymlink(p) => p,
DirEntryInner::BrokenSymlink(p, _) => p,
}
}
@@ -82,7 +84,7 @@ impl DirEntry {
pub fn file_type(&self) -> Option<FileType> {
match &self.inner {
DirEntryInner::Normal(e) => e.file_type(),
DirEntryInner::BrokenSymlink(_) => self.metadata().map(|m| m.file_type()),
DirEntryInner::BrokenSymlink(..) => self.metadata().map(|m| m.file_type()),
}
}
@@ -90,7 +92,7 @@ impl DirEntry {
self.metadata
.get_or_init(|| match &self.inner {
DirEntryInner::Normal(e) => e.metadata().ok(),
DirEntryInner::BrokenSymlink(path) => path.symlink_metadata().ok(),
DirEntryInner::BrokenSymlink(path, _) => path.symlink_metadata().ok(),
})
.as_ref()
}
@@ -98,7 +100,7 @@ impl DirEntry {
pub fn depth(&self) -> Option<usize> {
match &self.inner {
DirEntryInner::Normal(e) => Some(e.depth()),
DirEntryInner::BrokenSymlink(_) => None,
DirEntryInner::BrokenSymlink(_, depth) => Some(*depth),
}
}
@@ -144,7 +146,7 @@ impl Colorable for DirEntry {
fn file_name(&self) -> OsString {
let name = match &self.inner {
DirEntryInner::Normal(e) => e.file_name(),
DirEntryInner::BrokenSymlink(path) => {
DirEntryInner::BrokenSymlink(path, _) => {
// Path::file_name() only works if the last component is Normal,
// but we want it for all component types, so we open code it.
// Copied from LsColors::style_for_path_with_metadata().
+24 -4
View File
@@ -2,7 +2,7 @@ use std::borrow::Cow;
use std::ffi::OsStr;
use std::io::{self, Write};
use std::mem;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use std::thread;
@@ -440,7 +440,7 @@ impl WorkerState {
}
/// Spawn the sender threads.
fn spawn_senders(&self, walker: WalkParallel, tx: Sender<Batch>) {
fn spawn_senders(&self, walker: WalkParallel, roots: &[PathBuf], tx: Sender<Batch>) {
walker.run(|| {
let patterns = &self.patterns;
let config = &self.config;
@@ -495,7 +495,8 @@ impl WorkerState {
.ok()
.is_some_and(|m| m.file_type().is_symlink()) =>
{
DirEntry::broken_symlink(path)
let depth = depth_relative_to_roots(&path, roots);
DirEntry::broken_symlink(path, depth)
}
Err(err) => {
return match tx.send(WorkerResult::Error(err)) {
@@ -640,7 +641,7 @@ impl WorkerState {
let receiver = scope.spawn(|| self.receive(rx));
// Spawn the sender threads.
self.spawn_senders(walker, tx);
self.spawn_senders(walker, paths, tx);
receiver.join().unwrap()
});
@@ -653,6 +654,25 @@ impl WorkerState {
}
}
/// Compute the depth of `path` relative to the search root it was found under.
///
/// `ignore::DirEntry::depth()` provides this for normal entries, but broken
/// symlinks are surfaced as errors that carry no depth, so we derive it from the
/// path components instead (see issue #1017). The matching root is the longest
/// search root (by component count) that `path` starts with; if none matches we
/// fall back to the full component count, which keeps the entry visible rather
/// than silently dropping it.
fn depth_relative_to_roots(path: &Path, roots: &[PathBuf]) -> usize {
let components = path.components().count();
let root_components = roots
.iter()
.filter(|root| path.starts_with(root))
.map(|root| root.components().count())
.max()
.unwrap_or(0);
components.saturating_sub(root_components)
}
fn search_str_for_entry<'a>(
entry_path: &'a std::path::Path,
full_path_base: Option<&std::path::Path>,
+37
View File
@@ -1208,6 +1208,43 @@ fn test_min_depth() {
);
}
/// Minimum depth with a broken symlink (regression test for #1017)
///
/// A broken symlink, surfaced while following links, has no depth reported by
/// the walker, so --min-depth used to drop it unconditionally.
#[test]
fn test_min_depth_broken_symlink() {
let mut te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
te.create_broken_symlink("one/two/broken_symlink")
.expect("Failed to create broken symlink.");
// The broken symlink sits at depth 3, so it is kept up to that depth.
te.assert_output(
&[
"--follow",
"--type",
"symlink",
"--min-depth",
"3",
"broken_symlink",
],
"one/two/broken_symlink",
);
// A --min-depth beyond its actual depth must exclude it.
te.assert_output(
&[
"--follow",
"--type",
"symlink",
"--min-depth",
"4",
"broken_symlink",
],
"",
);
}
/// Exact depth (--exact-depth)
#[test]
fn test_exact_depth() {