1use xxhash_rust::xxh3::xxh3_64;
4
5use super::{FormatFamily, FormatRegistry, RegistryError};
6
7pub const FILE_HEADER_LEN: usize = 32;
9pub const FILE_TRAILER_LEN: usize = 8;
11pub const FILE_MAGIC: [u8; 8] = *b"ZEPEMBED";
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16#[repr(C)]
17pub struct FileHeader {
18 pub magic: [u8; 8],
20 pub family: u16,
22 pub version: u16,
24 pub flags: u32,
26 pub header_length: u64,
28 pub file_length: u64,
30}
31
32const _: [(); FILE_HEADER_LEN] = [(); std::mem::size_of::<FileHeader>()];
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum FormatCheck {
37 Length,
39 Magic,
41 Family,
43 Version,
45 HeaderLength,
47 FileLength,
49 BlockLength,
51 BlockChecksum,
53 FileChecksum,
55 ObjectIdentity,
57}
58
59#[derive(Clone, Debug, Eq, PartialEq)]
61pub enum FormatValues {
62 None,
64 Family {
66 expected: u16,
68 actual: u16,
70 },
71 Checksum {
73 expected: u64,
75 actual: u64,
77 },
78}
79
80#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct FormatError {
83 artifact: String,
84 check: FormatCheck,
85 values: FormatValues,
86 detail: String,
87 version_range: Option<(u16, u16, u16)>,
88}
89
90impl FormatError {
91 #[must_use]
93 pub fn new(artifact: impl Into<String>, check: FormatCheck, detail: impl Into<String>) -> Self {
94 Self {
95 artifact: artifact.into(),
96 check,
97 version_range: None,
98 values: FormatValues::None,
99 detail: detail.into(),
100 }
101 }
102
103 #[must_use]
105 pub(crate) fn wrong_family(artifact: impl Into<String>, expected: u16, actual: u16) -> Self {
106 Self {
107 artifact: artifact.into(),
108 check: FormatCheck::Family,
109 version_range: None,
110 values: FormatValues::Family { expected, actual },
111 detail: format!("expected {expected}, got {actual}"),
112 }
113 }
114
115 #[must_use]
117 pub(crate) fn checksum_mismatch(
118 artifact: impl Into<String>,
119 check: FormatCheck,
120 expected: u64,
121 actual: u64,
122 ) -> Self {
123 Self {
124 artifact: artifact.into(),
125 check,
126 version_range: None,
127 values: FormatValues::Checksum { expected, actual },
128 detail: format!("expected {expected:#018x}, computed {actual:#018x}"),
129 }
130 }
131
132 #[must_use]
134 pub fn artifact(&self) -> &str {
135 &self.artifact
136 }
137
138 #[must_use]
140 pub const fn check(&self) -> FormatCheck {
141 self.check
142 }
143
144 #[must_use]
146 pub const fn values(&self) -> &FormatValues {
147 &self.values
148 }
149
150 #[must_use]
152 pub const fn version_range(&self) -> Option<(u16, u16, u16)> {
153 self.version_range
154 }
155
156 #[must_use]
158 pub fn detail(&self) -> &str {
159 &self.detail
160 }
161}
162
163impl std::fmt::Display for FormatError {
164 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 write!(
166 formatter,
167 "artifact {} failed {:?}: {}",
168 self.artifact, self.check, self.detail
169 )
170 }
171}
172
173impl std::error::Error for FormatError {}
174
175#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177pub struct DecodedArtifact<'a> {
178 pub header: FileHeader,
180 pub payload: &'a [u8],
182}
183
184#[must_use]
186pub fn encode_artifact(family: FormatFamily, flags: u32, payload: &[u8]) -> Vec<u8> {
187 let block_overhead = 16_usize;
188 let file_length = FILE_HEADER_LEN
189 .saturating_add(block_overhead)
190 .saturating_add(payload.len())
191 .saturating_add(FILE_TRAILER_LEN);
192 let mut encoded = Vec::with_capacity(file_length);
193 encoded.extend_from_slice(&encode_header(FileHeader {
194 magic: FILE_MAGIC,
195 family: family.id(),
196 version: family.current_version(),
197 flags,
198 header_length: FILE_HEADER_LEN as u64,
199 file_length: file_length as u64,
200 }));
201 encoded.extend_from_slice(&(payload.len() as u64).to_le_bytes());
202 encoded.extend_from_slice(payload);
203 encoded.extend_from_slice(&xxh3_64(payload).to_le_bytes());
204 let file_checksum = xxh3_64(&encoded);
205 encoded.extend_from_slice(&file_checksum.to_le_bytes());
206 encoded
207}
208
209#[must_use]
211pub fn encode_header(header: FileHeader) -> Vec<u8> {
212 let mut encoded = Vec::with_capacity(FILE_HEADER_LEN);
213 encoded.extend_from_slice(&header.magic);
214 encoded.extend_from_slice(&header.family.to_le_bytes());
215 encoded.extend_from_slice(&header.version.to_le_bytes());
216 encoded.extend_from_slice(&header.flags.to_le_bytes());
217 encoded.extend_from_slice(&header.header_length.to_le_bytes());
218 encoded.extend_from_slice(&header.file_length.to_le_bytes());
219 encoded
220}
221
222pub fn decode_header(
224 artifact: &str,
225 expected_family: FormatFamily,
226 bytes: &[u8],
227) -> Result<FileHeader, FormatError> {
228 let fixed = bytes.get(..FILE_HEADER_LEN).ok_or_else(|| {
229 FormatError::new(
230 artifact,
231 FormatCheck::Length,
232 format!("need {FILE_HEADER_LEN} header bytes, got {}", bytes.len()),
233 )
234 })?;
235 let magic = fixed
236 .get(..8)
237 .ok_or_else(|| FormatError::new(artifact, FormatCheck::Length, "missing magic bytes"))?;
238 if magic != FILE_MAGIC {
239 return Err(FormatError::new(
240 artifact,
241 FormatCheck::Magic,
242 format!("expected {FILE_MAGIC:?}, got {magic:?}"),
243 ));
244 }
245 let family = read_u16(artifact, fixed, 8)?;
246 if family != expected_family.id() {
247 return Err(FormatError::wrong_family(
248 artifact,
249 expected_family.id(),
250 family,
251 ));
252 }
253 let version = read_u16(artifact, fixed, 10)?;
254 FormatRegistry::require(family, version).map_err(|error| registry_error(artifact, error))?;
255 let header = FileHeader {
256 magic: FILE_MAGIC,
257 family,
258 version,
259 flags: read_u32(artifact, fixed, 12)?,
260 header_length: read_u64(artifact, fixed, 16)?,
261 file_length: read_u64(artifact, fixed, 24)?,
262 };
263 if header.header_length < FILE_HEADER_LEN as u64 {
264 return Err(FormatError::new(
265 artifact,
266 FormatCheck::HeaderLength,
267 format!(
268 "header length {} is below {FILE_HEADER_LEN}",
269 header.header_length
270 ),
271 ));
272 }
273 Ok(header)
274}
275
276pub fn decode_artifact<'a>(
278 artifact: &str,
279 expected_family: FormatFamily,
280 bytes: &'a [u8],
281) -> Result<DecodedArtifact<'a>, FormatError> {
282 let header = decode_header(artifact, expected_family, bytes)?;
283 let actual_file_length = u64::try_from(bytes.len()).map_err(|_| {
284 FormatError::new(artifact, FormatCheck::FileLength, "file length exceeds u64")
285 })?;
286 if header.file_length != actual_file_length {
287 return Err(FormatError::new(
288 artifact,
289 FormatCheck::FileLength,
290 format!(
291 "declared {}, actual {actual_file_length}",
292 header.file_length
293 ),
294 ));
295 }
296 if header.header_length != FILE_HEADER_LEN as u64 {
297 return Err(FormatError::new(
298 artifact,
299 FormatCheck::HeaderLength,
300 format!(
301 "single-block header must be {FILE_HEADER_LEN}, got {}",
302 header.header_length
303 ),
304 ));
305 }
306 let trailer_start = bytes.len().checked_sub(FILE_TRAILER_LEN).ok_or_else(|| {
307 FormatError::new(artifact, FormatCheck::Length, "missing whole-file trailer")
308 })?;
309 let checksummed = bytes.get(..trailer_start).ok_or_else(|| {
310 FormatError::new(artifact, FormatCheck::Length, "invalid trailer position")
311 })?;
312 let expected_file_checksum = read_u64(artifact, bytes, trailer_start)?;
313 let actual_file_checksum = xxh3_64(checksummed);
314 if actual_file_checksum != expected_file_checksum {
315 return Err(FormatError::checksum_mismatch(
316 artifact,
317 FormatCheck::FileChecksum,
318 expected_file_checksum,
319 actual_file_checksum,
320 ));
321 }
322
323 let payload_length = read_u64(artifact, bytes, FILE_HEADER_LEN)?;
324 let payload_length = usize::try_from(payload_length).map_err(|_| {
325 FormatError::new(
326 artifact,
327 FormatCheck::BlockLength,
328 "block length exceeds usize",
329 )
330 })?;
331 let payload_start = FILE_HEADER_LEN.saturating_add(8);
332 let payload_end = payload_start.checked_add(payload_length).ok_or_else(|| {
333 FormatError::new(artifact, FormatCheck::BlockLength, "block end overflow")
334 })?;
335 let checksum_end = payload_end.checked_add(8).ok_or_else(|| {
336 FormatError::new(artifact, FormatCheck::BlockLength, "checksum end overflow")
337 })?;
338 if checksum_end != trailer_start {
339 return Err(FormatError::new(
340 artifact,
341 FormatCheck::BlockLength,
342 format!("framed block ends at {checksum_end}, trailer starts at {trailer_start}"),
343 ));
344 }
345 let payload = bytes.get(payload_start..payload_end).ok_or_else(|| {
346 FormatError::new(artifact, FormatCheck::Length, "block payload is truncated")
347 })?;
348 let expected_block_checksum = read_u64(artifact, bytes, payload_end)?;
349 let actual_block_checksum = xxh3_64(payload);
350 if actual_block_checksum != expected_block_checksum {
351 return Err(FormatError::checksum_mismatch(
352 artifact,
353 FormatCheck::BlockChecksum,
354 expected_block_checksum,
355 actual_block_checksum,
356 ));
357 }
358 Ok(DecodedArtifact { header, payload })
359}
360
361pub(crate) fn registry_error(artifact: &str, error: RegistryError) -> FormatError {
362 let mut result = FormatError::new(artifact, FormatCheck::Version, error.to_string());
363 if let RegistryError::UnsupportedVersion {
364 version,
365 minimum,
366 maximum,
367 ..
368 } = error
369 {
370 result.version_range = Some((version, minimum, maximum));
371 }
372 result
373}
374
375pub(crate) fn read_u16(artifact: &str, bytes: &[u8], offset: usize) -> Result<u16, FormatError> {
376 let raw = bytes
377 .get(offset..)
378 .and_then(|tail| tail.first_chunk::<2>())
379 .ok_or_else(|| {
380 FormatError::new(
381 artifact,
382 FormatCheck::Length,
383 format!("missing u16 at {offset}"),
384 )
385 })?;
386 Ok(u16::from_le_bytes(*raw))
387}
388
389pub(crate) fn read_u32(artifact: &str, bytes: &[u8], offset: usize) -> Result<u32, FormatError> {
390 let raw = bytes
391 .get(offset..)
392 .and_then(|tail| tail.first_chunk::<4>())
393 .ok_or_else(|| {
394 FormatError::new(
395 artifact,
396 FormatCheck::Length,
397 format!("missing u32 at {offset}"),
398 )
399 })?;
400 Ok(u32::from_le_bytes(*raw))
401}
402
403pub(crate) fn read_u64(artifact: &str, bytes: &[u8], offset: usize) -> Result<u64, FormatError> {
404 let raw = bytes
405 .get(offset..)
406 .and_then(|tail| tail.first_chunk::<8>())
407 .ok_or_else(|| {
408 FormatError::new(
409 artifact,
410 FormatCheck::Length,
411 format!("missing u64 at {offset}"),
412 )
413 })?;
414 Ok(u64::from_le_bytes(*raw))
415}