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 encode_artifact_version(family, family.current_version(), flags, payload)
188}
189
190pub(crate) fn encode_artifact_version(
192 family: FormatFamily,
193 version: u16,
194 flags: u32,
195 payload: &[u8],
196) -> Vec<u8> {
197 let block_overhead = 16_usize;
198 let file_length = FILE_HEADER_LEN
199 .saturating_add(block_overhead)
200 .saturating_add(payload.len())
201 .saturating_add(FILE_TRAILER_LEN);
202 let mut encoded = Vec::with_capacity(file_length);
203 encoded.extend_from_slice(&encode_header(FileHeader {
204 magic: FILE_MAGIC,
205 family: family.id(),
206 version,
207 flags,
208 header_length: FILE_HEADER_LEN as u64,
209 file_length: file_length as u64,
210 }));
211 encoded.extend_from_slice(&(payload.len() as u64).to_le_bytes());
212 encoded.extend_from_slice(payload);
213 encoded.extend_from_slice(&xxh3_64(payload).to_le_bytes());
214 let file_checksum = xxh3_64(&encoded);
215 encoded.extend_from_slice(&file_checksum.to_le_bytes());
216 encoded
217}
218
219#[must_use]
221pub fn encode_header(header: FileHeader) -> Vec<u8> {
222 let mut encoded = Vec::with_capacity(FILE_HEADER_LEN);
223 encoded.extend_from_slice(&header.magic);
224 encoded.extend_from_slice(&header.family.to_le_bytes());
225 encoded.extend_from_slice(&header.version.to_le_bytes());
226 encoded.extend_from_slice(&header.flags.to_le_bytes());
227 encoded.extend_from_slice(&header.header_length.to_le_bytes());
228 encoded.extend_from_slice(&header.file_length.to_le_bytes());
229 encoded
230}
231
232pub fn decode_header(
234 artifact: &str,
235 expected_family: FormatFamily,
236 bytes: &[u8],
237) -> Result<FileHeader, FormatError> {
238 let fixed = bytes.get(..FILE_HEADER_LEN).ok_or_else(|| {
239 FormatError::new(
240 artifact,
241 FormatCheck::Length,
242 format!("need {FILE_HEADER_LEN} header bytes, got {}", bytes.len()),
243 )
244 })?;
245 let magic = fixed
246 .get(..8)
247 .ok_or_else(|| FormatError::new(artifact, FormatCheck::Length, "missing magic bytes"))?;
248 if magic != FILE_MAGIC {
249 return Err(FormatError::new(
250 artifact,
251 FormatCheck::Magic,
252 format!("expected {FILE_MAGIC:?}, got {magic:?}"),
253 ));
254 }
255 let family = read_u16(artifact, fixed, 8)?;
256 if family != expected_family.id() {
257 return Err(FormatError::wrong_family(
258 artifact,
259 expected_family.id(),
260 family,
261 ));
262 }
263 let version = read_u16(artifact, fixed, 10)?;
264 FormatRegistry::require(family, version).map_err(|error| registry_error(artifact, error))?;
265 let header = FileHeader {
266 magic: FILE_MAGIC,
267 family,
268 version,
269 flags: read_u32(artifact, fixed, 12)?,
270 header_length: read_u64(artifact, fixed, 16)?,
271 file_length: read_u64(artifact, fixed, 24)?,
272 };
273 if header.header_length < FILE_HEADER_LEN as u64 {
274 return Err(FormatError::new(
275 artifact,
276 FormatCheck::HeaderLength,
277 format!(
278 "header length {} is below {FILE_HEADER_LEN}",
279 header.header_length
280 ),
281 ));
282 }
283 Ok(header)
284}
285
286pub fn decode_artifact<'a>(
288 artifact: &str,
289 expected_family: FormatFamily,
290 bytes: &'a [u8],
291) -> Result<DecodedArtifact<'a>, FormatError> {
292 let header = decode_header(artifact, expected_family, bytes)?;
293 let actual_file_length = u64::try_from(bytes.len()).map_err(|_| {
294 FormatError::new(artifact, FormatCheck::FileLength, "file length exceeds u64")
295 })?;
296 if header.file_length != actual_file_length {
297 return Err(FormatError::new(
298 artifact,
299 FormatCheck::FileLength,
300 format!(
301 "declared {}, actual {actual_file_length}",
302 header.file_length
303 ),
304 ));
305 }
306 if header.header_length != FILE_HEADER_LEN as u64 {
307 return Err(FormatError::new(
308 artifact,
309 FormatCheck::HeaderLength,
310 format!(
311 "single-block header must be {FILE_HEADER_LEN}, got {}",
312 header.header_length
313 ),
314 ));
315 }
316 let trailer_start = bytes.len().checked_sub(FILE_TRAILER_LEN).ok_or_else(|| {
317 FormatError::new(artifact, FormatCheck::Length, "missing whole-file trailer")
318 })?;
319 let checksummed = bytes.get(..trailer_start).ok_or_else(|| {
320 FormatError::new(artifact, FormatCheck::Length, "invalid trailer position")
321 })?;
322 let expected_file_checksum = read_u64(artifact, bytes, trailer_start)?;
323 let actual_file_checksum = xxh3_64(checksummed);
324 if actual_file_checksum != expected_file_checksum {
325 return Err(FormatError::checksum_mismatch(
326 artifact,
327 FormatCheck::FileChecksum,
328 expected_file_checksum,
329 actual_file_checksum,
330 ));
331 }
332
333 let payload_length = read_u64(artifact, bytes, FILE_HEADER_LEN)?;
334 let payload_length = usize::try_from(payload_length).map_err(|_| {
335 FormatError::new(
336 artifact,
337 FormatCheck::BlockLength,
338 "block length exceeds usize",
339 )
340 })?;
341 let payload_start = FILE_HEADER_LEN.saturating_add(8);
342 let payload_end = payload_start.checked_add(payload_length).ok_or_else(|| {
343 FormatError::new(artifact, FormatCheck::BlockLength, "block end overflow")
344 })?;
345 let checksum_end = payload_end.checked_add(8).ok_or_else(|| {
346 FormatError::new(artifact, FormatCheck::BlockLength, "checksum end overflow")
347 })?;
348 if checksum_end != trailer_start {
349 return Err(FormatError::new(
350 artifact,
351 FormatCheck::BlockLength,
352 format!("framed block ends at {checksum_end}, trailer starts at {trailer_start}"),
353 ));
354 }
355 let payload = bytes.get(payload_start..payload_end).ok_or_else(|| {
356 FormatError::new(artifact, FormatCheck::Length, "block payload is truncated")
357 })?;
358 let expected_block_checksum = read_u64(artifact, bytes, payload_end)?;
359 let actual_block_checksum = xxh3_64(payload);
360 if actual_block_checksum != expected_block_checksum {
361 return Err(FormatError::checksum_mismatch(
362 artifact,
363 FormatCheck::BlockChecksum,
364 expected_block_checksum,
365 actual_block_checksum,
366 ));
367 }
368 Ok(DecodedArtifact { header, payload })
369}
370
371pub(crate) fn registry_error(artifact: &str, error: RegistryError) -> FormatError {
372 let mut result = FormatError::new(artifact, FormatCheck::Version, error.to_string());
373 if let RegistryError::UnsupportedVersion {
374 version,
375 minimum,
376 maximum,
377 ..
378 } = error
379 {
380 result.version_range = Some((version, minimum, maximum));
381 }
382 result
383}
384
385pub(crate) fn read_u16(artifact: &str, bytes: &[u8], offset: usize) -> Result<u16, FormatError> {
386 let raw = bytes
387 .get(offset..)
388 .and_then(|tail| tail.first_chunk::<2>())
389 .ok_or_else(|| {
390 FormatError::new(
391 artifact,
392 FormatCheck::Length,
393 format!("missing u16 at {offset}"),
394 )
395 })?;
396 Ok(u16::from_le_bytes(*raw))
397}
398
399pub(crate) fn read_u32(artifact: &str, bytes: &[u8], offset: usize) -> Result<u32, FormatError> {
400 let raw = bytes
401 .get(offset..)
402 .and_then(|tail| tail.first_chunk::<4>())
403 .ok_or_else(|| {
404 FormatError::new(
405 artifact,
406 FormatCheck::Length,
407 format!("missing u32 at {offset}"),
408 )
409 })?;
410 Ok(u32::from_le_bytes(*raw))
411}
412
413pub(crate) fn read_u64(artifact: &str, bytes: &[u8], offset: usize) -> Result<u64, FormatError> {
414 let raw = bytes
415 .get(offset..)
416 .and_then(|tail| tail.first_chunk::<8>())
417 .ok_or_else(|| {
418 FormatError::new(
419 artifact,
420 FormatCheck::Length,
421 format!("missing u64 at {offset}"),
422 )
423 })?;
424 Ok(u64::from_le_bytes(*raw))
425}