dvb-si 12.0.0

ETSI EN 300 468 DVB Service Information parser + builder. MPEG-2 PSI included.
Documentation
//! INT descriptor namespace — [`AnyIntDescriptor`] + [`parse_int_loop`].
//!
//! The IP/MAC Notification Table (ETSI EN 301 192 V1.7.1 §8.4) has its OWN
//! descriptor tag space for `0x00..=0x3F` (Table 19, clause 8.4.5.1): "MPEG
//! descriptors in the range 0x00 to 0x3F cannot be used in the INT", and those
//! tags "share a common descriptor name space with UNT descriptors". So
//! INT tag 0x06 is `target_smartcard_descriptor`, never SI/MPEG
//! `data_stream_alignment_descriptor` (issue #1285). Tags `0x40..=0xFF` "shall
//! have their standard semantics as defined in ETSI EN 300 468" (telephone
//! 0x57, private_data_specifier 0x5F, user private) and are dispatched through
//! [`AnyIntDescriptor::Si`].
//!
//! The list below is generated by `declare_scoped_namespace!` (one line per
//! tag, ascending); the loop wrapper [`IntDescriptorLoop`] is what the
//! `platform_descriptor_loop` / `target_descriptor_loop` /
//! `operational_descriptor_loop` of [`IntSection`](crate::tables::int::IntSection)
//! hold.
//!
//! ```
//! use dvb_si::descriptors::int::{parse_int_loop, AnyIntDescriptor};
//!
//! // target_serial_number (INT tag 0x08) then telephone-range SI tag 0x5F
//! // (private_data_specifier) then unknown INT tag 0x20.
//! let raw = [
//!     0x08, 0x02, 0xAB, 0xCD,
//!     0x5F, 0x04, 0x00, 0x00, 0x00, 0x28,
//!     0x20, 0x01, 0xFF,
//! ];
//! let items: Vec<_> = parse_int_loop(&raw).collect();
//! assert!(matches!(items[0].as_ref().unwrap(), AnyIntDescriptor::TargetSerialNumber(_)));
//! assert!(matches!(items[1].as_ref().unwrap(), AnyIntDescriptor::Si(_)));
//! assert!(matches!(items[2].as_ref().unwrap(), AnyIntDescriptor::Unknown { tag: 0x20, .. }));
//! ```

pub mod generic_stream_location;
pub mod isp_access_mode;
pub mod platform_name;
pub mod platform_provider_name;
pub mod stream_location;
pub mod target_ip_slash;
pub mod target_ip_source_slash;
pub mod target_ipv6_slash;
pub mod target_ipv6_source_slash;
pub mod target_mac_address_range;

pub use generic_stream_location::{
    GenericStreamSelector, IpMacGenericStreamLocationDescriptor, ModulationSystemType, PhyStreamId,
};
pub use isp_access_mode::{IspAccessMode, IspAccessModeDescriptor};
pub use platform_name::IpMacPlatformNameDescriptor;
pub use platform_provider_name::IpMacPlatformProviderNameDescriptor;
pub use stream_location::IpMacStreamLocationDescriptor;
pub use target_ip_slash::{Ipv4Slash, TargetIpSlashDescriptor};
pub use target_ip_source_slash::{Ipv4SourceDestSlash, TargetIpSourceSlashDescriptor};
pub use target_ipv6_slash::{Ipv6Slash, TargetIpv6SlashDescriptor};
pub use target_ipv6_source_slash::{Ipv6SourceDestSlash, TargetIpv6SourceSlashDescriptor};
pub use target_mac_address_range::{MacAddressRange, TargetMacAddressRangeDescriptor};

use crate::descriptors::scoped::declare_scoped_namespace;

declare_scoped_namespace! {'a;
    doc "Every crate-implemented INT descriptor (ETSI EN 301 192 Table 19), the SI fall-through for tags `0x40..=0xFF`, and `Unknown` for an unimplemented tag in the INT-owned `0x00..=0x3F` range.";
    enum AnyIntDescriptor, iter IntDescriptorIter, loop IntDescriptorLoop, parse parse_int_loop;
    @owned 0x00..=0x3F,
    TargetSmartcard = 0x06 => crate::descriptors::target::smartcard::TargetSmartcardDescriptor<'a>,
    TargetMacAddress = 0x07 => crate::descriptors::target::mac_address::TargetMacAddressDescriptor<'a>,
    TargetSerialNumber = 0x08 => crate::descriptors::target::serial_number::TargetSerialNumberDescriptor<'a>,
    TargetIpAddress = 0x09 => crate::descriptors::target::ip_address::TargetIpAddressDescriptor<'a>,
    TargetIpv6Address = 0x0A => crate::descriptors::target::ipv6_address::TargetIpv6AddressDescriptor<'a>,
    IpMacPlatformName = 0x0C => crate::descriptors::int::platform_name::IpMacPlatformNameDescriptor<'a>,
    IpMacPlatformProviderName = 0x0D => crate::descriptors::int::platform_provider_name::IpMacPlatformProviderNameDescriptor<'a>,
    TargetMacAddressRange = 0x0E => crate::descriptors::int::target_mac_address_range::TargetMacAddressRangeDescriptor<'a>,
    TargetIpSlash = 0x0F => crate::descriptors::int::target_ip_slash::TargetIpSlashDescriptor<'a>,
    TargetIpSourceSlash = 0x10 => crate::descriptors::int::target_ip_source_slash::TargetIpSourceSlashDescriptor<'a>,
    TargetIpv6Slash = 0x11 => crate::descriptors::int::target_ipv6_slash::TargetIpv6SlashDescriptor<'a>,
    TargetIpv6SourceSlash = 0x12 => crate::descriptors::int::target_ipv6_source_slash::TargetIpv6SourceSlashDescriptor<'a>,
    IpMacStreamLocation = 0x13 => crate::descriptors::int::stream_location::IpMacStreamLocationDescriptor<'a>,
    IspAccessMode = 0x14 => crate::descriptors::int::isp_access_mode::IspAccessModeDescriptor<'a>,
    IpMacGenericStreamLocation = 0x15 => crate::descriptors::int::generic_stream_location::IpMacGenericStreamLocationDescriptor<'a>,
}

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

    #[test]
    fn dispatched_tags_are_the_table_19_set() {
        assert_eq!(
            AnyIntDescriptor::DISPATCHED_TAGS,
            &[
                0x06, 0x07, 0x08, 0x09, 0x0A, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
                0x15
            ]
        );
    }

    /// The #1285 bug: SI/MPEG tag 0x06 (data_stream_alignment), 0x09 (CA),
    /// 0x0A (ISO_639_language) must NOT be decoded in the INT namespace, and
    /// an MPEG tag with no INT meaning is `Unknown`, never SI.
    #[test]
    fn mpeg_range_is_not_decoded_as_si() {
        let raw = [
            0x09, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, // INT target_IP_address (mask only)
            0x0B, 0x00, // SI system_clock tag, but INT-unassigned
            0x25, 0x01, 0xAA, // MPEG-range tag, never SI here
        ];
        let items: Vec<_> = parse_int_loop(&raw).collect();
        assert!(matches!(
            items[0].as_ref().unwrap(),
            AnyIntDescriptor::TargetIpAddress(_)
        ));
        assert!(matches!(
            items[1].as_ref().unwrap(),
            AnyIntDescriptor::Unknown { tag: 0x0B, .. }
        ));
        assert!(matches!(
            items[2].as_ref().unwrap(),
            AnyIntDescriptor::Unknown { tag: 0x25, .. }
        ));
    }

    #[test]
    fn si_range_falls_back_to_si_dispatch() {
        // telephone_descriptor (0x57) is an SI descriptor allowed in the INT.
        let raw = [0x57, 0x03, 0x00, 0x00, 0x00];
        let item = parse_int_loop(&raw).next().unwrap().unwrap();
        match item {
            AnyIntDescriptor::Si(d) => assert_eq!(d.name(), "TELEPHONE"),
            other => panic!("expected Si, got {other:?}"),
        }
    }

    #[test]
    fn loop_wrapper_iter_raw_and_truncation() {
        let raw = [0x14, 0x01, 0x01];
        let l = IntDescriptorLoop::new(&raw);
        assert_eq!(l.raw(), &raw[..]);
        assert!(matches!(
            l.iter().next().unwrap().unwrap(),
            AnyIntDescriptor::IspAccessMode(_)
        ));
        // truncated body: one Err then fused.
        let bad = [0x14, 0x05, 0x01];
        let mut it = parse_int_loop(&bad);
        assert!(it.next().unwrap().is_err());
        assert!(it.next().is_none());
    }

    #[cfg(feature = "serde")]
    #[test]
    fn serde_surfaces_typed_and_parse_errors() {
        let ok = IntDescriptorLoop::new(&[0x14, 0x01, 0x01]);
        let json = serde_json::to_string(&ok).unwrap();
        assert!(
            json.contains("ispAccessMode") && json.contains("Dialup"),
            "{json}"
        );
        let bad = IntDescriptorLoop::new(&[0x14, 0xFF]);
        assert!(serde_json::to_string(&bad).unwrap().contains("parseError"));
    }
}