dvb_si/tables/any.rs
1//! Unified table-section dispatch: [`AnyTableSection`].
2//!
3//! [`AnyTableSection`] is generated from a single declarative list
4//! (`declare_tables!`) — one line per crate-implemented section type.
5//! The list is the single source of truth: it produces the enum, the
6//! `From<T>` conversions, the table_id range dispatcher, and a drift test
7//! that pins each range literal to the type's
8//! [`crate::traits::TableDef::TABLE_ID_RANGES`].
9//!
10//! [`AnyTableSection::parse`] dispatches on the first byte (table_id) using range
11//! patterns. An unrecognised table_id yields
12//! `AnyTableSection::Unknown { table_id, raw }` — the full section bytes are
13//! retained.
14//!
15//! [`AnyTableSection::parse_as`] is a type-keyed thin alias to `T::parse` — it
16//! bypasses dispatch entirely and lets callers obtain, for example, a
17//! [`crate::tables::mpe::MpeDatagramSection`] for a `0x3E` section that the
18//! default dispatcher would route to `DsmccSection`.
19//!
20//! ```
21//! use dvb_common::Serialize;
22//! use dvb_si::tables::AnyTableSection;
23//! use dvb_si::tables::pat::{PatSection, PatEntry};
24//!
25//! // Serialize a small PAT, then dispatch the bytes back through AnyTableSection::parse.
26//! let pat = PatSection {
27//! transport_stream_id: 1, version_number: 0, current_next_indicator: true,
28//! section_number: 0, last_section_number: 0,
29//! entries: vec![PatEntry { program_number: 1, pid: 0x0100 }],
30//! };
31//! let mut section = vec![0u8; pat.serialized_len()];
32//! pat.serialize_into(&mut section).unwrap();
33//!
34//! match AnyTableSection::parse(§ion).unwrap() {
35//! AnyTableSection::PatSection(parsed) => assert_eq!(parsed.entries[0].pid, 0x0100),
36//! other => panic!("expected PatSection, got {other:?}"),
37//! }
38//! ```
39//!
40//! # Adding a section parser
41//!
42//! 1. Create the module with the wire layout, a `pub const TABLE_ID: u8` (or
43//! `TABLE_ID_FIRST`/`TABLE_ID_LAST` for a range), and the symmetric
44//! [`dvb_common::Parse`]/[`dvb_common::Serialize`] impls + round-trip tests
45//! (copy an existing module).
46//! 2. `impl TableDef` for the type (`TABLE_ID_RANGES` covering the full
47//! table_id range, `NAME` in SCREAMING_SNAKE without the `_section` or
48//! `_table` suffix).
49//! 3. Add one line to the `declare_tables!` invocation below — the enum
50//! variant, dispatcher arm, and drift test entry are generated from it.
51//! If the type should NOT be auto-dispatched (e.g. `MpeDatagramSection`,
52//! whose `0x3E` id is claimed by the `DsmccSection` range), add it to the
53//! `@no_dispatch` section instead.
54//! 4. The disjointness test in `declare_tables_tests` catches any overlapping
55//! range entries automatically — no manual test edits needed.
56
57use alloc::boxed::Box;
58
59/// Declares [`AnyTableSection`] + its dispatcher from one range list.
60///
61/// Each dispatch line is `Variant = [lo..=hi, …] => module::Type[<'a>]`.
62/// The optional trailing `@no_dispatch …` section adds variants that are NOT
63/// reachable from the generated dispatcher — the variant exists for callers
64/// that obtain the type via `AnyTableSection::parse_as` or direct `T::parse`.
65macro_rules! declare_tables {
66 (
67 $lt:lifetime;
68 $( $variant:ident = [ $( $lo:literal ..= $hi:literal ),+ ] => $($path:ident)::+ $(<$plt:lifetime>)? ),+ $(,)?
69 $( ; @no_dispatch $( $nd_variant:ident => $($nd_path:ident)::+ $(<$nd_plt:lifetime>)? ),+ $(,)? )?
70 ) => {
71 /// Every crate-implemented table-section parser, plus an `Unknown`
72 /// fallthrough.
73 ///
74 /// serde uses external tagging with camelCase variant keys — a parsed
75 /// PAT section serializes as `{"patSection": {…}}`.
76 /// Variant names map 1:1 to section parser types; see each module for
77 /// the wire layout.
78 ///
79 /// `0x3E` (`datagram_section`) is routed to `DsmccSection` by the
80 /// default dispatcher. The typed MPE view is reachable via
81 /// `AnyTableSection::parse_as::<MpeDatagramSection>` or
82 /// `MpeDatagramSection::parse` directly; the `MpeDatagram` variant
83 /// exists in this enum for API completeness but is never produced by
84 /// `AnyTableSection::parse`.
85 #[derive(Debug)]
86 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
87 #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
88 // Covariant in `$lt`: every variant holds only lifetime-parametrised
89 // table views or `&$lt [u8]` (`Unknown`), so the derived impl is sound.
90 #[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
91 #[non_exhaustive]
92 pub enum AnyTableSection<$lt> {
93 $(
94 #[allow(missing_docs)]
95 $variant($($path)::+ $(<$plt>)?),
96 )+
97 $($(
98 #[allow(missing_docs)]
99 $nd_variant($($nd_path)::+ $(<$nd_plt>)?),
100 )+)?
101 /// Runtime-registered custom table section (see [`TableRegistry`]).
102 ///
103 /// [`TableRegistry`]: crate::tables::registry::TableRegistry
104 Other {
105 /// The raw table_id byte.
106 table_id: u8,
107 /// The parsed, type-erased table-section value. Call `downcast_ref`
108 /// on it (see [`TableObject`](crate::tables::registry::TableObject))
109 /// to recover the concrete type.
110 #[cfg_attr(
111 feature = "serde",
112 serde(serialize_with = "crate::tables::registry::serialize_erased")
113 )]
114 value: Box<dyn crate::tables::registry::TableObject>,
115 },
116 /// table_id with no typed implementation; `raw` is the full
117 /// section bytes including the table_id header.
118 Unknown {
119 /// The raw table_id byte.
120 table_id: u8,
121 /// The raw section bytes (full, header included).
122 raw: &$lt [u8],
123 },
124 }
125
126 $(
127 impl<$lt> From<$($path)::+ $(<$plt>)?> for AnyTableSection<$lt> {
128 fn from(t: $($path)::+ $(<$plt>)?) -> Self {
129 Self::$variant(t)
130 }
131 }
132 )+
133 $($(
134 impl<$lt> From<$($nd_path)::+ $(<$nd_plt>)?> for AnyTableSection<$lt> {
135 fn from(t: $($nd_path)::+ $(<$nd_plt>)?) -> Self {
136 Self::$nd_variant(t)
137 }
138 }
139 )+)?
140
141 impl<$lt> AnyTableSection<$lt> {
142 /// All table_id ranges covered by the auto-dispatcher (excludes
143 /// `@no_dispatch` variants). Each entry is `(lo, hi)` inclusive.
144 pub const DISPATCHED_RANGES: &'static [(u8, u8)] =
145 &[$( $( ($lo, $hi) ),+ ),+];
146
147 /// Diagnostic name of the contained table — the type's
148 /// [`TableDef::NAME`](crate::traits::TableDef::NAME)
149 /// (`"EVENT_INFORMATION"`, `"PROGRAM_ASSOCIATION"`, …);
150 /// `"UNKNOWN"` for [`AnyTableSection::Unknown`].
151 #[must_use]
152 pub fn name(&self) -> &'static str {
153 match self {
154 $(
155 Self::$variant(_) =>
156 <$($path)::+ as crate::traits::TableDef>::NAME,
157 )+
158 $($(
159 Self::$nd_variant(_) =>
160 <$($nd_path)::+ as crate::traits::TableDef>::NAME,
161 )+)?
162 Self::Other { .. } => "CUSTOM",
163 Self::Unknown { .. } => "UNKNOWN",
164 }
165 }
166
167 /// Dispatch one complete section by its table_id (byte 0).
168 ///
169 /// Returns `Err(BufferTooShort)` when `bytes` is empty.
170 /// Unknown table_ids produce `Ok(AnyTableSection::Unknown { … })`.
171 ///
172 /// # Errors
173 /// - [`crate::Error::BufferTooShort`] — `bytes` is empty.
174 /// - Any parse error from the dispatched type.
175 pub fn parse(bytes: &$lt [u8]) -> crate::Result<Self> {
176 let table_id = *bytes.first().ok_or(crate::Error::BufferTooShort {
177 need: 1,
178 have: 0,
179 what: "section table_id",
180 })?;
181 match table_id {
182 $(
183 $( $lo..=$hi )|+ => {
184 <$($path)::+ as dvb_common::Parse>::parse(bytes).map(Self::$variant)
185 }
186 )+
187 _ => Ok(Self::Unknown { table_id, raw: bytes }),
188 }
189 }
190
191 /// Type-keyed parse: bypass the dispatcher and parse `bytes`
192 /// directly as `T`. Useful for types excluded from the default
193 /// dispatch, e.g.:
194 ///
195 /// ```rust
196 /// use dvb_si::tables::AnyTableSection;
197 /// use dvb_si::tables::mpe::MpeDatagramSection;
198 ///
199 /// // A deliberately-too-short slice: parse_as propagates the
200 /// // BufferTooShort error from MpeDatagramSection::parse.
201 /// let err = AnyTableSection::parse_as::<MpeDatagramSection>(&[0x3E, 0x00]);
202 /// assert!(err.is_err());
203 /// ```
204 ///
205 /// # Errors
206 /// Propagates `T::parse` errors.
207 pub fn parse_as<T>(bytes: &$lt [u8]) -> crate::Result<T>
208 where
209 T: crate::traits::TableDef<$lt>,
210 {
211 <T as dvb_common::Parse>::parse(bytes)
212 }
213 }
214
215 #[cfg(test)]
216 mod macro_drift {
217 #[test]
218 fn ranges_match_tabledef() {
219 use crate::traits::TableDef;
220 $(
221 assert_eq!(
222 &[ $( ($lo, $hi) ),+ ][..],
223 <$($path)::+ as TableDef>::TABLE_ID_RANGES,
224 concat!("TABLE_ID_RANGES drift for ", stringify!($variant)),
225 );
226 assert!(
227 !<$($path)::+ as TableDef>::NAME.is_empty(),
228 concat!("empty NAME for ", stringify!($variant)),
229 );
230 )+
231 $($(
232 assert!(
233 !<$($nd_path)::+ as TableDef>::NAME.is_empty(),
234 concat!("empty NAME for no-dispatch ", stringify!($nd_variant)),
235 );
236 )+)?
237 }
238
239 #[test]
240 fn dispatched_ranges_are_disjoint() {
241 // Collect all (lo, hi) pairs, sort by lo, then check no
242 // two adjacent entries overlap.
243 let mut ranges: Vec<(u8, u8)> = vec![
244 $( $( ($lo, $hi), )+ )+
245 ];
246 ranges.sort_by_key(|r| r.0);
247 for w in ranges.windows(2) {
248 let (_, prev_hi) = w[0];
249 let (next_lo, _) = w[1];
250 assert!(
251 next_lo > prev_hi,
252 "overlapping dispatch ranges: {w:?}",
253 );
254 }
255 }
256 }
257 };
258}
259
260declare_tables! {'a;
261 // MPEG-2 systems tables (ISO/IEC 13818-1).
262 PatSection = [0x00..=0x00] => crate::tables::pat::PatSection,
263 CatSection = [0x01..=0x01] => crate::tables::cat::CatSection<'a>,
264 PmtSection = [0x02..=0x02] => crate::tables::pmt::PmtSection<'a>,
265 TsdtSection = [0x03..=0x03] => crate::tables::tsdt::TsdtSection<'a>,
266 // DSM-CC sections (ISO/IEC 13818-6) — 0x3E is included; the MPE typed
267 // view (`MpeDatagramSection`) is reachable via `AnyTableSection::parse_as` or
268 // `MpeDatagramSection::parse`.
269 DsmccSection = [0x3A..=0x3F] => crate::tables::dsmcc::DsmccSection<'a>,
270 // DVB tables (ETSI EN 300 468).
271 NitSection = [0x40..=0x41] => crate::tables::nit::NitSection<'a>,
272 SdtSection = [0x42..=0x42, 0x46..=0x46] => crate::tables::sdt::SdtSection<'a>,
273 BatSection = [0x4A..=0x4A] => crate::tables::bat::BatSection<'a>,
274 UntSection = [0x4B..=0x4B] => crate::tables::unt::UntSection<'a>,
275 IntSection = [0x4C..=0x4C] => crate::tables::int::IntSection<'a>,
276 SatSection = [0x4D..=0x4D] => crate::tables::sat::SatSection,
277 EitSection = [0x4E..=0x6F] => crate::tables::eit::EitSection<'a>,
278 TdtSection = [0x70..=0x70] => crate::tables::tdt::TdtSection,
279 RstSection = [0x71..=0x71] => crate::tables::rst::RstSection,
280 StSection = [0x72..=0x72] => crate::tables::st::StSection,
281 TotSection = [0x73..=0x73] => crate::tables::tot::TotSection<'a>,
282 AitSection = [0x74..=0x74] => crate::tables::ait::AitSection<'a>,
283 ContainerSection = [0x75..=0x75] => crate::tables::container::ContainerSection<'a>,
284 RctSection = [0x76..=0x76] => crate::tables::rct::RctSection<'a>,
285 CitSection = [0x77..=0x77] => crate::tables::cit::CitSection<'a>,
286 MpeFecSection = [0x78..=0x78] => crate::tables::mpe_fec::MpeFecSection<'a>,
287 RntSection = [0x79..=0x79] => crate::tables::rnt::RntSection<'a>,
288 MpeIfecSection = [0x7A..=0x7A] => crate::tables::mpe_ifec::MpeIfecSection<'a>,
289 ProtectionMessage = [0x7B..=0x7B] => crate::tables::protection_message::ProtectionMessageSection<'a>,
290 DownloadableFontInfo = [0x7C..=0x7C] => crate::tables::downloadable_font_info::DownloadableFontInfoSection<'a>,
291 DitSection = [0x7E..=0x7E] => crate::tables::dit::DitSection,
292 SitSection = [0x7F..=0x7F] => crate::tables::sit::SitSection<'a>;
293 // MPE datagram_section (ETSI EN 301 192 §7.1): table_id 0x3E overlaps
294 // the DsmccSection range above, so it is NOT auto-dispatched. Use
295 // `AnyTableSection::parse_as::<MpeDatagramSection>(bytes)` for the typed view.
296 @no_dispatch
297 MpeDatagram => crate::tables::mpe::MpeDatagramSection<'a>,
298}
299
300impl<'a> AnyTableSection<'a> {
301 /// Dispatch one complete section with custom-registry support.
302 ///
303 /// Precedence: (1) if the registry has a custom parser for the section's
304 /// table_id, use it → [`AnyTableSection::Other`]; (2) else delegate to
305 /// [`AnyTableSection::parse`] (built-in dispatch, which itself falls to
306 /// [`AnyTableSection::Unknown`]).
307 ///
308 /// # Errors
309 /// - [`crate::Error::BufferTooShort`] — `bytes` is empty.
310 /// - Any parse error from the dispatched type.
311 pub fn parse_with(
312 registry: &crate::tables::registry::TableRegistry,
313 bytes: &'a [u8],
314 ) -> crate::Result<Self> {
315 let table_id = *bytes.first().ok_or(crate::Error::BufferTooShort {
316 need: 1,
317 have: 0,
318 what: "section table_id",
319 })?;
320 if let Some(parse_fn) = registry.lookup(table_id) {
321 let value = parse_fn(bytes)?;
322 return Ok(Self::Other { table_id, value });
323 }
324 Self::parse(bytes)
325 }
326}