Skip to main content

dvb_ci/
any.rs

1//! Unified APDU dispatch: [`AnyApdu`].
2//!
3//! [`AnyApdu`] is generated from a single declarative list (`declare_apdus!`) —
4//! one line per resource `apdu_tag`. The list is the single source of truth: it
5//! produces the enum, the `From<T>` conversions, the tag → parser dispatcher,
6//! and a drift test that pins each tag literal to the type's
7//! [`ApduDef::TAG`](crate::traits::ApduDef::TAG). Mirrors dvb-si's
8//! `AnyDescriptor` and scte35-splice's `AnyCommand`.
9//!
10//! An `apdu_tag` with no typed implementation (or one not yet implemented —
11//! e.g. the MMI high-level and low-speed-comms objects) falls through to
12//! [`AnyApdu::Unknown`], which keeps the raw APDU body so the unit round-trips
13//! byte-for-byte.
14
15use crate::error::{Error, Result};
16use crate::tag::ApduTag;
17use broadcast_common::{Parse, Serialize};
18
19/// Declares [`AnyApdu`] + its dispatcher from one `apdu_tag` list.
20macro_rules! declare_apdus {
21    (
22        $lt:lifetime;
23        $( $variant:ident = $($path:ident)::+ $(<$plt:lifetime>)?
24            $( [ $( $alt:path ),+ ] )? ),+ $(,)?
25    ) => {
26        /// Every crate-implemented APDU object, plus an `Unknown` fallthrough
27        /// that preserves the raw APDU header + body for lossless round-trips.
28        ///
29        /// serde uses external tagging with camelCase variant keys.
30        #[derive(Debug, Clone, PartialEq, Eq)]
31        #[cfg_attr(feature = "serde", derive(serde::Serialize))]
32        #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
33        #[non_exhaustive]
34        pub enum AnyApdu<$lt> {
35            $(
36                #[allow(missing_docs)]
37                $variant($($path)::+ $(<$plt>)?),
38            )+
39            /// An `apdu_tag` with no typed implementation; the fields are the
40            /// raw 3-byte tag and the verbatim body bytes (the `length_value`
41            /// bytes that followed the `length_field`).
42            Unknown {
43                /// The raw 3-byte `apdu_tag`.
44                tag: ApduTag,
45                /// The raw body bytes.
46                #[cfg_attr(feature = "serde", serde(with = "crate::objects::bytes_serde"))]
47                body: &$lt [u8],
48            },
49        }
50
51        $(
52            impl<$lt> From<$($path)::+ $(<$plt>)?> for AnyApdu<$lt> {
53                fn from(v: $($path)::+ $(<$plt>)?) -> Self {
54                    Self::$variant(v)
55                }
56            }
57        )+
58
59        impl<$lt> AnyApdu<$lt> {
60            /// Every `apdu_tag` the generated dispatcher routes (excludes
61            /// [`AnyApdu::Unknown`]).
62            pub const DISPATCHED_TAGS: &'static [ApduTag] = &[
63                $( <$($path)::+ as crate::traits::ApduDef>::TAG
64                   $(, $( $alt ),+ )? ),+
65            ];
66
67            /// Diagnostic SCREAMING_SNAKE name of the contained object
68            /// ([`ApduDef::NAME`](crate::traits::ApduDef::NAME)); `"UNKNOWN"` for
69            /// [`AnyApdu::Unknown`].
70            #[must_use]
71            pub fn name(&self) -> &'static str {
72                match self {
73                    $( Self::$variant(_) =>
74                        <$($path)::+ as crate::traits::ApduDef>::NAME, )+
75                    Self::Unknown { .. } => "UNKNOWN",
76                }
77            }
78
79            /// The object's `apdu_tag`.
80            #[must_use]
81            pub fn tag(&self) -> ApduTag {
82                match self {
83                    $( Self::$variant(_) =>
84                        <$($path)::+ as crate::traits::ApduDef>::TAG, )+
85                    Self::Unknown { tag, .. } => *tag,
86                }
87            }
88
89            /// Parse a complete APDU (header + body) by routing on its 3-byte
90            /// `apdu_tag`. Unrecognised tags yield [`AnyApdu::Unknown`].
91            pub fn parse(bytes: &$lt [u8]) -> Result<Self> {
92                if bytes.len() < 3 {
93                    return Err(Error::BufferTooShort { need: 3, have: bytes.len(), what: "apdu_tag" });
94                }
95                let tag = ApduTag::from_bytes(bytes[0], bytes[1], bytes[2]);
96                match tag {
97                    $( t if t == <$($path)::+ as crate::traits::ApduDef>::TAG
98                        $( || $( t == $alt )||+ )? =>
99                        <$($path)::+>::parse(bytes).map(Self::$variant), )+
100                    _ => {
101                        let body = crate::objects::parse_apdu_header(bytes, tag, "unknown apdu")?;
102                        Ok(Self::Unknown { tag, body })
103                    }
104                }
105            }
106        }
107
108        impl Serialize for AnyApdu<'_> {
109            type Error = Error;
110            fn serialized_len(&self) -> usize {
111                match self {
112                    $( Self::$variant(v) => v.serialized_len(), )+
113                    Self::Unknown { body, .. } => crate::objects::apdu_len(body.len()),
114                }
115            }
116            fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
117                match self {
118                    $( Self::$variant(v) => v.serialize_into(buf), )+
119                    Self::Unknown { tag, body } => {
120                        let mut pos = crate::objects::write_apdu_header(*tag, body.len(), buf)?;
121                        buf[pos..pos + body.len()].copy_from_slice(body);
122                        pos += body.len();
123                        Ok(pos)
124                    }
125                }
126            }
127        }
128
129        #[cfg(test)]
130        mod macro_drift {
131            #[test]
132            fn tag_literals_match_apdu_def() {
133                use crate::traits::ApduDef;
134                $(
135                    assert!(
136                        !<$($path)::+ as ApduDef>::NAME.is_empty(),
137                        concat!("empty NAME for ", stringify!($variant)),
138                    );
139                    // The list carries no separate literal: the dispatcher and
140                    // DISPATCHED_TAGS both read ApduDef::TAG, so this asserts the
141                    // tag is a public 0x9F-prefixed tag (Figure 16).
142                    assert_eq!(
143                        <$($path)::+ as ApduDef>::TAG.to_bytes()[0],
144                        crate::tag::APDU_TAG_PREFIX,
145                        concat!("non-0x9F apdu_tag for ", stringify!($variant)),
146                    );
147                )+
148            }
149        }
150    };
151}
152
153declare_apdus! {'a;
154    ProfileEnq          = crate::objects::resource_manager::ProfileEnq,
155    Profile             = crate::objects::resource_manager::Profile,
156    ProfileChange       = crate::objects::resource_manager::ProfileChange,
157    ApplicationInfoEnq  = crate::objects::application_info::ApplicationInfoEnq,
158    ApplicationInfo     = crate::objects::application_info::ApplicationInfo<'a>,
159    EnterMenu           = crate::objects::application_info::EnterMenu,
160    CaInfoEnq           = crate::objects::ca_info::CaInfoEnq,
161    CaInfo              = crate::objects::ca_info::CaInfo,
162    CaPmt               = crate::objects::ca_pmt::CaPmt<'a>,
163    CaPmtReply          = crate::objects::ca_pmt_reply::CaPmtReply,
164    DateTimeEnq         = crate::objects::date_time::DateTimeEnq,
165    DateTime            = crate::objects::date_time::DateTime,
166    CloseMmi            = crate::objects::mmi_close::CloseMmi,
167    // Host Control (§8.5).
168    Tune                = crate::objects::host_control::Tune,
169    Replace             = crate::objects::host_control::Replace,
170    ClearReplace        = crate::objects::host_control::ClearReplace,
171    AskRelease          = crate::objects::host_control::AskRelease,
172    // High-level MMI (§8.6.5).
173    Text                = crate::objects::mmi_high::Text<'a>         [crate::tag::TEXT_MORE],
174    Enq                 = crate::objects::mmi_high::Enq<'a>,
175    Answ                = crate::objects::mmi_high::Answ<'a>,
176    Menu                = crate::objects::mmi_high::Menu<'a>         [crate::tag::MENU_MORE],
177    MenuAnsw            = crate::objects::mmi_high::MenuAnsw,
178    List                = crate::objects::mmi_high::List<'a>         [crate::tag::LIST_MORE],
179    // Low-level / display / scene / download MMI (§8.6.2-8.6.4).
180    DisplayControl      = crate::objects::mmi_display::DisplayControl,
181    DisplayReply        = crate::objects::mmi_display::DisplayReply,
182    KeypadControl       = crate::objects::mmi_display::KeypadControl,
183    Keypress            = crate::objects::mmi_display::Keypress,
184    SubtitleSegment     = crate::objects::mmi_display::SubtitleSegment<'a>  [crate::tag::SUBTITLE_SEGMENT_MORE],
185    DisplayMessage      = crate::objects::mmi_display::DisplayMessage,
186    SceneEndMark        = crate::objects::mmi_display::SceneEndMark,
187    SceneDoneMessage    = crate::objects::mmi_display::SceneDoneMessage,
188    SceneControl        = crate::objects::mmi_display::SceneControl,
189    SubtitleDownload    = crate::objects::mmi_display::SubtitleDownload<'a> [crate::tag::SUBTITLE_DOWNLOAD_MORE],
190    FlushDownload       = crate::objects::mmi_display::FlushDownload,
191    DownloadReply       = crate::objects::mmi_display::DownloadReply,
192    // Low-speed comms (§8.7).
193    CommsCmd            = crate::objects::low_speed_comms::CommsCmd<'a>,
194    ConnectionDescriptor = crate::objects::low_speed_comms::ConnectionDescriptor<'a>,
195    CommsReply          = crate::objects::low_speed_comms::CommsReply,
196    CommsSend           = crate::objects::low_speed_comms::CommsSend<'a>    [crate::tag::COMMS_SEND_MORE],
197    CommsRcv            = crate::objects::low_speed_comms::CommsRcv<'a>     [crate::tag::COMMS_RCV_MORE],
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::objects::ca_pmt::{CaPmt, CaPmtListManagement};
204
205    #[test]
206    fn dispatch_ca_pmt() {
207        let pmt = CaPmt {
208            list_management: CaPmtListManagement::Only,
209            program_number: 1,
210            version_number: 0,
211            current_next_indicator: true,
212            cmd_id: None,
213            program_ca_descriptors: &[],
214            streams: alloc::vec::Vec::new(),
215        };
216        let bytes = pmt.to_bytes();
217        let any = AnyApdu::parse(&bytes).unwrap();
218        assert_eq!(any.name(), "CA_PMT");
219        assert_eq!(any.tag(), crate::tag::CA_PMT);
220        // round-trips through AnyApdu.
221        assert_eq!(any.to_bytes(), bytes);
222    }
223
224    #[test]
225    fn unknown_tag_round_trips() {
226        // Pick a private/unallocated tag (not in Table 58).
227        let bytes = [0x9F, 0x90, 0x01, 0x02, 0xAA, 0xBB];
228        let any = AnyApdu::parse(&bytes).unwrap();
229        assert!(matches!(any, AnyApdu::Unknown { .. }));
230        assert_eq!(any.name(), "UNKNOWN");
231        assert_eq!(any.to_bytes(), bytes);
232    }
233
234    #[test]
235    fn dispatches_more_tag_to_same_variant() {
236        use crate::objects::mmi_high::Text;
237        // text_more (9F8804) must dispatch to the Text variant, not Unknown.
238        let t = Text {
239            more: true,
240            text_chars: b"HI",
241        };
242        let bytes = t.to_bytes();
243        assert_eq!(bytes[2], 0x04);
244        let any = AnyApdu::parse(&bytes).unwrap();
245        assert_eq!(any.name(), "TEXT");
246        assert_eq!(any.to_bytes(), bytes);
247    }
248
249    #[test]
250    fn dispatched_tags_listed() {
251        assert!(AnyApdu::DISPATCHED_TAGS.contains(&crate::tag::CA_PMT));
252        assert!(AnyApdu::DISPATCHED_TAGS.contains(&crate::tag::PROFILE_ENQ));
253        assert!(AnyApdu::DISPATCHED_TAGS.contains(&crate::tag::TEXT_MORE));
254        assert!(AnyApdu::DISPATCHED_TAGS.contains(&crate::tag::COMMS_SEND_MORE));
255        // 40 primary tags (one per typed object) + 7 alt (_more) chaining tags.
256        assert_eq!(AnyApdu::DISPATCHED_TAGS.len(), 40 + 7);
257    }
258}