Skip to main content

ic_backup/policy/download_integrity/
mod.rs

1//! Pure original-plan admission of durable local download declarations.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    download_journal::{ArtifactStateRecord, DownloadArtifactRecord, DownloadJournalRecord},
6    operation_plan::OperationPlanRecord,
7};
8use thiserror::Error;
9
10/// Borrowed durable artifact declaration with its required exact checksum.
11///
12/// This is a structural projection, not a fresh byte verification or remote receipt.
13#[derive(Debug)]
14pub struct DurableDownloadArtifactView<'a> {
15    artifact: &'a DownloadArtifactRecord,
16    checksum: &'a ArtifactChecksumRecord,
17}
18
19impl<'a> DurableDownloadArtifactView<'a> {
20    /// Read exact retained canister/snapshot identity, metadata and canonical path.
21    #[must_use]
22    pub const fn artifact(&self) -> &'a DownloadArtifactRecord {
23        self.artifact
24    }
25
26    /// Read the retained required checksum without reconstructing its metadata.
27    #[must_use]
28    pub const fn checksum(&self) -> &'a ArtifactChecksumRecord {
29        self.checksum
30    }
31}
32
33/// Read-only exact selected set under the original plan and durable journal.
34///
35/// No IO, fresh verification flag, terminal proof or reference-release admission
36/// is provided. Transfer completeness and actual capture identity remain with
37/// the integration; durable state is only a retained local declaration.
38#[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    /// Read the full original plan, including original selection and spending limits.
47    #[must_use]
48    pub const fn plan(&self) -> &'a OperationPlanRecord {
49        self.plan
50    }
51
52    /// Read exact retained journal identity and snapshot declarations.
53    #[must_use]
54    pub const fn journal(&self) -> &'a DownloadJournalRecord {
55        self.journal
56    }
57
58    /// Read the complete selected artifact set in canonical principal order.
59    #[must_use]
60    pub fn artifacts(&self) -> &[DurableDownloadArtifactView<'a>] {
61        &self.artifacts
62    }
63}
64
65/// Require original intent, exact selected-target coverage and durable checksums.
66///
67/// The canonical record owners already bound/normalize entries and reject duplicates.
68/// This comparison neither observes bytes nor authenticates retained declarations.
69/// Snapshot tokens/timestamp/size remain exactly those retained by the journal;
70/// the pre-capture plan cannot establish an eventual captured snapshot's identity.
71///
72/// # Errors
73/// Rejects changed full-plan intent, missing/extra/different selected targets and
74/// any artifact without retained durable publication and checksum.
75pub 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/// Typed structural mismatch, before any artifact IO or remote effect.
113#[derive(Debug, Error, Eq, PartialEq)]
114pub enum DownloadIntegrityPolicyError {
115    /// The journal is not bound to the full original plan digest.
116    #[error("download journal differs from original plan intent")]
117    IntentMismatch,
118    /// Journal targets are not exactly the original selected physical set.
119    #[error("download journal differs from the exact selected target set")]
120    TargetSetMismatch,
121    /// An exact artifact lacks retained durable publication and its checksum.
122    #[error("artifact for {canister_id} lacks durable checksum evidence ({state:?})")]
123    NonDurableArtifact {
124        /// Canonical exact physical target.
125        canister_id: String,
126        /// Actually retained local state.
127        state: ArtifactStateRecord,
128    },
129}
130
131#[cfg(test)]
132mod tests;