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//! [`dvb_common::Parse`] / [`dvb_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
31/// Declares [`AnyPayload`] + its dispatcher from one packet-type list.
32///
33/// Each line is `Variant = 0xTYPE => module::Type[<'a>]`.
34macro_rules! declare_payloads {
35 (
36 $lt:lifetime;
37 $( $variant:ident = $ptype:literal => $($path:ident)::+ $(<$plt:lifetime>)? ),+ $(,)?
38 ) => {
39 /// Every crate-implemented T2-MI payload, plus an `Unknown` fallthrough.
40 ///
41 /// serde uses external tagging with camelCase variant keys.
42 /// Variant names map 1:1 to the payload modules; see each module
43 /// for the wire layout.
44 ///
45 /// # Dispatch contract
46 ///
47 /// Use [`AnyPayload::dispatch`] with the payload bytes (post-header,
48 /// pre-CRC). See the module-level documentation for details.
49 #[derive(Debug)]
50 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
51 #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
52 #[non_exhaustive]
53 pub enum AnyPayload<$lt> {
54 $(
55 #[allow(missing_docs)]
56 $variant($($path)::+ $(<$plt>)?),
57 )+
58 /// Packet type with no typed implementation; `body` contains the
59 /// raw payload bytes (post-header, pre-CRC).
60 Unknown {
61 /// The raw `packet_type` byte.
62 packet_type: u8,
63 /// The raw payload bytes.
64 body: &$lt [u8],
65 },
66 }
67
68 $(
69 impl<$lt> From<$($path)::+ $(<$plt>)?> for AnyPayload<$lt> {
70 fn from(p: $($path)::+ $(<$plt>)?) -> Self {
71 Self::$variant(p)
72 }
73 }
74 )+
75
76 impl<$lt> AnyPayload<$lt> {
77 /// Every `packet_type` the generated dispatcher routes (excludes
78 /// [`AnyPayload::Unknown`]).
79 pub const DISPATCHED_TYPES: &'static [u8] = &[$($ptype),+];
80
81 /// Parse one payload by its `packet_type`.
82 ///
83 /// `payload_bytes` must be the **payload-only slice** (bytes after
84 /// the 6-byte T2-MI header, before the 4-byte CRC trailer).
85 ///
86 /// Returns `None` when `packet_type` has no typed implementation
87 /// (the caller turns that into [`AnyPayload::Unknown`]).
88 /// Returns `Some(Err)` on a typed parse failure for a recognised type.
89 ///
90 /// See the [module-level documentation][self] for the dispatch
91 /// contract (payload-only bytes, header and CRC excluded).
92 pub fn dispatch(
93 packet_type: u8,
94 payload_bytes: &$lt [u8],
95 ) -> Option<crate::Result<Self>> {
96 use dvb_common::Parse;
97 match packet_type {
98 $(
99 $ptype => Some(
100 <$($path)::+>::parse(payload_bytes).map(Self::$variant),
101 ),
102 )+
103 _ => None,
104 }
105 }
106 }
107
108 #[cfg(test)]
109 mod macro_drift {
110 #[test]
111 fn packet_type_literals_match_payload_def() {
112 use crate::traits::PayloadDef;
113 $(
114 assert_eq!(
115 $ptype,
116 <$($path)::+ as PayloadDef>::PACKET_TYPE,
117 concat!("PACKET_TYPE literal drift for ", stringify!($variant)),
118 );
119 assert!(
120 !<$($path)::+ as PayloadDef>::NAME.is_empty(),
121 concat!("empty NAME for ", stringify!($variant)),
122 );
123 )+
124 }
125 }
126 };
127}
128
129declare_payloads! {'a;
130 // TS 102 773 Table 1 — all 12 defined packet types in numerical order.
131 Bbframe = 0x00 => crate::payload::bbframe::BbframePayload<'a>,
132 AuxIq = 0x01 => crate::payload::aux_iq::AuxIqPayload<'a>,
133 ArbitraryCells = 0x02 => crate::payload::arbitrary_cells::ArbitraryCellsPayload<'a>,
134 L1Current = 0x10 => crate::payload::l1_current::L1CurrentPayload<'a>,
135 L1Future = 0x11 => crate::payload::l1_future::L1FuturePayload<'a>,
136 P2Bias = 0x12 => crate::payload::p2_bias::P2BiasPayload,
137 Timestamp = 0x20 => crate::payload::timestamp::T2TimestampPayload,
138 IndividualAddressing = 0x21 => crate::payload::individual_addressing::IndividualAddressingPayload<'a>,
139 FefNull = 0x30 => crate::payload::fef_null::FefNullPayload,
140 FefIq = 0x31 => crate::payload::fef_iq::FefIqPayload<'a>,
141 FefComposite = 0x32 => crate::payload::fef_composite::FefCompositePayload,
142 FefSubpart = 0x33 => crate::payload::fef_subpart::FefSubPartPayload<'a>,
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 // ── Completeness ─────────────────────────────────────────────────────────
150
151 /// Every entry in DISPATCHED_TYPES must dispatch to a non-Unknown variant.
152 #[test]
153 fn every_dispatched_type_routes_non_unknown() {
154 // Minimal valid payload bytes for each packet type (all RFU = 0 — the
155 // parsers reject non-zero reserved bits). See each payload module's
156 // own tests for full boundary coverage.
157
158 // 0x00 BBFrame: frame_idx(1) + plp_id(1) + intl_frame_start+rfu(1) = 3 bytes.
159 let bbframe_bytes: &[u8] = &[0x00, 0x00, 0x00];
160 // 0x01 AuxIq: frame_idx(1) + aux_id(4bits, must be 1..=15)+rfu(4bits)(1) + rfu(1) = 3 bytes.
161 // aux_id=1: byte1 = (1<<4) = 0x10.
162 let aux_iq_bytes: &[u8] = &[0x00, 0x10, 0x00];
163 // 0x02 ArbitraryCells: 8-byte header (rfu bytes 3,4 = 0, byte5 top 2 = 0).
164 let arb_cells_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
165 // 0x10 L1Current: frame_idx(1) + freq_source(2bits)+rfu(6bits)(1) = 2 bytes.
166 let l1_current_bytes: &[u8] = &[0x00, 0x00];
167 // 0x11 L1Future: frame_idx(1) + rfu(1) = 2 bytes.
168 let l1_future_bytes: &[u8] = &[0x00, 0x00];
169 // 0x12 P2Bias: 5 bytes, all rfu = 0.
170 let p2_bias_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00];
171 // 0x20 Timestamp: 11 bytes, rfu top 4 bits of byte0 = 0, bw=0 (1.7 MHz).
172 let timestamp_bytes: &[u8] = &[
173 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
174 ];
175 // 0x21 IndividualAddressing: rfu(1) + length(1, value 0) = 2 bytes.
176 let indiv_addr_bytes: &[u8] = &[0x00, 0x00];
177 // 0x30 FefNull: fef_idx(1) + rfu(1, must be 0) + s1_field+s2_field(1) = 3 bytes.
178 let fef_null_bytes: &[u8] = &[0x00, 0x00, 0x00];
179 // 0x31 FefIq: fef_idx(1) + rfu(1, must be 0) + s1+s2(1) = 3 bytes.
180 let fef_iq_bytes: &[u8] = &[0x00, 0x00, 0x00];
181 // 0x32 FefComposite: 8 bytes. byte1 [7]=rfu1=0, bytes2-5=rfu2=0.
182 let fef_composite_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
183 // 0x33 FefSubpart: 15 bytes.
184 // bytes 3-6 = rfu1 = 0, byte 11 = rfu2 = 0, byte 12 top 2 = 0.
185 // subpart_variety bytes 9-10 = 0x0000 = Null.
186 let fef_subpart_bytes: &[u8] = &[
187 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
188 0x00,
189 ];
190
191 let fixtures: &[(u8, &[u8])] = &[
192 (0x00, bbframe_bytes),
193 (0x01, aux_iq_bytes),
194 (0x02, arb_cells_bytes),
195 (0x10, l1_current_bytes),
196 (0x11, l1_future_bytes),
197 (0x12, p2_bias_bytes),
198 (0x20, timestamp_bytes),
199 (0x21, indiv_addr_bytes),
200 (0x30, fef_null_bytes),
201 (0x31, fef_iq_bytes),
202 (0x32, fef_composite_bytes),
203 (0x33, fef_subpart_bytes),
204 ];
205
206 for &(pt, bytes) in fixtures {
207 let result = AnyPayload::dispatch(pt, bytes);
208 assert!(result.is_some(), "0x{pt:02x} returned None from dispatch");
209 let parsed = result.unwrap();
210 assert!(
211 parsed.is_ok(),
212 "0x{pt:02x} dispatch parse failed: {:?}",
213 parsed.unwrap_err()
214 );
215 assert!(
216 !matches!(parsed.unwrap(), AnyPayload::Unknown { .. }),
217 "0x{pt:02x} was dispatched to Unknown"
218 );
219 }
220 }
221
222 /// DISPATCHED_TYPES has exactly 12 entries (one per TS 102 773 Table 1 type).
223 #[test]
224 fn dispatched_types_count_is_twelve() {
225 assert_eq!(AnyPayload::DISPATCHED_TYPES.len(), 12);
226 }
227
228 /// DISPATCHED_TYPES contains all 12 defined packet_type values.
229 #[test]
230 fn dispatched_types_contains_all_defined_packet_types() {
231 let expected = [
232 0x00u8, 0x01, 0x02, 0x10, 0x11, 0x12, 0x20, 0x21, 0x30, 0x31, 0x32, 0x33,
233 ];
234 for pt in expected {
235 assert!(
236 AnyPayload::DISPATCHED_TYPES.contains(&pt),
237 "0x{pt:02x} missing from DISPATCHED_TYPES"
238 );
239 }
240 }
241
242 // ── Unknown fallthrough ───────────────────────────────────────────────────
243
244 /// An undispatched packet_type returns None from dispatch (caller makes Unknown).
245 #[test]
246 fn undispatched_packet_type_returns_none() {
247 // 0x22..=0x2F are RFU, never defined.
248 assert!(AnyPayload::dispatch(0x22, &[]).is_none());
249 assert!(AnyPayload::dispatch(0xFF, &[]).is_none());
250 }
251
252 // ── From impls ────────────────────────────────────────────────────────────
253
254 #[test]
255 fn from_bbframe_payload_into_any_payload() {
256 use crate::payload::bbframe::BbframePayload;
257 let p = BbframePayload {
258 frame_idx: 1,
259 plp_id: 2,
260 intl_frame_start: false,
261 bbframe: &[],
262 };
263 let any = AnyPayload::from(p);
264 assert!(matches!(any, AnyPayload::Bbframe(_)));
265 }
266
267 #[test]
268 fn from_fef_null_payload_into_any_payload() {
269 use crate::payload::fef_null::{FefNullPayload, S1Field};
270 let p = FefNullPayload {
271 fef_idx: 0,
272 s1_field: S1Field::V0,
273 s2_field: 0,
274 };
275 let any = AnyPayload::from(p);
276 assert!(matches!(any, AnyPayload::FefNull(_)));
277 }
278
279 // ── serde ─────────────────────────────────────────────────────────────────
280
281 #[cfg(feature = "serde")]
282 #[test]
283 fn bbframe_serializes_as_camel_case_external_tag() {
284 use crate::payload::bbframe::BbframePayload;
285 let p = BbframePayload {
286 frame_idx: 0x42,
287 plp_id: 0x05,
288 intl_frame_start: true,
289 bbframe: &[],
290 };
291 let any = AnyPayload::Bbframe(p);
292 let json = serde_json::to_value(&any).unwrap();
293 assert!(
294 json.get("bbframe").is_some(),
295 "expected camelCase 'bbframe' key, got: {json}"
296 );
297 assert_eq!(json["bbframe"]["frame_idx"], 0x42);
298 }
299
300 #[cfg(feature = "serde")]
301 #[test]
302 fn unknown_serializes_with_packet_type_and_body() {
303 let any = AnyPayload::Unknown {
304 packet_type: 0x22,
305 body: &[0xDE, 0xAD],
306 };
307 let json = serde_json::to_value(&any).unwrap();
308 assert!(
309 json.get("unknown").is_some(),
310 "expected 'unknown' key, got: {json}"
311 );
312 assert_eq!(json["unknown"]["packet_type"], 0x22);
313 }
314}