1use regex::Regex;
2use std::path::{Path, PathBuf};
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum PathError {
7 #[error("Invalid path: {0}")]
8 InvalidPath(String),
9 #[error("Path traversal attempt detected")]
10 PathTraversal,
11}
12
13pub fn normalize_path(path: &str) -> Result<String, PathError> {
15 let full_path = path.to_string();
16
17 let normalized = Path::new(&full_path)
18 .components()
19 .fold(PathBuf::new(), |mut acc, comp| {
20 match comp {
21 std::path::Component::Normal(name) => {
22 acc.push(name);
23 }
24 std::path::Component::ParentDir => {
25 acc.pop();
26 }
27 _ => {} }
29 acc
30 });
31
32 if normalized.to_string_lossy().contains("..") {
34 return Err(PathError::PathTraversal);
35 }
36
37 let normalized_str = normalized.to_string_lossy().to_string();
38 Ok(normalized_str)
39}
40
41pub fn relative_to_base(path: &str, base: &str) -> String {
43 let base = base.trim_matches('/');
44 if base.is_empty() {
45 return path.to_string();
46 }
47 let re = Regex::new(&format!("^{}/?", base)).unwrap();
48 let relative = re.replace(path, "");
49
50 if relative.is_empty() {
51 "index.html".to_string()
52 } else {
53 relative.to_string()
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn test_normalize_path() {
63 assert_eq!(normalize_path("a/b/../c").unwrap(), "/a/c");
64 assert_eq!(normalize_path("/a//b/").unwrap(), "/a/b");
65 assert_eq!(normalize_path("").unwrap(), "/base");
66 }
67
68 #[test]
69 fn test_relative_to_base() {
70 assert_eq!(relative_to_base("/app/index.html", "app"), "index.html");
71 assert_eq!(relative_to_base("/app/", "app"), "index.html");
72 assert_eq!(relative_to_base("/app/css/style.css", "app"), "css/style.css");
73 assert_eq!(relative_to_base("/other", "app"), "other");
74 }
75}