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 owning builder.
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::Vec};
14
15use zerocopy::{
16    FromBytes, Immutable, IntoBytes, KnownLayout,
17    byteorder::{LE, U32, U64},
18};
19
20use crate::container_error::{PlanError, SectionViewError, SnapshotError};
21
22/// Magic bytes identifying the topology snapshot container format.
23///
24/// Producers MUST write these eight bytes at offset 0; readers MUST reject
25/// snapshots whose first eight bytes differ.
26///
27/// # Performance
28///
29/// `perf: unspecified`; this is a compile-time constant.
30pub const FORMAT_MAGIC: [u8; 8] = *b"OXGTOPO\0";
31
32/// Format major version this library reads and writes.
33///
34/// A snapshot whose `format_major` field does not equal this constant is
35/// rejected at open time. Major bumps are permitted to break compatibility
36/// in arbitrary ways.
37///
38/// # Performance
39///
40/// `perf: unspecified`; this is a compile-time constant.
41pub const FORMAT_MAJOR: u32 = 1;
42
43/// Format minor version written by this library's builder.
44///
45/// Minor bumps are reserved for backward-compatible additions (e.g. enabling
46/// previously reserved bits or fields). Producers using this library will
47/// emit this value unconditionally.
48///
49/// # Performance
50///
51/// `perf: unspecified`; this is a compile-time constant.
52pub const FORMAT_MINOR: u32 = 0;
53
54/// Highest format minor version this library can read.
55///
56/// Snapshots with `format_minor > MAX_SUPPORTED_MINOR` are rejected at open
57/// time. v1 is intentionally strict; raising this value is a deliberate
58/// per-minor decision once the new minor is proven safely readable here.
59///
60/// # Performance
61///
62/// `perf: unspecified`; this is a compile-time constant.
63pub const MAX_SUPPORTED_MINOR: u32 = 0;
64
65/// Size of the snapshot header in bytes.
66///
67/// # Performance
68///
69/// `perf: unspecified`; this is a compile-time constant.
70pub const HEADER_SIZE: usize = 32;
71
72/// Size of one section table entry in bytes.
73///
74/// # Performance
75///
76/// `perf: unspecified`; this is a compile-time constant.
77pub const SECTION_ENTRY_SIZE: usize = 32;
78
79/// Maximum permitted `alignment_log2` value (2^12 = 4 KiB, page-friendly).
80///
81/// # Performance
82///
83/// `perf: unspecified`; this is a compile-time constant.
84pub const MAX_ALIGNMENT_LOG2: u8 = 12;
85
86/// Maximum permitted section count for v1 snapshots.
87///
88/// Bounds the duplicate-kind detection in `O(s^2)` validation and keeps
89/// kani proofs tractable. Future minors may raise this if validation moves
90/// to a sorted-by-kind side index.
91///
92/// # Performance
93///
94/// `perf: unspecified`; this is a compile-time constant.
95pub const MAX_SECTION_COUNT: u32 = 1024;
96
97/// `HEADER_SIZE` rendered as a `u32` for header-field comparisons.
98const HEADER_SIZE_U32: u32 = 32;
99
100/// Converts a checked `u64` into `usize`, asserting in debug mode that the
101/// value already fits because validation enforced an earlier bound.
102///
103/// # Panics
104///
105/// Panics via `unreachable!()` only on a target where `usize` is narrower
106/// than `u64` AND the caller has supplied a value that was not first vetted
107/// by the snapshot's `Layout` validation pass (which surfaces the failure
108/// as [`SnapshotError::UsizeOverflow`] before any `_validated` call).
109///
110/// # Performance
111///
112/// This function is `O(1)`.
113fn u64_to_usize_validated(value: u64) -> usize {
114    match usize::try_from(value) {
115        Ok(converted) => converted,
116        Err(_error) => unreachable!("validated u64 must fit usize on this target"),
117    }
118}
119
120/// Byte-level snapshot header.
121///
122/// Layout is `#[repr(C)]` with all multi-byte fields stored as zerocopy's
123/// unaligned little-endian wrappers. The struct itself has alignment 1, so
124/// it can be borrowed from any byte slice that is at least `HEADER_SIZE`
125/// long without an alignment check.
126#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes, KnownLayout)]
127#[repr(C)]
128struct RawHeader {
129    /// Magic bytes; must equal [`FORMAT_MAGIC`].
130    magic: [u8; 8],
131    /// Format major version.
132    format_major: U32<LE>,
133    /// Format minor version.
134    format_minor: U32<LE>,
135    /// Header size in bytes; v1.0 mandates `HEADER_SIZE`.
136    header_size: U32<LE>,
137    /// Number of section table entries.
138    section_count: U32<LE>,
139    /// Reserved; must be zero.
140    reserved: [u8; 8],
141}
142
143/// Parses the fixed header from the start of `bytes`.
144///
145/// # Errors
146///
147/// Returns [`SnapshotError::TruncatedHeader`] when fewer than [`HEADER_SIZE`]
148/// bytes are provided. Header field validation is performed separately in
149/// [`validate_magic_versions_reserved`].
150///
151/// # Performance
152///
153/// This function is `O(1)`.
154fn parse_header(bytes: &[u8]) -> Result<(&RawHeader, &[u8]), SnapshotError> {
155    if bytes.len() < HEADER_SIZE {
156        return Err(SnapshotError::TruncatedHeader {
157            needed: HEADER_SIZE,
158            actual: bytes.len(),
159        });
160    }
161
162    match RawHeader::ref_from_prefix(bytes) {
163        Ok((header, rest)) => Ok((header, rest)),
164        Err(_error) => Err(SnapshotError::MalformedHeader),
165    }
166}
167
168/// Validates header magic, version, header size, and reserved bytes.
169///
170/// # Errors
171///
172/// Returns [`SnapshotError`] for any header-level invariant violation.
173///
174/// # Performance
175///
176/// This function is `O(1)`.
177fn validate_magic_versions_reserved(header: &RawHeader) -> Result<(), SnapshotError> {
178    if header.magic != FORMAT_MAGIC {
179        return Err(SnapshotError::BadMagic {
180            actual: header.magic,
181        });
182    }
183
184    let major = header.format_major.get();
185    if major != FORMAT_MAJOR {
186        return Err(SnapshotError::FormatMajorMismatch {
187            actual: major,
188            supported: FORMAT_MAJOR,
189        });
190    }
191
192    let minor = header.format_minor.get();
193    if minor > MAX_SUPPORTED_MINOR {
194        return Err(SnapshotError::FormatMinorTooNew {
195            actual: minor,
196            max_supported: MAX_SUPPORTED_MINOR,
197        });
198    }
199
200    let header_size = header.header_size.get();
201    if header_size != HEADER_SIZE_U32 {
202        return Err(SnapshotError::HeaderSizeMismatch {
203            actual: header_size,
204            expected: HEADER_SIZE_U32,
205        });
206    }
207
208    if header.reserved != [0; 8] {
209        return Err(SnapshotError::NonZeroHeaderReserved);
210    }
211
212    Ok(())
213}
214
215/// Byte-level section table entry.
216///
217/// Layout is `#[repr(C)]` with unaligned little-endian fields, mirroring
218/// [`RawHeader`]'s alignment policy.
219#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes, KnownLayout)]
220#[repr(C)]
221struct RawSectionEntry {
222    /// Byte offset of the section payload from the start of the snapshot.
223    offset: U64<LE>,
224    /// Byte length of the section payload.
225    length: U64<LE>,
226    /// Opaque section kind; the container assigns no semantics.
227    kind: U32<LE>,
228    /// Opaque section version; consumers interpret per kind.
229    version: U32<LE>,
230    /// Reserved bytes for a future per-section CRC32C; must be zero in v1.
231    reserved_checksum: [u8; 4],
232    /// `log2` of the producer's chosen payload alignment; v1 cap is 12.
233    alignment_log2: u8,
234    /// Reserved flag bits; must be zero in v1.
235    flags: u8,
236    /// Trailing reserved bytes; must be zero in v1.
237    reserved: [u8; 2],
238}
239
240/// Borrowed view of one validated section in a snapshot.
241///
242/// A `Section` carries the section's byte payload along with its declared
243/// metadata. Payload bytes are bounds- and overlap-checked at snapshot open
244/// time. Typed-slice access via [`Section::try_as_slice`] verifies the
245/// actual borrowed pointer's alignment at the call site.
246///
247/// # Performance
248///
249/// All methods are `O(1)` or `O(payload.len())` for typed conversions.
250#[derive(Clone, Copy, Debug)]
251pub struct Section<'view> {
252    /// Borrowed payload bytes.
253    payload: &'view [u8],
254    /// Section kind, as recorded in the section entry.
255    kind: u32,
256    /// Section version, as recorded in the section entry.
257    version: u32,
258    /// `log2` of the declared payload alignment.
259    alignment_log2: u8,
260}
261
262impl<'view> Section<'view> {
263    /// Constructs a [`Section`] from a previously validated entry.
264    ///
265    /// # Performance
266    ///
267    /// This function is `O(1)`.
268    #[must_use]
269    fn from_entry(bytes: &'view [u8], entry: &RawSectionEntry) -> Self {
270        let offset = u64_to_usize_validated(entry.offset.get());
271        let length = u64_to_usize_validated(entry.length.get());
272        Self {
273            payload: &bytes[offset..offset + length],
274            kind: entry.kind.get(),
275            version: entry.version.get(),
276            alignment_log2: entry.alignment_log2,
277        }
278    }
279
280    /// Returns the section's opaque kind identifier.
281    ///
282    /// # Performance
283    ///
284    /// This method is `O(1)`.
285    #[must_use]
286    pub const fn kind(&self) -> u32 {
287        self.kind
288    }
289
290    /// Returns the section's opaque version identifier.
291    ///
292    /// # Performance
293    ///
294    /// This method is `O(1)`.
295    #[must_use]
296    pub const fn version(&self) -> u32 {
297        self.version
298    }
299
300    /// Returns the alignment the producer declared for this payload.
301    ///
302    /// This is metadata recorded at build time, not a guarantee about the
303    /// actual borrowed pointer. Callers that intend to interpret the payload
304    /// as a typed slice should prefer [`Section::try_as_slice`], which
305    /// checks the actual payload pointer.
306    ///
307    /// # Performance
308    ///
309    /// This method is `O(1)`.
310    #[must_use]
311    pub const fn declared_alignment(&self) -> usize {
312        1usize << self.alignment_log2
313    }
314
315    /// Returns the section's borrowed payload bytes.
316    ///
317    /// # Performance
318    ///
319    /// This method is `O(1)`.
320    #[must_use]
321    pub const fn bytes(&self) -> &'view [u8] {
322        self.payload
323    }
324
325    /// Borrows the payload as a typed slice of `T`.
326    ///
327    /// Errors if (a) `payload.len()` is not a multiple of
328    /// `core::mem::size_of::<T>()` or (b) the payload's actual base address
329    /// does not satisfy `core::mem::align_of::<T>()`. The producer's
330    /// declared `alignment_log2` is not consulted; the actual borrowed
331    /// pointer is checked directly so that mmap'd or sub-sliced inputs
332    /// cannot bypass the check.
333    ///
334    /// # Errors
335    ///
336    /// Returns [`SectionViewError`] when the payload cannot be borrowed
337    /// as `&[T]` without copying.
338    ///
339    /// # Performance
340    ///
341    /// This method is `O(1)` modulo the bounds and alignment checks; it
342    /// performs no allocation and no per-element work.
343    pub fn try_as_slice<T>(&self) -> Result<&'view [T], SectionViewError>
344    where
345        T: zerocopy::FromBytes + zerocopy::Immutable + zerocopy::KnownLayout,
346    {
347        let elem_size = core::mem::size_of::<T>();
348        let length = self.payload.len();
349
350        if elem_size == 0 {
351            return Err(SectionViewError::LengthNotMultipleOfSize { length, elem_size });
352        }
353
354        if !length.is_multiple_of(elem_size) {
355            return Err(SectionViewError::LengthNotMultipleOfSize { length, elem_size });
356        }
357
358        let required = core::mem::align_of::<T>();
359        let ptr_addr = self.payload.as_ptr().addr();
360        if !ptr_addr.is_multiple_of(required) {
361            return Err(SectionViewError::AlignmentMismatch { ptr_addr, required });
362        }
363
364        let count = length / elem_size;
365        match <[T]>::ref_from_bytes_with_elems(self.payload, count) {
366            Ok(slice) => Ok(slice),
367            Err(_error) => Err(SectionViewError::AlignmentMismatch { ptr_addr, required }),
368        }
369    }
370}
371
372/// Validation depth applied at snapshot open time.
373///
374/// Validation responsibilities are layered. Header-only validation is not a
375/// member of this enum; callers wanting it should use
376/// [`HeaderOnlySnapshot::open`] instead, so the type system distinguishes a
377/// section-bearing handle from one whose section table has not been
378/// validated.
379///
380/// - [`SectionTable`](Self::SectionTable) parses the section table and per-entry self-consistency
381///   (alignment bound, reserved bytes zero, flags zero).
382/// - [`Layout`](Self::Layout) is the default; it adds payload bounds, monotonic-offset enforcement,
383///   and duplicate-kind detection.
384///
385/// Topology-level validation (CSR offset monotonicity, hypergraph role
386/// consistency, etc.) is the consumer's responsibility — the container
387/// has no kind registry and cannot validate semantics it does not know.
388///
389/// # Performance
390///
391/// `perf: unspecified`; this is a metadata enum.
392#[non_exhaustive]
393#[derive(Clone, Copy, Debug, Eq, PartialEq)]
394pub enum ValidationLevel {
395    /// Validate header and section table self-consistency.
396    SectionTable,
397    /// Validate header, section table, and full payload layout.
398    Layout,
399}
400
401/// Walks the section table once and checks all v1 invariants.
402///
403/// `bytes` is the entire snapshot byte slice; `entries` is the parsed
404/// section table; `level` controls how deep the walk goes. Header-level
405/// invariants are presumed already validated by the caller.
406///
407/// # Errors
408///
409/// Returns [`SnapshotError`] for any per-entry or layout violation.
410///
411/// # Performance
412///
413/// This function is `O(s)` for the per-entry self-consistency walk and
414/// `O(s^2)` for the duplicate-kind walk at [`ValidationLevel::Layout`].
415fn validate_section_table(
416    bytes: &[u8],
417    entries: &[RawSectionEntry],
418    level: ValidationLevel,
419) -> Result<(), SnapshotError> {
420    for entry in entries {
421        let kind = entry.kind.get();
422        if entry.reserved_checksum != [0; 4] {
423            return Err(SnapshotError::NonZeroEntryChecksum { kind });
424        }
425        if entry.flags != 0 {
426            return Err(SnapshotError::UnsupportedFlags {
427                kind,
428                flags: entry.flags,
429            });
430        }
431        if entry.reserved != [0; 2] {
432            return Err(SnapshotError::NonZeroEntryReserved { kind });
433        }
434        if entry.alignment_log2 > MAX_ALIGNMENT_LOG2 {
435            return Err(SnapshotError::AlignmentLog2TooLarge {
436                kind,
437                alignment_log2: entry.alignment_log2,
438            });
439        }
440    }
441
442    if matches!(level, ValidationLevel::SectionTable) {
443        return Ok(());
444    }
445
446    let snapshot_len = bytes.len() as u64;
447    let header_plus_table = (HEADER_SIZE as u64)
448        .checked_add((entries.len() as u64).saturating_mul(SECTION_ENTRY_SIZE as u64))
449        .ok_or(SnapshotError::SectionRangeOverflow { kind: 0 })?;
450    let mut prev_end = header_plus_table;
451
452    for (index, entry) in entries.iter().enumerate() {
453        let kind = entry.kind.get();
454        let offset = entry.offset.get();
455        let length = entry.length.get();
456        let end = offset
457            .checked_add(length)
458            .ok_or(SnapshotError::SectionRangeOverflow { kind })?;
459        if end > snapshot_len {
460            return Err(SnapshotError::SectionOutOfBounds {
461                kind,
462                offset,
463                length,
464                snapshot_len,
465            });
466        }
467        if offset < prev_end {
468            return Err(SnapshotError::UnsortedSectionTable { index });
469        }
470        prev_end = end;
471
472        for prior in &entries[..index] {
473            if prior.kind.get() == kind {
474                return Err(SnapshotError::DuplicateKind { kind });
475            }
476        }
477    }
478
479    Ok(())
480}
481
482/// Header-only handle to a snapshot's bytes.
483///
484/// `HeaderOnlySnapshot` is the typestate-distinct counterpart to
485/// [`Snapshot`]: it validates only the fixed header (magic, format
486/// versions, header size, reserved bytes) and exposes the format
487/// versions, but it deliberately does not parse or expose the section
488/// table. Callers who only need to inspect format compatibility (e.g.,
489/// to decide whether the snapshot is readable at all) should use this
490/// type rather than asking [`Snapshot`] to skip section validation.
491///
492/// # Performance
493///
494/// [`HeaderOnlySnapshot::open`] is `O(1)` — it does not walk the section
495/// table or payload region. Subsequent accessors are `O(1)`.
496#[derive(Clone, Copy, Debug)]
497pub struct HeaderOnlySnapshot<'view> {
498    /// Borrowed snapshot bytes.
499    bytes: &'view [u8],
500    /// Format major version recorded in the header.
501    format_major: u32,
502    /// Format minor version recorded in the header.
503    format_minor: u32,
504}
505
506impl<'view> HeaderOnlySnapshot<'view> {
507    /// Opens `bytes` as a header-validated snapshot handle.
508    ///
509    /// Validates the magic bytes, format major and minor, header size, and
510    /// reserved bytes only. The section table and payload region are not
511    /// inspected and may still be malformed.
512    ///
513    /// # Errors
514    ///
515    /// Returns [`SnapshotError`] for any header-level invariant violation.
516    ///
517    /// # Performance
518    ///
519    /// This function is `O(1)`.
520    pub fn open(bytes: &'view [u8]) -> Result<Self, SnapshotError> {
521        let (header, _after_header) = parse_header(bytes)?;
522        validate_magic_versions_reserved(header)?;
523        Ok(Self {
524            bytes,
525            format_major: header.format_major.get(),
526            format_minor: header.format_minor.get(),
527        })
528    }
529
530    /// Returns the borrowed snapshot bytes.
531    ///
532    /// # Performance
533    ///
534    /// This method is `O(1)`.
535    #[must_use]
536    pub const fn bytes(&self) -> &'view [u8] {
537        self.bytes
538    }
539
540    /// Returns the format major version recorded in the snapshot header.
541    ///
542    /// # Performance
543    ///
544    /// This method is `O(1)`.
545    #[must_use]
546    pub const fn format_major(&self) -> u32 {
547        self.format_major
548    }
549
550    /// Returns the format minor version recorded in the snapshot header.
551    ///
552    /// # Performance
553    ///
554    /// This method is `O(1)`.
555    #[must_use]
556    pub const fn format_minor(&self) -> u32 {
557        self.format_minor
558    }
559}
560
561/// Validated, borrowed handle to a snapshot's bytes and section table.
562///
563/// A `Snapshot` is constructed via [`Snapshot::open`] (default
564/// [`ValidationLevel::Layout`]) or [`Snapshot::open_with`]. The handle
565/// itself is `Copy` and trivially cheap to pass; cloning it does not
566/// re-validate.
567///
568/// For header-only inspection without parsing the section table, use
569/// [`HeaderOnlySnapshot`] instead — `Snapshot` always carries a validated
570/// section table.
571///
572/// # Performance
573///
574/// Open is `O(s^2)` for `s` sections at [`ValidationLevel::Layout`] due to
575/// duplicate-kind detection, otherwise `O(s)`. Subsequent reads are `O(1)`
576/// to `O(s)` per call. No allocation occurs.
577#[derive(Clone, Copy, Debug)]
578pub struct Snapshot<'view> {
579    /// Borrowed snapshot bytes.
580    bytes: &'view [u8],
581    /// Format major version recorded in the header.
582    format_major: u32,
583    /// Format minor version recorded in the header.
584    format_minor: u32,
585    /// Borrowed, validated section table entries.
586    entries: &'view [RawSectionEntry],
587}
588
589impl<'view> Snapshot<'view> {
590    /// Opens `bytes` as a validated snapshot at [`ValidationLevel::Layout`].
591    ///
592    /// # Errors
593    ///
594    /// Returns [`SnapshotError`] for any header, section table, or layout
595    /// invariant violation.
596    ///
597    /// # Performance
598    ///
599    /// `O(s^2)` for `s` section entries.
600    pub fn open(bytes: &'view [u8]) -> Result<Self, SnapshotError> {
601        Self::open_with(bytes, ValidationLevel::Layout)
602    }
603
604    /// Opens `bytes` as a snapshot validated at the requested level.
605    ///
606    /// `level` selects between [`ValidationLevel::SectionTable`] (per-entry
607    /// self-consistency only) and [`ValidationLevel::Layout`] (full
608    /// payload-bounds and duplicate-kind walk). Header-only validation is
609    /// deliberately not selectable here; callers wanting it should use
610    /// [`HeaderOnlySnapshot::open`].
611    ///
612    /// # Errors
613    ///
614    /// Returns [`SnapshotError`] for any invariant violation visible at
615    /// the requested level.
616    ///
617    /// # Performance
618    ///
619    /// `O(s)` at [`ValidationLevel::SectionTable`], `O(s^2)` at
620    /// [`ValidationLevel::Layout`].
621    pub fn open_with(bytes: &'view [u8], level: ValidationLevel) -> Result<Self, SnapshotError> {
622        let (header, after_header) = parse_header(bytes)?;
623        validate_magic_versions_reserved(header)?;
624
625        let format_major = header.format_major.get();
626        let format_minor = header.format_minor.get();
627
628        let section_count = header.section_count.get();
629        if section_count > MAX_SECTION_COUNT {
630            return Err(SnapshotError::SectionCountTooLarge {
631                count: section_count,
632                max: MAX_SECTION_COUNT,
633            });
634        }
635        let Ok(section_count_usize) = usize::try_from(section_count) else {
636            return Err(SnapshotError::UsizeOverflow {
637                value: u64::from(section_count),
638            });
639        };
640        let Some(table_len) = section_count_usize.checked_mul(SECTION_ENTRY_SIZE) else {
641            return Err(SnapshotError::SectionCountTooLarge {
642                count: section_count,
643                max: MAX_SECTION_COUNT,
644            });
645        };
646        if after_header.len() < table_len {
647            return Err(SnapshotError::TruncatedSectionTable {
648                needed: table_len,
649                actual: after_header.len(),
650            });
651        }
652
653        let table_bytes = &after_header[..table_len];
654        let entries =
655            <[RawSectionEntry]>::ref_from_bytes_with_elems(table_bytes, section_count_usize)
656                .map_err(|_error| SnapshotError::MalformedSectionTable)?;
657
658        validate_section_table(bytes, entries, level)?;
659
660        Ok(Self {
661            bytes,
662            format_major,
663            format_minor,
664            entries,
665        })
666    }
667
668    /// Returns the format major version recorded in the snapshot header.
669    ///
670    /// # Performance
671    ///
672    /// This method is `O(1)`.
673    #[must_use]
674    pub const fn format_major(&self) -> u32 {
675        self.format_major
676    }
677
678    /// Returns the format minor version recorded in the snapshot header.
679    ///
680    /// # Performance
681    ///
682    /// This method is `O(1)`.
683    #[must_use]
684    pub const fn format_minor(&self) -> u32 {
685        self.format_minor
686    }
687
688    /// Returns the number of validated sections.
689    ///
690    /// # Performance
691    ///
692    /// This method is `O(1)`.
693    #[must_use]
694    pub const fn section_count(&self) -> usize {
695        self.entries.len()
696    }
697
698    /// Returns an iterator over all validated sections.
699    ///
700    /// # Performance
701    ///
702    /// Constructing the iterator is `O(1)`; advancing it is `O(1)` per step.
703    #[must_use]
704    pub fn sections(&self) -> SectionIter<'view> {
705        SectionIter {
706            bytes: self.bytes,
707            entries: self.entries.iter(),
708        }
709    }
710
711    /// Returns the section with the given `kind`, when present.
712    ///
713    /// # Performance
714    ///
715    /// This method is `O(s)` for `s` section entries.
716    #[must_use]
717    pub fn section(&self, kind: u32) -> Option<Section<'view>> {
718        let bytes = self.bytes;
719        self.entries.iter().find_map(|entry| {
720            if entry.kind.get() == kind {
721                Some(Section::from_entry(bytes, entry))
722            } else {
723                None
724            }
725        })
726    }
727}
728
729/// Iterator over a snapshot's validated sections.
730///
731/// Yields each [`Section`] in section-table order. The iterator does not
732/// allocate and borrows from the snapshot's underlying byte slice.
733///
734/// # Performance
735///
736/// Advancing the iterator is `O(1)` per step.
737#[derive(Clone, Debug)]
738pub struct SectionIter<'view> {
739    /// Borrowed snapshot bytes.
740    bytes: &'view [u8],
741    /// Remaining section table entries to yield.
742    entries: core::slice::Iter<'view, RawSectionEntry>,
743}
744
745impl<'view> Iterator for SectionIter<'view> {
746    type Item = Section<'view>;
747
748    fn next(&mut self) -> Option<Self::Item> {
749        self.entries
750            .next()
751            .map(|entry| Section::from_entry(self.bytes, entry))
752    }
753
754    fn size_hint(&self) -> (usize, Option<usize>) {
755        self.entries.size_hint()
756    }
757}
758
759impl ExactSizeIterator for SectionIter<'_> {
760    fn len(&self) -> usize {
761        self.entries.len()
762    }
763}
764
765/// Description of one section to include in a snapshot.
766///
767/// Every field is opaque to the encoder. `kind` and `version` are passed
768/// through unchanged; `alignment_log2` controls payload alignment relative
769/// to the snapshot's start; `payload` is the section's raw bytes.
770///
771/// # Performance
772///
773/// `perf: unspecified`; this is a metadata struct.
774#[derive(Clone, Copy, Debug)]
775pub struct PendingSection<'a> {
776    /// Section kind to record in the entry.
777    pub kind: u32,
778    /// Section version to record in the entry.
779    pub version: u32,
780    /// `log2` of the requested payload alignment; capped at
781    /// [`MAX_ALIGNMENT_LOG2`].
782    pub alignment_log2: u8,
783    /// Section payload bytes.
784    pub payload: &'a [u8],
785}
786
787/// Validated plan that can compute its encoded length and write itself.
788///
789/// `SnapshotPlan` performs all duplicate-kind, alignment, and count checks
790/// at construction. After construction, [`encoded_len`](Self::encoded_len)
791/// and [`write_into`](Self::write_into) are guaranteed to succeed for any
792/// caller-supplied buffer that is at least `encoded_len()` bytes long.
793///
794/// # Performance
795///
796/// Construction is `O(s^2)` for `s` sections (duplicate-kind check).
797/// `encoded_len` and `write_into` are `O(s + total payload bytes)`.
798#[derive(Clone, Copy, Debug)]
799pub struct SnapshotPlan<'a> {
800    /// Borrowed pending section descriptors, in declaration order.
801    sections: &'a [PendingSection<'a>],
802}
803
804impl<'a> SnapshotPlan<'a> {
805    /// Validates a slice of pending sections and constructs a plan.
806    ///
807    /// # Errors
808    ///
809    /// Returns [`PlanError`] when alignment is too large, too many sections
810    /// are supplied, or duplicate kinds are present.
811    ///
812    /// # Performance
813    ///
814    /// This function is `O(s^2)` for `s` sections.
815    pub fn new(sections: &'a [PendingSection<'a>]) -> Result<Self, PlanError> {
816        if sections.len() > MAX_SECTION_COUNT as usize {
817            return Err(PlanError::TooManySections {
818                count: sections.len(),
819            });
820        }
821
822        for (index, section) in sections.iter().enumerate() {
823            if section.alignment_log2 > MAX_ALIGNMENT_LOG2 {
824                return Err(PlanError::AlignmentTooLarge {
825                    alignment_log2: section.alignment_log2,
826                });
827            }
828            if sections[..index]
829                .iter()
830                .any(|prior| prior.kind == section.kind)
831            {
832                return Err(PlanError::DuplicateKind { kind: section.kind });
833            }
834        }
835
836        Ok(Self { sections })
837    }
838
839    /// Returns the number of sections in this plan.
840    ///
841    /// # Performance
842    ///
843    /// This method is `O(1)`.
844    #[must_use]
845    pub const fn section_count(&self) -> usize {
846        self.sections.len()
847    }
848
849    /// Computes the total bytes the encoded snapshot will occupy.
850    ///
851    /// # Errors
852    ///
853    /// Returns [`PlanError::PayloadOverflow`] when offset arithmetic
854    /// exceeds `usize` or `u64` representable values.
855    ///
856    /// # Performance
857    ///
858    /// This function is `O(s)` for `s` sections.
859    pub fn encoded_len(&self) -> Result<usize, PlanError> {
860        let table_len = self
861            .sections
862            .len()
863            .checked_mul(SECTION_ENTRY_SIZE)
864            .ok_or(PlanError::PayloadOverflow)?;
865        let mut total = HEADER_SIZE
866            .checked_add(table_len)
867            .ok_or(PlanError::PayloadOverflow)?;
868
869        for section in self.sections {
870            total = align_up_checked(total, section.alignment_log2)?;
871            total = total
872                .checked_add(section.payload.len())
873                .ok_or(PlanError::PayloadOverflow)?;
874        }
875
876        u64::try_from(total).map_err(|_error| PlanError::PayloadOverflow)?;
877        Ok(total)
878    }
879
880    /// Writes the encoded snapshot into `out` and returns the number of
881    /// bytes written.
882    ///
883    /// Padding bytes between the section table and each section payload
884    /// are zero-filled deterministically; the resulting bytes are stable
885    /// for any logical input.
886    ///
887    /// # Errors
888    ///
889    /// Returns [`PlanError::BufferTooSmall`] when `out.len()` is less than
890    /// [`encoded_len`](Self::encoded_len) or [`PlanError::PayloadOverflow`]
891    /// when offset arithmetic overflows during the write walk.
892    ///
893    /// # Performance
894    ///
895    /// This function is `O(s + total payload bytes)`.
896    pub fn write_into(&self, out: &mut [u8]) -> Result<usize, PlanError> {
897        let needed = self.encoded_len()?;
898        if out.len() < needed {
899            return Err(PlanError::BufferTooSmall {
900                needed,
901                actual: out.len(),
902            });
903        }
904
905        let prefix = &mut out[..needed];
906        prefix.fill(0);
907
908        let section_count_u32 = match u32::try_from(self.sections.len()) {
909            Ok(value) => value,
910            Err(_error) => {
911                return Err(PlanError::TooManySections {
912                    count: self.sections.len(),
913                });
914            }
915        };
916        let header = RawHeader {
917            magic: FORMAT_MAGIC,
918            format_major: U32::new(FORMAT_MAJOR),
919            format_minor: U32::new(FORMAT_MINOR),
920            header_size: U32::new(HEADER_SIZE_U32),
921            section_count: U32::new(section_count_u32),
922            reserved: [0; 8],
923        };
924        prefix[..HEADER_SIZE].copy_from_slice(header.as_bytes());
925
926        let table_start = HEADER_SIZE;
927        let payload_start = table_start
928            .checked_add(
929                self.sections
930                    .len()
931                    .checked_mul(SECTION_ENTRY_SIZE)
932                    .ok_or(PlanError::PayloadOverflow)?,
933            )
934            .ok_or(PlanError::PayloadOverflow)?;
935        let mut cursor = payload_start;
936
937        for (index, section) in self.sections.iter().enumerate() {
938            cursor = align_up_checked(cursor, section.alignment_log2)?;
939            let payload_end = cursor
940                .checked_add(section.payload.len())
941                .ok_or(PlanError::PayloadOverflow)?;
942
943            let offset_u64 = u64::try_from(cursor).map_err(|_error| PlanError::PayloadOverflow)?;
944            let length_u64 = u64::try_from(section.payload.len())
945                .map_err(|_error| PlanError::PayloadOverflow)?;
946            let entry = RawSectionEntry {
947                offset: U64::new(offset_u64),
948                length: U64::new(length_u64),
949                kind: U32::new(section.kind),
950                version: U32::new(section.version),
951                reserved_checksum: [0; 4],
952                alignment_log2: section.alignment_log2,
953                flags: 0,
954                reserved: [0; 2],
955            };
956            let entry_offset = table_start
957                .checked_add(
958                    index
959                        .checked_mul(SECTION_ENTRY_SIZE)
960                        .ok_or(PlanError::PayloadOverflow)?,
961                )
962                .ok_or(PlanError::PayloadOverflow)?;
963            prefix[entry_offset..entry_offset + SECTION_ENTRY_SIZE]
964                .copy_from_slice(entry.as_bytes());
965
966            prefix[cursor..payload_end].copy_from_slice(section.payload);
967            cursor = payload_end;
968        }
969
970        Ok(needed)
971    }
972}
973
974/// Rounds `value` up to the next multiple of `1 << alignment_log2`.
975///
976/// # Errors
977///
978/// Returns [`PlanError::PayloadOverflow`] on `usize` overflow.
979///
980/// # Performance
981///
982/// This function is `O(1)`.
983fn align_up_checked(value: usize, alignment_log2: u8) -> Result<usize, PlanError> {
984    let alignment = 1usize << alignment_log2;
985    let mask = alignment - 1;
986    let added = value.checked_add(mask).ok_or(PlanError::PayloadOverflow)?;
987    Ok(added & !mask)
988}
989
990/// One owned section pending in a [`SnapshotBuilder`].
991#[cfg(feature = "alloc")]
992#[derive(Clone, Debug)]
993struct OwnedSection {
994    /// Section kind.
995    kind: u32,
996    /// Section version.
997    version: u32,
998    /// Declared payload alignment as `log2`.
999    alignment_log2: u8,
1000    /// Owned payload bytes.
1001    payload: Vec<u8>,
1002}
1003
1004/// Owning snapshot builder that produces a `Vec<u8>` on finish.
1005///
1006/// The builder rejects duplicate kinds, alignment overflows, and section
1007/// count overflows at `add_section*` time, so the only failure that can
1008/// reach [`SnapshotBuilder::finish`] is [`PlanError::PayloadOverflow`] —
1009/// when the cumulative encoded length would overflow `u64` or `usize`.
1010///
1011/// # Performance
1012///
1013/// `add_section*` methods are `O(s)` for the in-progress section count.
1014/// [`finish`](Self::finish) is `O(s + total payload bytes)`.
1015#[cfg(feature = "alloc")]
1016#[derive(Clone, Debug, Default)]
1017#[must_use]
1018pub struct SnapshotBuilder {
1019    /// Owned, in-order sections.
1020    sections: Vec<OwnedSection>,
1021}
1022
1023#[cfg(feature = "alloc")]
1024impl SnapshotBuilder {
1025    /// Constructs an empty builder.
1026    ///
1027    /// # Performance
1028    ///
1029    /// This function is `O(1)`.
1030    pub fn new() -> Self {
1031        Self::default()
1032    }
1033
1034    /// Appends a section with the given metadata and owned payload.
1035    ///
1036    /// # Errors
1037    ///
1038    /// Returns [`PlanError`] when `alignment_log2` is too large, when the
1039    /// builder already holds the maximum permitted section count, or when
1040    /// `kind` collides with an earlier section.
1041    ///
1042    /// # Performance
1043    ///
1044    /// This method is `O(s)` for the in-progress section count.
1045    pub fn add_section(
1046        &mut self,
1047        kind: u32,
1048        version: u32,
1049        alignment_log2: u8,
1050        payload: Vec<u8>,
1051    ) -> Result<&mut Self, PlanError> {
1052        if alignment_log2 > MAX_ALIGNMENT_LOG2 {
1053            return Err(PlanError::AlignmentTooLarge { alignment_log2 });
1054        }
1055        if self.sections.len() >= MAX_SECTION_COUNT as usize {
1056            return Err(PlanError::TooManySections {
1057                count: self.sections.len() + 1,
1058            });
1059        }
1060        for prior in &self.sections {
1061            if prior.kind == kind {
1062                return Err(PlanError::DuplicateKind { kind });
1063            }
1064        }
1065        self.sections.push(OwnedSection {
1066            kind,
1067            version,
1068            alignment_log2,
1069            payload,
1070        });
1071        Ok(self)
1072    }
1073
1074    /// Appends a section whose alignment is derived from `T`.
1075    ///
1076    /// The payload is copied via [`zerocopy::IntoBytes`].
1077    ///
1078    /// # Errors
1079    ///
1080    /// Returns [`PlanError`] for the same reasons as
1081    /// [`add_section`](Self::add_section), plus
1082    /// [`PlanError::AlignmentTooLarge`] when `align_of::<T>()` exceeds
1083    /// the v1 cap.
1084    ///
1085    /// # Performance
1086    ///
1087    /// This method is `O(s + payload.len() * size_of::<T>())`.
1088    pub fn add_section_typed<T>(
1089        &mut self,
1090        kind: u32,
1091        version: u32,
1092        payload: &[T],
1093    ) -> Result<&mut Self, PlanError>
1094    where
1095        T: zerocopy::IntoBytes + zerocopy::Immutable,
1096    {
1097        let alignment = core::mem::align_of::<T>();
1098        let alignment_log2 = match u8::try_from(alignment.trailing_zeros()) {
1099            Ok(value) => value,
1100            Err(_error) => {
1101                return Err(PlanError::AlignmentTooLarge {
1102                    alignment_log2: u8::MAX,
1103                });
1104            }
1105        };
1106        if alignment_log2 > MAX_ALIGNMENT_LOG2 {
1107            return Err(PlanError::AlignmentTooLarge { alignment_log2 });
1108        }
1109        let bytes = payload.as_bytes().to_vec();
1110        self.add_section(kind, version, alignment_log2, bytes)
1111    }
1112
1113    /// Appends a section containing explicit little-endian typed words.
1114    ///
1115    /// This is a naming-level guardrail for snapshot exporters: callers should
1116    /// pass portable byteorder words such as `zerocopy::byteorder::U32<LE>`,
1117    /// not native integer slices. The payload is copied via
1118    /// [`zerocopy::IntoBytes`] and the alignment is derived from `T`.
1119    ///
1120    /// # Errors
1121    ///
1122    /// Returns [`PlanError`] for the same reasons as
1123    /// [`add_section_typed`](Self::add_section_typed).
1124    ///
1125    /// # Performance
1126    ///
1127    /// This method is `O(s + payload.len() * size_of::<T>())`.
1128    pub fn add_section_little_endian<T>(
1129        &mut self,
1130        kind: u32,
1131        version: u32,
1132        payload: &[T],
1133    ) -> Result<&mut Self, PlanError>
1134    where
1135        T: zerocopy::IntoBytes + zerocopy::Immutable,
1136    {
1137        self.add_section_typed(kind, version, payload)
1138    }
1139
1140    /// Returns the number of pending sections.
1141    ///
1142    /// # Performance
1143    ///
1144    /// This method is `O(1)`.
1145    #[must_use]
1146    pub const fn section_count(&self) -> usize {
1147        self.sections.len()
1148    }
1149
1150    /// Encodes the pending sections into an owned snapshot byte vector.
1151    ///
1152    /// The builder enforces per-insert invariants
1153    /// ([`PlanError::AlignmentTooLarge`], [`PlanError::TooManySections`],
1154    /// [`PlanError::DuplicateKind`]) on `add_section*`, so this method
1155    /// only fails when the cumulative payload arithmetic overflows
1156    /// (`u64`/`usize`), surfaced as [`PlanError::PayloadOverflow`].
1157    ///
1158    /// # Errors
1159    ///
1160    /// Returns [`PlanError::PayloadOverflow`] when the total encoded length
1161    /// would overflow `u64` or `usize`. All other [`PlanError`] variants
1162    /// are caught at `add_section*` time and cannot reach this method.
1163    ///
1164    /// # Performance
1165    ///
1166    /// This method is `O(s + total payload bytes)`.
1167    pub fn finish(self) -> Result<Vec<u8>, PlanError> {
1168        let pending: Vec<PendingSection<'_>> = self
1169            .sections
1170            .iter()
1171            .map(|section| PendingSection {
1172                kind: section.kind,
1173                version: section.version,
1174                alignment_log2: section.alignment_log2,
1175                payload: section.payload.as_slice(),
1176            })
1177            .collect();
1178
1179        let plan = SnapshotPlan::new(&pending)?;
1180        let needed = plan.encoded_len()?;
1181        let mut out = vec![0u8; needed];
1182        plan.write_into(&mut out)?;
1183        Ok(out)
1184    }
1185}