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(broadcast_common::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;
60
61broadcast_common::declare_object_trait! {
62    /// Object-safe face of a runtime-registered table section value.
63    ///
64    /// Registered types must be owned (`'static`) because the `dyn Any` downcast
65    /// path requires it.  See the [module docs][self] for details.
66    ///
67    /// Implemented automatically via the blanket impl for any `T` satisfying the
68    /// supertraits; you do not need to write this by hand.
69    pub trait TableObject, "table section"
70}
71
72// ---------------------------------------------------------------------------
73// Internal parse closure type
74// ---------------------------------------------------------------------------
75
76/// A heap-allocated parse closure that takes a full section (header + body)
77/// and returns an owned, type-erased table-section value.
78pub(crate) type CustomParse =
79    Box<dyn for<'a> Fn(&'a [u8]) -> crate::Result<Box<dyn TableObject>> + Send + Sync>;
80
81// ---------------------------------------------------------------------------
82// TableRegistry
83// ---------------------------------------------------------------------------
84
85/// Runtime-configurable table registry.
86///
87/// By default the registry has no custom parsers.  Use
88/// [`register`][Self::register] to add one.
89///
90/// # Precedence (per table_id)
91///
92/// 1. Custom-registered parser (table_id in the [`custom`][Self::register] map) →
93///    [`crate::tables::AnyTableSection::Other`]
94/// 2. Built-in dispatch ([`crate::tables::AnyTableSection::parse`]) → typed variant
95/// 3. Unknown → [`crate::tables::AnyTableSection::Unknown`]
96#[derive(Default)]
97pub struct TableRegistry {
98    custom: BTreeMap<u8, CustomParse>,
99}
100
101impl TableRegistry {
102    /// Create an empty registry (built-in dispatch only).
103    #[must_use]
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    /// Look up a custom parser for the given `table_id`.
109    #[must_use]
110    pub(crate) fn lookup(&self, table_id: u8) -> Option<&CustomParse> {
111        self.custom.get(&table_id)
112    }
113
114    /// Register an owned custom table-section type for every table_id in its
115    /// [`TableDef::TABLE_ID_RANGES`][crate::traits::TableDef::TABLE_ID_RANGES].
116    ///
117    /// # Owned types only
118    ///
119    /// `T` must be `'static` — no borrowed slices.  The registered value is
120    /// type-erased as `Box<dyn TableObject>`; `dyn Any` downcast requires
121    /// the concrete type to be `'static`.
122    ///
123    /// # Multi-range registration
124    ///
125    /// A type may cover multiple table_id ranges (e.g.
126    /// `&[(0x90, 0x90), (0x92, 0x93)]`).  Every table_id in every range is
127    /// registered under the same parse closure.
128    ///
129    /// Registering a type whose table_id is already used by a built-in **overrides**
130    /// the built-in for that table_id.
131    ///
132    /// Re-registering the same table_id replaces the prior custom parser (last wins).
133    /// A failing custom parse surfaces the client's `Parse::Error` unwrapped —
134    /// embed identifying context (type/table_id) in your error's `what`/`reason` fields.
135    pub fn register<T>(&mut self) -> &mut Self
136    where
137        T: for<'a> crate::traits::TableDef<'a> + TableObject + 'static,
138    {
139        let ranges = <T as crate::traits::TableDef<'static>>::TABLE_ID_RANGES;
140        for &(lo, hi) in ranges {
141            for id in lo..=hi {
142                self.custom.insert(
143                    id,
144                    Box::new(|b| {
145                        Ok(Box::new(<T as broadcast_common::Parse>::parse(b)?)
146                            as Box<dyn TableObject>)
147                    }),
148                );
149            }
150        }
151        self
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::tables::AnyTableSection;
159    use crate::traits::TableDef;
160    use alloc::{vec, vec::Vec};
161    use broadcast_common::Parse;
162
163    // -- A trivial custom table for a private table_id -------------------------
164    const CUSTOM_TABLE_ID: u8 = 0x90;
165
166    #[derive(Debug, PartialEq)]
167    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
168    struct CustomTable {
169        table_id: u8,
170        payload: Vec<u8>,
171    }
172
173    impl<'a> Parse<'a> for CustomTable {
174        type Error = crate::Error;
175        fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
176            if bytes.is_empty() {
177                return Err(crate::Error::BufferTooShort(
178                    broadcast_common::error::BufferTooShort {
179                        need: 1,
180                        have: 0,
181                        what: "CustomTable",
182                    },
183                ));
184            }
185            Ok(Self {
186                table_id: bytes[0],
187                payload: bytes[1..].to_vec(),
188            })
189        }
190    }
191
192    impl TableDef<'_> for CustomTable {
193        const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(CUSTOM_TABLE_ID, CUSTOM_TABLE_ID)];
194        const NAME: &'static str = "CUSTOM_TABLE";
195    }
196
197    // -- A multi-range custom table --------------------------------------------
198    const MULTI_LO: u8 = 0x90;
199    const MULTI_MID: u8 = 0x92;
200    const MULTI_HI: u8 = 0x93;
201
202    #[derive(Debug, PartialEq)]
203    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
204    struct MultiRangeTable {
205        table_id: u8,
206    }
207
208    impl<'a> Parse<'a> for MultiRangeTable {
209        type Error = crate::Error;
210        fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
211            if bytes.is_empty() {
212                return Err(crate::Error::BufferTooShort(
213                    broadcast_common::error::BufferTooShort {
214                        need: 1,
215                        have: 0,
216                        what: "MultiRangeTable",
217                    },
218                ));
219            }
220            Ok(Self { table_id: bytes[0] })
221        }
222    }
223
224    impl TableDef<'_> for MultiRangeTable {
225        const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(MULTI_LO, MULTI_LO), (MULTI_MID, MULTI_HI)];
226        const NAME: &'static str = "MULTI_RANGE_TABLE";
227    }
228
229    // -- A custom table that overrides a built-in (PAT = 0x00) -----------------
230    #[derive(Debug, PartialEq)]
231    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
232    struct OverridePat {
233        table_id: u8,
234    }
235
236    impl<'a> Parse<'a> for OverridePat {
237        type Error = crate::Error;
238        fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
239            if bytes.is_empty() {
240                return Err(crate::Error::BufferTooShort(
241                    broadcast_common::error::BufferTooShort {
242                        need: 1,
243                        have: 0,
244                        what: "OverridePat",
245                    },
246                ));
247            }
248            Ok(Self { table_id: bytes[0] })
249        }
250    }
251
252    impl TableDef<'_> for OverridePat {
253        const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(0x00, 0x00)];
254        const NAME: &'static str = "OVERRIDE_PAT";
255    }
256
257    #[test]
258    fn custom_table_dispatches_to_other() {
259        let mut reg = TableRegistry::new();
260        reg.register::<CustomTable>();
261
262        let bytes = [CUSTOM_TABLE_ID, 0xAA, 0xBB];
263        let result = AnyTableSection::parse_with(&reg, &bytes).unwrap();
264        match result {
265            AnyTableSection::Other {
266                table_id,
267                ref value,
268            } => {
269                assert_eq!(table_id, CUSTOM_TABLE_ID);
270                let ct = value.downcast_ref::<CustomTable>().unwrap();
271                assert_eq!(ct.table_id, CUSTOM_TABLE_ID);
272                assert_eq!(ct.payload, &[0xAA, 0xBB]);
273            }
274            other => panic!("expected Other, got {other:?}"),
275        }
276    }
277
278    #[test]
279    fn multi_range_registers_all_ids() {
280        let mut reg = TableRegistry::new();
281        reg.register::<MultiRangeTable>();
282
283        assert!(reg.lookup(MULTI_LO).is_some());
284        assert!(reg.lookup(MULTI_MID).is_some());
285        assert!(reg.lookup(MULTI_MID + 1).is_some());
286        assert!(reg.lookup(MULTI_HI).is_some());
287        // 0x91 is NOT in any range
288        assert!(reg.lookup(MULTI_LO + 1).is_none());
289
290        // Each registered id dispatches to Other
291        for id in [MULTI_LO, MULTI_MID, MULTI_MID + 1, MULTI_HI] {
292            let bytes = [id, 0x00];
293            let result = AnyTableSection::parse_with(&reg, &bytes).unwrap();
294            match result {
295                AnyTableSection::Other { table_id, .. } => assert_eq!(table_id, id),
296                other => panic!("id {id:#04x}: expected Other, got {other:?}"),
297            }
298        }
299
300        // 0x91 falls to Unknown
301        let bytes = [MULTI_LO + 1, 0x00];
302        let result = AnyTableSection::parse_with(&reg, &bytes).unwrap();
303        assert!(
304            matches!(result, AnyTableSection::Unknown { .. }),
305            "expected Unknown for unregistered id 0x91, got {result:?}",
306        );
307    }
308
309    #[test]
310    fn fallback_to_builtin_for_unregistered() {
311        let reg = TableRegistry::new();
312        // Build a minimal PAT (table_id 0x00)
313        use crate::tables::pat::{PatEntry, PatSection};
314        use broadcast_common::Serialize;
315        use mpeg_ts::pid::Pid;
316        let pat = PatSection::new(1, 0, true, 0, 0, vec![PatEntry::new(1, Pid::new(0x0100))]);
317        let mut buf = vec![0u8; pat.serialized_len()];
318        pat.serialize_into(&mut buf).unwrap();
319
320        let result = AnyTableSection::parse_with(&reg, &buf).unwrap();
321        match result {
322            AnyTableSection::PatSection(p) => assert_eq!(p.entries.len(), 1),
323            other => panic!("expected PatSection, got {other:?}"),
324        }
325    }
326
327    #[test]
328    fn override_builtin_yields_other() {
329        let mut reg = TableRegistry::new();
330        reg.register::<OverridePat>();
331
332        // Minimal PAT bytes (just table_id + enough to not error in OverridePat)
333        let bytes = [0x00u8, 0x01, 0x02];
334        let result = AnyTableSection::parse_with(&reg, &bytes).unwrap();
335        match result {
336            AnyTableSection::Other {
337                table_id,
338                ref value,
339            } => {
340                assert_eq!(table_id, 0x00);
341                let op = value.downcast_ref::<OverridePat>().unwrap();
342                assert_eq!(op.table_id, 0x00);
343            }
344            other => panic!("expected Other (override), got {other:?}"),
345        }
346    }
347
348    #[test]
349    fn empty_bytes_returns_buffer_too_short() {
350        let reg = TableRegistry::new();
351        let result = AnyTableSection::parse_with(&reg, &[]);
352        assert!(matches!(
353            result,
354            Err(crate::Error::BufferTooShort(
355                broadcast_common::error::BufferTooShort {
356                    need: 1,
357                    have: 0,
358                    ..
359                }
360            ))
361        ));
362    }
363
364    #[cfg(feature = "serde")]
365    #[test]
366    fn serde_other_variant() {
367        let mut reg = TableRegistry::new();
368        reg.register::<CustomTable>();
369
370        let bytes = [CUSTOM_TABLE_ID, 0xAA, 0xBB];
371        let result = AnyTableSection::parse_with(&reg, &bytes).unwrap();
372
373        let json = serde_json::to_string(&result).unwrap();
374        assert!(json.contains("\"other\""), "unexpected JSON: {json}");
375    }
376}