Skip to main content

qualia_core_db/container_10d/
section.rs

1//! `.10d` self-describing section table + tiered alignment + caller-buffered
2//! writer (P0.2).
3//!
4//! A `.10d` file is: the 64-byte [`super::header::Container10dHeader`], then
5//! (optionally) a section table — an array of [`SectionDescriptor`] rows —
6//! then the section payloads, each aligned to its declared tier. The header's
7//! `section_table_offset` + `section_count` point at the table.
8//!
9//! **Canonical encoding (determinism).** Sections are written in ascending
10//! `section_type` order. Duplicate `section_type` values are rejected, so two
11//! encodes of the same section set — even if the caller passes them in
12//! permuted order — produce byte-identical output. This is the P0.2
13//! "two encodes (incl. permuted section order) are byte-identical" gate.
14//!
15//! **Tiered alignment.** Each section declares an [`AlignmentTier`]; the
16//! writer inserts zero padding so every section start meets its tier. The
17//! reader rejects any section whose start is misaligned, whose descriptor
18//! overlaps another, whose `byte_offset`/`byte_length` is out of bounds, or
19//! whose `stride * element_count != byte_length` (stride-inconsistent).
20//!
21//! **Per-section CRC-32C.** Each descriptor carries a CRC-32C over its
22//! payload bytes; the reader rejects a flipped bit. The CRC-32C routine here
23//! is a local copy of the Castagnoli-reflected algorithm used in
24//! `q42/p64_weight.rs`; **P0.3 consolidates the two into a shared module and
25//! delegates both call sites** (the P0.3 acceptance gate verifies p64's
26//! checksums stay byte-identical after delegation).
27//!
28//! **Caller-buffered / zero-heap.** [`encode_container`] takes a caller-
29//! supplied `&mut [u8]` output buffer and returns the bytes written; it
30//! allocates no `Vec`/`String`/`Box` on the hot path (the only allocation is
31//! the canonical-order index sort, which uses a stack array — see
32//! `sort_indices_stack`). [`parse_section_table`] returns a zero-copy
33//! `&[SectionDescriptor]` view into the input bytes.
34
35use bytemuck::{Pod, Zeroable};
36
37use crate::container_10d::crc32c::crc32c;
38use crate::container_10d::header::{Container10dHeader, HEADER_BYTE_SIZE, MAX_SECTION_COUNT};
39
40/// Size of one [`SectionDescriptor`] in bytes.
41pub const SECTION_DESCRIPTOR_SIZE: usize = 24;
42
43/// Maximum number of sections the writer will accept (mirrors the header's
44/// `MAX_SECTION_COUNT` so the writer cannot produce an unreadable file).
45const MAX_SECTIONS_ENCODE: usize = MAX_SECTION_COUNT as usize;
46
47/// A section type tag. v1 defines the types the runtime actually fills;
48/// future types are listed as `SpecReserved*` and the writer rejects them
49/// (a `SpecReserved*` type in a v1 file is a forward-incompatibility signal,
50/// not a payload to read blindly).
51#[repr(u8)]
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum SectionType {
54    /// Sentinel — the reader rejects this.
55    Undefined = 0,
56    /// Quantized triangle mesh (P0.4 — u16-quantized vertices within the
57    /// mesh's bounding box + u16/u32 triangle indices; see
58    /// [`super::mesh_section`]).
59    QuantizedMesh = 1,
60    /// Tensor10D node section — the 40-byte epistemic atom (P0.5).
61    Tensor10DNodes = 2,
62    /// Reconstruction output — mesh/complex/operator (P6.7).
63    Reconstruction = 3,
64    // --- spec-reserved (reader rejects in v1; do NOT read blindly) ---
65    SpecReservedGovernance = 4,
66    SpecReservedTemporalIndex = 5,
67    SpecReservedManifoldHeadTable = 6,
68    /// Provenance sidecar — source bytes + licence + verifiable credential
69    /// bundled in-envelope so context is byte-inseparable (P1; see
70    /// [`super::provenance_section`]). No longer spec-reserved.
71    ProvenanceSidecar = 7,
72    SpecReservedFieldSidecar = 8,
73    SpecReservedCorrespondenceMap = 9,
74    /// Topology section — half-edge graph + CSR adjacency + connectivity
75    /// summary (P2.8). Contains a TopologyMiniHeader followed by the
76    /// half-edge array, vertex-adjacency CSR (offsets + neighbours), and
77    /// face-adjacency CSR (offsets + neighbours).
78    Topology = 10,
79    /// Spatial-index section — BVH + kd-tree node arrays for scan-free
80    /// spatial queries (P3.7).
81    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    /// True if this section type is implemented (the runtime can read/write
105    /// it) vs spec-reserved (defined in the format but not yet filled).
106    #[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/// Alignment tier for a section's start offset. The tier determines the
121/// power-of-two alignment the writer enforces (and the reader verifies).
122#[repr(u8)]
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum AlignmentTier {
125    /// 1-byte aligned (no requirement).
126    Byte = 0,
127    /// 4-byte aligned.
128    Word = 1,
129    /// 16-byte aligned (typical for SoA tensor lanes).
130    CacheLine = 2,
131    /// 64-byte aligned (page-aligned, matches `q42/p64_weight.rs`).
132    Page = 3,
133}
134
135impl AlignmentTier {
136    /// The alignment in bytes for this tier.
137    #[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/// Align `offset` up to the next multiple of `align` (power of two).
160#[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/// One row of the section table — a self-describing section descriptor.
169///
170/// Layout: 24 bytes, `repr(C)`, naturally aligned, no padding.
171/// ```text
172/// offset  size  field
173/// 0       1     section_type     (SectionType u8)
174/// 1       1     alignment_tier   (AlignmentTier u8)
175/// 2       2     reserved16       (must be zero)
176/// 4       4     byte_offset      (from file start)
177/// 8       4     byte_length      (payload length in bytes)
178/// 12      4     stride           (bytes per element for AoS sections; 0 = non-strided)
179/// 16      4     element_count    (for array sections; 0 = non-array)
180/// 20      4     crc32c           (CRC-32C over the payload bytes)
181/// ```
182#[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    /// The alignment tier as a typed enum (or `None` if the raw byte is
197    /// undefined — the reader rejects this).
198    #[inline]
199    pub fn tier(&self) -> Option<AlignmentTier> {
200        AlignmentTier::from_u8(self.alignment_tier)
201    }
202
203    /// The section type as a typed enum (or `None` if undefined).
204    #[inline]
205    pub fn typ(&self) -> Option<SectionType> {
206        SectionType::from_u8(self.section_type)
207    }
208}
209
210/// A caller-supplied section input for [`encode_container`]. The writer
211/// computes `byte_offset`, `crc32c`, and the canonical order; the caller
212/// supplies the type, tier, stride/element_count (for array sections), and
213/// the payload bytes.
214#[derive(Debug, Clone, Copy)]
215pub struct SectionInput<'a> {
216    pub section_type: SectionType,
217    pub alignment_tier: AlignmentTier,
218    /// Bytes per element for array-of-structs sections. `0` for non-strided
219    /// (blob) sections. If non-zero, `stride * element_count` must equal
220    /// `payload.len()` or the writer rejects the input as stride-inconsistent.
221    pub stride: u32,
222    /// Element count for array sections. `0` for non-array (blob) sections.
223    pub element_count: u32,
224    pub payload: &'a [u8],
225}
226
227/// Section-table encode/decode error.
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub enum SectionTableError {
230    /// More than `MAX_SECTION_COUNT` sections.
231    TooManySections { count: usize },
232    /// Two sections share the same `section_type` (canonical encoding
233    /// requires unique types in v1).
234    DuplicateSectionType { section_type: u8 },
235    /// A section type byte is not a defined variant, or is `Undefined`, or is
236    /// a `SpecReserved*` type the v1 writer refuses to emit.
237    UnsupportedSectionType { got: u8 },
238    /// An alignment tier byte is not a defined variant.
239    UnsupportedAlignmentTier { got: u8 },
240    /// `stride * element_count != payload.len()`.
241    StrideInconsistent {
242        section_type: u8,
243        stride: u32,
244        element_count: u32,
245        payload_len: usize,
246    },
247    /// The caller-supplied output buffer is too small.
248    OutputBufferTooSmall { needed: usize, have: usize },
249    /// The input bytes are too short to hold the header + section table.
250    InputTooShort { got: usize, need: usize },
251    /// The header's section-table pointer is inconsistent (offset/count
252    /// mismatch, offset below header, or count over the max).
253    BadSectionTablePointer { offset: u32, count: u32 },
254    /// A descriptor's `reserved16` is non-zero.
255    NonZeroDescriptorReserved { index: usize },
256    /// A descriptor's `byte_offset` is misaligned relative to its tier.
257    MisalignedSection {
258        index: usize,
259        offset: u32,
260        tier: AlignmentTier,
261    },
262    /// A descriptor's `byte_offset`/`byte_length` is out of bounds.
263    OutOfBounds {
264        index: usize,
265        offset: u32,
266        length: u32,
267        file_len: usize,
268    },
269    /// Two descriptors' payload regions overlap.
270    OverlappingSections { index_a: usize, index_b: usize },
271    /// A descriptor's `stride * element_count != byte_length`.
272    StrideInconsistentDescriptor {
273        index: usize,
274        stride: u32,
275        element_count: u32,
276        byte_length: u32,
277    },
278    /// A descriptor's section type is `Undefined` or an unknown byte.
279    UndefinedSectionType { index: usize, got: u8 },
280    /// A descriptor's alignment tier is undefined.
281    UndefinedAlignmentTier { index: usize, got: u8 },
282    /// A payload's CRC-32C does not match the stored value (a flipped bit).
283    CrcMismatch {
284        index: usize,
285        section_type: u8,
286        expected: u32,
287        got: u32,
288    },
289    /// A padding region between sections is non-zero.
290    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
319// ---------------------------------------------------------------------------
320// Canonical-order index sort (stack-only, no Vec).
321// Returns the indices of `inputs` sorted by `section_type` ascending. Insertion
322// sort is fine — MAX_SECTIONS_ENCODE is small and the section count in any
323// realistic .10d is <10. No heap allocation.
324// ---------------------------------------------------------------------------
325
326fn 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    // Insertion sort by section_type (stable — preserves input order for
332    // equal keys, though equal keys are rejected later as duplicates).
333    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
343/// Compute the total encoded byte length for a set of sections (header +
344/// section table + aligned payloads + inter-section padding). Pure, no
345/// allocation. Returns the total and fills `order` with the canonical index
346/// order and `descs` with the computed descriptors (offsets/lengths/CRCs).
347fn 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    // Reject duplicate section types and unsupported types/tiers up front.
360    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        // Validate stride consistency against the payload.
370        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    // Layout: header (64) -> section table (count * 24) -> payloads (aligned).
384    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
410/// Encode a `.10d` container into a caller-supplied buffer. Zero-heap on the
411/// hot path (the only stack arrays are the index order and descriptor table,
412/// both fixed-size). Returns the number of bytes written.
413///
414/// The header is written with `section_table_offset` and `section_count`
415/// filled in to point at the table; `header_crc32c` is left as the caller
416/// supplied it (P0.3 wires the shared CRC-32C over the header).
417pub 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    // Zero the whole output region we will write into (so padding is zero).
433    for b in out[..total].iter_mut() {
434        *b = 0;
435    }
436
437    // Write the header with the section-table pointer filled in.
438    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    // Write the section table.
455    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    // Write the payloads (padding is already zeroed).
463    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
473/// A const-zero `SectionDescriptor` for initialising the fixed-size stack
474/// array in [`encode_container`] without going through `Zeroable::zeroed()`.
475const 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
486/// Parse the section table from a `.10d` byte slice. Returns a zero-copy
487/// `&[SectionDescriptor]` view into the input. Runs every P0.2 reader gate:
488/// pointer consistency, per-descriptor type/tier/reserved validation, tier
489/// alignment, in-bounds, overlap, stride consistency, and per-section CRC.
490///
491/// Padding-between-sections is verified zero as part of the overlap/scan pass.
492pub 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    // Pointer consistency (the header parser also checks this, but re-check
501    // for callers that construct a header by hand).
502    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    // SAFETY: SectionDescriptor is repr(C) + Pod + size 24 with no padding;
530    // the table slice is byte-aligned and fully within `data`.
531    let descs: &[SectionDescriptor] = bytemuck::cast_slice(&data[table_start..table_end]);
532
533    // Per-descriptor validation + cross-descriptor overlap/alignment scan.
534    // We scan in table order (which is canonical = ascending section_type
535    // order for files we wrote; for files we didn't write, we sort the
536    // offset check by byte_offset to detect overlaps correctly).
537    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        // Stride consistency.
584        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        // Per-section CRC.
596        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    // Overlap detection: sort descriptor indices by byte_offset and check
609    // adjacent ranges don't overlap. Use a stack array (count is bounded by
610    // MAX_SECTION_COUNT, but that's 1024 — too big for a fixed stack array in
611    // a zero-heap function). Instead, do an O(n^2) pairwise check (n is small
612    // in practice; the 42MB Sentinel bounds the file and section count is
613    // realistically <10). This is zero-heap.
614    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                // Two zero-length sections at the same offset are not an
622                // overlap (an empty section is allowed to share an offset).
623                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    // Padding-between-sections is zero: scan the gaps. The gap before the
635    // first section (between table_end and the first section's offset) and
636    // between consecutive sections. Walk in offset order.
637    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    // Insertion sort by byte_offset.
643    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    // Tail padding (after the last section to the end of the encoded region)
662    // is not checked here — the caller may have a larger buffer. Only the
663    // encoded region's padding matters, and that is covered by `total` from
664    // the writer; a reader does not know `total` without the header CRC region
665    // (P0.3). For P0.2, inter-section padding is the gate.
666
667    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        // Tensor10D nodes: 40 bytes each, 16-byte aligned (SoA lane friendly).
687        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]; // 3 Tensor10D nodes
729        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        // Canonical order: QuantizedMesh(1) before Tensor10DNodes(2).
736        assert_eq!(descs[0].section_type, SectionType::QuantizedMesh as u8);
737        assert_eq!(descs[1].section_type, SectionType::Tensor10DNodes as u8);
738        // Payloads round-trip.
739        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        // Stride/element_count for the node section.
748        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]; // odd length, will force padding before next section
756        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]; // forces padding before the 16-byte-aligned node section
779        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        // The gap between the mesh payload end and the node section start must be zero.
786        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)]; // permuted
804        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        // Claim stride=40, element_count=3 (=> 120 bytes) but payload is 100.
837        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]; // way too small
859        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        // Flip one bit in the payload region.
875        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        // Corrupt the descriptor's reserved16 (offset table_start + 2).
895        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        // Move the section's byte_offset to a misaligned address (the table
917        // is at 64, descriptor 0's byte_offset is at table_start + 4).
918        let table_start = parsed_h.section_table_offset as usize;
919        let off_field = table_start + 4;
920        // Set byte_offset to 65 (misaligned for Word tier = 4).
921        out[off_field..off_field + 4].copy_from_slice(&65u32.to_le_bytes());
922        // Re-parse the header (the section-table pointer is unchanged).
923        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        // Set byte_length to a value that runs past the file end.
941        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        // Encode two valid, non-overlapping sections first, then patch the
954        // second descriptor's byte_offset so its range overlaps the first.
955        // Recompute the second's CRC over the patched range so the per-section
956        // CRC passes and the reader reaches the overlap check.
957        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        // Read the first section's offset.
966        let first_off =
967            u32::from_le_bytes(out[table_start + 4..table_start + 8].try_into().unwrap()) as usize;
968        // Patch the second descriptor's byte_offset to overlap the first.
969        // The node section declares CacheLine (16-byte) tier, so the patched
970        // offset must stay 16-aligned; align up from first_off+10 so it
971        // remains within the first section's payload range (first_off..
972        // first_off+100) and thus overlaps.
973        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        // The patched file's header still parses (pointer unchanged).
986        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]; // forces a padding gap before the 16-aligned node section
998        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        // Corrupt a padding byte.
1006        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, // spec-reserved, not yet implemented
1020            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}