ic_backup/policy/download_integrity/
mod.rs1use crate::model::{
4 artifacts::ArtifactChecksumRecord,
5 download_journal::{ArtifactStateRecord, DownloadArtifactRecord, DownloadJournalRecord},
6 operation_plan::OperationPlanRecord,
7};
8use thiserror::Error;
9
10#[derive(Debug)]
14pub struct DurableDownloadArtifactView<'a> {
15 artifact: &'a DownloadArtifactRecord,
16 checksum: &'a ArtifactChecksumRecord,
17}
18
19impl<'a> DurableDownloadArtifactView<'a> {
20 #[must_use]
22 pub const fn artifact(&self) -> &'a DownloadArtifactRecord {
23 self.artifact
24 }
25
26 #[must_use]
28 pub const fn checksum(&self) -> &'a ArtifactChecksumRecord {
29 self.checksum
30 }
31}
32
33#[derive(Debug)]
39pub struct DurableDownloadView<'a> {
40 plan: &'a OperationPlanRecord,
41 journal: &'a DownloadJournalRecord,
42 artifacts: Vec<DurableDownloadArtifactView<'a>>,
43}
44
45impl<'a> DurableDownloadView<'a> {
46 #[must_use]
48 pub const fn plan(&self) -> &'a OperationPlanRecord {
49 self.plan
50 }
51
52 #[must_use]
54 pub const fn journal(&self) -> &'a DownloadJournalRecord {
55 self.journal
56 }
57
58 #[must_use]
60 pub fn artifacts(&self) -> &[DurableDownloadArtifactView<'a>] {
61 &self.artifacts
62 }
63}
64
65pub fn validate<'a>(
76 plan: &'a OperationPlanRecord,
77 journal: &'a DownloadJournalRecord,
78) -> Result<DurableDownloadView<'a>, DownloadIntegrityPolicyError> {
79 if plan.digest().hash() != journal.intent() {
80 return Err(DownloadIntegrityPolicyError::IntentMismatch);
81 }
82 if plan.selected_targets().len() != journal.artifacts().len()
83 || plan
84 .selected_targets()
85 .iter()
86 .zip(journal.artifacts())
87 .any(|(target, artifact)| target != artifact.canister_id())
88 {
89 return Err(DownloadIntegrityPolicyError::TargetSetMismatch);
90 }
91 let artifacts = journal
92 .artifacts()
93 .iter()
94 .map(|artifact| {
95 let checksum = artifact
96 .checksum()
97 .filter(|_| artifact.state() == ArtifactStateRecord::Durable)
98 .ok_or_else(|| DownloadIntegrityPolicyError::NonDurableArtifact {
99 canister_id: artifact.canister_id().to_owned(),
100 state: artifact.state(),
101 })?;
102 Ok(DurableDownloadArtifactView { artifact, checksum })
103 })
104 .collect::<Result<Vec<_>, DownloadIntegrityPolicyError>>()?;
105 Ok(DurableDownloadView {
106 plan,
107 journal,
108 artifacts,
109 })
110}
111
112#[derive(Debug, Error, Eq, PartialEq)]
114pub enum DownloadIntegrityPolicyError {
115 #[error("download journal differs from original plan intent")]
117 IntentMismatch,
118 #[error("download journal differs from the exact selected target set")]
120 TargetSetMismatch,
121 #[error("artifact for {canister_id} lacks durable checksum evidence ({state:?})")]
123 NonDurableArtifact {
124 canister_id: String,
126 state: ArtifactStateRecord,
128 },
129}
130
131#[cfg(test)]
132mod tests;