use std::path::{Path, PathBuf};
pub struct ValidatedArtifact {
pub manifest_json: String,
pub program_path: PathBuf,
}
pub(super) fn validate_filename(name: &str) -> Result<(), String> {
if name.is_empty()
|| matches!(name, "." | "..")
|| !name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
{
return Err("artifact name must be a plain filename without traversal".into());
}
Ok(())
}
pub fn load_artifact(directory: &Path) -> Result<ValidatedArtifact, String> {
let manifest_json = std::fs::read_to_string(directory.join("manifest.json"))
.map_err(|e| format!("cannot read package manifest: {e}"))?;
let manifest: serde_json::Value = serde_json::from_str(&manifest_json)
.map_err(|e| format!("invalid package manifest: {e}"))?;
let filename = manifest
.get("program_file")
.and_then(|v| v.as_str())
.ok_or("manifest lacks program_file; repackage with the current compiler")?;
validate_filename(filename)?;
if filename == "manifest.json" {
return Err("manifest cannot name itself as the compiled program".into());
}
let directory = directory
.canonicalize()
.map_err(|e| format!("cannot resolve package directory: {e}"))?;
let program_path = directory
.join(filename)
.canonicalize()
.map_err(|e| format!("cannot resolve declared program '{filename}': {e}"))?;
if program_path.parent() != Some(directory.as_path()) || !program_path.is_file() {
return Err("declared program is not a file inside the package directory".into());
}
let expected = manifest
.get("program_digest")
.and_then(|v| v.as_str())
.ok_or("manifest lacks program_digest")?;
let assembly =
std::fs::read(&program_path).map_err(|e| format!("cannot read declared program: {e}"))?;
let actual = crate::hash::ContentHash(crate::hash::content_hash_bytes(&assembly)).to_hex();
if expected != actual {
return Err("declared program content does not match manifest digest".into());
}
Ok(ValidatedArtifact {
manifest_json,
program_path,
})
}