Skip to main content

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}