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_digest = ic_host_artifacts::artifact::Sha256Digest::compute(&chunk);
208 let chunk_checksum = ArtifactChecksumRecord::from_digest(*chunk_digest.as_bytes());
209 match request.kind() {
210 SnapshotDataKind::WasmModule { size, .. }
211 | SnapshotDataKind::WasmMemory { size, .. }
212 | SnapshotDataKind::StableMemory { size, .. } => {
213 if u64::try_from(chunk.len()).ok() != Some(*size) {
214 return Err(IcSnapshotDataError::LengthMismatch);
215 }
216 }
217 SnapshotDataKind::WasmChunk { hash } => {
218 if chunk_digest.as_bytes().as_slice() != hash.as_slice() {
219 return Err(IcSnapshotDataError::ChunkHashMismatch);
220 }
221 }
222 }
223 Ok(Self {
224 request,
225 chunk,
226 chunk_checksum,
227 payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
228 })
229 }
230
231 #[must_use]
233 pub const fn request(&self) -> &'request IcSnapshotDataRequest<'metadata> {
234 self.request
235 }
236
237 #[must_use]
239 pub fn chunk(&self) -> &[u8] {
240 &self.chunk
241 }
242
243 #[must_use]
245 pub const fn chunk_checksum(&self) -> &ArtifactChecksumRecord {
246 &self.chunk_checksum
247 }
248
249 #[must_use]
251 pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
252 &self.payload_checksum
253 }
254
255 #[must_use]
257 pub fn digest(&self) -> ArtifactChecksumRecord {
258 super::evidence_digest::reply(
259 b"ic-backup/ic-snapshot-data-reply/v1\0",
260 [
261 &self.request.metadata().digest(),
262 &self.request.digest(),
263 &self.payload_checksum,
264 ],
265 )
266 }
267}
268
269impl fmt::Debug for IcSnapshotDataReply<'_, '_> {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 f.debug_struct("IcSnapshotDataReply")
272 .field("request", &self.request)
273 .field("chunk_bytes", &self.chunk.len())
274 .field("payload_checksum", &self.payload_checksum)
275 .finish_non_exhaustive()
276 }
277}
278
279#[derive(Debug, Error, Eq, PartialEq)]
281pub enum IcSnapshotDataError {
282 #[error(transparent)]
284 Request(#[from] IcRequestError),
285 #[error("snapshot range size must be 1..={MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES}")]
287 InvalidRangeSize,
288 #[error("snapshot range is outside retained metadata")]
290 RangeOutsideMetadata,
291 #[error("snapshot chunk identity must be 32 bytes")]
293 InvalidChunkHash,
294 #[error("snapshot chunk identity is absent from retained metadata")]
296 ChunkNotInMetadata,
297 #[error("snapshot data reply exceeds {MAX_IC_SNAPSHOT_DATA_REPLY_BYTES} bytes")]
299 ReplyTooLarge,
300 #[error("invalid or unsupported snapshot data reply")]
302 InvalidReply,
303 #[error("snapshot data length differs from the requested range")]
305 LengthMismatch,
306 #[error("snapshot data differs from the requested chunk hash")]
308 ChunkHashMismatch,
309}
310
311#[cfg(test)]
312mod tests;