ic_backup/model/ic_snapshot_data/
mod.rs1mod wire;
4
5use super::{
6 artifacts::ArtifactChecksumRecord,
7 ic_request::{IcRequestError, MAX_IC_ARGUMENT_BYTES, management_request_digest},
8 ic_snapshot_metadata::IcSnapshotMetadataReply,
9};
10use candid::Principal;
11use ic_management_canister_types::{ReadCanisterSnapshotDataArgs, SnapshotDataKind};
12use std::fmt;
13use thiserror::Error;
14
15pub const MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES: usize = 1024 * 1024;
17pub const MAX_IC_SNAPSHOT_DATA_REPLY_BYTES: usize = 2 * 1024 * 1024;
19
20pub struct IcSnapshotDataRequest<'metadata> {
26 metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
27 kind: SnapshotDataKind,
28 target_bytes: Vec<u8>,
29 arguments: Vec<u8>,
30}
31
32impl<'metadata> IcSnapshotDataRequest<'metadata> {
33 pub fn new(
39 metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
40 kind: SnapshotDataKind,
41 ) -> Result<Self, IcSnapshotDataError> {
42 validate_kind(metadata, &kind)?;
43 let target = Principal::from_text(metadata.request().target())
44 .map_err(|_| IcRequestError::InvalidTarget)?;
45 let arguments = candid::encode_one(ReadCanisterSnapshotDataArgs {
46 canister_id: target,
47 snapshot_id: metadata.request().snapshot_id().to_vec(),
48 kind: kind.clone(),
49 })
50 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
51 if arguments.len() > MAX_IC_ARGUMENT_BYTES {
52 return Err(IcRequestError::ArgumentsTooLarge.into());
53 }
54 Ok(Self {
55 metadata,
56 kind,
57 target_bytes: target.as_slice().to_vec(),
58 arguments,
59 })
60 }
61
62 #[must_use]
64 pub const fn metadata(&self) -> &'metadata IcSnapshotMetadataReply<'metadata> {
65 self.metadata
66 }
67
68 #[must_use]
70 pub const fn kind(&self) -> &SnapshotDataKind {
71 &self.kind
72 }
73
74 #[must_use]
76 pub fn target(&self) -> &str {
77 self.metadata.request().target()
78 }
79
80 #[must_use]
82 pub fn snapshot_id(&self) -> &[u8] {
83 self.metadata.request().snapshot_id()
84 }
85
86 #[must_use]
88 pub fn arguments(&self) -> &[u8] {
89 &self.arguments
90 }
91
92 #[must_use]
94 pub const fn receiver(&self) -> &'static str {
95 "aaaaa-aa"
96 }
97
98 #[must_use]
100 pub const fn method(&self) -> &'static str {
101 "read_canister_snapshot_data"
102 }
103
104 #[must_use]
109 pub fn digest(&self) -> ArtifactChecksumRecord {
110 management_request_digest(&self.target_bytes, self.method(), &self.arguments)
111 }
112}
113
114pub(crate) fn validate_kind(
115 metadata: &IcSnapshotMetadataReply<'_>,
116 kind: &SnapshotDataKind,
117) -> Result<(), IcSnapshotDataError> {
118 let values = metadata.metadata();
119 match kind {
120 SnapshotDataKind::WasmModule { offset, size } => {
121 range(*offset, *size, values.wasm_module_size)?;
122 }
123 SnapshotDataKind::WasmMemory { offset, size } => {
124 range(*offset, *size, values.wasm_memory_size)?;
125 }
126 SnapshotDataKind::StableMemory { offset, size } => {
127 range(*offset, *size, values.stable_memory_size)?;
128 }
129 SnapshotDataKind::WasmChunk { hash } => {
130 if hash.len() != 32 {
131 return Err(IcSnapshotDataError::InvalidChunkHash);
132 }
133 if !values
134 .wasm_chunk_store
135 .iter()
136 .any(|chunk| chunk.hash == *hash)
137 {
138 return Err(IcSnapshotDataError::ChunkNotInMetadata);
139 }
140 }
141 }
142 Ok(())
143}
144
145fn range(offset: u64, size: u64, total: u64) -> Result<(), IcSnapshotDataError> {
146 let admitted_size = usize::try_from(size)
147 .is_ok_and(|size| (1..=MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES).contains(&size));
148 if !admitted_size {
149 return Err(IcSnapshotDataError::InvalidRangeSize);
150 }
151 if offset.checked_add(size).is_none_or(|end| end > total) {
152 return Err(IcSnapshotDataError::RangeOutsideMetadata);
153 }
154 Ok(())
155}
156
157impl fmt::Debug for IcSnapshotDataRequest<'_> {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 f.debug_struct("IcSnapshotDataRequest")
160 .field("target", &self.target())
161 .field("argument_bytes", &self.arguments.len())
162 .finish_non_exhaustive()
163 }
164}
165
166pub struct IcSnapshotDataReply<'request, 'metadata> {
172 request: &'request IcSnapshotDataRequest<'metadata>,
173 chunk: Vec<u8>,
174 chunk_checksum: ArtifactChecksumRecord,
175 payload_checksum: ArtifactChecksumRecord,
176}
177
178impl<'request, 'metadata> IcSnapshotDataReply<'request, 'metadata> {
179 pub fn decode(
185 request: &'request IcSnapshotDataRequest<'metadata>,
186 bytes: &[u8],
187 ) -> Result<Self, IcSnapshotDataError> {
188 if bytes.len() > MAX_IC_SNAPSHOT_DATA_REPLY_BYTES {
189 return Err(IcSnapshotDataError::ReplyTooLarge);
190 }
191 let chunk = wire::decode(bytes)?;
192 let chunk_checksum = ArtifactChecksumRecord::from_bytes(&chunk);
193 match request.kind() {
194 SnapshotDataKind::WasmModule { size, .. }
195 | SnapshotDataKind::WasmMemory { size, .. }
196 | SnapshotDataKind::StableMemory { size, .. } => {
197 if u64::try_from(chunk.len()).ok() != Some(*size) {
198 return Err(IcSnapshotDataError::LengthMismatch);
199 }
200 }
201 SnapshotDataKind::WasmChunk { hash } => {
202 if chunk_checksum.hash() != crate::hash::hex_bytes(hash) {
203 return Err(IcSnapshotDataError::ChunkHashMismatch);
204 }
205 }
206 }
207 Ok(Self {
208 request,
209 chunk,
210 chunk_checksum,
211 payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
212 })
213 }
214
215 #[must_use]
217 pub const fn request(&self) -> &'request IcSnapshotDataRequest<'metadata> {
218 self.request
219 }
220
221 #[must_use]
223 pub fn chunk(&self) -> &[u8] {
224 &self.chunk
225 }
226
227 #[must_use]
229 pub const fn chunk_checksum(&self) -> &ArtifactChecksumRecord {
230 &self.chunk_checksum
231 }
232
233 #[must_use]
235 pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
236 &self.payload_checksum
237 }
238
239 #[must_use]
241 pub fn digest(&self) -> ArtifactChecksumRecord {
242 let mut bytes = b"ic-backup/ic-snapshot-data-reply/v1\0".to_vec();
243 bytes.extend_from_slice(self.request.metadata().digest().hash().as_bytes());
244 bytes.extend_from_slice(self.request.digest().hash().as_bytes());
245 bytes.extend_from_slice(self.payload_checksum.hash().as_bytes());
246 ArtifactChecksumRecord::from_bytes(&bytes)
247 }
248}
249
250impl fmt::Debug for IcSnapshotDataReply<'_, '_> {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 f.debug_struct("IcSnapshotDataReply")
253 .field("request", &self.request)
254 .field("chunk_bytes", &self.chunk.len())
255 .field("payload_checksum", &self.payload_checksum)
256 .finish_non_exhaustive()
257 }
258}
259
260#[derive(Debug, Error, Eq, PartialEq)]
262pub enum IcSnapshotDataError {
263 #[error(transparent)]
265 Request(#[from] IcRequestError),
266 #[error("snapshot range size must be 1..={MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES}")]
268 InvalidRangeSize,
269 #[error("snapshot range is outside retained metadata")]
271 RangeOutsideMetadata,
272 #[error("snapshot chunk identity must be 32 bytes")]
274 InvalidChunkHash,
275 #[error("snapshot chunk identity is absent from retained metadata")]
277 ChunkNotInMetadata,
278 #[error("snapshot data reply exceeds {MAX_IC_SNAPSHOT_DATA_REPLY_BYTES} bytes")]
280 ReplyTooLarge,
281 #[error("invalid or unsupported snapshot data reply")]
283 InvalidReply,
284 #[error("snapshot data length differs from the requested range")]
286 LengthMismatch,
287 #[error("snapshot data differs from the requested chunk hash")]
289 ChunkHashMismatch,
290}
291
292#[cfg(test)]
293mod tests;