diff --git a/src/lib.rs b/src/lib.rs index 929c565..094e4b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, @@ -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, @@ -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. /// @@ -523,6 +535,7 @@ impl IntoIterator for WalkDir { oldest_opened: 0, depth: 0, deferred_dirs: vec![], + last_failed_link: None, root_device: None, } } @@ -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, + /// The saved link entry that could not be followed, to be yielded next. + last_failed_link: Option, /// The device of the root file path when the first call to `next` was /// made. /// @@ -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); } } @@ -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); } } @@ -817,9 +841,18 @@ impl IntoIter { fn handle_entry( &mut self, mut dent: DirEntry, + follow_links: bool, ) -> Option> { - 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 { @@ -932,16 +965,19 @@ impl IntoIter { self.oldest_opened = min(self.oldest_opened, self.stack_list.len()); } - fn follow(&self, mut dent: DirEntry) -> Result { - dent = - DirEntry::from_path(self.depth, dent.path().to_path_buf(), true)?; + fn follow(&self, link_dent: &DirEntry) -> Result { + 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>(&self, child: P) -> Result<()> { diff --git a/src/tests/recursive.rs b/src/tests/recursive.rs index 4119f46..d130d64 100644 --- a/src/tests/recursive.rs +++ b/src/tests/recursive.rs @@ -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();