1use std::path::{Component, Path, PathBuf};
2
3use anyhow::{Context, Result};
4
5pub fn normalize_under_root(root: &Path, candidate: &Path) -> Result<PathBuf> {
8 if candidate.is_absolute() {
9 anyhow::bail!("absolute paths are not allowed: {}", candidate.display());
10 }
11
12 let canon_root = root
13 .canonicalize()
14 .with_context(|| format!("failed to canonicalize root {}", root.display()))?;
15 let mut normalized = canon_root.clone();
16 let root_depth = canon_root.components().count();
17
18 for comp in candidate.components() {
19 match comp {
20 Component::CurDir => {}
21 Component::Normal(part) => normalized.push(part),
22 Component::ParentDir => {
23 if root_depth == 0 || !normalized.pop() {
24 anyhow::bail!(
25 "path escapes root ({}): {}",
26 canon_root.display(),
27 candidate.display()
28 );
29 }
30 if normalized.components().count() < root_depth {
32 anyhow::bail!(
33 "path escapes root ({}): {}",
34 canon_root.display(),
35 candidate.display()
36 );
37 }
38 }
39 Component::Prefix(_) | Component::RootDir => {
40 anyhow::bail!("invalid path component in {}", candidate.display());
41 }
42 }
43 }
44
45 if !normalized.starts_with(&canon_root) {
46 anyhow::bail!(
47 "path escapes root ({}): {}",
48 canon_root.display(),
49 candidate.display()
50 );
51 }
52
53 Ok(normalized)
54}