Skip to main content

dvb_ci/ci_plus/
multistream.rs

1//! Multi-stream resource objects — ETSI TS 103 205 V1.4.1 §6.4.2, Tables 2-5
2//! (PDF pp. 31-33). See `docs/ts_103_205/multi-stream-resource.md`.
3//!
4//! Resource ID `0x00900041` (Class 144, Type 1, Version 1) — a **new** CI Plus
5//! resource with no EN 50221 equivalent. The CICAM advertises its multi-stream
6//! capabilities and the Host/CICAM negotiate PID selection in the Local TSs.
7//!
8//! - `CICAM_multistream_capability` (`9F 92 00`, Table 3) — CICAM → Host.
9//! - `PID_select_req` (`9F 92 01`, Table 4) — CICAM → Host.
10//! - `PID_select_reply` (`9F 92 02`, Table 5) — Host → CICAM.
11//!
12//! These apdu_tags live in the CI Plus `0x9F92xx` namespace and are dispatched
13//! resource-scoped by [`crate::ci_plus::CiPlusApdu`].
14
15use crate::error::{Error, Result};
16use crate::objects;
17use crate::tag::ApduTag;
18use alloc::vec::Vec;
19use broadcast_common::{Parse, Serialize};
20
21/// Resource-scoped `apdu_tag`s for the Multi-stream resource (Table 2).
22pub mod tag {
23    use crate::tag::ApduTag;
24    /// `CICAM_multistream_capability_tag` = `9F 92 00`.
25    pub const CICAM_MULTISTREAM_CAPABILITY: ApduTag = ApduTag::from_bytes(0x9F, 0x92, 0x00);
26    /// `PID_select_req_tag` = `9F 92 01`.
27    pub const PID_SELECT_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x92, 0x01);
28    /// `PID_select_reply_tag` = `9F 92 02`.
29    pub const PID_SELECT_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0x92, 0x02);
30}
31
32/// The reserved PID value `0x1FFF` — the Host shall ignore a request for it
33/// (§6.4.2.3, Table 4 semantics).
34pub const NULL_PID: u16 = 0x1FFF;
35
36// --- CICAM_multistream_capability (Table 3) ---
37
38/// `CICAM_multistream_capability()` (Table 3): CICAM → Host.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize))]
41pub struct CicamMultistreamCapability {
42    /// `max_local_TS` (8) — maximum number of Local TSs the CICAM can receive
43    /// concurrently.
44    pub max_local_ts: u8,
45    /// `max_descramblers` (16) — total number of descramblers the CICAM can
46    /// provide concurrently across all Local TSs.
47    pub max_descramblers: u16,
48}
49
50// max_local_TS(1) + max_descramblers(2).
51const CAPABILITY_BODY: usize = 1 + 2;
52
53impl<'a> Parse<'a> for CicamMultistreamCapability {
54    type Error = Error;
55    fn parse(bytes: &'a [u8]) -> Result<Self> {
56        let body = objects::parse_apdu_header(
57            bytes,
58            tag::CICAM_MULTISTREAM_CAPABILITY,
59            "CICAM_multistream_capability",
60        )?;
61        if body.len() < CAPABILITY_BODY {
62            return Err(Error::BufferTooShort {
63                need: CAPABILITY_BODY,
64                have: body.len(),
65                what: "CICAM_multistream_capability",
66            });
67        }
68        Ok(Self {
69            max_local_ts: body[0],
70            max_descramblers: u16::from_be_bytes([body[1], body[2]]),
71        })
72    }
73}
74impl Serialize for CicamMultistreamCapability {
75    type Error = Error;
76    fn serialized_len(&self) -> usize {
77        objects::apdu_len(CAPABILITY_BODY)
78    }
79    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
80        let pos =
81            objects::write_apdu_header(tag::CICAM_MULTISTREAM_CAPABILITY, CAPABILITY_BODY, buf)?;
82        buf[pos] = self.max_local_ts;
83        buf[pos + 1..pos + 3].copy_from_slice(&self.max_descramblers.to_be_bytes());
84        Ok(pos + CAPABILITY_BODY)
85    }
86}
87
88// --- PID_select_req (Table 4) ---
89
90/// One entry of the `PID_select_req` loop (Table 4): `reserved(2)` +
91/// `critical_for_descrambling_flag(1)` + `PID(13)`.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize))]
94pub struct PidSelectRequest {
95    /// `critical_for_descrambling_flag` (1) — `true` if the PID is critical for
96    /// descrambling.
97    pub critical_for_descrambling: bool,
98    /// `PID` (13) — requested PID value (the Host ignores [`NULL_PID`]).
99    pub pid: u16,
100}
101
102/// `PID_select_req()` (Table 4): CICAM → Host. PIDs are listed in descending
103/// priority order.
104#[derive(Debug, Clone, PartialEq, Eq)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize))]
106pub struct PidSelectReq {
107    /// `LTS_id` (8) — Local TS identifier.
108    pub lts_id: u8,
109    /// The requested PIDs (loop count = `num_PID`).
110    pub pids: Vec<PidSelectRequest>,
111}
112
113// Each loop entry is 2 bytes.
114const PID_ENTRY_LEN: usize = 2;
115// Mask for the 13-bit PID field.
116const PID_MASK: u16 = 0x1FFF;
117// The critical_for_descrambling_flag bit within the high byte.
118const CRITICAL_FLAG_BIT: u8 = 0x20;
119
120impl<'a> Parse<'a> for PidSelectReq {
121    type Error = Error;
122    fn parse(bytes: &'a [u8]) -> Result<Self> {
123        let body = objects::parse_apdu_header(bytes, tag::PID_SELECT_REQ, "PID_select_req")?;
124        // LTS_id(1) + num_PID(1).
125        if body.len() < 2 {
126            return Err(Error::BufferTooShort {
127                need: 2,
128                have: body.len(),
129                what: "PID_select_req",
130            });
131        }
132        let lts_id = body[0];
133        let num_pid = body[1] as usize;
134        let loop_bytes = &body[2..];
135        if loop_bytes.len() < num_pid * PID_ENTRY_LEN {
136            return Err(Error::BufferTooShort {
137                need: num_pid * PID_ENTRY_LEN,
138                have: loop_bytes.len(),
139                what: "PID_select_req loop",
140            });
141        }
142        let mut pids = Vec::with_capacity(num_pid);
143        for chunk in loop_bytes[..num_pid * PID_ENTRY_LEN].chunks_exact(PID_ENTRY_LEN) {
144            let critical = chunk[0] & CRITICAL_FLAG_BIT != 0;
145            let pid = u16::from_be_bytes([chunk[0], chunk[1]]) & PID_MASK;
146            pids.push(PidSelectRequest {
147                critical_for_descrambling: critical,
148                pid,
149            });
150        }
151        Ok(Self { lts_id, pids })
152    }
153}
154impl Serialize for PidSelectReq {
155    type Error = Error;
156    fn serialized_len(&self) -> usize {
157        objects::apdu_len(2 + self.pids.len() * PID_ENTRY_LEN)
158    }
159    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
160        let body_len = 2 + self.pids.len() * PID_ENTRY_LEN;
161        let mut pos = objects::write_apdu_header(tag::PID_SELECT_REQ, body_len, buf)?;
162        buf[pos] = self.lts_id;
163        buf[pos + 1] = self.pids.len() as u8;
164        pos += 2;
165        for entry in &self.pids {
166            let mut hi = (entry.pid >> 8) as u8 & (PID_MASK >> 8) as u8;
167            if entry.critical_for_descrambling {
168                hi |= CRITICAL_FLAG_BIT;
169            }
170            buf[pos] = hi;
171            buf[pos + 1] = entry.pid as u8;
172            pos += PID_ENTRY_LEN;
173        }
174        Ok(pos)
175    }
176}
177
178// --- PID_select_reply (Table 5) ---
179
180/// One entry of the `PID_select_reply` loop (Table 5): `reserved(2)` +
181/// `PID_selected_flag(1)` + `PID(13)`.
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183#[cfg_attr(feature = "serde", derive(serde::Serialize))]
184pub struct PidSelectedEntry {
185    /// `PID_selected_flag` (1) — `true` if the PID could be selected successfully.
186    pub pid_selected: bool,
187    /// `PID` (13) — PID value to which `PID_selected_flag` applies.
188    pub pid: u16,
189}
190
191/// `PID_select_reply()` (Table 5): Host → CICAM.
192#[derive(Debug, Clone, PartialEq, Eq)]
193#[cfg_attr(feature = "serde", derive(serde::Serialize))]
194pub struct PidSelectReply {
195    /// `LTS_id` (8) — Local TS identifier.
196    pub lts_id: u8,
197    /// `PID_selection_flag` (1) — `false` = whole TS sent (`num_PID` shall be 0);
198    /// `true` = PID selection applied and the loop lists the selected PIDs.
199    pub pid_selection: bool,
200    /// The per-PID selection results (loop count = `num_PID`).
201    pub pids: Vec<PidSelectedEntry>,
202}
203
204// The PID_selected_flag bit within the high byte (bit 5 of the 16-bit word).
205const PID_SELECTED_FLAG_BIT: u8 = 0x20;
206// The PID_selection_flag bit (LSB of the reserved(7)+flag(1) byte).
207const PID_SELECTION_FLAG_BIT: u8 = 0x01;
208
209impl<'a> Parse<'a> for PidSelectReply {
210    type Error = Error;
211    fn parse(bytes: &'a [u8]) -> Result<Self> {
212        let body = objects::parse_apdu_header(bytes, tag::PID_SELECT_REPLY, "PID_select_reply")?;
213        // LTS_id(1) + reserved(7)+flag(1) byte + num_PID(1).
214        if body.len() < 3 {
215            return Err(Error::BufferTooShort {
216                need: 3,
217                have: body.len(),
218                what: "PID_select_reply",
219            });
220        }
221        let lts_id = body[0];
222        let pid_selection = body[1] & PID_SELECTION_FLAG_BIT != 0;
223        let num_pid = body[2] as usize;
224        let loop_bytes = &body[3..];
225        if loop_bytes.len() < num_pid * PID_ENTRY_LEN {
226            return Err(Error::BufferTooShort {
227                need: num_pid * PID_ENTRY_LEN,
228                have: loop_bytes.len(),
229                what: "PID_select_reply loop",
230            });
231        }
232        let mut pids = Vec::with_capacity(num_pid);
233        for chunk in loop_bytes[..num_pid * PID_ENTRY_LEN].chunks_exact(PID_ENTRY_LEN) {
234            let selected = chunk[0] & PID_SELECTED_FLAG_BIT != 0;
235            let pid = u16::from_be_bytes([chunk[0], chunk[1]]) & PID_MASK;
236            pids.push(PidSelectedEntry {
237                pid_selected: selected,
238                pid,
239            });
240        }
241        Ok(Self {
242            lts_id,
243            pid_selection,
244            pids,
245        })
246    }
247}
248impl Serialize for PidSelectReply {
249    type Error = Error;
250    fn serialized_len(&self) -> usize {
251        objects::apdu_len(3 + self.pids.len() * PID_ENTRY_LEN)
252    }
253    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
254        let body_len = 3 + self.pids.len() * PID_ENTRY_LEN;
255        let mut pos = objects::write_apdu_header(tag::PID_SELECT_REPLY, body_len, buf)?;
256        buf[pos] = self.lts_id;
257        buf[pos + 1] = if self.pid_selection {
258            PID_SELECTION_FLAG_BIT
259        } else {
260            0
261        };
262        buf[pos + 2] = self.pids.len() as u8;
263        pos += 3;
264        for entry in &self.pids {
265            let mut hi = (entry.pid >> 8) as u8 & (PID_MASK >> 8) as u8;
266            if entry.pid_selected {
267                hi |= PID_SELECTED_FLAG_BIT;
268            }
269            buf[pos] = hi;
270            buf[pos + 1] = entry.pid as u8;
271            pos += PID_ENTRY_LEN;
272        }
273        Ok(pos)
274    }
275}
276
277/// Resource-scoped dispatch over the Multi-stream resource objects.
278#[derive(Debug, Clone, PartialEq, Eq)]
279#[cfg_attr(feature = "serde", derive(serde::Serialize))]
280#[non_exhaustive]
281pub enum MultistreamApdu {
282    /// `CICAM_multistream_capability` (`9F 92 00`).
283    CicamMultistreamCapability(CicamMultistreamCapability),
284    /// `PID_select_req` (`9F 92 01`).
285    PidSelectReq(PidSelectReq),
286    /// `PID_select_reply` (`9F 92 02`).
287    PidSelectReply(PidSelectReply),
288}
289
290impl MultistreamApdu {
291    /// Parse a Multi-stream APDU, dispatching on the leading `apdu_tag`.
292    pub fn parse(body: &[u8]) -> Result<Self> {
293        if body.len() < 3 {
294            return Err(Error::BufferTooShort {
295                need: 3,
296                have: body.len(),
297                what: "multistream apdu_tag",
298            });
299        }
300        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
301        match t {
302            tag::CICAM_MULTISTREAM_CAPABILITY => Ok(Self::CicamMultistreamCapability(
303                CicamMultistreamCapability::parse(body)?,
304            )),
305            tag::PID_SELECT_REQ => Ok(Self::PidSelectReq(PidSelectReq::parse(body)?)),
306            tag::PID_SELECT_REPLY => Ok(Self::PidSelectReply(PidSelectReply::parse(body)?)),
307            _ => Err(Error::UnexpectedApduTag {
308                got: t.as_u24(),
309                expected: tag::CICAM_MULTISTREAM_CAPABILITY.as_u24(),
310                what: "multistream",
311            }),
312        }
313    }
314}
315
316impl Serialize for MultistreamApdu {
317    type Error = Error;
318    fn serialized_len(&self) -> usize {
319        match self {
320            Self::CicamMultistreamCapability(o) => o.serialized_len(),
321            Self::PidSelectReq(o) => o.serialized_len(),
322            Self::PidSelectReply(o) => o.serialized_len(),
323        }
324    }
325    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
326        match self {
327            Self::CicamMultistreamCapability(o) => o.serialize_into(buf),
328            Self::PidSelectReq(o) => o.serialize_into(buf),
329            Self::PidSelectReply(o) => o.serialize_into(buf),
330        }
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn capability_round_trips_and_bites() {
340        let cap = CicamMultistreamCapability {
341            max_local_ts: 0x04,
342            max_descramblers: 0x0102,
343        };
344        let bytes = cap.to_bytes();
345        // tag(3) + len(0x03) + max_local_TS(0x04) + max_descramblers(01 02).
346        assert_eq!(bytes, [0x9F, 0x92, 0x00, 0x03, 0x04, 0x01, 0x02]);
347        assert_eq!(CicamMultistreamCapability::parse(&bytes).unwrap(), cap);
348        let mut other = cap;
349        other.max_descramblers = 0x0103;
350        assert_ne!(bytes, other.to_bytes());
351    }
352
353    #[test]
354    fn pid_select_req_round_trips_and_bites() {
355        let req = PidSelectReq {
356            lts_id: 0x07,
357            pids: alloc::vec![
358                PidSelectRequest {
359                    critical_for_descrambling: true,
360                    pid: 0x0123,
361                },
362                PidSelectRequest {
363                    critical_for_descrambling: false,
364                    pid: 0x1FFE,
365                },
366            ],
367        };
368        let bytes = req.to_bytes();
369        // tag(3) + len(0x06) + LTS_id(07) + num_PID(02)
370        //   entry0: critical=1 -> 0x20 | (0x0123>>8=0x01) = 0x21, lo=0x23
371        //   entry1: critical=0 -> (0x1FFE>>8=0x1F), lo=0xFE
372        assert_eq!(
373            bytes,
374            [0x9F, 0x92, 0x01, 0x06, 0x07, 0x02, 0x21, 0x23, 0x1F, 0xFE]
375        );
376        assert_eq!(PidSelectReq::parse(&bytes).unwrap(), req);
377        // Field-mutation: flip critical flag changes the wire.
378        let mut other = req.clone();
379        other.pids[0].critical_for_descrambling = false;
380        assert_ne!(bytes, other.to_bytes());
381        assert_eq!(other.to_bytes()[6], 0x01);
382    }
383
384    #[test]
385    fn pid_select_req_empty_loop() {
386        let req = PidSelectReq {
387            lts_id: 0x00,
388            pids: Vec::new(),
389        };
390        let bytes = req.to_bytes();
391        assert_eq!(bytes, [0x9F, 0x92, 0x01, 0x02, 0x00, 0x00]);
392        assert_eq!(PidSelectReq::parse(&bytes).unwrap(), req);
393    }
394
395    #[test]
396    fn pid_select_reply_round_trips_with_two_entries() {
397        let reply = PidSelectReply {
398            lts_id: 0x05,
399            pid_selection: true,
400            pids: alloc::vec![
401                PidSelectedEntry {
402                    pid_selected: true,
403                    pid: 0x0064,
404                },
405                PidSelectedEntry {
406                    pid_selected: false,
407                    pid: 0x00C8,
408                },
409            ],
410        };
411        let bytes = reply.to_bytes();
412        // tag(3) + len(0x07) + LTS_id(05) + flag-byte(0x01) + num_PID(02)
413        //   entry0: selected=1 -> 0x20 | 0x00 = 0x20, lo=0x64
414        //   entry1: selected=0 -> 0x00, lo=0xC8
415        assert_eq!(
416            bytes,
417            [
418                0x9F, 0x92, 0x02, 0x07, 0x05, 0x01, 0x02, 0x20, 0x64, 0x00, 0xC8
419            ]
420        );
421        assert_eq!(PidSelectReply::parse(&bytes).unwrap(), reply);
422        // Field-mutation: clear selection flag changes the wire byte.
423        let mut other = reply.clone();
424        other.pid_selection = false;
425        assert_eq!(other.to_bytes()[5], 0x00);
426        assert_ne!(bytes, other.to_bytes());
427    }
428
429    #[test]
430    fn pid_select_reply_whole_ts() {
431        let reply = PidSelectReply {
432            lts_id: 0x01,
433            pid_selection: false,
434            pids: Vec::new(),
435        };
436        let bytes = reply.to_bytes();
437        assert_eq!(bytes, [0x9F, 0x92, 0x02, 0x03, 0x01, 0x00, 0x00]);
438        assert_eq!(PidSelectReply::parse(&bytes).unwrap(), reply);
439    }
440
441    #[test]
442    fn dispatch_routes_each_tag() {
443        let cap = CicamMultistreamCapability {
444            max_local_ts: 1,
445            max_descramblers: 1,
446        }
447        .to_bytes();
448        assert!(matches!(
449            MultistreamApdu::parse(&cap).unwrap(),
450            MultistreamApdu::CicamMultistreamCapability(_)
451        ));
452        let reply = PidSelectReply {
453            lts_id: 0,
454            pid_selection: false,
455            pids: Vec::new(),
456        }
457        .to_bytes();
458        let parsed = MultistreamApdu::parse(&reply).unwrap();
459        assert!(matches!(parsed, MultistreamApdu::PidSelectReply(_)));
460        assert_eq!(parsed.to_bytes(), reply);
461    }
462}