1use std::io;
8
9use serde::{Serialize, de::DeserializeOwned};
10use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt};
11use wcode_harness::protocol::Frame;
12
13pub 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
25pub const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
30
31pub 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 let mut limited = (&mut *reader).take(MAX_FRAME_BYTES as u64 + 1);
44 let n = limited.read_line(&mut line).await?;
45 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 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 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 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 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}