use std::path::{Component, Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error, PartialEq)]
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 mut buf = PathBuf::new();
for comp in Path::new(path).components() {
match comp {
Component::Normal(name) => {
buf.push(name);
}
Component::ParentDir if !buf.pop() => {
return Err(PathError::PathTraversal);
}
Component::ParentDir => {}
_ => {}
}
}
Ok(buf.to_string_lossy().to_string())
}
pub fn collapse_slashes(path: &str) -> String {
let mut result = String::with_capacity(path.len());
let mut prev_slash = false;
for ch in path.chars() {
if ch == '/' {
if !prev_slash {
result.push(ch);
}
prev_slash = true;
} else {
result.push(ch);
prev_slash = false;
}
}
result
}
pub fn relative_to_base(path: &str, base: &str) -> String {
let base = base.trim_matches('/');
if base.is_empty() {
return path.trim_start_matches('/').to_string();
}
let base_with_slash = format!("{}/", base);
let path_no_lead = path.trim_start_matches('/');
if let Some(stripped) = path_no_lead.strip_prefix(&base_with_slash) {
if stripped.is_empty() {
"index.html".to_string()
} else {
stripped.to_string()
}
} else if path_no_lead == base {
"index.html".to_string()
} else {
path_no_lead.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(), "");
assert_eq!(normalize_path("css/style.css").unwrap(), "css/style.css");
assert_eq!(normalize_path("a/../../etc/passwd"), Err(PathError::PathTraversal));
}
#[test]
fn test_collapse_slashes() {
assert_eq!(collapse_slashes("/a//b///c/"), "/a/b/c/");
assert_eq!(collapse_slashes("/"), "/");
assert_eq!(collapse_slashes("///"), "/");
assert_eq!(collapse_slashes("css//style.css"), "css/style.css");
}
#[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");
assert_eq!(relative_to_base("/css/style.css", "/"), "css/style.css");
}
}