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