sysknife_daemon/transport/
framing.rs1use std::io;
2use tokio::io::{AsyncReadExt, AsyncWriteExt};
3
4pub const MAX_MESSAGE_BYTES: usize = 4 * 1024 * 1024;
7
8#[derive(Debug, thiserror::Error)]
9pub enum FramingError {
10 #[error("message too large: {0} bytes (max {MAX_MESSAGE_BYTES})")]
11 MessageTooLarge(usize),
12 #[error("io error: {0}")]
13 Io(#[from] io::Error),
14}
15
16pub struct FramedStream<S> {
22 inner: S,
23}
24
25impl<S> FramedStream<S> {
26 pub fn new(inner: S) -> Self {
27 Self { inner }
28 }
29
30 pub fn into_inner(self) -> S {
31 self.inner
32 }
33}
34
35impl<S: AsyncReadExt + AsyncWriteExt + Unpin> FramedStream<S> {
36 pub async fn send(&mut self, data: &[u8]) -> Result<(), FramingError> {
37 if data.len() > MAX_MESSAGE_BYTES {
38 return Err(FramingError::MessageTooLarge(data.len()));
39 }
40 self.inner
41 .write_all(&(data.len() as u32).to_le_bytes())
42 .await?;
43 self.inner.write_all(data).await?;
44 Ok(())
45 }
46
47 pub async fn recv(&mut self) -> Result<Vec<u8>, FramingError> {
48 let mut header = [0u8; 4];
49 self.inner.read_exact(&mut header).await?;
50 let len = u32::from_le_bytes(header) as usize;
51 if len > MAX_MESSAGE_BYTES {
52 return Err(FramingError::MessageTooLarge(len));
53 }
54 let mut body = Vec::with_capacity(len.min(INITIAL_BODY_CAPACITY));
61 let mut chunk = [0u8; READ_CHUNK_BYTES];
62 while body.len() < len {
63 let want = (len - body.len()).min(chunk.len());
64 let n = self.inner.read(&mut chunk[..want]).await?;
65 if n == 0 {
66 return Err(FramingError::Io(io::Error::new(
67 io::ErrorKind::UnexpectedEof,
68 "connection closed before the full frame body arrived",
69 )));
70 }
71 body.extend_from_slice(&chunk[..n]);
72 }
73 Ok(body)
74 }
75}
76
77const INITIAL_BODY_CAPACITY: usize = 64 * 1024;
81
82const READ_CHUNK_BYTES: usize = 64 * 1024;
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use std::pin::Pin;
89 use std::sync::atomic::{AtomicUsize, Ordering};
90 use std::sync::Arc;
91 use std::task::{Context, Poll};
92 use tokio::io::{duplex, AsyncWriteExt, ReadBuf};
93
94 struct RecordingReader<R> {
100 inner: R,
101 max_requested: Arc<AtomicUsize>,
102 }
103 impl<R: tokio::io::AsyncRead + Unpin> tokio::io::AsyncRead for RecordingReader<R> {
104 fn poll_read(
105 mut self: Pin<&mut Self>,
106 cx: &mut Context<'_>,
107 buf: &mut ReadBuf<'_>,
108 ) -> Poll<io::Result<()>> {
109 self.max_requested
110 .fetch_max(buf.remaining(), Ordering::SeqCst);
111 Pin::new(&mut self.inner).poll_read(cx, buf)
112 }
113 }
114 impl<W: tokio::io::AsyncWrite + Unpin> tokio::io::AsyncWrite for RecordingReader<W> {
117 fn poll_write(
118 mut self: Pin<&mut Self>,
119 cx: &mut Context<'_>,
120 buf: &[u8],
121 ) -> Poll<io::Result<usize>> {
122 Pin::new(&mut self.inner).poll_write(cx, buf)
123 }
124 fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
125 Pin::new(&mut self.inner).poll_flush(cx)
126 }
127 fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
128 Pin::new(&mut self.inner).poll_shutdown(cx)
129 }
130 }
131
132 #[tokio::test]
133 async fn recv_never_requests_more_than_one_chunk_per_read() {
134 let payload = vec![0x5Au8; 5 * READ_CHUNK_BYTES + 7];
141 let (mut a, b) = duplex(MAX_MESSAGE_BYTES + 8);
142 let max_requested = Arc::new(AtomicUsize::new(0));
143 let mut recvr = FramedStream::new(RecordingReader {
144 inner: b,
145 max_requested: Arc::clone(&max_requested),
146 });
147 let p2 = payload.clone();
148 let writer = tokio::spawn(async move {
149 a.write_all(&(p2.len() as u32).to_le_bytes()).await.unwrap();
150 a.write_all(&p2).await.unwrap();
151 });
152 let got = recvr.recv().await.expect("recv should reassemble the body");
153 writer.await.unwrap();
154 assert_eq!(got, payload);
155 let peak = max_requested.load(Ordering::SeqCst);
156 assert!(
157 peak <= READ_CHUNK_BYTES,
158 "recv asked to fill {peak} bytes in one read; must be <= {READ_CHUNK_BYTES}"
159 );
160 }
161
162 #[tokio::test]
163 async fn round_trip_a_full_max_size_body() {
164 let data = vec![0x9Cu8; MAX_MESSAGE_BYTES];
168 assert_eq!(round_trip(&data).await, data);
169 }
170
171 #[tokio::test]
172 async fn round_trip_a_body_of_exactly_one_chunk() {
173 let data = vec![0x3Fu8; READ_CHUNK_BYTES];
176 assert_eq!(round_trip(&data).await, data);
177 }
178
179 async fn round_trip(data: &[u8]) -> Vec<u8> {
181 let (a, b) = duplex(MAX_MESSAGE_BYTES + 8);
182 let mut sender = FramedStream::new(a);
183 let mut recvr = FramedStream::new(b);
184 sender.send(data).await.expect("send failed");
185 recvr.recv().await.expect("recv failed")
186 }
187
188 #[tokio::test]
189 async fn round_trip_empty_message() {
190 assert_eq!(round_trip(b"").await, b"");
191 }
192
193 #[tokio::test]
194 async fn round_trip_single_byte() {
195 assert_eq!(round_trip(b"x").await, b"x");
196 }
197
198 #[tokio::test]
199 async fn round_trip_4095_bytes() {
200 let data = vec![0xABu8; 4095];
201 assert_eq!(round_trip(&data).await, data);
202 }
203
204 #[tokio::test]
205 async fn round_trip_4096_bytes() {
206 let data = vec![0xCDu8; 4096];
207 assert_eq!(round_trip(&data).await, data);
208 }
209
210 #[tokio::test]
211 async fn round_trip_json_payload() {
212 let msg = br#"{"type":"preview","action_name":"GetSystemState"}"#;
213 assert_eq!(round_trip(msg).await, msg);
214 }
215
216 #[tokio::test]
217 async fn a_body_arriving_in_many_small_chunks_is_reassembled() {
218 let payload = vec![0xABu8; 3 * READ_CHUNK_BYTES + 123];
221 let (mut a, b) = duplex(4096);
222 let mut recvr = FramedStream::new(b);
223 let p2 = payload.clone();
224 let writer = tokio::spawn(async move {
225 a.write_all(&(p2.len() as u32).to_le_bytes()).await.unwrap();
226 a.write_all(&p2).await.unwrap();
227 });
228 let got = recvr.recv().await.expect("recv should reassemble the body");
229 writer.await.unwrap();
230 assert_eq!(got, payload);
231 }
232
233 #[tokio::test]
234 async fn a_withheld_body_after_a_large_length_claim_errors_promptly() {
235 let (mut a, b) = duplex(1024);
241 let mut recvr = FramedStream::new(b);
242 let writer = tokio::spawn(async move {
243 a.write_all(&(MAX_MESSAGE_BYTES as u32).to_le_bytes())
244 .await
245 .unwrap();
246 a.write_all(b"partial").await.unwrap();
247 drop(a); });
249 let err = recvr
250 .recv()
251 .await
252 .expect_err("a truncated body must error, not hang or succeed");
253 writer.await.unwrap();
254 match err {
255 FramingError::Io(e) => assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof),
256 other => panic!("expected UnexpectedEof, got {other}"),
257 }
258 }
259
260 #[tokio::test]
261 async fn send_rejects_message_over_4mib() {
262 let (a, _b) = duplex(8);
263 let mut sender = FramedStream::new(a);
264 let oversized = vec![0u8; MAX_MESSAGE_BYTES + 1];
265 let err = sender.send(&oversized).await.unwrap_err();
266 assert!(
267 matches!(err, FramingError::MessageTooLarge(n) if n == MAX_MESSAGE_BYTES + 1),
268 "expected MessageTooLarge, got: {err}"
269 );
270 }
271
272 #[tokio::test]
273 async fn recv_rejects_header_claiming_over_4mib() {
274 let (a, b) = duplex(16);
275 let mut raw_sender = a;
276 let mut recvr = FramedStream::new(b);
277 let oversized_len = (MAX_MESSAGE_BYTES + 1) as u32;
279 raw_sender
280 .write_all(&oversized_len.to_le_bytes())
281 .await
282 .unwrap();
283 let err = recvr.recv().await.unwrap_err();
284 assert!(
285 matches!(err, FramingError::MessageTooLarge(n) if n == MAX_MESSAGE_BYTES + 1),
286 "expected MessageTooLarge, got: {err}"
287 );
288 }
289
290 #[tokio::test]
291 async fn truncated_body_errors_instead_of_hanging() {
292 let (a, b) = duplex(MAX_MESSAGE_BYTES + 8);
297 let mut recvr = FramedStream::new(b);
298 let mut raw_sender = a;
299
300 raw_sender.write_all(&8u32.to_le_bytes()).await.unwrap();
301 raw_sender.write_all(b"abc").await.unwrap(); drop(raw_sender); let err = recvr.recv().await.unwrap_err();
305 assert!(
306 matches!(err, FramingError::Io(_)),
307 "a truncated body must surface as an I/O error, got: {err}"
308 );
309 }
310
311 #[tokio::test]
312 async fn truncated_header_errors_instead_of_hanging() {
313 let (a, b) = duplex(MAX_MESSAGE_BYTES + 8);
316 let mut recvr = FramedStream::new(b);
317 let mut raw_sender = a;
318
319 raw_sender.write_all(&[0x01, 0x02]).await.unwrap(); drop(raw_sender);
321
322 let err = recvr.recv().await.unwrap_err();
323 assert!(
324 matches!(err, FramingError::Io(_)),
325 "a truncated header must surface as an I/O error, got: {err}"
326 );
327 }
328
329 #[tokio::test]
330 async fn multiple_messages_on_same_stream() {
331 let (a, b) = duplex(MAX_MESSAGE_BYTES + 8);
332 let mut sender = FramedStream::new(a);
333 let mut recvr = FramedStream::new(b);
334 sender.send(b"first").await.unwrap();
335 sender.send(b"second").await.unwrap();
336 assert_eq!(recvr.recv().await.unwrap(), b"first");
337 assert_eq!(recvr.recv().await.unwrap(), b"second");
338 }
339}