Skip to main content

dvb_ci/ci_plus/
sample_decryption.rs

1//! Sample decryption resource objects — ETSI TS 103 205 V1.4.1 §7.4, Tables
2//! 30-39 (PDF pp. 52-60). See `docs/ts_103_205/sample-decryption.md`.
3//!
4//! Resource ID `0x00920041` (Class 146, Type 1, Version 1). The resource controls
5//! decryption by the CICAM of a set of consecutive media Samples packaged into an
6//! MPEG-2 TS (the IP-delivery Host-player mode). Tags live in the CI Plus
7//! `0x9F98xx` namespace.
8//!
9//! - `sd_info_req` (`9F 98 00`, Table 31) — Host → CICAM, header-only.
10//! - `sd_info_reply` (`9F 98 01`, Table 32) — CICAM → Host.
11//! - `sd_start` (`9F 98 02`, Table 33) — Host → CICAM.
12//! - `sd_start_reply` (`9F 98 03`, Table 35) — CICAM → Host.
13//! - `sd_update` (`9F 98 04`, Table 38) — Host → CICAM.
14//! - `sd_update_reply` (`9F 98 05`, Table 39) — CICAM → Host.
15//!
16//! The `drm_metadata_byte` bodies (pssh/sinf/CASD/MPD/OSDT blobs, Table 34) are
17//! **opaque** to the wire parser — carried as borrowed `&[u8]`.
18
19use crate::error::{Error, Result};
20use crate::objects;
21use crate::tag::ApduTag;
22use alloc::vec::Vec;
23use dvb_common::{Parse, Serialize};
24
25/// Resource-scoped `apdu_tag`s for the Sample decryption resource (Table 30).
26pub mod tag {
27    use crate::tag::ApduTag;
28    /// `sd_info_req_tag` = `9F 98 00`.
29    pub const SD_INFO_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x98, 0x00);
30    /// `sd_info_reply_tag` = `9F 98 01`.
31    pub const SD_INFO_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0x98, 0x01);
32    /// `sd_start_tag` = `9F 98 02`.
33    pub const SD_START: ApduTag = ApduTag::from_bytes(0x9F, 0x98, 0x02);
34    /// `sd_start_reply_tag` = `9F 98 03`.
35    pub const SD_START_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0x98, 0x03);
36    /// `sd_update_tag` = `9F 98 04`.
37    pub const SD_UPDATE: ApduTag = ApduTag::from_bytes(0x9F, 0x98, 0x04);
38    /// `sd_update_reply_tag` = `9F 98 05`.
39    pub const SD_UPDATE_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0x98, 0x05);
40}
41
42/// A 128-bit (16-byte) DRM UUID (`drm_uuid`). All-`0xFF` means "not used".
43pub const DRM_UUID_LEN: usize = 16;
44
45// --- sd_info_req (Table 31) ---
46
47/// `sd_info_req()` (Table 31): Host → CICAM, header-only.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize))]
50pub struct SdInfoReq;
51
52impl<'a> Parse<'a> for SdInfoReq {
53    type Error = Error;
54    fn parse(bytes: &'a [u8]) -> Result<Self> {
55        objects::parse_empty_apdu(bytes, tag::SD_INFO_REQ, "sd_info_req")?;
56        Ok(Self)
57    }
58}
59impl Serialize for SdInfoReq {
60    type Error = Error;
61    fn serialized_len(&self) -> usize {
62        objects::empty_apdu_len()
63    }
64    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
65        objects::serialize_empty_apdu(tag::SD_INFO_REQ, buf)
66    }
67}
68
69// --- sd_info_reply (Table 32) ---
70
71/// `sd_info_reply()` (Table 32): CICAM → Host. Lists `drm_system_id`s and DRM
72/// UUIDs the CICAM supports for Sample decryption.
73#[derive(Debug, Clone, PartialEq, Eq)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize))]
75pub struct SdInfoReply {
76    /// `drm_system_id` list (loop count `number_of_drm_system_id`). Values are the
77    /// same as `ca_system_id` per the DVB allocation \[11\].
78    pub drm_system_ids: Vec<u16>,
79    /// `drm_uuid` list (loop count `number_of_drm_uuid`), each 128 bits.
80    pub drm_uuids: Vec<[u8; DRM_UUID_LEN]>,
81}
82
83impl<'a> Parse<'a> for SdInfoReply {
84    type Error = Error;
85    fn parse(bytes: &'a [u8]) -> Result<Self> {
86        let body = objects::parse_apdu_header(bytes, tag::SD_INFO_REPLY, "sd_info_reply")?;
87        let mut r = Reader::new(body, "sd_info_reply");
88        let n_sys = r.u8()? as usize;
89        let mut drm_system_ids = Vec::with_capacity(n_sys);
90        for _ in 0..n_sys {
91            drm_system_ids.push(r.u16()?);
92        }
93        let n_uuid = r.u8()? as usize;
94        let mut drm_uuids = Vec::with_capacity(n_uuid);
95        for _ in 0..n_uuid {
96            drm_uuids.push(r.uuid()?);
97        }
98        Ok(Self {
99            drm_system_ids,
100            drm_uuids,
101        })
102    }
103}
104impl Serialize for SdInfoReply {
105    type Error = Error;
106    fn serialized_len(&self) -> usize {
107        objects::apdu_len(
108            1 + self.drm_system_ids.len() * 2 + 1 + self.drm_uuids.len() * DRM_UUID_LEN,
109        )
110    }
111    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
112        let body_len = 1 + self.drm_system_ids.len() * 2 + 1 + self.drm_uuids.len() * DRM_UUID_LEN;
113        let pos = objects::write_apdu_header(tag::SD_INFO_REPLY, body_len, buf)?;
114        let mut w = Writer::new(&mut buf[pos..]);
115        w.u8(self.drm_system_ids.len() as u8);
116        for id in &self.drm_system_ids {
117            w.u16(*id);
118        }
119        w.u8(self.drm_uuids.len() as u8);
120        for uuid in &self.drm_uuids {
121            w.uuid(uuid);
122        }
123        Ok(pos + body_len)
124    }
125}
126
127// --- DRM metadata record (shared by sd_start / sd_update, Table 33/34/38) ---
128
129/// One `drm_metadata` record (Tables 33/38): the `drm_metadata_source`,
130/// `drm_system_id`, `drm_uuid`, and the opaque `drm_metadata_byte` body.
131#[derive(Debug, Clone, PartialEq, Eq)]
132#[cfg_attr(feature = "serde", derive(serde::Serialize))]
133pub struct DrmMetadataRecord<'a> {
134    /// `drm_metadata_source` (8) — source of the metadata, per Table 34.
135    pub drm_metadata_source: u8,
136    /// `drm_system_id` (16) — DRM system the metadata relates to; `0xFFFF` if not
137    /// used. Same values as `ca_system_id` \[11\].
138    pub drm_system_id: u16,
139    /// `drm_uuid` (128) — UUID of the DRM; all `0xFF` if not used.
140    pub drm_uuid: [u8; DRM_UUID_LEN],
141    /// `drm_metadata_byte` body (`drm_metadata_length` bytes) — opaque blob.
142    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
143    pub drm_metadata: &'a [u8],
144}
145
146// drm_metadata_source(1) + drm_system_id(2) + drm_uuid(16) + drm_metadata_length(2).
147const METADATA_FIXED: usize = 1 + 2 + DRM_UUID_LEN + 2;
148
149impl<'a> DrmMetadataRecord<'a> {
150    fn parse_from(r: &mut Reader<'a>) -> Result<Self> {
151        let drm_metadata_source = r.u8()?;
152        let drm_system_id = r.u16()?;
153        let drm_uuid = r.uuid()?;
154        let len = r.u16()? as usize;
155        let drm_metadata = r.take(len)?;
156        Ok(Self {
157            drm_metadata_source,
158            drm_system_id,
159            drm_uuid,
160            drm_metadata,
161        })
162    }
163    fn body_len(&self) -> usize {
164        METADATA_FIXED + self.drm_metadata.len()
165    }
166    fn write_into(&self, w: &mut Writer<'_>) {
167        w.u8(self.drm_metadata_source);
168        w.u16(self.drm_system_id);
169        w.uuid(&self.drm_uuid);
170        w.u16(self.drm_metadata.len() as u16);
171        w.bytes(self.drm_metadata);
172    }
173}
174
175/// One Sample Track entry (the `ts_flag == 0` branch of Tables 33/38):
176/// `track_PID` + a `number_of_metadata_records` loop of [`DrmMetadataRecord`].
177#[derive(Debug, Clone, PartialEq, Eq)]
178#[cfg_attr(feature = "serde", derive(serde::Serialize))]
179pub struct SampleTrack<'a> {
180    /// `track_PID` (13) — PID on which the Samples for this Sample Track are sent.
181    /// Values `0x0000`–`0x001F` are reserved.
182    pub track_pid: u16,
183    /// The DRM metadata records for this track.
184    #[cfg_attr(feature = "serde", serde(borrow))]
185    pub records: Vec<DrmMetadataRecord<'a>>,
186}
187
188// reserved(3)+track_PID(13) = 2 bytes, then number_of_metadata_records(1).
189const TRACK_FIXED: usize = 2 + 1;
190const TRACK_PID_MASK: u16 = 0x1FFF;
191
192/// The Sample-decryption payload that follows the per-LTS fixed header in both
193/// `sd_start` (Table 33) and `sd_update` (Table 38): selected by `ts_flag`.
194#[derive(Debug, Clone, PartialEq, Eq)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize))]
196pub enum SamplePayload<'a> {
197    /// `ts_flag == 1` — a TS-level metadata-record loop
198    /// (`number_of_metadata_records`).
199    Ts(#[cfg_attr(feature = "serde", serde(borrow))] Vec<DrmMetadataRecord<'a>>),
200    /// `ts_flag == 0` — a `number_of_Sample_Tracks` loop, each a
201    /// [`SampleTrack`].
202    Tracks(#[cfg_attr(feature = "serde", serde(borrow))] Vec<SampleTrack<'a>>),
203}
204
205impl<'a> SamplePayload<'a> {
206    /// `true` if this is the TS-level (`ts_flag == 1`) variant.
207    #[must_use]
208    pub fn ts_flag(&self) -> bool {
209        matches!(self, Self::Ts(_))
210    }
211
212    fn parse_from(r: &mut Reader<'a>, ts_flag: bool) -> Result<Self> {
213        if ts_flag {
214            let n = r.u8()? as usize;
215            let mut records = Vec::with_capacity(n);
216            for _ in 0..n {
217                records.push(DrmMetadataRecord::parse_from(r)?);
218            }
219            Ok(Self::Ts(records))
220        } else {
221            let n = r.u8()? as usize;
222            let mut tracks = Vec::with_capacity(n);
223            for _ in 0..n {
224                let pid_word = r.u16()?;
225                let track_pid = pid_word & TRACK_PID_MASK;
226                let m = r.u8()? as usize;
227                let mut records = Vec::with_capacity(m);
228                for _ in 0..m {
229                    records.push(DrmMetadataRecord::parse_from(r)?);
230                }
231                tracks.push(SampleTrack { track_pid, records });
232            }
233            Ok(Self::Tracks(tracks))
234        }
235    }
236
237    fn body_len(&self) -> usize {
238        match self {
239            Self::Ts(records) => {
240                1 + records
241                    .iter()
242                    .map(DrmMetadataRecord::body_len)
243                    .sum::<usize>()
244            }
245            Self::Tracks(tracks) => {
246                1 + tracks
247                    .iter()
248                    .map(|t| {
249                        TRACK_FIXED
250                            + t.records
251                                .iter()
252                                .map(DrmMetadataRecord::body_len)
253                                .sum::<usize>()
254                    })
255                    .sum::<usize>()
256            }
257        }
258    }
259
260    fn write_into(&self, w: &mut Writer<'_>) {
261        match self {
262            Self::Ts(records) => {
263                w.u8(records.len() as u8);
264                for rec in records {
265                    rec.write_into(w);
266                }
267            }
268            Self::Tracks(tracks) => {
269                w.u8(tracks.len() as u8);
270                for track in tracks {
271                    // reserved(3)='000' + track_PID(13).
272                    w.u16(track.track_pid & TRACK_PID_MASK);
273                    w.u8(track.records.len() as u8);
274                    for rec in &track.records {
275                        rec.write_into(w);
276                    }
277                }
278            }
279        }
280    }
281}
282
283// --- sd_start (Table 33) ---
284
285/// `sd_start()` (Table 33): Host → CICAM.
286#[derive(Debug, Clone, PartialEq, Eq)]
287#[cfg_attr(feature = "serde", derive(serde::Serialize))]
288pub struct SdStart<'a> {
289    /// `LTS_id` (8) — identifier of the Local TS.
290    pub lts_id: u8,
291    /// `program_number` (16) — used by the CICAM in URI messages.
292    pub program_number: u16,
293    /// The `ts_flag`-selected payload.
294    #[cfg_attr(feature = "serde", serde(borrow))]
295    pub payload: SamplePayload<'a>,
296}
297
298// LTS_id(1) + program_number(2) + reserved(7)+ts_flag(1) byte.
299const SD_START_FIXED: usize = 1 + 2 + 1;
300// ts_flag is the LSB of the reserved(7)+ts_flag(1) byte.
301const TS_FLAG_BIT: u8 = 0x01;
302
303impl<'a> Parse<'a> for SdStart<'a> {
304    type Error = Error;
305    fn parse(bytes: &'a [u8]) -> Result<Self> {
306        let body = objects::parse_apdu_header(bytes, tag::SD_START, "sd_start")?;
307        let mut r = Reader::new(body, "sd_start");
308        let lts_id = r.u8()?;
309        let program_number = r.u16()?;
310        let ts_flag = r.u8()? & TS_FLAG_BIT != 0;
311        let payload = SamplePayload::parse_from(&mut r, ts_flag)?;
312        Ok(Self {
313            lts_id,
314            program_number,
315            payload,
316        })
317    }
318}
319impl Serialize for SdStart<'_> {
320    type Error = Error;
321    fn serialized_len(&self) -> usize {
322        objects::apdu_len(SD_START_FIXED + self.payload.body_len())
323    }
324    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
325        let body_len = SD_START_FIXED + self.payload.body_len();
326        let pos = objects::write_apdu_header(tag::SD_START, body_len, buf)?;
327        let mut w = Writer::new(&mut buf[pos..]);
328        w.u8(self.lts_id);
329        w.u16(self.program_number);
330        w.u8(if self.payload.ts_flag() {
331            TS_FLAG_BIT
332        } else {
333            0
334        });
335        self.payload.write_into(&mut w);
336        Ok(pos + body_len)
337    }
338}
339
340// --- sd_start_reply (Table 35) ---
341
342/// `transmission_status` values (Table 36).
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
344#[cfg_attr(feature = "serde", derive(serde::Serialize))]
345#[non_exhaustive]
346pub enum TransmissionStatus {
347    /// `0x00` — Ready to receive.
348    ReadyToReceive,
349    /// `0x01` — Error: CICAM busy.
350    CicamBusy,
351    /// `0x02` — Error: other reason.
352    OtherReason,
353    /// Reserved (`0x03`–`0xFF`).
354    Reserved(u8),
355}
356impl TransmissionStatus {
357    /// Decode a `transmission_status` byte.
358    #[must_use]
359    pub fn from_u8(v: u8) -> Self {
360        match v {
361            0x00 => Self::ReadyToReceive,
362            0x01 => Self::CicamBusy,
363            0x02 => Self::OtherReason,
364            other => Self::Reserved(other),
365        }
366    }
367    /// Wire byte.
368    #[must_use]
369    pub fn to_u8(self) -> u8 {
370        match self {
371            Self::ReadyToReceive => 0x00,
372            Self::CicamBusy => 0x01,
373            Self::OtherReason => 0x02,
374            Self::Reserved(v) => v,
375        }
376    }
377    /// Spec token, or `"reserved"`.
378    #[must_use]
379    pub fn name(&self) -> &'static str {
380        match self {
381            Self::ReadyToReceive => "ready_to_receive",
382            Self::CicamBusy => "error_cicam_busy",
383            Self::OtherReason => "error_other_reason",
384            Self::Reserved(_) => "reserved",
385        }
386    }
387}
388dvb_common::impl_spec_display!(TransmissionStatus, Reserved);
389
390/// `drm_status` values (Table 37).
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
392#[cfg_attr(feature = "serde", derive(serde::Serialize))]
393#[non_exhaustive]
394pub enum DrmStatus {
395    /// `0x00` — Decryption possible.
396    DecryptionPossible,
397    /// `0x01` — Status currently undetermined.
398    Undetermined,
399    /// `0x02` — Error: no entitlement.
400    NoEntitlement,
401    /// Reserved (`0x03`–`0xFF`).
402    Reserved(u8),
403}
404impl DrmStatus {
405    /// Decode a `drm_status` byte.
406    #[must_use]
407    pub fn from_u8(v: u8) -> Self {
408        match v {
409            0x00 => Self::DecryptionPossible,
410            0x01 => Self::Undetermined,
411            0x02 => Self::NoEntitlement,
412            other => Self::Reserved(other),
413        }
414    }
415    /// Wire byte.
416    #[must_use]
417    pub fn to_u8(self) -> u8 {
418        match self {
419            Self::DecryptionPossible => 0x00,
420            Self::Undetermined => 0x01,
421            Self::NoEntitlement => 0x02,
422            Self::Reserved(v) => v,
423        }
424    }
425    /// Spec token, or `"reserved"`.
426    #[must_use]
427    pub fn name(&self) -> &'static str {
428        match self {
429            Self::DecryptionPossible => "decryption_possible",
430            Self::Undetermined => "status_undetermined",
431            Self::NoEntitlement => "error_no_entitlement",
432            Self::Reserved(_) => "reserved",
433        }
434    }
435}
436dvb_common::impl_spec_display!(DrmStatus, Reserved);
437
438/// `sd_start_reply()` (Table 35): CICAM → Host.
439#[derive(Debug, Clone, Copy, PartialEq, Eq)]
440#[cfg_attr(feature = "serde", derive(serde::Serialize))]
441pub struct SdStartReply {
442    /// `LTS_id` (8).
443    pub lts_id: u8,
444    /// `transmission_status` (8) — Table 36.
445    pub transmission_status: TransmissionStatus,
446    /// `drm_status` (8) — Table 37.
447    pub drm_status: DrmStatus,
448    /// `drm_system_id` (16) — `0xFFFF` if not used.
449    pub drm_system_id: u16,
450    /// `drm_uuid` (128) — all `0xFF` if not used.
451    pub drm_uuid: [u8; DRM_UUID_LEN],
452    /// `buffer_size` (16) — CICAM buffer in transport packets (min 5000).
453    pub buffer_size: u16,
454    /// `data_block_size` (16) — `0` = no transfer-size constraints.
455    pub data_block_size: u16,
456}
457
458// LTS_id(1)+transmission_status(1)+drm_status(1)+drm_system_id(2)+drm_uuid(16)
459//   +buffer_size(2)+data_block_size(2).
460const SD_START_REPLY_BODY: usize = 1 + 1 + 1 + 2 + DRM_UUID_LEN + 2 + 2;
461
462impl<'a> Parse<'a> for SdStartReply {
463    type Error = Error;
464    fn parse(bytes: &'a [u8]) -> Result<Self> {
465        let body = objects::parse_apdu_header(bytes, tag::SD_START_REPLY, "sd_start_reply")?;
466        if body.len() < SD_START_REPLY_BODY {
467            return Err(Error::BufferTooShort {
468                need: SD_START_REPLY_BODY,
469                have: body.len(),
470                what: "sd_start_reply",
471            });
472        }
473        let mut r = Reader::new(body, "sd_start_reply");
474        Ok(Self {
475            lts_id: r.u8()?,
476            transmission_status: TransmissionStatus::from_u8(r.u8()?),
477            drm_status: DrmStatus::from_u8(r.u8()?),
478            drm_system_id: r.u16()?,
479            drm_uuid: r.uuid()?,
480            buffer_size: r.u16()?,
481            data_block_size: r.u16()?,
482        })
483    }
484}
485impl Serialize for SdStartReply {
486    type Error = Error;
487    fn serialized_len(&self) -> usize {
488        objects::apdu_len(SD_START_REPLY_BODY)
489    }
490    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
491        let pos = objects::write_apdu_header(tag::SD_START_REPLY, SD_START_REPLY_BODY, buf)?;
492        let mut w = Writer::new(&mut buf[pos..]);
493        w.u8(self.lts_id);
494        w.u8(self.transmission_status.to_u8());
495        w.u8(self.drm_status.to_u8());
496        w.u16(self.drm_system_id);
497        w.uuid(&self.drm_uuid);
498        w.u16(self.buffer_size);
499        w.u16(self.data_block_size);
500        Ok(pos + SD_START_REPLY_BODY)
501    }
502}
503
504// --- sd_update (Table 38) ---
505
506/// `sd_update()` (Table 38): Host → CICAM. Same `ts_flag`-selected payload shape
507/// as [`SdStart`], but the per-LTS header has no `program_number`.
508#[derive(Debug, Clone, PartialEq, Eq)]
509#[cfg_attr(feature = "serde", derive(serde::Serialize))]
510pub struct SdUpdate<'a> {
511    /// `LTS_id` (8) — Local TS for which the update applies.
512    pub lts_id: u8,
513    /// The `ts_flag`-selected payload (shall match the `sd_start` `ts_flag`).
514    #[cfg_attr(feature = "serde", serde(borrow))]
515    pub payload: SamplePayload<'a>,
516}
517
518// LTS_id(1) + reserved(7)+ts_flag(1) byte.
519const SD_UPDATE_FIXED: usize = 1 + 1;
520
521impl<'a> Parse<'a> for SdUpdate<'a> {
522    type Error = Error;
523    fn parse(bytes: &'a [u8]) -> Result<Self> {
524        let body = objects::parse_apdu_header(bytes, tag::SD_UPDATE, "sd_update")?;
525        let mut r = Reader::new(body, "sd_update");
526        let lts_id = r.u8()?;
527        let ts_flag = r.u8()? & TS_FLAG_BIT != 0;
528        let payload = SamplePayload::parse_from(&mut r, ts_flag)?;
529        Ok(Self { lts_id, payload })
530    }
531}
532impl Serialize for SdUpdate<'_> {
533    type Error = Error;
534    fn serialized_len(&self) -> usize {
535        objects::apdu_len(SD_UPDATE_FIXED + self.payload.body_len())
536    }
537    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
538        let body_len = SD_UPDATE_FIXED + self.payload.body_len();
539        let pos = objects::write_apdu_header(tag::SD_UPDATE, body_len, buf)?;
540        let mut w = Writer::new(&mut buf[pos..]);
541        w.u8(self.lts_id);
542        w.u8(if self.payload.ts_flag() {
543            TS_FLAG_BIT
544        } else {
545            0
546        });
547        self.payload.write_into(&mut w);
548        Ok(pos + body_len)
549    }
550}
551
552// --- sd_update_reply (Table 39) ---
553
554/// `sd_update_reply()` (Table 39): CICAM → Host.
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556#[cfg_attr(feature = "serde", derive(serde::Serialize))]
557pub struct SdUpdateReply {
558    /// `LTS_id` (8).
559    pub lts_id: u8,
560    /// `drm_status` (8) — Table 37.
561    pub drm_status: DrmStatus,
562}
563
564// LTS_id(1) + drm_status(1).
565const SD_UPDATE_REPLY_BODY: usize = 2;
566
567impl<'a> Parse<'a> for SdUpdateReply {
568    type Error = Error;
569    fn parse(bytes: &'a [u8]) -> Result<Self> {
570        let body = objects::parse_apdu_header(bytes, tag::SD_UPDATE_REPLY, "sd_update_reply")?;
571        if body.len() < SD_UPDATE_REPLY_BODY {
572            return Err(Error::BufferTooShort {
573                need: SD_UPDATE_REPLY_BODY,
574                have: body.len(),
575                what: "sd_update_reply",
576            });
577        }
578        Ok(Self {
579            lts_id: body[0],
580            drm_status: DrmStatus::from_u8(body[1]),
581        })
582    }
583}
584impl Serialize for SdUpdateReply {
585    type Error = Error;
586    fn serialized_len(&self) -> usize {
587        objects::apdu_len(SD_UPDATE_REPLY_BODY)
588    }
589    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
590        let pos = objects::write_apdu_header(tag::SD_UPDATE_REPLY, SD_UPDATE_REPLY_BODY, buf)?;
591        buf[pos] = self.lts_id;
592        buf[pos + 1] = self.drm_status.to_u8();
593        Ok(pos + SD_UPDATE_REPLY_BODY)
594    }
595}
596
597/// Resource-scoped dispatch over the Sample decryption resource objects.
598#[derive(Debug, Clone, PartialEq, Eq)]
599#[cfg_attr(feature = "serde", derive(serde::Serialize))]
600#[non_exhaustive]
601pub enum SampleDecryptionApdu<'a> {
602    /// `sd_info_req` (`9F 98 00`).
603    SdInfoReq(SdInfoReq),
604    /// `sd_info_reply` (`9F 98 01`).
605    SdInfoReply(SdInfoReply),
606    /// `sd_start` (`9F 98 02`).
607    SdStart(#[cfg_attr(feature = "serde", serde(borrow))] SdStart<'a>),
608    /// `sd_start_reply` (`9F 98 03`).
609    SdStartReply(SdStartReply),
610    /// `sd_update` (`9F 98 04`).
611    SdUpdate(#[cfg_attr(feature = "serde", serde(borrow))] SdUpdate<'a>),
612    /// `sd_update_reply` (`9F 98 05`).
613    SdUpdateReply(SdUpdateReply),
614}
615
616impl<'a> SampleDecryptionApdu<'a> {
617    /// Parse a Sample decryption APDU, dispatching on the leading `apdu_tag`.
618    pub fn parse(body: &'a [u8]) -> Result<Self> {
619        if body.len() < 3 {
620            return Err(Error::BufferTooShort {
621                need: 3,
622                have: body.len(),
623                what: "sample_decryption apdu_tag",
624            });
625        }
626        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
627        match t {
628            tag::SD_INFO_REQ => Ok(Self::SdInfoReq(SdInfoReq::parse(body)?)),
629            tag::SD_INFO_REPLY => Ok(Self::SdInfoReply(SdInfoReply::parse(body)?)),
630            tag::SD_START => Ok(Self::SdStart(SdStart::parse(body)?)),
631            tag::SD_START_REPLY => Ok(Self::SdStartReply(SdStartReply::parse(body)?)),
632            tag::SD_UPDATE => Ok(Self::SdUpdate(SdUpdate::parse(body)?)),
633            tag::SD_UPDATE_REPLY => Ok(Self::SdUpdateReply(SdUpdateReply::parse(body)?)),
634            _ => Err(Error::UnexpectedApduTag {
635                got: t.as_u24(),
636                expected: tag::SD_INFO_REQ.as_u24(),
637                what: "sample_decryption",
638            }),
639        }
640    }
641}
642
643impl Serialize for SampleDecryptionApdu<'_> {
644    type Error = Error;
645    fn serialized_len(&self) -> usize {
646        match self {
647            Self::SdInfoReq(o) => o.serialized_len(),
648            Self::SdInfoReply(o) => o.serialized_len(),
649            Self::SdStart(o) => o.serialized_len(),
650            Self::SdStartReply(o) => o.serialized_len(),
651            Self::SdUpdate(o) => o.serialized_len(),
652            Self::SdUpdateReply(o) => o.serialized_len(),
653        }
654    }
655    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
656        match self {
657            Self::SdInfoReq(o) => o.serialize_into(buf),
658            Self::SdInfoReply(o) => o.serialize_into(buf),
659            Self::SdStart(o) => o.serialize_into(buf),
660            Self::SdStartReply(o) => o.serialize_into(buf),
661            Self::SdUpdate(o) => o.serialize_into(buf),
662            Self::SdUpdateReply(o) => o.serialize_into(buf),
663        }
664    }
665}
666
667// --- Small big-endian cursor helpers (parse/serialize without magic offsets) ---
668
669struct Reader<'a> {
670    buf: &'a [u8],
671    pos: usize,
672    what: &'static str,
673}
674impl<'a> Reader<'a> {
675    fn new(buf: &'a [u8], what: &'static str) -> Self {
676        Self { buf, pos: 0, what }
677    }
678    fn take(&mut self, n: usize) -> Result<&'a [u8]> {
679        if self.buf.len() < self.pos + n {
680            return Err(Error::BufferTooShort {
681                need: n,
682                have: self.buf.len().saturating_sub(self.pos),
683                what: self.what,
684            });
685        }
686        let s = &self.buf[self.pos..self.pos + n];
687        self.pos += n;
688        Ok(s)
689    }
690    fn u8(&mut self) -> Result<u8> {
691        Ok(self.take(1)?[0])
692    }
693    fn u16(&mut self) -> Result<u16> {
694        let s = self.take(2)?;
695        Ok(u16::from_be_bytes([s[0], s[1]]))
696    }
697    fn uuid(&mut self) -> Result<[u8; DRM_UUID_LEN]> {
698        let s = self.take(DRM_UUID_LEN)?;
699        let mut u = [0u8; DRM_UUID_LEN];
700        u.copy_from_slice(s);
701        Ok(u)
702    }
703}
704
705struct Writer<'a> {
706    buf: &'a mut [u8],
707    pos: usize,
708}
709impl<'a> Writer<'a> {
710    fn new(buf: &'a mut [u8]) -> Self {
711        Self { buf, pos: 0 }
712    }
713    fn u8(&mut self, v: u8) {
714        self.buf[self.pos] = v;
715        self.pos += 1;
716    }
717    fn u16(&mut self, v: u16) {
718        self.buf[self.pos..self.pos + 2].copy_from_slice(&v.to_be_bytes());
719        self.pos += 2;
720    }
721    fn uuid(&mut self, v: &[u8; DRM_UUID_LEN]) {
722        self.buf[self.pos..self.pos + DRM_UUID_LEN].copy_from_slice(v);
723        self.pos += DRM_UUID_LEN;
724    }
725    fn bytes(&mut self, v: &[u8]) {
726        self.buf[self.pos..self.pos + v.len()].copy_from_slice(v);
727        self.pos += v.len();
728    }
729}
730
731#[cfg(test)]
732mod tests {
733    use super::*;
734
735    const UUID_A: [u8; DRM_UUID_LEN] = [
736        0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
737        0xFF,
738    ];
739    const UUID_B: [u8; DRM_UUID_LEN] = [0xFF; DRM_UUID_LEN];
740
741    #[test]
742    fn sd_info_req_round_trips() {
743        let bytes = SdInfoReq.to_bytes();
744        assert_eq!(bytes, [0x9F, 0x98, 0x00, 0x00]);
745        assert_eq!(SdInfoReq::parse(&bytes).unwrap(), SdInfoReq);
746    }
747
748    #[test]
749    fn sd_info_reply_round_trips_and_bites() {
750        let r = SdInfoReply {
751            drm_system_ids: alloc::vec![0x4AD4, 0x1234],
752            drm_uuids: alloc::vec![UUID_A, UUID_B],
753        };
754        let bytes = r.to_bytes();
755        // body: n_sys(02) 4A D4 12 34, n_uuid(02), 16+16 uuid bytes.
756        // body_len = 1 + 4 + 1 + 32 = 38 = 0x26.
757        assert_eq!(bytes[0..4], [0x9F, 0x98, 0x01, 0x26]);
758        assert_eq!(bytes[4], 0x02);
759        assert_eq!(&bytes[5..9], &[0x4A, 0xD4, 0x12, 0x34]);
760        assert_eq!(bytes[9], 0x02);
761        assert_eq!(&bytes[10..26], &UUID_A);
762        assert_eq!(&bytes[26..42], &UUID_B);
763        assert_eq!(SdInfoReply::parse(&bytes).unwrap(), r);
764        let mut other = r.clone();
765        other.drm_system_ids[0] = 0x0000;
766        assert_ne!(bytes, other.to_bytes());
767    }
768
769    #[test]
770    fn sd_info_reply_empty_loops() {
771        let r = SdInfoReply {
772            drm_system_ids: Vec::new(),
773            drm_uuids: Vec::new(),
774        };
775        let bytes = r.to_bytes();
776        assert_eq!(bytes, [0x9F, 0x98, 0x01, 0x02, 0x00, 0x00]);
777        assert_eq!(SdInfoReply::parse(&bytes).unwrap(), r);
778    }
779
780    #[test]
781    fn sd_start_ts_flag_round_trips_and_bites() {
782        let s = SdStart {
783            lts_id: 0x07,
784            program_number: 0x0042,
785            payload: SamplePayload::Ts(alloc::vec![DrmMetadataRecord {
786                drm_metadata_source: 0x03, // pssh
787                drm_system_id: 0xFFFF,
788                drm_uuid: UUID_A,
789                drm_metadata: &[0xDE, 0xAD, 0xBE, 0xEF],
790            }]),
791        };
792        let bytes = s.to_bytes();
793        // LTS_id(07) prog(00 42) ts_flag-byte(01) n_records(01)
794        //   source(03) sysid(FF FF) uuid(16) len(00 04) data(DE AD BE EF)
795        // body_len = 1+2+1 + 1 + (1+2+16+2+4) = 5 + 26 = 30... let's just check head.
796        assert_eq!(bytes[0..3], [0x9F, 0x98, 0x02]);
797        assert_eq!(bytes[4], 0x07); // LTS_id
798        assert_eq!(&bytes[5..7], &[0x00, 0x42]); // program_number
799        assert_eq!(bytes[7], 0x01); // ts_flag
800        assert_eq!(bytes[8], 0x01); // number_of_metadata_records
801        assert_eq!(bytes[9], 0x03); // drm_metadata_source
802        assert_eq!(&bytes[10..12], &[0xFF, 0xFF]); // drm_system_id
803        assert_eq!(&bytes[12..28], &UUID_A); // drm_uuid
804        assert_eq!(&bytes[28..30], &[0x00, 0x04]); // drm_metadata_length
805        assert_eq!(&bytes[30..34], &[0xDE, 0xAD, 0xBE, 0xEF]);
806        assert_eq!(SdStart::parse(&bytes).unwrap(), s);
807        // Mutation: flip a metadata byte.
808        let mut other = s.clone();
809        other.payload = SamplePayload::Ts(alloc::vec![DrmMetadataRecord {
810            drm_metadata_source: 0x03,
811            drm_system_id: 0xFFFF,
812            drm_uuid: UUID_A,
813            drm_metadata: &[0xDE, 0xAD, 0xBE, 0x00],
814        }]);
815        assert_ne!(bytes, other.to_bytes());
816    }
817
818    #[test]
819    fn sd_start_tracks_two_tracks() {
820        let s = SdStart {
821            lts_id: 0x01,
822            program_number: 0x1000,
823            payload: SamplePayload::Tracks(alloc::vec![
824                SampleTrack {
825                    track_pid: 0x0100,
826                    records: alloc::vec![DrmMetadataRecord {
827                        drm_metadata_source: 0x01,
828                        drm_system_id: 0x4AD4,
829                        drm_uuid: UUID_A,
830                        drm_metadata: &[0x01, 0x02],
831                    }],
832                },
833                SampleTrack {
834                    track_pid: 0x0101,
835                    records: Vec::new(),
836                },
837            ]),
838        };
839        let bytes = s.to_bytes();
840        assert_eq!(bytes[7], 0x00); // ts_flag == 0
841        assert_eq!(bytes[8], 0x02); // number_of_Sample_Tracks
842                                    // track0: reserved(3)+PID 0x0100 => 0x01 0x00, n_records=01.
843        assert_eq!(&bytes[9..11], &[0x01, 0x00]);
844        assert_eq!(bytes[11], 0x01);
845        assert_eq!(SdStart::parse(&bytes).unwrap(), s);
846        // 13-bit PID masking: top 3 bits ignored on the wire.
847        let parsed = SdStart::parse(&bytes).unwrap();
848        if let SamplePayload::Tracks(t) = &parsed.payload {
849            assert_eq!(t[0].track_pid, 0x0100);
850            assert_eq!(t[1].track_pid, 0x0101);
851            assert_eq!(t.len(), 2);
852        } else {
853            panic!("expected Tracks");
854        }
855    }
856
857    #[test]
858    fn sd_start_reply_round_trips_and_bites() {
859        let r = SdStartReply {
860            lts_id: 0x05,
861            transmission_status: TransmissionStatus::ReadyToReceive,
862            drm_status: DrmStatus::DecryptionPossible,
863            drm_system_id: 0x4AD4,
864            drm_uuid: UUID_A,
865            buffer_size: 5000,
866            data_block_size: 0,
867        };
868        let bytes = r.to_bytes();
869        // body_len = 1+1+1+2+16+2+2 = 25 = 0x19.
870        assert_eq!(bytes[0..4], [0x9F, 0x98, 0x03, 0x19]);
871        assert_eq!(bytes[4], 0x05); // LTS_id
872        assert_eq!(bytes[5], 0x00); // transmission_status
873        assert_eq!(bytes[6], 0x00); // drm_status
874        assert_eq!(&bytes[7..9], &[0x4A, 0xD4]);
875        assert_eq!(&bytes[9..25], &UUID_A);
876        assert_eq!(&bytes[25..27], &5000u16.to_be_bytes());
877        assert_eq!(&bytes[27..29], &[0x00, 0x00]);
878        assert_eq!(SdStartReply::parse(&bytes).unwrap(), r);
879        let mut other = r;
880        other.drm_status = DrmStatus::NoEntitlement;
881        assert_eq!(other.to_bytes()[6], 0x02);
882        assert_ne!(bytes, other.to_bytes());
883    }
884
885    #[test]
886    fn sd_update_round_trips() {
887        let u = SdUpdate {
888            lts_id: 0x09,
889            payload: SamplePayload::Tracks(alloc::vec![
890                SampleTrack {
891                    track_pid: 0x0200,
892                    records: alloc::vec![DrmMetadataRecord {
893                        drm_metadata_source: 0x05,
894                        drm_system_id: 0xFFFF,
895                        drm_uuid: UUID_B,
896                        drm_metadata: &[],
897                    }],
898                },
899                SampleTrack {
900                    track_pid: 0x0201,
901                    records: Vec::new(),
902                },
903            ]),
904        };
905        let bytes = u.to_bytes();
906        assert_eq!(bytes[0..3], [0x9F, 0x98, 0x04]);
907        assert_eq!(bytes[4], 0x09); // LTS_id
908        assert_eq!(bytes[5], 0x00); // ts_flag
909        assert_eq!(bytes[6], 0x02); // number_of_Sample_Tracks
910        assert_eq!(SdUpdate::parse(&bytes).unwrap(), u);
911    }
912
913    #[test]
914    fn sd_update_reply_round_trips_and_bites() {
915        let r = SdUpdateReply {
916            lts_id: 0x03,
917            drm_status: DrmStatus::Undetermined,
918        };
919        let bytes = r.to_bytes();
920        assert_eq!(bytes, [0x9F, 0x98, 0x05, 0x02, 0x03, 0x01]);
921        assert_eq!(SdUpdateReply::parse(&bytes).unwrap(), r);
922        let other = SdUpdateReply {
923            lts_id: 0x03,
924            drm_status: DrmStatus::DecryptionPossible,
925        };
926        assert_ne!(bytes, other.to_bytes());
927    }
928
929    #[test]
930    fn dispatch_routes_each_tag() {
931        let req = SdInfoReq.to_bytes();
932        assert!(matches!(
933            SampleDecryptionApdu::parse(&req).unwrap(),
934            SampleDecryptionApdu::SdInfoReq(_)
935        ));
936        let reply = SdUpdateReply {
937            lts_id: 0,
938            drm_status: DrmStatus::DecryptionPossible,
939        }
940        .to_bytes();
941        let parsed = SampleDecryptionApdu::parse(&reply).unwrap();
942        assert!(matches!(parsed, SampleDecryptionApdu::SdUpdateReply(_)));
943        assert_eq!(parsed.to_bytes(), reply);
944        assert!(matches!(
945            SampleDecryptionApdu::parse(&[0x9F, 0x98, 0x7E, 0x00]),
946            Err(Error::UnexpectedApduTag { .. })
947        ));
948    }
949}