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;
8
9use super::{
10 artifacts::ArtifactChecksumRecord,
11 ic_request::{IcRequestError, encode_arguments, management_request_digest},
12 ic_snapshot_metadata::IcSnapshotMetadataReply,
13};
14use ic_platform::Principal;
15use ic_platform::management_canister_types::{ReadCanisterSnapshotDataArgs, SnapshotDataKind};
16use std::fmt;
17use thiserror::Error;
18
19pub const MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES: usize = 1024 * 1024;
21pub const MAX_IC_SNAPSHOT_DATA_REPLY_BYTES: usize = 2 * 1024 * 1024;
23
24pub struct IcSnapshotDataRequest<'metadata> {
30 metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
31 kind: SnapshotDataKind,
32 target_bytes: Vec<u8>,
33 arguments: Vec<u8>,
34}
35
36impl<'metadata> IcSnapshotDataRequest<'metadata> {
37 pub fn new(
43 metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
44 kind: SnapshotDataKind,
45 ) -> Result<Self, IcSnapshotDataError> {
46 validate_kind(metadata, &kind)?;
47 let target = Principal::from_text(metadata.request().target())
48 .map_err(|_| IcRequestError::InvalidTarget)?;
49 let arguments = encode_arguments(ReadCanisterSnapshotDataArgs {
50 canister_id: target.into(),
51 snapshot_id: metadata.request().snapshot_id().to_vec(),
52 kind: kind.clone(),
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 DataExtent::from(&self.kind) == DataExtent::from(original)
76 }
77
78 #[must_use]
80 pub fn target(&self) -> &str {
81 self.metadata.request().target()
82 }
83
84 #[must_use]
86 pub fn snapshot_id(&self) -> &[u8] {
87 self.metadata.request().snapshot_id()
88 }
89
90 #[must_use]
92 pub fn arguments(&self) -> &[u8] {
93 &self.arguments
94 }
95
96 #[must_use]
98 pub const fn receiver(&self) -> &'static str {
99 "aaaaa-aa"
100 }
101
102 #[must_use]
104 pub const fn method(&self) -> &'static str {
105 "read_canister_snapshot_data"
106 }
107
108 #[must_use]
113 pub fn digest(&self) -> ArtifactChecksumRecord {
114 management_request_digest(&self.target_bytes, self.method(), &self.arguments)
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 index,
125 offset,
126 size,
127 } => {
128 range(offset, size, metadata.region_sizes()[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;