Skip to main content

ic_backup/model/ic_snapshot_reply/
mod.rs

1//! Bounded capture/inventory reply decoding; transport authenticity stays external.
2
3mod wire;
4pub(crate) use wire::snapshot_id;
5
6use super::{
7    artifacts::ArtifactChecksumRecord,
8    ic_request::{IcManagementMethodRecord, IcManagementRequestRecord},
9};
10use thiserror::Error;
11
12/// Maximum raw Candid reply bytes admitted before decoding.
13pub const MAX_IC_SNAPSHOT_REPLY_BYTES: usize = 1024 * 1024;
14/// Maximum retained inventory entries; this is a local bound, not an IC capacity claim.
15pub const MAX_IC_SNAPSHOT_REPLY_ENTRIES: usize = 1024;
16
17/// Exact decoded snapshot identity and declared timestamp/size.
18///
19/// Raw identifiers remain opaque bytes. Timestamp and size may be zero or any
20/// nat64 value; neither proves artifact completeness or unique effect settlement.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct IcSnapshotInfo {
23    id: Vec<u8>,
24    taken_at_timestamp: u64,
25    total_size: u64,
26}
27
28impl IcSnapshotInfo {
29    /// Read exact raw snapshot bytes, without interpreting a backend token.
30    #[must_use]
31    pub fn id(&self) -> &[u8] {
32        &self.id
33    }
34
35    /// Read the reply's declared snapshot timestamp in nanoseconds.
36    #[must_use]
37    pub const fn taken_at_timestamp(&self) -> u64 {
38        self.taken_at_timestamp
39    }
40
41    /// Read the reply's declared snapshot size, distinct from local artifact bytes.
42    #[must_use]
43    pub const fn total_size(&self) -> u64 {
44        self.total_size
45    }
46}
47
48/// Read-only decoded reply associated with the caller's exact request.
49///
50/// Capture has one entry; inventory has 0..=1,024 unique entries in raw-ID order.
51/// The wire contains no network, caller or target, so this association and digest
52/// do not authenticate response origin, freshness, permissions or execution.
53/// No Serde admission, completion receipt, reservation or settlement is provided.
54#[derive(Debug)]
55pub struct IcSnapshotReply<'request> {
56    request: &'request IcManagementRequestRecord,
57    snapshots: Vec<IcSnapshotInfo>,
58    payload_checksum: ArtifactChecksumRecord,
59}
60
61impl<'request> IcSnapshotReply<'request> {
62    /// Decode the pinned capture or inventory result under finite local bounds.
63    ///
64    /// Exactly one Candid argument with the required snapshot fields is admitted.
65    /// Extra arguments, skipped fields, trailing bytes and duplicate IDs reject.
66    /// The integration must independently qualify the transport/request association.
67    ///
68    /// # Errors
69    /// Returns typed unsupported-method, raw-size, invalid-reply or duplicate-ID
70    /// failures. No journal or caller-owned state changes on any outcome.
71    pub fn decode(
72        request: &'request IcManagementRequestRecord,
73        bytes: &[u8],
74    ) -> Result<Self, IcSnapshotReplyError> {
75        let method = request.method();
76        if !matches!(
77            method,
78            IcManagementMethodRecord::TakeCanisterSnapshot
79                | IcManagementMethodRecord::ListCanisterSnapshots
80        ) {
81            return Err(IcSnapshotReplyError::UnsupportedMethod { method });
82        }
83        if bytes.len() > MAX_IC_SNAPSHOT_REPLY_BYTES {
84            return Err(IcSnapshotReplyError::ReplyTooLarge);
85        }
86        let mut snapshots = wire::decode(method, bytes)?;
87        snapshots.sort_unstable_by(|left, right| left.id.cmp(&right.id));
88        if snapshots.windows(2).any(|pair| pair[0].id == pair[1].id) {
89            return Err(IcSnapshotReplyError::DuplicateSnapshotId);
90        }
91        Ok(Self {
92            request,
93            snapshots,
94            payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
95        })
96    }
97
98    /// Read the original request; its owner supplies exact target/method/byte identity.
99    #[must_use]
100    pub const fn request(&self) -> &'request IcManagementRequestRecord {
101        self.request
102    }
103
104    /// Read the capture singleton or canonical inventory without mutation access.
105    #[must_use]
106    pub fn snapshots(&self) -> &[IcSnapshotInfo] {
107        &self.snapshots
108    }
109
110    /// Read the SHA-256 checksum of the exact raw reply, including its wire ordering.
111    #[must_use]
112    pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
113        &self.payload_checksum
114    }
115
116    /// Hash the exact request digest and raw-reply checksum in a separate v1 domain.
117    ///
118    /// Two fixed 64-byte lowercase SHA-256 strings follow the domain. A canonical
119    /// inventory view does not erase raw wire differences or request identity.
120    /// This is local evidence identity, not an authenticated IC receipt.
121    #[must_use]
122    pub fn digest(&self) -> ArtifactChecksumRecord {
123        let mut bytes = b"ic-backup/ic-snapshot-reply/v1\0".to_vec();
124        bytes.extend_from_slice(self.request.digest().hash().as_bytes());
125        bytes.extend_from_slice(self.payload_checksum.hash().as_bytes());
126        ArtifactChecksumRecord::from_bytes(&bytes)
127    }
128}
129
130/// Typed snapshot wire admission failure, with no raw payload in diagnostics.
131#[derive(Debug, Error, Eq, PartialEq)]
132pub enum IcSnapshotReplyError {
133    /// Only the existing capture and inventory request methods have this reply shape.
134    #[error("snapshot reply codec does not support {method:?}")]
135    UnsupportedMethod {
136        /// Actual request method, as admitted by the existing request owner.
137        method: IcManagementMethodRecord,
138    },
139    /// Raw input exceeded the local bound before Candid parsing.
140    #[error("snapshot reply exceeds {MAX_IC_SNAPSHOT_REPLY_BYTES} bytes")]
141    ReplyTooLarge,
142    /// Shape, field/count/ID bounds, decoding quota or exact consumption failed.
143    #[error("invalid or unbounded snapshot Candid reply")]
144    InvalidReply,
145    /// Identical raw identifiers appeared more than once, even with different metadata.
146    #[error("snapshot inventory contains duplicate raw identifiers")]
147    DuplicateSnapshotId,
148}
149
150#[cfg(test)]
151mod tests;