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