use regex::Regex;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PathError {
#[error("Invalid path: {0}")]
InvalidPath(String),
#[error("Path traversal attempt detected")]
PathTraversal,
}
pub fn normalize_path(path: &str) -> Result<String, PathError> {
let full_path = path.to_string();
let normalized = Path::new(&full_path)
.components()
.fold(PathBuf::new(), |mut acc, comp| {
match comp {
std::path::Component::Normal(name) => {
acc.push(name);
}
std::path::Component::ParentDir => {
acc.pop();
}
_ => {} }
acc
});
if normalized.to_string_lossy().contains("..") {
return Err(PathError::PathTraversal);
}
let normalized_str = normalized.to_string_lossy().to_string();
Ok(normalized_str)
}
pub fn relative_to_base(path: &str, base: &str) -> String {
let base = base.trim_matches('/');
if base.is_empty() {
return path.to_string();
}
let re = Regex::new(&format!("^{}/?", base)).unwrap();
let relative = re.replace(path, "");
if relative.is_empty() {
"index.html".to_string()
} else {
relative.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path() {
assert_eq!(normalize_path("a/b/../c").unwrap(), "/a/c");
assert_eq!(normalize_path("/a//b/").unwrap(), "/a/b");
assert_eq!(normalize_path("").unwrap(), "/base");
}
#[test]
fn test_relative_to_base() {
assert_eq!(relative_to_base("/app/index.html", "app"), "index.html");
assert_eq!(relative_to_base("/app/", "app"), "index.html");
assert_eq!(relative_to_base("/app/css/style.css", "app"), "css/style.css");
assert_eq!(relative_to_base("/other", "app"), "other");
}
}