Skip to main content

mpeg_ts/
ts.rs

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