use deps_core::{Ecosystem, EcosystemRegistry};
use ignore::WalkBuilder;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
pub const MAX_WALKED_FILES: usize = 50_000;
const PRUNED_DIRECTORIES: &[&str] = &[
".git",
".hg",
".svn",
".bzr",
"node_modules",
"bower_components",
"target",
"vendor",
".venv",
"venv",
"__pycache__",
".tox",
".mypy_cache",
".pytest_cache",
".ruff_cache",
".bundle",
".dart_tool",
".gradle",
".build",
"Pods",
"DerivedData",
"dist",
"build",
];
fn is_not_pruned_directory(entry: &ignore::DirEntry) -> bool {
entry.depth() == 0
|| !entry
.file_type()
.is_some_and(|file_type| file_type.is_dir())
|| !entry
.file_name()
.to_str()
.is_some_and(|name| PRUNED_DIRECTORIES.contains(&name))
}
fn is_not_escaping_directory(
entry: &ignore::DirEntry,
follow_symlinks: bool,
canonical_root: Option<&Path>,
) -> bool {
if !follow_symlinks || entry.depth() == 0 {
return true;
}
if !entry
.file_type()
.is_some_and(|file_type| file_type.is_dir())
{
return true;
}
let Some(canonical_root) = canonical_root else {
return false;
};
let Ok(canonical_path) = std::fs::canonicalize(entry.path()) else {
return false;
};
canonical_path.starts_with(canonical_root)
}
pub struct DiscoveredManifest {
pub path: PathBuf,
pub uri_path: PathBuf,
pub display_path: PathBuf,
pub ecosystem: Arc<dyn Ecosystem>,
}
#[derive(Default)]
pub struct WalkOutcome {
pub manifests: Vec<DiscoveredManifest>,
pub walk_errors: Vec<String>,
pub truncated: bool,
pub unrecognized_explicit_paths: Vec<PathBuf>,
pub ignored_manifests: Vec<PathBuf>,
}
#[must_use]
pub fn walk(
roots: &[PathBuf],
registry: &EcosystemRegistry,
respect_gitignore: bool,
follow_symlinks: bool,
) -> WalkOutcome {
walk_with_limit(
roots,
registry,
MAX_WALKED_FILES,
respect_gitignore,
follow_symlinks,
)
}
fn walk_with_limit(
roots: &[PathBuf],
registry: &EcosystemRegistry,
limit: usize,
respect_gitignore: bool,
follow_symlinks: bool,
) -> WalkOutcome {
let mut ctx = WalkCtx {
registry,
limit,
entries_walked: 0,
outcome: WalkOutcome::default(),
};
let hidden_ecosystem_dirs = hidden_ecosystem_directories(registry);
'roots: for root in roots {
if ctx.outcome.truncated {
break;
}
let Ok(absolute_root) = std::path::absolute(root) else {
ctx.outcome
.walk_errors
.push(format!("could not resolve path: {}", root.display()));
continue;
};
if root.is_file() {
if ctx.entries_walked >= ctx.limit {
ctx.outcome.truncated = true;
tracing::warn!(
limit,
"walk truncated: reached the maximum number of entries per run"
);
break;
}
ctx.entries_walked += 1;
let matched_before = ctx.outcome.manifests.len();
route_file(
&absolute_root,
&absolute_root,
root,
registry,
&mut ctx.outcome,
);
if ctx.outcome.manifests.len() == matched_before {
ctx.outcome.unrecognized_explicit_paths.push(root.clone());
}
continue;
}
let canonical_root = std::fs::canonicalize(&absolute_root).ok();
if !walk_directory(
&absolute_root,
&absolute_root,
true,
respect_gitignore,
follow_symlinks,
canonical_root.as_deref(),
&mut ctx,
) {
break 'roots;
}
for dir_name in &hidden_ecosystem_dirs {
let sub_root = absolute_root.join(dir_name);
if !sub_root.is_dir() {
continue;
}
match (
canonical_root.as_deref(),
std::fs::canonicalize(&sub_root).ok(),
) {
(Some(canonical_root), Some(canonical_sub_root))
if canonical_sub_root.starts_with(canonical_root) => {}
_ => continue,
}
if !walk_directory(
&sub_root,
&absolute_root,
false,
respect_gitignore,
follow_symlinks,
canonical_root.as_deref(),
&mut ctx,
) {
break 'roots;
}
}
}
ctx.outcome
}
struct WalkCtx<'a> {
registry: &'a EcosystemRegistry,
limit: usize,
entries_walked: usize,
outcome: WalkOutcome,
}
fn walk_directory(
walk_root: &Path,
display_root: &Path,
hidden: bool,
respect_gitignore: bool,
follow_symlinks: bool,
canonical_root: Option<&Path>,
ctx: &mut WalkCtx<'_>,
) -> bool {
let pruned_dirs: Arc<Mutex<Vec<PathBuf>>> = Arc::new(Mutex::new(Vec::new()));
let mut builder = WalkBuilder::new(walk_root);
builder
.hidden(hidden)
.parents(true)
.ignore(respect_gitignore)
.git_ignore(respect_gitignore)
.git_global(true)
.git_exclude(true)
.follow_links(follow_symlinks);
{
let pruned_dirs = Arc::clone(&pruned_dirs);
let canonical_root_owned = canonical_root.map(Path::to_path_buf);
builder.filter_entry(move |entry| {
if !is_not_pruned_directory(entry) {
pruned_dirs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(entry.path().to_path_buf());
return false;
}
is_not_escaping_directory(entry, follow_symlinks, canonical_root_owned.as_deref())
});
}
let mut visited: BTreeSet<PathBuf> = BTreeSet::new();
for entry in builder.build() {
if ctx.entries_walked >= ctx.limit {
ctx.outcome.truncated = true;
tracing::warn!(
limit = ctx.limit,
"walk truncated: reached the maximum number of entries per run"
);
return false;
}
ctx.entries_walked += 1;
match entry {
Ok(entry) if entry.file_type().is_some_and(|t| t.is_file()) => {
let path = entry.path();
let display = path
.strip_prefix(display_root)
.unwrap_or(path)
.to_path_buf();
if respect_gitignore {
visited.insert(display.clone());
}
if follow_symlinks {
match canonicalize_within_root(path, canonical_root) {
Some(canonical_path) => {
route_file(
path,
&canonical_path,
&display,
ctx.registry,
&mut ctx.outcome,
);
}
None => {
if symlink_is_manifest_shaped(path, ctx.registry) {
ctx.outcome.ignored_manifests.push(display);
}
}
}
} else {
route_file(path, path, &display, ctx.registry, &mut ctx.outcome);
}
}
Ok(entry) => {
if entry.path_is_symlink() && symlink_is_manifest_shaped(entry.path(), ctx.registry)
{
let path = entry.path();
let display = path
.strip_prefix(display_root)
.unwrap_or(path)
.to_path_buf();
if respect_gitignore {
visited.insert(display.clone());
}
ctx.outcome.ignored_manifests.push(display);
}
}
Err(error) => ctx.outcome.walk_errors.push(error.to_string()),
}
}
for pruned_dir in pruned_dirs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
{
warn_on_pruned_directory_manifest(pruned_dir, display_root, ctx);
}
if respect_gitignore {
detect_ignored_manifests(walk_root, display_root, hidden, ctx, &visited);
}
true
}
fn warn_on_pruned_directory_manifest(
pruned_dir: &Path,
display_root: &Path,
ctx: &mut WalkCtx<'_>,
) {
let Ok(read_dir) = std::fs::read_dir(pruned_dir) else {
return;
};
for entry in read_dir.flatten() {
let path = entry.path();
if !symlink_is_manifest_shaped(&path, ctx.registry) {
continue;
}
let display = path
.strip_prefix(display_root)
.unwrap_or(&path)
.to_path_buf();
ctx.outcome.ignored_manifests.push(display);
}
}
fn detect_ignored_manifests(
walk_root: &Path,
display_root: &Path,
hidden: bool,
ctx: &mut WalkCtx<'_>,
visited: &BTreeSet<PathBuf>,
) {
let mut builder = WalkBuilder::new(walk_root);
builder
.hidden(hidden)
.ignore(false)
.git_ignore(false)
.git_global(true)
.git_exclude(true)
.filter_entry(is_not_pruned_directory);
for (detection_entries, entry) in builder.build().enumerate() {
if detection_entries >= ctx.limit {
tracing::warn!(
limit = ctx.limit,
"ignored-manifest detection walk truncated: reached the maximum number of \
entries per run; some .gitignore/.ignore exclusions may go unreported"
);
return;
}
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
ctx.outcome.walk_errors.push(error.to_string());
continue;
}
};
if !entry.file_type().is_some_and(|t| t.is_file()) {
let path = entry.path();
if entry.path_is_symlink() && symlink_is_manifest_shaped(path, ctx.registry) {
let display = path
.strip_prefix(display_root)
.unwrap_or(path)
.to_path_buf();
if !visited.contains(&display) {
ctx.outcome.ignored_manifests.push(display);
}
}
continue;
}
let path = entry.path();
let display = path
.strip_prefix(display_root)
.unwrap_or(path)
.to_path_buf();
if visited.contains(&display) {
continue;
}
match url::Url::from_file_path(path) {
Ok(uri) => {
if ctx.registry.for_uri(&uri).is_some() {
ctx.outcome.ignored_manifests.push(display);
}
}
Err(()) => {
ctx.outcome.walk_errors.push(format!(
"could not convert to a file URI while checking for ignore-suppressed \
manifests, skipping: {}",
display.display()
));
}
}
}
}
fn hidden_ecosystem_directories(registry: &EcosystemRegistry) -> BTreeSet<String> {
let mut dirs = BTreeSet::new();
for id in registry.ecosystem_ids() {
let Some(ecosystem) = registry.get(id) else {
continue;
};
for (dir_pattern, _suffix) in ecosystem.manifest_directory_patterns() {
if let Some(first) = dir_pattern.split('/').next()
&& first.starts_with('.')
{
dirs.insert(first.to_string());
}
}
}
dirs
}
fn canonicalize_within_root(path: &Path, canonical_root: Option<&Path>) -> Option<PathBuf> {
let canonical_root = canonical_root?;
let canonical_path = std::fs::canonicalize(path).ok()?;
canonical_path
.starts_with(canonical_root)
.then_some(canonical_path)
}
fn symlink_is_manifest_shaped(path: &Path, registry: &EcosystemRegistry) -> bool {
let Ok(metadata) = std::fs::metadata(path) else {
return false;
};
if !metadata.is_file() {
return false;
}
let Ok(uri) = url::Url::from_file_path(path) else {
return false;
};
registry.for_uri(&uri).is_some()
}
fn route_file(
route_path: &Path,
read_path: &Path,
display_path: &Path,
registry: &EcosystemRegistry,
outcome: &mut WalkOutcome,
) {
let Ok(uri) = url::Url::from_file_path(route_path) else {
outcome.walk_errors.push(format!(
"could not convert to a file URI, skipping: {}",
display_path.display()
));
return;
};
if let Some(ecosystem) = registry.for_uri(&uri) {
outcome.manifests.push(DiscoveredManifest {
path: read_path.to_path_buf(),
uri_path: route_path.to_path_buf(),
display_path: display_path.to_path_buf(),
ecosystem,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::sync::Mutex;
static CWD_LOCK: Mutex<()> = Mutex::new(());
struct CwdGuard {
original: PathBuf,
_lock: std::sync::MutexGuard<'static, ()>,
}
impl CwdGuard {
fn chdir(dir: &Path) -> Self {
let lock = CWD_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let original = std::env::current_dir().expect("read cwd");
std::env::set_current_dir(dir).expect("chdir");
Self {
original,
_lock: lock,
}
}
}
impl Drop for CwdGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.original);
}
}
fn test_registry() -> EcosystemRegistry {
let registry = EcosystemRegistry::new();
let runtime = deps_engine::setup::EcosystemRuntime::from_policy(
&deps_core::policy_config::PolicyConfig::default(),
);
deps_engine::setup::register_ecosystems(
®istry,
Arc::new(deps_core::HttpCache::new()),
&runtime,
);
registry
}
#[test]
fn test_walk_empty_directory_finds_nothing() {
let dir = tempfile::tempdir().expect("create temp dir");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert!(outcome.manifests.is_empty());
assert!(!outcome.truncated);
}
#[test]
fn test_walk_finds_cargo_toml() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n")
.expect("write manifest");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert_eq!(outcome.manifests.len(), 1);
assert_eq!(
outcome.manifests[0].display_path,
PathBuf::from("Cargo.toml")
);
assert_eq!(outcome.manifests[0].ecosystem.id(), "cargo");
}
#[test]
fn test_walk_respect_gitignore_true_skips_gitignored_manifest() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir(dir.path().join(".git")).expect("create .git marker");
fs::write(dir.path().join(".gitignore"), "ignored/\n").expect("write gitignore");
fs::create_dir(dir.path().join("ignored")).expect("mkdir");
fs::write(dir.path().join("ignored").join("Cargo.toml"), "[package]\n")
.expect("write manifest");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), true, false);
assert!(outcome.manifests.is_empty());
assert_eq!(
outcome.ignored_manifests,
vec![PathBuf::from("ignored").join("Cargo.toml")]
);
}
#[test]
fn test_walk_default_does_not_respect_gitignore() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir(dir.path().join(".git")).expect("create .git marker");
fs::write(dir.path().join(".gitignore"), "Cargo.toml\n").expect("write gitignore");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert_eq!(outcome.manifests.len(), 1);
assert!(outcome.ignored_manifests.is_empty());
}
#[test]
fn test_walk_default_ignores_nested_gitignore() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir(dir.path().join(".git")).expect("create .git marker");
fs::create_dir(dir.path().join("sub")).expect("mkdir sub");
fs::write(dir.path().join("sub").join(".gitignore"), "Cargo.toml\n")
.expect("write nested gitignore");
fs::write(dir.path().join("sub").join("Cargo.toml"), "[package]\n")
.expect("write manifest");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert_eq!(outcome.manifests.len(), 1);
}
#[test]
fn test_walk_default_ignores_dot_ignore_file_without_git_repo() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::write(dir.path().join(".ignore"), "Cargo.toml\n").expect("write .ignore");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert_eq!(outcome.manifests.len(), 1);
}
#[test]
fn test_walk_default_prunes_node_modules() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
fs::create_dir_all(dir.path().join("node_modules").join("left-pad"))
.expect("mkdir node_modules/left-pad");
fs::write(
dir.path()
.join("node_modules")
.join("left-pad")
.join("package.json"),
"{}",
)
.expect("write vendored manifest");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert_eq!(outcome.manifests.len(), 1);
assert_eq!(
outcome.manifests[0].display_path,
PathBuf::from("Cargo.toml")
);
}
#[test]
fn test_walk_default_warns_on_manifest_directly_inside_pruned_directory() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir(dir.path().join("vendor")).expect("mkdir vendor");
fs::write(dir.path().join("vendor").join("Cargo.toml"), "[package]\n")
.expect("write manifest directly under pruned dir");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert!(
outcome.manifests.is_empty(),
"still pruned from the primary scan"
);
assert_eq!(
outcome.ignored_manifests,
vec![PathBuf::from("vendor").join("Cargo.toml")]
);
}
#[test]
fn test_walk_manifest_nested_two_levels_inside_pruned_directory_remains_unreported() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir_all(dir.path().join("vendor").join("sub")).expect("mkdir vendor/sub");
fs::write(
dir.path().join("vendor").join("sub").join("Cargo.toml"),
"[package]\n",
)
.expect("write nested manifest");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert!(outcome.manifests.is_empty());
assert!(outcome.ignored_manifests.is_empty());
}
#[test]
fn test_detect_ignored_manifests_uses_its_own_budget_and_does_not_set_truncated() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir(dir.path().join(".git")).expect("create .git marker");
fs::write(dir.path().join(".gitignore"), "noise/\n").expect("write gitignore");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
fs::create_dir(dir.path().join("noise")).expect("mkdir noise");
for i in 0..20 {
fs::write(dir.path().join("noise").join(format!("f{i}.txt")), "")
.expect("write noise file");
}
let outcome = walk_with_limit(
&[dir.path().to_path_buf()],
&test_registry(),
5,
true,
false,
);
assert!(
!outcome.truncated,
"the diagnostic pass' own budget exhaustion must not mark the primary walk truncated"
);
assert_eq!(
outcome.manifests.len(),
1,
"primary walk result must still be complete"
);
}
#[test]
fn test_walk_respect_gitignore_does_not_warn_on_pruned_directory() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir(dir.path().join(".git")).expect("create .git marker");
fs::write(dir.path().join(".gitignore"), "node_modules/\n").expect("write gitignore");
fs::create_dir_all(dir.path().join("node_modules").join("left-pad"))
.expect("mkdir node_modules/left-pad");
fs::write(
dir.path()
.join("node_modules")
.join("left-pad")
.join("package.json"),
"{}",
)
.expect("write vendored manifest");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), true, false);
assert!(outcome.ignored_manifests.is_empty());
}
#[test]
fn test_walk_git_info_exclude_suppression_not_misreported_as_ignored_manifest() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir_all(dir.path().join(".git").join("info")).expect("mkdir .git/info");
fs::write(
dir.path().join(".git").join("info").join("exclude"),
"Cargo.toml\n",
)
.expect("write git info/exclude");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), true, false);
assert!(outcome.manifests.is_empty());
assert!(
outcome.ignored_manifests.is_empty(),
".git/info/exclude is operator-controlled, not a .gitignore/.ignore rule"
);
}
#[test]
fn test_walk_single_file_path_bypasses_gitignore() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::write(dir.path().join(".gitignore"), "Cargo.toml\n").expect("write gitignore");
let manifest = dir.path().join("Cargo.toml");
fs::write(&manifest, "[package]\n").expect("write manifest");
let outcome = walk(&[manifest], &test_registry(), true, false);
assert_eq!(outcome.manifests.len(), 1);
}
#[test]
fn test_walk_relative_root_finds_manifest() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
let _guard = CwdGuard::chdir(dir.path());
let outcome = walk(&[PathBuf::from(".")], &test_registry(), false, false);
assert_eq!(outcome.manifests.len(), 1);
assert!(outcome.walk_errors.is_empty());
assert_eq!(
outcome.manifests[0].display_path,
PathBuf::from("Cargo.toml")
);
}
#[test]
fn test_walk_relative_explicit_file_path_is_found() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
let _guard = CwdGuard::chdir(dir.path());
let outcome = walk(
&[PathBuf::from("Cargo.toml")],
&test_registry(),
false,
false,
);
assert_eq!(outcome.manifests.len(), 1);
assert!(outcome.unrecognized_explicit_paths.is_empty());
}
#[test]
fn test_walk_with_limit_truncates_on_walked_entries_not_just_manifests() {
let dir = tempfile::tempdir().expect("create temp dir");
for i in 0..5 {
fs::write(dir.path().join(format!("noise-{i}.txt")), "").expect("write noise file");
}
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
let outcome = walk_with_limit(
&[dir.path().to_path_buf()],
&test_registry(),
2,
false,
false,
);
assert!(
outcome.truncated,
"a 2-entry limit against a 6-entry tree must truncate"
);
}
#[test]
fn test_walk_with_limit_does_not_truncate_when_under_the_cap() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
let outcome = walk_with_limit(
&[dir.path().to_path_buf()],
&test_registry(),
100,
false,
false,
);
assert!(!outcome.truncated);
assert_eq!(outcome.manifests.len(), 1);
}
#[test]
fn test_walk_explicit_unrecognized_path_is_reported() {
let dir = tempfile::tempdir().expect("create temp dir");
let unknown = dir.path().join("notes.txt");
fs::write(&unknown, "not a manifest").expect("write file");
let outcome = walk(
std::slice::from_ref(&unknown),
&test_registry(),
false,
false,
);
assert!(outcome.manifests.is_empty());
assert_eq!(outcome.unrecognized_explicit_paths, vec![unknown]);
}
#[test]
fn test_walk_unrecognized_file_found_during_directory_walk_is_not_reported() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::write(dir.path().join("notes.txt"), "not a manifest").expect("write file");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert!(outcome.unrecognized_explicit_paths.is_empty());
}
#[test]
fn test_walk_multiple_ecosystems_in_one_tree() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write cargo manifest");
fs::write(dir.path().join("package.json"), "{}").expect("write npm manifest");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert_eq!(outcome.manifests.len(), 2);
}
#[test]
fn test_walk_never_descends_into_dot_git() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir(dir.path().join(".git")).expect("create .git marker");
for i in 0..100 {
fs::write(dir.path().join(".git").join(format!("object-{i}")), "")
.expect("write dummy git-internal file");
}
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
let outcome = walk_with_limit(
&[dir.path().to_path_buf()],
&test_registry(),
3,
false,
false,
);
assert!(
!outcome.truncated,
".git's 100 dummy files must never be walked, so a limit of 3 must suffice"
);
assert_eq!(outcome.manifests.len(), 1);
assert_eq!(
outcome.manifests[0].display_path,
PathBuf::from("Cargo.toml")
);
}
#[test]
fn test_walk_still_finds_github_workflows_alongside_excluded_dot_git() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir(dir.path().join(".git")).expect("create .git marker");
fs::create_dir_all(dir.path().join(".github").join("workflows")).expect("mkdir");
fs::write(
dir.path().join(".github").join("workflows").join("ci.yml"),
"on: push\njobs:\n x:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n",
)
.expect("write workflow");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert_eq!(outcome.manifests.len(), 1);
assert_eq!(
outcome.manifests[0].display_path,
PathBuf::from(".github").join("workflows").join("ci.yml")
);
assert_eq!(outcome.manifests[0].ecosystem.id(), "github-actions");
}
#[test]
fn test_walk_with_limit_truncates_on_explicit_path_list() {
let dir = tempfile::tempdir().expect("create temp dir");
let paths: Vec<PathBuf> = (0..5)
.map(|i| {
let subdir = dir.path().join(format!("pkg{i}"));
fs::create_dir(&subdir).expect("mkdir");
let path = subdir.join("Cargo.toml");
fs::write(&path, "[package]\n").expect("write manifest");
path
})
.collect();
let outcome = walk_with_limit(&paths, &test_registry(), 2, false, false);
assert!(
outcome.truncated,
"a 2-entry limit against 5 explicit paths must truncate"
);
assert_eq!(outcome.manifests.len(), 2);
}
#[cfg(unix)]
#[test]
fn test_walk_default_detects_symlinked_manifest_without_following() {
let dir = tempfile::tempdir().expect("create temp dir");
let real = dir.path().join("real").join("manifest-data");
fs::create_dir(dir.path().join("real")).expect("mkdir real");
fs::write(&real, "[package]\n").expect("write real manifest");
std::os::unix::fs::symlink(&real, dir.path().join("Cargo.toml")).expect("create symlink");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert!(outcome.manifests.is_empty());
assert_eq!(outcome.ignored_manifests, vec![PathBuf::from("Cargo.toml")]);
}
#[cfg(unix)]
#[test]
fn test_walk_broken_symlink_is_not_reported_as_manifest() {
let dir = tempfile::tempdir().expect("create temp dir");
std::os::unix::fs::symlink(
dir.path().join("does-not-exist"),
dir.path().join("Cargo.toml"),
)
.expect("create broken symlink");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert!(outcome.manifests.is_empty());
assert!(outcome.ignored_manifests.is_empty());
}
#[cfg(unix)]
#[test]
fn test_walk_symlink_to_directory_is_not_reported_as_manifest() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir(dir.path().join("real_dir")).expect("mkdir real_dir");
std::os::unix::fs::symlink(dir.path().join("real_dir"), dir.path().join("link_dir"))
.expect("create symlink to directory");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert!(outcome.manifests.is_empty());
assert!(outcome.ignored_manifests.is_empty());
}
#[cfg(unix)]
#[test]
fn test_walk_pruned_directory_symlinked_manifest_is_still_warned() {
let dir = tempfile::tempdir().expect("create temp dir");
let real = dir.path().join("real-cargo.toml");
fs::write(&real, "[package]\n").expect("write real manifest");
fs::create_dir(dir.path().join("vendor")).expect("mkdir vendor");
std::os::unix::fs::symlink(&real, dir.path().join("vendor").join("Cargo.toml"))
.expect("create symlink inside pruned directory");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert!(
outcome.manifests.is_empty(),
"still pruned from the primary scan"
);
assert_eq!(
outcome.ignored_manifests,
vec![PathBuf::from("vendor").join("Cargo.toml")]
);
}
#[cfg(unix)]
#[test]
fn test_walk_follow_symlinks_resolves_and_routes_manifest() {
let dir = tempfile::tempdir().expect("create temp dir");
let real = dir.path().join("real").join("manifest-data");
fs::create_dir(dir.path().join("real")).expect("mkdir real");
fs::write(&real, "[package]\n").expect("write real manifest");
std::os::unix::fs::symlink(&real, dir.path().join("Cargo.toml")).expect("create symlink");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, true);
assert_eq!(outcome.manifests.len(), 1);
assert!(outcome.ignored_manifests.is_empty());
assert_eq!(outcome.manifests[0].ecosystem.id(), "cargo");
assert_eq!(
outcome.manifests[0]
.path
.canonicalize()
.expect("canonicalize actual path"),
real.canonicalize()
.expect("canonicalize expected real path")
);
assert_ne!(
outcome.manifests[0].path,
dir.path().join("Cargo.toml"),
"path must be the resolved real path, not the symlink's own raw path"
);
}
#[cfg(unix)]
#[test]
fn test_walk_follow_symlinks_toggles_between_ignored_and_routed() {
let dir = tempfile::tempdir().expect("create temp dir");
let real = dir.path().join("real").join("manifest-data");
fs::create_dir(dir.path().join("real")).expect("mkdir real");
fs::write(&real, "[package]\n").expect("write real manifest");
std::os::unix::fs::symlink(&real, dir.path().join("Cargo.toml")).expect("create symlink");
let disabled = walk(&[dir.path().to_path_buf()], &test_registry(), false, false);
assert!(disabled.manifests.is_empty());
assert_eq!(
disabled.ignored_manifests,
vec![PathBuf::from("Cargo.toml")]
);
let enabled = walk(&[dir.path().to_path_buf()], &test_registry(), false, true);
assert_eq!(enabled.manifests.len(), 1);
assert!(enabled.ignored_manifests.is_empty());
}
#[cfg(unix)]
#[test]
fn test_walk_follow_symlinks_display_path_is_symlink_path_not_target() {
let dir = tempfile::tempdir().expect("create temp dir");
let real = dir.path().join("real").join("manifest-data");
fs::create_dir(dir.path().join("real")).expect("mkdir real");
fs::write(&real, "[package]\n").expect("write real manifest");
std::os::unix::fs::symlink(&real, dir.path().join("Cargo.toml")).expect("create symlink");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, true);
assert_eq!(outcome.manifests.len(), 1);
assert_eq!(
outcome.manifests[0].display_path,
PathBuf::from("Cargo.toml")
);
}
#[cfg(unix)]
#[test]
fn test_walk_follow_symlinks_still_prunes_node_modules() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
let real_vendored = dir.path().join("real-vendored-manifest");
fs::write(&real_vendored, "{}").expect("write real vendored manifest");
fs::create_dir_all(dir.path().join("node_modules").join("left-pad"))
.expect("mkdir node_modules/left-pad");
std::os::unix::fs::symlink(
&real_vendored,
dir.path()
.join("node_modules")
.join("left-pad")
.join("package.json"),
)
.expect("symlink vendored manifest inside pruned directory");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, true);
assert_eq!(
outcome.manifests.len(),
1,
"a symlinked manifest inside a pruned directory must still be pruned, not routed"
);
assert_eq!(
outcome.manifests[0].display_path,
PathBuf::from("Cargo.toml")
);
}
#[cfg(unix)]
#[test]
fn test_walk_follow_symlinks_still_enforces_max_walked_files() {
let dir = tempfile::tempdir().expect("create temp dir");
let noise_target = dir.path().join(".noise-source");
fs::create_dir(&noise_target).expect("mkdir .noise-source");
for i in 0..5 {
fs::write(noise_target.join(format!("noise-{i}.txt")), "").expect("write noise file");
}
std::os::unix::fs::symlink(&noise_target, dir.path().join("noise-link"))
.expect("symlink noise directory");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
let outcome = walk_with_limit(
&[dir.path().to_path_buf()],
&test_registry(),
4,
false,
true,
);
assert!(
outcome.truncated,
"a 4-entry limit against a tree inflated by a symlinked directory must truncate"
);
}
#[cfg(unix)]
#[test]
fn test_walk_follow_symlinks_rejects_target_outside_root() {
let outside = tempfile::tempdir().expect("create outside temp dir");
let outside_manifest = outside.path().join("Cargo.toml");
fs::write(&outside_manifest, "[package]\n").expect("write outside manifest");
let root = tempfile::tempdir().expect("create walked root");
std::os::unix::fs::symlink(&outside_manifest, root.path().join("Cargo.toml"))
.expect("create symlink escaping the walked root");
let outcome = walk(&[root.path().to_path_buf()], &test_registry(), false, true);
assert!(
outcome.manifests.is_empty(),
"must never route a symlink target outside the walked root"
);
assert_eq!(outcome.ignored_manifests, vec![PathBuf::from("Cargo.toml")]);
}
#[cfg(unix)]
#[test]
fn test_walk_follow_symlinks_reports_symlink_loop_via_walk_errors() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir_all(dir.path().join("a")).expect("mkdir a");
fs::create_dir_all(dir.path().join("b")).expect("mkdir b");
std::os::unix::fs::symlink(dir.path().join("b"), dir.path().join("a").join("loop"))
.expect("create a/loop -> b");
std::os::unix::fs::symlink(dir.path().join("a"), dir.path().join("b").join("loop"))
.expect("create b/loop -> a");
fs::write(dir.path().join("Cargo.toml"), "[package]\n").expect("write manifest");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), false, true);
assert!(
outcome
.walk_errors
.iter()
.any(|error| error.to_lowercase().contains("loop")),
"a symlink loop must be reported via walk_errors naming the loop, not just any \
error, and must not hang or crash: {:?}",
outcome.walk_errors
);
assert!(
outcome
.manifests
.iter()
.any(|m| m.display_path == Path::new("Cargo.toml")),
"other manifests in the same tree must still be found"
);
}
#[cfg(unix)]
#[test]
fn test_walk_follow_symlinks_and_respect_gitignore_together_still_honors_gitignore() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir(dir.path().join(".git")).expect("create .git marker");
fs::write(dir.path().join(".gitignore"), "Cargo.toml\n").expect("write gitignore");
let real = dir.path().join("real").join("manifest-data");
fs::create_dir(dir.path().join("real")).expect("mkdir real");
fs::write(&real, "[package]\n").expect("write real manifest");
std::os::unix::fs::symlink(&real, dir.path().join("Cargo.toml")).expect("create symlink");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), true, true);
assert!(
outcome.manifests.is_empty(),
"a .gitignore-excluded symlinked manifest must not be routed even under \
--follow-symlinks"
);
assert_eq!(outcome.ignored_manifests, vec![PathBuf::from("Cargo.toml")]);
}
#[cfg(unix)]
#[test]
fn test_walk_follow_symlinks_rejects_directory_symlink_escaping_root() {
let outside = tempfile::tempdir().expect("create outside temp dir");
fs::write(outside.path().join("Cargo.toml"), "[package]\n")
.expect("write outside manifest");
let root = tempfile::tempdir().expect("create walked root");
fs::write(root.path().join("Cargo.toml"), "[package]\n").expect("write root manifest");
std::os::unix::fs::symlink(outside.path(), root.path().join("evil"))
.expect("symlink a directory escaping the walked root");
let outcome = walk(&[root.path().to_path_buf()], &test_registry(), false, true);
assert!(
outcome
.manifests
.iter()
.all(|m| m.display_path != PathBuf::from("evil").join("Cargo.toml")),
"a manifest reached only by descending into a symlinked directory outside the \
walked root must never be routed: {:?}",
outcome
.manifests
.iter()
.map(|m| &m.display_path)
.collect::<Vec<_>>()
);
assert_eq!(
outcome.manifests.len(),
1,
"the root's own, non-escaping Cargo.toml must still be found"
);
}
#[cfg(unix)]
#[test]
fn test_walk_follow_symlinks_escaping_directory_does_not_exhaust_the_budget() {
let outside = tempfile::tempdir().expect("create outside temp dir");
for i in 0..50 {
fs::write(outside.path().join(format!("noise-{i}.txt")), "")
.expect("write outside noise file");
}
let root = tempfile::tempdir().expect("create walked root");
fs::write(root.path().join("Cargo.toml"), "[package]\n").expect("write root manifest");
std::os::unix::fs::symlink(outside.path(), root.path().join("evil"))
.expect("symlink a directory escaping the walked root");
let outcome = walk_with_limit(
&[root.path().to_path_buf()],
&test_registry(),
5,
false,
true,
);
assert!(
!outcome.truncated,
"pruning the escaping directory before descent must keep the walk well under the \
budget, not exhaust it on external content"
);
assert_eq!(
outcome.manifests.len(),
1,
"the root's own manifest must still be found"
);
}
#[cfg(unix)]
#[test]
fn test_walk_respect_gitignore_does_not_duplicate_symlinked_manifest_warning() {
let dir = tempfile::tempdir().expect("create temp dir");
fs::create_dir(dir.path().join(".git")).expect("create .git marker");
fs::write(dir.path().join(".gitignore"), "*.log\n").expect("write gitignore");
let real = dir.path().join("real").join("manifest-data");
fs::create_dir(dir.path().join("real")).expect("mkdir real");
fs::write(&real, "[package]\n").expect("write real manifest");
std::os::unix::fs::symlink(&real, dir.path().join("Cargo.toml")).expect("create symlink");
let outcome = walk(&[dir.path().to_path_buf()], &test_registry(), true, false);
assert_eq!(
outcome.ignored_manifests,
vec![PathBuf::from("Cargo.toml")],
"a non-gitignored symlinked manifest must be reported exactly once"
);
}
#[cfg(unix)]
#[test]
fn test_walk_default_rejects_symlinked_hidden_ecosystem_directory_escaping_root() {
let outside = tempfile::tempdir().expect("create outside temp dir");
fs::create_dir_all(outside.path().join("workflows")).expect("mkdir workflows");
fs::write(
outside.path().join("workflows").join("ci.yml"),
"on: push\njobs:\n x:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n",
)
.expect("write workflow");
let root = tempfile::tempdir().expect("create walked root");
std::os::unix::fs::symlink(outside.path(), root.path().join(".github"))
.expect("symlink .github escaping the walked root");
let outcome = walk(&[root.path().to_path_buf()], &test_registry(), false, false);
assert!(
outcome.manifests.is_empty(),
"a symlinked .github escaping the walked root must never be scanned, even in the \
default mode: {:?}",
outcome
.manifests
.iter()
.map(|m| &m.display_path)
.collect::<Vec<_>>()
);
}
}