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