use crate::error::{Error, Result};
use crate::objects;
use crate::tag::ApduTag;
use alloc::vec::Vec;
use dvb_common::{Parse, Serialize};
pub mod tag {
use crate::tag::ApduTag;
pub const CC_PIN_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0x90, 0x14);
pub const CC_PIN_EVENT: ApduTag = ApduTag::from_bytes(0x9F, 0x90, 0x15);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CcPinReply {
pub lts_id: Option<u8>,
pub pincode_status: u8,
}
const CC_PIN_REPLY_BODY: usize = 3;
const LTS_BOUND_FLAG_BIT: u8 = 0x01;
impl<'a> Parse<'a> for CcPinReply {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let body = objects::parse_apdu_header(bytes, tag::CC_PIN_REPLY, "cc_PIN_reply")?;
if body.len() < CC_PIN_REPLY_BODY {
return Err(Error::BufferTooShort {
need: CC_PIN_REPLY_BODY,
have: body.len(),
what: "cc_PIN_reply",
});
}
let lts_bound = body[0] & LTS_BOUND_FLAG_BIT != 0;
let lts_id = if lts_bound { Some(body[1]) } else { None };
Ok(Self {
lts_id,
pincode_status: body[2],
})
}
}
impl Serialize for CcPinReply {
type Error = Error;
fn serialized_len(&self) -> usize {
objects::apdu_len(CC_PIN_REPLY_BODY)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = objects::write_apdu_header(tag::CC_PIN_REPLY, CC_PIN_REPLY_BODY, buf)?;
match self.lts_id {
Some(id) => {
buf[pos] = LTS_BOUND_FLAG_BIT;
buf[pos + 1] = id;
}
None => {
buf[pos] = 0;
buf[pos + 1] = 0;
}
}
buf[pos + 2] = self.pincode_status;
Ok(pos + CC_PIN_REPLY_BODY)
}
}
pub const PIN_EVENT_PRIVATE_DATA_LEN: usize = 15;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CcPinEvent {
pub lts_id: u8,
pub program_number: u16,
pub pincode_status: u8,
pub rating: u8,
pub pin_event_time_utc: u64,
pub pin_event_time_centiseconds: u8,
pub private_data: [u8; PIN_EVENT_PRIVATE_DATA_LEN],
}
const CC_PIN_EVENT_BODY: usize = 1 + 2 + 1 + 1 + 5 + 1 + PIN_EVENT_PRIVATE_DATA_LEN;
impl<'a> Parse<'a> for CcPinEvent {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let body = objects::parse_apdu_header(bytes, tag::CC_PIN_EVENT, "cc_PIN_event")?;
if body.len() < CC_PIN_EVENT_BODY {
return Err(Error::BufferTooShort {
need: CC_PIN_EVENT_BODY,
have: body.len(),
what: "cc_PIN_event",
});
}
let mut utc = 0u64;
for &b in &body[5..10] {
utc = (utc << 8) | b as u64;
}
let mut private_data = [0u8; PIN_EVENT_PRIVATE_DATA_LEN];
private_data.copy_from_slice(&body[11..11 + PIN_EVENT_PRIVATE_DATA_LEN]);
Ok(Self {
lts_id: body[0],
program_number: u16::from_be_bytes([body[1], body[2]]),
pincode_status: body[3],
rating: body[4],
pin_event_time_utc: utc,
pin_event_time_centiseconds: body[10],
private_data,
})
}
}
impl Serialize for CcPinEvent {
type Error = Error;
fn serialized_len(&self) -> usize {
objects::apdu_len(CC_PIN_EVENT_BODY)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = objects::write_apdu_header(tag::CC_PIN_EVENT, CC_PIN_EVENT_BODY, buf)?;
buf[pos] = self.lts_id;
buf[pos + 1..pos + 3].copy_from_slice(&self.program_number.to_be_bytes());
buf[pos + 3] = self.pincode_status;
buf[pos + 4] = self.rating;
let utc = self.pin_event_time_utc.to_be_bytes();
buf[pos + 5..pos + 10].copy_from_slice(&utc[3..8]);
buf[pos + 10] = self.pin_event_time_centiseconds;
buf[pos + 11..pos + 11 + PIN_EVENT_PRIVATE_DATA_LEN].copy_from_slice(&self.private_data);
Ok(pos + CC_PIN_EVENT_BODY)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum DatatypeId {
UriMessage,
ProgramNumber,
UriConfirm,
CicamLicense,
LicenseStatus,
LicenseRcvdStatus,
OperatingMode,
PinCode,
RecordStartStatus,
ModeChangeStatus,
RecordStopStatus,
LtsId,
Other(u8),
}
impl DatatypeId {
#[must_use]
pub fn from_u8(v: u8) -> Self {
match v {
25 => Self::UriMessage,
26 => Self::ProgramNumber,
27 => Self::UriConfirm,
33 => Self::CicamLicense,
34 => Self::LicenseStatus,
35 => Self::LicenseRcvdStatus,
38 => Self::OperatingMode,
39 => Self::PinCode,
40 => Self::RecordStartStatus,
41 => Self::ModeChangeStatus,
42 => Self::RecordStopStatus,
50 => Self::LtsId,
other => Self::Other(other),
}
}
#[must_use]
pub const fn to_u8(self) -> u8 {
match self {
Self::UriMessage => 25,
Self::ProgramNumber => 26,
Self::UriConfirm => 27,
Self::CicamLicense => 33,
Self::LicenseStatus => 34,
Self::LicenseRcvdStatus => 35,
Self::OperatingMode => 38,
Self::PinCode => 39,
Self::RecordStartStatus => 40,
Self::ModeChangeStatus => 41,
Self::RecordStopStatus => 42,
Self::LtsId => 50,
Self::Other(v) => v,
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::UriMessage => "uri_message",
Self::ProgramNumber => "program_number",
Self::UriConfirm => "uri_confirm",
Self::CicamLicense => "cicam_license",
Self::LicenseStatus => "license_status",
Self::LicenseRcvdStatus => "license_rcvd_status",
Self::OperatingMode => "operating_mode",
Self::PinCode => "PINcode",
Self::RecordStartStatus => "record_start_status",
Self::ModeChangeStatus => "mode_change_status",
Self::RecordStopStatus => "record_stop_status",
Self::LtsId => "LTS_id",
Self::Other(_) => "reserved",
}
}
}
dvb_common::impl_spec_display!(DatatypeId, Other);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum OperatingMode {
Timeshift,
UnattendedRecording,
Other(u8),
}
impl OperatingMode {
#[must_use]
pub fn from_u8(v: u8) -> Self {
match v {
0x01 => Self::Timeshift,
0x02 => Self::UnattendedRecording,
other => Self::Other(other),
}
}
#[must_use]
pub const fn to_u8(self) -> u8 {
match self {
Self::Timeshift => 0x01,
Self::UnattendedRecording => 0x02,
Self::Other(v) => v,
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Timeshift => "Timeshift",
Self::UnattendedRecording => "Unattended_Recording",
Self::Other(_) => "reserved",
}
}
}
dvb_common::impl_spec_display!(OperatingMode, Other);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct SacDatatype<'a> {
pub datatype_id: DatatypeId,
#[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
pub value: &'a [u8],
}
const SAC_DATATYPE_HEADER: usize = 3;
impl<'a> SacDatatype<'a> {
#[must_use]
pub fn serialized_len(&self) -> usize {
SAC_DATATYPE_HEADER + self.value.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct SacMessage<'a> {
#[cfg_attr(feature = "serde", serde(borrow))]
pub datatypes: Vec<SacDatatype<'a>>,
}
impl<'a> SacMessage<'a> {
pub fn parse(mut body: &'a [u8]) -> Result<Self> {
let mut datatypes = Vec::new();
while !body.is_empty() {
if body.len() < SAC_DATATYPE_HEADER {
return Err(Error::BufferTooShort {
need: SAC_DATATYPE_HEADER,
have: body.len(),
what: "SAC datatype header",
});
}
let datatype_id = DatatypeId::from_u8(body[0]);
let len = u16::from_be_bytes([body[1], body[2]]) as usize;
let end = SAC_DATATYPE_HEADER + len;
if body.len() < end {
return Err(Error::BufferTooShort {
need: end,
have: body.len(),
what: "SAC datatype value",
});
}
datatypes.push(SacDatatype {
datatype_id,
value: &body[SAC_DATATYPE_HEADER..end],
});
body = &body[end..];
}
Ok(Self { datatypes })
}
#[must_use]
pub fn serialized_len(&self) -> usize {
self.datatypes.iter().map(SacDatatype::serialized_len).sum()
}
pub fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let total = self.serialized_len();
if buf.len() < total {
return Err(Error::OutputBufferTooSmall {
need: total,
have: buf.len(),
});
}
let mut pos = 0;
for dt in &self.datatypes {
if dt.value.len() > u16::MAX as usize {
return Err(Error::LengthTooLarge(dt.value.len()));
}
buf[pos] = dt.datatype_id.to_u8();
buf[pos + 1..pos + 3].copy_from_slice(&(dt.value.len() as u16).to_be_bytes());
pos += SAC_DATATYPE_HEADER;
buf[pos..pos + dt.value.len()].copy_from_slice(dt.value);
pos += dt.value.len();
}
Ok(pos)
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = alloc::vec![0u8; self.serialized_len()];
let n = self.serialize_into(&mut buf).expect("buffer sized exactly");
debug_assert_eq!(n, buf.len());
buf
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum ContentControlApdu {
CcPinReply(CcPinReply),
CcPinEvent(CcPinEvent),
}
impl ContentControlApdu {
pub fn parse(body: &[u8]) -> Result<Self> {
if body.len() < 3 {
return Err(Error::BufferTooShort {
need: 3,
have: body.len(),
what: "content_control apdu_tag",
});
}
let t = ApduTag::from_bytes(body[0], body[1], body[2]);
match t {
tag::CC_PIN_REPLY => Ok(Self::CcPinReply(CcPinReply::parse(body)?)),
tag::CC_PIN_EVENT => Ok(Self::CcPinEvent(CcPinEvent::parse(body)?)),
_ => Err(Error::UnexpectedApduTag {
got: t.as_u24(),
expected: tag::CC_PIN_REPLY.as_u24(),
what: "content_control",
}),
}
}
}
impl Serialize for ContentControlApdu {
type Error = Error;
fn serialized_len(&self) -> usize {
match self {
Self::CcPinReply(o) => o.serialized_len(),
Self::CcPinEvent(o) => o.serialized_len(),
}
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
match self {
Self::CcPinReply(o) => o.serialize_into(buf),
Self::CcPinEvent(o) => o.serialize_into(buf),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cc_pin_reply_bound_round_trips_and_bites() {
let r = CcPinReply {
lts_id: Some(0x07),
pincode_status: 0x42,
};
let bytes = r.to_bytes();
assert_eq!(bytes, [0x9F, 0x90, 0x14, 0x03, 0x01, 0x07, 0x42]);
assert_eq!(CcPinReply::parse(&bytes).unwrap(), r);
let other = CcPinReply {
lts_id: None,
pincode_status: 0x42,
};
let ob = other.to_bytes();
assert_eq!(ob, [0x9F, 0x90, 0x14, 0x03, 0x00, 0x00, 0x42]);
assert_ne!(bytes, ob);
assert_eq!(CcPinReply::parse(&ob).unwrap(), other);
}
#[test]
fn cc_pin_event_round_trips_and_bites() {
let e = CcPinEvent {
lts_id: 0x03,
program_number: 0x1234,
pincode_status: 0x05,
rating: 0x0A,
pin_event_time_utc: 0x01_0203_0405,
pin_event_time_centiseconds: 0x63,
private_data: [
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
0x1E,
],
};
let bytes = e.to_bytes();
let expected = [
0x9F, 0x90, 0x15, 0x1A, 0x03, 0x12, 0x34, 0x05, 0x0A, 0x01, 0x02, 0x03, 0x04, 0x05, 0x63, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
0x1E, ];
assert_eq!(bytes, expected);
assert_eq!(CcPinEvent::parse(&bytes).unwrap(), e);
let mut other = e;
other.pin_event_time_utc += 1;
assert_ne!(bytes, other.to_bytes());
}
#[test]
fn sac_message_two_datatypes_round_trips() {
let msg = SacMessage {
datatypes: alloc::vec![
SacDatatype {
datatype_id: DatatypeId::ProgramNumber,
value: &[0x12, 0x34],
},
SacDatatype {
datatype_id: DatatypeId::LtsId,
value: &[0x07],
},
],
};
let bytes = msg.to_bytes();
assert_eq!(
bytes,
[0x1A, 0x00, 0x02, 0x12, 0x34, 0x32, 0x00, 0x01, 0x07]
);
assert_eq!(SacMessage::parse(&bytes).unwrap(), msg);
let mut other = msg.clone();
other.datatypes[1].datatype_id = DatatypeId::Other(99);
assert_ne!(bytes, other.to_bytes());
assert_eq!(other.to_bytes()[5], 99);
}
#[test]
fn sac_message_with_opaque_license() {
let msg = SacMessage {
datatypes: alloc::vec![SacDatatype {
datatype_id: DatatypeId::CicamLicense,
value: &[0xDE, 0xAD, 0xBE, 0xEF],
}],
};
let bytes = msg.to_bytes();
assert_eq!(bytes, [0x21, 0x00, 0x04, 0xDE, 0xAD, 0xBE, 0xEF]);
let parsed = SacMessage::parse(&bytes).unwrap();
assert_eq!(parsed, msg);
assert_eq!(parsed.datatypes[0].datatype_id.name(), "cicam_license");
}
#[test]
fn datatype_id_and_operating_mode_labels() {
assert_eq!(DatatypeId::LtsId.to_u8(), 50);
assert_eq!(DatatypeId::from_u8(50), DatatypeId::LtsId);
assert_eq!(DatatypeId::from_u8(33), DatatypeId::CicamLicense);
assert_eq!(DatatypeId::from_u8(39), DatatypeId::PinCode);
assert_eq!(DatatypeId::PinCode.name(), "PINcode");
assert_eq!(DatatypeId::Other(7).name(), "reserved");
assert_eq!(OperatingMode::from_u8(0x01), OperatingMode::Timeshift);
assert_eq!(
OperatingMode::from_u8(0x02),
OperatingMode::UnattendedRecording
);
assert_eq!(OperatingMode::Timeshift.to_u8(), 0x01);
}
#[test]
fn dispatch_routes_each_tag() {
let r = CcPinReply {
lts_id: None,
pincode_status: 0,
}
.to_bytes();
let parsed = ContentControlApdu::parse(&r).unwrap();
assert!(matches!(parsed, ContentControlApdu::CcPinReply(_)));
assert_eq!(parsed.to_bytes(), r);
let cc_open = [0x9F, 0x90, 0x01, 0x00];
assert!(matches!(
ContentControlApdu::parse(&cc_open),
Err(Error::UnexpectedApduTag { .. })
));
}
}