use std::path::{Component, Path, PathBuf};
fn lexical(path: &Path) -> Option<PathBuf> {
let mut out = PathBuf::new();
for c in path.components() {
match c {
Component::CurDir => {}
Component::ParentDir => {
if !out.pop() || out.as_os_str().is_empty() {
return None;
}
}
other => out.push(other.as_os_str()),
}
}
Some(out)
}
fn canonical(path: &Path) -> PathBuf {
let mut rest: Vec<std::ffi::OsString> = Vec::new();
let mut at = path.to_path_buf();
loop {
if let Ok(real) = dunce::canonicalize(&at) {
let mut out = real;
for part in rest.iter().rev() {
out.push(part);
}
return out;
}
match (at.file_name().map(|n| n.to_os_string()), at.parent()) {
(Some(name), Some(parent)) => {
rest.push(name);
at = parent.to_path_buf();
}
_ => return path.to_path_buf(),
}
}
}
fn key(path: PathBuf) -> PathBuf {
if cfg!(windows) {
PathBuf::from(path.to_string_lossy().to_lowercase())
} else {
path
}
}
pub fn resolve(path: &str) -> Option<PathBuf> {
let p = Path::new(path);
if path.is_empty() || !p.is_absolute() {
return None;
}
Some(canonical(&lexical(p)?))
}
pub fn fence_of(fence: &[String]) -> Vec<PathBuf> {
fence.iter().filter_map(|p| resolve(p)).map(key).collect()
}
pub fn within(real: &Path, fence: &[PathBuf]) -> bool {
let k = key(real.to_path_buf());
fence.iter().any(|f| k.starts_with(f))
}
#[cfg(test)]
mod tests {
use super::*;
fn root() -> &'static str {
if cfg!(windows) {
"C:\\x"
} else {
"/x"
}
}
#[test]
fn dots_are_taken_out_and_a_relative_path_does_not_hold() {
let sep = std::path::MAIN_SEPARATOR;
let r = root();
let fence = fence_of(&[format!("{r}{sep}private")]);
let climbed = resolve(&format!("{r}{sep}public{sep}..{sep}private{sep}secret")).unwrap();
assert!(within(&climbed, &fence));
assert!(!within(
&resolve(&format!("{r}{sep}privateer")).unwrap(),
&fence
));
assert!(resolve("private/secret").is_none());
assert!(resolve("").is_none());
}
}