Skip to main content

dvb_ci/ci_ext/
application_mmi.rs

1//! Application MMI objects — ETSI TS 101 699 V1.1.1 §6.5, Tables 62-68
2//! (PDF pp. 57-61). See `docs/ci_plus/application-mmi.md`.
3//!
4//! Resource ID `0x00410041` (fixed). Lets a module interact with the user by
5//! launching an application on the host's application execution environment.
6//!
7//! - `RequestStart` (`9F 80 00`, Table 62) — app → host: application domain +
8//!   initial object (two length-prefixed byte strings).
9//! - `RequestStartAck` (`9F 80 01`, Table 63) — host → app: an `AckCode`.
10//! - `FileReq` (`9F 80 02`, Table 65) — host → app: a filename byte string.
11//! - `FileAck` (`9F 80 03`, Table 66) — app → host: `FileOK` flag + file bytes.
12//! - `AppAbortReq` (`9F 80 04`, Table 67) — either direction: opaque abort code.
13//! - `AppAbortAck` (`9F 80 05`, Table 68) — response: opaque abort-ack code.
14
15use crate::error::{Error, Result};
16use crate::objects;
17use crate::tag::ApduTag;
18use broadcast_common::{Parse, Serialize};
19
20/// Resource-scoped `apdu_tag`s for Application MMI (Tables 62-68).
21pub mod tag {
22    use crate::tag::ApduTag;
23    /// `RequestStartTag` = `9F 80 00`.
24    pub const REQUEST_START: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x00);
25    /// `RequestStartAckTag` = `9F 80 01`.
26    pub const REQUEST_START_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x01);
27    /// `FileReqTag` = `9F 80 02`.
28    pub const FILE_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x02);
29    /// `FileAckTag` = `9F 80 03`.
30    pub const FILE_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x03);
31    /// `AppAbortReqTag` = `9F 80 04`.
32    pub const APP_ABORT_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x04);
33    /// `AppAbortAckTag` = `9F 80 05`.
34    pub const APP_ABORT_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x05);
35}
36
37/// `AckCode` — response to a `RequestStart` (Table 64).
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize))]
40#[non_exhaustive]
41pub enum AckCode {
42    /// `0x01` — OK: the execution environment will load and run the initial object.
43    Ok,
44    /// `0x02` — Wrong API: application domain not supported.
45    WrongApi,
46    /// `0x03` — API busy: domain supported but not currently available.
47    ApiBusy,
48    /// `0x80`-`0xFF` — Domain-specific API busy.
49    DomainSpecificApiBusy(u8),
50    /// `0x00` and `0x04`-`0x7F` — reserved for future use.
51    Reserved(u8),
52}
53
54impl AckCode {
55    /// Decode an `AckCode` byte.
56    #[must_use]
57    pub fn from_u8(v: u8) -> Self {
58        match v {
59            0x01 => Self::Ok,
60            0x02 => Self::WrongApi,
61            0x03 => Self::ApiBusy,
62            0x80..=0xFF => Self::DomainSpecificApiBusy(v),
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::Ok => 0x01,
71            Self::WrongApi => 0x02,
72            Self::ApiBusy => 0x03,
73            Self::DomainSpecificApiBusy(v) | Self::Reserved(v) => v,
74        }
75    }
76    /// Spec token, or `"reserved"`.
77    #[must_use]
78    pub fn name(&self) -> &'static str {
79        match self {
80            Self::Ok => "OK",
81            Self::WrongApi => "Wrong API",
82            Self::ApiBusy => "API busy",
83            Self::DomainSpecificApiBusy(_) => "Domain specific API busy",
84            Self::Reserved(_) => "reserved",
85        }
86    }
87}
88broadcast_common::impl_spec_display!(AckCode, DomainSpecificApiBusy, Reserved);
89
90/// `RequestStart()` (Table 62): app → host.
91#[derive(Debug, Clone, PartialEq, Eq, Default)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize))]
93pub struct RequestStart<'a> {
94    /// `AppDomainIdentifier` — bytes specifying the required application domain.
95    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
96    pub app_domain_identifier: &'a [u8],
97    /// `InitialObject` — bytes specifying the initial object.
98    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
99    pub initial_object: &'a [u8],
100}
101
102/// `RequestStartAck()` (Table 63): host → app.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize))]
105pub struct RequestStartAck {
106    /// `AckCode`.
107    pub ack_code: AckCode,
108}
109
110/// `FileReq()` (Table 65): host → app — the requested filename bytes.
111#[derive(Debug, Clone, PartialEq, Eq, Default)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize))]
113pub struct FileReq<'a> {
114    /// `FileNameByte`s — the filename requested.
115    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
116    pub file_name: &'a [u8],
117}
118
119/// `FileAck()` (Table 66): app → host — delivers the requested file (or signals
120/// it is unavailable via `file_ok = false`).
121#[derive(Debug, Clone, PartialEq, Eq, Default)]
122#[cfg_attr(feature = "serde", derive(serde::Serialize))]
123pub struct FileAck<'a> {
124    /// `FileOK` — `true` if the file is available; `false` otherwise.
125    pub file_ok: bool,
126    /// `FileByte`s — the file payload (opaque; empty when `file_ok == false`).
127    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
128    pub file: &'a [u8],
129}
130
131/// `AppAbortReq()` (Table 67): host → app **or** app → host — opaque,
132/// application-domain-specific abort qualification.
133#[derive(Debug, Clone, PartialEq, Eq, Default)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize))]
135pub struct AppAbortReq<'a> {
136    /// `AbortReqCode` octet string.
137    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
138    pub abort_req_code: &'a [u8],
139}
140
141/// `AppAbortAck()` (Table 68): response to `AppAbortReq` — opaque,
142/// application-domain-specific abort-ack.
143#[derive(Debug, Clone, PartialEq, Eq, Default)]
144#[cfg_attr(feature = "serde", derive(serde::Serialize))]
145pub struct AppAbortAck<'a> {
146    /// `AbortAckCode` octet string.
147    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
148    pub abort_ack_code: &'a [u8],
149}
150
151// --- RequestStart ---
152
153// AppDomainIdentifierLength(1) + InitialObjectLength(1) + the two strings.
154const REQUEST_START_FIXED: usize = 2;
155
156impl<'a> Parse<'a> for RequestStart<'a> {
157    type Error = Error;
158    fn parse(bytes: &'a [u8]) -> Result<Self> {
159        let body = objects::parse_apdu_header(bytes, tag::REQUEST_START, "RequestStart")?;
160        if body.len() < REQUEST_START_FIXED {
161            return Err(Error::BufferTooShort {
162                need: REQUEST_START_FIXED,
163                have: body.len(),
164                what: "RequestStart",
165            });
166        }
167        let domain_len = body[0] as usize;
168        let object_len = body[1] as usize;
169        let need = REQUEST_START_FIXED + domain_len + object_len;
170        if body.len() < need {
171            return Err(Error::BufferTooShort {
172                need,
173                have: body.len(),
174                what: "RequestStart",
175            });
176        }
177        let domain_start = REQUEST_START_FIXED;
178        let object_start = domain_start + domain_len;
179        Ok(Self {
180            app_domain_identifier: &body[domain_start..object_start],
181            initial_object: &body[object_start..object_start + object_len],
182        })
183    }
184}
185impl Serialize for RequestStart<'_> {
186    type Error = Error;
187    fn serialized_len(&self) -> usize {
188        objects::apdu_len(
189            REQUEST_START_FIXED + self.app_domain_identifier.len() + self.initial_object.len(),
190        )
191    }
192    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
193        let domain_len = self.app_domain_identifier.len();
194        let object_len = self.initial_object.len();
195        if domain_len > u8::MAX as usize {
196            return Err(Error::InvalidObject {
197                what: "RequestStart",
198                reason: "AppDomainIdentifier longer than 255 bytes",
199            });
200        }
201        if object_len > u8::MAX as usize {
202            return Err(Error::InvalidObject {
203                what: "RequestStart",
204                reason: "InitialObject longer than 255 bytes",
205            });
206        }
207        let body_len = REQUEST_START_FIXED + domain_len + object_len;
208        let mut pos = objects::write_apdu_header(tag::REQUEST_START, body_len, buf)?;
209        buf[pos] = domain_len as u8;
210        buf[pos + 1] = object_len as u8;
211        pos += REQUEST_START_FIXED;
212        buf[pos..pos + domain_len].copy_from_slice(self.app_domain_identifier);
213        pos += domain_len;
214        buf[pos..pos + object_len].copy_from_slice(self.initial_object);
215        Ok(pos + object_len)
216    }
217}
218
219// --- RequestStartAck ---
220
221const REQUEST_START_ACK_BODY: usize = 1;
222
223impl<'a> Parse<'a> for RequestStartAck {
224    type Error = Error;
225    fn parse(bytes: &'a [u8]) -> Result<Self> {
226        let body = objects::parse_apdu_header(bytes, tag::REQUEST_START_ACK, "RequestStartAck")?;
227        if body.len() < REQUEST_START_ACK_BODY {
228            return Err(Error::BufferTooShort {
229                need: REQUEST_START_ACK_BODY,
230                have: body.len(),
231                what: "RequestStartAck",
232            });
233        }
234        Ok(Self {
235            ack_code: AckCode::from_u8(body[0]),
236        })
237    }
238}
239impl Serialize for RequestStartAck {
240    type Error = Error;
241    fn serialized_len(&self) -> usize {
242        objects::apdu_len(REQUEST_START_ACK_BODY)
243    }
244    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
245        let pos = objects::write_apdu_header(tag::REQUEST_START_ACK, REQUEST_START_ACK_BODY, buf)?;
246        buf[pos] = self.ack_code.to_u8();
247        Ok(pos + REQUEST_START_ACK_BODY)
248    }
249}
250
251// --- FileReq ---
252
253impl<'a> Parse<'a> for FileReq<'a> {
254    type Error = Error;
255    fn parse(bytes: &'a [u8]) -> Result<Self> {
256        let body = objects::parse_apdu_header(bytes, tag::FILE_REQ, "FileReq")?;
257        Ok(Self { file_name: body })
258    }
259}
260impl Serialize for FileReq<'_> {
261    type Error = Error;
262    fn serialized_len(&self) -> usize {
263        objects::apdu_len(self.file_name.len())
264    }
265    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
266        let body_len = self.file_name.len();
267        let pos = objects::write_apdu_header(tag::FILE_REQ, body_len, buf)?;
268        buf[pos..pos + body_len].copy_from_slice(self.file_name);
269        Ok(pos + body_len)
270    }
271}
272
273// --- FileAck ---
274
275/// The `FileOK` flag occupies the low bit of the leading byte; the upper 7 bits
276/// are reserved (`0`).
277const FILE_OK_BIT: u8 = 0x01;
278
279impl<'a> Parse<'a> for FileAck<'a> {
280    type Error = Error;
281    fn parse(bytes: &'a [u8]) -> Result<Self> {
282        let body = objects::parse_apdu_header(bytes, tag::FILE_ACK, "FileAck")?;
283        if body.is_empty() {
284            return Err(Error::BufferTooShort {
285                need: 1,
286                have: 0,
287                what: "FileAck",
288            });
289        }
290        Ok(Self {
291            file_ok: body[0] & FILE_OK_BIT != 0,
292            file: &body[1..],
293        })
294    }
295}
296impl Serialize for FileAck<'_> {
297    type Error = Error;
298    fn serialized_len(&self) -> usize {
299        objects::apdu_len(1 + self.file.len())
300    }
301    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
302        let body_len = 1 + self.file.len();
303        let mut pos = objects::write_apdu_header(tag::FILE_ACK, body_len, buf)?;
304        buf[pos] = u8::from(self.file_ok);
305        pos += 1;
306        buf[pos..pos + self.file.len()].copy_from_slice(self.file);
307        Ok(pos + self.file.len())
308    }
309}
310
311// --- AppAbortReq / AppAbortAck (opaque code loops) ---
312
313impl<'a> Parse<'a> for AppAbortReq<'a> {
314    type Error = Error;
315    fn parse(bytes: &'a [u8]) -> Result<Self> {
316        let body = objects::parse_apdu_header(bytes, tag::APP_ABORT_REQ, "AppAbortReq")?;
317        Ok(Self {
318            abort_req_code: body,
319        })
320    }
321}
322impl Serialize for AppAbortReq<'_> {
323    type Error = Error;
324    fn serialized_len(&self) -> usize {
325        objects::apdu_len(self.abort_req_code.len())
326    }
327    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
328        let body_len = self.abort_req_code.len();
329        let pos = objects::write_apdu_header(tag::APP_ABORT_REQ, body_len, buf)?;
330        buf[pos..pos + body_len].copy_from_slice(self.abort_req_code);
331        Ok(pos + body_len)
332    }
333}
334
335impl<'a> Parse<'a> for AppAbortAck<'a> {
336    type Error = Error;
337    fn parse(bytes: &'a [u8]) -> Result<Self> {
338        let body = objects::parse_apdu_header(bytes, tag::APP_ABORT_ACK, "AppAbortAck")?;
339        Ok(Self {
340            abort_ack_code: body,
341        })
342    }
343}
344impl Serialize for AppAbortAck<'_> {
345    type Error = Error;
346    fn serialized_len(&self) -> usize {
347        objects::apdu_len(self.abort_ack_code.len())
348    }
349    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
350        let body_len = self.abort_ack_code.len();
351        let pos = objects::write_apdu_header(tag::APP_ABORT_ACK, body_len, buf)?;
352        buf[pos..pos + body_len].copy_from_slice(self.abort_ack_code);
353        Ok(pos + body_len)
354    }
355}
356
357/// Resource-scoped dispatch over the Application MMI objects (Tables 62-68).
358#[derive(Debug, Clone, PartialEq, Eq)]
359#[cfg_attr(feature = "serde", derive(serde::Serialize))]
360#[non_exhaustive]
361pub enum ApplicationMmiApdu<'a> {
362    /// `RequestStart` (`9F 80 00`).
363    RequestStart(RequestStart<'a>),
364    /// `RequestStartAck` (`9F 80 01`).
365    RequestStartAck(RequestStartAck),
366    /// `FileReq` (`9F 80 02`).
367    FileReq(FileReq<'a>),
368    /// `FileAck` (`9F 80 03`).
369    FileAck(FileAck<'a>),
370    /// `AppAbortReq` (`9F 80 04`).
371    AppAbortReq(AppAbortReq<'a>),
372    /// `AppAbortAck` (`9F 80 05`).
373    AppAbortAck(AppAbortAck<'a>),
374}
375
376impl<'a> ApplicationMmiApdu<'a> {
377    /// Parse an Application MMI APDU, dispatching on the leading `apdu_tag`.
378    pub fn parse(body: &'a [u8]) -> Result<Self> {
379        if body.len() < 3 {
380            return Err(Error::BufferTooShort {
381                need: 3,
382                have: body.len(),
383                what: "application_mmi apdu_tag",
384            });
385        }
386        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
387        match t {
388            tag::REQUEST_START => Ok(Self::RequestStart(RequestStart::parse(body)?)),
389            tag::REQUEST_START_ACK => Ok(Self::RequestStartAck(RequestStartAck::parse(body)?)),
390            tag::FILE_REQ => Ok(Self::FileReq(FileReq::parse(body)?)),
391            tag::FILE_ACK => Ok(Self::FileAck(FileAck::parse(body)?)),
392            tag::APP_ABORT_REQ => Ok(Self::AppAbortReq(AppAbortReq::parse(body)?)),
393            tag::APP_ABORT_ACK => Ok(Self::AppAbortAck(AppAbortAck::parse(body)?)),
394            _ => Err(Error::UnexpectedApduTag {
395                got: t.as_u24(),
396                expected: tag::REQUEST_START.as_u24(),
397                what: "application_mmi",
398            }),
399        }
400    }
401}
402
403impl Serialize for ApplicationMmiApdu<'_> {
404    type Error = Error;
405    fn serialized_len(&self) -> usize {
406        match self {
407            Self::RequestStart(o) => o.serialized_len(),
408            Self::RequestStartAck(o) => o.serialized_len(),
409            Self::FileReq(o) => o.serialized_len(),
410            Self::FileAck(o) => o.serialized_len(),
411            Self::AppAbortReq(o) => o.serialized_len(),
412            Self::AppAbortAck(o) => o.serialized_len(),
413        }
414    }
415    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
416        match self {
417            Self::RequestStart(o) => o.serialize_into(buf),
418            Self::RequestStartAck(o) => o.serialize_into(buf),
419            Self::FileReq(o) => o.serialize_into(buf),
420            Self::FileAck(o) => o.serialize_into(buf),
421            Self::AppAbortReq(o) => o.serialize_into(buf),
422            Self::AppAbortAck(o) => o.serialize_into(buf),
423        }
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn request_start_round_trips_and_bites() {
433        let req = RequestStart {
434            app_domain_identifier: &[0x01, 0x02, 0x03],
435            initial_object: &[0xAA, 0xBB],
436        };
437        let bytes = req.to_bytes();
438        // tag(3) + len(1) + domLen(1=3) + objLen(1=2) + dom(3) + obj(2); body = 7 = 0x07.
439        assert_eq!(
440            bytes,
441            [
442                0x9F, 0x80, 0x00, 0x07, 0x03, 0x02, 0x01, 0x02, 0x03, 0xAA, 0xBB
443            ]
444        );
445        assert_eq!(RequestStart::parse(&bytes).unwrap(), req);
446        let other = RequestStart {
447            app_domain_identifier: &[0x01, 0x02, 0x04],
448            initial_object: &[0xAA, 0xBB],
449        };
450        assert_ne!(bytes, other.to_bytes());
451    }
452
453    #[test]
454    fn request_start_empty_strings() {
455        let req = RequestStart {
456            app_domain_identifier: &[],
457            initial_object: &[],
458        };
459        let bytes = req.to_bytes();
460        assert_eq!(bytes, [0x9F, 0x80, 0x00, 0x02, 0x00, 0x00]);
461        assert_eq!(RequestStart::parse(&bytes).unwrap(), req);
462    }
463
464    #[test]
465    fn request_start_ack_codes() {
466        for (byte, code) in [
467            (0x01u8, AckCode::Ok),
468            (0x02, AckCode::WrongApi),
469            (0x03, AckCode::ApiBusy),
470            (0x90, AckCode::DomainSpecificApiBusy(0x90)),
471            (0x00, AckCode::Reserved(0x00)),
472            (0x40, AckCode::Reserved(0x40)),
473        ] {
474            let ack = RequestStartAck {
475                ack_code: AckCode::from_u8(byte),
476            };
477            assert_eq!(ack.ack_code, code);
478            let bytes = ack.to_bytes();
479            assert_eq!(bytes, [0x9F, 0x80, 0x01, 0x01, byte]);
480            assert_eq!(RequestStartAck::parse(&bytes).unwrap(), ack);
481        }
482    }
483
484    #[test]
485    fn file_req_round_trips() {
486        let req = FileReq {
487            file_name: b"app.bin",
488        };
489        let bytes = req.to_bytes();
490        assert_eq!(bytes[..4], [0x9F, 0x80, 0x02, 0x07]);
491        assert_eq!(&bytes[4..], b"app.bin");
492        assert_eq!(FileReq::parse(&bytes).unwrap(), req);
493    }
494
495    #[test]
496    fn file_ack_round_trips_and_bites() {
497        let ack = FileAck {
498            file_ok: true,
499            file: &[0xDE, 0xAD, 0xBE, 0xEF],
500        };
501        let bytes = ack.to_bytes();
502        // tag(3) + len(1=5) + flag(1=0x01) + 4 file bytes.
503        assert_eq!(
504            bytes,
505            [0x9F, 0x80, 0x03, 0x05, 0x01, 0xDE, 0xAD, 0xBE, 0xEF]
506        );
507        assert_eq!(FileAck::parse(&bytes).unwrap(), ack);
508        // file_ok false => flag byte 0x00.
509        let unavailable = FileAck {
510            file_ok: false,
511            file: &[],
512        };
513        let bytes = unavailable.to_bytes();
514        assert_eq!(bytes, [0x9F, 0x80, 0x03, 0x01, 0x00]);
515        assert_eq!(FileAck::parse(&bytes).unwrap(), unavailable);
516        // reserved upper bits ignored on parse (0xFF -> file_ok true).
517        let parsed = FileAck::parse(&[0x9F, 0x80, 0x03, 0x01, 0xFF]).unwrap();
518        assert!(parsed.file_ok);
519    }
520
521    #[test]
522    fn app_abort_req_and_ack_round_trip() {
523        let req = AppAbortReq {
524            abort_req_code: &[0x11, 0x22],
525        };
526        let bytes = req.to_bytes();
527        assert_eq!(bytes, [0x9F, 0x80, 0x04, 0x02, 0x11, 0x22]);
528        assert_eq!(AppAbortReq::parse(&bytes).unwrap(), req);
529
530        let ack = AppAbortAck {
531            abort_ack_code: &[0x33],
532        };
533        let bytes = ack.to_bytes();
534        assert_eq!(bytes, [0x9F, 0x80, 0x05, 0x01, 0x33]);
535        assert_eq!(AppAbortAck::parse(&bytes).unwrap(), ack);
536    }
537
538    #[test]
539    fn dispatch_routes_each_tag() {
540        let rs = RequestStart {
541            app_domain_identifier: &[0x01],
542            initial_object: &[],
543        }
544        .to_bytes();
545        assert!(matches!(
546            ApplicationMmiApdu::parse(&rs).unwrap(),
547            ApplicationMmiApdu::RequestStart(_)
548        ));
549        let aaa = AppAbortAck {
550            abort_ack_code: &[0x01],
551        }
552        .to_bytes();
553        let parsed = ApplicationMmiApdu::parse(&aaa).unwrap();
554        assert!(matches!(parsed, ApplicationMmiApdu::AppAbortAck(_)));
555        assert_eq!(parsed.to_bytes(), aaa);
556    }
557}