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    encode_artifact_version(family, family.current_version(), flags, payload)
188}
189
190/// Manifest writers select v2 or v3 without changing the graph-free default.
191pub(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/// Encodes the shared fixed-width persisted-file header without a body or trailer.
220#[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
232/// Parses only the fixed 32-byte header and validates its registry declaration.
233pub 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
286/// Validates and decodes one complete single-block artifact.
287pub 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}