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}