Skip to main content

dvb_si/descriptors/
content_labeling.rs

1//! Content Labeling Descriptor — ISO/IEC 13818-1 §2.6.56, Table 2-83 (tag 0x24).
2//!
3//! Associates metadata labelling with content, supporting time-base schemes
4//! (STC, NPT, or privately defined) and optional content-reference records.
5//! The time-base block is conditional on `content_time_base_indicator` 1 or 2;
6//! the contentId block only when value 2; the time_base_association block
7//! only when 3..=7.
8
9use super::descriptor_body;
10use crate::descriptors::content_time_base_indicator::ContentTimeBaseIndicator;
11use crate::error::{Error, Result};
12use dvb_common::{Parse, Serialize};
13
14/// Descriptor tag for content_labeling_descriptor.
15pub const TAG: u8 = 0x24;
16const HEADER_LEN: usize = 2;
17
18/// Time-base block — present when content_time_base_indicator is 1 or 2.
19#[derive(Debug, Clone, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize))]
21pub struct ContentTimeBase {
22    /// 33-bit content time base value (stored as u64, masked to 33 bits).
23    pub content_time_base_value: u64,
24    /// 33-bit metadata time base value (stored as u64, masked to 33 bits).
25    pub metadata_time_base_value: u64,
26}
27
28/// Time-base association block — present when content_time_base_indicator is 3..=7.
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize))]
31pub struct ContentTimeBaseAssociation<'a> {
32    /// Opaque time_base_association_data bytes.
33    #[cfg_attr(feature = "serde", serde(borrow))]
34    pub data: &'a [u8],
35}
36
37/// Content Labeling Descriptor.
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize))]
40#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
41pub struct ContentLabelingDescriptor<'a> {
42    /// Metadata application format (u16; see Table 2-84).
43    pub metadata_application_format: u16,
44    /// Metadata application format identifier — present when `metadata_application_format == 0xFFFF`.
45    pub metadata_application_format_identifier: Option<u32>,
46    /// Content reference ID record present flag.
47    pub content_reference_id_record_flag: bool,
48    /// Time base indicator (4-bit; Table 2-85).
49    pub content_time_base_indicator: ContentTimeBaseIndicator,
50    /// Content reference ID record — present when `content_reference_id_record_flag` is true.
51    #[cfg_attr(feature = "serde", serde(borrow))]
52    pub content_reference_id_record: Option<&'a [u8]>,
53    /// Time base block — present when indicator is 1 or 2.
54    pub time_base: Option<ContentTimeBase>,
55    /// contentId (7-bit) — present when indicator is 2.
56    pub content_id: Option<u8>,
57    /// Time base association block — present when indicator is 3..=7.
58    pub time_base_association: Option<ContentTimeBaseAssociation<'a>>,
59    /// Trailing private data bytes.
60    #[cfg_attr(feature = "serde", serde(borrow))]
61    pub private_data: &'a [u8],
62}
63
64fn read_33bit(bytes: &[u8], pos: usize) -> u64 {
65    // 33 bits = 4 bytes + 1 bit, laid out across 5 bytes: byte[0..3] full,
66    // byte[4] top bit is the MSB, bottom 7 bits reserved.
67    ((bytes[pos] as u64) << 25)
68        | ((bytes[pos + 1] as u64) << 17)
69        | ((bytes[pos + 2] as u64) << 9)
70        | ((bytes[pos + 3] as u64) << 1)
71        | ((bytes[pos + 4] >> 7) as u64)
72}
73
74fn write_33bit(buf: &mut [u8], pos: usize, value: u64) {
75    let v = value & 0x1_FFFF_FFFF;
76    buf[pos] = ((v >> 25) & 0xFF) as u8;
77    buf[pos + 1] = ((v >> 17) & 0xFF) as u8;
78    buf[pos + 2] = ((v >> 9) & 0xFF) as u8;
79    buf[pos + 3] = ((v >> 1) & 0xFF) as u8;
80    buf[pos + 4] = ((v & 0x01) as u8) << 7;
81}
82
83impl<'a> Parse<'a> for ContentLabelingDescriptor<'a> {
84    type Error = crate::error::Error;
85
86    fn parse(bytes: &'a [u8]) -> Result<Self> {
87        let body = descriptor_body(
88            bytes,
89            TAG,
90            "ContentLabelingDescriptor",
91            "unexpected tag for content_labeling_descriptor",
92        )?;
93
94        // Minimum: metadata_application_format(2) + flags(1) = 3 bytes
95        if body.len() < 3 {
96            return Err(Error::InvalidDescriptor {
97                tag: TAG,
98                reason: "content_labeling_descriptor too short (< 3 body bytes)",
99            });
100        }
101
102        let metadata_application_format = u16::from_be_bytes([body[0], body[1]]);
103        let mut pos = 2;
104
105        let metadata_application_format_identifier = if metadata_application_format == 0xFFFF {
106            if body.len() < pos + 4 {
107                return Err(Error::InvalidDescriptor {
108                        tag: TAG,
109                        reason: "content_labeling_descriptor too short for metadata_application_format_identifier",
110                    });
111            }
112            let id = u32::from_be_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]]);
113            pos += 4;
114            Some(id)
115        } else {
116            None
117        };
118
119        if body.len() < pos + 1 {
120            return Err(Error::InvalidDescriptor {
121                tag: TAG,
122                reason: "content_labeling_descriptor too short for flags byte",
123            });
124        }
125        let flags = body[pos];
126        let content_reference_id_record_flag = (flags & 0x80) != 0;
127        let indicator_raw = (flags >> 3) & 0x0F;
128        let content_time_base_indicator = ContentTimeBaseIndicator::from_u8(indicator_raw);
129        pos += 1;
130
131        let (content_reference_id_record, _) = if content_reference_id_record_flag {
132            if body.len() < pos + 1 {
133                return Err(Error::InvalidDescriptor {
134                        tag: TAG,
135                        reason: "content_labeling_descriptor too short for content_reference_id_record_length",
136                    });
137            }
138            let rec_len = body[pos] as usize;
139            pos += 1;
140            if body.len() < pos + rec_len {
141                return Err(Error::InvalidDescriptor {
142                    tag: TAG,
143                    reason: "content_labeling_descriptor too short for content_reference_id_record",
144                });
145            }
146            let rec = &body[pos..pos + rec_len];
147            pos += rec_len;
148            (Some(rec), pos)
149        } else {
150            (None, pos)
151        };
152
153        let time_base = if indicator_raw == 1 || indicator_raw == 2 {
154            // 7 reserved bits + 33-bit content_time_base_value + 7 reserved bits + 33-bit metadata_time_base_value
155            // = 80 bits = 10 bytes
156            if body.len() < pos + 10 {
157                return Err(Error::InvalidDescriptor {
158                    tag: TAG,
159                    reason: "content_labeling_descriptor too short for time base block",
160                });
161            }
162            // byte[pos]: reserved(7) | content_time_base_value top bit
163            // That's: 1 reserved byte + 4.125 bytes for the 33-bit value
164            // The 33-bit value spans bytes pos..pos+5, with byte[pos] having 7 reserved + 1 data bit
165            // Wait — the syntax table shows:
166            //   reserved(7) | content_time_base_value(33)
167            // The reserved(7) occupies the first 7 bits of byte pos, then content_time_base_value starts at bit 0 of byte pos
168            // Actually: 7 reserved + 33 = 40 bits = 5 bytes
169            // Then: reserved(7) | metadata_time_base_value(33) = 40 bits = 5 bytes
170            // Total = 10 bytes
171            let ctv = read_33bit(&body[pos..], 0);
172            let mtv = read_33bit(&body[pos + 5..], 0);
173            pos += 10;
174            Some(ContentTimeBase {
175                content_time_base_value: ctv,
176                metadata_time_base_value: mtv,
177            })
178        } else {
179            None
180        };
181
182        let content_id = if indicator_raw == 2 {
183            if body.len() < pos + 1 {
184                return Err(Error::InvalidDescriptor {
185                    tag: TAG,
186                    reason: "content_labeling_descriptor too short for contentId byte",
187                });
188            }
189            let id = body[pos] & 0x7F;
190            pos += 1;
191            Some(id)
192        } else {
193            None
194        };
195
196        let time_base_association = if (3..=7).contains(&indicator_raw) {
197            if body.len() < pos + 1 {
198                return Err(Error::InvalidDescriptor {
199                    tag: TAG,
200                    reason: "content_labeling_descriptor too short for time_base_association_data_length",
201                });
202            }
203            let assoc_len = body[pos] as usize;
204            pos += 1;
205            if body.len() < pos + assoc_len {
206                return Err(Error::InvalidDescriptor {
207                    tag: TAG,
208                    reason: "content_labeling_descriptor too short for time_base_association_data",
209                });
210            }
211            let data = &body[pos..pos + assoc_len];
212            pos += assoc_len;
213            Some(ContentTimeBaseAssociation { data })
214        } else {
215            None
216        };
217
218        let private_data = &body[pos..];
219
220        Ok(Self {
221            metadata_application_format,
222            metadata_application_format_identifier,
223            content_reference_id_record_flag,
224            content_time_base_indicator,
225            content_reference_id_record,
226            time_base,
227            content_id,
228            time_base_association,
229            private_data,
230        })
231    }
232}
233
234impl Serialize for ContentLabelingDescriptor<'_> {
235    type Error = crate::error::Error;
236
237    fn serialized_len(&self) -> usize {
238        let mut len: usize = HEADER_LEN + 3; // metadata_application_format(2) + flags(1)
239        if self.metadata_application_format_identifier.is_some() {
240            len += 4;
241        }
242        if let Some(rec) = self.content_reference_id_record {
243            len += 1 + rec.len();
244        }
245        if self.time_base.is_some() {
246            len += 10;
247        }
248        if self.content_id.is_some() {
249            len += 1;
250        }
251        if let Some(ref assoc) = self.time_base_association {
252            len += 1 + assoc.data.len();
253        }
254        len += self.private_data.len();
255        len
256    }
257
258    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
259        let len = self.serialized_len();
260        if buf.len() < len {
261            return Err(Error::OutputBufferTooSmall {
262                need: len,
263                have: buf.len(),
264            });
265        }
266        buf[0] = TAG;
267        buf[1] = (len - HEADER_LEN) as u8;
268
269        buf[HEADER_LEN] = (self.metadata_application_format >> 8) as u8;
270        buf[HEADER_LEN + 1] = self.metadata_application_format as u8;
271        let mut pos = HEADER_LEN + 2;
272
273        if let Some(id) = self.metadata_application_format_identifier {
274            buf[pos..pos + 4].copy_from_slice(&id.to_be_bytes());
275            pos += 4;
276        }
277
278        let mut flags = (self.content_time_base_indicator.to_u8() & 0x0F) << 3;
279        if self.content_reference_id_record_flag {
280            flags |= 0x80;
281        }
282        buf[pos] = flags;
283        pos += 1;
284
285        if let Some(rec) = self.content_reference_id_record {
286            buf[pos] = rec.len() as u8;
287            pos += 1;
288            buf[pos..pos + rec.len()].copy_from_slice(rec);
289            pos += rec.len();
290        }
291
292        if let Some(ref tb) = self.time_base {
293            // 7 reserved bits (0) + 33-bit content_time_base_value
294            write_33bit(buf, pos, tb.content_time_base_value);
295            pos += 5;
296            // 7 reserved bits (0) + 33-bit metadata_time_base_value
297            write_33bit(buf, pos, tb.metadata_time_base_value);
298            pos += 5;
299        }
300
301        if let Some(cid) = self.content_id {
302            buf[pos] = cid & 0x7F;
303            pos += 1;
304        }
305
306        if let Some(ref assoc) = self.time_base_association {
307            buf[pos] = assoc.data.len() as u8;
308            pos += 1;
309            buf[pos..pos + assoc.data.len()].copy_from_slice(assoc.data);
310            pos += assoc.data.len();
311        }
312
313        buf[pos..pos + self.private_data.len()].copy_from_slice(self.private_data);
314        Ok(len)
315    }
316}
317
318impl<'a> crate::traits::DescriptorDef<'a> for ContentLabelingDescriptor<'a> {
319    const TAG: u8 = TAG;
320    const NAME: &'static str = "CONTENT_LABELING";
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    // Helper to build a descriptor with all needed bytes
328    fn serialize_round_trip(d: &ContentLabelingDescriptor<'_>) {
329        let mut buf = vec![0u8; d.serialized_len()];
330        let written = d.serialize_into(&mut buf).unwrap();
331        assert_eq!(written, d.serialized_len());
332        let reparsed = ContentLabelingDescriptor::parse(&buf).unwrap();
333        assert_eq!(*d, reparsed, "round-trip mismatch");
334    }
335
336    #[test]
337    fn round_trip_indicator_0_minimal() {
338        let d = ContentLabelingDescriptor {
339            metadata_application_format: 0x0010,
340            metadata_application_format_identifier: None,
341            content_reference_id_record_flag: false,
342            content_time_base_indicator: ContentTimeBaseIndicator::None,
343            content_reference_id_record: None,
344            time_base: None,
345            content_id: None,
346            time_base_association: None,
347            private_data: &[],
348        };
349        serialize_round_trip(&d);
350    }
351
352    #[test]
353    fn round_trip_indicator_0_with_ref_id_and_private() {
354        let d = ContentLabelingDescriptor {
355            metadata_application_format: 0x0011,
356            metadata_application_format_identifier: None,
357            content_reference_id_record_flag: true,
358            content_time_base_indicator: ContentTimeBaseIndicator::None,
359            content_reference_id_record: Some(&[0xAA, 0xBB, 0xCC]),
360            time_base: None,
361            content_id: None,
362            time_base_association: None,
363            private_data: &[0xDD, 0xEE],
364        };
365        serialize_round_trip(&d);
366    }
367
368    #[test]
369    fn round_trip_indicator_1_stc_time_base() {
370        let d = ContentLabelingDescriptor {
371            metadata_application_format: 0x0100,
372            metadata_application_format_identifier: None,
373            content_reference_id_record_flag: false,
374            content_time_base_indicator: ContentTimeBaseIndicator::Stc,
375            content_reference_id_record: None,
376            time_base: Some(ContentTimeBase {
377                content_time_base_value: 0x123456789,
378                metadata_time_base_value: 0x1ABCDEF01,
379            }),
380            content_id: None,
381            time_base_association: None,
382            private_data: &[],
383        };
384        serialize_round_trip(&d);
385    }
386
387    #[test]
388    fn round_trip_indicator_2_npt_with_content_id() {
389        let d = ContentLabelingDescriptor {
390            metadata_application_format: 0x0100,
391            metadata_application_format_identifier: None,
392            content_reference_id_record_flag: false,
393            content_time_base_indicator: ContentTimeBaseIndicator::Npt,
394            content_reference_id_record: None,
395            time_base: Some(ContentTimeBase {
396                content_time_base_value: 0x1AABBCCDD,
397                metadata_time_base_value: 0x0,
398            }),
399            content_id: Some(42),
400            time_base_association: None,
401            private_data: &[],
402        };
403        serialize_round_trip(&d);
404    }
405
406    #[test]
407    fn round_trip_indicator_3_association() {
408        let d = ContentLabelingDescriptor {
409            metadata_application_format: 0x0100,
410            metadata_application_format_identifier: None,
411            content_reference_id_record_flag: false,
412            content_time_base_indicator: ContentTimeBaseIndicator::Reserved(3),
413            content_reference_id_record: None,
414            time_base: None,
415            content_id: None,
416            time_base_association: Some(ContentTimeBaseAssociation {
417                data: &[0x11, 0x22, 0x33, 0x44],
418            }),
419            private_data: &[],
420        };
421        serialize_round_trip(&d);
422    }
423
424    #[test]
425    fn round_trip_with_ffff_identifier() {
426        let d = ContentLabelingDescriptor {
427            metadata_application_format: 0xFFFF,
428            metadata_application_format_identifier: Some(0xDEADBEEF),
429            content_reference_id_record_flag: true,
430            content_time_base_indicator: ContentTimeBaseIndicator::None,
431            content_reference_id_record: Some(&[0x01, 0x02]),
432            time_base: None,
433            content_id: None,
434            time_base_association: None,
435            private_data: &[0xFF],
436        };
437        serialize_round_trip(&d);
438    }
439
440    #[test]
441    fn round_trip_indicator_private_8() {
442        // 8..=15 — privately defined, no blocks
443        let d = ContentLabelingDescriptor {
444            metadata_application_format: 0x0101,
445            metadata_application_format_identifier: None,
446            content_reference_id_record_flag: false,
447            content_time_base_indicator: ContentTimeBaseIndicator::Private(10),
448            content_reference_id_record: None,
449            time_base: None,
450            content_id: None,
451            time_base_association: None,
452            private_data: &[0x99],
453        };
454        serialize_round_trip(&d);
455    }
456
457    #[test]
458    fn parse_rejects_wrong_tag() {
459        let err = ContentLabelingDescriptor::parse(&[0x02, 3, 0, 0, 0]).unwrap_err();
460        assert!(matches!(err, Error::InvalidDescriptor { tag: 0x02, .. }));
461    }
462
463    #[test]
464    fn parse_rejects_too_short() {
465        let err = ContentLabelingDescriptor::parse(&[TAG, 0]).unwrap_err();
466        assert!(matches!(err, Error::InvalidDescriptor { tag: TAG, .. }));
467    }
468}