use std::path::{Component, Path, PathBuf};
use super::ArtifactVerifyError;
pub fn file_root_from_env() -> PathBuf {
std::env::var("IMAGE_TRUST_FILE_ROOT")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/"))
}
pub fn resolve_under_root(rel: &str, root: &Path) -> Result<PathBuf, ArtifactVerifyError> {
if rel.is_empty() {
return Err(ArtifactVerifyError::UnsafePath {
path: rel.to_string(),
reason: "empty path".into(),
});
}
if rel.contains('\0') {
return Err(ArtifactVerifyError::UnsafePath {
path: rel.to_string(),
reason: "NUL byte in path".into(),
});
}
let rel_path = Path::new(rel);
if rel_path.is_absolute() {
return Err(ArtifactVerifyError::UnsafePath {
path: rel.to_string(),
reason: "absolute paths are not allowed in manifest bindings".into(),
});
}
for component in rel_path.components() {
if matches!(component, Component::ParentDir) {
return Err(ArtifactVerifyError::UnsafePath {
path: rel.to_string(),
reason: "parent-dir (..) components are not allowed".into(),
});
}
}
let joined = root.join(rel_path);
let canonical_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let canonical = joined.canonicalize().map_err(|e| ArtifactVerifyError::Io {
path: rel.to_string(),
source: e,
})?;
if !canonical.starts_with(&canonical_root) {
return Err(ArtifactVerifyError::UnsafePath {
path: rel.to_string(),
reason: format!("path escapes IMAGE_TRUST_FILE_ROOT ({})", root.display()),
});
}
Ok(canonical)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn rejects_parent_dir() {
let root = PathBuf::from("/app");
assert!(resolve_under_root("../etc/passwd", &root).is_err());
}
#[test]
fn resolves_under_root() {
let dir = TempDir::new().unwrap();
let file = dir.path().join("bundle.wasm");
fs::write(&file, b"test").unwrap();
let resolved = resolve_under_root("bundle.wasm", dir.path()).unwrap();
assert_eq!(resolved, file.canonicalize().unwrap());
}
}