Skip to main content

dvb_subtitle/segments/
page_composition.rs

1//! Page Composition Segment — ETSI EN 300 743 §7.2.2, Table 9 (segment_type 0x10).
2
3use crate::error::{Error, Result};
4use broadcast_common::len::{need_in, need_out};
5use broadcast_common::{Parse, Serialize};
6
7/// The page_composition_segment segment_type.
8pub const SEGMENT_TYPE: u8 = 0x10;
9/// Header: sync_byte(1) + segment_type(1) + page_id(2) + segment_length(2) = 6 bytes.
10pub const HEADER_LEN: usize = 6;
11/// Fixed after header: page_time_out(1) + page_version(4b)+page_state(2b)+reserved(2b) = 2 bytes.
12pub const FIXED_LEN: usize = 2;
13/// Each region entry: region_id(1) + reserved(1) + region_horizontal_address(2) + region_vertical_address(2) = 6 bytes.
14pub const REGION_ENTRY_LEN: usize = 6;
15
16/// Page state as defined in Table 10.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize))]
19#[repr(u8)]
20#[non_exhaustive]
21pub enum PageState {
22    /// Normal case — page update.
23    Normal = 0x00,
24    /// Acquisition point — page refresh.
25    Acquisition = 0x01,
26    /// Mode change — new page.
27    ModeChange = 0x02,
28    /// Reserved.
29    Reserved(u8),
30}
31
32impl PageState {
33    /// Human-readable name.
34    #[must_use]
35    pub fn name(&self) -> &'static str {
36        match self {
37            Self::Normal => "normal",
38            Self::Acquisition => "acquisition",
39            Self::ModeChange => "mode_change",
40            Self::Reserved(_) => "reserved",
41        }
42    }
43
44    fn to_bits(self) -> u8 {
45        match self {
46            Self::Normal => 0x00,
47            Self::Acquisition => 0x01,
48            Self::ModeChange => 0x02,
49            Self::Reserved(v) => v,
50        }
51    }
52}
53
54broadcast_common::impl_spec_display!(PageState, Reserved);
55
56/// A single region entry within a page composition segment.
57#[derive(Debug, Clone, PartialEq, Eq)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize))]
59pub struct PageRegionEntry {
60    /// Region identifier.
61    pub region_id: u8,
62    /// Reserved byte in the region entry (must be preserved for round-trip).
63    pub reserved: u8,
64    /// Horizontal address of top-left pixel.
65    pub region_horizontal_address: u16,
66    /// Vertical address of top line.
67    pub region_vertical_address: u16,
68}
69
70impl PageRegionEntry {
71    #[allow(dead_code)]
72    fn serialized_len() -> usize {
73        REGION_ENTRY_LEN
74    }
75
76    fn serialize_into(&self, buf: &mut [u8]) {
77        buf[0] = self.region_id;
78        buf[1] = self.reserved;
79        buf[2..4].copy_from_slice(&self.region_horizontal_address.to_be_bytes());
80        buf[4..6].copy_from_slice(&self.region_vertical_address.to_be_bytes());
81    }
82
83    fn parse(bytes: &[u8]) -> Result<Self> {
84        need_in(bytes, REGION_ENTRY_LEN, "page_region_entry")?;
85        let reserved = bytes[1];
86        Ok(PageRegionEntry {
87            region_id: bytes[0],
88            reserved,
89            region_horizontal_address: u16::from_be_bytes([bytes[2], bytes[3]]),
90            region_vertical_address: u16::from_be_bytes([bytes[4], bytes[5]]),
91        })
92    }
93}
94
95/// Page Composition Segment.
96#[derive(Debug, Clone, PartialEq, Eq)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize))]
98pub struct PageCompositionSegment {
99    /// The page_id (from generic segment header).
100    pub page_id: u16,
101    /// Time-out period in seconds.
102    pub page_time_out: u8,
103    /// Version number (modulo 16).
104    pub page_version_number: u8,
105    /// Page state.
106    pub page_state: PageState,
107    /// Reserved bits in body byte 1 (bits `[1:0]`).
108    pub reserved: u8,
109    /// Region entries.
110    pub regions: alloc::vec::Vec<PageRegionEntry>,
111}
112
113impl<'a> Parse<'a> for PageCompositionSegment {
114    type Error = Error;
115
116    fn parse(bytes: &'a [u8]) -> Result<Self> {
117        need_in(bytes, HEADER_LEN + FIXED_LEN, "page_composition_segment")?;
118        if bytes[0] != crate::pes_data_field::SYNC_BYTE {
119            return Err(Error::InvalidSync(broadcast_common::error::InvalidSync {
120                what: "sync_byte",
121                expected: u32::from(crate::pes_data_field::SYNC_BYTE),
122                found: u32::from(bytes[0]),
123            }));
124        }
125        if bytes[1] != SEGMENT_TYPE {
126            return Err(Error::UnknownSegmentType(bytes[1]));
127        }
128        let page_id = u16::from_be_bytes([bytes[2], bytes[3]]);
129        let segment_length = u16::from_be_bytes([bytes[4], bytes[5]]) as usize;
130        let total = HEADER_LEN + segment_length;
131        need_in(bytes, total, "page_composition_segment data")?;
132        let body = &bytes[HEADER_LEN..HEADER_LEN + segment_length];
133        need_in(body, FIXED_LEN, "page_composition_segment body")?;
134        let page_time_out = body[0];
135        let page_version_number = body[1] >> 4;
136        let page_state_bits = (body[1] >> 2) & 0x03;
137        let reserved = body[1] & 0x03;
138        let page_state = match page_state_bits {
139            0x00 => PageState::Normal,
140            0x01 => PageState::Acquisition,
141            0x02 => PageState::ModeChange,
142            v => PageState::Reserved(v),
143        };
144
145        let region_data = &body[FIXED_LEN..];
146        let region_count = region_data.len() / REGION_ENTRY_LEN;
147        // Table 9's region loop is `while (processed_length < segment_length)`
148        // over whole 6-byte entries — there is no stuffing/suffix provision,
149        // so a remainder means the segment is malformed, not that trailing
150        // bytes should be captured (#1108/DS-W6: this used to be checked
151        // here and then ALSO stored in a `suffix` field that, because of
152        // this very check, could never end up holding anything).
153        if !region_data.len().is_multiple_of(REGION_ENTRY_LEN) {
154            return Err(Error::BufferTooShort(
155                broadcast_common::error::BufferTooShort {
156                    need: (region_count + 1) * REGION_ENTRY_LEN,
157                    have: region_data.len(),
158                    what: "page_composition_segment regions",
159                },
160            ));
161        }
162        let mut regions = alloc::vec::Vec::with_capacity(region_count);
163        for i in 0..region_count {
164            let entry_bytes = &region_data[i * REGION_ENTRY_LEN..][..REGION_ENTRY_LEN];
165            regions.push(PageRegionEntry::parse(entry_bytes)?);
166        }
167
168        Ok(PageCompositionSegment {
169            page_id,
170            page_time_out,
171            page_version_number,
172            page_state,
173            reserved,
174            regions,
175        })
176    }
177}
178
179impl Serialize for PageCompositionSegment {
180    type Error = Error;
181
182    fn serialized_len(&self) -> usize {
183        HEADER_LEN + FIXED_LEN + self.regions.len() * REGION_ENTRY_LEN
184    }
185
186    fn serialize_into(&self, buf: &mut [u8]) -> core::result::Result<usize, Self::Error> {
187        let len = self.serialized_len();
188        need_out(buf, len)?;
189        buf[0] = 0x0F;
190        buf[1] = SEGMENT_TYPE;
191        buf[2..4].copy_from_slice(&self.page_id.to_be_bytes());
192        let seg_len = crate::segments::check_segment_length(len - HEADER_LEN)?;
193        buf[4..6].copy_from_slice(&seg_len.to_be_bytes());
194
195        buf[6] = self.page_time_out;
196        buf[7] = (broadcast_common::len::fit_mask(
197            self.page_version_number,
198            0x0F,
199            "page_version_number",
200        )? << 4)
201            | (broadcast_common::len::fit_mask(self.page_state.to_bits(), 0x03, "page_state")?
202                << 2)
203            | broadcast_common::len::fit_mask(self.reserved, 0x03, "reserved")?;
204
205        for (i, region) in self.regions.iter().enumerate() {
206            let off = HEADER_LEN + FIXED_LEN + i * REGION_ENTRY_LEN;
207            region.serialize_into(&mut buf[off..off + REGION_ENTRY_LEN]);
208        }
209        Ok(len)
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use broadcast_common::{Parse, Serialize};
217
218    #[test]
219    fn round_trip() {
220        let bytes = [
221            0x0F, 0x10, 0x00, 0x01, 0x00, 0x0E, 0x0A, 0x04, 0x01, 0x00, 0x00, 0x64, 0x00, 0x32,
222            0x02, 0x00, 0x00, 0xC8, 0x00, 0x96,
223        ];
224        let seg = PageCompositionSegment::parse(&bytes).unwrap();
225        assert_eq!(seg.page_time_out, 10);
226        assert_eq!(seg.page_version_number, 0);
227        assert_eq!(seg.page_state, PageState::Acquisition);
228        assert_eq!(seg.regions.len(), 2);
229        assert_eq!(seg.regions[0].region_id, 1);
230        assert_eq!(seg.regions[0].region_horizontal_address, 100);
231        assert_eq!(seg.regions[0].region_vertical_address, 50);
232        let out = seg.try_to_bytes().unwrap();
233        assert_eq!(out, bytes);
234
235        // Biting test: mutate page_time_out changes output
236        let mut seg2 = seg.clone();
237        seg2.page_time_out = 20;
238        let out2 = seg2.try_to_bytes().unwrap();
239        assert_ne!(out2, bytes);
240        let reparse = PageCompositionSegment::parse(&out2).unwrap();
241        assert_eq!(reparse.page_time_out, 20);
242    }
243
244    /// DS-W2 (#1108): sync_byte wasn't checked by any typed segment parser.
245    #[test]
246    fn rejects_bad_sync_byte() {
247        let bytes = [
248            0x00, 0x10, 0x00, 0x01, 0x00, 0x0E, 0x0A, 0x04, 0x01, 0x00, 0x00, 0x64, 0x00, 0x32,
249            0x02, 0x00, 0x00, 0xC8, 0x00, 0x96,
250        ];
251        assert!(matches!(
252            PageCompositionSegment::parse(&bytes),
253            Err(Error::InvalidSync(broadcast_common::error::InvalidSync {
254                found: 0x00,
255                ..
256            }))
257        ));
258    }
259}