use crate::utils::error::ContextCreatorError;
use std::path::{Path, PathBuf};
pub fn validate_import_path(
base_dir: &Path,
import_path: &Path,
) -> Result<PathBuf, ContextCreatorError> {
if !base_dir.is_absolute() {
return Err(ContextCreatorError::SecurityError(
"Base directory must be absolute".to_string(),
));
}
let path_str = import_path.to_string_lossy();
let decoded = decode_url_path(&path_str)?;
if decoded != path_str {
return Err(ContextCreatorError::SecurityError(format!(
"URL-encoded paths are not allowed: {path_str}"
)));
}
let normalized = PathBuf::from(decoded.replace('\\', "/"));
let full_path = if normalized.is_absolute() {
normalized
} else {
base_dir.join(normalized)
};
let canonical_path = full_path.canonicalize().map_err(|e| {
ContextCreatorError::InvalidPath(format!(
"Path does not exist or cannot be resolved: {} ({})",
full_path.display(),
e
))
})?;
let canonical_base = base_dir.canonicalize().map_err(|e| {
ContextCreatorError::SecurityError(format!("Cannot canonicalize base directory: {e}"))
})?;
if !canonical_path.starts_with(&canonical_base) {
return Err(ContextCreatorError::SecurityError(format!(
"Path escapes project directory: {}",
import_path.display()
)));
}
Ok(canonical_path)
}
pub fn validate_module_name(module_name: &str) -> Result<(), ContextCreatorError> {
if module_name.is_empty() {
return Err(ContextCreatorError::SecurityError(
"Module name cannot be empty".to_string(),
));
}
if module_name.len() > 255 {
return Err(ContextCreatorError::SecurityError(
"Module name too long".to_string(),
));
}
if module_name.contains('\0') {
return Err(ContextCreatorError::SecurityError(
"Module name contains null byte".to_string(),
));
}
if module_name.contains("..") {
return Err(ContextCreatorError::SecurityError(format!(
"Invalid module name: {module_name}"
)));
}
let valid_chars = module_name.chars().all(|c| {
c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '@' || c == '/' || c == ':'
});
if !valid_chars {
return Err(ContextCreatorError::SecurityError(format!(
"Module name contains invalid characters: {module_name}"
)));
}
Ok(())
}
fn decode_url_path(path: &str) -> Result<String, ContextCreatorError> {
if !path.contains('%') {
return Ok(path.to_string());
}
let lower = path.to_lowercase();
if lower.contains("%2e") || lower.contains("%2f") || lower.contains("%5c") || lower.contains("%00") || lower.contains("%25") || lower.contains("%c0") || lower.contains("%e0") || lower.contains("%f0") || lower.contains("%u00")
{
return Err(ContextCreatorError::SecurityError(
"URL-encoded characters detected in path".to_string(),
));
}
Ok(path.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_valid_paths() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
fs::create_dir_all(base.join("src")).unwrap();
fs::write(base.join("src/lib.rs"), "").unwrap();
let result = validate_import_path(base, &PathBuf::from("src/lib.rs"));
assert!(result.is_ok());
}
#[test]
fn test_path_traversal_blocked() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
let target = base.join("target.txt");
fs::write(&target, "target").unwrap();
let escape_path = base.join("src/../../../etc/passwd");
let result = validate_import_path(base, &escape_path);
assert!(result.is_err());
}
#[test]
fn test_url_encoding_blocked() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
let encoded_paths = vec![
"src/%2e%2e/secret",
"src%2f%2e%2e%2fsecret",
"%2e%2e%2f%2e%2e%2fetc%2fpasswd",
];
for path in encoded_paths {
let result = validate_import_path(base, &PathBuf::from(path));
assert!(result.is_err(), "Should block: {path}");
}
}
#[test]
fn test_symlink_blocked() {
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
let link_path = base.join("evil_link");
symlink("/etc/passwd", &link_path).unwrap();
let result = validate_import_path(base, &link_path);
assert!(result.is_err());
}
#[cfg(not(unix))]
{
}
}
#[test]
fn test_nonexistent_file_fails() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
let result = validate_import_path(base, &PathBuf::from("does/not/exist.rs"));
assert!(result.is_err());
}
#[test]
fn test_module_name_validation() {
assert!(validate_module_name("lodash").is_ok());
assert!(validate_module_name("@angular/core").is_ok());
assert!(validate_module_name("@types/node").is_ok());
assert!(validate_module_name("").is_err());
assert!(validate_module_name("../../../etc/passwd").is_err());
assert!(validate_module_name("name\0with\0null").is_err());
assert!(validate_module_name(&"a".repeat(256)).is_err());
assert!(validate_module_name("rm -rf /").is_err());
}
}