use std::path::{Path, PathBuf};
use std::io;
#[derive(Debug, Clone)]
pub struct PathSanitizer {
root: PathBuf,
}
impl PathSanitizer {
pub fn new(root: PathBuf) -> Self {
let canonical_root = root.canonicalize().unwrap_or(root);
Self {
root: canonical_root
}
}
pub fn sanitize(&self, path_str: &str) -> io::Result<PathBuf> {
let path = Path::new(path_str);
let candidate = if path.is_absolute() {
path.to_path_buf()
} else {
self.root.join(path)
};
let canonical = candidate.canonicalize()?;
if canonical.starts_with(&self.root) {
Ok(canonical)
} else {
Err(io::Error::new(io::ErrorKind::PermissionDenied, format!("Path traversal detected: {:?}", canonical)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_sandbox_allows_valid_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "content").unwrap();
let sanitizer = PathSanitizer::new(temp_dir.path().to_path_buf());
let result = sanitizer.sanitize("test.txt");
assert!(result.is_ok());
assert_eq!(result.unwrap(), file_path.canonicalize().unwrap());
}
#[test]
fn test_sandbox_denies_traversal() {
let temp_dir = TempDir::new().unwrap();
let sanitizer = PathSanitizer::new(temp_dir.path().to_path_buf());
let _result = sanitizer.sanitize("../outside.txt");
let outside_dir = TempDir::new().unwrap();
let outside_file = outside_dir.path().join("outside.txt");
fs::write(&outside_file, "secret").unwrap();
let result = sanitizer.sanitize(outside_file.to_str().unwrap());
assert!(result.is_err());
}
}