Skip to main content

confium_sandbox_process/
protocol.rs

1//! Length-prefixed JSON-RPC wire types for subprocess communication.
2//!
3//! Confium spawns each plugin as a child process. The host writes
4//! [`Request`] frames to the plugin's stdin and reads [`Response`]
5//! frames from its stdout. Every frame is prefixed with a 4-byte
6//! big-endian length followed by that many bytes of UTF-8 JSON.
7//!
8//! The request envelope is intentionally minimal:
9//!
10//! ```jsonc
11//! {"method": "<function>", "args": [<value>, ...]}
12//! ```
13//!
14//! Responses carry exactly one of `result` or `error`:
15//!
16//! ```jsonc
17//! {"result": [<value>, ...]}            // success
18//! {"error": {"message": "<text>"}}      // failure
19//! ```
20//!
21//! [`Value`] variants map to/from JSON as documented on
22//! [`value_to_json`] and [`value_from_json`].
23
24use serde::Deserialize;
25use serde::Serialize;
26use serde_json::Value as JsonValue;
27use snafu::GenerateImplicitData;
28
29use crate::Error;
30use crate::Result;
31use crate::sandbox::Value;
32
33/// Length-prefix size, in bytes (u32 big-endian).
34pub(crate) const LEN_PREFIX_BYTES: usize = 4;
35
36/// Maximum frame size. Defends against a misbehaving plugin that
37/// claims a multi-GB length: the host refuses to allocate that much.
38pub(crate) const MAX_FRAME_BYTES: usize = 64 * 1024 * 1024;
39
40/// A request the host sends to the plugin subprocess.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Request {
43    pub method: String,
44    pub args: Vec<JsonValue>,
45}
46
47impl Request {
48    pub fn new(method: impl Into<String>, args: Vec<JsonValue>) -> Self {
49        Self {
50            method: method.into(),
51            args,
52        }
53    }
54
55    /// Serialize to a length-prefixed byte frame ready for stdin.
56    pub fn to_frame(&self) -> Result<Vec<u8>> {
57        let json = serde_json::to_vec(self).map_err(|e| Error::Protocol {
58            reason: format!("failed to serialize request: {e}"),
59            backtrace: snafu::Backtrace::generate(),
60        })?;
61        encode_frame(&json)
62    }
63}
64
65/// A response the plugin writes back to the host.
66#[derive(Debug, Clone, Deserialize)]
67pub struct Response {
68    /// Present on success: the function's return values.
69    #[serde(default)]
70    pub result: Option<Vec<JsonValue>>,
71    /// Present on failure: a human-readable message.
72    #[serde(default)]
73    pub error: Option<ResponseError>,
74}
75
76/// Error payload inside a [`Response`].
77#[derive(Debug, Clone, Deserialize)]
78pub struct ResponseError {
79    pub message: String,
80}
81
82impl Response {
83    /// Parse a length-prefixed frame from a raw JSON byte slice.
84    pub fn from_json_bytes(bytes: &[u8]) -> Result<Self> {
85        serde_json::from_slice::<Response>(bytes).map_err(|e| Error::Protocol {
86            reason: format!("failed to parse response: {e}"),
87            backtrace: snafu::Backtrace::generate(),
88        })
89    }
90
91    /// Split into success values or an [`Error::PluginError`].
92    pub fn into_result(self, method: &str) -> Result<Vec<Value>> {
93        if let Some(err) = self.error {
94            return Err(Error::PluginError {
95                method: method.to_string(),
96                message: err.message,
97                backtrace: snafu::Backtrace::generate(),
98            });
99        }
100        let raw = self.result.unwrap_or_default();
101        raw.into_iter()
102            .map(value_from_json)
103            .collect::<Result<Vec<_>>>()
104    }
105}
106
107/// Encode `payload` as a 4-byte big-endian length prefix + payload.
108pub(crate) fn encode_frame(payload: &[u8]) -> Result<Vec<u8>> {
109    let len = payload.len();
110    if len > MAX_FRAME_BYTES {
111        return Err(Error::Protocol {
112            reason: format!("frame too large: {len} > {MAX_FRAME_BYTES} bytes"),
113            backtrace: snafu::Backtrace::generate(),
114        });
115    }
116    let mut out = Vec::with_capacity(LEN_PREFIX_BYTES + len);
117    out.extend_from_slice(&(len as u32).to_be_bytes());
118    out.extend_from_slice(payload);
119    Ok(out)
120}
121
122/// Marshal a [`Value`] to its JSON wire form.
123///
124/// - `I32`/`I64` -> JSON integer
125/// - `F32`/`F64` -> JSON number
126/// - `Bytes`     -> JSON array of unsigned byte integers
127pub fn value_to_json(v: &Value) -> JsonValue {
128    match v {
129        Value::I32(x) => JsonValue::from(*x),
130        Value::I64(x) => JsonValue::from(*x),
131        Value::F32(x) => serde_json::Number::from_f64(f64::from(*x))
132            .map(JsonValue::Number)
133            .unwrap_or(JsonValue::Null),
134        Value::F64(x) => serde_json::Number::from_f64(*x)
135            .map(JsonValue::Number)
136            .unwrap_or(JsonValue::Null),
137        Value::Bytes(b) => JsonValue::Array(
138            b.iter()
139                .map(|&byte| JsonValue::from(u32::from(byte)))
140                .collect(),
141        ),
142    }
143}
144
145/// Inverse of [`value_to_json`].
146pub fn value_from_json(v: JsonValue) -> Result<Value> {
147    Ok(match v {
148        JsonValue::Bool(_) | JsonValue::Null => {
149            return Err(Error::Protocol {
150                reason: "null/bool are not valid sandbox values".into(),
151                backtrace: snafu::Backtrace::generate(),
152            });
153        }
154        JsonValue::String(_) => {
155            return Err(Error::Protocol {
156                reason: "string is not a valid sandbox value".into(),
157                backtrace: snafu::Backtrace::generate(),
158            });
159        }
160        JsonValue::Number(n) => {
161            if let Some(i) = n.as_i64() {
162                if let Ok(small) = i32::try_from(i) {
163                    Value::I32(small)
164                } else {
165                    Value::I64(i)
166                }
167            } else if let Some(u) = n.as_u64() {
168                // u64 that doesn't fit in i64: represent as I64 if possible.
169                if let Ok(i) = i64::try_from(u) {
170                    Value::I64(i)
171                } else {
172                    return Err(Error::Protocol {
173                        reason: format!("integer {u} overflows i64"),
174                        backtrace: snafu::Backtrace::generate(),
175                    });
176                }
177            } else if let Some(f) = n.as_f64() {
178                Value::F64(f)
179            } else {
180                return Err(Error::Protocol {
181                    reason: format!("unsupported number {n}"),
182                    backtrace: snafu::Backtrace::generate(),
183                });
184            }
185        }
186        JsonValue::Array(arr) => {
187            // Decode byte arrays back into Bytes. Every element must
188            // be a small non-negative integer.
189            let mut bytes = Vec::with_capacity(arr.len());
190            for el in arr {
191                let n = el.as_u64().ok_or_else(|| Error::Protocol {
192                    reason: "byte array element is not a non-negative integer".into(),
193                    backtrace: snafu::Backtrace::generate(),
194                })?;
195                let byte = u8::try_from(n).map_err(|_| Error::Protocol {
196                    reason: format!("byte array element {n} out of u8 range"),
197                    backtrace: snafu::Backtrace::generate(),
198                })?;
199                bytes.push(byte);
200            }
201            Value::Bytes(bytes)
202        }
203        JsonValue::Object(_) => {
204            return Err(Error::Protocol {
205                reason: "object is not a valid sandbox value".into(),
206                backtrace: snafu::Backtrace::generate(),
207            });
208        }
209    })
210}
211
212/// Read a `u32` big-endian length from a 4-byte slice.
213pub(crate) fn parse_len(buf: &[u8]) -> Result<usize> {
214    if buf.len() < LEN_PREFIX_BYTES {
215        return Err(Error::Protocol {
216            reason: format!(
217                "length header truncated: got {} bytes, need {LEN_PREFIX_BYTES}",
218                buf.len()
219            ),
220            backtrace: snafu::Backtrace::generate(),
221        });
222    }
223    let mut arr = [0u8; LEN_PREFIX_BYTES];
224    arr.copy_from_slice(&buf[..LEN_PREFIX_BYTES]);
225    let len = u32::from_be_bytes(arr) as usize;
226    if len > MAX_FRAME_BYTES {
227        return Err(Error::Protocol {
228            reason: format!("frame too large: {len} > {MAX_FRAME_BYTES} bytes"),
229            backtrace: snafu::Backtrace::generate(),
230        });
231    }
232    Ok(len)
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn request_frame_round_trips() {
241        let req = Request::new("add", vec![JsonValue::from(2), JsonValue::from(3)]);
242        let frame = req.to_frame().expect("frame encodes");
243        // 4-byte length prefix + JSON body.
244        assert!(frame.len() > LEN_PREFIX_BYTES);
245        let len = parse_len(&frame[..LEN_PREFIX_BYTES]).expect("len parses");
246        let body = &frame[LEN_PREFIX_BYTES..LEN_PREFIX_BYTES + len];
247        let parsed: Request = serde_json::from_slice(body).expect("request parses back");
248        assert_eq!(parsed.method, "add");
249        assert_eq!(parsed.args.len(), 2);
250    }
251
252    #[test]
253    fn encode_frame_writes_big_endian_length() {
254        let payload = b"hello";
255        let frame = encode_frame(payload).expect("encode");
256        assert_eq!(&frame[..4], &[0, 0, 0, 5]);
257        assert_eq!(&frame[4..], payload);
258    }
259
260    #[test]
261    fn parse_len_rejects_truncated_header() {
262        let err = parse_len(&[0, 0]).expect_err("must fail");
263        // Truncated length header surfaces as a Protocol error.
264        assert_eq!(err.code(), 0x2104);
265    }
266
267    #[test]
268    fn value_to_json_round_trip_integers() {
269        let v = Value::I32(42);
270        let j = value_to_json(&v);
271        let back = value_from_json(j).expect("round trips");
272        assert_eq!(back, Value::I32(42));
273
274        let v = Value::I64(5_000_000_000);
275        let j = value_to_json(&v);
276        let back = value_from_json(j).expect("round trips");
277        assert_eq!(back, Value::I64(5_000_000_000));
278    }
279
280    #[test]
281    fn value_to_json_round_trip_bytes() {
282        let v = Value::Bytes(vec![0, 127, 255, 1]);
283        let j = value_to_json(&v);
284        let back = value_from_json(j).expect("round trips");
285        assert_eq!(back, v);
286    }
287
288    #[test]
289    fn value_to_json_round_trip_floats() {
290        let v = Value::F64(2.5);
291        let j = value_to_json(&v);
292        let back = value_from_json(j).expect("round trips");
293        match back {
294            Value::F64(x) => assert_eq!(x, 2.5),
295            other => panic!("expected F64, got {:?}", other),
296        }
297    }
298
299    #[test]
300    fn response_success_parses() {
301        let raw = br#"{"result":[7]}"#;
302        let resp = Response::from_json_bytes(raw).expect("parses");
303        let out = resp.into_result("add").expect("ok");
304        assert_eq!(out, vec![Value::I32(7)]);
305    }
306
307    #[test]
308    fn response_error_becomes_plugin_error() {
309        let raw = br#"{"error":{"message":"no such function"}}"#;
310        let resp = Response::from_json_bytes(raw).expect("parses");
311        let err = resp.into_result("foo").expect_err("must fail");
312        match err {
313            Error::PluginError {
314                method, message, ..
315            } => {
316                assert_eq!(method, "foo");
317                assert_eq!(message, "no such function");
318            }
319            other => panic!("expected PluginError, got {:?}", other),
320        }
321    }
322
323    #[test]
324    fn response_with_empty_result_is_ok_empty() {
325        let raw = br#"{"result":[]}"#;
326        let resp = Response::from_json_bytes(raw).expect("parses");
327        let out = resp.into_result("void").expect("ok");
328        assert!(out.is_empty());
329    }
330}