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