dvb-si 12.1.0

ETSI EN 300 468 DVB Service Information parser + builder. MPEG-2 PSI included.
Documentation
//! Program Association Table — MPEG-2 ISO/IEC 13818-1 (H.222.0 08/2023) §2.4.4.4.
//!
//! PAT is carried on PID 0x0000 with table_id 0x00. It maps
//! program_number values to the PID on which their PMT is carried.
//! program_number 0x0000 is special — its PID is the NIT PID.

use crate::error::{Error, Result};
use alloc::vec::Vec;
use broadcast_common::{Parse, Serialize};
use mpeg_ts::pid::Pid;

/// PAT table_id (ISO/IEC 13818-1 Table 2-30).
pub const TABLE_ID: u8 = 0x00;
/// PAT well-known PID.
pub const PID: mpeg_ts::pid::Pid = mpeg_ts::pid::Pid::new(0x0000);
/// program_number value that carries the NIT PID rather than a PMT PID.
pub const PROGRAM_NUMBER_NIT: u16 = 0x0000;

const MIN_HEADER_LEN: usize = 3;
const EXTENSION_HEADER_LEN: usize = 5;
use super::CRC_LEN;
const MIN_SECTION_LEN: usize = MIN_HEADER_LEN + EXTENSION_HEADER_LEN + CRC_LEN;
const ENTRY_LEN: usize = 4;

/// One entry in the PAT program loop.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct PatEntry {
    /// program_number. 0x0000 means "next PID is the NIT PID".
    pub program_number: u16,
    /// PMT PID (or NIT PID when program_number == 0).
    pub pid: Pid,
    /// `reserved(3)` before the PID (ISO/IEC 13818-1 §2.4.4.4, `111`);
    /// preserved from the wire.
    #[cfg_attr(feature = "serde", serde(skip))]
    pub reserved: u8,
}

impl PatEntry {
    /// Construct an entry with the spec `reserved` fill (`111`).
    #[must_use]
    pub const fn new(program_number: u16, pid: Pid) -> Self {
        Self {
            program_number,
            pid,
            reserved: super::RESERVED_3,
        }
    }
}

/// Program Association Table.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct PatSection {
    /// transport_stream_id from the section header.
    pub transport_stream_id: u16,
    /// 5-bit version_number from the section header.
    pub version_number: u8,
    /// current_next_indicator bit.
    pub current_next_indicator: bool,
    /// section_number in the sub-table sequence.
    pub section_number: u8,
    /// last_section_number in the sub-table sequence.
    pub last_section_number: u8,
    /// Program entries in wire order.
    pub entries: Vec<PatEntry>,
    /// Header bit fields (`'0'` after `section_syntax_indicator`, the two
    /// `reserved(2)`; default [`SectionBits::psi`](super::SectionBits::psi)).
    #[cfg_attr(feature = "serde", serde(skip))]
    pub bits: super::SectionBits,
}

impl Default for PatSection {
    fn default() -> Self {
        Self::new(0, 0, false, 0, 0, Vec::new())
    }
}

impl PatSection {
    /// Construct a PAT section with the spec `reserved` fill.
    #[must_use]
    pub fn new(
        transport_stream_id: u16,
        version_number: u8,
        current_next_indicator: bool,
        section_number: u8,
        last_section_number: u8,
        entries: Vec<PatEntry>,
    ) -> Self {
        Self {
            transport_stream_id,
            version_number,
            current_next_indicator,
            section_number,
            last_section_number,
            entries,
            bits: super::SectionBits::psi(),
        }
    }
}

impl<'a> Parse<'a> for PatSection {
    type Error = crate::error::Error;
    fn parse(bytes: &'a [u8]) -> Result<Self> {
        if bytes.len() < MIN_HEADER_LEN + EXTENSION_HEADER_LEN + CRC_LEN {
            return Err(Error::BufferTooShort {
                need: MIN_HEADER_LEN + EXTENSION_HEADER_LEN + CRC_LEN,
                have: bytes.len(),
                what: "PatSection",
            });
        }

        if bytes[0] != TABLE_ID {
            return Err(Error::UnexpectedTableId {
                table_id: bytes[0],
                what: "PatSection",
                expected: &[TABLE_ID],
            });
        }

        let section_length = super::section_length_of(bytes);
        let total = super::check_section_length(
            bytes.len(),
            MIN_HEADER_LEN,
            section_length,
            MIN_SECTION_LEN,
        )?;

        let transport_stream_id = crate::wire::be_u16(bytes, 3, "pat")?;
        let version_number = super::version_number_of(bytes[5]);
        let current_next_indicator = super::current_next_of(bytes[5]);
        let section_number = bytes[6];
        let last_section_number = bytes[7];

        let end = total - CRC_LEN;
        let mut entries = Vec::new();
        let mut pos = 8;
        while pos < end {
            if pos + ENTRY_LEN > end {
                return Err(Error::BufferTooShort {
                    need: ENTRY_LEN,
                    have: end - pos,
                    what: "PatSection trailing entry bytes",
                });
            }
            let chunk = &bytes[pos..pos + ENTRY_LEN];
            let program_number = crate::wire::be_u16(chunk, 0, "pat")?;
            let pid = super::pid_of(chunk[2], chunk[3]);
            entries.push(PatEntry {
                program_number,
                pid,
                reserved: super::pid_reserved_of(chunk[2]),
            });
            pos += ENTRY_LEN;
        }

        Ok(PatSection {
            transport_stream_id,
            version_number,
            current_next_indicator,
            section_number,
            last_section_number,
            entries,
            bits: super::SectionBits::parse(bytes[1], Some(bytes[5])),
        })
    }
}

impl Serialize for PatSection {
    type Error = crate::error::Error;
    fn serialized_len(&self) -> usize {
        MIN_HEADER_LEN + EXTENSION_HEADER_LEN + self.entries.len() * ENTRY_LEN + CRC_LEN
    }

    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
        let len = self.serialized_len();
        if buf.len() < len {
            return Err(Error::OutputBufferTooSmall {
                need: len,
                have: buf.len(),
            });
        }

        let section_length = EXTENSION_HEADER_LEN + self.entries.len() * ENTRY_LEN + CRC_LEN;

        buf[0] = TABLE_ID;
        buf[1] = self.bits.b1()?;
        super::write_section_length(buf, section_length)?;
        buf[3..5].copy_from_slice(&self.transport_stream_id.to_be_bytes());
        buf[5] = self
            .bits
            .b5(self.version_number, self.current_next_indicator)?;
        buf[6] = self.section_number;
        buf[7] = self.last_section_number;

        let mut pos = 8;
        for entry in &self.entries {
            buf[pos..pos + 2].copy_from_slice(&entry.program_number.to_be_bytes());
            super::write_pid_r(&mut buf[pos + 2..pos + 4], entry.pid, entry.reserved)?;
            pos += ENTRY_LEN;
        }

        let crc_pos = len - CRC_LEN;
        super::write_crc(buf, crc_pos);

        Ok(len)
    }
}
impl crate::traits::TableDef<'_> for PatSection {
    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
    const NAME: &'static str = "PROGRAM_ASSOCIATION";
}

impl PatSection {
    /// Program entries excluding the NIT entry.
    pub fn programmes(&self) -> impl Iterator<Item = &PatEntry> {
        self.entries
            .iter()
            .filter(|e| e.program_number != PROGRAM_NUMBER_NIT)
    }

    /// NIT PID if this PAT carries an entry with program_number == 0.
    pub fn nit_pid(&self) -> Option<Pid> {
        self.entries
            .iter()
            .find(|e| e.program_number == PROGRAM_NUMBER_NIT)
            .map(|e| e.pid)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Build a PAT section with the given entries and a placeholder CRC.
    fn build_pat(tsid: u16, version: u8, entries: &[(u16, u16)]) -> Vec<u8> {
        let section_length: usize = EXTENSION_HEADER_LEN + entries.len() * ENTRY_LEN + CRC_LEN;
        let mut v = Vec::new();
        v.push(TABLE_ID);
        crate::tables::push_section_header(
            &mut v,
            crate::tables::SECTION_B1_FLAGS_PSI,
            section_length,
        );
        v.extend_from_slice(&tsid.to_be_bytes());
        v.push(crate::tables::version_byte(version, true));
        v.push(0x00); // section_number
        v.push(0x00); // last_section_number
        for &(pn, pid) in entries {
            v.extend_from_slice(&pn.to_be_bytes());
            let mut pid_bytes = [0u8; 2];
            crate::tables::write_pid(&mut pid_bytes, Pid::new(pid));
            v.extend_from_slice(&pid_bytes);
        }
        v.extend_from_slice(&[0, 0, 0, 0]); // placeholder CRC
        v
    }

    #[test]
    fn parse_empty_pat_zero_programs() {
        let bytes = build_pat(0x1234, 5, &[]);
        let pat = PatSection::parse(&bytes).expect("parse");
        assert_eq!(pat.transport_stream_id, 0x1234);
        assert_eq!(pat.version_number, 5);
        assert!(pat.current_next_indicator);
        assert_eq!(pat.section_number, 0);
        assert_eq!(pat.last_section_number, 0);
        assert_eq!(pat.entries.len(), 0);
    }

    #[test]
    fn parse_single_program_extracts_pmt_pid() {
        let bytes = build_pat(1, 0, &[(42, 0x1234)]);
        let pat = PatSection::parse(&bytes).unwrap();
        assert_eq!(pat.entries.len(), 1);
        assert_eq!(pat.entries[0].program_number, 42);
        assert_eq!(pat.entries[0].pid, Pid::new(0x1234));
    }

    #[test]
    fn parse_many_programs_preserves_order() {
        let entries: Vec<(u16, u16)> = (1..=10).map(|i| (i, 0x1000 + i)).collect();
        let bytes = build_pat(1, 0, &entries);
        let pat = PatSection::parse(&bytes).unwrap();
        assert_eq!(pat.entries.len(), 10);
        for (i, e) in pat.entries.iter().enumerate() {
            assert_eq!(e.program_number, (i + 1) as u16);
            assert_eq!(e.pid, Pid::new(0x1000 + (i + 1) as u16));
        }
    }

    #[test]
    fn parse_rejects_wrong_table_id() {
        let mut bytes = build_pat(1, 0, &[]);
        bytes[0] = 0x02; // PMT table_id
        let err = PatSection::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::UnexpectedTableId { table_id: 0x02, .. }
        ));
    }

    #[test]
    fn parse_rejects_short_buffer() {
        let err = PatSection::parse(&[0x00, 0x00]).unwrap_err();
        assert!(matches!(err, Error::BufferTooShort { .. }));
    }

    #[test]
    fn serialize_round_trip_empty() {
        let pat = PatSection::new(0x0001, 0, true, 0, 0, vec![]);
        let mut buf = vec![0u8; pat.serialized_len()];
        pat.serialize_into(&mut buf).expect("serialize");
        let reparsed = PatSection::parse(&buf).expect("reparse");
        assert_eq!(pat, reparsed);
    }

    #[test]
    fn serialize_round_trip_many_programs() {
        let entries: Vec<PatEntry> = (1..=5)
            .map(|i| PatEntry::new(i, Pid::new(0x1000 + i)))
            .collect();
        let pat = PatSection::new(0xABCD, 3, true, 0, 0, entries);
        let mut buf = vec![0u8; pat.serialized_len()];
        pat.serialize_into(&mut buf).unwrap();
        let reparsed = PatSection::parse(&buf).unwrap();
        assert_eq!(pat, reparsed);
    }

    /// section_length is 12 bits (max 4095). 1021 entries is the largest
    /// program loop that still fits (section_length 4093); 1022 pushes it to
    /// 4097 and must be rejected, not silently wrapped.
    #[test]
    fn serialize_rejects_program_loop_exceeding_section_length() {
        let entries: Vec<PatEntry> = (0..1022u32)
            .map(|i| PatEntry::new((i % u16::MAX as u32) as u16, Pid::new(0x0100)))
            .collect();
        let pat = PatSection::new(1, 0, true, 0, 0, entries);
        let mut buf = vec![0u8; pat.serialized_len()];
        assert!(matches!(
            pat.serialize_into(&mut buf).unwrap_err(),
            Error::FieldOverflow(_)
        ));
    }

    #[test]
    fn serialize_accepts_program_loop_at_section_length_boundary() {
        let entries: Vec<PatEntry> = (0..1021u32)
            .map(|i| PatEntry::new((i % u16::MAX as u32) as u16, Pid::new(0x0100)))
            .collect();
        let pat = PatSection::new(1, 0, true, 0, 0, entries);
        let mut buf = vec![0u8; pat.serialized_len()];
        pat.serialize_into(&mut buf).expect("boundary fits");
        let reparsed = PatSection::parse(&buf).expect("reparse");
        assert_eq!(pat, reparsed);
    }

    #[test]
    fn parse_rejects_zero_section_length() {
        let mut buf = vec![0u8; 64];
        buf[0] = TABLE_ID;
        buf[1] = 0xB0;
        buf[2] = 0x00;
        for b in &mut buf[3..] {
            *b = 0xFF;
        }
        assert!(matches!(
            PatSection::parse(&buf).unwrap_err(),
            Error::SectionLengthOverflow { .. }
        ));
    }

    #[test]
    fn network_pid_entry_identified_by_program_number_0() {
        let bytes = build_pat(1, 0, &[(0, 0x0010), (1, 0x0100)]);
        let pat = PatSection::parse(&bytes).unwrap();
        assert_eq!(pat.nit_pid(), Some(Pid::new(0x0010)));
        assert_eq!(pat.programmes().count(), 1);
        assert_eq!(pat.programmes().next().unwrap().program_number, 1);
    }

    #[test]
    fn parse_rejects_trailing_slack_bytes() {
        let mut bytes = build_pat(1, 0, &[(1, 0x100)]);
        let sl = (bytes.len() - MIN_HEADER_LEN) as u16;
        bytes[1] = (bytes[1] & 0xF0) | (((sl + 2) >> 8) as u8 & 0x0F);
        bytes[2] = ((sl + 2) & 0xFF) as u8;
        bytes.extend_from_slice(&[0xFF, 0xFF]);
        let crc_pos = bytes.len();
        let crc = broadcast_common::crc32_mpeg2::compute(&bytes[..crc_pos - CRC_LEN]);
        bytes[crc_pos - CRC_LEN..crc_pos].copy_from_slice(&crc.to_be_bytes());
        assert!(matches!(
            PatSection::parse(&bytes).unwrap_err(),
            Error::BufferTooShort {
                what: "PatSection trailing entry bytes",
                ..
            }
        ));
    }
}