dvb-si 12.0.0

ETSI EN 300 468 DVB Service Information parser + builder. MPEG-2 PSI included.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
//! AIT descriptor namespace — [`AnyAitDescriptor`] + [`parse_ait_loop`].
//!
//! The AIT (Application Information Table, ETSI TS 102 809 §5.3) uses a
//! SEPARATE descriptor namespace from the EN 300 468 SI descriptors. Low tag
//! bytes mean different things (e.g. AIT tag 0x02 = transport_protocol, but
//! SI tag 0x02 = video_stream). This module mirrors the structure of
//! [`crate::descriptors::any`] for the AIT namespace.
//!
//! [`AnyAitDescriptor`] is generated from a single declarative list
//! (`declare_ait_descriptors!`) — one line per AIT descriptor tag. The list
//! produces the enum, `From` impls, `name()`, `dispatch()`,
//! `DISPATCHED_TAGS`, and a drift test that pins each tag literal to the
//! type's [`crate::traits::DescriptorDef::TAG`].
//!
//! Tag 0x5F is the DVB-SI `private_data_specifier_descriptor` (ETSI EN 300 468
//! §6.2.31) reused verbatim: TS 102 809 §5.3.4.7 and Table 38 (note 2) say "the
//! DVB SI private data specifier descriptor is defined for use in the
//! Application Information Table to introduce private descriptors" and TS 102
//! 727 (its AIT allocation table, note 2) repeats it, so AIT dispatches to the same
//! [`PrivateDataSpecifierDescriptor`](crate::descriptors::private_data_specifier::PrivateDataSpecifierDescriptor).
//!
//! [`parse_ait_loop`] lazily walks a raw AIT descriptor loop (the
//! `descriptor()` sequence inside AIT common or per-application loops),
//! yielding one [`AnyAitDescriptor`] per entry.
//!
//! ```
//! use dvb_si::descriptors::ait::{parse_ait_loop, AnyAitDescriptor};
//!
//! // An AIT descriptor loop: application_usage (tag 0x16, usage_type=0x01)
//! // then an unknown tag 0xFE.
//! let loop_bytes = [
//!     0x16, 0x01, 0x01,             // application_usage: Digital Text
//!     0xFE, 0x02, 0xCA, 0xFE,       // unknown 0xFE
//! ];
//! let items: Vec<_> = parse_ait_loop(&loop_bytes).collect();
//! assert_eq!(items.len(), 2);
//! match items[0].as_ref().unwrap() {
//!     AnyAitDescriptor::ApplicationUsage(au) => {
//!         assert_eq!(au.usage_type, 0x01);
//!     }
//!     other => panic!("expected ApplicationUsage, got {other:?}"),
//! }
//! assert!(matches!(
//!     items[1].as_ref().unwrap(),
//!     AnyAitDescriptor::Unknown { tag: 0xFE, .. }
//! ));
//! ```

pub mod application;
pub mod application_icons;
pub mod application_name;
pub mod application_recording;
pub mod application_storage;
pub mod application_usage;
pub mod dii_location;
pub mod dvb_html_application_boundary;
pub mod dvb_html_application_location;
pub mod dvb_j_application;
pub mod dvb_j_application_location;
pub mod external_application_authorisation;
pub mod graphics_constraints;
pub mod ip_signalling;
pub mod label_bytes;
pub mod prefetch;
pub mod provider_export;
pub mod provider_usage;
pub mod simple_application_boundary;
pub mod simple_application_location;
pub mod transport_protocol;

pub use application::ApplicationDescriptor;
pub use application::Visibility;
pub use application_icons::{ApplicationIconsDescriptor, IconFlags, IconFormat};
pub use application_name::ApplicationNameDescriptor;
pub use application_recording::{
    ApplicationRecordingDescriptor, RecordingLabel, StorageProperties,
};
pub use application_storage::{ApplicationStorageDescriptor, StorageProperty};
pub use application_usage::ApplicationUsageDescriptor;
pub use dii_location::{DiiLocation, DiiLocationDescriptor};
pub use dvb_html_application_boundary::DvbHtmlApplicationBoundaryDescriptor;
pub use dvb_html_application_location::DvbHtmlApplicationLocationDescriptor;
pub use dvb_j_application::DvbJApplicationDescriptor;
pub use dvb_j_application_location::DvbJApplicationLocationDescriptor;
pub use external_application_authorisation::ExternalApplicationAuthorisationDescriptor;
pub use graphics_constraints::{GraphicsConfiguration, GraphicsConstraintsDescriptor};
pub use ip_signalling::IpSignallingDescriptor;
pub use label_bytes::LabelBytes;
pub use prefetch::{PrefetchDescriptor, PrefetchEntry};
pub use provider_export::{ProviderExport, ProviderExportDescriptor};
pub use provider_usage::ProviderUsageDescriptor;
pub use simple_application_boundary::SimpleApplicationBoundaryDescriptor;
pub use simple_application_location::SimpleApplicationLocationDescriptor;
pub use transport_protocol::TransportProtocolDescriptor;

/// Declares [`AnyAitDescriptor`] + its dispatcher from one tag list.
///
/// Mirrors the `declare_descriptors!` macro in `crate::descriptors::any`.
/// Each line is `Variant = 0xTAG => crate::descriptors::ait::module::Type[<'a>]`.
macro_rules! declare_ait_descriptors {
    (
        $lt:lifetime;
        $( $variant:ident = $tag:literal => $($path:ident)::+ $(<$plt:lifetime>)? ),+ $(,)?
    ) => {
        /// Every crate-implemented AIT descriptor, plus an `Unknown` fallthrough.
        ///
        /// AIT tags belong to a separate namespace from EN 300 468 SI tags
        /// (ETSI TS 102 809 §5.3). The same numeric tag value may map to
        /// a different type here.
        #[derive(Debug)]
        #[cfg_attr(feature = "serde", derive(serde::Serialize))]
        #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
        #[non_exhaustive]
        pub enum AnyAitDescriptor<$lt> {
            $(
                #[allow(missing_docs)]
                $variant($($path)::+ $(<$plt>)?),
            )+
            /// Tag with no typed implementation; `body` is the payload sans
            /// the 2-byte (tag, length) header.
            Unknown {
                /// The raw descriptor_tag byte.
                tag: u8,
                /// The raw payload bytes (descriptor_length bytes).
                body: &$lt [u8],
            },
        }

        $(
            impl<$lt> From<$($path)::+ $(<$plt>)?> for AnyAitDescriptor<$lt> {
                fn from(d: $($path)::+ $(<$plt>)?) -> Self {
                    Self::$variant(d)
                }
            }
        )+

        impl<$lt> AnyAitDescriptor<$lt> {
            /// Every tag the generated dispatcher routes (excludes
            /// [`AnyAitDescriptor::Unknown`]).
            pub const DISPATCHED_TAGS: &'static [u8] = &[$($tag),+];

            /// Diagnostic name of the contained AIT descriptor — the type's
            /// [`DescriptorDef::NAME`](crate::traits::DescriptorDef::NAME)
            /// (`"APPLICATION"`, `"TRANSPORT_PROTOCOL"`, …); `"UNKNOWN"`
            /// for [`AnyAitDescriptor::Unknown`].
            #[must_use]
            pub fn name(&self) -> &'static str {
                match self {
                    $(
                        Self::$variant(_) =>
                            <$($path)::+ as crate::traits::DescriptorDef>::NAME,
                    )+
                    Self::Unknown { .. } => "UNKNOWN",
                }
            }

            /// Parse one full AIT descriptor (2-byte header included) by its tag.
            ///
            /// `None` means no typed implementation exists for `tag` (the
            /// caller turns that into [`AnyAitDescriptor::Unknown`]). `Some(Err)`
            /// is a typed parse failure for a recognised tag.
            pub(crate) fn dispatch(tag: u8, full: &$lt [u8]) -> Option<crate::error::Result<Self>> {
                use broadcast_common::Parse;
                match tag {
                    $(
                        $tag => Some(<$($path)::+>::parse(full).map(Self::$variant)),
                    )+
                    _ => None,
                }
            }
        }

        #[cfg(test)]
        mod macro_drift {
            #[test]
            fn tag_literals_match_descriptor_def() {
                use crate::traits::DescriptorDef;
                $(
                    assert_eq!(
                        $tag,
                        <$($path)::+ as DescriptorDef>::TAG,
                        concat!("tag literal drift for ", stringify!($variant)),
                    );
                    assert!(
                        !<$($path)::+ as DescriptorDef>::NAME.is_empty(),
                        concat!("empty NAME for ", stringify!($variant)),
                    );
                )+
            }
        }
    };
}

declare_ait_descriptors! {'a;
    Application              = 0x00 => crate::descriptors::ait::application::ApplicationDescriptor,
    ApplicationName          = 0x01 => crate::descriptors::ait::application_name::ApplicationNameDescriptor<'a>,
    TransportProtocol        = 0x02 => crate::descriptors::ait::transport_protocol::TransportProtocolDescriptor<'a>,
    DvbJApplication          = 0x03 => crate::descriptors::ait::dvb_j_application::DvbJApplicationDescriptor<'a>,
    DvbJApplicationLocation  = 0x04 => crate::descriptors::ait::dvb_j_application_location::DvbJApplicationLocationDescriptor<'a>,
    ExternalAppAuthorisation = 0x05 => crate::descriptors::ait::external_application_authorisation::ExternalApplicationAuthorisationDescriptor,
    ApplicationRecording     = 0x06 => crate::descriptors::ait::application_recording::ApplicationRecordingDescriptor<'a>,
    DvbHtmlApplicationLocation = 0x09 => crate::descriptors::ait::dvb_html_application_location::DvbHtmlApplicationLocationDescriptor<'a>,
    DvbHtmlApplicationBoundary = 0x0A => crate::descriptors::ait::dvb_html_application_boundary::DvbHtmlApplicationBoundaryDescriptor<'a>,
    ApplicationIcons         = 0x0B => crate::descriptors::ait::application_icons::ApplicationIconsDescriptor<'a>,
    Prefetch                 = 0x0C => crate::descriptors::ait::prefetch::PrefetchDescriptor<'a>,
    DiiLocation              = 0x0D => crate::descriptors::ait::dii_location::DiiLocationDescriptor,
    ApplicationStorage       = 0x10 => crate::descriptors::ait::application_storage::ApplicationStorageDescriptor,
    IpSignalling             = 0x11 => crate::descriptors::ait::ip_signalling::IpSignallingDescriptor,
    ProviderExport           = 0x12 => crate::descriptors::ait::provider_export::ProviderExportDescriptor<'a>,
    ProviderUsage            = 0x13 => crate::descriptors::ait::provider_usage::ProviderUsageDescriptor<'a>,
    GraphicsConstraints      = 0x14 => crate::descriptors::ait::graphics_constraints::GraphicsConstraintsDescriptor,
    SimpleAppLocation        = 0x15 => crate::descriptors::ait::simple_application_location::SimpleApplicationLocationDescriptor<'a>,
    ApplicationUsage         = 0x16 => crate::descriptors::ait::application_usage::ApplicationUsageDescriptor,
    SimpleAppBoundary        = 0x17 => crate::descriptors::ait::simple_application_boundary::SimpleApplicationBoundaryDescriptor<'a>,
    PrivateDataSpecifier     = 0x5F => crate::descriptors::private_data_specifier::PrivateDataSpecifierDescriptor,
}

/// Lazily walk a raw AIT descriptor loop. Never panics.
///
/// Per-descriptor parse errors yield `Err` and iteration continues (the
/// `descriptor_length` field bounds each entry, so the walker can always
/// advance past a malformed body). A truncated final header or body yields
/// one `Err` and then the iterator fuses.
#[must_use]
pub fn parse_ait_loop(bytes: &[u8]) -> AitDescriptorIter<'_> {
    AitDescriptorIter {
        bytes,
        pos: 0,
        fused: false,
    }
}

/// Iterator over a raw AIT descriptor loop; see [`parse_ait_loop`].
#[derive(Debug, Clone)]
pub struct AitDescriptorIter<'a> {
    bytes: &'a [u8],
    pos: usize,
    fused: bool,
}

impl<'a> Iterator for AitDescriptorIter<'a> {
    type Item = crate::error::Result<AnyAitDescriptor<'a>>;

    fn next(&mut self) -> Option<Self::Item> {
        let (tag, full) = match crate::descriptors::any::next_loop_entry(
            self.bytes,
            &mut self.pos,
            &mut self.fused,
        )? {
            Ok(v) => v,
            Err(e) => return Some(Err(e)),
        };
        Some(match AnyAitDescriptor::dispatch(tag, full) {
            Some(res) => res,
            None => Ok(AnyAitDescriptor::Unknown {
                tag,
                body: &full[2..],
            }),
        })
    }
}

impl core::iter::FusedIterator for AitDescriptorIter<'_> {}

/// A raw AIT descriptor loop, borrowed from the section. Zero-copy: walk it
/// typed via [`AitDescriptorLoop::iter`]; serde serializes the typed walk.
///
/// This mirrors [`crate::descriptors::DescriptorLoop`] for the AIT namespace.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct AitDescriptorLoop<'a>(&'a [u8]);

impl<'a> AitDescriptorLoop<'a> {
    /// Wrap a raw AIT descriptor-loop slice (the `descriptor()` bytes only —
    /// no enclosing length field).
    #[must_use]
    pub const fn new(raw: &'a [u8]) -> Self {
        Self(raw)
    }

    /// The raw wire bytes of the loop, verbatim.
    #[must_use]
    pub const fn raw(&self) -> &'a [u8] {
        self.0
    }

    /// Lazily walk the loop, yielding one typed [`AnyAitDescriptor`] per entry
    /// (or [`AnyAitDescriptor::Unknown`] for tags with no implementation).
    /// Delegates to [`parse_ait_loop`]; never panics.
    #[must_use]
    pub fn iter(&self) -> AitDescriptorIter<'a> {
        parse_ait_loop(self.0)
    }
}

#[cfg(feature = "serde")]
impl serde::Serialize for AitDescriptorLoop<'_> {
    /// Serializes as a sequence of the typed walk: each `Ok(d)` becomes the
    /// [`AnyAitDescriptor`] (camelCase external tagging), and each `Err(e)`
    /// becomes a `{"parseError": "<Display>"}` map — parse errors are
    /// surfaced, never silently dropped (r03-W13, matching
    /// [`DescriptorLoop`](crate::descriptors::DescriptorLoop)'s serde impl).
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        struct Entry<'a>(crate::error::Result<AnyAitDescriptor<'a>>);
        impl serde::Serialize for Entry<'_> {
            fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
                match &self.0 {
                    Ok(d) => d.serialize(s),
                    Err(e) => {
                        use serde::ser::SerializeMap;
                        let mut m = s.serialize_map(Some(1))?;
                        m.serialize_entry("parseError", &alloc::string::ToString::to_string(e))?;
                        m.end()
                    }
                }
            }
        }
        s.collect_seq(self.iter().map(Entry))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Regression for r03-W13: a parse error inside the loop must be
    /// surfaced in the serde output (as `{"parseError": ...}`), not
    /// silently dropped from the sequence.
    #[cfg(feature = "serde")]
    #[test]
    fn serde_surfaces_parse_errors_instead_of_dropping_them() {
        // tag=application_usage(0x16), declared length=0xFF, but no bytes
        // follow — a malformed entry that must parse as an Err, not be
        // silently skipped.
        let buf = alloc::vec![0x16, 0xFF];
        let loop_ = AitDescriptorLoop::new(&buf);
        let json = serde_json::to_string(&loop_).unwrap();
        assert!(
            json.contains("parseError"),
            "expected a surfaced parseError, got {json}"
        );
    }

    #[test]
    fn walk_ait_loop_yields_typed_variants() {
        // application_usage (0x16) + simple_app_location (0x15) + unknown (0xFE).
        let buf = alloc::vec![
            0x16, 0x01, 0x01, 0x15, 0x05, b'/', b'a', b'p', b'p', b'/', 0xFE, 0x02, 0xCA, 0xFE,
        ];
        let items: Vec<_> = parse_ait_loop(&buf).collect();
        assert_eq!(items.len(), 3);

        match items[0].as_ref().unwrap() {
            AnyAitDescriptor::ApplicationUsage(au) => {
                assert_eq!(au.usage_type, 0x01);
            }
            other => panic!("expected ApplicationUsage, got {other:?}"),
        }

        match items[1].as_ref().unwrap() {
            AnyAitDescriptor::SimpleAppLocation(loc) => {
                assert_eq!(loc.initial_path_bytes.raw(), b"/app/");
            }
            other => panic!("expected SimpleAppLocation, got {other:?}"),
        }

        match items[2].as_ref().unwrap() {
            AnyAitDescriptor::Unknown { tag, body } => {
                assert_eq!(*tag, 0xFE);
                assert_eq!(*body, &[0xCA, 0xFE]);
            }
            other => panic!("expected Unknown, got {other:?}"),
        }
    }

    #[test]
    fn ait_descriptor_loop_iter() {
        let buf = [0x16, 0x01, 0x01];
        let loop_ = AitDescriptorLoop::new(&buf);
        let items: Vec<_> = loop_.iter().collect();
        assert_eq!(items.len(), 1);
        assert!(matches!(
            items[0].as_ref().unwrap(),
            AnyAitDescriptor::ApplicationUsage(_)
        ));
        assert_eq!(loop_.raw(), &buf[..]);
    }

    #[test]
    fn dispatched_tags_covers_all_known() {
        assert_eq!(
            AnyAitDescriptor::DISPATCHED_TAGS,
            &[
                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x10, 0x11,
                0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x5F
            ]
        );
    }

    #[test]
    fn provider_and_private_data_specifier_dispatch() {
        // provider_export (0x12): "ab" + class "C"; provider_usage (0x13): "ab";
        // private_data_specifier (0x5F): EACEM 0x00000028 (EN 300 468 layout).
        let buf = [
            0x12, 0x05, 2, b'a', b'b', 1, b'C', 0x13, 0x03, 2, b'a', b'b', 0x5F, 0x04, 0x00, 0x00,
            0x00, 0x28,
        ];
        let items: Vec<_> = parse_ait_loop(&buf).collect();
        assert_eq!(items.len(), 3);
        match items[0].as_ref().unwrap() {
            AnyAitDescriptor::ProviderExport(d) => {
                assert_eq!(d.providers[0].provider_name, b"ab");
                assert_eq!(d.providers[0].provider_class_name, b"C");
            }
            other => panic!("expected ProviderExport, got {other:?}"),
        }
        match items[1].as_ref().unwrap() {
            AnyAitDescriptor::ProviderUsage(d) => assert_eq!(d.provider_names, [&b"ab"[..]]),
            other => panic!("expected ProviderUsage, got {other:?}"),
        }
        match items[2].as_ref().unwrap() {
            AnyAitDescriptor::PrivateDataSpecifier(d) => {
                assert_eq!(d.private_data_specifier, 0x0000_0028);
            }
            other => panic!("expected PrivateDataSpecifier, got {other:?}"),
        }
    }
}