Skip to main content

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

1//! Immutable local download declarations using the existing v1 journal schema.
2
3use super::super::json::check_json_size;
4use super::{DownloadIntegrityError, DownloadJournalError, DownloadJournalGuard};
5use crate::{
6    model::{
7        artifacts::ArtifactChecksumRecord,
8        download_journal::{DownloadJournalRecord, MAX_DOWNLOAD_JOURNAL_BYTES},
9        operation_plan::OperationPlanRecord,
10    },
11    ops::persistence::{
12        BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError, create_json_durable,
13        read_json, read_operation_plan,
14    },
15    policy::download_integrity::validate,
16};
17use std::path::Path;
18use thiserror::Error;
19
20const MANIFEST_FILE: &str = "download-manifest.json";
21
22impl DownloadJournalGuard<'_> {
23    /// Replay exact manifest evidence while borrowing the already-held original journal.
24    ///
25    /// Requires the retained plan and unchanged guarded journal before/after admission,
26    /// without acquiring a second journal lock or reading artifact trees. This grants
27    /// no current byte, backend, application or effect authority.
28    /// # Errors
29    /// Rejects unsafe/missing/changed originals, wrong identity and manifest contention.
30    pub fn read_download_manifest(
31        &self,
32        plan: &OperationPlanRecord,
33        expected: &ArtifactChecksumRecord,
34    ) -> Result<DownloadJournalRecord, DownloadManifestError> {
35        self.check_usable()?;
36        self.require_unchanged_integrity_journal()?;
37        let path = self.layout.root().join(MANIFEST_FILE);
38        let _lock = JournalLock::acquire(&path)?;
39        let record = read_manifest_record(self.layout, plan, expected)?;
40        if self.record()? != &record {
41            return Err(DownloadManifestError::JournalChanged);
42        }
43        self.require_unchanged_integrity_journal()?;
44        self.layout.check_root()?;
45        Ok(record)
46    }
47
48    /// Freshly verify and immutably publish the exact original durable download set.
49    ///
50    /// Reuses the existing journal schema/identity owner and guarded no-follow byte
51    /// verification. The private bounded file is never replaced. An existing file
52    /// or lost publication reply requires explicit exact local replay. This changes
53    /// no journal, spending or references and invokes no provider. Stable byte
54    /// custody, complete transfer, authenticated snapshots and consistency remain
55    /// integration-owned; this is not the full product backup manifest/terminal proof.
56    /// # Errors
57    /// Rejects original/evidence/byte drift, incomplete sets, contention and publication failures.
58    pub fn publish_download_manifest(
59        &self,
60        plan: &OperationPlanRecord,
61    ) -> Result<ArtifactChecksumRecord, DownloadManifestError> {
62        self.publish_manifest_with(plan, create_json_durable)
63    }
64
65    fn publish_manifest_with(
66        &self,
67        plan: &OperationPlanRecord,
68        writer: impl FnOnce(&Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
69    ) -> Result<ArtifactChecksumRecord, DownloadManifestError> {
70        self.check_usable()?;
71        let path = self.layout.root().join(MANIFEST_FILE);
72        let _lock = JournalLock::acquire(&path)?;
73        self.verify_durable_artifacts(plan)?;
74        writer(&path, self.record()?)?;
75        Ok(self.record()?.digest())
76    }
77}
78
79/// Replay exact immutable download evidence under its retained original plan/journal.
80///
81/// This reads bounded machine records only; artifact trees may be absent or changed.
82/// It never rechecks bytes or remote state, recreates evidence, repairs a mismatch,
83/// resets allowances or releases references. Drop active download guards first.
84/// Fresh local byte verification remains an explicit separate operation.
85/// # Errors
86/// Rejects unsafe/missing/excessive evidence, wrong identity, incomplete selected sets,
87/// original journal drift and contention. No conflicting evidence is overwritten.
88pub fn read_download_manifest(
89    layout: &BackupLayoutGuard,
90    plan: &OperationPlanRecord,
91    expected: &ArtifactChecksumRecord,
92) -> Result<DownloadJournalRecord, DownloadManifestError> {
93    layout.check_root()?;
94    let path = layout.root().join(MANIFEST_FILE);
95    let _lock = JournalLock::acquire(&path)?;
96    let record = read_manifest_record(layout, plan, expected)?;
97    let journal = DownloadJournalGuard::open(layout, plan.digest().hash())?;
98    if journal.record()? != &record {
99        return Err(DownloadManifestError::JournalChanged);
100    }
101    layout.check_root()?;
102    Ok(record)
103}
104
105fn read_manifest_record(
106    layout: &BackupLayoutGuard,
107    plan: &OperationPlanRecord,
108    expected: &ArtifactChecksumRecord,
109) -> Result<DownloadJournalRecord, DownloadManifestError> {
110    let path = layout.root().join(MANIFEST_FILE);
111    let record: DownloadJournalRecord = read_json(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
112    check_json_size(&record, MAX_DOWNLOAD_JOURNAL_BYTES)?;
113    if &record.digest() != expected {
114        return Err(DownloadManifestError::DigestMismatch);
115    }
116    read_operation_plan(layout, &plan.digest()).map_err(DownloadIntegrityError::from)?;
117    validate(plan, &record).map_err(DownloadIntegrityError::from)?;
118    Ok(record)
119}
120
121/// Typed immutable local declaration publication/replay failure.
122#[derive(Debug, Error)]
123pub enum DownloadManifestError {
124    /// The supplied exact manifest fingerprint differs from retained evidence.
125    #[error("download manifest digest mismatch")]
126    DigestMismatch,
127    /// Immutable manifest and original retained journal no longer agree.
128    #[error("download manifest differs from original retained journal")]
129    JournalChanged,
130    /// Original-plan, selected-set or fresh local byte verification failed.
131    #[error(transparent)]
132    Integrity(#[from] DownloadIntegrityError),
133    /// Original journal/layout admission failed.
134    #[error(transparent)]
135    Journal(#[from] DownloadJournalError),
136    /// Manifest exclusion failed.
137    #[error(transparent)]
138    Lock(#[from] JournalLockError),
139    /// Bounded records or immutable durable publication failed.
140    #[error(transparent)]
141    Persistence(#[from] PersistenceError),
142}
143
144#[cfg(all(test, unix))]
145mod tests;