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