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