Skip to main content

oxgraph_snapshot/
container.rs

1//! Topology-agnostic snapshot container: format constants, byte-level header
2//! and section table, validation, reader, no-`alloc` planner, and the
3//! `alloc`-gated write-through encoder.
4//!
5//! All public types are re-exported through the crate root; consumers should
6//! depend on the crate-level paths rather than reaching in here.
7//!
8//! When the container ever graduates to a separate `topology-snapshot` crate
9//! the whole module moves wholesale, and the crate root becomes a shim of
10//! `pub use topology_snapshot::*`.
11
12#[cfg(feature = "alloc")]
13use alloc::vec::Vec;
14use core::fmt;
15
16use oxgraph_layout_util::SnapshotWidth;
17use zerocopy::{
18    FromBytes, Immutable, IntoBytes, KnownLayout,
19    byteorder::{LE, U32, U64},
20};
21
22use crate::container_error::{PlanError, SectionBindError, SectionViewError, SnapshotError};
23
24/// Magic bytes identifying the topology snapshot container format.
25///
26/// Producers MUST write these eight bytes at offset 0; readers MUST reject
27/// snapshots whose first eight bytes differ.
28///
29/// # Performance
30///
31/// `perf: unspecified`; this is a compile-time constant.
32pub const FORMAT_MAGIC: [u8; 8] = *b"OXGTOPO\0";
33
34/// Format major version this library reads and writes.
35///
36/// A snapshot whose `format_major` field does not equal this constant is
37/// rejected at open time with
38/// [`SnapshotError::FormatMajorMismatch`](crate::SnapshotError::FormatMajorMismatch).
39/// Major bumps are permitted to break compatibility in arbitrary ways.
40///
41/// # Performance
42///
43/// `perf: unspecified`; this is a compile-time constant.
44pub const FORMAT_MAJOR: u32 = 1;
45
46/// Format minor version written by this library's builder.
47///
48/// Minor bumps are reserved for backward-compatible additions (e.g. enabling
49/// previously reserved bits or fields). Producers using this library will
50/// emit this value unconditionally.
51///
52/// # Performance
53///
54/// `perf: unspecified`; this is a compile-time constant.
55pub const FORMAT_MINOR: u32 = 0;
56
57/// Highest format minor version this library can read.
58///
59/// Snapshots with `format_minor > MAX_SUPPORTED_MINOR` are rejected at open
60/// time. Raising this value is a deliberate per-minor decision once the new
61/// minor is proven safely readable here.
62///
63/// # Performance
64///
65/// `perf: unspecified`; this is a compile-time constant.
66pub const MAX_SUPPORTED_MINOR: u32 = 0;
67
68/// Continuation-style CRC-32C (Castagnoli, polynomial `0x1EDC_6F41`) fold.
69///
70/// `checksum(seed, bytes)` continues a checksum: seeding with `0` starts a
71/// fresh fold, folding further byte runs continues it, and the final fold is
72/// the stored value. Implementations MUST satisfy the continuation law
73/// `f(f(0, a), b) == f(0, ab)` and the standard check vector: folding
74/// [`CRC32C_CHECK_INPUT`] from seed `0` yields [`CRC32C_CHECK_VALUE`]
75/// (which implies `f(0, b"") == 0`, the stored value for an empty section).
76///
77/// The container is `no_std` and deliberately does not bundle a CRC
78/// implementation: writers and checked readers inject one. The pure-software
79/// [`oxgraph_layout_util::crc32c_append`] satisfies this contract, as does
80/// the hardware-accelerated `crc32c` crate's `crc32c_append`.
81///
82/// # Performance
83///
84/// Implementations are expected to be `O(bytes.len())` per fold.
85pub type Checksum32 = fn(u32, &[u8]) -> u32;
86
87/// Standard CRC-32C check-vector input (the ASCII digits `123456789`).
88///
89/// Any [`Checksum32`] implementation must map this input (seed `0`) to
90/// [`CRC32C_CHECK_VALUE`]; tests use the pair to pin the algorithm.
91///
92/// # Performance
93///
94/// `perf: unspecified`; this is a compile-time constant.
95pub const CRC32C_CHECK_INPUT: &[u8] = b"123456789";
96
97/// Standard CRC-32C check-vector result: `crc32c(0, b"123456789")`.
98///
99/// # Performance
100///
101/// `perf: unspecified`; this is a compile-time constant.
102pub const CRC32C_CHECK_VALUE: u32 = 0xE306_9283;
103
104/// Size of the snapshot header in bytes.
105///
106/// # Performance
107///
108/// `perf: unspecified`; this is a compile-time constant.
109pub const HEADER_SIZE: usize = 32;
110
111/// Size of one section table entry in bytes.
112///
113/// # Performance
114///
115/// `perf: unspecified`; this is a compile-time constant.
116pub const SECTION_ENTRY_SIZE: usize = 32;
117
118/// Maximum permitted `alignment_log2` value (2^12 = 4 KiB, page-friendly).
119///
120/// # Performance
121///
122/// `perf: unspecified`; this is a compile-time constant.
123pub const MAX_ALIGNMENT_LOG2: u8 = 12;
124
125/// Maximum permitted section count for v2 snapshots.
126///
127/// Bounds the `O(s)` table-validation walk and keeps kani proofs tractable.
128///
129/// # Performance
130///
131/// `perf: unspecified`; this is a compile-time constant.
132pub const MAX_SECTION_COUNT: u32 = 1024;
133
134/// `HEADER_SIZE` rendered as a `u32` for header-field comparisons.
135const HEADER_SIZE_U32: u32 = 32;
136
137/// Typed wrapper over a section's opaque `u32` kind tag.
138///
139/// The container still treats the value opaquely, but [`SectionKind`] plus the
140/// [`kinds`] band registry give the wire format a single documented authority
141/// over the kind namespace. Layout crates declare their kind constants inside
142/// the band the registry reserves for them so that distinct subsystems cannot
143/// silently collide on a value.
144///
145/// # Performance
146///
147/// All methods are `O(1)`.
148#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
149pub struct SectionKind(u32);
150
151impl SectionKind {
152    /// Wraps a raw kind tag.
153    ///
154    /// # Performance
155    ///
156    /// This function is `O(1)`.
157    #[must_use]
158    pub const fn new(value: u32) -> Self {
159        Self(value)
160    }
161
162    /// Returns the raw kind tag.
163    ///
164    /// # Performance
165    ///
166    /// This function is `O(1)`.
167    #[must_use]
168    pub const fn get(self) -> u32 {
169        self.0
170    }
171}
172
173impl From<u32> for SectionKind {
174    fn from(value: u32) -> Self {
175        Self(value)
176    }
177}
178
179impl fmt::Display for SectionKind {
180    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
181        write!(formatter, "{:#06x}", self.0)
182    }
183}
184
185/// Section-kind band allocation registry: the single documented authority for
186/// who owns which range of the opaque `u32` kind namespace.
187///
188/// The container assigns no semantics to kinds, but every in-tree layer
189/// declares its `SNAPSHOT_KIND_*` constants inside the band reserved here, and
190/// the bands are mutually exclusive, so distinct subsystems cannot collide.
191/// Each band is a half-open `[start, end)` range of raw kind values.
192///
193/// # Performance
194///
195/// `perf: unspecified`; these are compile-time constants.
196pub mod kinds {
197    use core::ops::Range;
198
199    /// CSR graph layout sections (offsets/targets, all widths).
200    pub const CSR_BAND: Range<u32> = 0x0001..0x0020;
201    /// Bipartite-CSR hypergraph layout sections (all widths).
202    pub const BCSR_BAND: Range<u32> = 0x0020..0x0100;
203    /// Property and identity-map sections (all widths).
204    pub const PROPERTY_BAND: Range<u32> = 0x0100..0x0200;
205    /// `PostgreSQL` engine sections, including the inbound CSC layout.
206    pub const POSTGRES_BAND: Range<u32> = 0x0200..0x0300;
207    /// Embedded `OxGraph` database state sections.
208    pub const DATABASE_BAND: Range<u32> = 0x0300..0x0400;
209    /// Application/custom sections; the container reserves nothing here.
210    pub const CUSTOM_BASE: u32 = 0x0400;
211
212    /// Returns whether `kind` falls within the half-open `band`.
213    ///
214    /// Layout crates use this in `const`-checked tests to prove their kind
215    /// constants stay inside their reserved band.
216    ///
217    /// # Performance
218    ///
219    /// This function is `O(1)`.
220    #[must_use]
221    pub const fn in_band(kind: u32, band: Range<u32>) -> bool {
222        kind >= band.start && kind < band.end
223    }
224}
225
226/// Converts a checked `u64` into `usize`, asserting in debug mode that the
227/// value already fits because validation enforced an earlier bound.
228///
229/// # Panics
230///
231/// Panics via `unreachable!()` only on a target where `usize` is narrower
232/// than `u64` AND the caller has supplied a value that was not first vetted
233/// by the snapshot's `Layout` validation pass (which surfaces the failure
234/// as [`SnapshotError::UsizeOverflow`] before any `_validated` call).
235///
236/// # Performance
237///
238/// This function is `O(1)`.
239fn u64_to_usize_validated(value: u64) -> usize {
240    match usize::try_from(value) {
241        Ok(converted) => converted,
242        Err(_error) => unreachable!("validated u64 must fit usize on this target"),
243    }
244}
245
246/// Byte-level snapshot header.
247///
248/// Layout is `#[repr(C)]` with all multi-byte fields stored as zerocopy's
249/// unaligned little-endian wrappers. The struct itself has alignment 1, so
250/// it can be borrowed from any byte slice that is at least `HEADER_SIZE`
251/// long without an alignment check.
252#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes, KnownLayout)]
253#[repr(C)]
254struct RawHeader {
255    /// Magic bytes; must equal [`FORMAT_MAGIC`].
256    magic: [u8; 8],
257    /// Format major version.
258    format_major: U32<LE>,
259    /// Format minor version.
260    format_minor: U32<LE>,
261    /// Header size in bytes; v2.0 mandates `HEADER_SIZE`.
262    header_size: U32<LE>,
263    /// Number of section table entries.
264    section_count: U32<LE>,
265    /// CRC-32C over the section-table bytes (`section_count` entries of
266    /// [`SECTION_ENTRY_SIZE`] bytes immediately after this header).
267    table_crc32c: U32<LE>,
268    /// Reserved; must be zero.
269    reserved: [u8; 4],
270}
271
272/// Parses the fixed header from the start of `bytes`.
273///
274/// # Errors
275///
276/// Returns [`SnapshotError::TruncatedHeader`] when fewer than [`HEADER_SIZE`]
277/// bytes are provided. Header field validation is performed separately in
278/// [`validate_magic_versions_reserved`].
279///
280/// # Performance
281///
282/// This function is `O(1)`.
283fn parse_header(bytes: &[u8]) -> Result<(&RawHeader, &[u8]), SnapshotError> {
284    if bytes.len() < HEADER_SIZE {
285        return Err(SnapshotError::TruncatedHeader {
286            needed: HEADER_SIZE,
287            actual: bytes.len(),
288        });
289    }
290
291    match RawHeader::ref_from_prefix(bytes) {
292        Ok((header, rest)) => Ok((header, rest)),
293        Err(_error) => Err(SnapshotError::MalformedHeader),
294    }
295}
296
297/// Validates header magic, version, header size, and reserved bytes.
298///
299/// # Errors
300///
301/// Returns [`SnapshotError`] for any header-level invariant violation.
302///
303/// # Performance
304///
305/// This function is `O(1)`.
306fn validate_magic_versions_reserved(header: &RawHeader) -> Result<(), SnapshotError> {
307    if header.magic != FORMAT_MAGIC {
308        return Err(SnapshotError::BadMagic {
309            actual: header.magic,
310        });
311    }
312
313    let major = header.format_major.get();
314    if major != FORMAT_MAJOR {
315        return Err(SnapshotError::FormatMajorMismatch {
316            actual: major,
317            supported: FORMAT_MAJOR,
318        });
319    }
320
321    let minor = header.format_minor.get();
322    if minor > MAX_SUPPORTED_MINOR {
323        return Err(SnapshotError::FormatMinorTooNew {
324            actual: minor,
325            max_supported: MAX_SUPPORTED_MINOR,
326        });
327    }
328
329    let header_size = header.header_size.get();
330    if header_size != HEADER_SIZE_U32 {
331        return Err(SnapshotError::HeaderSizeMismatch {
332            actual: header_size,
333            expected: HEADER_SIZE_U32,
334        });
335    }
336
337    if header.reserved != [0; 4] {
338        return Err(SnapshotError::NonZeroHeaderReserved);
339    }
340
341    Ok(())
342}
343
344/// Byte-level section table entry.
345///
346/// Layout is `#[repr(C)]` with unaligned little-endian fields, mirroring
347/// [`RawHeader`]'s alignment policy.
348#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes, KnownLayout)]
349#[repr(C)]
350struct RawSectionEntry {
351    /// Byte offset of the section payload from the start of the snapshot.
352    offset: U64<LE>,
353    /// Byte length of the section payload.
354    length: U64<LE>,
355    /// Opaque section kind; the container assigns no semantics. the format mandates
356    /// strictly-ascending kind order across the table.
357    kind: U32<LE>,
358    /// Opaque section version; consumers interpret per kind.
359    version: U32<LE>,
360    /// CRC-32C over this section's payload bytes; mandatory
361    /// (`crc32c(b"") == 0` covers empty sections).
362    crc32c: U32<LE>,
363    /// `log2` of the producer's chosen payload alignment; v2 cap is 12.
364    alignment_log2: u8,
365    /// Reserved flag bits; must be zero.
366    flags: u8,
367    /// Trailing reserved bytes; must be zero.
368    reserved: [u8; 2],
369}
370
371/// Borrowed view of one validated section in a snapshot.
372///
373/// A `Section` carries the section's byte payload along with its declared
374/// metadata. Payload bytes are bounds- and overlap-checked at snapshot open
375/// time. Typed-slice access via [`Section::try_as_slice`] verifies the
376/// actual borrowed pointer's alignment at the call site.
377///
378/// # Performance
379///
380/// All methods are `O(1)` or `O(payload.len())` for typed conversions.
381#[derive(Clone, Copy, Debug)]
382pub struct Section<'view> {
383    /// Borrowed payload bytes.
384    payload: &'view [u8],
385    /// Section kind, as recorded in the section entry.
386    kind: u32,
387    /// Section version, as recorded in the section entry.
388    version: u32,
389    /// CRC-32C the entry records for the payload bytes.
390    crc32c: u32,
391    /// `log2` of the declared payload alignment.
392    alignment_log2: u8,
393}
394
395impl<'view> Section<'view> {
396    /// Constructs a [`Section`] from a previously validated entry.
397    ///
398    /// # Performance
399    ///
400    /// This function is `O(1)`.
401    #[must_use]
402    fn from_entry(bytes: &'view [u8], entry: &RawSectionEntry) -> Self {
403        let offset = u64_to_usize_validated(entry.offset.get());
404        let length = u64_to_usize_validated(entry.length.get());
405        Self {
406            payload: &bytes[offset..offset + length],
407            kind: entry.kind.get(),
408            version: entry.version.get(),
409            crc32c: entry.crc32c.get(),
410            alignment_log2: entry.alignment_log2,
411        }
412    }
413
414    /// Returns the section's opaque kind identifier.
415    ///
416    /// # Performance
417    ///
418    /// This method is `O(1)`.
419    #[must_use]
420    pub const fn kind(&self) -> u32 {
421        self.kind
422    }
423
424    /// Returns the section's opaque version identifier.
425    ///
426    /// # Performance
427    ///
428    /// This method is `O(1)`.
429    #[must_use]
430    pub const fn version(&self) -> u32 {
431        self.version
432    }
433
434    /// Returns the alignment the producer declared for this payload.
435    ///
436    /// This is metadata recorded at build time, not a guarantee about the
437    /// actual borrowed pointer. Callers that intend to interpret the payload
438    /// as a typed slice should prefer [`Section::try_as_slice`], which
439    /// checks the actual payload pointer.
440    ///
441    /// # Performance
442    ///
443    /// This method is `O(1)`.
444    #[must_use]
445    pub const fn declared_alignment(&self) -> usize {
446        1usize << self.alignment_log2
447    }
448
449    /// Returns the section's borrowed payload bytes.
450    ///
451    /// # Performance
452    ///
453    /// This method is `O(1)`.
454    #[must_use]
455    pub const fn bytes(&self) -> &'view [u8] {
456        self.payload
457    }
458
459    /// Returns the CRC-32C the section entry records for this payload.
460    ///
461    /// This is the stored value, not a recomputation; use
462    /// [`Section::verify`] to check it against the actual payload bytes.
463    ///
464    /// # Performance
465    ///
466    /// This method is `O(1)`.
467    #[must_use]
468    pub const fn expected_crc32c(&self) -> u32 {
469        self.crc32c
470    }
471
472    /// Verifies the payload bytes against the entry's recorded CRC-32C.
473    ///
474    /// # Errors
475    ///
476    /// Returns [`SectionViewError::ChecksumMismatch`] when the recomputed
477    /// checksum differs from [`Section::expected_crc32c`].
478    ///
479    /// # Performance
480    ///
481    /// This method is `O(payload.len())` (one checksum fold).
482    pub fn verify(&self, checksum: Checksum32) -> Result<(), SectionViewError> {
483        let actual = checksum(0, self.payload);
484        if actual == self.crc32c {
485            Ok(())
486        } else {
487            Err(SectionViewError::ChecksumMismatch {
488                kind: self.kind,
489                expected: self.crc32c,
490                actual,
491            })
492        }
493    }
494
495    /// Borrows the payload as a typed slice of `T`.
496    ///
497    /// Errors if (a) `payload.len()` is not a multiple of
498    /// `core::mem::size_of::<T>()` or (b) the payload's actual base address
499    /// does not satisfy `core::mem::align_of::<T>()`. The producer's
500    /// declared `alignment_log2` is not consulted; the actual borrowed
501    /// pointer is checked directly so that mmap'd or sub-sliced inputs
502    /// cannot bypass the check.
503    ///
504    /// # Errors
505    ///
506    /// Returns [`SectionViewError`] when the payload cannot be borrowed
507    /// as `&[T]` without copying.
508    ///
509    /// # Performance
510    ///
511    /// This method is `O(1)` modulo the bounds and alignment checks; it
512    /// performs no allocation and no per-element work.
513    pub fn try_as_slice<T>(&self) -> Result<&'view [T], SectionViewError>
514    where
515        T: zerocopy::FromBytes + zerocopy::Immutable + zerocopy::KnownLayout,
516    {
517        let elem_size = core::mem::size_of::<T>();
518        let length = self.payload.len();
519
520        if elem_size == 0 {
521            return Err(SectionViewError::ZeroSizedType);
522        }
523
524        if !length.is_multiple_of(elem_size) {
525            return Err(SectionViewError::LengthNotMultipleOfSize { length, elem_size });
526        }
527
528        let required = core::mem::align_of::<T>();
529        let ptr_addr = self.payload.as_ptr().addr();
530        if !ptr_addr.is_multiple_of(required) {
531            return Err(SectionViewError::AlignmentMismatch { ptr_addr, required });
532        }
533
534        let count = length / elem_size;
535        match <[T]>::ref_from_bytes_with_elems(self.payload, count) {
536            Ok(slice) => Ok(slice),
537            Err(_error) => Err(SectionViewError::AlignmentMismatch { ptr_addr, required }),
538        }
539    }
540}
541
542/// Validation depth applied at snapshot open time.
543///
544/// Validation responsibilities are layered. Header-only validation is not a
545/// member of this enum; callers wanting it should use
546/// [`HeaderOnlySnapshot::open`] instead, so the type system distinguishes a
547/// section-bearing handle from one whose section table has not been
548/// validated.
549///
550/// - [`SectionTable`](Self::SectionTable) parses the section table, per-entry self-consistency
551///   (alignment bound, reserved bytes zero, flags zero), payload bounds, and the v2
552///   strictly-ascending kind order.
553/// - [`Layout`](Self::Layout) is the default; it adds non-overlapping monotonic-offset enforcement.
554///
555/// Topology-level validation (CSR offset monotonicity, hypergraph role
556/// consistency, etc.) is the consumer's responsibility — the container
557/// has no kind registry and cannot validate semantics it does not know.
558///
559/// # Performance
560///
561/// `perf: unspecified`; this is a metadata enum.
562#[non_exhaustive]
563#[derive(Clone, Copy, Debug, Eq, PartialEq)]
564pub enum ValidationLevel {
565    /// Validate header and section table self-consistency.
566    SectionTable,
567    /// Validate header, section table, and full payload layout.
568    Layout,
569}
570
571/// Walks the section table once and checks all v2 invariants.
572///
573/// `bytes` is the entire snapshot byte slice; `entries` is the parsed
574/// section table; `level` controls how deep the walk goes. Header-level
575/// invariants are presumed already validated by the caller.
576///
577/// Per-entry self-consistency, payload bounds (`offset + length` does not
578/// overflow and stays within the snapshot), **and** the strictly-ascending
579/// kind order are enforced at every level, so every [`Section`] a
580/// [`Snapshot`] hands out is bounds-safe and [`Snapshot::section`]'s binary
581/// search is sound regardless of the requested [`ValidationLevel`]. The
582/// ascending order also makes the table duplicate-free by construction,
583/// so no separate duplicate-kind walk is needed. [`ValidationLevel::Layout`]
584/// additionally enforces non-overlapping monotonic offset ordering.
585///
586/// # Errors
587///
588/// Returns [`SnapshotError`] for any per-entry, bounds, or layout violation.
589///
590/// # Performance
591///
592/// This function is `O(s)` at every validation level.
593fn validate_section_table(
594    bytes: &[u8],
595    entries: &[RawSectionEntry],
596    level: ValidationLevel,
597) -> Result<(), SnapshotError> {
598    let snapshot_len = bytes.len() as u64;
599
600    // Always-run: per-entry self-consistency, payload-bounds safety, and the
601    // strictly-ascending kind mandate. The bounds check guarantees
602    // `Section::from_entry`'s `bytes[offset..end]` slice is in range, so
603    // accessors are panic-free at SectionTable level too; the ascending check
604    // keeps `Snapshot::section`'s binary search sound at every level.
605    let mut prev_kind: Option<u32> = None;
606    for entry in entries {
607        let kind = entry.kind.get();
608        if let Some(prev) = prev_kind
609            && kind <= prev
610        {
611            return Err(SnapshotError::NonAscendingKind { kind, prev });
612        }
613        prev_kind = Some(kind);
614        if entry.flags != 0 {
615            return Err(SnapshotError::UnsupportedFlags {
616                kind,
617                flags: entry.flags,
618            });
619        }
620        if entry.reserved != [0; 2] {
621            return Err(SnapshotError::NonZeroEntryReserved { kind });
622        }
623        if entry.alignment_log2 > MAX_ALIGNMENT_LOG2 {
624            return Err(SnapshotError::AlignmentLog2TooLarge {
625                kind,
626                alignment_log2: entry.alignment_log2,
627            });
628        }
629        let offset = entry.offset.get();
630        let length = entry.length.get();
631        let end = offset
632            .checked_add(length)
633            .ok_or(SnapshotError::SectionRangeOverflow { kind })?;
634        if end > snapshot_len {
635            return Err(SnapshotError::SectionOutOfBounds {
636                kind,
637                offset,
638                length,
639                snapshot_len,
640            });
641        }
642    }
643
644    if matches!(level, ValidationLevel::SectionTable) {
645        return Ok(());
646    }
647
648    // Layout-only: non-overlapping monotonic ordering (sections start at or
649    // after the end of the header+table and never overlap a predecessor).
650    let header_plus_table = (HEADER_SIZE as u64)
651        .checked_add((entries.len() as u64).saturating_mul(SECTION_ENTRY_SIZE as u64))
652        .ok_or(SnapshotError::SectionRangeOverflow { kind: 0 })?;
653    let mut prev_end = header_plus_table;
654    for (index, entry) in entries.iter().enumerate() {
655        let offset = entry.offset.get();
656        // `end` cannot overflow: the always-run walk above already proved it.
657        let end = offset.saturating_add(entry.length.get());
658        if offset < prev_end {
659            return Err(SnapshotError::UnsortedSectionTable { index });
660        }
661        prev_end = end;
662    }
663
664    Ok(())
665}
666
667/// Computes the CRC-32C over the section-table bytes following the header.
668///
669/// The covered range is exactly `section_count * SECTION_ENTRY_SIZE` bytes
670/// starting at [`HEADER_SIZE`] — the value stored in the header's
671/// `table_crc32c` field. The caller guarantees `table_bytes` is that range.
672///
673/// # Performance
674///
675/// This function is `O(table_bytes.len())` (one checksum fold).
676fn table_checksum(table_bytes: &[u8], checksum: Checksum32) -> u32 {
677    checksum(0, table_bytes)
678}
679
680/// Header-only handle to a snapshot's bytes.
681///
682/// `HeaderOnlySnapshot` is the typestate-distinct counterpart to
683/// [`Snapshot`]: it validates only the fixed header (magic, format
684/// versions, header size, reserved bytes) and exposes the format
685/// versions, but it deliberately does not parse or expose the section
686/// table. Callers who only need to inspect format compatibility (e.g.,
687/// to decide whether the snapshot is readable at all) should use this
688/// type rather than asking [`Snapshot`] to skip section validation.
689///
690/// # Performance
691///
692/// [`HeaderOnlySnapshot::open`] is `O(1)` — it does not walk the section
693/// table or payload region. Subsequent accessors are `O(1)`.
694#[derive(Clone, Copy, Debug)]
695pub struct HeaderOnlySnapshot<'view> {
696    /// Borrowed snapshot bytes.
697    bytes: &'view [u8],
698    /// Format major version recorded in the header.
699    format_major: u32,
700    /// Format minor version recorded in the header.
701    format_minor: u32,
702}
703
704impl<'view> HeaderOnlySnapshot<'view> {
705    /// Opens `bytes` as a header-validated snapshot handle.
706    ///
707    /// Validates the magic bytes, format major and minor, header size, and
708    /// reserved bytes only. The section table and payload region are not
709    /// inspected and may still be malformed.
710    ///
711    /// # Errors
712    ///
713    /// Returns [`SnapshotError`] for any header-level invariant violation.
714    ///
715    /// # Performance
716    ///
717    /// This function is `O(1)`.
718    pub fn open(bytes: &'view [u8]) -> Result<Self, SnapshotError> {
719        let (header, _after_header) = parse_header(bytes)?;
720        validate_magic_versions_reserved(header)?;
721        Ok(Self {
722            bytes,
723            format_major: header.format_major.get(),
724            format_minor: header.format_minor.get(),
725        })
726    }
727
728    /// Returns the borrowed snapshot bytes.
729    ///
730    /// # Performance
731    ///
732    /// This method is `O(1)`.
733    #[must_use]
734    pub const fn bytes(&self) -> &'view [u8] {
735        self.bytes
736    }
737
738    /// Returns the format major version recorded in the snapshot header.
739    ///
740    /// # Performance
741    ///
742    /// This method is `O(1)`.
743    #[must_use]
744    pub const fn format_major(&self) -> u32 {
745        self.format_major
746    }
747
748    /// Returns the format minor version recorded in the snapshot header.
749    ///
750    /// # Performance
751    ///
752    /// This method is `O(1)`.
753    #[must_use]
754    pub const fn format_minor(&self) -> u32 {
755        self.format_minor
756    }
757}
758
759/// Validated, borrowed handle to a snapshot's bytes and section table.
760///
761/// A `Snapshot` is constructed via [`Snapshot::open`] (structural, default
762/// [`ValidationLevel::Layout`]), [`Snapshot::open_with`], or
763/// [`Snapshot::open_checked`] (structural plus table-checksum
764/// verification). The handle itself is `Copy` and trivially cheap to pass;
765/// cloning it does not re-validate.
766///
767/// For header-only inspection without parsing the section table, use
768/// [`HeaderOnlySnapshot`] instead — `Snapshot` always carries a validated
769/// section table.
770///
771/// # Performance
772///
773/// Open is `O(s)` for `s` sections (header + table walk; payload bytes are
774/// never scanned). Subsequent reads are `O(1)` to `O(log s)` per call;
775/// checksum verification ([`Snapshot::verify_all`], [`Section::verify`]) is
776/// `O(covered bytes)`. No allocation occurs.
777#[derive(Clone, Copy, Debug)]
778pub struct Snapshot<'view> {
779    /// Borrowed snapshot bytes.
780    bytes: &'view [u8],
781    /// Format major version recorded in the header.
782    format_major: u32,
783    /// Format minor version recorded in the header.
784    format_minor: u32,
785    /// Section-table CRC-32C recorded in the header.
786    table_crc32c: u32,
787    /// Borrowed, validated section table entries.
788    entries: &'view [RawSectionEntry],
789}
790
791impl<'view> Snapshot<'view> {
792    /// Opens `bytes` as a structurally validated snapshot at
793    /// [`ValidationLevel::Layout`].
794    ///
795    /// This is a structural open: the header, section table shape, kind
796    /// order, and payload bounds are validated, but **no payload bytes are
797    /// verified** and the header's `table_crc32c` is not checked — the
798    /// container is `no_std` and carries no checksum implementation, so a
799    /// checksum-bearing open must go through [`Snapshot::open_checked`].
800    /// Payload integrity is checked on demand via [`Snapshot::verify_all`]
801    /// or [`Section::verify`].
802    ///
803    /// # Errors
804    ///
805    /// Returns [`SnapshotError`] for any header, section table, or layout
806    /// invariant violation.
807    ///
808    /// # Performance
809    ///
810    /// `O(s)` for `s` section entries (header + table walk only).
811    pub fn open(bytes: &'view [u8]) -> Result<Self, SnapshotError> {
812        Self::open_with(bytes, ValidationLevel::Layout)
813    }
814
815    /// Opens `bytes` structurally and verifies the header's `table_crc32c`
816    /// against the section-table bytes.
817    ///
818    /// Section payloads are still **not** verified; use
819    /// [`Snapshot::verify_all`] for that.
820    ///
821    /// # Errors
822    ///
823    /// Returns [`SnapshotError::TableChecksumMismatch`] when the recomputed
824    /// table checksum differs from the header's, or any structural
825    /// [`SnapshotError`] from [`Snapshot::open`].
826    ///
827    /// # Performance
828    ///
829    /// `O(s)` for `s` section entries (table walk plus one checksum fold
830    /// over the table bytes).
831    pub fn open_checked(bytes: &'view [u8], checksum: Checksum32) -> Result<Self, SnapshotError> {
832        let snapshot = Self::open(bytes)?;
833        let actual = table_checksum(snapshot.entries.as_bytes(), checksum);
834        if actual != snapshot.table_crc32c {
835            return Err(SnapshotError::TableChecksumMismatch {
836                expected: snapshot.table_crc32c,
837                actual,
838            });
839        }
840        Ok(snapshot)
841    }
842
843    /// Opens `bytes` as a snapshot validated at the requested level.
844    ///
845    /// `level` selects between [`ValidationLevel::SectionTable`] (per-entry
846    /// self-consistency, bounds, and kind order) and
847    /// [`ValidationLevel::Layout`] (adds non-overlapping monotonic offset
848    /// enforcement). Header-only validation is deliberately not selectable
849    /// here; callers wanting it should use [`HeaderOnlySnapshot::open`].
850    ///
851    /// # Errors
852    ///
853    /// Returns [`SnapshotError`] for any invariant violation visible at
854    /// the requested level.
855    ///
856    /// # Performance
857    ///
858    /// `O(s)` at either level.
859    pub fn open_with(bytes: &'view [u8], level: ValidationLevel) -> Result<Self, SnapshotError> {
860        let (header, after_header) = parse_header(bytes)?;
861        validate_magic_versions_reserved(header)?;
862
863        let format_major = header.format_major.get();
864        let format_minor = header.format_minor.get();
865
866        let section_count = header.section_count.get();
867        if section_count > MAX_SECTION_COUNT {
868            return Err(SnapshotError::SectionCountTooLarge {
869                count: section_count,
870                max: MAX_SECTION_COUNT,
871            });
872        }
873        let Ok(section_count_usize) = usize::try_from(section_count) else {
874            return Err(SnapshotError::UsizeOverflow {
875                value: u64::from(section_count),
876            });
877        };
878        let Some(table_len) = section_count_usize.checked_mul(SECTION_ENTRY_SIZE) else {
879            return Err(SnapshotError::SectionCountTooLarge {
880                count: section_count,
881                max: MAX_SECTION_COUNT,
882            });
883        };
884        if after_header.len() < table_len {
885            return Err(SnapshotError::TruncatedSectionTable {
886                needed: table_len,
887                actual: after_header.len(),
888            });
889        }
890
891        let table_bytes = &after_header[..table_len];
892        let entries =
893            <[RawSectionEntry]>::ref_from_bytes_with_elems(table_bytes, section_count_usize)
894                .map_err(|_error| SnapshotError::MalformedSectionTable)?;
895
896        validate_section_table(bytes, entries, level)?;
897
898        Ok(Self {
899            bytes,
900            format_major,
901            format_minor,
902            table_crc32c: header.table_crc32c.get(),
903            entries,
904        })
905    }
906
907    /// Verifies every section payload against its entry's recorded CRC-32C.
908    ///
909    /// # Errors
910    ///
911    /// Returns [`SnapshotError::SectionChecksumMismatch`] naming the first
912    /// section whose payload bytes do not hash to the recorded value.
913    ///
914    /// # Performance
915    ///
916    /// This method is `O(total payload bytes)` (one checksum fold per
917    /// section).
918    pub fn verify_all(&self, checksum: Checksum32) -> Result<(), SnapshotError> {
919        for section in self.sections() {
920            let actual = checksum(0, section.bytes());
921            if actual != section.expected_crc32c() {
922                return Err(SnapshotError::SectionChecksumMismatch {
923                    kind: section.kind(),
924                    expected: section.expected_crc32c(),
925                    actual,
926                });
927            }
928        }
929        Ok(())
930    }
931
932    /// Returns the format major version recorded in the snapshot header.
933    ///
934    /// # Performance
935    ///
936    /// This method is `O(1)`.
937    #[must_use]
938    pub const fn format_major(&self) -> u32 {
939        self.format_major
940    }
941
942    /// Returns the format minor version recorded in the snapshot header.
943    ///
944    /// # Performance
945    ///
946    /// This method is `O(1)`.
947    #[must_use]
948    pub const fn format_minor(&self) -> u32 {
949        self.format_minor
950    }
951
952    /// Returns the number of validated sections.
953    ///
954    /// # Performance
955    ///
956    /// This method is `O(1)`.
957    #[must_use]
958    pub const fn section_count(&self) -> usize {
959        self.entries.len()
960    }
961
962    /// Returns an iterator over all validated sections.
963    ///
964    /// # Performance
965    ///
966    /// Constructing the iterator is `O(1)`; advancing it is `O(1)` per step.
967    #[must_use]
968    pub fn sections(&self) -> SectionIter<'view> {
969        SectionIter {
970            bytes: self.bytes,
971            entries: self.entries.iter(),
972        }
973    }
974
975    /// Returns the section with the given `kind`, when present.
976    ///
977    /// # Performance
978    ///
979    /// This method is `O(log s)` for `s` section entries: the v2
980    /// strictly-ascending kind mandate makes the table binary-searchable.
981    #[must_use]
982    pub fn section(&self, kind: u32) -> Option<Section<'view>> {
983        self.entries
984            .binary_search_by(|entry| entry.kind.get().cmp(&kind))
985            .ok()
986            .map(|index| Section::from_entry(self.bytes, &self.entries[index]))
987    }
988
989    /// Binds a width-typed section by kind and version in one step.
990    ///
991    /// Looks up the section, checks its version against `expected_version`, and
992    /// borrows the payload as `&[W::LittleEndianWord]`. This is the single
993    /// section-open primitive every layout crate reuses instead of
994    /// re-implementing the lookup/version/typed-view sequence with its own error
995    /// variants; callers map [`SectionBindError`] into their own typed error at
996    /// the boundary.
997    ///
998    /// # Errors
999    ///
1000    /// Returns [`SectionBindError::Missing`] when no section has `kind`,
1001    /// [`SectionBindError::VersionMismatch`] when the recorded version differs,
1002    /// and [`SectionBindError::View`] when the payload cannot be borrowed as the
1003    /// requested little-endian word.
1004    ///
1005    /// # Performance
1006    ///
1007    /// This method is `O(log s)` for `s` section entries plus the typed-view
1008    /// checks.
1009    pub fn typed_section<W>(
1010        &self,
1011        kind: u32,
1012        expected_version: u32,
1013    ) -> Result<&'view [W::LittleEndianWord], SectionBindError>
1014    where
1015        W: SnapshotWidth,
1016    {
1017        let section = self
1018            .section(kind)
1019            .ok_or(SectionBindError::Missing { kind })?;
1020        if section.version() != expected_version {
1021            return Err(SectionBindError::VersionMismatch {
1022                kind,
1023                expected: expected_version,
1024                actual: section.version(),
1025            });
1026        }
1027        section
1028            .try_as_slice::<W::LittleEndianWord>()
1029            .map_err(|error| SectionBindError::View { kind, error })
1030    }
1031}
1032
1033/// Iterator over a snapshot's validated sections.
1034///
1035/// Yields each [`Section`] in section-table order. The iterator does not
1036/// allocate and borrows from the snapshot's underlying byte slice.
1037///
1038/// # Performance
1039///
1040/// Advancing the iterator is `O(1)` per step.
1041#[derive(Clone, Debug)]
1042pub struct SectionIter<'view> {
1043    /// Borrowed snapshot bytes.
1044    bytes: &'view [u8],
1045    /// Remaining section table entries to yield.
1046    entries: core::slice::Iter<'view, RawSectionEntry>,
1047}
1048
1049impl<'view> Iterator for SectionIter<'view> {
1050    type Item = Section<'view>;
1051
1052    fn next(&mut self) -> Option<Self::Item> {
1053        self.entries
1054            .next()
1055            .map(|entry| Section::from_entry(self.bytes, entry))
1056    }
1057
1058    fn size_hint(&self) -> (usize, Option<usize>) {
1059        self.entries.size_hint()
1060    }
1061}
1062
1063impl ExactSizeIterator for SectionIter<'_> {
1064    fn len(&self) -> usize {
1065        self.entries.len()
1066    }
1067}
1068
1069/// Description of one section to include in a snapshot.
1070///
1071/// Every field is opaque to the encoder. `kind` and `version` are passed
1072/// through unchanged; `alignment_log2` controls payload alignment relative
1073/// to the snapshot's start; `payload` is the section's raw bytes. Sections
1074/// must be supplied in strictly-ascending `kind` order (the format's ascending-kind mandate).
1075///
1076/// # Performance
1077///
1078/// `perf: unspecified`; this is a metadata struct.
1079#[derive(Clone, Copy, Debug)]
1080pub struct PendingSection<'a> {
1081    /// Section kind to record in the entry.
1082    pub kind: u32,
1083    /// Section version to record in the entry.
1084    pub version: u32,
1085    /// `log2` of the requested payload alignment; capped at
1086    /// [`MAX_ALIGNMENT_LOG2`].
1087    pub alignment_log2: u8,
1088    /// Section payload bytes.
1089    pub payload: &'a [u8],
1090}
1091
1092/// Validated plan that can compute its encoded length and write itself.
1093///
1094/// `SnapshotPlan` performs all kind-order, alignment, and count checks at
1095/// construction. After construction, [`encoded_len`](Self::encoded_len)
1096/// and [`write_into`](Self::write_into) are guaranteed to succeed for any
1097/// caller-supplied buffer that is at least `encoded_len()` bytes long.
1098///
1099/// # Performance
1100///
1101/// Construction is `O(s)` for `s` sections. `encoded_len` is `O(s)`;
1102/// `write_into` is `O(s + total payload bytes)` (payload copies plus one
1103/// checksum fold per section).
1104#[derive(Clone, Copy, Debug)]
1105pub struct SnapshotPlan<'a> {
1106    /// Borrowed pending section descriptors, in declaration order.
1107    sections: &'a [PendingSection<'a>],
1108}
1109
1110impl<'a> SnapshotPlan<'a> {
1111    /// Validates a slice of pending sections and constructs a plan.
1112    ///
1113    /// # Errors
1114    ///
1115    /// Returns [`PlanError`] when alignment is too large, too many sections
1116    /// are supplied, or the kinds are not in strictly-ascending order.
1117    ///
1118    /// # Performance
1119    ///
1120    /// This function is `O(s)` for `s` sections.
1121    pub fn new(sections: &'a [PendingSection<'a>]) -> Result<Self, PlanError> {
1122        if sections.len() > MAX_SECTION_COUNT as usize {
1123            return Err(PlanError::TooManySections {
1124                count: sections.len(),
1125            });
1126        }
1127
1128        let mut prev_kind: Option<u32> = None;
1129        for section in sections {
1130            if section.alignment_log2 > MAX_ALIGNMENT_LOG2 {
1131                return Err(PlanError::AlignmentTooLarge {
1132                    alignment_log2: section.alignment_log2,
1133                });
1134            }
1135            if let Some(prev) = prev_kind
1136                && section.kind <= prev
1137            {
1138                return Err(PlanError::NonAscendingKind {
1139                    kind: section.kind,
1140                    prev,
1141                });
1142            }
1143            prev_kind = Some(section.kind);
1144        }
1145
1146        Ok(Self { sections })
1147    }
1148
1149    /// Returns the number of sections in this plan.
1150    ///
1151    /// # Performance
1152    ///
1153    /// This method is `O(1)`.
1154    #[must_use]
1155    pub const fn section_count(&self) -> usize {
1156        self.sections.len()
1157    }
1158
1159    /// Computes the total bytes the encoded snapshot will occupy.
1160    ///
1161    /// # Errors
1162    ///
1163    /// Returns [`PlanError::PayloadOverflow`] when offset arithmetic
1164    /// exceeds `usize` or `u64` representable values.
1165    ///
1166    /// # Performance
1167    ///
1168    /// This function is `O(s)` for `s` sections.
1169    pub fn encoded_len(&self) -> Result<usize, PlanError> {
1170        let table_len = self
1171            .sections
1172            .len()
1173            .checked_mul(SECTION_ENTRY_SIZE)
1174            .ok_or(PlanError::PayloadOverflow)?;
1175        let mut total = HEADER_SIZE
1176            .checked_add(table_len)
1177            .ok_or(PlanError::PayloadOverflow)?;
1178
1179        for section in self.sections {
1180            total = align_up_checked(total, section.alignment_log2)?;
1181            total = total
1182                .checked_add(section.payload.len())
1183                .ok_or(PlanError::PayloadOverflow)?;
1184        }
1185
1186        u64::try_from(total).map_err(|_error| PlanError::PayloadOverflow)?;
1187        Ok(total)
1188    }
1189
1190    /// Writes the encoded snapshot into `out` and returns the number of
1191    /// bytes written.
1192    ///
1193    /// Each section entry records `checksum(0, payload)`; the header records
1194    /// the table checksum over the entry bytes. Padding bytes between the
1195    /// section table and each section payload are zero-filled
1196    /// deterministically; the resulting bytes are stable for any logical
1197    /// input and checksum function.
1198    ///
1199    /// # Errors
1200    ///
1201    /// Returns [`PlanError::BufferTooSmall`] when `out.len()` is less than
1202    /// [`encoded_len`](Self::encoded_len) or [`PlanError::PayloadOverflow`]
1203    /// when offset arithmetic overflows during the write walk.
1204    ///
1205    /// # Performance
1206    ///
1207    /// This function is `O(s + total payload bytes)` (payload copies plus
1208    /// one checksum fold per section and one over the table).
1209    pub fn write_into(&self, out: &mut [u8], checksum: Checksum32) -> Result<usize, PlanError> {
1210        let needed = self.encoded_len()?;
1211        if out.len() < needed {
1212            return Err(PlanError::BufferTooSmall {
1213                needed,
1214                actual: out.len(),
1215            });
1216        }
1217
1218        let prefix = &mut out[..needed];
1219        prefix.fill(0);
1220
1221        let section_count_u32 = match u32::try_from(self.sections.len()) {
1222            Ok(value) => value,
1223            Err(_error) => {
1224                return Err(PlanError::TooManySections {
1225                    count: self.sections.len(),
1226                });
1227            }
1228        };
1229
1230        let table_start = HEADER_SIZE;
1231        let table_len = self
1232            .sections
1233            .len()
1234            .checked_mul(SECTION_ENTRY_SIZE)
1235            .ok_or(PlanError::PayloadOverflow)?;
1236        let payload_start = table_start
1237            .checked_add(table_len)
1238            .ok_or(PlanError::PayloadOverflow)?;
1239        let mut cursor = payload_start;
1240
1241        for (index, section) in self.sections.iter().enumerate() {
1242            cursor = align_up_checked(cursor, section.alignment_log2)?;
1243            let payload_end = cursor
1244                .checked_add(section.payload.len())
1245                .ok_or(PlanError::PayloadOverflow)?;
1246
1247            let offset_u64 = u64::try_from(cursor).map_err(|_error| PlanError::PayloadOverflow)?;
1248            let length_u64 = u64::try_from(section.payload.len())
1249                .map_err(|_error| PlanError::PayloadOverflow)?;
1250            let entry = RawSectionEntry {
1251                offset: U64::new(offset_u64),
1252                length: U64::new(length_u64),
1253                kind: U32::new(section.kind),
1254                version: U32::new(section.version),
1255                crc32c: U32::new(checksum(0, section.payload)),
1256                alignment_log2: section.alignment_log2,
1257                flags: 0,
1258                reserved: [0; 2],
1259            };
1260            let entry_offset = table_start
1261                .checked_add(
1262                    index
1263                        .checked_mul(SECTION_ENTRY_SIZE)
1264                        .ok_or(PlanError::PayloadOverflow)?,
1265                )
1266                .ok_or(PlanError::PayloadOverflow)?;
1267            prefix[entry_offset..entry_offset + SECTION_ENTRY_SIZE]
1268                .copy_from_slice(entry.as_bytes());
1269
1270            prefix[cursor..payload_end].copy_from_slice(section.payload);
1271            cursor = payload_end;
1272        }
1273
1274        // The header is written last so its `table_crc32c` covers the final
1275        // entry bytes.
1276        let table_crc = table_checksum(&prefix[table_start..payload_start], checksum);
1277        let header = RawHeader {
1278            magic: FORMAT_MAGIC,
1279            format_major: U32::new(FORMAT_MAJOR),
1280            format_minor: U32::new(FORMAT_MINOR),
1281            header_size: U32::new(HEADER_SIZE_U32),
1282            section_count: U32::new(section_count_u32),
1283            table_crc32c: U32::new(table_crc),
1284            reserved: [0; 4],
1285        };
1286        prefix[..HEADER_SIZE].copy_from_slice(header.as_bytes());
1287
1288        Ok(needed)
1289    }
1290}
1291
1292/// Rounds `value` up to the next multiple of `1 << alignment_log2`.
1293///
1294/// # Errors
1295///
1296/// Returns [`PlanError::PayloadOverflow`] on `usize` overflow.
1297///
1298/// # Performance
1299///
1300/// This function is `O(1)`.
1301fn align_up_checked(value: usize, alignment_log2: u8) -> Result<usize, PlanError> {
1302    let alignment = 1usize << alignment_log2;
1303    let mask = alignment - 1;
1304    let added = value.checked_add(mask).ok_or(PlanError::PayloadOverflow)?;
1305    Ok(added & !mask)
1306}
1307
1308/// Write-through snapshot encoder that lays payload bytes out at their final
1309/// offsets in a single buffer.
1310///
1311/// This is the one owning write path: each payload streams directly into the
1312/// final buffer, so peak memory stays at ~1x the encoded size (an
1313/// own-then-copy builder would hold ~2x at finish).
1314///
1315/// The table region is reserved up-front for `max_sections` entries; sections
1316/// written beyond the reservation are rejected. When fewer sections are
1317/// written, the unused table slots remain zero between the table and the first
1318/// payload — the same class of never-dereferenced bytes as alignment padding
1319/// (every entry offset stays in bounds and monotonic, so validation accepts
1320/// the layout). Writing exactly `max_sections` sections produces bytes
1321/// identical to [`SnapshotPlan::write_into`] for the same logical input.
1322///
1323/// # Performance
1324///
1325/// Each write appends `O(written bytes)`; [`finish`](Self::finish) is `O(s)`
1326/// for `s` sections (header + table patch, no payload copy).
1327#[cfg(feature = "alloc")]
1328#[derive(Clone, Debug)]
1329#[must_use]
1330pub struct SnapshotWriter {
1331    /// Final snapshot bytes, laid out in place from byte zero (header and
1332    /// table are zero until [`Self::finish`] patches them).
1333    buf: Vec<u8>,
1334    /// Staged section entries, patched into the reserved table at finish.
1335    entries: Vec<RawSectionEntry>,
1336    /// Reserved table capacity in entries.
1337    max_sections: usize,
1338    /// Checksum fold recorded into every section entry and the header.
1339    checksum: Checksum32,
1340}
1341
1342#[cfg(feature = "alloc")]
1343impl SnapshotWriter {
1344    /// Constructs a writer whose table region reserves `max_sections` entries
1345    /// and which folds checksums with `checksum`.
1346    ///
1347    /// v2 checksums are mandatory: every section's payload CRC is tracked
1348    /// incrementally as bytes are written and recorded at
1349    /// [`SectionSink::end`]; [`Self::finish`] records the table checksum.
1350    ///
1351    /// # Errors
1352    ///
1353    /// Returns [`PlanError::TooManySections`] when `max_sections` exceeds
1354    /// [`MAX_SECTION_COUNT`].
1355    ///
1356    /// # Performance
1357    ///
1358    /// This function is `O(reserved table bytes)` (one zero-filled
1359    /// allocation).
1360    pub fn new(max_sections: usize, checksum: Checksum32) -> Result<Self, PlanError> {
1361        Self::with_payload_capacity(max_sections, 0, checksum)
1362    }
1363
1364    /// Constructs a writer reserving `max_sections` table entries and
1365    /// pre-allocating `payload_capacity` additional buffer bytes.
1366    ///
1367    /// # Errors
1368    ///
1369    /// Returns [`PlanError::TooManySections`] when `max_sections` exceeds
1370    /// [`MAX_SECTION_COUNT`].
1371    ///
1372    /// # Performance
1373    ///
1374    /// This function is `O(reserved table bytes)` (one zero-filled
1375    /// allocation).
1376    pub fn with_payload_capacity(
1377        max_sections: usize,
1378        payload_capacity: usize,
1379        checksum: Checksum32,
1380    ) -> Result<Self, PlanError> {
1381        if max_sections > MAX_SECTION_COUNT as usize {
1382            return Err(PlanError::TooManySections {
1383                count: max_sections,
1384            });
1385        }
1386        let table_len = max_sections
1387            .checked_mul(SECTION_ENTRY_SIZE)
1388            .ok_or(PlanError::PayloadOverflow)?;
1389        let reserved = HEADER_SIZE
1390            .checked_add(table_len)
1391            .ok_or(PlanError::PayloadOverflow)?;
1392        let mut buf = Vec::with_capacity(reserved.saturating_add(payload_capacity));
1393        buf.resize(reserved, 0);
1394        Ok(Self {
1395            buf,
1396            entries: Vec::with_capacity(max_sections),
1397            max_sections,
1398            checksum,
1399        })
1400    }
1401
1402    /// Starts a section, zero-padding the buffer to the requested alignment,
1403    /// and returns the sink that streams its payload bytes.
1404    ///
1405    /// The section's entry is recorded when the sink's [`SectionSink::end`]
1406    /// is called; a sink dropped without `end` leaves its bytes as
1407    /// never-referenced slack and records no entry.
1408    ///
1409    /// # Errors
1410    ///
1411    /// Returns [`PlanError::AlignmentTooLarge`] when `alignment_log2` exceeds
1412    /// the format cap, [`PlanError::TooManySections`] when the reservation is
1413    /// exhausted, or [`PlanError::NonAscendingKind`] when `kind` is not
1414    /// strictly greater than the previous section's kind (the format's ascending-kind mandate,
1415    /// which also rules out duplicates).
1416    ///
1417    /// # Performance
1418    ///
1419    /// This method is `O(1)` plus `O(padding)` zero fill.
1420    pub fn begin_section(
1421        &mut self,
1422        kind: u32,
1423        version: u32,
1424        alignment_log2: u8,
1425    ) -> Result<SectionSink<'_>, PlanError> {
1426        if alignment_log2 > MAX_ALIGNMENT_LOG2 {
1427            return Err(PlanError::AlignmentTooLarge { alignment_log2 });
1428        }
1429        if self.entries.len() >= self.max_sections {
1430            return Err(PlanError::TooManySections {
1431                count: self.entries.len() + 1,
1432            });
1433        }
1434        if let Some(prior) = self.entries.last() {
1435            let prev = prior.kind.get();
1436            if kind <= prev {
1437                return Err(PlanError::NonAscendingKind { kind, prev });
1438            }
1439        }
1440        let aligned = align_up_checked(self.buf.len(), alignment_log2)?;
1441        self.buf.resize(aligned, 0);
1442        Ok(SectionSink {
1443            start: aligned,
1444            kind,
1445            version,
1446            crc: 0,
1447            alignment_log2,
1448            writer: self,
1449        })
1450    }
1451
1452    /// Writes one whole section whose alignment is derived from `T`, copying
1453    /// the records via [`zerocopy::IntoBytes`] directly into the final buffer.
1454    ///
1455    /// # Errors
1456    ///
1457    /// Returns [`PlanError`] for the same reasons as
1458    /// [`begin_section`](Self::begin_section), plus
1459    /// [`PlanError::AlignmentTooLarge`] when `align_of::<T>()` exceeds the
1460    /// format cap.
1461    ///
1462    /// # Performance
1463    ///
1464    /// This method is `O(s + records.len() * size_of::<T>())`.
1465    pub fn section_typed<T>(
1466        &mut self,
1467        kind: u32,
1468        version: u32,
1469        records: &[T],
1470    ) -> Result<(), PlanError>
1471    where
1472        T: zerocopy::IntoBytes + zerocopy::Immutable,
1473    {
1474        let alignment = core::mem::align_of::<T>();
1475        let alignment_log2 = match u8::try_from(alignment.trailing_zeros()) {
1476            Ok(value) => value,
1477            Err(_error) => {
1478                return Err(PlanError::AlignmentTooLarge {
1479                    alignment_log2: u8::MAX,
1480                });
1481            }
1482        };
1483        let mut sink = self.begin_section(kind, version, alignment_log2)?;
1484        sink.write_typed(records);
1485        sink.end()
1486    }
1487
1488    /// Writes one whole section of raw payload bytes at the requested
1489    /// alignment, copying them directly into the final buffer.
1490    ///
1491    /// Convenience over [`begin_section`](Self::begin_section) + one
1492    /// [`SectionSink::write`] + [`SectionSink::end`].
1493    ///
1494    /// # Errors
1495    ///
1496    /// Returns [`PlanError`] for the same reasons as
1497    /// [`begin_section`](Self::begin_section).
1498    ///
1499    /// # Performance
1500    ///
1501    /// This method is `O(bytes.len())` (one append plus one checksum fold).
1502    pub fn section_bytes(
1503        &mut self,
1504        kind: u32,
1505        version: u32,
1506        alignment_log2: u8,
1507        bytes: &[u8],
1508    ) -> Result<(), PlanError> {
1509        let mut sink = self.begin_section(kind, version, alignment_log2)?;
1510        sink.write(bytes);
1511        sink.end()
1512    }
1513
1514    /// Writes one whole section of explicit little-endian typed words.
1515    ///
1516    /// Prefer [`section_widths`](Self::section_widths), which takes a native
1517    /// index slice and lowers it through `slice_to_le`, enforcing the
1518    /// little-endian guarantee in the type system. This method exists for
1519    /// callers that already hold portable byteorder words such as
1520    /// `zerocopy::byteorder::U32<LE>`; the records are copied via
1521    /// [`zerocopy::IntoBytes`] and the alignment is derived from `T`.
1522    ///
1523    /// # Errors
1524    ///
1525    /// Returns [`PlanError`] for the same reasons as
1526    /// [`section_typed`](Self::section_typed).
1527    ///
1528    /// # Performance
1529    ///
1530    /// This method is `O(records.len() * size_of::<T>())`.
1531    pub fn section_little_endian<T>(
1532        &mut self,
1533        kind: u32,
1534        version: u32,
1535        records: &[T],
1536    ) -> Result<(), PlanError>
1537    where
1538        T: zerocopy::IntoBytes + zerocopy::Immutable,
1539    {
1540        self.section_typed(kind, version, records)
1541    }
1542
1543    /// Writes one whole section from a native-width index slice, lowering it
1544    /// to its explicit little-endian storage words first.
1545    ///
1546    /// Convenience wrapper that calls
1547    /// `oxgraph_layout_util::build::slice_to_le` and then
1548    /// [`section_little_endian`](Self::section_little_endian), so exporters
1549    /// can pass native `&[u32]`-style slices without converting by hand.
1550    /// Requires the `alloc` feature (it allocates the converted words).
1551    ///
1552    /// # Errors
1553    ///
1554    /// Returns [`PlanError`] for the same reasons as
1555    /// [`section_typed`](Self::section_typed).
1556    ///
1557    /// # Performance
1558    ///
1559    /// This method is `O(values.len())` plus one allocation for the
1560    /// converted words.
1561    pub fn section_widths<W>(
1562        &mut self,
1563        kind: u32,
1564        version: u32,
1565        values: &[W],
1566    ) -> Result<(), PlanError>
1567    where
1568        W: SnapshotWidth,
1569    {
1570        let words = oxgraph_layout_util::build::slice_to_le(values);
1571        self.section_little_endian(kind, version, &words)
1572    }
1573
1574    /// Returns the number of sections recorded so far.
1575    ///
1576    /// # Performance
1577    ///
1578    /// This method is `O(1)`.
1579    #[must_use]
1580    pub const fn section_count(&self) -> usize {
1581        self.entries.len()
1582    }
1583
1584    /// Patches the header and the reserved table with the recorded entries and
1585    /// returns the encoded snapshot bytes.
1586    ///
1587    /// The table checksum is computed after the entries are patched, so the
1588    /// header's `table_crc32c` covers the final entry bytes.
1589    ///
1590    /// # Errors
1591    ///
1592    /// Returns [`PlanError::PayloadOverflow`] when the total encoded length is
1593    /// not representable as `u64`.
1594    ///
1595    /// # Performance
1596    ///
1597    /// This method is `O(s)` for `s` sections plus one checksum fold over the
1598    /// table bytes; payload bytes are not copied.
1599    pub fn finish(mut self) -> Result<Vec<u8>, PlanError> {
1600        u64::try_from(self.buf.len()).map_err(|_error| PlanError::PayloadOverflow)?;
1601        let section_count_u32 = match u32::try_from(self.entries.len()) {
1602            Ok(value) => value,
1603            Err(_error) => {
1604                return Err(PlanError::TooManySections {
1605                    count: self.entries.len(),
1606                });
1607            }
1608        };
1609        for (index, entry) in self.entries.iter().enumerate() {
1610            let entry_offset = HEADER_SIZE + index * SECTION_ENTRY_SIZE;
1611            self.buf[entry_offset..entry_offset + SECTION_ENTRY_SIZE]
1612                .copy_from_slice(entry.as_bytes());
1613        }
1614        let table_end = HEADER_SIZE + self.entries.len() * SECTION_ENTRY_SIZE;
1615        let table_crc = table_checksum(&self.buf[HEADER_SIZE..table_end], self.checksum);
1616        let header = RawHeader {
1617            magic: FORMAT_MAGIC,
1618            format_major: U32::new(FORMAT_MAJOR),
1619            format_minor: U32::new(FORMAT_MINOR),
1620            header_size: U32::new(HEADER_SIZE_U32),
1621            section_count: U32::new(section_count_u32),
1622            table_crc32c: U32::new(table_crc),
1623            reserved: [0; 4],
1624        };
1625        self.buf[..HEADER_SIZE].copy_from_slice(header.as_bytes());
1626        Ok(self.buf)
1627    }
1628}
1629
1630/// Streaming payload sink for one in-progress [`SnapshotWriter`] section.
1631///
1632/// Bytes written here land directly at their final offsets in the snapshot
1633/// buffer. [`Self::end`] records the section's table entry; dropping the sink
1634/// without `end` records nothing and leaves the written bytes as
1635/// never-referenced slack.
1636///
1637/// # Performance
1638///
1639/// Each write is `O(written bytes)` (one `Vec` append).
1640#[cfg(feature = "alloc")]
1641#[must_use]
1642pub struct SectionSink<'writer> {
1643    /// Writer whose buffer receives the payload bytes.
1644    writer: &'writer mut SnapshotWriter,
1645    /// Buffer offset where this section's payload starts.
1646    start: usize,
1647    /// Section kind recorded at [`Self::end`].
1648    kind: u32,
1649    /// Section version recorded at [`Self::end`].
1650    version: u32,
1651    /// Incrementally folded payload CRC-32C, recorded at [`Self::end`].
1652    crc: u32,
1653    /// Declared payload alignment recorded at [`Self::end`].
1654    alignment_log2: u8,
1655}
1656
1657#[cfg(feature = "alloc")]
1658impl SectionSink<'_> {
1659    /// Appends raw payload bytes to this section, folding them into the
1660    /// section's incremental payload checksum.
1661    ///
1662    /// # Performance
1663    ///
1664    /// This method is `O(bytes.len())` (one `Vec` append plus one checksum
1665    /// fold).
1666    pub fn write(&mut self, bytes: &[u8]) {
1667        self.crc = (self.writer.checksum)(self.crc, bytes);
1668        self.writer.buf.extend_from_slice(bytes);
1669    }
1670
1671    /// Appends typed records to this section via [`zerocopy::IntoBytes`].
1672    ///
1673    /// # Performance
1674    ///
1675    /// This method is `O(records.len() * size_of::<T>())`.
1676    pub fn write_typed<T>(&mut self, records: &[T])
1677    where
1678        T: zerocopy::IntoBytes + zerocopy::Immutable,
1679    {
1680        self.write(records.as_bytes());
1681    }
1682
1683    /// Finishes this section, recording its table entry.
1684    ///
1685    /// # Errors
1686    ///
1687    /// Returns [`PlanError::PayloadOverflow`] when the section offset or
1688    /// length is not representable as `u64`.
1689    ///
1690    /// # Performance
1691    ///
1692    /// This method is `O(1)`.
1693    pub fn end(self) -> Result<(), PlanError> {
1694        let offset = u64::try_from(self.start).map_err(|_error| PlanError::PayloadOverflow)?;
1695        let length = u64::try_from(self.writer.buf.len() - self.start)
1696            .map_err(|_error| PlanError::PayloadOverflow)?;
1697        self.writer.entries.push(RawSectionEntry {
1698            offset: U64::new(offset),
1699            length: U64::new(length),
1700            kind: U32::new(self.kind),
1701            version: U32::new(self.version),
1702            crc32c: U32::new(self.crc),
1703            alignment_log2: self.alignment_log2,
1704            flags: 0,
1705            reserved: [0; 2],
1706        });
1707        Ok(())
1708    }
1709}
1710
1711/// Recomputes and patches one section's entry CRC-32C (and the header's
1712/// `table_crc32c`, which covers that entry) in already-encoded snapshot
1713/// bytes.
1714///
1715/// This is the escape hatch for producers that must patch a section's
1716/// payload *after* encoding — e.g. a trailer whose payload is derived from
1717/// the encoded bytes themselves. After mutating the payload in place, call
1718/// this to restore the checksum invariants for that section and the
1719/// table. All other entries are left untouched.
1720///
1721/// # Errors
1722///
1723/// Returns any structural [`SnapshotError`] from [`Snapshot::open`], or
1724/// [`SnapshotError::SectionMissing`] when no section has `kind`.
1725///
1726/// # Performance
1727///
1728/// This function is `O(s + section payload bytes)`: one structural open,
1729/// one fold over the section's payload, and one fold over the table bytes.
1730pub fn patch_section_crc(
1731    bytes: &mut [u8],
1732    kind: u32,
1733    checksum: Checksum32,
1734) -> Result<(), SnapshotError> {
1735    let (entry_offset, payload_crc, section_count) = {
1736        let snapshot = Snapshot::open(bytes)?;
1737        let index = snapshot
1738            .entries
1739            .binary_search_by(|entry| entry.kind.get().cmp(&kind))
1740            .map_err(|_index| SnapshotError::SectionMissing { kind })?;
1741        let section = Section::from_entry(snapshot.bytes, &snapshot.entries[index]);
1742        (
1743            HEADER_SIZE + index * SECTION_ENTRY_SIZE,
1744            checksum(0, section.bytes()),
1745            snapshot.entries.len(),
1746        )
1747    };
1748
1749    // Patch the entry's crc32c word.
1750    let crc_field_offset = entry_offset + core::mem::offset_of!(RawSectionEntry, crc32c);
1751    bytes[crc_field_offset..crc_field_offset + 4]
1752        .copy_from_slice(U32::<LE>::new(payload_crc).as_bytes());
1753
1754    // The entry changed, so recompute the table checksum and patch the
1755    // header's table_crc32c word.
1756    let table_end = HEADER_SIZE + section_count * SECTION_ENTRY_SIZE;
1757    let table_crc = table_checksum(&bytes[HEADER_SIZE..table_end], checksum);
1758    let header_crc_offset = core::mem::offset_of!(RawHeader, table_crc32c);
1759    bytes[header_crc_offset..header_crc_offset + 4]
1760        .copy_from_slice(U32::<LE>::new(table_crc).as_bytes());
1761    Ok(())
1762}