Skip to main content

dvb_ci/ci_plus/
ca_support.rs

1//! Conditional Access Support — CI Plus multi-stream `ca_pmt` / `ca_pmt_reply` —
2//! ETSI TS 103 205 V1.4.1 §6.4.4, Tables 14/16 (PDF pp. 38-40). See
3//! `docs/ts_103_205/ca-support.md`.
4//!
5//! For multi-stream functionality the CA Support resource gets a new
6//! `resource_type` (= 2, version = 1) in which `ca_pmt()` and `ca_pmt_reply()`
7//! are extended with a leading `LTS_id`; `ca_pmt()` additionally carries a
8//! `PMT_PID`. The apdu_tags are unchanged (`ca_pmt` `0x9F8032`, `ca_pmt_reply`
9//! `0x9F8033`).
10//!
11//! ## Deferred resource_id
12//!
13//! TS 103 205 §6.4.4.1 (render-verified) does **not** print a full 32-bit
14//! `resource_identifier` for this resource_type — only the prose
15//! "resource_type = 2, version = 1". The authoritative value is in CI Plus V1.3
16//! \[3\]. So these objects are provided as standalone, directly-constructible /
17//! parseable typed structs (full `Parse`/`Serialize`) and are **not** wired into
18//! [`crate::ci_plus::CiPlusApdu`]'s resource dispatch — there is no invented
19//! resource_id constant. A small [`CaSupportApdu::parse`] helper dispatches on
20//! the apdu_tag for callers that already know they are in a multi-stream CA
21//! Support session.
22//!
23//! These extended bodies reuse the EN 50221 value enums (`ca_pmt_list_management`,
24//! `ca_pmt_cmd_id`, `CA_enable`); Table 15 narrows `ca_pmt_list_management` to the
25//! `{Only=0x03, Update=0x05}` subset in multi-stream mode (a usage constraint —
26//! any value still round-trips via the full enum).
27
28use crate::error::{Error, Result};
29use crate::objects::ca_pmt::{CaPmtCmdId, CaPmtListManagement};
30use crate::objects::ca_pmt_reply::CaEnable;
31use crate::tag::{ApduTag, CA_PMT, CA_PMT_REPLY};
32use alloc::vec::Vec;
33use dvb_common::{Parse, Serialize};
34
35// Re-export the shared value enums so callers needn't reach into `objects`.
36pub use crate::objects::ca_pmt::{
37    CaPmtCmdId as MsCaPmtCmdId, CaPmtListManagement as MsCaPmtListManagement,
38};
39pub use crate::objects::ca_pmt_reply::CaEnable as MsCaEnable;
40
41// ---------------------------------------------------------------------------
42// ca_pmt (Table 14)
43// ---------------------------------------------------------------------------
44
45/// One elementary-stream entry of a CI Plus multi-stream `ca_pmt` (Table 14).
46#[derive(Debug, Clone, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize))]
48pub struct MsCaPmtStream<'a> {
49    /// MPEG-2 `stream_type`.
50    pub stream_type: u8,
51    /// 13-bit `elementary_PID`.
52    pub elementary_pid: u16,
53    /// `ca_pmt_cmd_id` for this ES — present only when `ES_info_length != 0`.
54    pub cmd_id: Option<CaPmtCmdId>,
55    /// ES-level `CA_descriptor()` loop, verbatim wire bytes (no `cmd_id`).
56    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
57    pub ca_descriptors: &'a [u8],
58}
59
60/// CI Plus multi-stream `ca_pmt()` (Table 14): Host → CICAM. Differs from the
61/// EN 50221 `ca_pmt` by the leading `LTS_id` and the added `PMT_PID`.
62#[derive(Debug, Clone, PartialEq, Eq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize))]
64pub struct MsCaPmt<'a> {
65    /// `LTS_id` (8) — Local TS identifier.
66    pub lts_id: u8,
67    /// `ca_pmt_list_management` (Table 15 narrows to `{Only, Update}`).
68    pub list_management: CaPmtListManagement,
69    /// `program_number` (16).
70    pub program_number: u16,
71    /// `PMT_PID` (13) — PID of the selected service's PMT.
72    pub pmt_pid: u16,
73    /// 5-bit `version_number`.
74    pub version_number: u8,
75    /// `current_next_indicator`.
76    pub current_next_indicator: bool,
77    /// Programme-level `ca_pmt_cmd_id` — present only when `program_info_length != 0`.
78    pub cmd_id: Option<CaPmtCmdId>,
79    /// Programme-level `CA_descriptor()` loop, verbatim wire bytes (no `cmd_id`).
80    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
81    pub program_ca_descriptors: &'a [u8],
82    /// Elementary streams in wire order.
83    #[cfg_attr(feature = "serde", serde(borrow))]
84    pub streams: Vec<MsCaPmtStream<'a>>,
85}
86
87// LTS_id(1) + list_management(1) + program_number(2) + reserved/PMT_PID(2) +
88// reserved/version/cni(1) + reserved/program_info_length(2).
89const MS_CA_PMT_PREFIX: usize = 1 + 1 + 2 + 2 + 1 + 2;
90// Per-ES: stream_type(1) + reserved/elem_pid(2) + reserved/ES_info_length(2).
91const MS_ES_PREFIX: usize = 5;
92
93fn info_block_len(cmd_id: Option<CaPmtCmdId>, descriptors: &[u8]) -> usize {
94    if cmd_id.is_some() || !descriptors.is_empty() {
95        1 + descriptors.len()
96    } else {
97        0
98    }
99}
100
101fn parse_cmd_and_descriptors<'a>(
102    body: &'a [u8],
103    pos: &mut usize,
104    info_length: usize,
105    what: &'static str,
106) -> Result<(Option<CaPmtCmdId>, &'a [u8])> {
107    if info_length == 0 {
108        return Ok((None, &body[..0]));
109    }
110    let end = *pos + info_length;
111    if end > body.len() {
112        return Err(Error::LengthMismatch {
113            what,
114            declared: info_length,
115            actual: body.len().saturating_sub(*pos),
116        });
117    }
118    let cmd_id = CaPmtCmdId::from_u8(body[*pos]);
119    let descriptors = &body[*pos + 1..end];
120    *pos = end;
121    Ok((Some(cmd_id), descriptors))
122}
123
124fn write_info_block(
125    cmd_id: Option<CaPmtCmdId>,
126    descriptors: &[u8],
127    buf: &mut [u8],
128) -> Result<usize> {
129    let len = info_block_len(cmd_id, descriptors);
130    if len == 0 {
131        return Ok(0);
132    }
133    if buf.len() < len {
134        return Err(Error::OutputBufferTooSmall {
135            need: len,
136            have: buf.len(),
137        });
138    }
139    buf[0] = cmd_id.unwrap_or(CaPmtCmdId::OkDescrambling).to_u8();
140    buf[1..len].copy_from_slice(descriptors);
141    Ok(len)
142}
143
144impl<'a> Parse<'a> for MsCaPmt<'a> {
145    type Error = Error;
146    fn parse(bytes: &'a [u8]) -> Result<Self> {
147        let body = crate::objects::parse_apdu_header(bytes, CA_PMT, "ms ca_pmt")?;
148        if body.len() < MS_CA_PMT_PREFIX {
149            return Err(Error::BufferTooShort {
150                need: MS_CA_PMT_PREFIX,
151                have: body.len(),
152                what: "ms ca_pmt prefix",
153            });
154        }
155        let lts_id = body[0];
156        let list_management = CaPmtListManagement::from_u8(body[1]);
157        let program_number = u16::from_be_bytes([body[2], body[3]]);
158        // reserved(3) + PMT_PID(13).
159        let pmt_pid = (((body[4] & 0x1F) as u16) << 8) | body[5] as u16;
160        // reserved(2) + version(5) + cni(1).
161        let version_number = (body[6] >> 1) & 0x1F;
162        let current_next_indicator = (body[6] & 0x01) != 0;
163        // reserved(4) + program_info_length(12).
164        let program_info_length = (((body[7] & 0x0F) as usize) << 8) | body[8] as usize;
165
166        let mut pos = MS_CA_PMT_PREFIX;
167        let (cmd_id, program_ca_descriptors) = parse_cmd_and_descriptors(
168            body,
169            &mut pos,
170            program_info_length,
171            "ms ca_pmt program_info",
172        )?;
173
174        let mut streams = Vec::new();
175        while pos < body.len() {
176            if pos + MS_ES_PREFIX > body.len() {
177                return Err(Error::BufferTooShort {
178                    need: pos + MS_ES_PREFIX,
179                    have: body.len(),
180                    what: "ms ca_pmt ES prefix",
181                });
182            }
183            let stream_type = body[pos];
184            let elementary_pid = (((body[pos + 1] & 0x1F) as u16) << 8) | body[pos + 2] as u16;
185            let es_info_length = (((body[pos + 3] & 0x0F) as usize) << 8) | body[pos + 4] as usize;
186            pos += MS_ES_PREFIX;
187            let (es_cmd, ca_descriptors) =
188                parse_cmd_and_descriptors(body, &mut pos, es_info_length, "ms ca_pmt ES_info")?;
189            streams.push(MsCaPmtStream {
190                stream_type,
191                elementary_pid,
192                cmd_id: es_cmd,
193                ca_descriptors,
194            });
195        }
196
197        Ok(Self {
198            lts_id,
199            list_management,
200            program_number,
201            pmt_pid,
202            version_number,
203            current_next_indicator,
204            cmd_id,
205            program_ca_descriptors,
206            streams,
207        })
208    }
209}
210
211impl Serialize for MsCaPmt<'_> {
212    type Error = Error;
213    fn serialized_len(&self) -> usize {
214        let mut body = MS_CA_PMT_PREFIX + info_block_len(self.cmd_id, self.program_ca_descriptors);
215        for s in &self.streams {
216            body += MS_ES_PREFIX + info_block_len(s.cmd_id, s.ca_descriptors);
217        }
218        crate::objects::apdu_len(body)
219    }
220    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
221        let program_info_length = info_block_len(self.cmd_id, self.program_ca_descriptors);
222        let mut body = MS_CA_PMT_PREFIX + program_info_length;
223        for s in &self.streams {
224            body += MS_ES_PREFIX + info_block_len(s.cmd_id, s.ca_descriptors);
225        }
226        let mut pos = crate::objects::write_apdu_header(CA_PMT, body, buf)?;
227        buf[pos] = self.lts_id;
228        buf[pos + 1] = self.list_management.to_u8();
229        buf[pos + 2..pos + 4].copy_from_slice(&self.program_number.to_be_bytes());
230        // reserved(3)='111', PMT_PID(13).
231        buf[pos + 4] = 0xE0 | ((self.pmt_pid >> 8) as u8 & 0x1F);
232        buf[pos + 5] = self.pmt_pid as u8;
233        // reserved(2)='11', version(5), current_next(1).
234        buf[pos + 6] =
235            0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
236        // reserved(4)='1111', program_info_length(12).
237        buf[pos + 7] = 0xF0 | ((program_info_length >> 8) as u8 & 0x0F);
238        buf[pos + 8] = program_info_length as u8;
239        pos += MS_CA_PMT_PREFIX;
240        pos += write_info_block(self.cmd_id, self.program_ca_descriptors, &mut buf[pos..])?;
241
242        for s in &self.streams {
243            let es_info_length = info_block_len(s.cmd_id, s.ca_descriptors);
244            buf[pos] = s.stream_type;
245            buf[pos + 1] = 0xE0 | ((s.elementary_pid >> 8) as u8 & 0x1F);
246            buf[pos + 2] = s.elementary_pid as u8;
247            buf[pos + 3] = 0xF0 | ((es_info_length >> 8) as u8 & 0x0F);
248            buf[pos + 4] = es_info_length as u8;
249            pos += MS_ES_PREFIX;
250            pos += write_info_block(s.cmd_id, s.ca_descriptors, &mut buf[pos..])?;
251        }
252        Ok(pos)
253    }
254}
255
256// ---------------------------------------------------------------------------
257// ca_pmt_reply (Table 16)
258// ---------------------------------------------------------------------------
259
260/// One ES entry of a CI Plus multi-stream `ca_pmt_reply` (Table 16).
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262#[cfg_attr(feature = "serde", derive(serde::Serialize))]
263pub struct MsCaPmtReplyStream {
264    /// 13-bit `elementary_PID`.
265    pub elementary_pid: u16,
266    /// ES-level `CA_enable` — `Some` iff the `CA_enable_flag` bit was set.
267    pub ca_enable: Option<CaEnable>,
268}
269
270/// CI Plus multi-stream `ca_pmt_reply()` (Table 16): CICAM → Host. Differs from
271/// the EN 50221 `ca_pmt_reply` by the leading `LTS_id`.
272#[derive(Debug, Clone, PartialEq, Eq)]
273#[cfg_attr(feature = "serde", derive(serde::Serialize))]
274pub struct MsCaPmtReply {
275    /// `LTS_id` (8) — Local TS identifier.
276    pub lts_id: u8,
277    /// `program_number` (16).
278    pub program_number: u16,
279    /// 5-bit `version_number`.
280    pub version_number: u8,
281    /// `current_next_indicator`.
282    pub current_next_indicator: bool,
283    /// Programme-level `CA_enable` — `Some` iff the programme `CA_enable_flag` bit
284    /// was set.
285    pub ca_enable: Option<CaEnable>,
286    /// Per-ES entries in wire order.
287    pub streams: Vec<MsCaPmtReplyStream>,
288}
289
290// LTS_id(1) + program_number(2) + reserved/version/cni(1) + flag/enable(1).
291const MS_REPLY_PREFIX: usize = 1 + 2 + 1 + 1;
292const MS_REPLY_ES_LEN: usize = 3; // reserved/elem_pid(2) + flag/enable(1)
293
294/// Encode a `CA_enable_flag` + 7-bit `CA_enable`/reserved byte (absent → `0x7F`).
295fn encode_enable_byte(enable: Option<CaEnable>) -> u8 {
296    match enable {
297        Some(e) => 0x80 | (e.to_u8() & 0x7F),
298        None => 0x7F,
299    }
300}
301
302impl<'a> Parse<'a> for MsCaPmtReply {
303    type Error = Error;
304    fn parse(bytes: &'a [u8]) -> Result<Self> {
305        let body = crate::objects::parse_apdu_header(bytes, CA_PMT_REPLY, "ms ca_pmt_reply")?;
306        if body.len() < MS_REPLY_PREFIX {
307            return Err(Error::BufferTooShort {
308                need: MS_REPLY_PREFIX,
309                have: body.len(),
310                what: "ms ca_pmt_reply prefix",
311            });
312        }
313        let lts_id = body[0];
314        let program_number = u16::from_be_bytes([body[1], body[2]]);
315        let version_number = (body[3] >> 1) & 0x1F;
316        let current_next_indicator = (body[3] & 0x01) != 0;
317        let ca_enable_flag = (body[4] & 0x80) != 0;
318        let ca_enable = if ca_enable_flag {
319            Some(CaEnable::from_u8(body[4] & 0x7F))
320        } else {
321            None
322        };
323
324        let mut pos = MS_REPLY_PREFIX;
325        let mut streams = Vec::new();
326        while pos < body.len() {
327            if pos + MS_REPLY_ES_LEN > body.len() {
328                return Err(Error::BufferTooShort {
329                    need: pos + MS_REPLY_ES_LEN,
330                    have: body.len(),
331                    what: "ms ca_pmt_reply ES",
332                });
333            }
334            let elementary_pid = (((body[pos] & 0x1F) as u16) << 8) | body[pos + 1] as u16;
335            let es_flag = (body[pos + 2] & 0x80) != 0;
336            let es_enable = if es_flag {
337                Some(CaEnable::from_u8(body[pos + 2] & 0x7F))
338            } else {
339                None
340            };
341            streams.push(MsCaPmtReplyStream {
342                elementary_pid,
343                ca_enable: es_enable,
344            });
345            pos += MS_REPLY_ES_LEN;
346        }
347
348        Ok(Self {
349            lts_id,
350            program_number,
351            version_number,
352            current_next_indicator,
353            ca_enable,
354            streams,
355        })
356    }
357}
358
359impl Serialize for MsCaPmtReply {
360    type Error = Error;
361    fn serialized_len(&self) -> usize {
362        crate::objects::apdu_len(MS_REPLY_PREFIX + self.streams.len() * MS_REPLY_ES_LEN)
363    }
364    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
365        let body = MS_REPLY_PREFIX + self.streams.len() * MS_REPLY_ES_LEN;
366        let mut pos = crate::objects::write_apdu_header(CA_PMT_REPLY, body, buf)?;
367        buf[pos] = self.lts_id;
368        buf[pos + 1..pos + 3].copy_from_slice(&self.program_number.to_be_bytes());
369        // reserved(2)='11', version(5), current_next(1).
370        buf[pos + 3] =
371            0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
372        buf[pos + 4] = encode_enable_byte(self.ca_enable);
373        pos += MS_REPLY_PREFIX;
374        for s in &self.streams {
375            buf[pos] = 0xE0 | ((s.elementary_pid >> 8) as u8 & 0x1F);
376            buf[pos + 1] = s.elementary_pid as u8;
377            buf[pos + 2] = encode_enable_byte(s.ca_enable);
378            pos += MS_REPLY_ES_LEN;
379        }
380        Ok(pos)
381    }
382}
383
384// ---------------------------------------------------------------------------
385// apdu-tag dispatch helper (no resource_id — see module doc)
386// ---------------------------------------------------------------------------
387
388/// A parsed CI Plus multi-stream CA-support object.
389///
390/// There is intentionally **no** `resource_id`-keyed entry point for these
391/// objects: TS 103 205 does not print the resource_id (see the module doc), so
392/// dispatch is on the apdu_tag alone, for callers already in a multi-stream CA
393/// Support session.
394#[derive(Debug, Clone, PartialEq, Eq)]
395#[cfg_attr(feature = "serde", derive(serde::Serialize))]
396#[non_exhaustive]
397pub enum CaSupportApdu<'a> {
398    /// `ca_pmt` (`9F 80 32`), CI Plus multi-stream variant.
399    CaPmt(MsCaPmt<'a>),
400    /// `ca_pmt_reply` (`9F 80 33`), CI Plus multi-stream variant.
401    CaPmtReply(MsCaPmtReply),
402}
403
404impl<'a> CaSupportApdu<'a> {
405    /// Parse a CI Plus multi-stream CA-support APDU by its apdu_tag.
406    pub fn parse(body: &'a [u8]) -> Result<Self> {
407        if body.len() < 3 {
408            return Err(Error::BufferTooShort {
409                need: 3,
410                have: body.len(),
411                what: "ca_support apdu_tag",
412            });
413        }
414        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
415        match t {
416            CA_PMT => Ok(Self::CaPmt(MsCaPmt::parse(body)?)),
417            CA_PMT_REPLY => Ok(Self::CaPmtReply(MsCaPmtReply::parse(body)?)),
418            _ => Err(Error::UnexpectedApduTag {
419                got: t.as_u24(),
420                expected: CA_PMT.as_u24(),
421                what: "ca_support",
422            }),
423        }
424    }
425}
426
427impl Serialize for CaSupportApdu<'_> {
428    type Error = Error;
429    fn serialized_len(&self) -> usize {
430        match self {
431            Self::CaPmt(o) => o.serialized_len(),
432            Self::CaPmtReply(o) => o.serialized_len(),
433        }
434    }
435    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
436        match self {
437            Self::CaPmt(o) => o.serialize_into(buf),
438            Self::CaPmtReply(o) => o.serialize_into(buf),
439        }
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use crate::objects::ca_pmt::CA_DESCRIPTOR_TAG;
447
448    fn sample_ca_descriptor(ca_system_id: u16, pid: u16) -> [u8; 6] {
449        [
450            CA_DESCRIPTOR_TAG,
451            0x04,
452            (ca_system_id >> 8) as u8,
453            ca_system_id as u8,
454            0xE0 | ((pid >> 8) as u8 & 0x1F),
455            pid as u8,
456        ]
457    }
458
459    #[test]
460    fn ms_ca_pmt_round_trips_and_bites() {
461        let prog = sample_ca_descriptor(0x1234, 0x0100);
462        let es_desc = sample_ca_descriptor(0x1234, 0x0101);
463        let pmt = MsCaPmt {
464            lts_id: 0x02,
465            list_management: CaPmtListManagement::Only,
466            program_number: 0x0001,
467            pmt_pid: 0x0064,
468            version_number: 1,
469            current_next_indicator: true,
470            cmd_id: Some(CaPmtCmdId::OkDescrambling),
471            program_ca_descriptors: &prog,
472            streams: alloc::vec![MsCaPmtStream {
473                stream_type: 0x02,
474                elementary_pid: 0x0200,
475                cmd_id: Some(CaPmtCmdId::OkDescrambling),
476                ca_descriptors: &es_desc,
477            }],
478        };
479        let bytes = pmt.to_bytes();
480        // Hand-computed body:
481        //   LTS_id 02 | list_mgmt 03 | prog_num 00 01 | reserved+PMT_PID E0 64
482        //   reserved+ver1+cni1 = 0xC0|0x02|0x01 = 0xC3
483        //   reserved+prog_info_len(7) = F0 07 ; cmd_id 01 + 6 desc bytes
484        //   ES: stream_type 02 | E0 00 (pid 0x200 -> hi 0x02) wait pid 0x0200
485        let pid = 0x0200u16;
486        let es_hi = 0xE0 | ((pid >> 8) as u8 & 0x1F);
487        let expected = {
488            let mut v = alloc::vec![
489                0x9F, 0x80, 0x32, // tag
490                0x1C, // length (28 body bytes)
491                0x02, // LTS_id
492                0x03, // list_management Only
493                0x00, 0x01, // program_number
494                0xE0, 0x64, // reserved + PMT_PID 0x0064
495                0xC3, // reserved + version 1 + cni 1
496                0xF0, 0x07, // reserved + program_info_length 7
497                0x01, // program ca_pmt_cmd_id ok_descrambling
498            ];
499            v.extend_from_slice(&prog); // 6 bytes
500            v.extend_from_slice(&[0x02, es_hi, 0x00, 0xF0, 0x07, 0x01]); // ES prefix + es_info_len + cmd
501            v.extend_from_slice(&es_desc); // 6 bytes
502            v
503        };
504        assert_eq!(bytes, expected);
505        assert_eq!(MsCaPmt::parse(&bytes).unwrap(), pmt);
506        // Field-mutation: change PMT_PID.
507        let mut other = pmt.clone();
508        other.pmt_pid = 0x0065;
509        assert_ne!(bytes, other.to_bytes());
510    }
511
512    #[test]
513    fn ms_ca_pmt_no_descriptors() {
514        let pmt = MsCaPmt {
515            lts_id: 0x00,
516            list_management: CaPmtListManagement::Update,
517            program_number: 0x0009,
518            pmt_pid: 0x1FFF,
519            version_number: 0,
520            current_next_indicator: true,
521            cmd_id: None,
522            program_ca_descriptors: &[],
523            streams: Vec::new(),
524        };
525        let bytes = pmt.to_bytes();
526        // PMT_PID 0x1FFF -> hi 0xE0|0x1F=0xFF, lo 0xFF; prog_info_len 0 -> F0 00.
527        assert_eq!(
528            bytes,
529            [0x9F, 0x80, 0x32, 0x09, 0x00, 0x05, 0x00, 0x09, 0xFF, 0xFF, 0xC1, 0xF0, 0x00]
530        );
531        assert_eq!(MsCaPmt::parse(&bytes).unwrap(), pmt);
532    }
533
534    #[test]
535    fn ms_ca_pmt_reply_round_trips_and_bites() {
536        let reply = MsCaPmtReply {
537            lts_id: 0x03,
538            program_number: 0x0001,
539            version_number: 1,
540            current_next_indicator: true,
541            ca_enable: Some(CaEnable::Possible),
542            streams: alloc::vec![
543                MsCaPmtReplyStream {
544                    elementary_pid: 0x0200,
545                    ca_enable: Some(CaEnable::Possible),
546                },
547                MsCaPmtReplyStream {
548                    elementary_pid: 0x0201,
549                    ca_enable: None,
550                },
551            ],
552        };
553        let bytes = reply.to_bytes();
554        let expected = [
555            0x9F, 0x80, 0x33, // tag
556            0x0B, // length (11 body bytes)
557            0x03, // LTS_id
558            0x00, 0x01, // program_number
559            0xC3, // reserved + version 1 + cni 1
560            0x81, // flag=1 + CA_enable Possible(0x01)
561            0xE2, 0x00, 0x81, // ES pid 0x200 + flag/enable Possible
562            0xE2, 0x01, 0x7F, // ES pid 0x201 + no enable (0x7F)
563        ];
564        assert_eq!(bytes, expected);
565        assert_eq!(MsCaPmtReply::parse(&bytes).unwrap(), reply);
566        // Field-mutation: change LTS_id.
567        let mut other = reply.clone();
568        other.lts_id = 0x04;
569        assert_ne!(bytes, other.to_bytes());
570        assert_eq!(other.to_bytes()[4], 0x04);
571    }
572
573    #[test]
574    fn dispatch_helper_routes_by_tag() {
575        let pmt = MsCaPmt {
576            lts_id: 0,
577            list_management: CaPmtListManagement::Only,
578            program_number: 1,
579            pmt_pid: 0x64,
580            version_number: 0,
581            current_next_indicator: true,
582            cmd_id: None,
583            program_ca_descriptors: &[],
584            streams: Vec::new(),
585        };
586        let bytes = pmt.to_bytes();
587        let parsed = CaSupportApdu::parse(&bytes).unwrap();
588        assert!(matches!(parsed, CaSupportApdu::CaPmt(_)));
589        assert_eq!(parsed.to_bytes(), bytes);
590
591        let reply = MsCaPmtReply {
592            lts_id: 1,
593            program_number: 1,
594            version_number: 0,
595            current_next_indicator: true,
596            ca_enable: None,
597            streams: Vec::new(),
598        };
599        let rb = reply.to_bytes();
600        assert!(matches!(
601            CaSupportApdu::parse(&rb).unwrap(),
602            CaSupportApdu::CaPmtReply(_)
603        ));
604    }
605}