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