use std::path::{Path, PathBuf};
use crate::error::{AgitError, Result};
pub fn validate_path_is_internal(repo_root: &Path, target_path: &str) -> Result<PathBuf> {
let canonical_root = repo_root.canonicalize().map_err(|e| {
AgitError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Cannot canonicalize repo root: {}", e),
))
})?;
let resolved = if Path::new(target_path).is_absolute() {
PathBuf::from(target_path)
} else {
repo_root.join(target_path)
};
if !resolved.exists() {
return Err(AgitError::FileNotFound {
path: target_path.to_string(),
repo_root: canonical_root.display().to_string(),
});
}
let canonical_target = resolved.canonicalize().map_err(|e| {
AgitError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Cannot canonicalize target path '{}': {}", target_path, e),
))
})?;
if !canonical_target.starts_with(&canonical_root) {
return Err(AgitError::PathOutsideRepository {
path: target_path.to_string(),
repo_root: canonical_root.display().to_string(),
});
}
Ok(canonical_target)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_valid_relative_path() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("file.rs"), "").unwrap();
let result = validate_path_is_internal(temp.path(), "file.rs");
assert!(result.is_ok());
}
#[test]
fn test_valid_nested_path() {
let temp = TempDir::new().unwrap();
let src_dir = temp.path().join("src");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::write(src_dir.join("main.rs"), "").unwrap();
let result = validate_path_is_internal(temp.path(), "src/main.rs");
assert!(result.is_ok());
}
#[test]
fn test_path_traversal_blocked() {
let temp = TempDir::new().unwrap();
let result = validate_path_is_internal(temp.path(), "../outside.rs");
assert!(result.is_err());
match result {
Err(AgitError::PathOutsideRepository { path, .. }) => {
assert_eq!(path, "../outside.rs");
},
Err(AgitError::FileNotFound { path, .. }) => {
assert_eq!(path, "../outside.rs");
},
_ => panic!("Expected PathOutsideRepository or FileNotFound error"),
}
}
#[test]
fn test_absolute_path_inside_allowed() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("file.rs"), "").unwrap();
let abs_path = temp.path().join("file.rs");
let result = validate_path_is_internal(temp.path(), abs_path.to_str().unwrap());
assert!(result.is_ok());
}
#[test]
fn test_absolute_path_outside_blocked() {
let temp = TempDir::new().unwrap();
#[cfg(unix)]
let outside_path = "/tmp";
#[cfg(windows)]
let outside_path = "C:\\Windows";
let result = validate_path_is_internal(temp.path(), outside_path);
assert!(result.is_err());
}
#[test]
fn test_nonexistent_file_rejected() {
let temp = TempDir::new().unwrap();
let src_dir = temp.path().join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let result = validate_path_is_internal(temp.path(), "src/new_file.rs");
assert!(result.is_err());
if let Err(AgitError::FileNotFound { path, .. }) = result {
assert_eq!(path, "src/new_file.rs");
} else {
panic!("Expected FileNotFound error");
}
}
#[test]
fn test_deep_path_traversal_blocked() {
let temp = TempDir::new().unwrap();
let src_dir = temp.path().join("src").join("deep");
std::fs::create_dir_all(&src_dir).unwrap();
let result = validate_path_is_internal(temp.path(), "src/deep/../../../outside.rs");
assert!(result.is_err());
}
}