Skip to content

Commit 83b7a24

Browse files
committed
walkdir: fix root symlink bug
This commit fixes a nasty bug where the root path given to walkdir was always reported as a symlink, even when 'follow_links' was enabled. This appears to be a regression introduced by commit 6f72fce as part of fixing BurntSushi/ripgrep#984. The central problem was that since root paths should always be followed, we were creating a DirEntry whose internal file type was always resolved by following a symlink, but whose 'metadata' method still returned the metadata of the symlink and not the target. This was problematic and inconsistent both with and without 'follow_links' enabled. We also fix the documentation. In particular, we make the docs of 'new' more unambiguous, where it previously could have been interpreted as contradictory to the docs on 'DirEntry'. Specifically, 'WalkDir::new' says: If root is a symlink, then it is always followed. But the docs for 'DirEntry::metadata' say This always calls std::fs::symlink_metadata. If this entry is a symbolic link and follow_links is enabled, then std::fs::metadata is called instead. Similarly, 'DirEntry::file_type' said If this is a symbolic link and follow_links is true, then this returns the type of the target. That is, if 'root' is a symlink and 'follow_links' is NOT enabled, then the previous incorrect behavior resulted in 'DirEntry::file_type' behaving as if 'follow_links' was enabled. If 'follow_links' was enabled, then the previous incorrect behavior resulted in 'DirEntry::metadata' reporting the metadata of the symlink itself. We fix this by correctly constructing the DirEntry in the first place, and then adding special case logic to path traversal that will always attempt to follow the root path if it's a symlink and 'follow_links' was not enabled. We also tweak the docs on 'WalkDir::new' to be more precise. Fixes #115
1 parent 4879ce2 commit 83b7a24

File tree

2 files changed

+99
-17
lines changed

2 files changed

+99
-17
lines changed

src/lib.rs

+54-17
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,9 @@ impl WalkDir {
277277
/// file path `root`. If `root` is a directory, then it is the first item
278278
/// yielded by the iterator. If `root` is a file, then it is the first
279279
/// and only item yielded by the iterator. If `root` is a symlink, then it
280-
/// is always followed.
280+
/// is always followed for the purposes of directory traversal. (A root
281+
/// `DirEntry` still obeys its documentation with respect to symlinks and
282+
/// the `follow_links` setting.)
281283
pub fn new<P: AsRef<Path>>(root: P) -> Self {
282284
WalkDir {
283285
opts: WalkDirOptions {
@@ -681,7 +683,7 @@ impl Iterator for IntoIter {
681683
.map_err(|e| Error::from_path(0, start.clone(), e));
682684
self.root_device = Some(itry!(result));
683685
}
684-
let dent = itry!(DirEntry::from_path(0, start, false));
686+
let mut dent = itry!(DirEntry::from_path(0, start, false));
685687
if let Some(result) = self.handle_entry(dent) {
686688
return Some(result);
687689
}
@@ -844,6 +846,20 @@ impl IntoIter {
844846
} else {
845847
itry!(self.push(&dent));
846848
}
849+
} else if dent.depth() == 0 && dent.file_type().is_symlink() {
850+
// As a special case, if we are processing a root entry, then we
851+
// always follow it even if it's a symlink and follow_links is
852+
// false. We are careful to not let this change the semantics of
853+
// the DirEntry however. Namely, the DirEntry should still respect
854+
// the follow_links setting. When it's disabled, it should report
855+
// itself as a symlink. When it's enabled, it should always report
856+
// itself as the target.
857+
let md = itry!(fs::metadata(dent.path()).map_err(|err| {
858+
Error::from_path(dent.depth(), dent.path().to_path_buf(), err)
859+
}));
860+
if md.file_type().is_dir() {
861+
itry!(self.push(&dent));
862+
}
847863
}
848864
if is_normal_dir && self.opts.contents_first {
849865
self.deferred_dirs.push(dent);
@@ -1181,44 +1197,65 @@ impl DirEntry {
11811197
}
11821198

11831199
#[cfg(windows)]
1184-
fn from_path(depth: usize, pb: PathBuf, link: bool) -> Result<DirEntry> {
1185-
let md = fs::metadata(&pb).map_err(|err| {
1186-
Error::from_path(depth, pb.clone(), err)
1187-
})?;
1200+
fn from_path(depth: usize, pb: PathBuf, follow: bool) -> Result<DirEntry> {
1201+
let md =
1202+
if follow {
1203+
fs::metadata(&pb).map_err(|err| {
1204+
Error::from_path(depth, pb.clone(), err)
1205+
})?
1206+
} else {
1207+
fs::symlink_metadata(&pb).map_err(|err| {
1208+
Error::from_path(depth, pb.clone(), err)
1209+
})?
1210+
};
11881211
Ok(DirEntry {
11891212
path: pb,
11901213
ty: md.file_type(),
1191-
follow_link: link,
1214+
follow_link: follow,
11921215
depth: depth,
11931216
metadata: md,
11941217
})
11951218
}
11961219

11971220
#[cfg(unix)]
1198-
fn from_path(depth: usize, pb: PathBuf, link: bool) -> Result<DirEntry> {
1221+
fn from_path(depth: usize, pb: PathBuf, follow: bool) -> Result<DirEntry> {
11991222
use std::os::unix::fs::MetadataExt;
12001223

1201-
let md = fs::metadata(&pb).map_err(|err| {
1202-
Error::from_path(depth, pb.clone(), err)
1203-
})?;
1224+
let md =
1225+
if follow {
1226+
fs::metadata(&pb).map_err(|err| {
1227+
Error::from_path(depth, pb.clone(), err)
1228+
})?
1229+
} else {
1230+
fs::symlink_metadata(&pb).map_err(|err| {
1231+
Error::from_path(depth, pb.clone(), err)
1232+
})?
1233+
};
12041234
Ok(DirEntry {
12051235
path: pb,
12061236
ty: md.file_type(),
1207-
follow_link: link,
1237+
follow_link: follow,
12081238
depth: depth,
12091239
ino: md.ino(),
12101240
})
12111241
}
12121242

12131243
#[cfg(not(any(unix, windows)))]
1214-
fn from_path(depth: usize, pb: PathBuf, link: bool) -> Result<DirEntry> {
1215-
let md = fs::metadata(&pb).map_err(|err| {
1216-
Error::from_path(depth, pb.clone(), err)
1217-
})?;
1244+
fn from_path(depth: usize, pb: PathBuf, follow: bool) -> Result<DirEntry> {
1245+
let md =
1246+
if follow {
1247+
fs::metadata(&pb).map_err(|err| {
1248+
Error::from_path(depth, pb.clone(), err)
1249+
})?
1250+
} else {
1251+
fs::symlink_metadata(&pb).map_err(|err| {
1252+
Error::from_path(depth, pb.clone(), err)
1253+
})?
1254+
};
12181255
Ok(DirEntry {
12191256
path: pb,
12201257
ty: md.file_type(),
1221-
follow_link: link,
1258+
follow_link: follow,
12221259
depth: depth,
12231260
})
12241261
}

src/tests.rs

+45
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,51 @@ fn first_path_not_symlink() {
531531
assert!(!dents[0].path_is_symlink());
532532
}
533533

534+
// Like first_path_not_symlink, but checks that the first path is not reported
535+
// as a symlink even when we are supposed to be following them.
536+
#[test]
537+
#[cfg(unix)]
538+
fn first_path_not_symlink_follow() {
539+
let exp = td("foo", vec![]);
540+
let (tmp, _got) = dir_setup(&exp);
541+
542+
let dents = WalkDir::new(tmp.path().join("foo"))
543+
.follow_links(true)
544+
.into_iter()
545+
.collect::<Result<Vec<_>, _>>()
546+
.unwrap();
547+
assert_eq!(1, dents.len());
548+
assert!(!dents[0].path_is_symlink());
549+
}
550+
551+
// See: https://github.com/BurntSushi/walkdir/issues/115
552+
#[test]
553+
#[cfg(unix)]
554+
fn first_path_is_followed() {
555+
let exp = td("foo", vec![
556+
td("a", vec![tf("a1"), tf("a2")]),
557+
td("b", vec![tlf("../a/a1", "alink")]),
558+
]);
559+
let (tmp, _got) = dir_setup(&exp);
560+
561+
let dents = WalkDir::new(tmp.path().join("foo/b/alink"))
562+
.into_iter()
563+
.collect::<Result<Vec<_>, _>>()
564+
.unwrap();
565+
assert_eq!(1, dents.len());
566+
assert!(dents[0].file_type().is_symlink());
567+
assert!(dents[0].metadata().unwrap().file_type().is_symlink());
568+
569+
let dents = WalkDir::new(tmp.path().join("foo/b/alink"))
570+
.follow_links(true)
571+
.into_iter()
572+
.collect::<Result<Vec<_>, _>>()
573+
.unwrap();
574+
assert_eq!(1, dents.len());
575+
assert!(!dents[0].file_type().is_symlink());
576+
assert!(!dents[0].metadata().unwrap().file_type().is_symlink());
577+
}
578+
534579
#[test]
535580
#[cfg(unix)]
536581
fn walk_dir_sym_detect_no_follow_no_loop() {

0 commit comments

Comments
 (0)