Skip to main content

runmat_execution_artifact/program/
artifact.rs

1use 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}