use mini_static::resolve_with_canonical_root;
use proptest::prelude::*;
use tempfile::TempDir;
proptest! {
#[test]
fn any_resolved_path_is_under_root_canonical(
path_segments in prop::collection::vec("[a-zA-Z0-9._-]{1,20}", 0..10)
) {
let root = TempDir::new().unwrap();
let root_canon = root.path().canonicalize().unwrap();
let request_path = format!("/{}", path_segments.join("/"));
if let Ok(resolved) = resolve_with_canonical_root(&root_canon, &request_path) {
assert!(
resolved.starts_with(&root_canon),
"resolved path {:?} is not under root {:?}",
resolved,
root_canon
);
}
}
#[test]
fn traversal_attempts_always_rejected(
_segments_count in 1..20usize,
leading_traversals in 1..10usize,
) {
let root = TempDir::new().unwrap();
let root_canon = root.path().canonicalize().unwrap();
let traversals = (0..leading_traversals)
.map(|_| "..")
.collect::<Vec<_>>()
.join("/");
let request_path = format!("/{}/etc/passwd", traversals);
let result = resolve_with_canonical_root(&root_canon, &request_path);
assert!(
result.is_err(),
"traversal attempt {:?} should be rejected",
request_path
);
}
#[test]
fn null_bytes_in_path_rejected(
prefix in "[a-zA-Z0-9._-]{0,20}",
suffix in "[a-zA-Z0-9._-]{0,20}",
) {
let root = TempDir::new().unwrap();
let root_canon = root.path().canonicalize().unwrap();
let request_path = format!("/{}\0{}", prefix, suffix);
let result = resolve_with_canonical_root(&root_canon, &request_path);
assert!(
result.is_err(),
"path with null bytes should be rejected"
);
}
#[test]
fn percent_encoded_path_decoded_safely(
path_segments in prop::collection::vec("[a-zA-Z0-9]{1,20}", 0..5)
) {
let root = TempDir::new().unwrap();
let root_canon = root.path().canonicalize().unwrap();
let request_path = format!(
"/{}",
path_segments
.iter()
.map(|s| percent_encoding::percent_encode(s.as_bytes(), percent_encoding::NON_ALPHANUMERIC).to_string())
.collect::<Vec<_>>()
.join("/")
);
if let Ok(resolved) = resolve_with_canonical_root(&root_canon, &request_path) {
assert!(
resolved.starts_with(&root_canon),
"percent-decoded path {:?} escaped root {:?}",
resolved,
root_canon
);
}
}
#[test]
fn deep_nesting_bounded_safely(
depth in 0..100usize,
) {
let root = TempDir::new().unwrap();
let root_canon = root.path().canonicalize().unwrap();
let nested = (0..depth)
.map(|i| format!("d{}", i))
.collect::<Vec<_>>()
.join("/");
let request_path = format!("/{}/file.txt", nested);
if let Ok(resolved) = resolve_with_canonical_root(&root_canon, &request_path) {
assert!(
resolved.starts_with(&root_canon),
"deeply nested path escaped root"
);
}
}
#[test]
fn mixed_slash_directions_rejected_or_contained(
forward_segments in prop::collection::vec("[a-zA-Z0-9]{1,10}", 0..3),
mixed_slashes in "[/\\\\]{1,5}",
) {
let root = TempDir::new().unwrap();
let root_canon = root.path().canonicalize().unwrap();
let request_path = if forward_segments.is_empty() {
format!("/{}", mixed_slashes)
} else {
format!("/{}{}", forward_segments[0], mixed_slashes)
};
if let Ok(resolved) = resolve_with_canonical_root(&root_canon, &request_path) {
assert!(
resolved.starts_with(&root_canon),
"path with mixed slashes escaped root"
);
}
}
}