atagen

headroom

public
Star0
Watch0
c454b10854660e0f55220e43b21a60913b505716
1
ssh://brewery@pls.lobotomise.me:33/atagen/headroom.git
crates/headroom-ipc/src/codec.rsRust248 lines7.8 KB
1
//! length-prefixed json framing: 4-byte big-endian length, then that many bytes of utf-8 json.
2
3
use std::io::{Read, Write};
4
5
use serde::{de::DeserializeOwned, Serialize};
6
7
use crate::error::Error;
8
9
/// default frame payload cap.
10
pub const DEFAULT_MAX_FRAME_BYTES: usize = 1024 * 1024; // 1 MiB
11
12
/// floor on the frame cap; a smaller cap is misuse, not normal traffic.
13
pub const MIN_MAX_FRAME_BYTES: usize = 256;
14
15
/// stateless framing codec; owns no buffers, callers supply reader/writer.
16
#[derive(Debug, Clone, Copy)]
17
pub struct Codec {
18
    max_frame_bytes: usize,
19
}
20
21
impl Default for Codec {
22
    fn default() -> Self {
23
        Self {
24
            max_frame_bytes: DEFAULT_MAX_FRAME_BYTES,
25
        }
26
    }
27
}
28
29
impl Codec {
30
    #[must_use]
31
    pub fn new() -> Self {
32
        Self::default()
33
    }
34
35
    /// cap clamped up to [`MIN_MAX_FRAME_BYTES`].
36
    #[must_use]
37
    pub fn with_max_frame_size(bytes: usize) -> Self {
38
        Self {
39
            max_frame_bytes: bytes.max(MIN_MAX_FRAME_BYTES),
40
        }
41
    }
42
43
    #[must_use]
44
    pub fn max_frame_bytes(self) -> usize {
45
        self.max_frame_bytes
46
    }
47
48
    /// # Errors
49
    /// - [`Error::Json`] on serialize failure.
50
    /// - [`Error::FrameTooLarge`] if the serialized form exceeds the cap.
51
    /// - [`Error::Io`] on write failure.
52
    pub fn write<W: Write, T: Serialize>(self, mut w: W, msg: &T) -> Result<(), Error> {
53
        // serialize first to know the exact length
54
        let buf = serde_json::to_vec(msg)?;
55
        if buf.len() > self.max_frame_bytes {
56
            return Err(Error::FrameTooLarge {
57
                actual: buf.len(),
58
                limit: self.max_frame_bytes,
59
            });
60
        }
61
        let len = u32::try_from(buf.len()).expect("buf.len() <= max_frame_bytes <= u32::MAX");
62
        w.write_all(&len.to_be_bytes())?;
63
        w.write_all(&buf)?;
64
        w.flush()?;
65
        Ok(())
66
    }
67
68
    /// # Errors
69
    /// - [`Error::Closed`] on EOF before any length-prefix byte (graceful close).
70
    /// - [`Error::Io`] on partial reads or other i/o failure.
71
    /// - [`Error::FrameTooLarge`] if the announced length exceeds the cap.
72
    /// - [`Error::Json`] on deserialize failure.
73
    pub fn read<R: Read, T: DeserializeOwned>(self, mut r: R) -> Result<T, Error> {
74
        let mut len_buf = [0u8; 4];
75
        match read_full_or_eof(&mut r, &mut len_buf)? {
76
            ReadOutcome::Full => {}
77
            ReadOutcome::ZeroAtStart => return Err(Error::Closed),
78
        }
79
        let len = u32::from_be_bytes(len_buf) as usize;
80
        if len > self.max_frame_bytes {
81
            return Err(Error::FrameTooLarge {
82
                actual: len,
83
                limit: self.max_frame_bytes,
84
            });
85
        }
86
        let mut buf = vec![0u8; len];
87
        r.read_exact(&mut buf)?;
88
        Ok(serde_json::from_slice(&buf)?)
89
    }
90
}
91
92
enum ReadOutcome {
93
    Full,
94
    ZeroAtStart,
95
}
96
97
fn read_full_or_eof<R: Read>(r: &mut R, buf: &mut [u8]) -> Result<ReadOutcome, std::io::Error> {
98
    let mut read = 0;
99
    while read < buf.len() {
100
        match r.read(&mut buf[read..]) {
101
            Ok(0) => {
102
                if read == 0 {
103
                    return Ok(ReadOutcome::ZeroAtStart);
104
                }
105
                return Err(std::io::Error::new(
106
                    std::io::ErrorKind::UnexpectedEof,
107
                    "eof mid-length-prefix",
108
                ));
109
            }
110
            Ok(n) => read += n,
111
            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
112
            Err(e) => return Err(e),
113
        }
114
    }
115
    Ok(ReadOutcome::Full)
116
}
117
118
#[cfg(test)]
119
mod tests {
120
    use super::*;
121
    use crate::{Event, Op, Request, Response, ServerFrame, Topic};
122
    use std::io::Cursor;
123
124
    #[test]
125
    fn write_read_request_roundtrip() {
126
        let codec = Codec::new();
127
        let req = Request::new(
128
            42,
129
            Op::ProfileUse {
130
                name: "night".into(),
131
            },
132
        );
133
134
        let mut buf = Vec::new();
135
        codec.write(&mut buf, &req).unwrap();
136
137
        let mut cur = Cursor::new(&buf);
138
        let back: Request = codec.read(&mut cur).unwrap();
139
        assert_eq!(back, req);
140
    }
141
142
    #[test]
143
    fn server_frame_round_trip_response() {
144
        let codec = Codec::new();
145
        let resp = Response::ok(1, &serde_json::json!({"name": "default"})).unwrap();
146
        let frame = ServerFrame::Response(resp.clone());
147
148
        let mut buf = Vec::new();
149
        codec.write(&mut buf, &frame).unwrap();
150
        let mut cur = Cursor::new(&buf);
151
        let back: ServerFrame = codec.read(&mut cur).unwrap();
152
        match back {
153
            ServerFrame::Response(r) => assert_eq!(r, resp),
154
            ServerFrame::Event(_) => panic!("decoded as event"),
155
        }
156
    }
157
158
    #[test]
159
    fn server_frame_round_trip_event() {
160
        let codec = Codec::new();
161
        let ev = Event::new(Topic::Daemon, "shutdown", &serde_json::json!({})).unwrap();
162
        let frame = ServerFrame::Event(ev.clone());
163
164
        let mut buf = Vec::new();
165
        codec.write(&mut buf, &frame).unwrap();
166
        let mut cur = Cursor::new(&buf);
167
        let back: ServerFrame = codec.read(&mut cur).unwrap();
168
        match back {
169
            ServerFrame::Event(e) => assert_eq!(e, ev),
170
            ServerFrame::Response(_) => panic!("decoded as response"),
171
        }
172
    }
173
174
    #[test]
175
    fn rejects_oversized_frames_on_write() {
176
        let codec = Codec::with_max_frame_size(MIN_MAX_FRAME_BYTES);
177
        // A big string that will serialize > 256 bytes.
178
        let req = Request::new(
179
            1,
180
            Op::SettingSet {
181
                key: "x".into(),
182
                value: serde_json::Value::String("a".repeat(1024)),
183
            },
184
        );
185
        let mut buf = Vec::new();
186
        let err = codec.write(&mut buf, &req).unwrap_err();
187
        assert!(matches!(err, Error::FrameTooLarge { .. }));
188
    }
189
190
    #[test]
191
    fn rejects_oversized_frames_on_read() {
192
        let codec = Codec::with_max_frame_size(MIN_MAX_FRAME_BYTES);
193
        // Hand-craft a length prefix that exceeds the cap.
194
        let mut buf = Vec::new();
195
        let bad_len: u32 = MIN_MAX_FRAME_BYTES as u32 + 1;
196
        buf.extend_from_slice(&bad_len.to_be_bytes());
197
        // No need to follow with payload; we expect early rejection.
198
        let mut cur = Cursor::new(&buf);
199
        let err = codec.read::<_, serde_json::Value>(&mut cur).unwrap_err();
200
        assert!(matches!(err, Error::FrameTooLarge { .. }));
201
    }
202
203
    #[test]
204
    fn graceful_eof_at_frame_boundary() {
205
        let codec = Codec::new();
206
        let mut cur = Cursor::new(Vec::<u8>::new());
207
        let err = codec.read::<_, Request>(&mut cur).unwrap_err();
208
        assert!(matches!(err, Error::Closed));
209
    }
210
211
    #[test]
212
    fn mid_frame_eof_is_io_error() {
213
        let codec = Codec::new();
214
        // Half a length prefix.
215
        let mut cur = Cursor::new(vec![0u8, 0u8]);
216
        let err = codec.read::<_, Request>(&mut cur).unwrap_err();
217
        assert!(matches!(err, Error::Io(_)));
218
    }
219
220
    #[test]
221
    fn rejects_invalid_json_payload() {
222
        let codec = Codec::new();
223
        let payload = b"not-json";
224
        let mut buf = Vec::new();
225
        buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
226
        buf.extend_from_slice(payload);
227
        let mut cur = Cursor::new(&buf);
228
        let err = codec.read::<_, Request>(&mut cur).unwrap_err();
229
        assert!(matches!(err, Error::Json(_)));
230
    }
231
232
    #[test]
233
    fn back_to_back_frames() {
234
        let codec = Codec::new();
235
        let a = Request::new(1, Op::Status);
236
        let b = Request::new(2, Op::ProfileList);
237
238
        let mut buf = Vec::new();
239
        codec.write(&mut buf, &a).unwrap();
240
        codec.write(&mut buf, &b).unwrap();
241
242
        let mut cur = Cursor::new(&buf);
243
        let ra: Request = codec.read(&mut cur).unwrap();
244
        let rb: Request = codec.read(&mut cur).unwrap();
245
        assert_eq!(ra, a);
246
        assert_eq!(rb, b);
247
    }
248
}