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