use std::path::{Component, Path};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathRejection {
Allowed,
Absolute,
Traversal,
}
#[must_use]
pub fn classify_relative_path(path: &str) -> PathRejection {
let p = Path::new(path);
if p.is_absolute() || path.starts_with('/') {
return PathRejection::Absolute;
}
if p.components().any(|c| c == Component::ParentDir) {
return PathRejection::Traversal;
}
PathRejection::Allowed
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allows_plain_relative_path() {
assert_eq!(classify_relative_path("photo.jpg"), PathRejection::Allowed);
}
#[test]
fn allows_nested_relative_path() {
assert_eq!(
classify_relative_path("sub/dir/photo.jpg"),
PathRejection::Allowed
);
}
#[test]
fn rejects_absolute_unix_path() {
assert_eq!(
classify_relative_path("/etc/passwd"),
PathRejection::Absolute
);
}
#[test]
fn rejects_leading_slash_even_when_is_absolute_would_miss_it() {
assert_eq!(
classify_relative_path("/tmp/screenshot.png"),
PathRejection::Absolute
);
}
#[test]
fn rejects_parent_dir_traversal() {
assert_eq!(
classify_relative_path("../../etc/passwd"),
PathRejection::Traversal
);
}
#[test]
fn rejects_nested_parent_dir_traversal() {
assert_eq!(
classify_relative_path("sub/../../etc/passwd"),
PathRejection::Traversal
);
}
#[test]
fn absolute_takes_priority_over_traversal() {
assert_eq!(
classify_relative_path("/etc/../etc/passwd"),
PathRejection::Absolute
);
}
#[test]
fn allows_single_dot_component() {
assert_eq!(
classify_relative_path("./photo.jpg"),
PathRejection::Allowed
);
}
}