Skip to main content

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