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 pub(crate) fn matches_kind(&self, original: &SnapshotDataKind) -> bool {
75 use SnapshotDataKind::{StableMemory, WasmChunk, WasmMemory, WasmModule};
76 match (&self.kind, original) {
77 (WasmModule { offset: a, size: b }, WasmModule { offset: c, size: d })
78 | (WasmMemory { offset: a, size: b }, WasmMemory { offset: c, size: d })
79 | (StableMemory { offset: a, size: b }, StableMemory { offset: c, size: d }) => {
80 a == c && b == d
81 }
82 (WasmChunk { hash: a }, WasmChunk { hash: b }) => a == b,
83 _ => false,
84 }
85 }
86
87 #[must_use]
89 pub fn target(&self) -> &str {
90 self.metadata.request().target()
91 }
92
93 #[must_use]
95 pub fn snapshot_id(&self) -> &[u8] {
96 self.metadata.request().snapshot_id()
97 }
98
99 #[must_use]
101 pub fn arguments(&self) -> &[u8] {
102 &self.arguments
103 }
104
105 #[must_use]
107 pub const fn receiver(&self) -> &'static str {
108 "aaaaa-aa"
109 }
110
111 #[must_use]
113 pub const fn method(&self) -> &'static str {
114 "read_canister_snapshot_data"
115 }
116
117 #[must_use]
122 pub fn digest(&self) -> ArtifactChecksumRecord {
123 management_request_digest(&self.target_bytes, self.method(), &self.arguments)
124 }
125}
126
127pub(crate) fn validate_kind(
128 metadata: &IcSnapshotMetadataReply<'_>,
129 kind: &SnapshotDataKind,
130) -> Result<(), IcSnapshotDataError> {
131 let values = metadata.metadata();
132 match kind {
133 SnapshotDataKind::WasmModule { offset, size } => {
134 range(*offset, *size, values.wasm_module_size)?;
135 }
136 SnapshotDataKind::WasmMemory { offset, size } => {
137 range(*offset, *size, values.wasm_memory_size)?;
138 }
139 SnapshotDataKind::StableMemory { offset, size } => {
140 range(*offset, *size, values.stable_memory_size)?;
141 }
142 SnapshotDataKind::WasmChunk { hash } => {
143 if hash.len() != 32 {
144 return Err(IcSnapshotDataError::InvalidChunkHash);
145 }
146 if !values
147 .wasm_chunk_store
148 .iter()
149 .any(|chunk| chunk.hash == *hash)
150 {
151 return Err(IcSnapshotDataError::ChunkNotInMetadata);
152 }
153 }
154 }
155 Ok(())
156}
157
158fn range(offset: u64, size: u64, total: u64) -> Result<(), IcSnapshotDataError> {
159 let admitted_size = usize::try_from(size)
160 .is_ok_and(|size| (1..=MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES).contains(&size));
161 if !admitted_size {
162 return Err(IcSnapshotDataError::InvalidRangeSize);
163 }
164 if offset.checked_add(size).is_none_or(|end| end > total) {
165 return Err(IcSnapshotDataError::RangeOutsideMetadata);
166 }
167 Ok(())
168}
169
170impl fmt::Debug for IcSnapshotDataRequest<'_> {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 f.debug_struct("IcSnapshotDataRequest")
173 .field("target", &self.target())
174 .field("argument_bytes", &self.arguments.len())
175 .finish_non_exhaustive()
176 }
177}
178
179pub struct IcSnapshotDataReply<'request, 'metadata> {
185 request: &'request IcSnapshotDataRequest<'metadata>,
186 chunk: Vec<u8>,
187 chunk_checksum: ArtifactChecksumRecord,
188 payload_checksum: ArtifactChecksumRecord,
189}
190
191impl<'request, 'metadata> IcSnapshotDataReply<'request, 'metadata> {
192 pub fn decode(
198 request: &'request IcSnapshotDataRequest<'metadata>,
199 bytes: &[u8],
200 ) -> Result<Self, IcSnapshotDataError> {
201 if bytes.len() > MAX_IC_SNAPSHOT_DATA_REPLY_BYTES {
202 return Err(IcSnapshotDataError::ReplyTooLarge);
203 }
204 let chunk = wire::decode(bytes)?;
205 let chunk_checksum = ArtifactChecksumRecord::from_bytes(&chunk);
206 match request.kind() {
207 SnapshotDataKind::WasmModule { size, .. }
208 | SnapshotDataKind::WasmMemory { size, .. }
209 | SnapshotDataKind::StableMemory { size, .. } => {
210 if u64::try_from(chunk.len()).ok() != Some(*size) {
211 return Err(IcSnapshotDataError::LengthMismatch);
212 }
213 }
214 SnapshotDataKind::WasmChunk { hash } => {
215 if chunk_checksum.hash() != crate::hash::hex_bytes(hash) {
216 return Err(IcSnapshotDataError::ChunkHashMismatch);
217 }
218 }
219 }
220 Ok(Self {
221 request,
222 chunk,
223 chunk_checksum,
224 payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
225 })
226 }
227
228 #[must_use]
230 pub const fn request(&self) -> &'request IcSnapshotDataRequest<'metadata> {
231 self.request
232 }
233
234 #[must_use]
236 pub fn chunk(&self) -> &[u8] {
237 &self.chunk
238 }
239
240 #[must_use]
242 pub const fn chunk_checksum(&self) -> &ArtifactChecksumRecord {
243 &self.chunk_checksum
244 }
245
246 #[must_use]
248 pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
249 &self.payload_checksum
250 }
251
252 #[must_use]
254 pub fn digest(&self) -> ArtifactChecksumRecord {
255 let mut bytes = b"ic-backup/ic-snapshot-data-reply/v1\0".to_vec();
256 bytes.extend_from_slice(self.request.metadata().digest().hash().as_bytes());
257 bytes.extend_from_slice(self.request.digest().hash().as_bytes());
258 bytes.extend_from_slice(self.payload_checksum.hash().as_bytes());
259 ArtifactChecksumRecord::from_bytes(&bytes)
260 }
261}
262
263impl fmt::Debug for IcSnapshotDataReply<'_, '_> {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 f.debug_struct("IcSnapshotDataReply")
266 .field("request", &self.request)
267 .field("chunk_bytes", &self.chunk.len())
268 .field("payload_checksum", &self.payload_checksum)
269 .finish_non_exhaustive()
270 }
271}
272
273#[derive(Debug, Error, Eq, PartialEq)]
275pub enum IcSnapshotDataError {
276 #[error(transparent)]
278 Request(#[from] IcRequestError),
279 #[error("snapshot range size must be 1..={MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES}")]
281 InvalidRangeSize,
282 #[error("snapshot range is outside retained metadata")]
284 RangeOutsideMetadata,
285 #[error("snapshot chunk identity must be 32 bytes")]
287 InvalidChunkHash,
288 #[error("snapshot chunk identity is absent from retained metadata")]
290 ChunkNotInMetadata,
291 #[error("snapshot data reply exceeds {MAX_IC_SNAPSHOT_DATA_REPLY_BYTES} bytes")]
293 ReplyTooLarge,
294 #[error("invalid or unsupported snapshot data reply")]
296 InvalidReply,
297 #[error("snapshot data length differs from the requested range")]
299 LengthMismatch,
300 #[error("snapshot data differs from the requested chunk hash")]
302 ChunkHashMismatch,
303}
304
305#[cfg(test)]
306mod tests;