sqry_daemon_protocol/
framing.rs1use std::io;
24
25use serde::{Serialize, de::DeserializeOwned};
26use thiserror::Error;
27use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
28
29pub const MAX_FRAME_BYTES: usize = 64 * 1024 * 1024;
35
36#[derive(Debug, Error)]
38pub enum FrameError {
39 #[error("frame io: {0}")]
42 Io(#[from] io::Error),
43
44 #[error("frame json: {0}")]
47 Json(#[from] serde_json::Error),
48}
49
50pub async fn write_frame<W>(w: &mut W, body: &[u8]) -> io::Result<()>
57where
58 W: AsyncWrite + Unpin,
59{
60 if body.len() > MAX_FRAME_BYTES {
61 return Err(io::Error::new(
62 io::ErrorKind::InvalidInput,
63 format!(
64 "frame body length {} exceeds MAX_FRAME_BYTES ({MAX_FRAME_BYTES})",
65 body.len()
66 ),
67 ));
68 }
69 let len = u32::try_from(body.len()).map_err(|_| {
70 io::Error::new(
71 io::ErrorKind::InvalidInput,
72 "frame body length exceeds u32::MAX",
73 )
74 })?;
75 w.write_all(&len.to_le_bytes()).await?;
76 w.write_all(body).await?;
77 w.flush().await?;
78 Ok(())
79}
80
81pub async fn read_frame<R>(r: &mut R) -> io::Result<Option<Vec<u8>>>
94where
95 R: AsyncRead + Unpin,
96{
97 let mut len_buf = [0u8; 4];
98 let mut filled = 0usize;
99 while filled < 4 {
100 match r.read(&mut len_buf[filled..]).await? {
101 0 if filled == 0 => return Ok(None),
102 0 => {
103 return Err(io::Error::new(
104 io::ErrorKind::UnexpectedEof,
105 format!("truncated frame: got {filled}/4 length bytes before EOF"),
106 ));
107 }
108 n => filled += n,
109 }
110 }
111 let len = u32::from_le_bytes(len_buf) as usize;
112 if len > MAX_FRAME_BYTES {
113 return Err(io::Error::new(
114 io::ErrorKind::InvalidData,
115 format!("frame len {len} exceeds MAX_FRAME_BYTES ({MAX_FRAME_BYTES})"),
116 ));
117 }
118 let mut body = vec![0u8; len];
119 match r.read_exact(&mut body).await {
120 Ok(_) => Ok(Some(body)),
121 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Err(io::Error::new(
122 io::ErrorKind::UnexpectedEof,
123 format!("truncated frame body: expected {len} bytes"),
124 )),
125 Err(e) => Err(e),
126 }
127}
128
129pub async fn write_frame_json<W, T>(w: &mut W, value: &T) -> Result<(), FrameError>
136where
137 W: AsyncWrite + Unpin,
138 T: Serialize + ?Sized,
139{
140 let body = serde_json::to_vec(value)?;
141 write_frame(w, &body).await?;
142 Ok(())
143}
144
145pub async fn read_frame_json<R, T>(r: &mut R) -> Result<Option<T>, FrameError>
153where
154 R: AsyncRead + Unpin,
155 T: DeserializeOwned,
156{
157 let Some(bytes) = read_frame(r).await? else {
158 return Ok(None);
159 };
160 let value = serde_json::from_slice(&bytes)?;
161 Ok(Some(value))
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use tokio::io::duplex;
168
169 #[tokio::test]
170 async fn round_trip_small_frame() {
171 let (mut a, mut b) = duplex(1024);
172 let payload = br#"{"hello":"world"}"#;
173 write_frame(&mut a, payload).await.expect("write");
174 drop(a);
175 let got = read_frame(&mut b).await.expect("read").expect("some");
176 assert_eq!(got, payload);
177 }
178
179 #[tokio::test]
180 async fn rejects_oversize_frame_on_write() {
181 let (mut a, _b) = duplex(1024);
182 let body = vec![0u8; MAX_FRAME_BYTES + 1];
183 let err = write_frame(&mut a, &body)
184 .await
185 .expect_err("oversize must fail");
186 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
187 }
188
189 #[tokio::test]
190 async fn clean_eof_at_frame_boundary_returns_ok_none() {
191 let (a, mut b) = duplex(64);
192 drop(a); let got = read_frame(&mut b).await.expect("no error on clean EOF");
194 assert!(got.is_none());
195 }
196
197 #[tokio::test]
198 async fn truncated_prefix_is_error() {
199 let (mut a, mut b) = duplex(64);
200 a.write_all(&[0x01, 0x00]).await.unwrap(); drop(a);
202 let err = read_frame(&mut b).await.expect_err("truncated prefix");
203 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
204 assert!(
205 err.to_string().contains("got 2/4 length bytes"),
206 "got unexpected message: {err}"
207 );
208 }
209
210 #[tokio::test]
211 async fn truncated_body_is_error() {
212 let (mut a, mut b) = duplex(64);
213 let len = 16u32.to_le_bytes();
215 a.write_all(&len).await.unwrap();
216 a.write_all(b"short").await.unwrap();
217 drop(a);
218 let err = read_frame(&mut b).await.expect_err("truncated body");
219 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
220 assert!(
221 err.to_string().contains("truncated frame body"),
222 "got unexpected message: {err}"
223 );
224 }
225
226 #[tokio::test]
227 async fn oversize_read_is_rejected() {
228 let (mut a, mut b) = duplex(64);
229 let bad_len = (MAX_FRAME_BYTES as u32 + 1).to_le_bytes();
230 a.write_all(&bad_len).await.unwrap();
231 let err = read_frame(&mut b).await.expect_err("oversize claim");
232 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
233 }
234}