dvb-ci 0.10.0

DVB Common Interface (EN 50221) — APDU/resource objects, session/transport PDUs, and a CA_PMT builder from a dvb-si PMT.
Documentation
//! CA PMT Reply object (`ca_pmt_reply`) — ETSI EN 50221 §8.4.3.5, Table 26
//! (PDF p. 32).
//!
//! `ca_pmt_reply` (`9F 80 33`, app → host) reports descrambling capability. It
//! has one optional programme-level `CA_enable` (gated by a `CA_enable_flag`
//! bit) followed by a per-ES list, each ES with its own `CA_enable_flag` +
//! optional 7-bit `CA_enable`.

use crate::error::{Error, Result};
use crate::reserved::ReservedOnes;
use crate::tag::{self, ApduTag};
use crate::traits::ApduDef;
use alloc::vec::Vec;
use broadcast_common::{Parse, Serialize};
use mpeg_ts::pid::Pid;

/// `CA_enable` 7-bit value (Table, p. 32).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum CaEnable {
    /// `01` — descrambling possible.
    Possible,
    /// `02` — possible under conditions (purchase dialogue).
    PossiblePurchaseDialogue,
    /// `03` — possible under conditions (technical dialogue).
    PossibleTechnicalDialogue,
    /// `71` — not possible (no entitlement).
    NotPossibleNoEntitlement,
    /// `73` — not possible (technical reasons).
    NotPossibleTechnical,
    /// Any other 7-bit value (RFU).
    Rfu(u8),
}

impl CaEnable {
    /// Decode the low 7 bits of a `CA_enable` byte.
    #[must_use]
    pub fn from_u8(v: u8) -> Self {
        match v & 0x7F {
            0x01 => Self::Possible,
            0x02 => Self::PossiblePurchaseDialogue,
            0x03 => Self::PossibleTechnicalDialogue,
            0x71 => Self::NotPossibleNoEntitlement,
            0x73 => Self::NotPossibleTechnical,
            other => Self::Rfu(other),
        }
    }
    /// The 7-bit wire value.
    #[must_use]
    pub const fn to_u8(self) -> u8 {
        match self {
            Self::Possible => 0x01,
            Self::PossiblePurchaseDialogue => 0x02,
            Self::PossibleTechnicalDialogue => 0x03,
            Self::NotPossibleNoEntitlement => 0x71,
            Self::NotPossibleTechnical => 0x73,
            Self::Rfu(v) => v & 0x7F,
        }
    }
    /// Spec token, or `"reserved"`.
    #[must_use]
    pub fn name(&self) -> &'static str {
        match self {
            Self::Possible => "descrambling_possible",
            Self::PossiblePurchaseDialogue => "possible_purchase_dialogue",
            Self::PossibleTechnicalDialogue => "possible_technical_dialogue",
            Self::NotPossibleNoEntitlement => "not_possible_no_entitlement",
            Self::NotPossibleTechnical => "not_possible_technical",
            Self::Rfu(_) => "reserved",
        }
    }
}
broadcast_common::impl_spec_display!(CaEnable, Rfu);

/// One ES entry in a `ca_pmt_reply`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CaPmtReplyStream {
    /// 13-bit `elementary_PID`.
    pub elementary_pid: Pid,
    /// ES-level `CA_enable` — `Some` iff the `CA_enable_flag` bit was set.
    pub ca_enable: Option<CaEnable>,
    /// The 3-bit `reserved` field in front of `elementary_PID` as received,
    /// preserved so the object re-serializes byte-for-byte; `0b111` when
    /// hand-built.
    #[cfg_attr(feature = "serde", serde(skip))]
    pub reserved_pid: ReservedOnes<3>,
    /// The 7-bit field after `CA_enable_flag` when the flag is clear (`reserved`),
    /// as received so the object re-serializes byte-for-byte; ignored on
    /// serialize when `ca_enable` is `Some`. `0b111_1111` when hand-built.
    #[cfg_attr(feature = "serde", serde(skip))]
    pub reserved_ca_enable: ReservedOnes<7>,
}

/// `ca_pmt_reply()` object (Table 26).
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CaPmtReply {
    /// How the `length_field` was coded on the wire: a non-minimal long form
    /// (e.g. `82 00 09`) is preserved so the object re-serializes
    /// byte-for-byte; [`LengthForm::Minimal`](crate::length::LengthForm::Minimal) for a hand-built value.
    #[cfg_attr(feature = "serde", serde(skip))]
    pub length_form: crate::length::LengthForm,

    /// `program_number`.
    pub program_number: u16,
    /// 5-bit `version_number`.
    pub version_number: u8,
    /// `current_next_indicator`.
    pub current_next_indicator: bool,
    /// Programme-level `CA_enable` — `Some` iff the programme `CA_enable_flag` bit
    /// was set.
    pub ca_enable: Option<CaEnable>,
    /// Per-ES entries in wire order.
    pub streams: Vec<CaPmtReplyStream>,
    /// The 2-bit `reserved` field in front of `version_number` as received,
    /// preserved so the object re-serializes byte-for-byte; `0b11` when
    /// hand-built.
    #[cfg_attr(feature = "serde", serde(skip))]
    pub reserved_version: ReservedOnes<2>,
    /// The 7-bit field after `CA_enable_flag` when the flag is clear (`reserved`),
    /// as received so the object re-serializes byte-for-byte; ignored on
    /// serialize when `ca_enable` is `Some`. `0b111_1111` when hand-built.
    #[cfg_attr(feature = "serde", serde(skip))]
    pub reserved_ca_enable: ReservedOnes<7>,
}

const REPLY_PREFIX: usize = 4; // program_number(2) + version/cni/flag/enable(2)
const ES_LEN: usize = 3; // reserved/elem_pid(2) + flag/enable(1)

impl<'a> Parse<'a> for CaPmtReply {
    type Error = Error;
    fn parse(bytes: &'a [u8]) -> Result<Self> {
        let (length_form, body) =
            super::parse_apdu_header(bytes, tag::CA_PMT_REPLY, "ca_pmt_reply")?;
        if body.len() < REPLY_PREFIX {
            return Err(Error::BufferTooShort {
                need: REPLY_PREFIX,
                have: body.len(),
                what: "ca_pmt_reply prefix",
            });
        }
        let program_number = u16::from_be_bytes([body[0], body[1]]);
        let version_number = (body[2] >> 1) & 0x1F;
        let current_next_indicator = (body[2] & 0x01) != 0;
        let (ca_enable, reserved_ca_enable) = decode_enable_byte(body[3]);

        let mut pos = REPLY_PREFIX;
        let mut streams = Vec::new();
        while pos < body.len() {
            if pos + ES_LEN > body.len() {
                return Err(Error::BufferTooShort {
                    need: pos + ES_LEN,
                    have: body.len(),
                    what: "ca_pmt_reply ES",
                });
            }
            let elementary_pid =
                Pid::new((((body[pos] & 0x1F) as u16) << 8) | body[pos + 1] as u16);
            let (es_enable, es_reserved) = decode_enable_byte(body[pos + 2]);
            streams.push(CaPmtReplyStream {
                elementary_pid,
                ca_enable: es_enable,
                reserved_pid: ReservedOnes::from_bits(body[pos] >> 5),
                reserved_ca_enable: es_reserved,
            });
            pos += ES_LEN;
        }

        Ok(Self {
            length_form,
            program_number,
            version_number,
            current_next_indicator,
            ca_enable,
            streams,
            reserved_version: ReservedOnes::from_bits(body[2] >> 6),
            reserved_ca_enable,
        })
    }
}

impl Serialize for CaPmtReply {
    type Error = Error;
    fn serialized_len(&self) -> usize {
        super::apdu_len(REPLY_PREFIX + self.streams.len() * ES_LEN, self.length_form)
    }
    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
        super::fit_bits(
            u64::from(self.version_number),
            5,
            "ca_pmt_reply version_number",
        )?;
        let body = REPLY_PREFIX + self.streams.len() * ES_LEN;
        let mut pos = super::write_apdu_header(tag::CA_PMT_REPLY, body, self.length_form, buf)?;
        buf[pos..pos + 2].copy_from_slice(&self.program_number.to_be_bytes());
        // reserved(2)='11', version(5), current_next(1). Range-checked above.
        buf[pos + 2] = (self.reserved_version.get() << 6)
            | (self.version_number << 1)
            | u8::from(self.current_next_indicator);
        buf[pos + 3] = encode_enable_byte(self.ca_enable, self.reserved_ca_enable);
        pos += REPLY_PREFIX;
        for s in &self.streams {
            // reserved(3)='111', elementary_PID(13). Range-checked above.
            buf[pos] = (s.reserved_pid.get() << 5) | (s.elementary_pid.value() >> 8) as u8;
            buf[pos + 1] = s.elementary_pid.value() as u8;
            buf[pos + 2] = encode_enable_byte(s.ca_enable, s.reserved_ca_enable);
            pos += ES_LEN;
        }
        Ok(pos)
    }
}

/// Encode a `CA_enable_flag` + 7-bit `CA_enable`/reserved byte. When `enable`
/// is absent the flag is 0 and the 7 bits are the `reserved` field as parsed.
pub(crate) fn encode_enable_byte(enable: Option<CaEnable>, reserved: ReservedOnes<7>) -> u8 {
    match enable {
        Some(e) => 0x80 | (e.to_u8() & 0x7F),
        None => reserved.get(),
    }
}

/// Decode a `CA_enable_flag` + 7-bit `CA_enable`/reserved byte (the inverse of
/// [`encode_enable_byte`]); the reserved bits are only meaningful (and only
/// kept) when the flag is clear.
pub(crate) fn decode_enable_byte(b: u8) -> (Option<CaEnable>, ReservedOnes<7>) {
    if b & 0x80 != 0 {
        (Some(CaEnable::from_u8(b & 0x7F)), ReservedOnes::default())
    } else {
        (None, ReservedOnes::from_bits(b))
    }
}

impl ApduDef<'_> for CaPmtReply {
    const TAG: ApduTag = tag::CA_PMT_REPLY;
    const NAME: &'static str = "CA_PMT_REPLY";
}

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

    #[test]
    fn reply_with_prog_and_es_enable_round_trips() {
        let r = CaPmtReply {
            reserved_ca_enable: Default::default(),
            reserved_version: Default::default(),
            length_form: crate::length::LengthForm::Minimal,
            program_number: 0x0001,
            version_number: 1,
            current_next_indicator: true,
            ca_enable: Some(CaEnable::Possible),
            streams: alloc::vec![
                CaPmtReplyStream {
                    reserved_ca_enable: Default::default(),
                    reserved_pid: Default::default(),
                    elementary_pid: Pid::new(0x0200),
                    ca_enable: Some(CaEnable::Possible),
                },
                CaPmtReplyStream {
                    reserved_ca_enable: Default::default(),
                    reserved_pid: Default::default(),
                    elementary_pid: Pid::new(0x0201),
                    ca_enable: Some(CaEnable::NotPossibleNoEntitlement),
                },
            ],
        };
        let bytes = r.try_to_bytes().unwrap();
        assert_eq!(&bytes[..3], &[0x9F, 0x80, 0x33]);
        let parsed = CaPmtReply::parse(&bytes).unwrap();
        assert_eq!(parsed, r);
        assert_eq!(parsed.streams.len(), 2);
        assert_eq!(parsed.ca_enable.unwrap().name(), "descrambling_possible");
    }

    #[test]
    fn reply_without_enable_flags() {
        let r = CaPmtReply {
            reserved_ca_enable: Default::default(),
            reserved_version: Default::default(),
            length_form: crate::length::LengthForm::Minimal,
            program_number: 9,
            version_number: 0,
            current_next_indicator: true,
            ca_enable: None,
            streams: alloc::vec![CaPmtReplyStream {
                reserved_ca_enable: Default::default(),
                reserved_pid: Default::default(),
                elementary_pid: Pid::new(0x00FF),
                ca_enable: None,
            }],
        };
        let bytes = r.try_to_bytes().unwrap();
        let parsed = CaPmtReply::parse(&bytes).unwrap();
        assert_eq!(parsed, r);
        assert!(parsed.ca_enable.is_none());
        assert!(parsed.streams[0].ca_enable.is_none());
    }

    #[test]
    fn oversized_pid_is_unrepresentable() {
        // A 14-bit value cannot become the typed 13-bit `elementary_pid`.
        assert!(Pid::try_new(0x2001).is_err());
    }

    #[test]
    fn oversized_version_number_is_rejected_not_wrapped() {
        // Before the fix, 0x20 (exceeds 5 bits) silently wrapped to 0x00 in
        // the version_number field and returned Ok.
        let r = CaPmtReply {
            reserved_ca_enable: Default::default(),
            reserved_version: Default::default(),
            length_form: crate::length::LengthForm::Minimal,
            program_number: 1,
            version_number: 0x20,
            current_next_indicator: true,
            ca_enable: None,
            streams: Vec::new(),
        };
        let mut buf = [0u8; 16];
        assert!(matches!(
            r.serialize_into(&mut buf),
            Err(Error::InvalidObject { .. })
        ));
    }

    #[test]
    fn max_version_number_still_serializes_and_round_trips() {
        let r = CaPmtReply {
            reserved_ca_enable: Default::default(),
            reserved_version: Default::default(),
            length_form: crate::length::LengthForm::Minimal,
            program_number: 1,
            version_number: 0x1F, // the 5-bit maximum
            current_next_indicator: true,
            ca_enable: None,
            streams: Vec::new(),
        };
        let bytes = r.try_to_bytes().unwrap();
        assert_eq!(CaPmtReply::parse(&bytes).unwrap(), r);
    }

    #[test]
    fn mutating_enable_changes_bytes() {
        let r = CaPmtReply {
            reserved_ca_enable: Default::default(),
            reserved_version: Default::default(),
            length_form: crate::length::LengthForm::Minimal,
            program_number: 1,
            version_number: 1,
            current_next_indicator: true,
            ca_enable: Some(CaEnable::Possible),
            streams: Vec::new(),
        };
        let a = r.try_to_bytes().unwrap();
        let mut other = r.clone();
        other.ca_enable = Some(CaEnable::NotPossibleNoEntitlement);
        assert_ne!(a, other.try_to_bytes().unwrap());
    }
}