ic_backup/model/ic_snapshot_coverage/mod.rs
1//! Incremental coverage of admitted snapshot replies, without IO or durable progress.
2
3use super::{ic_snapshot_data::IcSnapshotDataReply, ic_snapshot_metadata::IcSnapshotMetadataReply};
4use ic_management_canister_types::SnapshotDataKind;
5use std::fmt;
6use thiserror::Error;
7
8/// Ephemeral coverage of exact retained metadata by already decoded data replies.
9///
10/// Each region advances contiguously from zero; regions may be interleaved and
11/// chunk-store replies may arrive in any order. Retained state is three nat64
12/// cursors and at most 1,024 chunk-presence bits, regardless of snapshot size.
13/// Admission retains no data bytes and performs no IO or provider calls. It does
14/// not establish authenticated origin, byte custody, durable storage, accounting
15/// or permission to repeat reads. Reconstructing this value starts empty; it is
16/// never a resume journal or a substitute for one.
17pub struct IcSnapshotDataCoverage<'metadata> {
18 metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
19 regions: [u64; 3],
20 chunks: Vec<bool>,
21}
22
23impl<'metadata> IcSnapshotDataCoverage<'metadata> {
24 /// Start empty coverage under one exact original metadata evidence owner.
25 #[must_use]
26 pub fn new(metadata: &'metadata IcSnapshotMetadataReply<'metadata>) -> Self {
27 Self {
28 metadata,
29 regions: [0; 3],
30 chunks: vec![false; metadata.metadata().wasm_chunk_store.len()],
31 }
32 }
33
34 /// Admit one validated reply, advancing only its region or exact chunk bit.
35 ///
36 /// Reply bytes may be dropped afterwards. That permits bounded streaming,
37 /// but coverage alone therefore makes no retained-byte or transfer attestation.
38 /// The order required within a region is a local admission order, not a grant
39 /// of remote dispatch order or another accounted read.
40 ///
41 /// # Errors
42 /// Rejects different metadata request/raw-reply evidence, gaps, overlaps,
43 /// duplicate ranges or duplicate chunk identities. Every error preserves
44 /// the previous coverage unchanged.
45 pub fn admit(
46 &mut self,
47 reply: &IcSnapshotDataReply<'_, '_>,
48 ) -> Result<(), IcSnapshotDataCoverageError> {
49 if reply.request().metadata().digest() != self.metadata.digest() {
50 return Err(IcSnapshotDataCoverageError::MetadataMismatch);
51 }
52 let (index, offset, size) = match reply.request().kind() {
53 SnapshotDataKind::WasmModule { offset, size } => (0, *offset, *size),
54 SnapshotDataKind::WasmMemory { offset, size } => (1, *offset, *size),
55 SnapshotDataKind::StableMemory { offset, size } => (2, *offset, *size),
56 SnapshotDataKind::WasmChunk { hash } => {
57 let index = self
58 .metadata
59 .metadata()
60 .wasm_chunk_store
61 .iter()
62 .position(|chunk| chunk.hash == *hash)
63 .ok_or(IcSnapshotDataCoverageError::MetadataMismatch)?;
64 if self.chunks[index] {
65 return Err(IcSnapshotDataCoverageError::DuplicateChunk);
66 }
67 self.chunks[index] = true;
68 return Ok(());
69 }
70 };
71 if offset != self.regions[index] {
72 return Err(IcSnapshotDataCoverageError::NoncontiguousRange);
73 }
74 // Request admission already checked the exact metadata region and nat64
75 // addition. Keep this transition fallible without a second range policy.
76 let end = offset
77 .checked_add(size)
78 .ok_or(IcSnapshotDataCoverageError::NoncontiguousRange)?;
79 self.regions[index] = end;
80 Ok(())
81 }
82
83 /// Read exact original metadata, including non-data globals and optional fields.
84 #[must_use]
85 pub const fn metadata(&self) -> &'metadata IcSnapshotMetadataReply<'metadata> {
86 self.metadata
87 }
88
89 /// Read admitted module, heap and stable byte counts in that order.
90 #[must_use]
91 pub const fn covered_region_bytes(&self) -> [u64; 3] {
92 self.regions
93 }
94
95 /// Read the count of distinct admitted chunk-store replies, including empty chunks.
96 #[must_use]
97 pub fn covered_chunks(&self) -> usize {
98 self.chunks.iter().filter(|covered| **covered).count()
99 }
100
101 /// Project complete declared data coverage, without an effect or storage permit.
102 ///
103 /// Every region must reach its exact declared size and every known chunk must
104 /// have an admitted reply. Empty regions require no read; empty stored chunks
105 /// still require their hash-checked reply. There is no combined-size sum that
106 /// could overflow when independent regions use the full nat64 domain.
107 #[must_use]
108 pub fn complete(&self) -> Option<IcSnapshotDataCoverageView<'_, 'metadata>> {
109 let values = self.metadata.metadata();
110 let sizes = [
111 values.wasm_module_size,
112 values.wasm_memory_size,
113 values.stable_memory_size,
114 ];
115 (self.regions == sizes && self.chunks.iter().all(|covered| *covered))
116 .then_some(IcSnapshotDataCoverageView { coverage: self })
117 }
118}
119
120impl fmt::Debug for IcSnapshotDataCoverage<'_> {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 f.debug_struct("IcSnapshotDataCoverage")
123 .field("covered_region_bytes", &self.regions)
124 .field("covered_chunks", &self.covered_chunks())
125 .finish_non_exhaustive()
126 }
127}
128
129/// Read-only complete coverage of locally admitted replies, borrowing its owner.
130///
131/// This view authenticates no snapshot, retains no bytes and grants no durable
132/// transfer, journal completion, upload/load/start, terminal or release authority.
133#[derive(Debug)]
134pub struct IcSnapshotDataCoverageView<'coverage, 'metadata> {
135 coverage: &'coverage IcSnapshotDataCoverage<'metadata>,
136}
137
138impl<'metadata> IcSnapshotDataCoverageView<'_, 'metadata> {
139 /// Read the exact original metadata whose declared data has been covered.
140 #[must_use]
141 pub const fn metadata(&self) -> &'metadata IcSnapshotMetadataReply<'metadata> {
142 self.coverage.metadata()
143 }
144}
145
146/// Typed coverage rejection without payloads, identifiers or raw decoder details.
147#[derive(Debug, Error, Eq, PartialEq)]
148pub enum IcSnapshotDataCoverageError {
149 /// The request or raw metadata evidence differs from the original owner.
150 #[error("snapshot data coverage metadata mismatch")]
151 MetadataMismatch,
152 /// The next range leaves a gap, overlaps or duplicates admitted bytes.
153 #[error("snapshot data coverage requires the next contiguous range")]
154 NoncontiguousRange,
155 /// The exact stored chunk already has an admitted reply.
156 #[error("snapshot data coverage already contains this chunk")]
157 DuplicateChunk,
158}
159
160#[cfg(test)]
161mod tests;