use std::path::{Path, PathBuf};
/// Normalize a path for cross-platform compatibility
pub fn normalize(path: &str) -> PathBuf {
let path = path.replace('\\', "/");
let path = if path.starts_with("//") && !path.starts_with("//?/") {
// UNC path on Windows
path
} else {
// Remove duplicate slashes
let mut result = String::with_capacity(path.len());
let mut prev_slash = false;
for c in path.chars() {
if c == '/' {
if !prev_slash {
result.push('/');
}
prev_slash = true;
} else {
result.push(c);
prev_slash = false;
}
}
result
};
PathBuf::from(path)
}
/// Join two paths, handling cross-platform separators
pub fn join(base: &Path, relative: &str) -> PathBuf {
let relative = relative.replace('\\', "/");
let relative = relative.trim_start_matches('/');
let mut result = base.to_path_buf();
for component in relative.split('/') {
if component == ".." {
result.pop();
} else if component != "." && !component.is_empty() {
result.push(component);
}
}
result
}
/// Check if a path is safe (no directory traversal)
pub fn is_safe_path(path: &str) -> bool {
let components: Vec<&str> = path.split('/').collect();
let mut depth: i32 = 0;
for component in &components {
match *component {
".." => {
depth -= 1;
// Reject any `..` that climbs back to (or above) the starting
// level — such a path can resolve to the document root itself
// or escape it, which we treat as traversal.
if depth <= 0 {
return false;
}
}
"." | "" => {}
_ => {
depth += 1;
}
}
}
true
}
/// Convert a URI path to a filesystem path
pub fn uri_to_path(root: &Path, uri: &str) -> PathBuf {
let uri = uri.split('?').next().unwrap_or(uri);
let uri = percent_encoding::percent_decode_str(uri)
.decode_utf8_lossy()
.to_string();
join(root, &uri)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize() {
assert_eq!(normalize("/foo//bar///baz"), PathBuf::from("/foo/bar/baz"));
assert_eq!(normalize("C:\\Users\\test"), PathBuf::from("C:/Users/test"));
}
#[test]
fn test_is_safe_path() {
assert!(is_safe_path("/foo/bar"));
assert!(is_safe_path("/foo/bar/baz.txt"));
assert!(!is_safe_path("/foo/../etc/passwd"));
assert!(!is_safe_path("/../../../etc/passwd"));
assert!(is_safe_path("/foo/bar/../baz"));
}
#[test]
fn test_join() {
let base = Path::new("/var/www");
assert_eq!(
join(base, "html/index.html"),
PathBuf::from("/var/www/html/index.html")
);
assert_eq!(
join(base, "/html/index.html"),
PathBuf::from("/var/www/html/index.html")
);
assert_eq!(
join(base, "html/../test.txt"),
PathBuf::from("/var/www/test.txt")
);
}
}