Skip to main content

ic_backup/model/ic_snapshot_transfer_read/
mod.rs

1//! Exact originally reserved metadata/data reads; no dispatch or transfer attestation.
2
3use crate::model::attempt_journal::MutationReservation;
4use crate::model::ic_update_response::IcUpdateResponseInput;
5use crate::model::{
6    artifacts::ArtifactChecksumRecord,
7    attempt_journal::{AttemptAuthorityRecord, AttemptJournalRecord},
8    ic_snapshot_data::{IcSnapshotDataRequest, MAX_IC_SNAPSHOT_DATA_REPLY_BYTES},
9    ic_snapshot_metadata::IcSnapshotMetadataRequest,
10    operation_plan::{OperationPlanError, OperationPlanRecord},
11};
12use std::fmt;
13use thiserror::Error;
14
15/// The existing two bounded replicated-update read payloads; no new encoder.
16#[derive(Clone, Copy, Debug)]
17pub enum IcSnapshotTransferReadPayload<'request, 'metadata> {
18    /// Read the exact original raw snapshot ID's metadata.
19    Metadata(&'request IcSnapshotMetadataRequest),
20    /// Read one metadata-bound range or known chunk hash.
21    Data(&'request IcSnapshotDataRequest<'metadata>),
22}
23
24impl<'request> IcSnapshotTransferReadPayload<'request, '_> {
25    pub(crate) fn validate_binding(
26        &self,
27        authority: &AttemptAuthorityRecord,
28    ) -> Result<(), IcSnapshotTransferReadError> {
29        if self.target() != authority.binding().target()
30            || self.digest().hash() != authority.binding().request()
31        {
32            return Err(IcSnapshotTransferReadError::PayloadMismatch);
33        }
34        Ok(())
35    }
36    /// Read the canonical effective routing target.
37    #[must_use]
38    pub fn target(&self) -> &'request str {
39        match self {
40            Self::Metadata(payload) => payload.target(),
41            Self::Data(payload) => payload.target(),
42        }
43    }
44    /// Read the management receiver; both methods use host replicated updates.
45    #[must_use]
46    pub const fn receiver(&self) -> &'static str {
47        match self {
48            Self::Metadata(payload) => payload.receiver(),
49            Self::Data(payload) => payload.receiver(),
50        }
51    }
52    /// Read the exact original method name.
53    #[must_use]
54    pub const fn method(&self) -> &'static str {
55        match self {
56            Self::Metadata(payload) => payload.method(),
57            Self::Data(payload) => payload.method(),
58        }
59    }
60    /// Read existing canonical bounded Candid bytes.
61    #[must_use]
62    pub fn arguments(&self) -> &'request [u8] {
63        match self {
64            Self::Metadata(payload) => payload.arguments(),
65            Self::Data(payload) => payload.arguments(),
66        }
67    }
68    /// Reuse the original nonrecursive wire digest; metadata evidence stays separate.
69    #[must_use]
70    pub fn digest(&self) -> ArtifactChecksumRecord {
71        match self {
72            Self::Metadata(payload) => payload.digest(),
73            Self::Data(payload) => payload.digest(),
74        }
75    }
76}
77
78/// Full original plan and already consumed update reservation for one transfer read.
79///
80/// A semantic read still uses replicated update ingress and the existing mutation
81/// reservation lane. It grants no fresh permission or proof of never-dispatched
82/// custody; reconstruction never permits repeating a lost read.
83#[derive(Debug)]
84pub struct IcSnapshotTransferReadRequest<'request, 'metadata> {
85    plan: &'request OperationPlanRecord,
86    payload: IcSnapshotTransferReadPayload<'request, 'metadata>,
87    reservation: MutationReservation,
88}
89
90impl<'request, 'metadata> IcSnapshotTransferReadRequest<'request, 'metadata> {
91    /// Bind exact original target/wire bytes and immutable original allowance.
92    /// # Errors
93    /// Rejects another plan/operation/payload, absent or changed update reservation,
94    /// or already pending recovery. Creates no journal and spends nothing.
95    pub fn new(
96        plan: &'request OperationPlanRecord,
97        operation_sequence: u64,
98        journal: &AttemptJournalRecord,
99        payload: IcSnapshotTransferReadPayload<'request, 'metadata>,
100    ) -> Result<Self, IcSnapshotTransferReadError> {
101        let authority = plan.attempt_authority_for_journal(
102            operation_sequence,
103            journal,
104            IcSnapshotTransferReadError::Reservation(
105                crate::model::attempt_journal::MutationReservationError::AuthorityMismatch,
106            ),
107        )?;
108        payload.validate_binding(&authority)?;
109        let reservation = MutationReservation::new(authority, journal)?;
110        Ok(Self {
111            plan,
112            payload,
113            reservation,
114        })
115    }
116    /// Read full original context, inventory, graph and allowances.
117    #[must_use]
118    pub const fn plan(&self) -> &'request OperationPlanRecord {
119        self.plan
120    }
121    /// Read the exact metadata/data payload without reconstructing its bytes.
122    #[must_use]
123    pub const fn payload(&self) -> IcSnapshotTransferReadPayload<'request, 'metadata> {
124        self.payload
125    }
126    /// Read the full original operation authority.
127    #[must_use]
128    pub const fn authority(&self) -> &AttemptAuthorityRecord {
129        &self.reservation.authority
130    }
131    /// Read the already consumed replicated-update attempt.
132    #[must_use]
133    pub const fn mutation_attempt(&self) -> u32 {
134        self.reservation.mutation_attempt
135    }
136    /// Recheck original reservation before passive response association.
137    /// # Errors
138    /// Rejects changed authority, settled/replaced update or pending recovery.
139    pub fn validate_journal(
140        &self,
141        journal: &AttemptJournalRecord,
142    ) -> Result<(), IcSnapshotTransferReadError> {
143        self.reservation.validate(journal).map_err(Into::into)
144    }
145}
146
147/// Bounded immutable raw reply, with no receipt, persisted authority or default.
148#[derive(Clone)]
149pub struct IcSnapshotTransferReadResponse {
150    input: IcUpdateResponseInput,
151}
152
153impl IcSnapshotTransferReadResponse {
154    /// Admit the existing finite raw-data ceiling and original attempt range.
155    /// # Errors
156    /// Rejects invalid attempt, excessive bytes and invalid target principals.
157    pub fn new(mut input: IcUpdateResponseInput) -> Result<Self, IcSnapshotTransferReadError> {
158        input.admit(MAX_IC_SNAPSHOT_DATA_REPLY_BYTES)?;
159        Ok(Self { input })
160    }
161    /// Read exact retained claims and raw bytes; no mutable access is exposed.
162    #[must_use]
163    pub const fn input(&self) -> &IcUpdateResponseInput {
164        &self.input
165    }
166}
167
168impl fmt::Debug for IcSnapshotTransferReadResponse {
169    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170        self.input
171            .fmt_redacted(formatter, "IcSnapshotTransferReadResponse")
172    }
173}
174
175/// Structural read rejection; every denial retains original accounting and evidence.
176#[derive(Debug, Error)]
177pub enum IcSnapshotTransferReadError {
178    /// Original reservation denial, retaining the canonical typed accounting cause.
179    #[error("original mutation reservation rejected: {0}")]
180    Reservation(#[from] crate::model::attempt_journal::MutationReservationError),
181    /// Original target or exact wire digest differs.
182    #[error("snapshot transfer read payload differs")]
183    PayloadMismatch,
184    /// Attempt is outside the existing 1..=1,024 bound.
185    #[error("invalid snapshot transfer read attempt")]
186    InvalidAttempt,
187    /// Raw reply exceeds the existing 2 MiB data wire ceiling.
188    #[error("snapshot transfer read reply too large")]
189    ReplyTooLarge,
190    /// Claimed actual target is not a principal.
191    #[error("invalid snapshot transfer read target")]
192    InvalidTarget,
193    /// Original plan cannot derive this operation authority.
194    #[error(transparent)]
195    Plan(#[from] OperationPlanError),
196}
197
198impl From<crate::model::ic_update_response::UpdateResponseAdmissionError>
199    for IcSnapshotTransferReadError
200{
201    fn from(error: crate::model::ic_update_response::UpdateResponseAdmissionError) -> Self {
202        match error {
203            crate::model::ic_update_response::UpdateResponseAdmissionError::InvalidAttempt => {
204                Self::InvalidAttempt
205            }
206            crate::model::ic_update_response::UpdateResponseAdmissionError::ReplyTooLarge => {
207                Self::ReplyTooLarge
208            }
209            crate::model::ic_update_response::UpdateResponseAdmissionError::InvalidTarget => {
210                Self::InvalidTarget
211            }
212        }
213    }
214}