use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use crate::archive::{list_zip_entries, read_zip_entry_text, ArchiveEntry};
use crate::contract::documents::SignedDocument;
use crate::contract::links::EntryKind;
use crate::contract::targets::{assert_python_entry_point, box_target_adapter, BoxTargetAdapter};
use crate::error::{fail, Error, Result};
use crate::execution::assert_execution_files;
use crate::filesystem::sha256_file;
use crate::release::{BoxManifest, ReleaseManifest};
use crate::trust::{verify_signed_document, TrustAnchors};
#[derive(Debug, Clone)]
pub struct InspectedRelease {
pub release_path: PathBuf,
pub signed: SignedDocument,
pub release: ReleaseManifest,
pub adapter: &'static BoxTargetAdapter,
}
pub fn inspect_release_document(
release_document_path: &Path,
trust: TrustAnchors<'_>,
) -> Result<InspectedRelease> {
let trusted = trust.resolve()?;
let release_path = release_document_path
.canonicalize()
.unwrap_or_else(|_| release_document_path.to_path_buf());
let raw = std::fs::read(&release_path).map_err(|error| {
Error::new(format!(
"Invalid signed release document {}: {error}",
release_path.display()
))
})?;
let signed = SignedDocument::parse(&raw)?;
let payload = verify_signed_document(&signed, &trusted)?;
if payload.value.get("schemaVersion").and_then(serde_json::Value::as_u64) == Some(1) {
fail!("Unsupported schemaVersion 1; rebuild this box with Scrollcase v2.");
}
let release: ReleaseManifest = serde_json::from_value(payload.value)
.map_err(|error| Error::new(format!("Invalid release manifest: {error}.")))?;
release.validate()?;
let adapter = box_target_adapter(&release.target)?;
assert_python_entry_point(adapter, &release.python_entry_point)?;
Ok(InspectedRelease {
release_path,
signed,
release,
adapter,
})
}
pub fn assert_box_manifest_agreement(
box_manifest: &BoxManifest,
release: &ReleaseManifest,
) -> Result<()> {
let mismatch = if box_manifest.schema_version != release.schema_version {
Some("schemaVersion")
} else if box_manifest.box_id != release.box_id {
Some("boxId")
} else if box_manifest.model_id != release.model_id {
Some("modelId")
} else if box_manifest.runtime_id != release.runtime_id {
Some("runtimeId")
} else if box_manifest.version != release.version {
Some("version")
} else if box_manifest.target != release.target {
Some("target")
} else if box_manifest.python_entry_point != release.python_entry_point {
Some("pythonEntryPoint")
} else if box_manifest.model_cache_subdir != release.model_cache_subdir {
Some("modelCacheSubdir")
} else if box_manifest.environment != release.environment {
Some("environment")
} else if box_manifest.self_test != release.self_test {
Some("selfTest")
} else if box_manifest.execution != release.execution {
Some("execution")
} else if box_manifest.weights != release.weights {
Some("weights")
} else if box_manifest.assets != release.assets {
Some("assets")
} else if box_manifest.provenance != release.provenance {
Some("provenance")
} else {
None
};
if let Some(field) = mismatch {
fail!("box.json mismatch: {field}");
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct InspectedArchive {
pub release: InspectedRelease,
pub archive_path: PathBuf,
pub box_manifest: BoxManifest,
pub entries: Vec<ArchiveEntry>,
}
pub fn inspect_box_archive(
release_document_path: &Path,
trust: TrustAnchors<'_>,
archive_override: Option<&Path>,
) -> Result<InspectedArchive> {
let release = inspect_release_document(release_document_path, trust)?;
inspect_archive_for(release, archive_override)
}
pub fn inspect_archive_for(
inspected: InspectedRelease,
archive_override: Option<&Path>,
) -> Result<InspectedArchive> {
let release = &inspected.release;
let archive_path = match archive_override {
Some(path) => path.to_path_buf(),
None => inspected
.release_path
.parent()
.unwrap_or(Path::new("."))
.join(format!("{}.zip", release.archive.sha256)),
};
let metadata = std::fs::metadata(&archive_path)
.map_err(|_| Error::new(format!("Archive not found: {}", archive_path.display())))?;
if metadata.len() != release.archive.size_bytes {
fail!("Archive size mismatch.");
}
if sha256_file(&archive_path)? != release.archive.sha256 {
fail!("Archive SHA-256 mismatch.");
}
let entries = list_zip_entries(&archive_path)?;
let files: BTreeSet<String> = entries
.iter()
.filter(|entry| entry.kind == EntryKind::File)
.map(|entry| entry.path.clone())
.collect();
let resolvable: BTreeSet<String> = entries
.iter()
.filter(|entry| matches!(entry.kind, EntryKind::File | EntryKind::Link))
.map(|entry| entry.path.clone())
.collect();
if !files.contains("box.json") {
fail!("Archive is missing box.json.");
}
let raw = read_zip_entry_text(&archive_path, "box.json")?;
let box_manifest: BoxManifest = serde_json::from_str(&raw)
.map_err(|error| Error::new(format!("Invalid box.json: {error}.")))?;
assert_box_manifest_agreement(&box_manifest, release)?;
if !resolvable.contains(&release.python_entry_point) {
fail!("Archive is missing {}.", release.python_entry_point);
}
assert_execution_files(
release.execution.as_ref(),
inspected.adapter,
&release.provenance.python_version,
&resolvable,
)?;
Ok(InspectedArchive {
release: inspected,
archive_path,
box_manifest,
entries,
})
}