Skip to main content

mpeg_ts/
ts.rs

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