Skip to main content

oxideav_mpegts/
psi.rs

1//! Program-Specific Information tables — PAT + PMT.
2//!
3//! Wire layout (ISO/IEC 13818-1 §2.4.4):
4//!
5//! When carried in a TS packet whose `payload_unit_start_indicator`
6//! is set, the payload begins with a one-byte `pointer_field` that
7//! gives the offset (from the byte that follows it) to the start of
8//! the first section. Subsequent TS packets with the same PID and
9//! `payload_unit_start_indicator == 0` carry continuation bytes for
10//! the same section.
11//!
12//! A PSI section's common header (§2.4.4.10):
13//!
14//! ```text
15//! table_id (8)
16//! section_syntax_indicator (1) | '0' (1) | reserved (2) | section_length (12)
17//! table_id_extension (16)
18//! reserved (2) | version_number (5) | current_next_indicator (1)
19//! section_number (8)
20//! last_section_number (8)
21//! ... body ...
22//! CRC_32 (32, MPEG-2 left-shifting, init 0xFFFFFFFF, no reflect, no final XOR)
23//! ```
24//!
25//! `section_length` counts the bytes AFTER the section_length field
26//! itself, INCLUDING the CRC trailer.
27
28use crate::descriptor::{iter_descriptors, DescriptorIter};
29use crate::TsError;
30
31/// PAT table_id per §2.4.4.3.
32pub const PAT_TABLE_ID: u8 = 0x00;
33/// CAT table_id per §2.4.4.6 (Table 2-26).
34pub const CAT_TABLE_ID: u8 = 0x01;
35/// PMT table_id per §2.4.4.8.
36pub const PMT_TABLE_ID: u8 = 0x02;
37/// TSDT (Transport Stream Description Table) table_id per §2.4.4.12 /
38/// Table 2-26.
39pub const TSDT_TABLE_ID: u8 = 0x03;
40/// SDT (Service Description Table) `table_id` for sections describing the
41/// **actual** Transport Stream — the one that carries the SDT (ETSI
42/// EN 300 468 §5.2.3 / Table 2, `service_description_section`).
43pub const SDT_ACTUAL_TABLE_ID: u8 = 0x42;
44/// SDT `table_id` for sections describing **other** Transport Streams
45/// (ETSI EN 300 468 §5.2.3 / Table 2).
46pub const SDT_OTHER_TABLE_ID: u8 = 0x46;
47/// EIT `table_id` for present/following events on the **actual** TS
48/// (ETSI EN 300 468 §5.2.4 / Table 2 — `0x4E`).
49pub const EIT_ACTUAL_PF_TABLE_ID: u8 = 0x4E;
50/// EIT `table_id` for present/following events on **other** TSs
51/// (ETSI EN 300 468 §5.2.4 / Table 2 — `0x4F`).
52pub const EIT_OTHER_PF_TABLE_ID: u8 = 0x4F;
53/// First `table_id` of the EIT **actual**-TS schedule range
54/// (ETSI EN 300 468 §5.2.4 / Table 2 — `0x50`–`0x5F` inclusive).
55pub const EIT_ACTUAL_SCHEDULE_FIRST: u8 = 0x50;
56/// Last `table_id` of the EIT actual-TS schedule range (`0x5F`).
57pub const EIT_ACTUAL_SCHEDULE_LAST: u8 = 0x5F;
58/// First `table_id` of the EIT **other**-TS schedule range
59/// (ETSI EN 300 468 §5.2.4 / Table 2 — `0x60`–`0x6F` inclusive).
60pub const EIT_OTHER_SCHEDULE_FIRST: u8 = 0x60;
61/// Last `table_id` of the EIT other-TS schedule range (`0x6F`).
62pub const EIT_OTHER_SCHEDULE_LAST: u8 = 0x6F;
63
64/// PID reserved for the Program Association Table per §2.4.4.3 / Table 2-3.
65pub const PAT_PID: u16 = 0x0000;
66/// PID reserved for the Conditional Access Table per §2.4.4.6 / Table 2-3.
67pub const CAT_PID: u16 = 0x0001;
68/// PID reserved for the Transport Stream Description Table per §2.4.4.12
69/// / Table 2-3.
70pub const TSDT_PID: u16 = 0x0002;
71/// Fixed PID carrying the DVB SDT / BAT / ST sections (ETSI EN 300 468
72/// §5.1.3 Table 1 — `0x0011`).
73pub const SDT_PID: u16 = 0x0011;
74/// Fixed PID carrying the DVB EIT sections (ETSI EN 300 468 §5.1.3
75/// Table 1 / §5.2.4 — `0x0012`).
76pub const EIT_PID: u16 = 0x0012;
77
78/// Header bytes common to every long-form PSI section (table_id +
79/// section_length field through last_section_number).
80const SECTION_HEADER_LEN: usize = 8;
81/// Length of the CRC-32 trailer.
82const SECTION_CRC_LEN: usize = 4;
83/// Upper bound on a single PSI section per §2.4.4: 3 header bytes
84/// (table_id + section_length wrappers) plus `section_length` ≤ 0x3FD
85/// (1021). For private sections this rises to 4096 — kept conservative
86/// here since the assembler is sized for ITU-T-defined PSI tables.
87pub const MAX_PSI_SECTION_LEN: usize = 3 + 0x3FD;
88
89/// Parsed Program Association Table.
90#[derive(Debug, Default, Clone)]
91pub struct ProgramAssociationTable {
92    /// `transport_stream_id` carried in `table_id_extension`.
93    pub transport_stream_id: u16,
94    /// 5-bit `version_number`.
95    pub version_number: u8,
96    /// `current_next_indicator`.
97    pub current_next_indicator: bool,
98    /// `section_number`.
99    pub section_number: u8,
100    /// `last_section_number`.
101    pub last_section_number: u8,
102    /// `(program_number, pmt_pid)` pairs.
103    ///
104    /// `program_number == 0` denotes the network PID; otherwise the
105    /// PID is the PMT for that program.
106    pub programs: Vec<(u16, u16)>,
107}
108
109impl ProgramAssociationTable {
110    /// Parse a single PAT section. The slice must run from
111    /// `table_id` through the CRC trailer (i.e. the pointer_field has
112    /// already been skipped and the section has been extracted from
113    /// the TS payload).
114    pub fn parse(section: &[u8]) -> Result<Self, TsError> {
115        let (hdr, body) = parse_section_header(section, PAT_TABLE_ID)?;
116        let mut programs = Vec::new();
117        let mut i = 0;
118        while i + 4 <= body.len() {
119            let program_number = u16::from_be_bytes([body[i], body[i + 1]]);
120            let pid = ((((body[i + 2] & 0b0001_1111) as u16) << 8) | (body[i + 3] as u16)) & 0x1FFF;
121            programs.push((program_number, pid));
122            i += 4;
123        }
124        Ok(Self {
125            transport_stream_id: hdr.table_id_extension,
126            version_number: hdr.version_number,
127            current_next_indicator: hdr.current_next_indicator,
128            section_number: hdr.section_number,
129            last_section_number: hdr.last_section_number,
130            programs,
131        })
132    }
133}
134
135/// One elementary-stream descriptor inside a PMT.
136#[derive(Debug, Clone)]
137pub struct PmtStream {
138    /// Per ISO/IEC 13818-1 Table 2-29 (`stream_type`).
139    pub stream_type: u8,
140    /// 13-bit elementary-stream PID.
141    pub elementary_pid: u16,
142    /// Raw descriptor bytes — the contents of the
143    /// `ES_info_length`-bytes block, copied verbatim.
144    pub descriptors: Vec<u8>,
145}
146
147impl PmtStream {
148    /// Iterate per-elementary-stream descriptors as typed TLV records.
149    /// See [`crate::descriptor::iter_descriptors`].
150    pub fn iter_descriptors(&self) -> DescriptorIter<'_> {
151        iter_descriptors(&self.descriptors)
152    }
153}
154
155/// Parsed Program Map Table for one program.
156#[derive(Debug, Default, Clone)]
157pub struct ProgramMapTable {
158    /// Program number this PMT serves (from `table_id_extension`).
159    pub program_number: u16,
160    /// 5-bit `version_number`.
161    pub version_number: u8,
162    /// `current_next_indicator`.
163    pub current_next_indicator: bool,
164    /// PID carrying the program's PCR (13 bits).
165    pub pcr_pid: u16,
166    /// Raw `program_info` descriptor bytes (length given by
167    /// `program_info_length`).
168    pub program_info: Vec<u8>,
169    /// Per-stream descriptors keyed by `elementary_pid`.
170    pub streams: Vec<PmtStream>,
171}
172
173impl ProgramMapTable {
174    /// Iterate the program-wide descriptors carried in `program_info`
175    /// as typed TLV records. See
176    /// [`crate::descriptor::iter_descriptors`].
177    pub fn iter_program_descriptors(&self) -> DescriptorIter<'_> {
178        iter_descriptors(&self.program_info)
179    }
180
181    /// Parse a single PMT section. The slice must run from
182    /// `table_id` through the CRC trailer.
183    pub fn parse(section: &[u8]) -> Result<Self, TsError> {
184        let (hdr, body) = parse_section_header(section, PMT_TABLE_ID)?;
185        if body.len() < 4 {
186            return Err(TsError::Truncated {
187                what: "PMT body",
188                have: body.len(),
189                need: 4,
190            });
191        }
192        let pcr_pid = u16::from_be_bytes([body[0] & 0b0001_1111, body[1]]);
193        let program_info_length = (u16::from_be_bytes([body[2] & 0b0000_1111, body[3]])) as usize;
194        let after_pcr: usize = 4;
195        let pi_end =
196            after_pcr
197                .checked_add(program_info_length)
198                .ok_or(TsError::SectionLengthOverrun {
199                    claimed: program_info_length,
200                    have: body.len() - after_pcr,
201                })?;
202        if pi_end > body.len() {
203            return Err(TsError::SectionLengthOverrun {
204                claimed: program_info_length,
205                have: body.len() - after_pcr,
206            });
207        }
208        let program_info = body[after_pcr..pi_end].to_vec();
209
210        let mut streams = Vec::new();
211        let mut i = pi_end;
212        while i + 5 <= body.len() {
213            let stream_type = body[i];
214            let elementary_pid = u16::from_be_bytes([body[i + 1] & 0b0001_1111, body[i + 2]]);
215            let es_info_length =
216                (u16::from_be_bytes([body[i + 3] & 0b0000_1111, body[i + 4]])) as usize;
217            let descr_start = i + 5;
218            let descr_end =
219                descr_start
220                    .checked_add(es_info_length)
221                    .ok_or(TsError::SectionLengthOverrun {
222                        claimed: es_info_length,
223                        have: body.len() - descr_start,
224                    })?;
225            if descr_end > body.len() {
226                return Err(TsError::SectionLengthOverrun {
227                    claimed: es_info_length,
228                    have: body.len() - descr_start,
229                });
230            }
231            let descriptors = body[descr_start..descr_end].to_vec();
232            streams.push(PmtStream {
233                stream_type,
234                elementary_pid,
235                descriptors,
236            });
237            i = descr_end;
238        }
239        Ok(Self {
240            program_number: hdr.table_id_extension,
241            version_number: hdr.version_number,
242            current_next_indicator: hdr.current_next_indicator,
243            pcr_pid,
244            program_info,
245            streams,
246        })
247    }
248}
249
250/// Parsed Conditional Access Table per §2.4.4.6 / Table 2-27.
251///
252/// The CAT carries the program-wide CA descriptor block — one or more
253/// CA_descriptors (§2.6.16) keyed by `CA_system_ID` that point at the
254/// EMM PIDs. The CAT is signalled on the fixed PID `CAT_PID` and uses
255/// `table_id == CAT_TABLE_ID`. Like PAT and PMT it may be segmented
256/// across multiple sections, all of which share `table_id_extension`
257/// (reserved; not used to identify a CAT).
258#[derive(Debug, Default, Clone)]
259pub struct ConditionalAccessTable {
260    /// 5-bit `version_number`.
261    pub version_number: u8,
262    /// `current_next_indicator`.
263    pub current_next_indicator: bool,
264    /// `section_number`.
265    pub section_number: u8,
266    /// `last_section_number`.
267    pub last_section_number: u8,
268    /// Raw bytes of the descriptor() loop carried in the CAT body —
269    /// walk with [`Self::iter_descriptors`] to get typed CA entries.
270    pub descriptors: Vec<u8>,
271}
272
273impl ConditionalAccessTable {
274    /// Parse a single CAT section. The slice must run from `table_id`
275    /// through the CRC trailer (i.e. the pointer_field has already
276    /// been skipped).
277    pub fn parse(section: &[u8]) -> Result<Self, TsError> {
278        let (hdr, body) = parse_section_header(section, CAT_TABLE_ID)?;
279        Ok(Self {
280            version_number: hdr.version_number,
281            current_next_indicator: hdr.current_next_indicator,
282            section_number: hdr.section_number,
283            last_section_number: hdr.last_section_number,
284            descriptors: body.to_vec(),
285        })
286    }
287
288    /// Walk the carried descriptor() loop as typed TLV records.
289    pub fn iter_descriptors(&self) -> DescriptorIter<'_> {
290        iter_descriptors(&self.descriptors)
291    }
292}
293
294/// Parsed Transport Stream Description Table per §2.4.4.12 /
295/// Table 2-30-1.
296///
297/// The TSDT is optional. When present it is carried on the fixed PID
298/// [`TSDT_PID`] (`0x0002`) with `table_id == TSDT_TABLE_ID` (`0x03`)
299/// and carries a single `descriptor()` loop (§2.6) that applies to the
300/// **entire** Transport Stream rather than to one program or stream.
301/// Structurally it mirrors the CAT: the long-form section header
302/// (`table_id` … `last_section_number`), then a run of descriptors,
303/// then the CRC. The 16 bits at byte offsets 3–4 are reserved per the
304/// `reserved (18 bits)` field of Table 2-30-1 and carry no
305/// `table_id_extension` meaning; like the CAT they are ignored on
306/// parse. Sections may be segmented across the [`TSDT_PID`] stream and
307/// reassembled with [`PsiSectionAssembler`] before parsing.
308#[derive(Debug, Default, Clone)]
309pub struct TransportStreamDescriptionTable {
310    /// 5-bit `version_number`.
311    pub version_number: u8,
312    /// `current_next_indicator`.
313    pub current_next_indicator: bool,
314    /// `section_number`.
315    pub section_number: u8,
316    /// `last_section_number`.
317    pub last_section_number: u8,
318    /// Raw bytes of the descriptor() loop carried in the TSDT body —
319    /// walk with [`Self::iter_descriptors`] to get typed records.
320    pub descriptors: Vec<u8>,
321}
322
323impl TransportStreamDescriptionTable {
324    /// Parse a single TSDT section. The slice must run from `table_id`
325    /// through the CRC trailer (i.e. the pointer_field has already been
326    /// skipped).
327    pub fn parse(section: &[u8]) -> Result<Self, TsError> {
328        let (hdr, body) = parse_section_header(section, TSDT_TABLE_ID)?;
329        Ok(Self {
330            version_number: hdr.version_number,
331            current_next_indicator: hdr.current_next_indicator,
332            section_number: hdr.section_number,
333            last_section_number: hdr.last_section_number,
334            descriptors: body.to_vec(),
335        })
336    }
337
338    /// Walk the carried descriptor() loop as typed TLV records.
339    pub fn iter_descriptors(&self) -> DescriptorIter<'_> {
340        iter_descriptors(&self.descriptors)
341    }
342}
343
344/// Running status of a service (ETSI EN 300 468 §5.2.3 Table 6).
345///
346/// A 3-bit field; values 6 and 7 are reserved and surface as
347/// [`RunningStatus::Reserved`] preserving the raw value.
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
349pub enum RunningStatus {
350    /// `0` — undefined.
351    Undefined,
352    /// `1` — not running.
353    NotRunning,
354    /// `2` — starts in a few seconds (e.g. for video recording).
355    StartsSoon,
356    /// `3` — pausing.
357    Pausing,
358    /// `4` — running.
359    Running,
360    /// `5` — service off-air.
361    OffAir,
362    /// `6`–`7` — reserved for future use; raw value preserved.
363    Reserved(u8),
364}
365
366impl RunningStatus {
367    /// Map the 3-bit wire value to a typed status.
368    pub fn from_bits(value: u8) -> Self {
369        match value & 0b0000_0111 {
370            0 => RunningStatus::Undefined,
371            1 => RunningStatus::NotRunning,
372            2 => RunningStatus::StartsSoon,
373            3 => RunningStatus::Pausing,
374            4 => RunningStatus::Running,
375            5 => RunningStatus::OffAir,
376            other => RunningStatus::Reserved(other),
377        }
378    }
379}
380
381/// One service entry from the SDT service loop (ETSI EN 300 468
382/// §5.2.3 Table 5).
383#[derive(Debug, Clone)]
384pub struct SdtService {
385    /// 16-bit `service_id` — equals the corresponding PMT
386    /// `program_number` for normal services.
387    pub service_id: u16,
388    /// `EIT_schedule_flag` — EIT schedule info present in this TS.
389    pub eit_schedule_flag: bool,
390    /// `EIT_present_following_flag` — EIT present/following info present.
391    pub eit_present_following_flag: bool,
392    /// 3-bit `running_status` (Table 6).
393    pub running_status: RunningStatus,
394    /// `free_CA_mode` — when `false` every component stream is
395    /// unscrambled; when `true` one or more streams may be CA-controlled.
396    pub free_ca_mode: bool,
397    /// Raw bytes of this service's `descriptor()` loop — walk with
398    /// [`Self::iter_descriptors`]. The DVB `service_descriptor`
399    /// (tag `0x48`) carried here decodes the service / provider names.
400    pub descriptors: Vec<u8>,
401}
402
403impl SdtService {
404    /// Walk this service's descriptor loop as typed TLV records.
405    pub fn iter_descriptors(&self) -> DescriptorIter<'_> {
406        iter_descriptors(&self.descriptors)
407    }
408}
409
410/// Parsed DVB Service Description Table (ETSI EN 300 468 §5.2.3
411/// Table 5).
412///
413/// The SDT is a DVB Service Information table carried on the fixed PID
414/// [`SDT_PID`] (`0x0011`). Sections describing the **actual** TS use
415/// `table_id == SDT_ACTUAL_TABLE_ID` (`0x42`); sections describing
416/// **other** TSs use `SDT_OTHER_TABLE_ID` (`0x46`). It shares the
417/// 8-byte long-form PSI section header (parsed and CRC-verified the
418/// same way as PAT/PMT), then carries `original_network_id` (16 bits)
419/// plus one reserved byte, then a loop of [`SdtService`] entries, then
420/// the CRC.
421///
422/// `transport_stream_id` is taken from the `table_id_extension` slot
423/// per §5.2.3. The service loop's per-service descriptor blocks most
424/// commonly carry the DVB `service_descriptor` (tag `0x48`), which the
425/// crate's descriptor decoder lifts into
426/// [`crate::descriptor::ServiceDescriptor`] — the source of a service's
427/// human-readable name and provider.
428#[derive(Debug, Default, Clone)]
429pub struct ServiceDescriptionTable {
430    /// `transport_stream_id` (from `table_id_extension`).
431    pub transport_stream_id: u16,
432    /// `true` when the section's `table_id` was `0x46` (describes another
433    /// TS); `false` for `0x42` (the actual TS carrying the SDT).
434    pub other_transport_stream: bool,
435    /// 5-bit `version_number`.
436    pub version_number: u8,
437    /// `current_next_indicator`.
438    pub current_next_indicator: bool,
439    /// `section_number`.
440    pub section_number: u8,
441    /// `last_section_number`.
442    pub last_section_number: u8,
443    /// `original_network_id`.
444    pub original_network_id: u16,
445    /// Service entries carried in this section.
446    pub services: Vec<SdtService>,
447}
448
449impl ServiceDescriptionTable {
450    /// Parse a single SDT section. The slice must run from `table_id`
451    /// through the CRC trailer (i.e. the pointer_field has already been
452    /// skipped). Accepts both the actual-TS (`0x42`) and other-TS
453    /// (`0x46`) table_ids.
454    pub fn parse(section: &[u8]) -> Result<Self, TsError> {
455        let table_id = section.first().copied().unwrap_or(0);
456        let other_transport_stream = match table_id {
457            SDT_ACTUAL_TABLE_ID => false,
458            SDT_OTHER_TABLE_ID => true,
459            _ => {
460                return Err(TsError::Unsupported(
461                    "PSI table_id does not match expected value",
462                ))
463            }
464        };
465        let (hdr, body) = parse_section_header(section, table_id)?;
466        // Body layout (Table 5): original_network_id (16) +
467        // reserved_future_use (8) + service loop.
468        if body.len() < 3 {
469            return Err(TsError::Truncated {
470                what: "SDT body",
471                have: body.len(),
472                need: 3,
473            });
474        }
475        let original_network_id = u16::from_be_bytes([body[0], body[1]]);
476        // body[2] is reserved_future_use.
477        let mut services = Vec::new();
478        let mut i = 3;
479        // Each service entry: service_id (16) + flags/running_status/
480        // free_CA_mode (8) + descriptors_length (12, top 4 of next byte)
481        // + descriptor loop. Fixed head = 5 bytes before descriptors.
482        while i + 5 <= body.len() {
483            let service_id = u16::from_be_bytes([body[i], body[i + 1]]);
484            let b = body[i + 2];
485            let eit_schedule_flag = (b & 0b0000_0010) != 0;
486            let eit_present_following_flag = (b & 0b0000_0001) != 0;
487            let b3 = body[i + 3];
488            let running_status = RunningStatus::from_bits(b3 >> 5);
489            let free_ca_mode = (b3 & 0b0001_0000) != 0;
490            let descriptors_length = (u16::from_be_bytes([b3 & 0b0000_1111, body[i + 4]])) as usize;
491            let descr_start = i + 5;
492            let descr_end = descr_start.checked_add(descriptors_length).ok_or(
493                TsError::SectionLengthOverrun {
494                    claimed: descriptors_length,
495                    have: body.len() - descr_start,
496                },
497            )?;
498            if descr_end > body.len() {
499                return Err(TsError::SectionLengthOverrun {
500                    claimed: descriptors_length,
501                    have: body.len() - descr_start,
502                });
503            }
504            services.push(SdtService {
505                service_id,
506                eit_schedule_flag,
507                eit_present_following_flag,
508                running_status,
509                free_ca_mode,
510                descriptors: body[descr_start..descr_end].to_vec(),
511            });
512            i = descr_end;
513        }
514        Ok(Self {
515            transport_stream_id: hdr.table_id_extension,
516            other_transport_stream,
517            version_number: hdr.version_number,
518            current_next_indicator: hdr.current_next_indicator,
519            section_number: hdr.section_number,
520            last_section_number: hdr.last_section_number,
521            original_network_id,
522            services,
523        })
524    }
525}
526
527/// A calendar date + wall-clock time decoded from an EIT 40-bit
528/// `start_time` field (ETSI EN 300 468 §5.2.4 / annex C).
529///
530/// On the wire the field is 16 bits of Modified Julian Date (the 16
531/// least-significant bits of the MJD) followed by 24 bits of 6-digit
532/// 4-bit BCD encoding the UTC hours/minutes/seconds. The date portion
533/// is converted to the proleptic Gregorian `(year, month, day)` using
534/// the integer formula in annex C; the time portion is the decoded BCD.
535///
536/// When every bit of the 40-bit field is set (`0xFF_FFFF_FFFF`) the
537/// start time is *undefined* (e.g. for an NVOD reference event) and the
538/// parser yields `None` rather than a bogus date.
539#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub struct EitDateTime {
541    /// Full Gregorian year (e.g. `2003`), reconstructed from the 16-bit
542    /// MJD plus the annex-C `Y = year − 1900` intermediate.
543    pub year: u16,
544    /// Month, 1 (January) through 12 (December).
545    pub month: u8,
546    /// Day of month, 1 through 31.
547    pub day: u8,
548    /// UTC hour, 0 through 23 (decoded from BCD; not range-clamped).
549    pub hour: u8,
550    /// UTC minute, 0 through 59 (decoded from BCD; not range-clamped).
551    pub minute: u8,
552    /// UTC second, 0 through 59 (decoded from BCD; not range-clamped).
553    pub second: u8,
554    /// The raw 16-bit MJD value, preserved for callers that prefer to do
555    /// their own date arithmetic.
556    pub mjd: u16,
557}
558
559/// An event duration decoded from an EIT 24-bit `duration` field
560/// (ETSI EN 300 468 §5.2.4) — 6 digits of 4-bit BCD giving hours,
561/// minutes, and seconds.
562#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
563pub struct EitDuration {
564    /// Hours component (BCD-decoded; may exceed 24 for long events).
565    pub hours: u8,
566    /// Minutes component (BCD-decoded, 0–59).
567    pub minutes: u8,
568    /// Seconds component (BCD-decoded, 0–59).
569    pub seconds: u8,
570}
571
572impl EitDuration {
573    /// Total duration expressed in whole seconds.
574    pub fn as_seconds(&self) -> u32 {
575        (self.hours as u32) * 3600 + (self.minutes as u32) * 60 + (self.seconds as u32)
576    }
577}
578
579/// Decode one 4-bit-BCD byte into its 0–99 integer value.
580fn bcd_byte(b: u8) -> u8 {
581    (b >> 4) * 10 + (b & 0x0F)
582}
583
584/// Decode the 40-bit EIT `start_time` (16-bit MJD + 24-bit BCD time).
585///
586/// Returns `None` when the field is the all-ones "undefined" sentinel.
587/// The MJD→(Y,M,D) conversion follows the integer formula in
588/// ETSI EN 300 468 annex C:
589///
590/// ```text
591/// Y' = int((MJD − 15078,2) / 365,25)
592/// M' = int((MJD − 14956,1 − int(Y' × 365,25)) / 30,6001)
593/// D  = MJD − 14956 − int(Y' × 365,25) − int(M' × 30,6001)
594/// K  = 1 if M' == 14 or M' == 15 else 0
595/// Y  = Y' + K                 (years since 1900)
596/// M  = M' − 1 − K × 12
597/// ```
598fn decode_eit_start_time(bytes: [u8; 5]) -> Option<EitDateTime> {
599    if bytes == [0xFF, 0xFF, 0xFF, 0xFF, 0xFF] {
600        return None;
601    }
602    let mjd = u16::from_be_bytes([bytes[0], bytes[1]]);
603    // Annex C integer arithmetic. The constants 365,25 and 30,6001 are
604    // applied via scaled integer multiplication to avoid floating point:
605    // int(Y' × 365,25) == (Y' × 36525) / 100, etc.
606    let mjd_i = mjd as i64;
607    let yp = ((mjd_i - 15078) * 100 - 20) / 36525; // int((MJD − 15078,2)/365,25)
608    let yp_days = (yp * 36525) / 100; // int(Y' × 365,25)
609                                      // int((MJD − 14956,1 − yp_days)/30,6001)
610    let mp = ((mjd_i - 14956 - yp_days) * 10000 - 1) / 306001;
611    let mp_days = (mp * 306001) / 10000; // int(M' × 30,6001)
612    let d = mjd_i - 14956 - yp_days - mp_days;
613    let k: i64 = if mp == 14 || mp == 15 { 1 } else { 0 };
614    let y = yp + k;
615    let m = mp - 1 - k * 12;
616    let year = (1900 + y) as u16;
617    let month = m as u8;
618    let day = d as u8;
619    let hour = bcd_byte(bytes[2]);
620    let minute = bcd_byte(bytes[3]);
621    let second = bcd_byte(bytes[4]);
622    Some(EitDateTime {
623        year,
624        month,
625        day,
626        hour,
627        minute,
628        second,
629        mjd,
630    })
631}
632
633/// One event entry from the EIT event loop (ETSI EN 300 468 §5.2.4
634/// Table 7).
635#[derive(Debug, Clone)]
636pub struct EitEvent {
637    /// 16-bit `event_id`, unique within a service.
638    pub event_id: u16,
639    /// Decoded `start_time` — `None` when the field was the all-ones
640    /// "undefined" sentinel (e.g. for an NVOD reference event).
641    pub start_time: Option<EitDateTime>,
642    /// Decoded `duration` (BCD hours/minutes/seconds).
643    pub duration: EitDuration,
644    /// 3-bit `running_status` (Table 6).
645    pub running_status: RunningStatus,
646    /// `free_CA_mode` — `false` when every component stream of the event
647    /// is unscrambled; `true` when one or more may be CA-controlled.
648    pub free_ca_mode: bool,
649    /// Raw bytes of this event's `descriptor()` loop — walk with
650    /// [`Self::iter_descriptors`]. The DVB `short_event_descriptor`
651    /// (tag `0x4D`) carried here decodes the event name + short text.
652    pub descriptors: Vec<u8>,
653}
654
655impl EitEvent {
656    /// Walk this event's descriptor loop as typed TLV records.
657    pub fn iter_descriptors(&self) -> DescriptorIter<'_> {
658        iter_descriptors(&self.descriptors)
659    }
660}
661
662/// Parsed DVB Event Information Table (ETSI EN 300 468 §5.2.4 Table 7).
663///
664/// The EIT carries chronological per-event metadata (start time,
665/// duration, running status, descriptors) for the services in a
666/// multiplex. It is carried on the fixed PID [`EIT_PID`] (`0x0012`) and
667/// distinguished from other tables by `table_id`:
668///
669/// * `0x4E` — present/following events on the **actual** TS.
670/// * `0x4F` — present/following events on **other** TSs.
671/// * `0x50`–`0x5F` — event schedule on the actual TS.
672/// * `0x60`–`0x6F` — event schedule on other TSs.
673///
674/// The section shares the 8-byte long-form PSI header (CRC-verified the
675/// same way as PAT/PMT/SDT) where `service_id` lives in the
676/// `table_id_extension` slot. After the header the body carries
677/// `transport_stream_id` (16) + `original_network_id` (16) +
678/// `segment_last_section_number` (8) + `last_table_id` (8), then a loop
679/// of [`EitEvent`] entries, then the CRC.
680///
681/// The `service_id` equals the corresponding PMT `program_number` for
682/// normal services, so an EIT lets the `oxideav remux bluray://` path
683/// attach human-readable event names (via the per-event
684/// `short_event_descriptor`) and time ranges to each program.
685#[derive(Debug, Default, Clone)]
686pub struct EventInformationTable {
687    /// `service_id` (from `table_id_extension`).
688    pub service_id: u16,
689    /// `true` when the section describes other TSs (`table_id` `0x4F`
690    /// or `0x60`–`0x6F`); `false` for the actual TS (`0x4E` /
691    /// `0x50`–`0x5F`).
692    pub other_transport_stream: bool,
693    /// `true` when the section is event-schedule information
694    /// (`0x50`–`0x6F`); `false` for present/following (`0x4E` / `0x4F`).
695    pub schedule: bool,
696    /// Raw `table_id` of the parsed section.
697    pub table_id: u8,
698    /// 5-bit `version_number`.
699    pub version_number: u8,
700    /// `current_next_indicator`.
701    pub current_next_indicator: bool,
702    /// `section_number`.
703    pub section_number: u8,
704    /// `last_section_number`.
705    pub last_section_number: u8,
706    /// `transport_stream_id`.
707    pub transport_stream_id: u16,
708    /// `original_network_id`.
709    pub original_network_id: u16,
710    /// `segment_last_section_number` — last section of this segment of
711    /// the sub_table (equals `last_section_number` for unsegmented
712    /// sub_tables).
713    pub segment_last_section_number: u8,
714    /// `last_table_id` — the largest `table_id` used by this service's
715    /// sub_table (per §5.2.4 may differ per service).
716    pub last_table_id: u8,
717    /// Event entries carried in this section, in chronological order.
718    pub events: Vec<EitEvent>,
719}
720
721impl EventInformationTable {
722    /// `true` when `table_id` is any of the four EIT classifications
723    /// (`0x4E`, `0x4F`, `0x50`–`0x5F`, `0x60`–`0x6F`).
724    pub fn is_eit_table_id(table_id: u8) -> bool {
725        matches!(table_id, EIT_ACTUAL_PF_TABLE_ID | EIT_OTHER_PF_TABLE_ID)
726            || (EIT_ACTUAL_SCHEDULE_FIRST..=EIT_ACTUAL_SCHEDULE_LAST).contains(&table_id)
727            || (EIT_OTHER_SCHEDULE_FIRST..=EIT_OTHER_SCHEDULE_LAST).contains(&table_id)
728    }
729
730    /// Parse a single EIT section. The slice must run from `table_id`
731    /// through the CRC trailer (i.e. the pointer_field has already been
732    /// skipped). Accepts any of the four EIT `table_id` classifications.
733    pub fn parse(section: &[u8]) -> Result<Self, TsError> {
734        let table_id = section.first().copied().unwrap_or(0);
735        if !Self::is_eit_table_id(table_id) {
736            return Err(TsError::Unsupported(
737                "PSI table_id does not match expected value",
738            ));
739        }
740        let other_transport_stream = table_id == EIT_OTHER_PF_TABLE_ID
741            || (EIT_OTHER_SCHEDULE_FIRST..=EIT_OTHER_SCHEDULE_LAST).contains(&table_id);
742        let schedule = (EIT_ACTUAL_SCHEDULE_FIRST..=EIT_OTHER_SCHEDULE_LAST).contains(&table_id);
743        let (hdr, body) = parse_section_header(section, table_id)?;
744        // Body layout (Table 7): transport_stream_id (16) +
745        // original_network_id (16) + segment_last_section_number (8) +
746        // last_table_id (8), then the event loop.
747        if body.len() < 6 {
748            return Err(TsError::Truncated {
749                what: "EIT body",
750                have: body.len(),
751                need: 6,
752            });
753        }
754        let transport_stream_id = u16::from_be_bytes([body[0], body[1]]);
755        let original_network_id = u16::from_be_bytes([body[2], body[3]]);
756        let segment_last_section_number = body[4];
757        let last_table_id = body[5];
758        let mut events = Vec::new();
759        let mut i = 6;
760        // Each event entry: event_id (16) + start_time (40) +
761        // duration (24) + running_status (3) / free_CA_mode (1) /
762        // descriptors_length (12). Fixed head = 12 bytes before the
763        // descriptor loop.
764        while i + 12 <= body.len() {
765            let event_id = u16::from_be_bytes([body[i], body[i + 1]]);
766            let start_time = decode_eit_start_time([
767                body[i + 2],
768                body[i + 3],
769                body[i + 4],
770                body[i + 5],
771                body[i + 6],
772            ]);
773            let duration = EitDuration {
774                hours: bcd_byte(body[i + 7]),
775                minutes: bcd_byte(body[i + 8]),
776                seconds: bcd_byte(body[i + 9]),
777            };
778            let b10 = body[i + 10];
779            let running_status = RunningStatus::from_bits(b10 >> 5);
780            let free_ca_mode = (b10 & 0b0001_0000) != 0;
781            let descriptors_length =
782                (u16::from_be_bytes([b10 & 0b0000_1111, body[i + 11]])) as usize;
783            let descr_start = i + 12;
784            let descr_end = descr_start.checked_add(descriptors_length).ok_or(
785                TsError::SectionLengthOverrun {
786                    claimed: descriptors_length,
787                    have: body.len() - descr_start,
788                },
789            )?;
790            if descr_end > body.len() {
791                return Err(TsError::SectionLengthOverrun {
792                    claimed: descriptors_length,
793                    have: body.len() - descr_start,
794                });
795            }
796            events.push(EitEvent {
797                event_id,
798                start_time,
799                duration,
800                running_status,
801                free_ca_mode,
802                descriptors: body[descr_start..descr_end].to_vec(),
803            });
804            i = descr_end;
805        }
806        Ok(Self {
807            service_id: hdr.table_id_extension,
808            other_transport_stream,
809            schedule,
810            table_id,
811            version_number: hdr.version_number,
812            current_next_indicator: hdr.current_next_indicator,
813            section_number: hdr.section_number,
814            last_section_number: hdr.last_section_number,
815            transport_stream_id,
816            original_network_id,
817            segment_last_section_number,
818            last_table_id,
819            events,
820        })
821    }
822}
823
824/// Per-PID PSI section reassembler — joins TS payloads carrying the
825/// same `table_id` across multiple 188-byte TS packets per §2.4.4.
826///
827/// Real PMTs that carry many ES_descriptors (e.g. an HEVC + multi-
828/// language audio + multi-language PGS Blu-ray title) routinely run
829/// past one TS packet's ~184-byte payload budget. The spec carries
830/// the overflow into the next same-PID packet whose
831/// `payload_unit_start_indicator == 0`; the assembler concatenates
832/// those continuation payloads onto the in-flight section until
833/// `3 + section_length` bytes have been collected, then yields the
834/// completed section as a borrow over the internal buffer.
835///
836/// Wire rules enforced (§2.4.4 / §2.4.4.1):
837///
838/// * A PUSI=1 TS payload starts with a `pointer_field`. The bytes
839///   from the byte immediately following `pointer_field` for
840///   `pointer_field` bytes complete the previous in-flight section,
841///   then the next section begins. A `pointer_field == 0` means the
842///   first section starts immediately after the pointer.
843/// * A PUSI=0 TS payload contains continuation bytes for the section
844///   in flight at the end of the previous same-PID packet.
845/// * `0xFF` table_id terminates section iteration inside a single TS
846///   payload (stuffing bytes after a section).
847/// * The 4-bit `continuity_counter` advances by +1 (mod 16) between
848///   payload-carrying same-PID packets. A skipped count (per
849///   §2.4.3.3) discards the in-flight buffer rather than blindly
850///   concatenating misordered bytes — the next PUSI=1 packet rebuilds
851///   from scratch.
852///
853/// The assembler is `table_id`-agnostic — it yields raw section bytes
854/// and leaves CRC verification + table parsing to the caller (the
855/// `Parse::parse` methods on [`ProgramAssociationTable`],
856/// [`ProgramMapTable`], and [`ConditionalAccessTable`] each verify
857/// CRC-32/MPEG-2 themselves). Sections that exceed
858/// [`MAX_PSI_SECTION_LEN`] are dropped with [`TsError::SectionLengthOverrun`].
859#[derive(Debug, Default)]
860pub struct PsiSectionAssembler {
861    /// Bytes of the section being assembled, including the 3-byte
862    /// length-bearing header (table_id + 12-bit section_length).
863    in_flight: Vec<u8>,
864    /// Total length of `in_flight` once complete (= 3 + section_length).
865    /// `None` until the first 3 bytes have been collected.
866    target_len: Option<usize>,
867    /// Last `continuity_counter` observed for the assembler's PID.
868    /// `None` until the first TS packet has been fed. Used to detect a
869    /// CC skip that invalidates the in-flight buffer.
870    last_cc: Option<u8>,
871}
872
873impl PsiSectionAssembler {
874    /// Create an empty assembler.
875    pub fn new() -> Self {
876        Self::default()
877    }
878
879    /// Drop any in-flight section bytes — used when the caller knows
880    /// the underlying PID just signalled a `discontinuity_indicator`
881    /// (§2.4.3.5) or the input stream restarted.
882    pub fn reset(&mut self) {
883        self.in_flight.clear();
884        self.target_len = None;
885        self.last_cc = None;
886    }
887
888    /// Feed one TS-packet payload to the assembler.
889    ///
890    /// * `payload` is the TS packet's payload bytes (after the
891    ///   adaptation field has been stripped) — the same byte slice
892    ///   `iter_sections` would consume on a single-payload section.
893    /// * `pusi` is the value of the TS packet's
894    ///   `payload_unit_start_indicator`.
895    /// * `continuity_counter` is the 4-bit field from the TS packet's
896    ///   byte-3 low nibble — used to detect a dropped same-PID packet
897    ///   that would corrupt the in-flight section if blindly
898    ///   concatenated.
899    ///
900    /// Returns every complete section gathered by this call (a single
901    /// payload can carry many short sections, or finish off one
902    /// section and start another). Each yielded `Vec<u8>` runs from
903    /// `table_id` through the CRC trailer — exactly what
904    /// [`ProgramAssociationTable::parse`] /
905    /// [`ProgramMapTable::parse`] / [`ConditionalAccessTable::parse`]
906    /// expect.
907    pub fn feed(
908        &mut self,
909        payload: &[u8],
910        pusi: bool,
911        continuity_counter: u8,
912    ) -> Result<Vec<Vec<u8>>, TsError> {
913        // CC continuity check — the 4-bit field wraps mod 16. A skip
914        // means we missed a same-PID payload; the in-flight section
915        // is no longer trustable, so drop it and resume on the next
916        // PUSI=1 packet.
917        let cc = continuity_counter & 0x0F;
918        if let Some(prev) = self.last_cc {
919            let expected = (prev + 1) & 0x0F;
920            if cc != expected {
921                // CC skip — discard buffer.
922                self.in_flight.clear();
923                self.target_len = None;
924            }
925        }
926        self.last_cc = Some(cc);
927
928        let mut out = Vec::new();
929        let mut rest: &[u8] = payload;
930
931        if pusi {
932            // pointer_field = first byte of the payload (§2.4.4.1).
933            if rest.is_empty() {
934                return Ok(out);
935            }
936            let ptr = rest[0] as usize;
937            rest = &rest[1..];
938            if ptr > rest.len() {
939                // Pointer overruns the payload — corrupt header, drop
940                // whatever was in flight and stop.
941                self.in_flight.clear();
942                self.target_len = None;
943                return Ok(out);
944            }
945            let (tail_of_prev, after_ptr) = rest.split_at(ptr);
946            // `tail_of_prev` finishes the previous in-flight section
947            // (when there was one). It may be padded with 0xFF
948            // stuffing if no continuation was due — both cases share
949            // the same "extend then check completion" path.
950            if !self.in_flight.is_empty() || self.target_len.is_some() {
951                if let Some(section) = self.extend_in_flight(tail_of_prev)? {
952                    out.push(section);
953                }
954                // If the in-flight section isn't done after consuming
955                // `tail_of_prev`, drop it: a PUSI=1 packet promises a
956                // fresh section is about to begin, so the previous
957                // section can't be straddled further into this
958                // packet.
959                self.in_flight.clear();
960                self.target_len = None;
961            }
962            rest = after_ptr;
963        } else if self.target_len.is_none() {
964            // PUSI=0 with nothing in flight — payload bytes belong to
965            // a section that started before we attached. Skip.
966            return Ok(out);
967        } else {
968            // PUSI=0 continuation — every payload byte feeds the
969            // in-flight section.
970            if let Some(section) = self.extend_in_flight(rest)? {
971                out.push(section);
972            }
973            return Ok(out);
974        }
975
976        // After pointer_field handling, `rest` points at one-or-more
977        // freshly-starting sections. Each begins with a 3-byte
978        // length-bearing header; 0xFF is a stuffing terminator.
979        while !rest.is_empty() {
980            if rest[0] == 0xFF {
981                // Stuffing — rest of payload is filler.
982                break;
983            }
984            if rest.len() < 3 {
985                // Header straddles into the next TS packet — buffer
986                // what we have and wait for the continuation.
987                self.in_flight.extend_from_slice(rest);
988                self.target_len = None;
989                break;
990            }
991            let section_length =
992                ((((rest[1] & 0b0000_1111) as usize) << 8) | (rest[2] as usize)) & 0x0FFF;
993            let total = 3 + section_length;
994            if total > MAX_PSI_SECTION_LEN {
995                self.in_flight.clear();
996                self.target_len = None;
997                return Err(TsError::SectionLengthOverrun {
998                    claimed: section_length,
999                    have: rest.len() - 3,
1000                });
1001            }
1002            if rest.len() >= total {
1003                // Section fits entirely within this payload — emit
1004                // and advance.
1005                out.push(rest[..total].to_vec());
1006                rest = &rest[total..];
1007            } else {
1008                // Section straddles into next TS packet — buffer.
1009                self.in_flight.clear();
1010                self.in_flight.extend_from_slice(rest);
1011                self.target_len = Some(total);
1012                break;
1013            }
1014        }
1015
1016        Ok(out)
1017    }
1018
1019    /// Extend the in-flight section with `bytes`. If the section
1020    /// completes, return it (and clear the buffer). Returns `Ok(None)`
1021    /// when more bytes are still needed.
1022    fn extend_in_flight(&mut self, bytes: &[u8]) -> Result<Option<Vec<u8>>, TsError> {
1023        if bytes.is_empty() {
1024            return Ok(None);
1025        }
1026        // If we don't yet have a target_len, we're still gathering the
1027        // 3-byte length-bearing header.
1028        if self.target_len.is_none() {
1029            let want = 3usize.saturating_sub(self.in_flight.len());
1030            let take = want.min(bytes.len());
1031            self.in_flight.extend_from_slice(&bytes[..take]);
1032            if self.in_flight.len() < 3 {
1033                return Ok(None);
1034            }
1035            let section_length = ((((self.in_flight[1] & 0b0000_1111) as usize) << 8)
1036                | (self.in_flight[2] as usize))
1037                & 0x0FFF;
1038            let total = 3 + section_length;
1039            if total > MAX_PSI_SECTION_LEN {
1040                self.in_flight.clear();
1041                self.target_len = None;
1042                return Err(TsError::SectionLengthOverrun {
1043                    claimed: section_length,
1044                    have: bytes.len() - take,
1045                });
1046            }
1047            self.target_len = Some(total);
1048            // Recurse on the remainder past the header bytes.
1049            return self.extend_in_flight(&bytes[take..]);
1050        }
1051        let target = self.target_len.expect("checked above");
1052        let want = target.saturating_sub(self.in_flight.len());
1053        let take = want.min(bytes.len());
1054        self.in_flight.extend_from_slice(&bytes[..take]);
1055        if self.in_flight.len() < target {
1056            return Ok(None);
1057        }
1058        // Section complete.
1059        let done = std::mem::take(&mut self.in_flight);
1060        self.target_len = None;
1061        Ok(Some(done))
1062    }
1063}
1064
1065/// Shared header parse — verifies sync, length, CRC.
1066struct SectionHeader {
1067    table_id_extension: u16,
1068    version_number: u8,
1069    current_next_indicator: bool,
1070    section_number: u8,
1071    last_section_number: u8,
1072}
1073
1074fn parse_section_header(
1075    section: &[u8],
1076    expected_table_id: u8,
1077) -> Result<(SectionHeader, &[u8]), TsError> {
1078    if section.len() < SECTION_HEADER_LEN + SECTION_CRC_LEN {
1079        return Err(TsError::Truncated {
1080            what: "PSI section header",
1081            have: section.len(),
1082            need: SECTION_HEADER_LEN + SECTION_CRC_LEN,
1083        });
1084    }
1085    let table_id = section[0];
1086    if table_id != expected_table_id {
1087        return Err(TsError::Unsupported(
1088            "PSI table_id does not match expected value",
1089        ));
1090    }
1091    let b1 = section[1];
1092    let b2 = section[2];
1093    // section_length is 12 bits across the bottom 4 of b1 + b2.
1094    let section_length = ((((b1 & 0b0000_1111) as usize) << 8) | (b2 as usize)) & 0x0FFF;
1095
1096    // section_length counts bytes after itself — i.e. from `section[3]`
1097    // through the CRC. So total section size = 3 + section_length.
1098    let total = 3 + section_length;
1099    if total > section.len() {
1100        return Err(TsError::SectionLengthOverrun {
1101            claimed: section_length,
1102            have: section.len() - 3,
1103        });
1104    }
1105    let section = &section[..total];
1106
1107    // Verify the MPEG-2 CRC over [section_start .. section_end - 4].
1108    let crc_pos = total - SECTION_CRC_LEN;
1109    let computed = mpeg2_crc32(&section[..crc_pos]);
1110    let header_crc = u32::from_be_bytes([
1111        section[crc_pos],
1112        section[crc_pos + 1],
1113        section[crc_pos + 2],
1114        section[crc_pos + 3],
1115    ]);
1116    if computed != header_crc {
1117        return Err(TsError::PsiCrcMismatch {
1118            header: header_crc,
1119            computed,
1120        });
1121    }
1122
1123    let table_id_extension = u16::from_be_bytes([section[3], section[4]]);
1124    let b5 = section[5];
1125    let version_number = (b5 >> 1) & 0b0001_1111;
1126    let current_next_indicator = (b5 & 0b0000_0001) != 0;
1127    let section_number = section[6];
1128    let last_section_number = section[7];
1129
1130    let body = &section[SECTION_HEADER_LEN..crc_pos];
1131    Ok((
1132        SectionHeader {
1133            table_id_extension,
1134            version_number,
1135            current_next_indicator,
1136            section_number,
1137            last_section_number,
1138        },
1139        body,
1140    ))
1141}
1142
1143/// MPEG-2 CRC-32 (poly 0x04C11DB7, init 0xFFFFFFFF, MSB-first, no
1144/// reflect, no final XOR).
1145pub fn mpeg2_crc32(bytes: &[u8]) -> u32 {
1146    let mut crc: u32 = 0xFFFF_FFFF;
1147    for &b in bytes {
1148        crc ^= (b as u32) << 24;
1149        for _ in 0..8 {
1150            if (crc & 0x8000_0000) != 0 {
1151                crc = (crc << 1) ^ 0x04C1_1DB7;
1152            } else {
1153                crc <<= 1;
1154            }
1155        }
1156    }
1157    crc
1158}
1159
1160/// Iterate the long-form PSI sections carried in one TS-packet payload.
1161///
1162/// `ts_payload` is the TS payload bytes (after AF stripping) of a TS
1163/// packet whose `payload_unit_start_indicator` was set. The first
1164/// byte is treated as the `pointer_field`. Each successive section is
1165/// located by reading its `section_length`. Stuffing bytes (`0xFF`
1166/// table_id) terminate iteration.
1167///
1168/// Each yielded slice runs from `table_id` through the CRC trailer.
1169pub fn iter_sections(ts_payload: &[u8]) -> SectionIter<'_> {
1170    if ts_payload.is_empty() {
1171        return SectionIter { rest: &[][..] };
1172    }
1173    let ptr = ts_payload[0] as usize;
1174    let start = 1 + ptr;
1175    if start > ts_payload.len() {
1176        return SectionIter { rest: &[][..] };
1177    }
1178    SectionIter {
1179        rest: &ts_payload[start..],
1180    }
1181}
1182
1183/// Iterator returned by [`iter_sections`].
1184#[derive(Debug)]
1185pub struct SectionIter<'a> {
1186    rest: &'a [u8],
1187}
1188
1189impl<'a> Iterator for SectionIter<'a> {
1190    type Item = &'a [u8];
1191
1192    fn next(&mut self) -> Option<Self::Item> {
1193        // Need at least the 3-byte length-bearing header.
1194        if self.rest.len() < 3 {
1195            return None;
1196        }
1197        // `0xFF` is the stuffing byte that fills out a section-bearing
1198        // TS payload.
1199        if self.rest[0] == 0xFF {
1200            return None;
1201        }
1202        let section_length =
1203            ((((self.rest[1] & 0b0000_1111) as usize) << 8) | (self.rest[2] as usize)) & 0x0FFF;
1204        let total = 3 + section_length;
1205        if total > self.rest.len() {
1206            return None;
1207        }
1208        let (head, tail) = self.rest.split_at(total);
1209        self.rest = tail;
1210        Some(head)
1211    }
1212}
1213
1214#[cfg(test)]
1215mod tests {
1216    use super::*;
1217    use crate::descriptor::DescriptorBody;
1218
1219    /// Build a PAT section for a single (program_number, pmt_pid)
1220    /// pair. Section bytes run from table_id through CRC.
1221    fn build_pat_section(tsid: u16, version: u8, programs: &[(u16, u16)]) -> Vec<u8> {
1222        // Body = 4 bytes per program.
1223        // Section total after section_length = 5 (rest of header) + body + 4 (CRC).
1224        let body_len = programs.len() * 4;
1225        let section_length = 5 + body_len + 4;
1226        let mut s = Vec::with_capacity(3 + section_length);
1227        s.push(PAT_TABLE_ID);
1228        // section_syntax_indicator=1, '0', reserved=0b11, then 12-bit
1229        // length.
1230        let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
1231        s.push(len_hi);
1232        s.push((section_length & 0xFF) as u8);
1233        s.extend_from_slice(&tsid.to_be_bytes());
1234        // reserved=0b11, version (5), current_next=1.
1235        s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
1236        s.push(0); // section_number
1237        s.push(0); // last_section_number
1238        for (prog, pid) in programs {
1239            s.extend_from_slice(&prog.to_be_bytes());
1240            // reserved=0b111, then 13-bit PID.
1241            s.push(0b1110_0000 | ((pid >> 8) & 0x1F) as u8);
1242            s.push((pid & 0xFF) as u8);
1243        }
1244        let crc = mpeg2_crc32(&s);
1245        s.extend_from_slice(&crc.to_be_bytes());
1246        s
1247    }
1248
1249    fn build_pmt_section(
1250        program_number: u16,
1251        version: u8,
1252        pcr_pid: u16,
1253        program_info: &[u8],
1254        streams: &[(u8, u16, &[u8])],
1255    ) -> Vec<u8> {
1256        // Body = 4 bytes (pcr/program_info_length) + program_info + Σ(5+es_info).
1257        let body_len: usize =
1258            4 + program_info.len() + streams.iter().map(|(_, _, d)| 5 + d.len()).sum::<usize>();
1259        let section_length = 5 + body_len + 4;
1260        let mut s = Vec::with_capacity(3 + section_length);
1261        s.push(PMT_TABLE_ID);
1262        let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
1263        s.push(len_hi);
1264        s.push((section_length & 0xFF) as u8);
1265        s.extend_from_slice(&program_number.to_be_bytes());
1266        s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
1267        s.push(0);
1268        s.push(0);
1269        // PCR_PID
1270        s.push(0b1110_0000 | ((pcr_pid >> 8) & 0x1F) as u8);
1271        s.push((pcr_pid & 0xFF) as u8);
1272        // program_info_length
1273        let pil = program_info.len() as u16;
1274        s.push(0b1111_0000 | ((pil >> 8) & 0x0F) as u8);
1275        s.push((pil & 0xFF) as u8);
1276        s.extend_from_slice(program_info);
1277        for (stype, epid, descr) in streams {
1278            s.push(*stype);
1279            s.push(0b1110_0000 | ((*epid >> 8) & 0x1F) as u8);
1280            s.push((*epid & 0xFF) as u8);
1281            let el = descr.len() as u16;
1282            s.push(0b1111_0000 | ((el >> 8) & 0x0F) as u8);
1283            s.push((el & 0xFF) as u8);
1284            s.extend_from_slice(descr);
1285        }
1286        let crc = mpeg2_crc32(&s);
1287        s.extend_from_slice(&crc.to_be_bytes());
1288        s
1289    }
1290
1291    #[test]
1292    fn mpeg2_crc32_known_vector() {
1293        // CRC of the byte "1": MPEG-2 CRC of the ASCII string "1"
1294        // is 0xA6B15CD4 — easy to confirm with any spec-matching
1295        // implementation. We assert internal consistency via the
1296        // round-trip below; this anchors the polynomial wiring.
1297        let crc = mpeg2_crc32(b"123456789");
1298        // The classic check value for CRC-32/MPEG-2 over "123456789"
1299        // is 0x0376E6E7 (see CRC-Catalogue / Greg Cook).
1300        assert_eq!(crc, 0x0376_E6E7);
1301    }
1302
1303    #[test]
1304    fn pat_one_program_round_trip() {
1305        let section = build_pat_section(1, 3, &[(1, 0x100)]);
1306        let pat = ProgramAssociationTable::parse(&section).unwrap();
1307        assert_eq!(pat.transport_stream_id, 1);
1308        assert_eq!(pat.version_number, 3);
1309        assert!(pat.current_next_indicator);
1310        assert_eq!(pat.programs, vec![(1, 0x100)]);
1311    }
1312
1313    #[test]
1314    fn pmt_avc_ac3_pgs_round_trip() {
1315        // Stream descriptors: one AVC (0x1B), one AC-3 (0x81), one
1316        // PGS (0x90), each with a tiny descriptor blob to prove the
1317        // length/bytes survive.
1318        let avc_descr: &[u8] = &[0x52, 0x01, 0x00]; // dummy stream_identifier
1319        let ac3_descr: &[u8] = &[0x6A, 0x01, 0x80];
1320        let pgs_descr: &[u8] = &[];
1321        let section = build_pmt_section(
1322            1,
1323            5,
1324            0x100,
1325            &[],
1326            &[
1327                (0x1B, 0x1011, avc_descr),
1328                (0x81, 0x1100, ac3_descr),
1329                (0x90, 0x1200, pgs_descr),
1330            ],
1331        );
1332        let pmt = ProgramMapTable::parse(&section).unwrap();
1333        assert_eq!(pmt.program_number, 1);
1334        assert_eq!(pmt.version_number, 5);
1335        assert!(pmt.current_next_indicator);
1336        assert_eq!(pmt.pcr_pid, 0x100);
1337        assert!(pmt.program_info.is_empty());
1338        assert_eq!(pmt.streams.len(), 3);
1339        assert_eq!(pmt.streams[0].stream_type, 0x1B);
1340        assert_eq!(pmt.streams[0].elementary_pid, 0x1011);
1341        assert_eq!(pmt.streams[0].descriptors, avc_descr);
1342        assert_eq!(pmt.streams[1].stream_type, 0x81);
1343        assert_eq!(pmt.streams[1].elementary_pid, 0x1100);
1344        assert_eq!(pmt.streams[1].descriptors, ac3_descr);
1345        assert_eq!(pmt.streams[2].stream_type, 0x90);
1346        assert_eq!(pmt.streams[2].elementary_pid, 0x1200);
1347        assert!(pmt.streams[2].descriptors.is_empty());
1348    }
1349
1350    #[test]
1351    fn psi_crc_corruption_is_rejected() {
1352        let mut section = build_pat_section(7, 0, &[(1, 0x100)]);
1353        // Flip a payload bit.
1354        section[3] ^= 0x01;
1355        let err = ProgramAssociationTable::parse(&section).unwrap_err();
1356        match err {
1357            TsError::PsiCrcMismatch { .. } => {}
1358            other => panic!("expected PsiCrcMismatch, got {other:?}"),
1359        }
1360    }
1361
1362    #[test]
1363    fn iter_sections_skips_pointer_field_and_stuffing() {
1364        let section = build_pat_section(1, 0, &[(1, 0x100)]);
1365        // Simulate a TS payload: pointer_field=0, then the section,
1366        // then stuffing.
1367        let mut payload = Vec::new();
1368        payload.push(0u8); // pointer_field
1369        payload.extend_from_slice(&section);
1370        payload.extend(std::iter::repeat(0xFF).take(10));
1371        let mut it = iter_sections(&payload);
1372        let s = it.next().expect("section");
1373        assert_eq!(s, section);
1374        assert!(it.next().is_none());
1375    }
1376
1377    #[test]
1378    fn iter_sections_with_nonzero_pointer_field() {
1379        let section = build_pat_section(1, 0, &[(1, 0x100)]);
1380        let mut payload = Vec::new();
1381        payload.push(3u8); // pointer_field = 3
1382        payload.extend_from_slice(&[0xAA, 0xBB, 0xCC]); // 3 stuffing-ish bytes
1383        payload.extend_from_slice(&section);
1384        let s = iter_sections(&payload).next().expect("section");
1385        assert_eq!(s, section);
1386    }
1387
1388    #[test]
1389    fn pmt_per_stream_descriptors_decode_iso639() {
1390        // ES descriptor block: an ISO-639 language descriptor with two
1391        // entries — eng/0, jpn/2.
1392        let es_descr: &[u8] = &[0x0A, 0x08, b'e', b'n', b'g', 0x00, b'j', b'p', b'n', 0x02];
1393        let section = build_pmt_section(1, 0, 0x100, &[], &[(0x81, 0x1100, es_descr)]);
1394        let pmt = ProgramMapTable::parse(&section).unwrap();
1395        assert_eq!(pmt.streams.len(), 1);
1396        let descriptors: Vec<_> = pmt.streams[0]
1397            .iter_descriptors()
1398            .collect::<Result<_, _>>()
1399            .unwrap();
1400        assert_eq!(descriptors.len(), 1);
1401        assert_eq!(descriptors[0].tag, 0x0A);
1402        match &descriptors[0].body {
1403            crate::descriptor::DescriptorBody::Iso639Language(langs) => {
1404                assert_eq!(langs.len(), 2);
1405                assert_eq!(&langs[0].language, b"eng");
1406                assert_eq!(&langs[1].language, b"jpn");
1407                assert_eq!(langs[1].audio_type, 2);
1408            }
1409            other => panic!("expected Iso639Language, got {other:?}"),
1410        }
1411    }
1412
1413    #[test]
1414    fn pmt_program_info_descriptors_decode_registration() {
1415        // program_info: registration descriptor with format_identifier=HDMV.
1416        let program_info: &[u8] = &[0x05, 0x04, b'H', b'D', b'M', b'V'];
1417        let section = build_pmt_section(1, 0, 0x100, program_info, &[(0x1B, 0x1011, &[])]);
1418        let pmt = ProgramMapTable::parse(&section).unwrap();
1419        let descriptors: Vec<_> = pmt
1420            .iter_program_descriptors()
1421            .collect::<Result<_, _>>()
1422            .unwrap();
1423        assert_eq!(descriptors.len(), 1);
1424        match &descriptors[0].body {
1425            crate::descriptor::DescriptorBody::Registration {
1426                format_identifier, ..
1427            } => {
1428                assert_eq!(format_identifier, b"HDMV");
1429            }
1430            other => panic!("expected Registration, got {other:?}"),
1431        }
1432    }
1433
1434    #[test]
1435    fn pat_network_pid_program_zero() {
1436        let section = build_pat_section(2, 0, &[(0, 0x10), (1, 0x100)]);
1437        let pat = ProgramAssociationTable::parse(&section).unwrap();
1438        assert_eq!(pat.programs[0], (0, 0x10));
1439        assert_eq!(pat.programs[1], (1, 0x100));
1440    }
1441
1442    /// Build a CAT section carrying `descriptors` as its body.
1443    /// Section bytes run from table_id through CRC.
1444    fn build_cat_section(version: u8, descriptors: &[u8]) -> Vec<u8> {
1445        let section_length = 5 + descriptors.len() + 4;
1446        let mut s = Vec::with_capacity(3 + section_length);
1447        s.push(CAT_TABLE_ID);
1448        let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
1449        s.push(len_hi);
1450        s.push((section_length & 0xFF) as u8);
1451        // reserved (18 bits high padding) -> for CAT, table_id_extension
1452        // is the 16 reserved bits of bytes 3..5 — encode as 0xFFFF.
1453        s.push(0xFF);
1454        s.push(0xFF);
1455        // reserved | version | current_next.
1456        s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
1457        s.push(0);
1458        s.push(0);
1459        s.extend_from_slice(descriptors);
1460        let crc = mpeg2_crc32(&s);
1461        s.extend_from_slice(&crc.to_be_bytes());
1462        s
1463    }
1464
1465    #[test]
1466    fn cat_round_trip_single_ca_descriptor() {
1467        // CA_descriptor (tag 0x09): CA_system_ID=0x0500, CA_PID=0x0123,
1468        // private_data=0xCA 0xFE.
1469        let ca_descr: &[u8] = &[0x09, 0x06, 0x05, 0x00, 0xE1, 0x23, 0xCA, 0xFE];
1470        let section = build_cat_section(3, ca_descr);
1471        let cat = ConditionalAccessTable::parse(&section).unwrap();
1472        assert_eq!(cat.version_number, 3);
1473        assert!(cat.current_next_indicator);
1474        let descrs: Vec<_> = cat.iter_descriptors().collect::<Result<_, _>>().unwrap();
1475        assert_eq!(descrs.len(), 1);
1476        match &descrs[0].body {
1477            crate::descriptor::DescriptorBody::Ca(ca) => {
1478                assert_eq!(ca.ca_system_id, 0x0500);
1479                assert_eq!(ca.ca_pid, 0x0123);
1480                assert_eq!(ca.private_data, &[0xCA, 0xFE]);
1481            }
1482            other => panic!("expected CA, got {other:?}"),
1483        }
1484    }
1485
1486    #[test]
1487    fn cat_rejects_wrong_table_id() {
1488        // Build a PAT-shaped section and parse it as CAT — table_id
1489        // mismatch must surface as an error.
1490        let section = build_pat_section(1, 0, &[(1, 0x100)]);
1491        let err = ConditionalAccessTable::parse(&section).unwrap_err();
1492        match err {
1493            TsError::Unsupported(_) => {}
1494            other => panic!("expected Unsupported, got {other:?}"),
1495        }
1496    }
1497
1498    /// Build a TSDT section (table_id 0x03) carrying `descriptors` as
1499    /// its body. Section bytes run from table_id through CRC. The TSDT
1500    /// shares the CAT's wire layout: a long-form header whose bytes 3–4
1501    /// are reserved, then a descriptor() loop, then CRC.
1502    fn build_tsdt_section(version: u8, section_number: u8, descriptors: &[u8]) -> Vec<u8> {
1503        let section_length = 5 + descriptors.len() + 4;
1504        let mut s = Vec::with_capacity(3 + section_length);
1505        s.push(TSDT_TABLE_ID);
1506        let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
1507        s.push(len_hi);
1508        s.push((section_length & 0xFF) as u8);
1509        // reserved 16 bits at bytes 3–4 (no table_id_extension meaning).
1510        s.push(0xFF);
1511        s.push(0xFF);
1512        // reserved | version | current_next.
1513        s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
1514        s.push(section_number);
1515        s.push(section_number); // last_section_number = section_number
1516        s.extend_from_slice(descriptors);
1517        let crc = mpeg2_crc32(&s);
1518        s.extend_from_slice(&crc.to_be_bytes());
1519        s
1520    }
1521
1522    #[test]
1523    fn tsdt_round_trip_registration_descriptor() {
1524        // Table 2-39 restricts the TSDT to 2.6 descriptors; a
1525        // registration_descriptor (tag 0x05) is one such — carry
1526        // format_identifier "HDMV" + a private byte.
1527        let reg_descr: &[u8] = &[0x05, 0x05, b'H', b'D', b'M', b'V', 0xAB];
1528        let section = build_tsdt_section(9, 0, reg_descr);
1529        let tsdt = TransportStreamDescriptionTable::parse(&section).unwrap();
1530        assert_eq!(tsdt.version_number, 9);
1531        assert!(tsdt.current_next_indicator);
1532        assert_eq!(tsdt.section_number, 0);
1533        assert_eq!(tsdt.last_section_number, 0);
1534        let descrs: Vec<_> = tsdt.iter_descriptors().collect::<Result<_, _>>().unwrap();
1535        assert_eq!(descrs.len(), 1);
1536        match &descrs[0].body {
1537            crate::descriptor::DescriptorBody::Registration {
1538                format_identifier, ..
1539            } => assert_eq!(format_identifier, b"HDMV"),
1540            other => panic!("expected Registration, got {other:?}"),
1541        }
1542    }
1543
1544    #[test]
1545    fn tsdt_empty_descriptor_loop() {
1546        // A TSDT with no descriptors is well-formed: header + CRC only.
1547        let section = build_tsdt_section(0, 0, &[]);
1548        let tsdt = TransportStreamDescriptionTable::parse(&section).unwrap();
1549        assert_eq!(tsdt.version_number, 0);
1550        assert!(tsdt.descriptors.is_empty());
1551        assert_eq!(tsdt.iter_descriptors().count(), 0);
1552    }
1553
1554    #[test]
1555    fn tsdt_rejects_wrong_table_id() {
1556        // A CAT-shaped section (table_id 0x01) must not parse as TSDT.
1557        let section = build_cat_section(1, &[0x09, 0x04, 0x05, 0x00, 0xE1, 0x23]);
1558        let err = TransportStreamDescriptionTable::parse(&section).unwrap_err();
1559        match err {
1560            TsError::Unsupported(_) => {}
1561            other => panic!("expected Unsupported, got {other:?}"),
1562        }
1563    }
1564
1565    #[test]
1566    fn tsdt_reassembles_via_psi_assembler() {
1567        // The TSDT shares the PSI section envelope, so the generic
1568        // assembler must reassemble + hand it to TSDT::parse. Drive a
1569        // single-payload feed (pointer_field = 0).
1570        let reg_descr: &[u8] = &[0x05, 0x04, b'A', b'V', b'0', b'1'];
1571        let section = build_tsdt_section(2, 0, reg_descr);
1572        let mut payload = vec![0u8]; // pointer_field = 0
1573        payload.extend_from_slice(&section);
1574        payload.resize(184, 0xFF);
1575        let mut asm = PsiSectionAssembler::new();
1576        let out = asm.feed(&payload, true, 0).unwrap();
1577        assert_eq!(out.len(), 1);
1578        let tsdt = TransportStreamDescriptionTable::parse(&out[0]).unwrap();
1579        assert_eq!(tsdt.version_number, 2);
1580        assert_eq!(tsdt.iter_descriptors().count(), 1);
1581    }
1582
1583    /// Build a synthetic TS packet carrying `payload` with the given
1584    /// PUSI bit and continuity counter. Used to drive
1585    /// `PsiSectionAssembler` without going through the full
1586    /// `TsPacket::parse` path.
1587    fn fake_ts_payload(payload: &[u8], _pusi: bool, _cc: u8) -> Vec<u8> {
1588        // The assembler receives the already-extracted TS payload,
1589        // not the wire bytes — so this helper just hands back the
1590        // payload as-is. Kept for symmetry with the test layout.
1591        payload.to_vec()
1592    }
1593
1594    #[test]
1595    fn assembler_single_packet_section() {
1596        // A short PAT fits in one TS payload — the assembler must
1597        // emit it on a single feed() call.
1598        let section = build_pat_section(7, 0, &[(1, 0x100)]);
1599        let mut payload = vec![0u8]; // pointer_field = 0
1600        payload.extend_from_slice(&section);
1601        // Pad with stuffing so the payload looks like a real 184-byte
1602        // TS data area.
1603        payload.resize(184, 0xFF);
1604        let mut asm = PsiSectionAssembler::new();
1605        let out = asm
1606            .feed(&fake_ts_payload(&payload, true, 0), true, 0)
1607            .unwrap();
1608        assert_eq!(out.len(), 1);
1609        assert_eq!(out[0], section);
1610        let pat = ProgramAssociationTable::parse(&out[0]).unwrap();
1611        assert_eq!(pat.programs, vec![(1, 0x100)]);
1612    }
1613
1614    #[test]
1615    fn assembler_section_spans_two_ts_packets() {
1616        // Build a PMT large enough to overflow one TS payload — use
1617        // many ES_descriptors per stream entry to inflate ES_info.
1618        let stuff_descr = vec![0u8; 100]; // raw 100-byte descriptor blob
1619        let mut descr_block = vec![];
1620        descr_block.push(0xC0); // user-private tag → DescriptorBody::Raw
1621        descr_block.push(stuff_descr.len() as u8);
1622        descr_block.extend_from_slice(&stuff_descr);
1623        let section = build_pmt_section(
1624            1,
1625            0,
1626            0x100,
1627            &[],
1628            &[(0x1B, 0x1011, &descr_block), (0x81, 0x1100, &descr_block)],
1629        );
1630        assert!(
1631            section.len() > 184,
1632            "section ({} bytes) must exceed a single TS payload to exercise the assembler",
1633            section.len()
1634        );
1635
1636        // First TS payload: pointer_field=0, then first 183 bytes of
1637        // section. The TS packet has a 184-byte payload (no
1638        // adaptation_field), pointer_field eats one byte, leaving 183
1639        // bytes of section.
1640        let mut p0 = vec![0u8]; // pointer_field
1641        p0.extend_from_slice(&section[..183]);
1642        // Second TS payload: continuation = rest of section, plus
1643        // stuffing to fill 184 bytes.
1644        let mut p1 = Vec::new();
1645        p1.extend_from_slice(&section[183..]);
1646        p1.resize(184, 0xFF);
1647
1648        let mut asm = PsiSectionAssembler::new();
1649        let out0 = asm.feed(&p0, true, 5).unwrap();
1650        assert!(
1651            out0.is_empty(),
1652            "section straddles two TS packets — first feed must not yield"
1653        );
1654        let out1 = asm.feed(&p1, false, 6).unwrap();
1655        assert_eq!(out1.len(), 1, "second feed must complete the section");
1656        assert_eq!(out1[0], section);
1657        let pmt = ProgramMapTable::parse(&out1[0]).unwrap();
1658        assert_eq!(pmt.streams.len(), 2);
1659    }
1660
1661    #[test]
1662    fn assembler_section_spans_three_ts_packets() {
1663        // Stuff a single PMT so it needs three TS payloads. Pad the
1664        // program_info area with two ~250-byte raw descriptor blobs;
1665        // each TLV uses an 8-bit length so the upper bound per blob
1666        // is 255 + 2 header bytes.
1667        let payload_chunk = vec![0xABu8; 250];
1668        let mut descr_block: Vec<u8> = Vec::new();
1669        for tag in [0xC0u8, 0xC1] {
1670            descr_block.push(tag);
1671            descr_block.push(payload_chunk.len() as u8);
1672            descr_block.extend_from_slice(&payload_chunk);
1673        }
1674        let section = build_pmt_section(1, 0, 0x100, &descr_block, &[(0x1B, 0x1011, &[])]);
1675        assert!(
1676            section.len() > 2 * 184,
1677            "need >2 TS payloads worth, got {} bytes",
1678            section.len()
1679        );
1680
1681        // Slice into three chunks. First payload carries 183 section
1682        // bytes (after pointer_field=0); the next two carry up to 184
1683        // continuation bytes each.
1684        let mut p0 = vec![0u8];
1685        p0.extend_from_slice(&section[..183]);
1686        let p1 = section[183..183 + 184].to_vec();
1687        let mut p2 = section[183 + 184..].to_vec();
1688        p2.resize(184, 0xFF);
1689
1690        let mut asm = PsiSectionAssembler::new();
1691        assert!(asm.feed(&p0, true, 0).unwrap().is_empty());
1692        assert!(asm.feed(&p1, false, 1).unwrap().is_empty());
1693        let out = asm.feed(&p2, false, 2).unwrap();
1694        assert_eq!(out.len(), 1);
1695        assert_eq!(out[0], section);
1696    }
1697
1698    #[test]
1699    fn assembler_cc_skip_discards_in_flight_section() {
1700        // Begin a section in packet A (CC=4), then jump to CC=8 in
1701        // packet B. The assembler must discard the in-flight bytes
1702        // rather than concatenate them blindly.
1703        let section = build_pat_section(1, 0, &[(1, 0x100), (2, 0x200), (3, 0x300)]);
1704        // Make sure the section doesn't fit in one payload, so the
1705        // CC skip actually matters.
1706        let mut padded = Vec::new();
1707        for _ in 0..200 {
1708            padded.extend_from_slice(&section);
1709        }
1710        let big_section = build_pmt_section(
1711            1,
1712            0,
1713            0x100,
1714            &[0u8; 200],
1715            &[(0x1B, 0x1011, &[]), (0x81, 0x1100, &[])],
1716        );
1717        let mut p0 = vec![0u8];
1718        p0.extend_from_slice(&big_section[..183]);
1719
1720        let mut asm = PsiSectionAssembler::new();
1721        assert!(asm.feed(&p0, true, 0).unwrap().is_empty());
1722        // Skip CC from 0 → 2 (expected was 1). Assembler drops the
1723        // in-flight buffer.
1724        let p1 = vec![0xFFu8; 184];
1725        let out = asm.feed(&p1, false, 2).unwrap();
1726        assert!(out.is_empty());
1727        // Confirm the buffer was actually dropped: feeding the rest
1728        // of `big_section` as a continuation now produces no section.
1729        let mut p2 = big_section[183..].to_vec();
1730        p2.resize(184, 0xFF);
1731        let out2 = asm.feed(&p2, false, 3).unwrap();
1732        assert!(out2.is_empty(), "CC-skip must have dropped in-flight bytes");
1733
1734        let _ = padded;
1735    }
1736
1737    #[test]
1738    fn assembler_stuffing_terminates_payload() {
1739        // A single short section followed by stuffing — assembler
1740        // must yield the section and stop at the first 0xFF.
1741        let section = build_pat_section(9, 0, &[(1, 0x100)]);
1742        let mut payload = vec![0u8]; // pointer_field
1743        payload.extend_from_slice(&section);
1744        payload.extend_from_slice(&[0xFFu8; 64]);
1745        let mut asm = PsiSectionAssembler::new();
1746        let out = asm.feed(&payload, true, 0).unwrap();
1747        assert_eq!(out.len(), 1);
1748        assert_eq!(out[0], section);
1749    }
1750
1751    #[test]
1752    fn assembler_two_sections_same_payload() {
1753        // Two short PAT sections packed back-to-back in one PUSI
1754        // payload — pointer_field=0 puts the first section
1755        // immediately after the pointer.
1756        let s0 = build_pat_section(1, 0, &[(1, 0x100)]);
1757        let s1 = build_pat_section(2, 0, &[(2, 0x200)]);
1758        let mut payload = vec![0u8];
1759        payload.extend_from_slice(&s0);
1760        payload.extend_from_slice(&s1);
1761        payload.resize(184, 0xFF);
1762        let mut asm = PsiSectionAssembler::new();
1763        let out = asm.feed(&payload, true, 0).unwrap();
1764        assert_eq!(out.len(), 2);
1765        assert_eq!(out[0], s0);
1766        assert_eq!(out[1], s1);
1767    }
1768
1769    #[test]
1770    fn assembler_pointer_field_finishes_prev_section() {
1771        // First TS payload starts a section that runs past the
1772        // 184-byte data area. Second TS payload has PUSI=1 with
1773        // pointer_field = N where N is the remaining bytes of the
1774        // previous section; after those N bytes, a new section
1775        // begins.
1776        //
1777        // s0 must overflow one TS payload — build it with a stuffed
1778        // PMT body. s1 is the trailing short section.
1779        let big_descr = vec![0u8; 200];
1780        let s0 = build_pmt_section(1, 0, 0x100, &big_descr, &[(0x1B, 0x1011, &[])]);
1781        let s1 = build_pat_section(7, 0, &[(3, 0x300)]);
1782        assert!(s0.len() > 184, "s0 must straddle a TS packet boundary");
1783        let mut p0 = vec![0u8]; // pointer_field = 0
1784        p0.extend_from_slice(&s0[..183]);
1785        // p1: pointer_field = remaining s0 bytes, then s1.
1786        let remaining = s0.len() - 183;
1787        let mut p1 = vec![remaining as u8];
1788        p1.extend_from_slice(&s0[183..]);
1789        p1.extend_from_slice(&s1);
1790        p1.resize(184, 0xFF);
1791
1792        let mut asm = PsiSectionAssembler::new();
1793        let out0 = asm.feed(&p0, true, 0).unwrap();
1794        assert!(out0.is_empty());
1795        let out1 = asm.feed(&p1, true, 1).unwrap();
1796        assert_eq!(out1.len(), 2);
1797        assert_eq!(out1[0], s0);
1798        assert_eq!(out1[1], s1);
1799    }
1800
1801    #[test]
1802    fn assembler_reset_drops_buffer() {
1803        let section = build_pmt_section(1, 0, 0x100, &[0u8; 200], &[(0x1B, 0x1011, &[])]);
1804        let mut p0 = vec![0u8];
1805        p0.extend_from_slice(&section[..183]);
1806        let mut asm = PsiSectionAssembler::new();
1807        assert!(asm.feed(&p0, true, 0).unwrap().is_empty());
1808        asm.reset();
1809        // Feeding the continuation now does nothing — the buffer
1810        // was wiped.
1811        let mut p1 = section[183..].to_vec();
1812        p1.resize(184, 0xFF);
1813        let out = asm.feed(&p1, false, 1).unwrap();
1814        assert!(out.is_empty());
1815    }
1816
1817    /// Build one service-loop entry for an SDT section.
1818    fn build_sdt_service(
1819        service_id: u16,
1820        eit_sched: bool,
1821        eit_pf: bool,
1822        running_status: u8,
1823        free_ca: bool,
1824        descriptors: &[u8],
1825    ) -> Vec<u8> {
1826        let mut s = Vec::new();
1827        s.extend_from_slice(&service_id.to_be_bytes());
1828        // reserved_future_use (6) | EIT_schedule_flag | EIT_present_following.
1829        let mut b = 0b1111_1100u8;
1830        if eit_sched {
1831            b |= 0b10;
1832        }
1833        if eit_pf {
1834            b |= 0b01;
1835        }
1836        s.push(b);
1837        // running_status (3) | free_CA_mode (1) | descriptors_length (12).
1838        let dlen = descriptors.len() as u16;
1839        let b3 = ((running_status & 0b111) << 5)
1840            | (if free_ca { 0b0001_0000 } else { 0 })
1841            | ((dlen >> 8) & 0x0F) as u8;
1842        s.push(b3);
1843        s.push((dlen & 0xFF) as u8);
1844        s.extend_from_slice(descriptors);
1845        s
1846    }
1847
1848    /// Build a full SDT section (table_id through CRC).
1849    fn build_sdt_section(
1850        table_id: u8,
1851        tsid: u16,
1852        version: u8,
1853        original_network_id: u16,
1854        services: &[Vec<u8>],
1855    ) -> Vec<u8> {
1856        // body = onid(2) + reserved(1) + Σ service entries.
1857        let body_len: usize = 3 + services.iter().map(|s| s.len()).sum::<usize>();
1858        let section_length = 5 + body_len + 4;
1859        let mut s = Vec::with_capacity(3 + section_length);
1860        s.push(table_id);
1861        let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
1862        s.push(len_hi);
1863        s.push((section_length & 0xFF) as u8);
1864        s.extend_from_slice(&tsid.to_be_bytes());
1865        s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
1866        s.push(0); // section_number
1867        s.push(0); // last_section_number
1868        s.extend_from_slice(&original_network_id.to_be_bytes());
1869        s.push(0xFF); // reserved_future_use
1870        for svc in services {
1871            s.extend_from_slice(svc);
1872        }
1873        let crc = mpeg2_crc32(&s);
1874        s.extend_from_slice(&crc.to_be_bytes());
1875        s
1876    }
1877
1878    /// Build a service_descriptor (tag 0x48) TLV.
1879    fn build_service_descriptor(service_type: u8, provider: &[u8], name: &[u8]) -> Vec<u8> {
1880        let mut body = vec![service_type, provider.len() as u8];
1881        body.extend_from_slice(provider);
1882        body.push(name.len() as u8);
1883        body.extend_from_slice(name);
1884        let mut v = vec![0x48u8, body.len() as u8];
1885        v.extend_from_slice(&body);
1886        v
1887    }
1888
1889    #[test]
1890    fn sdt_actual_single_service_round_trip() {
1891        let sd = build_service_descriptor(0x01, b"Provider", b"Channel One");
1892        let svc = build_sdt_service(0x0064, true, true, 4, false, &sd);
1893        let section = build_sdt_section(SDT_ACTUAL_TABLE_ID, 0x0001, 7, 0x2024, &[svc]);
1894        let sdt = ServiceDescriptionTable::parse(&section).unwrap();
1895        assert!(!sdt.other_transport_stream);
1896        assert_eq!(sdt.transport_stream_id, 0x0001);
1897        assert_eq!(sdt.version_number, 7);
1898        assert!(sdt.current_next_indicator);
1899        assert_eq!(sdt.original_network_id, 0x2024);
1900        assert_eq!(sdt.services.len(), 1);
1901        let s = &sdt.services[0];
1902        assert_eq!(s.service_id, 0x0064);
1903        assert!(s.eit_schedule_flag);
1904        assert!(s.eit_present_following_flag);
1905        assert_eq!(s.running_status, RunningStatus::Running);
1906        assert!(!s.free_ca_mode);
1907        let d = s.iter_descriptors().next().unwrap().unwrap();
1908        match d.body {
1909            crate::descriptor::DescriptorBody::Service(svc) => {
1910                assert_eq!(svc.service_type, 0x01);
1911                assert_eq!(svc.service_provider_name, b"Provider");
1912                assert_eq!(svc.service_name, b"Channel One");
1913            }
1914            other => panic!("expected Service, got {other:?}"),
1915        }
1916    }
1917
1918    #[test]
1919    fn sdt_other_table_id_sets_flag() {
1920        let svc = build_sdt_service(0x0001, false, false, 1, true, &[]);
1921        let section = build_sdt_section(SDT_OTHER_TABLE_ID, 0x0009, 0, 0x0001, &[svc]);
1922        let sdt = ServiceDescriptionTable::parse(&section).unwrap();
1923        assert!(sdt.other_transport_stream);
1924        assert_eq!(sdt.services.len(), 1);
1925        let s = &sdt.services[0];
1926        assert_eq!(s.running_status, RunningStatus::NotRunning);
1927        assert!(s.free_ca_mode);
1928        assert!(!s.eit_schedule_flag);
1929        assert!(!s.eit_present_following_flag);
1930    }
1931
1932    #[test]
1933    fn sdt_multiple_services() {
1934        let svc0 = build_sdt_service(0x0064, true, true, 4, false, &[]);
1935        let svc1 = build_sdt_service(0x0065, false, true, 5, true, &[]);
1936        let section = build_sdt_section(SDT_ACTUAL_TABLE_ID, 0x0002, 1, 0x1000, &[svc0, svc1]);
1937        let sdt = ServiceDescriptionTable::parse(&section).unwrap();
1938        assert_eq!(sdt.services.len(), 2);
1939        assert_eq!(sdt.services[0].service_id, 0x0064);
1940        assert_eq!(sdt.services[1].service_id, 0x0065);
1941        assert_eq!(sdt.services[1].running_status, RunningStatus::OffAir);
1942    }
1943
1944    #[test]
1945    fn sdt_rejects_wrong_table_id() {
1946        // A PMT section fed to the SDT parser must be rejected.
1947        let section = build_pmt_section(1, 0, 0x100, &[], &[(0x1B, 0x1011, &[])]);
1948        assert!(ServiceDescriptionTable::parse(&section).is_err());
1949    }
1950
1951    #[test]
1952    fn sdt_crc_mismatch_rejected() {
1953        let svc = build_sdt_service(0x0064, true, true, 4, false, &[]);
1954        let mut section = build_sdt_section(SDT_ACTUAL_TABLE_ID, 0x0001, 7, 0x2024, &[svc]);
1955        let last = section.len() - 1;
1956        section[last] ^= 0xFF;
1957        assert!(matches!(
1958            ServiceDescriptionTable::parse(&section),
1959            Err(TsError::PsiCrcMismatch { .. })
1960        ));
1961    }
1962
1963    #[test]
1964    fn running_status_reserved_values() {
1965        assert_eq!(RunningStatus::from_bits(6), RunningStatus::Reserved(6));
1966        assert_eq!(RunningStatus::from_bits(7), RunningStatus::Reserved(7));
1967        assert_eq!(RunningStatus::from_bits(0), RunningStatus::Undefined);
1968        assert_eq!(RunningStatus::from_bits(2), RunningStatus::StartsSoon);
1969        assert_eq!(RunningStatus::from_bits(3), RunningStatus::Pausing);
1970    }
1971
1972    /// Build a short_event_descriptor (tag 0x4D) TLV.
1973    fn build_short_event_descriptor(lang: &[u8; 3], name: &[u8], text: &[u8]) -> Vec<u8> {
1974        let mut body = Vec::new();
1975        body.extend_from_slice(lang);
1976        body.push(name.len() as u8);
1977        body.extend_from_slice(name);
1978        body.push(text.len() as u8);
1979        body.extend_from_slice(text);
1980        let mut v = vec![0x4Du8, body.len() as u8];
1981        v.extend_from_slice(&body);
1982        v
1983    }
1984
1985    /// Build one EIT event entry (event loop element, no CRC).
1986    #[allow(clippy::too_many_arguments)]
1987    fn build_eit_event(
1988        event_id: u16,
1989        start_time: [u8; 5],
1990        duration_bcd: [u8; 3],
1991        running_status: u8,
1992        free_ca: bool,
1993        descriptors: &[u8],
1994    ) -> Vec<u8> {
1995        let mut s = Vec::new();
1996        s.extend_from_slice(&event_id.to_be_bytes());
1997        s.extend_from_slice(&start_time);
1998        s.extend_from_slice(&duration_bcd);
1999        let dlen = descriptors.len() as u16;
2000        let b = ((running_status & 0b111) << 5)
2001            | (if free_ca { 0b0001_0000 } else { 0 })
2002            | ((dlen >> 8) & 0x0F) as u8;
2003        s.push(b);
2004        s.push((dlen & 0xFF) as u8);
2005        s.extend_from_slice(descriptors);
2006        s
2007    }
2008
2009    /// Build a full EIT section (table_id through CRC).
2010    #[allow(clippy::too_many_arguments)]
2011    fn build_eit_section(
2012        table_id: u8,
2013        service_id: u16,
2014        version: u8,
2015        tsid: u16,
2016        onid: u16,
2017        segment_last: u8,
2018        last_table_id: u8,
2019        events: &[Vec<u8>],
2020    ) -> Vec<u8> {
2021        // body = tsid(2) + onid(2) + segment_last(1) + last_table_id(1)
2022        //        + Σ events.
2023        let body_len: usize = 6 + events.iter().map(|e| e.len()).sum::<usize>();
2024        let section_length = 5 + body_len + 4;
2025        let mut s = Vec::with_capacity(3 + section_length);
2026        s.push(table_id);
2027        let len_hi = 0b1011_0000 | ((section_length >> 8) & 0x0F) as u8;
2028        s.push(len_hi);
2029        s.push((section_length & 0xFF) as u8);
2030        s.extend_from_slice(&service_id.to_be_bytes());
2031        s.push(0b1100_0001 | ((version & 0b1_1111) << 1));
2032        s.push(0); // section_number
2033        s.push(0); // last_section_number
2034        s.extend_from_slice(&tsid.to_be_bytes());
2035        s.extend_from_slice(&onid.to_be_bytes());
2036        s.push(segment_last);
2037        s.push(last_table_id);
2038        for e in events {
2039            s.extend_from_slice(e);
2040        }
2041        let crc = mpeg2_crc32(&s);
2042        s.extend_from_slice(&crc.to_be_bytes());
2043        s
2044    }
2045
2046    #[test]
2047    fn eit_start_time_spec_example() {
2048        // EN 300 468 §5.2.4 example 2: 93/10/13 12:45:00 is coded as
2049        // 0xC0 7912 4500 (MJD 0xC079 = 49273, then 12 45 00 in BCD).
2050        let dt = decode_eit_start_time([0xC0, 0x79, 0x12, 0x45, 0x00]).unwrap();
2051        assert_eq!(dt.mjd, 0xC079);
2052        assert_eq!(dt.year, 1993);
2053        assert_eq!(dt.month, 10);
2054        assert_eq!(dt.day, 13);
2055        assert_eq!(dt.hour, 12);
2056        assert_eq!(dt.minute, 45);
2057        assert_eq!(dt.second, 0);
2058    }
2059
2060    #[test]
2061    fn eit_start_time_undefined_sentinel() {
2062        assert!(decode_eit_start_time([0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).is_none());
2063    }
2064
2065    #[test]
2066    fn eit_duration_spec_example() {
2067        // EN 300 468 §5.2.4 example 3: 01:45:30 is coded as 0x01 4530.
2068        let d = EitDuration {
2069            hours: bcd_byte(0x01),
2070            minutes: bcd_byte(0x45),
2071            seconds: bcd_byte(0x30),
2072        };
2073        assert_eq!(d.hours, 1);
2074        assert_eq!(d.minutes, 45);
2075        assert_eq!(d.seconds, 30);
2076        assert_eq!(d.as_seconds(), 3600 + 45 * 60 + 30);
2077    }
2078
2079    #[test]
2080    fn eit_actual_pf_single_event_round_trip() {
2081        let sed = build_short_event_descriptor(b"eng", b"The Event", b"A short description");
2082        let ev = build_eit_event(
2083            0x1234,
2084            [0xC0, 0x79, 0x12, 0x45, 0x00],
2085            [0x01, 0x45, 0x30],
2086            4, // running
2087            false,
2088            &sed,
2089        );
2090        let section = build_eit_section(
2091            EIT_ACTUAL_PF_TABLE_ID,
2092            0x0064, // service_id
2093            5,
2094            0x0001, // tsid
2095            0x2024, // onid
2096            0,
2097            EIT_ACTUAL_PF_TABLE_ID,
2098            &[ev],
2099        );
2100        let eit = EventInformationTable::parse(&section).unwrap();
2101        assert!(!eit.other_transport_stream);
2102        assert!(!eit.schedule);
2103        assert_eq!(eit.table_id, EIT_ACTUAL_PF_TABLE_ID);
2104        assert_eq!(eit.service_id, 0x0064);
2105        assert_eq!(eit.version_number, 5);
2106        assert!(eit.current_next_indicator);
2107        assert_eq!(eit.transport_stream_id, 0x0001);
2108        assert_eq!(eit.original_network_id, 0x2024);
2109        assert_eq!(eit.last_table_id, EIT_ACTUAL_PF_TABLE_ID);
2110        assert_eq!(eit.events.len(), 1);
2111        let e = &eit.events[0];
2112        assert_eq!(e.event_id, 0x1234);
2113        let st = e.start_time.unwrap();
2114        assert_eq!((st.year, st.month, st.day), (1993, 10, 13));
2115        assert_eq!((st.hour, st.minute, st.second), (12, 45, 0));
2116        assert_eq!(e.duration.as_seconds(), 6330);
2117        assert_eq!(e.running_status, RunningStatus::Running);
2118        assert!(!e.free_ca_mode);
2119        // The short_event_descriptor decodes to the event name + text.
2120        let d = e.iter_descriptors().next().unwrap().unwrap();
2121        match d.body {
2122            DescriptorBody::ShortEvent(se) => {
2123                assert_eq!(&se.language_code, b"eng");
2124                assert_eq!(se.event_name, b"The Event");
2125                assert_eq!(se.text, b"A short description");
2126            }
2127            other => panic!("expected ShortEvent, got {other:?}"),
2128        }
2129    }
2130
2131    #[test]
2132    fn eit_schedule_and_other_flags() {
2133        // 0x60 = other-TS schedule.
2134        let ev = build_eit_event(1, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF], [0, 0, 0], 0, true, &[]);
2135        let section = build_eit_section(0x60, 0x0001, 0, 0x0001, 0x0001, 0, 0x62, &[ev]);
2136        let eit = EventInformationTable::parse(&section).unwrap();
2137        assert!(eit.other_transport_stream);
2138        assert!(eit.schedule);
2139        assert_eq!(eit.last_table_id, 0x62);
2140        // Undefined start time surfaces as None; free_CA_mode set.
2141        assert!(eit.events[0].start_time.is_none());
2142        assert!(eit.events[0].free_ca_mode);
2143    }
2144
2145    #[test]
2146    fn eit_multiple_events() {
2147        let e0 = build_eit_event(
2148            10,
2149            [0xC0, 0x79, 0x12, 0x00, 0x00],
2150            [0, 0x30, 0],
2151            4,
2152            false,
2153            &[],
2154        );
2155        let e1 = build_eit_event(
2156            11,
2157            [0xC0, 0x79, 0x12, 0x30, 0x00],
2158            [0x01, 0, 0],
2159            1,
2160            false,
2161            &[],
2162        );
2163        let section = build_eit_section(
2164            EIT_ACTUAL_PF_TABLE_ID,
2165            0x0064,
2166            0,
2167            0x0001,
2168            0x0001,
2169            0,
2170            0x4E,
2171            &[e0, e1],
2172        );
2173        let eit = EventInformationTable::parse(&section).unwrap();
2174        assert_eq!(eit.events.len(), 2);
2175        assert_eq!(eit.events[0].event_id, 10);
2176        assert_eq!(eit.events[1].event_id, 11);
2177        assert_eq!(eit.events[0].duration.minutes, 30);
2178        assert_eq!(eit.events[1].duration.hours, 1);
2179    }
2180
2181    #[test]
2182    fn eit_table_id_classification() {
2183        assert!(EventInformationTable::is_eit_table_id(0x4E));
2184        assert!(EventInformationTable::is_eit_table_id(0x4F));
2185        assert!(EventInformationTable::is_eit_table_id(0x50));
2186        assert!(EventInformationTable::is_eit_table_id(0x5F));
2187        assert!(EventInformationTable::is_eit_table_id(0x60));
2188        assert!(EventInformationTable::is_eit_table_id(0x6F));
2189        assert!(!EventInformationTable::is_eit_table_id(0x4D));
2190        assert!(!EventInformationTable::is_eit_table_id(0x70));
2191        assert!(!EventInformationTable::is_eit_table_id(0x42));
2192    }
2193
2194    #[test]
2195    fn eit_rejects_wrong_table_id() {
2196        let ev = build_eit_event(1, [0xFF; 5], [0, 0, 0], 0, false, &[]);
2197        // Build with a valid EIT id, then corrupt the table_id byte so
2198        // the CRC still matches the original — parse must reject on id.
2199        let mut section = build_eit_section(0x70, 0, 0, 0, 0, 0, 0, &[ev]);
2200        section[0] = 0x70;
2201        assert!(matches!(
2202            EventInformationTable::parse(&section),
2203            Err(TsError::Unsupported(_))
2204        ));
2205    }
2206
2207    #[test]
2208    fn eit_crc_mismatch_rejected() {
2209        let ev = build_eit_event(1, [0xC0, 0x79, 0x12, 0x45, 0x00], [0, 0, 0], 4, false, &[]);
2210        let mut section = build_eit_section(
2211            EIT_ACTUAL_PF_TABLE_ID,
2212            0x0064,
2213            0,
2214            0x0001,
2215            0x0001,
2216            0,
2217            0x4E,
2218            &[ev],
2219        );
2220        let last = section.len() - 1;
2221        section[last] ^= 0xFF;
2222        assert!(matches!(
2223            EventInformationTable::parse(&section),
2224            Err(TsError::PsiCrcMismatch { .. })
2225        ));
2226    }
2227}