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