Skip to main content

rtmp_runtime/
amf0.rs

1//! AMF0 (Action Message Format 0) value encoding/decoding, used by RTMP
2//! command and data messages.
3//!
4//! See [`docs/rtmp.md`](../docs/rtmp.md) §8 (AMF0) for the full transcription
5//! of `[AMF0]` (the companion spec this doc cites): §8.1 for the type marker
6//! table, §8.2 for the wire encoding of each data type needed by RTMP command
7//! messages.
8//!
9//! RTMP does not wrap command/data message bodies in AMF0's own
10//! `amf-packet` framing — a message body is simply a sequence of AMF0
11//! `value-type`s (marker + body) concatenated one after another (§8.2). This
12//! module implements that sequence-of-values contract via [`Amf0Value`]
13//! (single value parse/serialize) and [`Command`] (the `name` +
14//! `transaction_id` + `arguments` sequence carried by Command Messages,
15//! §7.1.1).
16//!
17//! # Scope
18//!
19//! Implements the markers actually needed to decode/encode the ingest
20//! command set (`connect`/`createStream`/`publish`/`_result`/`onStatus`/…):
21//! Number, Boolean, String, Object, Null, Undefined, ECMA Array, Strict
22//! Array, plus Date and Long String (trivial once String/Number exist).
23//!
24//! Out of scope, and rejected as [`RtmpError::Unsupported`] rather than
25//! panicking or silently misparsing: `movieclip-marker` (`0x04`, reserved),
26//! `reference-marker` (`0x07`), `unsupported-marker` (`0x0D`),
27//! `recordset-marker` (`0x0E`, reserved), `xml-document-marker` (`0x0F`),
28//! `typed-object-marker` (`0x10`), and `avmplus-object-marker` (`0x11`, the
29//! AMF3 switch — AMF3 itself, `[AMF3]`, is a separate spec and explicitly
30//! out of scope for this ingest engine per docs/rtmp.md §8.3).
31//!
32//! [`Amf0Value`] is a data-carrying ADT (like `Fmt`/`MessageHeader`'s payload
33//! variants), not a closed label enum, so it is a `#204` `label_coverage`
34//! SKIP-list candidate rather than a `name()`/`impl_spec_display!` target
35//! (tracked for the crate's Task 10 label-coverage pass).
36
37use broadcast_common::{Parse, Serialize};
38
39use crate::RtmpError;
40
41type Result<T> = core::result::Result<T, RtmpError>;
42
43/// AMF0 type markers (`[AMF0]` §2.1, docs/rtmp.md §8.1) — 1 byte.
44pub mod marker {
45    /// Number (§2.2): `DOUBLE`, 8-byte big-endian IEEE-754.
46    pub const NUMBER: u8 = 0x00;
47    /// Boolean (§2.3): 1 byte, `0` = false, nonzero = true.
48    pub const BOOLEAN: u8 = 0x01;
49    /// String (§2.4): `U16` length + UTF-8 bytes.
50    pub const STRING: u8 = 0x02;
51    /// Object (§2.5): key/value pairs terminated by [`OBJECT_END`].
52    pub const OBJECT: u8 = 0x03;
53    /// null (§2.7): no payload.
54    pub const NULL: u8 = 0x05;
55    /// undefined (§2.8): no payload.
56    pub const UNDEFINED: u8 = 0x06;
57    /// ECMA Array (§2.10): `U32` associative-count + key/value pairs
58    /// terminated by [`OBJECT_END`].
59    pub const ECMA_ARRAY: u8 = 0x08;
60    /// Object End (§2.11): always preceded by an empty (`U16` = 0) key — the
61    /// 3-byte sequence `00 00 09`.
62    pub const OBJECT_END: u8 = 0x09;
63    /// Strict Array (§2.12): `U32` count + that many values, ordinal only.
64    pub const STRICT_ARRAY: u8 = 0x0A;
65    /// Date (§2.13): `DOUBLE` (ms since Unix epoch, UTC) + reserved `S16`
66    /// time zone (MUST be `0x0000`).
67    pub const DATE: u8 = 0x0B;
68    /// Long String (§2.14): `U32` length + UTF-8 bytes, for strings over
69    /// 65535 bytes.
70    pub const LONG_STRING: u8 = 0x0C;
71}
72
73/// Maximum AMF0 container nesting depth (Object/ECMA Array/Strict Array)
74/// [`Amf0Value::parse`] will descend into. Bounds recursion so a
75/// pathologically nested input returns [`RtmpError::Unsupported`] instead of
76/// overflowing the native call stack — the guard is checked on *entering*
77/// each nested container, so recursion never actually reaches an
78/// attacker-chosen depth, only this constant.
79pub const MAX_AMF0_DEPTH: usize = 32;
80
81const MARKER_LEN: usize = 1;
82const NUMBER_LEN: usize = 8;
83const BOOLEAN_LEN: usize = 1;
84const U16_LEN: usize = 2;
85const U32_LEN: usize = 4;
86const DATE_RESERVED_LEN: usize = 2;
87/// `00 00 09`: empty key + [`marker::OBJECT_END`].
88const OBJECT_END_LEN: usize = 3;
89
90/// A single AMF0 value (`[AMF0]` §2, docs/rtmp.md §8.2).
91///
92/// AMF3 (`avmplus-object-marker`, `0x11`) and the reserved/legacy markers
93/// (`movieclip`, `reference`, `unsupported`, `recordset`, `xml-document`,
94/// `typed-object`) are out of scope — see the module doc.
95///
96/// `#[non_exhaustive]`: this models a documented subset of `[AMF0]` §2's
97/// value types (see the module doc's Scope section); a future release may
98/// add a variant for one of the currently-`Unsupported` markers (e.g. a
99/// typed AMF3 bridge) without that being a breaking change for existing
100/// `match` callers.
101#[non_exhaustive]
102#[derive(Debug, Clone, PartialEq)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
104pub enum Amf0Value {
105    /// Number (§2.2).
106    Number(f64),
107    /// Boolean (§2.3).
108    Boolean(bool),
109    /// String (§2.4): `U16`-length UTF-8, at most 65535 bytes.
110    String(String),
111    /// Object (§2.5): ordered key/value pairs.
112    Object(Vec<(String, Amf0Value)>),
113    /// null (§2.7).
114    Null,
115    /// undefined (§2.8).
116    Undefined,
117    /// ECMA Array (§2.10): an associative array, encoded like Object plus a
118    /// leading (informational) `U32` count.
119    EcmaArray(Vec<(String, Amf0Value)>),
120    /// Strict Array (§2.12): an ordinal array of values.
121    StrictArray(Vec<Amf0Value>),
122    /// Date (§2.13): milliseconds since the Unix epoch, UTC.
123    Date(f64),
124    /// Long String (§2.14): `U32`-length UTF-8, for strings over 65535
125    /// bytes.
126    LongString(String),
127}
128
129fn buffer_too_short(need: usize, have: usize, what: &'static str) -> RtmpError {
130    RtmpError::BufferTooShort { need, have, what }
131}
132
133/// Read a `U16`-length-prefixed UTF-8 string from the front of `bytes`.
134/// Returns the decoded string and the total bytes consumed (`2 + len`).
135/// Used for both the String value body and Object/ECMA-Array keys, which
136/// share this exact encoding (§2.4 / §2.5).
137fn read_utf8_short(bytes: &[u8], what: &'static str) -> Result<(String, usize)> {
138    if bytes.len() < U16_LEN {
139        return Err(buffer_too_short(U16_LEN, bytes.len(), what));
140    }
141    let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
142    // `len` is at most `u16::MAX`, so `U16_LEN + len` cannot actually
143    // overflow `usize` on any real target, but guard it anyway (rather than
144    // a bare `+`) so this stays correct even on a hypothetical narrow
145    // `usize` platform, and matches the same guard on `read_utf8_long`
146    // below where the addend genuinely can overflow.
147    let total = U16_LEN
148        .checked_add(len)
149        .ok_or(RtmpError::Malformed { what })?;
150    if bytes.len() < total {
151        return Err(buffer_too_short(total, bytes.len(), what));
152    }
153    let s = String::from_utf8(bytes[U16_LEN..total].to_vec())
154        .map_err(|_| RtmpError::Malformed { what })?;
155    Ok((s, total))
156}
157
158/// Read a `U32`-length-prefixed UTF-8 string (Long String, §2.14).
159fn read_utf8_long(bytes: &[u8], what: &'static str) -> Result<(String, usize)> {
160    if bytes.len() < U32_LEN {
161        return Err(buffer_too_short(U32_LEN, bytes.len(), what));
162    }
163    let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
164    // `len` can be up to `u32::MAX`: on a 32-bit target, `U32_LEN + len`
165    // wraps `usize` (e.g. `len == usize::MAX - 1`), which would make
166    // `bytes.len() < total` compare against a garbage-small wrapped value
167    // and misparse a length claim that should instead be rejected as too
168    // short. `checked_add` catches that instead of wrapping.
169    let total = U32_LEN
170        .checked_add(len)
171        .ok_or(RtmpError::Malformed { what })?;
172    if bytes.len() < total {
173        return Err(buffer_too_short(total, bytes.len(), what));
174    }
175    let s = String::from_utf8(bytes[U32_LEN..total].to_vec())
176        .map_err(|_| RtmpError::Malformed { what })?;
177    Ok((s, total))
178}
179
180/// Parse the key/value pairs of an Object or ECMA Array body (§2.5/§2.10),
181/// starting right after any leading count field. Terminates on an empty key
182/// followed by [`marker::OBJECT_END`] (§2.11). `depth` is the nesting depth
183/// of the *values* in this container (already incremented past the
184/// container itself by the caller).
185fn parse_pairs(bytes: &[u8], depth: usize) -> Result<(Vec<(String, Amf0Value)>, usize)> {
186    let mut consumed = 0;
187    let mut pairs = Vec::new();
188    loop {
189        let (key, key_len) = read_utf8_short(&bytes[consumed..], "amf0 object key")?;
190        let after_key = consumed + key_len;
191        if key.is_empty() {
192            if bytes.len() < after_key + MARKER_LEN {
193                return Err(buffer_too_short(
194                    after_key + MARKER_LEN,
195                    bytes.len(),
196                    "amf0 object-end marker",
197                ));
198            }
199            if bytes[after_key] == marker::OBJECT_END {
200                return Ok((pairs, after_key + MARKER_LEN));
201            }
202        }
203        let value = parse_value(&bytes[after_key..], depth)?;
204        let value_len = value.serialized_len();
205        pairs.push((key, value));
206        consumed = after_key + value_len;
207    }
208}
209
210/// Parse one AMF0 value (marker + body) from the front of `bytes`, ignoring
211/// any surplus trailing bytes. `depth` counts container nesting already
212/// entered (0 at the top level); checked *before* descending into a nested
213/// Object/ECMA-Array/Strict-Array so recursion is bounded by
214/// [`MAX_AMF0_DEPTH`] regardless of how deeply the input is (adversarially)
215/// nested.
216fn parse_value(bytes: &[u8], depth: usize) -> Result<Amf0Value> {
217    if bytes.is_empty() {
218        return Err(buffer_too_short(MARKER_LEN, 0, "amf0 value marker"));
219    }
220    let body = &bytes[MARKER_LEN..];
221    match bytes[0] {
222        marker::NUMBER => {
223            if body.len() < NUMBER_LEN {
224                return Err(buffer_too_short(NUMBER_LEN, body.len(), "amf0 number"));
225            }
226            let mut b = [0u8; NUMBER_LEN];
227            b.copy_from_slice(&body[..NUMBER_LEN]);
228            Ok(Amf0Value::Number(f64::from_be_bytes(b)))
229        }
230        marker::BOOLEAN => {
231            if body.is_empty() {
232                return Err(buffer_too_short(BOOLEAN_LEN, 0, "amf0 boolean"));
233            }
234            Ok(Amf0Value::Boolean(body[0] != 0))
235        }
236        marker::STRING => {
237            let (s, _) = read_utf8_short(body, "amf0 string")?;
238            Ok(Amf0Value::String(s))
239        }
240        marker::OBJECT => {
241            if depth >= MAX_AMF0_DEPTH {
242                return Err(RtmpError::Unsupported {
243                    what: "amf0 nesting depth exceeded",
244                });
245            }
246            let (pairs, _) = parse_pairs(body, depth + 1)?;
247            Ok(Amf0Value::Object(pairs))
248        }
249        marker::NULL => Ok(Amf0Value::Null),
250        marker::UNDEFINED => Ok(Amf0Value::Undefined),
251        marker::ECMA_ARRAY => {
252            if depth >= MAX_AMF0_DEPTH {
253                return Err(RtmpError::Unsupported {
254                    what: "amf0 nesting depth exceeded",
255                });
256            }
257            if body.len() < U32_LEN {
258                return Err(buffer_too_short(
259                    U32_LEN,
260                    body.len(),
261                    "amf0 ecma array count",
262                ));
263            }
264            // The associative-count is informational only (§2.10); the
265            // object-end terminator is authoritative, so it is read and
266            // discarded rather than cross-checked against the parsed pair
267            // count.
268            let (pairs, _) = parse_pairs(&body[U32_LEN..], depth + 1)?;
269            Ok(Amf0Value::EcmaArray(pairs))
270        }
271        marker::STRICT_ARRAY => {
272            if depth >= MAX_AMF0_DEPTH {
273                return Err(RtmpError::Unsupported {
274                    what: "amf0 nesting depth exceeded",
275                });
276            }
277            if body.len() < U32_LEN {
278                return Err(buffer_too_short(
279                    U32_LEN,
280                    body.len(),
281                    "amf0 strict array count",
282                ));
283            }
284            let count = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
285            let mut rest = &body[U32_LEN..];
286            let mut values = Vec::new();
287            for _ in 0..count {
288                let value = parse_value(rest, depth + 1)?;
289                let consumed = value.serialized_len();
290                values.push(value);
291                rest = &rest[consumed..];
292            }
293            Ok(Amf0Value::StrictArray(values))
294        }
295        marker::DATE => {
296            if body.len() < NUMBER_LEN + DATE_RESERVED_LEN {
297                return Err(buffer_too_short(
298                    NUMBER_LEN + DATE_RESERVED_LEN,
299                    body.len(),
300                    "amf0 date",
301                ));
302            }
303            let mut b = [0u8; NUMBER_LEN];
304            b.copy_from_slice(&body[..NUMBER_LEN]);
305            let tz = u16::from_be_bytes([body[NUMBER_LEN], body[NUMBER_LEN + 1]]);
306            if tz != 0 {
307                return Err(RtmpError::Malformed {
308                    what: "amf0 date reserved time zone (must be 0x0000)",
309                });
310            }
311            Ok(Amf0Value::Date(f64::from_be_bytes(b)))
312        }
313        marker::LONG_STRING => {
314            let (s, _) = read_utf8_long(body, "amf0 long string")?;
315            Ok(Amf0Value::LongString(s))
316        }
317        _ => Err(RtmpError::Unsupported {
318            what: "amf0 value marker (reserved, legacy, or amf3-switch)",
319        }),
320    }
321}
322
323fn pairs_body_len(pairs: &[(String, Amf0Value)]) -> usize {
324    pairs
325        .iter()
326        .map(|(k, v)| U16_LEN + k.len() + v.serialized_len())
327        .sum::<usize>()
328        + OBJECT_END_LEN
329}
330
331fn write_pairs(pairs: &[(String, Amf0Value)], buf: &mut [u8]) -> Result<usize> {
332    let mut offset = 0;
333    for (k, v) in pairs {
334        let key_total = U16_LEN + k.len();
335        if buf.len() < offset + key_total {
336            return Err(buffer_too_short(
337                offset + key_total,
338                buf.len(),
339                "amf0 object key output",
340            ));
341        }
342        buf[offset..offset + U16_LEN].copy_from_slice(&(k.len() as u16).to_be_bytes());
343        buf[offset + U16_LEN..offset + key_total].copy_from_slice(k.as_bytes());
344        offset += key_total;
345        offset += v.serialize_into(&mut buf[offset..])?;
346    }
347    if buf.len() < offset + OBJECT_END_LEN {
348        return Err(buffer_too_short(
349            offset + OBJECT_END_LEN,
350            buf.len(),
351            "amf0 object-end output",
352        ));
353    }
354    buf[offset] = 0;
355    buf[offset + 1] = 0;
356    buf[offset + 2] = marker::OBJECT_END;
357    Ok(offset + OBJECT_END_LEN)
358}
359
360impl<'a> Parse<'a> for Amf0Value {
361    type Error = RtmpError;
362
363    fn parse(bytes: &'a [u8]) -> Result<Self> {
364        parse_value(bytes, 0)
365    }
366}
367
368impl Serialize for Amf0Value {
369    type Error = RtmpError;
370
371    fn serialized_len(&self) -> usize {
372        MARKER_LEN
373            + match self {
374                Amf0Value::Number(_) | Amf0Value::Date(_) => NUMBER_LEN,
375                Amf0Value::Boolean(_) => BOOLEAN_LEN,
376                Amf0Value::String(s) => U16_LEN + s.len(),
377                Amf0Value::LongString(s) => U32_LEN + s.len(),
378                Amf0Value::Object(pairs) => pairs_body_len(pairs),
379                Amf0Value::Null | Amf0Value::Undefined => 0,
380                Amf0Value::EcmaArray(pairs) => U32_LEN + pairs_body_len(pairs),
381                Amf0Value::StrictArray(values) => {
382                    U32_LEN + values.iter().map(Serialize::serialized_len).sum::<usize>()
383                }
384            }
385            + match self {
386                Amf0Value::Date(_) => DATE_RESERVED_LEN,
387                _ => 0,
388            }
389    }
390
391    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
392        let written = self.serialized_len();
393        if buf.len() < written {
394            return Err(buffer_too_short(written, buf.len(), "amf0 value output"));
395        }
396        let (marker_byte, body) = buf[..written].split_at_mut(MARKER_LEN);
397        match self {
398            Amf0Value::Number(v) => {
399                marker_byte[0] = marker::NUMBER;
400                body[..NUMBER_LEN].copy_from_slice(&v.to_be_bytes());
401            }
402            Amf0Value::Boolean(v) => {
403                marker_byte[0] = marker::BOOLEAN;
404                body[0] = u8::from(*v);
405            }
406            Amf0Value::String(s) => {
407                if s.len() > usize::from(u16::MAX) {
408                    return Err(RtmpError::Unsupported {
409                        what: "amf0 string exceeds u16 length (use long string)",
410                    });
411                }
412                marker_byte[0] = marker::STRING;
413                body[..U16_LEN].copy_from_slice(&(s.len() as u16).to_be_bytes());
414                body[U16_LEN..].copy_from_slice(s.as_bytes());
415            }
416            Amf0Value::LongString(s) => {
417                marker_byte[0] = marker::LONG_STRING;
418                body[..U32_LEN].copy_from_slice(&(s.len() as u32).to_be_bytes());
419                body[U32_LEN..].copy_from_slice(s.as_bytes());
420            }
421            Amf0Value::Object(pairs) => {
422                marker_byte[0] = marker::OBJECT;
423                write_pairs(pairs, body)?;
424            }
425            Amf0Value::Null => marker_byte[0] = marker::NULL,
426            Amf0Value::Undefined => marker_byte[0] = marker::UNDEFINED,
427            Amf0Value::EcmaArray(pairs) => {
428                marker_byte[0] = marker::ECMA_ARRAY;
429                body[..U32_LEN].copy_from_slice(&(pairs.len() as u32).to_be_bytes());
430                write_pairs(pairs, &mut body[U32_LEN..])?;
431            }
432            Amf0Value::StrictArray(values) => {
433                marker_byte[0] = marker::STRICT_ARRAY;
434                body[..U32_LEN].copy_from_slice(&(values.len() as u32).to_be_bytes());
435                let mut offset = U32_LEN;
436                for v in values {
437                    offset += v.serialize_into(&mut body[offset..])?;
438                }
439            }
440            Amf0Value::Date(v) => {
441                marker_byte[0] = marker::DATE;
442                body[..NUMBER_LEN].copy_from_slice(&v.to_be_bytes());
443                body[NUMBER_LEN..NUMBER_LEN + DATE_RESERVED_LEN].copy_from_slice(&[0, 0]);
444            }
445        }
446        Ok(written)
447    }
448}
449
450/// An RTMP Command Message body (§7.1.1): `name` (AMF0 String) + AMF0
451/// `transaction_id` (AMF0 Number) + zero or more argument values (typically
452/// a Command Object, then optional trailing arguments) — see docs/rtmp.md
453/// §8.2's closing note and §6.1 (Command Message).
454///
455/// This is the message-body-level container: unlike [`Amf0Value`] it is not
456/// itself a single AMF0 `value-type`, so it uses inherent `parse`/`to_body`
457/// methods rather than the [`Parse`]/[`Serialize`] traits.
458#[derive(Debug, Clone, PartialEq)]
459#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
460pub struct Command {
461    /// The command name, e.g. `"connect"`, `"createStream"`, `"publish"`,
462    /// `"_result"`.
463    pub name: String,
464    /// The transaction id correlating a response to its request (`0` for
465    /// commands that expect no response, e.g. `onStatus`).
466    pub transaction_id: f64,
467    /// The remaining AMF0 values in the command body, in wire order
468    /// (conventionally: Command Object, then any further arguments).
469    pub arguments: Vec<Amf0Value>,
470}
471
472impl Command {
473    /// Parse a Command Message body: AMF0 String (`name`) + AMF0 Number
474    /// (`transaction_id`) + the remaining AMF0 values (`arguments`), read
475    /// until `payload` is exhausted.
476    ///
477    /// # Errors
478    /// [`RtmpError::Malformed`] if the first value is not a String or the
479    /// second is not a Number; [`RtmpError::BufferTooShort`] /
480    /// [`RtmpError::Unsupported`] propagated from [`Amf0Value::parse`].
481    pub fn parse(payload: &[u8]) -> Result<Self> {
482        let name_value = Amf0Value::parse(payload)?;
483        let mut offset = name_value.serialized_len();
484        let name = match name_value {
485            Amf0Value::String(s) => s,
486            _ => {
487                return Err(RtmpError::Malformed {
488                    what: "rtmp command name (expected amf0 string)",
489                });
490            }
491        };
492
493        let txn_value = Amf0Value::parse(&payload[offset..])?;
494        offset += txn_value.serialized_len();
495        let transaction_id = match txn_value {
496            Amf0Value::Number(n) => n,
497            _ => {
498                return Err(RtmpError::Malformed {
499                    what: "rtmp command transaction id (expected amf0 number)",
500                });
501            }
502        };
503
504        let mut arguments = Vec::new();
505        while offset < payload.len() {
506            let value = Amf0Value::parse(&payload[offset..])?;
507            offset += value.serialized_len();
508            arguments.push(value);
509        }
510
511        Ok(Command {
512            name,
513            transaction_id,
514            arguments,
515        })
516    }
517
518    /// Serialize this command back to an AMF0 command payload: `name` +
519    /// `transaction_id` + `arguments`, in that order.
520    #[must_use]
521    pub fn to_body(&self) -> Vec<u8> {
522        let mut out = Amf0Value::String(self.name.clone()).to_bytes();
523        out.extend(Amf0Value::Number(self.transaction_id).to_bytes());
524        for arg in &self.arguments {
525            out.extend(arg.to_bytes());
526        }
527        out
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    fn round_trip(v: &Amf0Value) {
536        let bytes = v.to_bytes();
537        assert_eq!(bytes.len(), v.serialized_len());
538        let parsed = Amf0Value::parse(&bytes).expect("parse");
539        assert_eq!(&parsed, v);
540        // parse -> serialize -> byte-identical
541        assert_eq!(parsed.to_bytes(), bytes);
542    }
543
544    #[test]
545    fn number_round_trips() {
546        round_trip(&Amf0Value::Number(0.0));
547        round_trip(&Amf0Value::Number(-1.5));
548        round_trip(&Amf0Value::Number(1_000_000.25));
549    }
550
551    #[test]
552    fn boolean_round_trips() {
553        round_trip(&Amf0Value::Boolean(true));
554        round_trip(&Amf0Value::Boolean(false));
555    }
556
557    #[test]
558    fn string_round_trips_including_empty_and_multibyte() {
559        round_trip(&Amf0Value::String(String::new()));
560        round_trip(&Amf0Value::String("live".to_string()));
561        round_trip(&Amf0Value::String("héllo wörld 日本語".to_string()));
562    }
563
564    #[test]
565    fn null_and_undefined_round_trip() {
566        round_trip(&Amf0Value::Null);
567        round_trip(&Amf0Value::Undefined);
568    }
569
570    #[test]
571    fn date_round_trips() {
572        round_trip(&Amf0Value::Date(1_700_000_000_000.0));
573    }
574
575    #[test]
576    fn long_string_round_trips() {
577        round_trip(&Amf0Value::LongString("x".repeat(70_000)));
578    }
579
580    #[test]
581    fn object_round_trips_including_nested_object() {
582        round_trip(&Amf0Value::Object(vec![]));
583        round_trip(&Amf0Value::Object(vec![
584            ("app".to_string(), Amf0Value::String("live".to_string())),
585            ("audioSampleRate".to_string(), Amf0Value::Number(44100.0)),
586            ("live".to_string(), Amf0Value::Boolean(true)),
587        ]));
588        // Nested Object (the `connect` command object has string/number/
589        // boolean fields, and encoders may nest e.g. a capabilities object).
590        round_trip(&Amf0Value::Object(vec![(
591            "capabilities".to_string(),
592            Amf0Value::Object(vec![("videoCodecs".to_string(), Amf0Value::Number(252.0))]),
593        )]));
594    }
595
596    #[test]
597    fn ecma_array_round_trips() {
598        round_trip(&Amf0Value::EcmaArray(vec![]));
599        round_trip(&Amf0Value::EcmaArray(vec![
600            ("duration".to_string(), Amf0Value::Number(0.0)),
601            ("width".to_string(), Amf0Value::Number(1920.0)),
602        ]));
603    }
604
605    #[test]
606    fn strict_array_round_trips() {
607        round_trip(&Amf0Value::StrictArray(vec![]));
608        round_trip(&Amf0Value::StrictArray(vec![
609            Amf0Value::Number(1.0),
610            Amf0Value::String("two".to_string()),
611            Amf0Value::Boolean(false),
612            Amf0Value::Object(vec![("k".to_string(), Amf0Value::Null)]),
613        ]));
614    }
615
616    #[test]
617    fn ecma_array_count_is_informational_not_cross_checked() {
618        // A real encoder writes the true pair count, but §2.10 makes the
619        // object-end terminator authoritative — a lying count must still
620        // parse correctly off the terminator, not the count field.
621        let mut bytes = vec![marker::ECMA_ARRAY];
622        bytes.extend_from_slice(&999u32.to_be_bytes()); // lying count
623        bytes.extend_from_slice(&1u16.to_be_bytes());
624        bytes.extend_from_slice(b"k");
625        bytes.push(marker::NULL);
626        bytes.extend_from_slice(&[0, 0, marker::OBJECT_END]);
627
628        let parsed = Amf0Value::parse(&bytes).expect("parse");
629        assert_eq!(
630            parsed,
631            Amf0Value::EcmaArray(vec![("k".to_string(), Amf0Value::Null)])
632        );
633    }
634
635    #[test]
636    fn depth_guard_rejects_pathological_nesting_without_stack_overflow() {
637        // Build a deeply-nested Object payload by pure byte manipulation
638        // (no recursive construction/serialization of our own types), so
639        // the test itself can never stack-overflow regardless of the depth
640        // guard's correctness.
641        let mut inner = vec![marker::NULL];
642        for _ in 0..(MAX_AMF0_DEPTH * 4) {
643            let mut wrapped = vec![marker::OBJECT];
644            wrapped.extend_from_slice(&1u16.to_be_bytes());
645            wrapped.push(b'a');
646            wrapped.extend_from_slice(&inner);
647            wrapped.extend_from_slice(&[0, 0, marker::OBJECT_END]);
648            inner = wrapped;
649        }
650
651        let result = Amf0Value::parse(&inner);
652        assert!(matches!(result, Err(RtmpError::Unsupported { .. })));
653    }
654
655    #[test]
656    fn depth_guard_allows_nesting_at_the_limit() {
657        let mut inner = vec![marker::NULL];
658        for _ in 0..(MAX_AMF0_DEPTH - 1) {
659            let mut wrapped = vec![marker::OBJECT];
660            wrapped.extend_from_slice(&1u16.to_be_bytes());
661            wrapped.push(b'a');
662            wrapped.extend_from_slice(&inner);
663            wrapped.extend_from_slice(&[0, 0, marker::OBJECT_END]);
664            inner = wrapped;
665        }
666        assert!(Amf0Value::parse(&inner).is_ok());
667    }
668
669    #[test]
670    fn dropping_object_end_marker_is_rejected() {
671        // Mutation check: an Object with its `00 00 09` terminator dropped
672        // must fail to parse (BufferTooShort), not silently succeed.
673        let full = Amf0Value::Object(vec![("k".to_string(), Amf0Value::Null)]).to_bytes();
674        let truncated = &full[..full.len() - 3];
675        assert!(Amf0Value::parse(truncated).is_err());
676    }
677
678    #[test]
679    fn mis_sized_string_length_is_rejected() {
680        // Mutation check: claiming a longer string length than the buffer
681        // actually holds must fail (BufferTooShort), not read garbage.
682        let mut bytes = vec![marker::STRING];
683        bytes.extend_from_slice(&100u16.to_be_bytes()); // claims 100 bytes
684        bytes.extend_from_slice(b"short"); // only 5 present
685        assert!(matches!(
686            Amf0Value::parse(&bytes),
687            Err(RtmpError::BufferTooShort { .. })
688        ));
689    }
690
691    #[test]
692    fn invalid_utf8_string_is_malformed() {
693        let mut bytes = vec![marker::STRING];
694        bytes.extend_from_slice(&2u16.to_be_bytes());
695        bytes.extend_from_slice(&[0xFF, 0xFE]); // invalid UTF-8
696        assert!(matches!(
697            Amf0Value::parse(&bytes),
698            Err(RtmpError::Malformed { .. })
699        ));
700    }
701
702    #[test]
703    fn unsupported_marker_is_rejected_not_panicking() {
704        assert!(matches!(
705            Amf0Value::parse(&[0x11]), // avmplus-object-marker (AMF3 switch)
706            Err(RtmpError::Unsupported { .. })
707        ));
708        assert!(matches!(
709            Amf0Value::parse(&[0x07]), // reference-marker
710            Err(RtmpError::Unsupported { .. })
711        ));
712    }
713
714    #[test]
715    fn date_rejects_nonzero_reserved_timezone() {
716        let mut bytes = vec![marker::DATE];
717        bytes.extend_from_slice(&0.0f64.to_be_bytes());
718        bytes.extend_from_slice(&1u16.to_be_bytes()); // reserved must be 0
719        assert!(matches!(
720            Amf0Value::parse(&bytes),
721            Err(RtmpError::Malformed { .. })
722        ));
723    }
724
725    #[test]
726    fn empty_buffer_and_truncated_marker_are_buffer_too_short_not_panics() {
727        assert!(matches!(
728            Amf0Value::parse(&[]),
729            Err(RtmpError::BufferTooShort { .. })
730        ));
731        assert!(matches!(
732            Amf0Value::parse(&[marker::NUMBER, 0, 0, 0]),
733            Err(RtmpError::BufferTooShort { .. })
734        ));
735    }
736
737    // ── Command ──────────────────────────────────────────────────────────
738
739    fn connect_command() -> Command {
740        Command {
741            name: "connect".to_string(),
742            transaction_id: 1.0,
743            arguments: vec![Amf0Value::Object(vec![
744                ("app".to_string(), Amf0Value::String("live".to_string())),
745                (
746                    "flashVer".to_string(),
747                    Amf0Value::String("FMLE/3.0".to_string()),
748                ),
749                (
750                    "tcUrl".to_string(),
751                    Amf0Value::String("rtmp://example.test/live".to_string()),
752                ),
753                ("fpad".to_string(), Amf0Value::Boolean(false)),
754            ])],
755        }
756    }
757
758    fn publish_command() -> Command {
759        Command {
760            name: "publish".to_string(),
761            transaction_id: 5.0,
762            arguments: vec![
763                Amf0Value::Null,
764                Amf0Value::String("stream_key_123".to_string()),
765                Amf0Value::String("live".to_string()),
766            ],
767        }
768    }
769
770    #[test]
771    fn connect_command_round_trips_byte_identically() {
772        let cmd = connect_command();
773        let bytes = cmd.to_body();
774        let parsed = Command::parse(&bytes).expect("parse connect");
775        assert_eq!(parsed, cmd);
776        assert_eq!(parsed.to_body(), bytes);
777    }
778
779    #[test]
780    fn publish_command_round_trips_byte_identically() {
781        let cmd = publish_command();
782        let bytes = cmd.to_body();
783        let parsed = Command::parse(&bytes).expect("parse publish");
784        assert_eq!(parsed, cmd);
785        assert_eq!(parsed.to_body(), bytes);
786    }
787
788    #[test]
789    fn command_name_must_be_string() {
790        let bytes = Amf0Value::Number(1.0).to_bytes();
791        assert!(matches!(
792            Command::parse(&bytes),
793            Err(RtmpError::Malformed { .. })
794        ));
795    }
796
797    #[test]
798    fn command_transaction_id_must_be_number() {
799        let mut bytes = Amf0Value::String("connect".to_string()).to_bytes();
800        bytes.extend(Amf0Value::String("not a number".to_string()).to_bytes());
801        assert!(matches!(
802            Command::parse(&bytes),
803            Err(RtmpError::Malformed { .. })
804        ));
805    }
806
807    // ── 32-bit overflow guards (mutation checks) ─────────────────────────
808
809    #[test]
810    fn long_string_length_overflowing_usize_is_rejected_not_wrapped() {
811        // Mutation check: a Long String claiming `usize::MAX - 1` bytes must
812        // be rejected as too-short/malformed, not silently wrap `U32_LEN +
813        // len` into a small `total` that then compares as if the string
814        // were actually present.
815        let mut bytes = vec![marker::LONG_STRING];
816        bytes.extend_from_slice(&(u32::MAX - 1).to_be_bytes());
817        bytes.extend_from_slice(b"short");
818        let err = Amf0Value::parse(&bytes).unwrap_err();
819        assert!(matches!(
820            err,
821            RtmpError::Malformed { .. } | RtmpError::BufferTooShort { .. }
822        ));
823    }
824
825    // ── serde (feature "serde") ───────────────────────────────────────────
826
827    #[cfg(feature = "serde")]
828    #[test]
829    fn amf0_value_and_command_serde_round_trip() {
830        let value = Amf0Value::Object(vec![
831            ("app".to_string(), Amf0Value::String("live".to_string())),
832            ("live".to_string(), Amf0Value::Boolean(true)),
833            ("duration".to_string(), Amf0Value::Number(0.0)),
834            (
835                "items".to_string(),
836                Amf0Value::StrictArray(vec![Amf0Value::Null, Amf0Value::Undefined]),
837            ),
838        ]);
839        let json = serde_json::to_string(&value).expect("serialize Amf0Value");
840        let back: Amf0Value = serde_json::from_str(&json).expect("deserialize Amf0Value");
841        assert_eq!(back, value);
842
843        let cmd = publish_command();
844        let json = serde_json::to_string(&cmd).expect("serialize Command");
845        let back: Command = serde_json::from_str(&json).expect("deserialize Command");
846        assert_eq!(back, cmd);
847    }
848}