runmat_execution_artifact/program/
artifact.rs1use minicbor::Encoder;
2use runmat_execution::Digest;
3use serde::{Deserialize, Serialize};
4
5use super::{ProgramArtifactId, ProgramBuildRecipe, ProgramRecipeId, ProgramTarget};
6use crate::{ArtifactError, ArtifactResult};
7
8pub const PROGRAM_ARTIFACT_SCHEMA_VERSION: u16 = 2;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12#[repr(u8)]
13pub enum ExecutableForm {
14 InterpreterBytecodeV1 = 0,
15 InterpreterScriptV1 = 1,
16 TestAttemptV1 = 2,
17 ExecutableUnitV3 = 3,
18 NativeObjectV1 = 4,
19 MeshingWorkload = 5,
20}
21
22#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct ProgramArtifact {
25 pub schema_version: u16,
26 pub id: ProgramArtifactId,
27 pub recipe_id: ProgramRecipeId,
28 pub target: ProgramTarget,
29 pub form: ExecutableForm,
30 pub executable_bytes: Vec<u8>,
31}
32
33impl ProgramArtifact {
34 pub fn native_object(&self) -> ArtifactResult<Option<super::NativeObjectPayload>> {
35 if self.form != ExecutableForm::NativeObjectV1 {
36 return Ok(None);
37 }
38 super::NativeObjectPayload::from_canonical_bytes(&self.executable_bytes).map(Some)
39 }
40
41 pub fn executable_unit(
42 &self,
43 ) -> ArtifactResult<Option<runmat_execution::ExecutableUnitEnvelope>> {
44 if self.form != ExecutableForm::ExecutableUnitV3 {
45 return Ok(None);
46 }
47 runmat_execution::ExecutableUnitEnvelope::from_canonical_bytes(&self.executable_bytes)
48 .map(Some)
49 .map_err(|error| ArtifactError::Invalid(error.to_string()))
50 }
51
52 pub fn materialize(
53 recipe: &ProgramBuildRecipe,
54 form: ExecutableForm,
55 executable_bytes: Vec<u8>,
56 ) -> ArtifactResult<Self> {
57 let recipe_id = recipe.id()?;
58 if executable_bytes.is_empty() {
59 return Err(ArtifactError::Invalid(
60 "program artifact executable is empty".into(),
61 ));
62 }
63 recipe.target.validate_form(form)?;
64 let id = derive_id(recipe_id, &recipe.target, form, &executable_bytes)?;
65 let artifact = Self {
66 schema_version: PROGRAM_ARTIFACT_SCHEMA_VERSION,
67 id,
68 recipe_id,
69 target: recipe.target.clone(),
70 form,
71 executable_bytes,
72 };
73 artifact.validate_against(recipe)?;
74 Ok(artifact)
75 }
76
77 pub fn validate_against(&self, recipe: &ProgramBuildRecipe) -> ArtifactResult<()> {
78 if self.schema_version != PROGRAM_ARTIFACT_SCHEMA_VERSION
79 || self.recipe_id != recipe.id()?
80 || self.target != recipe.target
81 || self.id
82 != derive_id(
83 self.recipe_id,
84 &self.target,
85 self.form,
86 &self.executable_bytes,
87 )?
88 || recipe
89 .expected_artifact_id
90 .is_some_and(|expected| expected != self.id)
91 {
92 return Err(ArtifactError::Identity(
93 "program artifact does not converge with its exact recipe".into(),
94 ));
95 }
96 self.target.validate_form(self.form)?;
97 if let Some(envelope) = self.executable_unit()? {
98 if envelope.manifest.identity.program != recipe.program_revision {
99 return Err(ArtifactError::Identity(
100 "executable unit does not match its exact program revision".into(),
101 ));
102 }
103 }
104 if self.form == ExecutableForm::NativeObjectV1 {
105 let payload = self
106 .native_object()?
107 .expect("native-object form returns its validated payload");
108 let native = self.target.native.as_ref().ok_or_else(|| {
109 ArtifactError::Invalid("native object artifact has no native target".into())
110 })?;
111 if payload.object_format != native.object_format {
112 return Err(ArtifactError::Identity(
113 "native object format differs from its target identity".into(),
114 ));
115 }
116 }
117 Ok(())
118 }
119}
120
121fn derive_id(
122 recipe_id: ProgramRecipeId,
123 target: &ProgramTarget,
124 form: ExecutableForm,
125 executable_bytes: &[u8],
126) -> ArtifactResult<ProgramArtifactId> {
127 let mut bytes = b"runmat-program-artifact-v2\0".to_vec();
128 let mut encoder = Encoder::new(&mut bytes);
129 encoder
130 .array(4)
131 .and_then(|encoder| encoder.bytes(recipe_id.0.bytes()))
132 .and_then(|encoder| {
133 encoder.bytes(
134 &target
135 .canonical_bytes()
136 .map_err(|_| minicbor::encode::Error::message("invalid program target"))?,
137 )
138 })
139 .and_then(|encoder| encoder.u8(form as u8))
140 .and_then(|encoder| encoder.bytes(executable_bytes))
141 .map_err(|error| ArtifactError::Encoding(error.to_string()))?;
142 Ok(ProgramArtifactId(Digest::sha256(bytes)))
143}