Skip to main content

dvb_ci/objects/
mmi_high.rs

1//! High-Level MMI objects — ETSI EN 50221 §8.6.5, Tables 46-51 (PDF pp. 47-50).
2//!
3//! - `text` (`9F 88 03` last / `9F 88 04` more, Table 46) — a text-char run; used
4//!   standalone and as the `TEXT()` component of Menu / List.
5//! - `enq` (`9F 88 07`, Table 47) — request a single user input (e.g. a PIN).
6//! - `answ` (`9F 88 08`, Table 48) — the user input reply.
7//! - `menu` (`9F 88 09` last / `9F 88 0A` more, Table 49) — title/sub/bottom +
8//!   choices.
9//! - `menu_answ` (`9F 88 0B`, Table 50) — the chosen `choice_ref`.
10//! - `list` (`9F 88 0C` last / `9F 88 0D` more, Table 51) — same shape as Menu.
11
12use crate::error::{Error, Result};
13use crate::length;
14use crate::tag::{self, ApduTag};
15use crate::traits::ApduDef;
16use alloc::vec::Vec;
17use broadcast_common::{Parse, Serialize};
18
19/// `text()` object (Table 46): a run of `text_char` bytes (EN 300 468 Annex A).
20///
21/// The `_last` (`9F 88 03`) and `_more` (`9F 88 04`) tags share this one body;
22/// [`Text::more`] selects which tag is written and is set from the parsed tag.
23#[derive(Debug, Clone, PartialEq, Eq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize))]
25pub struct Text<'a> {
26    /// `true` = `text_more` (more APDUs follow); `false` = `text_last`.
27    pub more: bool,
28    /// The `text_char` bytes (may be empty: a null text object).
29    #[cfg_attr(feature = "serde", serde(borrow, with = "super::bytes_serde"))]
30    pub text_chars: &'a [u8],
31}
32
33impl<'a> Text<'a> {
34    /// The `apdu_tag` this text object serializes to, given [`Text::more`].
35    #[must_use]
36    pub fn tag(&self) -> ApduTag {
37        if self.more {
38            tag::TEXT_MORE
39        } else {
40            tag::TEXT_LAST
41        }
42    }
43
44    /// Parse a nested `TEXT()` component at the front of `bytes`, returning the
45    /// object and the number of bytes it consumed (tag + length + chars).
46    pub(crate) fn parse_component(bytes: &'a [u8]) -> Result<(Self, usize)> {
47        if bytes.len() < 3 {
48            return Err(Error::BufferTooShort {
49                need: 3,
50                have: bytes.len(),
51                what: "text component tag",
52            });
53        }
54        let t = ApduTag::from_bytes(bytes[0], bytes[1], bytes[2]);
55        let more = match t {
56            tag::TEXT_LAST => false,
57            tag::TEXT_MORE => true,
58            _ => {
59                return Err(Error::UnexpectedApduTag {
60                    got: t.as_u24(),
61                    expected: tag::TEXT_LAST.as_u24(),
62                    what: "text component",
63                });
64            }
65        };
66        let (len_value, len_hdr) = length::decode(&bytes[3..])?;
67        let body_start = 3 + len_hdr;
68        let body_end = body_start + len_value;
69        if bytes.len() < body_end {
70            return Err(Error::LengthMismatch {
71                what: "text component",
72                declared: len_value,
73                actual: bytes.len().saturating_sub(body_start),
74            });
75        }
76        Ok((
77            Self {
78                more,
79                text_chars: &bytes[body_start..body_end],
80            },
81            body_end,
82        ))
83    }
84
85    /// Serialized length of this `TEXT()` component (tag + length + chars).
86    pub(crate) fn component_len(&self) -> usize {
87        super::apdu_len(self.text_chars.len())
88    }
89
90    /// Serialize this `TEXT()` component into the front of `buf`.
91    pub(crate) fn serialize_component(&self, buf: &mut [u8]) -> Result<usize> {
92        let mut pos = super::write_apdu_header(self.tag(), self.text_chars.len(), buf)?;
93        buf[pos..pos + self.text_chars.len()].copy_from_slice(self.text_chars);
94        pos += self.text_chars.len();
95        Ok(pos)
96    }
97}
98
99impl<'a> Parse<'a> for Text<'a> {
100    type Error = Error;
101    fn parse(bytes: &'a [u8]) -> Result<Self> {
102        let (t, consumed) = Self::parse_component(bytes)?;
103        // The whole APDU must be exactly this component.
104        if consumed != bytes.len() {
105            return Err(Error::LengthMismatch {
106                what: "text",
107                declared: consumed,
108                actual: bytes.len(),
109            });
110        }
111        Ok(t)
112    }
113}
114
115impl Serialize for Text<'_> {
116    type Error = Error;
117    fn serialized_len(&self) -> usize {
118        self.component_len()
119    }
120    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
121        self.serialize_component(buf)
122    }
123}
124
125impl<'a> ApduDef<'a> for Text<'a> {
126    const TAG: ApduTag = tag::TEXT_LAST;
127    const NAME: &'static str = "TEXT";
128}
129
130/// `enq()` object (Table 47): request a single user input.
131#[derive(Debug, Clone, PartialEq, Eq)]
132#[cfg_attr(feature = "serde", derive(serde::Serialize))]
133pub struct Enq<'a> {
134    /// `blind_answer` — when set, the user input is not displayed as typed.
135    pub blind_answer: bool,
136    /// `answer_text_length` — expected answer length (`0xFF` = unknown).
137    pub answer_text_length: u8,
138    /// The prompt `text_char` bytes (EN 300 468 Annex A).
139    #[cfg_attr(feature = "serde", serde(borrow, with = "super::bytes_serde"))]
140    pub text_chars: &'a [u8],
141}
142
143// reserved/blind_answer(1) + answer_text_length(1).
144const ENQ_PREFIX: usize = 2;
145
146impl<'a> Parse<'a> for Enq<'a> {
147    type Error = Error;
148    fn parse(bytes: &'a [u8]) -> Result<Self> {
149        let body = super::parse_apdu_header(bytes, tag::ENQ, "enq")?;
150        if body.len() < ENQ_PREFIX {
151            return Err(Error::BufferTooShort {
152                need: ENQ_PREFIX,
153                have: body.len(),
154                what: "enq",
155            });
156        }
157        Ok(Self {
158            blind_answer: (body[0] & 0x01) != 0,
159            answer_text_length: body[1],
160            text_chars: &body[ENQ_PREFIX..],
161        })
162    }
163}
164
165impl Serialize for Enq<'_> {
166    type Error = Error;
167    fn serialized_len(&self) -> usize {
168        super::apdu_len(ENQ_PREFIX + self.text_chars.len())
169    }
170    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
171        let body_len = ENQ_PREFIX + self.text_chars.len();
172        let mut pos = super::write_apdu_header(tag::ENQ, body_len, buf)?;
173        // reserved(7)='1111111', blind_answer(1).
174        buf[pos] = 0xFE | u8::from(self.blind_answer);
175        buf[pos + 1] = self.answer_text_length;
176        pos += ENQ_PREFIX;
177        buf[pos..pos + self.text_chars.len()].copy_from_slice(self.text_chars);
178        pos += self.text_chars.len();
179        Ok(pos)
180    }
181}
182
183impl<'a> ApduDef<'a> for Enq<'a> {
184    const TAG: ApduTag = tag::ENQ;
185    const NAME: &'static str = "ENQ";
186}
187
188/// `answ_id` values (Table 48, p. 48).
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190#[cfg_attr(feature = "serde", derive(serde::Serialize))]
191#[non_exhaustive]
192pub enum AnswId {
193    /// `00` — the user wishes to abort the dialogue.
194    Cancel,
195    /// `01` — the object contains the user input.
196    Answer,
197    /// Any other value (reserved).
198    Reserved(u8),
199}
200
201impl AnswId {
202    /// Decode an `answ_id` byte.
203    #[must_use]
204    pub fn from_u8(v: u8) -> Self {
205        match v {
206            0x00 => Self::Cancel,
207            0x01 => Self::Answer,
208            other => Self::Reserved(other),
209        }
210    }
211    /// Wire byte.
212    #[must_use]
213    pub const fn to_u8(self) -> u8 {
214        match self {
215            Self::Cancel => 0x00,
216            Self::Answer => 0x01,
217            Self::Reserved(v) => v,
218        }
219    }
220    /// Spec token, or `"reserved"`.
221    #[must_use]
222    pub fn name(&self) -> &'static str {
223        match self {
224            Self::Cancel => "cancel",
225            Self::Answer => "answer",
226            Self::Reserved(_) => "reserved",
227        }
228    }
229}
230broadcast_common::impl_spec_display!(AnswId, Reserved);
231
232/// `answ()` object (Table 48): the user input reply.
233#[derive(Debug, Clone, PartialEq, Eq)]
234#[cfg_attr(feature = "serde", derive(serde::Serialize))]
235pub struct Answ<'a> {
236    /// `answ_id`.
237    pub answ_id: AnswId,
238    /// The answer `text_char` bytes — present only when `answ_id == answer`.
239    #[cfg_attr(feature = "serde", serde(borrow, with = "super::bytes_serde"))]
240    pub text_chars: &'a [u8],
241}
242
243impl<'a> Parse<'a> for Answ<'a> {
244    type Error = Error;
245    fn parse(bytes: &'a [u8]) -> Result<Self> {
246        let body = super::parse_apdu_header(bytes, tag::ANSW, "answ")?;
247        let id_byte = *body.first().ok_or(Error::BufferTooShort {
248            need: 1,
249            have: 0,
250            what: "answ answ_id",
251        })?;
252        let answ_id = AnswId::from_u8(id_byte);
253        let text_chars = if answ_id == AnswId::Answer {
254            &body[1..]
255        } else {
256            &body[..0]
257        };
258        Ok(Self {
259            answ_id,
260            text_chars,
261        })
262    }
263}
264
265impl Serialize for Answ<'_> {
266    type Error = Error;
267    fn serialized_len(&self) -> usize {
268        let body = 1 + if self.answ_id == AnswId::Answer {
269            self.text_chars.len()
270        } else {
271            0
272        };
273        super::apdu_len(body)
274    }
275    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
276        let with_text = self.answ_id == AnswId::Answer;
277        let body_len = 1 + if with_text { self.text_chars.len() } else { 0 };
278        let mut pos = super::write_apdu_header(tag::ANSW, body_len, buf)?;
279        buf[pos] = self.answ_id.to_u8();
280        pos += 1;
281        if with_text {
282            buf[pos..pos + self.text_chars.len()].copy_from_slice(self.text_chars);
283            pos += self.text_chars.len();
284        }
285        Ok(pos)
286    }
287}
288
289impl<'a> ApduDef<'a> for Answ<'a> {
290    const TAG: ApduTag = tag::ANSW;
291    const NAME: &'static str = "ANSW";
292}
293
294/// `menu()` object (Table 49) and `list()` object (Table 51) share this layout:
295/// `choice_nb` / `item_nb`, three header TEXT()s, then the choice/item TEXT()s.
296///
297/// [`Menu::more`] selects the `_last` vs `_more` tag and is set from the parsed
298/// tag. `0xFF` for the count means "count not carried; read to end of object".
299#[derive(Debug, Clone, PartialEq, Eq)]
300#[cfg_attr(feature = "serde", derive(serde::Serialize))]
301pub struct Menu<'a> {
302    /// `true` = `_more` tag (more APDUs follow); `false` = `_last`.
303    pub more: bool,
304    /// `choice_nb` / `item_nb` — `0xFF` means the count is not carried on the wire.
305    pub choice_nb: u8,
306    /// Title TEXT().
307    #[cfg_attr(feature = "serde", serde(borrow))]
308    pub title: Text<'a>,
309    /// Sub-title TEXT().
310    #[cfg_attr(feature = "serde", serde(borrow))]
311    pub subtitle: Text<'a>,
312    /// Bottom-line TEXT().
313    #[cfg_attr(feature = "serde", serde(borrow))]
314    pub bottom: Text<'a>,
315    /// The per-choice / per-item TEXT()s, in wire order.
316    #[cfg_attr(feature = "serde", serde(borrow))]
317    pub choices: Vec<Text<'a>>,
318}
319
320/// `list()` object (Table 51) — byte-identical to `menu()` except for its tag
321/// pair (`9F 88 0C` last / `9F 88 0D` more). Wraps a [`Menu`]; the inner
322/// `Menu::more` selects `_last` vs `_more`.
323#[derive(Debug, Clone, PartialEq, Eq)]
324#[cfg_attr(feature = "serde", derive(serde::Serialize))]
325pub struct List<'a>(
326    /// The shared menu/list body.
327    #[cfg_attr(feature = "serde", serde(borrow))]
328    pub Menu<'a>,
329);
330
331impl<'a> Parse<'a> for List<'a> {
332    type Error = Error;
333    fn parse(bytes: &'a [u8]) -> Result<Self> {
334        Menu::parse_list(bytes).map(List)
335    }
336}
337
338impl Serialize for List<'_> {
339    type Error = Error;
340    fn serialized_len(&self) -> usize {
341        self.0.list_serialized_len()
342    }
343    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
344        self.0.serialize_list(buf)
345    }
346}
347
348impl<'a> ApduDef<'a> for List<'a> {
349    const TAG: ApduTag = tag::LIST_LAST;
350    const NAME: &'static str = "LIST";
351}
352
353impl<'a> Menu<'a> {
354    fn parse_with_tag(bytes: &'a [u8], expected: ApduTag, what: &'static str) -> Result<Self> {
355        let more = expected == tag::MENU_MORE || expected == tag::LIST_MORE;
356        let body = super::parse_apdu_header(bytes, expected, what)?;
357        let choice_nb = *body.first().ok_or(Error::BufferTooShort {
358            need: 1,
359            have: 0,
360            what,
361        })?;
362        let mut pos = 1usize;
363        let (title, n) = Text::parse_component(&body[pos..])?;
364        pos += n;
365        let (subtitle, n) = Text::parse_component(&body[pos..])?;
366        pos += n;
367        let (bottom, n) = Text::parse_component(&body[pos..])?;
368        pos += n;
369        let mut choices = Vec::new();
370        while pos < body.len() {
371            let (c, n) = Text::parse_component(&body[pos..])?;
372            pos += n;
373            choices.push(c);
374        }
375        Ok(Self {
376            more,
377            choice_nb,
378            title,
379            subtitle,
380            bottom,
381            choices,
382        })
383    }
384
385    fn body_len(&self) -> usize {
386        let mut n = 1
387            + self.title.component_len()
388            + self.subtitle.component_len()
389            + self.bottom.component_len();
390        for c in &self.choices {
391            n += c.component_len();
392        }
393        n
394    }
395
396    fn serialize_with_tag(&self, t: ApduTag, buf: &mut [u8]) -> Result<usize> {
397        let body_len = self.body_len();
398        let mut pos = super::write_apdu_header(t, body_len, buf)?;
399        buf[pos] = self.choice_nb;
400        pos += 1;
401        pos += self.title.serialize_component(&mut buf[pos..])?;
402        pos += self.subtitle.serialize_component(&mut buf[pos..])?;
403        pos += self.bottom.serialize_component(&mut buf[pos..])?;
404        for c in &self.choices {
405            pos += c.serialize_component(&mut buf[pos..])?;
406        }
407        Ok(pos)
408    }
409
410    /// The menu `apdu_tag` for this object given [`Menu::more`].
411    #[must_use]
412    pub fn menu_tag(&self) -> ApduTag {
413        if self.more {
414            tag::MENU_MORE
415        } else {
416            tag::MENU_LAST
417        }
418    }
419
420    /// The list `apdu_tag` for this object given [`Menu::more`] (for use as a
421    /// [`List`]).
422    #[must_use]
423    pub fn list_tag(&self) -> ApduTag {
424        if self.more {
425            tag::LIST_MORE
426        } else {
427            tag::LIST_LAST
428        }
429    }
430
431    /// Parse a `list()` object (Tables 51) from a complete APDU.
432    pub fn parse_list(bytes: &'a [u8]) -> Result<Self> {
433        if bytes.len() < 3 {
434            return Err(Error::BufferTooShort {
435                need: 3,
436                have: bytes.len(),
437                what: "list tag",
438            });
439        }
440        let t = ApduTag::from_bytes(bytes[0], bytes[1], bytes[2]);
441        let expected = if t == tag::LIST_MORE {
442            tag::LIST_MORE
443        } else {
444            tag::LIST_LAST
445        };
446        Self::parse_with_tag(bytes, expected, "list")
447    }
448
449    /// Serialized length of this object as a `list()` (identical to a menu).
450    #[must_use]
451    pub fn list_serialized_len(&self) -> usize {
452        super::apdu_len(self.body_len())
453    }
454
455    /// Serialize this object as a `list()` into `buf`.
456    pub fn serialize_list(&self, buf: &mut [u8]) -> Result<usize> {
457        self.serialize_with_tag(self.list_tag(), buf)
458    }
459}
460
461impl<'a> Parse<'a> for Menu<'a> {
462    type Error = Error;
463    fn parse(bytes: &'a [u8]) -> Result<Self> {
464        if bytes.len() < 3 {
465            return Err(Error::BufferTooShort {
466                need: 3,
467                have: bytes.len(),
468                what: "menu tag",
469            });
470        }
471        let t = ApduTag::from_bytes(bytes[0], bytes[1], bytes[2]);
472        let expected = if t == tag::MENU_MORE {
473            tag::MENU_MORE
474        } else {
475            tag::MENU_LAST
476        };
477        Self::parse_with_tag(bytes, expected, "menu")
478    }
479}
480
481impl Serialize for Menu<'_> {
482    type Error = Error;
483    fn serialized_len(&self) -> usize {
484        super::apdu_len(self.body_len())
485    }
486    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
487        self.serialize_with_tag(self.menu_tag(), buf)
488    }
489}
490
491impl<'a> ApduDef<'a> for Menu<'a> {
492    const TAG: ApduTag = tag::MENU_LAST;
493    const NAME: &'static str = "MENU";
494}
495
496/// `menu_answ()` object (Table 50): the chosen `choice_ref` (also used to close a
497/// list). `00` = the user cancelled.
498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
499#[cfg_attr(feature = "serde", derive(serde::Serialize))]
500pub struct MenuAnsw {
501    /// `choice_ref` — `01` = first choice, …; `00` = cancelled.
502    pub choice_ref: u8,
503}
504
505impl<'a> Parse<'a> for MenuAnsw {
506    type Error = Error;
507    fn parse(bytes: &'a [u8]) -> Result<Self> {
508        let body = super::parse_apdu_header(bytes, tag::MENU_ANSW, "menu_answ")?;
509        let choice_ref = *body.first().ok_or(Error::BufferTooShort {
510            need: 1,
511            have: 0,
512            what: "menu_answ choice_ref",
513        })?;
514        Ok(Self { choice_ref })
515    }
516}
517
518impl Serialize for MenuAnsw {
519    type Error = Error;
520    fn serialized_len(&self) -> usize {
521        super::apdu_len(1)
522    }
523    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
524        let mut pos = super::write_apdu_header(tag::MENU_ANSW, 1, buf)?;
525        buf[pos] = self.choice_ref;
526        pos += 1;
527        Ok(pos)
528    }
529}
530
531impl ApduDef<'_> for MenuAnsw {
532    const TAG: ApduTag = tag::MENU_ANSW;
533    const NAME: &'static str = "MENU_ANSW";
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539
540    fn text(more: bool, s: &[u8]) -> Text<'_> {
541        Text {
542            more,
543            text_chars: s,
544        }
545    }
546
547    #[test]
548    fn text_round_trips_and_more_bites() {
549        let t = text(false, b"HELLO");
550        let bytes = t.to_bytes();
551        assert_eq!(
552            bytes,
553            [0x9F, 0x88, 0x03, 0x05, b'H', b'E', b'L', b'L', b'O']
554        );
555        assert_eq!(Text::parse(&bytes).unwrap(), t);
556        // flipping `more` flips the tag byte.
557        let mut other = t.clone();
558        other.more = true;
559        let ob = other.to_bytes();
560        assert_eq!(ob[2], 0x04);
561        assert_ne!(bytes, ob);
562    }
563
564    #[test]
565    fn text_empty_is_null_object() {
566        let t = text(false, b"");
567        let bytes = t.to_bytes();
568        assert_eq!(bytes, [0x9F, 0x88, 0x03, 0x00]);
569        assert_eq!(Text::parse(&bytes).unwrap(), t);
570    }
571
572    #[test]
573    fn enq_pin_example_round_trips() {
574        // Spec worked example: enq "PLEASE TYPE YOUR PIN CODE", blind_answer=1.
575        let prompt = b"PLEASE TYPE YOUR PIN CODE";
576        let e = Enq {
577            blind_answer: true,
578            answer_text_length: 4,
579            text_chars: prompt,
580        };
581        let bytes = e.to_bytes();
582        // tag 9F8807, body = reserved|blind(0xFF) + answer_text_length(0x04) + prompt
583        assert_eq!(&bytes[..3], &[0x9F, 0x88, 0x07]);
584        assert_eq!(bytes[4], 0xFF); // reserved(7)=1 + blind_answer=1
585        assert_eq!(bytes[5], 0x04);
586        let parsed = Enq::parse(&bytes).unwrap();
587        assert_eq!(parsed, e);
588        assert!(parsed.blind_answer);
589        // bite: clear blind_answer changes the byte.
590        let mut other = e.clone();
591        other.blind_answer = false;
592        let ob = other.to_bytes();
593        assert_eq!(ob[4], 0xFE);
594        assert_ne!(bytes, ob);
595    }
596
597    #[test]
598    fn answ_answer_and_cancel() {
599        let a = Answ {
600            answ_id: AnswId::Answer,
601            text_chars: b"1234",
602        };
603        let bytes = a.to_bytes();
604        assert_eq!(
605            bytes,
606            [0x9F, 0x88, 0x08, 0x05, 0x01, b'1', b'2', b'3', b'4']
607        );
608        assert_eq!(Answ::parse(&bytes).unwrap(), a);
609
610        // cancel carries no text (even if text_chars set, it is dropped).
611        let c = Answ {
612            answ_id: AnswId::Cancel,
613            text_chars: &[],
614        };
615        let cb = c.to_bytes();
616        assert_eq!(cb, [0x9F, 0x88, 0x08, 0x01, 0x00]);
617        assert_eq!(Answ::parse(&cb).unwrap(), c);
618        assert_eq!(c.answ_id.name(), "cancel");
619
620        // bite
621        let mut other = a.clone();
622        other.text_chars = b"9999";
623        assert_ne!(bytes, other.to_bytes());
624    }
625
626    #[test]
627    fn menu_two_choice_example_round_trips_and_bites() {
628        // Spec worked example: menu "DO YOU WANT TO BUY?" / "JURASSIC PARK"
629        // with two choices.
630        let m = Menu {
631            more: false,
632            choice_nb: 2,
633            title: text(false, b"DO YOU WANT TO BUY?"),
634            subtitle: text(false, b"JURASSIC PARK"),
635            bottom: text(false, b""),
636            choices: alloc::vec![text(false, b"YES"), text(false, b"NO")],
637        };
638        let bytes = m.to_bytes();
639        assert_eq!(&bytes[..3], &[0x9F, 0x88, 0x09]);
640        let parsed = Menu::parse(&bytes).unwrap();
641        assert_eq!(parsed, m);
642        assert_eq!(parsed.choices.len(), 2);
643        assert_eq!(parsed.title.text_chars, b"DO YOU WANT TO BUY?");
644
645        // hand-compute the body length: choice_nb(1) + title(4+19) + sub(4+13)
646        // + bottom(4+0) + YES(4+3) + NO(4+2) = 1+23+17+4+7+6 = 58.
647        assert_eq!(bytes[3], 58);
648
649        // bite: mutate choice_nb.
650        let mut other = m.clone();
651        other.choice_nb = 0xFF;
652        assert_ne!(bytes, other.to_bytes());
653
654        // more bite: flipping `more` flips the tag.
655        let mut more = m.clone();
656        more.more = true;
657        let mb = more.to_bytes();
658        assert_eq!(mb[2], 0x0A);
659        assert_ne!(bytes, mb);
660    }
661
662    #[test]
663    fn list_uses_list_tags_and_round_trips() {
664        let l = List(Menu {
665            more: false,
666            choice_nb: 0xFF, // count not carried
667            title: text(false, b"ENTITLEMENTS"),
668            subtitle: text(false, b""),
669            bottom: text(false, b""),
670            choices: alloc::vec![text(false, b"A"), text(false, b"B"), text(false, b"C")],
671        });
672        let bytes = l.to_bytes();
673        assert_eq!(&bytes[..3], &[0x9F, 0x88, 0x0C]);
674        let parsed = List::parse(&bytes).unwrap();
675        assert_eq!(parsed, l);
676        assert_eq!(parsed.0.choices.len(), 3);
677
678        // list more tag flips.
679        let mut more = l.clone();
680        more.0.more = true;
681        let mb = more.to_bytes();
682        assert_eq!(mb[2], 0x0D);
683        assert_eq!(List::parse(&mb).unwrap(), more);
684        assert_ne!(bytes, mb);
685    }
686
687    #[test]
688    fn menu_answ_round_trips_and_bites() {
689        let a = MenuAnsw { choice_ref: 0x02 };
690        let bytes = a.to_bytes();
691        assert_eq!(bytes, [0x9F, 0x88, 0x0B, 0x01, 0x02]);
692        assert_eq!(MenuAnsw::parse(&bytes).unwrap(), a);
693        let other = MenuAnsw { choice_ref: 0x00 };
694        assert_ne!(bytes, other.to_bytes());
695    }
696}