Skip to main content

dvb_t2mi/payload/
any.rs

1//! Unified payload dispatch: [`AnyPayload`].
2//!
3//! [`AnyPayload`] is generated from a single declarative list
4//! (`declare_payloads!`) — one line per T2-MI payload type.
5//! The list is the single source of truth: it produces the enum, the
6//! `From<T>` conversions, the `packet_type` → parser dispatcher, and a drift
7//! test that pins each literal to the type's
8//! [`crate::traits::PayloadDef::PACKET_TYPE`].
9//!
10//! # Dispatch contract
11//!
12//! [`AnyPayload::dispatch`] takes the **payload bytes only** (the bytes after
13//! the 6-byte T2-MI header, up to but not including the 4-byte CRC trailer).
14//! Each payload parser expects exactly those bytes — the header and CRC are NOT
15//! passed in.  To recover the payload slice from a raw packet buffer use
16//! [`crate::packet::Header::payload_bytes`].
17//!
18//! # Adding a payload
19//!
20//! 1. Create the module with the wire layout and the symmetric
21//!    [`broadcast_common::Parse`] / [`broadcast_common::Serialize`] impls + round-trip
22//!    tests (copy an existing module).
23//! 2. `impl PayloadDef` for the type (`PACKET_TYPE` from the spec / the
24//!    [`crate::packet::PacketType`] enum value, `NAME` in SCREAMING_SNAKE
25//!    without the `_payload` suffix).
26//! 3. Add one line to the `declare_payloads!` invocation below — the enum
27//!    variant, dispatcher arm, and drift test are generated from it.
28//! 4. The integration completeness test walks the generated
29//!    [`AnyPayload::DISPATCHED_TYPES`] automatically — no test edits needed.
30
31use alloc::boxed::Box;
32
33/// Declares [`AnyPayload`] + its dispatcher from one packet-type list.
34///
35/// Each line is `Variant = 0xTYPE => module::Type[<'a>]`.
36macro_rules! declare_payloads {
37    (
38        $lt:lifetime;
39        $( $variant:ident = $ptype:literal => $($path:ident)::+ $(<$plt:lifetime>)? ),+ $(,)?
40    ) => {
41        /// Every crate-implemented T2-MI payload, plus an `Unknown` fallthrough.
42        ///
43        /// serde uses external tagging with camelCase variant keys.
44        /// Variant names map 1:1 to the payload modules; see each module
45        /// for the wire layout.
46        ///
47        /// # Dispatch contract
48        ///
49        /// Use [`AnyPayload::dispatch`] with the payload bytes (post-header,
50        /// pre-CRC). See the module-level documentation for details.
51        #[derive(Debug)]
52        #[cfg_attr(feature = "serde", derive(serde::Serialize))]
53        #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
54        // Covariant in `$lt`: every variant holds only lifetime-parametrised
55        // payload views or `&$lt [u8]` (`Unknown`), so the derive is sound.
56        #[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
57        #[non_exhaustive]
58        pub enum AnyPayload<$lt> {
59            $(
60                #[allow(missing_docs)]
61                $variant($($path)::+ $(<$plt>)?),
62            )+
63            /// Runtime-registered custom payload (see [`PayloadRegistry`]).
64            ///
65            /// [`PayloadRegistry`]: crate::payload::registry::PayloadRegistry
66            Other {
67                /// The raw `packet_type` byte.
68                packet_type: u8,
69                /// The parsed, type-erased payload value. Call `downcast_ref` on
70                /// it (see [`PayloadObject`][crate::payload::registry::PayloadObject]) to recover the concrete type.
71                #[cfg_attr(
72                    feature = "serde",
73                    serde(serialize_with = "crate::payload::registry::serialize_erased")
74                )]
75                value: Box<dyn crate::payload::registry::PayloadObject>,
76            },
77            /// Packet type with no typed implementation; `body` contains the
78            /// raw payload bytes (post-header, pre-CRC).
79            Unknown {
80                /// The raw `packet_type` byte.
81                packet_type: u8,
82                /// The raw payload bytes.
83                body: &$lt [u8],
84            },
85        }
86
87        $(
88            impl<$lt> From<$($path)::+ $(<$plt>)?> for AnyPayload<$lt> {
89                fn from(p: $($path)::+ $(<$plt>)?) -> Self {
90                    Self::$variant(p)
91                }
92            }
93        )+
94
95        impl<$lt> AnyPayload<$lt> {
96            /// Every `packet_type` the generated dispatcher routes (excludes
97            /// [`AnyPayload::Unknown`]).
98            pub const DISPATCHED_TYPES: &'static [u8] = &[$($ptype),+];
99
100            /// Diagnostic name of the contained payload — the type's
101            /// [`PayloadDef::NAME`](crate::traits::PayloadDef::NAME)
102            /// (`"BBFRAME"`, `"L1_CURRENT"`, …); `"CUSTOM"` for
103            /// [`AnyPayload::Other`] (runtime-registered) and `"UNKNOWN"`
104            /// for [`AnyPayload::Unknown`].
105            #[must_use]
106            pub fn name(&self) -> &'static str {
107                match self {
108                    $(
109                        Self::$variant(_) =>
110                            <$($path)::+ as crate::traits::PayloadDef>::NAME,
111                    )+
112                    Self::Other { .. } => "CUSTOM",
113                    Self::Unknown { .. } => "UNKNOWN",
114                }
115            }
116
117            /// Parse one payload by its `packet_type`.
118            ///
119            /// `payload_bytes` must be the **payload-only slice** (bytes after
120            /// the 6-byte T2-MI header, before the 4-byte CRC trailer).
121            ///
122            /// Returns `None` when `packet_type` has no typed implementation
123            /// (the caller turns that into [`AnyPayload::Unknown`]).
124            /// Returns `Some(Err)` on a typed parse failure for a recognised type.
125            ///
126            /// See the [module-level documentation][self] for the dispatch
127            /// contract (payload-only bytes, header and CRC excluded).
128            pub fn dispatch(
129                packet_type: u8,
130                payload_bytes: &$lt [u8],
131            ) -> Option<crate::Result<Self>> {
132                use broadcast_common::Parse;
133                match packet_type {
134                    $(
135                        $ptype => Some(
136                            <$($path)::+>::parse(payload_bytes).map(Self::$variant),
137                        ),
138                    )+
139                    _ => None,
140                }
141            }
142        }
143
144        #[cfg(test)]
145        mod macro_drift {
146            #[test]
147            fn packet_type_literals_match_payload_def() {
148                use crate::traits::PayloadDef;
149                $(
150                    assert_eq!(
151                        $ptype,
152                        <$($path)::+ as PayloadDef>::PACKET_TYPE,
153                        concat!("PACKET_TYPE literal drift for ", stringify!($variant)),
154                    );
155                    assert!(
156                        !<$($path)::+ as PayloadDef>::NAME.is_empty(),
157                        concat!("empty NAME for ", stringify!($variant)),
158                    );
159                )+
160            }
161        }
162    };
163}
164
165declare_payloads! {'a;
166    // TS 102 773 Table 1 — all 12 defined packet types in numerical order.
167    Bbframe              = 0x00 => crate::payload::bbframe::BbframePayload<'a>,
168    AuxIq                = 0x01 => crate::payload::aux_iq::AuxIqPayload<'a>,
169    ArbitraryCells       = 0x02 => crate::payload::arbitrary_cells::ArbitraryCellsPayload<'a>,
170    L1Current            = 0x10 => crate::payload::l1_current::L1CurrentPayload<'a>,
171    L1Future             = 0x11 => crate::payload::l1_future::L1FuturePayload<'a>,
172    P2Bias               = 0x12 => crate::payload::p2_bias::P2BiasPayload,
173    Timestamp            = 0x20 => crate::payload::timestamp::T2TimestampPayload,
174    IndividualAddressing = 0x21 => crate::payload::individual_addressing::IndividualAddressingPayload<'a>,
175    FefNull              = 0x30 => crate::payload::fef_null::FefNullPayload,
176    FefIq                = 0x31 => crate::payload::fef_iq::FefIqPayload<'a>,
177    FefComposite         = 0x32 => crate::payload::fef_composite::FefCompositePayload,
178    FefSubpart           = 0x33 => crate::payload::fef_subpart::FefSubPartPayload<'a>,
179}
180
181impl<'a> AnyPayload<'a> {
182    /// Parse one payload by its `packet_type`, preferring the registry's custom
183    /// parsers over the built-in dispatch.
184    ///
185    /// `payload_bytes` must be the **payload-only slice** (bytes after the
186    /// 6-byte T2-MI header, before the 4-byte CRC trailer).
187    ///
188    /// # Precedence
189    ///
190    /// 1. If `registry` holds a custom parser for `packet_type`, it is called;
191    ///    the result becomes [`AnyPayload::Other`] (or an error on parse failure).
192    /// 2. Otherwise, falls back to the built-in [`AnyPayload::dispatch`].
193    /// 3. If neither route handles `packet_type`, returns `None` — the caller
194    ///    turns that into [`AnyPayload::Unknown`].
195    ///
196    /// See the [module-level documentation][self] for the dispatch contract
197    /// (payload-only bytes, header and CRC excluded).
198    pub fn dispatch_with(
199        registry: &crate::payload::registry::PayloadRegistry,
200        packet_type: u8,
201        payload_bytes: &'a [u8],
202    ) -> Option<crate::Result<Self>> {
203        if let Some(parse_fn) = registry.lookup(packet_type) {
204            return Some(match parse_fn(payload_bytes) {
205                Ok(value) => Ok(Self::Other { packet_type, value }),
206                Err(e) => Err(e),
207            });
208        }
209        // Fall back to built-in dispatch
210        Self::dispatch(packet_type, payload_bytes)
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    // ── Completeness ─────────────────────────────────────────────────────────
219
220    /// `AnyPayload::name()` reflects `PayloadDef::NAME`; `UNKNOWN` for unknowns.
221    #[test]
222    fn name_maps_variant_to_payloaddef_name() {
223        let bb = AnyPayload::dispatch(0x00, &[0x00, 0x00, 0x00])
224            .expect("dispatched")
225            .expect("valid bbframe payload");
226        assert_eq!(bb.name(), "BBFRAME");
227        let unknown = AnyPayload::Unknown {
228            packet_type: 0x7F,
229            body: &[],
230        };
231        assert_eq!(unknown.name(), "UNKNOWN");
232
233        // A runtime-registered custom payload reports "CUSTOM".
234        use crate::payload::registry::PayloadRegistry;
235
236        // An unregistered packet_type yields no custom dispatch.
237        let empty = PayloadRegistry::new();
238        assert!(AnyPayload::dispatch_with(&empty, 0x40, &[]).is_none());
239
240        #[derive(Debug)]
241        #[cfg_attr(feature = "serde", derive(serde::Serialize))]
242        struct NameTestPayload {
243            _x: u8,
244        }
245
246        impl<'a> broadcast_common::Parse<'a> for NameTestPayload {
247            type Error = crate::Error;
248            fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
249                if bytes.is_empty() {
250                    return Err(crate::Error::BufferTooShort {
251                        need: 1,
252                        have: 0,
253                        what: "NameTest",
254                    });
255                }
256                Ok(Self { _x: bytes[0] })
257            }
258        }
259
260        impl<'a> crate::traits::PayloadDef<'a> for NameTestPayload {
261            const PACKET_TYPE: u8 = 0x41;
262            const NAME: &'static str = "NAME_TEST";
263        }
264
265        let mut reg = PayloadRegistry::new();
266        reg.register::<NameTestPayload>();
267        let parsed = AnyPayload::dispatch_with(&reg, 0x41, &[0xAA])
268            .unwrap()
269            .unwrap();
270        assert_eq!(parsed.name(), "CUSTOM");
271    }
272
273    /// Every entry in DISPATCHED_TYPES must dispatch to a non-Unknown variant.
274    #[test]
275    fn every_dispatched_type_routes_non_unknown() {
276        // Minimal valid payload bytes for each packet type (all RFU = 0 — the
277        // parsers reject non-zero reserved bits). See each payload module's
278        // own tests for full boundary coverage.
279
280        // 0x00 BBFrame: frame_idx(1) + plp_id(1) + intl_frame_start+rfu(1) = 3 bytes.
281        let bbframe_bytes: &[u8] = &[0x00, 0x00, 0x00];
282        // 0x01 AuxIq: frame_idx(1) + aux_id(4bits, must be 1..=15)+rfu(4bits)(1) + rfu(1) = 3 bytes.
283        // aux_id=1: byte1 = (1<<4) = 0x10.
284        let aux_iq_bytes: &[u8] = &[0x00, 0x10, 0x00];
285        // 0x02 ArbitraryCells: 8-byte header (rfu bytes 3,4 = 0, byte5 top 2 = 0).
286        let arb_cells_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
287        // 0x10 L1Current: frame_idx(1) + freq_source(2bits)+rfu(6bits)(1) = 2 bytes.
288        let l1_current_bytes: &[u8] = &[0x00, 0x00];
289        // 0x11 L1Future: frame_idx(1) + rfu(1) = 2 bytes.
290        let l1_future_bytes: &[u8] = &[0x00, 0x00];
291        // 0x12 P2Bias: 5 bytes, all rfu = 0.
292        let p2_bias_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00];
293        // 0x20 Timestamp: 11 bytes, rfu top 4 bits of byte0 = 0, bw=0 (1.7 MHz).
294        let timestamp_bytes: &[u8] = &[
295            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
296        ];
297        // 0x21 IndividualAddressing: rfu(1) + length(1, value 0) = 2 bytes.
298        let indiv_addr_bytes: &[u8] = &[0x00, 0x00];
299        // 0x30 FefNull: fef_idx(1) + rfu(1, must be 0) + s1_field+s2_field(1) = 3 bytes.
300        let fef_null_bytes: &[u8] = &[0x00, 0x00, 0x00];
301        // 0x31 FefIq: fef_idx(1) + rfu(1, must be 0) + s1+s2(1) = 3 bytes.
302        let fef_iq_bytes: &[u8] = &[0x00, 0x00, 0x00];
303        // 0x32 FefComposite: 8 bytes. byte1 [7]=rfu1=0, bytes2-5=rfu2=0.
304        let fef_composite_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
305        // 0x33 FefSubpart: 15 bytes.
306        // bytes 3-6 = rfu1 = 0, byte 11 = rfu2 = 0, byte 12 top 2 = 0.
307        // subpart_variety bytes 9-10 = 0x0000 = Null.
308        let fef_subpart_bytes: &[u8] = &[
309            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
310            0x00,
311        ];
312
313        let fixtures: &[(u8, &[u8])] = &[
314            (0x00, bbframe_bytes),
315            (0x01, aux_iq_bytes),
316            (0x02, arb_cells_bytes),
317            (0x10, l1_current_bytes),
318            (0x11, l1_future_bytes),
319            (0x12, p2_bias_bytes),
320            (0x20, timestamp_bytes),
321            (0x21, indiv_addr_bytes),
322            (0x30, fef_null_bytes),
323            (0x31, fef_iq_bytes),
324            (0x32, fef_composite_bytes),
325            (0x33, fef_subpart_bytes),
326        ];
327
328        for &(pt, bytes) in fixtures {
329            let result = AnyPayload::dispatch(pt, bytes);
330            assert!(result.is_some(), "0x{pt:02x} returned None from dispatch");
331            let parsed = result.unwrap();
332            assert!(
333                parsed.is_ok(),
334                "0x{pt:02x} dispatch parse failed: {:?}",
335                parsed.unwrap_err()
336            );
337            assert!(
338                !matches!(parsed.unwrap(), AnyPayload::Unknown { .. }),
339                "0x{pt:02x} was dispatched to Unknown"
340            );
341        }
342    }
343
344    /// DISPATCHED_TYPES has exactly 12 entries (one per TS 102 773 Table 1 type).
345    #[test]
346    fn dispatched_types_count_is_twelve() {
347        assert_eq!(AnyPayload::DISPATCHED_TYPES.len(), 12);
348    }
349
350    /// DISPATCHED_TYPES contains all 12 defined packet_type values.
351    #[test]
352    fn dispatched_types_contains_all_defined_packet_types() {
353        let expected = [
354            0x00u8, 0x01, 0x02, 0x10, 0x11, 0x12, 0x20, 0x21, 0x30, 0x31, 0x32, 0x33,
355        ];
356        for pt in expected {
357            assert!(
358                AnyPayload::DISPATCHED_TYPES.contains(&pt),
359                "0x{pt:02x} missing from DISPATCHED_TYPES"
360            );
361        }
362    }
363
364    // ── Unknown fallthrough ───────────────────────────────────────────────────
365
366    /// An undispatched packet_type returns None from dispatch (caller makes Unknown).
367    #[test]
368    fn undispatched_packet_type_returns_none() {
369        // 0x22..=0x2F are RFU, never defined.
370        assert!(AnyPayload::dispatch(0x22, &[]).is_none());
371        assert!(AnyPayload::dispatch(0xFF, &[]).is_none());
372    }
373
374    // ── From impls ────────────────────────────────────────────────────────────
375
376    #[test]
377    fn from_bbframe_payload_into_any_payload() {
378        use crate::payload::bbframe::BbframePayload;
379        let p = BbframePayload {
380            frame_idx: 1,
381            plp_id: 2,
382            intl_frame_start: false,
383            bbframe: &[],
384        };
385        let any = AnyPayload::from(p);
386        assert!(matches!(any, AnyPayload::Bbframe(_)));
387    }
388
389    #[test]
390    fn from_fef_null_payload_into_any_payload() {
391        use crate::payload::fef_null::{FefNullPayload, S1Field};
392        let p = FefNullPayload {
393            fef_idx: 0,
394            s1_field: S1Field::V0,
395            s2_field: 0,
396        };
397        let any = AnyPayload::from(p);
398        assert!(matches!(any, AnyPayload::FefNull(_)));
399    }
400
401    // ── serde ─────────────────────────────────────────────────────────────────
402
403    #[cfg(feature = "serde")]
404    #[test]
405    fn bbframe_serializes_as_camel_case_external_tag() {
406        use crate::payload::bbframe::BbframePayload;
407        let p = BbframePayload {
408            frame_idx: 0x42,
409            plp_id: 0x05,
410            intl_frame_start: true,
411            bbframe: &[],
412        };
413        let any = AnyPayload::Bbframe(p);
414        let json = serde_json::to_value(&any).unwrap();
415        assert!(
416            json.get("bbframe").is_some(),
417            "expected camelCase 'bbframe' key, got: {json}"
418        );
419        assert_eq!(json["bbframe"]["frame_idx"], 0x42);
420    }
421
422    #[cfg(feature = "serde")]
423    #[test]
424    fn unknown_serializes_with_packet_type_and_body() {
425        let any = AnyPayload::Unknown {
426            packet_type: 0x22,
427            body: &[0xDE, 0xAD],
428        };
429        let json = serde_json::to_value(&any).unwrap();
430        assert!(
431            json.get("unknown").is_some(),
432            "expected 'unknown' key, got: {json}"
433        );
434        assert_eq!(json["unknown"]["packet_type"], 0x22);
435    }
436}