Skip to main content

ic_backup/ops/persistence/download_journal/local_restore_artifact/
mod.rs

1//! Private original-operation artifact copies and explicit retained-copy verification.
2
3use super::{DownloadJournalGuard, LocalRestoreSourceError};
4use crate::{
5    model::{
6        artifacts::ChecksumError,
7        download_journal::DownloadArtifactRecord,
8        operation_plan::{OperationPlanError, OperationPlanRecord, PlannedOperationRecord},
9        restore_safety::RestoreSafetyRequirementRecord,
10    },
11    ops::{
12        artifacts::{ArtifactError, checksum_directory, stage_relative_path},
13        persistence::{BackupLayoutGuard, JournalLock, JournalLockError},
14    },
15    policy::local_restore_source::LocalRestoreSourceView,
16};
17use std::path::{Path, PathBuf};
18use thiserror::Error;
19
20/// Borrowed original source/operation identity and the exact freshly checked private copy.
21///
22/// This view retains both layout/source-journal lifetimes. It grants no future path
23/// stability, durable publication, complete backend transfer, authenticated snapshot,
24/// command dispatch or upload/load permission. Dropping it deletes nothing.
25#[derive(Debug)]
26pub struct LocalRestoreArtifactView<'a> {
27    source: LocalRestoreSourceView<'a>,
28    operation: &'a PlannedOperationRecord,
29    artifact: &'a DownloadArtifactRecord,
30    path: PathBuf,
31}
32
33impl<'a> LocalRestoreArtifactView<'a> {
34    /// Read exact original source, restore and safety declarations.
35    #[must_use]
36    pub const fn source(&self) -> &LocalRestoreSourceView<'a> {
37        &self.source
38    }
39    /// Read original opaque operation sequence, target/request and attempt allowances.
40    #[must_use]
41    pub const fn operation(&self) -> &'a PlannedOperationRecord {
42        self.operation
43    }
44    /// Read original exact snapshot metadata, canonical source path and retained checksum.
45    #[must_use]
46    pub const fn artifact(&self) -> &'a DownloadArtifactRecord {
47        self.artifact
48    }
49    /// Read the private copy location; integrations maintain byte custody before actual use.
50    #[must_use]
51    pub fn path(&self) -> &Path {
52        &self.path
53    }
54}
55
56impl DownloadJournalGuard<'_> {
57    /// Create an exact private artifact copy for one original selected restore operation.
58    ///
59    /// Complete original source verification precedes descriptor-based no-follow
60    /// copying to fixed `restore-artifact-{sequence}.tmp` directly under the held
61    /// restore layout. Existing destinations are never adopted, replaced or deleted.
62    /// Copy hash and fresh destination hash must equal the original artifact checksum;
63    /// retained declarations are re-admitted before returning. Directories/files are
64    /// private 0700/0600. This is staging, without fsync/durable publication or dispatch.
65    /// Failures/drop retain partial bytes and all original spending/references. After
66    /// a lost reply, explicitly verify the retained copy; an invalid partial copy needs
67    /// operator-owned disposition. Stable noncooperating destination custody remains
68    /// integration-owned. The operation sequence associates bytes, not effect authority.
69    /// # Errors
70    /// Rejects unknown original operations, changed/unsafe source or copy, contention,
71    /// existing destinations, lost IO replies and original record/custody mismatch.
72    pub fn stage_local_restore_artifact<'a>(
73        &'a self,
74        restore_layout: &'a BackupLayoutGuard,
75        restore: &'a OperationPlanRecord,
76        source: &'a OperationPlanRecord,
77        requirement: &'a RestoreSafetyRequirementRecord,
78        operation_sequence: u64,
79    ) -> Result<LocalRestoreArtifactView<'a>, LocalRestoreArtifactError> {
80        self.stage_restore_artifact_with(
81            restore_layout,
82            restore,
83            source,
84            requirement,
85            operation_sequence,
86            stage_relative_path,
87        )
88    }
89
90    fn stage_restore_artifact_with<'a>(
91        &'a self,
92        restore_layout: &'a BackupLayoutGuard,
93        restore: &'a OperationPlanRecord,
94        source: &'a OperationPlanRecord,
95        requirement: &'a RestoreSafetyRequirementRecord,
96        operation_sequence: u64,
97        copy: impl FnOnce(
98            &Path,
99            &Path,
100            &Path,
101        )
102            -> Result<crate::model::artifacts::ArtifactChecksumRecord, ArtifactError>,
103    ) -> Result<LocalRestoreArtifactView<'a>, LocalRestoreArtifactError> {
104        let operation = restore.operation(operation_sequence)?;
105        let view =
106            self.verify_local_restore_source(restore_layout, restore, source, requirement)?;
107        let artifact = selected_artifact(&view, operation)?;
108        let path = staged_path(restore_layout, operation_sequence);
109        let _lock = JournalLock::acquire(&path)?;
110        copy(
111            self.layout.root(),
112            Path::new(artifact.artifact_path()),
113            &path,
114        )?
115        .verify(
116            artifact
117                .checksum()
118                .ok_or(LocalRestoreArtifactError::ArtifactUnavailable)?
119                .hash(),
120        )?;
121        self.verify_restore_artifact_at(
122            restore_layout,
123            restore,
124            source,
125            requirement,
126            operation,
127            path,
128        )
129    }
130
131    /// Explicitly check a retained private copy against exact original declarations.
132    ///
133    /// Reads retained original plans/requirement/manifest/journal and the copy's bytes,
134    /// without re-reading source trees or repeating a copy. Original source trees may
135    /// be absent; exact retained metadata remains required. Missing/unsafe/incomplete
136    /// or conflicting copies are retained, never repaired or recreated. This is fresh
137    /// local copy verification, not ordinary resume/terminal replay or effect authority.
138    /// # Errors
139    /// Rejects original identity/custody mismatch, unknown operations, unsafe/missing
140    /// copies, checksum drift and contention without altering recovery evidence.
141    pub fn verify_staged_local_restore_artifact<'a>(
142        &'a self,
143        restore_layout: &'a BackupLayoutGuard,
144        restore: &'a OperationPlanRecord,
145        source: &'a OperationPlanRecord,
146        requirement: &'a RestoreSafetyRequirementRecord,
147        operation_sequence: u64,
148    ) -> Result<LocalRestoreArtifactView<'a>, LocalRestoreArtifactError> {
149        let operation = restore.operation(operation_sequence)?;
150        let path = staged_path(restore_layout, operation_sequence);
151        let _lock = JournalLock::acquire(&path)?;
152        self.verify_restore_artifact_at(
153            restore_layout,
154            restore,
155            source,
156            requirement,
157            operation,
158            path,
159        )
160    }
161
162    fn verify_restore_artifact_at<'a>(
163        &'a self,
164        restore_layout: &'a BackupLayoutGuard,
165        restore: &'a OperationPlanRecord,
166        source: &'a OperationPlanRecord,
167        requirement: &'a RestoreSafetyRequirementRecord,
168        operation: &'a PlannedOperationRecord,
169        path: PathBuf,
170    ) -> Result<LocalRestoreArtifactView<'a>, LocalRestoreArtifactError> {
171        let view = self.admit_local_restore_source(restore_layout, restore, source, requirement)?;
172        let artifact = selected_artifact(&view, operation)?;
173        checksum_directory(&path)?.verify(
174            artifact
175                .checksum()
176                .ok_or(LocalRestoreArtifactError::ArtifactUnavailable)?
177                .hash(),
178        )?;
179        let source =
180            self.admit_local_restore_source(restore_layout, restore, source, requirement)?;
181        Ok(LocalRestoreArtifactView {
182            source,
183            operation,
184            artifact,
185            path,
186        })
187    }
188}
189
190fn staged_path(layout: &BackupLayoutGuard, sequence: u64) -> PathBuf {
191    layout
192        .root()
193        .join(format!("restore-artifact-{sequence}.tmp"))
194}
195fn selected_artifact<'a>(
196    view: &LocalRestoreSourceView<'a>,
197    operation: &PlannedOperationRecord,
198) -> Result<&'a DownloadArtifactRecord, LocalRestoreArtifactError> {
199    view.selected_artifacts()
200        .find(|artifact| artifact.artifact().canister_id() == operation.target())
201        .map(crate::policy::download_integrity::DurableDownloadArtifactView::artifact)
202        .ok_or(LocalRestoreArtifactError::ArtifactUnavailable)
203}
204
205/// Typed local copy/source admission denial; no error deletes or dispatches anything.
206#[derive(Debug, Error)]
207pub enum LocalRestoreArtifactError {
208    /// Original local source, safety requirement or retained custody failed admission.
209    #[error(transparent)]
210    Source(#[from] LocalRestoreSourceError),
211    /// Supplied opaque operation is absent from the exact original restore plan.
212    #[error(transparent)]
213    Operation(#[from] OperationPlanError),
214    /// Original selected artifact/checksum could not be projected.
215    #[error("original selected restore artifact unavailable")]
216    ArtifactUnavailable,
217    /// Descriptor copying or no-follow local traversal failed; partial bytes remain.
218    #[error(transparent)]
219    Artifact(#[from] ArtifactError),
220    /// Actual copied or retained bytes differ from the original checksum.
221    #[error(transparent)]
222    Checksum(#[from] ChecksumError),
223    /// Original-operation staging exclusion failed.
224    #[error(transparent)]
225    Lock(#[from] JournalLockError),
226}
227
228#[cfg(all(test, unix))]
229mod tests;