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}
88
89impl FormatError {
90 #[must_use]
92 pub fn new(artifact: impl Into<String>, check: FormatCheck, detail: impl Into<String>) -> Self {
93 Self {
94 artifact: artifact.into(),
95 check,
96 values: FormatValues::None,
97 detail: detail.into(),
98 }
99 }
100
101 #[must_use]
103 pub(crate) fn wrong_family(artifact: impl Into<String>, expected: u16, actual: u16) -> Self {
104 Self {
105 artifact: artifact.into(),
106 check: FormatCheck::Family,
107 values: FormatValues::Family { expected, actual },
108 detail: format!("expected {expected}, got {actual}"),
109 }
110 }
111
112 #[must_use]
114 pub(crate) fn checksum_mismatch(
115 artifact: impl Into<String>,
116 check: FormatCheck,
117 expected: u64,
118 actual: u64,
119 ) -> Self {
120 Self {
121 artifact: artifact.into(),
122 check,
123 values: FormatValues::Checksum { expected, actual },
124 detail: format!("expected {expected:#018x}, computed {actual:#018x}"),
125 }
126 }
127
128 #[must_use]
130 pub fn artifact(&self) -> &str {
131 &self.artifact
132 }
133
134 #[must_use]
136 pub const fn check(&self) -> FormatCheck {
137 self.check
138 }
139
140 #[must_use]
142 pub const fn values(&self) -> &FormatValues {
143 &self.values
144 }
145
146 #[must_use]
148 pub fn detail(&self) -> &str {
149 &self.detail
150 }
151}
152
153impl std::fmt::Display for FormatError {
154 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 write!(
156 formatter,
157 "artifact {} failed {:?}: {}",
158 self.artifact, self.check, self.detail
159 )
160 }
161}
162
163impl std::error::Error for FormatError {}
164
165#[derive(Clone, Copy, Debug, Eq, PartialEq)]
167pub struct DecodedArtifact<'a> {
168 pub header: FileHeader,
170 pub payload: &'a [u8],
172}
173
174#[must_use]
176pub fn encode_artifact(family: FormatFamily, flags: u32, payload: &[u8]) -> Vec<u8> {
177 let block_overhead = 16_usize;
178 let file_length = FILE_HEADER_LEN
179 .saturating_add(block_overhead)
180 .saturating_add(payload.len())
181 .saturating_add(FILE_TRAILER_LEN);
182 let mut encoded = Vec::with_capacity(file_length);
183 encoded.extend_from_slice(&encode_header(FileHeader {
184 magic: FILE_MAGIC,
185 family: family.id(),
186 version: family.current_version(),
187 flags,
188 header_length: FILE_HEADER_LEN as u64,
189 file_length: file_length as u64,
190 }));
191 encoded.extend_from_slice(&(payload.len() as u64).to_le_bytes());
192 encoded.extend_from_slice(payload);
193 encoded.extend_from_slice(&xxh3_64(payload).to_le_bytes());
194 let file_checksum = xxh3_64(&encoded);
195 encoded.extend_from_slice(&file_checksum.to_le_bytes());
196 encoded
197}
198
199#[must_use]
201pub fn encode_header(header: FileHeader) -> Vec<u8> {
202 let mut encoded = Vec::with_capacity(FILE_HEADER_LEN);
203 encoded.extend_from_slice(&header.magic);
204 encoded.extend_from_slice(&header.family.to_le_bytes());
205 encoded.extend_from_slice(&header.version.to_le_bytes());
206 encoded.extend_from_slice(&header.flags.to_le_bytes());
207 encoded.extend_from_slice(&header.header_length.to_le_bytes());
208 encoded.extend_from_slice(&header.file_length.to_le_bytes());
209 encoded
210}
211
212pub fn decode_header(
214 artifact: &str,
215 expected_family: FormatFamily,
216 bytes: &[u8],
217) -> Result<FileHeader, FormatError> {
218 let fixed = bytes.get(..FILE_HEADER_LEN).ok_or_else(|| {
219 FormatError::new(
220 artifact,
221 FormatCheck::Length,
222 format!("need {FILE_HEADER_LEN} header bytes, got {}", bytes.len()),
223 )
224 })?;
225 let magic = fixed
226 .get(..8)
227 .ok_or_else(|| FormatError::new(artifact, FormatCheck::Length, "missing magic bytes"))?;
228 if magic != FILE_MAGIC {
229 return Err(FormatError::new(
230 artifact,
231 FormatCheck::Magic,
232 format!("expected {FILE_MAGIC:?}, got {magic:?}"),
233 ));
234 }
235 let family = read_u16(artifact, fixed, 8)?;
236 if family != expected_family.id() {
237 return Err(FormatError::wrong_family(
238 artifact,
239 expected_family.id(),
240 family,
241 ));
242 }
243 let version = read_u16(artifact, fixed, 10)?;
244 FormatRegistry::require(family, version).map_err(|error| registry_error(artifact, error))?;
245 let header = FileHeader {
246 magic: FILE_MAGIC,
247 family,
248 version,
249 flags: read_u32(artifact, fixed, 12)?,
250 header_length: read_u64(artifact, fixed, 16)?,
251 file_length: read_u64(artifact, fixed, 24)?,
252 };
253 if header.header_length < FILE_HEADER_LEN as u64 {
254 return Err(FormatError::new(
255 artifact,
256 FormatCheck::HeaderLength,
257 format!(
258 "header length {} is below {FILE_HEADER_LEN}",
259 header.header_length
260 ),
261 ));
262 }
263 Ok(header)
264}
265
266pub fn decode_artifact<'a>(
268 artifact: &str,
269 expected_family: FormatFamily,
270 bytes: &'a [u8],
271) -> Result<DecodedArtifact<'a>, FormatError> {
272 let header = decode_header(artifact, expected_family, bytes)?;
273 let actual_file_length = u64::try_from(bytes.len()).map_err(|_| {
274 FormatError::new(artifact, FormatCheck::FileLength, "file length exceeds u64")
275 })?;
276 if header.file_length != actual_file_length {
277 return Err(FormatError::new(
278 artifact,
279 FormatCheck::FileLength,
280 format!(
281 "declared {}, actual {actual_file_length}",
282 header.file_length
283 ),
284 ));
285 }
286 if header.header_length != FILE_HEADER_LEN as u64 {
287 return Err(FormatError::new(
288 artifact,
289 FormatCheck::HeaderLength,
290 format!(
291 "single-block header must be {FILE_HEADER_LEN}, got {}",
292 header.header_length
293 ),
294 ));
295 }
296 let trailer_start = bytes.len().checked_sub(FILE_TRAILER_LEN).ok_or_else(|| {
297 FormatError::new(artifact, FormatCheck::Length, "missing whole-file trailer")
298 })?;
299 let checksummed = bytes.get(..trailer_start).ok_or_else(|| {
300 FormatError::new(artifact, FormatCheck::Length, "invalid trailer position")
301 })?;
302 let expected_file_checksum = read_u64(artifact, bytes, trailer_start)?;
303 let actual_file_checksum = xxh3_64(checksummed);
304 if actual_file_checksum != expected_file_checksum {
305 return Err(FormatError::checksum_mismatch(
306 artifact,
307 FormatCheck::FileChecksum,
308 expected_file_checksum,
309 actual_file_checksum,
310 ));
311 }
312
313 let payload_length = read_u64(artifact, bytes, FILE_HEADER_LEN)?;
314 let payload_length = usize::try_from(payload_length).map_err(|_| {
315 FormatError::new(
316 artifact,
317 FormatCheck::BlockLength,
318 "block length exceeds usize",
319 )
320 })?;
321 let payload_start = FILE_HEADER_LEN.saturating_add(8);
322 let payload_end = payload_start.checked_add(payload_length).ok_or_else(|| {
323 FormatError::new(artifact, FormatCheck::BlockLength, "block end overflow")
324 })?;
325 let checksum_end = payload_end.checked_add(8).ok_or_else(|| {
326 FormatError::new(artifact, FormatCheck::BlockLength, "checksum end overflow")
327 })?;
328 if checksum_end != trailer_start {
329 return Err(FormatError::new(
330 artifact,
331 FormatCheck::BlockLength,
332 format!("framed block ends at {checksum_end}, trailer starts at {trailer_start}"),
333 ));
334 }
335 let payload = bytes.get(payload_start..payload_end).ok_or_else(|| {
336 FormatError::new(artifact, FormatCheck::Length, "block payload is truncated")
337 })?;
338 let expected_block_checksum = read_u64(artifact, bytes, payload_end)?;
339 let actual_block_checksum = xxh3_64(payload);
340 if actual_block_checksum != expected_block_checksum {
341 return Err(FormatError::checksum_mismatch(
342 artifact,
343 FormatCheck::BlockChecksum,
344 expected_block_checksum,
345 actual_block_checksum,
346 ));
347 }
348 Ok(DecodedArtifact { header, payload })
349}
350
351fn registry_error(artifact: &str, error: RegistryError) -> FormatError {
352 FormatError::new(artifact, FormatCheck::Version, error.to_string())
353}
354
355pub(crate) fn read_u16(artifact: &str, bytes: &[u8], offset: usize) -> Result<u16, FormatError> {
356 let raw = bytes
357 .get(offset..)
358 .and_then(|tail| tail.first_chunk::<2>())
359 .ok_or_else(|| {
360 FormatError::new(
361 artifact,
362 FormatCheck::Length,
363 format!("missing u16 at {offset}"),
364 )
365 })?;
366 Ok(u16::from_le_bytes(*raw))
367}
368
369pub(crate) fn read_u32(artifact: &str, bytes: &[u8], offset: usize) -> Result<u32, FormatError> {
370 let raw = bytes
371 .get(offset..)
372 .and_then(|tail| tail.first_chunk::<4>())
373 .ok_or_else(|| {
374 FormatError::new(
375 artifact,
376 FormatCheck::Length,
377 format!("missing u32 at {offset}"),
378 )
379 })?;
380 Ok(u32::from_le_bytes(*raw))
381}
382
383pub(crate) fn read_u64(artifact: &str, bytes: &[u8], offset: usize) -> Result<u64, FormatError> {
384 let raw = bytes
385 .get(offset..)
386 .and_then(|tail| tail.first_chunk::<8>())
387 .ok_or_else(|| {
388 FormatError::new(
389 artifact,
390 FormatCheck::Length,
391 format!("missing u64 at {offset}"),
392 )
393 })?;
394 Ok(u64::from_le_bytes(*raw))
395}