Skip to main content

runmat_execution_artifact/program/
recipe.rs

1use std::collections::BTreeSet;
2
3use minicbor::Encoder;
4use runmat_execution::{Digest, OutputContract, ProgramRevision};
5use serde::{Deserialize, Serialize};
6
7use super::{ProgramRecipeId, ProgramTarget};
8use crate::{ArtifactError, ArtifactResult, ObjectDescriptor};
9
10pub const PROGRAM_BUILD_RECIPE_SCHEMA_VERSION: u16 = 2;
11
12#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct ProgramBuildRecipe {
15    pub schema_version: u16,
16    pub program_revision: ProgramRevision,
17    pub entrypoint: String,
18    pub outputs: OutputContract,
19    pub execution_mode: String,
20    pub target: ProgramTarget,
21    pub features: BTreeSet<String>,
22    pub compile_options: BTreeSet<String>,
23    pub source_objects: Vec<ObjectDescriptor>,
24    pub expected_artifact_id: Option<super::ProgramArtifactId>,
25}
26
27impl ProgramBuildRecipe {
28    pub fn validate(&self) -> ArtifactResult<()> {
29        self.program_revision
30            .validate()
31            .map_err(|error| ArtifactError::Invalid(error.to_string()))?;
32        if self.schema_version != PROGRAM_BUILD_RECIPE_SCHEMA_VERSION
33            || !valid_token(&self.entrypoint, 512)
34            || !valid_token(&self.execution_mode, 64)
35            || self.features.iter().any(|value| !valid_token(value, 128))
36            || self
37                .compile_options
38                .iter()
39                .any(|value| !valid_token(value, 256))
40            || self
41                .source_objects
42                .windows(2)
43                .any(|pair| pair[0] >= pair[1])
44        {
45            return Err(ArtifactError::Invalid(
46                "program build recipe is not canonical".into(),
47            ));
48        }
49        self.target.validate()?;
50        for source in &self.source_objects {
51            source.validate()?;
52        }
53        Ok(())
54    }
55
56    pub fn id(&self) -> ArtifactResult<ProgramRecipeId> {
57        self.validate()?;
58        let revision = self
59            .program_revision
60            .canonical_bytes()
61            .map_err(|error| ArtifactError::Encoding(error.to_string()))?;
62        let mut bytes = b"runmat-program-build-recipe-v2\0".to_vec();
63        let mut encoder = Encoder::new(&mut bytes);
64        encoder
65            .array(9)
66            .and_then(|encoder| encoder.bytes(&revision))
67            .and_then(|encoder| encoder.str(&self.entrypoint))
68            .and_then(|encoder| encoder.u16(self.outputs.requested_outputs))
69            .and_then(|encoder| encoder.str(&self.execution_mode))
70            .and_then(|encoder| {
71                encoder.bytes(
72                    &self
73                        .target
74                        .canonical_bytes()
75                        .map_err(|_| minicbor::encode::Error::message("invalid program target"))?,
76                )
77            })
78            .and_then(|encoder| encoder.array(self.features.len() as u64))
79            .map_err(encoding)?;
80        for feature in &self.features {
81            encoder.str(feature).map_err(encoding)?;
82        }
83        encoder
84            .array(self.compile_options.len() as u64)
85            .map_err(encoding)?;
86        for option in &self.compile_options {
87            encoder.str(option).map_err(encoding)?;
88        }
89        encoder
90            .array(self.source_objects.len() as u64)
91            .map_err(encoding)?;
92        for source in &self.source_objects {
93            encoder
94                .array(5)
95                .and_then(|encoder| encoder.u8(source.namespace as u8))
96                .and_then(|encoder| encoder.str(&source.logical_name))
97                .and_then(|encoder| encoder.bytes(source.digest.bytes()))
98                .and_then(|encoder| encoder.u64(source.encoded_length))
99                .and_then(|encoder| encoder.str(&source.media_type))
100                .map_err(encoding)?;
101        }
102        match self.expected_artifact_id {
103            Some(id) => encoder.bytes(id.0.bytes()).map_err(encoding)?,
104            None => encoder.null().map_err(encoding)?,
105        };
106        Ok(ProgramRecipeId(Digest::sha256(bytes)))
107    }
108}
109
110fn valid_token(value: &str, max: usize) -> bool {
111    !value.is_empty()
112        && value.len() <= max
113        && value.is_ascii()
114        && !value.chars().any(char::is_control)
115}
116
117fn encoding(error: minicbor::encode::Error<std::convert::Infallible>) -> ArtifactError {
118    ArtifactError::Encoding(error.to_string())
119}