Skip to main content

dvb_ci/objects/
application_info.rs

1//! Application Information objects — ETSI EN 50221 §8.4.2, Tables 20-22
2//! (PDF pp. 27-28).
3//!
4//! - `application_info_enq` (`9F 80 20`, Table 20) — header-only enquiry.
5//! - `application_info` (`9F 80 21`, Table 21) — type/manufacturer + menu string.
6//! - `enter_menu` (`9F 80 22`, Table 22) — header-only command.
7
8use crate::error::{Error, Result};
9use crate::tag::{self, ApduTag};
10use crate::traits::ApduDef;
11use broadcast_common::{Parse, Serialize};
12
13/// `application_type` (Table, p. 28).
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize))]
16#[non_exhaustive]
17pub enum ApplicationType {
18    /// `01` — Conditional Access.
19    ConditionalAccess,
20    /// `02` — Electronic Programme Guide.
21    ElectronicProgrammeGuide,
22    /// Any other value (reserved).
23    Reserved(u8),
24}
25
26impl ApplicationType {
27    /// Decode an `application_type` byte.
28    #[must_use]
29    pub fn from_u8(v: u8) -> Self {
30        match v {
31            0x01 => Self::ConditionalAccess,
32            0x02 => Self::ElectronicProgrammeGuide,
33            other => Self::Reserved(other),
34        }
35    }
36    /// Wire byte for this `application_type`.
37    #[must_use]
38    pub const fn to_u8(self) -> u8 {
39        match self {
40            Self::ConditionalAccess => 0x01,
41            Self::ElectronicProgrammeGuide => 0x02,
42            Self::Reserved(v) => v,
43        }
44    }
45    /// Spec token, or `"reserved"`.
46    #[must_use]
47    pub fn name(&self) -> &'static str {
48        match self {
49            Self::ConditionalAccess => "Conditional_Access",
50            Self::ElectronicProgrammeGuide => "Electronic_Programme_Guide",
51            Self::Reserved(_) => "reserved",
52        }
53    }
54}
55broadcast_common::impl_spec_display!(ApplicationType, Reserved);
56
57/// `application_info_enq()` — header-only enquiry (Table 20).
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize))]
60pub struct ApplicationInfoEnq;
61
62/// `enter_menu()` — header-only command (Table 22).
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize))]
65pub struct EnterMenu;
66
67/// `application_info()` reply (Table 21). The `menu_string` is the raw text
68/// bytes of the top-level menu title (EN 300 468 Annex A coding); carried
69/// verbatim so it round-trips.
70#[derive(Debug, Clone, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize))]
72pub struct ApplicationInfo<'a> {
73    /// `application_type`.
74    pub application_type: ApplicationType,
75    /// `application_manufacturer` (16-bit).
76    pub application_manufacturer: u16,
77    /// `manufacturer_code` (16-bit).
78    pub manufacturer_code: u16,
79    /// `text_char` bytes of the top-level menu title (length is the
80    /// `menu_string_length` field, max 255).
81    #[cfg_attr(feature = "serde", serde(borrow, with = "super::bytes_serde"))]
82    pub menu_string: &'a [u8],
83}
84
85// --- application_info_enq (empty body) ---
86
87impl<'a> Parse<'a> for ApplicationInfoEnq {
88    type Error = Error;
89    fn parse(bytes: &'a [u8]) -> Result<Self> {
90        super::parse_empty_apdu(bytes, tag::APPLICATION_INFO_ENQ, "application_info_enq")?;
91        Ok(Self)
92    }
93}
94impl Serialize for ApplicationInfoEnq {
95    type Error = Error;
96    fn serialized_len(&self) -> usize {
97        super::empty_apdu_len()
98    }
99    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
100        super::serialize_empty_apdu(tag::APPLICATION_INFO_ENQ, buf)
101    }
102}
103impl ApduDef<'_> for ApplicationInfoEnq {
104    const TAG: ApduTag = tag::APPLICATION_INFO_ENQ;
105    const NAME: &'static str = "APPLICATION_INFO_ENQ";
106}
107
108// --- enter_menu (empty body) ---
109
110impl<'a> Parse<'a> for EnterMenu {
111    type Error = Error;
112    fn parse(bytes: &'a [u8]) -> Result<Self> {
113        super::parse_empty_apdu(bytes, tag::ENTER_MENU, "enter_menu")?;
114        Ok(Self)
115    }
116}
117impl Serialize for EnterMenu {
118    type Error = Error;
119    fn serialized_len(&self) -> usize {
120        super::empty_apdu_len()
121    }
122    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
123        super::serialize_empty_apdu(tag::ENTER_MENU, buf)
124    }
125}
126impl ApduDef<'_> for EnterMenu {
127    const TAG: ApduTag = tag::ENTER_MENU;
128    const NAME: &'static str = "ENTER_MENU";
129}
130
131// --- application_info ---
132
133/// Fixed-prefix length: application_type(1) + manufacturer(2) + code(2) +
134/// menu_string_length(1).
135const APP_INFO_PREFIX: usize = 6;
136
137impl<'a> Parse<'a> for ApplicationInfo<'a> {
138    type Error = Error;
139    fn parse(bytes: &'a [u8]) -> Result<Self> {
140        let body = super::parse_apdu_header(bytes, tag::APPLICATION_INFO, "application_info")?;
141        if body.len() < APP_INFO_PREFIX {
142            return Err(Error::BufferTooShort {
143                need: APP_INFO_PREFIX,
144                have: body.len(),
145                what: "application_info",
146            });
147        }
148        let application_type = ApplicationType::from_u8(body[0]);
149        let application_manufacturer = u16::from_be_bytes([body[1], body[2]]);
150        let manufacturer_code = u16::from_be_bytes([body[3], body[4]]);
151        let menu_string_length = body[5] as usize;
152        let menu_end = APP_INFO_PREFIX + menu_string_length;
153        if body.len() < menu_end {
154            return Err(Error::LengthMismatch {
155                what: "application_info menu_string",
156                declared: menu_string_length,
157                actual: body.len() - APP_INFO_PREFIX,
158            });
159        }
160        Ok(Self {
161            application_type,
162            application_manufacturer,
163            manufacturer_code,
164            menu_string: &body[APP_INFO_PREFIX..menu_end],
165        })
166    }
167}
168
169impl Serialize for ApplicationInfo<'_> {
170    type Error = Error;
171    fn serialized_len(&self) -> usize {
172        super::apdu_len(APP_INFO_PREFIX + self.menu_string.len())
173    }
174    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
175        if self.menu_string.len() > u8::MAX as usize {
176            return Err(Error::InvalidObject {
177                what: "application_info",
178                reason: "menu_string longer than 255 bytes",
179            });
180        }
181        let body_len = APP_INFO_PREFIX + self.menu_string.len();
182        let mut pos = super::write_apdu_header(tag::APPLICATION_INFO, body_len, buf)?;
183        buf[pos] = self.application_type.to_u8();
184        buf[pos + 1..pos + 3].copy_from_slice(&self.application_manufacturer.to_be_bytes());
185        buf[pos + 3..pos + 5].copy_from_slice(&self.manufacturer_code.to_be_bytes());
186        buf[pos + 5] = self.menu_string.len() as u8;
187        pos += APP_INFO_PREFIX;
188        buf[pos..pos + self.menu_string.len()].copy_from_slice(self.menu_string);
189        Ok(pos + self.menu_string.len())
190    }
191}
192
193impl<'a> ApduDef<'a> for ApplicationInfo<'a> {
194    const TAG: ApduTag = tag::APPLICATION_INFO;
195    const NAME: &'static str = "APPLICATION_INFO";
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn enq_round_trip() {
204        let bytes = ApplicationInfoEnq.to_bytes();
205        assert_eq!(bytes, [0x9F, 0x80, 0x20, 0x00]);
206        assert_eq!(
207            ApplicationInfoEnq::parse(&bytes).unwrap(),
208            ApplicationInfoEnq
209        );
210    }
211
212    #[test]
213    fn enter_menu_round_trip() {
214        let bytes = EnterMenu.to_bytes();
215        assert_eq!(bytes, [0x9F, 0x80, 0x22, 0x00]);
216        assert_eq!(EnterMenu::parse(&bytes).unwrap(), EnterMenu);
217    }
218
219    #[test]
220    fn application_info_round_trip() {
221        let info = ApplicationInfo {
222            application_type: ApplicationType::ConditionalAccess,
223            application_manufacturer: 0x1234,
224            manufacturer_code: 0x5678,
225            menu_string: b"CA Module",
226        };
227        let bytes = info.to_bytes();
228        // tag(3)+len(1)+prefix(6)+9
229        assert_eq!(bytes.len(), 19);
230        assert_eq!(&bytes[..4], &[0x9F, 0x80, 0x21, 0x0F]); // len 15
231        let parsed = ApplicationInfo::parse(&bytes).unwrap();
232        assert_eq!(parsed, info);
233        assert_eq!(parsed.application_type.name(), "Conditional_Access");
234    }
235
236    #[test]
237    fn mutating_field_changes_bytes() {
238        let info = ApplicationInfo {
239            application_type: ApplicationType::ConditionalAccess,
240            application_manufacturer: 0x1234,
241            manufacturer_code: 0x5678,
242            menu_string: b"abc",
243        };
244        let a = info.to_bytes();
245        let mut other = info.clone();
246        other.application_manufacturer = 0x9999;
247        assert_ne!(a, other.to_bytes());
248    }
249
250    #[test]
251    fn empty_menu_string() {
252        let info = ApplicationInfo {
253            application_type: ApplicationType::ElectronicProgrammeGuide,
254            application_manufacturer: 0,
255            manufacturer_code: 0,
256            menu_string: b"",
257        };
258        let bytes = info.to_bytes();
259        assert_eq!(ApplicationInfo::parse(&bytes).unwrap(), info);
260    }
261}