Skip to main content

dvb_ci_runtime/
descrambler.rs

1//! Turnkey CAS descrambler — #763 Layer 2.
2//!
3//! Layer 1 ([`ManagedCa`](crate::managed::ManagedCa) on [`Driver`]) is the
4//! sans-IO control plane over the CA device (`caM`): parsed PMT/CAT in,
5//! `ca_pmt` APDUs out. This module adds the turnkey wrapper that *also* owns
6//! the CI slot's TS **data plane** (`CiDataDevice`, `ciM`): the caller shovels
7//! scrambled TS in and gets descrambled TS back out, with no PID math of its
8//! own to do.
9//!
10//! # Feed policy — filter, don't shovel
11//!
12//! A CI slot descrambles the single TS routed to it, and only needs four PID
13//! classes out of that TS: the target services' **ES PIDs**
14//! ([`descramble_pids`](crate::driver::Driver::descramble_pids)), their
15//! **ECM PIDs** ([`ca_pids`](crate::driver::Driver::ca_pids) — ISO/IEC
16//! 13818-1 §2.6.16 `CA_descriptor` `CA_PID`, carrying the control words
17//! without which the module has ES to descramble but no key to do it with),
18//! the **EMM PIDs** ([`emm_pids`](crate::driver::Driver::emm_pids) —
19//! entitlements), and each service's **PCR PID** (ISO/IEC 13818-1 §2.4.4.8 —
20//! when it names a dedicated PID distinct from every ES/CA PID, a legitimate
21//! DVB configuration, the descrambled TS still needs its clock reference).
22//! [`CaDescrambler::feed_ts`] filters the input TS to
23//! [`required_pids`](CaDescrambler::required_pids) = `descramble_pids ∪
24//! ca_pids ∪ emm_pids ∪ PCR` and writes only those packets to `ci0` — a 30–50
25//! Mbit/s mux collapses to the handful of wanted services plus a low-rate
26//! ECM/EMM trickle. PAT/PMT are **not** fed on `ci0`: the CAM receives the
27//! PMT via the `ca_pmt` control-plane APDU (Layer 1). [`required_pids`](CaDescrambler::required_pids)
28//! is also exposed directly so an efficient caller can pre-filter at the
29//! tuner/HW PID filter and never hand `feed_ts` the full mux.
30//!
31//! **Multi-tuner is multi-slot.** One [`CaDescrambler`] = one CI slot = one
32//! input TS path. Descrambling services spread across several tuners means
33//! one `CaDescrambler` per slot, each fed its own tuner's filtered subset;
34//! merging selected services from multiple muxes into a single slot needs a
35//! remux + PID remap (PIDs collide across muxes) and is explicitly out of
36//! scope here — that's a muxer's job, upstream.
37//!
38//! The 13-bit PID is read inline (named consts below) rather than pulling in
39//! an `mpeg-ts` dependency for one field.
40
41use std::collections::BTreeSet;
42use std::io;
43use std::time::Duration;
44
45use dvb_si::tables::cat::CatSection;
46use dvb_si::tables::pmt::PmtSection;
47
48use crate::dataplane::{CiDataDevice, TS_PACKET_LEN};
49use crate::device::CaDevice;
50use crate::driver::Driver;
51use crate::event::Notification;
52use crate::managed::CaError;
53
54/// MPEG-2 TS sync byte (ISO/IEC 13818-1 §2.4.3.3).
55const TS_SYNC_BYTE: u8 = 0x47;
56/// Mask for the PID's upper byte within a TS packet header (byte 1): the top
57/// 3 bits are `transport_error_indicator`/`payload_unit_start_indicator`/
58/// `transport_priority`, the low 5 are `PID[12:8]`.
59const TS_PID_HIGH_MASK: u8 = 0x1F;
60/// Number of whole TS packets to read from `ci0` per [`CiDataDevice::read`]
61/// call while draining descrambled output in `feed_ts` — a batch buffer size,
62/// not a wire value.
63const READ_BATCH_PACKETS: usize = 32;
64
65/// The 13-bit PID carried by one 188-byte TS packet's header (bytes 1–2),
66/// masking off the non-PID flag bits in byte 1.
67fn packet_pid(packet: &[u8]) -> u16 {
68    (u16::from(packet[1] & TS_PID_HIGH_MASK) << 8) | u16::from(packet[2])
69}
70
71/// Keep only the packets in `scrambled` whose PID is in `allow`, concatenated
72/// in order.
73///
74/// # Errors
75/// [`io::ErrorKind::InvalidInput`] if `scrambled` is not a whole number of
76/// [`TS_PACKET_LEN`]-byte packets, or if any packet's sync byte isn't `0x47`
77/// (misaligned input — filtering garbage would silently corrupt the PID
78/// read).
79fn filter_ts(scrambled: &[u8], allow: &BTreeSet<u16>) -> io::Result<Vec<u8>> {
80    if !scrambled.len().is_multiple_of(TS_PACKET_LEN) {
81        return Err(io::Error::new(
82            io::ErrorKind::InvalidInput,
83            "scrambled TS is not a whole number of 188-byte packets",
84        ));
85    }
86    let mut out = Vec::new();
87    for packet in scrambled.chunks_exact(TS_PACKET_LEN) {
88        if packet[0] != TS_SYNC_BYTE {
89            return Err(io::Error::new(
90                io::ErrorKind::InvalidInput,
91                "TS packet sync byte != 0x47 (misaligned input)",
92            ));
93        }
94        if allow.contains(&packet_pid(packet)) {
95            out.extend_from_slice(packet);
96        }
97    }
98    Ok(out)
99}
100
101/// Turnkey CAS descrambler (#763 Layer 2): a [`Driver`] (control plane, `caM`)
102/// paired with a [`CiDataDevice`] (data plane, `ciM`) for one CI slot. See the
103/// module docs for the feed-filter policy.
104pub struct CaDescrambler<D: CaDevice, C: CiDataDevice> {
105    driver: Driver<D>,
106    ci: C,
107}
108
109impl<D: CaDevice, C: CiDataDevice> CaDescrambler<D, C> {
110    /// New descrambler over an already-constructed control-plane `driver` and
111    /// data-plane `ci` device (both left for the caller to `init()`/wire up
112    /// as needed before use).
113    #[must_use]
114    pub fn new(driver: Driver<D>, ci: C) -> Self {
115        Self { driver, ci }
116    }
117
118    /// Add a service to the descrambled set (delegates to
119    /// [`Driver::add_service`]).
120    ///
121    /// # Errors
122    /// See [`Driver::add_service`].
123    pub fn add_service(&mut self, pmt: &PmtSection<'_>) -> Result<(), CaError> {
124        self.driver.add_service(pmt)
125    }
126
127    /// Feed a freshly-parsed CAT to the managed CAS-layer state (delegates to
128    /// [`Driver::set_cat`]).
129    ///
130    /// # Errors
131    /// See [`Driver::set_cat`].
132    pub fn set_cat(&mut self, cat: &CatSection<'_>) -> Result<(), CaError> {
133        self.driver.set_cat(cat)
134    }
135
136    /// Filter `scrambled` to [`required_pids`](Self::required_pids) and write
137    /// only those packets to `ci0`, then drain and return all
138    /// currently-available descrambled TS.
139    ///
140    /// # Errors
141    /// [`io::ErrorKind::InvalidInput`] if `scrambled` is not a whole number
142    /// of [`TS_PACKET_LEN`]-byte packets or carries a misaligned packet
143    /// (sync byte != `0x47`); otherwise any I/O error from the underlying
144    /// [`CiDataDevice`].
145    pub fn feed_ts(&mut self, scrambled: &[u8]) -> io::Result<Vec<u8>> {
146        let allow: BTreeSet<u16> = self.required_pids().into_iter().collect();
147        let kept = filter_ts(scrambled, &allow)?;
148        if !kept.is_empty() {
149            self.ci.write(&kept)?;
150        }
151
152        let mut out = Vec::new();
153        let mut buf = [0u8; READ_BATCH_PACKETS * TS_PACKET_LEN];
154        loop {
155            let n = self.ci.read(&mut buf)?;
156            if n == 0 {
157                break;
158            }
159            out.extend_from_slice(&buf[..n]);
160        }
161        Ok(out)
162    }
163
164    /// `descramble_pids ∪ ca_pids ∪ emm_pids ∪ PCR` — the PIDs the CAM needs
165    /// on `ci0` (delegates to [`Driver::required_pids`]).
166    #[must_use]
167    pub fn required_pids(&self) -> Vec<u16> {
168        self.driver.required_pids()
169    }
170
171    /// Drain the notifications collected so far (delegates to
172    /// [`Driver::take_notifications`]).
173    pub fn take_notifications(&mut self) -> Vec<Notification> {
174        self.driver.take_notifications()
175    }
176
177    /// Pump the control-plane device (delegates to [`Driver::pump`]).
178    ///
179    /// # Errors
180    /// See [`Driver::pump`].
181    pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
182        self.driver.pump(timeout)
183    }
184
185    /// Borrow the control-plane [`Driver`].
186    #[must_use]
187    pub fn driver(&self) -> &Driver<D> {
188        &self.driver
189    }
190
191    /// Mutably borrow the control-plane [`Driver`] (e.g. to drive it
192    /// directly for control-plane-only operations this wrapper doesn't
193    /// re-expose).
194    pub fn driver_mut(&mut self) -> &mut Driver<D> {
195        &mut self.driver
196    }
197
198    /// Borrow the data-plane [`CiDataDevice`].
199    #[must_use]
200    pub fn ci(&self) -> &C {
201        &self.ci
202    }
203
204    /// Mutably borrow the data-plane [`CiDataDevice`].
205    pub fn ci_mut(&mut self) -> &mut C {
206        &mut self.ci
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::dataplane::MockCiDataDevice;
214    use crate::device::MockCaDevice;
215    use crate::driver::tests::{
216        CA_SESSION, build_ca_pmt_fixture, build_ca_pmt_fixture_dedicated_pcr, build_cat_fixture,
217        build_clear_pmt_fixture, ca_descriptor, ca_pmt_reply_for, driver_with_sessions, feed,
218        r_apdu, ser,
219    };
220    use crate::managed::CaError;
221    use broadcast_common::Parse;
222
223    fn packet(pid: u16, fill: u8) -> Vec<u8> {
224        let mut p = vec![fill; TS_PACKET_LEN];
225        p[0] = TS_SYNC_BYTE;
226        // Set the reserved top bits (transport_error/pusi/priority) to
227        // exercise the mask, per the brief's `hi = 0x40 | (pid>>8)`.
228        p[1] = 0x40 | ((pid >> 8) as u8);
229        p[2] = pid as u8;
230        p
231    }
232
233    #[test]
234    fn filter_ts_keeps_only_allowed_pids() {
235        let p_100 = packet(0x100, 0xAA);
236        let p_64 = packet(0x64, 0xBB);
237        let p_200 = packet(0x200, 0xCC);
238
239        let mut scrambled = Vec::new();
240        scrambled.extend_from_slice(&p_100);
241        scrambled.extend_from_slice(&p_64);
242        scrambled.extend_from_slice(&p_200);
243
244        let allow: BTreeSet<u16> = [0x100, 0x64].into_iter().collect();
245        let kept = filter_ts(&scrambled, &allow).unwrap();
246
247        let mut expected = Vec::new();
248        expected.extend_from_slice(&p_100);
249        expected.extend_from_slice(&p_64);
250        assert_eq!(
251            kept, expected,
252            "0x200 must be dropped, the two allowed packets kept byte-exact and in order"
253        );
254
255        // Bite: an empty allow-set drops everything — a reintroduced
256        // no-filter passthrough would keep 0x200 and fail this.
257        let empty: BTreeSet<u16> = BTreeSet::new();
258        assert!(filter_ts(&scrambled, &empty).unwrap().is_empty());
259
260        // Unaligned input.
261        assert_eq!(
262            filter_ts(&scrambled[..scrambled.len() - 1], &allow)
263                .unwrap_err()
264                .kind(),
265            io::ErrorKind::InvalidInput
266        );
267
268        // Misaligned sync byte.
269        let mut bad = p_100.clone();
270        bad[0] = 0x00;
271        assert_eq!(
272            filter_ts(&bad, &allow).unwrap_err().kind(),
273            io::ErrorKind::InvalidInput
274        );
275    }
276
277    /// Wrap a `Driver<MockCaDevice>` already carrying CAS-layer state into a
278    /// `CaDescrambler` over a scripted `MockCiDataDevice`.
279    fn descrambler_with(
280        driver: Driver<MockCaDevice>,
281        descrambled: impl IntoIterator<Item = Vec<u8>>,
282    ) -> CaDescrambler<MockCaDevice, MockCiDataDevice> {
283        CaDescrambler::new(driver, MockCiDataDevice::new(descrambled))
284    }
285
286    #[test]
287    fn feed_ts_filters_to_required_pids_and_returns_descrambled() {
288        use dvb_ci::objects::ca_info::CaInfo;
289
290        let mut d = driver_with_sessions();
291        d.take_notifications();
292
293        // add_service: descramble_pids = [0x100, 0x101], ca_pids = [0x64, 0x65].
294        let pmt_bytes = build_ca_pmt_fixture(1546);
295        let pmt = PmtSection::parse(&pmt_bytes).unwrap();
296        d.add_service(&pmt).unwrap();
297
298        // ca_info + set_cat: emm_pids = [0x1FF0] (CAT ∩ ca_info CAIDs, mirrors
299        // the Task 4 pattern).
300        feed(
301            &mut d,
302            r_apdu(
303                CA_SESSION,
304                &ser(&CaInfo {
305                    ca_system_ids: vec![0x0648],
306                }),
307            ),
308        );
309        d.take_notifications();
310        let mut descriptors = Vec::new();
311        descriptors.extend_from_slice(&ca_descriptor(0x0648, 0x1FF0));
312        let cat_bytes = build_cat_fixture(&descriptors);
313        let cat = CatSection::parse(&cat_bytes).unwrap();
314        d.set_cat(&cat).unwrap();
315
316        assert_eq!(
317            d.required_pids(),
318            vec![0x0064, 0x0065, 0x0100, 0x0101, 0x1FF0],
319            "precondition: required_pids = descramble_pids ∪ ca_pids ∪ emm_pids"
320        );
321
322        let descrambled_script = packet(0x100, 0xEE);
323        let mut descrambler = descrambler_with(d, [descrambled_script.clone()]);
324        assert_eq!(
325            descrambler.required_pids(),
326            vec![0x0064, 0x0065, 0x0100, 0x0101, 0x1FF0],
327            "required_pids must delegate through the wrapper"
328        );
329
330        // One required-PID packet (0x100, an ES PID) + one junk packet on a
331        // PID NOT in required_pids.
332        let required_pkt = packet(0x100, 0x11);
333        let junk_pkt = packet(0x999, 0x22);
334        let mut scrambled = Vec::new();
335        scrambled.extend_from_slice(&required_pkt);
336        scrambled.extend_from_slice(&junk_pkt);
337
338        let out = descrambler.feed_ts(&scrambled).unwrap();
339
340        assert_eq!(
341            descrambler.ci().written_ts(),
342            required_pkt,
343            "ci0 must receive ONLY the required-PID packet; the junk packet on 0x999 must be dropped"
344        );
345        assert_eq!(
346            out, descrambled_script,
347            "feed_ts must return the scripted descrambled TS read back from ci0"
348        );
349    }
350
351    #[test]
352    fn feed_ts_keeps_a_dedicated_pcr_pid_packet() {
353        // #763 final-review Fix 1: a service whose PCR is carried on a
354        // dedicated PID (distinct from every ES/CA PID) must still have that
355        // PID routed to ci0 — otherwise the descrambled TS read back has no
356        // clock reference.
357        let mut d = driver_with_sessions();
358        d.take_notifications();
359
360        let pmt_bytes = build_ca_pmt_fixture_dedicated_pcr(1550);
361        let pmt = PmtSection::parse(&pmt_bytes).unwrap();
362        d.add_service(&pmt).unwrap();
363
364        assert!(
365            d.required_pids().contains(&0x00FF),
366            "precondition: required_pids must include the dedicated PCR PID, got {:?}",
367            d.required_pids()
368        );
369
370        let mut descrambler = descrambler_with(d, []);
371
372        // One packet on the dedicated PCR PID + one junk packet on a PID not
373        // in required_pids.
374        let pcr_pkt = packet(0x00FF, 0x33);
375        let junk_pkt = packet(0x0AAA, 0x44);
376        let mut scrambled = Vec::new();
377        scrambled.extend_from_slice(&pcr_pkt);
378        scrambled.extend_from_slice(&junk_pkt);
379
380        descrambler.feed_ts(&scrambled).unwrap();
381
382        assert_eq!(
383            descrambler.ci().written_ts(),
384            pcr_pkt,
385            "ci0 must receive the dedicated PCR PID packet (clock reference); \
386             the junk packet must be dropped"
387        );
388    }
389
390    #[test]
391    fn take_notifications_delegates_entitlement() {
392        use dvb_ci::objects::ca_pmt_reply::CaEnable;
393
394        let mut d = driver_with_sessions();
395        d.take_notifications();
396
397        let pmt_bytes = build_ca_pmt_fixture(1546);
398        let pmt = PmtSection::parse(&pmt_bytes).unwrap();
399        d.add_service(&pmt).unwrap();
400        d.take_notifications();
401
402        let mut descrambler = descrambler_with(d, []);
403
404        // First-ever ca_pmt_reply for a tracked program: establishes the
405        // baseline AND reports it (ManagedCa::record_reply), so it must
406        // surface as Notification::Entitlement through the wrapper's
407        // take_notifications() — not via feed_ts.
408        feed(
409            descrambler.driver_mut(),
410            r_apdu(
411                CA_SESSION,
412                &ser(&ca_pmt_reply_for(1546, Some(CaEnable::Possible))),
413            ),
414        );
415
416        let notes = descrambler.take_notifications();
417        let hits = notes
418            .iter()
419            .filter(|n| {
420                matches!(
421                    n,
422                    Notification::Entitlement {
423                        program_number: 1546,
424                        ca_enable: CaEnable::Possible,
425                        descrambling_ok: true,
426                    }
427                )
428            })
429            .count();
430        assert_eq!(
431            hits, 1,
432            "expected exactly one Entitlement notification to surface via CaDescrambler::take_notifications(), got {notes:?}"
433        );
434    }
435
436    #[test]
437    fn add_service_delegates() {
438        let d = Driver::new(MockCaDevice::new([]));
439        let mut descrambler = descrambler_with(d, []);
440
441        let pmt_bytes = build_clear_pmt_fixture(999);
442        let pmt = PmtSection::parse(&pmt_bytes).unwrap();
443
444        let err = descrambler.add_service(&pmt).unwrap_err();
445        assert!(
446            matches!(
447                err,
448                CaError::NoCaDescriptor {
449                    program_number: 999
450                }
451            ),
452            "expected CaError::NoCaDescriptor via delegation, got {err:?}"
453        );
454    }
455}