dvb-si 13.0.0

ETSI EN 300 468 DVB Service Information parser + builder. MPEG-2 PSI included.
Documentation
//! 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_tag_namespace!`) — 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;

use crate::descriptors::tag_namespace::declare_tag_namespace;

declare_tag_namespace! {'a;
    enum AnyAitDescriptor, space "AIT", spec "ETSI TS 102 809 §5.3", names "`\"APPLICATION\"`, `\"TRANSPORT_PROTOCOL\"`, …";
    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:?}"),
        }
    }
}