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 let values = metadata.metadata();
43 match &kind {
44 SnapshotDataKind::WasmModule { offset, size } => {
45 range(*offset, *size, values.wasm_module_size)?;
46 }
47 SnapshotDataKind::WasmMemory { offset, size } => {
48 range(*offset, *size, values.wasm_memory_size)?;
49 }
50 SnapshotDataKind::StableMemory { offset, size } => {
51 range(*offset, *size, values.stable_memory_size)?;
52 }
53 SnapshotDataKind::WasmChunk { hash } => {
54 if hash.len() != 32 {
55 return Err(IcSnapshotDataError::InvalidChunkHash);
56 }
57 if !values
58 .wasm_chunk_store
59 .iter()
60 .any(|chunk| chunk.hash == *hash)
61 {
62 return Err(IcSnapshotDataError::ChunkNotInMetadata);
63 }
64 }
65 }
66 let target = Principal::from_text(metadata.request().target())
67 .map_err(|_| IcRequestError::InvalidTarget)?;
68 let arguments = candid::encode_one(ReadCanisterSnapshotDataArgs {
69 canister_id: target,
70 snapshot_id: metadata.request().snapshot_id().to_vec(),
71 kind: kind.clone(),
72 })
73 .map_err(|error| IcRequestError::Encoding(error.to_string()))?;
74 if arguments.len() > MAX_IC_ARGUMENT_BYTES {
75 return Err(IcRequestError::ArgumentsTooLarge.into());
76 }
77 Ok(Self {
78 metadata,
79 kind,
80 target_bytes: target.as_slice().to_vec(),
81 arguments,
82 })
83 }
84
85 #[must_use]
87 pub const fn metadata(&self) -> &'metadata IcSnapshotMetadataReply<'metadata> {
88 self.metadata
89 }
90
91 #[must_use]
93 pub const fn kind(&self) -> &SnapshotDataKind {
94 &self.kind
95 }
96
97 #[must_use]
99 pub fn target(&self) -> &str {
100 self.metadata.request().target()
101 }
102
103 #[must_use]
105 pub fn snapshot_id(&self) -> &[u8] {
106 self.metadata.request().snapshot_id()
107 }
108
109 #[must_use]
111 pub fn arguments(&self) -> &[u8] {
112 &self.arguments
113 }
114
115 #[must_use]
117 pub const fn receiver(&self) -> &'static str {
118 "aaaaa-aa"
119 }
120
121 #[must_use]
123 pub const fn method(&self) -> &'static str {
124 "read_canister_snapshot_data"
125 }
126
127 #[must_use]
132 pub fn digest(&self) -> ArtifactChecksumRecord {
133 management_request_digest(&self.target_bytes, self.method(), &self.arguments)
134 }
135}
136
137fn range(offset: u64, size: u64, total: u64) -> Result<(), IcSnapshotDataError> {
138 let admitted_size = usize::try_from(size)
139 .is_ok_and(|size| (1..=MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES).contains(&size));
140 if !admitted_size {
141 return Err(IcSnapshotDataError::InvalidRangeSize);
142 }
143 if offset.checked_add(size).is_none_or(|end| end > total) {
144 return Err(IcSnapshotDataError::RangeOutsideMetadata);
145 }
146 Ok(())
147}
148
149impl fmt::Debug for IcSnapshotDataRequest<'_> {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 f.debug_struct("IcSnapshotDataRequest")
152 .field("target", &self.target())
153 .field("argument_bytes", &self.arguments.len())
154 .finish_non_exhaustive()
155 }
156}
157
158pub struct IcSnapshotDataReply<'request, 'metadata> {
164 request: &'request IcSnapshotDataRequest<'metadata>,
165 chunk: Vec<u8>,
166 chunk_checksum: ArtifactChecksumRecord,
167 payload_checksum: ArtifactChecksumRecord,
168}
169
170impl<'request, 'metadata> IcSnapshotDataReply<'request, 'metadata> {
171 pub fn decode(
177 request: &'request IcSnapshotDataRequest<'metadata>,
178 bytes: &[u8],
179 ) -> Result<Self, IcSnapshotDataError> {
180 if bytes.len() > MAX_IC_SNAPSHOT_DATA_REPLY_BYTES {
181 return Err(IcSnapshotDataError::ReplyTooLarge);
182 }
183 let chunk = wire::decode(bytes)?;
184 let chunk_checksum = ArtifactChecksumRecord::from_bytes(&chunk);
185 match request.kind() {
186 SnapshotDataKind::WasmModule { size, .. }
187 | SnapshotDataKind::WasmMemory { size, .. }
188 | SnapshotDataKind::StableMemory { size, .. } => {
189 if u64::try_from(chunk.len()).ok() != Some(*size) {
190 return Err(IcSnapshotDataError::LengthMismatch);
191 }
192 }
193 SnapshotDataKind::WasmChunk { hash } => {
194 if chunk_checksum.hash() != crate::hash::hex_bytes(hash) {
195 return Err(IcSnapshotDataError::ChunkHashMismatch);
196 }
197 }
198 }
199 Ok(Self {
200 request,
201 chunk,
202 chunk_checksum,
203 payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
204 })
205 }
206
207 #[must_use]
209 pub const fn request(&self) -> &'request IcSnapshotDataRequest<'metadata> {
210 self.request
211 }
212
213 #[must_use]
215 pub fn chunk(&self) -> &[u8] {
216 &self.chunk
217 }
218
219 #[must_use]
221 pub const fn chunk_checksum(&self) -> &ArtifactChecksumRecord {
222 &self.chunk_checksum
223 }
224
225 #[must_use]
227 pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
228 &self.payload_checksum
229 }
230
231 #[must_use]
233 pub fn digest(&self) -> ArtifactChecksumRecord {
234 let mut bytes = b"ic-backup/ic-snapshot-data-reply/v1\0".to_vec();
235 bytes.extend_from_slice(self.request.metadata().digest().hash().as_bytes());
236 bytes.extend_from_slice(self.request.digest().hash().as_bytes());
237 bytes.extend_from_slice(self.payload_checksum.hash().as_bytes());
238 ArtifactChecksumRecord::from_bytes(&bytes)
239 }
240}
241
242impl fmt::Debug for IcSnapshotDataReply<'_, '_> {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 f.debug_struct("IcSnapshotDataReply")
245 .field("request", &self.request)
246 .field("chunk_bytes", &self.chunk.len())
247 .field("payload_checksum", &self.payload_checksum)
248 .finish_non_exhaustive()
249 }
250}
251
252#[derive(Debug, Error, Eq, PartialEq)]
254pub enum IcSnapshotDataError {
255 #[error(transparent)]
257 Request(#[from] IcRequestError),
258 #[error("snapshot range size must be 1..={MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES}")]
260 InvalidRangeSize,
261 #[error("snapshot range is outside retained metadata")]
263 RangeOutsideMetadata,
264 #[error("snapshot chunk identity must be 32 bytes")]
266 InvalidChunkHash,
267 #[error("snapshot chunk identity is absent from retained metadata")]
269 ChunkNotInMetadata,
270 #[error("snapshot data reply exceeds {MAX_IC_SNAPSHOT_DATA_REPLY_BYTES} bytes")]
272 ReplyTooLarge,
273 #[error("invalid or unsupported snapshot data reply")]
275 InvalidReply,
276 #[error("snapshot data length differs from the requested range")]
278 LengthMismatch,
279 #[error("snapshot data differs from the requested chunk hash")]
281 ChunkHashMismatch,
282}
283
284#[cfg(test)]
285mod tests;