Skip to main content

ic_backup/ops/persistence/download_journal/ic_snapshot_artifact/upload/
mod.rs

1//! Fresh original source-byte preparation; payloads confer no upload permission.
2
3mod plan;
4pub use plan::IcSnapshotDataUploadPreparationError;
5
6use super::{
7    DownloadJournalGuard, File, IcSnapshotArtifactError, REGIONS, member,
8    verification::open_regular_child, with_artifact_directory,
9};
10use crate::model::{
11    ic_snapshot_data::{MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES, validate_kind},
12    ic_snapshot_metadata::IcSnapshotMetadataReply,
13    ic_snapshot_upload::{IcSnapshotUploadError, IcSnapshotUploadRequest},
14    operation_plan::OperationPlanRecord,
15};
16use ic_platform::management_canister_types::SnapshotDataKind;
17use std::io::{Read, Seek, SeekFrom};
18use thiserror::Error;
19
20impl DownloadJournalGuard<'_> {
21    /// Freshly verify an original published IC source and encode exact upload metadata.
22    ///
23    /// Requires the full retained source plan, unchanged journal and complete Durable
24    /// selection. Target remains the source canister and replacement stays absent.
25    /// The payload must precede its separately retained original upload plan/reservation.
26    /// Stable future bytes, authentic source, actual context/permissions and same-release
27    /// restore obligations stay integration-owned. Nothing is written or dispatched.
28    /// # Errors
29    /// Rejects original/byte/custody drift or unrepresentable original metadata.
30    pub fn prepare_ic_snapshot_upload_metadata<'source>(
31        &self,
32        plan: &'source OperationPlanRecord,
33        snapshot: &str,
34        metadata: &'source IcSnapshotMetadataReply<'source>,
35    ) -> Result<IcSnapshotUploadRequest<'source>, IcSnapshotUploadArtifactError> {
36        let checksum = self.verify_ic_snapshot_artifact(plan, snapshot, metadata)?;
37        Ok(IcSnapshotUploadRequest::metadata(
38            plan, metadata, &checksum,
39        )?)
40    }
41
42    /// Read one bounded exact source extent/chunk and encode a distinct destination ID.
43    ///
44    /// Fresh full IC-tree verification before/after descriptor-relative reading binds
45    /// actual bytes to the original metadata/checksum and guarded source plan/journal.
46    /// This is an explicit local byte operation, not resume or a transfer-completion view.
47    /// Only one <=1 MiB chunk is buffered; no aggregate region or allowance is allocated.
48    /// The caller qualifies destination allocation and retains the new exact data intent
49    /// before its own reservation. No source/restore reference, journal or fence changes.
50    /// # Errors
51    /// Rejects wrong source declaration, ranges/hash/IDs, changed/unsafe files or custody,
52    /// short reads, encoding failures and mismatching retained original evidence.
53    pub fn prepare_ic_snapshot_upload_data<'source>(
54        &self,
55        plan: &OperationPlanRecord,
56        snapshot: &str,
57        metadata_upload: &IcSnapshotUploadRequest<'source>,
58        snapshot_id: &[u8],
59        source_kind: SnapshotDataKind,
60    ) -> Result<IcSnapshotUploadRequest<'source>, IcSnapshotUploadArtifactError> {
61        let metadata = metadata_upload.source();
62        metadata_upload.validate_data_destination(snapshot_id)?;
63        validate_kind(metadata, &source_kind).map_err(IcSnapshotUploadError::from)?;
64        self.with_upload_source(plan, snapshot, metadata_upload, |directory| {
65            let chunk = read_chunk(directory, metadata, &source_kind)?;
66            Ok(IcSnapshotUploadRequest::data(
67                metadata_upload,
68                snapshot_id,
69                source_kind,
70                &chunk,
71            )?)
72        })
73    }
74
75    fn with_upload_source<T>(
76        &self,
77        plan: &OperationPlanRecord,
78        snapshot: &str,
79        metadata_upload: &IcSnapshotUploadRequest<'_>,
80        read: impl FnOnce(&File) -> Result<T, IcSnapshotUploadArtifactError>,
81    ) -> Result<T, IcSnapshotUploadArtifactError> {
82        let metadata = metadata_upload.source();
83        self.require_upload_source(plan, snapshot, metadata_upload)?;
84        let entry = self
85            .record
86            .artifact(metadata.request().target(), snapshot)
87            .map_err(super::DownloadJournalError::from)
88            .map_err(IcSnapshotArtifactError::from)?;
89        with_artifact_directory(self.layout.root(), entry.artifact_path(), read, || {
90            self.require_upload_source(plan, snapshot, metadata_upload)
91        })
92    }
93
94    fn require_upload_source(
95        &self,
96        plan: &OperationPlanRecord,
97        snapshot: &str,
98        upload: &IcSnapshotUploadRequest<'_>,
99    ) -> Result<(), IcSnapshotUploadArtifactError> {
100        if plan.digest() != upload.source_plan().digest() {
101            return Err(IcSnapshotUploadArtifactError::SourceMismatch);
102        }
103        let actual = self.verify_ic_snapshot_artifact(plan, snapshot, upload.source())?;
104        if actual != *upload.source_checksum() {
105            return Err(IcSnapshotUploadArtifactError::SourceMismatch);
106        }
107        Ok(())
108    }
109}
110
111fn read_chunk(
112    directory: &File,
113    metadata: &IcSnapshotMetadataReply<'_>,
114    kind: &SnapshotDataKind,
115) -> Result<Vec<u8>, IcSnapshotArtifactError> {
116    let values = metadata.metadata();
117    let (name, length, offset, size) = match member::select(kind)? {
118        member::Member::Region {
119            index,
120            offset,
121            size,
122        } => {
123            let sizes = [
124                values.wasm_module_size,
125                values.wasm_memory_size,
126                values.stable_memory_size,
127            ];
128            (
129                REGIONS[index].to_owned(),
130                Some(sizes[index]),
131                offset,
132                Some(size),
133            )
134        }
135        member::Member::Chunk { name } => (name, None, 0, None),
136    };
137    let maximum = length.unwrap_or(MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES as u64);
138    let (mut file, original) = open_regular_child(directory, &name, length, maximum)?;
139    file.seek(SeekFrom::Start(offset))?;
140    let size = size.unwrap_or(original.len());
141    read_chunk_bytes(file, size)
142}
143
144// Both individual preparation and batch binding buffer one exact admitted chunk.
145// Full-tree verification and closing custody checks remain with their callers.
146fn read_chunk_bytes(reader: impl Read, size: u64) -> Result<Vec<u8>, IcSnapshotArtifactError> {
147    let limit = usize::try_from(size)
148        .ok()
149        .filter(|size| *size <= MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES)
150        .ok_or(IcSnapshotArtifactError::FileShape)?;
151    ic_host_artifacts::artifact::read_exact_reader(reader.take(size), limit).map_err(|error| {
152        use ic_host_artifacts::artifact::ArtifactError;
153        match error {
154            ArtifactError::LimitExceeded { .. } | ArtifactError::LengthMismatch { .. } => {
155                IcSnapshotArtifactError::FileShape
156            }
157            error => IcSnapshotArtifactError::Io(error.into()),
158        }
159    })
160}
161
162/// Typed local preparation failure; all original bytes, spending and references survive.
163#[derive(Debug, Error)]
164pub enum IcSnapshotUploadArtifactError {
165    /// Original declared source checksum differs from the freshly verified retained tree.
166    #[error("snapshot upload source checksum differs")]
167    SourceMismatch,
168    /// Existing guarded IC artifact admission or descriptor IO failed.
169    #[error(transparent)]
170    Artifact(#[from] IcSnapshotArtifactError),
171    /// Canonical pure bounded upload construction failed.
172    #[error(transparent)]
173    Upload(#[from] IcSnapshotUploadError),
174}