use crate::SandboxError;
pub fn validate_relative_path(path: &str) -> Result<(), SandboxError> {
if path.is_empty() {
return Err(SandboxError::PathTraversal { path: path.to_string() });
}
if path.starts_with('/') || path.starts_with('\\') {
return Err(SandboxError::PathTraversal { path: path.to_string() });
}
if path.len() >= 2 {
let bytes = path.as_bytes();
if bytes[0].is_ascii_alphabetic() && (bytes[1] == b':') {
return Err(SandboxError::PathTraversal { path: path.to_string() });
}
}
let mut depth: i32 = 0;
for component in path.split(['/', '\\']) {
match component {
"" => continue,
"." => continue,
".." => {
depth -= 1;
if depth < 0 {
return Err(SandboxError::PathTraversal { path: path.to_string() });
}
}
_ => {
depth += 1;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_simple_file() {
assert!(validate_relative_path("file.txt").is_ok());
}
#[test]
fn test_valid_nested_path() {
assert!(validate_relative_path("src/main.rs").is_ok());
}
#[test]
fn test_valid_dotdot_within_bounds() {
assert!(validate_relative_path("a/b/../c/file.txt").is_ok());
}
#[test]
fn test_valid_dot_component() {
assert!(validate_relative_path("./file.txt").is_ok());
}
#[test]
fn test_valid_multiple_dots() {
assert!(validate_relative_path("./src/./main.rs").is_ok());
}
#[test]
fn test_valid_deep_path() {
assert!(validate_relative_path("a/b/c/d/e/f.txt").is_ok());
}
#[test]
fn test_invalid_empty_path() {
let result = validate_relative_path("");
assert!(result.is_err());
match result.unwrap_err() {
SandboxError::PathTraversal { path } => assert_eq!(path, ""),
other => panic!("expected PathTraversal, got: {other:?}"),
}
}
#[test]
fn test_invalid_absolute_unix() {
let result = validate_relative_path("/etc/passwd");
assert!(result.is_err());
match result.unwrap_err() {
SandboxError::PathTraversal { path } => assert_eq!(path, "/etc/passwd"),
other => panic!("expected PathTraversal, got: {other:?}"),
}
}
#[test]
fn test_invalid_parent_escape() {
assert!(validate_relative_path("../escape").is_err());
}
#[test]
fn test_invalid_deep_parent_escape() {
assert!(validate_relative_path("a/../../escape").is_err());
}
#[test]
fn test_invalid_triple_parent_escape() {
assert!(validate_relative_path("a/b/../../../escape").is_err());
}
#[test]
fn test_invalid_backslash_absolute() {
assert!(validate_relative_path("\\Windows\\System32").is_err());
}
#[test]
fn test_invalid_windows_drive() {
assert!(validate_relative_path("C:\\Users\\file.txt").is_err());
}
#[test]
fn test_valid_dotdot_exact_boundary() {
assert!(validate_relative_path("a/b/..").is_ok());
}
#[test]
fn test_valid_multiple_dotdot_within_bounds() {
assert!(validate_relative_path("a/b/c/../../d").is_ok());
}
#[test]
fn test_invalid_just_dotdot() {
assert!(validate_relative_path("..").is_err());
}
}