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::{create_encoded, encode_json, read_record};
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_operation_plan,
13    },
14    policy::download_integrity::validate,
15};
16use std::path::Path;
17use thiserror::Error;
18
19const MANIFEST_FILE: &str = "download-manifest.json";
20
21impl DownloadJournalGuard<'_> {
22    /// Replay exact manifest evidence while borrowing the already-held original journal.
23    ///
24    /// Requires the retained plan and unchanged guarded journal before/after admission,
25    /// without acquiring a second journal lock or reading artifact trees. This grants
26    /// no current byte, backend, application or effect authority.
27    /// # Errors
28    /// Rejects unsafe/missing/changed originals, wrong identity and manifest contention.
29    pub fn read_download_manifest(
30        &self,
31        plan: &OperationPlanRecord,
32        expected: &ArtifactChecksumRecord,
33    ) -> Result<DownloadJournalRecord, DownloadManifestError> {
34        self.check_usable()?;
35        self.require_unchanged_integrity_journal()?;
36        let path = self.layout.root().join(MANIFEST_FILE);
37        let _lock = JournalLock::acquire(&path)?;
38        let record = read_manifest_record(self.layout, plan, expected)?;
39        if self.record()? != &record {
40            return Err(DownloadManifestError::JournalChanged);
41        }
42        self.require_unchanged_integrity_journal()?;
43        self.layout.check_root()?;
44        Ok(record)
45    }
46
47    /// Freshly verify and immutably publish the exact original durable download set.
48    ///
49    /// Reuses the existing journal schema/identity owner and guarded no-follow byte
50    /// verification. The private bounded file is never replaced. An existing file
51    /// or lost publication reply requires explicit exact local replay. This changes
52    /// no journal, spending or references and invokes no provider. Stable byte
53    /// custody, complete transfer, authenticated snapshots and consistency remain
54    /// integration-owned; this is not the full product backup manifest/terminal proof.
55    /// # Errors
56    /// Rejects original/evidence/byte drift, incomplete sets, contention and publication failures.
57    pub fn publish_download_manifest(
58        &self,
59        plan: &OperationPlanRecord,
60    ) -> Result<ArtifactChecksumRecord, DownloadManifestError> {
61        self.publish_manifest_with(plan, create_encoded)
62    }
63
64    fn publish_manifest_with(
65        &self,
66        plan: &OperationPlanRecord,
67        writer: impl FnOnce(&Path, &[u8]) -> Result<(), PersistenceError>,
68    ) -> Result<ArtifactChecksumRecord, DownloadManifestError> {
69        self.check_usable()?;
70        let path = self.layout.root().join(MANIFEST_FILE);
71        let _lock = JournalLock::acquire(&path)?;
72        self.verify_durable_artifacts(plan)?;
73        let bytes = encode_json(self.record()?, MAX_DOWNLOAD_JOURNAL_BYTES)?;
74        writer(&path, &bytes)?;
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_record(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
112    if &record.digest() != expected {
113        return Err(DownloadManifestError::DigestMismatch);
114    }
115    read_operation_plan(layout, &plan.digest()).map_err(DownloadIntegrityError::from)?;
116    validate(plan, &record).map_err(DownloadIntegrityError::from)?;
117    Ok(record)
118}
119
120/// Typed immutable local declaration publication/replay failure.
121#[derive(Debug, Error)]
122pub enum DownloadManifestError {
123    /// The supplied exact manifest fingerprint differs from retained evidence.
124    #[error("download manifest digest mismatch")]
125    DigestMismatch,
126    /// Immutable manifest and original retained journal no longer agree.
127    #[error("download manifest differs from original retained journal")]
128    JournalChanged,
129    /// Original-plan, selected-set or fresh local byte verification failed.
130    #[error(transparent)]
131    Integrity(#[from] DownloadIntegrityError),
132    /// Original journal/layout admission failed.
133    #[error(transparent)]
134    Journal(#[from] DownloadJournalError),
135    /// Manifest exclusion failed.
136    #[error(transparent)]
137    Lock(#[from] JournalLockError),
138    /// Bounded records or immutable durable publication failed.
139    #[error(transparent)]
140    Persistence(#[from] PersistenceError),
141}
142
143#[cfg(all(test, unix))]
144mod tests;