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}
88
89impl FormatError {
90    /// Creates an artifact-scoped validation error.
91    #[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    /// Creates an exact wrong-family refusal without relying on display text.
102    #[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    /// Creates an exact checksum refusal without relying on display text.
113    #[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    /// Returns the caller-supplied artifact name.
129    #[must_use]
130    pub fn artifact(&self) -> &str {
131        &self.artifact
132    }
133
134    /// Returns the exact failed validation check.
135    #[must_use]
136    pub const fn check(&self) -> FormatCheck {
137        self.check
138    }
139
140    /// Returns stable typed value-bearing facts for this validation failure.
141    #[must_use]
142    pub const fn values(&self) -> &FormatValues {
143        &self.values
144    }
145
146    /// Returns the value-bearing failure detail.
147    #[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/// One validated framed artifact borrowing its original payload bytes.
166#[derive(Clone, Copy, Debug, Eq, PartialEq)]
167pub struct DecodedArtifact<'a> {
168    /// Validated fixed-width file header.
169    pub header: FileHeader,
170    /// Validated single-block payload.
171    pub payload: &'a [u8],
172}
173
174/// Encodes one complete single-block artifact with xxh3-64 block and file checksums.
175#[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/// Encodes the shared fixed-width persisted-file header without a body or trailer.
200#[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
212/// Parses only the fixed 32-byte header and validates its registry declaration.
213pub 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
266/// Validates and decodes one complete single-block artifact.
267pub 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}