Skip to main content

ic_backup/model/application_ingress/payload/
mod.rs

1//! Exact bounded application bytes; no application codec or effect permission.
2
3use super::ApplicationIngressError;
4use crate::model::{
5    artifacts::ArtifactChecksumRecord, attempt_journal::OperationBindingRecord,
6    evidence_digest::EvidenceDigest,
7};
8use std::fmt;
9
10/// Maximum opaque argument bytes, checked before copying.
11pub const MAX_APPLICATION_INGRESS_ARGUMENT_BYTES: usize = 1024 * 1024;
12/// Maximum exact visible ASCII method bytes.
13pub const MAX_APPLICATION_INGRESS_METHOD_BYTES: usize = 128;
14/// Maximum caller-selected raw application reply ceiling.
15pub const MAX_APPLICATION_INGRESS_REPLY_BYTES: usize = 1024 * 1024;
16
17/// Immutable original receiver/routing target, update method, bytes and reply ceiling.
18///
19/// Retain these bytes before publishing their digest in the original plan or
20/// observation reservation. Receiver and effective target may differ, but both
21/// must be application principals. Management methods and query mode have separate
22/// owners; this declaration supports replicated update ingress only. No Serde,
23/// codec, fresh permission or application-specific interpretation is supplied.
24#[derive(Clone)]
25pub struct ApplicationIngressPayload {
26    receiver: String,
27    target: String,
28    receiver_bytes: Vec<u8>,
29    target_bytes: Vec<u8>,
30    method: String,
31    arguments: Vec<u8>,
32    reply_limit: usize,
33}
34
35impl ApplicationIngressPayload {
36    /// Admit exact opaque application ingress before allocating original spending.
37    /// # Errors
38    /// Rejects malformed/management principals, invalid methods and excessive bounds.
39    pub fn new(
40        receiver: &str,
41        target: &str,
42        method: &str,
43        arguments: &[u8],
44        reply_limit: usize,
45    ) -> Result<Self, ApplicationIngressError> {
46        if arguments.len() > MAX_APPLICATION_INGRESS_ARGUMENT_BYTES {
47            return Err(ApplicationIngressError::ArgumentsTooLarge);
48        }
49        if method.is_empty()
50            || method.len() > MAX_APPLICATION_INGRESS_METHOD_BYTES
51            || !method.bytes().all(|byte| byte.is_ascii_graphic())
52        {
53            return Err(ApplicationIngressError::InvalidMethod);
54        }
55        if !(1..=MAX_APPLICATION_INGRESS_REPLY_BYTES).contains(&reply_limit) {
56            return Err(ApplicationIngressError::InvalidReplyLimit);
57        }
58        let admit = |value| {
59            let (text, principal) = crate::model::principal::canonical_parts(value)
60                .ok_or(ApplicationIngressError::InvalidPrincipal)?;
61            if principal.as_slice().is_empty() {
62                return Err(ApplicationIngressError::ManagementPrincipal);
63            }
64            Ok((text, principal.as_slice().to_vec()))
65        };
66        let (receiver, receiver_bytes) = admit(receiver)?;
67        let (target, target_bytes) = admit(target)?;
68        Ok(Self {
69            receiver,
70            target,
71            receiver_bytes,
72            target_bytes,
73            method: method.into(),
74            arguments: arguments.into(),
75            reply_limit,
76        })
77    }
78    /// Read the canonical receiver encoded into the signed update.
79    #[must_use]
80    pub fn receiver(&self) -> &str {
81        &self.receiver
82    }
83    /// Read the canonical effective routing target bound to the original operation.
84    #[must_use]
85    pub fn target(&self) -> &str {
86        &self.target
87    }
88    /// Read exact original method spelling.
89    #[must_use]
90    pub fn method(&self) -> &str {
91        &self.method
92    }
93    /// Read exact opaque bytes without decoding application contracts.
94    #[must_use]
95    pub fn arguments(&self) -> &[u8] {
96        &self.arguments
97    }
98    /// Read the original caller-selected raw reply ceiling.
99    #[must_use]
100    pub const fn reply_limit(&self) -> usize {
101        self.reply_limit
102    }
103    /// Hash v1 NUL-terminated domain, u8-length/raw receiver and target principals,
104    /// fixed update byte 1, then u64 BE length/method, length/arguments and reply ceiling.
105    #[must_use]
106    pub fn digest(&self) -> ArtifactChecksumRecord {
107        let mut framing = EvidenceDigest::new(b"ic-backup/application-ingress/v1\0");
108        for principal in [&self.receiver_bytes, &self.target_bytes] {
109            framing.byte(principal.len().to_le_bytes()[0]);
110            framing.append(principal);
111        }
112        framing.byte(1);
113        for bytes in [self.method.as_bytes(), self.arguments.as_slice()] {
114            framing.u64_be(bytes.len() as u64);
115            framing.append(bytes);
116        }
117        framing.u64_be(self.reply_limit as u64);
118        framing.finish()
119    }
120    /// Check original mutation target and every exact bound update field before spending.
121    /// # Errors
122    /// Rejects changed target or payload digest without IO or reservation.
123    pub fn validate_mutation_binding(
124        &self,
125        binding: &OperationBindingRecord,
126    ) -> Result<(), ApplicationIngressError> {
127        self.validate_target(binding)?;
128        if self.digest().hash() != binding.request() {
129            return Err(ApplicationIngressError::PayloadMismatch);
130        }
131        Ok(())
132    }
133    /// Check recovery routing before reserving its separate exact payload digest.
134    ///
135    /// The original operation still binds the unresolved mutation's digest. The
136    /// observation request separately checks its own digest and current reservation.
137    /// # Errors
138    /// Rejects a changed original target without IO or reservation.
139    pub fn validate_observation_binding(
140        &self,
141        binding: &OperationBindingRecord,
142    ) -> Result<(), ApplicationIngressError> {
143        self.validate_target(binding)
144    }
145    fn validate_target(
146        &self,
147        binding: &OperationBindingRecord,
148    ) -> Result<(), ApplicationIngressError> {
149        if self.target != binding.target() {
150            return Err(ApplicationIngressError::TargetMismatch);
151        }
152        Ok(())
153    }
154}
155
156impl fmt::Debug for ApplicationIngressPayload {
157    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158        formatter
159            .debug_struct("ApplicationIngressPayload")
160            .field("receiver", &self.receiver)
161            .field("target", &self.target)
162            .field("method", &self.method)
163            .field("argument_bytes", &self.arguments.len())
164            .field("reply_limit", &self.reply_limit)
165            .finish_non_exhaustive()
166    }
167}