dvb_si/tables/registry.rs
1//! Runtime table registry — open registration of client private table_ids.
2//!
3//! [`TableRegistry`] allows clients to register their own private table types.
4//! Registered custom parsers win over built-in dispatch; an unregistered
5//! table_id falls through to [`crate::tables::AnyTableSection::Unknown`].
6//!
7//! # Owned types only
8//!
9//! Registered types must be `'static` (i.e. owned — no borrowed slices).
10//! This is required because the parsed value is heap-allocated as a
11//! `Box<dyn TableObject>` whose concrete type is erased; `dyn Any`
12//! downcast demands `'static`. If your wire layout contains borrowed bytes,
13//! copy them into a `Vec<u8>` in the struct.
14//!
15//! # Example
16//!
17//! ```rust,no_run
18//! use dvb_si::tables::{TableRegistry, AnyTableSection};
19//! use dvb_si::traits::TableDef;
20//! use dvb_common::Parse;
21//!
22//! // A registered type must be `serde::Serialize` only when the `serde`
23//! // feature is on (that is what `TableObject` requires there).
24//! #[derive(Debug)]
25//! #[cfg_attr(feature = "serde", derive(serde::Serialize))]
26//! struct MyPrivate { x: u8 }
27//!
28//! impl<'a> Parse<'a> for MyPrivate {
29//! type Error = dvb_si::Error;
30//! fn parse(bytes: &'a [u8]) -> dvb_si::Result<Self> {
31//! if bytes.len() < 2 {
32//! return Err(dvb_si::Error::BufferTooShort {
33//! need: 2, have: bytes.len(), what: "MyPrivate",
34//! });
35//! }
36//! Ok(Self { x: bytes[1] })
37//! }
38//! }
39//!
40//! impl<'a> TableDef<'a> for MyPrivate {
41//! const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(0x90, 0x90)];
42//! const NAME: &'static str = "MY_PRIVATE";
43//! }
44//!
45//! let mut reg = TableRegistry::new();
46//! reg.register::<MyPrivate>();
47//!
48//! let bytes = [0x90, 0x00, 0x42u8];
49//! match AnyTableSection::parse_with(®, &bytes).unwrap() {
50//! AnyTableSection::Other { table_id, ref value } => {
51//! assert_eq!(table_id, 0x90);
52//! assert_eq!(value.downcast_ref::<MyPrivate>().unwrap().x, 0x42);
53//! }
54//! other => panic!("expected Other, got {other:?}"),
55//! }
56//! ```
57
58use std::any::Any;
59use std::collections::HashMap;
60
61// ---------------------------------------------------------------------------
62// TableObject trait
63// ---------------------------------------------------------------------------
64
65/// Object-safe face of a runtime-registered table-section value.
66///
67/// Registered types must be owned (`'static`) because the `dyn Any` downcast
68/// path requires it. See the [module docs][self] for details.
69///
70/// Implemented automatically via the blanket impl for any `T` satisfying the
71/// supertraits; you do not need to write this by hand.
72#[cfg(not(feature = "serde"))]
73pub trait TableObject: std::fmt::Debug + Any + Send + Sync {
74 /// Borrow as `&dyn Any` so the caller can downcast to the concrete type.
75 fn as_any(&self) -> &dyn Any;
76}
77
78/// Object-safe face of a runtime-registered table-section value.
79///
80/// Registered types must be owned (`'static`) because the `dyn Any` downcast
81/// path requires it. See the [module docs][self] for details.
82///
83/// Implemented automatically via the blanket impl for any `T` satisfying the
84/// supertraits; you do not need to write this by hand.
85#[cfg(feature = "serde")]
86pub trait TableObject: std::fmt::Debug + Any + Send + Sync + erased_serde::Serialize {
87 /// Borrow as `&dyn Any` so the caller can downcast to the concrete type.
88 fn as_any(&self) -> &dyn Any;
89}
90
91// Blanket impl — no-serde arm.
92#[cfg(not(feature = "serde"))]
93impl<T> TableObject for T
94where
95 T: std::fmt::Debug + Any + Send + Sync,
96{
97 fn as_any(&self) -> &dyn Any {
98 self
99 }
100}
101
102// Blanket impl — serde arm.
103#[cfg(feature = "serde")]
104impl<T> TableObject for T
105where
106 T: std::fmt::Debug + Any + Send + Sync + serde::Serialize,
107{
108 fn as_any(&self) -> &dyn Any {
109 self
110 }
111}
112
113// Downcast helpers ON THE TRAIT OBJECT (not the blanket).
114//
115// The blanket `impl<T> TableObject for T` also covers `Box<dyn TableObject>`
116// itself whenever the box satisfies the bounds — it does under
117// `--no-default-features`, where the bound is just `Debug + Any + Send + Sync`.
118// So `the_box.as_any()` resolves to the *box's* impl and reports the box's
119// `TypeId`, not the inner value's — a silent downcast failure. (Under `serde`
120// the extra `serde::Serialize` bound excludes the box, which is why the footgun
121// only bites without default features.) Calling through `dyn TableObject`
122// (which `Box` derefs to) always hits the inner value, so always downcast via
123// these methods rather than `the_box.as_any()`.
124impl dyn TableObject {
125 /// Downcast a registered table section to its concrete type `T`.
126 ///
127 /// Works for `Box<dyn TableObject>` (it derefs to the trait object) under
128 /// every feature configuration.
129 #[must_use]
130 pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
131 self.as_any().downcast_ref::<T>()
132 }
133
134 /// `true` if the registered table section's concrete type is `T`.
135 #[must_use]
136 pub fn is<T: Any>(&self) -> bool {
137 self.as_any().is::<T>()
138 }
139}
140
141// ---------------------------------------------------------------------------
142// Erased serialisation helper (serde-gated)
143// ---------------------------------------------------------------------------
144
145/// `serialize_with` helper used on [`AnyTableSection::Other`]'s `value` field.
146///
147/// Delegates to [`erased_serde::serialize`] so the concrete type's
148/// `serde::Serialize` impl is invoked through the trait object.
149///
150/// The `&Box<T>` is required by serde's `serialize_with` codegen — the field
151/// type is `Box<dyn TableObject>` so serde passes `&Box<dyn TableObject>`.
152#[cfg(feature = "serde")]
153#[allow(clippy::borrowed_box)]
154pub(crate) fn serialize_erased<S: serde::Serializer>(
155 v: &Box<dyn TableObject>,
156 s: S,
157) -> Result<S::Ok, S::Error> {
158 erased_serde::serialize(&**v, s)
159}
160
161// ---------------------------------------------------------------------------
162// Internal parse closure type
163// ---------------------------------------------------------------------------
164
165/// A heap-allocated parse closure that takes a full section (header + body)
166/// and returns an owned, type-erased table-section value.
167pub(crate) type CustomParse =
168 Box<dyn for<'a> Fn(&'a [u8]) -> crate::Result<Box<dyn TableObject>> + Send + Sync>;
169
170// ---------------------------------------------------------------------------
171// TableRegistry
172// ---------------------------------------------------------------------------
173
174/// Runtime-configurable table registry.
175///
176/// By default the registry has no custom parsers. Use
177/// [`register`][Self::register] to add one.
178///
179/// # Precedence (per table_id)
180///
181/// 1. Custom-registered parser (table_id in the [`custom`][Self::register] map) →
182/// [`crate::tables::AnyTableSection::Other`]
183/// 2. Built-in dispatch ([`crate::tables::AnyTableSection::parse`]) → typed variant
184/// 3. Unknown → [`crate::tables::AnyTableSection::Unknown`]
185#[derive(Default)]
186pub struct TableRegistry {
187 custom: HashMap<u8, CustomParse>,
188}
189
190impl TableRegistry {
191 /// Create an empty registry (built-in dispatch only).
192 #[must_use]
193 pub fn new() -> Self {
194 Self::default()
195 }
196
197 /// Look up a custom parser for the given `table_id`.
198 #[must_use]
199 pub(crate) fn lookup(&self, table_id: u8) -> Option<&CustomParse> {
200 self.custom.get(&table_id)
201 }
202
203 /// Register an owned custom table-section type for every table_id in its
204 /// [`TableDef::TABLE_ID_RANGES`][crate::traits::TableDef::TABLE_ID_RANGES].
205 ///
206 /// # Owned types only
207 ///
208 /// `T` must be `'static` — no borrowed slices. The registered value is
209 /// type-erased as `Box<dyn TableObject>`; `dyn Any` downcast requires
210 /// the concrete type to be `'static`.
211 ///
212 /// # Multi-range registration
213 ///
214 /// A type may cover multiple table_id ranges (e.g.
215 /// `&[(0x90, 0x90), (0x92, 0x93)]`). Every table_id in every range is
216 /// registered under the same parse closure.
217 ///
218 /// Registering a type whose table_id is already used by a built-in **overrides**
219 /// the built-in for that table_id.
220 ///
221 /// Re-registering the same table_id replaces the prior custom parser (last wins).
222 /// A failing custom parse surfaces the client's `Parse::Error` unwrapped —
223 /// embed identifying context (type/table_id) in your error's `what`/`reason` fields.
224 pub fn register<T>(&mut self) -> &mut Self
225 where
226 T: for<'a> crate::traits::TableDef<'a> + TableObject + 'static,
227 {
228 let ranges = <T as crate::traits::TableDef<'static>>::TABLE_ID_RANGES;
229 for &(lo, hi) in ranges {
230 for id in lo..=hi {
231 self.custom.insert(
232 id,
233 Box::new(|b| {
234 Ok(Box::new(<T as dvb_common::Parse>::parse(b)?) as Box<dyn TableObject>)
235 }),
236 );
237 }
238 }
239 self
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::tables::AnyTableSection;
247 use crate::traits::TableDef;
248 use dvb_common::Parse;
249
250 // -- A trivial custom table for a private table_id -------------------------
251 const CUSTOM_TABLE_ID: u8 = 0x90;
252
253 #[derive(Debug, PartialEq)]
254 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
255 struct CustomTable {
256 table_id: u8,
257 payload: Vec<u8>,
258 }
259
260 impl<'a> Parse<'a> for CustomTable {
261 type Error = crate::Error;
262 fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
263 if bytes.is_empty() {
264 return Err(crate::Error::BufferTooShort {
265 need: 1,
266 have: 0,
267 what: "CustomTable",
268 });
269 }
270 Ok(Self {
271 table_id: bytes[0],
272 payload: bytes[1..].to_vec(),
273 })
274 }
275 }
276
277 impl<'a> TableDef<'a> for CustomTable {
278 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(CUSTOM_TABLE_ID, CUSTOM_TABLE_ID)];
279 const NAME: &'static str = "CUSTOM_TABLE";
280 }
281
282 // -- A multi-range custom table --------------------------------------------
283 const MULTI_LO: u8 = 0x90;
284 const MULTI_MID: u8 = 0x92;
285 const MULTI_HI: u8 = 0x93;
286
287 #[derive(Debug, PartialEq)]
288 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
289 struct MultiRangeTable {
290 table_id: u8,
291 }
292
293 impl<'a> Parse<'a> for MultiRangeTable {
294 type Error = crate::Error;
295 fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
296 if bytes.is_empty() {
297 return Err(crate::Error::BufferTooShort {
298 need: 1,
299 have: 0,
300 what: "MultiRangeTable",
301 });
302 }
303 Ok(Self { table_id: bytes[0] })
304 }
305 }
306
307 impl<'a> TableDef<'a> for MultiRangeTable {
308 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(MULTI_LO, MULTI_LO), (MULTI_MID, MULTI_HI)];
309 const NAME: &'static str = "MULTI_RANGE_TABLE";
310 }
311
312 // -- A custom table that overrides a built-in (PAT = 0x00) -----------------
313 #[derive(Debug, PartialEq)]
314 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
315 struct OverridePat {
316 table_id: u8,
317 }
318
319 impl<'a> Parse<'a> for OverridePat {
320 type Error = crate::Error;
321 fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
322 if bytes.is_empty() {
323 return Err(crate::Error::BufferTooShort {
324 need: 1,
325 have: 0,
326 what: "OverridePat",
327 });
328 }
329 Ok(Self { table_id: bytes[0] })
330 }
331 }
332
333 impl<'a> TableDef<'a> for OverridePat {
334 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(0x00, 0x00)];
335 const NAME: &'static str = "OVERRIDE_PAT";
336 }
337
338 #[test]
339 fn custom_table_dispatches_to_other() {
340 let mut reg = TableRegistry::new();
341 reg.register::<CustomTable>();
342
343 let bytes = [CUSTOM_TABLE_ID, 0xAA, 0xBB];
344 let result = AnyTableSection::parse_with(®, &bytes).unwrap();
345 match result {
346 AnyTableSection::Other {
347 table_id,
348 ref value,
349 } => {
350 assert_eq!(table_id, CUSTOM_TABLE_ID);
351 let ct = value.downcast_ref::<CustomTable>().unwrap();
352 assert_eq!(ct.table_id, CUSTOM_TABLE_ID);
353 assert_eq!(ct.payload, &[0xAA, 0xBB]);
354 }
355 other => panic!("expected Other, got {other:?}"),
356 }
357 }
358
359 #[test]
360 fn multi_range_registers_all_ids() {
361 let mut reg = TableRegistry::new();
362 reg.register::<MultiRangeTable>();
363
364 assert!(reg.lookup(MULTI_LO).is_some());
365 assert!(reg.lookup(MULTI_MID).is_some());
366 assert!(reg.lookup(MULTI_MID + 1).is_some());
367 assert!(reg.lookup(MULTI_HI).is_some());
368 // 0x91 is NOT in any range
369 assert!(reg.lookup(MULTI_LO + 1).is_none());
370
371 // Each registered id dispatches to Other
372 for id in [MULTI_LO, MULTI_MID, MULTI_MID + 1, MULTI_HI] {
373 let bytes = [id, 0x00];
374 let result = AnyTableSection::parse_with(®, &bytes).unwrap();
375 match result {
376 AnyTableSection::Other { table_id, .. } => assert_eq!(table_id, id),
377 other => panic!("id {id:#04x}: expected Other, got {other:?}"),
378 }
379 }
380
381 // 0x91 falls to Unknown
382 let bytes = [MULTI_LO + 1, 0x00];
383 let result = AnyTableSection::parse_with(®, &bytes).unwrap();
384 assert!(
385 matches!(result, AnyTableSection::Unknown { .. }),
386 "expected Unknown for unregistered id 0x91, got {result:?}",
387 );
388 }
389
390 #[test]
391 fn fallback_to_builtin_for_unregistered() {
392 let reg = TableRegistry::new();
393 // Build a minimal PAT (table_id 0x00)
394 use crate::tables::pat::{PatEntry, PatSection};
395 use dvb_common::Serialize;
396 let pat = PatSection {
397 transport_stream_id: 1,
398 version_number: 0,
399 current_next_indicator: true,
400 section_number: 0,
401 last_section_number: 0,
402 entries: vec![PatEntry {
403 program_number: 1,
404 pid: 0x0100,
405 }],
406 };
407 let mut buf = vec![0u8; pat.serialized_len()];
408 pat.serialize_into(&mut buf).unwrap();
409
410 let result = AnyTableSection::parse_with(®, &buf).unwrap();
411 match result {
412 AnyTableSection::PatSection(p) => assert_eq!(p.entries.len(), 1),
413 other => panic!("expected PatSection, got {other:?}"),
414 }
415 }
416
417 #[test]
418 fn override_builtin_yields_other() {
419 let mut reg = TableRegistry::new();
420 reg.register::<OverridePat>();
421
422 // Minimal PAT bytes (just table_id + enough to not error in OverridePat)
423 let bytes = [0x00u8, 0x01, 0x02];
424 let result = AnyTableSection::parse_with(®, &bytes).unwrap();
425 match result {
426 AnyTableSection::Other {
427 table_id,
428 ref value,
429 } => {
430 assert_eq!(table_id, 0x00);
431 let op = value.downcast_ref::<OverridePat>().unwrap();
432 assert_eq!(op.table_id, 0x00);
433 }
434 other => panic!("expected Other (override), got {other:?}"),
435 }
436 }
437
438 #[test]
439 fn empty_bytes_returns_buffer_too_short() {
440 let reg = TableRegistry::new();
441 let result = AnyTableSection::parse_with(®, &[]);
442 assert!(matches!(
443 result,
444 Err(crate::Error::BufferTooShort {
445 need: 1,
446 have: 0,
447 ..
448 })
449 ));
450 }
451
452 #[cfg(feature = "serde")]
453 #[test]
454 fn serde_other_variant() {
455 let mut reg = TableRegistry::new();
456 reg.register::<CustomTable>();
457
458 let bytes = [CUSTOM_TABLE_ID, 0xAA, 0xBB];
459 let result = AnyTableSection::parse_with(®, &bytes).unwrap();
460
461 let json = serde_json::to_string(&result).unwrap();
462 assert!(json.contains("\"other\""), "unexpected JSON: {json}");
463 }
464}