use super::{
CanwuError, ErrorCode, RunConfiguration, RunConfigurationSnapshot, Scenario, canonical_hash,
is_canonical_hash, policy,
};
use serde::{Deserialize, Serialize};
pub const RUN_MANIFEST_FORMAT_VERSION: u32 = 2;
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct ArtifactManifest {
pub namespace: String,
pub name: String,
pub version: String,
pub semantic_hash: String,
}
impl ArtifactManifest {
pub fn new(
namespace: impl Into<String>,
name: impl Into<String>,
version: impl Into<String>,
semantic_hash: impl Into<String>,
) -> Result<Self, CanwuError> {
let manifest = Self {
namespace: namespace.into(),
name: name.into(),
version: version.into(),
semantic_hash: semantic_hash.into(),
};
validate_artifact(&manifest, "artifact")?;
Ok(manifest)
}
pub fn from_bytes(
namespace: impl Into<String>,
name: impl Into<String>,
version: impl Into<String>,
bytes: &[u8],
) -> Result<Self, CanwuError> {
let mut hasher = blake3::Hasher::new();
hasher.update(b"canwu.artifact-bytes.v1");
hasher.update(&[0]);
hasher.update(bytes);
Self::new(
namespace,
name,
version,
hasher.finalize().to_hex().to_string(),
)
}
pub fn for_scenario(
namespace: impl Into<String>,
name: impl Into<String>,
version: impl Into<String>,
scenario: &Scenario,
) -> Result<Self, CanwuError> {
Self::new(namespace, name, version, scenario_semantic_hash(scenario)?)
}
pub fn for_run_configuration(
namespace: impl Into<String>,
name: impl Into<String>,
version: impl Into<String>,
configuration: &RunConfiguration,
) -> Result<Self, CanwuError> {
let mut configuration = configuration.clone();
configuration.canonicalize();
configuration.validate()?;
Self::new(namespace, name, version, configuration.semantic_hash()?)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "provenance", rename_all = "snake_case")]
pub enum RunManifest {
Declared {
format_version: u32,
scenario: ArtifactManifest,
#[serde(default)]
rules: Vec<ArtifactManifest>,
#[serde(default)]
content: Vec<ArtifactManifest>,
#[serde(default)]
localization_contracts: Vec<ArtifactManifest>,
run_configuration: Box<ArtifactManifest>,
#[serde(default)]
sources: Vec<ArtifactManifest>,
},
}
impl RunManifest {
pub fn for_scenario(
namespace: impl Into<String>,
name: impl Into<String>,
version: impl Into<String>,
scenario: &Scenario,
) -> Result<Self, CanwuError> {
let scenario = ArtifactManifest::for_scenario(namespace, name, version, scenario)?;
let run_configuration = default_run_configuration_manifest()?;
Ok(Self::Declared {
format_version: RUN_MANIFEST_FORMAT_VERSION,
scenario,
rules: Vec::new(),
content: Vec::new(),
localization_contracts: Vec::new(),
run_configuration: Box::new(run_configuration),
sources: Vec::new(),
})
}
#[must_use]
pub fn declared(scenario: ArtifactManifest, run_configuration: ArtifactManifest) -> Self {
Self::Declared {
format_version: RUN_MANIFEST_FORMAT_VERSION,
scenario,
rules: Vec::new(),
content: Vec::new(),
localization_contracts: Vec::new(),
run_configuration: Box::new(run_configuration),
sources: Vec::new(),
}
}
}
pub(crate) fn canonicalize(manifest: &mut RunManifest) {
let RunManifest::Declared {
rules,
content,
localization_contracts,
sources,
..
} = manifest;
rules.sort();
content.sort();
localization_contracts.sort();
sources.sort();
}
pub(crate) fn validate(
manifest: &RunManifest,
scenario: Option<&Scenario>,
) -> Result<(), CanwuError> {
match manifest {
RunManifest::Declared {
format_version,
scenario: scenario_manifest,
rules,
content,
localization_contracts,
run_configuration,
sources,
} => {
if *format_version != RUN_MANIFEST_FORMAT_VERSION {
return invalid_manifest(format!(
"run manifest format {format_version} is unsupported"
));
}
validate_artifact(scenario_manifest, "scenario")?;
validate_artifact(run_configuration, "run configuration")?;
validate_artifact_list(rules, "rules")?;
validate_artifact_list(content, "content")?;
validate_artifact_list(localization_contracts, "localization contract")?;
validate_artifact_list(sources, "source")?;
if let Some(scenario) = scenario
&& scenario_manifest.semantic_hash != scenario_semantic_hash(scenario)?
{
return invalid_manifest(
"scenario manifest hash does not match the admitted scenario",
);
}
Ok(())
}
}
}
pub(crate) fn hash(manifest: &RunManifest) -> Result<String, CanwuError> {
canonical_hash("canwu.run-manifest.v1", manifest)
}
pub(crate) fn validate_run_configuration(
manifest: &RunManifest,
configuration: &RunConfigurationSnapshot,
) -> Result<(), CanwuError> {
configuration.validate()?;
match (manifest, configuration) {
(
RunManifest::Declared {
run_configuration, ..
},
RunConfigurationSnapshot::Declared(_) | RunConfigurationSnapshot::CompatibilityV1,
) => {
if configuration.semantic_hash()?.as_deref()
!= Some(run_configuration.semantic_hash.as_str())
{
return invalid_manifest(
"run configuration snapshot does not match its manifest identity",
);
}
Ok(())
}
(
RunManifest::Declared {
run_configuration, ..
},
RunConfigurationSnapshot::ManifestOnlyV1,
) => {
if run_configuration.semantic_hash == policy::compatibility_configuration_hash()? {
return invalid_manifest(
"the default run configuration must use compatibility-v1 provenance",
);
}
Ok(())
}
(RunManifest::Declared { .. }, RunConfigurationSnapshot::LegacyUnspecified) => {
invalid_manifest("declared runs cannot use an identity-unbound run configuration")
}
}
}
fn scenario_semantic_hash(scenario: &Scenario) -> Result<String, CanwuError> {
let mut canonical = scenario.clone();
canonical.entities.sort();
canonical.entities.dedup();
if canonical.entities == super::scenario::legacy_entities(&canonical.world) {
canonical.entities.clear();
}
canonical.world.people.sort_by_key(|value| value.id);
canonical.world.governments.sort_by_key(|value| value.id);
canonical.world.territories.sort_by_key(|value| value.id);
canonical.world.routes.sort_by_key(|value| value.id);
canonical.world.armies.sort_by_key(|value| value.id);
canonical
.domain_records
.sort_by(|left, right| left.reference.cmp(&right.reference));
canonical_hash("canwu.scenario.v1", &canonical)
}
fn default_run_configuration_manifest() -> Result<ArtifactManifest, CanwuError> {
ArtifactManifest::new(
"canwu.core",
"default-run-configuration",
"1",
policy::compatibility_configuration_hash()?,
)
}
fn validate_artifact_list(manifests: &[ArtifactManifest], label: &str) -> Result<(), CanwuError> {
let mut previous = None;
for manifest in manifests {
validate_artifact(manifest, label)?;
if previous.is_some_and(|value: &ArtifactManifest| {
value >= manifest
|| (value.namespace == manifest.namespace && value.name == manifest.name)
}) {
return invalid_manifest(format!(
"{label} manifests must have unique identities and canonical order"
));
}
previous = Some(manifest);
}
Ok(())
}
fn validate_artifact(manifest: &ArtifactManifest, label: &str) -> Result<(), CanwuError> {
if !canonical_text(&manifest.namespace)
|| !canonical_text(&manifest.name)
|| !canonical_text(&manifest.version)
|| !is_canonical_hash(&manifest.semantic_hash)
{
return invalid_manifest(format!(
"{label} manifests require canonical namespace, name, version, and semantic hash"
));
}
Ok(())
}
fn canonical_text(value: &str) -> bool {
!value.is_empty() && value == value.trim()
}
fn invalid_manifest<T>(message: impl Into<String>) -> Result<T, CanwuError> {
Err(CanwuError::new(ErrorCode::InvalidRunManifest, message))
}