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 dvb-scte35'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 dvb_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    ) => {
25        /// Every crate-implemented APDU object, plus an `Unknown` fallthrough
26        /// that preserves the raw APDU header + body for lossless round-trips.
27        ///
28        /// serde uses external tagging with camelCase variant keys.
29        #[derive(Debug, Clone, PartialEq, Eq)]
30        #[cfg_attr(feature = "serde", derive(serde::Serialize))]
31        #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
32        #[non_exhaustive]
33        pub enum AnyApdu<$lt> {
34            $(
35                #[allow(missing_docs)]
36                $variant($($path)::+ $(<$plt>)?),
37            )+
38            /// An `apdu_tag` with no typed implementation; the fields are the
39            /// raw 3-byte tag and the verbatim body bytes (the `length_value`
40            /// bytes that followed the `length_field`).
41            Unknown {
42                /// The raw 3-byte `apdu_tag`.
43                tag: ApduTag,
44                /// The raw body bytes.
45                #[cfg_attr(feature = "serde", serde(with = "crate::objects::bytes_serde"))]
46                body: &$lt [u8],
47            },
48        }
49
50        $(
51            impl<$lt> From<$($path)::+ $(<$plt>)?> for AnyApdu<$lt> {
52                fn from(v: $($path)::+ $(<$plt>)?) -> Self {
53                    Self::$variant(v)
54                }
55            }
56        )+
57
58        impl<$lt> AnyApdu<$lt> {
59            /// Every `apdu_tag` the generated dispatcher routes (excludes
60            /// [`AnyApdu::Unknown`]).
61            pub const DISPATCHED_TAGS: &'static [ApduTag] = &[
62                $( <$($path)::+ as crate::traits::ApduDef>::TAG ),+
63            ];
64
65            /// Diagnostic SCREAMING_SNAKE name of the contained object
66            /// ([`ApduDef::NAME`](crate::traits::ApduDef::NAME)); `"UNKNOWN"` for
67            /// [`AnyApdu::Unknown`].
68            #[must_use]
69            pub fn name(&self) -> &'static str {
70                match self {
71                    $( Self::$variant(_) =>
72                        <$($path)::+ as crate::traits::ApduDef>::NAME, )+
73                    Self::Unknown { .. } => "UNKNOWN",
74                }
75            }
76
77            /// The object's `apdu_tag`.
78            #[must_use]
79            pub fn tag(&self) -> ApduTag {
80                match self {
81                    $( Self::$variant(_) =>
82                        <$($path)::+ as crate::traits::ApduDef>::TAG, )+
83                    Self::Unknown { tag, .. } => *tag,
84                }
85            }
86
87            /// Parse a complete APDU (header + body) by routing on its 3-byte
88            /// `apdu_tag`. Unrecognised tags yield [`AnyApdu::Unknown`].
89            pub fn parse(bytes: &$lt [u8]) -> Result<Self> {
90                if bytes.len() < 3 {
91                    return Err(Error::BufferTooShort { need: 3, have: bytes.len(), what: "apdu_tag" });
92                }
93                let tag = ApduTag::from_bytes(bytes[0], bytes[1], bytes[2]);
94                match tag {
95                    $( t if t == <$($path)::+ as crate::traits::ApduDef>::TAG =>
96                        <$($path)::+>::parse(bytes).map(Self::$variant), )+
97                    _ => {
98                        let body = crate::objects::parse_apdu_header(bytes, tag, "unknown apdu")?;
99                        Ok(Self::Unknown { tag, body })
100                    }
101                }
102            }
103        }
104
105        impl Serialize for AnyApdu<'_> {
106            type Error = Error;
107            fn serialized_len(&self) -> usize {
108                match self {
109                    $( Self::$variant(v) => v.serialized_len(), )+
110                    Self::Unknown { body, .. } => crate::objects::apdu_len(body.len()),
111                }
112            }
113            fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
114                match self {
115                    $( Self::$variant(v) => v.serialize_into(buf), )+
116                    Self::Unknown { tag, body } => {
117                        let mut pos = crate::objects::write_apdu_header(*tag, body.len(), buf)?;
118                        buf[pos..pos + body.len()].copy_from_slice(body);
119                        pos += body.len();
120                        Ok(pos)
121                    }
122                }
123            }
124        }
125
126        #[cfg(test)]
127        mod macro_drift {
128            #[test]
129            fn tag_literals_match_apdu_def() {
130                use crate::traits::ApduDef;
131                $(
132                    assert!(
133                        !<$($path)::+ as ApduDef>::NAME.is_empty(),
134                        concat!("empty NAME for ", stringify!($variant)),
135                    );
136                    // The list carries no separate literal: the dispatcher and
137                    // DISPATCHED_TAGS both read ApduDef::TAG, so this asserts the
138                    // tag is a public 0x9F-prefixed tag (Figure 16).
139                    assert_eq!(
140                        <$($path)::+ as ApduDef>::TAG.to_bytes()[0],
141                        crate::tag::APDU_TAG_PREFIX,
142                        concat!("non-0x9F apdu_tag for ", stringify!($variant)),
143                    );
144                )+
145            }
146        }
147    };
148}
149
150declare_apdus! {'a;
151    ProfileEnq          = crate::objects::resource_manager::ProfileEnq,
152    Profile             = crate::objects::resource_manager::Profile,
153    ProfileChange       = crate::objects::resource_manager::ProfileChange,
154    ApplicationInfoEnq  = crate::objects::application_info::ApplicationInfoEnq,
155    ApplicationInfo     = crate::objects::application_info::ApplicationInfo<'a>,
156    EnterMenu           = crate::objects::application_info::EnterMenu,
157    CaInfoEnq           = crate::objects::ca_info::CaInfoEnq,
158    CaInfo              = crate::objects::ca_info::CaInfo,
159    CaPmt               = crate::objects::ca_pmt::CaPmt<'a>,
160    CaPmtReply          = crate::objects::ca_pmt_reply::CaPmtReply,
161    DateTimeEnq         = crate::objects::date_time::DateTimeEnq,
162    DateTime            = crate::objects::date_time::DateTime,
163    CloseMmi            = crate::objects::mmi_close::CloseMmi,
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::objects::ca_pmt::{CaPmt, CaPmtListManagement};
170
171    #[test]
172    fn dispatch_ca_pmt() {
173        let pmt = CaPmt {
174            list_management: CaPmtListManagement::Only,
175            program_number: 1,
176            version_number: 0,
177            current_next_indicator: true,
178            cmd_id: None,
179            program_ca_descriptors: &[],
180            streams: alloc::vec::Vec::new(),
181        };
182        let bytes = pmt.to_bytes();
183        let any = AnyApdu::parse(&bytes).unwrap();
184        assert_eq!(any.name(), "CA_PMT");
185        assert_eq!(any.tag(), crate::tag::CA_PMT);
186        // round-trips through AnyApdu.
187        assert_eq!(any.to_bytes(), bytes);
188    }
189
190    #[test]
191    fn unknown_tag_round_trips() {
192        // Tenter_menu we DO implement; pick an unimplemented MMI tag (Tenq 9F8807).
193        let bytes = [0x9F, 0x88, 0x07, 0x02, 0xAA, 0xBB];
194        let any = AnyApdu::parse(&bytes).unwrap();
195        assert!(matches!(any, AnyApdu::Unknown { .. }));
196        assert_eq!(any.name(), "UNKNOWN");
197        assert_eq!(any.to_bytes(), bytes);
198    }
199
200    #[test]
201    fn dispatched_tags_listed() {
202        assert!(AnyApdu::DISPATCHED_TAGS.contains(&crate::tag::CA_PMT));
203        assert!(AnyApdu::DISPATCHED_TAGS.contains(&crate::tag::PROFILE_ENQ));
204        assert_eq!(AnyApdu::DISPATCHED_TAGS.len(), 13);
205    }
206}