use std::io;
use std::path::{Component, Path, PathBuf};
pub fn canonicalize_path(path: &Path) -> Result<PathBuf, io::Error> {
match std::fs::canonicalize(path) {
Ok(canonical) => Ok(canonical),
Err(e) => {
log::debug!(
"Canonicalization failed for '{}': {}. Using absolutize fallback.",
path.display(),
e
);
absolutize_without_resolution(path)
}
}
}
pub fn absolutize_without_resolution(path: &Path) -> Result<PathBuf, io::Error> {
let cwd = std::env::current_dir()?;
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
};
let normalized = normalize_path_components(&absolute);
Ok(normalized)
}
#[must_use]
pub fn normalize_path_components(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
Component::CurDir => {
}
Component::ParentDir => {
match components.last() {
Some(Component::Normal(_)) => {
components.pop();
}
Some(Component::ParentDir) | None => {
components.push(component);
}
_ => {
}
}
}
_ => {
components.push(component);
}
}
}
if components.is_empty() {
PathBuf::from(".")
} else {
components.iter().collect()
}
}
pub const DEFAULT_IGNORED_DIRS: &[&str] = &[
"node_modules",
"target",
"build",
"dist",
"vendor",
".cache",
".npm",
".cargo",
"__pycache__",
".pytest_cache",
".mypy_cache",
".tox",
".venv",
"venv",
".gradle",
".idea",
".vs",
".vscode",
];
#[must_use]
pub fn is_ignored_dir(name: &std::ffi::OsStr) -> bool {
is_ignored_dir_with_config(name, DEFAULT_IGNORED_DIRS)
}
#[must_use]
pub fn is_ignored_dir_with_config(name: &std::ffi::OsStr, ignored_dirs: &[&str]) -> bool {
if let Some(name_str) = name.to_str() {
ignored_dirs.contains(&name_str)
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_normalize_removes_current_dir() {
let path = Path::new("/home/./user/./project");
let result = normalize_path_components(path);
assert_eq!(result, Path::new("/home/user/project"));
}
#[test]
fn test_normalize_resolves_parent_dir() {
let path = Path::new("/home/user/../other/project");
let result = normalize_path_components(path);
assert_eq!(result, Path::new("/home/other/project"));
}
#[test]
fn test_normalize_combined() {
let path = Path::new("/home/user/../user/./project/./src/../lib");
let result = normalize_path_components(path);
assert_eq!(result, Path::new("/home/user/project/lib"));
}
#[test]
fn test_normalize_preserves_root() {
let path = Path::new("/");
let result = normalize_path_components(path);
assert_eq!(result, Path::new("/"));
}
#[test]
fn test_normalize_relative_path() {
let path = Path::new("foo/../bar");
let result = normalize_path_components(path);
assert_eq!(result, Path::new("bar"));
}
#[test]
fn test_normalize_relative_above_start() {
let path = Path::new("../foo");
let result = normalize_path_components(path);
assert_eq!(result, Path::new("../foo"));
}
#[test]
fn test_normalize_empty_result() {
let path = Path::new("foo/..");
let result = normalize_path_components(path);
assert_eq!(result, Path::new("."));
}
#[test]
fn test_absolutize_determinism() {
let result1 = absolutize_without_resolution(Path::new("./foo/../bar")).unwrap();
let result2 = absolutize_without_resolution(Path::new("bar")).unwrap();
assert_eq!(result1, result2);
}
#[test]
fn test_absolutize_absolute_path_unchanged() {
#[cfg(unix)]
let path = Path::new("/absolute/path");
#[cfg(windows)]
let path = Path::new("C:\\absolute\\path");
let result = absolutize_without_resolution(path).unwrap();
assert_eq!(result, path);
}
#[test]
fn test_canonicalize_existing_path() {
#[cfg(unix)]
{
let result = canonicalize_path(Path::new("/tmp"));
assert!(result.is_ok());
assert!(result.unwrap().is_absolute());
}
}
#[test]
fn test_canonicalize_nonexistent_path_uses_fallback() {
let path = Path::new("/nonexistent/deeply/nested/path");
let result = canonicalize_path(path);
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
assert!(resolved.to_string_lossy().contains("nonexistent"));
}
#[test]
fn test_is_ignored_dir() {
use std::ffi::OsStr;
assert!(is_ignored_dir(OsStr::new("node_modules")));
assert!(is_ignored_dir(OsStr::new("target")));
assert!(is_ignored_dir(OsStr::new("__pycache__")));
assert!(!is_ignored_dir(OsStr::new(".git")));
assert!(!is_ignored_dir(OsStr::new("src")));
assert!(!is_ignored_dir(OsStr::new("lib")));
}
#[test]
fn test_is_ignored_dir_with_config_custom_list() {
use std::ffi::OsStr;
let custom_ignores = &["my_deps", "cached_stuff", "third_party"];
assert!(is_ignored_dir_with_config(
OsStr::new("my_deps"),
custom_ignores
));
assert!(is_ignored_dir_with_config(
OsStr::new("cached_stuff"),
custom_ignores
));
assert!(is_ignored_dir_with_config(
OsStr::new("third_party"),
custom_ignores
));
assert!(!is_ignored_dir_with_config(
OsStr::new("node_modules"),
custom_ignores
));
assert!(!is_ignored_dir_with_config(
OsStr::new("target"),
custom_ignores
));
assert!(!is_ignored_dir_with_config(
OsStr::new("src"),
custom_ignores
));
assert!(!is_ignored_dir_with_config(
OsStr::new(".git"),
custom_ignores
));
}
#[test]
fn test_is_ignored_dir_with_config_empty_list() {
use std::ffi::OsStr;
let empty: &[&str] = &[];
assert!(!is_ignored_dir_with_config(
OsStr::new("node_modules"),
empty
));
assert!(!is_ignored_dir_with_config(OsStr::new("target"), empty));
assert!(!is_ignored_dir_with_config(OsStr::new("src"), empty));
}
#[test]
fn test_is_ignored_dir_with_config_default_list() {
use std::ffi::OsStr;
assert_eq!(
is_ignored_dir(OsStr::new("node_modules")),
is_ignored_dir_with_config(OsStr::new("node_modules"), DEFAULT_IGNORED_DIRS)
);
assert_eq!(
is_ignored_dir(OsStr::new("src")),
is_ignored_dir_with_config(OsStr::new("src"), DEFAULT_IGNORED_DIRS)
);
}
#[test]
fn test_default_ignored_dirs_contains_common_dirs() {
assert!(DEFAULT_IGNORED_DIRS.contains(&"node_modules"));
assert!(DEFAULT_IGNORED_DIRS.contains(&"target"));
assert!(DEFAULT_IGNORED_DIRS.contains(&"vendor"));
assert!(DEFAULT_IGNORED_DIRS.contains(&"__pycache__"));
assert!(DEFAULT_IGNORED_DIRS.contains(&".venv"));
assert!(DEFAULT_IGNORED_DIRS.contains(&".idea"));
assert!(!DEFAULT_IGNORED_DIRS.contains(&".git"));
}
#[cfg(unix)]
#[test]
fn test_canonicalize_symlink() {
use std::os::unix::fs::symlink;
use tempfile::TempDir;
let temp = TempDir::new().unwrap();
let target = temp.path().join("target_dir");
let link = temp.path().join("link");
std::fs::create_dir(&target).unwrap();
symlink(&target, &link).unwrap();
let result = canonicalize_path(&link).unwrap();
let expected = canonicalize_path(&target).unwrap();
assert_eq!(result, expected);
}
#[cfg(unix)]
#[test]
fn test_canonicalize_broken_symlink_uses_fallback() {
use std::os::unix::fs::symlink;
use tempfile::TempDir;
let temp = TempDir::new().unwrap();
let link = temp.path().join("broken_link");
symlink("/nonexistent/target", &link).unwrap();
let result = canonicalize_path(&link);
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
}
}