Skip to main content

mpeg_ts/
ts.rs

1//! MPEG-TS packet parser and section reassembler — ITU-T H.222.0 §2.4 (= ISO/IEC 13818-1).
2
3use crate::error::{Error, Result};
4
5/// Size of one MPEG-TS packet (ETSI EN 300 468 §3.2, ISO/IEC 13818-1 §2.4.3.2).
6pub const TS_PACKET_SIZE: usize = 188;
7/// Sync byte that every TS packet starts with (ISO/IEC 13818-1 §2.4.3.3).
8pub const TS_SYNC_BYTE: u8 = 0x47;
9/// Upper bound on a single section: `section_length` is 12 bits (max 4095)
10/// plus the 3-byte header = 4098. (Long-form SI caps `section_length` at
11/// 4093 → total 4096, but maximal short-form private sections may reach
12/// 4098; the reassembler accepts the absolute ceiling.)
13const MAX_SECTION_SIZE: usize = 4098;
14/// Bytes before the `section_length` payload: `table_id` (1) + the two bytes
15/// carrying the syntax/RFU flags and the 12-bit `section_length`
16/// (ISO/IEC 13818-1 §2.4.4.1).
17const SECTION_HEADER_LEN: usize = 3;
18/// Mask for the 4 most-significant `section_length` bits in a section's second
19/// byte (ISO/IEC 13818-1 §2.4.4.1 — `section_length` is 12 bits). Shared with
20/// the packetiser in `mux.rs`.
21pub(crate) const SECTION_LENGTH_HI_MASK: u8 = 0x0F;
22
23/// Decode the 12-bit `section_length` field from a section's first 3 bytes
24/// (byte 1 bits 3-0 hi, byte 2 lo) and return the section's **total** size:
25/// `section_length + SECTION_HEADER_LEN`, the 3-byte common header plus every
26/// declared byte after it (ISO/IEC 13818-1 §2.4.4.1). Returns `None` if
27/// `bytes` has fewer than `SECTION_HEADER_LEN` bytes.
28///
29/// W14 (#1074): this decode used to be written out separately in four places
30/// (twice in this file, once in `mux.rs`, once in `section.rs`).
31pub(crate) fn section_total_len(bytes: &[u8]) -> Option<usize> {
32    if bytes.len() < SECTION_HEADER_LEN {
33        return None;
34    }
35    let section_length = ((bytes[1] & SECTION_LENGTH_HI_MASK) as usize) << 8 | bytes[2] as usize;
36    Some(SECTION_HEADER_LEN + section_length)
37}
38
39/// 2-bit `transport_scrambling_control` field — ITU-T H.222.0 (08/2023) Table 2-4
40/// (defines only `00` = not scrambled); DVB assigns `01`/`10`/`11` in ETSI TS 100 289
41/// V1.1.1 §5.1 Table 1 (reserved, even CW, odd CW).
42///
43/// MPEG-2 leaves `01`/`10`/`11` as user-defined; DVB's common-scrambling convention
44/// assigns `10` = even control word, `11` = odd control word, `01` = reserved for future
45/// DVB use. The field lives in the TS header and is never applied to the header itself —
46/// only to the packet payload.
47#[non_exhaustive]
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize))]
50pub enum ScramblingControl {
51    /// `00` — not scrambled. The only MPEG-2-defined value (H.222.0 Table 2-4).
52    NotScrambled,
53    /// `01` — reserved for future DVB use (ETSI TS 100 289 V1.1.1 §5.1 Table 1).
54    Reserved,
55    /// `10` — TS packet payload scrambled with the **even** control word
56    /// (DVB common scrambling, ETSI TS 100 289 V1.1.1 §5.1 Table 1).
57    EvenKey,
58    /// `11` — TS packet payload scrambled with the **odd** control word
59    /// (DVB common scrambling, ETSI TS 100 289 V1.1.1 §5.1 Table 1).
60    OddKey,
61}
62
63impl ScramblingControl {
64    /// Decode from the 2-bit `transport_scrambling_control` value (masked to `[1:0]`).
65    pub fn from_bits(bits: u8) -> Self {
66        match bits & 0b11 {
67            0b00 => Self::NotScrambled,
68            0b01 => Self::Reserved,
69            0b10 => Self::EvenKey,
70            0b11 => Self::OddKey,
71            _ => unreachable!(),
72        }
73    }
74
75    /// Encode as the 2-bit `transport_scrambling_control` field value (`[1:0]`).
76    ///
77    /// The returned byte is in the range `0x00`–`0x03`; the caller shifts it
78    /// into position within the TS header byte 3 (H.222.0 Table 2-4).
79    pub fn to_bits(self) -> u8 {
80        match self {
81            Self::NotScrambled => 0b00,
82            Self::Reserved => 0b01,
83            Self::EvenKey => 0b10,
84            Self::OddKey => 0b11,
85        }
86    }
87
88    /// Short label for this value, per the #204 convention.
89    pub fn name(&self) -> &'static str {
90        match self {
91            Self::NotScrambled => "not_scrambled",
92            Self::Reserved => "reserved",
93            Self::EvenKey => "even_key",
94            Self::OddKey => "odd_key",
95        }
96    }
97}
98
99broadcast_common::impl_spec_display!(ScramblingControl);
100
101/// 2-bit `adaptation_field_control` field — ITU-T H.222.0 (08/2023) Table 2-5.
102///
103/// Decoders shall discard packets with value `00` (`Reserved`). Null packets use `01`
104/// (`PayloadOnly`). The two flags `has_adaptation`/`has_payload` on [`TsHeader`] carry
105/// the decoded booleans; this enum provides the typed composite view.
106#[non_exhaustive]
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize))]
109pub enum AdaptationFieldControl {
110    /// `00` — reserved for future use; decoders shall discard (H.222.0 Table 2-5).
111    Reserved,
112    /// `01` — no adaptation_field, payload only (H.222.0 Table 2-5).
113    PayloadOnly,
114    /// `10` — adaptation_field only, no payload (H.222.0 Table 2-5).
115    AdaptationOnly,
116    /// `11` — adaptation_field followed by payload (H.222.0 Table 2-5).
117    AdaptationAndPayload,
118}
119
120impl AdaptationFieldControl {
121    /// Derive from the two decoded boolean flags stored on [`TsHeader`].
122    pub fn from_flags(has_adaptation: bool, has_payload: bool) -> Self {
123        match (has_adaptation, has_payload) {
124            (false, false) => Self::Reserved,
125            (false, true) => Self::PayloadOnly,
126            (true, false) => Self::AdaptationOnly,
127            (true, true) => Self::AdaptationAndPayload,
128        }
129    }
130
131    /// Encode as the 2-bit `adaptation_field_control` field value (`[1:0]`).
132    ///
133    /// Bit 1 = adaptation present, bit 0 = payload present (H.222.0 Table 2-5).
134    /// The returned byte is in the range `0x00`–`0x03`; the caller shifts it
135    /// into bits `[5:4]` of TS header byte 3.
136    pub fn to_bits(self) -> u8 {
137        match self {
138            Self::Reserved => 0b00,
139            Self::PayloadOnly => 0b01,
140            Self::AdaptationOnly => 0b10,
141            Self::AdaptationAndPayload => 0b11,
142        }
143    }
144
145    /// Decode into the `(has_adaptation, has_payload)` flag pair stored on
146    /// [`TsHeader`]. Exact inverse of [`from_flags`](Self::from_flags).
147    pub fn to_flags(self) -> (bool, bool) {
148        match self {
149            Self::Reserved => (false, false),
150            Self::PayloadOnly => (false, true),
151            Self::AdaptationOnly => (true, false),
152            Self::AdaptationAndPayload => (true, true),
153        }
154    }
155
156    /// Short label for this value, per the #204 convention.
157    pub fn name(&self) -> &'static str {
158        match self {
159            Self::Reserved => "reserved",
160            Self::PayloadOnly => "payload_only",
161            Self::AdaptationOnly => "adaptation_only",
162            Self::AdaptationAndPayload => "adaptation_and_payload",
163        }
164    }
165}
166
167broadcast_common::impl_spec_display!(AdaptationFieldControl);
168
169/// ISO/IEC 13818-1 §2.4.3.3: transport header byte 1 bit 7 = tei (Transport Error Indicator).
170const TEI_MASK: u8 = 0x80;
171/// ISO/IEC 13818-1 §2.4.3.3: byte 1 bit 6 = pusi (Payload Unit Start Indicator).
172const PUSI_MASK: u8 = 0x40;
173/// ISO/IEC 13818-1 §2.4.3.3: byte 1 bits `[4:0]` = 13-bit PID (upper 5 bits).
174pub const PID_MASK_HI: u8 = 0x1F;
175/// ISO/IEC 13818-1 §2.4.3.3: byte 3 bits `[7:6]` = 2-bit scrambling control.
176pub const SCRAMBLING_MASK: u8 = 0xC0;
177/// ISO/IEC 13818-1 §2.4.3.3: byte 3 bit 5 = adaptation_field_control (bit 5 = 1 means adaptation present).
178pub const ADAPTATION_FLAG: u8 = 0x20;
179/// ISO/IEC 13818-1 §2.4.3.3: byte 3 bit 4 = adaptation_field_control (bit 4 = 1 means payload present).
180pub const PAYLOAD_FLAG: u8 = 0x10;
181/// ISO/IEC 13818-1 §2.4.3.3: byte 3 bits `[3:0]` = 4-bit continuity_counter.
182pub const CC_MASK: u8 = 0x0F;
183
184/// Parsed TS header — the 4-byte transport header fields.
185#[derive(Clone, Debug, PartialEq, Eq)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize))]
187pub struct TsHeader {
188    /// Transport Error Indicator — set by the demodulator when an
189    /// uncorrectable error is present in the packet.
190    pub tei: bool,
191    /// Payload Unit Start Indicator — first byte of the payload is a new
192    /// PES packet or PSI section header when set.
193    pub pusi: bool,
194    /// 13-bit Packet Identifier.
195    pub pid: u16,
196    /// 2-bit transport_scrambling_control (0 = not scrambled).
197    pub scrambling: u8,
198    /// Adaptation field present flag (adaptation_field_control bit 1).
199    pub has_adaptation: bool,
200    /// Payload present flag (adaptation_field_control bit 0).
201    pub has_payload: bool,
202    /// 4-bit continuity_counter (wraps 0..=15 per PID).
203    pub continuity_counter: u8,
204}
205
206/// Borrowed view into one 188-byte TS packet.
207///
208/// Serde: Serialize-only (re-parse from wire bytes to reconstruct). `raw` is
209/// excluded from the serialized form because it is redundant once the header
210/// has been parsed.
211#[derive(Clone, Debug)]
212#[cfg_attr(feature = "serde", derive(serde::Serialize))]
213pub struct TsPacket<'a> {
214    /// Parsed header fields.
215    pub header: TsHeader,
216    /// Slice into the packet's payload, or `None` when `has_payload == false`
217    /// or the adaptation field consumed the whole packet body.
218    pub payload: Option<&'a [u8]>,
219    /// The adaptation-field bytes (after the length byte). Internal capture
220    /// feeding [`adaptation_field`](Self::adaptation_field); not public.
221    #[cfg_attr(feature = "serde", serde(skip))]
222    adaptation: Option<&'a [u8]>,
223    /// The raw 188 bytes of the packet — kept for cheap forwarding.
224    #[cfg_attr(feature = "serde", serde(skip))]
225    pub raw: &'a [u8; TS_PACKET_SIZE],
226}
227
228impl TsHeader {
229    /// Parse a 4-byte TS transport header.
230    pub fn parse(raw4: &[u8]) -> Result<Self> {
231        if raw4.len() < 4 {
232            return Err(Error::BufferTooShort {
233                need: 4,
234                have: raw4.len(),
235                what: "TsHeader",
236            });
237        }
238        let b1 = raw4[1];
239        let b2 = raw4[2];
240        let b3 = raw4[3];
241
242        let tei = (b1 & TEI_MASK) != 0;
243        let pusi = (b1 & PUSI_MASK) != 0;
244        let pid = (((b1 & PID_MASK_HI) as u16) << 8) | (b2 as u16);
245        let scrambling = (b3 & SCRAMBLING_MASK) >> 6;
246        let has_adaptation = (b3 & ADAPTATION_FLAG) != 0;
247        let has_payload = (b3 & PAYLOAD_FLAG) != 0;
248        let continuity_counter = b3 & CC_MASK;
249
250        Ok(Self {
251            tei,
252            pusi,
253            pid,
254            scrambling,
255            has_adaptation,
256            has_payload,
257            continuity_counter,
258        })
259    }
260
261    /// Number of bytes written by [`serialize_into`](Self::serialize_into).
262    pub const fn serialized_len() -> usize {
263        4
264    }
265
266    /// Serialize this header into the first 4 bytes of `buf`.
267    pub fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
268        if buf.len() < 4 {
269            return Err(Error::OutputBufferTooSmall {
270                need: 4,
271                have: buf.len(),
272            });
273        }
274        buf[0] = TS_SYNC_BYTE;
275        buf[1] = 0;
276        if self.tei {
277            buf[1] |= TEI_MASK;
278        }
279        if self.pusi {
280            buf[1] |= PUSI_MASK;
281        }
282        buf[1] |= ((self.pid >> 8) as u8) & PID_MASK_HI;
283        buf[2] = self.pid.to_be_bytes()[1];
284        buf[3] = (self.scrambling << 6) & SCRAMBLING_MASK;
285        if self.has_adaptation {
286            buf[3] |= ADAPTATION_FLAG;
287        }
288        if self.has_payload {
289            buf[3] |= PAYLOAD_FLAG;
290        }
291        buf[3] |= self.continuity_counter & CC_MASK;
292        Ok(4)
293    }
294
295    /// Typed view of the 2-bit `transport_scrambling_control` field.
296    ///
297    /// See [`ScramblingControl`] for the spec citation (H.222.0 Table 2-4 +
298    /// ETSI TS 100 289 §5.1 Table 1).
299    pub fn scrambling_control(&self) -> ScramblingControl {
300        ScramblingControl::from_bits(self.scrambling)
301    }
302
303    /// Typed view of the `adaptation_field_control` 2-bit field, derived from the
304    /// `has_adaptation`/`has_payload` flags.
305    ///
306    /// See [`AdaptationFieldControl`] for the spec citation (H.222.0 Table 2-5).
307    pub fn adaptation_field_control(&self) -> AdaptationFieldControl {
308        AdaptationFieldControl::from_flags(self.has_adaptation, self.has_payload)
309    }
310}
311
312impl<'a> broadcast_common::Parse<'a> for TsHeader {
313    type Error = Error;
314
315    fn parse(bytes: &'a [u8]) -> Result<Self> {
316        TsHeader::parse(bytes)
317    }
318}
319
320impl broadcast_common::Serialize for TsHeader {
321    type Error = Error;
322
323    fn serialized_len(&self) -> usize {
324        TsHeader::serialized_len()
325    }
326
327    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
328        TsHeader::serialize_into(self, buf)
329    }
330}
331
332/// Bytes to skip from byte 4 (right after the 4-byte header) to reach the
333/// payload: `0` when there is no adaptation field, else
334/// `1 + adaptation_field_length` (the length byte itself, plus its declared
335/// body) — ISO/IEC 13818-1 §2.4.3.4. Does not itself validate that the
336/// declared length fits `pkt` (`TsPacket::parse` / `extract_ts_payload` do
337/// that with their own bounds checks on the resulting cursor).
338///
339/// W18 (#1074): shared by `TsPacket::parse` and `extract_ts_payload`, which
340/// used to walk this independently with its own `adaptation_field_control`
341/// bit-mask.
342fn adaptation_field_skip(pkt: &[u8], has_adaptation: bool) -> usize {
343    if !has_adaptation || pkt.len() <= 4 {
344        return 0;
345    }
346    let af_len = pkt[4] as usize;
347    1 + af_len
348}
349
350impl<'a> TsPacket<'a> {
351    /// Parse a single 188-byte TS packet from a buffer.
352    ///
353    /// Returns `Err(Error::InvalidSyncByte)` if the first byte is not `0x47`,
354    /// `Err(Error::BufferTooShort)` if fewer than 188 bytes, or `Ok` with
355    /// the parsed packet otherwise.
356    pub fn parse(buf: &'a [u8]) -> Result<Self> {
357        if buf.len() < TS_PACKET_SIZE {
358            return Err(Error::BufferTooShort {
359                need: TS_PACKET_SIZE,
360                have: buf.len(),
361                what: "TsPacket",
362            });
363        }
364        if buf[0] != TS_SYNC_BYTE {
365            return Err(Error::InvalidSyncByte { found: buf[0] });
366        }
367
368        let raw: &[u8; TS_PACKET_SIZE] =
369            buf[..TS_PACKET_SIZE]
370                .try_into()
371                .map_err(|_| Error::BufferTooShort {
372                    need: TS_PACKET_SIZE,
373                    have: buf.len(),
374                    what: "TsPacket::parse (array conversion)",
375                })?;
376
377        let header = TsHeader::parse(&raw[..4])?;
378
379        let mut cursor = 4usize;
380        let mut payload = None;
381        let mut adaptation = None;
382
383        // Capture the adaptation field if present, then skip it (the section
384        // path does not need it; decode lazily via `adaptation_field`).
385        //
386        // W6 (#1074): `adaptation_field_length` is only valid up to 183 (the
387        // 183 bytes remaining after the 4-byte header + this length byte,
388        // ISO/IEC 13818-1 §2.4.3.4). An oversized value used to be silently
389        // `.min()`-truncated to a shorter slice here, while
390        // `OwnedTsPacket::adaptation_field` rejected the same bytes as `None`
391        // — two views of one wire format disagreeing on a malformed AF.
392        // Reject (this crate's existing policy in `owned.rs`) rather than
393        // truncate: leave `adaptation` as `None` for an over-long field, same
394        // as `OwnedTsPacket`.
395        if header.has_adaptation && cursor < TS_PACKET_SIZE {
396            let af_len = raw[cursor] as usize;
397            let af_start = cursor + 1;
398            if af_len > 0 && af_start < TS_PACKET_SIZE && af_start + af_len <= TS_PACKET_SIZE {
399                adaptation = Some(&raw[af_start..af_start + af_len]);
400            }
401        }
402        // W18 (#1074): shared with `extract_ts_payload`, which used to
403        // re-derive this same "skip past the adaptation field" walk from
404        // scratch (with its own `adaptation_field_control` bit-mask) instead
405        // of reusing this one.
406        cursor += adaptation_field_skip(raw, header.has_adaptation);
407
408        if header.has_payload && cursor < TS_PACKET_SIZE {
409            payload = Some(&raw[cursor..]);
410        }
411
412        Ok(TsPacket {
413            header,
414            payload,
415            adaptation,
416            raw,
417        })
418    }
419
420    /// Decode the adaptation field, if present.
421    ///
422    /// Returns `None` when the packet carries no adaptation field, and
423    /// `Some(Err(..))` when a present field is truncated. Layout per
424    /// ISO/IEC 13818-1:2007 §2.4.3.4 (`mpeg-ts/docs/tables/h222_0/transport_packet.md`).
425    pub fn adaptation_field(&self) -> Option<crate::Result<AdaptationField<'a>>> {
426        self.adaptation.map(AdaptationField::parse)
427    }
428}
429
430// Adaptation-field flag bits, byte 0 (ISO/IEC 13818-1:2007 §2.4.3.4).
431pub(crate) const AF_DISCONTINUITY: u8 = 0x80;
432pub(crate) const AF_RANDOM_ACCESS: u8 = 0x40;
433pub(crate) const AF_ES_PRIORITY: u8 = 0x20;
434/// PCR present flag (bit 4 of adaptation field flags byte — §2.4.3.4).
435pub const AF_PCR_FLAG: u8 = 0x10;
436pub(crate) const AF_OPCR_FLAG: u8 = 0x08;
437pub(crate) const AF_SPLICING_FLAG: u8 = 0x04;
438pub(crate) const AF_TRANSPORT_PRIVATE_DATA_FLAG: u8 = 0x02;
439pub(crate) const AF_EXTENSION_FLAG: u8 = 0x01;
440/// Adaptation-field stuffing byte (ISO/IEC 13818-1:2007 §2.4.3.4 — `0xFF`).
441pub(crate) const AF_STUFFING_BYTE: u8 = 0xFF;
442/// PCR field byte 4 bits 6-1: six reserved bits, all set to `1`
443/// (ISO/IEC 13818-1 §2.4.3.5).
444const PCR_RESERVED_BITS: u8 = 0x7E;
445/// Encoded PCR / OPCR field width: 33-bit base + 6 reserved + 9-bit extension.
446pub(crate) const PCR_FIELD_LEN: usize = 6;
447
448/// Program Clock Reference (ISO/IEC 13818-1:2007 §2.4.3.5): a 33-bit base on a
449/// 90 kHz clock plus a 9-bit extension on a 27 MHz clock.
450#[derive(Clone, Copy, Debug, PartialEq, Eq)]
451#[cfg_attr(feature = "serde", derive(serde::Serialize))]
452pub struct Pcr {
453    /// 33-bit base (90 kHz units).
454    pub base: u64,
455    /// 9-bit extension (27 MHz units).
456    pub extension: u16,
457}
458
459impl Pcr {
460    /// Full PCR value on the 27 MHz clock: `base * 300 + extension`.
461    ///
462    /// ISO/IEC 13818-1:2007 §2.4.3.5: PCR = `PCR_base * 300 + PCR_ext`.
463    #[must_use]
464    pub fn as_27mhz(self) -> u64 {
465        self.base * 300 + self.extension as u64
466    }
467
468    /// Construct a [`Pcr`] from an absolute 27 MHz clock value.
469    ///
470    /// Decomposes `ticks` into `base = ticks / 300` and
471    /// `extension = ticks % 300`, clamping each to its wire width
472    /// (33-bit base, 9-bit extension) — ISO/IEC 13818-1:2007 §2.4.3.5.
473    ///
474    /// Round-trips with [`as_27mhz`](Self::as_27mhz):
475    /// `Pcr::from_27mhz(p.as_27mhz()) == p` for any valid `Pcr`.
476    #[must_use]
477    pub fn from_27mhz(ticks: u64) -> Self {
478        // 33-bit base mask: (1 << 33) - 1 = 0x1_FFFF_FFFF
479        const BASE_MASK: u64 = 0x1_FFFF_FFFF;
480        // 9-bit extension mask.
481        const EXT_MASK: u16 = 0x1FF;
482        Self {
483            base: (ticks / 300) & BASE_MASK,
484            extension: ((ticks % 300) as u16) & EXT_MASK,
485        }
486    }
487
488    /// Encode as the exact 6-byte PCR/OPCR field used in the adaptation field
489    /// (ISO/IEC 13818-1:2007 §2.4.3.5).
490    ///
491    /// Wire layout (big-endian, 48 bits):
492    /// `[base[32:25]][base[24:17]][base[16:9]][base[8:1]][base[0] | 6×reserved(1) | ext[8]][ext[7:0]]`
493    ///
494    /// The 6 reserved bits are set to `1` as per the spec's "reserved" convention.
495    /// Exact inverse of the private `parse` function.
496    #[must_use]
497    pub fn to_field_bytes(self) -> [u8; PCR_FIELD_LEN] {
498        let b = self.base;
499        let e = self.extension as u64;
500        [
501            ((b >> 25) & 0xFF) as u8,
502            ((b >> 17) & 0xFF) as u8,
503            ((b >> 9) & 0xFF) as u8,
504            ((b >> 1) & 0xFF) as u8,
505            // byte 4: base[0] in bit 7, bits 6-1 = reserved (set to 1), ext[8] in bit 0.
506            (((b & 0x01) as u8) << 7) | PCR_RESERVED_BITS | ((e >> 8) as u8 & 0x01),
507            (e & 0xFF) as u8,
508        ]
509    }
510
511    /// Decode the 6-byte PCR/OPCR field starting at `at` within `af`.
512    pub(crate) fn parse(af: &[u8], at: usize) -> Result<Self> {
513        let b: &[u8; PCR_FIELD_LEN] = af
514            .get(at..at + PCR_FIELD_LEN)
515            .and_then(|s| s.try_into().ok())
516            .ok_or(Error::BufferTooShort {
517                need: at + PCR_FIELD_LEN,
518                have: af.len(),
519                what: "adaptation_field PCR",
520            })?;
521        let base = ((b[0] as u64) << 25)
522            | ((b[1] as u64) << 17)
523            | ((b[2] as u64) << 9)
524            | ((b[3] as u64) << 1)
525            | ((b[4] as u64) >> 7);
526        let extension = (((b[4] & 0x01) as u16) << 8) | (b[5] as u16);
527        Ok(Self { base, extension })
528    }
529}
530
531// ── Adaptation-field extension sub-structures (ISO/IEC 13818-1 §2.4.3.5) ──────
532
533/// `legal_time_window` field within the adaptation-field extension
534/// (ISO/IEC 13818-1:2007 §2.4.3.5, `ltw_flag == 1`).
535///
536/// Wire layout: `ltw_valid_flag(1) | ltw_offset(15)` = 2 bytes.
537#[derive(Clone, Copy, Debug, PartialEq, Eq)]
538#[cfg_attr(feature = "serde", derive(serde::Serialize))]
539pub struct Ltw {
540    /// LTW offset valid flag.
541    pub ltw_valid_flag: bool,
542    /// 15-bit `ltw_offset` (lower bound of the legal time window).
543    pub ltw_offset: u16,
544}
545
546/// `seamless_splice` field within the adaptation-field extension
547/// (ISO/IEC 13818-1:2007 §2.4.3.5, `seamless_splice_flag == 1`).
548///
549/// Wire layout: 5 bytes — `splice_type(4) | DTS_next_AU[32:30](3) | marker(1) |
550/// DTS_next_AU[29:15](15) | marker(1) | DTS_next_AU[14:0](15) | marker(1)`.
551/// The DTS field uses the same marker-bit encoding as PTS/DTS in PES headers.
552#[derive(Clone, Copy, Debug, PartialEq, Eq)]
553#[cfg_attr(feature = "serde", derive(serde::Serialize))]
554pub struct SeamlessSplice {
555    /// 4-bit `splice_type`.
556    pub splice_type: u8,
557    /// 33-bit `DTS_next_AU` (90 kHz decoding time of the next splice unit).
558    pub dts_next_au: u64,
559}
560
561// W15 (#1074): named flag/mask bits for the adaptation-field extension's
562// flags byte and its LTW/piecewise-rate sub-fields (ISO/IEC 13818-1 §2.4.3.5).
563const AFE_LTW_FLAG: u8 = 0x80;
564const AFE_PIECEWISE_RATE_FLAG: u8 = 0x40;
565const AFE_SEAMLESS_SPLICE_FLAG: u8 = 0x20;
566/// `ltw_valid_flag` (1 bit) + `ltw_offset` high 7 bits share byte 0 of the LTW field.
567const LTW_OFFSET_HI_MASK: u8 = 0x7F;
568/// 2 reserved bits (set to `1` on serialize) + `piecewise_rate` high 6 bits.
569const PIECEWISE_RATE_HI_MASK: u8 = 0x3F;
570const PIECEWISE_RATE_RESERVED_BITS: u8 = 0xC0;
571
572/// Adaptation-field extension (ISO/IEC 13818-1:2007 §2.4.3.5,
573/// `adaptation_field_extension_flag == 1`).
574///
575/// Contains optional sub-fields gated by `ltw_flag`,
576/// `piecewise_rate_flag`, and `seamless_splice_flag`.
577#[derive(Clone, Copy, Debug, PartialEq, Eq)]
578#[cfg_attr(feature = "serde", derive(serde::Serialize))]
579pub struct AdaptationFieldExtension {
580    /// LTW (legal time window), if `ltw_flag` is set.
581    pub ltw: Option<Ltw>,
582    /// 22-bit piecewise rate, if `piecewise_rate_flag` is set.
583    pub piecewise_rate: Option<u32>,
584    /// Seamless splice info, if `seamless_splice_flag` is set.
585    pub seamless_splice: Option<SeamlessSplice>,
586}
587
588impl AdaptationFieldExtension {
589    /// Parse the adaptation-field extension starting at `data[0]`
590    /// (the `adaptation_field_extension_length` byte).
591    fn parse(data: &[u8]) -> Result<Self> {
592        if data.is_empty() {
593            return Err(Error::BufferTooShort {
594                need: 1,
595                have: 0,
596                what: "adaptation_field_extension_length",
597            });
598        }
599        let ext_len = data[0] as usize;
600        // At least 1 byte (the flags byte) must be present inside the extension.
601        if data.len() < 1 + ext_len || ext_len < 1 {
602            return Err(Error::BufferTooShort {
603                need: 2.max(1 + ext_len),
604                have: data.len(),
605                what: "adaptation_field_extension body",
606            });
607        }
608        let ext = &data[1..1 + ext_len]; // extension bytes, starts with flags byte
609        let flags = ext[0];
610        let mut cursor = 1usize;
611
612        let ltw = if flags & AFE_LTW_FLAG != 0 {
613            if ext.len() < cursor + 2 {
614                return Err(Error::BufferTooShort {
615                    need: cursor + 2,
616                    have: ext.len(),
617                    what: "ltw_offset",
618                });
619            }
620            let w0 = ext[cursor];
621            let w1 = ext[cursor + 1];
622            cursor += 2;
623            Some(Ltw {
624                ltw_valid_flag: (w0 & AFE_LTW_FLAG) != 0,
625                ltw_offset: (((w0 & LTW_OFFSET_HI_MASK) as u16) << 8) | (w1 as u16),
626            })
627        } else {
628            None
629        };
630
631        let piecewise_rate = if flags & AFE_PIECEWISE_RATE_FLAG != 0 {
632            if ext.len() < cursor + 3 {
633                return Err(Error::BufferTooShort {
634                    need: cursor + 3,
635                    have: ext.len(),
636                    what: "piecewise_rate",
637                });
638            }
639            let r = (((ext[cursor] & PIECEWISE_RATE_HI_MASK) as u32) << 16)
640                | ((ext[cursor + 1] as u32) << 8)
641                | (ext[cursor + 2] as u32);
642            cursor += 3;
643            Some(r)
644        } else {
645            None
646        };
647
648        // seamless_splice: 5 bytes with splice_type(4) + DTS_next_AU in PTS-field encoding
649        let seamless_splice = if flags & AFE_SEAMLESS_SPLICE_FLAG != 0 {
650            if ext.len() < cursor + 5 {
651                return Err(Error::BufferTooShort {
652                    need: cursor + 5,
653                    have: ext.len(),
654                    what: "seamless_splice DTS_next_AU",
655                });
656            }
657            let b = &ext[cursor..cursor + 5];
658            let splice_type = (b[0] >> 4) & 0x0F;
659            // DTS_next_AU uses the same 5-byte marker-bit encoding as PTS/DTS.
660            let hi = u64::from((b[0] >> 1) & 0x07); // [32:30]
661            let mid = (u64::from(b[1]) << 7) | u64::from(b[2] >> 1); // [29:15]
662            let lo = (u64::from(b[3]) << 7) | u64::from(b[4] >> 1); // [14:0]
663            let dts_next_au = (hi << 30) | (mid << 15) | lo;
664            cursor += 5;
665            Some(SeamlessSplice {
666                splice_type,
667                dts_next_au,
668            })
669        } else {
670            None
671        };
672        let _ = cursor; // remaining extension bytes (reserved) are skipped
673
674        Ok(AdaptationFieldExtension {
675            ltw,
676            piecewise_rate,
677            seamless_splice,
678        })
679    }
680
681    /// Number of bytes written by [`serialize_into`](Self::serialize_into),
682    /// **including** the leading `adaptation_field_extension_length` byte.
683    #[must_use]
684    pub fn serialized_len(&self) -> usize {
685        let body = 1 // flags byte
686            + self.ltw.map_or(0, |_| 2)
687            + self.piecewise_rate.map_or(0, |_| 3)
688            + self.seamless_splice.map_or(0, |_| 5);
689        1 + body // + length byte itself
690    }
691
692    /// Serialize into `buf` (includes the `adaptation_field_extension_length` byte).
693    pub fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
694        let need = self.serialized_len();
695        if buf.len() < need {
696            return Err(Error::OutputBufferTooSmall {
697                need,
698                have: buf.len(),
699            });
700        }
701        let body_len = need - 1;
702        buf[0] = body_len as u8;
703
704        let mut flags = 0u8;
705        if self.ltw.is_some() {
706            flags |= AFE_LTW_FLAG;
707        }
708        if self.piecewise_rate.is_some() {
709            flags |= AFE_PIECEWISE_RATE_FLAG;
710        }
711        if self.seamless_splice.is_some() {
712            flags |= AFE_SEAMLESS_SPLICE_FLAG;
713        }
714        buf[1] = flags;
715        let mut cursor = 2usize;
716
717        if let Some(ltw) = self.ltw {
718            let ltw_valid = if ltw.ltw_valid_flag {
719                AFE_LTW_FLAG
720            } else {
721                0x00
722            };
723            buf[cursor] = ltw_valid | ((ltw.ltw_offset >> 8) as u8 & LTW_OFFSET_HI_MASK);
724            buf[cursor + 1] = (ltw.ltw_offset & 0xFF) as u8;
725            cursor += 2;
726        }
727        if let Some(rate) = self.piecewise_rate {
728            // 2 reserved bits (set to 1) | 22-bit rate (ISO/IEC 13818-1 §2.4.3.5).
729            buf[cursor] =
730                PIECEWISE_RATE_RESERVED_BITS | ((rate >> 16) as u8 & PIECEWISE_RATE_HI_MASK);
731            buf[cursor + 1] = (rate >> 8) as u8;
732            buf[cursor + 2] = rate as u8;
733            cursor += 3;
734        }
735        if let Some(ss) = self.seamless_splice {
736            let ts = ss.dts_next_au & 0x1_FFFF_FFFF;
737            let st = ss.splice_type & 0x0F;
738            // byte 0: splice_type(4) | DTS[32:30](3) | marker(1)
739            buf[cursor] = (st << 4) | ((((ts >> 30) & 0x07) as u8) << 1) | 0x01;
740            buf[cursor + 1] = ((ts >> 22) & 0xFF) as u8;
741            buf[cursor + 2] = ((((ts >> 15) & 0x7F) as u8) << 1) | 0x01;
742            buf[cursor + 3] = ((ts >> 7) & 0xFF) as u8;
743            buf[cursor + 4] = (((ts & 0x7F) as u8) << 1) | 0x01;
744            cursor += 5;
745        }
746        Ok(cursor)
747    }
748}
749
750/// Decoded adaptation field — full §2.4.3.4 layout including transport-private
751/// data and the adaptation-field extension sub-structure.
752///
753/// The `transport_private_data` slice borrows from the original packet buffer
754/// (it is genuinely opaque caller-defined bytes per the spec — `&[u8]` is the
755/// correct public type). All other fields are fully typed.
756///
757/// ISO/IEC 13818-1:2007 §2.4.3.4.
758#[non_exhaustive]
759#[derive(Clone, Debug, PartialEq, Eq)]
760#[cfg_attr(feature = "serde", derive(serde::Serialize))]
761pub struct AdaptationField<'a> {
762    /// A timing/continuity discontinuity starts at this packet.
763    pub discontinuity_indicator: bool,
764    /// This packet is a random-access point.
765    pub random_access_indicator: bool,
766    /// Elementary-stream priority hint.
767    pub elementary_stream_priority_indicator: bool,
768    /// Program Clock Reference, present iff the PCR flag is set.
769    pub pcr: Option<Pcr>,
770    /// Original PCR, present iff the OPCR flag is set.
771    pub opcr: Option<Pcr>,
772    /// Splice countdown (packets until the splice point), iff the flag is set.
773    pub splice_countdown: Option<i8>,
774    /// Opaque transport-private data (caller-defined; `&[u8]` is spec-correct here).
775    ///
776    /// Present iff `transport_private_data_flag` is set in the flags byte
777    /// (ISO/IEC 13818-1:2007 §2.4.3.4).
778    pub transport_private_data: Option<&'a [u8]>,
779    /// Typed adaptation-field extension sub-structure, if the flag is set.
780    pub extension: Option<AdaptationFieldExtension>,
781    /// Number of trailing `0xFF` stuffing bytes that pad the adaptation-field
782    /// body out to its declared `adaptation_field_length` (ISO/IEC 13818-1:2007
783    /// §2.4.3.4 — "stuffing_byte: fixed 8-bit value `0xFF`").
784    ///
785    /// Real encoders pad the adaptation field so the packet payload begins at a
786    /// fixed offset; these bytes are part of the wire image. Capturing the count
787    /// lets [`serialize_into`](Self::serialize_into) reproduce the packet
788    /// byte-for-byte. Set to `0` when constructing an adaptation field with no
789    /// stuffing.
790    pub stuffing_len: usize,
791}
792
793impl<'a> AdaptationField<'a> {
794    /// Parse the adaptation-field bytes (those following the length byte).
795    ///
796    /// `af` must be exactly the adaptation-field body bytes: the slice starts
797    /// at the **flags byte** (byte 0 of `af`), i.e. the bytes AFTER the
798    /// `adaptation_field_length` byte itself. This matches how
799    /// `TsPacket::parse` captures and hands them off.
800    pub(crate) fn parse(af: &'a [u8]) -> Result<Self> {
801        let flags = *af.first().ok_or(Error::BufferTooShort {
802            need: 1,
803            have: 0,
804            what: "adaptation_field flags",
805        })?;
806        let mut cursor = 1usize;
807
808        let pcr = if flags & AF_PCR_FLAG != 0 {
809            let p = Pcr::parse(af, cursor)?;
810            cursor += PCR_FIELD_LEN;
811            Some(p)
812        } else {
813            None
814        };
815        let opcr = if flags & AF_OPCR_FLAG != 0 {
816            let p = Pcr::parse(af, cursor)?;
817            cursor += PCR_FIELD_LEN;
818            Some(p)
819        } else {
820            None
821        };
822        let splice_countdown = if flags & AF_SPLICING_FLAG != 0 {
823            let b = *af.get(cursor).ok_or(Error::BufferTooShort {
824                need: cursor + 1,
825                have: af.len(),
826                what: "adaptation_field splice_countdown",
827            })?;
828            cursor += 1;
829            Some(b as i8)
830        } else {
831            None
832        };
833
834        // transport_private_data (ISO/IEC 13818-1 §2.4.3.4):
835        // transport_private_data_length(8) + transport_private_data_byte * N
836        let transport_private_data = if flags & AF_TRANSPORT_PRIVATE_DATA_FLAG != 0 {
837            let tpd_len = *af.get(cursor).ok_or(Error::BufferTooShort {
838                need: cursor + 1,
839                have: af.len(),
840                what: "transport_private_data_length",
841            })? as usize;
842            cursor += 1;
843            let end = cursor + tpd_len;
844            let slice = af.get(cursor..end).ok_or(Error::BufferTooShort {
845                need: end,
846                have: af.len(),
847                what: "transport_private_data",
848            })?;
849            cursor = end;
850            Some(slice)
851        } else {
852            None
853        };
854
855        // adaptation_field_extension (ISO/IEC 13818-1 §2.4.3.5):
856        let extension = if flags & AF_EXTENSION_FLAG != 0 {
857            let ext_data = af.get(cursor..).ok_or(Error::BufferTooShort {
858                need: cursor + 1,
859                have: af.len(),
860                what: "adaptation_field_extension",
861            })?;
862            let ext = AdaptationFieldExtension::parse(ext_data)?;
863            // advance cursor past the extension (ext_data[0] = length byte)
864            if !ext_data.is_empty() {
865                let _ext_len = ext_data[0] as usize;
866                cursor += 1 + _ext_len;
867            }
868            Some(ext)
869        } else {
870            None
871        };
872
873        // Any bytes after the last present field, up to `adaptation_field_length`
874        // (= `af.len()`), are `0xFF` stuffing (ISO/IEC 13818-1:2007 §2.4.3.4).
875        // Record the count so serialization reproduces the body byte-for-byte.
876        let stuffing_len = af.len().saturating_sub(cursor);
877
878        Ok(AdaptationField {
879            discontinuity_indicator: flags & AF_DISCONTINUITY != 0,
880            random_access_indicator: flags & AF_RANDOM_ACCESS != 0,
881            elementary_stream_priority_indicator: flags & AF_ES_PRIORITY != 0,
882            pcr,
883            opcr,
884            splice_countdown,
885            transport_private_data,
886            extension,
887            stuffing_len,
888        })
889    }
890
891    /// Number of bytes written by [`serialize_into`](Self::serialize_into).
892    ///
893    /// This is the body length **excluding** the leading `adaptation_field_length`
894    /// byte — it is the value carried in that length byte itself
895    /// (ISO/IEC 13818-1:2007 §2.4.3.4).
896    #[must_use]
897    pub fn serialized_len(&self) -> usize {
898        let mut n = 1usize; // flags byte
899        if self.pcr.is_some() {
900            n += PCR_FIELD_LEN;
901        }
902        if self.opcr.is_some() {
903            n += PCR_FIELD_LEN;
904        }
905        if self.splice_countdown.is_some() {
906            n += 1;
907        }
908        if let Some(tpd) = self.transport_private_data {
909            n += 1 + tpd.len(); // length byte + data
910        }
911        if let Some(ref ext) = self.extension {
912            n += ext.serialized_len();
913        }
914        n += self.stuffing_len;
915        n
916    }
917
918    /// Serialize the adaptation field into `buf`.
919    ///
920    /// Writes the **body** bytes — the flags byte plus optional fields in the
921    /// order specified by ISO/IEC 13818-1:2007 §2.4.3.4. The
922    /// `adaptation_field_length` byte itself is **not** written here; the caller
923    /// must prepend it (it equals `serialized_len()`).
924    ///
925    /// Returns the number of bytes written on success, or
926    /// [`Error::OutputBufferTooSmall`] if `buf` is shorter than
927    /// `serialized_len()`.
928    pub fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
929        let need = self.serialized_len();
930        if buf.len() < need {
931            return Err(Error::OutputBufferTooSmall {
932                need,
933                have: buf.len(),
934            });
935        }
936
937        // Byte 0: flags (ISO/IEC 13818-1:2007 §2.4.3.4).
938        let mut flags = 0u8;
939        if self.discontinuity_indicator {
940            flags |= AF_DISCONTINUITY;
941        }
942        if self.random_access_indicator {
943            flags |= AF_RANDOM_ACCESS;
944        }
945        if self.elementary_stream_priority_indicator {
946            flags |= AF_ES_PRIORITY;
947        }
948        if self.pcr.is_some() {
949            flags |= AF_PCR_FLAG;
950        }
951        if self.opcr.is_some() {
952            flags |= AF_OPCR_FLAG;
953        }
954        if self.splice_countdown.is_some() {
955            flags |= AF_SPLICING_FLAG;
956        }
957        if self.transport_private_data.is_some() {
958            flags |= AF_TRANSPORT_PRIVATE_DATA_FLAG;
959        }
960        if self.extension.is_some() {
961            flags |= AF_EXTENSION_FLAG;
962        }
963        buf[0] = flags;
964
965        let mut cursor = 1usize;
966        if let Some(pcr) = self.pcr {
967            buf[cursor..cursor + PCR_FIELD_LEN].copy_from_slice(&pcr.to_field_bytes());
968            cursor += PCR_FIELD_LEN;
969        }
970        if let Some(opcr) = self.opcr {
971            buf[cursor..cursor + PCR_FIELD_LEN].copy_from_slice(&opcr.to_field_bytes());
972            cursor += PCR_FIELD_LEN;
973        }
974        if let Some(sc) = self.splice_countdown {
975            buf[cursor] = sc as u8;
976            cursor += 1;
977        }
978        if let Some(tpd) = self.transport_private_data {
979            buf[cursor] = broadcast_common::len::fit_u8(
980                tpd.len(),
981                "adaptation_field.transport_private_data_length",
982            )?;
983            cursor += 1;
984            buf[cursor..cursor + tpd.len()].copy_from_slice(tpd);
985            cursor += tpd.len();
986        }
987        if let Some(ref ext) = self.extension {
988            let written = ext.serialize_into(&mut buf[cursor..])?;
989            cursor += written;
990        }
991
992        // Trailing `0xFF` stuffing (ISO/IEC 13818-1:2007 §2.4.3.4), reproducing
993        // the padding the encoder used to fill `adaptation_field_length`.
994        for b in buf[cursor..cursor + self.stuffing_len].iter_mut() {
995            *b = AF_STUFFING_BYTE;
996        }
997        cursor += self.stuffing_len;
998
999        Ok(cursor)
1000    }
1001}
1002
1003/// Reassembles PSI/SI sections from TS packets on a single PID.
1004///
1005/// Feed each TS packet's payload with `feed`. Complete sections are
1006/// appended to an internal queue; drain them with `pop_section`.
1007#[derive(Default)]
1008pub struct SectionReassembler {
1009    buf: bytes::BytesMut,
1010    ready: alloc::collections::VecDeque<bytes::Bytes>,
1011}
1012
1013impl SectionReassembler {
1014    /// Feed a TS payload and whether its packet had PUSI set.
1015    ///
1016    /// Extracts complete SI sections into the internal queue. A single call
1017    /// can produce zero, one, or **several** sections — a payload may
1018    /// concatenate multiple complete sections after the `pointer_field`
1019    /// (EN 300 468 §5.1.4; common on EMM PIDs). Drain with a
1020    /// `while let Some(s) = r.pop_section()` loop, not a single `if let`.
1021    pub fn feed(&mut self, payload: &[u8], pusi: bool) {
1022        if pusi {
1023            // A PUSI packet whose adaptation field consumed the whole body is
1024            // malformed but constructible — drop sync rather than panic.
1025            if payload.is_empty() {
1026                self.buf.clear();
1027                return;
1028            }
1029            let pointer = payload[0] as usize;
1030
1031            // The `pointer_field` counts bytes that belong to a section still
1032            // in progress from a previous packet (ISO/IEC 13818-1 §2.4.4): the
1033            // `pointer` bytes immediately after it are that section's tail and
1034            // must complete it BEFORE new sections begin at `1 + pointer`.
1035            // Skipping them (or clearing `buf` first) drops any section that
1036            // spans into a PUSI packet — silent loss biased toward whichever
1037            // section happens to straddle a packet boundary.
1038            if !self.buf.is_empty() && pointer > 0 {
1039                let avail = payload.len() - 1;
1040                let tail_len = pointer.min(avail);
1041                if self.buf.len() + tail_len > MAX_SECTION_SIZE {
1042                    self.buf.clear();
1043                } else {
1044                    self.buf.extend_from_slice(&payload[1..1 + tail_len]);
1045                    self.drain_complete_sections();
1046                }
1047            }
1048
1049            // New sections start at `1 + pointer`; anything still buffered is
1050            // an incomplete (corrupt / lost-packet) section — discard it.
1051            self.buf.clear();
1052
1053            let start = 1 + pointer;
1054            if start >= payload.len() {
1055                // Pointer spans to (or past) the end — no new section here.
1056                return;
1057            }
1058            let new_data = &payload[start..];
1059            if new_data.len() > MAX_SECTION_SIZE {
1060                return;
1061            }
1062            self.buf.extend_from_slice(new_data);
1063        } else {
1064            if self.buf.is_empty() {
1065                return;
1066            }
1067            // Append only the bytes the in-progress section still needs. A new
1068            // section cannot start in a continuation (non-PUSI) packet
1069            // (ISO/IEC 13818-1 §2.4.4), so once the section's declared length
1070            // is satisfied the remaining payload bytes are 0xFF stuffing and
1071            // are ignored. Counting that stuffing toward `MAX_SECTION_SIZE`
1072            // previously dropped valid near-maximal sections (#148). Because
1073            // the 12-bit `section_length` caps a section at `MAX_SECTION_SIZE`,
1074            // `take` is inherently bounded and the buffer cannot grow without
1075            // limit.
1076            let take = if let Some(exp) = section_total_len(&self.buf) {
1077                exp.saturating_sub(self.buf.len()).min(payload.len())
1078            } else {
1079                // Header not yet complete (split across the packet boundary) —
1080                // take enough to read `section_length` on the next drain,
1081                // bounded by the maximum possible section size.
1082                payload.len().min(MAX_SECTION_SIZE - self.buf.len())
1083            };
1084            self.buf.extend_from_slice(&payload[..take]);
1085        }
1086
1087        self.drain_complete_sections();
1088    }
1089
1090    /// Queue every complete section the buffer currently holds.
1091    ///
1092    /// A single TS payload may concatenate multiple complete sections after
1093    /// the `pointer_field` (legal per ETSI EN 300 468 §5.1.4 and common on
1094    /// EMM PIDs, which pack several short messages into one payload). We must
1095    /// keep extracting until the buffer holds only a partial (multi-packet
1096    /// spanning) section, whose bytes stay buffered for the next packet to
1097    /// continue (the expected length is recomputed from the section header on
1098    /// each drain). A `0xFF` where a `table_id` is expected marks the rest of
1099    /// the payload as stuffing.
1100    fn drain_complete_sections(&mut self) {
1101        loop {
1102            if self.buf.len() < SECTION_HEADER_LEN {
1103                // Not enough for a section header yet; keep the partial bytes
1104                // and wait for the next packet to complete the header.
1105                break;
1106            }
1107            if self.buf[0] == 0xFF {
1108                // Stuffing where a table_id is expected — payload tail is fill.
1109                self.buf.clear();
1110                break;
1111            }
1112            let exp =
1113                section_total_len(&self.buf).expect("checked len >= SECTION_HEADER_LEN above");
1114            if self.buf.len() >= exp {
1115                // split_to returns the first `exp` bytes as an owned BytesMut,
1116                // leaving the remainder in self.buf — cheap (shifts pointers).
1117                let section = self.buf.split_to(exp).freeze();
1118                self.ready.push_back(section);
1119            } else {
1120                // Partial section spanning into later packets.
1121                break;
1122            }
1123        }
1124    }
1125
1126    /// Pop one complete section. Returns `None` when the queue is empty.
1127    pub fn pop_section(&mut self) -> Option<bytes::Bytes> {
1128        self.ready.pop_front()
1129    }
1130
1131    /// Number of bytes currently buffered (incomplete section).
1132    pub fn len(&self) -> usize {
1133        self.buf.len()
1134    }
1135
1136    /// True if no bytes are currently buffered.
1137    pub fn is_empty(&self) -> bool {
1138        self.buf.is_empty()
1139    }
1140}
1141
1142/// Iterate over all valid TS packets in a byte buffer.
1143///
1144/// Slices `buf` into 188-byte chunks (using [`slice::chunks_exact`]) and yields
1145/// each chunk for which [`TsPacket::parse`] succeeds. Chunks with a bad sync byte
1146/// (`!= 0x47`) or insufficient length are silently skipped — use
1147/// [`crate::resync::TsResync`] for byte-stream resynchronisation before calling
1148/// this when byte alignment is not guaranteed.
1149///
1150/// # Example
1151///
1152/// ```no_run
1153/// # use mpeg_ts::ts::iter_packets;
1154/// # let data: &[u8] = &[];
1155/// for pkt in iter_packets(data) {
1156///     println!("PID: 0x{:04X}", pkt.header.pid);
1157/// }
1158/// ```
1159pub fn iter_packets(buf: &[u8]) -> impl Iterator<Item = TsPacket<'_>> {
1160    buf.chunks_exact(TS_PACKET_SIZE)
1161        .filter_map(|chunk| TsPacket::parse(chunk).ok())
1162}
1163
1164/// Extract the payload bytes from a raw 188-byte TS packet slice.
1165///
1166/// Returns `None` when:
1167/// - `pkt` is fewer than 4 bytes,
1168/// - `adaptation_field_control` is `00` (reserved) or `10` (adaptation only), or
1169/// - the adaptation field length would place the payload start past the packet end.
1170///
1171/// No sync-byte check is performed — the caller is responsible for ensuring the
1172/// slice is properly aligned. Spec: ITU-T H.222.0 (08/2023) §2.4.3.3 Table 2-5.
1173pub fn extract_ts_payload(pkt: &[u8]) -> Option<&[u8]> {
1174    if pkt.len() < 4 {
1175        return None;
1176    }
1177    let header = TsHeader::parse(&pkt[..4]).ok()?;
1178    if !header.has_payload {
1179        return None;
1180    }
1181    let cursor = 4 + adaptation_field_skip(pkt, header.has_adaptation);
1182    if cursor < pkt.len() {
1183        Some(&pkt[cursor..])
1184    } else {
1185        None
1186    }
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191    use super::*;
1192    use alloc::string::ToString;
1193    use alloc::vec;
1194    use alloc::vec::Vec;
1195
1196    /// Helper: construct a minimal 188-byte TS packet buffer with given header flags and payload.
1197    fn make_packet(b1: u8, b2: u8, b3: u8, payload_data: &[u8]) -> [u8; TS_PACKET_SIZE] {
1198        let mut pkt = [0u8; TS_PACKET_SIZE];
1199        pkt[0] = TS_SYNC_BYTE;
1200        pkt[1] = b1;
1201        pkt[2] = b2;
1202        pkt[3] = b3;
1203        let payload_start = 4;
1204        let end = (payload_start + payload_data.len()).min(TS_PACKET_SIZE);
1205        let len = (end - payload_start).min(payload_data.len());
1206        pkt[payload_start..payload_start + len].copy_from_slice(&payload_data[..len]);
1207        pkt
1208    }
1209
1210    #[test]
1211    fn parse_rejects_non_0x47_sync_byte() {
1212        let mut pkt = [0u8; TS_PACKET_SIZE];
1213        pkt[0] = 0x46; // wrong sync byte
1214        let err = TsPacket::parse(&pkt).unwrap_err();
1215        match err {
1216            Error::InvalidSyncByte { found } => assert_eq!(found, 0x46),
1217            other => panic!("expected InvalidSyncByte, got {other:?}"),
1218        }
1219    }
1220
1221    #[test]
1222    fn ts_header_round_trip() {
1223        // struct → serialize → parse must reproduce the header (the project's
1224        // symmetric Parse/Serialize invariant) across flag/field combinations.
1225        let cases = [
1226            TsHeader {
1227                tei: false,
1228                pusi: true,
1229                pid: 0x0000,
1230                scrambling: 0,
1231                has_adaptation: false,
1232                has_payload: true,
1233                continuity_counter: 0,
1234            },
1235            TsHeader {
1236                tei: true,
1237                pusi: false,
1238                pid: 0x1FFF,
1239                scrambling: 0b11,
1240                has_adaptation: true,
1241                has_payload: true,
1242                continuity_counter: 0x0F,
1243            },
1244            TsHeader {
1245                tei: false,
1246                pusi: false,
1247                pid: 0x0100,
1248                scrambling: 0b10,
1249                has_adaptation: true,
1250                has_payload: false,
1251                continuity_counter: 7,
1252            },
1253        ];
1254        for h in cases {
1255            let mut buf = [0u8; 4];
1256            assert_eq!(h.serialize_into(&mut buf).unwrap(), 4);
1257            assert_eq!(TsHeader::parse(&buf).unwrap(), h, "round-trip mismatch");
1258        }
1259    }
1260
1261    #[test]
1262    fn parse_extracts_pid_and_continuity_counter() {
1263        // PID = 0x1234 → upper 5 bits = 0x12, lower 8 bits = 0x34
1264        // CC = 5 → 0x05
1265        // b1 bits: [tei:1][pusi:1][pid_hi:5]
1266        // pid_hi = 0x12 = 0b00100_10 → bits 5..=1 = 0x12
1267        // b1 = 0b00_010010 = 0x12 (no tei, no pusi)
1268        let pkt = make_packet(0x12, 0x34, 0x05, &[]);
1269        let pkt = TsPacket::parse(&pkt).unwrap();
1270        assert_eq!(pkt.header.pid, 0x1234);
1271        assert_eq!(pkt.header.continuity_counter, 5);
1272    }
1273
1274    #[test]
1275    fn payload_unit_start_indicator_flag_extracted() {
1276        // b1 = 0x40 → pusi = true (bit 6 set, no tei, no pid bits)
1277        let pkt1 = make_packet(0x40, 0x00, 0x00, &[]);
1278        let pkt1 = TsPacket::parse(&pkt1).unwrap();
1279        assert!(pkt1.header.pusi);
1280
1281        // b1 = 0x00 → pusi = false
1282        let pkt2 = make_packet(0x00, 0x00, 0x00, &[]);
1283        let pkt2 = TsPacket::parse(&pkt2).unwrap();
1284        assert!(!pkt2.header.pusi);
1285    }
1286
1287    /// Build a PSI-carrying TS payload: `pointer_field` byte followed by
1288    /// (optionally) some tail of a previous section, followed by a fresh
1289    /// section. `pointer_field` is the number of bytes of the previous
1290    /// section that precede the new one (per ETSI EN 300 468 §5.1.4).
1291    fn build_pusi_payload(pointer_field: u8, previous_tail: &[u8], section: &[u8]) -> Vec<u8> {
1292        assert_eq!(pointer_field as usize, previous_tail.len());
1293        let mut v = Vec::with_capacity(1 + previous_tail.len() + section.len());
1294        v.push(pointer_field);
1295        v.extend_from_slice(previous_tail);
1296        v.extend_from_slice(section);
1297        v
1298    }
1299
1300    /// Build a long-form section with the given table_id and body bytes.
1301    /// Returns the full section including its 3-byte + 5-byte header and a
1302    /// placeholder CRC — for reassembler testing we don't validate the CRC.
1303    fn build_section(table_id: u8, body_after_length: &[u8]) -> Vec<u8> {
1304        let section_length = body_after_length.len() as u16;
1305        let mut v = Vec::with_capacity(3 + section_length as usize);
1306        v.push(table_id);
1307        // ssi=1, pi=0, reserved=11, length hi 4 bits
1308        v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
1309        v.push((section_length & 0xFF) as u8);
1310        v.extend_from_slice(body_after_length);
1311        v
1312    }
1313
1314    // The reassembler tests below feed raw payload slices directly to
1315    // `feed()` rather than wrapping them in 188-byte TS packets. This avoids
1316    // the TS stuffing-byte tail (0xFF padding) bleeding into the reassembled
1317    // section and keeps the assertions exact.
1318
1319    #[test]
1320    fn reassembler_accumulates_multi_packet_section() {
1321        // 200-byte section that spans two payload slices.
1322        let body = vec![0xAAu8; 197];
1323        let section = build_section(0x02, &body);
1324        assert_eq!(section.len(), 200);
1325
1326        let first_chunk = 100;
1327        let payload1 = build_pusi_payload(0, &[], &section[..first_chunk]);
1328        let payload2 = section[first_chunk..].to_vec();
1329
1330        let mut reasm = SectionReassembler::default();
1331        reasm.feed(&payload1, true);
1332        reasm.feed(&payload2, false);
1333
1334        let out = reasm.pop_section().expect("section should be ready");
1335        assert_eq!(out.len(), 200);
1336        assert_eq!(out.as_ref(), &section[..]);
1337    }
1338
1339    #[test]
1340    fn reassembler_yields_complete_section_once_length_satisfied() {
1341        // 1-byte-body section: table_id=0x42, section_length=1, total=4 bytes.
1342        let section = build_section(0x42, &[0xAA]);
1343        assert_eq!(section.len(), 4);
1344        let payload = build_pusi_payload(0, &[], &section);
1345
1346        let mut reasm = SectionReassembler::default();
1347        reasm.feed(&payload, true);
1348
1349        let out = reasm
1350            .pop_section()
1351            .expect("single-packet section should pop");
1352        assert_eq!(out.as_ref(), &section[..]);
1353    }
1354
1355    #[test]
1356    fn reassembler_extracts_all_concatenated_sections_in_one_payload() {
1357        // Issue #29: a single PUSI payload packing three complete short
1358        // sections after the pointer_field. All three must be queued — the
1359        // old `feed` stopped after the first and the rest were silently lost
1360        // (the CAS/EMM data-loss bug: SHARED EMMs landing as the 2nd+ section).
1361        let s1 = build_section(0x42, &[0x11, 0x22]); // 5 bytes
1362        let s2 = build_section(0x46, &[0x33]); // 4 bytes
1363        let s3 = build_section(0x4A, &[0x44, 0x55, 0x66]); // 6 bytes
1364
1365        let mut concat = Vec::new();
1366        concat.extend_from_slice(&s1);
1367        concat.extend_from_slice(&s2);
1368        concat.extend_from_slice(&s3);
1369        let payload = build_pusi_payload(0, &[], &concat);
1370
1371        let mut reasm = SectionReassembler::default();
1372        reasm.feed(&payload, true);
1373
1374        // Consumers must drain with a loop, not a single `if let`.
1375        let got: Vec<_> = core::iter::from_fn(|| reasm.pop_section()).collect();
1376        assert_eq!(got.len(), 3, "all three concatenated sections must pop");
1377        assert_eq!(got[0].as_ref(), &s1[..]);
1378        assert_eq!(got[1].as_ref(), &s2[..]);
1379        assert_eq!(got[2].as_ref(), &s3[..]);
1380    }
1381
1382    #[test]
1383    fn reassembler_stops_at_stuffing_after_concatenated_sections() {
1384        // Two sections then 0xFF stuffing fill — the stuffing must not be
1385        // mistaken for a section header (0xFF table_id) nor leak into a
1386        // section; both real sections still pop.
1387        let s1 = build_section(0x42, &[0xAA]); // 4 bytes
1388        let s2 = build_section(0x46, &[0xBB, 0xCC]); // 5 bytes
1389        let mut concat = Vec::new();
1390        concat.extend_from_slice(&s1);
1391        concat.extend_from_slice(&s2);
1392        concat.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]); // stuffing tail
1393        let payload = build_pusi_payload(0, &[], &concat);
1394
1395        let mut reasm = SectionReassembler::default();
1396        reasm.feed(&payload, true);
1397
1398        let got: Vec<_> = core::iter::from_fn(|| reasm.pop_section()).collect();
1399        assert_eq!(got.len(), 2);
1400        assert_eq!(got[0].as_ref(), &s1[..]);
1401        assert_eq!(got[1].as_ref(), &s2[..]);
1402        assert!(
1403            reasm.is_empty(),
1404            "stuffing tail must be discarded, not buffered"
1405        );
1406    }
1407
1408    #[test]
1409    fn reassembler_concatenated_then_spanning_tail() {
1410        // One complete section followed by the head of a second that spans
1411        // into a continuation packet: first pops immediately, second pops
1412        // once the continuation arrives.
1413        let s1 = build_section(0x42, &[0x01, 0x02]); // 5 bytes
1414        let s2 = build_section(0x46, &[0x09u8; 60]); // 63 bytes
1415        let split = 30;
1416
1417        let mut head = Vec::new();
1418        head.extend_from_slice(&s1);
1419        head.extend_from_slice(&s2[..split]);
1420        let payload1 = build_pusi_payload(0, &[], &head);
1421        let payload2 = s2[split..].to_vec();
1422
1423        let mut reasm = SectionReassembler::default();
1424        reasm.feed(&payload1, true);
1425        let first = reasm.pop_section().expect("first section pops at once");
1426        assert_eq!(first.as_ref(), &s1[..]);
1427        assert!(reasm.pop_section().is_none(), "second is still partial");
1428
1429        reasm.feed(&payload2, false);
1430        let second = reasm.pop_section().expect("second pops after continuation");
1431        assert_eq!(second.as_ref(), &s2[..]);
1432    }
1433
1434    #[test]
1435    fn reassembler_completes_section_spanning_into_pusi_packet() {
1436        // Issue #29 (second case): a section starts late in packet A and spills
1437        // into packet B, but B is itself PUSI=1 because new sections begin in it.
1438        // B's pointer_field = the count of leading tail bytes belonging to the
1439        // section from A. Those bytes MUST complete A's section before new
1440        // sections start. 3.1.1 cleared buf + skipped them → the spanning
1441        // section was lost (the SHARED EMM the smartcard needed).
1442        let spanning = build_section(0x42, &[0x5Au8; 62]); // 65 bytes
1443        let head = 41;
1444        let tail = &spanning[head..]; // 24 bytes — lands in packet B
1445        assert_eq!(tail.len(), 24);
1446
1447        // New section that begins in packet B after the spanning tail.
1448        let next = build_section(0x46, &[0x77, 0x88]); // 5 bytes
1449
1450        // Packet A (PUSI): pointer 0, then the 41-byte head (incomplete).
1451        let payload_a = build_pusi_payload(0, &[], &spanning[..head]);
1452        // Packet B (PUSI): pointer = 24 (tail of A's section), then `next`.
1453        let payload_b = build_pusi_payload(24, tail, &next);
1454
1455        let mut reasm = SectionReassembler::default();
1456        reasm.feed(&payload_a, true);
1457        assert!(reasm.pop_section().is_none(), "head alone is incomplete");
1458
1459        reasm.feed(&payload_b, true);
1460        let got: Vec<_> = core::iter::from_fn(|| reasm.pop_section()).collect();
1461        assert_eq!(got.len(), 2, "spanning section + new section must both pop");
1462        assert_eq!(
1463            got[0].as_ref(),
1464            &spanning[..],
1465            "spanning section completed from B's pointer tail"
1466        );
1467        assert_eq!(got[1].as_ref(), &next[..]);
1468    }
1469
1470    #[test]
1471    fn reassembler_pusi_pointer_spans_whole_payload() {
1472        // A section spans into a PUSI packet whose pointer covers the ENTIRE
1473        // remaining payload (no new section starts here) — the tail must be
1474        // appended and the section completed once the count is satisfied.
1475        let spanning = build_section(0x42, &[0x33u8; 40]); // 43 bytes
1476        let head = 20;
1477        let payload_a = build_pusi_payload(0, &[], &spanning[..head]);
1478        let tail = &spanning[head..]; // 23 bytes — exactly the rest of payload B
1479
1480        let mut reasm = SectionReassembler::default();
1481        reasm.feed(&payload_a, true);
1482        // Packet B: pointer = 23 = all remaining bytes; no new section follows.
1483        reasm.feed(&build_pusi_payload_pointer_spanning_all(tail), true);
1484
1485        let out = reasm.pop_section().expect("spanning section completes");
1486        assert_eq!(out.as_ref(), &spanning[..]);
1487        assert!(reasm.pop_section().is_none());
1488    }
1489
1490    /// Build a PUSI payload whose `pointer_field` equals the whole tail (so the
1491    /// pointer spans to the end of the payload and no new section starts).
1492    fn build_pusi_payload_pointer_spanning_all(tail: &[u8]) -> Vec<u8> {
1493        let mut v = Vec::with_capacity(1 + tail.len());
1494        v.push(tail.len() as u8);
1495        v.extend_from_slice(tail);
1496        v
1497    }
1498
1499    #[test]
1500    fn reassembler_completes_max_length_section_and_stays_usable() {
1501        // A section declaring the maximum `section_length` (0xFFF → 4098 bytes
1502        // total). The 12-bit length structurally caps the buffer at
1503        // MAX_SECTION_SIZE, so there is no unbounded growth — and (unlike the
1504        // pre-#148 guard, which discarded once buf+payload crossed the cap) a
1505        // valid max-length section completes at its declared length.
1506        let mut section = Vec::with_capacity(MAX_SECTION_SIZE);
1507        section.push(0x00); // table_id
1508        section.push(0xB0 | ((4095u16 >> 8) as u8 & 0x0F));
1509        section.push(0xFF); // section_length = 0xFFF
1510        section.resize(MAX_SECTION_SIZE, 0u8);
1511        assert_eq!(section.len(), MAX_SECTION_SIZE);
1512
1513        let mut reasm = SectionReassembler::default();
1514        let mut first = vec![0x00u8]; // pointer_field 0
1515        first.extend_from_slice(&section[..183]);
1516        reasm.feed(&first, true);
1517        assert!(
1518            reasm.pop_section().is_none(),
1519            "incomplete until the declared length arrives"
1520        );
1521
1522        for chunk in section[183..].chunks(184) {
1523            reasm.feed(chunk, false);
1524        }
1525        let out = reasm
1526            .pop_section()
1527            .expect("max-length section completes at its declared length");
1528        assert_eq!(out.len(), MAX_SECTION_SIZE);
1529        assert_eq!(out.as_ref(), &section[..]);
1530        assert!(reasm.is_empty());
1531
1532        // Extra trailing continuation data after completion is ignored (the
1533        // buffer is empty, so a non-PUSI payload is dropped) — no panic, no
1534        // spurious section.
1535        reasm.feed(&[0u8; 184], false);
1536        assert!(reasm.pop_section().is_none());
1537
1538        // State must be resettable — a fresh valid PUSI section works.
1539        let valid_section = build_section(0x00, &[0xAA]);
1540        let payload2 = build_pusi_payload(0, &[], &valid_section);
1541        reasm.feed(&payload2, true);
1542        let out = reasm
1543            .pop_section()
1544            .expect("fresh section should pop after reset");
1545        assert_eq!(out.as_ref(), &valid_section[..]);
1546    }
1547
1548    #[test]
1549    fn reassembler_handles_pusi_with_nonzero_pointer_field() {
1550        // payload = pointer_field=3, 3 bytes of prior-section tail, then new section.
1551        let prior_tail = vec![0x11, 0x22, 0x33];
1552        let new_section = build_section(0x02, &[0xBB]);
1553        assert_eq!(new_section.len(), 4);
1554        let payload = build_pusi_payload(3, &prior_tail, &new_section);
1555
1556        let mut reasm = SectionReassembler::default();
1557        reasm.feed(&payload, true);
1558
1559        let out = reasm
1560            .pop_section()
1561            .expect("section after pointer_field skip should pop");
1562        assert_eq!(out.as_ref(), &new_section[..]);
1563    }
1564
1565    #[test]
1566    fn reassembler_ignores_continuation_before_pusi() {
1567        // Feed a non-PUSI payload first (no prior PUSI seen).
1568        // SectionReassembler should discard it and stay empty.
1569        let pkt = make_packet(0x00, 0x00, PAYLOAD_FLAG, &[0xAA, 0xBB, 0xCC]);
1570
1571        let mut reasm = SectionReassembler::default();
1572        reasm.feed(&pkt[4..], false); // no PUSI
1573
1574        assert!(
1575            reasm.pop_section().is_none(),
1576            "no section should appear without prior PUSI"
1577        );
1578        assert!(
1579            reasm.pop_section().is_none(),
1580            "second pop should also be none"
1581        );
1582    }
1583
1584    /// A PUSI packet with an empty payload (adaptation field ate the body)
1585    /// is malformed but must not panic — it drops sync.
1586    #[test]
1587    fn reassembler_empty_pusi_payload_does_not_panic() {
1588        let mut reasm = SectionReassembler::default();
1589        reasm.feed(&[], true);
1590        assert!(reasm.pop_section().is_none());
1591        // Recovers on the next clean PUSI.
1592        let payload = vec![0x00u8, 0x72, 0x70, 0x01, 0x00];
1593        reasm.feed(&payload, true);
1594        assert!(reasm.pop_section().is_some());
1595    }
1596
1597    /// A maximal short-form private section (section_length 0xFFF, total
1598    /// 4098 bytes) reassembles — the ceiling is 12-bit length + 3-byte
1599    /// header, not 4096.
1600    #[test]
1601    fn reassembler_accepts_maximal_private_section() {
1602        let mut section = vec![0x80u8, 0x7F, 0xFF]; // user-private tid, SSI=0, len 0xFFF
1603        section.resize(3 + 0xFFF, 0xAB);
1604
1605        let mut reasm = SectionReassembler::default();
1606        // First TS payload: pointer_field 0 then the section start.
1607        let mut first = vec![0x00];
1608        first.extend_from_slice(&section[..183]);
1609        reasm.feed(&first, true);
1610        for chunk in section[183..].chunks(184) {
1611            reasm.feed(chunk, false);
1612        }
1613        let out = reasm.pop_section().expect("4098-byte section should pop");
1614        assert_eq!(out.len(), 4098);
1615        assert_eq!(out.as_ref(), &section[..]);
1616    }
1617
1618    /// Issue #148: a near-maximal section whose final continuation packet
1619    /// carries the section tail followed by `0xFF` **stuffing** must still
1620    /// complete. The old overflow guard counted the trailing stuffing toward
1621    /// `MAX_SECTION_SIZE` and dropped the section.
1622    #[test]
1623    fn reassembler_completes_large_section_with_trailing_stuffing() {
1624        let body = vec![0x5Au8; 4096 - 3];
1625        let section = build_section(0x50, &body); // 4096 bytes total
1626        assert_eq!(section.len(), 4096);
1627
1628        let mut reasm = SectionReassembler::default();
1629        // First payload (PUSI): pointer_field 0 + first 183 section bytes.
1630        let mut first = vec![0x00u8];
1631        first.extend_from_slice(&section[..183]);
1632        reasm.feed(&first, true);
1633
1634        // Continuation payloads of a full 184 bytes each; the final one is
1635        // padded with 0xFF stuffing to a complete 184-byte payload, exactly as
1636        // a real TS packet would carry it.
1637        let mut pos = 183usize;
1638        while pos < section.len() {
1639            let take = (section.len() - pos).min(184);
1640            let mut payload = section[pos..pos + take].to_vec();
1641            if take < 184 {
1642                payload.resize(184, 0xFF); // stuffing
1643            }
1644            reasm.feed(&payload, false);
1645            pos += take;
1646        }
1647
1648        let out = reasm
1649            .pop_section()
1650            .expect("4096-byte section must complete despite trailing stuffing (#148)");
1651        assert_eq!(out.len(), 4096);
1652        assert_eq!(out.as_ref(), &section[..]);
1653        assert!(reasm.is_empty(), "stuffing tail must be discarded");
1654    }
1655
1656    // ── adaptation field / PCR (ISO/IEC 13818-1 §2.4.3.4–2.4.3.5) ──
1657
1658    #[test]
1659    fn pcr_as_27mhz_known_value() {
1660        assert_eq!(
1661            Pcr {
1662                base: 10_000,
1663                extension: 0
1664            }
1665            .as_27mhz(),
1666            3_000_000
1667        );
1668        // base*300 + extension: 1*300 + 100 = 400.
1669        assert_eq!(
1670            Pcr {
1671                base: 1,
1672                extension: 100
1673            }
1674            .as_27mhz(),
1675            400
1676        );
1677    }
1678
1679    #[test]
1680    fn pcr_decode_from_bytes() {
1681        // 6-byte PCR encoding base=10000, extension=0 (reserved bits set).
1682        let af = [0x10u8, 0x00, 0x00, 0x13, 0x88, 0x7E, 0x00];
1683        let pcr = Pcr::parse(&af, 1).expect("6 bytes present");
1684        assert_eq!(
1685            pcr,
1686            Pcr {
1687                base: 10_000,
1688                extension: 0
1689            }
1690        );
1691        assert_eq!(pcr.as_27mhz(), 3_000_000);
1692    }
1693
1694    #[test]
1695    fn adaptation_field_flags_and_pcr() {
1696        let mut raw = [0xAAu8; TS_PACKET_SIZE];
1697        raw[0] = TS_SYNC_BYTE;
1698        raw[1] = 0x01; // pid 0x0100
1699        raw[2] = 0x00;
1700        raw[3] = ADAPTATION_FLAG | PAYLOAD_FLAG;
1701        raw[4] = 7; // adaptation_field_length: 1 flags + 6 PCR
1702        raw[5] = AF_DISCONTINUITY | AF_PCR_FLAG;
1703        raw[6..12].copy_from_slice(&[0x00, 0x00, 0x13, 0x88, 0x7E, 0x00]);
1704        // raw[12..] stays 0xAA = payload.
1705
1706        let pkt = TsPacket::parse(&raw).expect("valid packet");
1707        let af = pkt
1708            .adaptation_field()
1709            .expect("has adaptation field")
1710            .expect("adaptation field parses");
1711        assert!(af.discontinuity_indicator);
1712        assert!(!af.random_access_indicator);
1713        assert_eq!(
1714            af.pcr,
1715            Some(Pcr {
1716                base: 10_000,
1717                extension: 0
1718            })
1719        );
1720        assert_eq!(af.pcr.unwrap().as_27mhz(), 3_000_000);
1721        assert!(af.opcr.is_none());
1722        assert!(af.splice_countdown.is_none());
1723        // Payload begins right after the adaptation field (cursor 4+1+7=12).
1724        let payload = pkt.payload.expect("payload present");
1725        assert_eq!(payload.len(), TS_PACKET_SIZE - 12);
1726        assert_eq!(payload[0], 0xAA);
1727    }
1728
1729    #[test]
1730    fn oversized_adaptation_field_length_is_rejected_not_truncated() {
1731        // W6 (#1074): `adaptation_field_length` (raw[4]) can only be 0..=183
1732        // (5 + af_len <= 188). 200 doesn't fit; pre-fix, `TsPacket::parse`
1733        // `.min()`-truncated the slice to whatever bytes remained instead of
1734        // rejecting it, disagreeing with `OwnedTsPacket::adaptation_field`
1735        // (which already returned `None` for the same bytes).
1736        let mut raw = [0xAAu8; TS_PACKET_SIZE];
1737        raw[0] = TS_SYNC_BYTE;
1738        raw[1] = 0x01;
1739        raw[2] = 0x00;
1740        raw[3] = ADAPTATION_FLAG; // no payload flag: has_payload = false
1741        raw[4] = 200; // adaptation_field_length: does not fit in 188 bytes
1742
1743        let pkt = TsPacket::parse(&raw).expect("header + af_len byte still parse");
1744        assert!(
1745            pkt.adaptation_field().is_none(),
1746            "an over-long adaptation field must be rejected, not truncated"
1747        );
1748        assert!(pkt.payload.is_none());
1749
1750        // Cross-check against the owned view, which already rejected this.
1751        let owned = crate::owned::OwnedTsPacket::parse(raw).unwrap();
1752        assert!(owned.adaptation_field().is_none());
1753    }
1754
1755    #[test]
1756    fn no_adaptation_returns_none() {
1757        let mut raw = [0x00u8; TS_PACKET_SIZE];
1758        raw[0] = TS_SYNC_BYTE;
1759        raw[1] = 0x01;
1760        raw[3] = PAYLOAD_FLAG; // payload only
1761        let pkt = TsPacket::parse(&raw).expect("valid");
1762        assert!(pkt.adaptation_field().is_none());
1763        assert!(pkt.adaptation.is_none());
1764    }
1765
1766    #[test]
1767    fn adaptation_field_splice_countdown_negative() {
1768        let mut raw = [0xAAu8; TS_PACKET_SIZE];
1769        raw[0] = TS_SYNC_BYTE;
1770        raw[1] = 0x01;
1771        raw[2] = 0x00;
1772        raw[3] = ADAPTATION_FLAG | PAYLOAD_FLAG;
1773        raw[4] = 2; // 1 flags + 1 splice_countdown
1774        raw[5] = AF_SPLICING_FLAG;
1775        raw[6] = 0xFB; // -5 as i8
1776        let pkt = TsPacket::parse(&raw).expect("valid");
1777        let af = pkt.adaptation_field().unwrap().unwrap();
1778        assert_eq!(af.splice_countdown, Some(-5));
1779        assert!(af.pcr.is_none());
1780    }
1781
1782    // ── ScramblingControl / AdaptationFieldControl enums ──
1783
1784    #[test]
1785    fn scrambling_control_all_values() {
1786        assert_eq!(
1787            ScramblingControl::from_bits(0b00),
1788            ScramblingControl::NotScrambled
1789        );
1790        assert_eq!(
1791            ScramblingControl::from_bits(0b01),
1792            ScramblingControl::Reserved
1793        );
1794        assert_eq!(
1795            ScramblingControl::from_bits(0b10),
1796            ScramblingControl::EvenKey
1797        );
1798        assert_eq!(
1799            ScramblingControl::from_bits(0b11),
1800            ScramblingControl::OddKey
1801        );
1802        // name() labels
1803        assert_eq!(ScramblingControl::NotScrambled.name(), "not_scrambled");
1804        assert_eq!(ScramblingControl::Reserved.name(), "reserved");
1805        assert_eq!(ScramblingControl::EvenKey.name(), "even_key");
1806        assert_eq!(ScramblingControl::OddKey.name(), "odd_key");
1807        // Display delegates to name()
1808        assert_eq!(ScramblingControl::NotScrambled.to_string(), "not_scrambled");
1809        assert_eq!(ScramblingControl::OddKey.to_string(), "odd_key");
1810        // Masking: only low 2 bits matter
1811        assert_eq!(
1812            ScramblingControl::from_bits(0xFF),
1813            ScramblingControl::OddKey
1814        );
1815    }
1816
1817    #[test]
1818    fn adaptation_field_control_all_values() {
1819        assert_eq!(
1820            AdaptationFieldControl::from_flags(false, false),
1821            AdaptationFieldControl::Reserved
1822        );
1823        assert_eq!(
1824            AdaptationFieldControl::from_flags(false, true),
1825            AdaptationFieldControl::PayloadOnly
1826        );
1827        assert_eq!(
1828            AdaptationFieldControl::from_flags(true, false),
1829            AdaptationFieldControl::AdaptationOnly
1830        );
1831        assert_eq!(
1832            AdaptationFieldControl::from_flags(true, true),
1833            AdaptationFieldControl::AdaptationAndPayload
1834        );
1835        // name()
1836        assert_eq!(AdaptationFieldControl::Reserved.name(), "reserved");
1837        assert_eq!(AdaptationFieldControl::PayloadOnly.name(), "payload_only");
1838        assert_eq!(
1839            AdaptationFieldControl::AdaptationOnly.name(),
1840            "adaptation_only"
1841        );
1842        assert_eq!(
1843            AdaptationFieldControl::AdaptationAndPayload.name(),
1844            "adaptation_and_payload"
1845        );
1846        // Display
1847        assert_eq!(
1848            AdaptationFieldControl::PayloadOnly.to_string(),
1849            "payload_only"
1850        );
1851    }
1852
1853    #[test]
1854    fn ts_header_scrambling_control_accessor() {
1855        let hdr = TsHeader {
1856            tei: false,
1857            pusi: false,
1858            pid: 0x0100,
1859            scrambling: 0b10,
1860            has_adaptation: false,
1861            has_payload: true,
1862            continuity_counter: 0,
1863        };
1864        assert_eq!(hdr.scrambling_control(), ScramblingControl::EvenKey);
1865    }
1866
1867    #[test]
1868    fn ts_header_adaptation_field_control_accessor() {
1869        let hdr_payload_only = TsHeader {
1870            tei: false,
1871            pusi: false,
1872            pid: 0x0100,
1873            scrambling: 0,
1874            has_adaptation: false,
1875            has_payload: true,
1876            continuity_counter: 0,
1877        };
1878        assert_eq!(
1879            hdr_payload_only.adaptation_field_control(),
1880            AdaptationFieldControl::PayloadOnly
1881        );
1882
1883        let hdr_both = TsHeader {
1884            tei: false,
1885            pusi: false,
1886            pid: 0x0100,
1887            scrambling: 0,
1888            has_adaptation: true,
1889            has_payload: true,
1890            continuity_counter: 0,
1891        };
1892        assert_eq!(
1893            hdr_both.adaptation_field_control(),
1894            AdaptationFieldControl::AdaptationAndPayload
1895        );
1896    }
1897
1898    // ── iter_packets / extract_ts_payload helpers ──
1899
1900    #[test]
1901    fn iter_packets_yields_valid_and_skips_bad_sync() {
1902        // Two valid packets back-to-back, then one bad-sync packet.
1903        let pkt1 = make_packet(0x00, 0x00, PAYLOAD_FLAG, &[0xAA; 10]);
1904        let pkt2 = make_packet(0x40, 0x64, PAYLOAD_FLAG, &[0xBB; 10]);
1905        let mut bad = [0u8; TS_PACKET_SIZE];
1906        bad[0] = 0x00; // bad sync byte
1907
1908        let mut buf = Vec::new();
1909        buf.extend_from_slice(&pkt1);
1910        buf.extend_from_slice(&pkt2);
1911        buf.extend_from_slice(&bad);
1912
1913        let pkts: Vec<_> = super::iter_packets(&buf).collect();
1914        assert_eq!(pkts.len(), 2, "bad sync packet must be skipped");
1915        assert_eq!(pkts[0].header.pid, 0x0000);
1916        assert_eq!(pkts[1].header.pid, 0x0064);
1917    }
1918
1919    #[test]
1920    fn extract_ts_payload_payload_only() {
1921        let pkt = make_packet(0x00, 0x00, PAYLOAD_FLAG, &[0xABu8; 10]);
1922        let p = super::extract_ts_payload(&pkt).expect("payload present");
1923        assert_eq!(p[0], 0xAB);
1924        assert_eq!(p.len(), TS_PACKET_SIZE - 4);
1925    }
1926
1927    #[test]
1928    fn extract_ts_payload_adaptation_only_returns_none() {
1929        let pkt = make_packet(0x00, 0x00, ADAPTATION_FLAG, &[]);
1930        assert!(super::extract_ts_payload(&pkt).is_none());
1931    }
1932
1933    // ── Pcr write-side ──────────────────────────────────────────────────────
1934
1935    /// `from_27mhz(v).as_27mhz() == v` for representative values
1936    /// (ISO/IEC 13818-1:2007 §2.4.3.5).
1937    #[test]
1938    fn pcr_from_27mhz_round_trips() {
1939        for &ticks in &[0u64, 1, 300, 27_000_000, u64::from(u32::MAX), 8_589_934_591] {
1940            let pcr = Pcr::from_27mhz(ticks);
1941            assert_eq!(pcr.as_27mhz(), ticks, "ticks={ticks}");
1942        }
1943    }
1944
1945    /// `to_field_bytes` → `parse` → same `Pcr` (field-bytes round-trip).
1946    #[test]
1947    fn pcr_to_field_bytes_round_trips_parse() {
1948        let cases = [
1949            Pcr {
1950                base: 0,
1951                extension: 0,
1952            },
1953            Pcr {
1954                base: 10_000,
1955                extension: 0,
1956            },
1957            Pcr {
1958                base: 1,
1959                extension: 100,
1960            },
1961            Pcr {
1962                base: 0x1_FFFF_FFFF,
1963                extension: 0x1FF,
1964            },
1965        ];
1966        for pcr in cases {
1967            let bytes = pcr.to_field_bytes();
1968            // Prefix the 6 bytes with a dummy flags byte so the offset is 1,
1969            // matching the parse() calling convention inside AdaptationField::parse.
1970            let mut af = [0u8; 7];
1971            af[1..7].copy_from_slice(&bytes);
1972            let decoded = Pcr::parse(&af, 1).expect("parse round-trip");
1973            assert_eq!(decoded, pcr, "round-trip failed for {pcr:?}");
1974        }
1975    }
1976
1977    /// Known vector from ts.rs existing test — base=10000, extension=0 produces
1978    /// the 6-byte encoding `[0x00, 0x00, 0x13, 0x88, 0x7E, 0x00]`.
1979    #[test]
1980    fn pcr_to_field_bytes_known_vector() {
1981        let pcr = Pcr {
1982            base: 10_000,
1983            extension: 0,
1984        };
1985        let bytes = pcr.to_field_bytes();
1986        assert_eq!(bytes, [0x00, 0x00, 0x13, 0x88, 0x7E, 0x00]);
1987    }
1988
1989    // ── ScramblingControl to_bits ───────────────────────────────────────────
1990
1991    #[test]
1992    fn scrambling_control_to_bits_inverse_of_from_bits() {
1993        for bits in 0u8..=3 {
1994            let sc = ScramblingControl::from_bits(bits);
1995            assert_eq!(sc.to_bits(), bits, "to_bits() != from_bits() for {bits}");
1996        }
1997    }
1998
1999    // ── AdaptationFieldControl to_bits / to_flags ──────────────────────────
2000
2001    #[test]
2002    fn adaptation_field_control_to_bits_inverse_of_from_flags() {
2003        let cases = [
2004            (false, false, 0b00u8),
2005            (false, true, 0b01),
2006            (true, false, 0b10),
2007            (true, true, 0b11),
2008        ];
2009        for (has_af, has_pl, expected_bits) in cases {
2010            let afc = AdaptationFieldControl::from_flags(has_af, has_pl);
2011            assert_eq!(afc.to_bits(), expected_bits);
2012            assert_eq!(afc.to_flags(), (has_af, has_pl));
2013        }
2014    }
2015
2016    // ── AdaptationField serialize_into ─────────────────────────────────────
2017
2018    /// Build an adaptation field with PCR, serialize → parse → verify equal.
2019    #[test]
2020    fn adaptation_field_serialize_round_trip_with_pcr() {
2021        let original = AdaptationField {
2022            discontinuity_indicator: true,
2023            random_access_indicator: false,
2024            elementary_stream_priority_indicator: false,
2025            pcr: Some(Pcr {
2026                base: 10_000,
2027                extension: 0,
2028            }),
2029            opcr: None,
2030            splice_countdown: None,
2031            transport_private_data: None,
2032            extension: None,
2033            stuffing_len: 0,
2034        };
2035        let len = original.serialized_len();
2036        assert_eq!(len, 7); // 1 flags + 6 PCR
2037        let mut buf = vec![0u8; len];
2038        let written = original.serialize_into(&mut buf).expect("serialize");
2039        assert_eq!(written, len);
2040        let decoded = AdaptationField::parse(&buf).expect("parse round-trip");
2041        assert_eq!(decoded, original);
2042    }
2043
2044    /// W10 (#1129): `transport_private_data_length` is an 8-bit field but
2045    /// used to be written with `tpd.len() as u8`, wrapping to a short length
2046    /// while the full data was still copied after it.
2047    #[test]
2048    fn transport_private_data_over_255_bytes_rejected_not_wrapped() {
2049        let data = vec![0u8; 256];
2050        let af = AdaptationField {
2051            discontinuity_indicator: false,
2052            random_access_indicator: false,
2053            elementary_stream_priority_indicator: false,
2054            pcr: None,
2055            opcr: None,
2056            splice_countdown: None,
2057            transport_private_data: Some(&data),
2058            extension: None,
2059            stuffing_len: 0,
2060        };
2061        let mut buf = vec![0u8; af.serialized_len()];
2062        assert!(af.serialize_into(&mut buf).is_err());
2063    }
2064
2065    /// Known-bytes test: flags=0x30 (discontinuity + PCR), known PCR vector.
2066    #[test]
2067    fn adaptation_field_serialize_produces_known_bytes() {
2068        // Matches the packet in ts.rs `adaptation_field_flags_and_pcr` test:
2069        // raw[5] = AF_DISCONTINUITY | AF_PCR_FLAG = 0x90
2070        // raw[6..12] = [0x00, 0x00, 0x13, 0x88, 0x7E, 0x00]
2071        let af = AdaptationField {
2072            discontinuity_indicator: true,
2073            random_access_indicator: false,
2074            elementary_stream_priority_indicator: false,
2075            pcr: Some(Pcr {
2076                base: 10_000,
2077                extension: 0,
2078            }),
2079            opcr: None,
2080            splice_countdown: None,
2081            transport_private_data: None,
2082            extension: None,
2083            stuffing_len: 0,
2084        };
2085        let mut buf = [0u8; 7];
2086        af.serialize_into(&mut buf).unwrap();
2087        assert_eq!(buf[0], AF_DISCONTINUITY | AF_PCR_FLAG);
2088        assert_eq!(&buf[1..7], &[0x00, 0x00, 0x13, 0x88, 0x7E, 0x00]);
2089    }
2090
2091    /// Serialize → parse round-trip for AdaptationField with OPCR + splice_countdown.
2092    #[test]
2093    fn adaptation_field_serialize_round_trip_opcr_and_splice() {
2094        let original = AdaptationField {
2095            discontinuity_indicator: false,
2096            random_access_indicator: true,
2097            elementary_stream_priority_indicator: true,
2098            pcr: Some(Pcr {
2099                base: 1,
2100                extension: 100,
2101            }),
2102            opcr: Some(Pcr {
2103                base: 999,
2104                extension: 5,
2105            }),
2106            splice_countdown: Some(-3),
2107            transport_private_data: None,
2108            extension: None,
2109            stuffing_len: 0,
2110        };
2111        let len = original.serialized_len();
2112        assert_eq!(len, 1 + 6 + 6 + 1); // flags + PCR + OPCR + splice
2113        let mut buf = vec![0u8; len];
2114        original.serialize_into(&mut buf).unwrap();
2115        let decoded = AdaptationField::parse(&buf).expect("parse");
2116        assert_eq!(decoded, original);
2117    }
2118
2119    /// Flags-only AdaptationField (no PCR/OPCR/splice).
2120    #[test]
2121    fn adaptation_field_serialize_flags_only() {
2122        let af = AdaptationField {
2123            discontinuity_indicator: false,
2124            random_access_indicator: true,
2125            elementary_stream_priority_indicator: false,
2126            pcr: None,
2127            opcr: None,
2128            splice_countdown: None,
2129            transport_private_data: None,
2130            extension: None,
2131            stuffing_len: 0,
2132        };
2133        assert_eq!(af.serialized_len(), 1);
2134        let mut buf = [0u8; 1];
2135        af.serialize_into(&mut buf).unwrap();
2136        assert_eq!(buf[0], AF_RANDOM_ACCESS);
2137        let decoded = AdaptationField::parse(&buf).unwrap();
2138        assert_eq!(decoded, af);
2139    }
2140
2141    /// OutputBufferTooSmall returned when buffer is too short.
2142    #[test]
2143    fn adaptation_field_serialize_rejects_small_buffer() {
2144        let af = AdaptationField {
2145            discontinuity_indicator: false,
2146            random_access_indicator: false,
2147            elementary_stream_priority_indicator: false,
2148            pcr: Some(Pcr {
2149                base: 0,
2150                extension: 0,
2151            }),
2152            opcr: None,
2153            splice_countdown: None,
2154            transport_private_data: None,
2155            extension: None,
2156            stuffing_len: 0,
2157        };
2158        let mut buf = [0u8; 3]; // need 7
2159        assert!(matches!(
2160            af.serialize_into(&mut buf),
2161            Err(Error::OutputBufferTooSmall { .. })
2162        ));
2163    }
2164
2165    // ── AdaptationFieldExtension (§2.4.3.5) ────────────────────────────────
2166
2167    /// Round-trip LTW field.
2168    #[test]
2169    fn adaptation_field_extension_ltw_round_trip() {
2170        let ext = AdaptationFieldExtension {
2171            ltw: Some(Ltw {
2172                ltw_valid_flag: true,
2173                ltw_offset: 0x1234,
2174            }),
2175            piecewise_rate: None,
2176            seamless_splice: None,
2177        };
2178        let mut buf = vec![0u8; ext.serialized_len()];
2179        ext.serialize_into(&mut buf).unwrap();
2180        // Parse via AdaptationField (extension is the last field)
2181        // Build a full AdaptationField with only the extension set.
2182        let af = AdaptationField {
2183            discontinuity_indicator: false,
2184            random_access_indicator: false,
2185            elementary_stream_priority_indicator: false,
2186            pcr: None,
2187            opcr: None,
2188            splice_countdown: None,
2189            transport_private_data: None,
2190            extension: Some(ext),
2191            stuffing_len: 0,
2192        };
2193        let len = af.serialized_len();
2194        let mut abuf = vec![0u8; len];
2195        af.serialize_into(&mut abuf).unwrap();
2196        let decoded = AdaptationField::parse(&abuf).unwrap();
2197        assert_eq!(decoded.extension, Some(ext));
2198    }
2199
2200    /// Round-trip piecewise_rate field.
2201    #[test]
2202    fn adaptation_field_extension_piecewise_rate_round_trip() {
2203        let af = AdaptationField {
2204            discontinuity_indicator: false,
2205            random_access_indicator: false,
2206            elementary_stream_priority_indicator: false,
2207            pcr: None,
2208            opcr: None,
2209            splice_countdown: None,
2210            transport_private_data: None,
2211            extension: Some(AdaptationFieldExtension {
2212                ltw: None,
2213                piecewise_rate: Some(0x3FFFFF), // max 22-bit
2214                seamless_splice: None,
2215            }),
2216            stuffing_len: 0,
2217        };
2218        let mut buf = vec![0u8; af.serialized_len()];
2219        af.serialize_into(&mut buf).unwrap();
2220        let decoded = AdaptationField::parse(&buf).unwrap();
2221        assert_eq!(decoded.extension.unwrap().piecewise_rate, Some(0x3FFFFF));
2222    }
2223
2224    /// Round-trip seamless_splice field.
2225    #[test]
2226    fn adaptation_field_extension_seamless_splice_round_trip() {
2227        let af = AdaptationField {
2228            discontinuity_indicator: false,
2229            random_access_indicator: false,
2230            elementary_stream_priority_indicator: false,
2231            pcr: None,
2232            opcr: None,
2233            splice_countdown: None,
2234            transport_private_data: None,
2235            extension: Some(AdaptationFieldExtension {
2236                ltw: None,
2237                piecewise_rate: None,
2238                seamless_splice: Some(SeamlessSplice {
2239                    splice_type: 0xA,
2240                    dts_next_au: 0x1_2345_6789,
2241                }),
2242            }),
2243            stuffing_len: 0,
2244        };
2245        let mut buf = vec![0u8; af.serialized_len()];
2246        af.serialize_into(&mut buf).unwrap();
2247        let decoded = AdaptationField::parse(&buf).unwrap();
2248        let ss = decoded.extension.unwrap().seamless_splice.unwrap();
2249        assert_eq!(ss.splice_type, 0xA);
2250        assert_eq!(ss.dts_next_au, 0x1_2345_6789);
2251    }
2252
2253    /// Round-trip with transport_private_data.
2254    #[test]
2255    fn adaptation_field_transport_private_data_round_trip() {
2256        let tpd = [0xDE, 0xAD, 0xBE, 0xEF];
2257        let af = AdaptationField {
2258            discontinuity_indicator: false,
2259            random_access_indicator: false,
2260            elementary_stream_priority_indicator: false,
2261            pcr: None,
2262            opcr: None,
2263            splice_countdown: None,
2264            transport_private_data: Some(&tpd),
2265            extension: None,
2266            stuffing_len: 0,
2267        };
2268        let mut buf = vec![0u8; af.serialized_len()];
2269        af.serialize_into(&mut buf).unwrap();
2270        let decoded = AdaptationField::parse(&buf).unwrap();
2271        assert_eq!(decoded.transport_private_data, Some(tpd.as_slice()));
2272    }
2273
2274    /// PCR + `0xFF` stuffing round-trips byte-identical, and the stuffing is
2275    /// re-emitted as `0xFF` (ISO/IEC 13818-1:2007 §2.4.3.4).
2276    #[test]
2277    fn adaptation_field_stuffing_round_trip() {
2278        let af = AdaptationField {
2279            discontinuity_indicator: false,
2280            random_access_indicator: false,
2281            elementary_stream_priority_indicator: false,
2282            pcr: Some(Pcr {
2283                base: 12_345,
2284                extension: 7,
2285            }),
2286            opcr: None,
2287            splice_countdown: None,
2288            transport_private_data: None,
2289            extension: None,
2290            stuffing_len: 20,
2291        };
2292        // 1 flags + 6 PCR + 20 stuffing.
2293        assert_eq!(af.serialized_len(), 27);
2294        let mut buf = vec![0u8; af.serialized_len()];
2295        af.serialize_into(&mut buf).unwrap();
2296        // Trailing bytes are 0xFF stuffing.
2297        assert!(buf[7..27].iter().all(|&b| b == AF_STUFFING_BYTE));
2298        let decoded = AdaptationField::parse(&buf).unwrap();
2299        assert_eq!(decoded.stuffing_len, 20);
2300        assert_eq!(decoded, af);
2301        // Pure stuffing (flags-only body padded out) also round-trips.
2302        let pure = AdaptationField {
2303            discontinuity_indicator: false,
2304            random_access_indicator: false,
2305            elementary_stream_priority_indicator: false,
2306            pcr: None,
2307            opcr: None,
2308            splice_countdown: None,
2309            transport_private_data: None,
2310            extension: None,
2311            stuffing_len: 5,
2312        };
2313        let mut pbuf = vec![0u8; pure.serialized_len()];
2314        pure.serialize_into(&mut pbuf).unwrap();
2315        assert_eq!(pbuf, vec![0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
2316        assert_eq!(AdaptationField::parse(&pbuf).unwrap(), pure);
2317    }
2318
2319    /// All optional fields together: PCR + splice + TPD + extension.
2320    #[test]
2321    fn adaptation_field_all_fields_round_trip() {
2322        let tpd = [0x01u8, 0x02, 0x03];
2323        let af = AdaptationField {
2324            discontinuity_indicator: true,
2325            random_access_indicator: false,
2326            elementary_stream_priority_indicator: false,
2327            pcr: Some(Pcr {
2328                base: 90_000,
2329                extension: 50,
2330            }),
2331            opcr: None,
2332            splice_countdown: Some(10),
2333            transport_private_data: Some(&tpd),
2334            extension: Some(AdaptationFieldExtension {
2335                ltw: Some(Ltw {
2336                    ltw_valid_flag: false,
2337                    ltw_offset: 500,
2338                }),
2339                piecewise_rate: Some(12345),
2340                seamless_splice: None,
2341            }),
2342            stuffing_len: 0,
2343        };
2344        let len = af.serialized_len();
2345        let mut buf = vec![0u8; len];
2346        af.serialize_into(&mut buf).unwrap();
2347        let decoded = AdaptationField::parse(&buf).unwrap();
2348        assert_eq!(decoded.pcr, af.pcr);
2349        assert_eq!(decoded.splice_countdown, af.splice_countdown);
2350        assert_eq!(decoded.transport_private_data, af.transport_private_data);
2351        assert_eq!(decoded.extension, af.extension);
2352        assert!(decoded.discontinuity_indicator);
2353    }
2354
2355    /// Full round-trip: build a packet with PCR in adaptation field, parse
2356    /// it, re-serialize the adaptation field, re-parse, assert PCR matches.
2357    #[test]
2358    fn adaptation_field_serialize_from_real_packet_bytes() {
2359        // Replicate the raw packet from `adaptation_field_flags_and_pcr`.
2360        let mut raw = [0xAAu8; TS_PACKET_SIZE];
2361        raw[0] = TS_SYNC_BYTE;
2362        raw[1] = 0x01;
2363        raw[2] = 0x00;
2364        raw[3] = ADAPTATION_FLAG | PAYLOAD_FLAG;
2365        raw[4] = 7;
2366        raw[5] = AF_DISCONTINUITY | AF_PCR_FLAG;
2367        raw[6..12].copy_from_slice(&[0x00, 0x00, 0x13, 0x88, 0x7E, 0x00]);
2368
2369        let pkt = TsPacket::parse(&raw).unwrap();
2370        let af = pkt.adaptation_field().unwrap().unwrap();
2371
2372        // Re-serialize.
2373        let mut ser = vec![0u8; af.serialized_len()];
2374        af.serialize_into(&mut ser).unwrap();
2375        let decoded = AdaptationField::parse(&ser).unwrap();
2376        assert_eq!(
2377            decoded.pcr,
2378            Some(Pcr {
2379                base: 10_000,
2380                extension: 0
2381            })
2382        );
2383        assert!(decoded.discontinuity_indicator);
2384    }
2385}