use std::path::{Component, Path, PathBuf};
use crate::error::{Error, Result};
pub fn validate_chroot_name(name: &str) -> Result<()> {
validate_segment(name, "chroot name")
}
pub fn resolve_under_root(root: &Path, relative: &str) -> Result<PathBuf> {
if relative.is_empty() {
return Err(Error::msg("path must not be empty"));
}
for component in Path::new(relative).components() {
match component {
Component::Normal(_) | Component::CurDir => {}
Component::ParentDir => {
return Err(Error::msg("path must not contain '..'"));
}
Component::RootDir | Component::Prefix(_) => {
return Err(Error::msg("path must be relative to the store root"));
}
}
}
let mut resolved = root_canonical(root)?;
for component in Path::new(relative).components() {
if let Component::Normal(part) = component {
resolved.push(part);
}
}
Ok(resolved)
}
pub fn path_within_root(root: &Path, path: &Path) -> Result<bool> {
let root = root_canonical(root)?;
let path = if path.exists() {
path.canonicalize().map_err(crate::Error::from)?
} else {
normalize_under_root(&root, path)?
};
Ok(path.starts_with(&root))
}
fn root_canonical(root: &Path) -> Result<PathBuf> {
match root.canonicalize() {
Ok(p) => Ok(p),
Err(_) => Ok(root.to_path_buf()),
}
}
fn normalize_under_root(root: &Path, path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
return Ok(path.to_path_buf());
}
let mut resolved = root.to_path_buf();
for component in path.components() {
match component {
Component::Normal(part) => resolved.push(part),
Component::CurDir => {}
Component::ParentDir => return Err(Error::msg("path escapes store root")),
Component::RootDir | Component::Prefix(_) => {}
}
}
Ok(resolved)
}
fn validate_segment(segment: &str, label: &str) -> Result<()> {
if segment.is_empty() || segment.contains('/') || segment.contains("..") {
return Err(Error::msg(format!("invalid {label}: {segment}")));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_parent_dir_in_relative() {
let root = PathBuf::from("/tmp/unifier");
assert!(resolve_under_root(&root, "../etc/passwd").is_err());
}
}