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