1use crate::pocket_ic::{ClientError, common::rest::RawMessageId};
4use candid::Principal;
5pub use ic_management_canister_types::{
6 ReadCanisterSnapshotDataArgs, ReadCanisterSnapshotDataResult, ReadCanisterSnapshotMetadataArgs,
7 ReadCanisterSnapshotMetadataResult, SnapshotDataKind, SnapshotDataOffset,
8 UploadCanisterSnapshotDataArgs, UploadCanisterSnapshotMetadataArgs,
9 UploadCanisterSnapshotMetadataResult,
10};
11
12#[derive(Clone, Copy, Debug)]
14pub struct CallLimits {
15 pub http_bytes: usize,
16 pub candid_bytes: usize,
17 pub decoding_quota: usize,
18 pub skipping_quota: usize,
19}
20
21#[derive(Clone, Copy, Debug)]
23pub struct TransferLimits {
24 pub call: CallLimits,
25 pub chunk_bytes: u64,
26 pub total_bytes: u64,
27}
28
29#[derive(Debug, thiserror::Error)]
30pub enum SnapshotError {
31 #[error("snapshot call failed: {source}")]
32 Call {
33 #[source]
34 source: ClientError,
35 ingress: Option<RawMessageId>,
37 },
38 #[error("invalid snapshot transfer: {0}")]
39 Invalid(&'static str),
40}
41
42#[derive(Clone, Debug)]
44pub enum TransferStatus {
45 Ready,
46 UploadingMetadata,
47 Reading(SnapshotDataKind),
48 Writing(SnapshotDataOffset),
49 Complete,
50}
51
52#[derive(Debug)]
60pub struct SnapshotTransfer {
61 pub(crate) canister: Principal,
62 pub(crate) sender: Principal,
63 pub(crate) original: Vec<u8>,
64 pub(crate) metadata: ReadCanisterSnapshotMetadataResult,
65 pub(crate) limits: TransferLimits,
66 pub(crate) uploaded: Option<Vec<u8>>,
67 pub(crate) status: TransferStatus,
68 pub(crate) ingress: Option<RawMessageId>,
69 pub(crate) region: usize,
70 pub(crate) offset: u64,
71 pub(crate) transferred: u64,
72}
73
74impl SnapshotTransfer {
75 pub fn new(
76 canister: Principal,
77 sender: Principal,
78 original: Vec<u8>,
79 metadata: ReadCanisterSnapshotMetadataResult,
80 limits: TransferLimits,
81 ) -> Result<Self, SnapshotError> {
82 if limits.chunk_bytes == 0 || limits.chunk_bytes > 1024 * 1024 {
83 return Err(SnapshotError::Invalid("chunk size must be 1..=1 MiB"));
84 }
85 if metadata.global_timer.is_none() || metadata.on_low_wasm_memory_hook_status.is_none() {
86 return Err(SnapshotError::Invalid(
87 "snapshot omitted timer or low-memory hook state",
88 ));
89 }
90 let bytes = metadata
91 .wasm_module_size
92 .checked_add(metadata.wasm_memory_size)
93 .and_then(|bytes| bytes.checked_add(metadata.stable_memory_size))
94 .ok_or(SnapshotError::Invalid("snapshot region sizes overflow"))?;
95 if bytes > limits.total_bytes {
96 return Err(SnapshotError::Invalid("snapshot exceeds total byte budget"));
97 }
98 let mut hashes = std::collections::BTreeSet::new();
99 for chunk in &metadata.wasm_chunk_store {
100 if chunk.hash.len() != 32 || !hashes.insert(chunk.hash.clone()) {
101 return Err(SnapshotError::Invalid("invalid or duplicate chunk hash"));
102 }
103 }
104 Ok(Self {
105 canister,
106 sender,
107 original,
108 metadata,
109 limits,
110 uploaded: None,
111 status: TransferStatus::Ready,
112 ingress: None,
113 region: 0,
114 offset: 0,
115 transferred: 0,
116 })
117 }
118
119 pub fn uploaded_snapshot(&self) -> Option<&[u8]> {
120 self.uploaded.as_deref()
121 }
122 pub fn status(&self) -> &TransferStatus {
123 &self.status
124 }
125 pub fn ingress(&self) -> Option<&RawMessageId> {
126 self.ingress.as_ref()
127 }
128 pub fn transferred_bytes(&self) -> u64 {
129 self.transferred
130 }
131
132 pub fn advance(&mut self, client: &crate::pocket_ic::PocketIc) -> Result<(), SnapshotError> {
134 client
135 .runtime
136 .block_on(self.advance_async(&client.pocket_ic))
137 }
138}
139
140impl crate::pocket_ic::PocketIc {
141 pub fn read_canister_snapshot_metadata(
142 &self,
143 sender: Principal,
144 args: ReadCanisterSnapshotMetadataArgs,
145 limits: CallLimits,
146 ) -> Result<ReadCanisterSnapshotMetadataResult, SnapshotError> {
147 self.runtime.block_on(
148 self.pocket_ic
149 .read_canister_snapshot_metadata(sender, args, limits),
150 )
151 }
152 pub fn read_canister_snapshot_data(
153 &self,
154 sender: Principal,
155 args: ReadCanisterSnapshotDataArgs,
156 limits: CallLimits,
157 ) -> Result<ReadCanisterSnapshotDataResult, SnapshotError> {
158 self.runtime.block_on(
159 self.pocket_ic
160 .read_canister_snapshot_data(sender, args, limits),
161 )
162 }
163 pub fn upload_canister_snapshot_metadata(
164 &self,
165 sender: Principal,
166 args: UploadCanisterSnapshotMetadataArgs,
167 limits: CallLimits,
168 ) -> Result<UploadCanisterSnapshotMetadataResult, SnapshotError> {
169 self.runtime.block_on(
170 self.pocket_ic
171 .upload_canister_snapshot_metadata(sender, args, limits),
172 )
173 }
174 pub fn upload_canister_snapshot_data(
175 &self,
176 sender: Principal,
177 args: UploadCanisterSnapshotDataArgs,
178 limits: CallLimits,
179 ) -> Result<(), SnapshotError> {
180 self.runtime.block_on(
181 self.pocket_ic
182 .upload_canister_snapshot_data(sender, args, limits),
183 )
184 }
185}