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, create_encoded, encode_json};
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, read_json,
13        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_encoded)
63    }
64
65    fn publish_manifest_with(
66        &self,
67        plan: &OperationPlanRecord,
68        writer: impl FnOnce(&Path, &[u8]) -> 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        let bytes = encode_json(self.record()?, MAX_DOWNLOAD_JOURNAL_BYTES)?;
75        writer(&path, &bytes)?;
76        Ok(self.record()?.digest())
77    }
78}
79
80/// Replay exact immutable download evidence under its retained original plan/journal.
81///
82/// This reads bounded machine records only; artifact trees may be absent or changed.
83/// It never rechecks bytes or remote state, recreates evidence, repairs a mismatch,
84/// resets allowances or releases references. Drop active download guards first.
85/// Fresh local byte verification remains an explicit separate operation.
86/// # Errors
87/// Rejects unsafe/missing/excessive evidence, wrong identity, incomplete selected sets,
88/// original journal drift and contention. No conflicting evidence is overwritten.
89pub fn read_download_manifest(
90    layout: &BackupLayoutGuard,
91    plan: &OperationPlanRecord,
92    expected: &ArtifactChecksumRecord,
93) -> Result<DownloadJournalRecord, DownloadManifestError> {
94    layout.check_root()?;
95    let path = layout.root().join(MANIFEST_FILE);
96    let _lock = JournalLock::acquire(&path)?;
97    let record = read_manifest_record(layout, plan, expected)?;
98    let journal = DownloadJournalGuard::open(layout, plan.digest().hash())?;
99    if journal.record()? != &record {
100        return Err(DownloadManifestError::JournalChanged);
101    }
102    layout.check_root()?;
103    Ok(record)
104}
105
106fn read_manifest_record(
107    layout: &BackupLayoutGuard,
108    plan: &OperationPlanRecord,
109    expected: &ArtifactChecksumRecord,
110) -> Result<DownloadJournalRecord, DownloadManifestError> {
111    let path = layout.root().join(MANIFEST_FILE);
112    let record: DownloadJournalRecord = read_json(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
113    check_json_size(&record, MAX_DOWNLOAD_JOURNAL_BYTES)?;
114    if &record.digest() != expected {
115        return Err(DownloadManifestError::DigestMismatch);
116    }
117    read_operation_plan(layout, &plan.digest()).map_err(DownloadIntegrityError::from)?;
118    validate(plan, &record).map_err(DownloadIntegrityError::from)?;
119    Ok(record)
120}
121
122/// Typed immutable local declaration publication/replay failure.
123#[derive(Debug, Error)]
124pub enum DownloadManifestError {
125    /// The supplied exact manifest fingerprint differs from retained evidence.
126    #[error("download manifest digest mismatch")]
127    DigestMismatch,
128    /// Immutable manifest and original retained journal no longer agree.
129    #[error("download manifest differs from original retained journal")]
130    JournalChanged,
131    /// Original-plan, selected-set or fresh local byte verification failed.
132    #[error(transparent)]
133    Integrity(#[from] DownloadIntegrityError),
134    /// Original journal/layout admission failed.
135    #[error(transparent)]
136    Journal(#[from] DownloadJournalError),
137    /// Manifest exclusion failed.
138    #[error(transparent)]
139    Lock(#[from] JournalLockError),
140    /// Bounded records or immutable durable publication failed.
141    #[error(transparent)]
142    Persistence(#[from] PersistenceError),
143}
144
145#[cfg(all(test, unix))]
146mod tests;