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