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(®, &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 broadcast_common::Parse;
161
162 // -- A trivial custom table for a private table_id -------------------------
163 const CUSTOM_TABLE_ID: u8 = 0x90;
164
165 #[derive(Debug, PartialEq)]
166 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
167 struct CustomTable {
168 table_id: u8,
169 payload: Vec<u8>,
170 }
171
172 impl<'a> Parse<'a> for CustomTable {
173 type Error = crate::Error;
174 fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
175 if bytes.is_empty() {
176 return Err(crate::Error::BufferTooShort(
177 broadcast_common::error::BufferTooShort {
178 need: 1,
179 have: 0,
180 what: "CustomTable",
181 },
182 ));
183 }
184 Ok(Self {
185 table_id: bytes[0],
186 payload: bytes[1..].to_vec(),
187 })
188 }
189 }
190
191 impl TableDef<'_> for CustomTable {
192 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(CUSTOM_TABLE_ID, CUSTOM_TABLE_ID)];
193 const NAME: &'static str = "CUSTOM_TABLE";
194 }
195
196 // -- A multi-range custom table --------------------------------------------
197 const MULTI_LO: u8 = 0x90;
198 const MULTI_MID: u8 = 0x92;
199 const MULTI_HI: u8 = 0x93;
200
201 #[derive(Debug, PartialEq)]
202 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
203 struct MultiRangeTable {
204 table_id: u8,
205 }
206
207 impl<'a> Parse<'a> for MultiRangeTable {
208 type Error = crate::Error;
209 fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
210 if bytes.is_empty() {
211 return Err(crate::Error::BufferTooShort(
212 broadcast_common::error::BufferTooShort {
213 need: 1,
214 have: 0,
215 what: "MultiRangeTable",
216 },
217 ));
218 }
219 Ok(Self { table_id: bytes[0] })
220 }
221 }
222
223 impl TableDef<'_> for MultiRangeTable {
224 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(MULTI_LO, MULTI_LO), (MULTI_MID, MULTI_HI)];
225 const NAME: &'static str = "MULTI_RANGE_TABLE";
226 }
227
228 // -- A custom table that overrides a built-in (PAT = 0x00) -----------------
229 #[derive(Debug, PartialEq)]
230 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
231 struct OverridePat {
232 table_id: u8,
233 }
234
235 impl<'a> Parse<'a> for OverridePat {
236 type Error = crate::Error;
237 fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
238 if bytes.is_empty() {
239 return Err(crate::Error::BufferTooShort(
240 broadcast_common::error::BufferTooShort {
241 need: 1,
242 have: 0,
243 what: "OverridePat",
244 },
245 ));
246 }
247 Ok(Self { table_id: bytes[0] })
248 }
249 }
250
251 impl TableDef<'_> for OverridePat {
252 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(0x00, 0x00)];
253 const NAME: &'static str = "OVERRIDE_PAT";
254 }
255
256 #[test]
257 fn custom_table_dispatches_to_other() {
258 let mut reg = TableRegistry::new();
259 reg.register::<CustomTable>();
260
261 let bytes = [CUSTOM_TABLE_ID, 0xAA, 0xBB];
262 let result = AnyTableSection::parse_with(®, &bytes).unwrap();
263 match result {
264 AnyTableSection::Other {
265 table_id,
266 ref value,
267 } => {
268 assert_eq!(table_id, CUSTOM_TABLE_ID);
269 let ct = value.downcast_ref::<CustomTable>().unwrap();
270 assert_eq!(ct.table_id, CUSTOM_TABLE_ID);
271 assert_eq!(ct.payload, &[0xAA, 0xBB]);
272 }
273 other => panic!("expected Other, got {other:?}"),
274 }
275 }
276
277 #[test]
278 fn multi_range_registers_all_ids() {
279 let mut reg = TableRegistry::new();
280 reg.register::<MultiRangeTable>();
281
282 assert!(reg.lookup(MULTI_LO).is_some());
283 assert!(reg.lookup(MULTI_MID).is_some());
284 assert!(reg.lookup(MULTI_MID + 1).is_some());
285 assert!(reg.lookup(MULTI_HI).is_some());
286 // 0x91 is NOT in any range
287 assert!(reg.lookup(MULTI_LO + 1).is_none());
288
289 // Each registered id dispatches to Other
290 for id in [MULTI_LO, MULTI_MID, MULTI_MID + 1, MULTI_HI] {
291 let bytes = [id, 0x00];
292 let result = AnyTableSection::parse_with(®, &bytes).unwrap();
293 match result {
294 AnyTableSection::Other { table_id, .. } => assert_eq!(table_id, id),
295 other => panic!("id {id:#04x}: expected Other, got {other:?}"),
296 }
297 }
298
299 // 0x91 falls to Unknown
300 let bytes = [MULTI_LO + 1, 0x00];
301 let result = AnyTableSection::parse_with(®, &bytes).unwrap();
302 assert!(
303 matches!(result, AnyTableSection::Unknown { .. }),
304 "expected Unknown for unregistered id 0x91, got {result:?}",
305 );
306 }
307
308 #[test]
309 fn fallback_to_builtin_for_unregistered() {
310 let reg = TableRegistry::new();
311 // Build a minimal PAT (table_id 0x00)
312 use crate::tables::pat::{PatEntry, PatSection};
313 use broadcast_common::Serialize;
314 use mpeg_ts::pid::Pid;
315 let pat = PatSection::new(1, 0, true, 0, 0, vec![PatEntry::new(1, Pid::new(0x0100))]);
316 let mut buf = vec![0u8; pat.serialized_len()];
317 pat.serialize_into(&mut buf).unwrap();
318
319 let result = AnyTableSection::parse_with(®, &buf).unwrap();
320 match result {
321 AnyTableSection::PatSection(p) => assert_eq!(p.entries.len(), 1),
322 other => panic!("expected PatSection, got {other:?}"),
323 }
324 }
325
326 #[test]
327 fn override_builtin_yields_other() {
328 let mut reg = TableRegistry::new();
329 reg.register::<OverridePat>();
330
331 // Minimal PAT bytes (just table_id + enough to not error in OverridePat)
332 let bytes = [0x00u8, 0x01, 0x02];
333 let result = AnyTableSection::parse_with(®, &bytes).unwrap();
334 match result {
335 AnyTableSection::Other {
336 table_id,
337 ref value,
338 } => {
339 assert_eq!(table_id, 0x00);
340 let op = value.downcast_ref::<OverridePat>().unwrap();
341 assert_eq!(op.table_id, 0x00);
342 }
343 other => panic!("expected Other (override), got {other:?}"),
344 }
345 }
346
347 #[test]
348 fn empty_bytes_returns_buffer_too_short() {
349 let reg = TableRegistry::new();
350 let result = AnyTableSection::parse_with(®, &[]);
351 assert!(matches!(
352 result,
353 Err(crate::Error::BufferTooShort(
354 broadcast_common::error::BufferTooShort {
355 need: 1,
356 have: 0,
357 ..
358 }
359 ))
360 ));
361 }
362
363 #[cfg(feature = "serde")]
364 #[test]
365 fn serde_other_variant() {
366 let mut reg = TableRegistry::new();
367 reg.register::<CustomTable>();
368
369 let bytes = [CUSTOM_TABLE_ID, 0xAA, 0xBB];
370 let result = AnyTableSection::parse_with(®, &bytes).unwrap();
371
372 let json = serde_json::to_string(&result).unwrap();
373 assert!(json.contains("\"other\""), "unexpected JSON: {json}");
374 }
375}