Skip to main content

dvb_ci/ci_ext/
application_info_v2.rs

1//! Application Information v2 objects — ETSI TS 101 699 V1.1.1 §5, Table 11
2//! (PDF p. 21). See `docs/ci_plus/application-info-v2.md`.
3//!
4//! Resource ID `0x00020042`. The object layouts (Application Info Enquiry,
5//! Application Info, Enter Menu) are **unchanged** from EN 50221 §8.4 — v2 only
6//! extends the `application_type` value set (§5.1.1) and adds the
7//! "unrecognized type → Unclassified" rule (§5.1.2). This module re-defines the
8//! objects with the v2 [`ApplicationTypeV2`] enum so the v2 resource owns its set.
9//!
10//! - `application_info_enq` (`9F 80 20`, EN 50221 Table 20) — header-only.
11//! - `application_info` (`9F 80 21`, EN 50221 Table 21) — type/manufacturer + menu.
12//! - `enter_menu` (`9F 80 22`, EN 50221 Table 22) — header-only.
13
14use crate::error::{Error, Result};
15use crate::objects;
16use crate::tag::ApduTag;
17use broadcast_common::{Parse, Serialize};
18
19/// Resource-scoped `apdu_tag`s for Application Information v2 (Table 87).
20pub mod tag {
21    use crate::tag::ApduTag;
22    /// `Tapplication_info_enq` = `9F 80 20`.
23    pub const APPLICATION_INFO_ENQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x20);
24    /// `Tapplication_info` = `9F 80 21`.
25    pub const APPLICATION_INFO: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x21);
26    /// `Tenter_menu` = `9F 80 22`.
27    pub const ENTER_MENU: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x22);
28}
29
30/// `application_type` — the v2 value set (TS 101 699 Table 11).
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize))]
33#[non_exhaustive]
34pub enum ApplicationTypeV2 {
35    /// `01` — Conditional Access (inherited from EN 50221 §8.4).
36    ConditionalAccess,
37    /// `02` — Electronic Programme Guide (inherited from EN 50221 §8.4).
38    ElectronicProgrammeGuide,
39    /// `03` — Software upgrade.
40    SoftwareUpgrade,
41    /// `04` — Network interface.
42    NetworkInterface,
43    /// `05` — Accessibility aids.
44    AccessibilityAids,
45    /// `06` — Unclassified.
46    Unclassified,
47    /// Any other value (reserved). §5.1.2: a v2 host treats an unrecognized type
48    /// as Unclassified, but the wire value is retained for lossless round-trip.
49    Reserved(u8),
50}
51
52impl ApplicationTypeV2 {
53    /// Decode an `application_type` byte.
54    #[must_use]
55    pub fn from_u8(v: u8) -> Self {
56        match v {
57            0x01 => Self::ConditionalAccess,
58            0x02 => Self::ElectronicProgrammeGuide,
59            0x03 => Self::SoftwareUpgrade,
60            0x04 => Self::NetworkInterface,
61            0x05 => Self::AccessibilityAids,
62            0x06 => Self::Unclassified,
63            other => Self::Reserved(other),
64        }
65    }
66    /// Wire byte.
67    #[must_use]
68    pub const fn to_u8(self) -> u8 {
69        match self {
70            Self::ConditionalAccess => 0x01,
71            Self::ElectronicProgrammeGuide => 0x02,
72            Self::SoftwareUpgrade => 0x03,
73            Self::NetworkInterface => 0x04,
74            Self::AccessibilityAids => 0x05,
75            Self::Unclassified => 0x06,
76            Self::Reserved(v) => v,
77        }
78    }
79    /// Spec token, or `"reserved"`.
80    #[must_use]
81    pub fn name(&self) -> &'static str {
82        match self {
83            Self::ConditionalAccess => "Conditional_Access",
84            Self::ElectronicProgrammeGuide => "Electronic_Programme_Guide",
85            Self::SoftwareUpgrade => "Software_upgrade",
86            Self::NetworkInterface => "Network_interface",
87            Self::AccessibilityAids => "Accessibility_aids",
88            Self::Unclassified => "Unclassified",
89            Self::Reserved(_) => "reserved",
90        }
91    }
92    /// The effective type a v2 host applies (§5.1.2): an unrecognized
93    /// (`Reserved`) type is treated as [`Unclassified`](Self::Unclassified).
94    #[must_use]
95    pub fn effective(self) -> Self {
96        match self {
97            Self::Reserved(_) => Self::Unclassified,
98            known => known,
99        }
100    }
101}
102broadcast_common::impl_spec_display!(ApplicationTypeV2, Reserved);
103
104/// `application_info_enq()` — header-only enquiry.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize))]
107pub struct ApplicationInfoEnq;
108
109/// `enter_menu()` — header-only command.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize))]
112pub struct EnterMenu;
113
114/// `application_info()` reply (EN 50221 Table 21, with the v2 type set). The
115/// `menu_string` is the raw top-level menu title text (EN 300 468 Annex A),
116/// carried verbatim so it round-trips.
117#[derive(Debug, Clone, PartialEq, Eq)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize))]
119pub struct ApplicationInfo<'a> {
120    /// `application_type`.
121    pub application_type: ApplicationTypeV2,
122    /// `application_manufacturer` (16-bit).
123    pub application_manufacturer: u16,
124    /// `manufacturer_code` (16-bit).
125    pub manufacturer_code: u16,
126    /// `text_char` bytes of the menu title (length is `menu_string_length`, max 255).
127    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
128    pub menu_string: &'a [u8],
129}
130
131// --- empty-body objects ---
132
133impl<'a> Parse<'a> for ApplicationInfoEnq {
134    type Error = Error;
135    fn parse(bytes: &'a [u8]) -> Result<Self> {
136        objects::parse_empty_apdu(bytes, tag::APPLICATION_INFO_ENQ, "application_info_enq")?;
137        Ok(Self)
138    }
139}
140impl Serialize for ApplicationInfoEnq {
141    type Error = Error;
142    fn serialized_len(&self) -> usize {
143        objects::empty_apdu_len()
144    }
145    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
146        objects::serialize_empty_apdu(tag::APPLICATION_INFO_ENQ, buf)
147    }
148}
149
150impl<'a> Parse<'a> for EnterMenu {
151    type Error = Error;
152    fn parse(bytes: &'a [u8]) -> Result<Self> {
153        objects::parse_empty_apdu(bytes, tag::ENTER_MENU, "enter_menu")?;
154        Ok(Self)
155    }
156}
157impl Serialize for EnterMenu {
158    type Error = Error;
159    fn serialized_len(&self) -> usize {
160        objects::empty_apdu_len()
161    }
162    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
163        objects::serialize_empty_apdu(tag::ENTER_MENU, buf)
164    }
165}
166
167// --- application_info ---
168
169// application_type(1) + manufacturer(2) + code(2) + menu_string_length(1).
170const APP_INFO_PREFIX: usize = 6;
171
172impl<'a> Parse<'a> for ApplicationInfo<'a> {
173    type Error = Error;
174    fn parse(bytes: &'a [u8]) -> Result<Self> {
175        let body = objects::parse_apdu_header(bytes, tag::APPLICATION_INFO, "application_info")?;
176        if body.len() < APP_INFO_PREFIX {
177            return Err(Error::BufferTooShort {
178                need: APP_INFO_PREFIX,
179                have: body.len(),
180                what: "application_info",
181            });
182        }
183        let menu_len = body[5] as usize;
184        let menu_end = APP_INFO_PREFIX + menu_len;
185        if body.len() < menu_end {
186            return Err(Error::LengthMismatch {
187                what: "application_info menu_string",
188                declared: menu_len,
189                actual: body.len() - APP_INFO_PREFIX,
190            });
191        }
192        Ok(Self {
193            application_type: ApplicationTypeV2::from_u8(body[0]),
194            application_manufacturer: u16::from_be_bytes([body[1], body[2]]),
195            manufacturer_code: u16::from_be_bytes([body[3], body[4]]),
196            menu_string: &body[APP_INFO_PREFIX..menu_end],
197        })
198    }
199}
200impl Serialize for ApplicationInfo<'_> {
201    type Error = Error;
202    fn serialized_len(&self) -> usize {
203        objects::apdu_len(APP_INFO_PREFIX + self.menu_string.len())
204    }
205    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
206        if self.menu_string.len() > u8::MAX as usize {
207            return Err(Error::InvalidObject {
208                what: "application_info",
209                reason: "menu_string longer than 255 bytes",
210            });
211        }
212        let body_len = APP_INFO_PREFIX + self.menu_string.len();
213        let mut pos = objects::write_apdu_header(tag::APPLICATION_INFO, body_len, buf)?;
214        buf[pos] = self.application_type.to_u8();
215        buf[pos + 1..pos + 3].copy_from_slice(&self.application_manufacturer.to_be_bytes());
216        buf[pos + 3..pos + 5].copy_from_slice(&self.manufacturer_code.to_be_bytes());
217        buf[pos + 5] = self.menu_string.len() as u8;
218        pos += APP_INFO_PREFIX;
219        buf[pos..pos + self.menu_string.len()].copy_from_slice(self.menu_string);
220        Ok(pos + self.menu_string.len())
221    }
222}
223
224/// Resource-scoped dispatch over the Application Information v2 objects.
225#[derive(Debug, Clone, PartialEq, Eq)]
226#[cfg_attr(feature = "serde", derive(serde::Serialize))]
227#[non_exhaustive]
228pub enum ApplicationInfoV2Apdu<'a> {
229    /// `application_info_enq` (`9F 80 20`).
230    ApplicationInfoEnq(ApplicationInfoEnq),
231    /// `application_info` (`9F 80 21`).
232    ApplicationInfo(ApplicationInfo<'a>),
233    /// `enter_menu` (`9F 80 22`).
234    EnterMenu(EnterMenu),
235}
236
237impl<'a> ApplicationInfoV2Apdu<'a> {
238    /// Parse an Application Information v2 APDU, dispatching on the `apdu_tag`.
239    pub fn parse(body: &'a [u8]) -> Result<Self> {
240        if body.len() < 3 {
241            return Err(Error::BufferTooShort {
242                need: 3,
243                have: body.len(),
244                what: "application_info_v2 apdu_tag",
245            });
246        }
247        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
248        match t {
249            tag::APPLICATION_INFO_ENQ => {
250                Ok(Self::ApplicationInfoEnq(ApplicationInfoEnq::parse(body)?))
251            }
252            tag::APPLICATION_INFO => Ok(Self::ApplicationInfo(ApplicationInfo::parse(body)?)),
253            tag::ENTER_MENU => Ok(Self::EnterMenu(EnterMenu::parse(body)?)),
254            _ => Err(Error::UnexpectedApduTag {
255                got: t.as_u24(),
256                expected: tag::APPLICATION_INFO.as_u24(),
257                what: "application_info_v2",
258            }),
259        }
260    }
261}
262
263impl Serialize for ApplicationInfoV2Apdu<'_> {
264    type Error = Error;
265    fn serialized_len(&self) -> usize {
266        match self {
267            Self::ApplicationInfoEnq(o) => o.serialized_len(),
268            Self::ApplicationInfo(o) => o.serialized_len(),
269            Self::EnterMenu(o) => o.serialized_len(),
270        }
271    }
272    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
273        match self {
274            Self::ApplicationInfoEnq(o) => o.serialize_into(buf),
275            Self::ApplicationInfo(o) => o.serialize_into(buf),
276            Self::EnterMenu(o) => o.serialize_into(buf),
277        }
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn enq_and_enter_menu_round_trip() {
287        assert_eq!(ApplicationInfoEnq.to_bytes(), [0x9F, 0x80, 0x20, 0x00]);
288        assert_eq!(EnterMenu.to_bytes(), [0x9F, 0x80, 0x22, 0x00]);
289        assert_eq!(
290            ApplicationInfoEnq::parse(&[0x9F, 0x80, 0x20, 0x00]).unwrap(),
291            ApplicationInfoEnq
292        );
293    }
294
295    #[test]
296    fn application_info_round_trips_and_bites() {
297        let info = ApplicationInfo {
298            application_type: ApplicationTypeV2::AccessibilityAids,
299            application_manufacturer: 0x1234,
300            manufacturer_code: 0x5678,
301            menu_string: b"Audio",
302        };
303        let bytes = info.to_bytes();
304        // tag(3) + len(1) + prefix(6) + 5 = 15; body len = 11 = 0x0B.
305        assert_eq!(
306            bytes,
307            [
308                0x9F, 0x80, 0x21, 0x0B, 0x05, 0x12, 0x34, 0x56, 0x78, 0x05, b'A', b'u', b'd', b'i',
309                b'o'
310            ]
311        );
312        assert_eq!(ApplicationInfo::parse(&bytes).unwrap(), info);
313        assert_eq!(info.application_type.name(), "Accessibility_aids");
314        let mut other = info.clone();
315        other.application_type = ApplicationTypeV2::Unclassified;
316        assert_ne!(bytes, other.to_bytes());
317    }
318
319    #[test]
320    fn unrecognized_type_treated_as_unclassified() {
321        let t = ApplicationTypeV2::from_u8(0x7F);
322        assert_eq!(t, ApplicationTypeV2::Reserved(0x7F));
323        assert_eq!(t.effective(), ApplicationTypeV2::Unclassified);
324        // wire value preserved for round-trip.
325        assert_eq!(t.to_u8(), 0x7F);
326    }
327
328    #[test]
329    fn dispatch_routes_each_tag() {
330        let info = ApplicationInfo {
331            application_type: ApplicationTypeV2::SoftwareUpgrade,
332            application_manufacturer: 0,
333            manufacturer_code: 0,
334            menu_string: b"",
335        }
336        .to_bytes();
337        assert!(matches!(
338            ApplicationInfoV2Apdu::parse(&info).unwrap(),
339            ApplicationInfoV2Apdu::ApplicationInfo(_)
340        ));
341    }
342}