Skip to main content

wcode_protocol/
frame.rs

1//! Newline-delimited JSON framing for the protocol's [`Frame`]s.
2//!
3//! One frame is one JSON object on one line, exactly as the design doc lays
4//! out (§5.2). The envelope's body is `#[serde(flatten)]`ed, so a frame is a
5//! single flat object on the wire.
6
7use std::io;
8
9use serde::{Serialize, de::DeserializeOwned};
10use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt};
11use wcode_harness::protocol::Frame;
12
13/// Write one frame as a single line and flush.
14pub async fn write_frame<W, P>(writer: &mut W, frame: &Frame<P>) -> io::Result<()>
15where
16    W: AsyncWrite + Unpin,
17    P: Serialize,
18{
19    let mut line = serde_json::to_vec(frame).map_err(json_error)?;
20    line.push(b'\n');
21    writer.write_all(&line).await?;
22    writer.flush().await
23}
24
25/// Maximum size (bytes, trailing newline excluded) of a single frame's JSON
26/// body. A peer that sends a longer line is rejected with `InvalidData` rather
27/// than buffered unboundedly. 16 MiB, matching jcode — far above any legitimate
28/// frame.
29pub const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
30
31/// Read one frame. Returns `Ok(None)` on a clean EOF. Blank lines are skipped,
32/// so a frame boundary never depends on a caller flushing an empty line.
33pub async fn read_frame<R, P>(reader: &mut R) -> io::Result<Option<Frame<P>>>
34where
35    R: AsyncBufRead + Unpin,
36    P: DeserializeOwned,
37{
38    let mut line = String::new();
39    loop {
40        line.clear();
41        // Cap the read: `+ 1` lets a payload of exactly `MAX_FRAME_BYTES` still
42        // consume its trailing '\n'.
43        let mut limited = (&mut *reader).take(MAX_FRAME_BYTES as u64 + 1);
44        let n = limited.read_line(&mut line).await?;
45        // No newline within the budget => the frame exceeds the cap.
46        if n > MAX_FRAME_BYTES && !line.ends_with('\n') {
47            return Err(io::Error::new(
48                io::ErrorKind::InvalidData,
49                format!("frame exceeds {MAX_FRAME_BYTES} bytes"),
50            ));
51        }
52        if n == 0 {
53            return Ok(None);
54        }
55        let trimmed = line.trim();
56        if trimmed.is_empty() {
57            continue;
58        }
59        return serde_json::from_str(trimmed).map(Some).map_err(json_error);
60    }
61}
62
63fn json_error(e: serde_json::Error) -> io::Error {
64    io::Error::new(io::ErrorKind::InvalidData, e)
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use serde::Deserialize;
71    use tokio::io::BufReader;
72    use wcode_harness::event::AgentEvent;
73    use wcode_harness::protocol::{PROTOCOL_VERSION, Request, SessionId};
74
75    #[tokio::test]
76    async fn frame_round_trips_through_ndjson() {
77        let frame = Frame::new(7, SessionId::new("s1"), Request::Cancel);
78        let mut buf: Vec<u8> = Vec::new();
79        write_frame(&mut buf, &frame).await.unwrap();
80        assert_eq!(buf.last(), Some(&b'\n'), "one frame is one line");
81
82        let mut reader = BufReader::new(buf.as_slice());
83        let back: Frame<Request> = read_frame(&mut reader).await.unwrap().unwrap();
84        assert_eq!(back, frame);
85        // A second read at EOF is a clean `None`.
86        assert!(
87            read_frame::<_, Request>(&mut reader)
88                .await
89                .unwrap()
90                .is_none()
91        );
92    }
93
94    #[tokio::test]
95    async fn blank_lines_are_skipped() {
96        let mut src = BufReader::new(&b"\n\n  \n"[..]);
97        assert!(read_frame::<_, Request>(&mut src).await.unwrap().is_none());
98    }
99
100    #[derive(Debug, Deserialize)]
101    #[serde(tag = "type", rename_all = "snake_case")]
102    enum Only {
103        Marker,
104    }
105
106    #[derive(Debug, Deserialize)]
107    #[serde(tag = "type", rename_all = "snake_case")]
108    enum Padded {
109        Marker { pad: String },
110    }
111
112    /// A valid `Frame<Padded>` JSON body of exactly `total` bytes (newline
113    /// excluded), and the `pad` length it was built with.
114    fn padded_frame_json(total: usize) -> (String, usize) {
115        const PREFIX: &str = r#"{"v":1,"id":0,"session":"s1","type":"marker","pad":""#;
116        const SUFFIX: &str = r#""}"#;
117        let pad = total - PREFIX.len() - SUFFIX.len();
118        (format!("{PREFIX}{}{SUFFIX}", "a".repeat(pad)), pad)
119    }
120
121    #[tokio::test]
122    async fn a_frame_of_exactly_the_cap_is_accepted() {
123        // Positive control: a legal max-size frame (valid JSON, EXACTLY the cap
124        // with its newline) is ACCEPTED. Serde is not the gate here — the point
125        // is that the cap does not reject a frame at the boundary.
126        let (json, pad_len) = padded_frame_json(MAX_FRAME_BYTES);
127        assert_eq!(json.len(), MAX_FRAME_BYTES);
128        let mut bytes = json.into_bytes();
129        bytes.push(b'\n');
130        let mut src = BufReader::new(bytes.as_slice());
131        let frame: Frame<Padded> = read_frame(&mut src).await.unwrap().unwrap();
132        match frame.body {
133            Padded::Marker { pad } => assert_eq!(pad.len(), pad_len),
134        }
135    }
136
137    #[tokio::test]
138    async fn a_frame_one_byte_over_the_cap_is_rejected() {
139        let (json, _) = padded_frame_json(MAX_FRAME_BYTES + 1);
140        let mut bytes = json.into_bytes();
141        bytes.push(b'\n');
142        let mut src = BufReader::new(bytes.as_slice());
143        let err = read_frame::<_, Padded>(&mut src).await.unwrap_err();
144        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
145    }
146
147    #[tokio::test]
148    async fn an_unterminated_frame_is_refused_rather_than_buffered_forever() {
149        // A reader that never yields a newline: the ONLY way for `read_frame` to
150        // return is the cap. Without it this would buffer forever.
151        let mut src = BufReader::new(tokio::io::repeat(b'A'));
152        let err = read_frame::<_, Only>(&mut src).await.unwrap_err();
153        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
154    }
155
156    #[tokio::test]
157    async fn a_non_frame_line_is_an_invalid_data_error() {
158        let mut src = BufReader::new(&b"not json\n"[..]);
159        let err = read_frame::<_, Only>(&mut src).await.unwrap_err();
160        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
161    }
162
163    #[tokio::test]
164    async fn an_event_frame_is_flat() {
165        let frame = Frame::new(0, SessionId::new("s1"), AgentEvent::Ack);
166        let mut buf: Vec<u8> = Vec::new();
167        write_frame(&mut buf, &frame).await.unwrap();
168        let text = String::from_utf8(buf).unwrap();
169        assert_eq!(
170            text.trim(),
171            format!(r#"{{"v":{PROTOCOL_VERSION},"id":0,"session":"s1","type":"ack"}}"#)
172        );
173    }
174}