Skip to main content

dvb_subtitle/segments/
region_composition.rs

1//! Region Composition Segment — ETSI EN 300 743 §7.2.3, Table 11 (segment_type 0x11).
2
3use crate::error::{Error, Result};
4use broadcast_common::len::{need_in, need_out};
5use broadcast_common::{Parse, Serialize};
6
7/// The region_composition_segment segment_type.
8pub const SEGMENT_TYPE: u8 = 0x11;
9/// Header: sync_byte(1) + segment_type(1) + page_id(2) + segment_length(2) = 6 bytes.
10pub const HEADER_LEN: usize = 6;
11/// Fixed body: 10 bytes.
12pub const FIXED_LEN: usize = 10;
13/// Each object entry base: object_id(2) + type(2b)+provider(2b)+hpos(12b)+reserved(4b)+vpos(12b) = 6 bytes.
14pub const OBJECT_ENTRY_BASE_LEN: usize = 6;
15/// Extra bytes for character/composite objects: foreground(1) + background(1) = 2 bytes.
16pub const OBJECT_EXTRA_LEN: usize = 2;
17/// Top 4 bits of the 12-bit object horizontal/vertical position, held in the
18/// low nibble of the third/fifth entry byte (EN 300 743 Table 11).
19const POS_HIGH_NIBBLE_MASK: u8 = 0x0F;
20
21/// Region level of compatibility as defined in Table 12.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize))]
24#[repr(u8)]
25#[non_exhaustive]
26pub enum RegionLevelOfCompatibility {
27    /// Reserved.
28    Reserved0 = 0x00,
29    /// 2-bit/entry CLUT required.
30    Clut2Bit = 0x01,
31    /// 4-bit/entry CLUT required.
32    Clut4Bit = 0x02,
33    /// 8-bit/entry CLUT required.
34    Clut8Bit = 0x03,
35    /// Reserved range.
36    Reserved(u8),
37}
38
39impl RegionLevelOfCompatibility {
40    /// Human-readable name for this compatibility level.
41    #[must_use]
42    pub fn name(&self) -> &'static str {
43        match self {
44            Self::Reserved0 => "reserved",
45            Self::Clut2Bit => "2-bit CLUT",
46            Self::Clut4Bit => "4-bit CLUT",
47            Self::Clut8Bit => "8-bit CLUT",
48            Self::Reserved(_) => "reserved",
49        }
50    }
51
52    fn to_bits(self) -> u8 {
53        match self {
54            Self::Reserved0 => 0x00,
55            Self::Clut2Bit => 0x01,
56            Self::Clut4Bit => 0x02,
57            Self::Clut8Bit => 0x03,
58            Self::Reserved(v) => v,
59        }
60    }
61}
62
63broadcast_common::impl_spec_display!(RegionLevelOfCompatibility, Reserved);
64
65/// Intended region pixel depth as defined in Table 13.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize))]
68#[repr(u8)]
69#[non_exhaustive]
70pub enum RegionDepth {
71    /// Reserved.
72    Reserved0 = 0x00,
73    /// 2-bit.
74    Depth2Bit = 0x01,
75    /// 4-bit.
76    Depth4Bit = 0x02,
77    /// 8-bit.
78    Depth8Bit = 0x03,
79    /// Reserved range.
80    Reserved(u8),
81}
82
83impl RegionDepth {
84    /// Human-readable name for this pixel depth.
85    #[must_use]
86    pub fn name(&self) -> &'static str {
87        match self {
88            Self::Reserved0 => "reserved",
89            Self::Depth2Bit => "2-bit",
90            Self::Depth4Bit => "4-bit",
91            Self::Depth8Bit => "8-bit",
92            Self::Reserved(_) => "reserved",
93        }
94    }
95
96    fn to_bits(self) -> u8 {
97        match self {
98            Self::Reserved0 => 0x00,
99            Self::Depth2Bit => 0x01,
100            Self::Depth4Bit => 0x02,
101            Self::Depth8Bit => 0x03,
102            Self::Reserved(v) => v,
103        }
104    }
105}
106
107broadcast_common::impl_spec_display!(RegionDepth, Reserved);
108
109/// Object type as defined in Table 14.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize))]
112#[repr(u8)]
113#[non_exhaustive]
114pub enum ObjectType {
115    /// Basic object, bitmap.
116    BasicBitmap = 0x00,
117    /// Basic object, character.
118    BasicCharacter = 0x01,
119    /// Composite object, string of characters.
120    CompositeString = 0x02,
121    /// Reserved.
122    Reserved(u8),
123}
124
125impl ObjectType {
126    /// Human-readable name for this object type.
127    #[must_use]
128    pub fn name(&self) -> &'static str {
129        match self {
130            Self::BasicBitmap => "basic_bitmap",
131            Self::BasicCharacter => "basic_character",
132            Self::CompositeString => "composite_string",
133            Self::Reserved(_) => "reserved",
134        }
135    }
136
137    fn to_bits(self) -> u8 {
138        match self {
139            Self::BasicBitmap => 0x00,
140            Self::BasicCharacter => 0x01,
141            Self::CompositeString => 0x02,
142            Self::Reserved(v) => v,
143        }
144    }
145}
146
147broadcast_common::impl_spec_display!(ObjectType, Reserved);
148
149/// Object provider flag as defined in Table 15.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize))]
152#[repr(u8)]
153#[non_exhaustive]
154pub enum ObjectProviderFlag {
155    /// Provided in the subtitling stream.
156    InStream = 0x00,
157    /// Provided by a ROM in the IRD.
158    InRom = 0x01,
159    /// Reserved.
160    Reserved(u8),
161}
162
163impl ObjectProviderFlag {
164    /// Human-readable name for this provision method.
165    #[must_use]
166    pub fn name(&self) -> &'static str {
167        match self {
168            Self::InStream => "in_stream",
169            Self::InRom => "in_rom",
170            Self::Reserved(_) => "reserved",
171        }
172    }
173
174    fn to_bits(self) -> u8 {
175        match self {
176            Self::InStream => 0x00,
177            Self::InRom => 0x01,
178            Self::Reserved(v) => v,
179        }
180    }
181}
182
183broadcast_common::impl_spec_display!(ObjectProviderFlag, Reserved);
184
185/// An object entry within a region composition segment.
186#[derive(Debug, Clone, PartialEq, Eq)]
187#[cfg_attr(feature = "serde", derive(serde::Serialize))]
188pub struct RegionObjectEntry {
189    /// Object identifier.
190    pub object_id: u16,
191    /// Object type.
192    pub object_type: ObjectType,
193    /// Object provider flag.
194    pub object_provider_flag: ObjectProviderFlag,
195    /// Horizontal position relative to region.
196    pub object_horizontal_position: u16,
197    /// Vertical position relative to region.
198    pub object_vertical_position: u16,
199    /// Foreground pixel code (only for character/composite objects).
200    pub foreground_pixel_code: Option<u8>,
201    /// Background pixel code (only for character/composite objects).
202    pub background_pixel_code: Option<u8>,
203}
204
205impl RegionObjectEntry {
206    fn serialized_len(&self) -> usize {
207        if self.foreground_pixel_code.is_some() {
208            OBJECT_ENTRY_BASE_LEN + OBJECT_EXTRA_LEN
209        } else {
210            OBJECT_ENTRY_BASE_LEN
211        }
212    }
213
214    fn serialize_into(&self, buf: &mut [u8]) -> Result<()> {
215        buf[0..2].copy_from_slice(&self.object_id.to_be_bytes());
216        let hpos = self.object_horizontal_position;
217        let vpos = self.object_vertical_position;
218        // EN 300 743 Table 11 (private/specs-verified/dvb/dvb-subtitle/tables/en_300_743/11-region-composition-segment.md:29-33):
219        // both object_horizontal_position and object_vertical_position are
220        // 12-bit uimsbf fields; range-check before narrowing so a value that
221        // does not fit is rejected instead of silently masked (#1129/#1044).
222        broadcast_common::len::fit_bits(u64::from(hpos), 12, "object_horizontal_position")?;
223        broadcast_common::len::fit_bits(u64::from(vpos), 12, "object_vertical_position")?;
224        buf[2] = (self.object_type.to_bits() << 6)
225            | (self.object_provider_flag.to_bits() << 4)
226            | ((hpos >> 8) as u8 & POS_HIGH_NIBBLE_MASK);
227        buf[3] = hpos as u8;
228        // reserved(4) then the top 4 bits of vpos, i.e. reserved is the HIGH
229        // nibble and vpos's top bits are the LOW nibble — the mirror of
230        // `parse`'s `(bytes[4] & 0x0F) << 8`.
231        buf[4] = (vpos >> 8) as u8 & POS_HIGH_NIBBLE_MASK;
232        buf[5] = vpos as u8;
233        if let (Some(fg), Some(bg)) = (self.foreground_pixel_code, self.background_pixel_code) {
234            buf[6] = fg;
235            buf[7] = bg;
236        }
237        Ok(())
238    }
239}
240
241/// Region Composition Segment.
242#[derive(Debug, Clone, PartialEq, Eq)]
243#[cfg_attr(feature = "serde", derive(serde::Serialize))]
244pub struct RegionCompositionSegment {
245    /// The page_id (from generic segment header).
246    pub page_id: u16,
247    /// Region identifier.
248    pub region_id: u8,
249    /// Region version number (modulo 16).
250    pub region_version_number: u8,
251    /// Fill flag.
252    pub region_fill_flag: bool,
253    /// Reserved bits in body byte 1 (bits `[2:0]`).
254    pub reserved_byte1: u8,
255    /// Region width in pixels.
256    pub region_width: u16,
257    /// Region height in pixels.
258    pub region_height: u16,
259    /// Minimum CLUT type required.
260    pub region_level_of_compatibility: RegionLevelOfCompatibility,
261    /// Intended pixel depth.
262    pub region_depth: RegionDepth,
263    /// Reserved bits in body byte 6 (bits `[1:0]`).
264    pub reserved_byte6: u8,
265    /// CLUT family identifier.
266    pub clut_id: u8,
267    /// Background colour for 8-bit CLUT.
268    pub region_8bit_pixel_code: u8,
269    /// Background colour for 4-bit CLUT.
270    pub region_4bit_pixel_code: u8,
271    /// Background colour for 2-bit CLUT.
272    pub region_2bit_pixel_code: u8,
273    /// Reserved bits in body byte 9 (bits `[1:0]`).
274    pub reserved_byte9: u8,
275    /// Object entries.
276    pub objects: alloc::vec::Vec<RegionObjectEntry>,
277}
278
279fn parse_object_entry(bytes: &[u8]) -> Result<(RegionObjectEntry, usize)> {
280    need_in(bytes, OBJECT_ENTRY_BASE_LEN, "region_object_entry")?;
281    let object_id = u16::from_be_bytes([bytes[0], bytes[1]]);
282    let obj_type_val = (bytes[2] >> 6) & 0x03;
283    let obj_provider_val = (bytes[2] >> 4) & 0x03;
284    let obj_hpos =
285        ((u16::from(bytes[2]) & u16::from(POS_HIGH_NIBBLE_MASK)) << 8) | u16::from(bytes[3]);
286    let reserved = (bytes[4] >> 4) & POS_HIGH_NIBBLE_MASK;
287    // reserved bits tolerated per §7.2.0.2 forward compatibility
288    let _ = reserved;
289    let obj_vpos =
290        ((u16::from(bytes[4]) & u16::from(POS_HIGH_NIBBLE_MASK)) << 8) | u16::from(bytes[5]);
291
292    let obj_type = match obj_type_val {
293        0x00 => ObjectType::BasicBitmap,
294        0x01 => ObjectType::BasicCharacter,
295        0x02 => ObjectType::CompositeString,
296        v => ObjectType::Reserved(v),
297    };
298    let obj_provider = match obj_provider_val {
299        0x00 => ObjectProviderFlag::InStream,
300        0x01 => ObjectProviderFlag::InRom,
301        v => ObjectProviderFlag::Reserved(v),
302    };
303
304    let has_extra = obj_type_val == 0x01 || obj_type_val == 0x02;
305    if has_extra {
306        need_in(
307            bytes,
308            OBJECT_ENTRY_BASE_LEN + OBJECT_EXTRA_LEN,
309            "region_object_entry extra",
310        )?;
311        Ok((
312            RegionObjectEntry {
313                object_id,
314                object_type: obj_type,
315                object_provider_flag: obj_provider,
316                object_horizontal_position: obj_hpos,
317                object_vertical_position: obj_vpos,
318                foreground_pixel_code: Some(bytes[6]),
319                background_pixel_code: Some(bytes[7]),
320            },
321            OBJECT_ENTRY_BASE_LEN + OBJECT_EXTRA_LEN,
322        ))
323    } else {
324        Ok((
325            RegionObjectEntry {
326                object_id,
327                object_type: obj_type,
328                object_provider_flag: obj_provider,
329                object_horizontal_position: obj_hpos,
330                object_vertical_position: obj_vpos,
331                foreground_pixel_code: None,
332                background_pixel_code: None,
333            },
334            OBJECT_ENTRY_BASE_LEN,
335        ))
336    }
337}
338
339impl<'a> Parse<'a> for RegionCompositionSegment {
340    type Error = Error;
341
342    fn parse(bytes: &'a [u8]) -> Result<Self> {
343        need_in(bytes, HEADER_LEN + FIXED_LEN, "region_composition_segment")?;
344        if bytes[0] != crate::pes_data_field::SYNC_BYTE {
345            return Err(Error::InvalidSync(broadcast_common::error::InvalidSync {
346                what: "sync_byte",
347                expected: u32::from(crate::pes_data_field::SYNC_BYTE),
348                found: u32::from(bytes[0]),
349            }));
350        }
351        if bytes[1] != SEGMENT_TYPE {
352            return Err(Error::UnknownSegmentType(bytes[1]));
353        }
354        let page_id = u16::from_be_bytes([bytes[2], bytes[3]]);
355        let segment_length = u16::from_be_bytes([bytes[4], bytes[5]]) as usize;
356        let total = HEADER_LEN + segment_length;
357        need_in(bytes, total, "region_composition_segment data")?;
358        let body = &bytes[HEADER_LEN..HEADER_LEN + segment_length];
359        need_in(body, FIXED_LEN, "region_composition_segment body")?;
360        let region_id = body[0];
361        let region_version_number = body[1] >> 4;
362        let region_fill_flag = (body[1] & 0x08) != 0;
363        let reserved_byte1 = body[1] & 0x07;
364        let region_width = u16::from_be_bytes([body[2], body[3]]);
365        let region_height = u16::from_be_bytes([body[4], body[5]]);
366        let rloc_val = (body[6] >> 5) & 0x07;
367        let region_level_of_compatibility = match rloc_val {
368            0x00 => RegionLevelOfCompatibility::Reserved0,
369            0x01 => RegionLevelOfCompatibility::Clut2Bit,
370            0x02 => RegionLevelOfCompatibility::Clut4Bit,
371            0x03 => RegionLevelOfCompatibility::Clut8Bit,
372            v => RegionLevelOfCompatibility::Reserved(v),
373        };
374        let rd_val = (body[6] >> 2) & 0x07;
375        let region_depth = match rd_val {
376            0x00 => RegionDepth::Reserved0,
377            0x01 => RegionDepth::Depth2Bit,
378            0x02 => RegionDepth::Depth4Bit,
379            0x03 => RegionDepth::Depth8Bit,
380            v => RegionDepth::Reserved(v),
381        };
382        let reserved_byte6 = body[6] & 0x03;
383        let clut_id = body[7];
384        let region_8bit_pixel_code = body[8];
385        let region_4bit_pixel_code = body[9] >> 4;
386        let region_2bit_pixel_code = (body[9] >> 2) & 0x03;
387        let reserved_byte9 = body[9] & 0x03;
388
389        let obj_data = &body[FIXED_LEN..];
390        let mut objects = alloc::vec::Vec::new();
391        let mut pos: usize = 0;
392        while pos < obj_data.len() {
393            let (entry, entry_len) = parse_object_entry(&obj_data[pos..])?;
394            objects.push(entry);
395            pos += entry_len;
396        }
397
398        Ok(RegionCompositionSegment {
399            page_id,
400            region_id,
401            region_version_number,
402            region_fill_flag,
403            reserved_byte1,
404            region_width,
405            region_height,
406            region_level_of_compatibility,
407            region_depth,
408            reserved_byte6,
409            clut_id,
410            region_8bit_pixel_code,
411            region_4bit_pixel_code,
412            region_2bit_pixel_code,
413            reserved_byte9,
414            objects,
415        })
416    }
417}
418
419impl Serialize for RegionCompositionSegment {
420    type Error = Error;
421
422    fn serialized_len(&self) -> usize {
423        HEADER_LEN
424            + FIXED_LEN
425            + self
426                .objects
427                .iter()
428                .map(|o| o.serialized_len())
429                .sum::<usize>()
430    }
431
432    fn serialize_into(&self, buf: &mut [u8]) -> core::result::Result<usize, Self::Error> {
433        let len = self.serialized_len();
434        need_out(buf, len)?;
435        buf[0] = 0x0F;
436        buf[1] = SEGMENT_TYPE;
437        buf[2..4].copy_from_slice(&self.page_id.to_be_bytes());
438        let seg_len = crate::segments::check_segment_length(len - HEADER_LEN)?;
439        buf[4..6].copy_from_slice(&seg_len.to_be_bytes());
440
441        buf[6] = self.region_id;
442        buf[7] = (broadcast_common::len::fit_mask(
443            self.region_version_number,
444            0x0F,
445            "region_version_number",
446        )? << 4)
447            | (u8::from(self.region_fill_flag) << 3)
448            | broadcast_common::len::fit_mask(self.reserved_byte1, 0x07, "reserved_byte1")?;
449        buf[8..10].copy_from_slice(&self.region_width.to_be_bytes());
450        buf[10..12].copy_from_slice(&self.region_height.to_be_bytes());
451        buf[12] = (broadcast_common::len::fit_mask(
452            self.region_level_of_compatibility.to_bits(),
453            0x07,
454            "region_level_of_compatibility",
455        )? << 5)
456            | (broadcast_common::len::fit_mask(self.region_depth.to_bits(), 0x07, "region_depth")?
457                << 2)
458            | broadcast_common::len::fit_mask(self.reserved_byte6, 0x03, "reserved_byte6")?;
459        buf[13] = self.clut_id;
460        buf[14] = self.region_8bit_pixel_code;
461        buf[15] = (broadcast_common::len::fit_mask(
462            self.region_4bit_pixel_code,
463            0x0F,
464            "region_4bit_pixel_code",
465        )? << 4)
466            | (broadcast_common::len::fit_mask(
467                self.region_2bit_pixel_code,
468                0x03,
469                "region_2bit_pixel_code",
470            )? << 2)
471            | broadcast_common::len::fit_mask(self.reserved_byte9, 0x03, "reserved_byte9")?;
472
473        let mut off = HEADER_LEN + FIXED_LEN;
474        for obj in &self.objects {
475            obj.serialize_into(&mut buf[off..])?;
476            off += obj.serialized_len();
477        }
478        Ok(len)
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use broadcast_common::{Parse, Serialize};
486
487    #[test]
488    fn round_trip_bitmap_objects() {
489        let bytes = [
490            0x0F, 0x11, 0x00, 0x01, 0x00, 0x16, 0x01, 0x88, 0x02, 0xCF, 0x00, 0x8F, 0x18, 0x03,
491            0x00, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x02, 0x10, 0x14, 0x00,
492            0x14, // obj 2: hpos=20, vpos=20, vpos upper nibble=0
493        ];
494        let seg = RegionCompositionSegment::parse(&bytes).unwrap();
495        assert_eq!(seg.region_id, 1);
496        assert!(seg.region_fill_flag);
497        assert_eq!(seg.region_width, 719);
498        assert_eq!(seg.objects.len(), 2);
499        assert_eq!(seg.objects[0].object_id, 1);
500        assert_eq!(seg.objects[0].object_type, ObjectType::BasicBitmap);
501        assert_eq!(seg.objects[0].object_horizontal_position, 10);
502        assert_eq!(seg.objects[1].object_id, 2);
503        assert_eq!(
504            seg.objects[1].object_provider_flag,
505            ObjectProviderFlag::InRom
506        );
507        let out = seg.try_to_bytes().unwrap();
508        assert_eq!(out, bytes);
509
510        // Biting test: mutate a field and re-parse
511        let mut seg2 = seg.clone();
512        seg2.region_width = 1280;
513        let out2 = seg2.try_to_bytes().unwrap();
514        assert_ne!(out2, bytes);
515        let reparse = RegionCompositionSegment::parse(&out2).unwrap();
516        assert_eq!(reparse.region_width, 1280);
517    }
518
519    /// DS-W2 (#1108): sync_byte wasn't checked by any typed segment parser.
520    #[test]
521    fn rejects_bad_sync_byte() {
522        let bytes = [
523            0x00, 0x11, 0x00, 0x01, 0x00, 0x16, 0x01, 0x88, 0x02, 0xCF, 0x00, 0x8F, 0x18, 0x03,
524            0x00, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x02, 0x10, 0x14, 0x00, 0x14,
525        ];
526        assert!(matches!(
527            RegionCompositionSegment::parse(&bytes),
528            Err(Error::InvalidSync(broadcast_common::error::InvalidSync {
529                found: 0x00,
530                ..
531            }))
532        ));
533    }
534
535    #[test]
536    fn round_trip_character_object() {
537        let bytes = [
538            0x0F, 0x11, 0x00, 0x01, 0x00, 0x12, 0x01, 0x80, 0x02, 0xCF, 0x00, 0x8F, 0x18, 0x03,
539            0x00, 0x00, 0x00, 0x01, 0x40, 0x0A, 0x00, 0x1E, 0xAA, 0xBB,
540        ];
541        let seg = RegionCompositionSegment::parse(&bytes).unwrap();
542        assert_eq!(seg.objects.len(), 1);
543        assert_eq!(seg.objects[0].object_type, ObjectType::BasicCharacter);
544        assert_eq!(seg.objects[0].foreground_pixel_code, Some(0xAA));
545        assert_eq!(seg.objects[0].background_pixel_code, Some(0xBB));
546        let out = seg.try_to_bytes().unwrap();
547        assert_eq!(out, bytes);
548
549        // Biting test
550        let mut seg2 = seg.clone();
551        seg2.objects[0].foreground_pixel_code = Some(0xCC);
552        let out2 = seg2.try_to_bytes().unwrap();
553        assert_ne!(out2, bytes);
554        let reparse = RegionCompositionSegment::parse(&out2).unwrap();
555        assert_eq!(reparse.objects[0].foreground_pixel_code, Some(0xCC));
556    }
557
558    /// Issue #1044: `object_vertical_position >= 0x100` (>= 256) must
559    /// round-trip. EN 300 743 Table 11 (`private/specs-verified/dvb/dvb-subtitle/tables/en_300_743/11-region-composition-segment.md:32-33`):
560    /// byte 4 of the object entry is `reserved(4)` then the *top 4 bits* of
561    /// the 12-bit `object_vertical_position` — the same split `parse` already
562    /// uses (`(bytes[4] & 0x0F) << 8 | bytes[5]`). Every existing round-trip
563    /// fixture keeps `vpos < 256` (a single byte), so this is the first test
564    /// to exercise the top nibble.
565    #[test]
566    fn round_trip_object_vertical_position_above_one_byte() {
567        let mut seg = RegionCompositionSegment::parse(&[
568            0x0F, 0x11, 0x00, 0x01, 0x00, 0x16, 0x01, 0x88, 0x02, 0xCF, 0x00, 0x8F, 0x18, 0x03,
569            0x00, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x02, 0x10, 0x14, 0x00, 0x14,
570        ])
571        .unwrap();
572        // 900 (0x384): the bottom-of-a-1080-line-frame subtitle position from
573        // the audit's failure scenario.
574        seg.objects[1].object_vertical_position = 0x384;
575
576        let out = seg.try_to_bytes().unwrap();
577        // object index 1 is the last (and, per the fixture, fg/bg-less 6-byte)
578        // entry in the loop, so its `object_vertical_position` bytes are the
579        // final 2 bytes of the whole serialized segment. They must be
580        // `reserved(0) | vpos[11:8]` = 0x03 (not 0x30 — that would put the
581        // position's top nibble into the reserved high nibble, the exact
582        // bug) then `vpos[7:0]` = 0x84.
583        let n = out.len();
584        assert_eq!(
585            out[n - 2],
586            0x03,
587            "vpos top nibble must land in the low nibble of byte 4, not the reserved high nibble"
588        );
589        assert_eq!(out[n - 1], 0x84);
590
591        let reparsed = RegionCompositionSegment::parse(&out).unwrap();
592        assert_eq!(
593            reparsed.objects[1].object_vertical_position, 0x384,
594            "byte-exact round-trip of a vertical position >= 256"
595        );
596    }
597
598    /// Issue #1044/#1129: `object_vertical_position`/`object_horizontal_position`
599    /// are 12-bit fields (EN 300 743 Table 11); a value over `0x0FFF` must be
600    /// rejected, not silently masked to a wrong (wrapped) position.
601    #[test]
602    fn oversized_object_position_is_rejected_not_masked() {
603        let mut seg = RegionCompositionSegment::parse(&[
604            0x0F, 0x11, 0x00, 0x01, 0x00, 0x16, 0x01, 0x88, 0x02, 0xCF, 0x00, 0x8F, 0x18, 0x03,
605            0x00, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x02, 0x10, 0x14, 0x00, 0x14,
606        ])
607        .unwrap();
608        let mut buf = alloc::vec![0u8; 64];
609
610        seg.objects[1].object_vertical_position = 0x1000; // 13 bits, one over max
611        assert!(matches!(
612            seg.serialize_into(&mut buf),
613            Err(Error::FieldOverflow(_))
614        ));
615
616        seg.objects[1].object_vertical_position = 0x0FFF; // the 12-bit maximum
617        seg.objects[1].object_horizontal_position = 0x1000;
618        assert!(matches!(
619            seg.serialize_into(&mut buf),
620            Err(Error::FieldOverflow(_))
621        ));
622
623        seg.objects[1].object_horizontal_position = 0x0FFF; // both at max: OK
624        assert!(seg.serialize_into(&mut buf).is_ok());
625    }
626}