dvb-si 13.0.0

ETSI EN 300 468 DVB Service Information parser + builder. MPEG-2 PSI included.
Documentation
//! VVC Subpictures Descriptor — ETSI EN 300 468 §6.4.17 (tag_extension 0x23).
use super::*;
use alloc::vec::Vec;
use broadcast_common::len::{need_in, need_out};

impl<'a> ExtensionBodyDef<'a> for VvcSubpictures<'a> {
    const TAG_EXTENSION: u8 = 0x23;
    const NAME: &'static str = "VVC_SUBPICTURES";
}

/// One VVC subpicture entry (Table 162a inner loop).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct VvcSubpicture {
    /// component_tag(8).
    pub component_tag: u8,
    /// vvc_subpicture_id(8).
    pub vvc_subpicture_id: u8,
}

/// Spec default of the 5-bit `reserved_zero_future_use` before
/// `processing_mode`: 0 (EN 300 468 Table 162a, V1.18.1-V1.20.1;
/// `private/specs-verified/dvb/dvb-si/reserved-bits-edition-audit.md` §6).
pub const VVC_SUBPICTURES_RESERVED_ZERO_FUTURE_USE_DEFAULT: u8 = 0x00;
const RESERVED_ZERO_MASK: u8 = 0x1F;
const PROCESSING_MODE_MASK: u8 = 0x07;

/// vvc_subpictures body (Table 162a) — fully typed.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
#[non_exhaustive]
pub struct VvcSubpictures<'a> {
    /// default_service_mode(1) — byte 0 bit 7.
    pub default_service_mode: bool,
    /// Subpicture entries in wire order.
    pub subpictures: Vec<VvcSubpicture>,
    /// `reserved_zero_future_use` (5 bits, right-aligned) — byte after the
    /// subpicture loop, bits `[7:3]`. Preserved on parse;
    /// [`VvcSubpictures::new`] uses [`VVC_SUBPICTURES_RESERVED_ZERO_FUTURE_USE_DEFAULT`].
    pub reserved_zero_future_use: u8,
    /// processing_mode(3) — byte after the subpicture loop, bits `[2:0]`.
    pub processing_mode: u8,
    /// Length-delimited service_description text, present iff
    /// `service_description_present` (byte 0 bit 6) is set.
    pub service_description: Option<DvbText<'a>>,
}

impl<'a> VvcSubpictures<'a> {
    /// Build a body with the spec-default (zero) reserved bits.
    #[must_use]
    pub const fn new(
        default_service_mode: bool,
        subpictures: Vec<VvcSubpicture>,
        processing_mode: u8,
        service_description: Option<DvbText<'a>>,
    ) -> Self {
        Self {
            default_service_mode,
            subpictures,
            reserved_zero_future_use: VVC_SUBPICTURES_RESERVED_ZERO_FUTURE_USE_DEFAULT,
            processing_mode,
            service_description,
        }
    }
}

impl<'a> Parse<'a> for VvcSubpictures<'a> {
    type Error = crate::error::Error;
    fn parse(sel: &'a [u8]) -> Result<Self> {
        if sel.is_empty() {
            return Err(Error::BufferTooShort(
                broadcast_common::error::BufferTooShort {
                    need: 1,
                    have: sel.len(),
                    what: "vvc_subpictures body",
                },
            ));
        }
        let byte0 = sel[0];
        let default_service_mode = (byte0 & 0x80) != 0;
        let service_description_present = (byte0 & 0x40) != 0;
        let n = (byte0 & 0x3F) as usize;

        // Table 162a: 1 fixed byte + n*2 subpicture bytes + 1 processing_mode byte.
        let subpicture_bytes = n * 2;
        let min_len = 1 + subpicture_bytes + 1;
        need_in(sel, min_len, "vvc_subpictures body")?;

        let mut pos = 1;
        let mut subpictures = Vec::with_capacity(n);
        for _ in 0..n {
            let component_tag = sel[pos];
            let vvc_subpicture_id = sel[pos + 1];
            subpictures.push(VvcSubpicture {
                component_tag,
                vvc_subpicture_id,
            });
            pos += 2;
        }

        let processing_mode = sel[pos] & PROCESSING_MODE_MASK;
        let reserved_zero_future_use = (sel[pos] >> 3) & RESERVED_ZERO_MASK;
        pos += 1;

        let service_description = if service_description_present {
            need_in(sel, pos + 1, "vvc_subpictures body")?;
            let len = sel[pos] as usize;
            pos += 1;
            need_in(sel, pos + len, "vvc_subpictures body")?;
            let text = DvbText::new(&sel[pos..pos + len]);
            pos += len;
            Some(text)
        } else {
            None
        };

        // Table 162a is exact; reject trailing bytes.
        if pos != sel.len() {
            return Err(invalid("vvc_subpictures: trailing data"));
        }

        Ok(VvcSubpictures {
            default_service_mode,
            subpictures,
            reserved_zero_future_use,
            processing_mode,
            service_description,
        })
    }
}

impl Serialize for VvcSubpictures<'_> {
    type Error = crate::error::Error;
    fn serialized_len(&self) -> usize {
        1 + self.subpictures.len() * 2
            + 1 // processing_mode
            + self
                .service_description
                .as_ref()
                .map_or(0, |t| 1 + t.len())
    }
    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
        let len = self.serialized_len();
        need_out(buf, len)?;
        // byte 0: default_service_mode(1) | service_description_present(1) | number_of_vvc_subpictures(6)
        let service_description_present = self.service_description.is_some();
        let number_of_vvc_subpictures = broadcast_common::len::fit_bits(
            self.subpictures.len() as u64,
            6,
            "number_of_vvc_subpictures",
        )? as u8;
        buf[0] = (u8::from(self.default_service_mode) << 7)
            | (u8::from(service_description_present) << 6)
            | number_of_vvc_subpictures;
        let mut p = 1;
        for sp in &self.subpictures {
            buf[p] = sp.component_tag;
            buf[p + 1] = sp.vvc_subpicture_id;
            p += 2;
        }
        buf[p] = (broadcast_common::len::fit_mask(
            self.reserved_zero_future_use,
            RESERVED_ZERO_MASK,
            "reserved_zero_future_use",
        )? << 3)
            | broadcast_common::len::fit_mask(
                self.processing_mode,
                PROCESSING_MODE_MASK,
                "processing_mode",
            )?;
        p += 1;
        if let Some(text) = &self.service_description {
            buf[p] = broadcast_common::len::fit_u8(text.len(), "service_description_length")?;
            p += 1;
            buf[p..p + text.len()].copy_from_slice(text.raw());
        }
        Ok(len)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(feature = "serde")]
    use crate::descriptors::extension::ExtensionTag;
    use crate::descriptors::extension::test_support::*;
    use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor};
    #[cfg(feature = "serde")]
    use crate::text::DvbText;

    #[cfg(feature = "serde")]
    #[test]
    fn parse_vvc_subpictures_with_description_round_trip() {
        // Table 162a: default_service_mode=true, 2 subpictures,
        // processing_mode=5, service_description = "Hi" (2 bytes).
        // byte0: ds=1(0x80) | sdp=1(0x40) | n=2(0x02) = 0xC2
        // subpicture 0: component_tag=0x10, vvc_subpicture_id=0x01
        // subpicture 1: component_tag=0x11, vvc_subpicture_id=0x02
        // processing_mode byte: 0x05
        // service_description_length: 2, then b'H', b'i'
        let sel = [0xC2, 0x10, 0x01, 0x11, 0x02, 0x05, 0x02, b'H', b'i'];
        let bytes = wrap(0x23, &sel);
        let d = ExtensionDescriptor::parse(&bytes).unwrap();
        assert_eq!(d.kind(), Some(ExtensionTag::VvcSubpictures));
        match &d.body {
            ExtensionBody::VvcSubpictures(b) => {
                assert!(b.default_service_mode);
                assert_eq!(b.subpictures.len(), 2);
                assert_eq!(b.subpictures[0].component_tag, 0x10);
                assert_eq!(b.subpictures[0].vvc_subpicture_id, 0x01);
                assert_eq!(b.subpictures[1].component_tag, 0x11);
                assert_eq!(b.subpictures[1].vvc_subpicture_id, 0x02);
                assert_eq!(b.processing_mode, 5);
                assert!(b.service_description.is_some());
                let desc = b.service_description.as_ref().unwrap();
                assert_eq!(desc.raw(), b"Hi");
                assert_eq!(desc.decode(), "Hi");
            }
            other => panic!("expected VvcSubpictures, got {other:?}"),
        }
        round_trip(&d);
    }

    #[test]
    fn parse_vvc_subpictures_no_description_round_trip() {
        // default_service_mode=false, 1 subpicture, processing_mode=1,
        // no service_description.
        // byte0: ds=0 | sdp=0 | n=1 = 0x01
        let sel = [0x01, 0x20, 0x03, 0x01];
        let bytes = wrap(0x23, &sel);
        let d = ExtensionDescriptor::parse(&bytes).unwrap();
        match &d.body {
            ExtensionBody::VvcSubpictures(b) => {
                assert!(!b.default_service_mode);
                assert_eq!(b.subpictures.len(), 1);
                assert_eq!(b.subpictures[0].component_tag, 0x20);
                assert_eq!(b.subpictures[0].vvc_subpicture_id, 0x03);
                assert_eq!(b.processing_mode, 1);
                assert!(b.service_description.is_none());
            }
            other => panic!("expected VvcSubpictures, got {other:?}"),
        }
        round_trip(&d);
    }

    #[test]
    fn parse_vvc_subpictures_rejects_truncated() {
        // Only 2 selector bytes, need at least 1 + 0*2 + 1 = 2 bytes.
        let sel = [0x00]; // n=0, but no processing_mode byte
        let bytes = wrap(0x23, &sel);
        assert!(matches!(
            ExtensionDescriptor::parse(&bytes).unwrap_err(),
            crate::error::Error::BufferTooShort(..)
        ));
    }

    #[test]
    fn parse_vvc_subpictures_rejects_overrun() {
        // service_description_present but no length byte
        let sel = [0xC0]; // ds=1, sdp=1, n=0 — missing processing_mode + desc
        let bytes = wrap(0x23, &sel);
        assert!(matches!(
            ExtensionDescriptor::parse(&bytes).unwrap_err(),
            crate::error::Error::BufferTooShort(..)
        ));
    }

    #[test]
    fn parse_vvc_subpictures_serialize_round_trip_no_subpictures() {
        // Zero subpictures, no description — minimal valid payload.
        // byte0: ds=1(0x80) | sdp=0 | n=0 = 0x80
        let sel = [0x80, 0x03];
        let bytes = wrap(0x23, &sel);
        let d = ExtensionDescriptor::parse(&bytes).unwrap();
        match &d.body {
            ExtensionBody::VvcSubpictures(b) => {
                assert!(b.default_service_mode);
                assert!(b.subpictures.is_empty());
                assert_eq!(b.processing_mode, 3);
                assert!(b.service_description.is_none());
            }
            other => panic!("expected VvcSubpictures, got {other:?}"),
        }
        round_trip(&d);
    }

    /// Every value of the 5-bit reserved_zero_future_use region, with every
    /// processing_mode, re-serializes byte-identically.
    #[test]
    fn vvc_subpictures_reserved_preserved() {
        for r in 0u8..=RESERVED_ZERO_MASK {
            for pm in 0u8..=PROCESSING_MODE_MASK {
                let sel = [0x01, 0x20, 0x03, (r << 3) | pm];
                let bytes = wrap(0x23, &sel);
                let d = ExtensionDescriptor::parse(&bytes).unwrap();
                let mut buf = vec![0u8; d.serialized_len()];
                d.serialize_into(&mut buf).unwrap();
                assert_eq!(buf, bytes, "r={r:#x} pm={pm}");
            }
        }
    }

    #[test]
    fn vvc_subpictures_default_is_spec_value() {
        let d = VvcSubpictures::new(false, vec![], 5, None);
        let mut buf = vec![0u8; d.serialized_len()];
        d.serialize_into(&mut buf).unwrap();
        assert_eq!(buf, [0x00, 0x05]);
    }

    /// Serialization is deterministic for an all-owned typed body.
    #[cfg(feature = "serde")]
    #[test]
    fn serde_serialize_vvc_subpictures() {
        let d = ExtensionDescriptor {
            tag_extension: 0x23,
            body: ExtensionBody::VvcSubpictures(VvcSubpictures::new(
                true,
                vec![VvcSubpicture {
                    component_tag: 0x10,
                    vvc_subpicture_id: 0x01,
                }],
                5,
                Some(DvbText::new(b"Hi")),
            )),
        };
        let json = serde_json::to_string(&d).unwrap();
        assert!(json.contains("\"tag_extension\":35"));
        assert!(json.contains("\"vvcSubpictures\""));
        assert!(json.contains("\"service_description\":\"Hi\""));
    }

    /// `number_of_vvc_subpictures` is a 6-bit field (max 63, #1129): 63
    /// entries still serialize; 64 must be rejected instead of silently
    /// wrapping the count.
    #[test]
    fn number_of_vvc_subpictures_boundary() {
        let make = |n: usize| {
            VvcSubpictures::new(
                false,
                (0..n)
                    .map(|_| VvcSubpicture {
                        component_tag: 0,
                        vvc_subpicture_id: 0,
                    })
                    .collect(),
                0,
                None,
            )
        };

        let ok = make(63);
        let mut buf = vec![0u8; ok.serialized_len()];
        ok.serialize_into(&mut buf).unwrap();
        assert_eq!(buf[0] & 0x3F, 63);

        let over = make(64);
        let mut buf = vec![0u8; over.serialized_len()];
        let err = over.serialize_into(&mut buf).unwrap_err();
        assert!(
            matches!(err, Error::FieldOverflow(_)),
            "expected FieldOverflow, got {err:?}"
        );
    }
}