Skip to main content

dvb_si/descriptors/
multilingual_component.rs

1//! Multilingual Component Descriptor — ETSI EN 300 468 §6.2.23 (tag 0x5E).
2//!
3//! Table 77 (PDF p. 94). Carried in the EIT / PMT. A leading `component_tag`
4//! byte ties the descriptor to a component, followed by a loop of (ISO 639-2
5//! language code, text) pairs, each text length-prefixed by an 8-bit field.
6
7use super::descriptor_body;
8use crate::error::{Error, Result};
9use crate::text::{DvbText, LangCode};
10use dvb_common::{Parse, Serialize};
11
12/// Descriptor tag for multilingual_component_descriptor.
13pub const TAG: u8 = 0x5E;
14const HEADER_LEN: usize = 2;
15const COMPONENT_TAG_LEN: usize = 1;
16const LANG_LEN: usize = 3;
17const TEXT_LEN_FIELD: usize = 1;
18
19/// One localised component description.
20#[derive(Debug, Clone, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize))]
22#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
23pub struct ComponentTextEntry<'a> {
24    /// ISO 639-2 language code.
25    pub language_code: LangCode,
26    /// DVB Annex-A encoded component description text.
27    pub text: DvbText<'a>,
28}
29
30/// Multilingual Component Descriptor (tag 0x5E).
31#[derive(Debug, Clone, PartialEq, Eq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize))]
33#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
34pub struct MultilingualComponentDescriptor<'a> {
35    /// component_tag linking this descriptor to a stream_identifier_descriptor.
36    pub component_tag: u8,
37    /// Localised descriptions in wire order.
38    pub entries: Vec<ComponentTextEntry<'a>>,
39}
40
41impl<'a> Parse<'a> for MultilingualComponentDescriptor<'a> {
42    type Error = crate::error::Error;
43    fn parse(bytes: &'a [u8]) -> Result<Self> {
44        let body = descriptor_body(
45            bytes,
46            TAG,
47            "MultilingualComponentDescriptor",
48            "unexpected tag for multilingual_component_descriptor",
49        )?;
50        if body.len() < COMPONENT_TAG_LEN {
51            return Err(Error::InvalidDescriptor {
52                tag: TAG,
53                reason: "multilingual_component_descriptor body missing component_tag",
54            });
55        }
56        let component_tag = body[0];
57        let mut entries = Vec::new();
58        let mut pos = COMPONENT_TAG_LEN;
59        while pos < body.len() {
60            if pos + LANG_LEN + TEXT_LEN_FIELD > body.len() {
61                return Err(Error::InvalidDescriptor {
62                    tag: TAG,
63                    reason: "entry header runs past descriptor end",
64                });
65            }
66            let language_code = LangCode([body[pos], body[pos + 1], body[pos + 2]]);
67            let text_len = body[pos + LANG_LEN] as usize;
68            let text_start = pos + LANG_LEN + TEXT_LEN_FIELD;
69            let text_end = text_start + text_len;
70            if text_end > body.len() {
71                return Err(Error::InvalidDescriptor {
72                    tag: TAG,
73                    reason: "text_length runs past descriptor end",
74                });
75            }
76            entries.push(ComponentTextEntry {
77                language_code,
78                text: DvbText::new(&body[text_start..text_end]),
79            });
80            pos = text_end;
81        }
82        Ok(Self {
83            component_tag,
84            entries,
85        })
86    }
87}
88
89impl Serialize for MultilingualComponentDescriptor<'_> {
90    type Error = crate::error::Error;
91    fn serialized_len(&self) -> usize {
92        HEADER_LEN
93            + COMPONENT_TAG_LEN
94            + self
95                .entries
96                .iter()
97                .map(|e| LANG_LEN + TEXT_LEN_FIELD + e.text.len())
98                .sum::<usize>()
99    }
100
101    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
102        for e in &self.entries {
103            if e.text.len() > u8::MAX as usize {
104                return Err(Error::InvalidDescriptor {
105                    tag: TAG,
106                    reason: "text exceeds 255 bytes (text_length is 8-bit)",
107                });
108            }
109        }
110        let len = self.serialized_len();
111        let body = len - HEADER_LEN;
112        if body > u8::MAX as usize {
113            return Err(Error::InvalidDescriptor {
114                tag: TAG,
115                reason: "multilingual_component_descriptor body exceeds 255 bytes",
116            });
117        }
118        if buf.len() < len {
119            return Err(Error::OutputBufferTooSmall {
120                need: len,
121                have: buf.len(),
122            });
123        }
124        buf[0] = TAG;
125        buf[1] = body as u8;
126        buf[HEADER_LEN] = self.component_tag;
127        let mut pos = HEADER_LEN + COMPONENT_TAG_LEN;
128        for e in &self.entries {
129            buf[pos..pos + LANG_LEN].copy_from_slice(&e.language_code.0);
130            buf[pos + LANG_LEN] = e.text.len() as u8;
131            let text_start = pos + LANG_LEN + TEXT_LEN_FIELD;
132            buf[text_start..text_start + e.text.len()].copy_from_slice(e.text.raw());
133            pos = text_start + e.text.len();
134        }
135        Ok(len)
136    }
137}
138impl<'a> crate::traits::DescriptorDef<'a> for MultilingualComponentDescriptor<'a> {
139    const TAG: u8 = TAG;
140    const NAME: &'static str = "MULTILINGUAL_COMPONENT";
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    fn build(component_tag: u8, entries: &[([u8; 3], &[u8])]) -> Vec<u8> {
148        let body: usize = COMPONENT_TAG_LEN
149            + entries
150                .iter()
151                .map(|(_, t)| LANG_LEN + 1 + t.len())
152                .sum::<usize>();
153        let mut v = Vec::with_capacity(HEADER_LEN + body);
154        v.push(TAG);
155        v.push(body as u8);
156        v.push(component_tag);
157        for (lang, text) in entries {
158            v.extend_from_slice(lang);
159            v.push(text.len() as u8);
160            v.extend_from_slice(text);
161        }
162        v
163    }
164
165    #[test]
166    fn parse_extracts_component_tag_and_entries() {
167        let bytes = build(0x12, &[(*b"eng", b"Video")]);
168        let d = MultilingualComponentDescriptor::parse(&bytes).unwrap();
169        assert_eq!(d.component_tag, 0x12);
170        assert_eq!(d.entries.len(), 1);
171        assert_eq!(d.entries[0].language_code, LangCode(*b"eng"));
172        assert_eq!(d.entries[0].text.raw(), b"Video");
173    }
174
175    #[test]
176    fn parse_multiple_entries() {
177        let bytes = build(0x03, &[(*b"eng", b"Audio"), (*b"fra", b"Son")]);
178        let d = MultilingualComponentDescriptor::parse(&bytes).unwrap();
179        assert_eq!(d.component_tag, 0x03);
180        assert_eq!(d.entries.len(), 2);
181        assert_eq!(d.entries[1].text.raw(), b"Son");
182    }
183
184    #[test]
185    fn parse_component_tag_only_valid() {
186        // Body = just the component_tag, no language entries.
187        let bytes = [TAG, 1, 0x09];
188        let d = MultilingualComponentDescriptor::parse(&bytes).unwrap();
189        assert_eq!(d.component_tag, 0x09);
190        assert_eq!(d.entries.len(), 0);
191    }
192
193    #[test]
194    fn parse_rejects_wrong_tag() {
195        let err = MultilingualComponentDescriptor::parse(&[0x5D, 1, 0x00]).unwrap_err();
196        assert!(matches!(err, Error::InvalidDescriptor { tag: 0x5D, .. }));
197    }
198
199    #[test]
200    fn parse_rejects_short_buffer() {
201        let err = MultilingualComponentDescriptor::parse(&[TAG]).unwrap_err();
202        assert!(matches!(err, Error::BufferTooShort { .. }));
203    }
204
205    #[test]
206    fn parse_rejects_missing_component_tag() {
207        // length=0: no component_tag.
208        let err = MultilingualComponentDescriptor::parse(&[TAG, 0]).unwrap_err();
209        assert!(matches!(err, Error::InvalidDescriptor { .. }));
210    }
211
212    #[test]
213    fn parse_rejects_text_length_overrun() {
214        // component_tag + lang + text_len=100 but no text bytes.
215        let bytes = [TAG, 5, 0x01, b'e', b'n', b'g', 100];
216        let err = MultilingualComponentDescriptor::parse(&bytes).unwrap_err();
217        assert!(matches!(err, Error::InvalidDescriptor { .. }));
218    }
219
220    #[test]
221    fn serialize_round_trip() {
222        let bytes = build(0x07, &[(*b"eng", b"Subtitle"), (*b"deu", b"Untertitel")]);
223        let parsed = MultilingualComponentDescriptor::parse(&bytes).unwrap();
224        let mut buf = vec![0u8; parsed.serialized_len()];
225        parsed.serialize_into(&mut buf).unwrap();
226        assert_eq!(buf, bytes);
227        let re = MultilingualComponentDescriptor::parse(&buf).unwrap();
228        assert_eq!(parsed, re);
229    }
230
231    #[test]
232    fn serialize_rejects_too_small_buffer() {
233        let d = MultilingualComponentDescriptor {
234            component_tag: 0x01,
235            entries: vec![],
236        };
237        let mut tiny = [0u8; 2];
238        let err = d.serialize_into(&mut tiny).unwrap_err();
239        assert!(matches!(err, Error::OutputBufferTooSmall { .. }));
240    }
241
242    #[test]
243    fn serialize_rejects_over_range_text() {
244        let text = vec![0u8; 256];
245        let d = MultilingualComponentDescriptor {
246            component_tag: 0x01,
247            entries: vec![ComponentTextEntry {
248                language_code: LangCode(*b"eng"),
249                text: DvbText::new(&text),
250            }],
251        };
252        let mut buf = vec![0u8; d.serialized_len()];
253        let err = d.serialize_into(&mut buf).unwrap_err();
254        assert!(matches!(err, Error::InvalidDescriptor { tag: TAG, .. }));
255    }
256
257    #[cfg(feature = "serde")]
258    #[test]
259    fn serde_serialize_is_stable() {
260        let d = MultilingualComponentDescriptor {
261            component_tag: 0x12,
262            entries: vec![ComponentTextEntry {
263                language_code: LangCode(*b"eng"),
264                text: DvbText::new(b"Video"),
265            }],
266        };
267        let json = serde_json::to_string(&d).unwrap();
268        assert!(json.contains("\"component_tag\""));
269        assert!(json.contains("\"language_code\""));
270        assert!(json.contains("\"eng\""));
271        assert!(json.contains("\"Video\""));
272    }
273}