greentic_component/
path_safety.rs1use std::io::ErrorKind;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5
6pub fn normalize_under_root(root: &Path, candidate: &Path) -> Result<PathBuf> {
9 if candidate.is_absolute() {
10 anyhow::bail!("absolute paths are not allowed: {}", candidate.display());
11 }
12
13 let root = root
14 .canonicalize()
15 .with_context(|| format!("failed to canonicalize root {}", root.display()))?;
16
17 let joined = root.join(candidate);
20 let canon =
21 match joined.canonicalize() {
22 Ok(path) => path,
23 Err(err) if err.kind() == ErrorKind::NotFound => {
24 let mut missing: Vec<PathBuf> = Vec::new();
25 let mut ancestor = joined.as_path();
26 loop {
27 if ancestor.try_exists().unwrap_or(false) {
28 break;
29 }
30 let parent = ancestor
31 .parent()
32 .with_context(|| format!("{} has no parent", ancestor.display()))?;
33 missing.push(ancestor.file_name().map(PathBuf::from).with_context(|| {
34 format!("{} missing final component", ancestor.display())
35 })?);
36 ancestor = parent;
37 }
38
39 let mut rebuilt = ancestor
40 .canonicalize()
41 .with_context(|| format!("failed to canonicalize {}", ancestor.display()))?;
42 while let Some(component) = missing.pop() {
43 rebuilt.push(component);
44 }
45 rebuilt
46 }
47 Err(err) => {
48 return Err(err)
49 .with_context(|| format!("failed to canonicalize {}", joined.display()));
50 }
51 };
52
53 if !canon.starts_with(&root) {
55 anyhow::bail!(
56 "path escapes root ({}): {}",
57 root.display(),
58 canon.display()
59 );
60 }
61
62 Ok(canon)
63}