Skip to main content

dvb_ci/
builder.rs

1//! `CA_PMT` builder — project a `dvb-si` PMT into the `ca_pmt` object handed to
2//! a CICAM — ETSI EN 50221 §8.4.3.4 (Table 25), per `docs/en_50221/ca-pmt.md`.
3//!
4//! The host extracts the PMT, strips every descriptor that is not a
5//! `CA_descriptor()` (ISO/IEC 13818-1 §2.6.16, tag `0x09`), and keeps the
6//! surviving CA descriptors at programme and elementary-stream level (per
7//! `ca-pmt.md` field notes: "Only CA_descriptors are present; all other
8//! descriptors are removed from the PMT by the host"). Each surviving descriptor
9//! loop is prefixed with a `ca_pmt_cmd_id` byte.
10//!
11//! The filtered descriptor bytes do not exist as a contiguous slice in the
12//! source PMT, so [`build_ca_pmt`] returns an owned [`CaPmtBuilt`] that holds the
13//! filtered loops; borrow a [`CaPmt`] view from it with
14//! [`CaPmtBuilt::as_ca_pmt`], or take the finished wire bytes with
15//! [`CaPmtBuilt::to_bytes`].
16//!
17//! [`CaPmt`]: crate::objects::ca_pmt::CaPmt
18
19use crate::objects::ca_pmt::{
20    CA_DESCRIPTOR_TAG, CaPmt, CaPmtCmdId, CaPmtListManagement, CaPmtStream,
21};
22use alloc::vec::Vec;
23use broadcast_common::Serialize;
24use dvb_si::descriptors::DescriptorLoop;
25use dvb_si::tables::pmt::PmtSection;
26
27/// An owned, CA-only projection of a PMT. Holds the filtered `CA_descriptor`
28/// loops (programme + per-ES) so a borrowed [`CaPmt`] can be reconstructed.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct CaPmtBuilt {
31    list_management: CaPmtListManagement,
32    program_number: u16,
33    version_number: u8,
34    current_next_indicator: bool,
35    cmd_id: CaPmtCmdId,
36    program_ca_descriptors: Vec<u8>,
37    streams: Vec<BuiltStream>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41struct BuiltStream {
42    stream_type: u8,
43    elementary_pid: u16,
44    ca_descriptors: Vec<u8>,
45}
46
47/// The `CA_system_id` is the first two bytes of a `CA_descriptor` body
48/// (ISO/IEC 13818-1 §2.6.16), big-endian.
49fn ca_system_id(body: &[u8]) -> Option<u16> {
50    body.first_chunk::<2>().map(|b| u16::from_be_bytes(*b))
51}
52
53/// Filter a descriptor loop to its `CA_descriptor()` entries (tag `0x09`),
54/// concatenating the surviving entries' verbatim TLV wire bytes. When `allowed`
55/// is `Some`, a `CA_descriptor` is kept only if its `CA_system_id` is in the
56/// list (a descriptor with no readable `CA_system_id` is dropped); `None` keeps
57/// every `CA_descriptor`.
58fn ca_descriptors_filtered(loop_: &DescriptorLoop<'_>, allowed: Option<&[u16]>) -> Vec<u8> {
59    let mut out = Vec::new();
60    for (tag, body) in loop_.raw_tags() {
61        if tag != CA_DESCRIPTOR_TAG {
62            continue;
63        }
64        if let Some(allow) = allowed {
65            match ca_system_id(body) {
66                Some(id) if allow.contains(&id) => {}
67                _ => continue,
68            }
69        }
70        // Re-emit the full TLV: tag, length, body. raw_tags has already
71        // validated `body.len()` fits in the declared length byte.
72        out.push(tag);
73        out.push(body.len() as u8);
74        out.extend_from_slice(body);
75    }
76    out
77}
78
79/// Drop every `CA_descriptor` TLV in `buf` whose `CA_system_id` is not in
80/// `allowed`. `buf` holds only tag-`0x09` TLVs (a built CA-descriptor loop).
81fn retain_loop(buf: &mut Vec<u8>, allowed: &[u16]) {
82    let mut out = Vec::new();
83    let mut pos = 0;
84    while pos + 2 <= buf.len() {
85        let end = pos + 2 + buf[pos + 1] as usize;
86        if end > buf.len() {
87            break;
88        }
89        if ca_system_id(&buf[pos + 2..end]).is_some_and(|id| allowed.contains(&id)) {
90            out.extend_from_slice(&buf[pos..end]);
91        }
92        pos = end;
93    }
94    *buf = out;
95}
96
97/// Build the `ca_pmt` projection of `pmt` for the given list-management and
98/// command-id. Strips all non-CA descriptors; keeps `CA_descriptor`s at
99/// programme and ES level.
100///
101/// Every elementary stream of the PMT is carried; a stream with no surviving CA
102/// descriptor has no `ca_pmt_cmd_id` (its `ES_info_length` is 0 per Table 25),
103/// so the CAM sees the full component list while only CA-bearing streams carry
104/// CA info.
105#[must_use]
106pub fn build_ca_pmt(
107    pmt: &PmtSection<'_>,
108    list_management: CaPmtListManagement,
109    cmd_id: CaPmtCmdId,
110) -> CaPmtBuilt {
111    build(pmt, None, list_management, cmd_id)
112}
113
114/// Build the `ca_pmt` projection of `pmt`, keeping only `CA_descriptor`s whose
115/// `CA_system_id` is in `allowed` (the intersection of the PMT's CAIDs and the
116/// CAM's advertised CAIDs, from its `ca_info`).
117///
118/// A CICAM rejects a `ca_pmt` carrying a `CA_descriptor` for a `CA_system_id` it
119/// does not support, declining even the streams it could descramble — so the
120/// host should transmit only the CAIDs the module advertised. `allowed` empty
121/// drops every `CA_descriptor`. [`build_ca_pmt`] is this with "allow all".
122#[must_use]
123pub fn build_ca_pmt_for_caids(
124    pmt: &PmtSection<'_>,
125    allowed: &[u16],
126    list_management: CaPmtListManagement,
127    cmd_id: CaPmtCmdId,
128) -> CaPmtBuilt {
129    build(pmt, Some(allowed), list_management, cmd_id)
130}
131
132fn build(
133    pmt: &PmtSection<'_>,
134    allowed: Option<&[u16]>,
135    list_management: CaPmtListManagement,
136    cmd_id: CaPmtCmdId,
137) -> CaPmtBuilt {
138    let program_ca_descriptors = ca_descriptors_filtered(&pmt.program_info, allowed);
139    let streams = pmt
140        .streams
141        .iter()
142        .map(|s| BuiltStream {
143            stream_type: s.stream_type.to_u8(),
144            elementary_pid: s.elementary_pid,
145            ca_descriptors: ca_descriptors_filtered(&s.es_info, allowed),
146        })
147        .collect();
148    CaPmtBuilt {
149        list_management,
150        program_number: pmt.program_number,
151        version_number: pmt.version_number,
152        current_next_indicator: pmt.current_next_indicator,
153        cmd_id,
154        program_ca_descriptors,
155        streams,
156    }
157}
158
159impl CaPmtBuilt {
160    /// Borrow a [`CaPmt`] view over the owned filtered descriptor loops. The
161    /// `ca_pmt_cmd_id` is attached to a loop only when that loop has surviving
162    /// CA descriptors (matching Table 25's `..._info_length != 0` guard).
163    #[must_use]
164    pub fn as_ca_pmt(&self) -> CaPmt<'_> {
165        CaPmt {
166            list_management: self.list_management,
167            program_number: self.program_number,
168            version_number: self.version_number,
169            current_next_indicator: self.current_next_indicator,
170            cmd_id: cmd_for(self.cmd_id, &self.program_ca_descriptors),
171            program_ca_descriptors: &self.program_ca_descriptors,
172            streams: self
173                .streams
174                .iter()
175                .map(|s| CaPmtStream {
176                    stream_type: s.stream_type,
177                    elementary_pid: s.elementary_pid,
178                    cmd_id: cmd_for(self.cmd_id, &s.ca_descriptors),
179                    ca_descriptors: &s.ca_descriptors,
180                })
181                .collect(),
182        }
183    }
184
185    /// Drop every `CA_descriptor` (programme- and ES-level) whose `CA_system_id`
186    /// is not in `allowed`. Use this to post-filter a `ca_pmt` to the CAM's
187    /// advertised CAIDs once its `ca_info` is known (equivalent to having built
188    /// it with [`build_ca_pmt_for_caids`]).
189    pub fn retain_caids(&mut self, allowed: &[u16]) {
190        retain_loop(&mut self.program_ca_descriptors, allowed);
191        for s in &mut self.streams {
192            retain_loop(&mut s.ca_descriptors, allowed);
193        }
194    }
195
196    /// Serialize the finished `ca_pmt` APDU (tag `9F 80 32` + length + body).
197    #[must_use]
198    pub fn to_bytes(&self) -> Vec<u8> {
199        self.as_ca_pmt().to_bytes()
200    }
201}
202
203/// A `ca_pmt_cmd_id` accompanies a descriptor loop only when that loop is
204/// non-empty (otherwise `..._info_length` is 0 and no cmd_id byte is present).
205fn cmd_for(cmd_id: CaPmtCmdId, descriptors: &[u8]) -> Option<CaPmtCmdId> {
206    if descriptors.is_empty() {
207        None
208    } else {
209        Some(cmd_id)
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::objects::ca_pmt::CaPmt;
217    use alloc::vec;
218    use broadcast_common::Parse;
219
220    #[test]
221    fn builds_from_real_pmt_fixture() {
222        // The m6-single.ts fixture in dvb-si carries a real broadcast PMT with
223        // CA descriptors. Build a PMT section from a hand-rolled wire buffer that
224        // mirrors a real CA-protected service: program CA_descriptor + two ES,
225        // one scrambled (with ES CA_descriptor) and one clear.
226        let pmt_bytes = build_test_pmt();
227        let pmt = PmtSection::parse(&pmt_bytes).expect("valid PMT");
228
229        let built = build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling);
230        let bytes = built.to_bytes();
231
232        // Round-trips through the ca_pmt parser.
233        let parsed = CaPmt::parse(&bytes).unwrap();
234        let view = built.as_ca_pmt();
235        assert_eq!(parsed, view);
236
237        // Programme-level CA descriptor survived; non-CA descriptors stripped.
238        assert!(!parsed.program_ca_descriptors.is_empty());
239        assert_eq!(parsed.program_ca_descriptors[0], CA_DESCRIPTOR_TAG);
240        assert_eq!(parsed.cmd_id, Some(CaPmtCmdId::OkDescrambling));
241
242        // Both ES carried; only the scrambled one has CA info + cmd_id.
243        assert_eq!(parsed.streams.len(), 2);
244        assert!(!parsed.streams[0].ca_descriptors.is_empty());
245        assert_eq!(parsed.streams[0].cmd_id, Some(CaPmtCmdId::OkDescrambling));
246        assert!(parsed.streams[1].ca_descriptors.is_empty());
247        assert_eq!(parsed.streams[1].cmd_id, None);
248    }
249
250    #[test]
251    fn strips_non_ca_descriptors() {
252        let pmt_bytes = build_test_pmt();
253        let pmt = PmtSection::parse(&pmt_bytes).unwrap();
254        let built = build_ca_pmt(&pmt, CaPmtListManagement::Add, CaPmtCmdId::Query);
255        let view = built.as_ca_pmt();
256        // The source program_info had a non-CA descriptor too; only 0x09 remains.
257        let mut pos = 0;
258        let d = view.program_ca_descriptors;
259        while pos < d.len() {
260            assert_eq!(d[pos], CA_DESCRIPTOR_TAG);
261            pos += 2 + d[pos + 1] as usize;
262        }
263    }
264
265    /// Collect the `CA_system_id`s present in a built CA-descriptor loop.
266    fn caids(buf: &[u8]) -> Vec<u16> {
267        let mut ids = Vec::new();
268        let mut pos = 0;
269        while pos + 2 <= buf.len() {
270            let end = pos + 2 + buf[pos + 1] as usize;
271            ids.push(u16::from_be_bytes([buf[pos + 2], buf[pos + 3]]));
272            pos = end;
273        }
274        ids
275    }
276
277    #[test]
278    fn for_caids_keeps_only_allowed_system_ids() {
279        let pmt_bytes = build_test_pmt();
280        let pmt = PmtSection::parse(&pmt_bytes).unwrap();
281
282        // Programme loop has CAIDs {0x0500, 0x1800}; allow only 0x0500.
283        let built = build_ca_pmt_for_caids(
284            &pmt,
285            &[0x0500],
286            CaPmtListManagement::Only,
287            CaPmtCmdId::OkDescrambling,
288        );
289        assert_eq!(caids(&built.program_ca_descriptors), vec![0x0500]);
290
291        // ES0's CA_descriptor (0x0500) survives; ES1 had none.
292        let view = built.as_ca_pmt();
293        assert!(!view.streams[0].ca_descriptors.is_empty());
294        assert!(view.streams[1].ca_descriptors.is_empty());
295        // Round-trips.
296        assert_eq!(CaPmt::parse(&built.to_bytes()).unwrap(), view);
297    }
298
299    #[test]
300    fn for_caids_empty_allowlist_drops_all_ca() {
301        let pmt_bytes = build_test_pmt();
302        let pmt = PmtSection::parse(&pmt_bytes).unwrap();
303        let built = build_ca_pmt_for_caids(&pmt, &[], CaPmtListManagement::Only, CaPmtCmdId::Query);
304        assert!(built.program_ca_descriptors.is_empty());
305        // With no CA descriptors anywhere, no cmd_id byte is emitted.
306        let view = built.as_ca_pmt();
307        assert_eq!(view.cmd_id, None);
308        assert!(view.streams.iter().all(|s| s.cmd_id.is_none()));
309    }
310
311    #[test]
312    fn retain_caids_matches_the_filtering_constructor() {
313        let pmt_bytes = build_test_pmt();
314        let pmt = PmtSection::parse(&pmt_bytes).unwrap();
315        let allow = [0x1800u16];
316
317        let mut post = build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling);
318        post.retain_caids(&allow);
319        let pre = build_ca_pmt_for_caids(
320            &pmt,
321            &allow,
322            CaPmtListManagement::Only,
323            CaPmtCmdId::OkDescrambling,
324        );
325        assert_eq!(post, pre);
326        // 0x1800 only existed at programme level → ES loops emptied.
327        assert_eq!(caids(&post.program_ca_descriptors), vec![0x1800]);
328    }
329
330    // --- helper: assemble a small but realistic PMT with CA descriptors ---
331
332    fn ca_descriptor(ca_system_id: u16, pid: u16) -> [u8; 6] {
333        [
334            0x09,
335            0x04,
336            (ca_system_id >> 8) as u8,
337            ca_system_id as u8,
338            0xE0 | ((pid >> 8) as u8 & 0x1F),
339            pid as u8,
340        ]
341    }
342
343    fn build_test_pmt() -> Vec<u8> {
344        // program_info: two CA_descriptors (CAIDs 0x0500 and 0x1800) + a (non-CA)
345        // registration descriptor(0x05).
346        let prog_ca = ca_descriptor(0x0500, 0x0100);
347        let prog_ca2 = ca_descriptor(0x1800, 0x0110);
348        let reg = [0x05u8, 0x04, b'H', b'D', b'M', b'V'];
349        let mut program_info = Vec::new();
350        program_info.extend_from_slice(&prog_ca);
351        program_info.extend_from_slice(&prog_ca2);
352        program_info.extend_from_slice(&reg);
353
354        // ES0: scrambled video, stream_type 0x02, pid 0x0200, with ES CA_descriptor.
355        let es0_ca = ca_descriptor(0x0500, 0x0101);
356        // ES1: clear audio, stream_type 0x03, pid 0x0201, only a language descriptor.
357        let lang = [0x0Au8, 0x04, b'e', b'n', b'g', 0x00];
358
359        let mut body = Vec::new();
360        // table_id 0x02
361        body.push(0x02);
362        // section_length placeholder (filled later): 2 bytes
363        body.push(0);
364        body.push(0);
365        // program_number 0x0001
366        body.extend_from_slice(&[0x00, 0x01]);
367        // reserved(2)|version(5)|cni(1): version 1, cni 1 -> 0b110000_11 = 0xC3
368        body.push(0xC3);
369        // section_number, last_section_number
370        body.push(0x00);
371        body.push(0x00);
372        // reserved(3)|PCR_PID(13): pid 0x0200
373        body.push(0xE0 | 0x02);
374        body.push(0x00);
375        // reserved(4)|program_info_length(12)
376        let pil = program_info.len();
377        body.push(0xF0 | ((pil >> 8) as u8 & 0x0F));
378        body.push(pil as u8);
379        body.extend_from_slice(&program_info);
380
381        // ES0
382        body.push(0x02); // stream_type
383        body.push(0xE0 | 0x02); // pid 0x0200
384        body.push(0x00);
385        body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
386        body.push(es0_ca.len() as u8);
387        body.extend_from_slice(&es0_ca);
388
389        // ES1
390        body.push(0x03);
391        body.push(0xE0 | 0x02); // pid 0x0201
392        body.push(0x01);
393        body.push(0xF0 | ((lang.len() >> 8) as u8 & 0x0F));
394        body.push(lang.len() as u8);
395        body.extend_from_slice(&lang);
396
397        // Now fix section_length = (bytes after the length field) + CRC(4).
398        let section_length = body.len() - 3 + 4;
399        body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
400        body[2] = section_length as u8;
401
402        // Append a CRC (the parser validates length, not CRC for construction;
403        // compute the real MPEG-2 CRC so the section is well-formed).
404        let crc = broadcast_common::crc32_mpeg2::compute(&body);
405        body.extend_from_slice(&crc.to_be_bytes());
406        body
407    }
408}