Skip to main content

dvb_ci/ci_plus/
multistream_host_control.rs

1//! Multi-stream Host Control resource objects — ETSI TS 103 205 V1.4.1 §6.4.5,
2//! Tables 17-22 (PDF pp. 40-44); base DVB Host Control v3 tune APDUs §13.2,
3//! Tables 97-102 (PDF pp. 126-130). See
4//! `docs/ts_103_205/multi-stream-host-control.md`.
5//!
6//! The multi-stream Host Control resource (**resource ID `0x00200081`**) is based
7//! on DVB Host Control version 3 (**resource ID `0x00200043`**). The multi-stream
8//! tune APDUs add a `background_tune_flag` to the base-v3 layouts; all other APDUs
9//! retain the v3 syntax.
10//!
11//! Tune-request APDUs implemented (tags inherited from the v3 resource, §13.2):
12//!
13//! - `tune_triplet_req` (`9F 84 09`, Tables 19/98) — CICAM → Host.
14//! - `tune_lcn_req` (`9F 84 07`, Tables 20/99) — CICAM → Host.
15//! - `tune_ip_req` (`9F 84 08`, Tables 21/100) — CICAM → Host.
16//! - `tuner_status_req` (`9F 84 0A`, Table 101) — CICAM → Host, header-only.
17//! - `tuner_status_reply` (`9F 84 0B`, Table 102) — Host → CICAM.
18//!
19//! ## The two resource ids and the `tune_ip_req` divergence
20//!
21//! The same tune objects appear under two resource ids that differ only in the bit
22//! budget of the byte preceding the three tune flags:
23//!
24//! - **Multi-stream** (`0x00200081`, [`HostControlMode::MultiStream`]) — Table 21
25//!   `tune_ip_req` is `reserved(1)` + `background_tune_flag(1)` + `tune_quietly(1)`
26//!   + `keep_app_running(1)` + `service_location_length(12)`.
27//! - **Base v3** (`0x00200043`, [`HostControlMode::BaseV3`]) — Table 100
28//!   `tune_ip_req` is `reserved(2)` + `tune_quietly(1)` + `keep_app_running(1)` +
29//!   `service_location_length(12)` (no `background_tune_flag`).
30//!
31//! Both layouts pack the four flag/reserved bits into the high nibble of the byte
32//! that begins the 12-bit `service_location_length`. The two render to **distinct**
33//! byte patterns whenever `background_tune_flag` is set, so the mode is carried on
34//! [`TuneIpReq::mode`] and the parser is told which resource it arrived on.
35//!
36//! `tune_broadcast_req`, `tune_reply`, `ask_release` and `ask_release_reply` tags
37//! and bodies are **deferred to CI Plus V1.3** (their tags are not numerically
38//! printed in TS 103 205) — they are intentionally NOT implemented here.
39
40use crate::error::{Error, Result};
41use crate::objects;
42use crate::tag::ApduTag;
43use alloc::vec::Vec;
44use dvb_common::{Parse, Serialize};
45
46/// Resource-scoped `apdu_tag`s, inherited from the DVB Host Control v3 resource
47/// (§13.2). These live in the `0x9F84xx` namespace alongside EN 50221's Host
48/// Control, but are distinct tag values.
49pub mod tag {
50    use crate::tag::ApduTag;
51    /// `tune_lcn_req_tag` = `9F 84 07` (Tables 20/99).
52    pub const TUNE_LCN_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x84, 0x07);
53    /// `tune_ip_req_tag` = `9F 84 08` (Tables 21/100).
54    pub const TUNE_IP_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x84, 0x08);
55    /// `tune_triplet_req_tag` = `9F 84 09` (Tables 19/98).
56    pub const TUNE_TRIPLET_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x84, 0x09);
57    /// `tuner_status_req_tag` = `9F 84 0A` (Table 101).
58    pub const TUNER_STATUS_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x84, 0x0A);
59    /// `tuner_status_reply_tag` = `9F 84 0B` (Table 102).
60    pub const TUNER_STATUS_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0x84, 0x0B);
61}
62
63/// Which resource layout a tune APDU uses — the multi-stream resource
64/// (`0x00200081`) or the base DVB Host Control v3 resource (`0x00200043`). Only
65/// affects [`TuneIpReq`]'s reserved-bit budget (Table 21 vs Table 100).
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize))]
68#[non_exhaustive]
69pub enum HostControlMode {
70    /// Multi-stream Host Control (`0x00200081`) — Table 21 layout
71    /// (`reserved(1)` + `background_tune_flag`).
72    MultiStream,
73    /// Base DVB Host Control v3 (`0x00200043`) — Table 100 layout
74    /// (`reserved(2)`, no `background_tune_flag`).
75    BaseV3,
76}
77
78impl HostControlMode {
79    /// Diagnostic spec token.
80    #[must_use]
81    pub fn name(&self) -> &'static str {
82        match self {
83            Self::MultiStream => "multi_stream",
84            Self::BaseV3 => "base_v3",
85        }
86    }
87}
88dvb_common::impl_spec_display!(HostControlMode);
89
90// --- The three tune flag bits, packed into the high bits of the prefix byte. ---
91//
92// In every tune APDU the three flags occupy the low three bits of a flag/reserved
93// byte: background_tune_flag, tune_quietly_flag, keep_app_running_flag — laid out
94// from the most-significant flag bit downward. Their exact bit positions depend on
95// how much reserved padding precedes them, so each struct defines its own masks.
96
97/// `tune_triplet_req()` (Tables 19/98): CICAM → Host. The §6.4.5.3 multi-stream
98/// layout, which adds `background_tune_flag` (the base-v3 Table 98 layout differs
99/// only by having `reserved(6)` and no `background_tune_flag` — not modelled here
100/// as a separate type; for v3 set `background_tune` = `false`).
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize))]
103pub struct TuneTripletReq {
104    /// `background_tune_flag` (1) — `true` = background (not presented).
105    pub background_tune: bool,
106    /// `tune_quietly_flag` (1).
107    pub tune_quietly: bool,
108    /// `keep_app_running_flag` (1).
109    pub keep_app_running: bool,
110    /// `original_network_id` (16).
111    pub original_network_id: u16,
112    /// `transport_stream_id` (16).
113    pub transport_stream_id: u16,
114    /// `service_id` (16).
115    pub service_id: u16,
116    /// `delivery_system_descriptor_tag` (8).
117    pub delivery_system_descriptor_tag: u8,
118    /// `descriptor_tag_extension` (8) when
119    /// `delivery_system_descriptor_tag == 0x7F`, else the trailing `reserved(8)`
120    /// byte is present but ignored (modeled as `None`).
121    pub descriptor_tag_extension: Option<u8>,
122}
123
124// flags/reserved(1) + onid(2) + tsid(2) + sid(2) + dsd_tag(1) + ext/reserved(1).
125const TRIPLET_BODY: usize = 1 + 2 + 2 + 2 + 1 + 1;
126// The descriptor_tag value that signals descriptor_tag_extension follows.
127const DSD_TAG_EXTENSION: u8 = 0x7F;
128
129// tune_triplet_req flag byte: reserved(5) + background(1) + quietly(1) + keep(1).
130const TRIPLET_BACKGROUND_BIT: u8 = 0x04;
131const TRIPLET_QUIETLY_BIT: u8 = 0x02;
132const TRIPLET_KEEP_BIT: u8 = 0x01;
133
134impl<'a> Parse<'a> for TuneTripletReq {
135    type Error = Error;
136    fn parse(bytes: &'a [u8]) -> Result<Self> {
137        let body = objects::parse_apdu_header(bytes, tag::TUNE_TRIPLET_REQ, "tune_triplet_req")?;
138        if body.len() < TRIPLET_BODY {
139            return Err(Error::BufferTooShort {
140                need: TRIPLET_BODY,
141                have: body.len(),
142                what: "tune_triplet_req",
143            });
144        }
145        let flags = body[0];
146        let dsd_tag = body[7];
147        let descriptor_tag_extension = if dsd_tag == DSD_TAG_EXTENSION {
148            Some(body[8])
149        } else {
150            None
151        };
152        Ok(Self {
153            background_tune: flags & TRIPLET_BACKGROUND_BIT != 0,
154            tune_quietly: flags & TRIPLET_QUIETLY_BIT != 0,
155            keep_app_running: flags & TRIPLET_KEEP_BIT != 0,
156            original_network_id: u16::from_be_bytes([body[1], body[2]]),
157            transport_stream_id: u16::from_be_bytes([body[3], body[4]]),
158            service_id: u16::from_be_bytes([body[5], body[6]]),
159            delivery_system_descriptor_tag: dsd_tag,
160            descriptor_tag_extension,
161        })
162    }
163}
164
165impl Serialize for TuneTripletReq {
166    type Error = Error;
167    fn serialized_len(&self) -> usize {
168        objects::apdu_len(TRIPLET_BODY)
169    }
170    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
171        let pos = objects::write_apdu_header(tag::TUNE_TRIPLET_REQ, TRIPLET_BODY, buf)?;
172        let mut flags = 0u8;
173        if self.background_tune {
174            flags |= TRIPLET_BACKGROUND_BIT;
175        }
176        if self.tune_quietly {
177            flags |= TRIPLET_QUIETLY_BIT;
178        }
179        if self.keep_app_running {
180            flags |= TRIPLET_KEEP_BIT;
181        }
182        buf[pos] = flags;
183        buf[pos + 1..pos + 3].copy_from_slice(&self.original_network_id.to_be_bytes());
184        buf[pos + 3..pos + 5].copy_from_slice(&self.transport_stream_id.to_be_bytes());
185        buf[pos + 5..pos + 7].copy_from_slice(&self.service_id.to_be_bytes());
186        buf[pos + 7] = self.delivery_system_descriptor_tag;
187        // descriptor_tag_extension when tag==0x7F, else reserved(8) = 0.
188        buf[pos + 8] = self.descriptor_tag_extension.unwrap_or(0);
189        Ok(pos + TRIPLET_BODY)
190    }
191}
192
193/// `tune_lcn_req()` (Tables 20/99): CICAM → Host. The §6.4.5.4 multi-stream
194/// layout, which adds `background_tune_flag`.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize))]
197pub struct TuneLcnReq {
198    /// `background_tune_flag` (1).
199    pub background_tune: bool,
200    /// `tune_quietly_flag` (1).
201    pub tune_quietly: bool,
202    /// `keep_app_running_flag` (1).
203    pub keep_app_running: bool,
204    /// `logical_channel_number` (14) — range 0..9999.
205    pub logical_channel_number: u16,
206}
207
208// flags/reserved(1) + flags/lcn-hi(1) + lcn-lo(1).
209const LCN_BODY: usize = 3;
210
211// tune_lcn_req: reserved(7) + background(1) | quietly(1)+keep(1)+lcn(14).
212// First byte: reserved(7) + background_tune_flag(1 = LSB).
213const LCN_BACKGROUND_BIT: u8 = 0x01;
214// Second byte high bits: quietly(1)=bit7, keep(1)=bit6, then lcn[13:8].
215const LCN_QUIETLY_BIT: u8 = 0x80;
216const LCN_KEEP_BIT: u8 = 0x40;
217// logical_channel_number occupies the low 14 bits of the last two bytes.
218const LCN_MASK: u16 = 0x3FFF;
219
220impl<'a> Parse<'a> for TuneLcnReq {
221    type Error = Error;
222    fn parse(bytes: &'a [u8]) -> Result<Self> {
223        let body = objects::parse_apdu_header(bytes, tag::TUNE_LCN_REQ, "tune_lcn_req")?;
224        if body.len() < LCN_BODY {
225            return Err(Error::BufferTooShort {
226                need: LCN_BODY,
227                have: body.len(),
228                what: "tune_lcn_req",
229            });
230        }
231        let background_tune = body[0] & LCN_BACKGROUND_BIT != 0;
232        let tune_quietly = body[1] & LCN_QUIETLY_BIT != 0;
233        let keep_app_running = body[1] & LCN_KEEP_BIT != 0;
234        let logical_channel_number = u16::from_be_bytes([body[1], body[2]]) & LCN_MASK;
235        Ok(Self {
236            background_tune,
237            tune_quietly,
238            keep_app_running,
239            logical_channel_number,
240        })
241    }
242}
243
244impl Serialize for TuneLcnReq {
245    type Error = Error;
246    fn serialized_len(&self) -> usize {
247        objects::apdu_len(LCN_BODY)
248    }
249    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
250        let pos = objects::write_apdu_header(tag::TUNE_LCN_REQ, LCN_BODY, buf)?;
251        buf[pos] = if self.background_tune {
252            LCN_BACKGROUND_BIT
253        } else {
254            0
255        };
256        let mut hi = (self.logical_channel_number >> 8) as u8 & (LCN_MASK >> 8) as u8;
257        if self.tune_quietly {
258            hi |= LCN_QUIETLY_BIT;
259        }
260        if self.keep_app_running {
261            hi |= LCN_KEEP_BIT;
262        }
263        buf[pos + 1] = hi;
264        buf[pos + 2] = self.logical_channel_number as u8;
265        Ok(pos + LCN_BODY)
266    }
267}
268
269/// `tune_ip_req()` (Tables 21/100): CICAM → Host. The reserved-bit budget before
270/// the flags differs by [`HostControlMode`] (Table 21 = 1 reserved bit +
271/// `background_tune_flag`; Table 100 = 2 reserved bits, no `background_tune_flag`).
272#[derive(Debug, Clone, PartialEq, Eq)]
273#[cfg_attr(feature = "serde", derive(serde::Serialize))]
274pub struct TuneIpReq<'a> {
275    /// Which resource layout this APDU uses (selects the reserved-bit budget).
276    pub mode: HostControlMode,
277    /// `background_tune_flag` (1) — only present in [`HostControlMode::MultiStream`];
278    /// for [`HostControlMode::BaseV3`] this field is not on the wire and must be
279    /// `false`.
280    pub background_tune: bool,
281    /// `tune_quietly_flag` (1).
282    pub tune_quietly: bool,
283    /// `keep_app_running_flag` (1).
284    pub keep_app_running: bool,
285    /// `service_location_data` (the `service_location_length`-byte body) — opaque.
286    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
287    pub service_location_data: &'a [u8],
288}
289
290// flags/reserved+length(2) prefix, then service_location_data.
291const TUNE_IP_PREFIX: usize = 2;
292// Multi-stream (Table 21): byte0 = reserved(1) | background(bit6) | quietly(bit5)
293// | keep(bit4) | service_location_length[11:8].
294const IP_MS_BACKGROUND_BIT: u8 = 0x40;
295const IP_MS_QUIETLY_BIT: u8 = 0x20;
296const IP_MS_KEEP_BIT: u8 = 0x10;
297// Base v3 (Table 100): byte0 = reserved(2) | quietly(bit5) | keep(bit4) |
298// service_location_length[11:8]. (No background_tune_flag.)
299const IP_V3_QUIETLY_BIT: u8 = 0x20;
300const IP_V3_KEEP_BIT: u8 = 0x10;
301// service_location_length is 12 bits: high nibble in byte0, low 8 in byte1.
302const SLL_HI_MASK: u8 = 0x0F;
303
304impl<'a> TuneIpReq<'a> {
305    /// Parse a `tune_ip_req` body under the given [`HostControlMode`].
306    pub fn parse_mode(bytes: &'a [u8], mode: HostControlMode) -> Result<Self> {
307        let body = objects::parse_apdu_header(bytes, tag::TUNE_IP_REQ, "tune_ip_req")?;
308        if body.len() < TUNE_IP_PREFIX {
309            return Err(Error::BufferTooShort {
310                need: TUNE_IP_PREFIX,
311                have: body.len(),
312                what: "tune_ip_req",
313            });
314        }
315        let (background_tune, tune_quietly, keep_app_running) = match mode {
316            HostControlMode::MultiStream => (
317                body[0] & IP_MS_BACKGROUND_BIT != 0,
318                body[0] & IP_MS_QUIETLY_BIT != 0,
319                body[0] & IP_MS_KEEP_BIT != 0,
320            ),
321            HostControlMode::BaseV3 => (
322                false,
323                body[0] & IP_V3_QUIETLY_BIT != 0,
324                body[0] & IP_V3_KEEP_BIT != 0,
325            ),
326        };
327        let sll = (((body[0] & SLL_HI_MASK) as usize) << 8) | body[1] as usize;
328        let data = &body[TUNE_IP_PREFIX..];
329        if data.len() < sll {
330            return Err(Error::BufferTooShort {
331                need: sll,
332                have: data.len(),
333                what: "tune_ip_req service_location_data",
334            });
335        }
336        Ok(Self {
337            mode,
338            background_tune,
339            tune_quietly,
340            keep_app_running,
341            service_location_data: &data[..sll],
342        })
343    }
344}
345
346impl Serialize for TuneIpReq<'_> {
347    type Error = Error;
348    fn serialized_len(&self) -> usize {
349        objects::apdu_len(TUNE_IP_PREFIX + self.service_location_data.len())
350    }
351    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
352        let sll = self.service_location_data.len();
353        let body_len = TUNE_IP_PREFIX + sll;
354        let pos = objects::write_apdu_header(tag::TUNE_IP_REQ, body_len, buf)?;
355        let mut byte0 = (sll >> 8) as u8 & SLL_HI_MASK;
356        match self.mode {
357            HostControlMode::MultiStream => {
358                if self.background_tune {
359                    byte0 |= IP_MS_BACKGROUND_BIT;
360                }
361                if self.tune_quietly {
362                    byte0 |= IP_MS_QUIETLY_BIT;
363                }
364                if self.keep_app_running {
365                    byte0 |= IP_MS_KEEP_BIT;
366                }
367            }
368            HostControlMode::BaseV3 => {
369                if self.tune_quietly {
370                    byte0 |= IP_V3_QUIETLY_BIT;
371                }
372                if self.keep_app_running {
373                    byte0 |= IP_V3_KEEP_BIT;
374                }
375            }
376        }
377        buf[pos] = byte0;
378        buf[pos + 1] = sll as u8;
379        buf[pos + 2..pos + 2 + sll].copy_from_slice(self.service_location_data);
380        Ok(pos + body_len)
381    }
382}
383
384/// `tuner_status_req()` (Table 101): CICAM → Host, header-only (`length_field = 0`).
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
386#[cfg_attr(feature = "serde", derive(serde::Serialize))]
387pub struct TunerStatusReq;
388
389impl<'a> Parse<'a> for TunerStatusReq {
390    type Error = Error;
391    fn parse(bytes: &'a [u8]) -> Result<Self> {
392        objects::parse_empty_apdu(bytes, tag::TUNER_STATUS_REQ, "tuner_status_req")?;
393        Ok(Self)
394    }
395}
396
397impl Serialize for TunerStatusReq {
398    type Error = Error;
399    fn serialized_len(&self) -> usize {
400        objects::empty_apdu_len()
401    }
402    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
403        objects::serialize_empty_apdu(tag::TUNER_STATUS_REQ, buf)
404    }
405}
406
407/// One DSD entry of [`TunerStatusReply`] (Table 102): `reserved(7)` +
408/// `connected_flag(1)` + `delivery_system_descriptor_tag(8)` + (extension or
409/// reserved)(8).
410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411#[cfg_attr(feature = "serde", derive(serde::Serialize))]
412pub struct TunerStatusDsd {
413    /// `connected_flag` (1) — Host believes at least one tuner of this DSD type is
414    /// connected.
415    pub connected: bool,
416    /// `delivery_system_descriptor_tag` (8).
417    pub delivery_system_descriptor_tag: u8,
418    /// `descriptor_tag_extension` (8) when
419    /// `delivery_system_descriptor_tag == 0x7F`, else the trailing `reserved(8)`
420    /// byte is ignored (`None`).
421    pub descriptor_tag_extension: Option<u8>,
422}
423
424/// `tuner_status_reply()` (Table 102): Host → CICAM.
425#[derive(Debug, Clone, PartialEq, Eq)]
426#[cfg_attr(feature = "serde", derive(serde::Serialize))]
427pub struct TunerStatusReply {
428    /// `IP_tune_capable_flag` (1) — Host can accept `tune_ip_req()` APDUs.
429    pub ip_tune_capable: bool,
430    /// The DSD entries (loop count = `num_dsd`).
431    pub dsds: Vec<TunerStatusDsd>,
432}
433
434// Each DSD entry is 3 bytes on the wire (flags+tag+ext/reserved).
435const DSD_ENTRY_LEN: usize = 3;
436// First byte: IP_tune_capable_flag(1 = bit7) + num_dsd(7).
437const IP_TUNE_CAPABLE_BIT: u8 = 0x80;
438const NUM_DSD_MASK: u8 = 0x7F;
439// Within a DSD entry, connected_flag is the LSB of the reserved(7)+connected(1) byte.
440const DSD_CONNECTED_BIT: u8 = 0x01;
441
442impl<'a> Parse<'a> for TunerStatusReply {
443    type Error = Error;
444    fn parse(bytes: &'a [u8]) -> Result<Self> {
445        let body =
446            objects::parse_apdu_header(bytes, tag::TUNER_STATUS_REPLY, "tuner_status_reply")?;
447        if body.is_empty() {
448            return Err(Error::BufferTooShort {
449                need: 1,
450                have: 0,
451                what: "tuner_status_reply",
452            });
453        }
454        let ip_tune_capable = body[0] & IP_TUNE_CAPABLE_BIT != 0;
455        let num_dsd = (body[0] & NUM_DSD_MASK) as usize;
456        let mut rest = &body[1..];
457        let mut dsds = Vec::with_capacity(num_dsd);
458        for _ in 0..num_dsd {
459            if rest.len() < DSD_ENTRY_LEN {
460                return Err(Error::BufferTooShort {
461                    need: DSD_ENTRY_LEN,
462                    have: rest.len(),
463                    what: "tuner_status_reply dsd",
464                });
465            }
466            let connected = rest[0] & DSD_CONNECTED_BIT != 0;
467            let dsd_tag = rest[1];
468            let descriptor_tag_extension = if dsd_tag == DSD_TAG_EXTENSION {
469                Some(rest[2])
470            } else {
471                None
472            };
473            dsds.push(TunerStatusDsd {
474                connected,
475                delivery_system_descriptor_tag: dsd_tag,
476                descriptor_tag_extension,
477            });
478            rest = &rest[DSD_ENTRY_LEN..];
479        }
480        Ok(Self {
481            ip_tune_capable,
482            dsds,
483        })
484    }
485}
486
487impl Serialize for TunerStatusReply {
488    type Error = Error;
489    fn serialized_len(&self) -> usize {
490        objects::apdu_len(1 + self.dsds.len() * DSD_ENTRY_LEN)
491    }
492    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
493        let body_len = 1 + self.dsds.len() * DSD_ENTRY_LEN;
494        let mut pos = objects::write_apdu_header(tag::TUNER_STATUS_REPLY, body_len, buf)?;
495        let mut byte0 = self.dsds.len() as u8 & NUM_DSD_MASK;
496        if self.ip_tune_capable {
497            byte0 |= IP_TUNE_CAPABLE_BIT;
498        }
499        buf[pos] = byte0;
500        pos += 1;
501        for dsd in &self.dsds {
502            buf[pos] = if dsd.connected { DSD_CONNECTED_BIT } else { 0 };
503            buf[pos + 1] = dsd.delivery_system_descriptor_tag;
504            buf[pos + 2] = dsd.descriptor_tag_extension.unwrap_or(0);
505            pos += DSD_ENTRY_LEN;
506        }
507        Ok(pos)
508    }
509}
510
511/// Resource-scoped dispatch over the Multi-stream Host Control resource objects.
512///
513/// `tune_broadcast_req` / `tune_reply` / `ask_release` / `ask_release_reply` are
514/// deferred to CI Plus V1.3 and are not members of this enum.
515#[derive(Debug, Clone, PartialEq, Eq)]
516#[cfg_attr(feature = "serde", derive(serde::Serialize))]
517#[non_exhaustive]
518pub enum MultistreamHostControlApdu<'a> {
519    /// `tune_triplet_req` (`9F 84 09`).
520    TuneTripletReq(TuneTripletReq),
521    /// `tune_lcn_req` (`9F 84 07`).
522    TuneLcnReq(TuneLcnReq),
523    /// `tune_ip_req` (`9F 84 08`).
524    TuneIpReq(TuneIpReq<'a>),
525    /// `tuner_status_req` (`9F 84 0A`).
526    TunerStatusReq(TunerStatusReq),
527    /// `tuner_status_reply` (`9F 84 0B`).
528    TunerStatusReply(TunerStatusReply),
529}
530
531impl<'a> MultistreamHostControlApdu<'a> {
532    /// Parse a Multi-stream Host Control APDU, dispatching on the leading
533    /// `apdu_tag`. `mode` selects the [`TuneIpReq`] reserved-bit budget.
534    pub fn parse_mode(body: &'a [u8], mode: HostControlMode) -> Result<Self> {
535        if body.len() < 3 {
536            return Err(Error::BufferTooShort {
537                need: 3,
538                have: body.len(),
539                what: "multistream_host_control apdu_tag",
540            });
541        }
542        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
543        match t {
544            tag::TUNE_TRIPLET_REQ => Ok(Self::TuneTripletReq(TuneTripletReq::parse(body)?)),
545            tag::TUNE_LCN_REQ => Ok(Self::TuneLcnReq(TuneLcnReq::parse(body)?)),
546            tag::TUNE_IP_REQ => Ok(Self::TuneIpReq(TuneIpReq::parse_mode(body, mode)?)),
547            tag::TUNER_STATUS_REQ => Ok(Self::TunerStatusReq(TunerStatusReq::parse(body)?)),
548            tag::TUNER_STATUS_REPLY => Ok(Self::TunerStatusReply(TunerStatusReply::parse(body)?)),
549            _ => Err(Error::UnexpectedApduTag {
550                got: t.as_u24(),
551                expected: tag::TUNE_TRIPLET_REQ.as_u24(),
552                what: "multistream_host_control",
553            }),
554        }
555    }
556}
557
558impl Serialize for MultistreamHostControlApdu<'_> {
559    type Error = Error;
560    fn serialized_len(&self) -> usize {
561        match self {
562            Self::TuneTripletReq(o) => o.serialized_len(),
563            Self::TuneLcnReq(o) => o.serialized_len(),
564            Self::TuneIpReq(o) => o.serialized_len(),
565            Self::TunerStatusReq(o) => o.serialized_len(),
566            Self::TunerStatusReply(o) => o.serialized_len(),
567        }
568    }
569    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
570        match self {
571            Self::TuneTripletReq(o) => o.serialize_into(buf),
572            Self::TuneLcnReq(o) => o.serialize_into(buf),
573            Self::TuneIpReq(o) => o.serialize_into(buf),
574            Self::TunerStatusReq(o) => o.serialize_into(buf),
575            Self::TunerStatusReply(o) => o.serialize_into(buf),
576        }
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583
584    #[test]
585    fn tune_triplet_round_trips_and_bites() {
586        let t = TuneTripletReq {
587            background_tune: true,
588            tune_quietly: false,
589            keep_app_running: true,
590            original_network_id: 0x1122,
591            transport_stream_id: 0x3344,
592            service_id: 0x5566,
593            delivery_system_descriptor_tag: 0x44,
594            descriptor_tag_extension: None,
595        };
596        let bytes = t.to_bytes();
597        // tag(3) + len(0x09) + flags(background|keep = 0x04|0x01 = 0x05)
598        //   + onid(11 22) + tsid(33 44) + sid(55 66) + dsd_tag(44) + reserved(00).
599        assert_eq!(
600            bytes,
601            [0x9F, 0x84, 0x09, 0x09, 0x05, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x44, 0x00]
602        );
603        assert_eq!(TuneTripletReq::parse(&bytes).unwrap(), t);
604        let mut other = t;
605        other.background_tune = false;
606        assert_eq!(other.to_bytes()[4], 0x01);
607        assert_ne!(bytes, other.to_bytes());
608    }
609
610    #[test]
611    fn tune_triplet_with_descriptor_extension() {
612        let t = TuneTripletReq {
613            background_tune: false,
614            tune_quietly: true,
615            keep_app_running: false,
616            original_network_id: 0x0001,
617            transport_stream_id: 0x0002,
618            service_id: 0x0003,
619            delivery_system_descriptor_tag: 0x7F,
620            descriptor_tag_extension: Some(0x79),
621        };
622        let bytes = t.to_bytes();
623        // flags = quietly = 0x02; dsd_tag=0x7F; ext=0x79.
624        assert_eq!(
625            bytes,
626            [0x9F, 0x84, 0x09, 0x09, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x7F, 0x79]
627        );
628        assert_eq!(TuneTripletReq::parse(&bytes).unwrap(), t);
629    }
630
631    #[test]
632    fn tune_lcn_round_trips_and_bites() {
633        let t = TuneLcnReq {
634            background_tune: true,
635            tune_quietly: true,
636            keep_app_running: false,
637            logical_channel_number: 0x0123,
638        };
639        let bytes = t.to_bytes();
640        // tag(3) + len(0x03) + byte0(background = 0x01)
641        //   + byte1(quietly=0x80 | lcn[13:8]=0x01 = 0x81) + byte2(lcn lo = 0x23).
642        assert_eq!(bytes, [0x9F, 0x84, 0x07, 0x03, 0x01, 0x81, 0x23]);
643        assert_eq!(TuneLcnReq::parse(&bytes).unwrap(), t);
644        let mut other = t;
645        other.logical_channel_number = 0x3FFE;
646        assert_ne!(bytes, other.to_bytes());
647        // 14-bit value preserved.
648        assert_eq!(
649            TuneLcnReq::parse(&other.to_bytes())
650                .unwrap()
651                .logical_channel_number,
652            0x3FFE
653        );
654    }
655
656    #[test]
657    fn tune_ip_multistream_vs_basev3_distinct_bytes() {
658        // Same logical request, both with background_tune set where applicable.
659        let data: &[u8] = &[0xAA, 0xBB];
660        let ms = TuneIpReq {
661            mode: HostControlMode::MultiStream,
662            background_tune: true,
663            tune_quietly: true,
664            keep_app_running: false,
665            service_location_data: data,
666        };
667        let v3 = TuneIpReq {
668            mode: HostControlMode::BaseV3,
669            background_tune: false, // not on the wire in v3
670            tune_quietly: true,
671            keep_app_running: false,
672            service_location_data: data,
673        };
674        let ms_bytes = ms.to_bytes();
675        let v3_bytes = v3.to_bytes();
676        // Multi-stream byte0: background(0x40) | quietly(0x20) | sll_hi(0) = 0x60.
677        assert_eq!(ms_bytes, [0x9F, 0x84, 0x08, 0x04, 0x60, 0x02, 0xAA, 0xBB]);
678        // Base v3 byte0: quietly(0x20) | sll_hi(0) = 0x20 (no background bit).
679        assert_eq!(v3_bytes, [0x9F, 0x84, 0x08, 0x04, 0x20, 0x02, 0xAA, 0xBB]);
680        // Distinct byte patterns even though quietly is set in both.
681        assert_ne!(ms_bytes, v3_bytes);
682        assert_eq!(
683            TuneIpReq::parse_mode(&ms_bytes, HostControlMode::MultiStream).unwrap(),
684            ms
685        );
686        assert_eq!(
687            TuneIpReq::parse_mode(&v3_bytes, HostControlMode::BaseV3).unwrap(),
688            v3
689        );
690    }
691
692    #[test]
693    fn tune_ip_multistream_background_bit_is_v3_reserved() {
694        // The multi-stream background bit (0x40) sits in a v3 reserved position:
695        // parsing the multi-stream bytes as v3 must NOT see a flag there, and the
696        // background flag is dropped (v3 has no background_tune_flag).
697        let ms = TuneIpReq {
698            mode: HostControlMode::MultiStream,
699            background_tune: true,
700            tune_quietly: false,
701            keep_app_running: false,
702            service_location_data: &[],
703        };
704        let bytes = ms.to_bytes();
705        // byte0 = 0x40 (background only).
706        assert_eq!(bytes[4], 0x40);
707        let as_v3 = TuneIpReq::parse_mode(&bytes, HostControlMode::BaseV3).unwrap();
708        assert!(!as_v3.background_tune);
709        assert!(!as_v3.tune_quietly);
710        assert!(!as_v3.keep_app_running);
711    }
712
713    #[test]
714    fn tune_ip_empty_location() {
715        let t = TuneIpReq {
716            mode: HostControlMode::MultiStream,
717            background_tune: false,
718            tune_quietly: false,
719            keep_app_running: false,
720            service_location_data: &[],
721        };
722        let bytes = t.to_bytes();
723        assert_eq!(bytes, [0x9F, 0x84, 0x08, 0x02, 0x00, 0x00]);
724        assert_eq!(
725            TuneIpReq::parse_mode(&bytes, HostControlMode::MultiStream).unwrap(),
726            t
727        );
728    }
729
730    #[test]
731    fn tuner_status_req_round_trips() {
732        let bytes = TunerStatusReq.to_bytes();
733        assert_eq!(bytes, [0x9F, 0x84, 0x0A, 0x00]);
734        assert_eq!(TunerStatusReq::parse(&bytes).unwrap(), TunerStatusReq);
735    }
736
737    #[test]
738    fn tuner_status_reply_round_trips_with_two_dsds() {
739        let r = TunerStatusReply {
740            ip_tune_capable: true,
741            dsds: alloc::vec![
742                TunerStatusDsd {
743                    connected: true,
744                    delivery_system_descriptor_tag: 0x44, // DVB-S2 dsd, say
745                    descriptor_tag_extension: None,
746                },
747                TunerStatusDsd {
748                    connected: false,
749                    delivery_system_descriptor_tag: 0x7F,
750                    descriptor_tag_extension: Some(0x79),
751                },
752            ],
753        };
754        let bytes = r.to_bytes();
755        // tag(3) + len(0x07) + byte0(ip_capable=0x80 | num_dsd=2 = 0x82)
756        //   dsd0: connected=1 -> 0x01, tag=0x44, reserved=0x00
757        //   dsd1: connected=0 -> 0x00, tag=0x7F, ext=0x79.
758        assert_eq!(
759            bytes,
760            [0x9F, 0x84, 0x0B, 0x07, 0x82, 0x01, 0x44, 0x00, 0x00, 0x7F, 0x79]
761        );
762        assert_eq!(TunerStatusReply::parse(&bytes).unwrap(), r);
763        let mut other = r.clone();
764        other.ip_tune_capable = false;
765        assert_eq!(other.to_bytes()[4], 0x02);
766        assert_ne!(bytes, other.to_bytes());
767    }
768
769    #[test]
770    fn tuner_status_reply_empty() {
771        let r = TunerStatusReply {
772            ip_tune_capable: false,
773            dsds: Vec::new(),
774        };
775        let bytes = r.to_bytes();
776        assert_eq!(bytes, [0x9F, 0x84, 0x0B, 0x01, 0x00]);
777        assert_eq!(TunerStatusReply::parse(&bytes).unwrap(), r);
778    }
779
780    #[test]
781    fn dispatch_routes_each_tag() {
782        let triplet = TuneTripletReq {
783            background_tune: false,
784            tune_quietly: false,
785            keep_app_running: false,
786            original_network_id: 1,
787            transport_stream_id: 2,
788            service_id: 3,
789            delivery_system_descriptor_tag: 0,
790            descriptor_tag_extension: None,
791        }
792        .to_bytes();
793        assert!(matches!(
794            MultistreamHostControlApdu::parse_mode(&triplet, HostControlMode::MultiStream).unwrap(),
795            MultistreamHostControlApdu::TuneTripletReq(_)
796        ));
797        let ip = TuneIpReq {
798            mode: HostControlMode::MultiStream,
799            background_tune: true,
800            tune_quietly: false,
801            keep_app_running: false,
802            service_location_data: &[0x01],
803        }
804        .to_bytes();
805        let parsed =
806            MultistreamHostControlApdu::parse_mode(&ip, HostControlMode::MultiStream).unwrap();
807        assert!(matches!(parsed, MultistreamHostControlApdu::TuneIpReq(_)));
808        assert_eq!(parsed.to_bytes(), ip);
809        // Unknown tag rejected.
810        assert!(matches!(
811            MultistreamHostControlApdu::parse_mode(
812                &[0x9F, 0x84, 0x00, 0x00],
813                HostControlMode::MultiStream
814            ),
815            Err(Error::UnexpectedApduTag { .. })
816        ));
817    }
818}