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