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;