Skip to content

Commit b05697d

Browse files
committed
Auto merge of #10188 - weihanglo:issue-9528, r=alexcrichton
Detect filesystem loop during walking the projects Resolves #9528 ~~This PR also adds a new dependency `same-file` but since it's already a dependency of `cargo-util`, so nothing added actually.~~ Use `walkdir` to detect filesystem loop and gain performance boost!
2 parents b9bc3d1 + b49fe50 commit b05697d

File tree

3 files changed

+81
-55
lines changed

3 files changed

+81
-55
lines changed

src/cargo/sources/path.rs

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use std::collections::HashSet;
22
use std::fmt::{self, Debug, Formatter};
3-
use std::fs;
43
use std::path::{Path, PathBuf};
54

65
use crate::core::source::MaybePackage;
@@ -11,8 +10,8 @@ use anyhow::Context as _;
1110
use cargo_util::paths;
1211
use filetime::FileTime;
1312
use ignore::gitignore::GitignoreBuilder;
14-
use ignore::Match;
1513
use log::{trace, warn};
14+
use walkdir::WalkDir;
1615

1716
pub struct PathSource<'cfg> {
1817
source_id: SourceId,
@@ -131,39 +130,36 @@ impl<'cfg> PathSource<'cfg> {
131130
}
132131
let ignore_include = include_builder.build()?;
133132

134-
let ignore_should_package = |relative_path: &Path, is_dir: bool| -> CargoResult<bool> {
133+
let ignore_should_package = |relative_path: &Path, is_dir: bool| {
135134
// "Include" and "exclude" options are mutually exclusive.
136135
if no_include_option {
137-
match ignore_exclude.matched_path_or_any_parents(relative_path, is_dir) {
138-
Match::None => Ok(true),
139-
Match::Ignore(_) => Ok(false),
140-
Match::Whitelist(_) => Ok(true),
141-
}
136+
!ignore_exclude
137+
.matched_path_or_any_parents(relative_path, is_dir)
138+
.is_ignore()
142139
} else {
143140
if is_dir {
144141
// Generally, include directives don't list every
145142
// directory (nor should they!). Just skip all directory
146143
// checks, and only check files.
147-
return Ok(true);
144+
return true;
148145
}
149-
match ignore_include
146+
ignore_include
150147
.matched_path_or_any_parents(relative_path, /* is_dir */ false)
151-
{
152-
Match::None => Ok(false),
153-
Match::Ignore(_) => Ok(true),
154-
Match::Whitelist(_) => Ok(false),
155-
}
148+
.is_ignore()
156149
}
157150
};
158151

159-
let mut filter = |path: &Path, is_dir: bool| -> CargoResult<bool> {
160-
let relative_path = path.strip_prefix(root)?;
152+
let mut filter = |path: &Path, is_dir: bool| {
153+
let relative_path = match path.strip_prefix(root) {
154+
Ok(p) => p,
155+
Err(_) => return false,
156+
};
161157

162158
let rel = relative_path.as_os_str();
163159
if rel == "Cargo.lock" {
164-
return Ok(pkg.include_lockfile());
160+
return pkg.include_lockfile();
165161
} else if rel == "Cargo.toml" {
166-
return Ok(true);
162+
return true;
167163
}
168164

169165
ignore_should_package(relative_path, is_dir)
@@ -225,7 +221,7 @@ impl<'cfg> PathSource<'cfg> {
225221
&self,
226222
pkg: &Package,
227223
repo: &git2::Repository,
228-
filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
224+
filter: &mut dyn FnMut(&Path, bool) -> bool,
229225
) -> CargoResult<Vec<PathBuf>> {
230226
warn!("list_files_git {}", pkg.package_id());
231227
let index = repo.index()?;
@@ -347,7 +343,7 @@ impl<'cfg> PathSource<'cfg> {
347343
PathSource::walk(&file_path, &mut ret, false, filter)?;
348344
}
349345
}
350-
} else if (*filter)(&file_path, is_dir)? {
346+
} else if filter(&file_path, is_dir) {
351347
assert!(!is_dir);
352348
// We found a file!
353349
warn!(" found {}", file_path.display());
@@ -379,7 +375,7 @@ impl<'cfg> PathSource<'cfg> {
379375
fn list_files_walk(
380376
&self,
381377
pkg: &Package,
382-
filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
378+
filter: &mut dyn FnMut(&Path, bool) -> bool,
383379
) -> CargoResult<Vec<PathBuf>> {
384380
let mut ret = Vec::new();
385381
PathSource::walk(pkg.root(), &mut ret, true, filter)?;
@@ -390,39 +386,46 @@ impl<'cfg> PathSource<'cfg> {
390386
path: &Path,
391387
ret: &mut Vec<PathBuf>,
392388
is_root: bool,
393-
filter: &mut dyn FnMut(&Path, bool) -> CargoResult<bool>,
389+
filter: &mut dyn FnMut(&Path, bool) -> bool,
394390
) -> CargoResult<()> {
395-
let is_dir = path.is_dir();
396-
if !is_root && !(*filter)(path, is_dir)? {
397-
return Ok(());
398-
}
399-
if !is_dir {
400-
ret.push(path.to_path_buf());
401-
return Ok(());
402-
}
403-
// Don't recurse into any sub-packages that we have.
404-
if !is_root && path.join("Cargo.toml").exists() {
405-
return Ok(());
406-
}
391+
let walkdir = WalkDir::new(path)
392+
.follow_links(true)
393+
.into_iter()
394+
.filter_entry(|entry| {
395+
let path = entry.path();
396+
let at_root = is_root && entry.depth() == 0;
397+
let is_dir = entry.file_type().is_dir();
398+
399+
if !at_root && !filter(path, is_dir) {
400+
return false;
401+
}
407402

408-
// For package integration tests, we need to sort the paths in a deterministic order to
409-
// be able to match stdout warnings in the same order.
410-
//
411-
// TODO: drop `collect` and sort after transition period and dropping warning tests.
412-
// See rust-lang/cargo#4268 and rust-lang/cargo#4270.
413-
let mut entries: Vec<PathBuf> = fs::read_dir(path)
414-
.with_context(|| format!("cannot read {:?}", path))?
415-
.map(|e| e.unwrap().path())
416-
.collect();
417-
entries.sort_unstable_by(|a, b| a.as_os_str().cmp(b.as_os_str()));
418-
for path in entries {
419-
let name = path.file_name().and_then(|s| s.to_str());
420-
if is_root && name == Some("target") {
421-
// Skip Cargo artifacts.
422-
continue;
403+
if !is_dir {
404+
return true;
405+
}
406+
407+
// Don't recurse into any sub-packages that we have.
408+
if !at_root && path.join("Cargo.toml").exists() {
409+
return false;
410+
}
411+
412+
// Skip root Cargo artifacts.
413+
if is_root
414+
&& entry.depth() == 1
415+
&& path.file_name().and_then(|s| s.to_str()) == Some("target")
416+
{
417+
return false;
418+
}
419+
420+
true
421+
});
422+
for entry in walkdir {
423+
let entry = entry?;
424+
if !entry.file_type().is_dir() {
425+
ret.push(entry.path().to_path_buf());
423426
}
424-
PathSource::walk(&path, ret, false, filter)?;
425427
}
428+
426429
Ok(())
427430
}
428431

tests/testsuite/build_script.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4653,7 +4653,7 @@ Caused by:
46534653
failed to determine list of files in [..]/foo
46544654
46554655
Caused by:
4656-
cannot read \"[..]/foo/secrets\"
4656+
IO error for operation on [..]/foo/secrets: [..]
46574657
46584658
Caused by:
46594659
[..]

tests/testsuite/package.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -794,10 +794,10 @@ fn broken_symlink() {
794794
.with_status(101)
795795
.with_stderr_contains(
796796
"\
797-
error: failed to prepare local package for uploading
797+
error: failed to determine list of files [..]/foo
798798
799799
Caused by:
800-
failed to open for archiving: `[..]foo.rs`
800+
IO error for operation on [..]/foo/src/foo.rs: [..]
801801
802802
Caused by:
803803
[..]
@@ -826,6 +826,28 @@ fn package_symlink_to_dir() {
826826
.run();
827827
}
828828

829+
#[cargo_test]
830+
/// Tests if a symlink to ancestor causes filesystem loop error.
831+
///
832+
/// This test requires you to be able to make symlinks.
833+
/// For windows, this may require you to enable developer mode.
834+
fn filesystem_loop() {
835+
if !symlink_supported() {
836+
return;
837+
}
838+
839+
project()
840+
.file("src/main.rs", r#"fn main() { println!("hello"); }"#)
841+
.symlink_dir("a/b", "a/b/c/d/foo")
842+
.build()
843+
.cargo("package -v")
844+
.with_status(101)
845+
.with_stderr_contains(
846+
" File system loop found: [..]/a/b/c/d/foo points to an ancestor [..]/a/b",
847+
)
848+
.run();
849+
}
850+
829851
#[cargo_test]
830852
fn do_not_package_if_repository_is_dirty() {
831853
let p = project().build();
@@ -1798,7 +1820,8 @@ fn package_restricted_windows() {
17981820
.build();
17991821

18001822
p.cargo("package")
1801-
.with_stderr(
1823+
// use unordered here because the order of the warning is different on each platform.
1824+
.with_stderr_unordered(
18021825
"\
18031826
[WARNING] file src/aux/mod.rs is a reserved Windows filename, it will not work on Windows platforms
18041827
[WARNING] file src/con.rs is a reserved Windows filename, it will not work on Windows platforms

0 commit comments

Comments
 (0)