Skip to main content

qualia_core_db/container_10d/
integrity.rs

1//! Whole-file content hash + canonical-encoding integrity gates (P0.3).
2//!
3//! The `.10d` container has two integrity layers:
4//!
5//! 1. **Per-section CRC-32C** (P0.2, in [`super::section`]) — each section
6//!    descriptor carries a CRC-32C over its payload; a flipped payload bit is
7//!    caught at section read.
8//! 2. **Whole-file content hash** (P0.3, here) — a CRC-32C over the entire
9//!    file (header + section table + payloads + padding), stored in the
10//!    header's `header_crc32c` field. This catches header corruption and
11//!    table corruption that the per-section CRC cannot (e.g. a flipped
12//!    `byte_offset` in a descriptor that still points somewhere valid).
13//!
14//! The whole-file hash is computed with `header_crc32c` zeroed during
15//! computation (the standard self-referential CRC technique): on encode, the
16//! field is written zero, the CRC is computed over the full buffer, then the
17//! CRC is written into the field; on verify, the stored value is saved, the
18//! field is zeroed in-place, the CRC is recomputed, and the two are compared.
19//!
20//! **Determinism / canonical bytes.** Because [`super::section::encode_container`]
21//! produces byte-identical output for identical input (canonical section
22//! order, zeroed padding), the whole-file hash is stable across encodes and
23//! changes on any payload-byte change. This is the P0.3 "whole-file hash is
24//! stable across encodes and changes on any payload-byte change, zero-alloc
25//! over the caller buffer" gate.
26
27use crate::container_10d::crc32c::crc32c;
28use crate::container_10d::header::HEADER_BYTE_SIZE;
29
30/// Offset of the `header_crc32c` field within the header (and thus within
31/// the file, since the header is at offset 0).
32const HEADER_CRC32C_OFFSET: usize = 52;
33
34/// Integrity verification error.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum IntegrityError {
37    /// The whole-file CRC-32C does not match the value stored in the header.
38    WholeFileCrcMismatch { expected: u32, got: u32 },
39    /// The input is shorter than the header.
40    TooShort { got: usize },
41}
42
43impl std::fmt::Display for IntegrityError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self {
46            Self::WholeFileCrcMismatch { expected, got } => write!(
47                f,
48                "10d whole-file CRC-32C mismatch: expected {expected:#010x}, got {got:#010x}"
49            ),
50            Self::TooShort { got } => write!(
51                f,
52                "10d input too short for integrity check: got {got}, need {HEADER_BYTE_SIZE}"
53            ),
54        }
55    }
56}
57
58impl std::error::Error for IntegrityError {}
59
60/// Compute the whole-file CRC-32C over `data`, treating the `header_crc32c`
61/// field (bytes 52..56) as zero. Zero-heap: operates directly over the caller
62/// buffer. Does not modify `data`.
63///
64/// This is the value that should be stored in the header's `header_crc32c`
65/// field after encoding.
66pub fn compute_whole_file_crc32c(data: &[u8]) -> u32 {
67    if data.len() < HEADER_BYTE_SIZE {
68        // A sub-header buffer: CRC over whatever is present (with the crc
69        // field region treated as zero if it's not even there).
70        return crc32c(data);
71    }
72    // CRC over [0..52] + [56..end], with [52..56] treated as zero.
73    // Compute incrementally: first the head, then four zero bytes, then the
74    // tail. This avoids allocating a modified copy.
75    let mut crc =
76        crate::container_10d::crc32c::crc32c_update(0xFFFF_FFFF, &data[..HEADER_CRC32C_OFFSET]);
77    // Four zero bytes for the crc field itself.
78    crc = crate::container_10d::crc32c::crc32c_update(crc, &[0u8, 0, 0, 0]);
79    crc = crate::container_10d::crc32c::crc32c_update(crc, &data[HEADER_CRC32C_OFFSET + 4..]);
80    !crc
81}
82
83/// Write the whole-file CRC-32C into the header's `header_crc32c` field
84/// in-place within `data`. Called by the encoder after the full file is
85/// written. Zero-heap.
86pub fn seal_whole_file_crc32c(data: &mut [u8]) {
87    if data.len() < HEADER_BYTE_SIZE {
88        return;
89    }
90    // Ensure the crc field is zero before computing (the encoder already
91    // writes it zero, but be defensive).
92    data[HEADER_CRC32C_OFFSET..HEADER_CRC32C_OFFSET + 4].copy_from_slice(&0u32.to_le_bytes());
93    let crc = compute_whole_file_crc32c(data);
94    data[HEADER_CRC32C_OFFSET..HEADER_CRC32C_OFFSET + 4].copy_from_slice(&crc.to_le_bytes());
95}
96
97/// Verify the whole-file CRC-32C stored in the header against a recomputed
98/// value. Returns `Ok(())` if they match, or an error naming both values.
99/// Zero-heap: saves the stored CRC, zeroes the field in-place, recomputes,
100/// restores the field.
101pub fn verify_whole_file_crc32c(data: &mut [u8]) -> Result<(), IntegrityError> {
102    if data.len() < HEADER_BYTE_SIZE {
103        return Err(IntegrityError::TooShort { got: data.len() });
104    }
105    let stored = u32::from_le_bytes(
106        data[HEADER_CRC32C_OFFSET..HEADER_CRC32C_OFFSET + 4]
107            .try_into()
108            .unwrap(),
109    );
110    // Zero the field, recompute, restore.
111    data[HEADER_CRC32C_OFFSET..HEADER_CRC32C_OFFSET + 4].copy_from_slice(&0u32.to_le_bytes());
112    let actual = compute_whole_file_crc32c(data);
113    data[HEADER_CRC32C_OFFSET..HEADER_CRC32C_OFFSET + 4].copy_from_slice(&stored.to_le_bytes());
114    if actual != stored {
115        return Err(IntegrityError::WholeFileCrcMismatch {
116            expected: stored,
117            got: actual,
118        });
119    }
120    Ok(())
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::container_10d::header::Container10dHeader;
127    use crate::container_10d::section::{
128        encode_container, parse_section_table, AlignmentTier, SectionInput, SectionType,
129    };
130
131    #[test]
132    fn whole_file_crc_is_stable_across_two_identical_encodes() {
133        let h = Container10dHeader::proposed();
134        let mesh_payload = [0xAAu8; 100];
135        let node_payload = [0xBBu8; 40 * 3];
136        let inputs = [
137            SectionInput {
138                section_type: SectionType::QuantizedMesh,
139                alignment_tier: AlignmentTier::Word,
140                stride: 0,
141                element_count: 0,
142                payload: &mesh_payload,
143            },
144            SectionInput {
145                section_type: SectionType::Tensor10DNodes,
146                alignment_tier: AlignmentTier::CacheLine,
147                stride: 40,
148                element_count: 3,
149                payload: &node_payload,
150            },
151        ];
152        let mut out_a = [0u8; 512];
153        let mut out_b = [0u8; 512];
154        let n_a = encode_container(&h, &inputs, &mut out_a).expect("encode a");
155        let n_b = encode_container(&h, &inputs, &mut out_b).expect("encode b");
156        seal_whole_file_crc32c(&mut out_a[..n_a]);
157        seal_whole_file_crc32c(&mut out_b[..n_b]);
158        let crc_a = compute_whole_file_crc32c(&out_a[..n_a]);
159        let crc_b = compute_whole_file_crc32c(&out_b[..n_b]);
160        assert_eq!(
161            crc_a, crc_b,
162            "whole-file CRC must be stable across identical encodes"
163        );
164        // And the sealed bytes are identical.
165        assert_eq!(&out_a[..n_a], &out_b[..n_b]);
166    }
167
168    #[test]
169    fn whole_file_crc_changes_on_payload_bit_flip() {
170        let h = Container10dHeader::proposed();
171        let payload = [0xAAu8; 100];
172        let inputs = [SectionInput {
173            section_type: SectionType::QuantizedMesh,
174            alignment_tier: AlignmentTier::Word,
175            stride: 0,
176            element_count: 0,
177            payload: &payload,
178        }];
179        let mut out = [0u8; 512];
180        let n = encode_container(&h, &inputs, &mut out).expect("encode");
181        seal_whole_file_crc32c(&mut out[..n]);
182        let crc_clean = compute_whole_file_crc32c(&out[..n]);
183        // Flip a payload bit.
184        let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
185        let descs = parse_section_table(&out[..n], &parsed_h).expect("table parse");
186        let p_off = descs[0].byte_offset as usize;
187        out[p_off] ^= 0x01;
188        let crc_flipped = compute_whole_file_crc32c(&out[..n]);
189        assert_ne!(
190            crc_clean, crc_flipped,
191            "whole-file CRC must change on a payload bit flip"
192        );
193    }
194
195    #[test]
196    fn whole_file_crc_changes_on_header_byte_flip() {
197        let h = Container10dHeader::proposed();
198        let payload = [0xAAu8; 100];
199        let inputs = [SectionInput {
200            section_type: SectionType::QuantizedMesh,
201            alignment_tier: AlignmentTier::Word,
202            stride: 0,
203            element_count: 0,
204            payload: &payload,
205        }];
206        let mut out = [0u8; 512];
207        let n = encode_container(&h, &inputs, &mut out).expect("encode");
208        seal_whole_file_crc32c(&mut out[..n]);
209        let crc_clean = compute_whole_file_crc32c(&out[..n]);
210        // Flip a header byte (the flags field at offset 6, avoiding the crc field).
211        out[6] ^= 0x01;
212        let crc_flipped = compute_whole_file_crc32c(&out[..n]);
213        assert_ne!(
214            crc_clean, crc_flipped,
215            "whole-file CRC must change on a header byte flip"
216        );
217    }
218
219    #[test]
220    fn verify_passes_on_clean_file() {
221        let h = Container10dHeader::proposed();
222        let payload = [0xAAu8; 100];
223        let inputs = [SectionInput {
224            section_type: SectionType::QuantizedMesh,
225            alignment_tier: AlignmentTier::Word,
226            stride: 0,
227            element_count: 0,
228            payload: &payload,
229        }];
230        let mut out = [0u8; 512];
231        let n = encode_container(&h, &inputs, &mut out).expect("encode");
232        seal_whole_file_crc32c(&mut out[..n]);
233        verify_whole_file_crc32c(&mut out[..n]).expect("clean file must verify");
234    }
235
236    #[test]
237    fn verify_rejects_flipped_payload_bit() {
238        let h = Container10dHeader::proposed();
239        let payload = [0xAAu8; 100];
240        let inputs = [SectionInput {
241            section_type: SectionType::QuantizedMesh,
242            alignment_tier: AlignmentTier::Word,
243            stride: 0,
244            element_count: 0,
245            payload: &payload,
246        }];
247        let mut out = [0u8; 512];
248        let n = encode_container(&h, &inputs, &mut out).expect("encode");
249        seal_whole_file_crc32c(&mut out[..n]);
250        // Flip a payload bit.
251        let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
252        let descs = parse_section_table(&out[..n], &parsed_h).expect("table parse");
253        let p_off = descs[0].byte_offset as usize;
254        out[p_off] ^= 0x01;
255        let err =
256            verify_whole_file_crc32c(&mut out[..n]).expect_err("flipped bit must fail verify");
257        assert!(
258            matches!(err, IntegrityError::WholeFileCrcMismatch { .. }),
259            "{err}"
260        );
261    }
262
263    #[test]
264    fn verify_restores_the_crc_field_after_check() {
265        // verify_whole_file_crc32c zeroes the field in-place, recomputes, then
266        // restores it. The field must be unchanged after the call (whether
267        // pass or fail).
268        let h = Container10dHeader::proposed();
269        let payload = [0xAAu8; 100];
270        let inputs = [SectionInput {
271            section_type: SectionType::QuantizedMesh,
272            alignment_tier: AlignmentTier::Word,
273            stride: 0,
274            element_count: 0,
275            payload: &payload,
276        }];
277        let mut out = [0u8; 512];
278        let n = encode_container(&h, &inputs, &mut out).expect("encode");
279        seal_whole_file_crc32c(&mut out[..n]);
280        let stored_before: [u8; 4] = out[HEADER_CRC32C_OFFSET..HEADER_CRC32C_OFFSET + 4]
281            .try_into()
282            .unwrap();
283        let _ = verify_whole_file_crc32c(&mut out[..n]);
284        let stored_after: [u8; 4] = out[HEADER_CRC32C_OFFSET..HEADER_CRC32C_OFFSET + 4]
285            .try_into()
286            .unwrap();
287        assert_eq!(
288            stored_before, stored_after,
289            "verify must restore the crc field"
290        );
291    }
292
293    #[test]
294    fn bare_header_seals_and_verifies() {
295        let h = Container10dHeader::proposed();
296        let mut out = [0u8; 128];
297        let n = encode_container(&h, &[], &mut out).expect("bare encode");
298        seal_whole_file_crc32c(&mut out[..n]);
299        verify_whole_file_crc32c(&mut out[..n]).expect("bare header must verify");
300    }
301}