ic_backup/model/ic_snapshot_data/
mod.rs1mod extents;
4mod kind;
5mod wire;
6pub(crate) use extents::{ExtentPlanningError, planned_extents};
7pub(crate) use kind::{DataExtent, SnapshotRegion};
8
9use super::{
10 artifacts::ArtifactChecksumRecord,
11 ic_request::{IcRequestError, encode_arguments, management_request_digest},
12 ic_snapshot_metadata::IcSnapshotMetadataReply,
13};
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 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 = metadata.request().principal();
46 let arguments = encode_arguments(ReadCanisterSnapshotDataArgs {
47 canister_id: target.into(),
48 snapshot_id: metadata.request().snapshot_id().to_vec(),
49 kind: kind.clone(),
50 })?;
51 Ok(Self {
52 metadata,
53 kind,
54 arguments,
55 })
56 }
57
58 #[must_use]
60 pub const fn metadata(&self) -> &'metadata IcSnapshotMetadataReply<'metadata> {
61 self.metadata
62 }
63
64 #[must_use]
66 pub const fn kind(&self) -> &SnapshotDataKind {
67 &self.kind
68 }
69
70 pub(crate) fn matches_kind(&self, original: &SnapshotDataKind) -> bool {
71 DataExtent::from(&self.kind) == DataExtent::from(original)
72 }
73
74 #[must_use]
76 pub fn target(&self) -> &str {
77 self.metadata.request().target()
78 }
79
80 #[must_use]
82 pub fn snapshot_id(&self) -> &[u8] {
83 self.metadata.request().snapshot_id()
84 }
85
86 #[must_use]
88 pub fn arguments(&self) -> &[u8] {
89 &self.arguments
90 }
91
92 #[must_use]
94 pub const fn receiver(&self) -> &'static str {
95 "aaaaa-aa"
96 }
97
98 #[must_use]
100 pub const fn method(&self) -> &'static str {
101 "read_canister_snapshot_data"
102 }
103
104 #[must_use]
109 pub fn digest(&self) -> ArtifactChecksumRecord {
110 management_request_digest(
111 self.metadata.request().principal().as_slice(),
112 self.method(),
113 &self.arguments,
114 )
115 }
116}
117
118pub(crate) fn validate_kind(
119 metadata: &IcSnapshotMetadataReply<'_>,
120 kind: &SnapshotDataKind,
121) -> Result<(), IcSnapshotDataError> {
122 match DataExtent::from(kind) {
123 DataExtent::Region {
124 region,
125 offset,
126 size,
127 } => {
128 range(offset, size, metadata.region_sizes()[region.index()])?;
129 }
130 DataExtent::Chunk { hash } => {
131 if hash.len() != 32 {
132 return Err(IcSnapshotDataError::InvalidChunkHash);
133 }
134 if metadata.chunk_index(hash).is_none() {
135 return Err(IcSnapshotDataError::ChunkNotInMetadata);
136 }
137 }
138 }
139 Ok(())
140}
141
142fn range(offset: u64, size: u64, total: u64) -> Result<(), IcSnapshotDataError> {
143 let admitted_size = usize::try_from(size)
144 .is_ok_and(|size| (1..=MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES).contains(&size));
145 if !admitted_size {
146 return Err(IcSnapshotDataError::InvalidRangeSize);
147 }
148 if offset.checked_add(size).is_none_or(|end| end > total) {
149 return Err(IcSnapshotDataError::RangeOutsideMetadata);
150 }
151 Ok(())
152}
153
154impl fmt::Debug for IcSnapshotDataRequest<'_> {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 f.debug_struct("IcSnapshotDataRequest")
157 .field("target", &self.target())
158 .field("argument_bytes", &self.arguments.len())
159 .finish_non_exhaustive()
160 }
161}
162
163pub struct IcSnapshotDataReply<'request, 'metadata> {
169 request: &'request IcSnapshotDataRequest<'metadata>,
170 chunk: Vec<u8>,
171 chunk_checksum: ArtifactChecksumRecord,
172 payload_checksum: ArtifactChecksumRecord,
173}
174
175impl<'request, 'metadata> IcSnapshotDataReply<'request, 'metadata> {
176 pub fn decode(
182 request: &'request IcSnapshotDataRequest<'metadata>,
183 bytes: &[u8],
184 ) -> Result<Self, IcSnapshotDataError> {
185 if bytes.len() > MAX_IC_SNAPSHOT_DATA_REPLY_BYTES {
186 return Err(IcSnapshotDataError::ReplyTooLarge);
187 }
188 let chunk = wire::decode(bytes)?;
189 let chunk_digest = ic_host_artifacts::artifact::Sha256Digest::compute(&chunk);
190 let chunk_checksum = ArtifactChecksumRecord::from_digest(*chunk_digest.as_bytes());
191 match DataExtent::from(request.kind()) {
192 DataExtent::Region { size, .. } => {
193 if u64::try_from(chunk.len()).ok() != Some(size) {
194 return Err(IcSnapshotDataError::LengthMismatch);
195 }
196 }
197 DataExtent::Chunk { hash } => {
198 if chunk_digest.as_bytes().as_slice() != hash {
199 return Err(IcSnapshotDataError::ChunkHashMismatch);
200 }
201 }
202 }
203 Ok(Self {
204 request,
205 chunk,
206 chunk_checksum,
207 payload_checksum: ArtifactChecksumRecord::from_bytes(bytes),
208 })
209 }
210
211 #[must_use]
213 pub const fn request(&self) -> &'request IcSnapshotDataRequest<'metadata> {
214 self.request
215 }
216
217 #[must_use]
219 pub fn chunk(&self) -> &[u8] {
220 &self.chunk
221 }
222
223 #[must_use]
225 pub const fn chunk_checksum(&self) -> &ArtifactChecksumRecord {
226 &self.chunk_checksum
227 }
228
229 #[must_use]
231 pub const fn payload_checksum(&self) -> &ArtifactChecksumRecord {
232 &self.payload_checksum
233 }
234
235 #[must_use]
237 pub fn digest(&self) -> ArtifactChecksumRecord {
238 super::evidence_digest::reply(
239 b"ic-backup/ic-snapshot-data-reply/v1\0",
240 [
241 &self.request.metadata().digest(),
242 &self.request.digest(),
243 &self.payload_checksum,
244 ],
245 )
246 }
247}
248
249impl fmt::Debug for IcSnapshotDataReply<'_, '_> {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 f.debug_struct("IcSnapshotDataReply")
252 .field("request", &self.request)
253 .field("chunk_bytes", &self.chunk.len())
254 .field("payload_checksum", &self.payload_checksum)
255 .finish_non_exhaustive()
256 }
257}
258
259#[derive(Debug, Error, Eq, PartialEq)]
261pub enum IcSnapshotDataError {
262 #[error(transparent)]
264 Request(#[from] IcRequestError),
265 #[error("snapshot range size must be 1..={MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES}")]
267 InvalidRangeSize,
268 #[error("snapshot range is outside retained metadata")]
270 RangeOutsideMetadata,
271 #[error("snapshot chunk identity must be 32 bytes")]
273 InvalidChunkHash,
274 #[error("snapshot chunk identity is absent from retained metadata")]
276 ChunkNotInMetadata,
277 #[error("snapshot data reply exceeds {MAX_IC_SNAPSHOT_DATA_REPLY_BYTES} bytes")]
279 ReplyTooLarge,
280 #[error("invalid or unsupported snapshot data reply")]
282 InvalidReply,
283 #[error("snapshot data length differs from the requested range")]
285 LengthMismatch,
286 #[error("snapshot data differs from the requested chunk hash")]
288 ChunkHashMismatch,
289}
290
291#[cfg(test)]
292mod tests;