1use serde::{de::DeserializeOwned, Serialize};
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8pub enum FramingError {
9 #[error("Serialization error: {0}")]
10 Serialize(#[from] rmp_serde::encode::Error),
11
12 #[error("Deserialization error: {0}")]
13 Deserialize(#[from] rmp_serde::decode::Error),
14
15 #[error("Message too large: {0} bytes (max: {1})")]
16 MessageTooLarge(usize, usize),
17
18 #[error("Invalid frame: {0}")]
19 InvalidFrame(String),
20}
21
22pub const MAX_MESSAGE_SIZE: usize = 16 * 1024 * 1024;
24
25pub fn encode<T: Serialize>(msg: &T) -> Result<Vec<u8>, FramingError> {
29 let payload = rmp_serde::to_vec_named(msg)?;
30
31 if payload.len() > MAX_MESSAGE_SIZE {
32 return Err(FramingError::MessageTooLarge(
33 payload.len(),
34 MAX_MESSAGE_SIZE,
35 ));
36 }
37
38 let len = payload.len() as u32;
39 let mut frame = Vec::with_capacity(4 + payload.len());
40 frame.extend_from_slice(&len.to_le_bytes());
41 frame.extend(payload);
42
43 Ok(frame)
44}
45
46pub fn encode_payload<T: Serialize>(msg: &T) -> Result<Vec<u8>, FramingError> {
48 let payload = rmp_serde::to_vec_named(msg)?;
49
50 if payload.len() > MAX_MESSAGE_SIZE {
51 return Err(FramingError::MessageTooLarge(
52 payload.len(),
53 MAX_MESSAGE_SIZE,
54 ));
55 }
56
57 Ok(payload)
58}
59
60pub fn decode<T: DeserializeOwned>(data: &[u8]) -> Result<T, FramingError> {
62 Ok(rmp_serde::from_slice(data)?)
63}
64
65pub fn parse_frame_header(data: &[u8]) -> Result<(usize, usize), FramingError> {
67 if data.len() < 4 {
68 return Err(FramingError::InvalidFrame(
69 "Frame too short for header".to_string(),
70 ));
71 }
72
73 let len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
74
75 if len > MAX_MESSAGE_SIZE {
76 return Err(FramingError::MessageTooLarge(len, MAX_MESSAGE_SIZE));
77 }
78
79 Ok((len, 4))
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use rpytest_core::protocol::{Request, Response, PROTOCOL_VERSION};
86
87 #[test]
88 fn encode_decode_roundtrip() {
89 let request = Request::InitContext {
90 protocol_version: PROTOCOL_VERSION,
91 repo_path: "/path/to/repo".to_string(),
92 python_path: Some("/usr/bin/python3".to_string()),
93 execution_mode: None,
94 };
95
96 let encoded = encode(&request).unwrap();
97
98 let (len, offset) = parse_frame_header(&encoded).unwrap();
100 assert_eq!(offset, 4);
101 assert_eq!(len, encoded.len() - 4);
102
103 let decoded: Request = decode(&encoded[offset..]).unwrap();
105 assert_eq!(request, decoded);
106 }
107
108 #[test]
109 fn encode_decode_response() {
110 let response = Response::ContextReady {
111 protocol_version: PROTOCOL_VERSION,
112 context_id: "ctx-123".to_string(),
113 inventory_hash: "abc123".to_string(),
114 };
115
116 let payload = encode_payload(&response).unwrap();
117 let decoded: Response = decode(&payload).unwrap();
118 assert_eq!(response, decoded);
119 }
120
121 #[test]
122 fn rejects_oversized_message() {
123 let large_data = vec![0u8; MAX_MESSAGE_SIZE + 1];
124
125 let result = encode_payload(&large_data);
127 assert!(matches!(result, Err(FramingError::MessageTooLarge(_, _))));
128 }
129
130 #[test]
131 fn parse_short_header_fails() {
132 let short_data = vec![0u8, 1u8, 2u8]; let result = parse_frame_header(&short_data);
134 assert!(matches!(result, Err(FramingError::InvalidFrame(_))));
135 }
136}