qualia_core_db/container_10d/
header.rs1use bytemuck::{Pod, Zeroable};
26
27use crate::container_10d::axis_role::{AxisRole, PROPOSED_AXIS_ROLES};
28use crate::container_10d::metric_check::{
29 proposed_metric_descriptor, verify_descriptor_against_reality, MetricCompletenessDescriptor,
30};
31
32pub const MAGIC_10D: [u8; 4] = *b"10d\0";
34
35pub const HEADER_VERSION: u16 = 1;
39
40pub const FLAG_DEFAULT_DISPOSITION_REFUSE: u16 = 1 << 0;
44
45pub const HEADER_BYTE_SIZE: usize = 64;
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum HeaderParseError {
52 TooShort { got: usize },
54 BadMagic { got: [u8; 4] },
56 UnknownVersion { got: u16 },
58 NonZeroPadding { field: &'static str },
60 UndefinedAxisRole { axis_index: usize, got: u8 },
62 MetricDivergence(String),
65 BadSectionTablePointer { offset: u32, count: u32 },
70}
71
72impl std::fmt::Display for HeaderParseError {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 Self::TooShort { got } => write!(
76 f,
77 "10d header too short: got {got} bytes, need {HEADER_BYTE_SIZE}"
78 ),
79 Self::BadMagic { got } => write!(f, "10d bad magic: got {got:?}, need {MAGIC_10D:?}"),
80 Self::UnknownVersion { got } => {
81 write!(f, "10d unknown version: got {got}, need {HEADER_VERSION}")
82 }
83 Self::NonZeroPadding { field } => write!(f, "10d non-zero padding in field {field:?}"),
84 Self::UndefinedAxisRole { axis_index, got } => write!(
85 f,
86 "10d undefined axis role at index {axis_index}: raw {got}"
87 ),
88 Self::MetricDivergence(msg) => write!(f, "10d {msg}"),
89 Self::BadSectionTablePointer { offset, count } => write!(
90 f,
91 "10d bad section-table pointer: offset={offset}, count={count}"
92 ),
93 }
94 }
95}
96
97impl std::error::Error for HeaderParseError {}
98
99pub const MAX_SECTION_COUNT: u32 = 1024;
104
105#[repr(C)]
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
127pub struct Container10dHeader {
128 pub magic: [u8; 4],
129 pub version: u16,
130 pub flags: u16,
131 pub axis_roles: [u8; 10],
132 pub pad0: [u8; 2],
133 pub metric_descriptor: MetricCompletenessDescriptor,
134 pub header_crc32c: u32,
135 pub section_table_offset: u32,
139 pub section_count: u32,
143}
144
145impl Default for Container10dHeader {
146 fn default() -> Self {
147 Self::proposed()
148 }
149}
150
151impl Container10dHeader {
152 pub fn proposed() -> Self {
157 let mut axis_roles = [0u8; 10];
158 for (i, role) in PROPOSED_AXIS_ROLES.iter().enumerate() {
159 axis_roles[i] = *role as u8;
160 }
161 Self {
162 magic: MAGIC_10D,
163 version: HEADER_VERSION,
164 flags: FLAG_DEFAULT_DISPOSITION_REFUSE,
165 axis_roles,
166 pad0: [0, 0],
167 metric_descriptor: proposed_metric_descriptor(),
168 header_crc32c: 0,
169 section_table_offset: 0,
170 section_count: 0,
171 }
172 }
173
174 pub fn encode(&self, out: &mut [u8; HEADER_BYTE_SIZE]) {
177 let bytes: &[u8; HEADER_BYTE_SIZE] = bytemuck::cast_ref(self);
180 *out = *bytes;
181 }
182
183 pub fn encode_to_vec64(&self) -> [u8; HEADER_BYTE_SIZE] {
186 let mut out = [0u8; HEADER_BYTE_SIZE];
187 self.encode(&mut out);
188 out
189 }
190
191 pub fn parse(data: &[u8]) -> Result<Container10dHeader, HeaderParseError> {
196 if data.len() < HEADER_BYTE_SIZE {
197 return Err(HeaderParseError::TooShort { got: data.len() });
198 }
199 let mut buf = [0u8; HEADER_BYTE_SIZE];
200 buf.copy_from_slice(&data[..HEADER_BYTE_SIZE]);
201 let header: Container10dHeader = *bytemuck::from_bytes(&buf);
204
205 if header.magic != MAGIC_10D {
206 return Err(HeaderParseError::BadMagic { got: header.magic });
207 }
208 if header.version != HEADER_VERSION {
209 return Err(HeaderParseError::UnknownVersion {
210 got: header.version,
211 });
212 }
213 if header.pad0 != [0, 0] {
214 return Err(HeaderParseError::NonZeroPadding { field: "pad0" });
215 }
216 let (off, cnt) = (header.section_table_offset, header.section_count);
223 let both_zero = off == 0 && cnt == 0;
224 let both_nonzero = off != 0 && cnt != 0;
225 let valid_nonzero = both_nonzero
226 && off as usize >= HEADER_BYTE_SIZE
227 && off as usize <= data.len()
228 && cnt <= MAX_SECTION_COUNT;
229 if !both_zero && !valid_nonzero {
230 return Err(HeaderParseError::BadSectionTablePointer {
231 offset: off,
232 count: cnt,
233 });
234 }
235 for (i, &raw) in header.axis_roles.iter().enumerate() {
236 if AxisRole::from_u8(raw).is_none() || raw == AxisRole::Undefined as u8 {
237 return Err(HeaderParseError::UndefinedAxisRole {
238 axis_index: i,
239 got: raw,
240 });
241 }
242 }
243 verify_descriptor_against_reality(&header.metric_descriptor)
244 .map_err(|d| HeaderParseError::MetricDivergence(d.to_string()))?;
245 Ok(header)
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::container_10d::axis_role::{AxisRole, AXIS_ORDER};
253 use crate::container_10d::metric_check::{MetricKind, BOUNDARY_CLIQUE_BRANCH_INDEX};
254
255 #[test]
256 fn header_is_pod_with_exact_size_and_zero_padding() {
257 assert_eq!(
258 std::mem::size_of::<Container10dHeader>(),
259 HEADER_BYTE_SIZE,
260 "header must be exactly {HEADER_BYTE_SIZE} bytes"
261 );
262 assert_eq!(std::mem::offset_of!(Container10dHeader, magic), 0);
264 assert_eq!(std::mem::offset_of!(Container10dHeader, version), 4);
265 assert_eq!(std::mem::offset_of!(Container10dHeader, flags), 6);
266 assert_eq!(std::mem::offset_of!(Container10dHeader, axis_roles), 8);
267 assert_eq!(std::mem::offset_of!(Container10dHeader, pad0), 18);
268 assert_eq!(
269 std::mem::offset_of!(Container10dHeader, metric_descriptor),
270 20
271 );
272 assert_eq!(std::mem::offset_of!(Container10dHeader, header_crc32c), 52);
273 assert_eq!(
274 std::mem::offset_of!(Container10dHeader, section_table_offset),
275 56
276 );
277 assert_eq!(std::mem::offset_of!(Container10dHeader, section_count), 60);
278 let h = Container10dHeader::proposed();
280 assert_eq!(h.pad0, [0, 0]);
281 assert_eq!(h.section_table_offset, 0);
282 assert_eq!(h.section_count, 0);
283 }
284
285 #[test]
286 fn encode_is_bit_identical_across_two_runs() {
287 let h = Container10dHeader::proposed();
288 let mut a = [0u8; HEADER_BYTE_SIZE];
289 let mut b = [0u8; HEADER_BYTE_SIZE];
290 h.encode(&mut a);
291 h.encode(&mut b);
292 assert_eq!(
293 a, b,
294 "two encodes of the same header must be byte-identical"
295 );
296 }
297
298 #[test]
299 fn round_trip_proposed_header() {
300 let h = Container10dHeader::proposed();
301 let bytes = h.encode_to_vec64();
302 let parsed = Container10dHeader::parse(&bytes).expect("proposed header must parse");
303 assert_eq!(parsed, h);
304 }
305
306 #[test]
307 fn parse_rejects_bad_magic() {
308 let mut bytes = Container10dHeader::proposed().encode_to_vec64();
309 bytes[0] = b'x';
310 let err = Container10dHeader::parse(&bytes).expect_err("bad magic must reject");
311 assert!(matches!(err, HeaderParseError::BadMagic { .. }), "{err}");
312 }
313
314 #[test]
315 fn parse_rejects_unknown_version() {
316 let mut bytes = Container10dHeader::proposed().encode_to_vec64();
317 bytes[4] = 0xff;
319 bytes[5] = 0xff;
320 let err = Container10dHeader::parse(&bytes).expect_err("unknown version must reject");
321 assert!(
322 matches!(err, HeaderParseError::UnknownVersion { got: 0xffff }),
323 "{err}"
324 );
325 }
326
327 #[test]
328 fn parse_rejects_undefined_axis_role() {
329 let mut bytes = Container10dHeader::proposed().encode_to_vec64();
330 bytes[8] = AxisRole::Undefined as u8;
332 let err = Container10dHeader::parse(&bytes).expect_err("undefined axis role must reject");
333 assert!(
334 matches!(
335 err,
336 HeaderParseError::UndefinedAxisRole { axis_index: 0, .. }
337 ),
338 "{err}"
339 );
340 }
341
342 #[test]
343 fn parse_rejects_unknown_axis_role_byte() {
344 let mut bytes = Container10dHeader::proposed().encode_to_vec64();
345 bytes[8 + 3] = 5;
347 let err =
348 Container10dHeader::parse(&bytes).expect_err("unknown axis role byte must reject");
349 assert!(
350 matches!(
351 err,
352 HeaderParseError::UndefinedAxisRole {
353 axis_index: 3,
354 got: 5
355 }
356 ),
357 "{err}"
358 );
359 }
360
361 #[test]
362 fn parse_rejects_non_zero_pad0() {
363 let mut bytes = Container10dHeader::proposed().encode_to_vec64();
364 bytes[18] = 1;
365 let err = Container10dHeader::parse(&bytes).expect_err("non-zero pad0 must reject");
366 assert!(
367 matches!(err, HeaderParseError::NonZeroPadding { field: "pad0" }),
368 "{err}"
369 );
370 }
371
372 #[test]
373 fn parse_rejects_section_table_offset_below_header_size() {
374 let mut bytes = Container10dHeader::proposed().encode_to_vec64();
377 bytes[56..60].copy_from_slice(&8u32.to_le_bytes());
378 bytes[60..64].copy_from_slice(&1u32.to_le_bytes());
379 let err = Container10dHeader::parse(&bytes).expect_err("offset < header size must reject");
380 assert!(
381 matches!(err, HeaderParseError::BadSectionTablePointer { .. }),
382 "{err}"
383 );
384 }
385
386 #[test]
387 fn parse_rejects_section_count_without_offset() {
388 let mut bytes = Container10dHeader::proposed().encode_to_vec64();
390 bytes[60..64].copy_from_slice(&1u32.to_le_bytes());
391 let err = Container10dHeader::parse(&bytes).expect_err("count without offset must reject");
392 assert!(
393 matches!(err, HeaderParseError::BadSectionTablePointer { .. }),
394 "{err}"
395 );
396 }
397
398 #[test]
399 fn parse_rejects_offset_without_count() {
400 let mut bytes = Container10dHeader::proposed().encode_to_vec64();
402 bytes[56..60].copy_from_slice(&64u32.to_le_bytes());
403 let err = Container10dHeader::parse(&bytes).expect_err("offset without count must reject");
404 assert!(
405 matches!(err, HeaderParseError::BadSectionTablePointer { .. }),
406 "{err}"
407 );
408 }
409
410 #[test]
411 fn parse_accepts_bare_header_no_section_table() {
412 let bytes = Container10dHeader::proposed().encode_to_vec64();
414 let parsed = Container10dHeader::parse(&bytes).expect("bare header must parse");
415 assert_eq!(parsed.section_table_offset, 0);
416 assert_eq!(parsed.section_count, 0);
417 }
418
419 #[test]
420 fn parse_rejects_metric_completeness_claiming_v1_folds_t() {
421 let mut bytes = Container10dHeader::proposed().encode_to_vec64();
422 let folded_offset = 28 + 2;
427 bytes[folded_offset] |= 1 << 6;
428 let err =
429 Container10dHeader::parse(&bytes).expect_err("diverging metric descriptor must reject");
430 match err {
431 HeaderParseError::MetricDivergence(msg) => {
432 assert!(msg.contains("v=1"), "message must name v=1: {msg}");
433 assert!(msg.contains("t"), "message must name axis t: {msg}");
434 }
435 other => panic!("expected MetricDivergence, got {other:?}"),
436 }
437 }
438
439 #[test]
440 fn parse_rejects_metric_completeness_claiming_v3_folds_x() {
441 let mut bytes = Container10dHeader::proposed().encode_to_vec64();
442 let folded_offset = 44 + 2;
445 bytes[folded_offset] |= 1 << 3;
446 let err =
447 Container10dHeader::parse(&bytes).expect_err("diverging metric descriptor must reject");
448 assert!(
449 matches!(err, HeaderParseError::MetricDivergence(_)),
450 "{err:?}"
451 );
452 }
453
454 #[test]
455 fn parse_rejects_too_short_input() {
456 let short = [0u8; 32];
457 let err = Container10dHeader::parse(&short).expect_err("short input must reject");
458 assert!(
459 matches!(err, HeaderParseError::TooShort { got: 32 }),
460 "{err}"
461 );
462 }
463
464 #[test]
465 fn proposed_header_carries_option_a_taxonomy() {
466 let h = Container10dHeader::proposed();
467 for (i, &raw) in h.axis_roles.iter().enumerate() {
468 let role = AxisRole::from_u8(raw).expect("proposed roles are all defined");
469 assert_eq!(
470 role as u8, PROPOSED_AXIS_ROLES[i] as u8,
471 "axis {}",
472 AXIS_ORDER[i]
473 );
474 }
475 assert_eq!(h.axis_roles[8], AxisRole::CoordinateCarrier as u8);
477 }
478
479 #[test]
480 fn proposed_header_carries_documented_limitation_descriptor() {
481 let h = Container10dHeader::proposed();
482 let d = &h.metric_descriptor;
483 assert_eq!(d.branches[0].v_class, 0);
485 assert_eq!(d.branches[0].metric_kind, MetricKind::Euclidean as u8);
486 assert_eq!(
487 d.branches[0].folded_axes.count_ones(),
488 7,
489 "v=0 must fold all seven coordinate axes"
490 );
491 assert_eq!(d.branches[1].folded_axes.count_ones(), 3);
493 assert_eq!(d.branches[2].folded_axes.count_ones(), 3);
494 assert_eq!(d.branches[BOUNDARY_CLIQUE_BRANCH_INDEX].folded_axes, 0);
496 assert_eq!(
497 d.branches[BOUNDARY_CLIQUE_BRANCH_INDEX].metric_kind,
498 MetricKind::BoundaryClique as u8
499 );
500 }
501
502 #[test]
503 fn proposed_header_default_disposition_is_refuse() {
504 let h = Container10dHeader::proposed();
505 assert_ne!(
506 h.flags & FLAG_DEFAULT_DISPOSITION_REFUSE,
507 0,
508 "v1 header must fail closed by default"
509 );
510 }
511}