Skip to main content

ic_backup/ops/persistence/restore_reference_release/
mod.rs

1//! Release exact source dependencies after complete in-process restore qualification.
2
3use super::{
4    json::{encode_json, write_encoded},
5    layout_lifetime::MAX_RESTORE_REFERENCE_BYTES,
6};
7use crate::{
8    model::restore_references::RestoreReferenceRecord,
9    ops::persistence::{BackupLayoutGuard, JournalLock, PersistenceError},
10    view::restore_reference_release::TerminalRestoreEvidence,
11};
12use std::{fs, path::Path};
13use thiserror::Error;
14
15/// Maximum bytes supplied to the integration's terminal-journal validator.
16pub const MAX_RESTORE_TERMINAL_BYTES: usize = 16 * 1024 * 1024;
17
18/// Validate complete original settlement of an exclusively in-process restore.
19///
20/// Implementations authenticate their maintained journal schema, original intent,
21/// complete application/effect obligations and that no external command was dispatched.
22/// An all-Applied projection alone is insufficient. The library supplies freshly
23/// read journal bytes under its held mutation lock; no cached completion flag is used.
24pub trait RestoreTerminalEvidenceProvider {
25    /// Product-owned typed terminal qualification failure.
26    type Error: std::error::Error + 'static;
27
28    /// Qualify current terminal bytes; reject any original external-command execution.
29    ///
30    /// # Errors
31    /// Reject incomplete, changed, unauthenticated or unsettled original evidence.
32    fn qualify_terminal(
33        &self,
34        journal: &Path,
35        bytes: &[u8],
36    ) -> Result<TerminalRestoreEvidence, Self::Error>;
37}
38
39/// Observable durable reference transition; neither outcome dispatches IC effects.
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum RestoreReferenceReleaseOutcome {
42    /// The exact dependency was durably removed.
43    Released,
44    /// Current terminal evidence passes and the dependency is already absent.
45    AlreadyReleased,
46}
47
48impl BackupLayoutGuard {
49    /// Release one exact dependency after fresh in-process terminal qualification.
50    ///
51    /// Hold the original journal mutation guard. Retained command sidecars reject
52    /// even after their holders exit. Their absence does not qualify command-free
53    /// execution: the provider must establish that from its complete original record.
54    /// Recheck journal bytes and sidecars before durable publication. Lost publication
55    /// responses reconcile by this same effect-free call; no fence is released.
56    /// # Errors
57    /// Rejects retired command evidence, terminal admission, changed originals,
58    /// unsafe layouts and publication failure, preserving unfinished dependencies.
59    pub fn release_restore<P: RestoreTerminalEvidenceProvider>(
60        &self,
61        journal_guard: &JournalLock,
62        provider: &P,
63    ) -> Result<RestoreReferenceReleaseOutcome, RestoreReferenceReleaseError<P::Error>> {
64        let journal = journal_guard.journal();
65        self.check_root()?;
66        reject_command_sidecars(journal)?;
67        let bytes = read_terminal(journal)?;
68        let terminal = provider
69            .qualify_terminal(journal, &bytes)
70            .map_err(RestoreReferenceReleaseError::Terminal)?;
71        let reference =
72            RestoreReferenceRecord::new(journal.to_path_buf(), terminal.authority().hash())
73                .map_err(PersistenceError::from)?;
74        let mut references = self.restore_references()?;
75        let released = references
76            .release(&reference)
77            .map_err(PersistenceError::from)?;
78        // The validator may perform product reads; preserve the exact bytes it admitted.
79        if read_terminal(journal)? != bytes {
80            return Err(RestoreReferenceReleaseError::JournalChanged);
81        }
82        self.check_root()?;
83        reject_command_sidecars(journal)?;
84        if released {
85            let encoded = encode_json(&references, MAX_RESTORE_REFERENCE_BYTES)?;
86            write_encoded(&self.root().join("restore-references.json"), &encoded)?;
87        }
88        self.sync_references()?;
89        Ok(if released {
90            RestoreReferenceReleaseOutcome::Released
91        } else {
92            RestoreReferenceReleaseOutcome::AlreadyReleased
93        })
94    }
95}
96
97fn reject_command_sidecars<E: std::error::Error + 'static>(
98    journal: &Path,
99) -> Result<(), RestoreReferenceReleaseError<E>> {
100    let parent = journal
101        .parent()
102        .ok_or_else(|| PersistenceError::Io(std::io::ErrorKind::InvalidInput.into()))?;
103    let mut prefix = journal
104        .file_name()
105        .ok_or_else(|| PersistenceError::Io(std::io::ErrorKind::InvalidInput.into()))?
106        .to_os_string();
107    prefix.push(".command-");
108    for entry in fs::read_dir(parent).map_err(PersistenceError::from)? {
109        let name = entry.map_err(PersistenceError::from)?.file_name();
110        if name
111            .as_encoded_bytes()
112            .starts_with(prefix.as_encoded_bytes())
113        {
114            return Err(RestoreReferenceReleaseError::RetiredCommandState);
115        }
116    }
117    Ok(())
118}
119
120fn read_terminal(path: &Path) -> Result<Vec<u8>, PersistenceError> {
121    ic_host_fs::read::read_file_no_follow(path, MAX_RESTORE_TERMINAL_BYTES).map_err(|error| {
122        match error {
123            ic_host_artifacts::artifact::ArtifactError::LimitExceeded { .. } => {
124                PersistenceError::RecordTooLarge {
125                    limit: MAX_RESTORE_TERMINAL_BYTES as u64,
126                }
127            }
128            error => PersistenceError::Io(error.into()),
129        }
130    })
131}
132
133/// Typed refusal preserves original dependency and custody evidence.
134#[derive(Debug, Error)]
135pub enum RestoreReferenceReleaseError<E: std::error::Error + 'static> {
136    /// The product did not qualify its complete original terminal obligations.
137    #[error("restore terminal admission failed: {0}")]
138    Terminal(#[source] E),
139    /// Retained external-command evidence requires its original qualified integration.
140    #[error("command-backed restore requires its original qualified integration")]
141    RetiredCommandState,
142    /// Journal bytes changed after product qualification.
143    #[error("restore terminal journal changed during qualification")]
144    JournalChanged,
145    /// Layout/reference/byte admission or durable publication failed.
146    #[error(transparent)]
147    Persistence(#[from] PersistenceError),
148}
149
150#[cfg(all(test, unix))]
151mod tests;