Skip to main content

rtmp_runtime/
chunk.rs

1//! RTMP chunk stream — basic header, message header, extended timestamp
2//! (Adobe RTMP 1.0 §5.3).
3//!
4//! See [`docs/rtmp.md`](../docs/rtmp.md) §3 (RTMP Chunk Stream) for the wire
5//! layout: chunk format (§5.3.1), basic header (§5.3.1.1), the four message
6//! header `fmt` variants (§5.3.1.2), and extended timestamp (§5.3.1.3).
7//!
8//! This module implements the chunk **header** wire types (`BasicHeader`,
9//! `MessageHeader`) and the stateful reassembly engine built on top of them:
10//! [`ChunkAssembler`] (inbound, tracks prior-chunk state per csid so `fmt`
11//! 1/2/3 headers can inherit the fields they omit, and reassembles chunked
12//! payload back into whole [`Message`]s) and [`ChunkWriter`] (outbound,
13//! splits a [`Message`] into `fmt` 0 + 3 chunks at the configured chunk
14//! size).
15
16use std::collections::HashMap;
17
18use broadcast_common::{Parse, Serialize};
19
20use crate::RtmpError;
21
22type Result<T> = core::result::Result<T, RtmpError>;
23
24/// Default maximum chunk size (§5.3, §5.4.1): 128 bytes, in effect until a
25/// Set Chunk Size protocol control message changes it.
26pub const DEFAULT_CHUNK_SIZE: u32 = 128;
27
28/// Largest chunk size [`ChunkAssembler::set_chunk_size`]/[`ChunkWriter::set_chunk_size`]
29/// will adopt from a Set Chunk Size protocol control message (§5.4.1): 16
30/// MiB. The wire field is a 31-bit value (up to ~2 GiB), but no real
31/// publisher/player needs a chunk size anywhere near that — a single chunk
32/// this large would already hold many seconds of encoded audio/video — so
33/// this is a defensive ceiling, not a spec limit: values above it are
34/// clamped down rather than rejected, matching the existing floor-of-1
35/// behaviour for values below it.
36pub const MAX_CHUNK_SIZE: u32 = 16 * 1024 * 1024;
37
38/// Largest total `message_length` (§5.3.1.2) [`ChunkAssembler`] will begin
39/// buffering for a single reassembled message: 8 MiB. `message_length` is a
40/// fully attacker-controlled 24-bit wire field (max ~16 MiB); real RTMP
41/// audio/video/command messages are always far smaller than this (a single
42/// compressed video frame, even a keyframe, is normally well under 1 MiB),
43/// so this is a generous ceiling that still bounds worst-case allocation
44/// per in-progress message. A Type 0/1 header declaring a larger
45/// `message_length` is rejected by [`ChunkAssembler`] before any buffer for
46/// it is allocated.
47pub const MAX_MESSAGE_LEN: u32 = 8 * 1024 * 1024;
48
49/// Largest number of distinct chunk stream ids [`ChunkAssembler`] will track
50/// reassembly state for concurrently. A well-behaved publisher uses only a
51/// handful of chunk streams (2/3 for control/command traffic, plus a few
52/// more for audio/video) — this bound is generous headroom above that, not
53/// a spec limit — so a flood of chunks opening many distinct (and mostly
54/// bogus) csids is rejected rather than growing the per-csid state map
55/// without bound.
56pub const MAX_CSIDS: usize = 64;
57
58/// The 24-bit sentinel value that, in a Type 0/1/2 message header's
59/// `timestamp`/`timestamp delta` field, signals that the field does not carry
60/// the real value: the full 32-bit value instead follows in a 4-byte
61/// Extended Timestamp (§5.3.1.3). Per §5.3.1.2.1, any real timestamp/delta
62/// `>= EXTENDED_TIMESTAMP_MARKER` is encoded this way.
63pub const EXTENDED_TIMESTAMP_MARKER: u32 = 0x00FF_FFFF;
64
65/// Byte width of a 24-bit (`u24`) wire field (`timestamp`, `timestamp delta`,
66/// `message length`).
67const U24_LEN: usize = 3;
68/// Byte width of the Extended Timestamp field (§5.3.1.3).
69const EXTENDED_TIMESTAMP_LEN: usize = 4;
70
71/// Byte width of a Type 0 message header (§5.3.1.2.1), excluding any
72/// Extended Timestamp.
73const TYPE0_LEN: usize = 11;
74/// Byte width of a Type 1 message header (§5.3.1.2.2), excluding any
75/// Extended Timestamp.
76const TYPE1_LEN: usize = 7;
77/// Byte width of a Type 2 message header (§5.3.1.2.3), excluding any
78/// Extended Timestamp.
79const TYPE2_LEN: usize = 3;
80/// Byte width of a Type 3 message header (§5.3.1.2.4): always empty.
81const TYPE3_LEN: usize = 0;
82
83/// Chunk stream id (csid) offset added back on the 2-/3-byte basic header
84/// forms (§5.3.1.1): both forms carry `csid - 64`.
85const BASIC_HEADER_CSID_OFFSET: u32 = 64;
86/// The 2-byte basic header form's marker value in byte 0's low 6 bits.
87const BASIC_HEADER_2BYTE_MARKER: u8 = 0;
88/// The 3-byte basic header form's marker value in byte 0's low 6 bits.
89const BASIC_HEADER_3BYTE_MARKER: u8 = 1;
90/// Bit shift of the 2-bit `fmt` field within basic header byte 0.
91const BASIC_HEADER_FMT_SHIFT: u8 = 6;
92/// Mask for the low 6 bits of basic header byte 0 (the 1-byte csid, or the
93/// 2-/3-byte form marker).
94const BASIC_HEADER_MARKER_MASK: u8 = 0x3F;
95
96/// Smallest csid encodable in the basic header's 1-byte form (§5.3.1.1).
97/// Csid values 0 and 1 are reserved as the 2-/3-byte form markers and so can
98/// never appear as a literal 1-byte-form csid; csid 2 is additionally
99/// reserved by the spec for low-level protocol control messages/commands but
100/// remains structurally encodable.
101const BASIC_HEADER_1BYTE_MIN_CSID: u32 = 2;
102/// Largest csid encodable in the basic header's 1-byte form.
103const BASIC_HEADER_1BYTE_MAX_CSID: u32 = 63;
104/// Smallest csid encodable in the basic header's 2-byte form.
105const BASIC_HEADER_2BYTE_MIN_CSID: u32 = 64;
106/// Largest csid encodable in the basic header's 2-byte form (csids 64-319
107/// are also representable in the 3-byte form; 2-byte is the minimal one).
108const BASIC_HEADER_2BYTE_MAX_CSID: u32 = 319;
109/// Smallest csid that requires the basic header's 3-byte form.
110const BASIC_HEADER_3BYTE_MIN_CSID: u32 = 320;
111/// Largest csid the protocol supports at all (§5.3.1.1: "up to 65597 chunk
112/// streams, IDs 3-65599" — the 3-byte form's 16-bit `csid - 64` field tops
113/// out here).
114const BASIC_HEADER_3BYTE_MAX_CSID: u32 = 65599;
115
116// ── u24 helpers ─────────────────────────────────────────────────────────
117
118/// Read a 3-byte big-endian unsigned integer. `b` must have at least
119/// [`U24_LEN`] bytes (caller-checked).
120fn read_u24_be(b: &[u8]) -> u32 {
121    (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2])
122}
123
124/// Write `v`'s low 24 bits as big-endian into `buf`. `buf` must have at
125/// least [`U24_LEN`] bytes (caller-checked). Bits above the low 24 are
126/// silently dropped — every wire user of this helper (`message length`, and
127/// `timestamp`/`timestamp delta` after the extended-timestamp check) is
128/// only ever asked to write a value already known to fit.
129fn write_u24_be(v: u32, buf: &mut [u8]) {
130    buf[0] = (v >> 16) as u8;
131    buf[1] = (v >> 8) as u8;
132    buf[2] = v as u8;
133}
134
135// ── fmt (chunk type) ────────────────────────────────────────────────────
136
137/// The 2-bit `fmt` field selecting one of the 4 Chunk Message Header formats
138/// (§5.3.1.1, §5.3.1.2).
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140#[non_exhaustive]
141pub enum Fmt {
142    /// Type 0 (§5.3.1.2.1): the full 11-byte header.
143    Type0,
144    /// Type 1 (§5.3.1.2.2): 7-byte header, inherits `message stream id`.
145    Type1,
146    /// Type 2 (§5.3.1.2.3): 3-byte header, inherits length/type/stream id.
147    Type2,
148    /// Type 3 (§5.3.1.2.4): no header, inherits everything.
149    Type3,
150}
151
152impl Fmt {
153    /// The spec token for this `fmt` value.
154    #[must_use]
155    pub fn name(&self) -> &'static str {
156        match self {
157            Fmt::Type0 => "type 0",
158            Fmt::Type1 => "type 1",
159            Fmt::Type2 => "type 2",
160            Fmt::Type3 => "type 3",
161        }
162    }
163
164    /// Decode the 2-bit wire value (0..=3) into a [`Fmt`].
165    ///
166    /// # Errors
167    /// [`RtmpError::Malformed`] if `bits` is not in `0..=3`.
168    pub const fn from_bits(bits: u8) -> core::result::Result<Self, RtmpError> {
169        match bits {
170            0 => Ok(Fmt::Type0),
171            1 => Ok(Fmt::Type1),
172            2 => Ok(Fmt::Type2),
173            3 => Ok(Fmt::Type3),
174            _ => Err(RtmpError::Malformed {
175                what: "chunk fmt (must be 0..=3)",
176            }),
177        }
178    }
179
180    /// Encode this `fmt` back to its 2-bit wire value (0..=3).
181    #[must_use]
182    pub const fn to_bits(self) -> u8 {
183        match self {
184            Fmt::Type0 => 0,
185            Fmt::Type1 => 1,
186            Fmt::Type2 => 2,
187            Fmt::Type3 => 3,
188        }
189    }
190}
191
192broadcast_common::impl_spec_display!(Fmt);
193
194// ── Basic Header (§5.3.1.1) ─────────────────────────────────────────────
195
196/// One of the three basic header forms, chosen purely by csid range
197/// (§5.3.1.1).
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199enum BasicHeaderForm {
200    One,
201    Two,
202    Three,
203}
204
205/// The minimal basic-header form that can encode `csid`.
206fn basic_header_form(csid: u32) -> Result<BasicHeaderForm> {
207    match csid {
208        BASIC_HEADER_1BYTE_MIN_CSID..=BASIC_HEADER_1BYTE_MAX_CSID => Ok(BasicHeaderForm::One),
209        BASIC_HEADER_2BYTE_MIN_CSID..=BASIC_HEADER_2BYTE_MAX_CSID => Ok(BasicHeaderForm::Two),
210        BASIC_HEADER_3BYTE_MIN_CSID..=BASIC_HEADER_3BYTE_MAX_CSID => Ok(BasicHeaderForm::Three),
211        _ => Err(RtmpError::Malformed {
212            what: "chunk stream id (must be 2..=65599)",
213        }),
214    }
215}
216
217/// Chunk Basic Header (§5.3.1.1): 1 to 3 bytes encoding the 2-bit `fmt` and
218/// the chunk stream id (csid). Length depends only on the csid value; the
219/// implementation SHOULD (and this [`Serialize`] impl does) use the smallest
220/// form that holds the id.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub struct BasicHeader {
223    /// Selects which of the 4 Chunk Message Header formats follows.
224    pub fmt: Fmt,
225    /// Chunk stream id. Valid range 2..=65599 on the wire (0/1 are the
226    /// 2-/3-byte form markers, not real ids; 2 is further reserved by the
227    /// spec for low-level protocol control messages/commands but is still a
228    /// structurally valid basic-header value).
229    pub chunk_stream_id: u32,
230}
231
232impl<'a> Parse<'a> for BasicHeader {
233    type Error = RtmpError;
234
235    fn parse(bytes: &'a [u8]) -> Result<Self> {
236        if bytes.is_empty() {
237            return Err(RtmpError::BufferTooShort {
238                need: 1,
239                have: 0,
240                what: "chunk basic header",
241            });
242        }
243        let byte0 = bytes[0];
244        let fmt = Fmt::from_bits((byte0 >> BASIC_HEADER_FMT_SHIFT) & 0x03)?;
245        let marker = byte0 & BASIC_HEADER_MARKER_MASK;
246
247        let chunk_stream_id = match marker {
248            BASIC_HEADER_2BYTE_MARKER => {
249                if bytes.len() < 2 {
250                    return Err(RtmpError::BufferTooShort {
251                        need: 2,
252                        have: bytes.len(),
253                        what: "chunk basic header (2-byte form)",
254                    });
255                }
256                u32::from(bytes[1]) + BASIC_HEADER_CSID_OFFSET
257            }
258            BASIC_HEADER_3BYTE_MARKER => {
259                if bytes.len() < 3 {
260                    return Err(RtmpError::BufferTooShort {
261                        need: 3,
262                        have: bytes.len(),
263                        what: "chunk basic header (3-byte form)",
264                    });
265                }
266                // §5.3.1.1: csid = (byte2 * 256) + byte1 + 64 — byte 1 is
267                // the low byte, byte 2 the high byte (little-endian).
268                u32::from(bytes[1]) + u32::from(bytes[2]) * 256 + BASIC_HEADER_CSID_OFFSET
269            }
270            literal => u32::from(literal),
271        };
272
273        Ok(BasicHeader {
274            fmt,
275            chunk_stream_id,
276        })
277    }
278}
279
280impl Serialize for BasicHeader {
281    type Error = RtmpError;
282
283    fn serialized_len(&self) -> usize {
284        match basic_header_form(self.chunk_stream_id) {
285            Ok(BasicHeaderForm::One) => 1,
286            Ok(BasicHeaderForm::Two) => 2,
287            Ok(BasicHeaderForm::Three) => 3,
288            // Out-of-range csid: nominal upper bound. `serialize_into`
289            // performs the real validation and returns the error.
290            Err(_) => 3,
291        }
292    }
293
294    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
295        let form = basic_header_form(self.chunk_stream_id)?;
296        let fmt_bits = self.fmt.to_bits() << BASIC_HEADER_FMT_SHIFT;
297
298        match form {
299            BasicHeaderForm::One => {
300                if buf.is_empty() {
301                    return Err(RtmpError::BufferTooShort {
302                        need: 1,
303                        have: 0,
304                        what: "chunk basic header output (1-byte form)",
305                    });
306                }
307                buf[0] = fmt_bits | (self.chunk_stream_id as u8);
308                Ok(1)
309            }
310            BasicHeaderForm::Two => {
311                if buf.len() < 2 {
312                    return Err(RtmpError::BufferTooShort {
313                        need: 2,
314                        have: buf.len(),
315                        what: "chunk basic header output (2-byte form)",
316                    });
317                }
318                buf[0] = fmt_bits | BASIC_HEADER_2BYTE_MARKER;
319                buf[1] = (self.chunk_stream_id - BASIC_HEADER_CSID_OFFSET) as u8;
320                Ok(2)
321            }
322            BasicHeaderForm::Three => {
323                if buf.len() < 3 {
324                    return Err(RtmpError::BufferTooShort {
325                        need: 3,
326                        have: buf.len(),
327                        what: "chunk basic header output (3-byte form)",
328                    });
329                }
330                buf[0] = fmt_bits | BASIC_HEADER_3BYTE_MARKER;
331                let rel = self.chunk_stream_id - BASIC_HEADER_CSID_OFFSET;
332                buf[1] = rel as u8;
333                buf[2] = (rel >> 8) as u8;
334                Ok(3)
335            }
336        }
337    }
338}
339
340// ── Message Header (§5.3.1.2) ───────────────────────────────────────────
341
342/// Whether `field` (a 24-bit `timestamp`/`timestamp delta`) needs the 4-byte
343/// Extended Timestamp (§5.3.1.3): any value `>= EXTENDED_TIMESTAMP_MARKER`.
344fn needs_extended_timestamp(field: u32) -> bool {
345    field >= EXTENDED_TIMESTAMP_MARKER
346}
347
348/// Chunk Message Header: one of 4 formats selected by [`Fmt`] (§5.3.1.2),
349/// carrying decreasing field sets — each format after Type 0 inherits the
350/// fields it omits from the preceding chunk on the same chunk stream.
351///
352/// Reassembling that "preceding chunk" state (so Type 1/2/3 headers can be
353/// resolved to absolute values) is a stateful job for the chunk-stream
354/// reassembler (#738 Task 4), not this header type. In particular, a Type 3
355/// header carrying zero bytes here does *not* by itself tell you whether an
356/// Extended Timestamp follows it on the wire: per §5.3.1.3, a Type 3 chunk
357/// carries the 4-byte Extended Timestamp when — and only when — the most
358/// recent Type 0/1/2 chunk on the same csid itself used one. Deciding that
359/// requires exactly that per-csid state, so [`MessageHeader::parse`] never
360/// consumes an Extended Timestamp for `Fmt::Type3`; the reassembler must
361/// apply this rule itself once it is tracking that state.
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363#[non_exhaustive]
364pub enum MessageHeader {
365    /// Type 0 (§5.3.1.2.1, 11 bytes on the wire before any Extended
366    /// Timestamp). MUST be used at the start of a chunk stream and whenever
367    /// the stream timestamp goes backward.
368    Type0 {
369        /// Absolute timestamp of the message (already resolved from any
370        /// Extended Timestamp).
371        timestamp: u32,
372        /// Length in bytes of the whole message (not the chunk payload).
373        message_length: u32,
374        /// Message type id (§6/§7.1).
375        message_type_id: u8,
376        /// Message stream id. The wire encoding of this field alone is
377        /// little-endian (§5.3.1.2.1).
378        message_stream_id: u32,
379    },
380    /// Type 1 (§5.3.1.2.2, 7 bytes before any Extended Timestamp). No
381    /// message stream id — inherits the preceding chunk's.
382    Type1 {
383        /// Delta from the previous chunk's timestamp on this csid (already
384        /// resolved from any Extended Timestamp).
385        timestamp_delta: u32,
386        /// Length in bytes of the whole message.
387        message_length: u32,
388        /// Message type id (§6/§7.1).
389        message_type_id: u8,
390    },
391    /// Type 2 (§5.3.1.2.3, 3 bytes before any Extended Timestamp). Neither
392    /// stream id nor message length included — both inherited.
393    Type2 {
394        /// Delta from the previous chunk's timestamp on this csid (already
395        /// resolved from any Extended Timestamp).
396        timestamp_delta: u32,
397    },
398    /// Type 3 (§5.3.1.2.4, 0 bytes). Stream id, message length, message type
399    /// id, and timestamp delta are all inherited from the preceding chunk on
400    /// the same csid.
401    Type3,
402}
403
404impl MessageHeader {
405    /// Parse the message header that follows a [`BasicHeader`] carrying
406    /// `fmt`. Returns the parsed variant and the number of bytes consumed
407    /// from `bytes` — the fixed per-`fmt` header length, plus 4 more if a
408    /// Type 0/1/2 field's 24-bit value read exactly [`EXTENDED_TIMESTAMP_MARKER`]
409    /// (in which case the real, unresolved value is read from the following
410    /// 4-byte big-endian Extended Timestamp — see §5.3.1.3).
411    ///
412    /// # Errors
413    /// [`RtmpError::BufferTooShort`] if `bytes` does not hold the full fixed
414    /// header (and, when signalled, the Extended Timestamp).
415    pub fn parse(fmt: Fmt, bytes: &[u8]) -> Result<(Self, usize)> {
416        match fmt {
417            Fmt::Type0 => {
418                if bytes.len() < TYPE0_LEN {
419                    return Err(RtmpError::BufferTooShort {
420                        need: TYPE0_LEN,
421                        have: bytes.len(),
422                        what: "type 0 message header",
423                    });
424                }
425                let raw_timestamp = read_u24_be(&bytes[0..U24_LEN]);
426                let message_length = read_u24_be(&bytes[U24_LEN..2 * U24_LEN]);
427                let message_type_id = bytes[2 * U24_LEN];
428                let message_stream_id =
429                    u32::from_le_bytes([bytes[7], bytes[8], bytes[9], bytes[10]]);
430
431                let (timestamp, consumed) = resolve_extended(raw_timestamp, bytes, TYPE0_LEN)?;
432
433                Ok((
434                    MessageHeader::Type0 {
435                        timestamp,
436                        message_length,
437                        message_type_id,
438                        message_stream_id,
439                    },
440                    consumed,
441                ))
442            }
443            Fmt::Type1 => {
444                if bytes.len() < TYPE1_LEN {
445                    return Err(RtmpError::BufferTooShort {
446                        need: TYPE1_LEN,
447                        have: bytes.len(),
448                        what: "type 1 message header",
449                    });
450                }
451                let raw_delta = read_u24_be(&bytes[0..U24_LEN]);
452                let message_length = read_u24_be(&bytes[U24_LEN..2 * U24_LEN]);
453                let message_type_id = bytes[2 * U24_LEN];
454
455                let (timestamp_delta, consumed) = resolve_extended(raw_delta, bytes, TYPE1_LEN)?;
456
457                Ok((
458                    MessageHeader::Type1 {
459                        timestamp_delta,
460                        message_length,
461                        message_type_id,
462                    },
463                    consumed,
464                ))
465            }
466            Fmt::Type2 => {
467                if bytes.len() < TYPE2_LEN {
468                    return Err(RtmpError::BufferTooShort {
469                        need: TYPE2_LEN,
470                        have: bytes.len(),
471                        what: "type 2 message header",
472                    });
473                }
474                let raw_delta = read_u24_be(&bytes[0..U24_LEN]);
475                let (timestamp_delta, consumed) = resolve_extended(raw_delta, bytes, TYPE2_LEN)?;
476
477                Ok((MessageHeader::Type2 { timestamp_delta }, consumed))
478            }
479            Fmt::Type3 => Ok((MessageHeader::Type3, TYPE3_LEN)),
480        }
481    }
482}
483
484/// Shared tail of Type 0/1/2 parsing: given the 24-bit field already read at
485/// `bytes[..3]`, resolve it to its real value (reading the trailing 4-byte
486/// Extended Timestamp if the field read the sentinel), and return
487/// `(value, total_consumed)` where `total_consumed = fixed_len (+4)`.
488fn resolve_extended(raw: u32, bytes: &[u8], fixed_len: usize) -> Result<(u32, usize)> {
489    if raw == EXTENDED_TIMESTAMP_MARKER {
490        let need = fixed_len + EXTENDED_TIMESTAMP_LEN;
491        if bytes.len() < need {
492            return Err(RtmpError::BufferTooShort {
493                need,
494                have: bytes.len(),
495                what: "extended timestamp",
496            });
497        }
498        let ext = u32::from_be_bytes([
499            bytes[fixed_len],
500            bytes[fixed_len + 1],
501            bytes[fixed_len + 2],
502            bytes[fixed_len + 3],
503        ]);
504        Ok((ext, need))
505    } else {
506        Ok((raw, fixed_len))
507    }
508}
509
510impl Serialize for MessageHeader {
511    type Error = RtmpError;
512
513    fn serialized_len(&self) -> usize {
514        match self {
515            MessageHeader::Type0 { timestamp, .. } => TYPE0_LEN + extended_len(*timestamp),
516            MessageHeader::Type1 {
517                timestamp_delta, ..
518            } => TYPE1_LEN + extended_len(*timestamp_delta),
519            MessageHeader::Type2 { timestamp_delta } => TYPE2_LEN + extended_len(*timestamp_delta),
520            MessageHeader::Type3 => TYPE3_LEN,
521        }
522    }
523
524    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
525        match *self {
526            MessageHeader::Type0 {
527                timestamp,
528                message_length,
529                message_type_id,
530                message_stream_id,
531            } => {
532                let extended = needs_extended_timestamp(timestamp);
533                let written = TYPE0_LEN + if extended { EXTENDED_TIMESTAMP_LEN } else { 0 };
534                if buf.len() < written {
535                    return Err(RtmpError::BufferTooShort {
536                        need: written,
537                        have: buf.len(),
538                        what: "type 0 message header output",
539                    });
540                }
541                let field = if extended {
542                    EXTENDED_TIMESTAMP_MARKER
543                } else {
544                    timestamp
545                };
546                write_u24_be(field, &mut buf[0..U24_LEN]);
547                write_u24_be(message_length, &mut buf[U24_LEN..2 * U24_LEN]);
548                buf[2 * U24_LEN] = message_type_id;
549                buf[7..11].copy_from_slice(&message_stream_id.to_le_bytes());
550                if extended {
551                    buf[11..15].copy_from_slice(&timestamp.to_be_bytes());
552                }
553                Ok(written)
554            }
555            MessageHeader::Type1 {
556                timestamp_delta,
557                message_length,
558                message_type_id,
559            } => {
560                let extended = needs_extended_timestamp(timestamp_delta);
561                let written = TYPE1_LEN + if extended { EXTENDED_TIMESTAMP_LEN } else { 0 };
562                if buf.len() < written {
563                    return Err(RtmpError::BufferTooShort {
564                        need: written,
565                        have: buf.len(),
566                        what: "type 1 message header output",
567                    });
568                }
569                let field = if extended {
570                    EXTENDED_TIMESTAMP_MARKER
571                } else {
572                    timestamp_delta
573                };
574                write_u24_be(field, &mut buf[0..U24_LEN]);
575                write_u24_be(message_length, &mut buf[U24_LEN..2 * U24_LEN]);
576                buf[2 * U24_LEN] = message_type_id;
577                if extended {
578                    buf[7..11].copy_from_slice(&timestamp_delta.to_be_bytes());
579                }
580                Ok(written)
581            }
582            MessageHeader::Type2 { timestamp_delta } => {
583                let extended = needs_extended_timestamp(timestamp_delta);
584                let written = TYPE2_LEN + if extended { EXTENDED_TIMESTAMP_LEN } else { 0 };
585                if buf.len() < written {
586                    return Err(RtmpError::BufferTooShort {
587                        need: written,
588                        have: buf.len(),
589                        what: "type 2 message header output",
590                    });
591                }
592                let field = if extended {
593                    EXTENDED_TIMESTAMP_MARKER
594                } else {
595                    timestamp_delta
596                };
597                write_u24_be(field, &mut buf[0..U24_LEN]);
598                if extended {
599                    buf[3..7].copy_from_slice(&timestamp_delta.to_be_bytes());
600                }
601                Ok(written)
602            }
603            MessageHeader::Type3 => Ok(0),
604        }
605    }
606}
607
608/// Extra bytes (0 or 4) [`Serialize`] will write for a 24-bit
609/// `timestamp`/`timestamp delta` value.
610fn extended_len(field: u32) -> usize {
611    if needs_extended_timestamp(field) {
612        EXTENDED_TIMESTAMP_LEN
613    } else {
614        0
615    }
616}
617
618// ── Message (the assembled unit) ────────────────────────────────────────
619
620/// One fully reassembled RTMP message: the payload of a single message
621/// stream at a single (resolved, absolute) timestamp (§6.1). Produced by
622/// [`ChunkAssembler::push`] and consumed by [`ChunkWriter::write`].
623///
624/// Task 5 (`message.rs`) adds typed interpretation of `payload`/
625/// `message_type_id`; this carrier stays stable underneath that.
626#[derive(Debug, Clone, PartialEq, Eq)]
627pub struct Message {
628    /// Chunk stream id this message was carried on.
629    pub chunk_stream_id: u32,
630    /// Absolute timestamp (already resolved from any timestamp delta /
631    /// Extended Timestamp — never a delta).
632    pub timestamp: u32,
633    /// Message type id (§6/§7.1).
634    pub message_type_id: u8,
635    /// Message stream id.
636    pub message_stream_id: u32,
637    /// The whole message payload (reassembled across every chunk it was
638    /// split into).
639    pub payload: Vec<u8>,
640}
641
642// ── ChunkAssembler (stateful inbound reassembly, §5.3) ──────────────────
643
644/// Per-csid reassembly state: the most recently resolved header fields (used
645/// by `fmt` 1/2/3 to inherit the fields they omit) plus the payload
646/// accumulated so far for the message currently in progress on this csid.
647#[derive(Debug, Clone, Default)]
648struct CsidState {
649    /// Absolute timestamp of the current/most recent message on this csid.
650    timestamp: u32,
651    /// Delta most recently applied to reach `timestamp` — re-applied
652    /// unchanged when a Type 3 chunk begins a new message (inherits the
653    /// prior delta). Per §3.1.2 Type 3: when a Type 3 chunk immediately
654    /// follows a Type 0 chunk with no intervening Type 1/2, its implied
655    /// delta equals the Type 0 chunk's own absolute timestamp — so a Type 0
656    /// chunk seeds this field with its `timestamp`, not `0`.
657    timestamp_delta: u32,
658    /// Total length in bytes of the current/most recent message.
659    message_length: u32,
660    /// Message type id of the current/most recent message.
661    message_type_id: u8,
662    /// Message stream id of the current/most recent message.
663    message_stream_id: u32,
664    /// Whether the most recent Type 0/1/2 header on this csid used the
665    /// Extended Timestamp (§5.3.1.3) — a Type 3 chunk then also carries (and
666    /// must consume) that 4-byte field, per csid, until the next Type 0/1/2
667    /// changes the flag.
668    extended: bool,
669    /// Whether a Type 0/1/2 header has ever been seen on this csid (Type
670    /// 1/2/3 headers inherit from it; nothing to inherit before the first
671    /// Type 0).
672    initialized: bool,
673    /// Whether a message is currently mid-accumulation on this csid (a
674    /// prior chunk started it but its `message_length` bytes are not all
675    /// in yet). Distinguishes, for a Type 3 chunk, a **continuation** of
676    /// that in-progress message (`true`) from the **start of a new**
677    /// message reusing the prior header (`false`, at a message boundary) —
678    /// `payload.len()` alone can't tell them apart once a message has
679    /// completed and `payload` was reset to empty for the next one.
680    in_progress: bool,
681    /// Payload bytes accumulated so far for the message in progress
682    /// (`message_length` total once complete). Reset to empty once a
683    /// message completes.
684    payload: Vec<u8>,
685}
686
687/// Stateful inbound chunk reassembler (§5.3): feed inbound bytes, get back
688/// each complete [`Message`] as soon as its last chunk arrives.
689///
690/// Maintains per-`chunk_stream_id` state, so multiple chunk streams may be
691/// interleaved on the same connection (as the wire format requires) and are
692/// each reassembled independently.
693#[derive(Debug)]
694pub struct ChunkAssembler {
695    chunk_size: u32,
696    csids: HashMap<u32, CsidState>,
697    /// Bytes carried over from a previous `push` call that did not yet form
698    /// a complete chunk (partial basic header, message header, extended
699    /// timestamp, or payload slice).
700    pending: Vec<u8>,
701}
702
703impl Default for ChunkAssembler {
704    fn default() -> Self {
705        Self::new()
706    }
707}
708
709impl ChunkAssembler {
710    /// New assembler, chunk size at the §5.3 default (128 bytes).
711    #[must_use]
712    pub fn new() -> Self {
713        Self {
714            chunk_size: DEFAULT_CHUNK_SIZE,
715            csids: HashMap::new(),
716            pending: Vec::new(),
717        }
718    }
719
720    /// Update the chunk size in effect for subsequent chunks (called on
721    /// receipt of a Set Chunk Size protocol control message, §5.4.1). Floored
722    /// at 1 (a chunk size of 0 would never make progress splitting payload)
723    /// and capped at [`MAX_CHUNK_SIZE`], matching
724    /// [`ChunkWriter::set_chunk_size`]'s floor/cap.
725    pub fn set_chunk_size(&mut self, n: u32) {
726        self.chunk_size = n.clamp(1, MAX_CHUNK_SIZE);
727    }
728
729    /// Feed inbound bytes; returns each complete [`Message`] decoded from
730    /// the buffer (in arrival order), leaving any trailing partial chunk or
731    /// partial message buffered internally for the next call.
732    ///
733    /// Callers that need to react to a message's side effects (e.g. a Set
734    /// Chunk Size protocol control message, §5.4.1) **before** parsing the
735    /// bytes that follow it in the same input — because the sender may
736    /// switch to the new chunk size for its very next chunk — should use the
737    /// crate-internal incremental `feed`/`next_message` pair instead,
738    /// dispatching each message immediately. `push` collects every message
739    /// from `input` under a single, unchanging `chunk_size`, which
740    /// misparses a buffer that itself contains a Set Chunk Size followed by
741    /// chunks already framed at the new size (see
742    /// [`ServerSession`](crate::server::ServerSession), which uses the
743    /// incremental form for exactly this reason).
744    ///
745    /// # Errors
746    /// [`RtmpError::Malformed`] on structurally invalid input (e.g. a Type
747    /// 1/2/3 chunk on a csid that has never seen a Type 0). Never errors
748    /// merely because the input ends mid-chunk — that is buffered, not an
749    /// error.
750    pub fn push(&mut self, input: &[u8]) -> Result<Vec<Message>> {
751        self.feed(input);
752        let mut out = Vec::new();
753        while let Some(msg) = self.next_message()? {
754            out.push(msg);
755        }
756        Ok(out)
757    }
758
759    /// Buffer inbound bytes without parsing them yet. Pair with repeated
760    /// calls to [`next_message`](Self::next_message) to parse and dispatch
761    /// one message at a time (see [`push`](Self::push)'s docs for why this
762    /// matters for Set Chunk Size).
763    pub(crate) fn feed(&mut self, input: &[u8]) {
764        self.pending.extend_from_slice(input);
765    }
766
767    /// Parse and return the next complete [`Message`] out of the
768    /// previously-[`feed`](Self::feed) bytes, or `Ok(None)` if what remains
769    /// buffered isn't (yet) a complete message. Internally keeps parsing
770    /// individual chunks — which may belong to other interleaved csids, or
771    /// be a non-final chunk of the same in-progress message — until either a
772    /// full message is assembled or the buffered bytes run out.
773    ///
774    /// # Errors
775    /// Same as [`push`](Self::push).
776    pub(crate) fn next_message(&mut self) -> Result<Option<Message>> {
777        loop {
778            match Self::try_parse_one(&self.pending, &self.csids, self.chunk_size) {
779                Ok(Some(parsed)) => {
780                    self.pending.drain(..parsed.consumed);
781                    let state = self.csids.entry(parsed.csid).or_default();
782                    state.timestamp = parsed.timestamp;
783                    state.timestamp_delta = parsed.timestamp_delta;
784                    state.message_length = parsed.message_length;
785                    state.message_type_id = parsed.message_type_id;
786                    state.message_stream_id = parsed.message_stream_id;
787                    state.extended = parsed.extended;
788                    state.initialized = true;
789                    if parsed.payload.len() as u32 == parsed.message_length {
790                        state.payload.clear();
791                        state.in_progress = false;
792                        return Ok(Some(Message {
793                            chunk_stream_id: parsed.csid,
794                            timestamp: parsed.timestamp,
795                            message_type_id: parsed.message_type_id,
796                            message_stream_id: parsed.message_stream_id,
797                            payload: parsed.payload,
798                        }));
799                    }
800                    state.payload = parsed.payload;
801                    state.in_progress = true;
802                    // This chunk only partially filled its message (or
803                    // belongs to a different, interleaved csid) — keep
804                    // looping to try the next chunk in `pending`.
805                }
806                Ok(None) => return Ok(None),
807                Err(e) => return Err(e),
808            }
809        }
810    }
811
812    /// Attempt to parse exactly one chunk (basic header + message header +
813    /// any extended timestamp + its payload slice) from the front of `buf`,
814    /// resolving it against the existing per-csid `states` without mutating
815    /// them. Returns:
816    /// - `Ok(Some(_))` — a full chunk was parsed; `consumed` bytes should be
817    ///   dropped from the front of the caller's buffer and the returned
818    ///   resolved fields committed to that csid's state.
819    /// - `Ok(None)` — not enough bytes yet for a full chunk (structurally
820    ///   plausible so far); caller should wait for more input.
821    /// - `Err(_)` — structurally invalid input.
822    fn try_parse_one(
823        buf: &[u8],
824        states: &HashMap<u32, CsidState>,
825        chunk_size: u32,
826    ) -> Result<Option<ParsedChunk>> {
827        let bh = match BasicHeader::parse(buf) {
828            Ok(bh) => bh,
829            Err(RtmpError::BufferTooShort { .. }) => return Ok(None),
830            Err(e) => return Err(e),
831        };
832        // `BasicHeader::parse` having succeeded means `buf` holds at least
833        // as many bytes as this form needs; re-derive that count from the
834        // marker bits actually on the wire (not from `basic_header_form`,
835        // which reflects the *minimal* form for the csid and may disagree
836        // with a wire that legally used a longer form for a csid in the
837        // 64..=319 overlap range).
838        let marker = buf[0] & BASIC_HEADER_MARKER_MASK;
839        let header_len = match marker {
840            BASIC_HEADER_2BYTE_MARKER => 2,
841            BASIC_HEADER_3BYTE_MARKER => 3,
842            _ => 1,
843        };
844
845        let existing = states.get(&bh.chunk_stream_id);
846
847        // Remote-DoS guard: a flood of chunks opening distinct, previously
848        // unseen csids would otherwise grow `states` without bound (one
849        // `CsidState`, each with its own payload buffer, per bogus csid).
850        // Reject before the caller ever inserts a new entry for this csid.
851        if existing.is_none() && states.len() >= MAX_CSIDS {
852            return Err(RtmpError::Malformed {
853                what: "too many concurrent chunk stream ids (csid flood)",
854            });
855        }
856
857        let rest = &buf[header_len..];
858        let (mh, mh_consumed) = match MessageHeader::parse(bh.fmt, rest) {
859            Ok(v) => v,
860            Err(RtmpError::BufferTooShort { .. }) => return Ok(None),
861            Err(e) => return Err(e),
862        };
863        let mut consumed = header_len + mh_consumed;
864
865        // Resolve this chunk's header fields (timestamp/length/type/stream
866        // id/extended-flag) and whether it begins a new message or
867        // continues the one already in progress on this csid.
868        let (resolved, starts_new) = match (bh.fmt, mh) {
869            (
870                Fmt::Type0,
871                MessageHeader::Type0 {
872                    timestamp,
873                    message_length,
874                    message_type_id,
875                    message_stream_id,
876                },
877            ) => {
878                let used_extended = mh_consumed > TYPE0_LEN;
879                (
880                    ResolvedHeader {
881                        // §3.1.2: a Type 3 immediately following this Type 0
882                        // (no intervening Type 1/2) implies a delta equal to
883                        // this Type 0's own absolute timestamp.
884                        timestamp_delta: timestamp,
885                        timestamp,
886                        message_length,
887                        message_type_id,
888                        message_stream_id,
889                        extended: used_extended,
890                    },
891                    true,
892                )
893            }
894            // TODO(#738 follow-up): a Type 1/2 header arriving while a
895            // message is already `in_progress` on this csid (a header
896            // interleaved mid-message, rather than at a message boundary)
897            // is not detected here — it silently resets `payload`/state and
898            // drops the in-flight bytes rather than erroring. Real streams
899            // shouldn't do this, but a malformed/desynced one could; needs
900            // its own test + design before implementing.
901            (
902                Fmt::Type1,
903                MessageHeader::Type1 {
904                    timestamp_delta,
905                    message_length,
906                    message_type_id,
907                },
908            ) => {
909                let existing = existing.ok_or(RtmpError::Malformed {
910                    what: "type 1 chunk header on a csid with no prior chunk to inherit from",
911                })?;
912                let used_extended = mh_consumed > TYPE1_LEN;
913                (
914                    ResolvedHeader {
915                        timestamp: existing.timestamp.wrapping_add(timestamp_delta),
916                        timestamp_delta,
917                        message_length,
918                        message_type_id,
919                        message_stream_id: existing.message_stream_id,
920                        extended: used_extended,
921                    },
922                    true,
923                )
924            }
925            (Fmt::Type2, MessageHeader::Type2 { timestamp_delta }) => {
926                let existing = existing.ok_or(RtmpError::Malformed {
927                    what: "type 2 chunk header on a csid with no prior chunk to inherit from",
928                })?;
929                let used_extended = mh_consumed > TYPE2_LEN;
930                (
931                    ResolvedHeader {
932                        timestamp: existing.timestamp.wrapping_add(timestamp_delta),
933                        timestamp_delta,
934                        message_length: existing.message_length,
935                        message_type_id: existing.message_type_id,
936                        message_stream_id: existing.message_stream_id,
937                        extended: used_extended,
938                    },
939                    true,
940                )
941            }
942            (Fmt::Type3, MessageHeader::Type3) => {
943                let existing = existing.ok_or(RtmpError::Malformed {
944                    what: "type 3 chunk header on a csid with no prior chunk to inherit from",
945                })?;
946                let continuation = existing.in_progress;
947                if existing.extended {
948                    if buf.len() < consumed + EXTENDED_TIMESTAMP_LEN {
949                        return Ok(None);
950                    }
951                    // Present per §3.1.3 whenever the most recent Type 0/1/2
952                    // on this csid used one. A continuation chunk's message
953                    // timestamp is already fixed (ignore the value); a
954                    // new-message Type 3 re-applies the inherited delta
955                    // (also ignoring the value: Type 3 has nothing of its
956                    // own to contribute, by definition it inherits).
957                    consumed += EXTENDED_TIMESTAMP_LEN;
958                }
959                if continuation {
960                    (
961                        ResolvedHeader {
962                            timestamp: existing.timestamp,
963                            timestamp_delta: existing.timestamp_delta,
964                            message_length: existing.message_length,
965                            message_type_id: existing.message_type_id,
966                            message_stream_id: existing.message_stream_id,
967                            extended: existing.extended,
968                        },
969                        false,
970                    )
971                } else {
972                    (
973                        ResolvedHeader {
974                            timestamp: existing.timestamp.wrapping_add(existing.timestamp_delta),
975                            timestamp_delta: existing.timestamp_delta,
976                            message_length: existing.message_length,
977                            message_type_id: existing.message_type_id,
978                            message_stream_id: existing.message_stream_id,
979                            extended: existing.extended,
980                        },
981                        true,
982                    )
983                }
984            }
985            // `MessageHeader::parse` is always called with the `Fmt` that
986            // selects its own variant, so every other pairing is
987            // unreachable.
988            _ => unreachable!("MessageHeader::parse always returns the variant for its Fmt"),
989        };
990
991        // Remote-DoS guard: `message_length` is a fully attacker-controlled
992        // 24-bit wire field (Type 0/1 headers set it directly; Type 2/3
993        // inherit an already-checked value). Reject before any payload
994        // buffer for this message is allocated — see `MAX_MESSAGE_LEN`'s
995        // doc for why this bound is safe for real RTMP traffic.
996        if resolved.message_length > MAX_MESSAGE_LEN {
997            return Err(RtmpError::Malformed {
998                what: "message length exceeds the maximum accepted message size",
999            });
1000        }
1001
1002        let already_accumulated = if starts_new {
1003            0
1004        } else {
1005            existing.map(|s| s.payload.len()).unwrap_or(0)
1006        };
1007        let remaining_needed =
1008            (resolved.message_length as usize).saturating_sub(already_accumulated);
1009        let take = (chunk_size as usize).min(remaining_needed);
1010
1011        if buf.len() < consumed + take {
1012            return Ok(None);
1013        }
1014
1015        // No `Vec::with_capacity(resolved.message_length)` here: that would
1016        // pre-reserve up to `MAX_MESSAGE_LEN` bytes off a single attacker-
1017        // supplied header field, before a single payload byte has actually
1018        // arrived. The payload instead grows incrementally via
1019        // `extend_from_slice` below, chunk by chunk, as real bytes show up —
1020        // pre-reserving the claimed length buys almost nothing since the
1021        // data arrives in `chunk_size` pieces anyway.
1022        let mut payload = if starts_new {
1023            Vec::new()
1024        } else {
1025            existing.map(|s| s.payload.clone()).unwrap_or_default()
1026        };
1027        payload.extend_from_slice(&buf[consumed..consumed + take]);
1028        consumed += take;
1029
1030        Ok(Some(ParsedChunk {
1031            csid: bh.chunk_stream_id,
1032            consumed,
1033            timestamp: resolved.timestamp,
1034            timestamp_delta: resolved.timestamp_delta,
1035            message_length: resolved.message_length,
1036            message_type_id: resolved.message_type_id,
1037            message_stream_id: resolved.message_stream_id,
1038            extended: resolved.extended,
1039            payload,
1040        }))
1041    }
1042}
1043
1044/// Header fields resolved for one chunk, after applying `fmt`-specific
1045/// inheritance from the csid's prior state.
1046struct ResolvedHeader {
1047    timestamp: u32,
1048    timestamp_delta: u32,
1049    message_length: u32,
1050    message_type_id: u8,
1051    message_stream_id: u32,
1052    extended: bool,
1053}
1054
1055/// One fully-parsed chunk (header resolved + its payload slice taken),
1056/// ready to be committed to the owning [`ChunkAssembler`]'s per-csid state.
1057struct ParsedChunk {
1058    csid: u32,
1059    consumed: usize,
1060    timestamp: u32,
1061    timestamp_delta: u32,
1062    message_length: u32,
1063    message_type_id: u8,
1064    message_stream_id: u32,
1065    extended: bool,
1066    payload: Vec<u8>,
1067}
1068
1069// ── ChunkWriter (outbound, §5.3) ─────────────────────────────────────────
1070
1071/// Stateless-per-message outbound chunk writer (§5.3): serializes a
1072/// [`Message`] into chunk bytes at the current chunk size.
1073///
1074/// Simple, always-correct strategy: the first chunk is always Type 0 (full
1075/// absolute-timestamp header) and every continuation chunk is Type 3
1076/// (0-byte header, inheriting everything). This is spec-valid — Type 1/2's
1077/// more compact delta-based headers are a size optimisation this writer
1078/// does not perform.
1079#[derive(Debug, Clone)]
1080pub struct ChunkWriter {
1081    chunk_size: u32,
1082}
1083
1084impl Default for ChunkWriter {
1085    fn default() -> Self {
1086        Self::new()
1087    }
1088}
1089
1090impl ChunkWriter {
1091    /// New writer, chunk size at the §5.3 default (128 bytes).
1092    #[must_use]
1093    pub fn new() -> Self {
1094        Self {
1095            chunk_size: DEFAULT_CHUNK_SIZE,
1096        }
1097    }
1098
1099    /// Update the chunk size used for subsequent [`ChunkWriter::write`]
1100    /// calls (called on sending a Set Chunk Size protocol control message,
1101    /// §5.4.1). Capped at [`MAX_CHUNK_SIZE`] (the floor-of-1 is applied in
1102    /// [`ChunkWriter::write`] itself).
1103    pub fn set_chunk_size(&mut self, n: u32) {
1104        self.chunk_size = n.min(MAX_CHUNK_SIZE);
1105    }
1106
1107    /// Serialize `msg` into chunk bytes at the current chunk size: a Type 0
1108    /// first chunk carrying up to `chunk_size` payload bytes, then Type 3
1109    /// continuation chunks for the remainder.
1110    ///
1111    /// # Panics
1112    /// If `msg.chunk_stream_id` is outside the basic header's encodable
1113    /// range (2..=65599) — the same precondition [`BasicHeader::serialize_into`]
1114    /// enforces. Every `chunk_stream_id` produced by [`ChunkAssembler::push`]
1115    /// satisfies this (`BasicHeader::parse` never yields one outside the
1116    /// range), so a `Message` round-tripped from the assembler never panics
1117    /// here; callers building a `Message` from scratch must respect it.
1118    #[must_use]
1119    pub fn write(&mut self, msg: &Message) -> Vec<u8> {
1120        let chunk_size = (self.chunk_size as usize).max(1);
1121        let message_length = msg.payload.len() as u32;
1122        let extended = needs_extended_timestamp(msg.timestamp);
1123
1124        let mut out = Vec::with_capacity(TYPE0_LEN + msg.payload.len() + 16);
1125
1126        let bh0 = BasicHeader {
1127            fmt: Fmt::Type0,
1128            chunk_stream_id: msg.chunk_stream_id,
1129        };
1130        let mh0 = MessageHeader::Type0 {
1131            timestamp: msg.timestamp,
1132            message_length,
1133            message_type_id: msg.message_type_id,
1134            message_stream_id: msg.message_stream_id,
1135        };
1136        write_serialized(&mut out, &bh0);
1137        write_serialized(&mut out, &mh0);
1138
1139        let mut offset = 0usize;
1140        let take0 = chunk_size.min(msg.payload.len());
1141        out.extend_from_slice(&msg.payload[offset..offset + take0]);
1142        offset += take0;
1143
1144        while offset < msg.payload.len() {
1145            let bh = BasicHeader {
1146                fmt: Fmt::Type3,
1147                chunk_stream_id: msg.chunk_stream_id,
1148            };
1149            write_serialized(&mut out, &bh);
1150            if extended {
1151                out.extend_from_slice(&msg.timestamp.to_be_bytes());
1152            }
1153            let take = chunk_size.min(msg.payload.len() - offset);
1154            out.extend_from_slice(&msg.payload[offset..offset + take]);
1155            offset += take;
1156        }
1157
1158        out
1159    }
1160}
1161
1162/// Serialize `item` and append the bytes to `out`.
1163///
1164/// # Panics
1165/// If `item.serialize_into` errors (only possible, for the [`BasicHeader`]s
1166/// this is used with, when `chunk_stream_id` is outside the encodable
1167/// range) — see [`ChunkWriter::write`]'s panics section.
1168fn write_serialized<T: Serialize<Error = RtmpError>>(out: &mut Vec<u8>, item: &T) {
1169    let len = item.serialized_len();
1170    let start = out.len();
1171    out.resize(start + len, 0);
1172    let n = item
1173        .serialize_into(&mut out[start..])
1174        .expect("valid chunk_stream_id (2..=65599) is a ChunkWriter::write precondition");
1175    out.truncate(start + n);
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180    use super::*;
1181
1182    // ── u24 helper ───────────────────────────────────────────────────────
1183
1184    #[test]
1185    fn u24_round_trip_zero() {
1186        let mut buf = [0xFFu8; U24_LEN];
1187        write_u24_be(0, &mut buf);
1188        assert_eq!(buf, [0, 0, 0]);
1189        assert_eq!(read_u24_be(&buf), 0);
1190    }
1191
1192    #[test]
1193    fn u24_round_trip_max() {
1194        let mut buf = [0u8; U24_LEN];
1195        write_u24_be(0x00FF_FFFF, &mut buf);
1196        assert_eq!(buf, [0xFF, 0xFF, 0xFF]);
1197        assert_eq!(read_u24_be(&buf), 0x00FF_FFFF);
1198    }
1199
1200    #[test]
1201    fn u24_round_trip_mid_value() {
1202        let mut buf = [0u8; U24_LEN];
1203        write_u24_be(0x0012_3456, &mut buf);
1204        assert_eq!(buf, [0x12, 0x34, 0x56]);
1205        assert_eq!(read_u24_be(&buf), 0x0012_3456);
1206    }
1207
1208    // ── Fmt ──────────────────────────────────────────────────────────────
1209
1210    #[test]
1211    fn fmt_from_bits_round_trip() {
1212        for (bits, fmt) in [
1213            (0u8, Fmt::Type0),
1214            (1, Fmt::Type1),
1215            (2, Fmt::Type2),
1216            (3, Fmt::Type3),
1217        ] {
1218            let parsed = Fmt::from_bits(bits).unwrap();
1219            assert_eq!(parsed, fmt);
1220            assert_eq!(parsed.to_bits(), bits);
1221        }
1222    }
1223
1224    #[test]
1225    fn fmt_from_bits_out_of_range_is_malformed() {
1226        assert!(matches!(
1227            Fmt::from_bits(4),
1228            Err(RtmpError::Malformed { .. })
1229        ));
1230    }
1231
1232    #[test]
1233    fn fmt_display_matches_name() {
1234        assert_eq!(Fmt::Type0.to_string(), "type 0");
1235        assert_eq!(Fmt::Type3.to_string(), "type 3");
1236    }
1237
1238    // ── BasicHeader: 1-byte form ─────────────────────────────────────────
1239
1240    #[test]
1241    fn basic_header_one_byte_form_round_trip_build_serialize_parse() {
1242        for csid in [BASIC_HEADER_1BYTE_MIN_CSID, 5, BASIC_HEADER_1BYTE_MAX_CSID] {
1243            let bh = BasicHeader {
1244                fmt: Fmt::Type1,
1245                chunk_stream_id: csid,
1246            };
1247            let mut buf = [0u8; 1];
1248            let n = bh.serialize_into(&mut buf).unwrap();
1249            assert_eq!(n, 1, "csid {csid} must use the 1-byte form");
1250            let parsed = BasicHeader::parse(&buf).unwrap();
1251            assert_eq!(parsed, bh);
1252        }
1253    }
1254
1255    #[test]
1256    fn basic_header_one_byte_form_parse_serialize_byte_identical() {
1257        // fmt=1 (bits 01), csid=5: byte0 = 0b01_000101 = 0x45.
1258        let bytes = [0x45u8];
1259        let bh = BasicHeader::parse(&bytes).unwrap();
1260        assert_eq!(bh.fmt, Fmt::Type1);
1261        assert_eq!(bh.chunk_stream_id, 5);
1262        let mut buf = [0u8; 1];
1263        bh.serialize_into(&mut buf).unwrap();
1264        assert_eq!(buf, bytes);
1265    }
1266
1267    // ── BasicHeader: 2-byte form ─────────────────────────────────────────
1268
1269    #[test]
1270    fn basic_header_two_byte_form_round_trip_boundaries() {
1271        for csid in [
1272            BASIC_HEADER_2BYTE_MIN_CSID,
1273            200,
1274            BASIC_HEADER_2BYTE_MAX_CSID,
1275        ] {
1276            let bh = BasicHeader {
1277                fmt: Fmt::Type2,
1278                chunk_stream_id: csid,
1279            };
1280            let mut buf = [0u8; 2];
1281            let n = bh.serialize_into(&mut buf).unwrap();
1282            assert_eq!(n, 2, "csid {csid} must use the minimal 2-byte form");
1283            let parsed = BasicHeader::parse(&buf).unwrap();
1284            assert_eq!(parsed, bh);
1285        }
1286    }
1287
1288    #[test]
1289    fn basic_header_two_byte_form_parse_serialize_byte_identical() {
1290        // fmt=2 (bits 10), marker 0, csid-64 = 0 => csid 64.
1291        let bytes = [0b10_000000u8, 0x00];
1292        let bh = BasicHeader::parse(&bytes).unwrap();
1293        assert_eq!(bh.fmt, Fmt::Type2);
1294        assert_eq!(bh.chunk_stream_id, 64);
1295        let mut buf = [0u8; 2];
1296        bh.serialize_into(&mut buf).unwrap();
1297        assert_eq!(buf, bytes);
1298    }
1299
1300    // ── BasicHeader: 3-byte form ─────────────────────────────────────────
1301
1302    #[test]
1303    fn basic_header_three_byte_form_round_trip_boundaries() {
1304        for csid in [
1305            BASIC_HEADER_3BYTE_MIN_CSID,
1306            40000,
1307            BASIC_HEADER_3BYTE_MAX_CSID,
1308        ] {
1309            let bh = BasicHeader {
1310                fmt: Fmt::Type3,
1311                chunk_stream_id: csid,
1312            };
1313            let mut buf = [0u8; 3];
1314            let n = bh.serialize_into(&mut buf).unwrap();
1315            assert_eq!(n, 3, "csid {csid} must use the 3-byte form");
1316            let parsed = BasicHeader::parse(&buf).unwrap();
1317            assert_eq!(parsed, bh);
1318        }
1319    }
1320
1321    #[test]
1322    fn basic_header_three_byte_form_parse_serialize_byte_identical() {
1323        // fmt=0, marker 1, csid-64 = 0xFFFF (LE: byte1=0xFF, byte2=0xFF) => csid 65599.
1324        let bytes = [0b00_000001u8, 0xFF, 0xFF];
1325        let bh = BasicHeader::parse(&bytes).unwrap();
1326        assert_eq!(bh.fmt, Fmt::Type0);
1327        assert_eq!(bh.chunk_stream_id, BASIC_HEADER_3BYTE_MAX_CSID);
1328        let mut buf = [0u8; 3];
1329        bh.serialize_into(&mut buf).unwrap();
1330        assert_eq!(buf, bytes);
1331    }
1332
1333    #[test]
1334    fn basic_header_2byte_and_3byte_csid_are_little_endian() {
1335        // csid = 64 + 0x0102 = 0x0142 = 322. 3-byte form: byte1=low=0x02, byte2=high=0x01.
1336        let bh = BasicHeader {
1337            fmt: Fmt::Type0,
1338            chunk_stream_id: 64 + 0x0102,
1339        };
1340        let mut buf = [0u8; 3];
1341        bh.serialize_into(&mut buf).unwrap();
1342        assert_eq!(buf[1], 0x02, "low byte of csid-64 must come first");
1343        assert_eq!(buf[2], 0x01, "high byte of csid-64 must come second");
1344        assert_eq!(BasicHeader::parse(&buf).unwrap(), bh);
1345    }
1346
1347    // ── BasicHeader: errors ──────────────────────────────────────────────
1348
1349    #[test]
1350    fn basic_header_csid_zero_is_malformed_on_serialize() {
1351        let bh = BasicHeader {
1352            fmt: Fmt::Type0,
1353            chunk_stream_id: 0,
1354        };
1355        let mut buf = [0u8; 3];
1356        assert!(matches!(
1357            bh.serialize_into(&mut buf),
1358            Err(RtmpError::Malformed { .. })
1359        ));
1360    }
1361
1362    #[test]
1363    fn basic_header_csid_one_is_malformed_on_serialize() {
1364        let bh = BasicHeader {
1365            fmt: Fmt::Type0,
1366            chunk_stream_id: 1,
1367        };
1368        let mut buf = [0u8; 3];
1369        assert!(matches!(
1370            bh.serialize_into(&mut buf),
1371            Err(RtmpError::Malformed { .. })
1372        ));
1373    }
1374
1375    #[test]
1376    fn basic_header_csid_above_max_is_malformed_on_serialize() {
1377        let bh = BasicHeader {
1378            fmt: Fmt::Type0,
1379            chunk_stream_id: BASIC_HEADER_3BYTE_MAX_CSID + 1,
1380        };
1381        let mut buf = [0u8; 3];
1382        assert!(matches!(
1383            bh.serialize_into(&mut buf),
1384            Err(RtmpError::Malformed { .. })
1385        ));
1386    }
1387
1388    #[test]
1389    fn basic_header_empty_input_is_buffer_too_short() {
1390        assert!(matches!(
1391            BasicHeader::parse(&[]),
1392            Err(RtmpError::BufferTooShort {
1393                need: 1,
1394                have: 0,
1395                ..
1396            })
1397        ));
1398    }
1399
1400    #[test]
1401    fn basic_header_truncated_two_byte_form_is_buffer_too_short() {
1402        let bytes = [0b00_000000u8]; // marker=0 (2-byte form) but only 1 byte given.
1403        assert!(matches!(
1404            BasicHeader::parse(&bytes),
1405            Err(RtmpError::BufferTooShort {
1406                need: 2,
1407                have: 1,
1408                ..
1409            })
1410        ));
1411    }
1412
1413    #[test]
1414    fn basic_header_truncated_three_byte_form_is_buffer_too_short() {
1415        let bytes = [0b00_000001u8, 0xAB]; // marker=1 (3-byte form) but only 2 bytes given.
1416        assert!(matches!(
1417            BasicHeader::parse(&bytes),
1418            Err(RtmpError::BufferTooShort {
1419                need: 3,
1420                have: 2,
1421                ..
1422            })
1423        ));
1424    }
1425
1426    // ── MessageHeader: Type 0 ────────────────────────────────────────────
1427
1428    #[test]
1429    fn type0_round_trip_build_serialize_parse_no_extended() {
1430        let mh = MessageHeader::Type0 {
1431            timestamp: 0x0011_2233,
1432            message_length: 0x0004_5566,
1433            message_type_id: 0x09,
1434            message_stream_id: 0xAABB_CCDD,
1435        };
1436        let mut buf = [0u8; TYPE0_LEN];
1437        let n = mh.serialize_into(&mut buf).unwrap();
1438        assert_eq!(n, TYPE0_LEN);
1439        let (parsed, consumed) = MessageHeader::parse(Fmt::Type0, &buf).unwrap();
1440        assert_eq!(consumed, TYPE0_LEN);
1441        assert_eq!(parsed, mh);
1442    }
1443
1444    #[test]
1445    fn type0_parse_serialize_byte_identical_no_extended_and_le_stream_id() {
1446        // timestamp = 0x001122 (< marker, no extension).
1447        // message_length = 0x334455.
1448        // message_type_id = 0x09.
1449        // message_stream_id = 0xAABBCCDD, wire LE => DD CC BB AA.
1450        let bytes: [u8; TYPE0_LEN] = [
1451            0x00, 0x11, 0x22, // timestamp
1452            0x33, 0x44, 0x55, // message_length
1453            0x09, // message_type_id
1454            0xDD, 0xCC, 0xBB, 0xAA, // message_stream_id, little-endian
1455        ];
1456        let (mh, consumed) = MessageHeader::parse(Fmt::Type0, &bytes).unwrap();
1457        assert_eq!(consumed, TYPE0_LEN);
1458        assert_eq!(
1459            mh,
1460            MessageHeader::Type0 {
1461                timestamp: 0x0000_1122,
1462                message_length: 0x0033_4455,
1463                message_type_id: 0x09,
1464                message_stream_id: 0xAABB_CCDD,
1465            }
1466        );
1467        let mut buf = [0u8; TYPE0_LEN];
1468        mh.serialize_into(&mut buf).unwrap();
1469        assert_eq!(
1470            buf, bytes,
1471            "byte-identical round trip, LE stream id included"
1472        );
1473    }
1474
1475    #[test]
1476    fn type0_extended_timestamp_parse_serialize_byte_identical() {
1477        // 24-bit timestamp field = sentinel 0xFFFFFF => extended 4-byte BE
1478        // timestamp follows, value 0x01020304 (chosen so BE != LE, catching
1479        // an endianness bug in the extended field).
1480        let bytes: [u8; TYPE0_LEN + 4] = [
1481            0xFF, 0xFF, 0xFF, // timestamp sentinel
1482            0x00, 0x00, 0x10, // message_length
1483            0x08, // message_type_id
1484            0x01, 0x00, 0x00, 0x00, // message_stream_id = 1, LE
1485            0x01, 0x02, 0x03, 0x04, // extended timestamp, big-endian
1486        ];
1487        let (mh, consumed) = MessageHeader::parse(Fmt::Type0, &bytes).unwrap();
1488        assert_eq!(consumed, TYPE0_LEN + 4);
1489        assert_eq!(
1490            mh,
1491            MessageHeader::Type0 {
1492                timestamp: 0x0102_0304,
1493                message_length: 0x0000_0010,
1494                message_type_id: 0x08,
1495                message_stream_id: 1,
1496            }
1497        );
1498        let mut buf = [0u8; TYPE0_LEN + 4];
1499        let n = mh.serialize_into(&mut buf).unwrap();
1500        assert_eq!(n, TYPE0_LEN + 4);
1501        assert_eq!(
1502            buf, bytes,
1503            "extended timestamp path must round-trip byte-identically"
1504        );
1505    }
1506
1507    #[test]
1508    fn type0_timestamp_exactly_at_marker_boundary_uses_extended_path() {
1509        // timestamp == EXTENDED_TIMESTAMP_MARKER exactly: per spec this MUST
1510        // still go through the extended-timestamp path (">=", not ">").
1511        let mh = MessageHeader::Type0 {
1512            timestamp: EXTENDED_TIMESTAMP_MARKER,
1513            message_length: 10,
1514            message_type_id: 1,
1515            message_stream_id: 0,
1516        };
1517        assert_eq!(mh.serialized_len(), TYPE0_LEN + 4);
1518        let mut buf = [0u8; TYPE0_LEN + 4];
1519        let n = mh.serialize_into(&mut buf).unwrap();
1520        assert_eq!(n, TYPE0_LEN + 4);
1521        assert_eq!(
1522            &buf[0..3],
1523            [0xFF, 0xFF, 0xFF],
1524            "24-bit field must be the sentinel"
1525        );
1526        assert_eq!(
1527            &buf[11..15],
1528            &EXTENDED_TIMESTAMP_MARKER.to_be_bytes()[..],
1529            "extended field carries the real value"
1530        );
1531        let (parsed, consumed) = MessageHeader::parse(Fmt::Type0, &buf).unwrap();
1532        assert_eq!(consumed, TYPE0_LEN + 4);
1533        assert_eq!(parsed, mh);
1534    }
1535
1536    // ── MessageHeader: Type 1 ────────────────────────────────────────────
1537
1538    #[test]
1539    fn type1_round_trip_build_serialize_parse_no_extended() {
1540        let mh = MessageHeader::Type1 {
1541            timestamp_delta: 20,
1542            message_length: 32,
1543            message_type_id: 8,
1544        };
1545        let mut buf = [0u8; TYPE1_LEN];
1546        let n = mh.serialize_into(&mut buf).unwrap();
1547        assert_eq!(n, TYPE1_LEN);
1548        let (parsed, consumed) = MessageHeader::parse(Fmt::Type1, &buf).unwrap();
1549        assert_eq!(consumed, TYPE1_LEN);
1550        assert_eq!(parsed, mh);
1551    }
1552
1553    #[test]
1554    fn type1_extended_timestamp_parse_serialize_byte_identical() {
1555        let bytes: [u8; TYPE1_LEN + 4] = [
1556            0xFF, 0xFF, 0xFF, // timestamp_delta sentinel
1557            0x00, 0x00, 0x20, // message_length
1558            0x09, // message_type_id
1559            0x0A, 0x0B, 0x0C, 0x0D, // extended timestamp delta, big-endian
1560        ];
1561        let (mh, consumed) = MessageHeader::parse(Fmt::Type1, &bytes).unwrap();
1562        assert_eq!(consumed, TYPE1_LEN + 4);
1563        assert_eq!(
1564            mh,
1565            MessageHeader::Type1 {
1566                timestamp_delta: 0x0A0B_0C0D,
1567                message_length: 0x0000_0020,
1568                message_type_id: 0x09,
1569            }
1570        );
1571        let mut buf = [0u8; TYPE1_LEN + 4];
1572        mh.serialize_into(&mut buf).unwrap();
1573        assert_eq!(buf, bytes);
1574    }
1575
1576    // ── MessageHeader: Type 2 ────────────────────────────────────────────
1577
1578    #[test]
1579    fn type2_round_trip_build_serialize_parse_no_extended() {
1580        let mh = MessageHeader::Type2 {
1581            timestamp_delta: 20,
1582        };
1583        let mut buf = [0u8; TYPE2_LEN];
1584        let n = mh.serialize_into(&mut buf).unwrap();
1585        assert_eq!(n, TYPE2_LEN);
1586        let (parsed, consumed) = MessageHeader::parse(Fmt::Type2, &buf).unwrap();
1587        assert_eq!(consumed, TYPE2_LEN);
1588        assert_eq!(parsed, mh);
1589    }
1590
1591    #[test]
1592    fn type2_extended_timestamp_parse_serialize_byte_identical() {
1593        let bytes: [u8; TYPE2_LEN + 4] = [
1594            0xFF, 0xFF, 0xFF, // timestamp_delta sentinel
1595            0x11, 0x22, 0x33, 0x44, // extended timestamp delta, big-endian
1596        ];
1597        let (mh, consumed) = MessageHeader::parse(Fmt::Type2, &bytes).unwrap();
1598        assert_eq!(consumed, TYPE2_LEN + 4);
1599        assert_eq!(
1600            mh,
1601            MessageHeader::Type2 {
1602                timestamp_delta: 0x1122_3344,
1603            }
1604        );
1605        let mut buf = [0u8; TYPE2_LEN + 4];
1606        mh.serialize_into(&mut buf).unwrap();
1607        assert_eq!(buf, bytes);
1608    }
1609
1610    // ── MessageHeader: Type 3 ────────────────────────────────────────────
1611
1612    #[test]
1613    fn type3_round_trip_is_zero_bytes() {
1614        let mh = MessageHeader::Type3;
1615        assert_eq!(mh.serialized_len(), 0);
1616        let mut buf: [u8; 0] = [];
1617        let n = mh.serialize_into(&mut buf).unwrap();
1618        assert_eq!(n, 0);
1619        let (parsed, consumed) = MessageHeader::parse(Fmt::Type3, &[]).unwrap();
1620        assert_eq!(consumed, 0);
1621        assert_eq!(parsed, MessageHeader::Type3);
1622    }
1623
1624    // ── MessageHeader: errors ────────────────────────────────────────────
1625
1626    #[test]
1627    fn type0_truncated_input_is_buffer_too_short() {
1628        let bytes = [0u8; TYPE0_LEN - 1];
1629        assert!(matches!(
1630            MessageHeader::parse(Fmt::Type0, &bytes),
1631            Err(RtmpError::BufferTooShort {
1632                need: TYPE0_LEN,
1633                ..
1634            })
1635        ));
1636    }
1637
1638    #[test]
1639    fn type0_extended_marker_but_truncated_extended_field_is_buffer_too_short() {
1640        let mut bytes = [0u8; TYPE0_LEN + 2]; // only 2 of the 4 extended bytes.
1641        bytes[0] = 0xFF;
1642        bytes[1] = 0xFF;
1643        bytes[2] = 0xFF;
1644        assert!(matches!(
1645            MessageHeader::parse(Fmt::Type0, &bytes),
1646            Err(RtmpError::BufferTooShort {
1647                need,
1648                ..
1649            }) if need == TYPE0_LEN + 4
1650        ));
1651    }
1652
1653    // ── Mutation-check sentinels ─────────────────────────────────────────
1654    // These pin exact wire-byte expectations (not just self-round-trip),
1655    // so a serializer that silently drops the extended-timestamp tail, or
1656    // mis-orders the little-endian message_stream_id, fails a test above:
1657    // `type0_parse_serialize_byte_identical_no_extended_and_le_stream_id`
1658    // hand-builds its expected bytes with the stream id reversed from host
1659    // order, and `type0_extended_timestamp_parse_serialize_byte_identical`
1660    // hand-builds a 15-byte fixture whose length alone (`TYPE0_LEN + 4`)
1661    // fails if the extended tail is ever omitted.
1662
1663    #[test]
1664    fn message_stream_id_le_differs_from_be_for_asymmetric_value() {
1665        // Sanity check that our fixture value's LE and BE encodings differ,
1666        // so the byte-identical test above truly exercises endianness (a
1667        // palindromic value like 0x01010101 would pass either order).
1668        let v: u32 = 0xAABB_CCDD;
1669        assert_ne!(v.to_le_bytes(), v.to_be_bytes());
1670    }
1671
1672    // ── ChunkAssembler / ChunkWriter ─────────────────────────────────────
1673
1674    fn msg(csid: u32, timestamp: u32, type_id: u8, stream_id: u32, payload: Vec<u8>) -> Message {
1675        Message {
1676            chunk_stream_id: csid,
1677            timestamp,
1678            message_type_id: type_id,
1679            message_stream_id: stream_id,
1680            payload,
1681        }
1682    }
1683
1684    #[test]
1685    fn writer_assembler_round_trip_small_message_single_chunk() {
1686        let original = msg(4, 1000, 9, 1, vec![0xAB; 50]);
1687        let mut writer = ChunkWriter::new();
1688        let bytes = writer.write(&original);
1689
1690        let mut assembler = ChunkAssembler::new();
1691        let out = assembler.push(&bytes).unwrap();
1692        assert_eq!(out.len(), 1, "one message must come back out");
1693        assert_eq!(out[0], original);
1694    }
1695
1696    #[test]
1697    fn writer_assembler_round_trip_message_larger_than_chunk_size() {
1698        // 300-byte payload at the default 128-byte chunk size => 3 chunks
1699        // (128 + 128 + 44): Type 0 first chunk, two Type 3 continuations.
1700        let original = msg(6, 5000, 9, 42, (0u8..=255).cycle().take(300).collect());
1701        let mut writer = ChunkWriter::new();
1702        let bytes = writer.write(&original);
1703
1704        // Sanity: verify the byte stream really contains 3 chunks (1 basic
1705        // header for csid 6 is 1 byte; Type 0 header is TYPE0_LEN; then 128
1706        // payload bytes; then two Type 3 (1-byte basic header, 0-byte
1707        // message header) + payload chunks of 128 and 44).
1708        let expected_len = 1 + TYPE0_LEN + 128 + (1 + 128) + (1 + 44);
1709        assert_eq!(bytes.len(), expected_len);
1710
1711        let mut assembler = ChunkAssembler::new();
1712        let out = assembler.push(&bytes).unwrap();
1713        assert_eq!(
1714            out.len(),
1715            1,
1716            "the 3 chunks must reassemble into ONE message"
1717        );
1718        assert_eq!(out[0], original);
1719        assert_eq!(out[0].payload.len(), 300);
1720    }
1721
1722    #[test]
1723    fn assembler_multi_chunk_payload_reassembled_in_order() {
1724        // Hand-built stream: Type 0 header (csid 3, len 10, type 8, stream
1725        // 0, timestamp 0) with 4 payload bytes, chunk size forced to 4, then
1726        // two Type 3 continuations of 4 and 2 bytes — assert the payload
1727        // comes back concatenated in the right order, not reordered.
1728        let mut assembler = ChunkAssembler::new();
1729        assembler.set_chunk_size(4);
1730
1731        let bh0 = BasicHeader {
1732            fmt: Fmt::Type0,
1733            chunk_stream_id: 3,
1734        };
1735        let mh0 = MessageHeader::Type0 {
1736            timestamp: 0,
1737            message_length: 10,
1738            message_type_id: 8,
1739            message_stream_id: 0,
1740        };
1741        let mut input = Vec::new();
1742        write_serialized(&mut input, &bh0);
1743        write_serialized(&mut input, &mh0);
1744        input.extend_from_slice(&[1, 2, 3, 4]);
1745
1746        let bh3 = BasicHeader {
1747            fmt: Fmt::Type3,
1748            chunk_stream_id: 3,
1749        };
1750        write_serialized(&mut input, &bh3);
1751        input.extend_from_slice(&[5, 6, 7, 8]);
1752        write_serialized(&mut input, &bh3);
1753        input.extend_from_slice(&[9, 10]);
1754
1755        let out = assembler.push(&input).unwrap();
1756        assert_eq!(out.len(), 1);
1757        assert_eq!(out[0].payload, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
1758    }
1759
1760    #[test]
1761    fn assembler_header_inheritance_type0_type1_type2_type3() {
1762        // fmt0 (ts=1000, len=5, type=8, stream=7) -> fmt1 (delta=20) ->
1763        // fmt2 (delta=30) -> fmt3 (inherits fmt2's delta=30). Each chunk's
1764        // message completes in one go (message_length == chunk_size == 5)
1765        // so every header starts a fresh message on this csid.
1766        let mut assembler = ChunkAssembler::new();
1767        assembler.set_chunk_size(5);
1768        let csid = 5;
1769
1770        let mut input = Vec::new();
1771        write_serialized(
1772            &mut input,
1773            &BasicHeader {
1774                fmt: Fmt::Type0,
1775                chunk_stream_id: csid,
1776            },
1777        );
1778        write_serialized(
1779            &mut input,
1780            &MessageHeader::Type0 {
1781                timestamp: 1000,
1782                message_length: 5,
1783                message_type_id: 8,
1784                message_stream_id: 7,
1785            },
1786        );
1787        input.extend_from_slice(&[0; 5]);
1788
1789        write_serialized(
1790            &mut input,
1791            &BasicHeader {
1792                fmt: Fmt::Type1,
1793                chunk_stream_id: csid,
1794            },
1795        );
1796        write_serialized(
1797            &mut input,
1798            &MessageHeader::Type1 {
1799                timestamp_delta: 20,
1800                message_length: 5,
1801                message_type_id: 8,
1802            },
1803        );
1804        input.extend_from_slice(&[1; 5]);
1805
1806        write_serialized(
1807            &mut input,
1808            &BasicHeader {
1809                fmt: Fmt::Type2,
1810                chunk_stream_id: csid,
1811            },
1812        );
1813        write_serialized(
1814            &mut input,
1815            &MessageHeader::Type2 {
1816                timestamp_delta: 30,
1817            },
1818        );
1819        input.extend_from_slice(&[2; 5]);
1820
1821        write_serialized(
1822            &mut input,
1823            &BasicHeader {
1824                fmt: Fmt::Type3,
1825                chunk_stream_id: csid,
1826            },
1827        );
1828        input.extend_from_slice(&[3; 5]);
1829
1830        let out = assembler.push(&input).unwrap();
1831        assert_eq!(out.len(), 4);
1832
1833        assert_eq!(out[0].timestamp, 1000);
1834        assert_eq!(out[0].message_stream_id, 7);
1835        assert_eq!(out[0].message_type_id, 8);
1836        assert_eq!(out[0].payload, vec![0; 5]);
1837
1838        assert_eq!(out[1].timestamp, 1020, "fmt1: 1000 + delta 20");
1839        assert_eq!(out[1].message_stream_id, 7, "fmt1 inherits stream id");
1840        assert_eq!(out[1].message_type_id, 8);
1841        assert_eq!(out[1].payload, vec![1; 5]);
1842
1843        assert_eq!(out[2].timestamp, 1050, "fmt2: 1020 + delta 30");
1844        assert_eq!(out[2].message_stream_id, 7, "fmt2 inherits stream id");
1845        assert_eq!(out[2].message_type_id, 8, "fmt2 inherits type id");
1846        assert_eq!(out[2].payload, vec![2; 5], "fmt2 inherits message length");
1847
1848        assert_eq!(
1849            out[3].timestamp, 1080,
1850            "fmt3 (new message) inherits fmt2's delta 30: 1050 + 30"
1851        );
1852        assert_eq!(out[3].message_stream_id, 7, "fmt3 inherits stream id");
1853        assert_eq!(out[3].message_type_id, 8, "fmt3 inherits type id");
1854        assert_eq!(out[3].payload, vec![3; 5], "fmt3 inherits message length");
1855    }
1856
1857    #[test]
1858    fn assembler_mid_stream_set_chunk_size_changes_split_boundary() {
1859        // First message at chunk_size 128 (default): a 10-byte message on
1860        // csid 7 fits in one chunk. Then shrink chunk_size to 4 and send a
1861        // second 10-byte message on the same csid (fresh Type 0): it must
1862        // now arrive in 3 physical chunks (4 + 4 + 2), and pushing only the
1863        // first two must NOT complete the message yet.
1864        let mut assembler = ChunkAssembler::new();
1865        let csid = 7;
1866
1867        let first = msg(csid, 100, 8, 1, vec![0xAA; 10]);
1868        let mut writer = ChunkWriter::new();
1869        let first_bytes = writer.write(&first);
1870        let out = assembler.push(&first_bytes).unwrap();
1871        assert_eq!(out, vec![first]);
1872
1873        assembler.set_chunk_size(4);
1874        let bh0 = BasicHeader {
1875            fmt: Fmt::Type0,
1876            chunk_stream_id: csid,
1877        };
1878        let mh0 = MessageHeader::Type0 {
1879            timestamp: 200,
1880            message_length: 10,
1881            message_type_id: 8,
1882            message_stream_id: 1,
1883        };
1884        let mut chunk1 = Vec::new();
1885        write_serialized(&mut chunk1, &bh0);
1886        write_serialized(&mut chunk1, &mh0);
1887        chunk1.extend_from_slice(&[1, 2, 3, 4]);
1888        let out = assembler.push(&chunk1).unwrap();
1889        assert!(
1890            out.is_empty(),
1891            "only 4 of 10 payload bytes arrived, message must not complete yet"
1892        );
1893
1894        let bh3 = BasicHeader {
1895            fmt: Fmt::Type3,
1896            chunk_stream_id: csid,
1897        };
1898        let mut chunk2 = Vec::new();
1899        write_serialized(&mut chunk2, &bh3);
1900        chunk2.extend_from_slice(&[5, 6, 7, 8]);
1901        let out = assembler.push(&chunk2).unwrap();
1902        assert!(out.is_empty(), "8 of 10 payload bytes, still incomplete");
1903
1904        let mut chunk3 = Vec::new();
1905        write_serialized(&mut chunk3, &bh3);
1906        chunk3.extend_from_slice(&[9, 10]);
1907        let out = assembler.push(&chunk3).unwrap();
1908        assert_eq!(out.len(), 1);
1909        assert_eq!(out[0].payload, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
1910    }
1911
1912    #[test]
1913    fn writer_assembler_round_trip_extended_timestamp_split_across_chunks() {
1914        // timestamp >= EXTENDED_TIMESTAMP_MARKER forces the Type 0 header
1915        // (and every Type 3 continuation) to carry the 4-byte Extended
1916        // Timestamp (§3.1.3) — payload longer than chunk_size so at least
1917        // one Type 3 continuation chunk exercises the "fmt3 also carries
1918        // extended timestamp" edge.
1919        let original = msg(8, EXTENDED_TIMESTAMP_MARKER + 12345, 9, 2, vec![0x7E; 300]);
1920        let mut writer = ChunkWriter::new();
1921        let bytes = writer.write(&original);
1922
1923        // Sanity: the first chunk's basic header + Type0 header must be
1924        // TYPE0_LEN + 4 (extended) bytes, and each Type 3 continuation
1925        // basic header must be immediately followed by 4 extended bytes
1926        // before its payload slice.
1927        let bh_len = 1; // csid 8 fits the 1-byte basic header form.
1928        let first_header_len = bh_len + TYPE0_LEN + EXTENDED_TIMESTAMP_LEN;
1929        assert_eq!(&bytes[bh_len..bh_len + 3], [0xFF, 0xFF, 0xFF]);
1930        let ext_offset = bh_len + TYPE0_LEN;
1931        assert_eq!(
1932            &bytes[ext_offset..ext_offset + 4],
1933            &original.timestamp.to_be_bytes()
1934        );
1935        let first_payload_take = 128usize;
1936        let second_chunk_start = first_header_len + first_payload_take;
1937        // second chunk: 1-byte Type 3 basic header + 4-byte extended ts.
1938        assert_eq!(bytes[second_chunk_start] >> 6, Fmt::Type3.to_bits());
1939        let second_ext_offset = second_chunk_start + 1;
1940        assert_eq!(
1941            &bytes[second_ext_offset..second_ext_offset + 4],
1942            &original.timestamp.to_be_bytes(),
1943            "fmt3 continuation must carry the same extended timestamp"
1944        );
1945
1946        let mut assembler = ChunkAssembler::new();
1947        let out = assembler.push(&bytes).unwrap();
1948        assert_eq!(out.len(), 1);
1949        assert_eq!(out[0], original);
1950        assert_eq!(out[0].timestamp, EXTENDED_TIMESTAMP_MARKER + 12345);
1951    }
1952
1953    #[test]
1954    fn assembler_partial_feed_split_mid_header_no_drop_or_duplicate() {
1955        let original = msg(9, 42, 8, 3, vec![0x11; 200]);
1956        let mut writer = ChunkWriter::new();
1957        let bytes = writer.write(&original);
1958
1959        // Split at an arbitrary offset that lands inside the Type 0 header
1960        // (byte 3 of 11), well before any payload.
1961        let split_at = 4;
1962        assert!(split_at < 1 + TYPE0_LEN);
1963
1964        let mut assembler = ChunkAssembler::new();
1965        let out1 = assembler.push(&bytes[..split_at]).unwrap();
1966        assert!(out1.is_empty(), "partial header must not error or complete");
1967        let out2 = assembler.push(&bytes[split_at..]).unwrap();
1968        assert_eq!(out2.len(), 1, "message must complete exactly once");
1969        assert_eq!(out2[0], original);
1970    }
1971
1972    #[test]
1973    fn assembler_partial_feed_split_mid_payload_no_drop_or_duplicate() {
1974        let original = msg(10, 42, 8, 3, vec![0x22; 300]);
1975        let mut writer = ChunkWriter::new();
1976        let bytes = writer.write(&original);
1977
1978        // Split partway through the first (128-byte) payload chunk.
1979        let split_at = 1 + TYPE0_LEN + 60;
1980        let mut assembler = ChunkAssembler::new();
1981        let out1 = assembler.push(&bytes[..split_at]).unwrap();
1982        assert!(out1.is_empty());
1983        let out2 = assembler.push(&bytes[split_at..]).unwrap();
1984        assert_eq!(out2.len(), 1);
1985        assert_eq!(out2[0], original);
1986    }
1987
1988    #[test]
1989    fn assembler_partial_feed_byte_at_a_time_never_drops_or_duplicates() {
1990        let original = msg(11, 7, 9, 4, vec![0x33; 260]);
1991        let mut writer = ChunkWriter::new();
1992        let bytes = writer.write(&original);
1993
1994        let mut assembler = ChunkAssembler::new();
1995        let mut collected = Vec::new();
1996        for b in &bytes {
1997            collected.extend(assembler.push(std::slice::from_ref(b)).unwrap());
1998        }
1999        assert_eq!(collected.len(), 1);
2000        assert_eq!(collected[0], original);
2001    }
2002
2003    #[test]
2004    fn assembler_type1_on_unseen_csid_is_malformed() {
2005        let mut assembler = ChunkAssembler::new();
2006        let mut input = Vec::new();
2007        write_serialized(
2008            &mut input,
2009            &BasicHeader {
2010                fmt: Fmt::Type1,
2011                chunk_stream_id: 20,
2012            },
2013        );
2014        write_serialized(
2015            &mut input,
2016            &MessageHeader::Type1 {
2017                timestamp_delta: 5,
2018                message_length: 3,
2019                message_type_id: 1,
2020            },
2021        );
2022        input.extend_from_slice(&[0, 0, 0]);
2023        assert!(matches!(
2024            assembler.push(&input),
2025            Err(RtmpError::Malformed { .. })
2026        ));
2027    }
2028
2029    #[test]
2030    fn assembler_type3_on_unseen_csid_is_malformed() {
2031        let mut assembler = ChunkAssembler::new();
2032        let mut input = Vec::new();
2033        write_serialized(
2034            &mut input,
2035            &BasicHeader {
2036                fmt: Fmt::Type3,
2037                chunk_stream_id: 21,
2038            },
2039        );
2040        assert!(matches!(
2041            assembler.push(&input),
2042            Err(RtmpError::Malformed { .. })
2043        ));
2044    }
2045
2046    #[test]
2047    fn assembler_truncated_input_never_panics_across_many_split_points() {
2048        let original = msg(12, 99, 8, 5, vec![0x44; 400]);
2049        let mut writer = ChunkWriter::new();
2050        let bytes = writer.write(&original);
2051
2052        // Feed every possible byte-prefix of the stream to a fresh
2053        // assembler each time: none may panic, and any that do parse fully
2054        // must reproduce the original message exactly.
2055        for split in 0..=bytes.len() {
2056            let mut assembler = ChunkAssembler::new();
2057            let first = assembler.push(&bytes[..split]);
2058            let Ok(first_msgs) = first else {
2059                continue;
2060            };
2061            let second = assembler.push(&bytes[split..]).unwrap();
2062            let mut all = first_msgs;
2063            all.extend(second);
2064            assert_eq!(all, vec![original.clone()]);
2065        }
2066    }
2067
2068    #[test]
2069    fn writer_default_chunk_size_matches_assembler_default() {
2070        assert_eq!(ChunkWriter::new().chunk_size, DEFAULT_CHUNK_SIZE);
2071        assert_eq!(ChunkAssembler::new().chunk_size, DEFAULT_CHUNK_SIZE);
2072    }
2073
2074    #[test]
2075    fn assembler_type3_immediately_after_type0_uses_type0_timestamp_as_implied_delta() {
2076        // §3.1.2: a Type 3 chunk that starts a NEW message immediately after
2077        // a Type 0 (nothing intervening) has an implied `timestamp_delta`
2078        // equal to that Type 0's own absolute timestamp — NOT 0. Every other
2079        // existing test intervenes a Type 1/2 before the fmt3, so this is
2080        // the only test that reaches this resolution path directly (bites a
2081        // mutation of `timestamp_delta: timestamp` -> `timestamp_delta: 0`
2082        // in the Type 0 arm).
2083        let mut assembler = ChunkAssembler::new();
2084        let csid = 40;
2085
2086        let mut input = Vec::new();
2087        write_serialized(
2088            &mut input,
2089            &BasicHeader {
2090                fmt: Fmt::Type0,
2091                chunk_stream_id: csid,
2092            },
2093        );
2094        write_serialized(
2095            &mut input,
2096            &MessageHeader::Type0 {
2097                timestamp: 1000,
2098                message_length: 5,
2099                message_type_id: 8,
2100                message_stream_id: 2,
2101            },
2102        );
2103        input.extend_from_slice(&[1, 2, 3, 4, 5]);
2104
2105        // Immediately (same csid, nothing between) a fmt3 chunk starting a
2106        // new message, inheriting message_length 5 from the Type 0 above.
2107        write_serialized(
2108            &mut input,
2109            &BasicHeader {
2110                fmt: Fmt::Type3,
2111                chunk_stream_id: csid,
2112            },
2113        );
2114        input.extend_from_slice(&[9, 9, 9, 9, 9]);
2115
2116        let out = assembler.push(&input).unwrap();
2117        assert_eq!(out.len(), 2);
2118        assert_eq!(out[0].timestamp, 1000);
2119        assert_eq!(
2120            out[1].timestamp, 2000,
2121            "fmt3 immediately after fmt0 implies delta == the fmt0's own timestamp (1000), not 0: 1000 + 1000"
2122        );
2123    }
2124
2125    #[test]
2126    fn assembler_type2_on_unseen_csid_is_malformed() {
2127        let mut assembler = ChunkAssembler::new();
2128        let mut input = Vec::new();
2129        write_serialized(
2130            &mut input,
2131            &BasicHeader {
2132                fmt: Fmt::Type2,
2133                chunk_stream_id: 22,
2134            },
2135        );
2136        write_serialized(&mut input, &MessageHeader::Type2 { timestamp_delta: 5 });
2137        assert!(matches!(
2138            assembler.push(&input),
2139            Err(RtmpError::Malformed { .. })
2140        ));
2141    }
2142
2143    #[test]
2144    fn assembler_set_chunk_size_zero_is_floored_to_one() {
2145        let mut assembler = ChunkAssembler::new();
2146        assembler.set_chunk_size(0);
2147        assert_eq!(assembler.chunk_size, 1, "floored at 1, not stuck at 0");
2148
2149        // A message chunked consistently with the floored size (1 payload
2150        // byte per physical chunk) still assembles correctly — the floor
2151        // makes progress possible rather than wedging on every push.
2152        let csid = 41;
2153        let mut input = Vec::new();
2154        write_serialized(
2155            &mut input,
2156            &BasicHeader {
2157                fmt: Fmt::Type0,
2158                chunk_stream_id: csid,
2159            },
2160        );
2161        write_serialized(
2162            &mut input,
2163            &MessageHeader::Type0 {
2164                timestamp: 1,
2165                message_length: 3,
2166                message_type_id: 8,
2167                message_stream_id: 0,
2168            },
2169        );
2170        input.push(0xAA);
2171        write_serialized(
2172            &mut input,
2173            &BasicHeader {
2174                fmt: Fmt::Type3,
2175                chunk_stream_id: csid,
2176            },
2177        );
2178        input.push(0xBB);
2179        write_serialized(
2180            &mut input,
2181            &BasicHeader {
2182                fmt: Fmt::Type3,
2183                chunk_stream_id: csid,
2184            },
2185        );
2186        input.push(0xCC);
2187
2188        let out = assembler.push(&input).unwrap();
2189        assert_eq!(out.len(), 1);
2190        assert_eq!(out[0].payload, vec![0xAA, 0xBB, 0xCC]);
2191    }
2192
2193    // ── Remote-DoS caps (excessive-allocation guard) ────────────────────
2194
2195    /// A complete, well-formed one-chunk message on `csid`: a Type 0 header
2196    /// declaring `message_length == payload.len()`, immediately followed by
2197    /// `payload` (so it fits in the default 128-byte chunk size and
2198    /// completes in a single chunk).
2199    fn single_chunk(csid: u32, payload: &[u8]) -> Vec<u8> {
2200        let mut input = Vec::new();
2201        write_serialized(
2202            &mut input,
2203            &BasicHeader {
2204                fmt: Fmt::Type0,
2205                chunk_stream_id: csid,
2206            },
2207        );
2208        write_serialized(
2209            &mut input,
2210            &MessageHeader::Type0 {
2211                timestamp: 0,
2212                message_length: payload.len() as u32,
2213                message_type_id: 9,
2214                message_stream_id: 1,
2215            },
2216        );
2217        input.extend_from_slice(payload);
2218        input
2219    }
2220
2221    #[test]
2222    fn oversized_message_length_header_is_rejected_without_allocating() {
2223        // Mutation check: a Type 0 header claims a ~16 MiB message_length
2224        // (the max a 24-bit field can encode) but only ever supplies a
2225        // single default-chunk-size (128-byte) slice of payload after it —
2226        // exactly the shape of the excessive-allocation DoS (attacker never
2227        // has to send anywhere near the claimed length). Without the
2228        // MAX_MESSAGE_LEN cap this used to `Vec::with_capacity(message_length)`
2229        // (~16 MiB) right here and return `Ok(vec![])` (message merely
2230        // in-progress, no error) — this test would then fail, since it
2231        // asserts an `Err` instead.
2232        let mut assembler = ChunkAssembler::new();
2233        let mut input = Vec::new();
2234        write_serialized(
2235            &mut input,
2236            &BasicHeader {
2237                fmt: Fmt::Type0,
2238                chunk_stream_id: 4,
2239            },
2240        );
2241        write_serialized(
2242            &mut input,
2243            &MessageHeader::Type0 {
2244                timestamp: 0,
2245                message_length: 0x00FF_FFFF, // ~16 MiB: the largest 24-bit value.
2246                message_type_id: 9,
2247                message_stream_id: 1,
2248            },
2249        );
2250        input.extend(std::iter::repeat_n(0u8, DEFAULT_CHUNK_SIZE as usize));
2251
2252        let err = assembler.push(&input).expect_err(
2253            "a message_length beyond MAX_MESSAGE_LEN must be rejected before any \
2254             message_length-sized buffer is allocated",
2255        );
2256        assert!(matches!(err, RtmpError::Malformed { .. }));
2257    }
2258
2259    #[test]
2260    fn message_length_at_the_cap_is_accepted() {
2261        // Boundary check: exactly MAX_MESSAGE_LEN must still be accepted
2262        // (only values strictly above the cap are rejected).
2263        let mut assembler = ChunkAssembler::new();
2264        let mut input = Vec::new();
2265        write_serialized(
2266            &mut input,
2267            &BasicHeader {
2268                fmt: Fmt::Type0,
2269                chunk_stream_id: 4,
2270            },
2271        );
2272        write_serialized(
2273            &mut input,
2274            &MessageHeader::Type0 {
2275                timestamp: 0,
2276                message_length: MAX_MESSAGE_LEN,
2277                message_type_id: 9,
2278                message_stream_id: 1,
2279            },
2280        );
2281        input.extend(std::iter::repeat_n(0u8, DEFAULT_CHUNK_SIZE as usize));
2282
2283        // Not yet complete (only one chunk of a much larger message has
2284        // arrived) but must not be rejected outright.
2285        assert!(assembler.push(&input).is_ok());
2286    }
2287
2288    #[test]
2289    fn csid_flood_beyond_max_csids_is_rejected() {
2290        // Mutation check: fill the bound with MAX_CSIDS distinct,
2291        // well-formed chunk streams first (none of these may error — the
2292        // cap must not reject legitimate, moderate csid usage), then assert
2293        // that one more previously-unseen csid is rejected rather than
2294        // silently growing the per-csid state map without bound. Without
2295        // the MAX_CSIDS cap this last `push` would also return `Ok(_)`,
2296        // failing this test's `Err` assertion.
2297        let mut assembler = ChunkAssembler::new();
2298        for i in 0..MAX_CSIDS {
2299            let csid = BASIC_HEADER_1BYTE_MIN_CSID + i as u32;
2300            let out = assembler
2301                .push(&single_chunk(csid, &[0xAB]))
2302                .unwrap_or_else(|e| panic!("csid {csid} (#{i}, within the bound) rejected: {e}"));
2303            assert_eq!(out.len(), 1);
2304        }
2305
2306        let flood_csid = BASIC_HEADER_1BYTE_MIN_CSID + MAX_CSIDS as u32;
2307        let err = assembler
2308            .push(&single_chunk(flood_csid, &[0xCD]))
2309            .expect_err("a new csid beyond MAX_CSIDS must be rejected, not silently accepted");
2310        assert!(matches!(err, RtmpError::Malformed { .. }));
2311    }
2312
2313    #[test]
2314    fn csid_flood_cap_does_not_count_repeats_of_the_same_csid() {
2315        // A single csid reused for many messages must never itself trip the
2316        // MAX_CSIDS cap (the bound is on distinct concurrent csids, not on
2317        // total message count).
2318        let mut assembler = ChunkAssembler::new();
2319        for i in 0..(MAX_CSIDS * 4) {
2320            let out = assembler
2321                .push(&single_chunk(BASIC_HEADER_1BYTE_MIN_CSID, &[i as u8]))
2322                .expect("repeated use of a single already-known csid must never be rejected");
2323            assert_eq!(out.len(), 1);
2324        }
2325    }
2326
2327    #[test]
2328    fn set_chunk_size_is_capped_at_max_chunk_size() {
2329        let mut assembler = ChunkAssembler::new();
2330        assembler.set_chunk_size(u32::MAX);
2331        assert_eq!(assembler.chunk_size, MAX_CHUNK_SIZE);
2332
2333        let mut writer = ChunkWriter::new();
2334        writer.set_chunk_size(u32::MAX);
2335        assert_eq!(writer.chunk_size, MAX_CHUNK_SIZE);
2336    }
2337}