use anyhow::Result;
use std::path::{Path, PathBuf};
#[must_use]
pub fn normalize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::CurDir => {} std::path::Component::ParentDir => {
components.pop(); }
c => components.push(c),
}
}
components.iter().collect()
}
#[must_use]
pub fn is_safe_path(base: &Path, path: &Path) -> bool {
let normalized_base = normalize_path(base);
let normalized_path = if path.is_absolute() {
normalize_path(path)
} else {
normalize_path(&base.join(path))
};
normalized_path.starts_with(normalized_base)
}
pub fn find_project_root(start: &Path) -> Result<PathBuf> {
let mut current = start.canonicalize().unwrap_or_else(|_| start.to_path_buf());
loop {
if current.join("agpm.toml").exists() {
return Ok(current);
}
if !current.pop() {
return Err(anyhow::anyhow!(
"No agpm.toml found in current directory or any parent directory"
));
}
}
}
pub fn get_global_config_path() -> Result<PathBuf> {
let home = crate::utils::platform::get_home_dir()?;
Ok(home.join(".config").join("agpm").join("config.toml"))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_normalize_path() {
let path = Path::new("/foo/./bar/../baz");
let normalized = normalize_path(path);
assert_eq!(normalized, PathBuf::from("/foo/baz"));
}
#[test]
fn test_normalize_path_complex() {
assert_eq!(normalize_path(Path::new("/")), PathBuf::from("/"));
assert_eq!(normalize_path(Path::new("/foo/bar")), PathBuf::from("/foo/bar"));
assert_eq!(normalize_path(Path::new("/foo/./bar")), PathBuf::from("/foo/bar"));
assert_eq!(normalize_path(Path::new("/foo/../bar")), PathBuf::from("/bar"));
assert_eq!(normalize_path(Path::new("/foo/bar/..")), PathBuf::from("/foo"));
assert_eq!(normalize_path(Path::new("foo/./bar")), PathBuf::from("foo/bar"));
assert_eq!(normalize_path(Path::new("./foo/bar")), PathBuf::from("foo/bar"));
}
#[test]
fn test_is_safe_path() {
let base = Path::new("/home/user/project");
assert!(is_safe_path(base, Path::new("subdir/file.txt")));
assert!(is_safe_path(base, Path::new("./subdir/file.txt")));
assert!(!is_safe_path(base, Path::new("../other/file.txt")));
assert!(!is_safe_path(base, Path::new("/etc/passwd")));
}
#[test]
fn test_is_safe_path_edge_cases() {
let base = Path::new("/home/user/project");
assert!(is_safe_path(base, Path::new("")));
assert!(is_safe_path(base, Path::new(".")));
assert!(is_safe_path(base, Path::new("./nested/./path")));
assert!(!is_safe_path(base, Path::new("..")));
assert!(!is_safe_path(base, Path::new("../../etc")));
assert!(!is_safe_path(base, Path::new("/absolute/path")));
if cfg!(windows) {
assert!(!is_safe_path(base, Path::new("C:\\Windows")));
}
}
#[test]
fn test_find_project_root() {
let temp = tempdir().unwrap();
let project = temp.path().join("project");
let subdir = project.join("src").join("subdir");
crate::utils::fs::ensure_dir(&subdir).unwrap();
std::fs::write(project.join("agpm.toml"), "[sources]").unwrap();
let root = find_project_root(&subdir).unwrap();
assert_eq!(root.canonicalize().unwrap(), project.canonicalize().unwrap());
}
#[test]
fn test_find_project_root_not_found() {
let temp = tempdir().unwrap();
let result = find_project_root(temp.path());
assert!(result.is_err());
}
#[test]
fn test_find_project_root_multiple_markers() {
let temp = tempdir().unwrap();
let root = temp.path().join("project");
let subproject = root.join("subproject");
let deep = subproject.join("src");
crate::utils::fs::ensure_dir(&deep).unwrap();
std::fs::write(root.join("agpm.toml"), "[sources]").unwrap();
std::fs::write(subproject.join("agpm.toml"), "[sources]").unwrap();
let found = find_project_root(&deep).unwrap();
assert_eq!(found.canonicalize().unwrap(), subproject.canonicalize().unwrap());
}
#[test]
fn test_get_global_config_path() {
let config_path = get_global_config_path().unwrap();
assert!(config_path.to_string_lossy().contains(".config"));
assert!(config_path.to_string_lossy().contains("agpm"));
}
}