use std::path::{Component, Path};
#[must_use]
pub fn path_stays_within_base(candidate: &Path, base_dir: &Path) -> bool {
let suffix = candidate.strip_prefix(base_dir).unwrap_or(candidate);
let mut depth: i32 = 0;
for comp in suffix.components() {
match comp {
Component::ParentDir => {
depth -= 1;
if depth < 0 {
return false;
}
}
Component::Normal(_) => depth += 1,
Component::CurDir => {}
Component::RootDir | Component::Prefix(_) => return false,
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn accepts_simple_relative_descendant() {
let base = Path::new("/pkg");
assert!(path_stays_within_base(
&base.join("scripts/install.sh"),
base
));
}
#[test]
fn accepts_curdir_components() {
let base = Path::new("/pkg");
assert!(path_stays_within_base(
&base.join("./helpers/./util.py"),
base
));
}
#[test]
fn rejects_parent_traversal_escape() {
let base = Path::new("/pkg");
let candidate = PathBuf::from("/pkg/../../../etc/evil.sh");
assert!(!path_stays_within_base(&candidate, base));
}
#[test]
fn allows_internal_parent_traversal_within_base() {
let base = Path::new("/pkg");
let candidate = PathBuf::from("/pkg/sub/../scripts/ok.sh");
assert!(path_stays_within_base(&candidate, base));
}
#[test]
fn rejects_absolute_root_inside_suffix() {
let base = Path::new("/pkg");
let mut candidate = PathBuf::from("/pkg");
candidate.push("/etc/evil.sh"); assert!(!path_stays_within_base(&candidate, base));
}
}