use std::path::{Component, Path, PathBuf};
use std::str::FromStr;
use crate::utils::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ValidatedPath {
inner: PathBuf,
}
impl ValidatedPath {
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
Self::check_no_null_bytes(path)?;
Self::check_no_parent_refs(path)?;
Self::check_not_absolute(path)?;
Self::check_shell_safety(path)?;
let canonical = if path.exists() {
path.canonicalize()
.map_err(|e| Error::invalid_input(format!("Failed to canonicalize path: {}", e)))?
} else {
path.to_path_buf()
};
Ok(Self { inner: canonical })
}
pub fn new_within(path: impl AsRef<Path>, base: impl AsRef<Path>) -> Result<Self> {
let validated = Self::new(path)?;
let full_path = base.as_ref().join(&validated.inner);
let base_canonical = base.as_ref().canonicalize().map_err(|e| {
Error::invalid_input(format!("Failed to canonicalize base directory: {}", e))
})?;
let full_canonical = if full_path.exists() {
full_path.canonicalize().map_err(|e| {
Error::invalid_input(format!("Failed to canonicalize full path: {}", e))
})?
} else {
full_path
};
if !full_canonical.starts_with(&base_canonical) {
return Err(Error::invalid_input(
"Path escapes base directory (path traversal detected)",
));
}
Ok(validated)
}
pub fn as_path(&self) -> &Path {
&self.inner
}
pub fn into_path_buf(self) -> PathBuf {
self.inner
}
fn check_no_null_bytes(path: &Path) -> Result<()> {
let path_str = path.to_string_lossy();
if path_str.contains('\0') {
return Err(Error::invalid_input("Path contains null byte"));
}
Ok(())
}
fn check_no_parent_refs(path: &Path) -> Result<()> {
for component in path.components() {
if component == Component::ParentDir {
return Err(Error::invalid_input(
"Path contains '..' (parent directory reference)",
));
}
}
Ok(())
}
fn check_not_absolute(path: &Path) -> Result<()> {
if path.is_absolute() {
return Err(Error::invalid_input("Path is absolute (expected relative)"));
}
Ok(())
}
fn check_shell_safety(path: &Path) -> Result<()> {
let path_str = path.to_string_lossy();
const DANGEROUS: &[char] = &['$', '`', ';', '|', '&', '>', '<', '\n', '\r'];
for ch in DANGEROUS {
if path_str.contains(*ch) {
return Err(Error::invalid_input(format!(
"Path contains dangerous shell metacharacter: '{}'",
ch
)));
}
}
Ok(())
}
}
impl AsRef<Path> for ValidatedPath {
fn as_ref(&self) -> &Path {
&self.inner
}
}
impl FromStr for ValidatedPath {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::new(s)
}
}
impl std::fmt::Display for ValidatedPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner.display())
}
}