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