Skip to main content

mpeg_ts/
ts.rs

1//! MPEG-TS packet parser and section reassembler — ITU-T H.222.0 §2.4 (= ISO/IEC 13818-1).
2
3use crate::error::{Error, Result};
4
5/// Size of one MPEG-TS packet (ETSI EN 300 468 §3.2, ISO/IEC 13818-1 §2.4.3.2).
6pub const TS_PACKET_SIZE: usize = 188;
7/// Sync byte that every TS packet starts with (ISO/IEC 13818-1 §2.4.3.2).
8pub const TS_SYNC_BYTE: u8 = 0x47;
9/// Upper bound on a single section: `section_length` is 12 bits (max 4095)
10/// plus the 3-byte header = 4098. (Long-form SI caps `section_length` at
11/// 4093 → total 4096, but maximal short-form private sections may reach
12/// 4098; the reassembler accepts the absolute ceiling.)
13const MAX_SECTION_SIZE: usize = 4098;
14/// Bytes before the `section_length` payload: `table_id` (1) + the two bytes
15/// carrying the syntax/RFU flags and the 12-bit `section_length`
16/// (ISO/IEC 13818-1 §2.4.4.1).
17const SECTION_HEADER_LEN: usize = 3;
18/// Mask for the 4 most-significant `section_length` bits in a section's second
19/// byte (ISO/IEC 13818-1 §2.4.4.1 — `section_length` is 12 bits). Shared with
20/// the packetizer in `mux.rs`.
21pub(crate) const SECTION_LENGTH_HI_MASK: u8 = 0x0F;
22
23/// 2-bit `transport_scrambling_control` field — ITU-T H.222.0 (08/2023) Table 2-4
24/// (defines only `00` = not scrambled); DVB assigns `01`/`10`/`11` in ETSI TS 100 289
25/// V1.1.1 §5.1 Table 1 (reserved, even CW, odd CW).
26///
27/// MPEG-2 leaves `01`/`10`/`11` as user-defined; DVB's common-scrambling convention
28/// assigns `10` = even control word, `11` = odd control word, `01` = reserved for future
29/// DVB use. The field lives in the TS header and is never applied to the header itself —
30/// only to the packet payload.
31#[non_exhaustive]
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize))]
34pub enum ScramblingControl {
35    /// `00` — not scrambled. The only MPEG-2-defined value (H.222.0 Table 2-4).
36    NotScrambled,
37    /// `01` — reserved for future DVB use (ETSI TS 100 289 V1.1.1 §5.1 Table 1).
38    Reserved,
39    /// `10` — TS packet payload scrambled with the **even** control word
40    /// (DVB common scrambling, ETSI TS 100 289 V1.1.1 §5.1 Table 1).
41    EvenKey,
42    /// `11` — TS packet payload scrambled with the **odd** control word
43    /// (DVB common scrambling, ETSI TS 100 289 V1.1.1 §5.1 Table 1).
44    OddKey,
45}
46
47impl ScramblingControl {
48    /// Decode from the 2-bit `transport_scrambling_control` value (masked to `[1:0]`).
49    pub fn from_bits(bits: u8) -> Self {
50        match bits & 0b11 {
51            0b00 => Self::NotScrambled,
52            0b01 => Self::Reserved,
53            0b10 => Self::EvenKey,
54            0b11 => Self::OddKey,
55            _ => unreachable!(),
56        }
57    }
58
59    /// Short label for this value, per the #204 convention.
60    pub fn name(&self) -> &'static str {
61        match self {
62            Self::NotScrambled => "not_scrambled",
63            Self::Reserved => "reserved",
64            Self::EvenKey => "even_key",
65            Self::OddKey => "odd_key",
66        }
67    }
68}
69
70dvb_common::impl_spec_display!(ScramblingControl);
71
72/// 2-bit `adaptation_field_control` field — ITU-T H.222.0 (08/2023) Table 2-5.
73///
74/// Decoders shall discard packets with value `00` (`Reserved`). Null packets use `01`
75/// (`PayloadOnly`). The two flags `has_adaptation`/`has_payload` on [`TsHeader`] carry
76/// the decoded booleans; this enum provides the typed composite view.
77#[non_exhaustive]
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79#[cfg_attr(feature = "serde", derive(serde::Serialize))]
80pub enum AdaptationFieldControl {
81    /// `00` — reserved for future use; decoders shall discard (H.222.0 Table 2-5).
82    Reserved,
83    /// `01` — no adaptation_field, payload only (H.222.0 Table 2-5).
84    PayloadOnly,
85    /// `10` — adaptation_field only, no payload (H.222.0 Table 2-5).
86    AdaptationOnly,
87    /// `11` — adaptation_field followed by payload (H.222.0 Table 2-5).
88    AdaptationAndPayload,
89}
90
91impl AdaptationFieldControl {
92    /// Derive from the two decoded boolean flags stored on [`TsHeader`].
93    pub fn from_flags(has_adaptation: bool, has_payload: bool) -> Self {
94        match (has_adaptation, has_payload) {
95            (false, false) => Self::Reserved,
96            (false, true) => Self::PayloadOnly,
97            (true, false) => Self::AdaptationOnly,
98            (true, true) => Self::AdaptationAndPayload,
99        }
100    }
101
102    /// Short label for this value, per the #204 convention.
103    pub fn name(&self) -> &'static str {
104        match self {
105            Self::Reserved => "reserved",
106            Self::PayloadOnly => "payload_only",
107            Self::AdaptationOnly => "adaptation_only",
108            Self::AdaptationAndPayload => "adaptation_and_payload",
109        }
110    }
111}
112
113dvb_common::impl_spec_display!(AdaptationFieldControl);
114
115/// ISO/IEC 13818-1 §2.4.3.3: transport header byte 1 bit 7 = tei (Transport Error Indicator).
116const TEI_MASK: u8 = 0x80;
117/// ISO/IEC 13818-1 §2.4.3.3: byte 1 bit 6 = pusi (Payload Unit Start Indicator).
118const PUSI_MASK: u8 = 0x40;
119/// ISO/IEC 13818-1 §2.4.3.3: byte 1 bits `[4:0]` = 13-bit PID (upper 5 bits).
120pub const PID_MASK_HI: u8 = 0x1F;
121/// ISO/IEC 13818-1 §2.4.3.3: byte 3 bits `[7:6]` = 2-bit scrambling control.
122pub const SCRAMBLING_MASK: u8 = 0xC0;
123/// ISO/IEC 13818-1 §2.4.3.3: byte 3 bit 5 = adaptation_field_control (bit 5 = 1 means adaptation present).
124pub const ADAPTATION_FLAG: u8 = 0x20;
125/// ISO/IEC 13818-1 §2.4.3.3: byte 3 bit 4 = adaptation_field_control (bit 4 = 1 means payload present).
126pub const PAYLOAD_FLAG: u8 = 0x10;
127/// ISO/IEC 13818-1 §2.4.3.3: byte 3 bits `[3:0]` = 4-bit continuity_counter.
128pub const CC_MASK: u8 = 0x0F;
129
130/// Parsed TS header — the 4-byte transport header fields.
131#[derive(Clone, Debug, PartialEq, Eq)]
132#[cfg_attr(feature = "serde", derive(serde::Serialize))]
133pub struct TsHeader {
134    /// Transport Error Indicator — set by the demodulator when an
135    /// uncorrectable error is present in the packet.
136    pub tei: bool,
137    /// Payload Unit Start Indicator — first byte of the payload is a new
138    /// PES packet or PSI section header when set.
139    pub pusi: bool,
140    /// 13-bit Packet Identifier.
141    pub pid: u16,
142    /// 2-bit transport_scrambling_control (0 = not scrambled).
143    pub scrambling: u8,
144    /// Adaptation field present flag (adaptation_field_control bit 1).
145    pub has_adaptation: bool,
146    /// Payload present flag (adaptation_field_control bit 0).
147    pub has_payload: bool,
148    /// 4-bit continuity_counter (wraps 0..=15 per PID).
149    pub continuity_counter: u8,
150}
151
152/// Borrowed view into one 188-byte TS packet.
153///
154/// Serde: Serialize-only (re-parse from wire bytes to reconstruct). `raw` is
155/// excluded from the serialized form because it is redundant once the header
156/// has been parsed.
157#[derive(Clone, Debug)]
158#[cfg_attr(feature = "serde", derive(serde::Serialize))]
159pub struct TsPacket<'a> {
160    /// Parsed header fields.
161    pub header: TsHeader,
162    /// Slice into the packet's payload, or `None` when `has_payload == false`
163    /// or the adaptation field consumed the whole packet body.
164    pub payload: Option<&'a [u8]>,
165    /// The adaptation-field bytes (after the length byte). Internal capture
166    /// feeding [`adaptation_field`](Self::adaptation_field); not public.
167    #[cfg_attr(feature = "serde", serde(skip))]
168    adaptation: Option<&'a [u8]>,
169    /// The raw 188 bytes of the packet — kept for cheap forwarding.
170    #[cfg_attr(feature = "serde", serde(skip))]
171    pub raw: &'a [u8; TS_PACKET_SIZE],
172}
173
174impl TsHeader {
175    /// Parse a 4-byte TS transport header.
176    pub fn parse(raw4: &[u8]) -> Result<Self> {
177        if raw4.len() < 4 {
178            return Err(Error::BufferTooShort {
179                need: 4,
180                have: raw4.len(),
181                what: "TsHeader",
182            });
183        }
184        let b1 = raw4[1];
185        let b2 = raw4[2];
186        let b3 = raw4[3];
187
188        let tei = (b1 & TEI_MASK) != 0;
189        let pusi = (b1 & PUSI_MASK) != 0;
190        let pid = (((b1 & PID_MASK_HI) as u16) << 8) | (b2 as u16);
191        let scrambling = (b3 & SCRAMBLING_MASK) >> 6;
192        let has_adaptation = (b3 & ADAPTATION_FLAG) != 0;
193        let has_payload = (b3 & PAYLOAD_FLAG) != 0;
194        let continuity_counter = b3 & CC_MASK;
195
196        Ok(Self {
197            tei,
198            pusi,
199            pid,
200            scrambling,
201            has_adaptation,
202            has_payload,
203            continuity_counter,
204        })
205    }
206
207    /// Number of bytes written by [`serialize_into`](Self::serialize_into).
208    pub const fn serialized_len() -> usize {
209        4
210    }
211
212    /// Serialize this header into the first 4 bytes of `buf`.
213    pub fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
214        if buf.len() < 4 {
215            return Err(Error::OutputBufferTooSmall {
216                need: 4,
217                have: buf.len(),
218            });
219        }
220        buf[0] = TS_SYNC_BYTE;
221        buf[1] = 0;
222        if self.tei {
223            buf[1] |= TEI_MASK;
224        }
225        if self.pusi {
226            buf[1] |= PUSI_MASK;
227        }
228        buf[1] |= ((self.pid >> 8) as u8) & PID_MASK_HI;
229        buf[2] = (self.pid & 0xFF) as u8;
230        buf[3] = (self.scrambling << 6) & SCRAMBLING_MASK;
231        if self.has_adaptation {
232            buf[3] |= ADAPTATION_FLAG;
233        }
234        if self.has_payload {
235            buf[3] |= PAYLOAD_FLAG;
236        }
237        buf[3] |= self.continuity_counter & CC_MASK;
238        Ok(4)
239    }
240
241    /// Typed view of the 2-bit `transport_scrambling_control` field.
242    ///
243    /// See [`ScramblingControl`] for the spec citation (H.222.0 Table 2-4 +
244    /// ETSI TS 100 289 §5.1 Table 1).
245    pub fn scrambling_control(&self) -> ScramblingControl {
246        ScramblingControl::from_bits(self.scrambling)
247    }
248
249    /// Typed view of the `adaptation_field_control` 2-bit field, derived from the
250    /// `has_adaptation`/`has_payload` flags.
251    ///
252    /// See [`AdaptationFieldControl`] for the spec citation (H.222.0 Table 2-5).
253    pub fn adaptation_field_control(&self) -> AdaptationFieldControl {
254        AdaptationFieldControl::from_flags(self.has_adaptation, self.has_payload)
255    }
256}
257
258impl<'a> dvb_common::Parse<'a> for TsHeader {
259    type Error = Error;
260
261    fn parse(bytes: &'a [u8]) -> Result<Self> {
262        TsHeader::parse(bytes)
263    }
264}
265
266impl dvb_common::Serialize for TsHeader {
267    type Error = Error;
268
269    fn serialized_len(&self) -> usize {
270        TsHeader::serialized_len()
271    }
272
273    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
274        TsHeader::serialize_into(self, buf)
275    }
276}
277
278impl<'a> TsPacket<'a> {
279    /// Parse a single 188-byte TS packet from a buffer.
280    ///
281    /// Returns `Err(Error::InvalidSyncByte)` if the first byte is not `0x47`,
282    /// `Err(Error::BufferTooShort)` if fewer than 188 bytes, or `Ok` with
283    /// the parsed packet otherwise.
284    pub fn parse(buf: &'a [u8]) -> Result<Self> {
285        if buf.len() < TS_PACKET_SIZE {
286            return Err(Error::BufferTooShort {
287                need: TS_PACKET_SIZE,
288                have: buf.len(),
289                what: "TsPacket",
290            });
291        }
292        if buf[0] != TS_SYNC_BYTE {
293            return Err(Error::InvalidSyncByte { found: buf[0] });
294        }
295
296        let raw: &[u8; TS_PACKET_SIZE] =
297            buf[..TS_PACKET_SIZE]
298                .try_into()
299                .map_err(|_| Error::BufferTooShort {
300                    need: TS_PACKET_SIZE,
301                    have: buf.len(),
302                    what: "TsPacket::parse (array conversion)",
303                })?;
304
305        let header = TsHeader::parse(&raw[..4])?;
306
307        let mut cursor = 4usize;
308        let mut payload = None;
309        let mut adaptation = None;
310
311        // Capture the adaptation field if present, then skip it (the section
312        // path does not need it; decode lazily via `adaptation_field`).
313        if header.has_adaptation && cursor < TS_PACKET_SIZE {
314            let af_len = raw[cursor] as usize;
315            let af_start = cursor + 1;
316            if af_len > 0 && af_start < TS_PACKET_SIZE {
317                let af_end = (af_start + af_len).min(TS_PACKET_SIZE);
318                adaptation = Some(&raw[af_start..af_end]);
319            }
320            cursor += 1 + af_len;
321        }
322
323        if header.has_payload && cursor < TS_PACKET_SIZE {
324            payload = Some(&raw[cursor..]);
325        }
326
327        Ok(TsPacket {
328            header,
329            payload,
330            adaptation,
331            raw,
332        })
333    }
334
335    /// Decode the adaptation field, if present.
336    ///
337    /// Returns `None` when the packet carries no adaptation field, and
338    /// `Some(Err(..))` when a present field is truncated. Layout per
339    /// ISO/IEC 13818-1:2007 §2.4.3.4 (`docs/iso_13818_1_systems.md`).
340    pub fn adaptation_field(&self) -> Option<crate::Result<AdaptationField>> {
341        self.adaptation.map(AdaptationField::parse)
342    }
343}
344
345// Adaptation-field flag bits, byte 0 (ISO/IEC 13818-1:2007 §2.4.3.4).
346const AF_DISCONTINUITY: u8 = 0x80;
347const AF_RANDOM_ACCESS: u8 = 0x40;
348const AF_ES_PRIORITY: u8 = 0x20;
349const AF_PCR_FLAG: u8 = 0x10;
350const AF_OPCR_FLAG: u8 = 0x08;
351const AF_SPLICING_FLAG: u8 = 0x04;
352/// Encoded PCR / OPCR field width: 33-bit base + 6 reserved + 9-bit extension.
353const PCR_FIELD_LEN: usize = 6;
354
355/// Program Clock Reference (ISO/IEC 13818-1:2007 §2.4.3.5): a 33-bit base on a
356/// 90 kHz clock plus a 9-bit extension on a 27 MHz clock.
357#[derive(Clone, Copy, Debug, PartialEq, Eq)]
358#[cfg_attr(feature = "serde", derive(serde::Serialize))]
359pub struct Pcr {
360    /// 33-bit base (90 kHz units).
361    pub base: u64,
362    /// 9-bit extension (27 MHz units).
363    pub extension: u16,
364}
365
366impl Pcr {
367    /// Full PCR value on the 27 MHz clock: `base * 300 + extension`.
368    #[must_use]
369    pub fn as_27mhz(self) -> u64 {
370        self.base * 300 + self.extension as u64
371    }
372
373    /// Decode the 6-byte PCR/OPCR field starting at `at` within `af`.
374    fn parse(af: &[u8], at: usize) -> Result<Self> {
375        let b: &[u8; PCR_FIELD_LEN] = af
376            .get(at..at + PCR_FIELD_LEN)
377            .and_then(|s| s.try_into().ok())
378            .ok_or(Error::BufferTooShort {
379                need: at + PCR_FIELD_LEN,
380                have: af.len(),
381                what: "adaptation_field PCR",
382            })?;
383        let base = ((b[0] as u64) << 25)
384            | ((b[1] as u64) << 17)
385            | ((b[2] as u64) << 9)
386            | ((b[3] as u64) << 1)
387            | ((b[4] as u64) >> 7);
388        let extension = (((b[4] & 0x01) as u16) << 8) | (b[5] as u16);
389        Ok(Self { base, extension })
390    }
391}
392
393/// Decoded adaptation field — flags plus PCR/OPCR and splice point per
394/// ISO/IEC 13818-1:2007 §2.4.3.4. Transport-private data and the
395/// adaptation-field extension are not yet surfaced; more fields may be
396/// added in future releases.
397#[non_exhaustive]
398#[derive(Clone, Copy, Debug, PartialEq, Eq)]
399#[cfg_attr(feature = "serde", derive(serde::Serialize))]
400pub struct AdaptationField {
401    /// A timing/continuity discontinuity starts at this packet.
402    pub discontinuity_indicator: bool,
403    /// This packet is a random-access point.
404    pub random_access_indicator: bool,
405    /// Elementary-stream priority hint.
406    pub elementary_stream_priority_indicator: bool,
407    /// Program Clock Reference, present iff the PCR flag is set.
408    pub pcr: Option<Pcr>,
409    /// Original PCR, present iff the OPCR flag is set.
410    pub opcr: Option<Pcr>,
411    /// Splice countdown (packets until the splice point), iff the flag is set.
412    pub splice_countdown: Option<i8>,
413}
414
415impl AdaptationField {
416    /// Parse the adaptation-field bytes (those following the length byte).
417    fn parse(af: &[u8]) -> Result<Self> {
418        let flags = *af.first().ok_or(Error::BufferTooShort {
419            need: 1,
420            have: 0,
421            what: "adaptation_field flags",
422        })?;
423        let mut cursor = 1usize;
424
425        let pcr = if flags & AF_PCR_FLAG != 0 {
426            let p = Pcr::parse(af, cursor)?;
427            cursor += PCR_FIELD_LEN;
428            Some(p)
429        } else {
430            None
431        };
432        let opcr = if flags & AF_OPCR_FLAG != 0 {
433            let p = Pcr::parse(af, cursor)?;
434            cursor += PCR_FIELD_LEN;
435            Some(p)
436        } else {
437            None
438        };
439        let splice_countdown = if flags & AF_SPLICING_FLAG != 0 {
440            let b = *af.get(cursor).ok_or(Error::BufferTooShort {
441                need: cursor + 1,
442                have: af.len(),
443                what: "adaptation_field splice_countdown",
444            })?;
445            Some(b as i8)
446        } else {
447            None
448        };
449
450        Ok(AdaptationField {
451            discontinuity_indicator: flags & AF_DISCONTINUITY != 0,
452            random_access_indicator: flags & AF_RANDOM_ACCESS != 0,
453            elementary_stream_priority_indicator: flags & AF_ES_PRIORITY != 0,
454            pcr,
455            opcr,
456            splice_countdown,
457        })
458    }
459}
460
461/// Reassembles PSI/SI sections from TS packets on a single PID.
462///
463/// Feed each TS packet's payload with `feed`. Complete sections are
464/// appended to an internal queue; drain them with `pop_section`.
465#[derive(Default)]
466pub struct SectionReassembler {
467    buf: bytes::BytesMut,
468    ready: alloc::collections::VecDeque<bytes::Bytes>,
469}
470
471impl SectionReassembler {
472    /// Feed a TS payload and whether its packet had PUSI set.
473    ///
474    /// Extracts complete SI sections into the internal queue. A single call
475    /// can produce zero, one, or **several** sections — a payload may
476    /// concatenate multiple complete sections after the `pointer_field`
477    /// (EN 300 468 §5.1.4; common on EMM PIDs). Drain with a
478    /// `while let Some(s) = r.pop_section()` loop, not a single `if let`.
479    pub fn feed(&mut self, payload: &[u8], pusi: bool) {
480        if pusi {
481            // A PUSI packet whose adaptation field consumed the whole body is
482            // malformed but constructible — drop sync rather than panic.
483            if payload.is_empty() {
484                self.buf.clear();
485                return;
486            }
487            let pointer = payload[0] as usize;
488
489            // The `pointer_field` counts bytes that belong to a section still
490            // in progress from a previous packet (ISO/IEC 13818-1 §2.4.4): the
491            // `pointer` bytes immediately after it are that section's tail and
492            // must complete it BEFORE new sections begin at `1 + pointer`.
493            // Skipping them (or clearing `buf` first) drops any section that
494            // spans into a PUSI packet — silent loss biased toward whichever
495            // section happens to straddle a packet boundary.
496            if !self.buf.is_empty() && pointer > 0 {
497                let avail = payload.len() - 1;
498                let tail_len = pointer.min(avail);
499                if self.buf.len() + tail_len > MAX_SECTION_SIZE {
500                    self.buf.clear();
501                } else {
502                    self.buf.extend_from_slice(&payload[1..1 + tail_len]);
503                    self.drain_complete_sections();
504                }
505            }
506
507            // New sections start at `1 + pointer`; anything still buffered is
508            // an incomplete (corrupt / lost-packet) section — discard it.
509            self.buf.clear();
510
511            let start = 1 + pointer;
512            if start >= payload.len() {
513                // Pointer spans to (or past) the end — no new section here.
514                return;
515            }
516            let new_data = &payload[start..];
517            if new_data.len() > MAX_SECTION_SIZE {
518                return;
519            }
520            self.buf.extend_from_slice(new_data);
521        } else {
522            if self.buf.is_empty() {
523                return;
524            }
525            // Append only the bytes the in-progress section still needs. A new
526            // section cannot start in a continuation (non-PUSI) packet
527            // (ISO/IEC 13818-1 §2.4.4), so once the section's declared length
528            // is satisfied the remaining payload bytes are 0xFF stuffing and
529            // are ignored. Counting that stuffing toward `MAX_SECTION_SIZE`
530            // previously dropped valid near-maximal sections (#148). Because
531            // the 12-bit `section_length` caps a section at `MAX_SECTION_SIZE`,
532            // `take` is inherently bounded and the buffer cannot grow without
533            // limit.
534            let take = if self.buf.len() >= SECTION_HEADER_LEN {
535                let exp = SECTION_HEADER_LEN
536                    + (((self.buf[1] & SECTION_LENGTH_HI_MASK) as usize) << 8
537                        | self.buf[2] as usize);
538                exp.saturating_sub(self.buf.len()).min(payload.len())
539            } else {
540                // Header not yet complete (split across the packet boundary) —
541                // take enough to read `section_length` on the next drain,
542                // bounded by the maximum possible section size.
543                payload.len().min(MAX_SECTION_SIZE - self.buf.len())
544            };
545            self.buf.extend_from_slice(&payload[..take]);
546        }
547
548        self.drain_complete_sections();
549    }
550
551    /// Queue every complete section the buffer currently holds.
552    ///
553    /// A single TS payload may concatenate multiple complete sections after
554    /// the `pointer_field` (legal per ETSI EN 300 468 §5.1.4 and common on
555    /// EMM PIDs, which pack several short messages into one payload). We must
556    /// keep extracting until the buffer holds only a partial (multi-packet
557    /// spanning) section, whose bytes stay buffered for the next packet to
558    /// continue (the expected length is recomputed from the section header on
559    /// each drain). A `0xFF` where a `table_id` is expected marks the rest of
560    /// the payload as stuffing.
561    fn drain_complete_sections(&mut self) {
562        loop {
563            if self.buf.len() < SECTION_HEADER_LEN {
564                // Not enough for a section header yet; keep the partial bytes
565                // and wait for the next packet to complete the header.
566                break;
567            }
568            if self.buf[0] == 0xFF {
569                // Stuffing where a table_id is expected — payload tail is fill.
570                self.buf.clear();
571                break;
572            }
573            let exp = SECTION_HEADER_LEN
574                + (((self.buf[1] & SECTION_LENGTH_HI_MASK) as usize) << 8 | self.buf[2] as usize);
575            if self.buf.len() >= exp {
576                // split_to returns the first `exp` bytes as an owned BytesMut,
577                // leaving the remainder in self.buf — cheap (shifts pointers).
578                let section = self.buf.split_to(exp).freeze();
579                self.ready.push_back(section);
580            } else {
581                // Partial section spanning into later packets.
582                break;
583            }
584        }
585    }
586
587    /// Pop one complete section. Returns `None` when the queue is empty.
588    pub fn pop_section(&mut self) -> Option<bytes::Bytes> {
589        self.ready.pop_front()
590    }
591
592    /// Number of bytes currently buffered (incomplete section).
593    pub fn len(&self) -> usize {
594        self.buf.len()
595    }
596
597    /// True if no bytes are currently buffered.
598    pub fn is_empty(&self) -> bool {
599        self.buf.is_empty()
600    }
601}
602
603/// Iterate over all valid TS packets in a byte buffer.
604///
605/// Slices `buf` into 188-byte chunks (using [`slice::chunks_exact`]) and yields
606/// each chunk for which [`TsPacket::parse`] succeeds. Chunks with a bad sync byte
607/// (`!= 0x47`) or insufficient length are silently skipped — use
608/// [`crate::resync::TsResync`] for byte-stream resynchronisation before calling
609/// this when byte alignment is not guaranteed.
610///
611/// # Example
612///
613/// ```no_run
614/// # use mpeg_ts::ts::iter_packets;
615/// # let data: &[u8] = &[];
616/// for pkt in iter_packets(data) {
617///     println!("PID: 0x{:04X}", pkt.header.pid);
618/// }
619/// ```
620pub fn iter_packets(buf: &[u8]) -> impl Iterator<Item = TsPacket<'_>> {
621    buf.chunks_exact(TS_PACKET_SIZE)
622        .filter_map(|chunk| TsPacket::parse(chunk).ok())
623}
624
625/// Extract the payload bytes from a raw 188-byte TS packet slice.
626///
627/// Returns `None` when:
628/// - `pkt` is fewer than 4 bytes,
629/// - `adaptation_field_control` is `00` (reserved) or `10` (adaptation only), or
630/// - the adaptation field length would place the payload start past the packet end.
631///
632/// No sync-byte check is performed — the caller is responsible for ensuring the
633/// slice is properly aligned. Spec: ITU-T H.222.0 (08/2023) §2.4.3.3 Table 2-5.
634pub fn extract_ts_payload(pkt: &[u8]) -> Option<&[u8]> {
635    if pkt.len() < 4 {
636        return None;
637    }
638    let afc = (pkt[3] >> 4) & 0x3;
639    match afc {
640        0x1 => {
641            // payload only: payload starts at byte 4
642            if pkt.len() > 4 {
643                Some(&pkt[4..])
644            } else {
645                None
646            }
647        }
648        0x3 => {
649            // adaptation field + payload
650            if pkt.len() < 5 {
651                return None;
652            }
653            let af_len = pkt[4] as usize;
654            let start = 5 + af_len;
655            if start < pkt.len() {
656                Some(&pkt[start..])
657            } else {
658                None
659            }
660        }
661        _ => None,
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668    use alloc::string::ToString;
669    use alloc::vec;
670    use alloc::vec::Vec;
671
672    /// Helper: construct a minimal 188-byte TS packet buffer with given header flags and payload.
673    fn make_packet(b1: u8, b2: u8, b3: u8, payload_data: &[u8]) -> [u8; TS_PACKET_SIZE] {
674        let mut pkt = [0u8; TS_PACKET_SIZE];
675        pkt[0] = TS_SYNC_BYTE;
676        pkt[1] = b1;
677        pkt[2] = b2;
678        pkt[3] = b3;
679        let payload_start = 4;
680        let end = (payload_start + payload_data.len()).min(TS_PACKET_SIZE);
681        let len = (end - payload_start).min(payload_data.len());
682        pkt[payload_start..payload_start + len].copy_from_slice(&payload_data[..len]);
683        pkt
684    }
685
686    #[test]
687    fn parse_rejects_non_0x47_sync_byte() {
688        let mut pkt = [0u8; TS_PACKET_SIZE];
689        pkt[0] = 0x46; // wrong sync byte
690        let err = TsPacket::parse(&pkt).unwrap_err();
691        match err {
692            Error::InvalidSyncByte { found } => assert_eq!(found, 0x46),
693            other => panic!("expected InvalidSyncByte, got {other:?}"),
694        }
695    }
696
697    #[test]
698    fn ts_header_round_trip() {
699        // struct → serialize → parse must reproduce the header (the project's
700        // symmetric Parse/Serialize invariant) across flag/field combinations.
701        let cases = [
702            TsHeader {
703                tei: false,
704                pusi: true,
705                pid: 0x0000,
706                scrambling: 0,
707                has_adaptation: false,
708                has_payload: true,
709                continuity_counter: 0,
710            },
711            TsHeader {
712                tei: true,
713                pusi: false,
714                pid: 0x1FFF,
715                scrambling: 0b11,
716                has_adaptation: true,
717                has_payload: true,
718                continuity_counter: 0x0F,
719            },
720            TsHeader {
721                tei: false,
722                pusi: false,
723                pid: 0x0100,
724                scrambling: 0b10,
725                has_adaptation: true,
726                has_payload: false,
727                continuity_counter: 7,
728            },
729        ];
730        for h in cases {
731            let mut buf = [0u8; 4];
732            assert_eq!(h.serialize_into(&mut buf).unwrap(), 4);
733            assert_eq!(TsHeader::parse(&buf).unwrap(), h, "round-trip mismatch");
734        }
735    }
736
737    #[test]
738    fn parse_extracts_pid_and_continuity_counter() {
739        // PID = 0x1234 → upper 5 bits = 0x12, lower 8 bits = 0x34
740        // CC = 5 → 0x05
741        // b1 bits: [tei:1][pusi:1][pid_hi:5]
742        // pid_hi = 0x12 = 0b00100_10 → bits 5..=1 = 0x12
743        // b1 = 0b00_010010 = 0x12 (no tei, no pusi)
744        let pkt = make_packet(0x12, 0x34, 0x05, &[]);
745        let pkt = TsPacket::parse(&pkt).unwrap();
746        assert_eq!(pkt.header.pid, 0x1234);
747        assert_eq!(pkt.header.continuity_counter, 5);
748    }
749
750    #[test]
751    fn payload_unit_start_indicator_flag_extracted() {
752        // b1 = 0x40 → pusi = true (bit 6 set, no tei, no pid bits)
753        let pkt1 = make_packet(0x40, 0x00, 0x00, &[]);
754        let pkt1 = TsPacket::parse(&pkt1).unwrap();
755        assert!(pkt1.header.pusi);
756
757        // b1 = 0x00 → pusi = false
758        let pkt2 = make_packet(0x00, 0x00, 0x00, &[]);
759        let pkt2 = TsPacket::parse(&pkt2).unwrap();
760        assert!(!pkt2.header.pusi);
761    }
762
763    /// Build a PSI-carrying TS payload: `pointer_field` byte followed by
764    /// (optionally) some tail of a previous section, followed by a fresh
765    /// section. `pointer_field` is the number of bytes of the previous
766    /// section that precede the new one (per ETSI EN 300 468 §5.1.4).
767    fn build_pusi_payload(pointer_field: u8, previous_tail: &[u8], section: &[u8]) -> Vec<u8> {
768        assert_eq!(pointer_field as usize, previous_tail.len());
769        let mut v = Vec::with_capacity(1 + previous_tail.len() + section.len());
770        v.push(pointer_field);
771        v.extend_from_slice(previous_tail);
772        v.extend_from_slice(section);
773        v
774    }
775
776    /// Build a long-form section with the given table_id and body bytes.
777    /// Returns the full section including its 3-byte + 5-byte header and a
778    /// placeholder CRC — for reassembler testing we don't validate the CRC.
779    fn build_section(table_id: u8, body_after_length: &[u8]) -> Vec<u8> {
780        let section_length = body_after_length.len() as u16;
781        let mut v = Vec::with_capacity(3 + section_length as usize);
782        v.push(table_id);
783        // ssi=1, pi=0, reserved=11, length hi 4 bits
784        v.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
785        v.push((section_length & 0xFF) as u8);
786        v.extend_from_slice(body_after_length);
787        v
788    }
789
790    // The reassembler tests below feed raw payload slices directly to
791    // `feed()` rather than wrapping them in 188-byte TS packets. This avoids
792    // the TS stuffing-byte tail (0xFF padding) bleeding into the reassembled
793    // section and keeps the assertions exact.
794
795    #[test]
796    fn reassembler_accumulates_multi_packet_section() {
797        // 200-byte section that spans two payload slices.
798        let body = vec![0xAAu8; 197];
799        let section = build_section(0x02, &body);
800        assert_eq!(section.len(), 200);
801
802        let first_chunk = 100;
803        let payload1 = build_pusi_payload(0, &[], &section[..first_chunk]);
804        let payload2 = section[first_chunk..].to_vec();
805
806        let mut reasm = SectionReassembler::default();
807        reasm.feed(&payload1, true);
808        reasm.feed(&payload2, false);
809
810        let out = reasm.pop_section().expect("section should be ready");
811        assert_eq!(out.len(), 200);
812        assert_eq!(out.as_ref(), &section[..]);
813    }
814
815    #[test]
816    fn reassembler_yields_complete_section_once_length_satisfied() {
817        // 1-byte-body section: table_id=0x42, section_length=1, total=4 bytes.
818        let section = build_section(0x42, &[0xAA]);
819        assert_eq!(section.len(), 4);
820        let payload = build_pusi_payload(0, &[], &section);
821
822        let mut reasm = SectionReassembler::default();
823        reasm.feed(&payload, true);
824
825        let out = reasm
826            .pop_section()
827            .expect("single-packet section should pop");
828        assert_eq!(out.as_ref(), &section[..]);
829    }
830
831    #[test]
832    fn reassembler_extracts_all_concatenated_sections_in_one_payload() {
833        // Issue #29: a single PUSI payload packing three complete short
834        // sections after the pointer_field. All three must be queued — the
835        // old `feed` stopped after the first and the rest were silently lost
836        // (the CAS/EMM data-loss bug: SHARED EMMs landing as the 2nd+ section).
837        let s1 = build_section(0x42, &[0x11, 0x22]); // 5 bytes
838        let s2 = build_section(0x46, &[0x33]); // 4 bytes
839        let s3 = build_section(0x4A, &[0x44, 0x55, 0x66]); // 6 bytes
840
841        let mut concat = Vec::new();
842        concat.extend_from_slice(&s1);
843        concat.extend_from_slice(&s2);
844        concat.extend_from_slice(&s3);
845        let payload = build_pusi_payload(0, &[], &concat);
846
847        let mut reasm = SectionReassembler::default();
848        reasm.feed(&payload, true);
849
850        // Consumers must drain with a loop, not a single `if let`.
851        let got: Vec<_> = core::iter::from_fn(|| reasm.pop_section()).collect();
852        assert_eq!(got.len(), 3, "all three concatenated sections must pop");
853        assert_eq!(got[0].as_ref(), &s1[..]);
854        assert_eq!(got[1].as_ref(), &s2[..]);
855        assert_eq!(got[2].as_ref(), &s3[..]);
856    }
857
858    #[test]
859    fn reassembler_stops_at_stuffing_after_concatenated_sections() {
860        // Two sections then 0xFF stuffing fill — the stuffing must not be
861        // mistaken for a section header (0xFF table_id) nor leak into a
862        // section; both real sections still pop.
863        let s1 = build_section(0x42, &[0xAA]); // 4 bytes
864        let s2 = build_section(0x46, &[0xBB, 0xCC]); // 5 bytes
865        let mut concat = Vec::new();
866        concat.extend_from_slice(&s1);
867        concat.extend_from_slice(&s2);
868        concat.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]); // stuffing tail
869        let payload = build_pusi_payload(0, &[], &concat);
870
871        let mut reasm = SectionReassembler::default();
872        reasm.feed(&payload, true);
873
874        let got: Vec<_> = core::iter::from_fn(|| reasm.pop_section()).collect();
875        assert_eq!(got.len(), 2);
876        assert_eq!(got[0].as_ref(), &s1[..]);
877        assert_eq!(got[1].as_ref(), &s2[..]);
878        assert!(
879            reasm.is_empty(),
880            "stuffing tail must be discarded, not buffered"
881        );
882    }
883
884    #[test]
885    fn reassembler_concatenated_then_spanning_tail() {
886        // One complete section followed by the head of a second that spans
887        // into a continuation packet: first pops immediately, second pops
888        // once the continuation arrives.
889        let s1 = build_section(0x42, &[0x01, 0x02]); // 5 bytes
890        let s2 = build_section(0x46, &[0x09u8; 60]); // 63 bytes
891        let split = 30;
892
893        let mut head = Vec::new();
894        head.extend_from_slice(&s1);
895        head.extend_from_slice(&s2[..split]);
896        let payload1 = build_pusi_payload(0, &[], &head);
897        let payload2 = s2[split..].to_vec();
898
899        let mut reasm = SectionReassembler::default();
900        reasm.feed(&payload1, true);
901        let first = reasm.pop_section().expect("first section pops at once");
902        assert_eq!(first.as_ref(), &s1[..]);
903        assert!(reasm.pop_section().is_none(), "second is still partial");
904
905        reasm.feed(&payload2, false);
906        let second = reasm.pop_section().expect("second pops after continuation");
907        assert_eq!(second.as_ref(), &s2[..]);
908    }
909
910    #[test]
911    fn reassembler_completes_section_spanning_into_pusi_packet() {
912        // Issue #29 (second case): a section starts late in packet A and spills
913        // into packet B, but B is itself PUSI=1 because new sections begin in it.
914        // B's pointer_field = the count of leading tail bytes belonging to the
915        // section from A. Those bytes MUST complete A's section before new
916        // sections start. 3.1.1 cleared buf + skipped them → the spanning
917        // section was lost (the SHARED EMM the smartcard needed).
918        let spanning = build_section(0x42, &[0x5Au8; 62]); // 65 bytes
919        let head = 41;
920        let tail = &spanning[head..]; // 24 bytes — lands in packet B
921        assert_eq!(tail.len(), 24);
922
923        // New section that begins in packet B after the spanning tail.
924        let next = build_section(0x46, &[0x77, 0x88]); // 5 bytes
925
926        // Packet A (PUSI): pointer 0, then the 41-byte head (incomplete).
927        let payload_a = build_pusi_payload(0, &[], &spanning[..head]);
928        // Packet B (PUSI): pointer = 24 (tail of A's section), then `next`.
929        let payload_b = build_pusi_payload(24, tail, &next);
930
931        let mut reasm = SectionReassembler::default();
932        reasm.feed(&payload_a, true);
933        assert!(reasm.pop_section().is_none(), "head alone is incomplete");
934
935        reasm.feed(&payload_b, true);
936        let got: Vec<_> = core::iter::from_fn(|| reasm.pop_section()).collect();
937        assert_eq!(got.len(), 2, "spanning section + new section must both pop");
938        assert_eq!(
939            got[0].as_ref(),
940            &spanning[..],
941            "spanning section completed from B's pointer tail"
942        );
943        assert_eq!(got[1].as_ref(), &next[..]);
944    }
945
946    #[test]
947    fn reassembler_pusi_pointer_spans_whole_payload() {
948        // A section spans into a PUSI packet whose pointer covers the ENTIRE
949        // remaining payload (no new section starts here) — the tail must be
950        // appended and the section completed once the count is satisfied.
951        let spanning = build_section(0x42, &[0x33u8; 40]); // 43 bytes
952        let head = 20;
953        let payload_a = build_pusi_payload(0, &[], &spanning[..head]);
954        let tail = &spanning[head..]; // 23 bytes — exactly the rest of payload B
955
956        let mut reasm = SectionReassembler::default();
957        reasm.feed(&payload_a, true);
958        // Packet B: pointer = 23 = all remaining bytes; no new section follows.
959        reasm.feed(&build_pusi_payload_pointer_spanning_all(tail), true);
960
961        let out = reasm.pop_section().expect("spanning section completes");
962        assert_eq!(out.as_ref(), &spanning[..]);
963        assert!(reasm.pop_section().is_none());
964    }
965
966    /// Build a PUSI payload whose `pointer_field` equals the whole tail (so the
967    /// pointer spans to the end of the payload and no new section starts).
968    fn build_pusi_payload_pointer_spanning_all(tail: &[u8]) -> Vec<u8> {
969        let mut v = Vec::with_capacity(1 + tail.len());
970        v.push(tail.len() as u8);
971        v.extend_from_slice(tail);
972        v
973    }
974
975    #[test]
976    fn reassembler_completes_max_length_section_and_stays_usable() {
977        // A section declaring the maximum `section_length` (0xFFF → 4098 bytes
978        // total). The 12-bit length structurally caps the buffer at
979        // MAX_SECTION_SIZE, so there is no unbounded growth — and (unlike the
980        // pre-#148 guard, which discarded once buf+payload crossed the cap) a
981        // valid max-length section completes at its declared length.
982        let mut section = Vec::with_capacity(MAX_SECTION_SIZE);
983        section.push(0x00); // table_id
984        section.push(0xB0 | ((4095u16 >> 8) as u8 & 0x0F));
985        section.push(0xFF); // section_length = 0xFFF
986        section.resize(MAX_SECTION_SIZE, 0u8);
987        assert_eq!(section.len(), MAX_SECTION_SIZE);
988
989        let mut reasm = SectionReassembler::default();
990        let mut first = vec![0x00u8]; // pointer_field 0
991        first.extend_from_slice(&section[..183]);
992        reasm.feed(&first, true);
993        assert!(
994            reasm.pop_section().is_none(),
995            "incomplete until the declared length arrives"
996        );
997
998        for chunk in section[183..].chunks(184) {
999            reasm.feed(chunk, false);
1000        }
1001        let out = reasm
1002            .pop_section()
1003            .expect("max-length section completes at its declared length");
1004        assert_eq!(out.len(), MAX_SECTION_SIZE);
1005        assert_eq!(out.as_ref(), &section[..]);
1006        assert!(reasm.is_empty());
1007
1008        // Extra trailing continuation data after completion is ignored (the
1009        // buffer is empty, so a non-PUSI payload is dropped) — no panic, no
1010        // spurious section.
1011        reasm.feed(&[0u8; 184], false);
1012        assert!(reasm.pop_section().is_none());
1013
1014        // State must be resettable — a fresh valid PUSI section works.
1015        let valid_section = build_section(0x00, &[0xAA]);
1016        let payload2 = build_pusi_payload(0, &[], &valid_section);
1017        reasm.feed(&payload2, true);
1018        let out = reasm
1019            .pop_section()
1020            .expect("fresh section should pop after reset");
1021        assert_eq!(out.as_ref(), &valid_section[..]);
1022    }
1023
1024    #[test]
1025    fn reassembler_handles_pusi_with_nonzero_pointer_field() {
1026        // payload = pointer_field=3, 3 bytes of prior-section tail, then new section.
1027        let prior_tail = vec![0x11, 0x22, 0x33];
1028        let new_section = build_section(0x02, &[0xBB]);
1029        assert_eq!(new_section.len(), 4);
1030        let payload = build_pusi_payload(3, &prior_tail, &new_section);
1031
1032        let mut reasm = SectionReassembler::default();
1033        reasm.feed(&payload, true);
1034
1035        let out = reasm
1036            .pop_section()
1037            .expect("section after pointer_field skip should pop");
1038        assert_eq!(out.as_ref(), &new_section[..]);
1039    }
1040
1041    #[test]
1042    fn reassembler_ignores_continuation_before_pusi() {
1043        // Feed a non-PUSI payload first (no prior PUSI seen).
1044        // SectionReassembler should discard it and stay empty.
1045        let pkt = make_packet(0x00, 0x00, PAYLOAD_FLAG, &[0xAA, 0xBB, 0xCC]);
1046
1047        let mut reasm = SectionReassembler::default();
1048        reasm.feed(&pkt[4..], false); // no PUSI
1049
1050        assert!(
1051            reasm.pop_section().is_none(),
1052            "no section should appear without prior PUSI"
1053        );
1054        assert!(
1055            reasm.pop_section().is_none(),
1056            "second pop should also be none"
1057        );
1058    }
1059
1060    /// A PUSI packet with an empty payload (adaptation field ate the body)
1061    /// is malformed but must not panic — it drops sync.
1062    #[test]
1063    fn reassembler_empty_pusi_payload_does_not_panic() {
1064        let mut reasm = SectionReassembler::default();
1065        reasm.feed(&[], true);
1066        assert!(reasm.pop_section().is_none());
1067        // Recovers on the next clean PUSI.
1068        let payload = vec![0x00u8, 0x72, 0x70, 0x01, 0x00];
1069        reasm.feed(&payload, true);
1070        assert!(reasm.pop_section().is_some());
1071    }
1072
1073    /// A maximal short-form private section (section_length 0xFFF, total
1074    /// 4098 bytes) reassembles — the ceiling is 12-bit length + 3-byte
1075    /// header, not 4096.
1076    #[test]
1077    fn reassembler_accepts_maximal_private_section() {
1078        let mut section = vec![0x80u8, 0x7F, 0xFF]; // user-private tid, SSI=0, len 0xFFF
1079        section.resize(3 + 0xFFF, 0xAB);
1080
1081        let mut reasm = SectionReassembler::default();
1082        // First TS payload: pointer_field 0 then the section start.
1083        let mut first = vec![0x00];
1084        first.extend_from_slice(&section[..183]);
1085        reasm.feed(&first, true);
1086        for chunk in section[183..].chunks(184) {
1087            reasm.feed(chunk, false);
1088        }
1089        let out = reasm.pop_section().expect("4098-byte section should pop");
1090        assert_eq!(out.len(), 4098);
1091        assert_eq!(out.as_ref(), &section[..]);
1092    }
1093
1094    /// Issue #148: a near-maximal section whose final continuation packet
1095    /// carries the section tail followed by `0xFF` **stuffing** must still
1096    /// complete. The old overflow guard counted the trailing stuffing toward
1097    /// `MAX_SECTION_SIZE` and dropped the section.
1098    #[test]
1099    fn reassembler_completes_large_section_with_trailing_stuffing() {
1100        let body = vec![0x5Au8; 4096 - 3];
1101        let section = build_section(0x50, &body); // 4096 bytes total
1102        assert_eq!(section.len(), 4096);
1103
1104        let mut reasm = SectionReassembler::default();
1105        // First payload (PUSI): pointer_field 0 + first 183 section bytes.
1106        let mut first = vec![0x00u8];
1107        first.extend_from_slice(&section[..183]);
1108        reasm.feed(&first, true);
1109
1110        // Continuation payloads of a full 184 bytes each; the final one is
1111        // padded with 0xFF stuffing to a complete 184-byte payload, exactly as
1112        // a real TS packet would carry it.
1113        let mut pos = 183usize;
1114        while pos < section.len() {
1115            let take = (section.len() - pos).min(184);
1116            let mut payload = section[pos..pos + take].to_vec();
1117            if take < 184 {
1118                payload.resize(184, 0xFF); // stuffing
1119            }
1120            reasm.feed(&payload, false);
1121            pos += take;
1122        }
1123
1124        let out = reasm
1125            .pop_section()
1126            .expect("4096-byte section must complete despite trailing stuffing (#148)");
1127        assert_eq!(out.len(), 4096);
1128        assert_eq!(out.as_ref(), &section[..]);
1129        assert!(reasm.is_empty(), "stuffing tail must be discarded");
1130    }
1131
1132    // ── adaptation field / PCR (ISO/IEC 13818-1 §2.4.3.4–2.4.3.5) ──
1133
1134    #[test]
1135    fn pcr_as_27mhz_known_value() {
1136        assert_eq!(
1137            Pcr {
1138                base: 10_000,
1139                extension: 0
1140            }
1141            .as_27mhz(),
1142            3_000_000
1143        );
1144        // base*300 + extension: 1*300 + 100 = 400.
1145        assert_eq!(
1146            Pcr {
1147                base: 1,
1148                extension: 100
1149            }
1150            .as_27mhz(),
1151            400
1152        );
1153    }
1154
1155    #[test]
1156    fn pcr_decode_from_bytes() {
1157        // 6-byte PCR encoding base=10000, extension=0 (reserved bits set).
1158        let af = [0x10u8, 0x00, 0x00, 0x13, 0x88, 0x7E, 0x00];
1159        let pcr = Pcr::parse(&af, 1).expect("6 bytes present");
1160        assert_eq!(
1161            pcr,
1162            Pcr {
1163                base: 10_000,
1164                extension: 0
1165            }
1166        );
1167        assert_eq!(pcr.as_27mhz(), 3_000_000);
1168    }
1169
1170    #[test]
1171    fn adaptation_field_flags_and_pcr() {
1172        let mut raw = [0xAAu8; TS_PACKET_SIZE];
1173        raw[0] = TS_SYNC_BYTE;
1174        raw[1] = 0x01; // pid 0x0100
1175        raw[2] = 0x00;
1176        raw[3] = ADAPTATION_FLAG | PAYLOAD_FLAG;
1177        raw[4] = 7; // adaptation_field_length: 1 flags + 6 PCR
1178        raw[5] = AF_DISCONTINUITY | AF_PCR_FLAG;
1179        raw[6..12].copy_from_slice(&[0x00, 0x00, 0x13, 0x88, 0x7E, 0x00]);
1180        // raw[12..] stays 0xAA = payload.
1181
1182        let pkt = TsPacket::parse(&raw).expect("valid packet");
1183        let af = pkt
1184            .adaptation_field()
1185            .expect("has adaptation field")
1186            .expect("adaptation field parses");
1187        assert!(af.discontinuity_indicator);
1188        assert!(!af.random_access_indicator);
1189        assert_eq!(
1190            af.pcr,
1191            Some(Pcr {
1192                base: 10_000,
1193                extension: 0
1194            })
1195        );
1196        assert_eq!(af.pcr.unwrap().as_27mhz(), 3_000_000);
1197        assert!(af.opcr.is_none());
1198        assert!(af.splice_countdown.is_none());
1199        // Payload begins right after the adaptation field (cursor 4+1+7=12).
1200        let payload = pkt.payload.expect("payload present");
1201        assert_eq!(payload.len(), TS_PACKET_SIZE - 12);
1202        assert_eq!(payload[0], 0xAA);
1203    }
1204
1205    #[test]
1206    fn no_adaptation_returns_none() {
1207        let mut raw = [0x00u8; TS_PACKET_SIZE];
1208        raw[0] = TS_SYNC_BYTE;
1209        raw[1] = 0x01;
1210        raw[3] = PAYLOAD_FLAG; // payload only
1211        let pkt = TsPacket::parse(&raw).expect("valid");
1212        assert!(pkt.adaptation_field().is_none());
1213        assert!(pkt.adaptation.is_none());
1214    }
1215
1216    #[test]
1217    fn adaptation_field_splice_countdown_negative() {
1218        let mut raw = [0xAAu8; TS_PACKET_SIZE];
1219        raw[0] = TS_SYNC_BYTE;
1220        raw[1] = 0x01;
1221        raw[2] = 0x00;
1222        raw[3] = ADAPTATION_FLAG | PAYLOAD_FLAG;
1223        raw[4] = 2; // 1 flags + 1 splice_countdown
1224        raw[5] = AF_SPLICING_FLAG;
1225        raw[6] = 0xFB; // -5 as i8
1226        let pkt = TsPacket::parse(&raw).expect("valid");
1227        let af = pkt.adaptation_field().unwrap().unwrap();
1228        assert_eq!(af.splice_countdown, Some(-5));
1229        assert!(af.pcr.is_none());
1230    }
1231
1232    // ── ScramblingControl / AdaptationFieldControl enums ──
1233
1234    #[test]
1235    fn scrambling_control_all_values() {
1236        assert_eq!(
1237            ScramblingControl::from_bits(0b00),
1238            ScramblingControl::NotScrambled
1239        );
1240        assert_eq!(
1241            ScramblingControl::from_bits(0b01),
1242            ScramblingControl::Reserved
1243        );
1244        assert_eq!(
1245            ScramblingControl::from_bits(0b10),
1246            ScramblingControl::EvenKey
1247        );
1248        assert_eq!(
1249            ScramblingControl::from_bits(0b11),
1250            ScramblingControl::OddKey
1251        );
1252        // name() labels
1253        assert_eq!(ScramblingControl::NotScrambled.name(), "not_scrambled");
1254        assert_eq!(ScramblingControl::Reserved.name(), "reserved");
1255        assert_eq!(ScramblingControl::EvenKey.name(), "even_key");
1256        assert_eq!(ScramblingControl::OddKey.name(), "odd_key");
1257        // Display delegates to name()
1258        assert_eq!(ScramblingControl::NotScrambled.to_string(), "not_scrambled");
1259        assert_eq!(ScramblingControl::OddKey.to_string(), "odd_key");
1260        // Masking: only low 2 bits matter
1261        assert_eq!(
1262            ScramblingControl::from_bits(0xFF),
1263            ScramblingControl::OddKey
1264        );
1265    }
1266
1267    #[test]
1268    fn adaptation_field_control_all_values() {
1269        assert_eq!(
1270            AdaptationFieldControl::from_flags(false, false),
1271            AdaptationFieldControl::Reserved
1272        );
1273        assert_eq!(
1274            AdaptationFieldControl::from_flags(false, true),
1275            AdaptationFieldControl::PayloadOnly
1276        );
1277        assert_eq!(
1278            AdaptationFieldControl::from_flags(true, false),
1279            AdaptationFieldControl::AdaptationOnly
1280        );
1281        assert_eq!(
1282            AdaptationFieldControl::from_flags(true, true),
1283            AdaptationFieldControl::AdaptationAndPayload
1284        );
1285        // name()
1286        assert_eq!(AdaptationFieldControl::Reserved.name(), "reserved");
1287        assert_eq!(AdaptationFieldControl::PayloadOnly.name(), "payload_only");
1288        assert_eq!(
1289            AdaptationFieldControl::AdaptationOnly.name(),
1290            "adaptation_only"
1291        );
1292        assert_eq!(
1293            AdaptationFieldControl::AdaptationAndPayload.name(),
1294            "adaptation_and_payload"
1295        );
1296        // Display
1297        assert_eq!(
1298            AdaptationFieldControl::PayloadOnly.to_string(),
1299            "payload_only"
1300        );
1301    }
1302
1303    #[test]
1304    fn ts_header_scrambling_control_accessor() {
1305        let hdr = TsHeader {
1306            tei: false,
1307            pusi: false,
1308            pid: 0x0100,
1309            scrambling: 0b10,
1310            has_adaptation: false,
1311            has_payload: true,
1312            continuity_counter: 0,
1313        };
1314        assert_eq!(hdr.scrambling_control(), ScramblingControl::EvenKey);
1315    }
1316
1317    #[test]
1318    fn ts_header_adaptation_field_control_accessor() {
1319        let hdr_payload_only = TsHeader {
1320            tei: false,
1321            pusi: false,
1322            pid: 0x0100,
1323            scrambling: 0,
1324            has_adaptation: false,
1325            has_payload: true,
1326            continuity_counter: 0,
1327        };
1328        assert_eq!(
1329            hdr_payload_only.adaptation_field_control(),
1330            AdaptationFieldControl::PayloadOnly
1331        );
1332
1333        let hdr_both = TsHeader {
1334            tei: false,
1335            pusi: false,
1336            pid: 0x0100,
1337            scrambling: 0,
1338            has_adaptation: true,
1339            has_payload: true,
1340            continuity_counter: 0,
1341        };
1342        assert_eq!(
1343            hdr_both.adaptation_field_control(),
1344            AdaptationFieldControl::AdaptationAndPayload
1345        );
1346    }
1347
1348    // ── iter_packets / extract_ts_payload helpers ──
1349
1350    #[test]
1351    fn iter_packets_yields_valid_and_skips_bad_sync() {
1352        // Two valid packets back-to-back, then one bad-sync packet.
1353        let pkt1 = make_packet(0x00, 0x00, PAYLOAD_FLAG, &[0xAA; 10]);
1354        let pkt2 = make_packet(0x40, 0x64, PAYLOAD_FLAG, &[0xBB; 10]);
1355        let mut bad = [0u8; TS_PACKET_SIZE];
1356        bad[0] = 0x00; // bad sync byte
1357
1358        let mut buf = Vec::new();
1359        buf.extend_from_slice(&pkt1);
1360        buf.extend_from_slice(&pkt2);
1361        buf.extend_from_slice(&bad);
1362
1363        let pkts: Vec<_> = super::iter_packets(&buf).collect();
1364        assert_eq!(pkts.len(), 2, "bad sync packet must be skipped");
1365        assert_eq!(pkts[0].header.pid, 0x0000);
1366        assert_eq!(pkts[1].header.pid, 0x0064);
1367    }
1368
1369    #[test]
1370    fn extract_ts_payload_payload_only() {
1371        let pkt = make_packet(0x00, 0x00, PAYLOAD_FLAG, &[0xABu8; 10]);
1372        let p = super::extract_ts_payload(&pkt).expect("payload present");
1373        assert_eq!(p[0], 0xAB);
1374        assert_eq!(p.len(), TS_PACKET_SIZE - 4);
1375    }
1376
1377    #[test]
1378    fn extract_ts_payload_adaptation_only_returns_none() {
1379        let pkt = make_packet(0x00, 0x00, ADAPTATION_FLAG, &[]);
1380        assert!(super::extract_ts_payload(&pkt).is_none());
1381    }
1382}