Skip to main content

termwright_protocol/
framing.rs

1//! Wire framing: a 4-byte big-endian length prefix and a UTF-8 JSON body.
2//!
3//! The declared length is checked against the ceiling before any body is
4//! decoded, and the first violation poisons the decoder for good:
5//! resynchronising on an attacker-chosen offset is worse than dropping the
6//! connection.
7
8use serde::Serialize;
9use serde_json::Value;
10
11use crate::error::Violation;
12
13/// Size of the length prefix that precedes every frame body.
14pub const FRAME_HEADER_BYTES: usize = 4;
15
16/// Property names that carry meaning in JavaScript engines. The reference
17/// implementation rejects them, so a Rust adapter cannot smuggle a payload
18/// past a JS driver's projection either.
19const RESERVED_KEYS: [&str; 3] = ["__proto__", "constructor", "prototype"];
20
21/// Serialise a value into one length-prefixed frame.
22///
23/// Pass a [`serde_json::value::RawValue`] when the exact body bytes matter;
24/// anything else is encoded by serde.
25///
26/// # Errors
27/// Returns a [`Violation`] when the value is not JSON-encodable or the encoded
28/// body exceeds `max_frame_bytes`.
29pub fn encode_frame<T: Serialize>(value: &T, max_frame_bytes: usize) -> Result<Vec<u8>, Violation> {
30    if max_frame_bytes == 0 {
31        return Err(Violation::new(
32            "frame-malformed",
33            "maxFrameBytes must be positive",
34        ));
35    }
36    let body = serde_json::to_vec(value)
37        .map_err(|_| Violation::new("frame-malformed", "message is not JSON-serialisable"))?;
38    if body.len() > max_frame_bytes {
39        return Err(Violation::new(
40            "frame-oversized",
41            format!(
42                "encoded frame is {} bytes, ceiling is {max_frame_bytes}",
43                body.len()
44            ),
45        ));
46    }
47    let mut frame = Vec::with_capacity(FRAME_HEADER_BYTES + body.len());
48    frame.extend_from_slice(&(body.len() as u32).to_be_bytes());
49    frame.extend_from_slice(&body);
50    Ok(frame)
51}
52
53/// Decode and check one frame body.
54///
55/// # Errors
56/// Returns a [`Violation`] for non-UTF-8 bytes, invalid JSON, reserved
57/// property names, or nesting beyond `max_depth`.
58pub fn decode_body(body: &[u8], max_depth: usize) -> Result<Value, Violation> {
59    let text = std::str::from_utf8(body)
60        .map_err(|_| Violation::new("frame-encoding", "frame body is not valid UTF-8"))?;
61    let value: Value = serde_json::from_str(text)
62        .map_err(|_| Violation::new("frame-malformed", "frame body is not valid JSON"))?;
63    project_dto(&value, max_depth)?;
64    Ok(value)
65}
66
67/// Check an untrusted parsed value against the DTO rules.
68///
69/// Rust cannot express the getter, proxy or sparse-array cases the reference
70/// implementation guards against — `serde_json` cannot produce them — so this
71/// enforces what remains: reserved property names and the depth ceiling.
72/// Non-finite numbers cannot survive JSON parsing at all.
73///
74/// # Errors
75/// Returns a [`Violation`] with code `dto-key` or `dto-depth`.
76pub fn project_dto(value: &Value, max_depth: usize) -> Result<(), Violation> {
77    project_node(value, 0, max_depth, "$")
78}
79
80fn project_node(
81    value: &Value,
82    depth: usize,
83    max_depth: usize,
84    path: &str,
85) -> Result<(), Violation> {
86    match value {
87        Value::Array(items) => {
88            if depth > max_depth {
89                return Err(depth_violation(max_depth, path));
90            }
91            for (index, item) in items.iter().enumerate() {
92                project_node(item, depth + 1, max_depth, &format!("{path}[{index}]"))?;
93            }
94            Ok(())
95        }
96        Value::Object(entries) => {
97            if depth > max_depth {
98                return Err(depth_violation(max_depth, path));
99            }
100            for (key, item) in entries {
101                if RESERVED_KEYS.contains(&key.as_str()) {
102                    return Err(Violation::new(
103                        "dto-key",
104                        format!("reserved property name \"{key}\" at {path}"),
105                    ));
106                }
107                project_node(item, depth + 1, max_depth, &format!("{path}.{key}"))?;
108            }
109            Ok(())
110        }
111        _ => Ok(()),
112    }
113}
114
115fn depth_violation(max_depth: usize, path: &str) -> Violation {
116    Violation::new(
117        "dto-depth",
118        format!("nesting exceeds {max_depth} at {path}"),
119    )
120}
121
122/// One decoded wire frame: the raw body plus its parsed value.
123#[derive(Debug, Clone)]
124pub struct Frame {
125    /// The body bytes exactly as they arrived.
126    pub raw: Vec<u8>,
127    /// The parsed, checked value.
128    pub value: Value,
129}
130
131/// Streaming decoder for length-prefixed JSON frames.
132#[derive(Debug)]
133pub struct FrameDecoder {
134    max_frame_bytes: usize,
135    max_depth: usize,
136    buffer: Vec<u8>,
137    failure: Option<Violation>,
138}
139
140impl FrameDecoder {
141    /// Create a decoder bounded by `max_frame_bytes` and `max_depth`.
142    pub fn new(max_frame_bytes: usize, max_depth: usize) -> Self {
143        Self {
144            max_frame_bytes,
145            max_depth,
146            buffer: Vec::new(),
147            failure: None,
148        }
149    }
150
151    /// Bytes held back waiting for the rest of a frame.
152    pub fn buffered(&self) -> usize {
153        self.buffer.len()
154    }
155
156    /// Feed raw bytes and take the frames that completed, in order.
157    ///
158    /// # Errors
159    /// Any violation is returned once and then latched: later calls fail with
160    /// `decoder-poisoned`.
161    pub fn push(&mut self, chunk: &[u8]) -> Result<Vec<Frame>, Violation> {
162        if let Some(failure) = &self.failure {
163            return Err(Violation::new(
164                "decoder-poisoned",
165                format!(
166                    "decoder failed earlier ({}) and accepts no further input",
167                    failure.code
168                ),
169            ));
170        }
171        match self.push_inner(chunk) {
172            Ok(frames) => Ok(frames),
173            Err(violation) => {
174                self.failure = Some(violation.clone());
175                self.buffer = Vec::new();
176                Err(violation)
177            }
178        }
179    }
180
181    fn push_inner(&mut self, chunk: &[u8]) -> Result<Vec<Frame>, Violation> {
182        self.buffer.extend_from_slice(chunk);
183        let mut frames = Vec::new();
184        let mut offset = 0usize;
185
186        while self.buffer.len() - offset >= FRAME_HEADER_BYTES {
187            let header: [u8; FRAME_HEADER_BYTES] = self.buffer[offset..offset + FRAME_HEADER_BYTES]
188                .try_into()
189                .expect("slice length checked above");
190            let length = u32::from_be_bytes(header) as usize;
191            if length == 0 {
192                return Err(Violation::new(
193                    "frame-malformed",
194                    "frame length must be non-zero",
195                ));
196            }
197            if length > self.max_frame_bytes {
198                return Err(Violation::new(
199                    "frame-oversized",
200                    format!(
201                        "frame declares {length} bytes, ceiling is {}",
202                        self.max_frame_bytes
203                    ),
204                ));
205            }
206            let end = offset + FRAME_HEADER_BYTES + length;
207            if self.buffer.len() < end {
208                break;
209            }
210            let body = &self.buffer[offset + FRAME_HEADER_BYTES..end];
211            let value = decode_body(body, self.max_depth)?;
212            frames.push(Frame {
213                raw: body.to_vec(),
214                value,
215            });
216            offset = end;
217        }
218
219        if offset > 0 {
220            self.buffer.drain(..offset);
221        }
222        if self.buffer.len() > self.max_frame_bytes + FRAME_HEADER_BYTES {
223            return Err(Violation::new(
224                "frame-oversized",
225                format!(
226                    "buffered {} bytes without a complete frame",
227                    self.buffer.len()
228                ),
229            ));
230        }
231        Ok(frames)
232    }
233}