Skip to main content

ic_backup/model/ic_snapshot_metadata/
mod.rs

1//! Bounded snapshot metadata reads; decoded declarations do not prove transfer.
2
3use ic_platform::candid::contract;
4mod wire;
5
6use super::{
7    artifacts::ArtifactChecksumRecord,
8    ic_request::{
9        IcRequestError, MAX_IC_ARGUMENT_BYTES, MAX_IC_SNAPSHOT_ID_BYTES, management_request_digest,
10    },
11};
12use ic_platform::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, principal) =
49            super::principal::canonical_parts(target).ok_or(IcRequestError::InvalidTarget)?;
50        let arguments = contract::encode_one(ReadCanisterSnapshotMetadataArgs {
51            canister_id: principal,
52            snapshot_id: snapshot_id.to_vec(),
53        })
54        .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
55        if arguments.len() > MAX_IC_ARGUMENT_BYTES {
56            return Err(IcRequestError::ArgumentsTooLarge);
57        }
58        Ok(Self {
59            target,
60            target_bytes: principal.as_slice().to_vec(),
61            snapshot_id: snapshot_id.to_vec(),
62            arguments,
63        })
64    }
65
66    /// Read the canonical effective routing target, also encoded in the arguments.
67    #[must_use]
68    pub fn target(&self) -> &str {
69        &self.target
70    }
71
72    /// Read exact raw snapshot bytes, distinct from a generic backend token.
73    #[must_use]
74    pub fn snapshot_id(&self) -> &[u8] {
75        &self.snapshot_id
76    }
77
78    /// Read exact Candid arguments; no transport or dispatch is installed.
79    #[must_use]
80    pub fn arguments(&self) -> &[u8] {
81        &self.arguments
82    }
83
84    /// Read the fixed management receiver, separate from the routing target.
85    #[must_use]
86    pub const fn receiver(&self) -> &'static str {
87        "aaaaa-aa"
88    }
89
90    /// Read the sole method, using replicated host update ingress.
91    #[must_use]
92    pub const fn method(&self) -> &'static str {
93        "read_canister_snapshot_metadata"
94    }
95
96    /// Hash exact receiver/routing/update-mode/method/arguments with the existing owner.
97    #[must_use]
98    pub fn digest(&self) -> ArtifactChecksumRecord {
99        management_request_digest(&self.target_bytes, self.method(), &self.arguments)
100    }
101}
102
103impl fmt::Debug for IcSnapshotMetadataRequest {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        f.debug_struct("IcSnapshotMetadataRequest")
106            .field("target", &self.target)
107            .field("snapshot_id_bytes", &self.snapshot_id.len())
108            .finish_non_exhaustive()
109    }
110}
111
112/// Read-only pinned metadata and exact raw evidence under a declared request.
113///
114/// Optional source, unavailable globals and optional timer/hook fields retain
115/// their actual presence. Missing information is not a default or upload input.
116/// Floating globals retain their bits; globals and chunk rows retain wire order.
117/// This evidence grants no complete transfer, authentic origin or effect outcome.
118pub struct IcSnapshotMetadataReply<'request> {
119    request: &'request IcSnapshotMetadataRequest,
120    metadata: ReadCanisterSnapshotMetadataResult,
121    payload_checksum: ArtifactChecksumRecord,
122}
123
124impl<'request> IcSnapshotMetadataReply<'request> {
125    /// Decode one bounded metadata argument without skipped fields or extra bytes.
126    ///
127    /// # Errors
128    /// Rejects excessive raw bytes, malformed/unsupported wire values, sequence
129    /// bounds, certified data above 32 bytes, non-SHA-256/duplicate chunk hashes
130    /// and v128 globals outside their unsigned 128-bit range.
131    pub fn decode(
132        request: &'request IcSnapshotMetadataRequest,
133        bytes: &[u8],
134    ) -> Result<Self, IcSnapshotMetadataError> {
135        if bytes.len() > MAX_IC_SNAPSHOT_METADATA_BYTES {
136            return Err(IcSnapshotMetadataError::ReplyTooLarge);
137        }
138        let metadata = wire::decode(bytes)?;
139        Ok(Self {
140            request,
141            metadata,
142            payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
143        })
144    }
145
146    /// Read the exact declared request; the reply wire authenticates no target.
147    #[must_use]
148    pub const fn request(&self) -> &'request IcSnapshotMetadataRequest {
149        self.request
150    }
151
152    /// Read admitted pinned DTO values without mutation access.
153    #[must_use]
154    pub const fn metadata(&self) -> &ReadCanisterSnapshotMetadataResult {
155        &self.metadata
156    }
157
158    /// Read SHA-256 of exact raw bytes, including optional fields and wire ordering.
159    #[must_use]
160    pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
161        &self.payload_checksum
162    }
163
164    /// Bind original request and raw reply checksums in the metadata evidence domain.
165    #[must_use]
166    pub fn digest(&self) -> ArtifactChecksumRecord {
167        super::evidence_digest::reply(
168            b"ic-backup/ic-snapshot-metadata-reply/v1\0",
169            [&self.request.digest(), &self.payload_checksum],
170        )
171    }
172}
173
174impl fmt::Debug for IcSnapshotMetadataReply<'_> {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        f.debug_struct("IcSnapshotMetadataReply")
177            .field("request", &self.request)
178            .field("payload_checksum", &self.payload_checksum)
179            .finish_non_exhaustive()
180    }
181}
182
183/// Typed metadata admission errors, excluding raw bytes and decoder diagnostics.
184#[derive(Debug, Error, Eq, PartialEq)]
185pub enum IcSnapshotMetadataError {
186    /// Input exceeded the local raw-byte bound before parsing.
187    #[error("snapshot metadata exceeds {MAX_IC_SNAPSHOT_METADATA_BYTES} bytes")]
188    ReplyTooLarge,
189    /// Wire shape, finite work, sequence or certified-data bounds failed.
190    #[error("invalid or unsupported snapshot metadata reply")]
191    InvalidReply,
192    /// A v128 value exceeded its unsigned 128-bit representation.
193    #[error("snapshot v128 global exceeds 128 bits")]
194    InvalidGlobal,
195    /// Chunk identities must be distinct exact SHA-256 hashes.
196    #[error("snapshot chunk hash is not a unique 32-byte identity")]
197    InvalidChunkHash,
198}
199
200#[cfg(test)]
201mod tests;