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