oxur_repl/protocol/
codec.rs

1// Postcard codec with length-prefixed framing
2//
3// Implements binary serialization for Request/Response messages using Postcard.
4// Each message is prefixed with a u32 length in little-endian byte order.
5//
6// Wire format:
7//   [4 bytes: message length (u32 LE)][N bytes: postcard payload]
8
9use super::{Request, Response};
10use std::io::{self, Read, Write};
11use thiserror::Error;
12
13/// Maximum message size (10 MB)
14///
15/// Prevents unbounded memory allocation from malicious or corrupted data.
16/// Based on design assumption that code snippets are reasonably sized.
17const MAX_MESSAGE_SIZE: u32 = 10 * 1024 * 1024;
18
19/// Codec errors
20#[derive(Debug, Error)]
21pub enum CodecError {
22    /// I/O error during read/write
23    #[error("I/O error: {0}")]
24    Io(#[from] io::Error),
25
26    /// Postcard serialization error
27    #[error("Serialization error: {0}")]
28    Serialization(#[from] postcard::Error),
29
30    /// Message exceeds maximum size
31    #[error("Message size {0} exceeds maximum {MAX_MESSAGE_SIZE}")]
32    MessageTooLarge(u32),
33
34    /// Connection closed unexpectedly
35    #[error("Connection closed unexpectedly")]
36    UnexpectedEof,
37}
38
39pub type Result<T> = std::result::Result<T, CodecError>;
40
41/// Postcard codec for Request/Response messages
42///
43/// Handles length-prefixed binary serialization using Postcard format.
44pub struct PostcardCodec;
45
46impl PostcardCodec {
47    /// Encode a Request to bytes
48    ///
49    /// Returns serialized message with length prefix.
50    pub fn encode_request(request: &Request) -> Result<Vec<u8>> {
51        let payload = postcard::to_allocvec(request)?;
52        Self::frame_message(&payload)
53    }
54
55    /// Encode a Response to bytes
56    ///
57    /// Returns serialized message with length prefix.
58    pub fn encode_response(response: &Response) -> Result<Vec<u8>> {
59        let payload = postcard::to_allocvec(response)?;
60        Self::frame_message(&payload)
61    }
62
63    /// Decode a Request from bytes
64    ///
65    /// # Errors
66    ///
67    /// Returns error if message is malformed or exceeds size limit.
68    pub fn decode_request(data: &[u8]) -> Result<Request> {
69        let payload = Self::unframe_message(data)?;
70        Ok(postcard::from_bytes(payload)?)
71    }
72
73    /// Decode a Response from bytes
74    ///
75    /// # Errors
76    ///
77    /// Returns error if message is malformed or exceeds size limit.
78    pub fn decode_response(data: &[u8]) -> Result<Response> {
79        let payload = Self::unframe_message(data)?;
80        Ok(postcard::from_bytes(payload)?)
81    }
82
83    /// Write a framed Request to a writer
84    ///
85    /// # Errors
86    ///
87    /// Returns error if serialization or write fails.
88    pub fn write_request<W: Write>(writer: &mut W, request: &Request) -> Result<()> {
89        let framed = Self::encode_request(request)?;
90        writer.write_all(&framed)?;
91        writer.flush()?;
92        Ok(())
93    }
94
95    /// Write a framed Response to a writer
96    ///
97    /// # Errors
98    ///
99    /// Returns error if serialization or write fails.
100    pub fn write_response<W: Write>(writer: &mut W, response: &Response) -> Result<()> {
101        let framed = Self::encode_response(response)?;
102        writer.write_all(&framed)?;
103        writer.flush()?;
104        Ok(())
105    }
106
107    /// Read a framed Request from a reader
108    ///
109    /// # Errors
110    ///
111    /// Returns error if read fails, message is malformed, or exceeds size limit.
112    pub fn read_request<R: Read>(reader: &mut R) -> Result<Request> {
113        let payload = Self::read_framed_message(reader)?;
114        Ok(postcard::from_bytes(&payload)?)
115    }
116
117    /// Read a framed Response from a reader
118    ///
119    /// # Errors
120    ///
121    /// Returns error if read fails, message is malformed, or exceeds size limit.
122    pub fn read_response<R: Read>(reader: &mut R) -> Result<Response> {
123        let payload = Self::read_framed_message(reader)?;
124        Ok(postcard::from_bytes(&payload)?)
125    }
126
127    // Internal framing helpers
128
129    fn frame_message(payload: &[u8]) -> Result<Vec<u8>> {
130        let len = payload.len();
131        if len > MAX_MESSAGE_SIZE as usize {
132            return Err(CodecError::MessageTooLarge(len as u32));
133        }
134
135        let mut framed = Vec::with_capacity(4 + len);
136        framed.extend_from_slice(&(len as u32).to_le_bytes());
137        framed.extend_from_slice(payload);
138        Ok(framed)
139    }
140
141    fn unframe_message(data: &[u8]) -> Result<&[u8]> {
142        if data.len() < 4 {
143            return Err(CodecError::UnexpectedEof);
144        }
145
146        let len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
147        if len > MAX_MESSAGE_SIZE {
148            return Err(CodecError::MessageTooLarge(len));
149        }
150
151        let payload_start = 4;
152        let payload_end = payload_start + len as usize;
153
154        if data.len() < payload_end {
155            return Err(CodecError::UnexpectedEof);
156        }
157
158        Ok(&data[payload_start..payload_end])
159    }
160
161    fn read_framed_message<R: Read>(reader: &mut R) -> Result<Vec<u8>> {
162        // Read 4-byte length prefix
163        let mut len_bytes = [0u8; 4];
164        reader.read_exact(&mut len_bytes)?;
165        let len = u32::from_le_bytes(len_bytes);
166
167        if len > MAX_MESSAGE_SIZE {
168            return Err(CodecError::MessageTooLarge(len));
169        }
170
171        // Read payload
172        let mut payload = vec![0u8; len as usize];
173        reader.read_exact(&mut payload)?;
174        Ok(payload)
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::protocol::{
182        MessageId, Operation, OperationResult, ReplMode, SessionId, SessionInfo, Status,
183    };
184    use std::io::Cursor;
185
186    #[test]
187    fn test_encode_decode_request() {
188        let request = Request {
189            id: MessageId::new(1),
190            session_id: SessionId::new("test-session"),
191            operation: Operation::Eval { code: "(+ 1 2)".to_string(), mode: ReplMode::Lisp },
192        };
193
194        let encoded = PostcardCodec::encode_request(&request).unwrap();
195        let decoded = PostcardCodec::decode_request(&encoded).unwrap();
196        assert_eq!(request, decoded);
197    }
198
199    #[test]
200    fn test_encode_decode_response() {
201        let response = Response {
202            request_id: MessageId::new(1),
203            session_id: SessionId::new("test-session"),
204            result: OperationResult::Success {
205                status: Status { tier: 1, cached: false, duration_ms: 5 },
206                value: Some("3".to_string()),
207                stdout: None,
208                stderr: None,
209            },
210        };
211
212        let encoded = PostcardCodec::encode_response(&response).unwrap();
213        let decoded = PostcardCodec::decode_response(&encoded).unwrap();
214        assert_eq!(response, decoded);
215    }
216
217    #[test]
218    fn test_write_read_request() {
219        let request = Request {
220            id: MessageId::new(42),
221            session_id: SessionId::new("write-read-test"),
222            operation: Operation::LsSessions,
223        };
224
225        let mut buffer = Vec::new();
226        PostcardCodec::write_request(&mut buffer, &request).unwrap();
227
228        let mut cursor = Cursor::new(buffer);
229        let decoded = PostcardCodec::read_request(&mut cursor).unwrap();
230        assert_eq!(request, decoded);
231    }
232
233    #[test]
234    fn test_write_read_response() {
235        let response = Response {
236            request_id: MessageId::new(99),
237            session_id: SessionId::new("response-test"),
238            result: OperationResult::Sessions {
239                sessions: vec![
240                    SessionInfo {
241                        id: SessionId::new("session1"),
242                        name: None,
243                        mode: ReplMode::Lisp,
244                        eval_count: 0,
245                        created_at: 0,
246                        last_active_at: 0,
247                        timeout_ms: 3600000,
248                    },
249                    SessionInfo {
250                        id: SessionId::new("session2"),
251                        name: None,
252                        mode: ReplMode::Sexpr,
253                        eval_count: 5,
254                        created_at: 1000,
255                        last_active_at: 1000,
256                        timeout_ms: 3600000,
257                    },
258                ],
259            },
260        };
261
262        let mut buffer = Vec::new();
263        PostcardCodec::write_response(&mut buffer, &response).unwrap();
264
265        let mut cursor = Cursor::new(buffer);
266        let decoded = PostcardCodec::read_response(&mut cursor).unwrap();
267        assert_eq!(response, decoded);
268    }
269
270    #[test]
271    fn test_message_too_large() {
272        // Create payload that exceeds MAX_MESSAGE_SIZE
273        let huge_code = "x".repeat((MAX_MESSAGE_SIZE + 1) as usize);
274        let request = Request {
275            id: MessageId::new(1),
276            session_id: SessionId::new("test"),
277            operation: Operation::Eval { code: huge_code, mode: ReplMode::Lisp },
278        };
279
280        let result = PostcardCodec::encode_request(&request);
281        assert!(matches!(result, Err(CodecError::MessageTooLarge(_))));
282    }
283
284    #[test]
285    fn test_unexpected_eof() {
286        // Incomplete message (length prefix without payload)
287        let incomplete = vec![0x0A, 0x00, 0x00, 0x00]; // Says 10 bytes follow, but nothing there
288
289        let result = PostcardCodec::decode_request(&incomplete);
290        assert!(matches!(result, Err(CodecError::UnexpectedEof)));
291    }
292
293    #[test]
294    fn test_framing_format() {
295        let request = Request {
296            id: MessageId::new(1),
297            session_id: SessionId::new("test"),
298            operation: Operation::Interrupt,
299        };
300
301        let encoded = PostcardCodec::encode_request(&request).unwrap();
302
303        // First 4 bytes should be length
304        let len = u32::from_le_bytes([encoded[0], encoded[1], encoded[2], encoded[3]]);
305        assert_eq!(len as usize, encoded.len() - 4);
306
307        // Remaining bytes should be postcard payload
308        let payload = &encoded[4..];
309        let decoded: Request = postcard::from_bytes(payload).unwrap();
310        assert_eq!(request, decoded);
311    }
312}