1use std::collections::BTreeMap;
18
19use bytes::{BufMut, Bytes, BytesMut};
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22use tokio_util::codec::{Decoder, LengthDelimitedCodec, LengthDelimitedCodecError};
23
24use crate::error::{codes, Error};
25
26pub const PROTOCOL_VERSION: u64 = 2;
28pub const DEFAULT_MAX_FRAME_SIZE: usize = 64 * 1024 * 1024;
30
31#[derive(Debug, thiserror::Error)]
33pub enum ProtocolError {
34 #[error("io error: {0}")]
36 Io(#[from] std::io::Error),
37 #[error("frame exceeds limit of {0} bytes")]
39 TooLarge(usize),
40 #[error("malformed frame: {0}")]
42 Malformed(String),
43}
44
45impl From<ProtocolError> for Error {
46 fn from(e: ProtocolError) -> Self {
47 match e {
48 ProtocolError::TooLarge(_) => Error::new(codes::FRAME_TOO_LARGE, e.to_string()),
49 _ => Error::new(codes::INTERNAL, e.to_string()),
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub struct AppInfo {
57 pub name: String,
59 pub version: String,
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
67pub struct Inline {
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub value: Option<Value>,
71 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
73 pub raw: bool,
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, strum::IntoStaticStr)]
80#[serde(tag = "type", rename_all = "snake_case")]
81#[strum(serialize_all = "snake_case")]
82pub enum Message {
83 Hello {
85 protocol: u64,
86 #[serde(default)]
87 sdk: String,
88 #[serde(default)]
89 commands: Vec<String>,
90 },
91 Ready {
93 protocol: u64,
94 app: AppInfo,
95 },
96 Shutdown {
98 #[serde(rename = "timeoutMs")]
99 timeout_ms: u64,
100 },
101 Request {
103 id: u64,
105 cmd: String,
106 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
108 channels: BTreeMap<String, u64>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 window: Option<String>,
112 #[serde(flatten)]
113 payload: Inline,
114 },
115 Response {
117 id: u64,
118 #[serde(flatten)]
119 payload: Inline,
120 },
121 Error {
123 id: u64,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 code: Option<String>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 message: Option<String>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 data: Option<Value>,
130 },
131 Cancel {
133 id: u64,
134 reason: String,
135 },
136 Event {
138 event: String,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 target: Option<String>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 window: Option<String>,
145 #[serde(flatten)]
146 payload: Inline,
147 },
148 ChannelData {
150 channel: u64,
151 #[serde(flatten)]
152 payload: Inline,
153 },
154 ChannelClose {
156 channel: u64,
157 },
158}
159
160#[derive(Debug, Clone, PartialEq)]
162pub enum Payload {
163 None,
165 Json(Value),
167 Raw(Vec<u8>),
169}
170
171impl Payload {
172 pub fn split(self) -> (Inline, Vec<u8>) {
174 match self {
175 Self::None => (Inline::default(), Vec::new()),
176 Self::Json(value) => (Inline { value: Some(value), raw: false }, Vec::new()),
177 Self::Raw(bytes) => (Inline { value: None, raw: true }, bytes),
178 }
179 }
180}
181
182impl Inline {
183 pub fn join(self, body: Vec<u8>) -> Result<Payload, ProtocolError> {
190 match (self.raw, self.value) {
191 (true, None) => Ok(Payload::Raw(body)),
192 (false, value) if body.is_empty() => Ok(value.map_or(Payload::None, Payload::Json)),
193 (true, Some(_)) => Err(ProtocolError::Malformed("raw payload with an inline value".into())),
194 (false, _) => Err(ProtocolError::Malformed("frame body without the raw flag".into())),
195 }
196 }
197}
198
199#[derive(Debug, Clone, PartialEq)]
201pub struct Frame {
202 pub message: Message,
204 pub body: Vec<u8>,
206}
207
208impl Frame {
209 pub fn new(message: Message) -> Self {
211 Self { message, body: Vec::new() }
212 }
213
214 pub fn encode(&self) -> Bytes {
220 let header = serde_json::to_vec(&self.message).expect("messages always serialize");
221 let mut buf = BytesMut::with_capacity(4 + header.len() + self.body.len());
222 buf.put_u32(header.len() as u32);
223 buf.put_slice(&header);
224 buf.put_slice(&self.body);
225 buf.freeze()
226 }
227
228 pub fn decode(buf: &[u8]) -> Result<Self, ProtocolError> {
235 let (len, rest) = buf
236 .split_first_chunk::<4>()
237 .ok_or_else(|| ProtocolError::Malformed("frame shorter than 4 bytes".into()))?;
238 let header_len = u32::from_be_bytes(*len) as usize;
239 if header_len > rest.len() {
240 return Err(ProtocolError::Malformed("header length exceeds frame".into()));
241 }
242 let (header, body) = rest.split_at(header_len);
243 let message =
244 serde_json::from_slice(header).map_err(|e| ProtocolError::Malformed(format!("invalid header: {e}")))?;
245 Ok(Self { message, body: body.to_vec() })
246 }
247}
248
249pub fn length_codec(max_frame_size: usize) -> LengthDelimitedCodec {
251 LengthDelimitedCodec::builder()
252 .big_endian()
253 .length_field_type::<u32>()
254 .max_frame_length(max_frame_size)
255 .new_codec()
256}
257
258pub struct FrameCodec {
264 inner: LengthDelimitedCodec,
265 max_frame_size: usize,
266}
267
268impl FrameCodec {
269 pub fn new(max_frame_size: usize) -> Self {
271 Self { inner: length_codec(max_frame_size), max_frame_size }
272 }
273}
274
275impl Decoder for FrameCodec {
276 type Item = Result<Frame, ProtocolError>;
277 type Error = ProtocolError;
278
279 fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, ProtocolError> {
280 let chunk = self.inner.decode(src).map_err(|e| {
281 if e.get_ref().is_some_and(|inner| inner.is::<LengthDelimitedCodecError>()) {
282 ProtocolError::TooLarge(self.max_frame_size)
283 } else {
284 ProtocolError::Io(e)
285 }
286 })?;
287 Ok(chunk.map(|buf| Frame::decode(&buf)))
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use serde_json::json;
295 use tokio_util::codec::Encoder;
296
297 fn wire(frame: &Frame) -> Vec<u8> {
298 let mut buf = BytesMut::new();
299 length_codec(DEFAULT_MAX_FRAME_SIZE).encode(frame.encode(), &mut buf).unwrap();
300 buf.to_vec()
301 }
302
303 fn raw_wire(header: &[u8]) -> Vec<u8> {
304 [&((4 + header.len()) as u32).to_be_bytes()[..], &(header.len() as u32).to_be_bytes(), header].concat()
305 }
306
307 fn decode_all(data: &[u8], max: usize) -> Vec<Result<Frame, ProtocolError>> {
308 let mut codec = FrameCodec::new(max);
309 let mut buf = BytesMut::from(data);
310 std::iter::from_fn(|| codec.decode(&mut buf).unwrap()).collect()
311 }
312
313 fn event(name: &str) -> Frame {
314 Frame::new(Message::Event { event: name.into(), target: None, window: None, payload: Inline::default() })
315 }
316
317 fn raw_response(id: u64, body: &[u8]) -> Frame {
318 let (payload, body) = Payload::Raw(body.to_vec()).split();
319 Frame { message: Message::Response { id, payload }, body }
320 }
321
322 fn golden_frames() -> Vec<(&'static str, Frame)> {
324 let json = |value: Value| Inline { value: Some(value), raw: false };
325 vec![
326 ("hello", Frame::new(Message::Hello { protocol: 2, sdk: "0.1.0".into(), commands: vec!["greet".into()] })),
327 (
328 "ready",
329 Frame::new(Message::Ready { protocol: 2, app: AppInfo { name: "app".into(), version: "1.0.0".into() } }),
330 ),
331 ("shutdown", Frame::new(Message::Shutdown { timeout_ms: 3000 })),
332 (
333 "request",
334 Frame::new(Message::Request {
335 id: 7,
336 cmd: "greet".into(),
337 channels: [("onItem".into(), 3)].into(),
338 window: Some("main".into()),
339 payload: json(json!({"n": [1, 2.5, null], "name": "中文"})),
340 }),
341 ),
342 ("response_raw", raw_response(7, &[0, 255])),
343 (
344 "error",
345 Frame::new(Message::Error {
346 id: 7,
347 code: Some("custom".into()),
348 message: Some("bad".into()),
349 data: Some(json!({"x": 1})),
350 }),
351 ),
352 ("cancel", Frame::new(Message::Cancel { id: 7, reason: "timeout".into() })),
353 (
354 "event",
355 Frame::new(Message::Event {
356 event: "tick".into(),
357 target: Some("main".into()),
358 window: None,
359 payload: Inline::default(),
360 }),
361 ),
362 ("channel_data", Frame::new(Message::ChannelData { channel: 3, payload: json(json!(1)) })),
363 ("channel_close", Frame::new(Message::ChannelClose { channel: 3 })),
364 ]
365 }
366
367 #[test]
368 fn golden_frames_match_python() {
369 let fixture: BTreeMap<String, String> = serde_json::from_str(include_str!(concat!(
370 env!("CARGO_MANIFEST_DIR"),
371 "/python-sdk/tests/fixtures/golden_frames.json"
372 )))
373 .unwrap();
374 let frames = golden_frames();
375 assert_eq!(fixture.keys().collect::<Vec<_>>(), {
376 let mut names: Vec<_> = frames.iter().map(|(name, _)| *name).collect();
377 names.sort();
378 names
379 });
380 for (name, frame) in frames {
381 let hex = &fixture[name];
382 let data: Vec<u8> = (0..hex.len()).step_by(2).map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap()).collect();
383 assert_eq!(wire(&frame), data, "{name}: encoding differs");
384 let decoded = decode_all(&data, DEFAULT_MAX_FRAME_SIZE).pop().unwrap().unwrap();
385 assert_eq!(decoded, frame, "{name}: decoding differs");
386 }
387 }
388
389 #[test]
390 fn layout() {
391 let data = wire(&raw_response(2, b"xy"));
392 let header = br#"{"type":"response","id":2,"raw":true}"#;
393 assert_eq!(&data[..4], &((4 + header.len() + 2) as u32).to_be_bytes());
394 assert_eq!(&data[4..8], &(header.len() as u32).to_be_bytes());
395 assert_eq!(&data[8..], [&header[..], b"xy"].concat().as_slice());
396 }
397
398 #[test]
399 fn partial_input_waits_and_truncated_eof_is_fatal() {
400 let data = wire(&raw_response(1, &[0, 255]));
401 let mut codec = FrameCodec::new(DEFAULT_MAX_FRAME_SIZE);
402 let mut buf = BytesMut::from(&data[..data.len() - 1]);
403 assert!(codec.decode(&mut buf).unwrap().is_none());
404 assert!(codec.decode_eof(&mut buf).is_err());
405 assert!(codec.decode_eof(&mut BytesMut::new()).unwrap().is_none());
406 }
407
408 #[test]
409 fn oversized_frame_is_fatal() {
410 let mut buf = BytesMut::from(&wire(&raw_response(1, &[1; 100]))[..]);
411 let err = FrameCodec::new(64).decode(&mut buf).unwrap_err();
412 assert!(matches!(err, ProtocolError::TooLarge(64)), "{err:?}");
413 assert_eq!(Error::from(err).code, codes::FRAME_TOO_LARGE);
414 }
415
416 #[test]
417 fn malformed_frames_are_skipped() {
418 let bad: [&[u8]; 6] = [
419 br#"{"type":"bogus"}"#,
420 br#"{"type":"response"}"#,
421 br#"{"type":"response","id":"1"}"#,
422 br#"{"id":1}"#,
423 b"[1]",
424 b"{not json",
425 ];
426 let mut data: Vec<u8> = bad.iter().flat_map(|h| raw_wire(h)).collect();
427 data.extend([0, 0, 0, 4, 0, 0, 0, 9]);
428 data.extend(wire(&event("ok")));
429 let frames = decode_all(&data, 1024);
430 assert_eq!(frames.len(), bad.len() + 2);
431 for frame in &frames[..bad.len() + 1] {
432 assert!(matches!(frame, Err(ProtocolError::Malformed(_))), "{frame:?}");
433 }
434 assert_eq!(frames.last().unwrap().as_ref().unwrap(), &event("ok"));
435 }
436
437 #[test]
438 fn payload_roundtrip_and_validation() {
439 for payload in [Payload::None, Payload::Json(json!({"a": [1, null]})), Payload::Raw(vec![1, 2])] {
440 let (inline, body) = payload.clone().split();
441 assert_eq!(inline.join(body).unwrap(), payload);
442 }
443 assert!(Inline { value: Some(json!(1)), raw: false }.join(vec![1]).is_err());
444 assert!(Inline { value: Some(json!(1)), raw: true }.join(vec![1]).is_err());
445 }
446
447 #[test]
448 fn absent_fields_are_omitted() {
449 let frame = Frame::new(Message::Request {
450 id: 1,
451 cmd: "hi".into(),
452 channels: BTreeMap::new(),
453 window: None,
454 payload: Inline::default(),
455 });
456 assert_eq!(&wire(&frame)[8..], br#"{"type":"request","id":1,"cmd":"hi"}"#);
457 let message: Message = serde_json::from_str(r#"{"type":"error","id":1,"extra":true}"#).unwrap();
458 assert_eq!(message, Message::Error { id: 1, code: None, message: None, data: None });
459 }
460}