use ic_host_process::tool::{CompletedExecution, OutputLimits, ToolError, capture_command};
use serde_json::{Value, json};
use std::{
collections::{BTreeMap, BTreeSet},
error::Error,
fmt::{self, Write as _},
fs, io,
path::{Path, PathBuf},
process::Command,
};
type Cause = Box<dyn Error>;
pub struct PackageArchiveSpec<'a> {
pub name: &'a str,
pub version: &'a str,
pub patch_registry: bool,
}
#[derive(Clone, Copy)]
pub struct PackageQualificationLimits {
pub process: OutputLimits,
pub file_bytes: usize,
pub source_entries: usize,
pub source_depth: usize,
}
#[derive(Debug)]
pub struct PackageCommandError {
pub execution: Option<ToolError>,
pub retention: Vec<io::Error>,
}
impl fmt::Display for PackageCommandError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(error) = &self.execution {
error.fmt(f)
} else {
f.write_str("package command diagnostic retention failed")
}
}
}
impl Error for PackageCommandError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.execution
.as_ref()
.map(|error| error as &dyn Error)
.or_else(|| self.retention.first().map(|error| error as &dyn Error))
}
}
#[derive(Debug)]
pub struct PackageQualificationError {
pub evidence: PathBuf,
pub cause: Cause,
}
impl fmt::Display for PackageQualificationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.cause.fmt(f)
}
}
impl Error for PackageQualificationError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self.cause.as_ref())
}
}
pub fn qualify_package_archives(
workspace: &Path,
archive_root: &Path,
evidence: &Path,
packages: &[PackageArchiveSpec<'_>],
limits: PackageQualificationLimits,
mut cargo: impl FnMut() -> Command,
mut extractor: impl FnMut(&Path, &Path) -> Command,
) -> Result<(), PackageQualificationError> {
qualify(
workspace,
archive_root,
evidence,
packages,
limits,
&mut cargo,
&mut extractor,
)
.map_err(|cause| PackageQualificationError {
evidence: evidence.to_owned(),
cause,
})
}
#[expect(
clippy::too_many_lines,
reason = "ordered archive qualification retains one before/after custody boundary"
)]
fn qualify(
workspace: &Path,
archive_root: &Path,
evidence: &Path,
packages: &[PackageArchiveSpec<'_>],
limits: PackageQualificationLimits,
cargo: &mut impl FnMut() -> Command,
extractor: &mut impl FnMut(&Path, &Path) -> Command,
) -> Result<(), Cause> {
if packages.is_empty() || limits.file_bytes == 0 || limits.source_entries == 0 {
return Err("empty package selection or admission budget".into());
}
let mut names = BTreeSet::new();
for package in packages {
if !identifier(package.name) || !identifier(package.version) || !names.insert(package.name)
{
return Err("invalid or duplicate package archive identity".into());
}
}
let workspace = workspace.canonicalize()?;
let archive_root = archive_root.canonicalize()?;
let evidence = evidence.canonicalize()?;
for path in [&workspace, &archive_root, &evidence] {
if path
.as_os_str()
.as_encoded_bytes()
.iter()
.any(|b| matches!(b, b'\r' | b'\n'))
{
return Err("invalid directory namespace".into());
}
}
if fs::read_dir(&evidence)?.next().is_some() {
return Err("evidence directory is not empty".into());
}
let lock = workspace.join("Cargo.lock");
let entry_lock = read(&lock, limits.file_bytes)?;
fs::write(evidence.join("entry-Cargo.lock"), &entry_lock)?;
let mut command = cargo();
command.current_dir(&workspace).args([
"metadata",
"--offline",
"--locked",
"--no-deps",
"--format-version",
"1",
]);
let metadata = execute(&mut command, &evidence, "entry-metadata", limits.process)?;
let metadata: Value = serde_json::from_slice(&metadata)?;
let mut originals = BTreeMap::new();
originals.insert(
workspace.join("Cargo.toml"),
read(&workspace.join("Cargo.toml"), limits.file_bytes)?,
);
for spec in packages {
let selected = package(&metadata, spec.name)?;
if selected["version"] != spec.version {
return Err("entry package version differs from selection".into());
}
let path = PathBuf::from(
selected["manifest_path"]
.as_str()
.ok_or("missing entry manifest")?,
);
originals.insert(path.clone(), read(&path, limits.file_bytes)?);
}
for (index, bytes) in originals.values().enumerate() {
fs::write(evidence.join(format!("entry-manifest-{index}")), bytes)?;
}
let mut command = cargo();
command.current_dir(&workspace).args([
"package",
"--offline",
"--locked",
"--allow-dirty",
"--no-verify",
]);
for spec in packages {
command.args(["-p", spec.name]);
}
execute(&mut command, &evidence, "package", limits.process)?;
let mut archives = BTreeMap::new();
for (index, spec) in packages.iter().enumerate() {
let path = archive_root.join(format!("{}-{}.crate", spec.name, spec.version));
archives.insert(path.clone(), hash(&path, limits.file_bytes)?);
execute(
&mut extractor(&path, &evidence),
&evidence,
&format!("extract-{index}"),
limits.process,
)?;
}
let mut archive_hashes = String::new();
for digest in archives.values() {
writeln!(archive_hashes, "{digest}")?;
}
fs::write(evidence.join("archive.sha256"), archive_hashes)?;
let sources = sources(&evidence, packages, limits)?;
fs::write(
evidence.join("entry-sources.json"),
serde_json::to_vec(&sources)?,
)?;
let members = packages
.iter()
.map(|p| format!("{}-{}", p.name, p.version))
.collect::<Vec<_>>();
let mut manifest = format!(
"[workspace]\nresolver = \"3\"\nmembers = {}\n[patch.crates-io]\n",
serde_json::to_string(&members)?
);
for spec in packages.iter().filter(|p| p.patch_registry) {
writeln!(
manifest,
"{} = {{ path = \"{}-{}\" }}",
spec.name, spec.name, spec.version
)?;
}
let manifest_path = evidence.join("Cargo.toml");
let payload_lock = evidence.join("Cargo.lock");
fs::write(&manifest_path, &manifest)?;
fs::write(&payload_lock, &entry_lock)?;
let mut command = cargo();
command
.current_dir(&evidence)
.env("CARGO_TARGET_DIR", evidence.join("target"))
.args([
"metadata",
"--offline",
"--format-version",
"1",
"--manifest-path",
])
.arg(&manifest_path);
let metadata = execute(&mut command, &evidence, "metadata", limits.process)?;
admit_graph(&serde_json::from_slice(&metadata)?, &evidence, packages)?;
let admitted_lock = read(&payload_lock, limits.file_bytes)?;
admit_lock(&entry_lock, &admitted_lock)?;
fs::write(evidence.join("admitted-Cargo.toml"), &manifest)?;
fs::write(evidence.join("admitted-Cargo.lock"), &admitted_lock)?;
for (label, selection) in [("test-targets", "--all-targets"), ("test-docs", "--doc")] {
let mut command = cargo();
command
.current_dir(&evidence)
.env("CARGO_TARGET_DIR", evidence.join("target"))
.args(["test", "--offline", "--locked", "--manifest-path"])
.arg(&manifest_path)
.args(["--workspace", selection]);
execute(&mut command, &evidence, label, limits.process)?;
}
if read(&lock, limits.file_bytes)? != entry_lock {
return Err("original lock changed".into());
}
for (path, bytes) in originals {
if read(&path, limits.file_bytes)? != bytes {
return Err("original manifest changed".into());
}
}
for (path, digest) in archives {
if hash(&path, limits.file_bytes)? != digest {
return Err("package archive changed".into());
}
}
let final_sources = self::sources(&evidence, packages, limits)?;
fs::write(
evidence.join("final-sources.json"),
serde_json::to_vec(&final_sources)?,
)?;
if final_sources != sources {
return Err("extracted package sources changed".into());
}
if read(&manifest_path, limits.file_bytes)? != manifest.as_bytes()
|| read(&payload_lock, limits.file_bytes)? != admitted_lock
{
return Err("admitted verification manifest or lock changed".into());
}
Ok(())
}
fn identifier(value: &str) -> bool {
!value.is_empty()
&& value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'+'))
}
fn read(path: &Path, limit: usize) -> Result<Vec<u8>, Cause> {
Ok(ic_host_fs::read::read_file_no_follow(path, limit)?)
}
fn hash(path: &Path, limit: usize) -> Result<String, Cause> {
Ok(
ic_host_fs::read::hash_file_no_follow(path, u64::try_from(limit)?)?
.sha256
.to_string(),
)
}
fn execute(
command: &mut Command,
evidence: &Path,
label: &str,
limits: OutputLimits,
) -> Result<Vec<u8>, Cause> {
let result = capture_command(command, limits)
.and_then(CompletedExecution::require_success)
.and_then(CompletedExecution::require_complete);
let (stdout, stderr) = match &result {
Ok(output) => (&output.stdout, &output.stderr),
Err(ToolError::Execution(error)) => (&error.evidence.stdout, &error.evidence.stderr),
Err(_) => (&Vec::new(), &Vec::new()),
};
let retention = [
fs::write(evidence.join(format!("{label}.json")), stdout),
fs::write(evidence.join(format!("{label}.stderr")), stderr),
]
.into_iter()
.filter_map(Result::err)
.collect::<Vec<_>>();
match result {
Ok(output) if retention.is_empty() => Ok(output.stdout),
other => Err(Box::new(PackageCommandError {
execution: other.err(),
retention,
})),
}
}
fn package<'a>(metadata: &'a Value, name: &str) -> Result<&'a Value, Cause> {
let selected = metadata["packages"]
.as_array()
.ok_or("missing metadata packages")?
.iter()
.filter(|p| p["name"] == name)
.collect::<Vec<_>>();
if selected.len() != 1 {
return Err(format!("expected one extracted package: {name}").into());
}
Ok(selected[0])
}
fn admit_graph(
metadata: &Value,
root: &Path,
packages: &[PackageArchiveSpec<'_>],
) -> Result<(), Cause> {
let members = metadata["workspace_members"]
.as_array()
.ok_or("missing workspace members")?;
let mut selected = BTreeSet::new();
for spec in packages {
let package = package(metadata, spec.name)?;
let manifest = root
.join(format!("{}-{}/Cargo.toml", spec.name, spec.version))
.canonicalize()?;
if package["version"] != spec.version
|| !package["source"].is_null()
|| package["manifest_path"].as_str().map(Path::new) != Some(manifest.as_path())
|| !members.contains(&package["id"])
{
return Err(format!(
"resolved package is not the exact extracted payload: {}",
spec.name
)
.into());
}
selected.insert(package["id"].as_str().ok_or("missing package identity")?);
}
if members.len() != packages.len() || selected.len() != members.len() {
return Err("unexpected extracted workspace member".into());
}
Ok(())
}
fn external_rows(bytes: &[u8]) -> Result<BTreeMap<(String, String, String), String>, Cause> {
let lock: toml::Value = toml::from_str(std::str::from_utf8(bytes)?)?;
let mut rows = BTreeMap::new();
for package in lock
.get("package")
.and_then(toml::Value::as_array)
.ok_or("missing lock packages")?
{
let Some(source) = package.get("source") else {
continue;
};
let text = |key| {
package
.get(key)
.and_then(toml::Value::as_str)
.ok_or_else(|| format!("missing lock {key}"))
};
let source = source.as_str().ok_or("invalid lock source")?;
let checksum = if source.starts_with("registry+") {
let checksum = text("checksum")?;
if checksum.len() != 64
|| !checksum
.bytes()
.all(|b| b.is_ascii_digit() || matches!(b, b'a'..=b'f'))
{
return Err("invalid package checksum".into());
}
checksum
} else {
""
};
if rows
.insert(
(text("name")?.into(), text("version")?.into(), source.into()),
checksum.into(),
)
.is_some()
{
return Err("duplicate package selection".into());
}
}
Ok(rows)
}
fn admit_lock(entry: &[u8], payload: &[u8]) -> Result<(), Cause> {
let entry = external_rows(entry)?;
let payload = external_rows(payload)?;
if payload.is_empty() {
return Err("empty package external graph".into());
}
if payload
.iter()
.any(|(key, checksum)| entry.get(key) != Some(checksum))
{
return Err("packaging changed external selection".into());
}
Ok(())
}
fn sources(
root: &Path,
packages: &[PackageArchiveSpec<'_>],
limits: PackageQualificationLimits,
) -> Result<BTreeMap<PathBuf, Value>, Cause> {
let mut files = BTreeMap::new();
let mut remaining = limits.source_entries;
for spec in packages {
inventory(
root,
&root.join(format!("{}-{}", spec.name, spec.version)),
limits,
0,
&mut remaining,
&mut files,
)?;
}
Ok(files)
}
fn inventory(
root: &Path,
path: &Path,
limits: PackageQualificationLimits,
depth: usize,
remaining: &mut usize,
files: &mut BTreeMap<PathBuf, Value>,
) -> Result<(), Cause> {
if depth > limits.source_depth || *remaining == 0 {
return Err("extracted source traversal budget exceeded".into());
}
*remaining -= 1;
let metadata = fs::symlink_metadata(path)?;
if metadata.is_dir() {
for entry in fs::read_dir(path)? {
inventory(root, &entry?.path(), limits, depth + 1, remaining, files)?;
}
} else if metadata.is_file() {
#[cfg(unix)]
let mode = {
use std::os::unix::fs::MetadataExt as _;
metadata.mode() & 0o777
};
#[cfg(not(unix))]
let mode = metadata.permissions().readonly();
files.insert(
path.strip_prefix(root)?.to_owned(),
json!({"sha256": hash(path, limits.file_bytes)?, "mode": mode}),
);
} else {
return Err(format!("unsupported extracted source entry: {}", path.display()).into());
}
Ok(())
}
#[cfg(test)]
mod tests;