use crate::error::{Error, Result};
use crate::objects;
use broadcast_common::{Parse, Serialize};
pub mod tag {
use crate::tag::ApduTag;
pub const CP_QUERY: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x00);
pub const CP_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x01);
pub const CP_COMMAND: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x02);
pub const CP_RESPONSE: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x03);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum CpStatus {
Inactive,
Active,
IdMismatch,
Reserved(u8),
}
impl CpStatus {
#[must_use]
pub fn from_u8(v: u8) -> Self {
match v {
0x01 => Self::Inactive,
0x02 => Self::Active,
0xFF => Self::IdMismatch,
other => Self::Reserved(other),
}
}
#[must_use]
pub const fn to_u8(self) -> u8 {
match self {
Self::Inactive => 0x01,
Self::Active => 0x02,
Self::IdMismatch => 0xFF,
Self::Reserved(v) => v,
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Inactive => "Copy Protection Inactive",
Self::Active => "Copy Protection Active",
Self::IdMismatch => "ID mismatch",
Self::Reserved(_) => "reserved",
}
}
}
broadcast_common::impl_spec_display!(CpStatus, Reserved);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CpQuery {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub copy_protection_id: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CpReply {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub copy_protection_id: u32,
pub status: CpStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CpCommand<'a> {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub copy_protection_id: u32,
#[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
pub command_bytes: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CpResponse<'a> {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub copy_protection_id: u32,
#[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
pub response_bytes: &'a [u8],
}
const CP_ID_LEN: usize = 3;
const CP_ID_BITS: u32 = 24;
fn read_cp_id(body: &[u8]) -> u32 {
((body[0] as u32) << 16) | ((body[1] as u32) << 8) | body[2] as u32
}
fn write_cp_id(id: u32, buf: &mut [u8]) -> Result<()> {
objects::fit_bits(u64::from(id), CP_ID_BITS, "copy_protection_id")?;
let be = id.to_be_bytes();
buf[..CP_ID_LEN].copy_from_slice(&be[be.len() - CP_ID_LEN..]);
Ok(())
}
impl<'a> Parse<'a> for CpQuery {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, body) = objects::parse_apdu_header(bytes, tag::CP_QUERY, "cp_query")?;
if body.len() < CP_ID_LEN {
return Err(Error::BufferTooShort {
need: CP_ID_LEN,
have: body.len(),
what: "cp_query",
});
}
objects::reject_trailing_body(body, CP_ID_LEN, "cp_query")?;
Ok(Self {
length_form,
copy_protection_id: read_cp_id(body),
})
}
}
impl Serialize for CpQuery {
type Error = Error;
fn serialized_len(&self) -> usize {
objects::apdu_len(CP_ID_LEN, self.length_form)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = objects::write_apdu_header(tag::CP_QUERY, CP_ID_LEN, self.length_form, buf)?;
write_cp_id(self.copy_protection_id, &mut buf[pos..])?;
Ok(pos + CP_ID_LEN)
}
}
const CP_REPLY_BODY: usize = CP_ID_LEN + 1;
impl<'a> Parse<'a> for CpReply {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, body) = objects::parse_apdu_header(bytes, tag::CP_REPLY, "cp_reply")?;
if body.len() < CP_REPLY_BODY {
return Err(Error::BufferTooShort {
need: CP_REPLY_BODY,
have: body.len(),
what: "cp_reply",
});
}
crate::objects::reject_trailing_body(body, CP_REPLY_BODY, "cp_reply")?;
Ok(Self {
length_form,
copy_protection_id: read_cp_id(body),
status: CpStatus::from_u8(body[CP_ID_LEN]),
})
}
}
impl Serialize for CpReply {
type Error = Error;
fn serialized_len(&self) -> usize {
objects::apdu_len(CP_REPLY_BODY, self.length_form)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = objects::write_apdu_header(tag::CP_REPLY, CP_REPLY_BODY, self.length_form, buf)?;
write_cp_id(self.copy_protection_id, &mut buf[pos..])?;
buf[pos + CP_ID_LEN] = self.status.to_u8();
Ok(pos + CP_REPLY_BODY)
}
}
impl<'a> Parse<'a> for CpCommand<'a> {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, body) = objects::parse_apdu_header(bytes, tag::CP_COMMAND, "cp_command")?;
if body.len() < CP_ID_LEN {
return Err(Error::BufferTooShort {
need: CP_ID_LEN,
have: body.len(),
what: "cp_command",
});
}
Ok(Self {
length_form,
copy_protection_id: read_cp_id(body),
command_bytes: &body[CP_ID_LEN..],
})
}
}
impl Serialize for CpCommand<'_> {
type Error = Error;
fn serialized_len(&self) -> usize {
objects::apdu_len(CP_ID_LEN + self.command_bytes.len(), self.length_form)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let body_len = CP_ID_LEN + self.command_bytes.len();
let mut pos = objects::write_apdu_header(tag::CP_COMMAND, body_len, self.length_form, buf)?;
write_cp_id(self.copy_protection_id, &mut buf[pos..])?;
pos += CP_ID_LEN;
buf[pos..pos + self.command_bytes.len()].copy_from_slice(self.command_bytes);
Ok(pos + self.command_bytes.len())
}
}
impl<'a> Parse<'a> for CpResponse<'a> {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, body) =
objects::parse_apdu_header(bytes, tag::CP_RESPONSE, "cp_response")?;
if body.len() < CP_ID_LEN {
return Err(Error::BufferTooShort {
need: CP_ID_LEN,
have: body.len(),
what: "cp_response",
});
}
Ok(Self {
length_form,
copy_protection_id: read_cp_id(body),
response_bytes: &body[CP_ID_LEN..],
})
}
}
impl Serialize for CpResponse<'_> {
type Error = Error;
fn serialized_len(&self) -> usize {
objects::apdu_len(CP_ID_LEN + self.response_bytes.len(), self.length_form)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let body_len = CP_ID_LEN + self.response_bytes.len();
let mut pos =
objects::write_apdu_header(tag::CP_RESPONSE, body_len, self.length_form, buf)?;
write_cp_id(self.copy_protection_id, &mut buf[pos..])?;
pos += CP_ID_LEN;
buf[pos..pos + self.response_bytes.len()].copy_from_slice(self.response_bytes);
Ok(pos + self.response_bytes.len())
}
}
crate::dispatch::declare_resource_apdus! {
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum CopyProtectionApdu<'a> ("copy_protection") {
CpQuery(CpQuery) = tag::CP_QUERY,
CpReply(CpReply) = tag::CP_REPLY,
CpCommand(CpCommand<'a>) = tag::CP_COMMAND,
CpResponse(CpResponse<'a>) = tag::CP_RESPONSE,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn copy_protection_id_over_24_bits_is_rejected_not_truncated() {
let q = CpQuery {
length_form: crate::length::LengthForm::Minimal,
copy_protection_id: 0x01AA_BBCC,
};
let mut buf = [0u8; 16];
assert!(matches!(
q.serialize_into(&mut buf),
Err(Error::InvalidObject { .. })
));
let max = CpQuery {
length_form: crate::length::LengthForm::Minimal,
copy_protection_id: 0x00FF_FFFF,
};
assert_eq!(
max.try_to_bytes().unwrap(),
[0x9F, 0x80, 0x00, 0x03, 0xFF, 0xFF, 0xFF]
);
}
#[test]
fn cp_query_round_trips_and_bites() {
let q = CpQuery {
length_form: crate::length::LengthForm::Minimal,
copy_protection_id: 0xAABBCC,
};
let bytes = q.try_to_bytes().unwrap();
assert_eq!(bytes, [0x9F, 0x80, 0x00, 0x03, 0xAA, 0xBB, 0xCC]);
assert_eq!(CpQuery::parse(&bytes).unwrap(), q);
let other = CpQuery {
length_form: crate::length::LengthForm::Minimal,
copy_protection_id: 0xAABBCD,
};
assert_ne!(bytes, other.try_to_bytes().unwrap());
}
#[test]
fn cp_reply_round_trips_and_bites() {
let r = CpReply {
length_form: crate::length::LengthForm::Minimal,
copy_protection_id: 0x010203,
status: CpStatus::Active,
};
let bytes = r.try_to_bytes().unwrap();
assert_eq!(bytes, [0x9F, 0x80, 0x01, 0x04, 0x01, 0x02, 0x03, 0x02]);
assert_eq!(CpReply::parse(&bytes).unwrap(), r);
assert_eq!(r.status.name(), "Copy Protection Active");
let mut other = r;
other.status = CpStatus::IdMismatch;
assert_ne!(bytes, other.try_to_bytes().unwrap());
assert_eq!(other.try_to_bytes().unwrap()[7], 0xFF);
}
#[test]
fn cp_command_round_trips_and_bites() {
let c = CpCommand {
length_form: crate::length::LengthForm::Minimal,
copy_protection_id: 0x112233,
command_bytes: &[0xDE, 0xAD, 0xBE, 0xEF],
};
let bytes = c.try_to_bytes().unwrap();
assert_eq!(
bytes,
[
0x9F, 0x80, 0x02, 0x07, 0x11, 0x22, 0x33, 0xDE, 0xAD, 0xBE, 0xEF
]
);
assert_eq!(CpCommand::parse(&bytes).unwrap(), c);
let other = CpCommand {
length_form: crate::length::LengthForm::Minimal,
copy_protection_id: 0x112233,
command_bytes: &[0xDE, 0xAD, 0xBE, 0x00],
};
assert_ne!(bytes, other.try_to_bytes().unwrap());
}
#[test]
fn cp_response_round_trips() {
let r = CpResponse {
length_form: crate::length::LengthForm::Minimal,
copy_protection_id: 0x445566,
response_bytes: &[0x01, 0x02],
};
let bytes = r.try_to_bytes().unwrap();
assert_eq!(
bytes,
[0x9F, 0x80, 0x03, 0x05, 0x44, 0x55, 0x66, 0x01, 0x02]
);
assert_eq!(CpResponse::parse(&bytes).unwrap(), r);
}
#[test]
fn dispatch_routes_each_tag() {
let q = CpQuery {
length_form: crate::length::LengthForm::Minimal,
copy_protection_id: 0,
}
.try_to_bytes()
.unwrap();
assert!(matches!(
CopyProtectionApdu::parse(&q).unwrap(),
CopyProtectionApdu::CpQuery(_)
));
let resp = CpResponse {
length_form: crate::length::LengthForm::Minimal,
copy_protection_id: 0x1,
response_bytes: &[0xFF],
}
.try_to_bytes()
.unwrap();
let parsed = CopyProtectionApdu::parse(&resp).unwrap();
assert!(matches!(parsed, CopyProtectionApdu::CpResponse(_)));
assert_eq!(parsed.try_to_bytes().unwrap(), resp);
}
}