runmat_execution_artifact/program/
recipe.rs1use 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}