use crate::Result;
use percent_encoding::percent_decode_str;
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
fn decode_url_encoded(s: &str) -> String {
let first_pass = percent_decode_str(s).decode_utf8_lossy().to_string();
percent_decode_str(&first_pass)
.decode_utf8_lossy()
.to_string()
}
fn contains_traversal_pattern(s: &str) -> bool {
if s.contains("..") {
return true;
}
if s.contains("..") || s.contains("。。") {
return true;
}
if s.contains("..\\") || s.contains("\\..") {
return true;
}
false
}
pub fn validate_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = path.as_ref();
debug!("Validating path: {:?}", path);
let path_str = path.to_string_lossy();
if path_str.contains('\0') || path_str.contains("%00") {
return Err(crate::Error::security(format!(
"Null byte in path detected: {:?}",
path
)));
}
let decoded = decode_url_encoded(&path_str);
if contains_traversal_pattern(&path_str) || contains_traversal_pattern(&decoded) {
return Err(crate::Error::security(format!(
"Path traversal pattern detected: {:?}",
path
)));
}
let canonical_path = match path.canonicalize() {
Ok(p) => p,
Err(e) => {
warn!("Failed to canonicalize path {:?}: {}", path, e);
return Err(crate::Error::security(format!(
"Invalid path or access denied: {:?}",
path
)));
}
};
let depth = canonical_path.components().count();
if depth > 20 {
return Err(crate::Error::security(format!(
"Path depth too deep ({}): {:?}",
depth, canonical_path
)));
}
debug!("Path validation successful: {:?}", canonical_path);
Ok(canonical_path)
}
pub fn validate_path_within<P: AsRef<Path>, B: AsRef<Path>>(path: P, base: B) -> Result<PathBuf> {
let validated_path = validate_path(path)?;
let base_path = base
.as_ref()
.canonicalize()
.map_err(|e| crate::Error::security(format!("Invalid base path: {}", e)))?;
if !validated_path.starts_with(&base_path) {
return Err(crate::Error::security(format!(
"Path outside allowed directory: {:?} not within {:?}",
validated_path, base_path
)));
}
Ok(validated_path)
}
pub fn validate_file_extension<P: AsRef<Path>>(path: P, allowed_extensions: &[&str]) -> Result<()> {
let path = path.as_ref();
match path.extension().and_then(|ext| ext.to_str()) {
Some(ext) => {
if allowed_extensions.contains(&ext) {
Ok(())
} else {
Err(crate::Error::security(format!(
"File extension '{}' not allowed",
ext
)))
}
}
None => {
if allowed_extensions.is_empty() {
Ok(()) } else {
Err(crate::Error::security(
"File must have an extension".to_string(),
))
}
}
}
}