use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::contract::documents::{parse_document_kind, DocumentType, BOX_SCHEMA_VERSION};
use crate::contract::targets::BoxTarget;
use crate::error::{fail, Result};
use crate::path::safe_relative_path;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Compatibility {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_host_app_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_host_app_version_exclusive: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_macos_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_ram_gb: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_nvidia_driver_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host_environments: Option<Vec<String>>,
#[serde(flatten)]
pub additional: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Archive {
pub format: String,
pub url: String,
pub sha256: String,
pub size_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PayloadDigestCommitment {
pub format: String,
pub sha256: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SelfTest {
pub python_imports: Vec<String>,
pub timeout_seconds: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum Execution {
#[serde(rename_all = "camelCase")]
PythonScript {
script: String,
default_args: Vec<String>,
},
#[serde(rename_all = "camelCase")]
PythonModule {
module: String,
default_args: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Provenance {
pub scroll_id: String,
pub scroll_version: String,
pub builder_revision: String,
pub source_tree_dirty: bool,
pub source_revision: String,
pub python_version: String,
pub dependency_lock_sha256: String,
pub built_at: String,
pub pixi_version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AssetDescriptor {
pub url: String,
pub relative_path: String,
pub size_bytes: u64,
pub sha256: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReleaseManifest {
pub schema_version: u32,
pub kind: String,
pub box_id: String,
pub model_id: String,
pub runtime_id: String,
pub version: String,
pub target: BoxTarget,
pub compatibility: Compatibility,
pub archive: Archive,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub installed_size_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload_digest: Option<PayloadDigestCommitment>,
pub python_entry_point: String,
pub model_cache_subdir: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment: Option<BTreeMap<String, String>>,
pub self_test: SelfTest,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub execution: Option<Execution>,
pub provenance: Provenance,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weights: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assets: Option<Vec<AssetDescriptor>>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BoxManifest {
pub schema_version: u32,
pub box_id: String,
pub model_id: String,
pub runtime_id: String,
pub version: String,
pub target: BoxTarget,
pub python_entry_point: String,
pub model_cache_subdir: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment: Option<BTreeMap<String, String>>,
pub self_test: SelfTest,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub execution: Option<Execution>,
pub provenance: Provenance,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weights: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assets: Option<Vec<AssetDescriptor>>,
}
fn is_lowercase_hex(value: &str, length: usize) -> bool {
value.len() == length
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
fn is_identifier(value: &str) -> bool {
if value.is_empty() {
return false;
}
let mut group_is_empty = true;
for character in value.chars() {
match character {
'a'..='z' | '0'..='9' => group_is_empty = false,
'-' | '.' if !group_is_empty => group_is_empty = true,
_ => return false,
}
}
!group_is_empty
}
fn is_python_module(value: &str) -> bool {
!value.is_empty()
&& value.split('.').all(|segment| {
let mut characters = segment.chars();
characters
.next()
.is_some_and(|first| first.is_ascii_alphabetic() || first == '_')
&& characters.all(|rest| rest.is_ascii_alphanumeric() || rest == '_')
})
}
impl Execution {
pub fn validate(&self) -> Result<()> {
match self {
Self::PythonScript { script, .. } => {
safe_relative_path(script)?;
}
Self::PythonModule { module, .. } => {
if !is_python_module(module) {
fail!("Invalid release manifest: execution module {module} is not a dotted Python module name.");
}
}
}
Ok(())
}
}
impl ReleaseManifest {
pub fn validate(&self) -> Result<()> {
if self.schema_version != BOX_SCHEMA_VERSION {
fail!(
"Unsupported schemaVersion {}; expected {BOX_SCHEMA_VERSION}.",
self.schema_version
);
}
if parse_document_kind(&self.kind).map(|parsed| parsed.document_type)
!= Some(DocumentType::Release)
{
fail!("Document is not a box release.");
}
crate::contract::targets::box_target_id(&self.target)?;
for (label, value) in [
("boxId", &self.box_id),
("modelId", &self.model_id),
("runtimeId", &self.runtime_id),
] {
if !is_identifier(value) {
fail!("Invalid release manifest: {label} is not a valid identifier.");
}
}
for (label, value) in [
("version", &self.version),
("pythonEntryPoint", &self.python_entry_point),
("modelCacheSubdir", &self.model_cache_subdir),
("archive.url", &self.archive.url),
] {
if value.is_empty() {
fail!("Invalid release manifest: {label} must not be empty.");
}
}
if self.archive.format != "zip" {
fail!("Invalid release manifest: archive format must be zip.");
}
if !is_lowercase_hex(&self.archive.sha256, 64) {
fail!("Invalid release manifest: archive sha256 is not a SHA-256 digest.");
}
if self.archive.size_bytes == 0 {
fail!("Invalid release manifest: archive sizeBytes must be positive.");
}
if self.installed_size_bytes == Some(0) {
fail!("Invalid installed size.");
}
if let Some(digest) = &self.payload_digest {
if digest.format != crate::contract::payload_digest::PAYLOAD_DIGEST_FORMAT
|| !is_lowercase_hex(&digest.sha256, 64)
{
fail!("Invalid release manifest: payloadDigest is not a supported commitment.");
}
}
if self.self_test.python_imports.is_empty()
|| self
.self_test
.python_imports
.iter()
.any(std::string::String::is_empty)
{
fail!("Invalid release manifest: selfTest pythonImports must be non-empty.");
}
if self.self_test.timeout_seconds == 0 {
fail!("Invalid release manifest: selfTest timeoutSeconds must be positive.");
}
validate_environment(self.environment.as_ref())?;
if let Some(execution) = &self.execution {
execution.validate()?;
}
validate_provenance(&self.provenance)?;
validate_compatibility(&self.compatibility)?;
self.validate_assets()?;
Ok(())
}
fn validate_assets(&self) -> Result<()> {
let assets = match (self.weights.as_deref(), self.assets.as_deref()) {
(None, None) => return Ok(()),
(Some("on-demand"), Some(assets)) => assets,
(Some(other), Some(_)) => {
fail!("Invalid release manifest: unsupported weights value {other}.")
}
(Some(_), None) | (None, Some(_)) => {
fail!("Invalid release manifest: weights and assets must be declared together.")
}
};
if assets.is_empty() {
fail!("Invalid release manifest: assets must not be empty.");
}
for asset in assets {
safe_relative_path(&asset.relative_path)?;
if asset.url.is_empty() {
fail!("Invalid release manifest: asset url must not be empty.");
}
if asset.size_bytes == 0 {
fail!("Invalid release manifest: asset sizeBytes must be positive.");
}
if !is_lowercase_hex(&asset.sha256, 64) {
fail!("Invalid release manifest: asset sha256 is not a SHA-256 digest.");
}
}
Ok(())
}
}
fn validate_environment(environment: Option<&BTreeMap<String, String>>) -> Result<()> {
let Some(environment) = environment else {
return Ok(());
};
for (name, value) in environment {
if name.is_empty() || name.contains('=') || name.contains('\0') || value.contains('\0') {
fail!("Invalid release manifest: environment variable {name} is not a valid name.");
}
}
Ok(())
}
fn validate_provenance(provenance: &Provenance) -> Result<()> {
if !is_lowercase_hex(&provenance.builder_revision, 40) {
fail!("Invalid release manifest: provenance builderRevision is not a commit.");
}
if !is_lowercase_hex(&provenance.dependency_lock_sha256, 64) {
fail!("Invalid release manifest: provenance dependencyLockSha256 is not a SHA-256 digest.");
}
for (label, value) in [
("scrollId", &provenance.scroll_id),
("scrollVersion", &provenance.scroll_version),
("sourceRevision", &provenance.source_revision),
("pythonVersion", &provenance.python_version),
("builtAt", &provenance.built_at),
("pixiVersion", &provenance.pixi_version),
] {
if value.is_empty() {
fail!("Invalid release manifest: provenance {label} must not be empty.");
}
}
Ok(())
}
fn validate_compatibility(compatibility: &Compatibility) -> Result<()> {
if compatibility.min_ram_gb.is_some_and(|value| value <= 0.0) {
fail!("Invalid release manifest: minRamGb must be positive.");
}
if let Some(environments) = &compatibility.host_environments {
if environments.is_empty() {
fail!("Invalid release manifest: hostEnvironments must not be empty.");
}
for environment in environments {
if environment != "native" && environment != "windows-wsl2" {
fail!("Invalid release manifest: unsupported host environment {environment}.");
}
}
}
for (label, value) in [
("minHostAppVersion", &compatibility.min_host_app_version),
(
"maxHostAppVersionExclusive",
&compatibility.max_host_app_version_exclusive,
),
("minMacosVersion", &compatibility.min_macos_version),
(
"minNvidiaDriverVersion",
&compatibility.min_nvidia_driver_version,
),
] {
if value.as_deref().is_some_and(str::is_empty) {
fail!("Invalid release manifest: compatibility {label} must not be empty.");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{is_identifier, is_python_module, Execution};
#[test]
fn identifiers_follow_the_shared_pattern() {
for valid in ["hello-box", "a", "example.model-1", "b0x"] {
assert!(is_identifier(valid), "{valid} was refused");
}
for invalid in ["", "-a", "a-", "a..b", "A", "a_b", "a b", ".a"] {
assert!(!is_identifier(invalid), "{invalid} was accepted");
}
}
#[test]
fn module_names_carry_no_command_line_syntax() {
for valid in ["main", "_pkg.main", "example_model.cli.main"] {
assert!(is_python_module(valid), "{valid} was refused");
}
for invalid in ["", "a b", "a;b", "-c", "a/b", "1abc", "a..b", "a."] {
assert!(!is_python_module(invalid), "{invalid} was accepted");
}
}
#[test]
fn execution_paths_are_screened_before_they_are_joined() {
let escape = Execution::PythonScript {
script: "../outside.py".to_string(),
default_args: vec![],
};
assert!(escape.validate().is_err());
let ok = Execution::PythonScript {
script: "app/main.py".to_string(),
default_args: vec![],
};
assert!(ok.validate().is_ok());
}
}