1use std::path::{Component, Path, PathBuf};
4
5use crate::error::{Error, Result};
6
7pub fn validate_chroot_name(name: &str) -> Result<()> {
9 validate_segment(name, "chroot name")
10}
11
12pub fn resolve_under_root(root: &Path, relative: &str) -> Result<PathBuf> {
14 if relative.is_empty() {
15 return Err(Error::msg("path must not be empty"));
16 }
17 for component in Path::new(relative).components() {
18 match component {
19 Component::Normal(_) | Component::CurDir => {}
20 Component::ParentDir => {
21 return Err(Error::msg("path must not contain '..'"));
22 }
23 Component::RootDir | Component::Prefix(_) => {
24 return Err(Error::msg("path must be relative to the store root"));
25 }
26 }
27 }
28
29 let mut resolved = root_canonical(root)?;
30 for component in Path::new(relative).components() {
31 if let Component::Normal(part) = component {
32 resolved.push(part);
33 }
34 }
35 Ok(resolved)
36}
37
38pub fn path_within_root(root: &Path, path: &Path) -> Result<bool> {
40 let root = root_canonical(root)?;
41 let path = if path.exists() {
42 path.canonicalize().map_err(crate::Error::from)?
43 } else {
44 normalize_under_root(&root, path)?
45 };
46 Ok(path.starts_with(&root))
47}
48
49fn root_canonical(root: &Path) -> Result<PathBuf> {
50 match root.canonicalize() {
51 Ok(p) => Ok(p),
52 Err(_) => Ok(root.to_path_buf()),
53 }
54}
55
56fn normalize_under_root(root: &Path, path: &Path) -> Result<PathBuf> {
57 if path.is_absolute() {
58 return Ok(path.to_path_buf());
59 }
60 let mut resolved = root.to_path_buf();
61 for component in path.components() {
62 match component {
63 Component::Normal(part) => resolved.push(part),
64 Component::CurDir => {}
65 Component::ParentDir => return Err(Error::msg("path escapes store root")),
66 Component::RootDir | Component::Prefix(_) => {}
67 }
68 }
69 Ok(resolved)
70}
71
72fn validate_segment(segment: &str, label: &str) -> Result<()> {
73 if segment.is_empty() || segment.contains('/') || segment.contains("..") {
74 return Err(Error::msg(format!("invalid {label}: {segment}")));
75 }
76 Ok(())
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn rejects_parent_dir_in_relative() {
85 let root = PathBuf::from("/tmp/unifier");
86 assert!(resolve_under_root(&root, "../etc/passwd").is_err());
87 }
88}