ledgence_worker_api/wire.rs
1use crate::{Error, ErrorKind, Result};
2use serde_json::Value;
3
4/// Maximum nested containers in a program result (a scalar has depth zero).
5pub const MAX_WIRE_VALUE_DEPTH: usize = 64;
6
7/// Maximum compact JSON data bytes in the submission/delivery profile (1 MiB).
8///
9/// This is an application-data budget, including JSON quotes and escapes. It
10/// does not impose a size validator on arbitrary local [`crate::CloudEvent`]s.
11pub const APPLICATION_INPUT_MAX_BYTES: usize = 1024 * 1024;
12
13/// Default complete runtime frame budget, including its newline (2 MiB).
14///
15/// The delivery profile reserves space beyond [`APPLICATION_INPUT_MAX_BYTES`]
16/// for generated CloudEvent metadata and the invocation protocol wrapper. The
17/// same bounded budget applies to result frames; their application output is
18/// not subject to the submission data limit. Runtime adapters may allow explicit
19/// local overrides, which need not be compatible with the delivery profile.
20pub const DEFAULT_RUNTIME_FRAME_MAX_BYTES: usize = 2 * 1024 * 1024;
21
22/// Checks the structural limit shared by the Rust and Python result protocol.
23///
24/// `Value` already guarantees string object keys, Unicode scalar strings and
25/// finite signed/unsigned 64-bit or binary64 numbers. The Python helper checks
26/// those properties before encoding so unsupported results cannot be coerced.
27///
28/// ```
29/// use ledgence_worker_api::validate_wire_value;
30/// let output = serde_json::json!({"invoice_id": "INV-1042", "issued": true});
31/// validate_wire_value(&output).unwrap();
32/// ```
33pub fn validate_wire_value(value: &Value) -> Result<()> {
34 fn visit(value: &Value, depth: usize) -> Result<()> {
35 if matches!(value, Value::Array(_) | Value::Object(_)) && depth >= MAX_WIRE_VALUE_DEPTH {
36 return Err(Error::new(
37 ErrorKind::Protocol,
38 "program output exceeds 64 nested containers",
39 ));
40 }
41 match value {
42 Value::Array(values) => {
43 for value in values {
44 visit(value, depth + 1)?;
45 }
46 }
47 Value::Object(values) => {
48 for value in values.values() {
49 visit(value, depth + 1)?;
50 }
51 }
52 _ => {}
53 }
54 Ok(())
55 }
56 visit(value, 0)
57}
58
59/// Envelope nesting allowance for opt-in interactive runtime payloads. Domain
60/// handlers must still validate each application value at the ordinary depth 64.
61pub const MAX_RUNTIME_VALUE_DEPTH: usize = 96;
62/// Complete extension payload budget (640 KiB).
63pub const RUNTIME_EXTENSION_MAX_BYTES: usize = 640 * 1024;
64/// Interactive request/reply budget, including room around two 64 KiB values.
65pub const RUNTIME_REQUEST_MAX_BYTES: usize = 144 * 1024;
66
67/// Validate interactive envelope nesting and compact size without allocating
68/// the encoded payload. Ordinary application values use `validate_wire_value`.
69pub fn validate_runtime_payload(value: &Value, max_bytes: usize) -> Result<()> {
70 fn visit(value: &Value, depth: usize, remaining: &mut usize) -> Result<()> {
71 if *remaining == 0 {
72 return Err(Error::new(
73 ErrorKind::Protocol,
74 "runtime payload exceeds its byte budget",
75 ));
76 }
77 *remaining -= 1;
78 if matches!(value, Value::Array(_) | Value::Object(_)) && depth >= MAX_RUNTIME_VALUE_DEPTH {
79 return Err(Error::new(
80 ErrorKind::Protocol,
81 "runtime payload exceeds 96 nested containers",
82 ));
83 }
84 match value {
85 Value::Array(values) => {
86 for value in values {
87 visit(value, depth + 1, remaining)?;
88 }
89 }
90 Value::Object(values) => {
91 for value in values.values() {
92 visit(value, depth + 1, remaining)?;
93 }
94 }
95 _ => {}
96 }
97 Ok(())
98 }
99 struct Budget(usize);
100 impl std::io::Write for Budget {
101 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
102 if bytes.len() > self.0 {
103 return Err(std::io::Error::other(
104 "runtime payload exceeds its byte budget",
105 ));
106 }
107 self.0 -= bytes.len();
108 Ok(bytes.len())
109 }
110 fn flush(&mut self) -> std::io::Result<()> {
111 Ok(())
112 }
113 }
114 let mut remaining_nodes = max_bytes;
115 visit(value, 0, &mut remaining_nodes)?;
116 serde_json::to_writer(Budget(max_bytes), value).map_err(|error| {
117 Error::new(
118 ErrorKind::Protocol,
119 format!("runtime payload exceeds its byte budget: {error}"),
120 )
121 })
122}