anycms_spa/core/
path.rs

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
13/// 路径规范化工具
14pub 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                _ => {} // 忽略根目录和前缀
28            }
29            acc
30        });
31    
32    // 防止路径遍历攻击
33    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
41/// 提取相对于基路径的路径
42pub 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}