runmat_execution_artifact/program/
native_object.rs1use minicbor::{Decoder, Encoder};
2use runmat_execution::Digest;
3use serde::{Deserialize, Serialize};
4
5use crate::{ArtifactError, ArtifactResult};
6
7pub const NATIVE_OBJECT_PAYLOAD_SCHEMA_VERSION: u16 = 1;
8const PREFIX: &[u8] = b"runmat-native-object-payload-v1\0";
9const MAX_METADATA_BYTES: usize = 8 * 1024 * 1024;
10const MAX_OBJECT_BYTES: usize = 512 * 1024 * 1024;
11
12#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct NativeObjectPayload {
15 pub schema_version: u16,
16 pub object_format: String,
17 pub metadata_digest: Digest,
18 pub object_digest: Digest,
19 pub metadata: Vec<u8>,
20 pub object: Vec<u8>,
21}
22
23impl NativeObjectPayload {
24 pub fn new(
25 object_format: impl Into<String>,
26 metadata: Vec<u8>,
27 object: Vec<u8>,
28 ) -> ArtifactResult<Self> {
29 let payload = Self {
30 schema_version: NATIVE_OBJECT_PAYLOAD_SCHEMA_VERSION,
31 object_format: object_format.into(),
32 metadata_digest: Digest::sha256(&metadata),
33 object_digest: Digest::sha256(&object),
34 metadata,
35 object,
36 };
37 payload.validate()?;
38 Ok(payload)
39 }
40
41 pub fn validate(&self) -> ArtifactResult<()> {
42 if self.schema_version != NATIVE_OBJECT_PAYLOAD_SCHEMA_VERSION
43 || self.object_format.is_empty()
44 || self.object_format.len() > 32
45 || !self.object_format.is_ascii()
46 || self.object_format.chars().any(char::is_control)
47 || self.metadata.is_empty()
48 || self.metadata.len() > MAX_METADATA_BYTES
49 || self.object.is_empty()
50 || self.object.len() > MAX_OBJECT_BYTES
51 || self.metadata_digest != Digest::sha256(&self.metadata)
52 || self.object_digest != Digest::sha256(&self.object)
53 {
54 return Err(ArtifactError::Invalid(
55 "native object payload is invalid or exceeds its bounds".into(),
56 ));
57 }
58 Ok(())
59 }
60
61 pub fn to_canonical_bytes(&self) -> ArtifactResult<Vec<u8>> {
62 self.validate()?;
63 let mut bytes = PREFIX.to_vec();
64 let mut encoder = Encoder::new(&mut bytes);
65 encoder
66 .array(6)
67 .and_then(|encoder| encoder.u16(self.schema_version))
68 .and_then(|encoder| encoder.str(&self.object_format))
69 .and_then(|encoder| encoder.bytes(self.metadata_digest.bytes()))
70 .and_then(|encoder| encoder.bytes(self.object_digest.bytes()))
71 .and_then(|encoder| encoder.bytes(&self.metadata))
72 .and_then(|encoder| encoder.bytes(&self.object))
73 .map_err(|error| ArtifactError::Encoding(error.to_string()))?;
74 Ok(bytes)
75 }
76
77 pub fn from_canonical_bytes(bytes: &[u8]) -> ArtifactResult<Self> {
78 let encoded = bytes.strip_prefix(PREFIX).ok_or_else(|| {
79 ArtifactError::Invalid("native object payload prefix is invalid".into())
80 })?;
81 let mut decoder = Decoder::new(encoded);
82 if decoder.array().map_err(decoding)? != Some(6) {
83 return Err(ArtifactError::Invalid(
84 "native object payload field count is invalid".into(),
85 ));
86 }
87 let schema_version = decoder.u16().map_err(decoding)?;
88 let object_format = decoder.str().map_err(decoding)?.to_string();
89 let metadata_digest = decode_digest(&mut decoder)?;
90 let object_digest = decode_digest(&mut decoder)?;
91 let metadata = decode_bounded_bytes(&mut decoder, MAX_METADATA_BYTES, "metadata")?;
92 let object = decode_bounded_bytes(&mut decoder, MAX_OBJECT_BYTES, "object")?;
93 if decoder.position() != encoded.len() {
94 return Err(ArtifactError::Invalid(
95 "native object payload has trailing data".into(),
96 ));
97 }
98 let payload = Self {
99 schema_version,
100 object_format,
101 metadata_digest,
102 object_digest,
103 metadata,
104 object,
105 };
106 payload.validate()?;
107 Ok(payload)
108 }
109}
110
111fn decode_digest(decoder: &mut Decoder<'_>) -> ArtifactResult<Digest> {
112 let bytes = decoder.bytes().map_err(decoding)?;
113 let bytes: [u8; 32] = bytes.try_into().map_err(|_| {
114 ArtifactError::Invalid("native object payload digest length is invalid".into())
115 })?;
116 Ok(Digest::from_bytes(bytes))
117}
118
119fn decode_bounded_bytes(
120 decoder: &mut Decoder<'_>,
121 maximum: usize,
122 label: &str,
123) -> ArtifactResult<Vec<u8>> {
124 let bytes = decoder.bytes().map_err(decoding)?;
125 if bytes.is_empty() || bytes.len() > maximum {
126 return Err(ArtifactError::Invalid(format!(
127 "native object {label} exceeds its payload bound"
128 )));
129 }
130 Ok(bytes.to_vec())
131}
132
133fn decoding(error: minicbor::decode::Error) -> ArtifactError {
134 ArtifactError::Encoding(error.to_string())
135}
136
137#[cfg(test)]
138mod tests {
139 use super::NativeObjectPayload;
140
141 #[test]
142 fn canonical_payload_round_trips_and_rejects_tampering() {
143 let payload =
144 NativeObjectPayload::new("mach-o", b"metadata".to_vec(), b"object".to_vec()).unwrap();
145 let bytes = payload.to_canonical_bytes().unwrap();
146 assert_eq!(
147 NativeObjectPayload::from_canonical_bytes(&bytes).unwrap(),
148 payload
149 );
150
151 let mut tampered = payload;
152 tampered.object.push(0);
153 assert!(tampered.validate().is_err());
154 }
155}