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