Skip to main content

packc/
path_safety.rs

1use std::path::{Component, Path, PathBuf};
2
3use anyhow::{Context, Result};
4
5/// Normalise a user-supplied path so it stays under `root`.
6/// Rejects absolute inputs and any traversal that would escape `root`.
7pub 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                // Prevent walking above the canonical root depth.
31                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}