use crate::error::{Error, Result};
use crate::resource::ResourceId;
pub mod ca_support;
pub mod cicam_player;
pub mod content_control;
pub mod descriptors;
pub mod file_retrieval;
pub mod low_speed_comms_v4;
pub mod multistream;
pub mod multistream_host_control;
pub mod sample_decryption;
pub mod uri;
pub const MULTISTREAM: ResourceId = ResourceId(0x0090_0041);
pub const CONTENT_CONTROL: ResourceId = ResourceId(0x008C_1041);
pub const MULTISTREAM_HOST_CONTROL: ResourceId = ResourceId(0x0020_0081);
pub const HOST_CONTROL_V3: ResourceId = ResourceId(0x0020_0043);
pub const SAMPLE_DECRYPTION: ResourceId = ResourceId(0x0092_0041);
pub const CICAM_PLAYER: ResourceId = ResourceId(0x0093_0041);
pub const FILE_RETRIEVAL: ResourceId = ResourceId(0x0091_0041);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum CiPlusResource {
Multistream,
ContentControl,
MultistreamHostControl(multistream_host_control::HostControlMode),
SampleDecryption,
CicamPlayer,
FileRetrieval,
}
impl CiPlusResource {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Multistream => "multistream",
Self::ContentControl => "content_control",
Self::MultistreamHostControl(_) => "multistream_host_control",
Self::SampleDecryption => "sample_decryption",
Self::CicamPlayer => "cicam_player",
Self::FileRetrieval => "file_retrieval",
}
}
}
broadcast_common::impl_spec_display!(CiPlusResource);
#[must_use]
pub fn classify(id: ResourceId) -> Option<CiPlusResource> {
use multistream_host_control::HostControlMode;
match id {
MULTISTREAM => Some(CiPlusResource::Multistream),
CONTENT_CONTROL => Some(CiPlusResource::ContentControl),
MULTISTREAM_HOST_CONTROL => Some(CiPlusResource::MultistreamHostControl(
HostControlMode::MultiStream,
)),
HOST_CONTROL_V3 => Some(CiPlusResource::MultistreamHostControl(
HostControlMode::BaseV3,
)),
SAMPLE_DECRYPTION => Some(CiPlusResource::SampleDecryption),
CICAM_PLAYER => Some(CiPlusResource::CicamPlayer),
FILE_RETRIEVAL => Some(CiPlusResource::FileRetrieval),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum CiPlusApdu<'a> {
Multistream(multistream::MultistreamApdu),
ContentControl(content_control::ContentControlApdu),
MultistreamHostControl(
#[cfg_attr(feature = "serde", serde(borrow))]
multistream_host_control::MultistreamHostControlApdu<'a>,
),
SampleDecryption(
#[cfg_attr(feature = "serde", serde(borrow))] sample_decryption::SampleDecryptionApdu<'a>,
),
CicamPlayer(#[cfg_attr(feature = "serde", serde(borrow))] cicam_player::CicamPlayerApdu<'a>),
FileRetrieval(
#[cfg_attr(feature = "serde", serde(borrow))] file_retrieval::FileRetrievalApdu<'a>,
),
}
impl<'a> CiPlusApdu<'a> {
pub fn parse(resource_id: ResourceId, body: &'a [u8]) -> Result<Self> {
match classify(resource_id) {
Some(CiPlusResource::Multistream) => Ok(Self::Multistream(
multistream::MultistreamApdu::parse(body)?,
)),
Some(CiPlusResource::ContentControl) => Ok(Self::ContentControl(
content_control::ContentControlApdu::parse(body)?,
)),
Some(CiPlusResource::MultistreamHostControl(mode)) => Ok(Self::MultistreamHostControl(
multistream_host_control::MultistreamHostControlApdu::parse_mode(body, mode)?,
)),
Some(CiPlusResource::SampleDecryption) => Ok(Self::SampleDecryption(
sample_decryption::SampleDecryptionApdu::parse(body)?,
)),
Some(CiPlusResource::CicamPlayer) => Ok(Self::CicamPlayer(
cicam_player::CicamPlayerApdu::parse(body)?,
)),
Some(CiPlusResource::FileRetrieval) => Ok(Self::FileRetrieval(
file_retrieval::FileRetrievalApdu::parse(body)?,
)),
None => Err(Error::UnknownResource {
resource_id: resource_id.0,
}),
}
}
}
impl broadcast_common::Serialize for CiPlusApdu<'_> {
type Error = Error;
fn serialized_len(&self) -> usize {
match self {
Self::Multistream(o) => o.serialized_len(),
Self::ContentControl(o) => o.serialized_len(),
Self::MultistreamHostControl(o) => o.serialized_len(),
Self::SampleDecryption(o) => o.serialized_len(),
Self::CicamPlayer(o) => o.serialized_len(),
Self::FileRetrieval(o) => o.serialized_len(),
}
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
match self {
Self::Multistream(o) => o.serialize_into(buf),
Self::ContentControl(o) => o.serialize_into(buf),
Self::MultistreamHostControl(o) => o.serialize_into(buf),
Self::SampleDecryption(o) => o.serialize_into(buf),
Self::CicamPlayer(o) => o.serialize_into(buf),
Self::FileRetrieval(o) => o.serialize_into(buf),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use broadcast_common::Serialize;
#[test]
fn classify_fixed_ids() {
use multistream_host_control::HostControlMode;
assert_eq!(classify(MULTISTREAM), Some(CiPlusResource::Multistream));
assert_eq!(
classify(CONTENT_CONTROL),
Some(CiPlusResource::ContentControl)
);
assert_eq!(
classify(MULTISTREAM_HOST_CONTROL),
Some(CiPlusResource::MultistreamHostControl(
HostControlMode::MultiStream
))
);
assert_eq!(
classify(HOST_CONTROL_V3),
Some(CiPlusResource::MultistreamHostControl(
HostControlMode::BaseV3
))
);
assert_eq!(
classify(SAMPLE_DECRYPTION),
Some(CiPlusResource::SampleDecryption)
);
assert_eq!(classify(CICAM_PLAYER), Some(CiPlusResource::CicamPlayer));
assert_eq!(
classify(FILE_RETRIEVAL),
Some(CiPlusResource::FileRetrieval)
);
assert_eq!(classify(ResourceId(0xDEAD_BEEF)), None);
assert_eq!(classify(ResourceId(0x000C_0041)), None);
}
#[test]
fn dispatch_routes_host_control_and_sample_decryption() {
let lcn = [0x9F, 0x84, 0x07, 0x03, 0x01, 0x81, 0x23];
let hc = CiPlusApdu::parse(MULTISTREAM_HOST_CONTROL, &lcn).unwrap();
assert!(matches!(
hc,
CiPlusApdu::MultistreamHostControl(
multistream_host_control::MultistreamHostControlApdu::TuneLcnReq(_)
)
));
assert_eq!(hc.to_bytes(), lcn);
let hc_v3 = CiPlusApdu::parse(HOST_CONTROL_V3, &lcn).unwrap();
assert!(matches!(hc_v3, CiPlusApdu::MultistreamHostControl(_)));
let sd = [0x9F, 0x98, 0x05, 0x02, 0x03, 0x01];
let parsed = CiPlusApdu::parse(SAMPLE_DECRYPTION, &sd).unwrap();
assert!(matches!(
parsed,
CiPlusApdu::SampleDecryption(sample_decryption::SampleDecryptionApdu::SdUpdateReply(_))
));
assert_eq!(parsed.to_bytes(), sd);
}
#[test]
fn tune_tag_9f8409_routes_resource_scoped_not_via_anyapdu() {
let triplet = [
0x9F, 0x84, 0x09, 0x09, 0x05, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x44, 0x00,
];
assert!(matches!(
CiPlusApdu::parse(MULTISTREAM_HOST_CONTROL, &triplet).unwrap(),
CiPlusApdu::MultistreamHostControl(_)
));
assert!(matches!(
CiPlusApdu::parse(MULTISTREAM, &triplet),
Err(Error::UnexpectedApduTag { .. })
));
let en_tune = [0x9F, 0x84, 0x00, 0x00];
assert!(matches!(
CiPlusApdu::parse(MULTISTREAM_HOST_CONTROL, &en_tune),
Err(Error::UnexpectedApduTag { .. })
));
}
#[test]
fn dispatch_routes_multistream_and_content_control() {
let ms_body = [0x9F, 0x92, 0x02, 0x03, 0x01, 0x00, 0x00];
let ms = CiPlusApdu::parse(MULTISTREAM, &ms_body).unwrap();
assert!(matches!(
ms,
CiPlusApdu::Multistream(multistream::MultistreamApdu::PidSelectReply(_))
));
assert_eq!(ms.to_bytes(), ms_body);
let cc_body = [0x9F, 0x90, 0x14, 0x03, 0x00, 0x00, 0x42];
let cc = CiPlusApdu::parse(CONTENT_CONTROL, &cc_body).unwrap();
assert!(matches!(
cc,
CiPlusApdu::ContentControl(content_control::ContentControlApdu::CcPinReply(_))
));
assert_eq!(cc.to_bytes(), cc_body);
}
#[test]
fn dispatch_routes_cicam_player_and_file_retrieval() {
let upd = [0x9F, 0xA0, 0x0F, 0x02, 0x06, 0x00];
let p = CiPlusApdu::parse(CICAM_PLAYER, &upd).unwrap();
assert!(matches!(
p,
CiPlusApdu::CicamPlayer(cicam_player::CicamPlayerApdu::UpdateReply(_))
));
assert_eq!(p.to_bytes(), upd);
assert!(matches!(
CiPlusApdu::parse(MULTISTREAM, &upd),
Err(Error::UnexpectedApduTag { .. })
));
assert!(matches!(
CiPlusApdu::parse(SAMPLE_DECRYPTION, &upd),
Err(Error::UnexpectedApduTag { .. })
));
let ack = [0x9F, 0x94, 0x01, 0x01, 0x01];
let f = CiPlusApdu::parse(FILE_RETRIEVAL, &ack).unwrap();
assert!(matches!(
f,
CiPlusApdu::FileRetrieval(file_retrieval::FileRetrievalApdu::FileSystemAck(_))
));
assert_eq!(f.to_bytes(), ack);
assert!(matches!(
CiPlusApdu::parse(CICAM_PLAYER, &ack),
Err(Error::UnexpectedApduTag { .. })
));
assert!(matches!(
CiPlusApdu::parse(FILE_RETRIEVAL, &upd),
Err(Error::UnexpectedApduTag { .. })
));
}
#[test]
fn unknown_resource_errors() {
let body = [0x9F, 0x92, 0x00, 0x00];
assert!(matches!(
CiPlusApdu::parse(ResourceId(0x1234_5678), &body),
Err(Error::UnknownResource { .. })
));
}
#[test]
fn ca_pmt_tag_collision_routes_per_resource_independently() {
let ca_pmt_like = [0x9F, 0x80, 0x32, 0x00];
assert!(matches!(
CiPlusApdu::parse(MULTISTREAM, &ca_pmt_like),
Err(Error::UnexpectedApduTag { .. })
));
assert!(matches!(
CiPlusApdu::parse(CONTENT_CONTROL, &ca_pmt_like),
Err(Error::UnexpectedApduTag { .. })
));
}
#[test]
fn tag_9f9200_under_multistream_vs_9f9014_under_content_control() {
let cap = [0x9F, 0x92, 0x00, 0x03, 0x04, 0x00, 0x01];
assert!(matches!(
CiPlusApdu::parse(MULTISTREAM, &cap).unwrap(),
CiPlusApdu::Multistream(multistream::MultistreamApdu::CicamMultistreamCapability(_))
));
assert!(matches!(
CiPlusApdu::parse(CONTENT_CONTROL, &cap),
Err(Error::UnexpectedApduTag { .. })
));
let pin = [0x9F, 0x90, 0x14, 0x03, 0x00, 0x00, 0x42];
assert!(matches!(
CiPlusApdu::parse(CONTENT_CONTROL, &pin).unwrap(),
CiPlusApdu::ContentControl(content_control::ContentControlApdu::CcPinReply(_))
));
assert!(matches!(
CiPlusApdu::parse(MULTISTREAM, &pin),
Err(Error::UnexpectedApduTag { .. })
));
}
}