Skip to main content

mpeg_ts/
ts.rs

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