Skip to main content

dvb_subtitle/segments/
disparity_signalling.rs

1//! Disparity Signalling Segment — ETSI EN 300 743 §7.2.7, Table 29 (segment_type 0x15).
2//!
3//! Supports plano-stereoscopic 3DTV subtitling by allowing disparity values
4//! to be ascribed to regions or subregions.
5
6use crate::error::{Error, Result};
7use broadcast_common::{Parse, Serialize};
8
9/// The disparity_signalling_segment segment_type.
10pub const SEGMENT_TYPE: u8 = 0x15;
11/// Header: 6 bytes.
12pub const HEADER_LEN: usize = 6;
13/// Fixed body: dss_version_number(4b)+page_flag(1b)+reserved(3b) + page_default_disparity_shift(1) = 2 bytes.
14pub const FIXED_LEN: usize = 2;
15
16/// A disparity shift update sequence as defined in Table 30.
17#[derive(Debug, Clone, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize))]
19pub struct DisparityShiftUpdateSequence {
20    /// Interval duration in 90kHz STC units (24-bit).
21    pub interval_duration: u32,
22    /// Interval count + disparity shift integer pairs. `division_period_count`
23    /// (Table 30) is exactly this list's length — "the number of ... time
24    /// intervals within the following ... 'for' loop" — so it is derived on
25    /// serialize rather than stored (#1108/DS-W3: a stored count could
26    /// diverge from the data it's supposed to describe).
27    pub intervals: alloc::vec::Vec<DisparityShiftInterval>,
28}
29
30/// An interval within a disparity shift update sequence.
31#[derive(Debug, Clone, PartialEq, Eq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize))]
33pub struct DisparityShiftInterval {
34    /// Interval count multiplier.
35    pub interval_count: u8,
36    /// Disparity shift integer part (signed).
37    pub disparity_shift_integer: i8,
38}
39
40impl DisparityShiftUpdateSequence {
41    fn serialized_len(&self) -> usize {
42        1 + 3 + 1 + self.intervals.len() * 2
43    }
44
45    fn serialize_into(&self, buf: &mut [u8]) -> Result<()> {
46        let body_len = 3 + 1 + self.intervals.len() * 2;
47        buf[0] = broadcast_common::len::fit_u8(body_len, "disparity_shift_update_sequence_length")?;
48        broadcast_common::len::fit_bits(
49            u64::from(self.interval_duration),
50            24,
51            "interval_duration",
52        )?;
53        buf[1] = (self.interval_duration >> 16) as u8;
54        buf[2] = (self.interval_duration >> 8) as u8;
55        buf[3] = self.interval_duration as u8;
56        buf[4] = broadcast_common::len::fit_u8(self.intervals.len(), "division_period_count")?;
57        for (i, interval) in self.intervals.iter().enumerate() {
58            let off = 5 + i * 2;
59            buf[off] = interval.interval_count;
60            buf[off + 1] = interval.disparity_shift_integer as u8;
61        }
62        Ok(())
63    }
64
65    fn parse(bytes: &[u8]) -> Result<(Self, usize)> {
66        if bytes.len() < 5 {
67            return Err(Error::BufferTooShort {
68                need: 5,
69                have: bytes.len(),
70                what: "disparity_shift_update_sequence",
71            });
72        }
73        let seq_len = bytes[0] as usize;
74        if bytes.len() < 1 + seq_len {
75            return Err(Error::BufferTooShort {
76                need: 1 + seq_len,
77                have: bytes.len(),
78                what: "disparity_shift_update_sequence data",
79            });
80        }
81        let dur = ((bytes[1] as u32) << 16) | ((bytes[2] as u32) << 8) | (bytes[3] as u32);
82        let count = bytes[4];
83        let total = 1 + seq_len;
84        let mut intervals = alloc::vec::Vec::new();
85        let mut ipos = 5;
86        for _ in 0..count {
87            if ipos + 2 > total {
88                return Err(Error::BufferTooShort {
89                    need: ipos + 2,
90                    have: total,
91                    what: "disparity_shift_update_sequence interval",
92                });
93            }
94            intervals.push(DisparityShiftInterval {
95                interval_count: bytes[ipos],
96                disparity_shift_integer: bytes[ipos + 1] as i8,
97            });
98            ipos += 2;
99        }
100        Ok((
101            DisparityShiftUpdateSequence {
102                interval_duration: dur,
103                intervals,
104            },
105            total,
106        ))
107    }
108}
109
110/// A subregion within a region for disparity signalling.
111#[derive(Debug, Clone, PartialEq, Eq)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize))]
113pub struct Subregion {
114    /// Horizontal position relative to page (only if multiple subregions).
115    pub subregion_horizontal_position: Option<u16>,
116    /// Width in pixels (only if multiple subregions).
117    pub subregion_width: Option<u16>,
118    /// Disparity shift integer part (signed).
119    pub subregion_disparity_shift_integer: i8,
120    /// Disparity shift fractional part (unsigned, in 1/16 pixel).
121    pub subregion_disparity_shift_fractional: u8,
122    /// Reserved bits in the subregion shift/frac byte (bits `[3:0]`).
123    pub reserved: u8,
124    /// Update sequence if flag is set.
125    pub update_sequence: Option<DisparityShiftUpdateSequence>,
126}
127
128impl Subregion {
129    fn serialized_len(&self) -> usize {
130        let base = if self.subregion_horizontal_position.is_some() {
131            4 + 2
132        } else {
133            2
134        };
135        base + self
136            .update_sequence
137            .as_ref()
138            .map_or(0, |u| u.serialized_len())
139    }
140
141    /// `has_pos` comes from the *region*, not this subregion (Table 29: the
142    /// position fields are present for every subregion in a region, or none
143    /// of them, per `number_of_subregions_minus_1`) — see DisparityRegion::
144    /// serialize_into. Errors if this subregion's stored `Option`s disagree
145    /// with that (#1108/DS-W3): writing this subregion's own state instead
146    /// could silently misframe the following subregions/regions.
147    fn serialize_into(&self, buf: &mut [u8], has_pos: bool) -> Result<()> {
148        let stored_has_pos =
149            self.subregion_horizontal_position.is_some() && self.subregion_width.is_some();
150        if stored_has_pos != has_pos {
151            return Err(Error::SubregionPositionMismatch);
152        }
153        let mut off = 0;
154        if let (Some(h), Some(w)) = (self.subregion_horizontal_position, self.subregion_width) {
155            buf[0..2].copy_from_slice(&h.to_be_bytes());
156            buf[2..4].copy_from_slice(&w.to_be_bytes());
157            off = 4;
158        }
159        buf[off] = self.subregion_disparity_shift_integer as u8;
160        buf[off + 1] = (broadcast_common::len::fit_mask(
161            self.subregion_disparity_shift_fractional,
162            0x0F,
163            "subregion_disparity_shift_fractional",
164        )? << 4)
165            | broadcast_common::len::fit_mask(self.reserved, 0x0F, "reserved")?;
166        off += 2;
167        if let Some(ref seq) = self.update_sequence {
168            seq.serialize_into(&mut buf[off..])?;
169        }
170        Ok(())
171    }
172}
173
174/// A region entry in the disparity signalling segment.
175#[derive(Debug, Clone, PartialEq, Eq)]
176#[cfg_attr(feature = "serde", derive(serde::Serialize))]
177pub struct DisparityRegion {
178    /// Region identifier.
179    pub region_id: u8,
180    /// Whether an update sequence follows for this region's subregions.
181    pub update_sequence_region_flag: bool,
182    /// Reserved bits in the region flags byte (bits `[6:2]`).
183    pub reserved_flags: u8,
184    /// Subregions within this region.
185    pub subregions: alloc::vec::Vec<Subregion>,
186}
187
188impl DisparityRegion {
189    fn serialized_len(&self) -> usize {
190        2 + self
191            .subregions
192            .iter()
193            .map(|s| s.serialized_len())
194            .sum::<usize>()
195    }
196
197    fn serialize_into(&self, buf: &mut [u8]) -> Result<()> {
198        let count = self.subregions.len();
199        // number_of_subregions_minus_1 is 2 bits (Table 29): only 1..=4
200        // subregions are representable. Pre-fix, `as u8` + `& 0x03` silently
201        // wrapped an out-of-range count instead of rejecting it (#1108/DS-W3).
202        if count == 0 || count > 4 {
203            return Err(Error::InvalidSubregionCount(count));
204        }
205        let num = (count - 1) as u8;
206        buf[0] = self.region_id;
207        buf[1] = (u8::from(self.update_sequence_region_flag) << 7)
208            | broadcast_common::len::fit_mask(self.reserved_flags, 0x7C, "reserved_flags")?
209            | (num & 0x03);
210        // Position fields are present for every subregion, or none, per
211        // whether the region itself has more than one (Table 29's
212        // `if (number_of_subregions_minus_1 > 0)` guards the whole `for` body).
213        let has_pos = count > 1;
214        let mut off = 2;
215        for sub in &self.subregions {
216            sub.serialize_into(&mut buf[off..], has_pos)?;
217            off += sub.serialized_len();
218        }
219        Ok(())
220    }
221}
222
223/// Disparity Signalling Segment.
224#[derive(Debug, Clone, PartialEq, Eq)]
225#[cfg_attr(feature = "serde", derive(serde::Serialize))]
226pub struct DisparitySignallingSegment {
227    /// The page_id from the segment header.
228    pub page_id: u16,
229    /// DSS version number (modulo 16).
230    pub dss_version_number: u8,
231    /// Whether a page-level update sequence follows.
232    pub update_sequence_page_flag: bool,
233    /// Reserved bits in body byte 0 (bits `[2:0]`).
234    pub reserved: u8,
235    /// Default disparity shift for the whole page (signed).
236    pub page_default_disparity_shift: i8,
237    /// Page-level update sequence if flag is set.
238    pub page_update_sequence: Option<DisparityShiftUpdateSequence>,
239    /// Region entries.
240    pub regions: alloc::vec::Vec<DisparityRegion>,
241}
242
243impl<'a> Parse<'a> for DisparitySignallingSegment {
244    type Error = Error;
245
246    fn parse(bytes: &'a [u8]) -> Result<Self> {
247        if bytes.len() < HEADER_LEN + FIXED_LEN {
248            return Err(Error::BufferTooShort {
249                need: HEADER_LEN + FIXED_LEN,
250                have: bytes.len(),
251                what: "disparity_signalling_segment",
252            });
253        }
254        if bytes[0] != crate::pes_data_field::SYNC_BYTE {
255            return Err(Error::BadSyncByte(bytes[0]));
256        }
257        if bytes[1] != SEGMENT_TYPE {
258            return Err(Error::UnknownSegmentType(bytes[1]));
259        }
260        let page_id = u16::from_be_bytes([bytes[2], bytes[3]]);
261        let segment_length = u16::from_be_bytes([bytes[4], bytes[5]]) as usize;
262        let total = HEADER_LEN + segment_length;
263        if bytes.len() < total {
264            return Err(Error::BufferTooShort {
265                need: total,
266                have: bytes.len(),
267                what: "disparity_signalling_segment data",
268            });
269        }
270        let body = &bytes[HEADER_LEN..HEADER_LEN + segment_length];
271        if body.len() < FIXED_LEN {
272            return Err(Error::BufferTooShort {
273                need: FIXED_LEN,
274                have: body.len(),
275                what: "disparity_signalling_segment body",
276            });
277        }
278        let dss_version_number = body[0] >> 4;
279        let update_sequence_page_flag = (body[0] & 0x08) != 0;
280        let reserved = body[0] & 0x07;
281        let page_default_disparity_shift = body[1] as i8;
282
283        let mut pos = FIXED_LEN;
284        let page_update_sequence = if update_sequence_page_flag {
285            let (seq, seq_len) = DisparityShiftUpdateSequence::parse(&body[pos..])?;
286            pos += seq_len;
287            Some(seq)
288        } else {
289            None
290        };
291
292        let mut regions = alloc::vec::Vec::new();
293        while pos < body.len() {
294            // Table 29's region loop is `while (processed_length <
295            // segment_length)` with no stuffing provision, so 1 leftover
296            // byte means the segment is malformed, not that it should be
297            // silently dropped (#1108/DS-W6: this used to just `break`).
298            if pos + 2 > body.len() {
299                return Err(Error::BufferTooShort {
300                    need: pos + 2,
301                    have: body.len(),
302                    what: "disparity_signalling_segment region header",
303                });
304            }
305            let region_id = body[pos];
306            pos += 1;
307            let update_flag = (body[pos] & 0x80) != 0;
308            let reserved_flags = body[pos] & 0x7C;
309            let num_subregions_minus_1 = body[pos] & 0x03;
310            pos += 1;
311
312            let mut subregions = alloc::vec::Vec::new();
313            for _n in 0..=num_subregions_minus_1 {
314                let has_pos = num_subregions_minus_1 > 0;
315                let entry_len = if has_pos { 4 + 2 } else { 2 };
316                if pos + entry_len > body.len() {
317                    return Err(Error::BufferTooShort {
318                        need: pos + entry_len,
319                        have: body.len(),
320                        what: "subregion entry",
321                    });
322                }
323
324                let (hpos, width) = if has_pos {
325                    let h = u16::from_be_bytes([body[pos], body[pos + 1]]);
326                    let w = u16::from_be_bytes([body[pos + 2], body[pos + 3]]);
327                    pos += 4;
328                    (Some(h), Some(w))
329                } else {
330                    (None, None)
331                };
332
333                let shift_int = body[pos] as i8;
334                let shift_frac = body[pos + 1] >> 4;
335                let reserved = body[pos + 1] & 0x0F;
336                pos += 2;
337
338                let sub_update = if update_flag {
339                    let (seq, seq_len) = DisparityShiftUpdateSequence::parse(&body[pos..])?;
340                    pos += seq_len;
341                    Some(seq)
342                } else {
343                    None
344                };
345
346                subregions.push(Subregion {
347                    subregion_horizontal_position: hpos,
348                    subregion_width: width,
349                    subregion_disparity_shift_integer: shift_int,
350                    subregion_disparity_shift_fractional: shift_frac,
351                    reserved,
352                    update_sequence: sub_update,
353                });
354            }
355
356            regions.push(DisparityRegion {
357                region_id,
358                update_sequence_region_flag: update_flag,
359                reserved_flags,
360                subregions,
361            });
362        }
363
364        Ok(DisparitySignallingSegment {
365            page_id,
366            dss_version_number,
367            update_sequence_page_flag,
368            reserved,
369            page_default_disparity_shift,
370            page_update_sequence,
371            regions,
372        })
373    }
374}
375
376impl Serialize for DisparitySignallingSegment {
377    type Error = Error;
378
379    fn serialized_len(&self) -> usize {
380        HEADER_LEN
381            + FIXED_LEN
382            + self
383                .page_update_sequence
384                .as_ref()
385                .map_or(0, |s| s.serialized_len())
386            + self
387                .regions
388                .iter()
389                .map(|r| r.serialized_len())
390                .sum::<usize>()
391    }
392
393    fn serialize_into(&self, buf: &mut [u8]) -> core::result::Result<usize, Self::Error> {
394        let len = self.serialized_len();
395        if buf.len() < len {
396            return Err(Error::BufferTooShort {
397                need: len,
398                have: buf.len(),
399                what: "disparity_signalling_segment serialize",
400            });
401        }
402        buf[0] = 0x0F;
403        buf[1] = SEGMENT_TYPE;
404        buf[2..4].copy_from_slice(&self.page_id.to_be_bytes());
405        let seg_len = crate::segments::check_segment_length(len - HEADER_LEN)?;
406        buf[4..6].copy_from_slice(&seg_len.to_be_bytes());
407
408        buf[6] =
409            (broadcast_common::len::fit_mask(self.dss_version_number, 0x0F, "dss_version_number")?
410                << 4)
411                | (u8::from(self.update_sequence_page_flag) << 3)
412                | broadcast_common::len::fit_mask(self.reserved, 0x07, "reserved")?;
413        buf[7] = self.page_default_disparity_shift as u8;
414
415        let mut off = HEADER_LEN + FIXED_LEN;
416        if let Some(ref seq) = self.page_update_sequence {
417            seq.serialize_into(&mut buf[off..])?;
418            off += seq.serialized_len();
419        }
420        for region in &self.regions {
421            region.serialize_into(&mut buf[off..])?;
422            off += region.serialized_len();
423        }
424        Ok(len)
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431    use broadcast_common::{Parse, Serialize};
432
433    #[test]
434    fn round_trip_simple() {
435        let bytes = [
436            0x0F, 0x15, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
437        ];
438        let seg = DisparitySignallingSegment::parse(&bytes).unwrap();
439        assert_eq!(seg.dss_version_number, 0);
440        assert!(!seg.update_sequence_page_flag);
441        assert_eq!(seg.regions.len(), 1);
442        assert_eq!(seg.regions[0].region_id, 1);
443        assert_eq!(seg.regions[0].subregions.len(), 1);
444        let out = seg.try_to_bytes().unwrap();
445        assert_eq!(out, bytes);
446
447        // Biting test
448        let mut seg2 = seg.clone();
449        seg2.regions[0].region_id = 5;
450        let out2 = seg2.try_to_bytes().unwrap();
451        assert_ne!(out2, bytes);
452        let reparse = DisparitySignallingSegment::parse(&out2).unwrap();
453        assert_eq!(reparse.regions[0].region_id, 5);
454    }
455
456    /// #1129: `interval_duration` is 24 bits; a wider value must error.
457    #[test]
458    fn interval_duration_over_24_bits_is_rejected() {
459        let mut seq = DisparityShiftUpdateSequence {
460            interval_duration: 0x0100_0000,
461            intervals: alloc::vec![],
462        };
463        let mut buf = [0u8; 16];
464        assert!(matches!(
465            seq.serialize_into(&mut buf),
466            Err(Error::FieldOverflow(_))
467        ));
468        seq.interval_duration = 0x00FF_FFFF;
469        assert!(seq.serialize_into(&mut buf).is_ok());
470        assert_eq!(&buf[1..4], &[0xFF, 0xFF, 0xFF]);
471    }
472
473    /// DS-W2 (#1108): sync_byte wasn't checked by any typed segment parser.
474    #[test]
475    fn rejects_bad_sync_byte() {
476        let bytes = [
477            0x00, 0x15, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
478        ];
479        assert!(matches!(
480            DisparitySignallingSegment::parse(&bytes),
481            Err(Error::BadSyncByte(0x00))
482        ));
483    }
484
485    /// DS-W6 (#1108): Table 29's region loop has no stuffing provision, so a
486    /// single leftover byte after the last complete region is now rejected,
487    /// not silently dropped via a bare `break`.
488    #[test]
489    fn rejects_trailing_partial_region() {
490        let bytes = [
491            0x0F, 0x15, 0x00, 0x01, 0x00, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF,
492        ];
493        assert!(matches!(
494            DisparitySignallingSegment::parse(&bytes),
495            Err(Error::BufferTooShort { .. })
496        ));
497    }
498
499    #[test]
500    fn round_trip_with_page_update_seq() {
501        let bytes = [
502            0x0F, 0x15, 0x00, 0x01, 0x00, 0x0B, 0x88, 0x05, 0x08, 0x00, 0x00, 0x0A, 0x02, 0x01,
503            0x10, 0x03, 0x20,
504        ];
505        let seg = DisparitySignallingSegment::parse(&bytes).unwrap();
506        assert_eq!(seg.dss_version_number, 8);
507        assert!(seg.update_sequence_page_flag);
508        assert_eq!(seg.regions.len(), 0);
509        let out = seg.try_to_bytes().unwrap();
510        assert_eq!(out, bytes);
511
512        // Biting test
513        let mut seg2 = seg.clone();
514        seg2.page_default_disparity_shift = 10;
515        let out2 = seg2.try_to_bytes().unwrap();
516        assert_ne!(out2, bytes);
517        let reparse = DisparitySignallingSegment::parse(&out2).unwrap();
518        assert_eq!(reparse.page_default_disparity_shift, 10);
519    }
520
521    #[test]
522    fn round_trip_with_update_sequence() {
523        let bytes = [
524            0x0F, 0x15, 0x00, 0x01, 0x00, 0x16, 0x88, 0x05, 0x08, 0x00, 0x00, 0x0A, 0x02, 0x01,
525            0x10, 0x03, 0x20, 0x01, 0x80, 0x10, 0x00, 0x06, 0x00, 0x00, 0x14, 0x01, 0x01, 0x08,
526        ];
527        let seg = DisparitySignallingSegment::parse(&bytes).unwrap();
528        assert_eq!(seg.dss_version_number, 8);
529        assert!(seg.update_sequence_page_flag);
530        assert_eq!(seg.page_default_disparity_shift, 5);
531        let seq = seg.page_update_sequence.as_ref().unwrap();
532        assert_eq!(seq.intervals.len(), 2);
533        assert_eq!(seg.regions.len(), 1);
534        assert_eq!(seg.regions[0].region_id, 1);
535        assert!(seg.regions[0].update_sequence_region_flag);
536        assert_eq!(seg.regions[0].subregions.len(), 1);
537        let out = seg.try_to_bytes().unwrap();
538        assert_eq!(out, bytes);
539
540        // Biting test
541        let mut seg2 = seg.clone();
542        seg2.dss_version_number = 3;
543        let out2 = seg2.try_to_bytes().unwrap();
544        assert_ne!(out2, bytes);
545        let reparse = DisparitySignallingSegment::parse(&out2).unwrap();
546        assert_eq!(reparse.dss_version_number, 3);
547    }
548
549    /// DS-W3 (#1108): `division_period_count` is now derived from
550    /// `intervals.len()`, so appending an interval must grow the on-wire
551    /// count byte and re-parse with the new interval intact.
552    #[test]
553    fn division_period_count_tracks_intervals_len() {
554        let bytes = [
555            0x0F, 0x15, 0x00, 0x01, 0x00, 0x0B, 0x88, 0x05, 0x08, 0x00, 0x00, 0x0A, 0x02, 0x01,
556            0x10, 0x03, 0x20,
557        ];
558        let seg = DisparitySignallingSegment::parse(&bytes).unwrap();
559        let mut seg2 = seg.clone();
560        seg2.page_update_sequence
561            .as_mut()
562            .unwrap()
563            .intervals
564            .push(DisparityShiftInterval {
565                interval_count: 5,
566                disparity_shift_integer: -1,
567            });
568        let out2 = seg2.try_to_bytes().unwrap();
569        assert_ne!(out2, bytes);
570        // division_period_count is body[4] of the update sequence, which
571        // starts right after the 8-byte fixed header (offset HEADER_LEN+FIXED_LEN).
572        let seq_off = HEADER_LEN + FIXED_LEN;
573        assert_eq!(
574            out2[seq_off + 4],
575            3,
576            "division_period_count must track intervals.len()"
577        );
578        let reparse = DisparitySignallingSegment::parse(&out2).unwrap();
579        assert_eq!(reparse.page_update_sequence.unwrap().intervals.len(), 3);
580    }
581
582    /// DS-W3 (#1108): `number_of_subregions_minus_1` is 2 bits (Table 29), so
583    /// 0 or more than 4 subregions must be rejected, not silently wrapped by
584    /// `as u8 & 0x03`.
585    #[test]
586    fn rejects_out_of_range_subregion_count() {
587        let region = DisparityRegion {
588            region_id: 1,
589            update_sequence_region_flag: false,
590            reserved_flags: 0,
591            subregions: alloc::vec::Vec::new(),
592        };
593        let mut buf = [0u8; 16];
594        assert!(matches!(
595            region.serialize_into(&mut buf),
596            Err(Error::InvalidSubregionCount(0))
597        ));
598
599        let sub = Subregion {
600            subregion_horizontal_position: None,
601            subregion_width: None,
602            subregion_disparity_shift_integer: 0,
603            subregion_disparity_shift_fractional: 0,
604            reserved: 0,
605            update_sequence: None,
606        };
607        let region5 = DisparityRegion {
608            region_id: 1,
609            update_sequence_region_flag: false,
610            reserved_flags: 0,
611            subregions: alloc::vec![sub; 5],
612        };
613        let mut buf = [0u8; 64];
614        assert!(matches!(
615            region5.serialize_into(&mut buf),
616            Err(Error::InvalidSubregionCount(5))
617        ));
618    }
619
620    /// DS-W3 (#1108): a region with 2+ subregions requires the position
621    /// fields present on ALL of them (Table 29); one subregion disagreeing
622    /// with the others must be rejected, not silently misframed.
623    #[test]
624    fn rejects_inconsistent_subregion_position() {
625        let with_pos = Subregion {
626            subregion_horizontal_position: Some(10),
627            subregion_width: Some(20),
628            subregion_disparity_shift_integer: 0,
629            subregion_disparity_shift_fractional: 0,
630            reserved: 0,
631            update_sequence: None,
632        };
633        let without_pos = Subregion {
634            subregion_horizontal_position: None,
635            subregion_width: None,
636            subregion_disparity_shift_integer: 0,
637            subregion_disparity_shift_fractional: 0,
638            reserved: 0,
639            update_sequence: None,
640        };
641        let region = DisparityRegion {
642            region_id: 1,
643            update_sequence_region_flag: false,
644            reserved_flags: 0,
645            subregions: alloc::vec![with_pos, without_pos],
646        };
647        let mut buf = [0u8; 32];
648        assert!(matches!(
649            region.serialize_into(&mut buf),
650            Err(Error::SubregionPositionMismatch)
651        ));
652    }
653}