From 9fa35858b34a1a5aa94076c3615e3134aef2fa76 Mon Sep 17 00:00:00 2001 From: Daniel Parks Date: Wed, 10 Aug 2022 14:20:35 -0700 Subject: [PATCH] bug: fix contents_first with root symlink This fixes two incorrect behaviors when `contents_first` is on and the root path is a symlink: 1. The root path was returned first in the iterator instead of last. 2. Some subdirectories were returned out of order. The issue was that root symlinks were returned immediately rather than being pushed onto the `deferred_dirs` vec. That lead to `deferred_dirs` and `depth` being out of sync, which lead to deferred directories being processed one ascent too late. This also changes the internal handling of the `same_file_system` option slightly. Previously, if a directory was found to be on a different file system it would _not_ be pushed to the stack, but it _would_ be pushed to the deferred list. This did not matter because in those cases `handle_entry()` would return to `next()`, which would immediately pop the directory off the deferred list and return it. This ensures that the directory is returned immediately, and is not pushed to either the stack or the deferred list. (I think this makes the code clearer.) Fixes: #163 --- src/lib.rs | 29 ++++++++++++++++++----------- src/tests/recursive.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 929c565..67d8a74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -821,16 +821,20 @@ impl IntoIter { if self.opts.follow_links && dent.file_type().is_symlink() { dent = itry!(self.follow(dent)); } - let is_normal_dir = !dent.file_type().is_symlink() && dent.is_dir(); - if is_normal_dir { - if self.opts.same_file_system && dent.depth() > 0 { - if itry!(self.is_same_file_system(&dent)) { - itry!(self.push(&dent)); + let should_descend = if !dent.file_type().is_symlink() { + if dent.is_dir() { + if self.opts.same_file_system && dent.depth() > 0 { + // Only descend into directories on the same filesystem as + // the root directory. + itry!(self.is_same_file_system(&dent)) + } else { + // Don't check the filesystem; descend all directories. + true } } else { - itry!(self.push(&dent)); + false } - } else if dent.depth() == 0 && dent.file_type().is_symlink() { + } else if dent.depth() == 0 { // As a special case, if we are processing a root entry, then we // always follow it even if it's a symlink and follow_links is // false. We are careful to not let this change the semantics of @@ -841,11 +845,14 @@ impl IntoIter { let md = itry!(fs::metadata(dent.path()).map_err(|err| { Error::from_path(dent.depth(), dent.path().to_path_buf(), err) })); - if md.file_type().is_dir() { - itry!(self.push(&dent)); - } + md.file_type().is_dir() + } else { + false + }; + if should_descend { + itry!(self.push(&dent)); } - if is_normal_dir && self.opts.contents_first { + if should_descend && self.opts.contents_first { self.deferred_dirs.push(dent); None } else if self.skippable() { diff --git a/src/tests/recursive.rs b/src/tests/recursive.rs index 4119f46..025cded 100644 --- a/src/tests/recursive.rs +++ b/src/tests/recursive.rs @@ -1021,3 +1021,44 @@ fn regression_skip_current_dir() { wd.skip_current_dir(); wd.next(); } + +// Tests that entries are returned in the correct order when contents_first is +// on and the root is a symlink to a directory. +// +// See: https://github.com/BurntSushi/walkdir/issues/163 +#[test] +fn regression_sym_root_dir_nofollow_contents_first() { + let dir = Dir::tmp(); + dir.mkdirp("dir/a/b"); + dir.touch("dir/a-file"); + dir.symlink_dir("dir", "link"); + + let wd = WalkDir::new(dir.join("link")).contents_first(true); + let r = dir.run_recursive(wd); + r.assert_no_errors(); + + let ents = r.ents(); + assert_eq!(4, ents.len()); + + // link (the root) should always be the final entry. + let link = &ents[3]; + assert_eq!(dir.join("link"), link.path()); + assert!(link.path_is_symlink()); + assert_eq!(dir.join("dir"), fs::read_link(link.path()).unwrap()); + + // link/a/b must be followed directly by link/a + let path_link_a_b = dir.join("link").join("a").join("b"); + let path_link_a = dir.join("link").join("a"); + let mut expect_link_a = false; + for ent in ents { + if expect_link_a { + assert_eq!(path_link_a, ent.path()); + break; + } else if ent.path() == path_link_a_b { + expect_link_a = true; + } + } + if !expect_link_a { + panic!("Did not find link/a/b in results"); + } +}