Skip to main content

zeppelin_embed/format/
frame.rs

1//! Length-prefixed xxh3-64 block framing.
2
3use xxhash_rust::xxh3::xxh3_64;
4
5use super::{FormatFamily, FormatRegistry, RegistryError};
6
7/// Fixed header length shared by every persisted file.
8pub const FILE_HEADER_LEN: usize = 32;
9/// Whole-file xxh3-64 trailer width.
10pub const FILE_TRAILER_LEN: usize = 8;
11/// Permanent eight-byte file magic.
12pub const FILE_MAGIC: [u8; 8] = *b"ZEPEMBED";
13
14/// Decoded fixed-width file header.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16#[repr(C)]
17pub struct FileHeader {
18    /// Permanent eight-byte file magic.
19    pub magic: [u8; 8],
20    /// Registered artifact family.
21    pub family: u16,
22    /// Additive family version.
23    pub version: u16,
24    /// Family-owned flags.
25    pub flags: u32,
26    /// Bytes occupied by the complete artifact header.
27    pub header_length: u64,
28    /// Exact file length including the whole-file trailer.
29    pub file_length: u64,
30}
31
32const _: [(); FILE_HEADER_LEN] = [(); std::mem::size_of::<FileHeader>()];
33
34/// The exact validation check that rejected an artifact.
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum FormatCheck {
37    /// A required byte range was truncated.
38    Length,
39    /// The permanent magic did not match.
40    Magic,
41    /// The artifact belonged to another registered family.
42    Family,
43    /// The format registry rejected the declared version.
44    Version,
45    /// The declared header length was invalid.
46    HeaderLength,
47    /// The declared file length did not match the bytes supplied.
48    FileLength,
49    /// A block length was invalid or did not consume the framed body exactly.
50    BlockLength,
51    /// A framed block's xxh3-64 did not match.
52    BlockChecksum,
53    /// The whole-file xxh3-64 trailer did not match.
54    FileChecksum,
55    /// The immutable object identity did not match the caller's expectation.
56    ObjectIdentity,
57}
58
59/// Typed value-bearing facts carried by persisted-format failures.
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub enum FormatValues {
62    /// The check has no stable pair of numeric values.
63    None,
64    /// Expected and observed registered artifact families.
65    Family {
66        /// Required family id.
67        expected: u16,
68        /// Persisted family id.
69        actual: u16,
70    },
71    /// Expected and computed checksum words.
72    Checksum {
73        /// Persisted checksum word.
74        expected: u64,
75        /// Independently computed checksum word.
76        actual: u64,
77    },
78}
79
80/// Typed, artifact-naming persisted-format validation failure.
81#[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    /// Creates an artifact-scoped validation error.
92    #[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    /// Creates an exact wrong-family refusal without relying on display text.
104    #[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    /// Creates an exact checksum refusal without relying on display text.
116    #[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    /// Returns the caller-supplied artifact name.
133    #[must_use]
134    pub fn artifact(&self) -> &str {
135        &self.artifact
136    }
137
138    /// Returns the exact failed validation check.
139    #[must_use]
140    pub const fn check(&self) -> FormatCheck {
141        self.check
142    }
143
144    /// Returns stable typed value-bearing facts for this validation failure.
145    #[must_use]
146    pub const fn values(&self) -> &FormatValues {
147        &self.values
148    }
149
150    /// Returns (found, minimum, maximum) for an unsupported format version.
151    #[must_use]
152    pub const fn version_range(&self) -> Option<(u16, u16, u16)> {
153        self.version_range
154    }
155
156    /// Returns the value-bearing failure detail.
157    #[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/// One validated framed artifact borrowing its original payload bytes.
176#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177pub struct DecodedArtifact<'a> {
178    /// Validated fixed-width file header.
179    pub header: FileHeader,
180    /// Validated single-block payload.
181    pub payload: &'a [u8],
182}
183
184/// Encodes one complete single-block artifact with xxh3-64 block and file checksums.
185#[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/// Encodes the shared fixed-width persisted-file header without a body or trailer.
210#[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
222/// Parses only the fixed 32-byte header and validates its registry declaration.
223pub 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
276/// Validates and decodes one complete single-block artifact.
277pub 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}