qualia_core_db/container_10d/
integrity.rs1use crate::container_10d::crc32c::crc32c;
28use crate::container_10d::header::HEADER_BYTE_SIZE;
29
30const HEADER_CRC32C_OFFSET: usize = 52;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum IntegrityError {
37 WholeFileCrcMismatch { expected: u32, got: u32 },
39 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
60pub fn compute_whole_file_crc32c(data: &[u8]) -> u32 {
67 if data.len() < HEADER_BYTE_SIZE {
68 return crc32c(data);
71 }
72 let mut crc =
76 crate::container_10d::crc32c::crc32c_update(0xFFFF_FFFF, &data[..HEADER_CRC32C_OFFSET]);
77 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
83pub fn seal_whole_file_crc32c(data: &mut [u8]) {
87 if data.len() < HEADER_BYTE_SIZE {
88 return;
89 }
90 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
97pub 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 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 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 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 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 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 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}