Skip to main content

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                region,
57                offset,
58                size,
59            } => (region.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;