use std::path::{Path, PathBuf};
use tracing::{debug, warn};
use crate::module::traits::ModuleError;
fn absolutize_path(path: &Path) -> PathBuf {
if path.as_os_str().is_empty() {
return PathBuf::from(".");
}
if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(path)
}
}
pub struct FileSystemSandbox {
allowed_path: PathBuf,
}
impl FileSystemSandbox {
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
let abs = absolutize_path(data_dir.as_ref());
let allowed_path = std::fs::canonicalize(&abs).unwrap_or(abs);
Self { allowed_path }
}
pub fn validate_path<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf, ModuleError> {
let path = path.as_ref();
if path.is_absolute() && path.starts_with(&self.allowed_path) {
}
let canonical = match path.canonicalize() {
Ok(canonical) => canonical,
Err(_) => {
let parent = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or(path);
let parent_canonical = parent.canonicalize().map_err(|e| {
ModuleError::OperationError(format!(
"Failed to canonicalize path {path:?}: {e}"
))
})?;
let name = path.file_name().ok_or_else(|| {
ModuleError::OperationError(format!(
"Access denied: path {path:?} has no file name"
))
})?;
if name == ".." || name == "." {
return Err(ModuleError::OperationError(format!(
"Access denied: path {path:?} is outside allowed directory {:?}",
self.allowed_path
)));
}
parent_canonical.join(name)
}
};
if !canonical.starts_with(&self.allowed_path) {
warn!(
"Module attempted to access path outside sandbox: {:?} (allowed: {:?})",
canonical, self.allowed_path
);
return Err(ModuleError::OperationError(format!(
"Access denied: path {:?} is outside allowed directory {:?}",
canonical, self.allowed_path
)));
}
debug!("Path validated: {:?} is within sandbox", canonical);
Ok(canonical)
}
pub fn allowed_path(&self) -> &Path {
&self.allowed_path
}
#[inline]
pub fn is_within_sandbox<P: AsRef<Path>>(&self, path: P) -> bool {
let path = path.as_ref();
if !path.starts_with(&self.allowed_path) {
return false;
}
if let Ok(canonical) = path.canonicalize() {
canonical.starts_with(&self.allowed_path)
} else {
true
}
}
}