runner_core/
path_safety.rs1use std::path::{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 root = root
13 .canonicalize()
14 .with_context(|| format!("failed to canonicalize {}", root.display()))?;
15 let joined = root.join(candidate);
16 let canon = joined
17 .canonicalize()
18 .with_context(|| format!("failed to canonicalize {}", joined.display()))?;
19
20 if !canon.starts_with(&root) {
21 anyhow::bail!(
22 "path escapes root ({}): {}",
23 root.display(),
24 canon.display()
25 );
26 }
27
28 Ok(canon)
29}
30
31#[cfg(test)]
32mod tests {
33 use super::normalize_under_root;
34 use anyhow::Result;
35 use std::fs;
36 use tempfile::TempDir;
37
38 #[test]
39 fn normalizes_relative_roots_before_comparing() -> Result<()> {
40 let current_dir = std::env::current_dir()?;
41 let temp = tempfile::tempdir_in(¤t_dir)?;
42 let relative_root = temp.path().strip_prefix(¤t_dir)?;
43 let file_path = temp.path().join("pack.gtpack");
44 fs::write(&file_path, b"pack")?;
45
46 let normalized = normalize_under_root(relative_root, std::path::Path::new("pack.gtpack"))?;
47
48 assert_eq!(normalized, file_path.canonicalize()?);
49 Ok(())
50 }
51
52 #[test]
53 fn rejects_parent_escape() -> Result<()> {
54 let temp = TempDir::new()?;
55 let sibling = temp
56 .path()
57 .parent()
58 .expect("tempdir parent")
59 .join("escape.gtpack");
60 fs::write(&sibling, b"escape")?;
61
62 let err = normalize_under_root(temp.path(), std::path::Path::new("../escape.gtpack"))
63 .expect_err("parent traversal should be rejected");
64
65 assert!(err.to_string().contains("path escapes root"));
66 Ok(())
67 }
68}