1use serde::Serialize;
17use serde::de::DeserializeOwned;
18use std::fmt;
19use std::io::{self, BufRead, Read};
20
21use plushie_core::codec_safety::{MAX_RMPV_DEPTH, check_msgpack_depth};
22
23pub const MAX_MESSAGE_SIZE: usize = 64 * 1024 * 1024;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Codec {
33 Json,
35 MsgPack,
37}
38
39impl fmt::Display for Codec {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Codec::Json => f.write_str("json"),
43 Codec::MsgPack => f.write_str("msgpack"),
44 }
45 }
46}
47
48impl Codec {
49 pub fn encode<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, String> {
64 match self {
65 Codec::Json => {
66 let mut json_value =
67 serde_json::to_value(value).map_err(|e| format!("json encode: {e}"))?;
68 sanitize_json_value(&mut json_value);
69 let mut bytes =
70 serde_json::to_vec(&json_value).map_err(|e| format!("json encode: {e}"))?;
71 bytes.push(b'\n');
72 Ok(bytes)
73 }
74 Codec::MsgPack => {
75 let payload =
76 rmp_serde::to_vec_named(value).map_err(|e| format!("msgpack encode: {e}"))?;
77 let mut msg = rmpv::decode::read_value(&mut &payload[..])
78 .map_err(|e| format!("msgpack encode: {e}"))?;
79 sanitize_rmpv_value(&mut msg);
80 let mut payload = Vec::new();
81 rmpv::encode::write_value(&mut payload, &msg)
82 .map_err(|e| format!("msgpack encode: {e}"))?;
83 let len = u32::try_from(payload.len()).map_err(|_| {
84 format!(
85 "payload exceeds 4 GiB frame limit ({} bytes)",
86 payload.len()
87 )
88 })?;
89 let mut bytes = Vec::with_capacity(4 + payload.len());
90 bytes.extend_from_slice(&len.to_be_bytes());
91 bytes.extend_from_slice(&payload);
92 Ok(bytes)
93 }
94 }
95 }
96
97 pub fn encode_binary_message(
115 &self,
116 map: serde_json::Map<String, serde_json::Value>,
117 binary_field: Option<(&str, &[u8])>,
118 ) -> Result<Vec<u8>, String> {
119 let mut val = serde_json::Value::Object(map);
120 sanitize_json_value(&mut val);
121 let mut map = match val {
122 serde_json::Value::Object(map) => map,
123 _ => unreachable!("object sanitizer must preserve object shape"),
124 };
125
126 match self {
127 Codec::Json => {
128 if let Some((key, bytes)) = binary_field
129 && !bytes.is_empty()
130 {
131 use base64::Engine;
132 let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
133 map.insert(key.to_string(), serde_json::Value::String(b64));
134 }
135 let val = serde_json::Value::Object(map);
136 let mut bytes =
137 serde_json::to_vec(&val).map_err(|e| format!("json encode: {e}"))?;
138 bytes.push(b'\n');
139 Ok(bytes)
140 }
141 Codec::MsgPack => {
142 use rmpv::Value as V;
143
144 let mut entries: Vec<(V, V)> = map
145 .into_iter()
146 .map(|(k, v)| (V::String(k.into()), json_to_rmpv(v)))
147 .collect();
148
149 if let Some((key, bytes)) = binary_field
150 && !bytes.is_empty()
151 {
152 entries.push((V::String(key.into()), V::Binary(bytes.to_vec())));
153 }
154
155 let msg = V::Map(entries);
156 let mut payload = Vec::new();
157 rmpv::encode::write_value(&mut payload, &msg)
158 .map_err(|e| format!("msgpack encode: {e}"))?;
159 let len = u32::try_from(payload.len()).map_err(|_| {
160 format!(
161 "payload exceeds 4 GiB frame limit ({} bytes)",
162 payload.len()
163 )
164 })?;
165 let mut bytes = Vec::with_capacity(4 + payload.len());
166 bytes.extend_from_slice(&len.to_be_bytes());
167 bytes.extend_from_slice(&payload);
168 Ok(bytes)
169 }
170 }
171 }
172
173 pub fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, String> {
191 match self {
192 Codec::Json => serde_json::from_slice(bytes).map_err(|e| format!("json decode: {e}")),
193 Codec::MsgPack => {
194 check_msgpack_depth(bytes, MAX_RMPV_DEPTH)
199 .map_err(|e| format!("msgpack depth check: {e}"))?;
200 let rmpv_val: rmpv::Value = rmpv::decode::read_value(&mut &bytes[..])
201 .map_err(|e| format!("msgpack decode (rmpv): {e}"))?;
202 let json_val = rmpv_to_json(rmpv_val)
203 .map_err(|e| format!("msgpack decode (invalid UTF-8): {e}"))?;
204 #[cfg(debug_assertions)]
209 {
210 let json_for_err = json_val.clone();
211 serde_json::from_value(json_val).map_err(|e| {
212 let dump = json_for_err.to_string();
213 let truncated = if dump.len() > 512 {
214 format!("{}...", &dump[..512])
215 } else {
216 dump
217 };
218 format!("msgpack decode (tag dispatch): {e} | json: {truncated}")
219 })
220 }
221 #[cfg(not(debug_assertions))]
222 {
223 serde_json::from_value(json_val)
224 .map_err(|e| format!("msgpack decode (tag dispatch): {e}"))
225 }
226 }
227 }
228 }
229
230 pub fn read_message<R: BufRead>(&self, reader: &mut R) -> io::Result<Option<Vec<u8>>> {
243 match self {
244 Codec::Json => loop {
245 let mut line = String::new();
246 let limit = (MAX_MESSAGE_SIZE + 1) as u64;
250 let n = (&mut *reader).take(limit).read_line(&mut line)?;
251 if n == 0 {
252 return Ok(None);
253 }
254 if line.len() > MAX_MESSAGE_SIZE {
255 return Err(io::Error::new(
256 io::ErrorKind::InvalidData,
257 format!(
258 "JSON message exceeds {} byte limit ({} bytes)",
259 MAX_MESSAGE_SIZE,
260 line.len()
261 ),
262 ));
263 }
264 let trimmed = line.trim();
265 if trimmed.is_empty() {
266 continue;
267 }
268 return Ok(Some(trimmed.as_bytes().to_vec()));
269 },
270 Codec::MsgPack => {
271 let mut len_buf = [0u8; 4];
272 match reader.read_exact(&mut len_buf) {
273 Ok(()) => {}
274 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
275 Err(e) => return Err(e),
276 }
277 let len = u32::from_be_bytes(len_buf) as usize;
278 if len == 0 {
279 return Err(io::Error::new(
280 io::ErrorKind::InvalidData,
281 "empty frame received",
282 ));
283 }
284 if len > MAX_MESSAGE_SIZE {
285 return Err(io::Error::new(
286 io::ErrorKind::InvalidData,
287 format!(
288 "msgpack frame exceeds {} byte limit ({} bytes)",
289 MAX_MESSAGE_SIZE, len
290 ),
291 ));
292 }
293 let mut payload = vec![0u8; len];
294 reader.read_exact(&mut payload)?;
295 Ok(Some(payload))
296 }
297 }
298 }
299
300 pub fn detect_from_first_byte(byte: u8) -> Codec {
305 if byte == b'{' {
306 Codec::Json
307 } else {
308 Codec::MsgPack
309 }
310 }
311}
312
313fn rmpv_to_json(val: rmpv::Value) -> Result<serde_json::Value, String> {
339 rmpv_to_json_inner(val, 0)
340}
341
342fn rmpv_to_json_inner(val: rmpv::Value, depth: usize) -> Result<serde_json::Value, String> {
343 if depth > MAX_RMPV_DEPTH {
344 log::error!("rmpv_to_json: recursion depth exceeded {MAX_RMPV_DEPTH}, replaced with null");
345 return Ok(serde_json::Value::Null);
346 }
347
348 Ok(match val {
349 rmpv::Value::Nil => serde_json::Value::Null,
350 rmpv::Value::Boolean(b) => serde_json::Value::Bool(b),
351 rmpv::Value::Integer(n) => {
352 if let Some(i) = n.as_i64() {
353 serde_json::Value::Number(i.into())
354 } else if let Some(u) = n.as_u64() {
355 serde_json::Value::Number(u.into())
356 } else {
357 serde_json::Value::Null
359 }
360 }
361 rmpv::Value::F32(f) => serde_json::Number::from_f64(f as f64)
362 .map(serde_json::Value::Number)
363 .unwrap_or_else(|| {
364 log::warn!("rmpv_to_json: non-finite f32 ({f}) replaced with null");
365 serde_json::Value::Null
366 }),
367 rmpv::Value::F64(f) => serde_json::Number::from_f64(f)
368 .map(serde_json::Value::Number)
369 .unwrap_or_else(|| {
370 log::warn!("rmpv_to_json: non-finite f64 ({f}) replaced with null");
371 serde_json::Value::Null
372 }),
373 rmpv::Value::String(s) => {
374 let bytes = s.as_bytes();
378 match std::str::from_utf8(bytes) {
379 Ok(valid) => serde_json::Value::String(valid.to_owned()),
380 Err(e) => {
381 return Err(format!(
382 "invalid UTF-8 in msgpack string at byte offset {}: {}",
383 e.valid_up_to(),
384 e
385 ));
386 }
387 }
388 }
389 rmpv::Value::Binary(bytes) => {
390 use base64::Engine as _;
391
392 serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(bytes))
393 }
394 rmpv::Value::Array(arr) => {
395 let mut out = Vec::with_capacity(arr.len());
396 for v in arr {
397 out.push(rmpv_to_json_inner(v, depth + 1)?);
398 }
399 serde_json::Value::Array(out)
400 }
401 rmpv::Value::Map(entries) => {
402 let mut map = serde_json::Map::new();
403 for (k, v) in entries {
404 let key = match k {
410 rmpv::Value::String(s) => match s.into_str() {
411 Some(valid) => valid,
412 None => {
413 return Err("invalid UTF-8 in msgpack map key".to_string());
414 }
415 },
416 rmpv::Value::Integer(n) => n.to_string(),
417 other => format!("{other}"),
418 };
419 map.insert(key, rmpv_to_json_inner(v, depth + 1)?);
420 }
421 serde_json::Value::Object(map)
422 }
423 rmpv::Value::Ext(type_id, _bytes) => {
424 log::warn!(
425 "rmpv_to_json: msgpack ext type {type_id} not supported, replaced with null"
426 );
427 serde_json::Value::Null
428 }
429 })
430}
431
432fn json_to_rmpv(val: serde_json::Value) -> rmpv::Value {
435 match val {
436 serde_json::Value::Null => rmpv::Value::Nil,
437 serde_json::Value::Bool(b) => rmpv::Value::Boolean(b),
438 serde_json::Value::Number(n) => {
439 if let Some(i) = n.as_i64() {
440 rmpv::Value::Integer(i.into())
441 } else if let Some(u) = n.as_u64() {
442 rmpv::Value::Integer(u.into())
443 } else if let Some(f) = n.as_f64() {
444 rmpv::Value::F64(f)
445 } else {
446 rmpv::Value::Nil
447 }
448 }
449 serde_json::Value::String(s) => rmpv::Value::String(s.into()),
450 serde_json::Value::Array(arr) => {
451 rmpv::Value::Array(arr.into_iter().map(json_to_rmpv).collect())
452 }
453 serde_json::Value::Object(map) => rmpv::Value::Map(
454 map.into_iter()
455 .map(|(k, v)| (rmpv::Value::String(k.into()), json_to_rmpv(v)))
456 .collect(),
457 ),
458 }
459}
460
461fn sanitize_json_value(value: &mut serde_json::Value) {
467 match value {
468 serde_json::Value::Array(arr) => {
469 for item in arr {
470 sanitize_json_value(item);
471 }
472 }
473 serde_json::Value::Object(map) => {
474 for item in map.values_mut() {
475 sanitize_json_value(item);
476 }
477 }
478 serde_json::Value::Number(number) => {
479 if let Some(float) = number.as_f64()
480 && !float.is_finite()
481 {
482 *value = serde_json::Value::Null;
483 }
484 }
485 serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::String(_) => {}
486 }
487}
488
489fn sanitize_rmpv_value(value: &mut rmpv::Value) {
494 match value {
495 rmpv::Value::F32(float) if !float.is_finite() => {
496 *value = rmpv::Value::Nil;
497 }
498 rmpv::Value::F64(float) if !float.is_finite() => {
499 *value = rmpv::Value::Nil;
500 }
501 rmpv::Value::Array(items) => {
502 for item in items {
503 sanitize_rmpv_value(item);
504 }
505 }
506 rmpv::Value::Map(entries) => {
507 for (_, item) in entries {
508 sanitize_rmpv_value(item);
509 }
510 }
511 _ => {}
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use serde::{Deserialize, Serialize};
519 use serde_json::json;
520
521 #[derive(Debug, Serialize, Deserialize, PartialEq)]
522 struct Simple {
523 name: String,
524 count: u32,
525 }
526
527 #[derive(Debug, Serialize, Deserialize, PartialEq)]
528 #[serde(tag = "type", rename_all = "snake_case")]
529 enum Tagged {
530 Alpha { value: String },
531 Beta { x: f64, y: f64 },
532 }
533
534 #[derive(Debug, Serialize, Deserialize, PartialEq)]
535 struct WithFlatten {
536 op: String,
537 #[serde(flatten)]
538 rest: serde_json::Value,
539 }
540
541 #[derive(Debug, Serialize)]
542 struct NonFiniteScalars {
543 nan: f64,
544 pos_inf: f64,
545 neg_inf: f64,
546 }
547
548 #[test]
551 fn json_roundtrip_simple() {
552 let original = Simple {
553 name: "test".into(),
554 count: 42,
555 };
556 let bytes = Codec::Json.encode(&original).unwrap();
557 assert!(bytes.ends_with(b"\n"));
558 let decoded: Simple = Codec::Json.decode(&bytes[..bytes.len() - 1]).unwrap();
559 assert_eq!(decoded, original);
560 }
561
562 #[test]
563 fn json_roundtrip_tagged_enum() {
564 let original = Tagged::Beta { x: 1.5, y: 2.5 };
565 let bytes = Codec::Json.encode(&original).unwrap();
566 let decoded: Tagged = Codec::Json.decode(&bytes[..bytes.len() - 1]).unwrap();
567 assert_eq!(decoded, original);
568 }
569
570 #[test]
571 fn json_encode_non_finite_floats_become_null() {
572 let bytes = Codec::Json
573 .encode(&NonFiniteScalars {
574 nan: f64::NAN,
575 pos_inf: f64::INFINITY,
576 neg_inf: f64::NEG_INFINITY,
577 })
578 .unwrap();
579 let decoded: serde_json::Value = serde_json::from_slice(&bytes[..bytes.len() - 1]).unwrap();
580 assert_eq!(
581 decoded,
582 json!({
583 "nan": null,
584 "pos_inf": null,
585 "neg_inf": null
586 })
587 );
588 }
589
590 #[test]
593 fn msgpack_roundtrip_simple() {
594 let original = Simple {
595 name: "test".into(),
596 count: 42,
597 };
598 let bytes = Codec::MsgPack.encode(&original).unwrap();
599 let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
601 assert_eq!(len, bytes.len() - 4);
602 let decoded: Simple = Codec::MsgPack.decode(&bytes[4..]).unwrap();
603 assert_eq!(decoded, original);
604 }
605
606 #[test]
607 fn msgpack_roundtrip_non_finite_floats_become_null() {
608 let bytes = Codec::MsgPack
609 .encode(&NonFiniteScalars {
610 nan: f64::NAN,
611 pos_inf: f64::INFINITY,
612 neg_inf: f64::NEG_INFINITY,
613 })
614 .unwrap();
615 let decoded: serde_json::Value = Codec::MsgPack.decode(&bytes[4..]).unwrap();
616 assert_eq!(
617 decoded,
618 json!({
619 "nan": null,
620 "pos_inf": null,
621 "neg_inf": null
622 })
623 );
624 }
625
626 #[test]
627 fn msgpack_encode_preserves_f32_wire_type() {
628 #[derive(Debug, Serialize)]
629 struct F32Value {
630 value: f32,
631 }
632
633 let bytes = Codec::MsgPack.encode(&F32Value { value: 1.25 }).unwrap();
634 let payload = &bytes[4..];
635 let decoded = rmpv::decode::read_value(&mut &payload[..]).unwrap();
636
637 match decoded {
638 rmpv::Value::Map(entries) => {
639 let value_entry = entries
640 .into_iter()
641 .find(|(key, _)| key == &rmpv::Value::String("value".into()))
642 .expect("value field present");
643 match value_entry.1 {
644 rmpv::Value::F32(value) => assert_eq!(value, 1.25),
645 other => panic!("expected f32 wire value, got {other:?}"),
646 }
647 }
648 other => panic!("expected map, got {other:?}"),
649 }
650 }
651
652 #[test]
653 fn msgpack_roundtrip_tagged_enum() {
654 let original = Tagged::Alpha {
655 value: "hello".into(),
656 };
657 let bytes = Codec::MsgPack.encode(&original).unwrap();
658 let payload = &bytes[4..];
659 let decoded: Tagged = Codec::MsgPack.decode(payload).unwrap();
660 assert_eq!(decoded, original);
661 }
662
663 #[test]
664 fn msgpack_roundtrip_tagged_enum_beta() {
665 let original = Tagged::Beta {
666 x: std::f64::consts::PI,
667 y: -1.0,
668 };
669 let bytes = Codec::MsgPack.encode(&original).unwrap();
670 let payload = &bytes[4..];
671 let decoded: Tagged = Codec::MsgPack.decode(payload).unwrap();
672 assert_eq!(decoded, original);
673 }
674
675 #[test]
676 fn msgpack_flatten_deserialize() {
677 let input = json!({"op": "props", "path": [0, 1], "props": {"label": "hi"}});
680 let bytes = rmp_serde::to_vec_named(&input).unwrap();
681 let decoded: WithFlatten = rmp_serde::from_slice(&bytes).unwrap();
682 assert_eq!(decoded.op, "props");
683 assert_eq!(decoded.rest["path"], json!([0, 1]));
684 assert_eq!(decoded.rest["props"]["label"], "hi");
685 }
686
687 #[test]
690 fn json_read_message_skips_blank_lines() {
691 let data = b"\n\n{\"name\":\"a\",\"count\":1}\n\n{\"name\":\"b\",\"count\":2}\n\n";
693 let mut reader = io::BufReader::new(&data[..]);
694
695 let msg1 = Codec::Json.read_message(&mut reader).unwrap().unwrap();
696 let s1: Simple = Codec::Json.decode(&msg1).unwrap();
697 assert_eq!(s1.name, "a");
698
699 let msg2 = Codec::Json.read_message(&mut reader).unwrap().unwrap();
700 let s2: Simple = Codec::Json.decode(&msg2).unwrap();
701 assert_eq!(s2.name, "b");
702
703 assert!(Codec::Json.read_message(&mut reader).unwrap().is_none());
705 }
706
707 #[test]
708 fn json_read_message() {
709 let data = b"{\"name\":\"a\",\"count\":1}\n{\"name\":\"b\",\"count\":2}\n";
710 let mut reader = io::BufReader::new(&data[..]);
711
712 let msg1 = Codec::Json.read_message(&mut reader).unwrap().unwrap();
713 let s1: Simple = Codec::Json.decode(&msg1).unwrap();
714 assert_eq!(s1.name, "a");
715
716 let msg2 = Codec::Json.read_message(&mut reader).unwrap().unwrap();
717 let s2: Simple = Codec::Json.decode(&msg2).unwrap();
718 assert_eq!(s2.name, "b");
719
720 assert!(Codec::Json.read_message(&mut reader).unwrap().is_none());
721 }
722
723 #[test]
724 fn msgpack_read_message() {
725 let s1 = Simple {
727 name: "x".into(),
728 count: 10,
729 };
730 let s2 = Simple {
731 name: "y".into(),
732 count: 20,
733 };
734 let p1 = rmp_serde::to_vec_named(&s1).unwrap();
735 let p2 = rmp_serde::to_vec_named(&s2).unwrap();
736
737 let mut data = Vec::new();
738 data.extend_from_slice(&(p1.len() as u32).to_be_bytes());
739 data.extend_from_slice(&p1);
740 data.extend_from_slice(&(p2.len() as u32).to_be_bytes());
741 data.extend_from_slice(&p2);
742
743 let mut reader = io::BufReader::new(&data[..]);
744
745 let msg1 = Codec::MsgPack.read_message(&mut reader).unwrap().unwrap();
746 let d1: Simple = Codec::MsgPack.decode(&msg1).unwrap();
747 assert_eq!(d1, s1);
748
749 let msg2 = Codec::MsgPack.read_message(&mut reader).unwrap().unwrap();
750 let d2: Simple = Codec::MsgPack.decode(&msg2).unwrap();
751 assert_eq!(d2, s2);
752
753 assert!(Codec::MsgPack.read_message(&mut reader).unwrap().is_none());
754 }
755
756 #[test]
759 fn json_read_message_rejects_oversized_line() {
760 let small_limit = 100;
770 let long_line: Vec<u8> = vec![b'x'; small_limit + 10];
772 let mut reader = io::BufReader::new(&long_line[..]);
773
774 let mut line = String::new();
777 let limit = (small_limit + 1) as u64;
778 let _n = (&mut reader).take(limit).read_line(&mut line).unwrap();
779 assert!(line.len() <= small_limit + 1);
781 }
783
784 #[test]
785 fn msgpack_read_message_rejects_oversized_frame() {
786 let len = (MAX_MESSAGE_SIZE + 1) as u32;
788 let mut data = Vec::new();
789 data.extend_from_slice(&len.to_be_bytes());
790 data.extend_from_slice(&[0u8; 64]); let mut reader = io::BufReader::new(&data[..]);
794 let result = Codec::MsgPack.read_message(&mut reader);
795 assert!(result.is_err());
796 let err = result.unwrap_err();
797 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
798 assert!(err.to_string().contains("byte limit"));
799 }
800
801 #[test]
802 fn msgpack_read_message_rejects_zero_length_frame() {
803 let mut data = Vec::new();
804 data.extend_from_slice(&0u32.to_be_bytes());
805
806 let mut reader = io::BufReader::new(&data[..]);
807 let result = Codec::MsgPack.read_message(&mut reader);
808 assert!(result.is_err());
809 assert!(result.unwrap_err().to_string().contains("empty frame"));
810 }
811
812 #[test]
822 fn msgpack_external_tagged_enum_alpha() {
823 let external = json!({"type": "alpha", "value": "hello"});
825 let bytes = rmp_serde::to_vec_named(&external).unwrap();
826 let decoded: Tagged = Codec::MsgPack.decode(&bytes).unwrap();
827 assert_eq!(
828 decoded,
829 Tagged::Alpha {
830 value: "hello".into()
831 }
832 );
833 }
834
835 #[test]
836 fn msgpack_external_tagged_enum_beta() {
837 let external = json!({"type": "beta", "x": 1.5, "y": -2.0});
838 let bytes = rmp_serde::to_vec_named(&external).unwrap();
839 let decoded: Tagged = Codec::MsgPack.decode(&bytes).unwrap();
840 assert_eq!(decoded, Tagged::Beta { x: 1.5, y: -2.0 });
841 }
842
843 #[test]
844 fn msgpack_external_incoming_settings() {
845 use plushie_core::protocol::IncomingMessage;
847 let external = json!({"type": "settings", "settings": {"antialiasing": false}});
848 let bytes = rmp_serde::to_vec_named(&external).unwrap();
849 let decoded: IncomingMessage = Codec::MsgPack.decode(&bytes).unwrap();
850 assert!(matches!(decoded, IncomingMessage::Settings { .. }));
851 }
852
853 #[test]
854 fn msgpack_external_incoming_snapshot() {
855 use plushie_core::protocol::IncomingMessage;
856 let external = json!({"type": "snapshot", "tree": {"id": "root", "type": "column", "props": {}, "children": []}});
857 let bytes = rmp_serde::to_vec_named(&external).unwrap();
858 let decoded: IncomingMessage = Codec::MsgPack.decode(&bytes).unwrap();
859 assert!(matches!(decoded, IncomingMessage::Snapshot { .. }));
860 }
861
862 #[test]
865 fn msgpack_image_op_with_native_binary() {
866 use rmpv::Value as RmpvValue;
869
870 let pixel_bytes: Vec<u8> = vec![255, 0, 0, 255, 0, 255, 0, 255]; let payload = RmpvValue::Map(vec![
872 (
873 RmpvValue::String("handle".into()),
874 RmpvValue::String("test_img".into()),
875 ),
876 (
877 RmpvValue::String("pixels".into()),
878 RmpvValue::Binary(pixel_bytes.clone()),
879 ),
880 (
881 RmpvValue::String("width".into()),
882 RmpvValue::Integer(1.into()),
883 ),
884 (
885 RmpvValue::String("height".into()),
886 RmpvValue::Integer(2.into()),
887 ),
888 ]);
889 let msg = RmpvValue::Map(vec![
890 (
891 RmpvValue::String("type".into()),
892 RmpvValue::String("image_op".into()),
893 ),
894 (
895 RmpvValue::String("op".into()),
896 RmpvValue::String("create_image".into()),
897 ),
898 (RmpvValue::String("payload".into()), payload),
899 ]);
900
901 let mut buf = Vec::new();
902 rmpv::encode::write_value(&mut buf, &msg).unwrap();
903
904 let decoded: plushie_core::protocol::IncomingMessage = Codec::MsgPack.decode(&buf).unwrap();
905 match decoded {
906 plushie_core::protocol::IncomingMessage::ImageOp { op, payload } => {
907 assert_eq!(op, "create_image");
908 assert_eq!(payload.handle, "test_img");
909 assert_eq!(payload.pixels, Some(pixel_bytes));
910 assert_eq!(payload.width, Some(1));
911 assert_eq!(payload.height, Some(2));
912 assert!(payload.data.is_none());
913 }
914 other => panic!("expected ImageOp, got {other:?}"),
915 }
916 }
917
918 #[test]
919 fn msgpack_image_op_with_base64_string() {
920 use base64::Engine as _;
922 use plushie_core::protocol::IncomingMessage;
923
924 let pixel_bytes: Vec<u8> = vec![255, 0, 0, 255];
925 let b64 = base64::engine::general_purpose::STANDARD.encode(&pixel_bytes);
926
927 let json_msg = json!({
928 "type": "image_op",
929 "op": "create_image",
930 "payload": {
931 "handle": "test_img",
932 "pixels": b64,
933 "width": 1,
934 "height": 1
935 }
936 });
937 let json_str = serde_json::to_string(&json_msg).unwrap();
938
939 let decoded: IncomingMessage = Codec::Json.decode(json_str.as_bytes()).unwrap();
940 match decoded {
941 IncomingMessage::ImageOp { payload, .. } => {
942 assert_eq!(payload.pixels, Some(pixel_bytes));
943 }
944 other => panic!("expected ImageOp, got {other:?}"),
945 }
946 }
947
948 #[test]
951 fn rmpv_to_json_preserves_binary_as_base64_string() {
952 use base64::Engine as _;
953
954 let binary = rmpv::Value::Binary(vec![1, 2, 3]);
955 let result = rmpv_to_json(binary).unwrap();
956 assert!(!result.is_array());
957 let encoded = result.as_str().unwrap();
958 let decoded = base64::engine::general_purpose::STANDARD
959 .decode(encoded)
960 .unwrap();
961 assert_eq!(decoded, vec![1, 2, 3]);
962 }
963
964 #[test]
965 fn rmpv_to_json_handles_nested_map() {
966 let val = rmpv::Value::Map(vec![
967 (
968 rmpv::Value::String("key".into()),
969 rmpv::Value::String("val".into()),
970 ),
971 (
972 rmpv::Value::String("num".into()),
973 rmpv::Value::Integer(42.into()),
974 ),
975 ]);
976 let result = rmpv_to_json(val).unwrap();
977 assert_eq!(result, json!({"key": "val", "num": 42}));
978 }
979
980 #[test]
981 fn rmpv_to_json_non_finite_floats_become_null() {
982 assert_eq!(
983 rmpv_to_json(rmpv::Value::F32(f32::NAN)).unwrap(),
984 json!(null)
985 );
986 assert_eq!(
987 rmpv_to_json(rmpv::Value::F64(f64::INFINITY)).unwrap(),
988 json!(null)
989 );
990 assert_eq!(
991 rmpv_to_json(rmpv::Value::F64(f64::NEG_INFINITY)).unwrap(),
992 json!(null)
993 );
994 }
995
996 #[test]
999 fn detect_json_from_brace() {
1000 assert_eq!(Codec::detect_from_first_byte(b'{'), Codec::Json);
1001 }
1002
1003 #[test]
1004 fn detect_msgpack_from_zero() {
1005 assert_eq!(Codec::detect_from_first_byte(0x00), Codec::MsgPack);
1006 }
1007
1008 #[test]
1009 fn detect_msgpack_from_fixmap() {
1010 assert_eq!(Codec::detect_from_first_byte(0x85), Codec::MsgPack);
1011 }
1012
1013 #[test]
1014 fn display_format() {
1015 assert_eq!(Codec::Json.to_string(), "json");
1016 assert_eq!(Codec::MsgPack.to_string(), "msgpack");
1017 }
1018
1019 #[test]
1022 fn rmpv_to_json_deeply_nested_maps() {
1023 let val = rmpv::Value::Map(vec![(
1025 rmpv::Value::String("outer".into()),
1026 rmpv::Value::Map(vec![(
1027 rmpv::Value::String("inner".into()),
1028 rmpv::Value::Map(vec![(
1029 rmpv::Value::String("deep".into()),
1030 rmpv::Value::Integer(42.into()),
1031 )]),
1032 )]),
1033 )]);
1034 let result = rmpv_to_json(val).unwrap();
1035 assert_eq!(result, json!({"outer": {"inner": {"deep": 42}}}));
1036 }
1037
1038 #[test]
1039 fn rmpv_to_json_binary_in_nested_map() {
1040 use base64::Engine as _;
1041
1042 let val = rmpv::Value::Map(vec![
1044 (
1045 rmpv::Value::String("name".into()),
1046 rmpv::Value::String("img".into()),
1047 ),
1048 (
1049 rmpv::Value::String("pixels".into()),
1050 rmpv::Value::Binary(vec![255, 128, 0, 255]),
1051 ),
1052 ]);
1053 let result = rmpv_to_json(val).unwrap();
1054 assert_eq!(result["name"], json!("img"));
1055 assert!(!result["pixels"].is_array());
1056 let encoded = result["pixels"].as_str().unwrap();
1057 let decoded = base64::engine::general_purpose::STANDARD
1058 .decode(encoded)
1059 .unwrap();
1060 assert_eq!(decoded, vec![255, 128, 0, 255]);
1061 }
1062
1063 #[test]
1064 fn msgpack_roundtrip_with_binary_field() {
1065 use base64::Engine as _;
1068 use rmpv::Value as RmpvValue;
1069
1070 let raw_bytes: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
1071 let msg = RmpvValue::Map(vec![
1072 (
1073 RmpvValue::String("type".into()),
1074 RmpvValue::String("alpha".into()),
1075 ),
1076 (
1077 RmpvValue::String("value".into()),
1078 RmpvValue::String("hello".into()),
1079 ),
1080 (
1081 RmpvValue::String("payload".into()),
1082 RmpvValue::Binary(raw_bytes.clone()),
1083 ),
1084 ]);
1085
1086 let mut buf = Vec::new();
1088 rmpv::encode::write_value(&mut buf, &msg).unwrap();
1089
1090 let rmpv_val: rmpv::Value = rmpv::decode::read_value(&mut &buf[..]).unwrap();
1092 let json_val = rmpv_to_json(rmpv_val).unwrap();
1093
1094 assert_eq!(json_val["type"], "alpha");
1096 assert_eq!(json_val["value"], "hello");
1097
1098 assert!(!json_val["payload"].is_array());
1099 let payload = json_val["payload"].as_str().unwrap();
1100 let decoded = base64::engine::general_purpose::STANDARD
1101 .decode(payload)
1102 .unwrap();
1103 assert_eq!(decoded, raw_bytes);
1104 }
1105
1106 #[test]
1107 fn rmpv_to_json_handles_nil_and_bool() {
1108 assert_eq!(rmpv_to_json(rmpv::Value::Nil).unwrap(), json!(null));
1109 assert_eq!(
1110 rmpv_to_json(rmpv::Value::Boolean(true)).unwrap(),
1111 json!(true)
1112 );
1113 assert_eq!(
1114 rmpv_to_json(rmpv::Value::Boolean(false)).unwrap(),
1115 json!(false)
1116 );
1117 }
1118
1119 #[test]
1122 fn rmpv_to_json_rejects_invalid_utf8_string() {
1123 let bytes = [0xd9, 0x03, 0xFF, 0xFE, 0xFD];
1128 let rmpv_val: rmpv::Value = rmpv::decode::read_value(&mut &bytes[..]).unwrap();
1129 let err = rmpv_to_json(rmpv_val).unwrap_err();
1130 assert!(err.contains("invalid UTF-8"), "unexpected error: {err}");
1131 }
1132
1133 #[test]
1134 fn msgpack_decode_rejects_invalid_utf8_in_type_field() {
1135 let mut bytes = vec![0x81]; bytes.extend_from_slice(&[0xa4, b't', b'y', b'p', b'e']);
1141 bytes.extend_from_slice(&[0xd9, 0x03, 0xFF, 0xFE, 0xFD]);
1143
1144 let result: Result<serde_json::Value, _> = Codec::MsgPack.decode(&bytes);
1145 let err = result.unwrap_err();
1146 assert!(
1147 err.contains("invalid UTF-8"),
1148 "expected UTF-8 diagnostic, got {err}"
1149 );
1150 assert!(
1151 !err.contains("unknown tag") && !err.contains("tag dispatch"),
1152 "expected error to surface at codec boundary, got {err}"
1153 );
1154 }
1155
1156 #[test]
1159 fn msgpack_depth_check_accepts_flat_map() {
1160 let val = json!({"a": 1, "b": "hello", "c": true});
1161 let bytes = rmp_serde::to_vec_named(&val).unwrap();
1162 assert!(check_msgpack_depth(&bytes, 128).is_ok());
1163 }
1164
1165 #[test]
1166 fn msgpack_depth_check_accepts_nested_within_limit() {
1167 let val = json!({"outer": {"middle": {"inner": 42}}});
1169 let bytes = rmp_serde::to_vec_named(&val).unwrap();
1170 assert!(check_msgpack_depth(&bytes, 3).is_ok());
1171 }
1172
1173 #[test]
1174 fn msgpack_depth_check_rejects_beyond_limit() {
1175 let val = json!({"a": {"b": {"c": 1}}});
1177 let bytes = rmp_serde::to_vec_named(&val).unwrap();
1178 assert!(check_msgpack_depth(&bytes, 2).is_err());
1179 }
1180
1181 #[test]
1182 fn msgpack_depth_check_accepts_flat_array() {
1183 let val = json!([1, 2, 3, 4, 5]);
1184 let bytes = rmp_serde::to_vec_named(&val).unwrap();
1185 assert!(check_msgpack_depth(&bytes, 1).is_ok());
1186 }
1187
1188 #[test]
1189 fn msgpack_depth_check_nested_arrays() {
1190 let val = json!([[[42]]]);
1191 let bytes = rmp_serde::to_vec_named(&val).unwrap();
1192 assert!(check_msgpack_depth(&bytes, 3).is_ok());
1193 assert!(check_msgpack_depth(&bytes, 2).is_err());
1194 }
1195
1196 #[test]
1197 fn msgpack_depth_check_mixed_containers() {
1198 let val = json!({"list": [{"nested": true}]});
1199 let bytes = rmp_serde::to_vec_named(&val).unwrap();
1200 assert!(check_msgpack_depth(&bytes, 3).is_ok());
1202 assert!(check_msgpack_depth(&bytes, 2).is_err());
1203 }
1204
1205 #[test]
1206 fn msgpack_depth_check_empty_containers() {
1207 let val = json!({"empty_map": {}, "empty_arr": []});
1208 let bytes = rmp_serde::to_vec_named(&val).unwrap();
1209 assert!(check_msgpack_depth(&bytes, 2).is_ok());
1210 }
1211
1212 #[test]
1213 fn msgpack_depth_check_sibling_arrays_dont_add_depth() {
1214 let val = json!([[1, 2], [3, 4]]);
1216 let bytes = rmp_serde::to_vec_named(&val).unwrap();
1217 assert!(check_msgpack_depth(&bytes, 2).is_ok());
1218 }
1219
1220 #[test]
1221 fn msgpack_depth_check_binary_data() {
1222 use rmpv::Value as V;
1223 let val = V::Map(vec![(
1224 V::String("data".into()),
1225 V::Binary(vec![0xDE, 0xAD]),
1226 )]);
1227 let mut bytes = Vec::new();
1228 rmpv::encode::write_value(&mut bytes, &val).unwrap();
1229 assert!(check_msgpack_depth(&bytes, 1).is_ok());
1230 }
1231
1232 #[test]
1233 fn msgpack_depth_check_deeply_nested_rejects() {
1234 use rmpv::Value as V;
1236 let depth = 200;
1237 let mut val = V::Integer(1.into());
1238 for _ in 0..depth {
1239 val = V::Map(vec![(V::String("a".into()), val)]);
1240 }
1241 let mut bytes = Vec::new();
1242 rmpv::encode::write_value(&mut bytes, &val).unwrap();
1243
1244 assert!(check_msgpack_depth(&bytes, 128).is_err());
1245 assert!(check_msgpack_depth(&bytes, 200).is_ok());
1246 }
1247
1248 #[test]
1249 fn msgpack_decode_rejects_deeply_nested() {
1250 use rmpv::Value as V;
1252 let mut val = V::Integer(1.into());
1253 for _ in 0..200 {
1254 val = V::Map(vec![(V::String("a".into()), val)]);
1255 }
1256 let mut bytes = Vec::new();
1257 rmpv::encode::write_value(&mut bytes, &val).unwrap();
1258
1259 let result: Result<serde_json::Value, _> = Codec::MsgPack.decode(&bytes);
1260 assert!(result.is_err());
1261 assert!(result.unwrap_err().contains("depth"));
1262 }
1263
1264 #[test]
1265 fn msgpack_depth_check_truncated_payload_does_not_panic() {
1266 let val = json!({"a": {"b": [1, 2, 3]}});
1270 let bytes = rmp_serde::to_vec_named(&val).unwrap();
1271 for cut in [1, 3, 5, bytes.len() / 2] {
1272 let _ = check_msgpack_depth(&bytes[..cut], 128);
1273 }
1274 assert!(check_msgpack_depth(&[0x81], 128).is_err()); assert!(check_msgpack_depth(&[0x91], 128).is_err()); assert!(check_msgpack_depth(&[0xdc], 128).is_ok()); assert!(check_msgpack_depth(&[0xde, 0x00], 128).is_ok()); }
1281
1282 #[test]
1283 fn msgpack_depth_check_empty_input() {
1284 assert!(check_msgpack_depth(&[], 128).is_ok());
1285 }
1286
1287 #[test]
1288 fn msgpack_depth_check_scalars_only() {
1289 let val = json!(42);
1291 let bytes = rmp_serde::to_vec_named(&val).unwrap();
1292 assert!(check_msgpack_depth(&bytes, 0).is_ok());
1293 }
1294
1295 #[test]
1296 fn msgpack_depth_check_rejects_forged_element_count() {
1297 let mut bytes = vec![0xdf]; bytes.extend_from_slice(&0xFFFF_FFFFu32.to_be_bytes()); bytes.extend_from_slice(&[0xa1, b'k', 0x01]); let result = check_msgpack_depth(&bytes, 128);
1305 assert!(result.is_err());
1306 assert!(result.unwrap_err().contains("elements"));
1307 }
1308
1309 #[test]
1310 fn msgpack_decode_rejects_forged_element_count() {
1311 let mut bytes = vec![0xdd]; bytes.extend_from_slice(&0x7FFF_FFFFu32.to_be_bytes()); bytes.push(0x01); let result: Result<serde_json::Value, _> = Codec::MsgPack.decode(&bytes);
1317 assert!(result.is_err());
1318 assert!(result.unwrap_err().contains("elements"));
1319 }
1320
1321 #[test]
1324 fn json_to_rmpv_scalars() {
1325 assert_eq!(json_to_rmpv(json!(null)), rmpv::Value::Nil);
1326 assert_eq!(json_to_rmpv(json!(true)), rmpv::Value::Boolean(true));
1327 assert_eq!(json_to_rmpv(json!(42)), rmpv::Value::Integer(42.into()));
1328 assert_eq!(json_to_rmpv(json!(2.5)), rmpv::Value::F64(2.5));
1329 assert_eq!(
1330 json_to_rmpv(json!("hello")),
1331 rmpv::Value::String("hello".into())
1332 );
1333 }
1334
1335 #[test]
1336 fn json_to_rmpv_nested() {
1337 let val = json!({"key": [1, "two", null]});
1338 let rmpv = json_to_rmpv(val);
1339 match rmpv {
1340 rmpv::Value::Map(entries) => {
1341 assert_eq!(entries.len(), 1);
1342 let (k, v) = &entries[0];
1343 assert_eq!(k, &rmpv::Value::String("key".into()));
1344 match v {
1345 rmpv::Value::Array(arr) => {
1346 assert_eq!(arr.len(), 3);
1347 assert_eq!(arr[0], rmpv::Value::Integer(1.into()));
1348 assert_eq!(arr[2], rmpv::Value::Nil);
1349 }
1350 other => panic!("expected array, got {other:?}"),
1351 }
1352 }
1353 other => panic!("expected map, got {other:?}"),
1354 }
1355 }
1356
1357 #[test]
1360 fn encode_binary_message_json_without_binary() {
1361 let mut map = serde_json::Map::new();
1362 map.insert("type".to_string(), json!("test"));
1363 map.insert("id".to_string(), json!("t1"));
1364
1365 let bytes = Codec::Json.encode_binary_message(map, None).unwrap();
1366 let s = std::str::from_utf8(&bytes).unwrap();
1367 assert!(s.ends_with('\n'));
1368 let parsed: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
1369 assert_eq!(parsed["type"], "test");
1370 assert_eq!(parsed["id"], "t1");
1371 assert!(parsed.get("rgba").is_none());
1372 }
1373
1374 #[test]
1375 fn encode_binary_message_json_with_binary() {
1376 use base64::Engine as _;
1377
1378 let mut map = serde_json::Map::new();
1379 map.insert("type".to_string(), json!("screenshot"));
1380 let pixel_data = vec![255u8, 0, 128, 64];
1381
1382 let bytes = Codec::Json
1383 .encode_binary_message(map, Some(("rgba", &pixel_data)))
1384 .unwrap();
1385 let parsed: serde_json::Value = serde_json::from_slice(&bytes[..bytes.len() - 1]).unwrap();
1386 let b64 = parsed["rgba"].as_str().unwrap();
1387 let decoded = base64::engine::general_purpose::STANDARD
1388 .decode(b64)
1389 .unwrap();
1390 assert_eq!(decoded, pixel_data);
1391 }
1392
1393 #[test]
1394 fn encode_binary_message_msgpack_with_binary() {
1395 let mut map = serde_json::Map::new();
1396 map.insert("type".to_string(), json!("screenshot"));
1397 map.insert("id".to_string(), json!("s1"));
1398 let pixel_data = vec![0xDE, 0xAD, 0xBE, 0xEF];
1399
1400 let bytes = Codec::MsgPack
1401 .encode_binary_message(map, Some(("rgba", &pixel_data)))
1402 .unwrap();
1403
1404 let payload = &bytes[4..];
1406 let rmpv_val: rmpv::Value = rmpv::decode::read_value(&mut &payload[..]).unwrap();
1407
1408 match rmpv_val {
1410 rmpv::Value::Map(entries) => {
1411 let rgba_entry = entries
1412 .iter()
1413 .find(|(k, _)| k == &rmpv::Value::String("rgba".into()));
1414 match rgba_entry {
1415 Some((_, rmpv::Value::Binary(data))) => {
1416 assert_eq!(data, &pixel_data);
1417 }
1418 other => panic!("expected Binary rgba field, got {other:?}"),
1419 }
1420 }
1421 other => panic!("expected Map, got {other:?}"),
1422 }
1423 }
1424
1425 #[test]
1426 fn encode_binary_message_msgpack_roundtrip_non_binary_fields() {
1427 let mut map = serde_json::Map::new();
1428 map.insert("type".to_string(), json!("test"));
1429 map.insert("count".to_string(), json!(42));
1430 map.insert("nested".to_string(), json!({"a": [1, 2]}));
1431
1432 let bytes = Codec::MsgPack.encode_binary_message(map, None).unwrap();
1433 let decoded: serde_json::Value = Codec::MsgPack.decode(&bytes[4..]).unwrap();
1434 assert_eq!(decoded["type"], "test");
1435 assert_eq!(decoded["count"], 42);
1436 assert_eq!(decoded["nested"]["a"][0], 1);
1437 }
1438
1439 #[test]
1440 fn external_msgpack_non_finite_float_decodes_to_null() {
1441 let val = rmpv::Value::Map(vec![
1442 (
1443 rmpv::Value::String("nan".into()),
1444 rmpv::Value::F64(f64::NAN),
1445 ),
1446 (
1447 rmpv::Value::String("pos_inf".into()),
1448 rmpv::Value::F64(f64::INFINITY),
1449 ),
1450 (
1451 rmpv::Value::String("neg_inf".into()),
1452 rmpv::Value::F64(f64::NEG_INFINITY),
1453 ),
1454 ]);
1455 let mut bytes = Vec::new();
1456 rmpv::encode::write_value(&mut bytes, &val).unwrap();
1457
1458 let decoded: serde_json::Value = Codec::MsgPack.decode(&bytes).unwrap();
1459 assert_eq!(
1460 decoded,
1461 json!({
1462 "nan": null,
1463 "pos_inf": null,
1464 "neg_inf": null
1465 })
1466 );
1467 }
1468
1469 mod op_roundtrip {
1477 use super::*;
1478 use plushie_core::outgoing_message::OutgoingMessage;
1479 use plushie_core::protocol::IncomingMessage;
1480 use std::io::Cursor;
1481
1482 fn roundtrip(codec: Codec, msg: &OutgoingMessage) -> IncomingMessage {
1483 let bytes = codec.encode(msg).expect("encode");
1487 let mut cursor = Cursor::new(&bytes);
1488 let frame = codec
1489 .read_message(&mut cursor)
1490 .expect("read_message io")
1491 .expect("frame present");
1492 codec.decode::<IncomingMessage>(&frame).expect("decode")
1493 }
1494
1495 fn roundtrip_both(msg: OutgoingMessage) -> (IncomingMessage, IncomingMessage) {
1496 (
1497 roundtrip(Codec::Json, &msg),
1498 roundtrip(Codec::MsgPack, &msg),
1499 )
1500 }
1501
1502 #[test]
1503 fn widget_op_roundtrip() {
1504 let out = OutgoingMessage::WidgetOp {
1505 session: "s1".into(),
1506 op: "focus".into(),
1507 payload: json!({"target": "btn1"}),
1508 };
1509 let (j, m) = roundtrip_both(out);
1510 match j {
1511 IncomingMessage::WidgetOp { op, payload } => {
1512 assert_eq!(op, "focus");
1513 assert_eq!(payload["target"], "btn1");
1514 }
1515 other => panic!("expected WidgetOp, got {other:?}"),
1516 }
1517 assert!(matches!(m, IncomingMessage::WidgetOp { .. }));
1518 }
1519
1520 #[test]
1521 fn window_op_roundtrip() {
1522 let out = OutgoingMessage::WindowOp {
1523 session: "s1".into(),
1524 op: "resize".into(),
1525 window_id: "main".into(),
1526 payload: json!({"width": 800, "height": 600}),
1527 };
1528 let (j, m) = roundtrip_both(out);
1529 match j {
1530 IncomingMessage::WindowOp {
1531 op,
1532 window_id,
1533 payload,
1534 } => {
1535 assert_eq!(op, "resize");
1536 assert_eq!(window_id, "main");
1537 assert_eq!(payload["width"], 800);
1538 }
1539 other => panic!("expected WindowOp, got {other:?}"),
1540 }
1541 assert!(matches!(m, IncomingMessage::WindowOp { .. }));
1542 }
1543
1544 #[test]
1545 fn system_op_roundtrip() {
1546 let out = OutgoingMessage::SystemOp {
1547 session: "s1".into(),
1548 op: "allow_automatic_tabbing".into(),
1549 payload: json!({"enabled": true}),
1550 };
1551 let (j, m) = roundtrip_both(out);
1552 match j {
1553 IncomingMessage::SystemOp { op, payload } => {
1554 assert_eq!(op, "allow_automatic_tabbing");
1555 assert_eq!(payload["enabled"], true);
1556 }
1557 other => panic!("expected SystemOp, got {other:?}"),
1558 }
1559 assert!(matches!(m, IncomingMessage::SystemOp { .. }));
1560 }
1561
1562 #[test]
1563 fn system_query_roundtrip() {
1564 let out = OutgoingMessage::SystemQuery {
1565 session: "s1".into(),
1566 op: "get_system_theme".into(),
1567 payload: json!({"tag": "theme-check"}),
1568 };
1569 let (j, m) = roundtrip_both(out);
1570 match j {
1571 IncomingMessage::SystemQuery { op, payload } => {
1572 assert_eq!(op, "get_system_theme");
1573 assert_eq!(payload["tag"], "theme-check");
1574 }
1575 other => panic!("expected SystemQuery, got {other:?}"),
1576 }
1577 assert!(matches!(m, IncomingMessage::SystemQuery { .. }));
1578 }
1579
1580 #[test]
1581 fn image_op_roundtrip() {
1582 let out = OutgoingMessage::ImageOp {
1583 session: "s1".into(),
1584 op: "delete".into(),
1585 payload: json!({"handle": "sprite"}),
1586 };
1587 let (j, m) = roundtrip_both(out);
1588 match j {
1589 IncomingMessage::ImageOp { op, payload } => {
1590 assert_eq!(op, "delete");
1591 assert_eq!(payload.handle, "sprite");
1592 }
1593 other => panic!("expected ImageOp, got {other:?}"),
1594 }
1595 assert!(matches!(m, IncomingMessage::ImageOp { .. }));
1596 }
1597 }
1598
1599 mod proptest_codec {
1602 use super::*;
1603 use proptest::prelude::*;
1604
1605 fn arb_json_value() -> impl Strategy<Value = serde_json::Value> {
1611 let leaf = prop_oneof![
1612 Just(serde_json::Value::Null),
1613 any::<bool>().prop_map(serde_json::Value::Bool),
1614 any::<i64>().prop_map(|n| serde_json::Value::Number(n.into())),
1615 "[a-zA-Z0-9_ ]{0,20}".prop_map(serde_json::Value::String),
1616 ];
1617
1618 leaf.prop_recursive(
1619 3, 32, 8, |inner| {
1623 prop_oneof![
1624 prop::collection::vec(inner.clone(), 0..5)
1625 .prop_map(serde_json::Value::Array),
1626 prop::collection::vec(("[a-z_]{1,8}", inner), 0..5).prop_map(|pairs| {
1627 serde_json::Value::Object(pairs.into_iter().collect())
1628 }),
1629 ]
1630 },
1631 )
1632 }
1633
1634 proptest! {
1635 #[test]
1636 fn json_encode_decode_roundtrip(val in arb_json_value()) {
1637 let bytes = Codec::Json.encode(&val).unwrap();
1638 let decoded: serde_json::Value =
1639 Codec::Json.decode(&bytes[..bytes.len() - 1]).unwrap();
1640 prop_assert_eq!(decoded, val);
1641 }
1642
1643 #[test]
1647 fn msgpack_encode_decode_roundtrip(val in arb_json_value()) {
1648 let bytes = Codec::MsgPack.encode(&val).unwrap();
1649 prop_assert!(bytes.len() >= 4, "encoded frame must include length prefix");
1650 let payload = &bytes[4..];
1651 let decoded: serde_json::Value =
1652 Codec::MsgPack.decode(payload).unwrap();
1653 prop_assert_eq!(decoded, val);
1654 }
1655
1656 #[test]
1670 fn msgpack_decode_random_bytes_never_panics(
1671 bytes in proptest::collection::vec(any::<u8>(), 0..256),
1672 ) {
1673 let result = Codec::MsgPack.decode::<serde_json::Value>(&bytes);
1674 let _ = result;
1678 }
1679
1680 #[test]
1690 fn from_value_into_incoming_message_never_panics(
1691 val in arb_json_value(),
1692 ) {
1693 let result = serde_json::from_value::<plushie_core::protocol::IncomingMessage>(val);
1694 let _ = result;
1695 }
1696
1697 #[test]
1703 fn image_op_with_arbitrary_payload_never_panics(
1704 op in "[a-z_]{1,32}",
1705 payload in arb_json_value(),
1706 ) {
1707 let envelope = serde_json::json!({
1708 "type": "image_op",
1709 "op": op,
1710 "payload": payload,
1711 });
1712 let _ = serde_json::from_value::<plushie_core::protocol::IncomingMessage>(envelope);
1713 }
1714 }
1715 }
1716}