Skip to main content

plushie_renderer_engine/
codec.rs

1//! Wire codec for the stdin/stdout protocol.
2//!
3//! The renderer communicates with the host process over stdin (incoming
4//! messages) and stdout (outgoing events). Two wire formats are supported:
5//!
6//! - **JSON** - newline-delimited JSON (JSONL). Each message is a UTF-8
7//!   JSON object terminated by `\n`. Human-readable, easy to debug.
8//!
9//! - **MsgPack** - 4-byte big-endian length-prefixed MessagePack. Each
10//!   message is `[u32 BE length][msgpack payload]`. Compact, faster to
11//!   parse, supports native binary fields (e.g. pixel data).
12//!
13//! The codec is auto-detected from the first byte of stdin (`{` = JSON,
14//! anything else = MsgPack) and threaded through call sites explicitly.
15
16use 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
23/// Maximum size for a single wire message (64 MiB). Applied to both JSON
24/// line reads and msgpack length-prefixed frames.
25pub const MAX_MESSAGE_SIZE: usize = 64 * 1024 * 1024;
26
27/// Wire codec for the stdin/stdout protocol.
28///
29/// `Json` uses newline-delimited JSON. `MsgPack` uses a 4-byte
30/// big-endian length prefix followed by a MessagePack payload.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Codec {
33    /// Newline-delimited JSON (JSONL).
34    Json,
35    /// Length-prefixed MessagePack.
36    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    /// Encode a value to wire bytes ready to write to stdout.
50    ///
51    /// - JSON: `serde_json` serialization + trailing `\n`.
52    /// - MsgPack: 4-byte BE u32 length prefix + `rmp_serde` named serialization.
53    ///
54    /// Allocates a new Vec per call. In practice, encode is called once
55    /// per outgoing message (not per render frame), and the messages are
56    /// small enough that the allocation is negligible relative to the
57    /// I/O cost. Buffer reuse would add complexity for no measurable gain.
58    ///
59    /// # Errors
60    ///
61    /// Returns an error when serialization fails or when the encoded
62    /// payload exceeds the 4 GiB msgpack frame limit.
63    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    /// Encode a JSON map with an optional binary field to wire bytes.
98    ///
99    /// For MsgPack: binary fields are encoded as native msgpack binary
100    /// (`rmpv::Value::Binary`), avoiding the ~33% size overhead of
101    /// base64. The map is built via `rmpv::Value::Map` to preserve
102    /// the binary type.
103    ///
104    /// For JSON: binary fields are base64-encoded as strings.
105    ///
106    /// Use this instead of [`encode`](Self::encode) when the message
107    /// contains raw byte data (e.g. pixel buffers) that should use
108    /// native binary encoding over msgpack.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error when JSON or msgpack serialization fails, or
113    /// when the encoded msgpack payload exceeds the 4 GiB frame limit.
114    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    /// Decode a raw payload (framing already stripped) into a typed value.
174    ///
175    /// For JSON, `bytes` is the UTF-8 JSON text (without the trailing newline).
176    /// For MsgPack, `bytes` is the raw msgpack payload (without the length prefix).
177    ///
178    /// MsgPack decoding routes through `rmpv::Value` as an intermediate. This
179    /// preserves binary data (msgpack's bin type) as base64 strings, which the
180    /// `deserialize_binary_field` custom deserializer in protocol.rs can
181    /// reconstruct into `Vec<u8>`. The `serde_json::Value` intermediate is
182    /// still needed for tag dispatch (`#[serde(tag = "type")]`) which rmp-serde
183    /// doesn't handle reliably for externally-produced msgpack.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error when the payload fails the msgpack depth check,
188    /// cannot be decoded from the selected wire format, or cannot be
189    /// deserialized into the requested target type.
190    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                // Pre-check nesting depth before rmpv deserialization.
195                // rmpv::read_value recurses without a depth limit, so a
196                // pathologically nested payload can cause stack overflow
197                // before our depth-limited rmpv_to_json conversion runs.
198                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                // Fast path: consume `json_val` directly on success so
205                // the happy path pays no clone cost. Only materialise
206                // the debug dump (and the clone needed to do so) when
207                // deserialisation fails in a debug build.
208                #[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    /// Read one framed message from a buffered reader, returning the raw payload.
231    ///
232    /// - JSON: reads until `\n`, returns the line bytes (without the newline).
233    /// - MsgPack: reads a 4-byte BE u32 length, then reads that many bytes.
234    ///
235    /// Returns `Ok(None)` on EOF (clean shutdown).
236    ///
237    /// # Errors
238    ///
239    /// Returns an I/O error when the reader fails, when a JSON line or
240    /// msgpack frame exceeds [`MAX_MESSAGE_SIZE`], or when msgpack
241    /// framing is truncated or otherwise malformed.
242    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                // Wrap in Take to bound allocation BEFORE the full line is
247                // buffered. Without this, a sender could transmit an arbitrarily
248                // long line without a newline, causing unbounded memory growth.
249                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    /// Detect codec from the first byte of input.
301    ///
302    /// `{` (0x7B) indicates JSON. Anything else indicates MsgPack (the first
303    /// byte of a 4-byte length prefix).
304    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
313// The msgpack nesting depth pre-check lives in
314// `plushie_core::codec_safety::check_msgpack_depth` so the widget-sdk codec
315// and the Rust SDK's wire bridge share one implementation. The module-level
316// `use` above pulls it in.
317
318// ---------------------------------------------------------------------------
319// rmpv::Value -> serde_json::Value conversion
320// ---------------------------------------------------------------------------
321
322/// Convert an rmpv::Value to serde_json::Value, preserving binary data as
323/// base64 strings. This is the key difference from the old rmp_serde ->
324/// serde_json::Value path, which silently dropped binary data
325/// (serde_json::Value has no binary type).
326///
327/// The `deserialize_binary_field` custom deserializer in protocol.rs knows
328/// how to reconstruct `Vec<u8>` from these strings.
329///
330/// Returns an error on invalid UTF-8 in msgpack strings: silently falling
331/// back (either to U+FFFD or an empty string) would corrupt the `type`
332/// field that tag dispatch keys off, producing a confusing downstream
333/// "unknown tag" error. Surfacing the UTF-8 failure at the codec boundary
334/// tells the host exactly where the wire payload went wrong.
335///
336/// Recursion depth is capped at `MAX_RMPV_DEPTH` to prevent stack overflow
337/// from deeply nested or malicious payloads.
338fn 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                // Fallback: shouldn't happen for msgpack integers
358                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            // rmpv::Utf8String may hold invalid UTF-8. Surface the failure
375            // so tag dispatch on the `type` field does not get handed a
376            // string of replacement characters.
377            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                // Map keys: try to use string representation. Non-UTF-8
405                // string keys surface the same error as non-UTF-8 values;
406                // the key is as load-bearing as the value, and silently
407                // dropping it (into_str().unwrap_or_default()) would
408                // merge entries under the empty key.
409                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
432/// Convert a serde_json::Value to rmpv::Value for msgpack encoding.
433/// Used by `encode_binary_message` to build rmpv maps from JSON maps.
434fn 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
461/// Recursively normalize JSON values before wire encoding.
462///
463/// Serde JSON already serializes non-finite floats as `null`; the
464/// explicit traversal here keeps JSON and msgpack encode paths aligned
465/// by feeding the same sanitized value tree into both serializers.
466fn 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
489/// Recursively normalize msgpack values before writing them to the wire.
490///
491/// Self-generated payloads should keep their existing scalar encodings, so
492/// this only rewrites non-finite floats to `nil`.
493fn 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    // -- JSON roundtrips --
549
550    #[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    // -- MsgPack roundtrips --
591
592    #[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        // First 4 bytes are length prefix
600        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        // Flatten on deserialize: encode a map with extra keys, decode into
678        // a struct with #[serde(flatten)] rest: Value.
679        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    // -- read_message --
688
689    #[test]
690    fn json_read_message_skips_blank_lines() {
691        // Blank lines between messages must be skipped, not treated as EOF.
692        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        // Trailing blank lines followed by real EOF should return None.
704        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        // Build two length-prefixed msgpack messages
726        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    // -- read_message size limit tests --
757
758    #[test]
759    fn json_read_message_rejects_oversized_line() {
760        // A line longer than MAX_MESSAGE_SIZE must be rejected.
761        // We can't allocate 64 MiB in a test, so use a smaller custom
762        // read_message-like flow. Instead, verify the Take wrapper works
763        // by constructing a line just over the limit.
764        //
765        // Since MAX_MESSAGE_SIZE is 64 MiB (too big for a unit test),
766        // we test the logic indirectly: a line of exactly MAX_MESSAGE_SIZE+1
767        // bytes (no newline) should be rejected. We use a small stand-in
768        // to verify the mechanics.
769        let small_limit = 100;
770        // Construct a line with no newline, longer than small_limit.
771        let long_line: Vec<u8> = vec![b'x'; small_limit + 10];
772        let mut reader = io::BufReader::new(&long_line[..]);
773
774        // Read using Take with the small limit (simulates what
775        // read_message does, just with a smaller limit).
776        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        // The Take capped the read, so line.len() <= small_limit + 1.
780        assert!(line.len() <= small_limit + 1);
781        // Without the Take, line.len() would be small_limit + 10.
782    }
783
784    #[test]
785    fn msgpack_read_message_rejects_oversized_frame() {
786        // Build a frame with length prefix claiming MAX_MESSAGE_SIZE + 1 bytes.
787        let len = (MAX_MESSAGE_SIZE + 1) as u32;
788        let mut data = Vec::new();
789        data.extend_from_slice(&len.to_be_bytes());
790        // Don't need the actual payload; the size check fires first.
791        data.extend_from_slice(&[0u8; 64]); // just enough to not EOF
792
793        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    // -- Cross-format: simulate external msgpack (e.g. Msgpax) --
813    //
814    // rmp-serde's own serializer produces bytes that its deserializer can
815    // roundtrip, but external msgpack producers encode maps differently.
816    // These tests build raw msgpack via serde_json::Value -> rmp_serde
817    // (which is format-agnostic, not tagged-enum-aware) to simulate what
818    // an external producer like Msgpax sends. The Codec::decode workaround
819    // (msgpack -> rmpv::Value -> serde_json::Value -> T) must handle these.
820
821    #[test]
822    fn msgpack_external_tagged_enum_alpha() {
823        // Simulate Msgpax encoding {"type": "alpha", "value": "hello"}
824        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        // This is exactly what a host sends: a plain map with "type":"settings".
846        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    // -- Binary data preservation through rmpv path --
863
864    #[test]
865    fn msgpack_image_op_with_native_binary() {
866        // Simulate what an external producer sends when using native binary fields.
867        // Build raw msgpack with a binary field using rmpv directly.
868        use rmpv::Value as RmpvValue;
869
870        let pixel_bytes: Vec<u8> = vec![255, 0, 0, 255, 0, 255, 0, 255]; // 2 RGBA pixels
871        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        // JSON mode: binary data arrives as base64-encoded string.
921        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    // -- rmpv_to_json unit tests --
949
950    #[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    // -- detect --
997
998    #[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    // -- Additional rmpv_to_json coverage --
1020
1021    #[test]
1022    fn rmpv_to_json_deeply_nested_maps() {
1023        // Nested map: {"outer": {"inner": {"deep": 42}}}
1024        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        // Binary data nested inside a map should be preserved as base64.
1043        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        // Encode a message containing binary data via msgpack, decode it,
1066        // and verify the binary field comes through as base64.
1067        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        // Encode to raw msgpack bytes.
1087        let mut buf = Vec::new();
1088        rmpv::encode::write_value(&mut buf, &msg).unwrap();
1089
1090        // The rmpv_to_json path preserves binary as base64.
1091        let rmpv_val: rmpv::Value = rmpv::decode::read_value(&mut &buf[..]).unwrap();
1092        let json_val = rmpv_to_json(rmpv_val).unwrap();
1093
1094        // The tagged enum fields decode fine.
1095        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    // -- Invalid UTF-8 handling --
1120
1121    #[test]
1122    fn rmpv_to_json_rejects_invalid_utf8_string() {
1123        // Build a msgpack payload with a str8 string holding invalid
1124        // UTF-8 bytes. Decoding through rmpv then conversion must
1125        // surface the failure, not fall back to replacement
1126        // characters.
1127        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        // Build a map with "type" key mapped to an invalid-UTF-8 value.
1136        // Decoding through Codec::decode must report the UTF-8 failure
1137        // rather than bubbling up a confusing "unknown tag" error.
1138        let mut bytes = vec![0x81]; // fixmap(1): type = <invalid>
1139        // key: fixstr(4) "type"
1140        bytes.extend_from_slice(&[0xa4, b't', b'y', b'p', b'e']);
1141        // value: str8 with len 3, bytes 0xFF 0xFE 0xFD
1142        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    // -- check_msgpack_depth --
1157
1158    #[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        // 3 levels: {"outer": {"middle": {"inner": 42}}}
1168        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        // 3 nested maps exceeds a limit of 2
1176        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        // depth: map(1) -> array(2) -> map(3) = 3 levels
1201        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        // [[1,2], [3,4]] has depth 2 (outer array -> inner array), not 3
1215        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        // Build a deeply nested msgpack: {a: {a: {a: ... {a: 1} ...}}}
1235        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        // Verify the full decode path rejects deeply nested payloads.
1251        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        // Truncated payloads must not panic. They may return Ok (for
1267        // scalars or truncated length fields) or Err (for containers
1268        // whose declared count exceeds remaining bytes).
1269        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        // Truncated containers: declared children > 0 remaining bytes
1275        assert!(check_msgpack_depth(&[0x81], 128).is_err()); // fixmap(1): 2 children, 0 bytes
1276        assert!(check_msgpack_depth(&[0x91], 128).is_err()); // fixarray(1): 1 child, 0 bytes
1277        // Truncated length fields: loop breaks before parsing children
1278        assert!(check_msgpack_depth(&[0xdc], 128).is_ok()); // array16, no length bytes
1279        assert!(check_msgpack_depth(&[0xde, 0x00], 128).is_ok()); // map16, partial length
1280    }
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        // Pure scalar value (no containers) should always pass.
1290        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        // map32 declaring 2^32-1 entries but only a few bytes of actual
1298        // data. Without the element count check, rmpv::read_value would
1299        // try Vec::with_capacity(4 billion) and OOM.
1300        let mut bytes = vec![0xdf]; // map32 marker
1301        bytes.extend_from_slice(&0xFFFF_FFFFu32.to_be_bytes()); // 4 billion entries
1302        bytes.extend_from_slice(&[0xa1, b'k', 0x01]); // one tiny key-value pair
1303
1304        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        // Verify the full decode path rejects forged counts.
1312        let mut bytes = vec![0xdd]; // array32 marker
1313        bytes.extend_from_slice(&0x7FFF_FFFFu32.to_be_bytes()); // 2 billion entries
1314        bytes.push(0x01); // one element
1315
1316        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    // -- json_to_rmpv ---------------------------------------------------------
1322
1323    #[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    // -- encode_binary_message ------------------------------------------------
1358
1359    #[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        // Strip 4-byte length prefix
1405        let payload = &bytes[4..];
1406        let rmpv_val: rmpv::Value = rmpv::decode::read_value(&mut &payload[..]).unwrap();
1407
1408        // Find the rgba field: should be native Binary, not a string
1409        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    // -- Per-variant round-trip tests ----------------------------------------
1470    //
1471    // Encode an OutgoingMessage via Codec::encode, decode it back through
1472    // Codec::read_message + Codec::decode as the renderer would, and assert
1473    // the variant shape survives framing + wire encoding. Catches schema
1474    // drift between SDK senders and renderer decoders the moment it happens.
1475
1476    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            // encode produces length-prefixed framed bytes for msgpack or a
1484            // newline-terminated line for JSON. read_message unwraps the frame
1485            // and hands us payload bytes ready for decode.
1486            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    // -- Property-based tests -------------------------------------------------
1600
1601    mod proptest_codec {
1602        use super::*;
1603        use proptest::prelude::*;
1604
1605        /// Generate arbitrary JSON values suitable for round-trip testing.
1606        ///
1607        /// Uses integers only (no floats) to avoid f64 text round-trip
1608        /// precision mismatches in serde_json::Number. Keeps nesting
1609        /// shallow to stay fast.
1610        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,  // depth
1620                32, // max nodes
1621                8,  // items per collection
1622                |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            /// MsgPack round-trip mirroring the JSON proptest. The
1644            /// encoded frame is `[u32 BE length][msgpack payload]`;
1645            /// strip the 4-byte prefix before decoding.
1646            #[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            /// Random byte sequences fed into the MsgPack decoder must
1657            /// produce a structured `Err` or a valid `Ok`, never panic.
1658            ///
1659            /// The codec sits at the wire boundary; a panic here would
1660            /// take the renderer down on malformed input. The decoder
1661            /// gates on a depth pre-check and routes through rmpv ->
1662            /// serde_json::Value -> typed value, so this test exercises
1663            /// every layer that could plausibly trip on bad bytes.
1664            ///
1665            /// `proptest::collection::vec(any::<u8>(), 0..256)` keeps
1666            /// the input bounded so the test stays fast. The decoder
1667            /// is byte-stream oblivious past 256 bytes; bigger inputs
1668            /// don't change the panic surface.
1669            #[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                // We don't care which variant of Result we get;
1675                // the contract is that the decoder always returns
1676                // a Result rather than unwinding.
1677                let _ = result;
1678            }
1679
1680            /// Random JSON values fed into the typed-message decoder
1681            /// must produce a structured `Err` (unknown variant,
1682            /// missing field, etc.) or a valid `Ok`, never panic.
1683            ///
1684            /// IncomingMessage uses `#[serde(tag = "type")]`; arbitrary
1685            /// JSON without a recognised tag should hit the "unknown
1686            /// variant" path without unwinding. Custom binary-field
1687            /// deserializers along the way must also reject malformed
1688            /// input cleanly.
1689            #[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            /// Targeted variant: well-formed JSON that's deliberately
1698            /// shaped like a recognised tag but with malformed
1699            /// payload fields. The custom binary-field deserializer
1700            /// path is the most likely place to hit panics, so
1701            /// concentrate fuzz coverage there.
1702            #[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}