1use bytemuck::{Pod, Zeroable};
36
37use crate::container_10d::crc32c::crc32c;
38use crate::container_10d::header::{Container10dHeader, HEADER_BYTE_SIZE, MAX_SECTION_COUNT};
39
40pub const SECTION_DESCRIPTOR_SIZE: usize = 24;
42
43const MAX_SECTIONS_ENCODE: usize = MAX_SECTION_COUNT as usize;
46
47#[repr(u8)]
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum SectionType {
54 Undefined = 0,
56 QuantizedMesh = 1,
60 Tensor10DNodes = 2,
62 Reconstruction = 3,
64 SpecReservedGovernance = 4,
66 SpecReservedTemporalIndex = 5,
67 SpecReservedManifoldHeadTable = 6,
68 ProvenanceSidecar = 7,
72 SpecReservedFieldSidecar = 8,
73 SpecReservedCorrespondenceMap = 9,
74 Topology = 10,
79 SpatialIndex = 11,
82}
83
84impl SectionType {
85 #[inline]
86 pub const fn from_u8(raw: u8) -> Option<SectionType> {
87 match raw {
88 0 => Some(SectionType::Undefined),
89 1 => Some(SectionType::QuantizedMesh),
90 2 => Some(SectionType::Tensor10DNodes),
91 3 => Some(SectionType::Reconstruction),
92 4 => Some(SectionType::SpecReservedGovernance),
93 5 => Some(SectionType::SpecReservedTemporalIndex),
94 6 => Some(SectionType::SpecReservedManifoldHeadTable),
95 7 => Some(SectionType::ProvenanceSidecar),
96 8 => Some(SectionType::SpecReservedFieldSidecar),
97 9 => Some(SectionType::SpecReservedCorrespondenceMap),
98 10 => Some(SectionType::Topology),
99 11 => Some(SectionType::SpatialIndex),
100 _ => None,
101 }
102 }
103
104 #[inline]
107 pub const fn is_implemented(self) -> bool {
108 matches!(
109 self,
110 SectionType::QuantizedMesh
111 | SectionType::Tensor10DNodes
112 | SectionType::Reconstruction
113 | SectionType::ProvenanceSidecar
114 | SectionType::Topology
115 | SectionType::SpatialIndex
116 )
117 }
118}
119
120#[repr(u8)]
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum AlignmentTier {
125 Byte = 0,
127 Word = 1,
129 CacheLine = 2,
131 Page = 3,
133}
134
135impl AlignmentTier {
136 #[inline]
138 pub const fn to_bytes(self) -> usize {
139 match self {
140 AlignmentTier::Byte => 1,
141 AlignmentTier::Word => 4,
142 AlignmentTier::CacheLine => 16,
143 AlignmentTier::Page => 64,
144 }
145 }
146
147 #[inline]
148 pub const fn from_u8(raw: u8) -> Option<AlignmentTier> {
149 match raw {
150 0 => Some(AlignmentTier::Byte),
151 1 => Some(AlignmentTier::Word),
152 2 => Some(AlignmentTier::CacheLine),
153 3 => Some(AlignmentTier::Page),
154 _ => None,
155 }
156 }
157}
158
159#[inline]
161const fn align_up(offset: usize, align: usize) -> usize {
162 if align <= 1 {
163 return offset;
164 }
165 (offset + align - 1) & !(align - 1)
166}
167
168#[repr(C)]
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
184pub struct SectionDescriptor {
185 pub section_type: u8,
186 pub alignment_tier: u8,
187 pub reserved16: u16,
188 pub byte_offset: u32,
189 pub byte_length: u32,
190 pub stride: u32,
191 pub element_count: u32,
192 pub crc32c: u32,
193}
194
195impl SectionDescriptor {
196 #[inline]
199 pub fn tier(&self) -> Option<AlignmentTier> {
200 AlignmentTier::from_u8(self.alignment_tier)
201 }
202
203 #[inline]
205 pub fn typ(&self) -> Option<SectionType> {
206 SectionType::from_u8(self.section_type)
207 }
208}
209
210#[derive(Debug, Clone, Copy)]
215pub struct SectionInput<'a> {
216 pub section_type: SectionType,
217 pub alignment_tier: AlignmentTier,
218 pub stride: u32,
222 pub element_count: u32,
224 pub payload: &'a [u8],
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
229pub enum SectionTableError {
230 TooManySections { count: usize },
232 DuplicateSectionType { section_type: u8 },
235 UnsupportedSectionType { got: u8 },
238 UnsupportedAlignmentTier { got: u8 },
240 StrideInconsistent {
242 section_type: u8,
243 stride: u32,
244 element_count: u32,
245 payload_len: usize,
246 },
247 OutputBufferTooSmall { needed: usize, have: usize },
249 InputTooShort { got: usize, need: usize },
251 BadSectionTablePointer { offset: u32, count: u32 },
254 NonZeroDescriptorReserved { index: usize },
256 MisalignedSection {
258 index: usize,
259 offset: u32,
260 tier: AlignmentTier,
261 },
262 OutOfBounds {
264 index: usize,
265 offset: u32,
266 length: u32,
267 file_len: usize,
268 },
269 OverlappingSections { index_a: usize, index_b: usize },
271 StrideInconsistentDescriptor {
273 index: usize,
274 stride: u32,
275 element_count: u32,
276 byte_length: u32,
277 },
278 UndefinedSectionType { index: usize, got: u8 },
280 UndefinedAlignmentTier { index: usize, got: u8 },
282 CrcMismatch {
284 index: usize,
285 section_type: u8,
286 expected: u32,
287 got: u32,
288 },
289 NonZeroPadding { at: usize },
291}
292
293impl std::fmt::Display for SectionTableError {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 match self {
296 Self::TooManySections { count } => write!(f, "10d too many sections: {count} > {MAX_SECTION_COUNT}"),
297 Self::DuplicateSectionType { section_type } => write!(f, "10d duplicate section type {section_type} (v1 requires unique types)"),
298 Self::UnsupportedSectionType { got } => write!(f, "10d unsupported section type byte {got}"),
299 Self::UnsupportedAlignmentTier { got } => write!(f, "10d unsupported alignment tier byte {got}"),
300 Self::StrideInconsistent { section_type, stride, element_count, payload_len } => write!(f, "10d stride inconsistent for type {section_type}: {stride} * {element_count} != {payload_len}"),
301 Self::OutputBufferTooSmall { needed, have } => write!(f, "10d output buffer too small: need {needed}, have {have}"),
302 Self::InputTooShort { got, need } => write!(f, "10d input too short: got {got}, need {need}"),
303 Self::BadSectionTablePointer { offset, count } => write!(f, "10d bad section-table pointer: offset={offset}, count={count}"),
304 Self::NonZeroDescriptorReserved { index } => write!(f, "10d non-zero reserved16 in descriptor {index}"),
305 Self::MisalignedSection { index, offset, tier } => write!(f, "10d section {index} offset {offset} misaligned for tier {tier:?}"),
306 Self::OutOfBounds { index, offset, length, file_len } => write!(f, "10d section {index} out of bounds: offset={offset} length={length} file_len={file_len}"),
307 Self::OverlappingSections { index_a, index_b } => write!(f, "10d sections {index_a} and {index_b} overlap"),
308 Self::StrideInconsistentDescriptor { index, stride, element_count, byte_length } => write!(f, "10d descriptor {index} stride inconsistent: {stride} * {element_count} != {byte_length}"),
309 Self::UndefinedSectionType { index, got } => write!(f, "10d descriptor {index} undefined section type {got}"),
310 Self::UndefinedAlignmentTier { index, got } => write!(f, "10d descriptor {index} undefined alignment tier {got}"),
311 Self::CrcMismatch { index, section_type, expected, got } => write!(f, "10d CRC mismatch in section {index} (type {section_type}): expected {expected:#010x}, got {got:#010x}"),
312 Self::NonZeroPadding { at } => write!(f, "10d non-zero padding at byte {at}"),
313 }
314 }
315}
316
317impl std::error::Error for SectionTableError {}
318
319fn sort_indices_stack(inputs: &[SectionInput<'_>]) -> [usize; MAX_SECTIONS_ENCODE] {
327 let mut idx = [0usize; MAX_SECTIONS_ENCODE];
328 for i in 0..inputs.len() {
329 idx[i] = i;
330 }
331 for i in 1..inputs.len() {
334 let mut j = i;
335 while j > 0 && inputs[idx[j - 1]].section_type as u8 > inputs[idx[j]].section_type as u8 {
336 idx.swap(j - 1, j);
337 j -= 1;
338 }
339 }
340 idx
341}
342
343fn plan_layout(
348 inputs: &[SectionInput<'_>],
349 order: &mut [usize; MAX_SECTIONS_ENCODE],
350 descs: &mut [SectionDescriptor; MAX_SECTIONS_ENCODE],
351) -> Result<usize, SectionTableError> {
352 if inputs.len() > MAX_SECTIONS_ENCODE {
353 return Err(SectionTableError::TooManySections {
354 count: inputs.len(),
355 });
356 }
357 *order = sort_indices_stack(inputs);
358
359 for k in 0..inputs.len() {
361 let i = order[k];
362 let st = inputs[i].section_type as u8;
363 if k > 0 && inputs[order[k - 1]].section_type as u8 == st {
364 return Err(SectionTableError::DuplicateSectionType { section_type: st });
365 }
366 if !inputs[i].section_type.is_implemented() {
367 return Err(SectionTableError::UnsupportedSectionType { got: st });
368 }
369 if inputs[i].stride > 0 {
371 let expected = inputs[i].stride as usize * inputs[i].element_count as usize;
372 if expected != inputs[i].payload.len() {
373 return Err(SectionTableError::StrideInconsistent {
374 section_type: st,
375 stride: inputs[i].stride,
376 element_count: inputs[i].element_count,
377 payload_len: inputs[i].payload.len(),
378 });
379 }
380 }
381 }
382
383 let table_off = HEADER_BYTE_SIZE;
385 let table_len = inputs.len() * SECTION_DESCRIPTOR_SIZE;
386 let mut cursor = table_off + table_len;
387
388 for k in 0..inputs.len() {
389 let i = order[k];
390 let align = inputs[i].alignment_tier.to_bytes();
391 cursor = align_up(cursor, align);
392 let off = cursor;
393 let len = inputs[i].payload.len();
394 let crc = crc32c(inputs[i].payload);
395 descs[k] = SectionDescriptor {
396 section_type: inputs[i].section_type as u8,
397 alignment_tier: inputs[i].alignment_tier as u8,
398 reserved16: 0,
399 byte_offset: off as u32,
400 byte_length: len as u32,
401 stride: inputs[i].stride,
402 element_count: inputs[i].element_count,
403 crc32c: crc,
404 };
405 cursor = off + len;
406 }
407 Ok(cursor)
408}
409
410pub fn encode_container(
418 header: &Container10dHeader,
419 inputs: &[SectionInput<'_>],
420 out: &mut [u8],
421) -> Result<usize, SectionTableError> {
422 let mut order = [0usize; MAX_SECTIONS_ENCODE];
423 let mut descs = [ZEROED_SECTION_DESCRIPTOR; MAX_SECTIONS_ENCODE];
424 let total = plan_layout(inputs, &mut order, &mut descs)?;
425 if out.len() < total {
426 return Err(SectionTableError::OutputBufferTooSmall {
427 needed: total,
428 have: out.len(),
429 });
430 }
431
432 for b in out[..total].iter_mut() {
434 *b = 0;
435 }
436
437 let mut h = *header;
439 if inputs.is_empty() {
440 h.section_table_offset = 0;
441 h.section_count = 0;
442 } else {
443 h.section_table_offset = HEADER_BYTE_SIZE as u32;
444 h.section_count = inputs.len() as u32;
445 }
446 let mut header_buf = [0u8; HEADER_BYTE_SIZE];
447 h.encode(&mut header_buf);
448 out[..HEADER_BYTE_SIZE].copy_from_slice(&header_buf);
449
450 if inputs.is_empty() {
451 return Ok(HEADER_BYTE_SIZE);
452 }
453
454 let table_off = HEADER_BYTE_SIZE;
456 for k in 0..inputs.len() {
457 let desc_bytes: &[u8; SECTION_DESCRIPTOR_SIZE] = bytemuck::cast_ref(&descs[k]);
458 let dst = table_off + k * SECTION_DESCRIPTOR_SIZE;
459 out[dst..dst + SECTION_DESCRIPTOR_SIZE].copy_from_slice(desc_bytes);
460 }
461
462 for k in 0..inputs.len() {
464 let i = order[k];
465 let off = descs[k].byte_offset as usize;
466 let len = descs[k].byte_length as usize;
467 out[off..off + len].copy_from_slice(inputs[i].payload);
468 }
469
470 Ok(total)
471}
472
473const ZEROED_SECTION_DESCRIPTOR: SectionDescriptor = SectionDescriptor {
476 section_type: 0,
477 alignment_tier: 0,
478 reserved16: 0,
479 byte_offset: 0,
480 byte_length: 0,
481 stride: 0,
482 element_count: 0,
483 crc32c: 0,
484};
485
486pub fn parse_section_table<'a>(
493 data: &'a [u8],
494 header: &Container10dHeader,
495) -> Result<&'a [SectionDescriptor], SectionTableError> {
496 let (off, cnt) = (header.section_table_offset, header.section_count);
497 if off == 0 && cnt == 0 {
498 return Ok(&[]);
499 }
500 let both_zero = off == 0 && cnt == 0;
503 let both_nonzero = off != 0 && cnt != 0;
504 let valid_nonzero = both_nonzero
505 && off as usize >= HEADER_BYTE_SIZE
506 && off as usize <= data.len()
507 && cnt <= MAX_SECTION_COUNT;
508 if !both_zero && !valid_nonzero {
509 return Err(SectionTableError::BadSectionTablePointer {
510 offset: off,
511 count: cnt,
512 });
513 }
514 let table_start = off as usize;
515 let table_bytes = cnt as usize * SECTION_DESCRIPTOR_SIZE;
516 let table_end =
517 table_start
518 .checked_add(table_bytes)
519 .ok_or(SectionTableError::BadSectionTablePointer {
520 offset: off,
521 count: cnt,
522 })?;
523 if table_end > data.len() {
524 return Err(SectionTableError::InputTooShort {
525 got: data.len(),
526 need: table_end,
527 });
528 }
529 let descs: &[SectionDescriptor] = bytemuck::cast_slice(&data[table_start..table_end]);
532
533 for (i, d) in descs.iter().enumerate() {
538 if d.reserved16 != 0 {
539 return Err(SectionTableError::NonZeroDescriptorReserved { index: i });
540 }
541 let st = SectionType::from_u8(d.section_type).ok_or(
542 SectionTableError::UndefinedSectionType {
543 index: i,
544 got: d.section_type,
545 },
546 )?;
547 if st == SectionType::Undefined {
548 return Err(SectionTableError::UndefinedSectionType {
549 index: i,
550 got: d.section_type,
551 });
552 }
553 let tier = AlignmentTier::from_u8(d.alignment_tier).ok_or(
554 SectionTableError::UndefinedAlignmentTier {
555 index: i,
556 got: d.alignment_tier,
557 },
558 )?;
559 let align = tier.to_bytes();
560 let o = d.byte_offset as usize;
561 let l = d.byte_length as usize;
562 if o % align != 0 {
563 return Err(SectionTableError::MisalignedSection {
564 index: i,
565 offset: d.byte_offset,
566 tier,
567 });
568 }
569 let end = o.checked_add(l).ok_or(SectionTableError::OutOfBounds {
570 index: i,
571 offset: d.byte_offset,
572 length: d.byte_length,
573 file_len: data.len(),
574 })?;
575 if end > data.len() {
576 return Err(SectionTableError::OutOfBounds {
577 index: i,
578 offset: d.byte_offset,
579 length: d.byte_length,
580 file_len: data.len(),
581 });
582 }
583 if d.stride > 0 {
585 let expected = d.stride as usize * d.element_count as usize;
586 if expected != l {
587 return Err(SectionTableError::StrideInconsistentDescriptor {
588 index: i,
589 stride: d.stride,
590 element_count: d.element_count,
591 byte_length: d.byte_length,
592 });
593 }
594 }
595 let stored = d.crc32c;
597 let actual = crc32c(&data[o..end]);
598 if actual != stored {
599 return Err(SectionTableError::CrcMismatch {
600 index: i,
601 section_type: d.section_type,
602 expected: stored,
603 got: actual,
604 });
605 }
606 }
607
608 for a in 0..descs.len() {
615 for b in (a + 1)..descs.len() {
616 let (ao, al) = (descs[a].byte_offset as usize, descs[a].byte_length as usize);
617 let (bo, bl) = (descs[b].byte_offset as usize, descs[b].byte_length as usize);
618 let a_end = ao + al;
619 let b_end = bo + bl;
620 if ao < b_end && bo < a_end {
621 if al == 0 || bl == 0 {
624 continue;
625 }
626 return Err(SectionTableError::OverlappingSections {
627 index_a: a,
628 index_b: b,
629 });
630 }
631 }
632 }
633
634 let mut order_by_off: [usize; MAX_SECTION_COUNT as usize] =
638 [0usize; MAX_SECTION_COUNT as usize];
639 for i in 0..descs.len() {
640 order_by_off[i] = i;
641 }
642 for i in 1..descs.len() {
644 let mut j = i;
645 while j > 0 && descs[order_by_off[j - 1]].byte_offset > descs[order_by_off[j]].byte_offset {
646 order_by_off.swap(j - 1, j);
647 j -= 1;
648 }
649 }
650 let mut gap_start = table_end;
651 for k in 0..descs.len() {
652 let idx = order_by_off[k];
653 let o = descs[idx].byte_offset as usize;
654 for b in &data[gap_start..o] {
655 if *b != 0 {
656 return Err(SectionTableError::NonZeroPadding { at: gap_start });
657 }
658 }
659 gap_start = o + descs[idx].byte_length as usize;
660 }
661 Ok(descs)
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673 use crate::container_10d::header::Container10dHeader;
674
675 fn mesh_input(payload: &[u8]) -> SectionInput<'_> {
676 SectionInput {
677 section_type: SectionType::QuantizedMesh,
678 alignment_tier: AlignmentTier::Word,
679 stride: 0,
680 element_count: 0,
681 payload,
682 }
683 }
684
685 fn node_input(payload: &[u8]) -> SectionInput<'_> {
686 SectionInput {
688 section_type: SectionType::Tensor10DNodes,
689 alignment_tier: AlignmentTier::CacheLine,
690 stride: 40,
691 element_count: (payload.len() / 40) as u32,
692 payload,
693 }
694 }
695
696 #[test]
697 fn descriptor_is_pod_with_exact_size() {
698 assert_eq!(
699 std::mem::size_of::<SectionDescriptor>(),
700 SECTION_DESCRIPTOR_SIZE
701 );
702 assert_eq!(std::mem::offset_of!(SectionDescriptor, section_type), 0);
703 assert_eq!(std::mem::offset_of!(SectionDescriptor, alignment_tier), 1);
704 assert_eq!(std::mem::offset_of!(SectionDescriptor, reserved16), 2);
705 assert_eq!(std::mem::offset_of!(SectionDescriptor, byte_offset), 4);
706 assert_eq!(std::mem::offset_of!(SectionDescriptor, byte_length), 8);
707 assert_eq!(std::mem::offset_of!(SectionDescriptor, stride), 12);
708 assert_eq!(std::mem::offset_of!(SectionDescriptor, element_count), 16);
709 assert_eq!(std::mem::offset_of!(SectionDescriptor, crc32c), 20);
710 }
711
712 #[test]
713 fn bare_header_encodes_and_parses_with_no_sections() {
714 let h = Container10dHeader::proposed();
715 let mut out = [0u8; 128];
716 let n = encode_container(&h, &[], &mut out).expect("bare encode");
717 assert_eq!(n, HEADER_BYTE_SIZE);
718 let parsed_h = Container10dHeader::parse(&out[..n]).expect("bare parse");
719 assert_eq!(parsed_h, h);
720 let table = parse_section_table(&out[..n], &parsed_h).expect("bare table parse");
721 assert!(table.is_empty());
722 }
723
724 #[test]
725 fn round_trip_two_sections_descriptors_match() {
726 let h = Container10dHeader::proposed();
727 let mesh_payload = [0xAAu8; 100];
728 let node_payload = [0xBBu8; 40 * 3]; let inputs = [mesh_input(&mesh_payload), node_input(&node_payload)];
730 let mut out = [0u8; 512];
731 let n = encode_container(&h, &inputs, &mut out).expect("encode");
732 let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
733 let descs = parse_section_table(&out[..n], &parsed_h).expect("table parse");
734 assert_eq!(descs.len(), 2);
735 assert_eq!(descs[0].section_type, SectionType::QuantizedMesh as u8);
737 assert_eq!(descs[1].section_type, SectionType::Tensor10DNodes as u8);
738 assert_eq!(
740 &out[descs[0].byte_offset as usize..][..mesh_payload.len()],
741 &mesh_payload
742 );
743 assert_eq!(
744 &out[descs[1].byte_offset as usize..][..node_payload.len()],
745 &node_payload
746 );
747 assert_eq!(descs[1].stride, 40);
749 assert_eq!(descs[1].element_count, 3);
750 }
751
752 #[test]
753 fn every_section_start_meets_its_declared_tier() {
754 let h = Container10dHeader::proposed();
755 let mesh_payload = [0u8; 7]; let node_payload = [0u8; 40];
757 let inputs = [mesh_input(&mesh_payload), node_input(&node_payload)];
758 let mut out = [0u8; 512];
759 let n = encode_container(&h, &inputs, &mut out).expect("encode");
760 let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
761 let descs = parse_section_table(&out[..n], &parsed_h).expect("table parse");
762 for d in descs {
763 let tier = AlignmentTier::from_u8(d.alignment_tier).unwrap();
764 assert_eq!(
765 (d.byte_offset as usize) % tier.to_bytes(),
766 0,
767 "section type {} at offset {} must meet tier {:?}",
768 d.section_type,
769 d.byte_offset,
770 tier
771 );
772 }
773 }
774
775 #[test]
776 fn padding_between_sections_is_zero() {
777 let h = Container10dHeader::proposed();
778 let mesh_payload = [0u8; 7]; let node_payload = [0u8; 40];
780 let inputs = [mesh_input(&mesh_payload), node_input(&node_payload)];
781 let mut out = [0u8; 512];
782 let n = encode_container(&h, &inputs, &mut out).expect("encode");
783 let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
784 let descs = parse_section_table(&out[..n], &parsed_h).expect("table parse");
785 let mesh_end = descs[0].byte_offset as usize + descs[0].byte_length as usize;
787 let node_start = descs[1].byte_offset as usize;
788 assert!(
789 node_start > mesh_end,
790 "there must be padding between the odd-length mesh and the 16-aligned node"
791 );
792 for b in &out[mesh_end..node_start] {
793 assert_eq!(*b, 0, "padding between sections must be zero");
794 }
795 }
796
797 #[test]
798 fn permuted_section_order_produces_byte_identical_output() {
799 let h = Container10dHeader::proposed();
800 let mesh_payload = [0xAAu8; 100];
801 let node_payload = [0xBBu8; 40 * 3];
802 let inputs_a = [mesh_input(&mesh_payload), node_input(&node_payload)];
803 let inputs_b = [node_input(&node_payload), mesh_input(&mesh_payload)]; let mut out_a = [0u8; 512];
805 let mut out_b = [0u8; 512];
806 let n_a = encode_container(&h, &inputs_a, &mut out_a).expect("encode a");
807 let n_b = encode_container(&h, &inputs_b, &mut out_b).expect("encode b");
808 assert_eq!(n_a, n_b);
809 assert_eq!(
810 &out_a[..n_a],
811 &out_b[..n_b],
812 "permuted input must produce byte-identical output"
813 );
814 }
815
816 #[test]
817 fn duplicate_section_type_is_rejected() {
818 let h = Container10dHeader::proposed();
819 let p1 = [0u8; 16];
820 let p2 = [0u8; 32];
821 let inputs = [mesh_input(&p1), mesh_input(&p2)];
822 let mut out = [0u8; 512];
823 let err = encode_container(&h, &inputs, &mut out).expect_err("duplicate type must reject");
824 assert!(
825 matches!(
826 err,
827 SectionTableError::DuplicateSectionType { section_type: 1 }
828 ),
829 "{err}"
830 );
831 }
832
833 #[test]
834 fn stride_inconsistent_input_is_rejected() {
835 let h = Container10dHeader::proposed();
836 let bad = SectionInput {
838 section_type: SectionType::Tensor10DNodes,
839 alignment_tier: AlignmentTier::CacheLine,
840 stride: 40,
841 element_count: 3,
842 payload: &[0u8; 100],
843 };
844 let mut out = [0u8; 512];
845 let err = encode_container(&h, std::slice::from_ref(&bad), &mut out)
846 .expect_err("stride inconsistent must reject");
847 assert!(
848 matches!(err, SectionTableError::StrideInconsistent { .. }),
849 "{err}"
850 );
851 }
852
853 #[test]
854 fn output_buffer_too_small_is_rejected() {
855 let h = Container10dHeader::proposed();
856 let payload = [0u8; 100];
857 let inputs = [mesh_input(&payload)];
858 let mut out = [0u8; 80]; let err = encode_container(&h, &inputs, &mut out).expect_err("small buffer must reject");
860 assert!(
861 matches!(err, SectionTableError::OutputBufferTooSmall { .. }),
862 "{err}"
863 );
864 }
865
866 #[test]
867 fn flipped_payload_bit_is_caught_by_crc() {
868 let h = Container10dHeader::proposed();
869 let payload = [0xAAu8; 100];
870 let inputs = [mesh_input(&payload)];
871 let mut out = [0u8; 512];
872 let n = encode_container(&h, &inputs, &mut out).expect("encode");
873 let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
874 let descs_ok = parse_section_table(&out[..n], &parsed_h).expect("clean table parses");
876 let p_off = descs_ok[0].byte_offset as usize;
877 out[p_off] ^= 0x01;
878 let err =
879 parse_section_table(&out[..n], &parsed_h).expect_err("flipped bit must be caught");
880 assert!(
881 matches!(err, SectionTableError::CrcMismatch { .. }),
882 "{err}"
883 );
884 }
885
886 #[test]
887 fn flipped_descriptor_byte_is_caught() {
888 let h = Container10dHeader::proposed();
889 let payload = [0xAAu8; 100];
890 let inputs = [mesh_input(&payload)];
891 let mut out = [0u8; 512];
892 let n = encode_container(&h, &inputs, &mut out).expect("encode");
893 let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
894 let table_start = parsed_h.section_table_offset as usize;
896 out[table_start + 2] = 0xFF;
897 let err =
898 parse_section_table(&out[..n], &parsed_h).expect_err("non-zero reserved16 must reject");
899 assert!(
900 matches!(
901 err,
902 SectionTableError::NonZeroDescriptorReserved { index: 0 }
903 ),
904 "{err}"
905 );
906 }
907
908 #[test]
909 fn misaligned_section_offset_is_rejected() {
910 let h = Container10dHeader::proposed();
911 let payload = [0xAAu8; 100];
912 let inputs = [mesh_input(&payload)];
913 let mut out = [0u8; 512];
914 let n = encode_container(&h, &inputs, &mut out).expect("encode");
915 let mut parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
916 let table_start = parsed_h.section_table_offset as usize;
919 let off_field = table_start + 4;
920 out[off_field..off_field + 4].copy_from_slice(&65u32.to_le_bytes());
922 parsed_h = Container10dHeader::parse(&out[..n]).expect("header still parses");
924 let err =
925 parse_section_table(&out[..n], &parsed_h).expect_err("misaligned offset must reject");
926 assert!(
927 matches!(err, SectionTableError::MisalignedSection { .. }),
928 "{err}"
929 );
930 }
931
932 #[test]
933 fn out_of_bounds_section_is_rejected() {
934 let h = Container10dHeader::proposed();
935 let payload = [0xAAu8; 100];
936 let inputs = [mesh_input(&payload)];
937 let mut out = [0u8; 512];
938 let n = encode_container(&h, &inputs, &mut out).expect("encode");
939 let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
940 let table_start = parsed_h.section_table_offset as usize;
942 let len_field = table_start + 8;
943 out[len_field..len_field + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
944 let err = parse_section_table(&out[..n], &parsed_h).expect_err("OOB must reject");
945 assert!(
946 matches!(err, SectionTableError::OutOfBounds { .. }),
947 "{err}"
948 );
949 }
950
951 #[test]
952 fn overlapping_sections_are_rejected() {
953 let h = Container10dHeader::proposed();
958 let mesh_payload = [0xAAu8; 100];
959 let node_payload = [0xBBu8; 40];
960 let inputs = [mesh_input(&mesh_payload), node_input(&node_payload)];
961 let mut out = [0u8; 512];
962 let n = encode_container(&h, &inputs, &mut out).expect("encode");
963 let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
964 let table_start = parsed_h.section_table_offset as usize;
965 let first_off =
967 u32::from_le_bytes(out[table_start + 4..table_start + 8].try_into().unwrap()) as usize;
968 let second_desc_off = table_start + SECTION_DESCRIPTOR_SIZE;
974 let new_second_off = align_up(first_off + 10, 16) as u32;
975 assert!(
976 (new_second_off as usize) < first_off + mesh_payload.len(),
977 "patched offset must overlap the first payload"
978 );
979 out[second_desc_off + 4..second_desc_off + 8]
980 .copy_from_slice(&new_second_off.to_le_bytes());
981 let crc_start = new_second_off as usize;
982 let crc_end = crc_start + 40;
983 let new_crc = crc32c(&out[crc_start..crc_end]);
984 out[second_desc_off + 20..second_desc_off + 24].copy_from_slice(&new_crc.to_le_bytes());
985 let parsed_h2 = Container10dHeader::parse(&out[..n]).expect("header still parses");
987 let err = parse_section_table(&out[..n], &parsed_h2).expect_err("overlap must reject");
988 assert!(
989 matches!(err, SectionTableError::OverlappingSections { .. }),
990 "{err}"
991 );
992 }
993
994 #[test]
995 fn non_zero_inter_section_padding_is_rejected() {
996 let h = Container10dHeader::proposed();
997 let mesh_payload = [0u8; 7]; let node_payload = [0u8; 40];
999 let inputs = [mesh_input(&mesh_payload), node_input(&node_payload)];
1000 let mut out = [0u8; 512];
1001 let n = encode_container(&h, &inputs, &mut out).expect("encode");
1002 let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
1003 let descs = parse_section_table(&out[..n], &parsed_h).expect("clean parses");
1004 let mesh_end = descs[0].byte_offset as usize + descs[0].byte_length as usize;
1005 out[mesh_end] = 0x42;
1007 let err =
1008 parse_section_table(&out[..n], &parsed_h).expect_err("non-zero padding must reject");
1009 assert!(
1010 matches!(err, SectionTableError::NonZeroPadding { .. }),
1011 "{err}"
1012 );
1013 }
1014
1015 #[test]
1016 fn unsupported_section_type_is_rejected() {
1017 let h = Container10dHeader::proposed();
1018 let bad = SectionInput {
1019 section_type: SectionType::SpecReservedGovernance, alignment_tier: AlignmentTier::Word,
1021 stride: 0,
1022 element_count: 0,
1023 payload: &[0u8; 16],
1024 };
1025 let mut out = [0u8; 512];
1026 let err = encode_container(&h, std::slice::from_ref(&bad), &mut out)
1027 .expect_err("spec-reserved type must reject");
1028 assert!(
1029 matches!(err, SectionTableError::UnsupportedSectionType { .. }),
1030 "{err}"
1031 );
1032 }
1033}