Ferrit Explore
中文·繁體·EN·日本語 Sign in Register
cielxl / veld / src / util / path.rs
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")
        );
    }
}