Skip to main content

dvb_ci/ci_ext/
mod.rs

1//! DVB CI Extensions (ETSI TS 101 699) — the resource-scoped APDU layer.
2//!
3//! Unlike the EN 50221 application objects (which carry globally-unique
4//! `apdu_tag`s and are dispatched by [`crate::AnyApdu`]), the TS 101 699
5//! extension resources **reuse the same `0x9F80xx` tag values across different
6//! resources** — per Table 87 several resources start their objects at
7//! `0x9F8000`. The same three tag bytes therefore mean different objects
8//! depending on which resource's session they arrive on, so they cannot join the
9//! global `AnyApdu`. This module provides a *resource-scoped* dispatch
10//! ([`CiExtApdu`]): parsing is keyed on the `resource_identifier()` first, then
11//! the leading 24-bit `apdu_tag` selects the object within that resource.
12//!
13//! Each `apdu_tag` const lives in its resource module's `tag` submodule, so the
14//! colliding values are namespaced per resource (`power_manager::tag` vs
15//! `copy_protection::tag` both define a `0x9F8000`).
16//!
17//! Spec: ETSI TS 101 699 V1.1.1 §8, Table 87 (resource IDs + tags) — see
18//! `docs/ci_plus/resource-ids.md`. The per-resource layouts are cited in their
19//! own module docs.
20//!
21//! Resources implemented: Resource Manager v2, Application Information v2, Power
22//! Manager, Event Manager, Copy Protection, StreamInput, ServiceGateway (Generic
23//! Service Gateway), BroadcastServiceGateway, Status Query (+ audience metering),
24//! Application MMI, Download (CAM firmware, + DSM-CC U-N messages), CA Pipeline —
25//! the full TS 101 699 §6 resource set.
26
27use crate::error::{Error, Result};
28use crate::resource::ResourceId;
29
30pub mod application_info_v2;
31pub mod application_mmi;
32pub mod broadcast_service_gateway;
33pub mod ca_pipeline;
34pub mod copy_protection;
35pub mod event_manager;
36pub mod power_manager;
37pub mod resource_manager_v2;
38pub mod service_gateway;
39pub mod software_download;
40pub mod status_query;
41pub mod stream_input;
42
43// --- Resource identifiers (TS 101 699 Table 87) ---
44
45/// Resource Manager v2 — class 1, type 1, version 2 (`0x00010042`). Fixed ID.
46pub const RESOURCE_MANAGER_V2: ResourceId = ResourceId(0x0001_0042);
47/// Application Information v2 — class 2, type 1, version 2 (`0x00020042`). Fixed ID.
48pub const APPLICATION_INFO_V2: ResourceId = ResourceId(0x0002_0042);
49/// Power Manager — class 34, type 1, version 1 (`0x00220041`). Fixed ID.
50pub const POWER_MANAGER: ResourceId = ResourceId(0x0022_0041);
51/// Application MMI — class 65, type 1, version 1 (`0x00410041`). Fixed ID.
52pub const APPLICATION_MMI: ResourceId = ResourceId(0x0041_0041);
53/// Download resource — class 5, type 1, version 1 (`0x00051041`). Fixed ID.
54///
55/// (Table 87 prints `0x000510041`, a 9-hex-digit spec typo; the authoritative
56/// binary packs to `0x00051041` — see `docs/ci_plus/resource-ids.md`.)
57pub const DOWNLOAD: ResourceId = ResourceId(0x0005_1041);
58
59/// StreamInput template — class 128, type 1\*, version 1 (`0x00801ii1`, `ii` =
60/// Module ID). Match with [`MODULE_ID_MASK`].
61pub const STREAM_INPUT_TEMPLATE: ResourceId = ResourceId(0x0080_1001);
62/// BroadcastServiceGateway template — class 129, type 1\* (`0x00811ii1`).
63pub const BROADCAST_SERVICE_GATEWAY_TEMPLATE: ResourceId = ResourceId(0x0081_1001);
64/// StatusQuery template — class 33, type 1\* (`0x00211ii1`).
65pub const STATUS_QUERY_TEMPLATE: ResourceId = ResourceId(0x0021_1001);
66/// Event Manager template — class 35, type 1\* (`0x00231ii1`).
67pub const EVENT_MANAGER_TEMPLATE: ResourceId = ResourceId(0x0023_1001);
68/// Copy Protection template — class 4, type 1\* (`0x00041ii1`).
69pub const COPY_PROTECTION_TEMPLATE: ResourceId = ResourceId(0x0004_1001);
70/// CA Pipeline template — class 6, type 1\* (`0x00061ii1`).
71pub const CA_PIPELINE_TEMPLATE: ResourceId = ResourceId(0x0006_1001);
72
73/// Mask that clears the 6-bit Module ID (`ii`) of a `type = 1*` resource ID —
74/// the low 6 bits of the 10-bit `resource_type` field, i.e. bits `[11:6]`
75/// (TS 101 699 §8.1). Applying it to a `1*` resource ID yields its template ID.
76pub const MODULE_ID_MASK: u32 = 0xFFFF_F03F;
77
78/// Extract the 6-bit Module ID (`ii`) from a `type = 1*` resource ID.
79#[must_use]
80pub const fn module_id(id: ResourceId) -> u8 {
81    ((id.0 >> 6) & 0x3F) as u8
82}
83
84/// The DVB CI-extension resource a [`ResourceId`] denotes. `type = 1*` resources
85/// are matched after masking out the Module ID; fixed-ID resources match exactly.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize))]
88#[non_exhaustive]
89pub enum CiExtResource {
90    /// Resource Manager v2 (`0x00010042`).
91    ResourceManagerV2,
92    /// Application Information v2 (`0x00020042`).
93    ApplicationInfoV2,
94    /// Power Manager (`0x00220041`).
95    PowerManager,
96    /// Application MMI (`0x00410041`).
97    ApplicationMmi,
98    /// Download resource (`0x00051041`).
99    Download,
100    /// StreamInput (`0x00801ii1`), carrying its Module ID.
101    StreamInput(u8),
102    /// BroadcastServiceGateway (`0x00811ii1`), carrying its Module ID.
103    BroadcastServiceGateway(u8),
104    /// StatusQuery (`0x00211ii1`), carrying its Module ID.
105    StatusQuery(u8),
106    /// Event Manager (`0x00231ii1`), carrying its Module ID.
107    EventManager(u8),
108    /// Copy Protection (`0x00041ii1`), carrying its Module ID.
109    CopyProtection(u8),
110    /// CA Pipeline (`0x00061ii1`), carrying its Module ID.
111    CaPipeline(u8),
112}
113
114impl CiExtResource {
115    /// Diagnostic spec token.
116    #[must_use]
117    pub fn name(&self) -> &'static str {
118        match self {
119            Self::ResourceManagerV2 => "resource_manager_v2",
120            Self::ApplicationInfoV2 => "application_information_v2",
121            Self::PowerManager => "power_manager",
122            Self::ApplicationMmi => "application_mmi",
123            Self::Download => "download",
124            Self::StreamInput(_) => "stream_input",
125            Self::BroadcastServiceGateway(_) => "broadcast_service_gateway",
126            Self::StatusQuery(_) => "status_query",
127            Self::EventManager(_) => "event_manager",
128            Self::CopyProtection(_) => "copy_protection",
129            Self::CaPipeline(_) => "ca_pipeline",
130        }
131    }
132}
133
134impl core::fmt::Display for CiExtResource {
135    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
136        match self {
137            Self::StreamInput(ii)
138            | Self::BroadcastServiceGateway(ii)
139            | Self::StatusQuery(ii)
140            | Self::EventManager(ii)
141            | Self::CopyProtection(ii)
142            | Self::CaPipeline(ii) => write!(f, "{}(module_id={ii})", self.name()),
143            other => f.write_str(other.name()),
144        }
145    }
146}
147
148/// Map a [`ResourceId`] to its DVB CI-extension resource. `type = 1*` resources
149/// are matched by masking out the Module ID byte; fixed-ID resources match
150/// exactly. Returns `None` for any non-CI-extension resource.
151#[must_use]
152pub fn classify(id: ResourceId) -> Option<CiExtResource> {
153    match id {
154        RESOURCE_MANAGER_V2 => return Some(CiExtResource::ResourceManagerV2),
155        APPLICATION_INFO_V2 => return Some(CiExtResource::ApplicationInfoV2),
156        POWER_MANAGER => return Some(CiExtResource::PowerManager),
157        APPLICATION_MMI => return Some(CiExtResource::ApplicationMmi),
158        DOWNLOAD => return Some(CiExtResource::Download),
159        _ => {}
160    }
161    let ii = module_id(id);
162    match ResourceId(id.0 & MODULE_ID_MASK) {
163        STREAM_INPUT_TEMPLATE => Some(CiExtResource::StreamInput(ii)),
164        BROADCAST_SERVICE_GATEWAY_TEMPLATE => Some(CiExtResource::BroadcastServiceGateway(ii)),
165        STATUS_QUERY_TEMPLATE => Some(CiExtResource::StatusQuery(ii)),
166        EVENT_MANAGER_TEMPLATE => Some(CiExtResource::EventManager(ii)),
167        COPY_PROTECTION_TEMPLATE => Some(CiExtResource::CopyProtection(ii)),
168        CA_PIPELINE_TEMPLATE => Some(CiExtResource::CaPipeline(ii)),
169        _ => None,
170    }
171}
172
173/// A parsed DVB CI-extension APDU, scoped to the resource it arrived on.
174///
175/// One variant per resource implemented this pass; each wraps that resource's
176/// own object enum (which dispatches on the leading `apdu_tag`).
177#[derive(Debug, Clone, PartialEq, Eq)]
178#[cfg_attr(feature = "serde", derive(serde::Serialize))]
179#[non_exhaustive]
180pub enum CiExtApdu<'a> {
181    /// Resource Manager v2 object (`0x00010042`).
182    ResourceManagerV2(resource_manager_v2::ResourceManagerV2Apdu),
183    /// Application Information v2 object (`0x00020042`).
184    ApplicationInfoV2(application_info_v2::ApplicationInfoV2Apdu<'a>),
185    /// Power Manager object (`0x00220041`).
186    PowerManager(power_manager::PowerManagerApdu),
187    /// Event Manager object (`0x00231ii1`).
188    EventManager(event_manager::EventManagerApdu<'a>),
189    /// Copy Protection object (`0x00041ii1`).
190    CopyProtection(copy_protection::CopyProtectionApdu<'a>),
191    /// StreamInput object (`0x00801ii1`).
192    StreamInput(stream_input::StreamInputApdu<'a>),
193    /// Broadcast Service Gateway object (`0x00811ii1`), including the inherited
194    /// Generic Service Gateway calls.
195    BroadcastServiceGateway(broadcast_service_gateway::BroadcastServiceGatewayApdu<'a>),
196    /// Status Query object (`0x00211ii1`).
197    StatusQuery(status_query::StatusQueryApdu<'a>),
198    /// Application MMI object (`0x00410041`).
199    ApplicationMmi(application_mmi::ApplicationMmiApdu<'a>),
200    /// Download (CAM firmware) object (`0x00051041`).
201    Download(software_download::DownloadApdu<'a>),
202    /// CA Pipeline object (`0x00061ii1`).
203    CaPipeline(ca_pipeline::CaPipelineApdu<'a>),
204}
205
206impl<'a> CiExtApdu<'a> {
207    /// Parse a CI-extension APDU, selecting the resource from `resource_id`
208    /// (Module ID masked out for `type = 1*` resources) and then delegating to
209    /// that resource's object dispatch on the leading `apdu_tag`.
210    ///
211    /// Errors with [`Error::UnknownResource`] if `resource_id` is not a
212    /// CI-extension resource handled this pass.
213    pub fn parse(resource_id: ResourceId, body: &'a [u8]) -> Result<Self> {
214        match classify(resource_id) {
215            Some(CiExtResource::ResourceManagerV2) => Ok(Self::ResourceManagerV2(
216                resource_manager_v2::ResourceManagerV2Apdu::parse(body)?,
217            )),
218            Some(CiExtResource::ApplicationInfoV2) => Ok(Self::ApplicationInfoV2(
219                application_info_v2::ApplicationInfoV2Apdu::parse(body)?,
220            )),
221            Some(CiExtResource::PowerManager) => Ok(Self::PowerManager(
222                power_manager::PowerManagerApdu::parse(body)?,
223            )),
224            Some(CiExtResource::EventManager(_)) => Ok(Self::EventManager(
225                event_manager::EventManagerApdu::parse(body)?,
226            )),
227            Some(CiExtResource::CopyProtection(_)) => Ok(Self::CopyProtection(
228                copy_protection::CopyProtectionApdu::parse(body)?,
229            )),
230            Some(CiExtResource::StreamInput(_)) => Ok(Self::StreamInput(
231                stream_input::StreamInputApdu::parse(body)?,
232            )),
233            Some(CiExtResource::BroadcastServiceGateway(_)) => Ok(Self::BroadcastServiceGateway(
234                broadcast_service_gateway::BroadcastServiceGatewayApdu::parse(body)?,
235            )),
236            Some(CiExtResource::StatusQuery(_)) => Ok(Self::StatusQuery(
237                status_query::StatusQueryApdu::parse(body)?,
238            )),
239            Some(CiExtResource::ApplicationMmi) => Ok(Self::ApplicationMmi(
240                application_mmi::ApplicationMmiApdu::parse(body)?,
241            )),
242            Some(CiExtResource::Download) => Ok(Self::Download(
243                software_download::DownloadApdu::parse(body)?,
244            )),
245            Some(CiExtResource::CaPipeline(_)) => {
246                Ok(Self::CaPipeline(ca_pipeline::CaPipelineApdu::parse(body)?))
247            }
248            _ => Err(Error::UnknownResource {
249                resource_id: resource_id.0,
250            }),
251        }
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn classify_fixed_ids() {
261        assert_eq!(
262            classify(RESOURCE_MANAGER_V2),
263            Some(CiExtResource::ResourceManagerV2)
264        );
265        assert_eq!(
266            classify(APPLICATION_INFO_V2),
267            Some(CiExtResource::ApplicationInfoV2)
268        );
269        assert_eq!(classify(POWER_MANAGER), Some(CiExtResource::PowerManager));
270        assert_eq!(classify(ResourceId(0xDEAD_BEEF)), None);
271        // The EN 50221 v1 Resource Manager (0x00010041) is NOT the v2 ID.
272        assert_eq!(classify(ResourceId(0x0001_0041)), None);
273    }
274
275    #[test]
276    fn classify_masks_module_id() {
277        // Copy Protection module_id 3 -> 0x000410C1 (TS 101 699 §6.6.1.1 example).
278        let cp3 = ResourceId(0x0004_10C1);
279        assert_eq!(classify(cp3), Some(CiExtResource::CopyProtection(3)));
280        assert_eq!(module_id(cp3), 3);
281        // Event Manager module_id 1 -> 0x00231041, module_id 2 -> 0x00231081.
282        assert_eq!(
283            classify(ResourceId(0x0023_1041)),
284            Some(CiExtResource::EventManager(1))
285        );
286        assert_eq!(
287            classify(ResourceId(0x0023_1081)),
288            Some(CiExtResource::EventManager(2))
289        );
290        // module_id 0 still classifies.
291        assert_eq!(
292            classify(EVENT_MANAGER_TEMPLATE),
293            Some(CiExtResource::EventManager(0))
294        );
295    }
296
297    #[test]
298    fn same_tag_different_resource_routes_differently() {
299        // 0x9F8000 means different objects under different resources.
300        // Power Manager activation request: tag + len(1) + reserved/state(1).
301        let pm_body = [0x9F, 0x80, 0x00, 0x01, 0x00];
302        // Copy Protection CP_query: tag + len(3) + CopyProtectionID(3).
303        let cp_body = [0x9F, 0x80, 0x00, 0x03, 0xAA, 0xBB, 0xCC];
304        // Event Manager event request: tag + len(1) + event_type(1).
305        let em_body = [0x9F, 0x80, 0x00, 0x01, 0x00];
306
307        let pm = CiExtApdu::parse(POWER_MANAGER, &pm_body).unwrap();
308        assert!(matches!(pm, CiExtApdu::PowerManager(_)));
309
310        let cp = CiExtApdu::parse(ResourceId(0x0004_10C1), &cp_body).unwrap();
311        assert!(matches!(cp, CiExtApdu::CopyProtection(_)));
312
313        let em = CiExtApdu::parse(ResourceId(0x0023_1041), &em_body).unwrap();
314        assert!(matches!(em, CiExtApdu::EventManager(_)));
315    }
316
317    #[test]
318    fn stream_input_and_bsg_classify_and_mask() {
319        // StreamInput type=1*: module_id 1 -> 0x00801041, module_id 5 -> 0x00801141.
320        assert_eq!(
321            classify(ResourceId(0x0080_1041)),
322            Some(CiExtResource::StreamInput(1))
323        );
324        assert_eq!(
325            classify(ResourceId(0x0080_1141)),
326            Some(CiExtResource::StreamInput(5))
327        );
328        assert_eq!(module_id(ResourceId(0x0080_1141)), 5);
329        // BroadcastServiceGateway module_id 1 -> 0x00811041 (md example).
330        assert_eq!(
331            classify(ResourceId(0x0081_1041)),
332            Some(CiExtResource::BroadcastServiceGateway(1))
333        );
334        // module_id 0 templates still classify.
335        assert_eq!(
336            classify(STREAM_INPUT_TEMPLATE),
337            Some(CiExtResource::StreamInput(0))
338        );
339        assert_eq!(
340            classify(BROADCAST_SERVICE_GATEWAY_TEMPLATE),
341            Some(CiExtResource::BroadcastServiceGateway(0))
342        );
343    }
344
345    #[test]
346    fn stream_input_9f8000_vs_power_manager_9f8000() {
347        // The resource-scoped invariant: 9F8000 means different objects per
348        // resource, even with the new resources added.
349        // StreamInput 9F8000 = DeliverySystemInfoReq (header-only).
350        let si_body = [0x9F, 0x80, 0x00, 0x00];
351        let si = CiExtApdu::parse(ResourceId(0x0080_1041), &si_body).unwrap();
352        assert!(matches!(
353            si,
354            CiExtApdu::StreamInput(stream_input::StreamInputApdu::DeliverySystemInfoReq(_))
355        ));
356        // PowerManager 9F8000 = activation_state_change_request (1-byte body).
357        let pm_body = [0x9F, 0x80, 0x00, 0x01, 0x00];
358        let pm = CiExtApdu::parse(POWER_MANAGER, &pm_body).unwrap();
359        assert!(matches!(pm, CiExtApdu::PowerManager(_)));
360        // Same leading tag, different parsed variant => routes by resource.
361        assert!(!matches!(si, CiExtApdu::PowerManager(_)));
362    }
363
364    #[test]
365    fn bsg_dispatch_routes_eit_and_inherited() {
366        let bsg = ResourceId(0x0081_1041);
367        // 9F8010 EITSectionReq (BSG-specific extension).
368        let eit = [
369            0x9F, 0x80, 0x10, 0x08, 0x00, 0x4E, 0x00, 0x64, 0x00, 0x00, 0x01, 0x00,
370        ];
371        let parsed = CiExtApdu::parse(bsg, &eit).unwrap();
372        assert!(matches!(
373            parsed,
374            CiExtApdu::BroadcastServiceGateway(
375                broadcast_service_gateway::BroadcastServiceGatewayApdu::EitSectionReq(_)
376            )
377        ));
378        // 9F8000 inherited Generic Service Gateway ServiceListReq.
379        let slr = [0x9F, 0x80, 0x00, 0x00];
380        let parsed = CiExtApdu::parse(bsg, &slr).unwrap();
381        assert!(matches!(
382            parsed,
383            CiExtApdu::BroadcastServiceGateway(
384                broadcast_service_gateway::BroadcastServiceGatewayApdu::ServiceGateway(_)
385            )
386        ));
387    }
388
389    #[test]
390    fn new_type1_ids_classify_and_mask() {
391        // StatusQuery type=1*: module_id 1 -> 0x00211041, module_id 2 -> 0x00211081.
392        assert_eq!(
393            classify(ResourceId(0x0021_1041)),
394            Some(CiExtResource::StatusQuery(1))
395        );
396        assert_eq!(
397            classify(ResourceId(0x0021_1081)),
398            Some(CiExtResource::StatusQuery(2))
399        );
400        assert_eq!(module_id(ResourceId(0x0021_1081)), 2);
401        // CA Pipeline type=1*: 0x00061041 (module 1), 0x00061081 (module 2) — md examples.
402        assert_eq!(
403            classify(ResourceId(0x0006_1041)),
404            Some(CiExtResource::CaPipeline(1))
405        );
406        assert_eq!(
407            classify(ResourceId(0x0006_1081)),
408            Some(CiExtResource::CaPipeline(2))
409        );
410        // module_id 0 templates still classify.
411        assert_eq!(
412            classify(STATUS_QUERY_TEMPLATE),
413            Some(CiExtResource::StatusQuery(0))
414        );
415        assert_eq!(
416            classify(CA_PIPELINE_TEMPLATE),
417            Some(CiExtResource::CaPipeline(0))
418        );
419        // ApplicationMMI / Download are fixed IDs.
420        assert_eq!(
421            classify(APPLICATION_MMI),
422            Some(CiExtResource::ApplicationMmi)
423        );
424        assert_eq!(classify(DOWNLOAD), Some(CiExtResource::Download));
425    }
426
427    #[test]
428    fn tag_9f8000_routes_per_resource_across_all_resources() {
429        // The resource-scoped invariant with the full resource set present:
430        // 9F8000 means a different object under each resource.
431        let body = [0x9F, 0x80, 0x00, 0x00]; // header-only / empty body
432
433        // StatusQuery 9F8000 = StatusQueryReq (needs a 4-byte StatusItem body).
434        let sq_body = [0x9F, 0x80, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01];
435        let sq = CiExtApdu::parse(ResourceId(0x0021_1041), &sq_body).unwrap();
436        assert!(matches!(
437            sq,
438            CiExtApdu::StatusQuery(status_query::StatusQueryApdu::StatusQueryReq(_))
439        ));
440
441        // ApplicationMMI 9F8000 = RequestStart (2-byte minimum body).
442        let mmi_body = [0x9F, 0x80, 0x00, 0x02, 0x00, 0x00];
443        let mmi = CiExtApdu::parse(APPLICATION_MMI, &mmi_body).unwrap();
444        assert!(matches!(
445            mmi,
446            CiExtApdu::ApplicationMmi(application_mmi::ApplicationMmiApdu::RequestStart(_))
447        ));
448
449        // Download 9F8000 = download_enq (opaque body).
450        let dl = CiExtApdu::parse(DOWNLOAD, &body).unwrap();
451        assert!(matches!(
452            dl,
453            CiExtApdu::Download(software_download::DownloadApdu::DownloadEnquiry(_))
454        ));
455
456        // CA Pipeline 9F8000 = CAPipelineRequest (opaque body).
457        let cap = CiExtApdu::parse(ResourceId(0x0006_1041), &body).unwrap();
458        assert!(matches!(
459            cap,
460            CiExtApdu::CaPipeline(ca_pipeline::CaPipelineApdu::Request(_))
461        ));
462
463        // StreamInput 9F8000 = DeliverySystemInfoReq — an *earlier* resource's 9F8000.
464        let si = CiExtApdu::parse(ResourceId(0x0080_1041), &body).unwrap();
465        assert!(matches!(
466            si,
467            CiExtApdu::StreamInput(stream_input::StreamInputApdu::DeliverySystemInfoReq(_))
468        ));
469
470        // All four new + the earlier one are distinct CiExtApdu variants.
471        assert!(!matches!(sq, CiExtApdu::ApplicationMmi(_)));
472        assert!(!matches!(dl, CiExtApdu::CaPipeline(_)));
473        assert!(!matches!(cap, CiExtApdu::Download(_)));
474    }
475
476    #[test]
477    fn unknown_resource_errors() {
478        let body = [0x9F, 0x80, 0x00, 0x00];
479        assert!(matches!(
480            CiExtApdu::parse(ResourceId(0x1234_5678), &body),
481            Err(Error::UnknownResource { .. })
482        ));
483    }
484
485    #[test]
486    fn display_carries_module_id() {
487        use alloc::format;
488        assert_eq!(
489            format!("{}", CiExtResource::CopyProtection(3)),
490            "copy_protection(module_id=3)"
491        );
492        assert_eq!(format!("{}", CiExtResource::PowerManager), "power_manager");
493    }
494}