1#[cfg(any(target_os = "macos", test))]
2use std::collections::BTreeSet;
3
4const MAGIC: [u8; 4] = *b"CMLP";
5const VERSION_MAJOR: u8 = 1;
6const VERSION_MINOR: u8 = 0;
7const HEADER_BYTES: usize = 16;
8const ENTRY_HEADER_BYTES: usize = 8;
9pub(crate) const MAX_ARTIFACT_BYTES: u64 = 64 * 1024;
10const MAX_MODEL_PATH_BYTES: usize = 4 * 1024;
11const MAX_FEATURE_NAME_BYTES: usize = 1024;
12const MAX_MAPPINGS: usize = 256;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16#[repr(u8)]
17pub enum FeatureRole {
18 Input = 1,
19 Output = 2,
20}
21
22impl FeatureRole {
23 #[cfg(any(target_os = "macos", test))]
24 fn from_wire(value: u8) -> Result<Self, DecodeError> {
25 match value {
26 1 => Ok(Self::Input),
27 2 => Ok(Self::Output),
28 _ => Err(DecodeError::Invalid),
29 }
30 }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum ArtifactBuildError {
36 EmptyModelPath,
37 ModelPathTooLong,
38 EmptyFeatureName,
39 FeatureNameTooLong,
40 DuplicateFeature,
41 EmptyMappings,
42 TooManyMappings,
43 ArtifactTooLarge,
44}
45
46impl std::fmt::Display for ArtifactBuildError {
47 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 write!(formatter, "{self:?}")
49 }
50}
51
52impl std::error::Error for ArtifactBuildError {}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub(crate) struct FeatureMapping {
56 pub slot: u32,
57 pub role: FeatureRole,
58 pub name: String,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
67pub struct CoreMlArtifact {
68 model_path: String,
69 mappings: Vec<FeatureMapping>,
70}
71
72impl CoreMlArtifact {
73 pub fn new(model_path: impl Into<String>) -> Result<Self, ArtifactBuildError> {
74 let model_path = model_path.into();
75 if model_path.is_empty() {
76 return Err(ArtifactBuildError::EmptyModelPath);
77 }
78 if model_path.len() > MAX_MODEL_PATH_BYTES || model_path.len() > u16::MAX as usize {
79 return Err(ArtifactBuildError::ModelPathTooLong);
80 }
81 Ok(Self {
82 model_path,
83 mappings: Vec::new(),
84 })
85 }
86
87 pub fn map_input(
88 self,
89 slot: u32,
90 feature_name: impl Into<String>,
91 ) -> Result<Self, ArtifactBuildError> {
92 self.map(slot, FeatureRole::Input, feature_name.into())
93 }
94
95 pub fn map_output(
96 self,
97 slot: u32,
98 feature_name: impl Into<String>,
99 ) -> Result<Self, ArtifactBuildError> {
100 self.map(slot, FeatureRole::Output, feature_name.into())
101 }
102
103 pub fn encode(&self) -> Result<Vec<u8>, ArtifactBuildError> {
104 if self.mappings.is_empty() {
105 return Err(ArtifactBuildError::EmptyMappings);
106 }
107 if self.mappings.len() > MAX_MAPPINGS || self.mappings.len() > u16::MAX as usize {
108 return Err(ArtifactBuildError::TooManyMappings);
109 }
110 let mut bytes = Vec::with_capacity(
111 HEADER_BYTES
112 + self.model_path.len()
113 + self
114 .mappings
115 .iter()
116 .map(|mapping| ENTRY_HEADER_BYTES + mapping.name.len())
117 .sum::<usize>(),
118 );
119 bytes.extend_from_slice(&MAGIC);
120 bytes.push(VERSION_MAJOR);
121 bytes.push(VERSION_MINOR);
122 bytes.extend_from_slice(&0u16.to_le_bytes());
123 bytes.extend_from_slice(&(self.model_path.len() as u16).to_le_bytes());
124 bytes.extend_from_slice(&(self.mappings.len() as u16).to_le_bytes());
125 bytes.extend_from_slice(&0u32.to_le_bytes());
126 bytes.extend_from_slice(self.model_path.as_bytes());
127 for mapping in &self.mappings {
128 bytes.extend_from_slice(&mapping.slot.to_le_bytes());
129 bytes.push(mapping.role as u8);
130 bytes.push(0);
131 bytes.extend_from_slice(&(mapping.name.len() as u16).to_le_bytes());
132 bytes.extend_from_slice(mapping.name.as_bytes());
133 }
134 if bytes.len() as u64 > MAX_ARTIFACT_BYTES {
135 return Err(ArtifactBuildError::ArtifactTooLarge);
136 }
137 Ok(bytes)
138 }
139
140 fn map(
141 mut self,
142 slot: u32,
143 role: FeatureRole,
144 name: String,
145 ) -> Result<Self, ArtifactBuildError> {
146 if name.is_empty() {
147 return Err(ArtifactBuildError::EmptyFeatureName);
148 }
149 if name.len() > MAX_FEATURE_NAME_BYTES || name.len() > u16::MAX as usize {
150 return Err(ArtifactBuildError::FeatureNameTooLong);
151 }
152 if self
153 .mappings
154 .iter()
155 .any(|mapping| mapping.role == role && mapping.name == name)
156 {
157 return Err(ArtifactBuildError::DuplicateFeature);
158 }
159 if self.mappings.len() == MAX_MAPPINGS {
160 return Err(ArtifactBuildError::TooManyMappings);
161 }
162 self.mappings.push(FeatureMapping { slot, role, name });
163 Ok(self)
164 }
165}
166
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168#[cfg(any(target_os = "macos", test))]
169pub(crate) enum DecodeError {
170 Invalid,
171 OutOfBounds,
172 ResourceLimit,
173}
174
175#[derive(Debug)]
176#[cfg(any(target_os = "macos", test))]
177pub(crate) struct DecodedArtifact {
178 pub model_path: String,
179 pub mappings: Vec<FeatureMapping>,
180}
181
182#[cfg(any(target_os = "macos", test))]
183pub(crate) fn decode<S: virtio_accel_core::ByteSource + ?Sized>(
184 source: &S,
185) -> Result<DecodedArtifact, DecodeError> {
186 if source.len() > MAX_ARTIFACT_BYTES {
187 return Err(DecodeError::ResourceLimit);
188 }
189 if source.len() < HEADER_BYTES as u64 {
190 return Err(DecodeError::Invalid);
191 }
192
193 let mut header = [0; HEADER_BYTES];
194 source
195 .read_at(0, &mut header)
196 .map_err(|_| DecodeError::OutOfBounds)?;
197 if header[..4] != MAGIC
198 || header[4] != VERSION_MAJOR
199 || header[5] != VERSION_MINOR
200 || header[6..8] != [0, 0]
201 || header[12..16] != [0, 0, 0, 0]
202 {
203 return Err(DecodeError::Invalid);
204 }
205 let path_len = u16::from_le_bytes([header[8], header[9]]) as usize;
206 let mapping_count = u16::from_le_bytes([header[10], header[11]]) as usize;
207 if path_len == 0 || path_len > MAX_MODEL_PATH_BYTES || mapping_count > MAX_MAPPINGS {
208 return Err(DecodeError::Invalid);
209 }
210
211 let mut cursor = HEADER_BYTES as u64;
212 let mut path = vec![0; path_len];
213 source
214 .read_at(cursor, &mut path)
215 .map_err(|_| DecodeError::OutOfBounds)?;
216 cursor += path_len as u64;
217 let model_path = String::from_utf8(path).map_err(|_| DecodeError::Invalid)?;
218
219 let mut mappings = Vec::new();
220 mappings
221 .try_reserve_exact(mapping_count)
222 .map_err(|_| DecodeError::ResourceLimit)?;
223 let mut features = BTreeSet::new();
224 for _ in 0..mapping_count {
225 let mut entry = [0; ENTRY_HEADER_BYTES];
226 source
227 .read_at(cursor, &mut entry)
228 .map_err(|_| DecodeError::OutOfBounds)?;
229 cursor += ENTRY_HEADER_BYTES as u64;
230 if entry[5] != 0 {
231 return Err(DecodeError::Invalid);
232 }
233 let slot = u32::from_le_bytes(entry[..4].try_into().unwrap());
234 let role = FeatureRole::from_wire(entry[4])?;
235 let name_len = u16::from_le_bytes([entry[6], entry[7]]) as usize;
236 if name_len == 0 || name_len > MAX_FEATURE_NAME_BYTES {
237 return Err(DecodeError::Invalid);
238 }
239 let mut name = vec![0; name_len];
240 source
241 .read_at(cursor, &mut name)
242 .map_err(|_| DecodeError::OutOfBounds)?;
243 cursor += name_len as u64;
244 let name = String::from_utf8(name).map_err(|_| DecodeError::Invalid)?;
245 if !features.insert((role as u8, name.clone())) {
246 return Err(DecodeError::Invalid);
247 }
248 mappings.push(FeatureMapping { slot, role, name });
249 }
250 if cursor != source.len() || mappings.is_empty() {
251 return Err(DecodeError::Invalid);
252 }
253 Ok(DecodedArtifact {
254 model_path,
255 mappings,
256 })
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn path_artifact_round_trips() {
265 let artifact = CoreMlArtifact::new("models/twice.mlmodel")
266 .unwrap()
267 .map_input(7, "x")
268 .unwrap()
269 .map_output(7, "y")
270 .unwrap();
271 let bytes = artifact.encode().unwrap();
272 let decoded = decode(bytes.as_slice()).unwrap();
273 assert_eq!(decoded.model_path, "models/twice.mlmodel");
274 assert_eq!(decoded.mappings, artifact.mappings);
275 }
276
277 #[test]
278 fn malformed_envelopes_are_rejected() {
279 assert_eq!(
280 CoreMlArtifact::new("model.mlmodel").unwrap().encode(),
281 Err(ArtifactBuildError::EmptyMappings)
282 );
283 let artifact = CoreMlArtifact::new("model.mlmodel")
284 .unwrap()
285 .map_input(0, "x")
286 .unwrap();
287 let mut bytes = artifact.encode().unwrap();
288 bytes[6] = 1;
289 assert_eq!(decode(bytes.as_slice()).unwrap_err(), DecodeError::Invalid);
290
291 let duplicate = CoreMlArtifact {
292 model_path: "model.mlmodel".into(),
293 mappings: vec![
294 FeatureMapping {
295 slot: 0,
296 role: FeatureRole::Input,
297 name: "x".into(),
298 },
299 FeatureMapping {
300 slot: 1,
301 role: FeatureRole::Input,
302 name: "x".into(),
303 },
304 ],
305 }
306 .encode()
307 .unwrap();
308 assert_eq!(
309 decode(duplicate.as_slice()).unwrap_err(),
310 DecodeError::Invalid
311 );
312 }
313}