dvb_ci/ci_plus/mod.rs
1//! CI Plus extensions (ETSI TS 103 205) — the resource-scoped APDU layer.
2//!
3//! Like the TS 101 699 extensions ([`crate::ci_ext`]), the CI Plus resources
4//! defined by TS 103 205 have **their own apdu_tag namespace** that collides
5//! with EN 50221 and TS 101 699 — e.g. the Multi-stream resource's tags live in
6//! `0x9F92xx`, Content Control's in `0x9F90xx`, and the extended CA Support
7//! `ca_pmt`/`ca_pmt_reply` reuse EN 50221's `0x9F8032`/`0x9F8033` verbatim. The
8//! same tag bytes therefore denote different objects depending on which
9//! resource's session they arrive on, so they cannot join the global
10//! [`crate::AnyApdu`]. This module provides a *resource-scoped* dispatch
11//! ([`CiPlusApdu`]): parsing keys on the `resource_identifier()` first
12//! ([`classify`]), then the leading `apdu_tag` selects the object within that
13//! resource.
14//!
15//! Spec: ETSI TS 103 205 V1.4.1 — resource IDs in `docs/ts_103_205/resource-ids.md`;
16//! per-resource layouts cited in their own module docs.
17//!
18//! ## Resources implemented this pass
19//!
20//! - **Multi-stream** (`0x00900041`, [`multistream`]) — `CICAM_multistream_capability`
21//! / `PID_select_req` / `PID_select_reply`.
22//! - **Content Control multi-stream** (`0x008C1041`, [`content_control`]) — the
23//! printed-syntax extended APDUs `cc_PIN_reply` / `cc_PIN_event`, plus the SAC
24//! protocol datatype model.
25//! - **Multi-stream Host Control** (`0x00200081`, base v3 `0x00200043`,
26//! [`multistream_host_control`]) — the tune APDUs `tune_triplet_req` /
27//! `tune_lcn_req` / `tune_ip_req` / `tuner_status_req` / `tuner_status_reply`.
28//! Both resource ids route to the same [`CiPlusResource::MultistreamHostControl`]
29//! kind, carrying the [`multistream_host_control::HostControlMode`] that selects
30//! the `tune_ip_req` reserved-bit budget. `tune_broadcast_req` / `tune_reply` /
31//! `ask_release(_reply)` are deferred to CI Plus V1.3 and not encoded.
32//! - **Sample decryption** (`0x00920041`, [`sample_decryption`]) — `sd_info_req` /
33//! `sd_info_reply` / `sd_start(_reply)` / `sd_update(_reply)`, with opaque DRM
34//! metadata / UUID payloads.
35//! - **CICAM Player** (`0x00930041`, [`cicam_player`]) — the full 16-APDU set
36//! `9FA000`–`9FA00F` (`CICAM_player_verify_req` … `CICAM_player_update_reply`).
37//! - **Auxiliary File System / file retrieval** (`0x00910041`, [`file_retrieval`])
38//! — `FileSystemOffer` / `FileSystemAck`, plus `FileRequest` / `FileAcknowledge`
39//! whose bodies defer to CI Plus V1.3 (carried opaquely; tags only established).
40//!
41//! ## Not dispatched here
42//!
43//! - **CA Support multi-stream** ([`ca_support`]) — TS 103 205 does not print a
44//! resource_id for the `resource_type = 2` variant (it defers to CI Plus V1.3),
45//! so its extended `ca_pmt`/`ca_pmt_reply` are standalone typed structs,
46//! directly constructible/parseable but **not** wired into [`CiPlusApdu`].
47//! - **Low-Speed Comms v4 extensions** ([`low_speed_comms_v4`]) — TS 103 205 §10
48//! prints no LSC v4 resource_id (the LSC resource_id + base `comms_*` APDUs defer
49//! to CI Plus V1.3), so the v4 `comms_info` / `comms_IP_config` APDUs and the
50//! hybrid/multicast descriptors are standalone typed structs (tag-dispatch
51//! helper), not wired into [`CiPlusApdu`].
52//! - **URI v3** ([`uri`]) — the `uri_message` is a SAC datatype (datatype_id 25),
53//! not an APDU/resource, so [`uri::UriMessage`] is a standalone typed struct used
54//! from the Content Control SAC layer.
55//! - **CI Plus descriptors** ([`descriptors`]) — Sample-Mode TLV descriptors, not
56//! APDUs, so they are standalone too.
57
58use crate::error::{Error, Result};
59use crate::resource::ResourceId;
60
61pub mod ca_support;
62pub mod cicam_player;
63pub mod content_control;
64pub mod descriptors;
65pub mod file_retrieval;
66pub mod low_speed_comms_v4;
67pub mod multistream;
68pub mod multistream_host_control;
69pub mod sample_decryption;
70pub mod uri;
71
72// --- Resource identifiers (TS 103 205 resource-summary tables) ---
73
74/// Multi-stream resource — Class 144, Type 1, Version 1 (`0x00900041`).
75/// §6.4.2.1 Table 2.
76pub const MULTISTREAM: ResourceId = ResourceId(0x0090_0041);
77/// Content Control multi-stream resource — Class 140, Type 65, Version 1
78/// (`0x008C1041`). §6.4.3.1 Table 6.
79pub const CONTENT_CONTROL: ResourceId = ResourceId(0x008C_1041);
80/// Multi-stream Host Control resource — Class 32, Type 2, Version 1
81/// (`0x00200081`). §6.4.5, Table 17. Based on DVB Host Control v3.
82pub const MULTISTREAM_HOST_CONTROL: ResourceId = ResourceId(0x0020_0081);
83/// Base DVB Host Control v3 resource — Class 32, Type 1, Version 3
84/// (`0x00200043`). §13. The multi-stream tune APDUs derive from this; both ids
85/// route to [`CiPlusResource::MultistreamHostControl`], distinguished only by the
86/// `tune_ip_req` reserved-bit budget ([`multistream_host_control::HostControlMode`]).
87pub const HOST_CONTROL_V3: ResourceId = ResourceId(0x0020_0043);
88/// Sample decryption resource — Class 146, Type 1, Version 1 (`0x00920041`).
89/// §7.4, Table 30.
90pub const SAMPLE_DECRYPTION: ResourceId = ResourceId(0x0092_0041);
91/// CICAM Player resource — Class 147, Type 1, Version 1 (`0x00930041`).
92/// §8.8, Table 71.
93pub const CICAM_PLAYER: ResourceId = ResourceId(0x0093_0041);
94/// Auxiliary File System resource (CICAM file retrieval) — Class 145, Type 1,
95/// Version 1 (`0x00910041`). §9, Table 75.
96pub const FILE_RETRIEVAL: ResourceId = ResourceId(0x0091_0041);
97
98/// The CI Plus resource a [`ResourceId`] denotes, for the resources dispatched by
99/// [`CiPlusApdu`]. Returns `None` for any other resource (including the
100/// deferred-resource_id CA Support multi-stream type).
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize))]
103#[non_exhaustive]
104pub enum CiPlusResource {
105 /// Multi-stream resource (`0x00900041`).
106 Multistream,
107 /// Content Control multi-stream resource (`0x008C1041`).
108 ContentControl,
109 /// Multi-stream Host Control — `0x00200081`
110 /// ([`MultiStream`](multistream_host_control::HostControlMode::MultiStream))
111 /// or the base DVB Host Control v3 `0x00200043`
112 /// ([`BaseV3`](multistream_host_control::HostControlMode::BaseV3)). The matched
113 /// mode is carried so the `tune_ip_req` reserved-bit budget can be selected.
114 MultistreamHostControl(multistream_host_control::HostControlMode),
115 /// Sample decryption resource (`0x00920041`).
116 SampleDecryption,
117 /// CICAM Player resource (`0x00930041`).
118 CicamPlayer,
119 /// Auxiliary File System / file retrieval resource (`0x00910041`).
120 FileRetrieval,
121}
122
123impl CiPlusResource {
124 /// Diagnostic spec token.
125 #[must_use]
126 pub fn name(&self) -> &'static str {
127 match self {
128 Self::Multistream => "multistream",
129 Self::ContentControl => "content_control",
130 Self::MultistreamHostControl(_) => "multistream_host_control",
131 Self::SampleDecryption => "sample_decryption",
132 Self::CicamPlayer => "cicam_player",
133 Self::FileRetrieval => "file_retrieval",
134 }
135 }
136}
137dvb_common::impl_spec_display!(CiPlusResource);
138
139/// Map a [`ResourceId`] to the CI Plus resource it denotes, or `None` for any
140/// resource not dispatched by [`CiPlusApdu`].
141#[must_use]
142pub fn classify(id: ResourceId) -> Option<CiPlusResource> {
143 use multistream_host_control::HostControlMode;
144 match id {
145 MULTISTREAM => Some(CiPlusResource::Multistream),
146 CONTENT_CONTROL => Some(CiPlusResource::ContentControl),
147 MULTISTREAM_HOST_CONTROL => Some(CiPlusResource::MultistreamHostControl(
148 HostControlMode::MultiStream,
149 )),
150 HOST_CONTROL_V3 => Some(CiPlusResource::MultistreamHostControl(
151 HostControlMode::BaseV3,
152 )),
153 SAMPLE_DECRYPTION => Some(CiPlusResource::SampleDecryption),
154 CICAM_PLAYER => Some(CiPlusResource::CicamPlayer),
155 FILE_RETRIEVAL => Some(CiPlusResource::FileRetrieval),
156 _ => None,
157 }
158}
159
160/// A parsed CI Plus APDU, scoped to the resource it arrived on.
161///
162/// One variant per resource dispatched this pass; each wraps that resource's own
163/// object enum (which dispatches on the leading `apdu_tag`).
164#[derive(Debug, Clone, PartialEq, Eq)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize))]
166#[non_exhaustive]
167pub enum CiPlusApdu<'a> {
168 /// Multi-stream resource object (`0x00900041`).
169 Multistream(multistream::MultistreamApdu),
170 /// Content Control multi-stream object (`0x008C1041`).
171 ContentControl(content_control::ContentControlApdu),
172 /// Multi-stream Host Control object (`0x00200081` / base v3 `0x00200043`).
173 MultistreamHostControl(
174 #[cfg_attr(feature = "serde", serde(borrow))]
175 multistream_host_control::MultistreamHostControlApdu<'a>,
176 ),
177 /// Sample decryption object (`0x00920041`).
178 SampleDecryption(
179 #[cfg_attr(feature = "serde", serde(borrow))] sample_decryption::SampleDecryptionApdu<'a>,
180 ),
181 /// CICAM Player object (`0x00930041`).
182 CicamPlayer(#[cfg_attr(feature = "serde", serde(borrow))] cicam_player::CicamPlayerApdu<'a>),
183 /// Auxiliary File System object (`0x00910041`).
184 FileRetrieval(
185 #[cfg_attr(feature = "serde", serde(borrow))] file_retrieval::FileRetrievalApdu<'a>,
186 ),
187}
188
189impl<'a> CiPlusApdu<'a> {
190 /// Parse a CI Plus APDU, selecting the resource from `resource_id`
191 /// ([`classify`]) and then delegating to that resource's object dispatch on
192 /// the leading `apdu_tag`.
193 ///
194 /// Errors with [`Error::UnknownResource`] if `resource_id` is not a CI Plus
195 /// resource handled this pass.
196 pub fn parse(resource_id: ResourceId, body: &'a [u8]) -> Result<Self> {
197 match classify(resource_id) {
198 Some(CiPlusResource::Multistream) => Ok(Self::Multistream(
199 multistream::MultistreamApdu::parse(body)?,
200 )),
201 Some(CiPlusResource::ContentControl) => Ok(Self::ContentControl(
202 content_control::ContentControlApdu::parse(body)?,
203 )),
204 Some(CiPlusResource::MultistreamHostControl(mode)) => Ok(Self::MultistreamHostControl(
205 multistream_host_control::MultistreamHostControlApdu::parse_mode(body, mode)?,
206 )),
207 Some(CiPlusResource::SampleDecryption) => Ok(Self::SampleDecryption(
208 sample_decryption::SampleDecryptionApdu::parse(body)?,
209 )),
210 Some(CiPlusResource::CicamPlayer) => Ok(Self::CicamPlayer(
211 cicam_player::CicamPlayerApdu::parse(body)?,
212 )),
213 Some(CiPlusResource::FileRetrieval) => Ok(Self::FileRetrieval(
214 file_retrieval::FileRetrievalApdu::parse(body)?,
215 )),
216 None => Err(Error::UnknownResource {
217 resource_id: resource_id.0,
218 }),
219 }
220 }
221}
222
223impl dvb_common::Serialize for CiPlusApdu<'_> {
224 type Error = Error;
225 fn serialized_len(&self) -> usize {
226 match self {
227 Self::Multistream(o) => o.serialized_len(),
228 Self::ContentControl(o) => o.serialized_len(),
229 Self::MultistreamHostControl(o) => o.serialized_len(),
230 Self::SampleDecryption(o) => o.serialized_len(),
231 Self::CicamPlayer(o) => o.serialized_len(),
232 Self::FileRetrieval(o) => o.serialized_len(),
233 }
234 }
235 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
236 match self {
237 Self::Multistream(o) => o.serialize_into(buf),
238 Self::ContentControl(o) => o.serialize_into(buf),
239 Self::MultistreamHostControl(o) => o.serialize_into(buf),
240 Self::SampleDecryption(o) => o.serialize_into(buf),
241 Self::CicamPlayer(o) => o.serialize_into(buf),
242 Self::FileRetrieval(o) => o.serialize_into(buf),
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use dvb_common::Serialize;
251
252 #[test]
253 fn classify_fixed_ids() {
254 use multistream_host_control::HostControlMode;
255 assert_eq!(classify(MULTISTREAM), Some(CiPlusResource::Multistream));
256 assert_eq!(
257 classify(CONTENT_CONTROL),
258 Some(CiPlusResource::ContentControl)
259 );
260 // The two Host Control ids both map to MultistreamHostControl, carrying
261 // the layout mode.
262 assert_eq!(
263 classify(MULTISTREAM_HOST_CONTROL),
264 Some(CiPlusResource::MultistreamHostControl(
265 HostControlMode::MultiStream
266 ))
267 );
268 assert_eq!(
269 classify(HOST_CONTROL_V3),
270 Some(CiPlusResource::MultistreamHostControl(
271 HostControlMode::BaseV3
272 ))
273 );
274 assert_eq!(
275 classify(SAMPLE_DECRYPTION),
276 Some(CiPlusResource::SampleDecryption)
277 );
278 assert_eq!(classify(CICAM_PLAYER), Some(CiPlusResource::CicamPlayer));
279 assert_eq!(
280 classify(FILE_RETRIEVAL),
281 Some(CiPlusResource::FileRetrieval)
282 );
283 assert_eq!(classify(ResourceId(0xDEAD_BEEF)), None);
284 // The deferred CA Support multi-stream type is intentionally not classified.
285 assert_eq!(classify(ResourceId(0x000C_0041)), None);
286 }
287
288 #[test]
289 fn dispatch_routes_host_control_and_sample_decryption() {
290 // tune_lcn_req under the multi-stream host control resource.
291 let lcn = [0x9F, 0x84, 0x07, 0x03, 0x01, 0x81, 0x23];
292 let hc = CiPlusApdu::parse(MULTISTREAM_HOST_CONTROL, &lcn).unwrap();
293 assert!(matches!(
294 hc,
295 CiPlusApdu::MultistreamHostControl(
296 multistream_host_control::MultistreamHostControlApdu::TuneLcnReq(_)
297 )
298 ));
299 assert_eq!(hc.to_bytes(), lcn);
300
301 // The same tune tag arrives on the base-v3 resource id and still routes.
302 let hc_v3 = CiPlusApdu::parse(HOST_CONTROL_V3, &lcn).unwrap();
303 assert!(matches!(hc_v3, CiPlusApdu::MultistreamHostControl(_)));
304
305 // sd_update_reply under the sample decryption resource.
306 let sd = [0x9F, 0x98, 0x05, 0x02, 0x03, 0x01];
307 let parsed = CiPlusApdu::parse(SAMPLE_DECRYPTION, &sd).unwrap();
308 assert!(matches!(
309 parsed,
310 CiPlusApdu::SampleDecryption(sample_decryption::SampleDecryptionApdu::SdUpdateReply(_))
311 ));
312 assert_eq!(parsed.to_bytes(), sd);
313 }
314
315 #[test]
316 fn tune_tag_9f8409_routes_resource_scoped_not_via_anyapdu() {
317 // 0x9F8409 (tune_triplet_req) is a multi-stream host-control tag. The
318 // EN 50221 Host Control resource owns 0x9F8400 (tune) / 0x9F8403
319 // (ask_release) — distinct values. The resource-scoped CiPlusApdu parses
320 // 0x9F8409 only under a host-control resource, proving it routes
321 // independently of the global AnyApdu (which has no 0x9F8409 member).
322 let triplet = [
323 0x9F, 0x84, 0x09, 0x09, 0x05, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x44, 0x00,
324 ];
325 assert!(matches!(
326 CiPlusApdu::parse(MULTISTREAM_HOST_CONTROL, &triplet).unwrap(),
327 CiPlusApdu::MultistreamHostControl(_)
328 ));
329 // Under an unrelated resource (Multistream PID resource), the same tag is
330 // not a member and is rejected.
331 assert!(matches!(
332 CiPlusApdu::parse(MULTISTREAM, &triplet),
333 Err(Error::UnexpectedApduTag { .. })
334 ));
335 // A global EN 50221 tune tag (0x9F8400) is not a member of the
336 // host-control resource's tune set either (different object namespace).
337 let en_tune = [0x9F, 0x84, 0x00, 0x00];
338 assert!(matches!(
339 CiPlusApdu::parse(MULTISTREAM_HOST_CONTROL, &en_tune),
340 Err(Error::UnexpectedApduTag { .. })
341 ));
342 }
343
344 #[test]
345 fn dispatch_routes_multistream_and_content_control() {
346 // Multistream PID_select_reply (whole-TS).
347 let ms_body = [0x9F, 0x92, 0x02, 0x03, 0x01, 0x00, 0x00];
348 let ms = CiPlusApdu::parse(MULTISTREAM, &ms_body).unwrap();
349 assert!(matches!(
350 ms,
351 CiPlusApdu::Multistream(multistream::MultistreamApdu::PidSelectReply(_))
352 ));
353 assert_eq!(ms.to_bytes(), ms_body);
354
355 // Content Control cc_PIN_reply (unbound).
356 let cc_body = [0x9F, 0x90, 0x14, 0x03, 0x00, 0x00, 0x42];
357 let cc = CiPlusApdu::parse(CONTENT_CONTROL, &cc_body).unwrap();
358 assert!(matches!(
359 cc,
360 CiPlusApdu::ContentControl(content_control::ContentControlApdu::CcPinReply(_))
361 ));
362 assert_eq!(cc.to_bytes(), cc_body);
363 }
364
365 #[test]
366 fn dispatch_routes_cicam_player_and_file_retrieval() {
367 // CICAM_player_update_reply (9F A0 0F) under the CICAM Player resource.
368 let upd = [0x9F, 0xA0, 0x0F, 0x02, 0x06, 0x00];
369 let p = CiPlusApdu::parse(CICAM_PLAYER, &upd).unwrap();
370 assert!(matches!(
371 p,
372 CiPlusApdu::CicamPlayer(cicam_player::CicamPlayerApdu::UpdateReply(_))
373 ));
374 assert_eq!(p.to_bytes(), upd);
375
376 // A 9FA0xx player tag is NOT a member of any other resource — it routes
377 // only under the player resource (resource-scoped invariant).
378 assert!(matches!(
379 CiPlusApdu::parse(MULTISTREAM, &upd),
380 Err(Error::UnexpectedApduTag { .. })
381 ));
382 assert!(matches!(
383 CiPlusApdu::parse(SAMPLE_DECRYPTION, &upd),
384 Err(Error::UnexpectedApduTag { .. })
385 ));
386
387 // FileSystemAck (9F 94 01) under the File Retrieval resource.
388 let ack = [0x9F, 0x94, 0x01, 0x01, 0x01];
389 let f = CiPlusApdu::parse(FILE_RETRIEVAL, &ack).unwrap();
390 assert!(matches!(
391 f,
392 CiPlusApdu::FileRetrieval(file_retrieval::FileRetrievalApdu::FileSystemAck(_))
393 ));
394 assert_eq!(f.to_bytes(), ack);
395 // The file-retrieval tag is not a player tag and vice-versa.
396 assert!(matches!(
397 CiPlusApdu::parse(CICAM_PLAYER, &ack),
398 Err(Error::UnexpectedApduTag { .. })
399 ));
400 assert!(matches!(
401 CiPlusApdu::parse(FILE_RETRIEVAL, &upd),
402 Err(Error::UnexpectedApduTag { .. })
403 ));
404 }
405
406 #[test]
407 fn unknown_resource_errors() {
408 let body = [0x9F, 0x92, 0x00, 0x00];
409 assert!(matches!(
410 CiPlusApdu::parse(ResourceId(0x1234_5678), &body),
411 Err(Error::UnknownResource { .. })
412 ));
413 }
414
415 #[test]
416 fn ca_pmt_tag_collision_routes_per_resource_independently() {
417 // 0x9F8032 (ca_pmt) is an EN 50221 tag AND a CI Plus CA-support tag, AND
418 // a TS 101 699-style 0x9F80xx value. Under the Multistream / Content
419 // Control resources it is simply *not* a member tag, so resource-scoped
420 // dispatch rejects it — proving CiPlusApdu routes independently of the
421 // global AnyApdu (which WOULD parse 0x9F8032 as ca_pmt).
422 let ca_pmt_like = [0x9F, 0x80, 0x32, 0x00];
423 assert!(matches!(
424 CiPlusApdu::parse(MULTISTREAM, &ca_pmt_like),
425 Err(Error::UnexpectedApduTag { .. })
426 ));
427 assert!(matches!(
428 CiPlusApdu::parse(CONTENT_CONTROL, &ca_pmt_like),
429 Err(Error::UnexpectedApduTag { .. })
430 ));
431 // The global EN 50221 dispatch, by contrast, recognizes 0x9F8032.
432 // (We don't construct a full ca_pmt here; the point is the tag space is
433 // shared, so only resource-scoping disambiguates.)
434 }
435
436 #[test]
437 fn tag_9f9200_under_multistream_vs_9f9014_under_content_control() {
438 // 9F9200 is a Multistream tag; under Content Control it is unknown.
439 let cap = [0x9F, 0x92, 0x00, 0x03, 0x04, 0x00, 0x01];
440 assert!(matches!(
441 CiPlusApdu::parse(MULTISTREAM, &cap).unwrap(),
442 CiPlusApdu::Multistream(multistream::MultistreamApdu::CicamMultistreamCapability(_))
443 ));
444 assert!(matches!(
445 CiPlusApdu::parse(CONTENT_CONTROL, &cap),
446 Err(Error::UnexpectedApduTag { .. })
447 ));
448 // And 9F9014 is a Content Control tag; under Multistream it is unknown.
449 let pin = [0x9F, 0x90, 0x14, 0x03, 0x00, 0x00, 0x42];
450 assert!(matches!(
451 CiPlusApdu::parse(CONTENT_CONTROL, &pin).unwrap(),
452 CiPlusApdu::ContentControl(content_control::ContentControlApdu::CcPinReply(_))
453 ));
454 assert!(matches!(
455 CiPlusApdu::parse(MULTISTREAM, &pin),
456 Err(Error::UnexpectedApduTag { .. })
457 ));
458 }
459}