use std::path::{Component, Path, PathBuf};
#[must_use]
pub fn normalize_capability_path(path: &str) -> Option<PathBuf> {
let mut out = PathBuf::new();
let mut has_root = false;
let mut depth: usize = 0;
for comp in Path::new(path).components() {
match comp {
Component::RootDir => {
has_root = true;
out.push(comp.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
if depth == 0 {
return None;
}
depth -= 1;
out.pop();
}
Component::Normal(c) => {
depth += 1;
out.push(c);
}
Component::Prefix(_) => return None,
}
}
if !has_root {
return None;
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_relative_paths() {
assert_eq!(normalize_capability_path("data/x"), None);
assert_eq!(normalize_capability_path("./data/x"), None);
assert_eq!(normalize_capability_path(""), None);
assert_eq!(normalize_capability_path("../etc/passwd"), None);
}
#[test]
fn rejects_escape_above_root() {
assert_eq!(normalize_capability_path("/data/../../etc/passwd"), None);
assert_eq!(normalize_capability_path("/.."), None);
assert_eq!(normalize_capability_path("/data/../.."), None);
}
#[test]
fn resolves_dot_and_redundant_separators() {
assert_eq!(
normalize_capability_path("/data/./sub/f"),
Some(PathBuf::from("/data/sub/f"))
);
assert_eq!(
normalize_capability_path("/data//sub///f"),
Some(PathBuf::from("/data/sub/f"))
);
assert_eq!(
normalize_capability_path("/data/sub/f"),
Some(PathBuf::from("/data/sub/f"))
);
}
#[test]
fn resolves_in_root_parent_without_rejecting() {
assert_eq!(
normalize_capability_path("/data/../etc"),
Some(PathBuf::from("/etc"))
);
assert_eq!(
normalize_capability_path("/data/.."),
Some(PathBuf::from("/"))
);
}
}