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