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