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}