Skip to content

Commit e36b65a

Browse files
committed
windows: fix OneDrive traversals
This commit fixes a bug on Windows where directory traversals were completely broken when attempting to scan OneDrive directories that use the "file on demand" strategy. The specific problem was that Rust's standard library treats OneDrive directories as reparse points instead of directories, which causes methods like `FileType::is_file` and `FileType::is_dir` to always return false, even when retrieved via methods like `metadata` that purport to follow symbolic links. We fix this by peppering our code with checks on the underlying file attributes exposed by Windows. We consider an entry a directory if and only if the directory bit is set on the attributes. We are careful to make sure that the code remains the same on non-Windows platforms. Note that we also bump the dependency on `walkdir`, which contains a similar fix for its traversals. This bug is recorded upstream: rust-lang/rust#46484 Upstream also has a pending PR: rust-lang/rust#47956 Fixes #705
1 parent 11ad7ab commit e36b65a

File tree

7 files changed

+192
-19
lines changed

7 files changed

+192
-19
lines changed

Cargo.lock

Lines changed: 6 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ same-file = "1"
5151
termcolor = { version = "0.3.3", path = "termcolor" }
5252
globset = { version = "0.2.1", path = "globset" }
5353

54+
[target.'cfg(windows)'.dependencies.winapi]
55+
version = "0.3"
56+
features = ["std", "winnt"]
57+
5458
[build-dependencies]
5559
clap = "2.26"
5660
lazy_static = "1"

ignore/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ same-file = "1"
2828
thread_local = "0.3.2"
2929
walkdir = "2"
3030

31+
[target.'cfg(windows)'.dependencies.winapi]
32+
version = "0.3"
33+
features = ["std", "winnt"]
34+
3135
[dev-dependencies]
3236
tempdir = "0.3.5"
3337

ignore/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ extern crate same_file;
5959
extern crate tempdir;
6060
extern crate thread_local;
6161
extern crate walkdir;
62+
#[cfg(windows)]
63+
extern crate winapi;
6264

6365
use std::error;
6466
use std::fmt;

ignore/src/walk.rs

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ impl DirEntry {
8989
self.err.as_ref()
9090
}
9191

92+
/// Returns true if and only if this entry points to a directory.
93+
fn is_dir(&self) -> bool {
94+
self.dent.is_dir()
95+
}
96+
9297
fn new_stdin() -> DirEntry {
9398
DirEntry {
9499
dent: DirEntryInner::Stdin,
@@ -208,6 +213,24 @@ impl DirEntryInner {
208213
Raw(ref x) => Some(x.ino()),
209214
}
210215
}
216+
217+
/// Returns true if and only if this entry points to a directory.
218+
///
219+
/// This works around a bug in Rust's standard library:
220+
/// https://github.com/rust-lang/rust/issues/46484
221+
#[cfg(windows)]
222+
fn is_dir(&self) -> bool {
223+
self.metadata().map(|md| metadata_is_dir(&md)).unwrap_or(false)
224+
}
225+
226+
/// Returns true if and only if this entry points to a directory.
227+
///
228+
/// This works around a bug in Rust's standard library:
229+
/// https://github.com/rust-lang/rust/issues/46484
230+
#[cfg(not(windows))]
231+
fn is_dir(&self) -> bool {
232+
self.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
233+
}
211234
}
212235

213236
/// DirEntryRaw is essentially copied from the walkdir crate so that we can
@@ -456,7 +479,7 @@ impl WalkBuilder {
456479
(p.to_path_buf(), None)
457480
} else {
458481
let mut wd = WalkDir::new(p);
459-
wd = wd.follow_links(follow_links || p.is_file());
482+
wd = wd.follow_links(follow_links || path_is_file(p));
460483
if let Some(max_depth) = max_depth {
461484
wd = wd.max_depth(max_depth);
462485
}
@@ -728,7 +751,7 @@ impl Walk {
728751
return false;
729752
}
730753

731-
let is_dir = ent.file_type().is_dir();
754+
let is_dir = walkdir_entry_is_dir(ent);
732755
let max_size = self.max_filesize;
733756
let should_skip_path = skip_path(&self.ig, ent.path(), is_dir);
734757
let should_skip_filesize = if !is_dir && max_size.is_some() {
@@ -757,7 +780,7 @@ impl Iterator for Walk {
757780
}
758781
Some((path, Some(it))) => {
759782
self.it = Some(it);
760-
if self.parents && path.is_dir() {
783+
if self.parents && path_is_dir(&path) {
761784
let (ig, err) = self.ig_root.add_parents(path);
762785
self.ig = ig;
763786
if let Some(err) = err {
@@ -847,7 +870,7 @@ impl Iterator for WalkEventIter {
847870
None => None,
848871
Some(Err(err)) => Some(Err(err)),
849872
Some(Ok(dent)) => {
850-
if dent.file_type().is_dir() {
873+
if walkdir_entry_is_dir(&dent) {
851874
self.depth += 1;
852875
Some(Ok(WalkEvent::Dir(dent)))
853876
} else {
@@ -1000,7 +1023,7 @@ struct Work {
10001023
impl Work {
10011024
/// Returns true if and only if this work item is a directory.
10021025
fn is_dir(&self) -> bool {
1003-
self.dent.file_type().map_or(false, |t| t.is_dir())
1026+
self.dent.is_dir()
10041027
}
10051028

10061029
/// Adds ignore rules for parent directories.
@@ -1174,13 +1197,13 @@ impl Worker {
11741197
return (self.f)(Err(err));
11751198
}
11761199
};
1177-
if dent.file_type().map_or(false, |ft| ft.is_dir()) {
1200+
if dent.is_dir() {
11781201
if let Err(err) = check_symlink_loop(ig, dent.path(), depth) {
11791202
return (self.f)(Err(err));
11801203
}
11811204
}
11821205
}
1183-
let is_dir = dent.file_type().map_or(false, |ft| ft.is_dir());
1206+
let is_dir = dent.is_dir();
11841207
let max_size = self.max_filesize;
11851208
let should_skip_path = skip_path(ig, dent.path(), is_dir);
11861209
let should_skip_filesize = if !is_dir && max_size.is_some() {
@@ -1376,6 +1399,62 @@ fn skip_path(ig: &Ignore, path: &Path, is_dir: bool) -> bool {
13761399
}
13771400
}
13781401

1402+
/// Returns true if and only if this path points to a directory.
1403+
///
1404+
/// This works around a bug in Rust's standard library:
1405+
/// https://github.com/rust-lang/rust/issues/46484
1406+
#[cfg(windows)]
1407+
fn path_is_dir(path: &Path) -> bool {
1408+
fs::metadata(path).map(|md| metadata_is_dir(&md)).unwrap_or(false)
1409+
}
1410+
1411+
/// Returns true if and only if this entry points to a directory.
1412+
#[cfg(not(windows))]
1413+
fn path_is_dir(path: &Path) -> bool {
1414+
path.is_dir()
1415+
}
1416+
1417+
/// Returns true if and only if this path points to a file.
1418+
///
1419+
/// This works around a bug in Rust's standard library:
1420+
/// https://github.com/rust-lang/rust/issues/46484
1421+
#[cfg(windows)]
1422+
fn path_is_file(path: &Path) -> bool {
1423+
!path_is_dir(path)
1424+
}
1425+
1426+
/// Returns true if and only if this entry points to a directory.
1427+
#[cfg(not(windows))]
1428+
fn path_is_file(path: &Path) -> bool {
1429+
path.is_file()
1430+
}
1431+
1432+
/// Returns true if and only if the given walkdir entry points to a directory.
1433+
///
1434+
/// This works around a bug in Rust's standard library:
1435+
/// https://github.com/rust-lang/rust/issues/46484
1436+
#[cfg(windows)]
1437+
fn walkdir_entry_is_dir(dent: &walkdir::DirEntry) -> bool {
1438+
dent.metadata().map(|md| metadata_is_dir(&md)).unwrap_or(false)
1439+
}
1440+
1441+
/// Returns true if and only if the given walkdir entry points to a directory.
1442+
#[cfg(not(windows))]
1443+
fn walkdir_entry_is_dir(dent: &walkdir::DirEntry) -> bool {
1444+
dent.file_type().is_dir()
1445+
}
1446+
1447+
/// Returns true if and only if the given metadata points to a directory.
1448+
///
1449+
/// This works around a bug in Rust's standard library:
1450+
/// https://github.com/rust-lang/rust/issues/46484
1451+
#[cfg(windows)]
1452+
fn metadata_is_dir(md: &fs::Metadata) -> bool {
1453+
use std::os::windows::fs::MetadataExt;
1454+
use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY;
1455+
md.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0
1456+
}
1457+
13791458
#[cfg(test)]
13801459
mod tests {
13811460
use std::fs::{self, File};

src/args.rs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ impl Args {
209209
/// Returns true if there is exactly one file path given to search.
210210
pub fn is_one_path(&self) -> bool {
211211
self.paths.len() == 1
212-
&& (self.paths[0] == Path::new("-") || self.paths[0].is_file())
212+
&& (self.paths[0] == Path::new("-") || path_is_file(&self.paths[0]))
213213
}
214214

215215
/// Create a worker whose configuration is taken from the
@@ -562,7 +562,7 @@ impl<'a> ArgMatches<'a> {
562562
self.is_present("with-filename")
563563
|| self.is_present("vimgrep")
564564
|| paths.len() > 1
565-
|| paths.get(0).map_or(false, |p| p.is_dir())
565+
|| paths.get(0).map_or(false, |p| path_is_dir(p))
566566
}
567567
}
568568

@@ -609,7 +609,7 @@ impl<'a> ArgMatches<'a> {
609609
} else {
610610
// If we're only searching a few paths and all of them are
611611
// files, then memory maps are probably faster.
612-
paths.len() <= 10 && paths.iter().all(|p| p.is_file())
612+
paths.len() <= 10 && paths.iter().all(|p| path_is_file(p))
613613
})
614614
}
615615

@@ -1036,3 +1036,44 @@ fn stdin_is_readable() -> bool {
10361036
// always return true.
10371037
true
10381038
}
1039+
1040+
/// Returns true if and only if this path points to a directory.
1041+
///
1042+
/// This works around a bug in Rust's standard library:
1043+
/// https://github.com/rust-lang/rust/issues/46484
1044+
#[cfg(windows)]
1045+
fn path_is_dir(path: &Path) -> bool {
1046+
fs::metadata(path).map(|md| metadata_is_dir(&md)).unwrap_or(false)
1047+
}
1048+
1049+
/// Returns true if and only if this entry points to a directory.
1050+
#[cfg(not(windows))]
1051+
fn path_is_dir(path: &Path) -> bool {
1052+
path.is_dir()
1053+
}
1054+
1055+
/// Returns true if and only if this path points to a file.
1056+
///
1057+
/// This works around a bug in Rust's standard library:
1058+
/// https://github.com/rust-lang/rust/issues/46484
1059+
#[cfg(windows)]
1060+
fn path_is_file(path: &Path) -> bool {
1061+
!path_is_dir(path)
1062+
}
1063+
1064+
/// Returns true if and only if this entry points to a directory.
1065+
#[cfg(not(windows))]
1066+
fn path_is_file(path: &Path) -> bool {
1067+
path.is_file()
1068+
}
1069+
1070+
/// Returns true if and only if the given metadata points to a directory.
1071+
///
1072+
/// This works around a bug in Rust's standard library:
1073+
/// https://github.com/rust-lang/rust/issues/46484
1074+
#[cfg(windows)]
1075+
fn metadata_is_dir(md: &fs::Metadata) -> bool {
1076+
use std::os::windows::fs::MetadataExt;
1077+
use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY;
1078+
md.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0
1079+
}

src/main.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ extern crate num_cpus;
1818
extern crate regex;
1919
extern crate same_file;
2020
extern crate termcolor;
21+
#[cfg(windows)]
22+
extern crate winapi;
2123

2224
use std::error::Error;
2325
use std::process;
@@ -267,15 +269,14 @@ fn get_or_log_dir_entry(
267269
eprintln!("{}", err);
268270
}
269271
}
270-
let ft = match dent.file_type() {
271-
None => return Some(dent), // entry is stdin
272-
Some(ft) => ft,
273-
};
272+
if dent.file_type().is_none() {
273+
return Some(dent); // entry is stdin
274+
}
274275
// A depth of 0 means the user gave the path explicitly, so we
275276
// should always try to search it.
276-
if dent.depth() == 0 && !ft.is_dir() {
277+
if dent.depth() == 0 && !ignore_entry_is_dir(&dent) {
277278
return Some(dent);
278-
} else if !ft.is_file() {
279+
} else if !ignore_entry_is_file(&dent) {
279280
return None;
280281
}
281282
// If we are redirecting stdout to a file, then don't search that
@@ -288,6 +289,45 @@ fn get_or_log_dir_entry(
288289
}
289290
}
290291

292+
/// Returns true if and only if the given `ignore::DirEntry` points to a
293+
/// directory.
294+
///
295+
/// This works around a bug in Rust's standard library:
296+
/// https://github.com/rust-lang/rust/issues/46484
297+
#[cfg(windows)]
298+
fn ignore_entry_is_dir(dent: &ignore::DirEntry) -> bool {
299+
use std::os::windows::fs::MetadataExt;
300+
use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY;
301+
302+
dent.metadata().map(|md| {
303+
md.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0
304+
}).unwrap_or(false)
305+
}
306+
307+
/// Returns true if and only if the given `ignore::DirEntry` points to a
308+
/// directory.
309+
#[cfg(not(windows))]
310+
fn ignore_entry_is_dir(dent: &ignore::DirEntry) -> bool {
311+
dent.file_type().map_or(false, |ft| ft.is_dir())
312+
}
313+
314+
/// Returns true if and only if the given `ignore::DirEntry` points to a
315+
/// file.
316+
///
317+
/// This works around a bug in Rust's standard library:
318+
/// https://github.com/rust-lang/rust/issues/46484
319+
#[cfg(windows)]
320+
fn ignore_entry_is_file(dent: &ignore::DirEntry) -> bool {
321+
!ignore_entry_is_dir(dent)
322+
}
323+
324+
/// Returns true if and only if the given `ignore::DirEntry` points to a
325+
/// file.
326+
#[cfg(not(windows))]
327+
fn ignore_entry_is_file(dent: &ignore::DirEntry) -> bool {
328+
dent.file_type().map_or(false, |ft| ft.is_file())
329+
}
330+
291331
fn is_stdout_file(
292332
dent: &ignore::DirEntry,
293333
stdout_handle: Option<&same_file::Handle>,

0 commit comments

Comments
 (0)