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