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