Skip to main content

ic_backup/ops/persistence/download_journal/integrity/
mod.rs

1//! Explicit fresh local artifact checks, borrowing journal and layout exclusion.
2
3use super::super::json::read_record;
4use super::{DownloadJournalError, DownloadJournalGuard};
5use crate::{
6    model::{
7        artifacts::ChecksumError,
8        download_journal::{DownloadJournalRecord, MAX_DOWNLOAD_JOURNAL_BYTES},
9        operation_plan::OperationPlanRecord,
10    },
11    ops::{
12        artifacts::{ArtifactError, checksum_directory},
13        persistence::{OperationPlanPersistenceError, PersistenceError, read_operation_plan},
14    },
15    policy::download_integrity::{DownloadIntegrityPolicyError, validate},
16};
17use thiserror::Error;
18
19impl DownloadJournalGuard<'_> {
20    // Manifest publication rechecks original records around its fresh tree hash.
21    pub(super) fn verify_manifest_source(
22        &self,
23        plan: &OperationPlanRecord,
24    ) -> Result<(), DownloadIntegrityError> {
25        self.admit_integrity_original(plan)?;
26        let expected = validate(plan, &self.record)?;
27        self.check_artifact_parent()?;
28        let path = self
29            .layout
30            .root()
31            .join(self.record.artifacts()[0].artifact_path());
32        checksum_directory(&path)?.verify(expected.hash())?;
33        // Detect changed retained declarations or a replaced root during traversal.
34        // This does not turn individual byte reads into an atomic whole-set snapshot.
35        self.admit_integrity_original(plan)?;
36        Ok(())
37    }
38
39    // Both generic and IC verification re-admit original records around byte IO.
40    // Method-specific policy remains with each caller, in its original order.
41    pub(super) fn admit_integrity_original(
42        &self,
43        plan: &OperationPlanRecord,
44    ) -> Result<(), DownloadIntegrityError> {
45        self.check_usable()?;
46        read_operation_plan(self.layout, &plan.digest())?;
47        self.require_unchanged_integrity_journal()
48    }
49
50    pub(super) fn require_unchanged_integrity_journal(&self) -> Result<(), DownloadIntegrityError> {
51        let retained: DownloadJournalRecord =
52            read_record(&self.path(), MAX_DOWNLOAD_JOURNAL_BYTES)?;
53        if retained != self.record {
54            return Err(DownloadIntegrityError::JournalChanged);
55        }
56        Ok(())
57    }
58}
59
60/// Typed fresh local verification failure; original evidence remains retained.
61#[derive(Debug, Error)]
62pub enum DownloadIntegrityError {
63    /// Retained journal differs from the exact declaration held by its guard.
64    #[error("retained download journal changed during integrity verification")]
65    JournalChanged,
66    /// Guard/layout custody is unusable, replaced or unsafe.
67    #[error(transparent)]
68    Journal(#[from] DownloadJournalError),
69    /// Retained original plan cannot be admitted under its exact expected digest.
70    #[error(transparent)]
71    Plan(#[from] OperationPlanPersistenceError),
72    /// Original-plan selected-set or durable-checksum declaration mismatch.
73    #[error(transparent)]
74    Policy(#[from] DownloadIntegrityPolicyError),
75    /// Bounded retained record admission failed.
76    #[error(transparent)]
77    Persistence(#[from] PersistenceError),
78    /// No-follow directory traversal or streaming failed.
79    #[error(transparent)]
80    Artifact(#[from] ArtifactError),
81    /// Current local bytes differ from the exact retained checksum.
82    #[error(transparent)]
83    Checksum(#[from] ChecksumError),
84}
85
86#[cfg(all(test, unix))]
87mod tests;