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;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum CaEnable {
Possible,
PossiblePurchaseDialogue,
PossibleTechnicalDialogue,
NotPossibleNoEntitlement,
NotPossibleTechnical,
Rfu(u8),
}
impl CaEnable {
#[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),
}
}
#[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,
}
}
#[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);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CaPmtReplyStream {
pub elementary_pid: Pid,
pub ca_enable: Option<CaEnable>,
#[cfg_attr(feature = "serde", serde(skip))]
pub reserved_pid: ReservedOnes<3>,
#[cfg_attr(feature = "serde", serde(skip))]
pub reserved_ca_enable: ReservedOnes<7>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CaPmtReply {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub program_number: u16,
pub version_number: u8,
pub current_next_indicator: bool,
pub ca_enable: Option<CaEnable>,
pub streams: Vec<CaPmtReplyStream>,
#[cfg_attr(feature = "serde", serde(skip))]
pub reserved_version: ReservedOnes<2>,
#[cfg_attr(feature = "serde", serde(skip))]
pub reserved_ca_enable: ReservedOnes<7>,
}
const REPLY_PREFIX: usize = 4; const ES_LEN: usize = 3;
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());
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 {
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)
}
}
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(),
}
}
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() {
assert!(Pid::try_new(0x2001).is_err());
}
#[test]
fn oversized_version_number_is_rejected_not_wrapped() {
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, 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());
}
}