Skip to main content

dvb_ci/objects/
ca_pmt.rs

1//! CA PMT object (`ca_pmt`) — ETSI EN 50221 §8.4.3.4, Table 25 (PDF pp. 30-31).
2//!
3//! `ca_pmt` (`9F 80 32`, host → app) is a CA-only projection of the MPEG-2 PMT:
4//! the host strips every non-CA descriptor and keeps only the `CA_descriptor()`s
5//! (ISO/IEC 13818-1 §2.6.16, tag `0x09`) at programme and elementary-stream
6//! level, prefixing each surviving descriptor loop with a `ca_pmt_cmd_id` byte.
7//!
8//! This module carries the surviving CA descriptor loops as their verbatim wire
9//! bytes (`&[u8]`); see [`crate::builder`] for the projection from a `dvb-si`
10//! `PmtSection`.
11
12use crate::error::{Error, Result};
13use crate::tag::{self, ApduTag};
14use crate::traits::ApduDef;
15use alloc::vec::Vec;
16use dvb_common::{Parse, Serialize};
17
18/// MPEG-2 `CA_descriptor` tag (ISO/IEC 13818-1 §2.6.16); the only descriptor a
19/// `ca_pmt` carries.
20pub const CA_DESCRIPTOR_TAG: u8 = 0x09;
21
22/// `ca_pmt_list_management` values (Table, p. 31).
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize))]
25#[non_exhaustive]
26pub enum CaPmtListManagement {
27    /// `00` — neither first nor last of the list.
28    More,
29    /// `01` — first of a new list (replaces previous selections).
30    First,
31    /// `02` — last of the list.
32    Last,
33    /// `03` — the list is a single CA PMT.
34    Only,
35    /// `04` — a newly selected programme; previous selections retained.
36    Add,
37    /// `05` — a programme already in the list re-sent (version change).
38    Update,
39    /// Any other value (reserved).
40    Reserved(u8),
41}
42
43impl CaPmtListManagement {
44    /// Decode a `ca_pmt_list_management` byte.
45    #[must_use]
46    pub fn from_u8(v: u8) -> Self {
47        match v {
48            0x00 => Self::More,
49            0x01 => Self::First,
50            0x02 => Self::Last,
51            0x03 => Self::Only,
52            0x04 => Self::Add,
53            0x05 => Self::Update,
54            other => Self::Reserved(other),
55        }
56    }
57    /// Wire byte.
58    #[must_use]
59    pub fn to_u8(self) -> u8 {
60        match self {
61            Self::More => 0x00,
62            Self::First => 0x01,
63            Self::Last => 0x02,
64            Self::Only => 0x03,
65            Self::Add => 0x04,
66            Self::Update => 0x05,
67            Self::Reserved(v) => v,
68        }
69    }
70    /// Spec token, or `"reserved"`.
71    #[must_use]
72    pub fn name(&self) -> &'static str {
73        match self {
74            Self::More => "more",
75            Self::First => "first",
76            Self::Last => "last",
77            Self::Only => "only",
78            Self::Add => "add",
79            Self::Update => "update",
80            Self::Reserved(_) => "reserved",
81        }
82    }
83}
84dvb_common::impl_spec_display!(CaPmtListManagement, Reserved);
85
86/// `ca_pmt_cmd_id` values (Table, p. 31).
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize))]
89#[non_exhaustive]
90pub enum CaPmtCmdId {
91    /// `01` — application may start descrambling / MMI immediately.
92    OkDescrambling,
93    /// `02` — application may start MMI but not descrambling yet.
94    OkMmi,
95    /// `03` — host expects a `ca_pmt_reply`.
96    Query,
97    /// `04` — host no longer needs this application to descramble.
98    NotSelected,
99    /// Any other value (RFU).
100    Rfu(u8),
101}
102
103impl CaPmtCmdId {
104    /// Decode a `ca_pmt_cmd_id` byte.
105    #[must_use]
106    pub fn from_u8(v: u8) -> Self {
107        match v {
108            0x01 => Self::OkDescrambling,
109            0x02 => Self::OkMmi,
110            0x03 => Self::Query,
111            0x04 => Self::NotSelected,
112            other => Self::Rfu(other),
113        }
114    }
115    /// Wire byte.
116    #[must_use]
117    pub fn to_u8(self) -> u8 {
118        match self {
119            Self::OkDescrambling => 0x01,
120            Self::OkMmi => 0x02,
121            Self::Query => 0x03,
122            Self::NotSelected => 0x04,
123            Self::Rfu(v) => v,
124        }
125    }
126    /// Spec token, or `"reserved"`.
127    #[must_use]
128    pub fn name(&self) -> &'static str {
129        match self {
130            Self::OkDescrambling => "ok_descrambling",
131            Self::OkMmi => "ok_mmi",
132            Self::Query => "query",
133            Self::NotSelected => "not_selected",
134            Self::Rfu(_) => "reserved",
135        }
136    }
137}
138dvb_common::impl_spec_display!(CaPmtCmdId, Rfu);
139
140/// One elementary-stream entry in a `ca_pmt`.
141#[derive(Debug, Clone, PartialEq, Eq)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize))]
143pub struct CaPmtStream<'a> {
144    /// MPEG-2 `stream_type`.
145    pub stream_type: u8,
146    /// 13-bit `elementary_PID`.
147    pub elementary_pid: u16,
148    /// `ca_pmt_cmd_id` for this ES — present only when the ES has CA info
149    /// (i.e. when `ca_descriptors` is non-empty), per the `ES_info_length != 0`
150    /// guard in Table 25.
151    pub cmd_id: Option<CaPmtCmdId>,
152    /// The ES-level `CA_descriptor()` loop, verbatim wire bytes (no `cmd_id`).
153    #[cfg_attr(feature = "serde", serde(borrow, with = "super::bytes_serde"))]
154    pub ca_descriptors: &'a [u8],
155}
156
157/// `ca_pmt()` object (Table 25).
158#[derive(Debug, Clone, PartialEq, Eq)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize))]
160pub struct CaPmt<'a> {
161    /// `ca_pmt_list_management`.
162    pub list_management: CaPmtListManagement,
163    /// `program_number`.
164    pub program_number: u16,
165    /// 5-bit `version_number`.
166    pub version_number: u8,
167    /// `current_next_indicator`.
168    pub current_next_indicator: bool,
169    /// Programme-level `ca_pmt_cmd_id` — present only when there is programme-level
170    /// CA info (`program_info_length != 0`).
171    pub cmd_id: Option<CaPmtCmdId>,
172    /// Programme-level `CA_descriptor()` loop, verbatim wire bytes (no `cmd_id`).
173    #[cfg_attr(feature = "serde", serde(borrow, with = "super::bytes_serde"))]
174    pub program_ca_descriptors: &'a [u8],
175    /// Elementary streams in wire order.
176    pub streams: Vec<CaPmtStream<'a>>,
177}
178
179// Fixed prefix after the header: list_management(1) + program_number(2) +
180// reserved/version/cni(1) + reserved/program_info_length(2).
181const CA_PMT_PREFIX: usize = 6;
182// Per-ES fixed prefix: stream_type(1) + reserved/elem_pid(2) +
183// reserved/ES_info_length(2).
184const ES_PREFIX: usize = 5;
185
186impl<'a> Parse<'a> for CaPmt<'a> {
187    type Error = Error;
188    fn parse(bytes: &'a [u8]) -> Result<Self> {
189        let body = super::parse_apdu_header(bytes, tag::CA_PMT, "ca_pmt")?;
190        if body.len() < CA_PMT_PREFIX {
191            return Err(Error::BufferTooShort {
192                need: CA_PMT_PREFIX,
193                have: body.len(),
194                what: "ca_pmt prefix",
195            });
196        }
197        let list_management = CaPmtListManagement::from_u8(body[0]);
198        let program_number = u16::from_be_bytes([body[1], body[2]]);
199        let version_number = (body[3] >> 1) & 0x1F;
200        let current_next_indicator = (body[3] & 0x01) != 0;
201        let program_info_length = (((body[4] & 0x0F) as usize) << 8) | body[5] as usize;
202
203        let mut pos = CA_PMT_PREFIX;
204        let (cmd_id, program_ca_descriptors) =
205            parse_cmd_and_descriptors(body, &mut pos, program_info_length, "ca_pmt program_info")?;
206
207        let mut streams = Vec::new();
208        while pos < body.len() {
209            if pos + ES_PREFIX > body.len() {
210                return Err(Error::BufferTooShort {
211                    need: pos + ES_PREFIX,
212                    have: body.len(),
213                    what: "ca_pmt ES prefix",
214                });
215            }
216            let stream_type = body[pos];
217            let elementary_pid = (((body[pos + 1] & 0x1F) as u16) << 8) | body[pos + 2] as u16;
218            let es_info_length = (((body[pos + 3] & 0x0F) as usize) << 8) | body[pos + 4] as usize;
219            pos += ES_PREFIX;
220            let (es_cmd, ca_descriptors) =
221                parse_cmd_and_descriptors(body, &mut pos, es_info_length, "ca_pmt ES_info")?;
222            streams.push(CaPmtStream {
223                stream_type,
224                elementary_pid,
225                cmd_id: es_cmd,
226                ca_descriptors,
227            });
228        }
229
230        Ok(Self {
231            list_management,
232            program_number,
233            version_number,
234            current_next_indicator,
235            cmd_id,
236            program_ca_descriptors,
237            streams,
238        })
239    }
240}
241
242/// Read an `info_length`-byte block at `*pos`: when non-zero it starts with a
243/// `ca_pmt_cmd_id` byte followed by `info_length - 1` descriptor bytes. Advances
244/// `*pos` past the block.
245fn parse_cmd_and_descriptors<'a>(
246    body: &'a [u8],
247    pos: &mut usize,
248    info_length: usize,
249    what: &'static str,
250) -> Result<(Option<CaPmtCmdId>, &'a [u8])> {
251    if info_length == 0 {
252        return Ok((None, &body[..0]));
253    }
254    let end = *pos + info_length;
255    if end > body.len() {
256        return Err(Error::LengthMismatch {
257            what,
258            declared: info_length,
259            actual: body.len().saturating_sub(*pos),
260        });
261    }
262    let cmd_id = CaPmtCmdId::from_u8(body[*pos]);
263    let descriptors = &body[*pos + 1..end];
264    *pos = end;
265    Ok((Some(cmd_id), descriptors))
266}
267
268/// Wire length of an info block (cmd_id + descriptors), or 0 when neither is
269/// present.
270fn info_block_len(cmd_id: Option<CaPmtCmdId>, descriptors: &[u8]) -> usize {
271    if cmd_id.is_some() || !descriptors.is_empty() {
272        1 + descriptors.len()
273    } else {
274        0
275    }
276}
277
278impl Serialize for CaPmt<'_> {
279    type Error = Error;
280    fn serialized_len(&self) -> usize {
281        let mut body = CA_PMT_PREFIX + info_block_len(self.cmd_id, self.program_ca_descriptors);
282        for s in &self.streams {
283            body += ES_PREFIX + info_block_len(s.cmd_id, s.ca_descriptors);
284        }
285        super::apdu_len(body)
286    }
287
288    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
289        let program_info_length = info_block_len(self.cmd_id, self.program_ca_descriptors);
290        let mut body = CA_PMT_PREFIX + program_info_length;
291        for s in &self.streams {
292            body += ES_PREFIX + info_block_len(s.cmd_id, s.ca_descriptors);
293        }
294        let mut pos = super::write_apdu_header(tag::CA_PMT, body, buf)?;
295
296        buf[pos] = self.list_management.to_u8();
297        buf[pos + 1..pos + 3].copy_from_slice(&self.program_number.to_be_bytes());
298        // reserved(2)='11', version(5), current_next(1).
299        buf[pos + 3] =
300            0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
301        // reserved(4)='1111', program_info_length(12).
302        buf[pos + 4] = 0xF0 | ((program_info_length >> 8) as u8 & 0x0F);
303        buf[pos + 5] = program_info_length as u8;
304        pos += CA_PMT_PREFIX;
305        pos += write_info_block(self.cmd_id, self.program_ca_descriptors, &mut buf[pos..])?;
306
307        for s in &self.streams {
308            let es_info_length = info_block_len(s.cmd_id, s.ca_descriptors);
309            buf[pos] = s.stream_type;
310            // reserved(3)='111', elementary_PID(13).
311            buf[pos + 1] = 0xE0 | ((s.elementary_pid >> 8) as u8 & 0x1F);
312            buf[pos + 2] = s.elementary_pid as u8;
313            // reserved(4)='1111', ES_info_length(12).
314            buf[pos + 3] = 0xF0 | ((es_info_length >> 8) as u8 & 0x0F);
315            buf[pos + 4] = es_info_length as u8;
316            pos += ES_PREFIX;
317            pos += write_info_block(s.cmd_id, s.ca_descriptors, &mut buf[pos..])?;
318        }
319        Ok(pos)
320    }
321}
322
323fn write_info_block(
324    cmd_id: Option<CaPmtCmdId>,
325    descriptors: &[u8],
326    buf: &mut [u8],
327) -> Result<usize> {
328    let len = info_block_len(cmd_id, descriptors);
329    if len == 0 {
330        return Ok(0);
331    }
332    if buf.len() < len {
333        return Err(Error::OutputBufferTooSmall {
334            need: len,
335            have: buf.len(),
336        });
337    }
338    // cmd_id defaults to ok_descrambling if a descriptor block exists without an
339    // explicit id (shouldn't occur via the builder, but keeps serialize total).
340    buf[0] = cmd_id.unwrap_or(CaPmtCmdId::OkDescrambling).to_u8();
341    buf[1..len].copy_from_slice(descriptors);
342    Ok(len)
343}
344
345impl<'a> ApduDef<'a> for CaPmt<'a> {
346    const TAG: ApduTag = tag::CA_PMT;
347    const NAME: &'static str = "CA_PMT";
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    fn sample_ca_descriptor(ca_system_id: u16, pid: u16) -> [u8; 6] {
355        // CA_descriptor: tag 0x09, len 4, CA_system_id(2), reserved(3)+CA_PID(13).
356        [
357            CA_DESCRIPTOR_TAG,
358            0x04,
359            (ca_system_id >> 8) as u8,
360            ca_system_id as u8,
361            0xE0 | ((pid >> 8) as u8 & 0x1F),
362            pid as u8,
363        ]
364    }
365
366    #[test]
367    fn empty_ca_pmt_round_trips() {
368        let pmt = CaPmt {
369            list_management: CaPmtListManagement::Only,
370            program_number: 0x1234,
371            version_number: 5,
372            current_next_indicator: true,
373            cmd_id: None,
374            program_ca_descriptors: &[],
375            streams: Vec::new(),
376        };
377        let bytes = pmt.to_bytes();
378        assert_eq!(&bytes[..4], &[0x9F, 0x80, 0x32, 0x06]);
379        assert_eq!(CaPmt::parse(&bytes).unwrap(), pmt);
380    }
381
382    #[test]
383    fn multi_es_round_trips_and_bites() {
384        let prog_desc = sample_ca_descriptor(0x0500, 0x0100);
385        let es0_desc = sample_ca_descriptor(0x0500, 0x0101);
386        let es1_desc = sample_ca_descriptor(0x0B00, 0x0201);
387        let pmt = CaPmt {
388            list_management: CaPmtListManagement::Only,
389            program_number: 0x0001,
390            version_number: 1,
391            current_next_indicator: true,
392            cmd_id: Some(CaPmtCmdId::OkDescrambling),
393            program_ca_descriptors: &prog_desc,
394            streams: alloc::vec![
395                CaPmtStream {
396                    stream_type: 0x02,
397                    elementary_pid: 0x0200,
398                    cmd_id: Some(CaPmtCmdId::OkDescrambling),
399                    ca_descriptors: &es0_desc,
400                },
401                CaPmtStream {
402                    stream_type: 0x03,
403                    elementary_pid: 0x0201,
404                    cmd_id: Some(CaPmtCmdId::OkDescrambling),
405                    ca_descriptors: &es1_desc,
406                },
407            ],
408        };
409        let bytes = pmt.to_bytes();
410        let parsed = CaPmt::parse(&bytes).unwrap();
411        assert_eq!(parsed, pmt);
412        assert_eq!(parsed.streams.len(), 2);
413        assert_eq!(parsed.list_management.name(), "only");
414
415        // bite: mutate the program_number.
416        let mut other = pmt.clone();
417        other.program_number = 0x0002;
418        assert_ne!(bytes, other.to_bytes());
419
420        // bite: mutate a stream's pid.
421        let mut other2 = pmt.clone();
422        other2.streams[0].elementary_pid = 0x0300;
423        assert_ne!(bytes, other2.to_bytes());
424    }
425
426    #[test]
427    fn es_without_ca_info_omits_cmd_id() {
428        let pmt = CaPmt {
429            list_management: CaPmtListManagement::Add,
430            program_number: 7,
431            version_number: 0,
432            current_next_indicator: true,
433            cmd_id: None,
434            program_ca_descriptors: &[],
435            streams: alloc::vec![CaPmtStream {
436                stream_type: 0x1B,
437                elementary_pid: 0x00FF,
438                cmd_id: None,
439                ca_descriptors: &[],
440            }],
441        };
442        let bytes = pmt.to_bytes();
443        let parsed = CaPmt::parse(&bytes).unwrap();
444        assert_eq!(parsed, pmt);
445        assert!(parsed.streams[0].cmd_id.is_none());
446    }
447}