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, DurableDownloadView, validate},
16};
17use thiserror::Error;
18
19impl DownloadJournalGuard<'_> {
20    /// Explicitly reverify every published artifact under the retained original plan.
21    ///
22    /// Requires the exact persisted plan and unchanged held journal before and after
23    /// no-follow checksumming. The returned view borrows journal/layout custody.
24    /// This reads local bytes; ordinary journal reopen/resume remains effect-free
25    /// and does not trigger verification. Nothing is written, pruned or released.
26    ///
27    /// File checks are sequential observations, not an atomic filesystem snapshot.
28    /// Integrations retain stable byte custody and qualify complete backend transfer,
29    /// authentic snapshot/receipt identity and terminal/reference-release evidence.
30    ///
31    /// # Errors
32    /// Rejects unusable/replaced custody, missing/changed plans or journals, incomplete
33    /// exact selected coverage, non-durable entries, unsafe/missing trees and changed bytes.
34    pub fn verify_durable_artifacts<'a>(
35        &'a self,
36        plan: &'a OperationPlanRecord,
37    ) -> Result<DurableDownloadView<'a>, DownloadIntegrityError> {
38        self.admit_integrity_original(plan)?;
39        let view = validate(plan, &self.record)?;
40        self.check_artifact_parent()?;
41        for artifact in view.artifacts() {
42            let path = self.layout.root().join(artifact.artifact().artifact_path());
43            checksum_directory(&path)?.verify(artifact.checksum().hash())?;
44        }
45        // Detect changed retained declarations or a replaced root during traversal.
46        // This does not turn individual byte reads into an atomic whole-set snapshot.
47        self.admit_integrity_original(plan)?;
48        Ok(view)
49    }
50
51    // Both generic and IC verification re-admit original records around byte IO.
52    // Method-specific policy remains with each caller, in its original order.
53    pub(super) fn admit_integrity_original(
54        &self,
55        plan: &OperationPlanRecord,
56    ) -> Result<(), DownloadIntegrityError> {
57        self.check_usable()?;
58        read_operation_plan(self.layout, &plan.digest())?;
59        self.require_unchanged_integrity_journal()
60    }
61
62    pub(super) fn require_unchanged_integrity_journal(&self) -> Result<(), DownloadIntegrityError> {
63        let retained: DownloadJournalRecord =
64            read_record(&self.path(), MAX_DOWNLOAD_JOURNAL_BYTES)?;
65        if retained != self.record {
66            return Err(DownloadIntegrityError::JournalChanged);
67        }
68        Ok(())
69    }
70}
71
72/// Typed fresh local verification failure; original evidence remains retained.
73#[derive(Debug, Error)]
74pub enum DownloadIntegrityError {
75    /// Retained journal differs from the exact declaration held by its guard.
76    #[error("retained download journal changed during integrity verification")]
77    JournalChanged,
78    /// Guard/layout custody is unusable, replaced or unsafe.
79    #[error(transparent)]
80    Journal(#[from] DownloadJournalError),
81    /// Retained original plan cannot be admitted under its exact expected digest.
82    #[error(transparent)]
83    Plan(#[from] OperationPlanPersistenceError),
84    /// Original-plan selected-set or durable-checksum declaration mismatch.
85    #[error(transparent)]
86    Policy(#[from] DownloadIntegrityPolicyError),
87    /// Bounded retained record admission failed.
88    #[error(transparent)]
89    Persistence(#[from] PersistenceError),
90    /// No-follow directory traversal or streaming failed.
91    #[error(transparent)]
92    Artifact(#[from] ArtifactError),
93    /// Current local bytes differ from the exact retained checksum.
94    #[error(transparent)]
95    Checksum(#[from] ChecksumError),
96}
97
98#[cfg(all(test, unix))]
99mod tests;