use std::path::{Component, Path, PathBuf};
use std::process::Command;
pub fn project_root(scan_path: &Path, configured_root: Option<&Path>) -> PathBuf {
if let Some(root) = configured_root {
return resolve_path_for_boundary(root);
}
git_repo_root(scan_path).unwrap_or_else(|| resolve_scan_root(scan_path))
}
pub fn resolve_scan_root(scan_path: &Path) -> PathBuf {
let scan_root = resolve_path_for_boundary(scan_path);
if scan_root.is_file() {
scan_root
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf()
} else {
scan_root
}
}
pub fn resolve_path_for_boundary(path: &Path) -> PathBuf {
if is_windows_drive_absolute_path(path) {
return normalize_path(path);
}
if let Ok(canonical) = path.canonicalize() {
return canonical;
}
let absolute = absolutize_path(path);
for ancestor in absolute.ancestors() {
if let Ok(canonical_ancestor) = ancestor.canonicalize() {
let suffix = absolute
.strip_prefix(ancestor)
.unwrap_or_else(|_| Path::new(""));
return normalize_path(&canonical_ancestor.join(suffix));
}
}
normalize_path(&absolute)
}
pub fn finding_path_key(root: &Path, finding_file: &str) -> String {
let value = normalize_separators(finding_file);
let path = Path::new(&value);
let resolved = if path.is_absolute() || is_windows_drive_absolute(&value) {
resolve_path_for_boundary(path)
} else {
resolve_path_for_boundary(¤t_dir().join(path))
};
root_relative_key(root, &resolved)
}
pub fn stored_path_key(root: &Path, stored_path: &str) -> String {
let value = normalize_separators(stored_path);
let path = Path::new(&value);
if path.is_absolute() || is_windows_drive_absolute(&value) {
root_relative_key(root, &resolve_path_for_boundary(path))
} else {
slash_path(&normalize_path(path))
}
}
fn root_relative_key(root: &Path, path: &Path) -> String {
let root = resolve_path_for_boundary(root);
let path = resolve_path_for_boundary(path);
let relative = path.strip_prefix(&root).unwrap_or(path.as_path());
slash_path(relative)
}
fn git_repo_root(scan_path: &Path) -> Option<PathBuf> {
let dir = if scan_path.is_dir() {
scan_path
} else {
scan_path.parent().unwrap_or_else(|| Path::new("."))
};
let output = Command::new("git")
.arg("-C")
.arg(dir)
.args(["rev-parse", "--show-toplevel"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let root = String::from_utf8_lossy(&output.stdout);
let root = root.trim();
if root.is_empty() {
None
} else {
Some(resolve_path_for_boundary(Path::new(root)))
}
}
fn current_dir() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
fn absolutize_path(path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
current_dir().join(path)
}
}
fn normalize_path(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
Component::RootDir => normalized.push(component.as_os_str()),
Component::CurDir => {}
Component::ParentDir => {
if !normalized.pop() {
normalized.push(component.as_os_str());
}
}
Component::Normal(part) => normalized.push(part),
}
}
normalized
}
fn normalize_separators(path: &str) -> String {
path.replace('\\', "/")
}
fn is_windows_drive_absolute_path(path: &Path) -> bool {
is_windows_drive_absolute(&path.to_string_lossy().replace('\\', "/"))
}
fn is_windows_drive_absolute(path: &str) -> bool {
let bytes = path.as_bytes();
bytes.len() >= 3 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' && bytes[2] == b'/'
}
fn slash_path(path: &Path) -> String {
let normalized = normalize_path(path).to_string_lossy().replace('\\', "/");
normalized
.strip_prefix("./")
.unwrap_or(normalized.as_str())
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn stored_relative_paths_use_forward_slashes() {
let repo = TempDir::new().expect("failed to create temp dir");
assert_eq!(
stored_path_key(repo.path(), "src\\nested\\app.py"),
"src/nested/app.py"
);
}
#[test]
fn absolute_paths_under_root_become_root_relative() {
let repo = TempDir::new().expect("failed to create temp dir");
let path = repo.path().join("src/app.py");
assert_eq!(
stored_path_key(repo.path(), path.to_str().expect("non-utf8 path")),
"src/app.py"
);
}
#[test]
fn windows_drive_absolute_paths_are_not_treated_as_cwd_relative() {
let root = Path::new("C:/repo");
assert_eq!(
finding_path_key(root, "C:\\repo\\src\\app.py"),
"src/app.py"
);
assert_eq!(stored_path_key(root, "C:\\repo\\src\\app.py"), "src/app.py");
}
}