Skip to main content

dvb_ci/ci_ext/
copy_protection.rs

1//! Copy Protection objects — ETSI TS 101 699 V1.1.1 §6.6, Tables 69-73
2//! (PDF pp. 62-63). See `docs/ci_plus/copy-protection.md`.
3//!
4//! Resource ID `0x00041ii1` (`ii` = Module ID). Generic control of a host's
5//! copy-protection function; the detailed semantics are CP-system-specific.
6//!
7//! - `cp_query` (`9F 80 00`, Table 69) — app → host: query status of a CP system.
8//! - `cp_reply` (`9F 80 01`, Table 70) — host → app: status reply.
9//! - `cp_command` (`9F 80 02`, Table 72) — app → host: opaque command bytes.
10//! - `cp_response` (`9F 80 03`, Table 73) — host → app: opaque response bytes.
11
12use crate::error::{Error, Result};
13use crate::objects;
14use crate::tag::ApduTag;
15use dvb_common::{Parse, Serialize};
16
17/// Resource-scoped `apdu_tag`s for Copy Protection (Tables 69-73).
18pub mod tag {
19    use crate::tag::ApduTag;
20    /// `CopyProtectionQueryTag` = `9F 80 00`.
21    pub const CP_QUERY: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x00);
22    /// `CPReplyTag` = `9F 80 01`.
23    pub const CP_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x01);
24    /// `CPCommandTag` = `9F 80 02`.
25    pub const CP_COMMAND: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x02);
26    /// `CPResponseTag` = `9F 80 03`.
27    pub const CP_RESPONSE: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x03);
28}
29
30/// `Status` — copy-protection status (Table 71).
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize))]
33#[non_exhaustive]
34pub enum CpStatus {
35    /// `01` — Copy Protection Inactive.
36    Inactive,
37    /// `02` — Copy Protection Active.
38    Active,
39    /// `FF` — ID mismatch.
40    IdMismatch,
41    /// Any other value (reserved).
42    Reserved(u8),
43}
44
45impl CpStatus {
46    /// Decode the `Status` byte.
47    #[must_use]
48    pub fn from_u8(v: u8) -> Self {
49        match v {
50            0x01 => Self::Inactive,
51            0x02 => Self::Active,
52            0xFF => Self::IdMismatch,
53            other => Self::Reserved(other),
54        }
55    }
56    /// Wire byte.
57    #[must_use]
58    pub fn to_u8(self) -> u8 {
59        match self {
60            Self::Inactive => 0x01,
61            Self::Active => 0x02,
62            Self::IdMismatch => 0xFF,
63            Self::Reserved(v) => v,
64        }
65    }
66    /// Spec token, or `"reserved"`.
67    #[must_use]
68    pub fn name(&self) -> &'static str {
69        match self {
70            Self::Inactive => "Copy Protection Inactive",
71            Self::Active => "Copy Protection Active",
72            Self::IdMismatch => "ID mismatch",
73            Self::Reserved(_) => "reserved",
74        }
75    }
76}
77dvb_common::impl_spec_display!(CpStatus, Reserved);
78
79/// `cp_query()` (Table 69): app → host.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize))]
82pub struct CpQuery {
83    /// 24-bit `CopyProtectionID` (an IEEE `company_id`).
84    pub copy_protection_id: u32,
85}
86
87/// `cp_reply()` (Table 70): host → app.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize))]
90pub struct CpReply {
91    /// 24-bit `CopyProtectionID`.
92    pub copy_protection_id: u32,
93    /// `Status`.
94    pub status: CpStatus,
95}
96
97/// `cp_command()` (Table 72): app → host. The `cp_command_byte`s are
98/// **opaque, CP-system-specific** bytes carried verbatim.
99#[derive(Debug, Clone, PartialEq, Eq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize))]
101pub struct CpCommand<'a> {
102    /// 24-bit `CopyProtectionID`.
103    pub copy_protection_id: u32,
104    /// Opaque command bytes.
105    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
106    pub command_bytes: &'a [u8],
107}
108
109/// `cp_response()` (Table 73): host → app. Identical to [`CpCommand`] except for
110/// the tag; the `cp_response_byte`s are opaque CP-system-specific bytes.
111#[derive(Debug, Clone, PartialEq, Eq)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize))]
113pub struct CpResponse<'a> {
114    /// 24-bit `CopyProtectionID`.
115    pub copy_protection_id: u32,
116    /// Opaque response bytes.
117    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
118    pub response_bytes: &'a [u8],
119}
120
121/// Width of the `CopyProtectionID` field (24 bits).
122const CP_ID_LEN: usize = 3;
123
124fn read_cp_id(body: &[u8]) -> u32 {
125    ((body[0] as u32) << 16) | ((body[1] as u32) << 8) | body[2] as u32
126}
127
128fn write_cp_id(id: u32, buf: &mut [u8]) {
129    buf[0] = (id >> 16) as u8;
130    buf[1] = (id >> 8) as u8;
131    buf[2] = id as u8;
132}
133
134// --- cp_query ---
135
136impl<'a> Parse<'a> for CpQuery {
137    type Error = Error;
138    fn parse(bytes: &'a [u8]) -> Result<Self> {
139        let body = objects::parse_apdu_header(bytes, tag::CP_QUERY, "cp_query")?;
140        if body.len() < CP_ID_LEN {
141            return Err(Error::BufferTooShort {
142                need: CP_ID_LEN,
143                have: body.len(),
144                what: "cp_query",
145            });
146        }
147        Ok(Self {
148            copy_protection_id: read_cp_id(body),
149        })
150    }
151}
152impl Serialize for CpQuery {
153    type Error = Error;
154    fn serialized_len(&self) -> usize {
155        objects::apdu_len(CP_ID_LEN)
156    }
157    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
158        let pos = objects::write_apdu_header(tag::CP_QUERY, CP_ID_LEN, buf)?;
159        write_cp_id(self.copy_protection_id, &mut buf[pos..]);
160        Ok(pos + CP_ID_LEN)
161    }
162}
163
164// --- cp_reply ---
165
166// CopyProtectionID(3) + Status(1).
167const CP_REPLY_BODY: usize = CP_ID_LEN + 1;
168
169impl<'a> Parse<'a> for CpReply {
170    type Error = Error;
171    fn parse(bytes: &'a [u8]) -> Result<Self> {
172        let body = objects::parse_apdu_header(bytes, tag::CP_REPLY, "cp_reply")?;
173        if body.len() < CP_REPLY_BODY {
174            return Err(Error::BufferTooShort {
175                need: CP_REPLY_BODY,
176                have: body.len(),
177                what: "cp_reply",
178            });
179        }
180        Ok(Self {
181            copy_protection_id: read_cp_id(body),
182            status: CpStatus::from_u8(body[CP_ID_LEN]),
183        })
184    }
185}
186impl Serialize for CpReply {
187    type Error = Error;
188    fn serialized_len(&self) -> usize {
189        objects::apdu_len(CP_REPLY_BODY)
190    }
191    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
192        let pos = objects::write_apdu_header(tag::CP_REPLY, CP_REPLY_BODY, buf)?;
193        write_cp_id(self.copy_protection_id, &mut buf[pos..]);
194        buf[pos + CP_ID_LEN] = self.status.to_u8();
195        Ok(pos + CP_REPLY_BODY)
196    }
197}
198
199// --- cp_command ---
200
201impl<'a> Parse<'a> for CpCommand<'a> {
202    type Error = Error;
203    fn parse(bytes: &'a [u8]) -> Result<Self> {
204        let body = objects::parse_apdu_header(bytes, tag::CP_COMMAND, "cp_command")?;
205        if body.len() < CP_ID_LEN {
206            return Err(Error::BufferTooShort {
207                need: CP_ID_LEN,
208                have: body.len(),
209                what: "cp_command",
210            });
211        }
212        Ok(Self {
213            copy_protection_id: read_cp_id(body),
214            command_bytes: &body[CP_ID_LEN..],
215        })
216    }
217}
218impl Serialize for CpCommand<'_> {
219    type Error = Error;
220    fn serialized_len(&self) -> usize {
221        objects::apdu_len(CP_ID_LEN + self.command_bytes.len())
222    }
223    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
224        let body_len = CP_ID_LEN + self.command_bytes.len();
225        let mut pos = objects::write_apdu_header(tag::CP_COMMAND, body_len, buf)?;
226        write_cp_id(self.copy_protection_id, &mut buf[pos..]);
227        pos += CP_ID_LEN;
228        buf[pos..pos + self.command_bytes.len()].copy_from_slice(self.command_bytes);
229        Ok(pos + self.command_bytes.len())
230    }
231}
232
233// --- cp_response ---
234
235impl<'a> Parse<'a> for CpResponse<'a> {
236    type Error = Error;
237    fn parse(bytes: &'a [u8]) -> Result<Self> {
238        let body = objects::parse_apdu_header(bytes, tag::CP_RESPONSE, "cp_response")?;
239        if body.len() < CP_ID_LEN {
240            return Err(Error::BufferTooShort {
241                need: CP_ID_LEN,
242                have: body.len(),
243                what: "cp_response",
244            });
245        }
246        Ok(Self {
247            copy_protection_id: read_cp_id(body),
248            response_bytes: &body[CP_ID_LEN..],
249        })
250    }
251}
252impl Serialize for CpResponse<'_> {
253    type Error = Error;
254    fn serialized_len(&self) -> usize {
255        objects::apdu_len(CP_ID_LEN + self.response_bytes.len())
256    }
257    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
258        let body_len = CP_ID_LEN + self.response_bytes.len();
259        let mut pos = objects::write_apdu_header(tag::CP_RESPONSE, body_len, buf)?;
260        write_cp_id(self.copy_protection_id, &mut buf[pos..]);
261        pos += CP_ID_LEN;
262        buf[pos..pos + self.response_bytes.len()].copy_from_slice(self.response_bytes);
263        Ok(pos + self.response_bytes.len())
264    }
265}
266
267/// Resource-scoped dispatch over the Copy Protection objects.
268#[derive(Debug, Clone, PartialEq, Eq)]
269#[cfg_attr(feature = "serde", derive(serde::Serialize))]
270#[non_exhaustive]
271pub enum CopyProtectionApdu<'a> {
272    /// `cp_query` (`9F 80 00`).
273    CpQuery(CpQuery),
274    /// `cp_reply` (`9F 80 01`).
275    CpReply(CpReply),
276    /// `cp_command` (`9F 80 02`).
277    CpCommand(CpCommand<'a>),
278    /// `cp_response` (`9F 80 03`).
279    CpResponse(CpResponse<'a>),
280}
281
282impl<'a> CopyProtectionApdu<'a> {
283    /// Parse a Copy Protection APDU, dispatching on the `apdu_tag`.
284    pub fn parse(body: &'a [u8]) -> Result<Self> {
285        if body.len() < 3 {
286            return Err(Error::BufferTooShort {
287                need: 3,
288                have: body.len(),
289                what: "copy_protection apdu_tag",
290            });
291        }
292        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
293        match t {
294            tag::CP_QUERY => Ok(Self::CpQuery(CpQuery::parse(body)?)),
295            tag::CP_REPLY => Ok(Self::CpReply(CpReply::parse(body)?)),
296            tag::CP_COMMAND => Ok(Self::CpCommand(CpCommand::parse(body)?)),
297            tag::CP_RESPONSE => Ok(Self::CpResponse(CpResponse::parse(body)?)),
298            _ => Err(Error::UnexpectedApduTag {
299                got: t.as_u24(),
300                expected: tag::CP_QUERY.as_u24(),
301                what: "copy_protection",
302            }),
303        }
304    }
305}
306
307impl Serialize for CopyProtectionApdu<'_> {
308    type Error = Error;
309    fn serialized_len(&self) -> usize {
310        match self {
311            Self::CpQuery(o) => o.serialized_len(),
312            Self::CpReply(o) => o.serialized_len(),
313            Self::CpCommand(o) => o.serialized_len(),
314            Self::CpResponse(o) => o.serialized_len(),
315        }
316    }
317    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
318        match self {
319            Self::CpQuery(o) => o.serialize_into(buf),
320            Self::CpReply(o) => o.serialize_into(buf),
321            Self::CpCommand(o) => o.serialize_into(buf),
322            Self::CpResponse(o) => o.serialize_into(buf),
323        }
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn cp_query_round_trips_and_bites() {
333        let q = CpQuery {
334            copy_protection_id: 0xAABBCC,
335        };
336        let bytes = q.to_bytes();
337        assert_eq!(bytes, [0x9F, 0x80, 0x00, 0x03, 0xAA, 0xBB, 0xCC]);
338        assert_eq!(CpQuery::parse(&bytes).unwrap(), q);
339        let other = CpQuery {
340            copy_protection_id: 0xAABBCD,
341        };
342        assert_ne!(bytes, other.to_bytes());
343    }
344
345    #[test]
346    fn cp_reply_round_trips_and_bites() {
347        let r = CpReply {
348            copy_protection_id: 0x010203,
349            status: CpStatus::Active,
350        };
351        let bytes = r.to_bytes();
352        assert_eq!(bytes, [0x9F, 0x80, 0x01, 0x04, 0x01, 0x02, 0x03, 0x02]);
353        assert_eq!(CpReply::parse(&bytes).unwrap(), r);
354        assert_eq!(r.status.name(), "Copy Protection Active");
355        let mut other = r;
356        other.status = CpStatus::IdMismatch;
357        assert_ne!(bytes, other.to_bytes());
358        assert_eq!(other.to_bytes()[7], 0xFF);
359    }
360
361    #[test]
362    fn cp_command_round_trips_and_bites() {
363        let c = CpCommand {
364            copy_protection_id: 0x112233,
365            command_bytes: &[0xDE, 0xAD, 0xBE, 0xEF],
366        };
367        let bytes = c.to_bytes();
368        // tag(3) + len(1) + id(3) + 4 = 11; body len = 7 = 0x07.
369        assert_eq!(
370            bytes,
371            [0x9F, 0x80, 0x02, 0x07, 0x11, 0x22, 0x33, 0xDE, 0xAD, 0xBE, 0xEF]
372        );
373        assert_eq!(CpCommand::parse(&bytes).unwrap(), c);
374        let other = CpCommand {
375            copy_protection_id: 0x112233,
376            command_bytes: &[0xDE, 0xAD, 0xBE, 0x00],
377        };
378        assert_ne!(bytes, other.to_bytes());
379    }
380
381    #[test]
382    fn cp_response_round_trips() {
383        let r = CpResponse {
384            copy_protection_id: 0x445566,
385            response_bytes: &[0x01, 0x02],
386        };
387        let bytes = r.to_bytes();
388        assert_eq!(
389            bytes,
390            [0x9F, 0x80, 0x03, 0x05, 0x44, 0x55, 0x66, 0x01, 0x02]
391        );
392        assert_eq!(CpResponse::parse(&bytes).unwrap(), r);
393    }
394
395    #[test]
396    fn dispatch_routes_each_tag() {
397        let q = CpQuery {
398            copy_protection_id: 0,
399        }
400        .to_bytes();
401        assert!(matches!(
402            CopyProtectionApdu::parse(&q).unwrap(),
403            CopyProtectionApdu::CpQuery(_)
404        ));
405        let resp = CpResponse {
406            copy_protection_id: 0x1,
407            response_bytes: &[0xFF],
408        }
409        .to_bytes();
410        let parsed = CopyProtectionApdu::parse(&resp).unwrap();
411        assert!(matches!(parsed, CopyProtectionApdu::CpResponse(_)));
412        assert_eq!(parsed.to_bytes(), resp);
413    }
414}