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