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 (receiver, receiver_bytes) = principal(receiver)?;
59        let (target, target_bytes) = principal(target)?;
60        Ok(Self {
61            receiver,
62            target,
63            receiver_bytes,
64            target_bytes,
65            method: method.into(),
66            arguments: arguments.into(),
67            reply_limit,
68        })
69    }
70    /// Read the canonical receiver encoded into the signed update.
71    #[must_use]
72    pub fn receiver(&self) -> &str {
73        &self.receiver
74    }
75    /// Read the canonical effective routing target bound to the original operation.
76    #[must_use]
77    pub fn target(&self) -> &str {
78        &self.target
79    }
80    /// Read exact original method spelling.
81    #[must_use]
82    pub fn method(&self) -> &str {
83        &self.method
84    }
85    /// Read exact opaque bytes without decoding application contracts.
86    #[must_use]
87    pub fn arguments(&self) -> &[u8] {
88        &self.arguments
89    }
90    /// Read the original caller-selected raw reply ceiling.
91    #[must_use]
92    pub const fn reply_limit(&self) -> usize {
93        self.reply_limit
94    }
95    /// Hash v1 NUL-terminated domain, u8-length/raw receiver and target principals,
96    /// fixed update byte 1, then u64 BE length/method, length/arguments and reply ceiling.
97    #[must_use]
98    pub fn digest(&self) -> ArtifactChecksumRecord {
99        let mut framing = EvidenceDigest::new(b"ic-backup/application-ingress/v1\0");
100        for principal in [&self.receiver_bytes, &self.target_bytes] {
101            framing.bytes_u8(principal);
102        }
103        framing.byte(1);
104        for bytes in [self.method.as_bytes(), self.arguments.as_slice()] {
105            framing.bytes_u64(bytes);
106        }
107        framing.u64_be(self.reply_limit as u64);
108        framing.finish()
109    }
110    /// Check original mutation target and every exact bound update field before spending.
111    /// # Errors
112    /// Rejects changed target or payload digest without IO or reservation.
113    pub fn validate_mutation_binding(
114        &self,
115        binding: &OperationBindingRecord,
116    ) -> Result<(), ApplicationIngressError> {
117        self.validate_target(binding)?;
118        if self.digest().hash() != binding.request() {
119            return Err(ApplicationIngressError::PayloadMismatch);
120        }
121        Ok(())
122    }
123    /// Check recovery routing before reserving its separate exact payload digest.
124    ///
125    /// The original operation still binds the unresolved mutation's digest. The
126    /// observation request separately checks its own digest and current reservation.
127    /// # Errors
128    /// Rejects a changed original target without IO or reservation.
129    pub fn validate_observation_binding(
130        &self,
131        binding: &OperationBindingRecord,
132    ) -> Result<(), ApplicationIngressError> {
133        self.validate_target(binding)
134    }
135    fn validate_target(
136        &self,
137        binding: &OperationBindingRecord,
138    ) -> Result<(), ApplicationIngressError> {
139        if self.target != binding.target() {
140            return Err(ApplicationIngressError::TargetMismatch);
141        }
142        Ok(())
143    }
144}
145
146impl fmt::Debug for ApplicationIngressPayload {
147    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148        formatter
149            .debug_struct("ApplicationIngressPayload")
150            .field("receiver", &self.receiver)
151            .field("target", &self.target)
152            .field("method", &self.method)
153            .field("argument_bytes", &self.arguments.len())
154            .field("reply_limit", &self.reply_limit)
155            .finish_non_exhaustive()
156    }
157}
158
159fn principal(value: &str) -> Result<(String, Vec<u8>), ApplicationIngressError> {
160    let (text, principal) = crate::model::principal::canonical_parts(value)
161        .ok_or(ApplicationIngressError::InvalidPrincipal)?;
162    if principal.as_slice().is_empty() {
163        return Err(ApplicationIngressError::ManagementPrincipal);
164    }
165    Ok((text, principal.as_slice().to_vec()))
166}