ic_backup/model/download_journal/
mod.rs1use crate::model::evidence_digest::EvidenceDigest;
4
5use super::artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash};
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9pub const MAX_DOWNLOAD_JOURNAL_BYTES: u64 = 1024 * 1024;
11pub const MAX_SNAPSHOT_ID_BYTES: usize = 256;
13
14#[derive(Clone, Debug)]
18pub struct DownloadArtifactRequest {
19 pub canister_id: String,
21 pub snapshot_id: String,
23 pub snapshot_taken_at_timestamp: u64,
25 pub snapshot_total_size_bytes: u64,
27}
28
29#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
31pub enum ArtifactStateRecord {
32 Created,
34 ChecksumVerified,
36 Durable,
38}
39
40pub(crate) enum ArtifactTransition {
43 ChecksumVerified(ArtifactChecksumRecord),
44 Durable,
45}
46
47impl ArtifactTransition {
48 const fn state(&self) -> ArtifactStateRecord {
49 match self {
50 Self::ChecksumVerified(_) => ArtifactStateRecord::ChecksumVerified,
51 Self::Durable => ArtifactStateRecord::Durable,
52 }
53 }
54}
55
56#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
58#[serde(try_from = "ArtifactFields")]
59pub struct DownloadArtifactRecord {
60 canister_id: String,
61 snapshot_id: String,
62 snapshot_taken_at_timestamp: u64,
63 snapshot_total_size_bytes: u64,
64 staging_path: String,
65 artifact_path: String,
66 #[serde(flatten)]
67 progress: ArtifactProgress,
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
72enum ArtifactProgress {
73 Created,
74 ChecksumVerified(ArtifactChecksumRecord),
75 Durable(ArtifactChecksumRecord),
76}
77impl ArtifactProgress {
78 const fn state(&self) -> ArtifactStateRecord {
79 match self {
80 Self::Created => ArtifactStateRecord::Created,
81 Self::ChecksumVerified(_) => ArtifactStateRecord::ChecksumVerified,
82 Self::Durable(_) => ArtifactStateRecord::Durable,
83 }
84 }
85 const fn checksum(&self) -> Option<&ArtifactChecksumRecord> {
86 match self {
87 Self::ChecksumVerified(checksum) | Self::Durable(checksum) => Some(checksum),
88 Self::Created => None,
89 }
90 }
91}
92impl Serialize for ArtifactProgress {
93 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
94 use serde::ser::SerializeStruct;
95 let mut fields = serializer.serialize_struct("ArtifactProgress", 2)?;
96 fields.serialize_field("state", &self.state())?;
97 fields.serialize_field("checksum", &self.checksum())?;
98 fields.end()
99 }
100}
101
102#[derive(Deserialize)]
103#[serde(deny_unknown_fields)]
104struct ArtifactFields {
105 canister_id: String,
106 snapshot_id: String,
107 snapshot_taken_at_timestamp: u64,
108 snapshot_total_size_bytes: u64,
109 staging_path: String,
110 artifact_path: String,
111 state: ArtifactStateRecord,
112 #[serde(deserialize_with = "crate::model::required_option::deserialize")]
113 checksum: Option<ArtifactChecksumRecord>,
114}
115
116impl TryFrom<ArtifactFields> for DownloadArtifactRecord {
117 type Error = DownloadJournalRecordError;
118 fn try_from(fields: ArtifactFields) -> Result<Self, Self::Error> {
119 let mut record = Self::new(DownloadArtifactRequest {
120 canister_id: fields.canister_id,
121 snapshot_id: fields.snapshot_id,
122 snapshot_taken_at_timestamp: fields.snapshot_taken_at_timestamp,
123 snapshot_total_size_bytes: fields.snapshot_total_size_bytes,
124 })?;
125 if fields.staging_path != record.staging_path
127 || fields.artifact_path != record.artifact_path
128 {
129 return Err(DownloadJournalRecordError::ArtifactPathMismatch);
130 }
131 record.progress = match (fields.state, fields.checksum) {
132 (ArtifactStateRecord::Created, None) => ArtifactProgress::Created,
133 (ArtifactStateRecord::ChecksumVerified, Some(checksum)) => {
134 ArtifactProgress::ChecksumVerified(checksum)
135 }
136 (ArtifactStateRecord::Durable, Some(checksum)) => ArtifactProgress::Durable(checksum),
137 (state, _) => return Err(DownloadJournalRecordError::InvalidChecksumState(state)),
138 };
139 Ok(record)
140 }
141}
142
143impl DownloadArtifactRecord {
144 fn new(request: DownloadArtifactRequest) -> Result<Self, DownloadJournalRecordError> {
145 let canister_id = normalize_principal(&request.canister_id)?;
146 if request.snapshot_id.is_empty()
147 || request.snapshot_id.len() > MAX_SNAPSHOT_ID_BYTES
148 || !request
149 .snapshot_id
150 .bytes()
151 .all(|byte| byte.is_ascii_graphic())
152 {
153 return Err(DownloadJournalRecordError::InvalidSnapshotId);
154 }
155 Ok(Self {
156 staging_path: format!("artifacts/{canister_id}.tmp"),
157 artifact_path: format!("artifacts/{canister_id}"),
158 canister_id,
159 snapshot_id: request.snapshot_id,
160 snapshot_taken_at_timestamp: request.snapshot_taken_at_timestamp,
161 snapshot_total_size_bytes: request.snapshot_total_size_bytes,
162 progress: ArtifactProgress::Created,
163 })
164 }
165
166 #[must_use]
168 pub fn canister_id(&self) -> &str {
169 &self.canister_id
170 }
171 #[must_use]
173 pub fn snapshot_id(&self) -> &str {
174 &self.snapshot_id
175 }
176 #[must_use]
178 pub const fn snapshot_taken_at_timestamp(&self) -> u64 {
179 self.snapshot_taken_at_timestamp
180 }
181 #[must_use]
183 pub const fn snapshot_total_size_bytes(&self) -> u64 {
184 self.snapshot_total_size_bytes
185 }
186 #[must_use]
188 pub fn staging_path(&self) -> &str {
189 &self.staging_path
190 }
191 #[must_use]
193 pub fn artifact_path(&self) -> &str {
194 &self.artifact_path
195 }
196 #[must_use]
198 pub const fn state(&self) -> ArtifactStateRecord {
199 self.progress.state()
200 }
201 #[must_use]
203 pub const fn checksum(&self) -> Option<&ArtifactChecksumRecord> {
204 self.progress.checksum()
205 }
206}
207
208#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
212#[serde(try_from = "JournalFields")]
213pub struct DownloadJournalRecord {
214 version: u16,
215 intent: String,
216 artifacts: [DownloadArtifactRecord; 1],
217}
218
219#[derive(Deserialize)]
220#[serde(deny_unknown_fields)]
221struct JournalFields {
222 version: u16,
223 intent: String,
224 artifacts: [DownloadArtifactRecord; 1],
225}
226
227impl TryFrom<JournalFields> for DownloadJournalRecord {
228 type Error = DownloadJournalRecordError;
229 fn try_from(fields: JournalFields) -> Result<Self, Self::Error> {
230 if fields.version != 1 {
231 return Err(DownloadJournalRecordError::UnsupportedVersion(
232 fields.version,
233 ));
234 }
235 Ok(Self {
236 version: 1,
237 intent: canonical_hash(&fields.intent)?,
238 artifacts: fields.artifacts,
239 })
240 }
241}
242
243impl DownloadJournalRecord {
244 pub fn new(
248 intent: &str,
249 request: DownloadArtifactRequest,
250 ) -> Result<Self, DownloadJournalRecordError> {
251 JournalFields {
252 version: 1,
253 intent: intent.to_owned(),
254 artifacts: [DownloadArtifactRecord::new(request)?],
255 }
256 .try_into()
257 }
258
259 #[must_use]
261 pub fn intent(&self) -> &str {
262 &self.intent
263 }
264 #[must_use]
266 pub const fn artifacts(&self) -> &[DownloadArtifactRecord; 1] {
267 &self.artifacts
268 }
269
270 #[must_use]
279 pub fn digest(&self) -> ArtifactChecksumRecord {
280 let mut bytes = EvidenceDigest::new(b"ic-backup/download-journal/v1\0");
281 bytes.append(self.intent.as_bytes());
282 bytes.u64_be(self.artifacts.len() as u64);
283 for entry in &self.artifacts {
284 for value in [
285 entry.canister_id(),
286 entry.snapshot_id(),
287 entry.staging_path(),
288 entry.artifact_path(),
289 ] {
290 bytes.bytes_u32(value.as_bytes());
291 }
292 bytes.u64_be(entry.snapshot_taken_at_timestamp);
293 bytes.u64_be(entry.snapshot_total_size_bytes);
294 bytes.byte(match entry.state() {
295 ArtifactStateRecord::Created => 0,
296 ArtifactStateRecord::ChecksumVerified => 2,
297 ArtifactStateRecord::Durable => 3,
298 });
299 bytes.byte(u8::from(entry.checksum().is_some()));
300 if let Some(checksum) = entry.checksum() {
301 bytes.checksum(checksum);
302 }
303 }
304 bytes.finish()
305 }
306
307 pub(crate) fn artifact(
308 &self,
309 canister: &str,
310 snapshot: &str,
311 ) -> Result<&DownloadArtifactRecord, DownloadJournalRecordError> {
312 self.admit_identity(canister, snapshot)?;
313 Ok(&self.artifacts[0])
314 }
315
316 fn admit_identity(
317 &self,
318 canister: &str,
319 snapshot: &str,
320 ) -> Result<(), DownloadJournalRecordError> {
321 let canister = normalize_principal(canister)?;
322 if self.artifacts[0].canister_id != canister {
323 return Err(DownloadJournalRecordError::UnknownArtifact);
324 }
325 if self.artifacts[0].snapshot_id != snapshot {
326 return Err(DownloadJournalRecordError::SnapshotMismatch);
327 }
328 Ok(())
329 }
330
331 pub(crate) fn advance(
332 &mut self,
333 canister: &str,
334 snapshot: &str,
335 transition: ArtifactTransition,
336 ) -> Result<(), DownloadJournalRecordError> {
337 self.admit_identity(canister, snapshot)?;
338 let entry = &mut self.artifacts[0];
339 entry.progress = match (&entry.progress, transition) {
340 (ArtifactProgress::Created, ArtifactTransition::ChecksumVerified(checksum)) => {
341 ArtifactProgress::ChecksumVerified(checksum)
342 }
343 (ArtifactProgress::ChecksumVerified(checksum), ArtifactTransition::Durable) => {
344 ArtifactProgress::Durable(checksum.clone())
345 }
346 (_, transition) => {
347 return Err(DownloadJournalRecordError::InvalidStateTransition {
348 from: entry.state(),
349 to: transition.state(),
350 });
351 }
352 };
353 Ok(())
354 }
355}
356
357fn normalize_principal(value: &str) -> Result<String, DownloadJournalRecordError> {
358 super::principal::canonical_text(value).ok_or(DownloadJournalRecordError::InvalidPrincipal)
359}
360
361#[derive(Debug, Error)]
363pub enum DownloadJournalRecordError {
364 #[error("unsupported download journal version {0}")]
366 UnsupportedVersion(u16),
367 #[error("invalid download canister principal")]
369 InvalidPrincipal,
370 #[error("invalid download snapshot token")]
372 InvalidSnapshotId,
373 #[error("download artifact path does not match source identity")]
375 ArtifactPathMismatch,
376 #[error("unknown download artifact")]
378 UnknownArtifact,
379 #[error("download snapshot identity mismatch")]
381 SnapshotMismatch,
382 #[error("invalid download state transition from {from:?} to {to:?}")]
384 InvalidStateTransition {
385 from: ArtifactStateRecord,
387 to: ArtifactStateRecord,
389 },
390 #[error("invalid checksum evidence for download state {0:?}")]
392 InvalidChecksumState(ArtifactStateRecord),
393 #[error(transparent)]
395 Checksum(#[from] ChecksumError),
396}
397
398#[cfg(test)]
399mod tests;