1use 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
54const TS_SYNC_BYTE: u8 = 0x47;
56const TS_PID_HIGH_MASK: u8 = 0x1F;
60const READ_BATCH_PACKETS: usize = 32;
64
65fn packet_pid(packet: &[u8]) -> u16 {
68 (u16::from(packet[1] & TS_PID_HIGH_MASK) << 8) | u16::from(packet[2])
69}
70
71fn 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
101pub struct CaDescrambler<D: CaDevice, C: CiDataDevice> {
105 driver: Driver<D>,
106 ci: C,
107}
108
109impl<D: CaDevice, C: CiDataDevice> CaDescrambler<D, C> {
110 #[must_use]
114 pub fn new(driver: Driver<D>, ci: C) -> Self {
115 Self { driver, ci }
116 }
117
118 pub fn add_service(&mut self, pmt: &PmtSection<'_>) -> Result<(), CaError> {
124 self.driver.add_service(pmt)
125 }
126
127 pub fn set_cat(&mut self, cat: &CatSection<'_>) -> Result<(), CaError> {
133 self.driver.set_cat(cat)
134 }
135
136 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 #[must_use]
167 pub fn required_pids(&self) -> Vec<u16> {
168 self.driver.required_pids()
169 }
170
171 pub fn take_notifications(&mut self) -> Vec<Notification> {
174 self.driver.take_notifications()
175 }
176
177 pub fn pump(&mut self, timeout: Duration) -> io::Result<bool> {
182 self.driver.pump(timeout)
183 }
184
185 #[must_use]
187 pub fn driver(&self) -> &Driver<D> {
188 &self.driver
189 }
190
191 pub fn driver_mut(&mut self) -> &mut Driver<D> {
195 &mut self.driver
196 }
197
198 #[must_use]
200 pub fn ci(&self) -> &C {
201 &self.ci
202 }
203
204 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 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 let empty: BTreeSet<u16> = BTreeSet::new();
258 assert!(filter_ts(&scrambled, &empty).unwrap().is_empty());
259
260 assert_eq!(
262 filter_ts(&scrambled[..scrambled.len() - 1], &allow)
263 .unwrap_err()
264 .kind(),
265 io::ErrorKind::InvalidInput
266 );
267
268 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 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 let pmt_bytes = build_ca_pmt_fixture(1546);
295 let pmt = PmtSection::parse(&pmt_bytes).unwrap();
296 d.add_service(&pmt).unwrap();
297
298 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 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 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 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 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}