use std::path::{Component, Path, PathBuf};
pub fn normalize(path: impl AsRef<Path>) -> PathBuf {
let mut out: Vec<Component> = Vec::new();
for component in path.as_ref().components() {
match component {
Component::CurDir => {}
Component::ParentDir => match out.last() {
Some(Component::Normal(_)) => {
out.pop();
}
_ => out.push(component),
},
other => out.push(other),
}
}
out.iter().collect()
}
pub fn escapes_root(path: impl AsRef<Path>) -> bool {
matches!(
normalize(path).components().next(),
Some(Component::ParentDir | Component::RootDir | Component::Prefix(_))
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn folds_dot_and_parent_components() {
assert_eq!(normalize("a/./b"), PathBuf::from("a/b"));
assert_eq!(normalize("a/b/../c"), PathBuf::from("a/c"));
assert_eq!(normalize("a/../../b"), PathBuf::from("../b"));
}
#[test]
fn escapes_only_when_the_climb_survives_folding() {
assert!(!escapes_root("notes/a.md"));
assert!(!escapes_root("notes/../a.md"));
assert!(escapes_root("../a.md"));
assert!(escapes_root("a/../../etc/passwd"));
assert!(escapes_root("/etc/passwd"));
}
}