Skip to main content

dvb_subtitle/segments/
clut_definition.rs

1//! CLUT Definition Segment โ€” ETSI EN 300 743 ยง7.2.4, Table 16 (segment_type 0x12).
2
3use crate::error::{Error, Result};
4use broadcast_common::{Parse, Serialize};
5
6/// The CLUT_definition_segment segment_type.
7pub const SEGMENT_TYPE: u8 = 0x12;
8/// Header: 6 bytes.
9pub const HEADER_LEN: usize = 6;
10/// Fixed body: CLUT_id(1) + CLUT_version_number(4b)+reserved(4b) = 2 bytes.
11pub const FIXED_LEN: usize = 2;
12/// Entry header: CLUT_entry_id(1) + flags(1) = 2 bytes.
13pub const ENTRY_HEADER_LEN: usize = 2;
14/// Full-range colour: 4 bytes.
15pub const ENTRY_FULL_LEN: usize = 4;
16/// Reduced-range colour: 2 bytes.
17pub const ENTRY_REDUCED_LEN: usize = 2;
18
19/// Bits `[4:1]` of the CLUT-entry flags byte are reserved (Table 16).
20const RESERVED_FLAGS_MASK: u8 = 0x1E;
21/// Largest reduced-range `Y_value` (6 bits, Table 16).
22const REDUCED_Y_MAX: u8 = 0x3F;
23/// Largest reduced-range `Cr_value` / `Cb_value` (4 bits, Table 16).
24const REDUCED_CR_CB_MAX: u8 = 0x0F;
25/// Largest reduced-range `T_value` (2 bits, Table 16).
26const REDUCED_T_MAX: u8 = 0x03;
27
28/// A single CLUT entry.
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize))]
31pub struct ClutEntry {
32    /// CLUT entry number.
33    pub clut_entry_id: u8,
34    /// Whether this loads into the 2-bit/entry CLUT.
35    pub flag_2bit: bool,
36    /// Whether this loads into the 4-bit/entry CLUT.
37    pub flag_4bit: bool,
38    /// Whether this loads into the 8-bit/entry CLUT.
39    pub flag_8bit: bool,
40    /// Reserved bits in the flags byte (bits `[4:1]`, must be preserved for round-trip).
41    pub reserved_flags: u8,
42    /// Whether full 8-bit resolution colour values follow.
43    pub full_range_flag: bool,
44    /// Y output value.
45    pub y_value: u8,
46    /// Cr output value.
47    pub cr_value: u8,
48    /// Cb output value.
49    pub cb_value: u8,
50    /// T (transparency) output value.
51    pub t_value: u8,
52}
53
54impl ClutEntry {
55    fn serialized_len(&self) -> usize {
56        ENTRY_HEADER_LEN
57            + if self.full_range_flag {
58                ENTRY_FULL_LEN
59            } else {
60                ENTRY_REDUCED_LEN
61            }
62    }
63
64    fn serialize_into(&self, buf: &mut [u8]) -> Result<()> {
65        // The flags byte's reserved bits are `[4:1]` only; a stray bit there
66        // would corrupt a flag (#1108/DS-O2 class).
67        if self.reserved_flags & !RESERVED_FLAGS_MASK != 0 {
68            return Err(Error::InvalidValue {
69                field: "CLUT_entry.reserved",
70                reason: "reserved bits must lie in bits [4:1] of the flags byte",
71            });
72        }
73        // Reduced-range entries pack Y(6) Cr(4) Cb(4) T(2) into two bytes
74        // (Table 16); a wider value would shift into its neighbour's bits
75        // and silently corrupt it (#1108/DS-O2).
76        if !self.full_range_flag
77            && (self.y_value > REDUCED_Y_MAX
78                || self.cr_value > REDUCED_CR_CB_MAX
79                || self.cb_value > REDUCED_CR_CB_MAX
80                || self.t_value > REDUCED_T_MAX)
81        {
82            return Err(Error::InvalidValue {
83                field: "CLUT_entry colour value",
84                reason: "reduced-range entry needs Y<=63, Cr<=15, Cb<=15, T<=3",
85            });
86        }
87        buf[0] = self.clut_entry_id;
88        let mut flags: u8 = self.reserved_flags;
89        if self.flag_2bit {
90            flags |= 0x80;
91        }
92        if self.flag_4bit {
93            flags |= 0x40;
94        }
95        if self.flag_8bit {
96            flags |= 0x20;
97        }
98        if self.full_range_flag {
99            flags |= 0x01;
100        }
101        buf[1] = flags;
102        if self.full_range_flag {
103            buf[2] = self.y_value;
104            buf[3] = self.cr_value;
105            buf[4] = self.cb_value;
106            buf[5] = self.t_value;
107        } else {
108            buf[2] = (self.y_value << 2) | (self.cr_value >> 2);
109            buf[3] = (self.cr_value << 6) | (self.cb_value << 2) | self.t_value;
110        }
111        Ok(())
112    }
113
114    fn parse(bytes: &[u8]) -> Result<(Self, usize)> {
115        if bytes.len() < ENTRY_HEADER_LEN {
116            return Err(Error::BufferTooShort {
117                need: ENTRY_HEADER_LEN,
118                have: bytes.len(),
119                what: "CLUT_entry header",
120            });
121        }
122        let clut_entry_id = bytes[0];
123        let flag_2bit = (bytes[1] & 0x80) != 0;
124        let flag_4bit = (bytes[1] & 0x40) != 0;
125        let flag_8bit = (bytes[1] & 0x20) != 0;
126        let reserved_flags = bytes[1] & RESERVED_FLAGS_MASK;
127        let full_range_flag = (bytes[1] & 0x01) != 0;
128
129        let total = ENTRY_HEADER_LEN
130            + if full_range_flag {
131                ENTRY_FULL_LEN
132            } else {
133                ENTRY_REDUCED_LEN
134            };
135        if bytes.len() < total {
136            return Err(Error::BufferTooShort {
137                need: total,
138                have: bytes.len(),
139                what: "CLUT_entry data",
140            });
141        }
142        let (y, cr, cb, t) = if full_range_flag {
143            (bytes[2], bytes[3], bytes[4], bytes[5])
144        } else {
145            let y = bytes[2] >> 2;
146            let cr = ((bytes[2] & 0x03) << 2) | (bytes[3] >> 6);
147            let cb = (bytes[3] >> 2) & 0x0F;
148            let t = bytes[3] & 0x03;
149            (y, cr, cb, t)
150        };
151        Ok((
152            ClutEntry {
153                clut_entry_id,
154                flag_2bit,
155                flag_4bit,
156                flag_8bit,
157                reserved_flags,
158                full_range_flag,
159                y_value: y,
160                cr_value: cr,
161                cb_value: cb,
162                t_value: t,
163            },
164            total,
165        ))
166    }
167}
168
169/// CLUT Definition Segment.
170#[derive(Debug, Clone, PartialEq, Eq)]
171#[cfg_attr(feature = "serde", derive(serde::Serialize))]
172pub struct ClutDefinitionSegment {
173    /// The page_id from the segment header.
174    pub page_id: u16,
175    /// CLUT family identifier.
176    pub clut_id: u8,
177    /// CLUT version number (modulo 16).
178    pub clut_version_number: u8,
179    /// Reserved bits in the body byte 1 (bits `[3:0]`).
180    pub reserved: u8,
181    /// CLUT entries.
182    pub entries: alloc::vec::Vec<ClutEntry>,
183    /// Trailing bytes after the last successfully-parsed entry (preserved for round-trip).
184    #[cfg_attr(feature = "serde", serde(skip))]
185    pub(crate) suffix: alloc::vec::Vec<u8>,
186}
187
188impl<'a> Parse<'a> for ClutDefinitionSegment {
189    type Error = Error;
190
191    fn parse(bytes: &'a [u8]) -> Result<Self> {
192        if bytes.len() < HEADER_LEN + FIXED_LEN {
193            return Err(Error::BufferTooShort {
194                need: HEADER_LEN + FIXED_LEN,
195                have: bytes.len(),
196                what: "CLUT_definition_segment",
197            });
198        }
199        if bytes[0] != crate::pes_data_field::SYNC_BYTE {
200            return Err(Error::BadSyncByte(bytes[0]));
201        }
202        if bytes[1] != SEGMENT_TYPE {
203            return Err(Error::UnknownSegmentType(bytes[1]));
204        }
205        let page_id = u16::from_be_bytes([bytes[2], bytes[3]]);
206        let segment_length = u16::from_be_bytes([bytes[4], bytes[5]]) as usize;
207        let total = HEADER_LEN + segment_length;
208        if bytes.len() < total {
209            return Err(Error::BufferTooShort {
210                need: total,
211                have: bytes.len(),
212                what: "CLUT_definition_segment data",
213            });
214        }
215        let body = &bytes[HEADER_LEN..HEADER_LEN + segment_length];
216        if body.len() < FIXED_LEN {
217            return Err(Error::BufferTooShort {
218                need: FIXED_LEN,
219                have: body.len(),
220                what: "CLUT_definition_segment body",
221            });
222        }
223        let clut_id = body[0];
224        let clut_version_number = body[1] >> 4;
225        let reserved = body[1] & 0x0F;
226
227        let rest = &body[FIXED_LEN..];
228        let mut entries = alloc::vec::Vec::new();
229        let mut pos: usize = 0;
230        let mut suffix = alloc::vec::Vec::new();
231        while pos < rest.len() {
232            match ClutEntry::parse(&rest[pos..]) {
233                Ok((entry, entry_len)) => {
234                    entries.push(entry);
235                    pos += entry_len;
236                }
237                Err(_) => {
238                    // Truncated or malformed entry โ€” preserve remainder as raw
239                    suffix.extend_from_slice(&rest[pos..]);
240                    break;
241                }
242            }
243        }
244
245        Ok(ClutDefinitionSegment {
246            page_id,
247            clut_id,
248            clut_version_number,
249            reserved,
250            entries,
251            suffix,
252        })
253    }
254}
255
256impl Serialize for ClutDefinitionSegment {
257    type Error = Error;
258
259    fn serialized_len(&self) -> usize {
260        HEADER_LEN
261            + FIXED_LEN
262            + self
263                .entries
264                .iter()
265                .map(|e| e.serialized_len())
266                .sum::<usize>()
267            + self.suffix.len()
268    }
269
270    fn serialize_into(&self, buf: &mut [u8]) -> core::result::Result<usize, Self::Error> {
271        let len = self.serialized_len();
272        if buf.len() < len {
273            return Err(Error::BufferTooShort {
274                need: len,
275                have: buf.len(),
276                what: "CLUT_definition_segment serialize",
277            });
278        }
279        buf[0] = 0x0F;
280        buf[1] = SEGMENT_TYPE;
281        buf[2..4].copy_from_slice(&self.page_id.to_be_bytes());
282        let seg_len = crate::segments::check_segment_length(len - HEADER_LEN)?;
283        buf[4..6].copy_from_slice(&seg_len.to_be_bytes());
284
285        buf[6] = self.clut_id;
286        buf[7] = (broadcast_common::len::fit_mask(
287            self.clut_version_number,
288            0x0F,
289            "clut_version_number",
290        )? << 4)
291            | broadcast_common::len::fit_mask(self.reserved, 0x0F, "reserved")?;
292
293        let mut off = HEADER_LEN + FIXED_LEN;
294        for entry in &self.entries {
295            entry.serialize_into(&mut buf[off..])?;
296            off += entry.serialized_len();
297        }
298        buf[off..off + self.suffix.len()].copy_from_slice(&self.suffix);
299        Ok(len)
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306    use broadcast_common::{Parse, Serialize};
307
308    /// DS-O2 (#1108): a reduced-range component wider than its 6/4/4/2-bit slot
309    /// used to be shifted into its neighbour's bits (silent corruption); it
310    /// must be rejected, while the in-range maximum still round-trips.
311    #[test]
312    fn reduced_range_overwide_components_are_rejected() {
313        let entry = |y, cr, cb, t| ClutEntry {
314            clut_entry_id: 1,
315            flag_2bit: true,
316            flag_4bit: false,
317            flag_8bit: false,
318            reserved_flags: 0,
319            full_range_flag: false,
320            y_value: y,
321            cr_value: cr,
322            cb_value: cb,
323            t_value: t,
324        };
325        let seg = |e: ClutEntry| ClutDefinitionSegment {
326            page_id: 1,
327            clut_id: 0,
328            clut_version_number: 0,
329            reserved: 0,
330            entries: alloc::vec![e],
331            suffix: alloc::vec::Vec::new(),
332        };
333        let max = seg(entry(0x3F, 0x0F, 0x0F, 0x03));
334        let bytes = max.try_to_bytes().unwrap();
335        assert_eq!(ClutDefinitionSegment::parse(&bytes).unwrap(), max);
336        for bad in [
337            entry(0x40, 0, 0, 0),
338            entry(0, 0x10, 0, 0),
339            entry(0, 0, 0x10, 0),
340            entry(0, 0, 0, 0x04),
341        ] {
342            assert!(matches!(
343                seg(bad).try_to_bytes(),
344                Err(Error::InvalidValue { .. })
345            ));
346        }
347        // Full-range entries keep the full 8 bits.
348        let mut full = entry(0xFF, 0xFF, 0xFF, 0xFF);
349        full.full_range_flag = true;
350        assert!(seg(full).try_to_bytes().is_ok());
351    }
352
353    #[test]
354    fn round_trip_full_range() {
355        let bytes = [
356            0x0F, 0x12, 0x00, 0x01, 0x00, 0x0E, 0x03, 0x10, 0x00, 0xA1, 0x80, 0x80, 0x80, 0x80,
357            0x01, 0x61, 0xFF, 0xFF, 0xFF, 0x00,
358        ];
359        let seg = ClutDefinitionSegment::parse(&bytes).unwrap();
360        assert_eq!(seg.clut_id, 3);
361        assert_eq!(seg.entries.len(), 2);
362        assert_eq!(seg.entries[0].clut_entry_id, 0);
363        assert!(seg.entries[0].flag_8bit);
364        assert!(seg.entries[0].full_range_flag);
365        assert_eq!(seg.entries[0].y_value, 128);
366        assert_eq!(seg.entries[1].clut_entry_id, 1);
367        assert!(seg.entries[1].flag_4bit);
368        let out = seg.try_to_bytes().unwrap();
369        assert_eq!(out, bytes);
370
371        // Biting test
372        let mut seg2 = seg.clone();
373        seg2.clut_id = 5;
374        let out2 = seg2.try_to_bytes().unwrap();
375        assert_ne!(out2, bytes);
376        let reparse = ClutDefinitionSegment::parse(&out2).unwrap();
377        assert_eq!(reparse.clut_id, 5);
378    }
379
380    /// DS-W2 (#1108): sync_byte wasn't checked by any typed segment parser.
381    #[test]
382    fn rejects_bad_sync_byte() {
383        let bytes = [
384            0x00, 0x12, 0x00, 0x01, 0x00, 0x0E, 0x03, 0x10, 0x00, 0xA1, 0x80, 0x80, 0x80, 0x80,
385            0x01, 0x61, 0xFF, 0xFF, 0xFF, 0x00,
386        ];
387        assert!(matches!(
388            ClutDefinitionSegment::parse(&bytes),
389            Err(Error::BadSyncByte(0x00))
390        ));
391    }
392
393    #[test]
394    fn round_trip_reduced_range() {
395        let bytes = [
396            0x0F, 0x12, 0x00, 0x01, 0x00, 0x0A, 0x03, 0x10, 0x00, 0x80, 0x00, 0x00, 0x01, 0x40,
397            0xFD, 0xFC,
398        ];
399        let seg = ClutDefinitionSegment::parse(&bytes).unwrap();
400        assert_eq!(seg.entries.len(), 2);
401        assert!(seg.entries[0].flag_2bit);
402        assert!(!seg.entries[0].full_range_flag);
403        assert_eq!(seg.entries[0].y_value, 0);
404        let out = seg.try_to_bytes().unwrap();
405        assert_eq!(out, bytes);
406
407        // Biting test
408        let mut seg2 = seg.clone();
409        seg2.entries[0].flag_2bit = false;
410        seg2.entries[0].flag_8bit = true;
411        let out2 = seg2.try_to_bytes().unwrap();
412        assert_ne!(out2, bytes);
413        let reparse = ClutDefinitionSegment::parse(&out2).unwrap();
414        assert!(reparse.entries[0].flag_8bit);
415    }
416
417    #[test]
418    fn tolerates_nonzero_reserved_bits() {
419        let bytes = [
420            0x0F, 0x12, 0x00, 0x01, 0x00,
421            0x06, // seg_len=6 = 2 fixed + 4 entry (2 header + 2 reduced)
422            0x03, 0x1A, // CLUT_id=3, version=1, reserved=0xA
423            0x00, 0x8A, 0x00,
424            0x00, // entry: flag_2bit=1, reserved_flags=0x0A, full=0, Y=Cr=Cb=T=0
425        ];
426        let seg = ClutDefinitionSegment::parse(&bytes).unwrap();
427        assert_eq!(seg.reserved, 0x0A);
428        assert_eq!(seg.entries.len(), 1);
429        assert_eq!(seg.entries[0].reserved_flags, 0x0A);
430        assert!(seg.entries[0].flag_2bit);
431        let out = seg.try_to_bytes().unwrap();
432        assert_eq!(out, bytes);
433        let seg2 = ClutDefinitionSegment::parse(&out).unwrap();
434        assert_eq!(seg2.reserved, 0x0A);
435        assert_eq!(seg2.entries[0].reserved_flags, 0x0A);
436    }
437}