use std::io;
use std::path::{Path, PathBuf};
#[must_use]
pub fn is_path_within(canonical: &Path, allowed_paths: &[PathBuf]) -> bool {
allowed_paths.iter().any(|a| canonical.starts_with(a))
}
pub fn validate_path_within(path: &Path, allowed_paths: &[PathBuf]) -> io::Result<PathBuf> {
let canonical = path.canonicalize()?;
if !is_path_within(&canonical, allowed_paths) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"path '{}' is outside the allowed sandbox",
canonical.display()
),
));
}
Ok(canonical)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_path_within_true_for_exact_match() {
let dir = tempfile::tempdir().unwrap();
let allowed = vec![dir.path().to_path_buf()];
assert!(is_path_within(dir.path(), &allowed));
}
#[test]
fn is_path_within_true_for_nested_path() {
let dir = tempfile::tempdir().unwrap();
let allowed = vec![dir.path().to_path_buf()];
let nested = dir.path().join("a").join("b");
assert!(is_path_within(&nested, &allowed));
}
#[test]
fn is_path_within_false_for_sibling_outside_root() {
let dir = tempfile::tempdir().unwrap();
let sibling = tempfile::tempdir().unwrap();
let allowed = vec![dir.path().to_path_buf()];
assert!(!is_path_within(sibling.path(), &allowed));
}
#[test]
fn is_path_within_true_when_any_of_multiple_roots_matches() {
let dir_a = tempfile::tempdir().unwrap();
let dir_b = tempfile::tempdir().unwrap();
let allowed = vec![dir_a.path().to_path_buf(), dir_b.path().to_path_buf()];
assert!(is_path_within(dir_b.path(), &allowed));
}
#[test]
fn validate_path_within_ok_for_existing_path_inside_root() {
let dir = tempfile::tempdir().unwrap();
let allowed = vec![dir.path().canonicalize().unwrap()];
let result = validate_path_within(dir.path(), &allowed);
assert!(result.is_ok());
}
#[test]
fn validate_path_within_rejects_path_outside_root() {
let dir = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let allowed = vec![dir.path().canonicalize().unwrap()];
let result = validate_path_within(outside.path(), &allowed);
let err = result.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
}
#[test]
fn validate_path_within_errors_on_nonexistent_path() {
let dir = tempfile::tempdir().unwrap();
let allowed = vec![dir.path().canonicalize().unwrap()];
let missing = dir.path().join("does-not-exist");
let result = validate_path_within(&missing, &allowed);
let err = result.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
}