Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for yielding the original links on resolution failure #160

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ pub struct WalkDir {

struct WalkDirOptions {
follow_links: bool,
yield_link_on_error: bool,
max_open: usize,
min_depth: usize,
max_depth: usize,
Expand Down Expand Up @@ -287,6 +288,7 @@ impl WalkDir {
WalkDir {
opts: WalkDirOptions {
follow_links: false,
yield_link_on_error: false,
max_open: 10,
min_depth: 0,
max_depth: ::std::usize::MAX,
Expand Down Expand Up @@ -344,6 +346,16 @@ impl WalkDir {
self
}

/// Yield link entries that cannot be followed. By default, this is
/// disabled.
///
/// When `yes` is true, and a symlink cannot be followed, after the error is
/// yielded, the link itself (not its target) will be yielded.
pub fn yield_link_on_error(mut self, yes: bool) -> Self {
self.opts.yield_link_on_error = yes;
self
}

/// Set the maximum number of simultaneously open file descriptors used
/// by the iterator.
///
Expand Down Expand Up @@ -523,6 +535,7 @@ impl IntoIterator for WalkDir {
oldest_opened: 0,
depth: 0,
deferred_dirs: vec![],
last_failed_link: None,
root_device: None,
}
}
Expand Down Expand Up @@ -573,6 +586,8 @@ pub struct IntoIter {
/// yielded after their contents has been fully yielded. This is only
/// used when `contents_first` is enabled.
deferred_dirs: Vec<DirEntry>,
/// The saved link entry that could not be followed, to be yielded next.
last_failed_link: Option<DirEntry>,
/// The device of the root file path when the first call to `next` was
/// made.
///
Expand Down Expand Up @@ -669,7 +684,14 @@ impl Iterator for IntoIter {
self.root_device = Some(itry!(result));
}
let dent = itry!(DirEntry::from_path(0, start, false));
if let Some(result) = self.handle_entry(dent) {
if let Some(result) =
self.handle_entry(dent, self.opts.follow_links)
{
return Some(result);
}
}
if let Some(dent) = self.last_failed_link.take() {
if let Some(result) = self.handle_entry(dent, false) {
return Some(result);
}
}
Expand All @@ -695,7 +717,9 @@ impl Iterator for IntoIter {
None => self.pop(),
Some(Err(err)) => return Some(Err(err)),
Some(Ok(dent)) => {
if let Some(result) = self.handle_entry(dent) {
if let Some(result) =
self.handle_entry(dent, self.opts.follow_links)
{
return Some(result);
}
}
Expand Down Expand Up @@ -817,9 +841,18 @@ impl IntoIter {
fn handle_entry(
&mut self,
mut dent: DirEntry,
follow_links: bool,
) -> Option<Result<DirEntry>> {
if self.opts.follow_links && dent.file_type().is_symlink() {
dent = itry!(self.follow(dent));
if follow_links && dent.file_type().is_symlink() {
match self.follow(&dent) {
Ok(followed_dent) => dent = followed_dent,
Err(err) => {
if self.opts.yield_link_on_error {
self.last_failed_link.replace(dent);
}
return Some(Err(err));
}
}
}
let is_normal_dir = !dent.file_type().is_symlink() && dent.is_dir();
if is_normal_dir {
Expand Down Expand Up @@ -932,16 +965,19 @@ impl IntoIter {
self.oldest_opened = min(self.oldest_opened, self.stack_list.len());
}

fn follow(&self, mut dent: DirEntry) -> Result<DirEntry> {
dent =
DirEntry::from_path(self.depth, dent.path().to_path_buf(), true)?;
fn follow(&self, link_dent: &DirEntry) -> Result<DirEntry> {
let followed_dent = DirEntry::from_path(
self.depth,
link_dent.path().to_path_buf(),
true,
)?;
// The only way a symlink can cause a loop is if it points
// to a directory. Otherwise, it always points to a leaf
// and we can omit any loop checks.
if dent.is_dir() {
self.check_loop(dent.path())?;
if followed_dent.is_dir() {
self.check_loop(followed_dent.path())?;
}
Ok(dent)
Ok(followed_dent)
}

fn check_loop<P: AsRef<Path>>(&self, child: P) -> Result<()> {
Expand Down
47 changes: 47 additions & 0 deletions src/tests/recursive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,53 @@ fn sym_dir_self_loop_io_error() {
assert!(err.io_error().is_some());
}

#[test]
fn sym_yield_on_error() {
let dir = Dir::tmp();
dir.symlink_file("a", "a");
dir.touch("b");

let wd =
WalkDir::new(dir.path()).follow_links(true).yield_link_on_error(true);
let r = dir.run_recursive(wd);

let (ents, errs) = (r.sorted_ents(), r.errs());
assert_eq!(3, ents.len());
assert_eq!(1, errs.len());

let link = &ents[1];
assert_eq!(dir.join("a"), link.path());
assert!(link.path_is_symlink());

assert!(link.file_type().is_symlink());
assert!(!link.file_type().is_file());
assert!(!link.file_type().is_dir());

assert!(link.metadata().unwrap().file_type().is_symlink());
assert!(!link.metadata().unwrap().file_type().is_file());
assert!(!link.metadata().unwrap().file_type().is_dir());

let ent = &ents[2];
assert_eq!(dir.join("b"), ent.path());
assert!(!ent.path_is_symlink());

assert!(ent.file_type().is_file());
assert!(!ent.file_type().is_symlink());
assert!(!ent.file_type().is_dir());

assert!(ent.metadata().unwrap().file_type().is_file());
assert!(!ent.metadata().unwrap().file_type().is_symlink());
assert!(!ent.metadata().unwrap().file_type().is_dir());

let err = &errs[0];

let expected = dir.join("a");
assert_eq!(Some(&*expected), err.path());
assert_eq!(1, err.depth());
assert!(err.loop_ancestor().is_none());
assert!(err.io_error().is_some());
}

#[test]
fn min_depth_1() {
let dir = Dir::tmp();
Expand Down