Skip to main content

ic_backup/ops/persistence/restore_reference_release/
mod.rs

1//! Release exact source dependencies under terminal journal and command custody.
2
3use crate::{
4    model::{
5        artifacts::ArtifactChecksumRecord, effect_graph::MAX_EFFECT_OPERATIONS,
6        restore_references::RestoreReferenceRecord,
7    },
8    ops::persistence::{
9        BackupLayoutGuard, CommandLifetimeLockError, CommandQuiescenceGuard, JournalLock,
10        PersistenceError, write_json_durable,
11    },
12    view::restore_reference_release::TerminalRestoreEvidence,
13};
14use std::{collections::BTreeSet, path::Path};
15use thiserror::Error;
16
17/// Maximum bytes supplied to the integration's terminal-journal validator.
18pub const MAX_RESTORE_TERMINAL_BYTES: usize = 16 * 1024 * 1024;
19
20/// Validate complete original restore settlement and return its exact command set.
21///
22/// Implementations authenticate their maintained journal schema, original intent,
23/// complete application/effect obligations and every external command identity.
24/// An all-Applied projection alone is insufficient. The library supplies freshly
25/// read journal bytes under its held mutation lock; no cached completion flag is used.
26pub trait RestoreTerminalEvidenceProvider {
27    /// Product-owned typed terminal qualification failure.
28    type Error: std::error::Error + 'static;
29
30    /// Qualify the current terminal bytes and complete retained command-custody set.
31    ///
32    /// # Errors
33    /// Reject incomplete, changed, unauthenticated or unsettled original evidence.
34    fn qualify_terminal(
35        &self,
36        journal: &Path,
37        bytes: &[u8],
38    ) -> Result<TerminalRestoreEvidence, Self::Error>;
39}
40
41/// Observable durable reference transition; neither outcome dispatches IC effects.
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum RestoreReferenceReleaseOutcome {
44    /// The exact dependency was durably removed.
45    Released,
46    /// Current terminal/custody evidence passes and the dependency is already absent.
47    AlreadyReleased,
48}
49
50impl BackupLayoutGuard {
51    /// Release one exact restore dependency after fresh terminal and command checks.
52    ///
53    /// Keep the original journal mutation guard held. Every declared command sidecar
54    /// must already exist with its retained inode/device identity and be exclusively
55    /// quiescent. Guards remain held through reference publication. Failures leave
56    /// dependencies retained; lost publication responses reconcile by the same call.
57    /// This performs no application fence release, retry or paid effect.
58    ///
59    /// # Errors
60    /// Refuses changed layouts/journals, terminal admission, changed intent, invalid
61    /// or conflicting custody identities, active descendants and publication failures.
62    pub fn release_restore<P: RestoreTerminalEvidenceProvider>(
63        &self,
64        journal_guard: &JournalLock,
65        provider: &P,
66    ) -> Result<RestoreReferenceReleaseOutcome, RestoreReferenceReleaseError<P::Error>> {
67        let journal = journal_guard.journal();
68        self.check_root()?;
69        let bytes = read_terminal(journal)?;
70        let terminal = provider
71            .qualify_terminal(journal, &bytes)
72            .map_err(RestoreReferenceReleaseError::Terminal)?;
73        let reference =
74            RestoreReferenceRecord::new(journal.to_path_buf(), terminal.authority().hash())
75                .map_err(PersistenceError::from)?;
76        if terminal.commands().len() > MAX_EFFECT_OPERATIONS {
77            return Err(RestoreReferenceReleaseError::InvalidCommandSet);
78        }
79        let mut operations = BTreeSet::new();
80        let mut quiescence = Vec::with_capacity(terminal.commands().len());
81        for command in terminal.commands() {
82            if command.journal() != journal || !operations.insert(command.operation_sequence()) {
83                return Err(RestoreReferenceReleaseError::InvalidCommandSet);
84            }
85            quiescence.push(CommandQuiescenceGuard::acquire(command)?);
86        }
87        let mut references = self.restore_references()?;
88        let released = references
89            .release(&reference)
90            .map_err(PersistenceError::from)?;
91        // The validator may perform product reads; preserve the exact bytes it admitted.
92        if ArtifactChecksumRecord::from_bytes(&read_terminal(journal)?)
93            != ArtifactChecksumRecord::from_bytes(&bytes)
94        {
95            return Err(RestoreReferenceReleaseError::JournalChanged);
96        }
97        self.check_root()?;
98        if released {
99            write_json_durable(&self.root().join("restore-references.json"), &references)?;
100        }
101        self.sync_references()?;
102        drop(quiescence);
103        Ok(if released {
104            RestoreReferenceReleaseOutcome::Released
105        } else {
106            RestoreReferenceReleaseOutcome::AlreadyReleased
107        })
108    }
109}
110
111fn read_terminal(path: &Path) -> Result<Vec<u8>, PersistenceError> {
112    ic_host_fs::read::read_file_no_follow(path, MAX_RESTORE_TERMINAL_BYTES).map_err(|error| {
113        match error {
114            ic_host_artifacts::artifact::ArtifactError::LimitExceeded { .. } => {
115                PersistenceError::RecordTooLarge {
116                    limit: MAX_RESTORE_TERMINAL_BYTES as u64,
117                }
118            }
119            error => PersistenceError::Io(error.into()),
120        }
121    })
122}
123
124/// Typed refusal preserves original dependency and custody evidence.
125#[derive(Debug, Error)]
126pub enum RestoreReferenceReleaseError<E: std::error::Error + 'static> {
127    /// The product did not qualify its complete original terminal obligations.
128    #[error("restore terminal admission failed: {0}")]
129    Terminal(#[source] E),
130    /// A command belongs to another journal or repeats an original identity.
131    #[error("restore terminal command set is invalid")]
132    InvalidCommandSet,
133    /// Journal bytes changed after product qualification.
134    #[error("restore terminal journal changed during qualification")]
135    JournalChanged,
136    /// Actual command exclusion or retained sidecar identity could not be established.
137    #[error(transparent)]
138    Custody(#[from] CommandLifetimeLockError),
139    /// Layout/reference/byte admission or durable publication failed.
140    #[error(transparent)]
141    Persistence(#[from] PersistenceError),
142}
143
144#[cfg(all(test, unix))]
145mod tests;