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::{DownloadIntegrityError, DownloadJournalError, DownloadJournalGuard, check_size};
4use crate::{
5    model::{
6        artifacts::ArtifactChecksumRecord,
7        download_journal::{DownloadJournalRecord, MAX_DOWNLOAD_JOURNAL_BYTES},
8        operation_plan::OperationPlanRecord,
9    },
10    ops::persistence::{
11        BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError, create_json_durable,
12        read_json, 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_json_durable)
62    }
63
64    fn publish_manifest_with(
65        &self,
66        plan: &OperationPlanRecord,
67        writer: impl FnOnce(&Path, &DownloadJournalRecord) -> 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        writer(&path, self.record()?)?;
74        Ok(self.record()?.digest())
75    }
76}
77
78/// Replay exact immutable download evidence under its retained original plan/journal.
79///
80/// This reads bounded machine records only; artifact trees may be absent or changed.
81/// It never rechecks bytes or remote state, recreates evidence, repairs a mismatch,
82/// resets allowances or releases references. Drop active download guards first.
83/// Fresh local byte verification remains an explicit separate operation.
84/// # Errors
85/// Rejects unsafe/missing/excessive evidence, wrong identity, incomplete selected sets,
86/// original journal drift and contention. No conflicting evidence is overwritten.
87pub fn read_download_manifest(
88    layout: &BackupLayoutGuard,
89    plan: &OperationPlanRecord,
90    expected: &ArtifactChecksumRecord,
91) -> Result<DownloadJournalRecord, DownloadManifestError> {
92    layout.check_root()?;
93    let path = layout.root().join(MANIFEST_FILE);
94    let _lock = JournalLock::acquire(&path)?;
95    let record = read_manifest_record(layout, plan, expected)?;
96    let journal = DownloadJournalGuard::open(layout, plan.digest().hash())?;
97    if journal.record()? != &record {
98        return Err(DownloadManifestError::JournalChanged);
99    }
100    layout.check_root()?;
101    Ok(record)
102}
103
104fn read_manifest_record(
105    layout: &BackupLayoutGuard,
106    plan: &OperationPlanRecord,
107    expected: &ArtifactChecksumRecord,
108) -> Result<DownloadJournalRecord, DownloadManifestError> {
109    let path = layout.root().join(MANIFEST_FILE);
110    let record: DownloadJournalRecord = read_json(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
111    check_size(&record)?;
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;