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    CaPmt, CaPmtCmdId, CaPmtListManagement, CaPmtStream, CA_DESCRIPTOR_TAG,
21};
22use alloc::vec::Vec;
23use dvb_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/// Filter a descriptor loop to only its `CA_descriptor()` entries (tag `0x09`),
48/// concatenating the surviving entries' verbatim TLV wire bytes.
49fn ca_descriptors_only(loop_: &DescriptorLoop<'_>) -> Vec<u8> {
50    let mut out = Vec::new();
51    for (tag, body) in loop_.raw_tags() {
52        if tag == CA_DESCRIPTOR_TAG {
53            // Re-emit the full TLV: tag, length, body. raw_tags has already
54            // validated `body.len()` fits in the declared length byte.
55            out.push(tag);
56            out.push(body.len() as u8);
57            out.extend_from_slice(body);
58        }
59    }
60    out
61}
62
63/// Build the `ca_pmt` projection of `pmt` for the given list-management and
64/// command-id. Strips all non-CA descriptors; keeps `CA_descriptor`s at
65/// programme and ES level.
66///
67/// Every elementary stream of the PMT is carried; a stream with no surviving CA
68/// descriptor has no `ca_pmt_cmd_id` (its `ES_info_length` is 0 per Table 25),
69/// so the CAM sees the full component list while only CA-bearing streams carry
70/// CA info.
71#[must_use]
72pub fn build_ca_pmt(
73    pmt: &PmtSection<'_>,
74    list_management: CaPmtListManagement,
75    cmd_id: CaPmtCmdId,
76) -> CaPmtBuilt {
77    let program_ca_descriptors = ca_descriptors_only(&pmt.program_info);
78    let streams = pmt
79        .streams
80        .iter()
81        .map(|s| BuiltStream {
82            stream_type: s.stream_type.to_u8(),
83            elementary_pid: s.elementary_pid,
84            ca_descriptors: ca_descriptors_only(&s.es_info),
85        })
86        .collect();
87    CaPmtBuilt {
88        list_management,
89        program_number: pmt.program_number,
90        version_number: pmt.version_number,
91        current_next_indicator: pmt.current_next_indicator,
92        cmd_id,
93        program_ca_descriptors,
94        streams,
95    }
96}
97
98impl CaPmtBuilt {
99    /// Borrow a [`CaPmt`] view over the owned filtered descriptor loops. The
100    /// `ca_pmt_cmd_id` is attached to a loop only when that loop has surviving
101    /// CA descriptors (matching Table 25's `..._info_length != 0` guard).
102    #[must_use]
103    pub fn as_ca_pmt(&self) -> CaPmt<'_> {
104        CaPmt {
105            list_management: self.list_management,
106            program_number: self.program_number,
107            version_number: self.version_number,
108            current_next_indicator: self.current_next_indicator,
109            cmd_id: cmd_for(self.cmd_id, &self.program_ca_descriptors),
110            program_ca_descriptors: &self.program_ca_descriptors,
111            streams: self
112                .streams
113                .iter()
114                .map(|s| CaPmtStream {
115                    stream_type: s.stream_type,
116                    elementary_pid: s.elementary_pid,
117                    cmd_id: cmd_for(self.cmd_id, &s.ca_descriptors),
118                    ca_descriptors: &s.ca_descriptors,
119                })
120                .collect(),
121        }
122    }
123
124    /// Serialize the finished `ca_pmt` APDU (tag `9F 80 32` + length + body).
125    #[must_use]
126    pub fn to_bytes(&self) -> Vec<u8> {
127        self.as_ca_pmt().to_bytes()
128    }
129}
130
131/// A `ca_pmt_cmd_id` accompanies a descriptor loop only when that loop is
132/// non-empty (otherwise `..._info_length` is 0 and no cmd_id byte is present).
133fn cmd_for(cmd_id: CaPmtCmdId, descriptors: &[u8]) -> Option<CaPmtCmdId> {
134    if descriptors.is_empty() {
135        None
136    } else {
137        Some(cmd_id)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::objects::ca_pmt::CaPmt;
145    use dvb_common::Parse;
146
147    #[test]
148    fn builds_from_real_pmt_fixture() {
149        // The m6-single.ts fixture in dvb-si carries a real broadcast PMT with
150        // CA descriptors. Build a PMT section from a hand-rolled wire buffer that
151        // mirrors a real CA-protected service: program CA_descriptor + two ES,
152        // one scrambled (with ES CA_descriptor) and one clear.
153        let pmt_bytes = build_test_pmt();
154        let pmt = PmtSection::parse(&pmt_bytes).expect("valid PMT");
155
156        let built = build_ca_pmt(&pmt, CaPmtListManagement::Only, CaPmtCmdId::OkDescrambling);
157        let bytes = built.to_bytes();
158
159        // Round-trips through the ca_pmt parser.
160        let parsed = CaPmt::parse(&bytes).unwrap();
161        let view = built.as_ca_pmt();
162        assert_eq!(parsed, view);
163
164        // Programme-level CA descriptor survived; non-CA descriptors stripped.
165        assert!(!parsed.program_ca_descriptors.is_empty());
166        assert_eq!(parsed.program_ca_descriptors[0], CA_DESCRIPTOR_TAG);
167        assert_eq!(parsed.cmd_id, Some(CaPmtCmdId::OkDescrambling));
168
169        // Both ES carried; only the scrambled one has CA info + cmd_id.
170        assert_eq!(parsed.streams.len(), 2);
171        assert!(!parsed.streams[0].ca_descriptors.is_empty());
172        assert_eq!(parsed.streams[0].cmd_id, Some(CaPmtCmdId::OkDescrambling));
173        assert!(parsed.streams[1].ca_descriptors.is_empty());
174        assert_eq!(parsed.streams[1].cmd_id, None);
175    }
176
177    #[test]
178    fn strips_non_ca_descriptors() {
179        let pmt_bytes = build_test_pmt();
180        let pmt = PmtSection::parse(&pmt_bytes).unwrap();
181        let built = build_ca_pmt(&pmt, CaPmtListManagement::Add, CaPmtCmdId::Query);
182        let view = built.as_ca_pmt();
183        // The source program_info had a non-CA descriptor too; only 0x09 remains.
184        let mut pos = 0;
185        let d = view.program_ca_descriptors;
186        while pos < d.len() {
187            assert_eq!(d[pos], CA_DESCRIPTOR_TAG);
188            pos += 2 + d[pos + 1] as usize;
189        }
190    }
191
192    // --- helper: assemble a small but realistic PMT with CA descriptors ---
193
194    fn ca_descriptor(ca_system_id: u16, pid: u16) -> [u8; 6] {
195        [
196            0x09,
197            0x04,
198            (ca_system_id >> 8) as u8,
199            ca_system_id as u8,
200            0xE0 | ((pid >> 8) as u8 & 0x1F),
201            pid as u8,
202        ]
203    }
204
205    fn build_test_pmt() -> Vec<u8> {
206        // program_info: a CA_descriptor + a (non-CA) registration descriptor(0x05).
207        let prog_ca = ca_descriptor(0x0500, 0x0100);
208        let reg = [0x05u8, 0x04, b'H', b'D', b'M', b'V'];
209        let mut program_info = Vec::new();
210        program_info.extend_from_slice(&prog_ca);
211        program_info.extend_from_slice(&reg);
212
213        // ES0: scrambled video, stream_type 0x02, pid 0x0200, with ES CA_descriptor.
214        let es0_ca = ca_descriptor(0x0500, 0x0101);
215        // ES1: clear audio, stream_type 0x03, pid 0x0201, only a language descriptor.
216        let lang = [0x0Au8, 0x04, b'e', b'n', b'g', 0x00];
217
218        let mut body = Vec::new();
219        // table_id 0x02
220        body.push(0x02);
221        // section_length placeholder (filled later): 2 bytes
222        body.push(0);
223        body.push(0);
224        // program_number 0x0001
225        body.extend_from_slice(&[0x00, 0x01]);
226        // reserved(2)|version(5)|cni(1): version 1, cni 1 -> 0b110000_11 = 0xC3
227        body.push(0xC3);
228        // section_number, last_section_number
229        body.push(0x00);
230        body.push(0x00);
231        // reserved(3)|PCR_PID(13): pid 0x0200
232        body.push(0xE0 | 0x02);
233        body.push(0x00);
234        // reserved(4)|program_info_length(12)
235        let pil = program_info.len();
236        body.push(0xF0 | ((pil >> 8) as u8 & 0x0F));
237        body.push(pil as u8);
238        body.extend_from_slice(&program_info);
239
240        // ES0
241        body.push(0x02); // stream_type
242        body.push(0xE0 | 0x02); // pid 0x0200
243        body.push(0x00);
244        body.push(0xF0 | ((es0_ca.len() >> 8) as u8 & 0x0F));
245        body.push(es0_ca.len() as u8);
246        body.extend_from_slice(&es0_ca);
247
248        // ES1
249        body.push(0x03);
250        body.push(0xE0 | 0x02); // pid 0x0201
251        body.push(0x01);
252        body.push(0xF0 | ((lang.len() >> 8) as u8 & 0x0F));
253        body.push(lang.len() as u8);
254        body.extend_from_slice(&lang);
255
256        // Now fix section_length = (bytes after the length field) + CRC(4).
257        let section_length = body.len() - 3 + 4;
258        body[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
259        body[2] = section_length as u8;
260
261        // Append a CRC (the parser validates length, not CRC for construction;
262        // compute the real MPEG-2 CRC so the section is well-formed).
263        let crc = dvb_common::crc32_mpeg2::compute(&body);
264        body.extend_from_slice(&crc.to_be_bytes());
265        body
266    }
267}