Skip to main content

ic_backup/model/ic_snapshot_metadata/
mod.rs

1//! Bounded snapshot metadata reads; decoded declarations do not prove transfer.
2
3mod wire;
4
5use super::{
6    artifacts::ArtifactChecksumRecord,
7    ic_request::{
8        IcRequestError, MAX_IC_ARGUMENT_BYTES, MAX_IC_SNAPSHOT_ID_BYTES, management_request_digest,
9    },
10};
11use candid::Principal;
12use ic_management_canister_types::{
13    ReadCanisterSnapshotMetadataArgs, ReadCanisterSnapshotMetadataResult,
14};
15use std::fmt;
16use thiserror::Error;
17
18/// Maximum raw metadata reply bytes, checked before decoding.
19pub const MAX_IC_SNAPSHOT_METADATA_BYTES: usize = 1024 * 1024;
20/// Local bound on ordered exported globals, including unavailable slots.
21pub const MAX_IC_SNAPSHOT_GLOBALS: usize = 4096;
22/// Local bound on distinct SHA-256 chunk-store entries.
23pub const MAX_IC_SNAPSHOT_CHUNKS: usize = 1024;
24
25/// Exact ephemeral metadata-read payload for one retained raw snapshot identity.
26///
27/// This separate transfer read leaves the six-method lifecycle/recovery record
28/// unchanged. It carries no plan, reservation, signing, freshness or permissions.
29/// Integrations own authenticated snapshot association and prior per-call accounting.
30#[derive(Clone)]
31pub struct IcSnapshotMetadataRequest {
32    target: String,
33    target_bytes: Vec<u8>,
34    snapshot_id: Vec<u8>,
35    arguments: Vec<u8>,
36}
37
38impl IcSnapshotMetadataRequest {
39    /// Normalize the target and encode the pinned upstream argument shape.
40    ///
41    /// # Errors
42    /// Rejects malformed principals, empty/oversized raw identifiers, encoding
43    /// failure or arguments exceeding the existing 4 KiB request bound.
44    pub fn new(target: &str, snapshot_id: &[u8]) -> Result<Self, IcRequestError> {
45        if snapshot_id.is_empty() || snapshot_id.len() > MAX_IC_SNAPSHOT_ID_BYTES {
46            return Err(IcRequestError::InvalidSnapshotId);
47        }
48        let target =
49            super::principal::canonical_text(target).ok_or(IcRequestError::InvalidTarget)?;
50        let principal = Principal::from_text(&target).map_err(|_| IcRequestError::InvalidTarget)?;
51        let arguments = candid::encode_one(ReadCanisterSnapshotMetadataArgs {
52            canister_id: principal,
53            snapshot_id: snapshot_id.to_vec(),
54        })
55        .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
56        if arguments.len() > MAX_IC_ARGUMENT_BYTES {
57            return Err(IcRequestError::ArgumentsTooLarge);
58        }
59        Ok(Self {
60            target,
61            target_bytes: principal.as_slice().to_vec(),
62            snapshot_id: snapshot_id.to_vec(),
63            arguments,
64        })
65    }
66
67    /// Read the canonical effective routing target, also encoded in the arguments.
68    #[must_use]
69    pub fn target(&self) -> &str {
70        &self.target
71    }
72
73    /// Read exact raw snapshot bytes, distinct from a generic backend token.
74    #[must_use]
75    pub fn snapshot_id(&self) -> &[u8] {
76        &self.snapshot_id
77    }
78
79    /// Read exact Candid arguments; no transport or dispatch is installed.
80    #[must_use]
81    pub fn arguments(&self) -> &[u8] {
82        &self.arguments
83    }
84
85    /// Read the fixed management receiver, separate from the routing target.
86    #[must_use]
87    pub const fn receiver(&self) -> &'static str {
88        "aaaaa-aa"
89    }
90
91    /// Read the sole method, using replicated host update ingress.
92    #[must_use]
93    pub const fn method(&self) -> &'static str {
94        "read_canister_snapshot_metadata"
95    }
96
97    /// Hash exact receiver/routing/update-mode/method/arguments with the existing owner.
98    #[must_use]
99    pub fn digest(&self) -> ArtifactChecksumRecord {
100        management_request_digest(&self.target_bytes, self.method(), &self.arguments)
101    }
102}
103
104impl fmt::Debug for IcSnapshotMetadataRequest {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        f.debug_struct("IcSnapshotMetadataRequest")
107            .field("target", &self.target)
108            .field("snapshot_id_bytes", &self.snapshot_id.len())
109            .finish_non_exhaustive()
110    }
111}
112
113/// Read-only pinned metadata and exact raw evidence under a declared request.
114///
115/// Optional source, unavailable globals and optional timer/hook fields retain
116/// their actual presence. Missing information is not a default or upload input.
117/// Floating globals retain their bits; globals and chunk rows retain wire order.
118/// This evidence grants no complete transfer, authentic origin or effect outcome.
119pub struct IcSnapshotMetadataReply<'request> {
120    request: &'request IcSnapshotMetadataRequest,
121    metadata: ReadCanisterSnapshotMetadataResult,
122    payload_checksum: ArtifactChecksumRecord,
123}
124
125impl<'request> IcSnapshotMetadataReply<'request> {
126    /// Decode one bounded metadata argument without skipped fields or extra bytes.
127    ///
128    /// # Errors
129    /// Rejects excessive raw bytes, malformed/unsupported wire values, sequence
130    /// bounds, certified data above 32 bytes, non-SHA-256/duplicate chunk hashes
131    /// and v128 globals outside their unsigned 128-bit range.
132    pub fn decode(
133        request: &'request IcSnapshotMetadataRequest,
134        bytes: &[u8],
135    ) -> Result<Self, IcSnapshotMetadataError> {
136        if bytes.len() > MAX_IC_SNAPSHOT_METADATA_BYTES {
137            return Err(IcSnapshotMetadataError::ReplyTooLarge);
138        }
139        let metadata = wire::decode(bytes)?;
140        Ok(Self {
141            request,
142            metadata,
143            payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
144        })
145    }
146
147    /// Read the exact declared request; the reply wire authenticates no target.
148    #[must_use]
149    pub const fn request(&self) -> &'request IcSnapshotMetadataRequest {
150        self.request
151    }
152
153    /// Read admitted pinned DTO values without mutation access.
154    #[must_use]
155    pub const fn metadata(&self) -> &ReadCanisterSnapshotMetadataResult {
156        &self.metadata
157    }
158
159    /// Read SHA-256 of exact raw bytes, including optional fields and wire ordering.
160    #[must_use]
161    pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
162        &self.payload_checksum
163    }
164
165    /// Bind original request and raw reply checksums in the metadata evidence domain.
166    #[must_use]
167    pub fn digest(&self) -> ArtifactChecksumRecord {
168        let mut bytes = b"ic-backup/ic-snapshot-metadata-reply/v1\0".to_vec();
169        bytes.extend_from_slice(self.request.digest().hash().as_bytes());
170        bytes.extend_from_slice(self.payload_checksum.hash().as_bytes());
171        ArtifactChecksumRecord::from_bytes(&bytes)
172    }
173}
174
175impl fmt::Debug for IcSnapshotMetadataReply<'_> {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        f.debug_struct("IcSnapshotMetadataReply")
178            .field("request", &self.request)
179            .field("payload_checksum", &self.payload_checksum)
180            .finish_non_exhaustive()
181    }
182}
183
184/// Typed metadata admission errors, excluding raw bytes and decoder diagnostics.
185#[derive(Debug, Error, Eq, PartialEq)]
186pub enum IcSnapshotMetadataError {
187    /// Input exceeded the local raw-byte bound before parsing.
188    #[error("snapshot metadata exceeds {MAX_IC_SNAPSHOT_METADATA_BYTES} bytes")]
189    ReplyTooLarge,
190    /// Wire shape, finite work, sequence or certified-data bounds failed.
191    #[error("invalid or unsupported snapshot metadata reply")]
192    InvalidReply,
193    /// A v128 value exceeded its unsigned 128-bit representation.
194    #[error("snapshot v128 global exceeds 128 bits")]
195    InvalidGlobal,
196    /// Chunk identities must be distinct exact SHA-256 hashes.
197    #[error("snapshot chunk hash is not a unique 32-byte identity")]
198    InvalidChunkHash,
199}
200
201#[cfg(test)]
202mod tests;