Skip to main content

dvb_si/descriptors/
iso_639_language.rs

1//! ISO 639 Language Descriptor — MPEG-2 ISO/IEC 13818-1 §2.6.19 (tag 0x0A).
2
3use super::descriptor_body;
4use crate::error::{Error, Result};
5use crate::text::LangCode;
6use dvb_common::{Parse, Serialize};
7
8/// Descriptor tag for iso_639_language_descriptor.
9pub const TAG: u8 = 0x0A;
10const HEADER_LEN: usize = 2;
11const ENTRY_LEN: usize = 4;
12
13/// Audio type — ETSI EN 300 468 §6.2.22.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize))]
16#[non_exhaustive]
17pub enum AudioType {
18    /// 0x00 — undefined.
19    Undefined,
20    /// 0x01 — clean effects.
21    CleanEffects,
22    /// 0x02 — hearing impaired.
23    HearingImpaired,
24    /// 0x03 — visual impaired commentary.
25    VisualImpairedCommentary,
26    /// Reserved/unallocated wire value, preserved verbatim for round-trip.
27    Reserved(u8),
28}
29
30impl AudioType {
31    #[must_use]
32    /// Creates a value from a wire byte, preserving every possible
33    /// byte value for lossless round-trip.
34    pub fn from_u8(v: u8) -> Self {
35        match v {
36            0x00 => Self::Undefined,
37            0x01 => Self::CleanEffects,
38            0x02 => Self::HearingImpaired,
39            0x03 => Self::VisualImpairedCommentary,
40            v => Self::Reserved(v),
41        }
42    }
43
44    #[must_use]
45    /// Returns the wire byte for this value.
46    pub fn to_u8(self) -> u8 {
47        match self {
48            Self::Undefined => 0x00,
49            Self::CleanEffects => 0x01,
50            Self::HearingImpaired => 0x02,
51            Self::VisualImpairedCommentary => 0x03,
52            Self::Reserved(v) => v,
53        }
54    }
55
56    #[must_use]
57    /// Returns a human-readable spec name for this value.
58    pub fn name(self) -> &'static str {
59        match self {
60            Self::Undefined => "undefined",
61            Self::CleanEffects => "clean effects",
62            Self::HearingImpaired => "hearing impaired",
63            Self::VisualImpairedCommentary => "visual impaired commentary",
64            Self::Reserved(_) => "reserved",
65        }
66    }
67}
68
69/// One (language code, audio type) pair.
70#[derive(Debug, Clone, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize))]
72pub struct LanguageEntry {
73    /// Three-character ISO 639-2 language code (e.g. `LangCode(*b"eng")`).
74    pub language_code: LangCode,
75    /// Audio type (ETSI EN 300 468 §6.2.22).
76    pub audio_type: AudioType,
77}
78
79/// ISO 639 Language Descriptor.
80#[derive(Debug, Clone, PartialEq, Eq)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize))]
82pub struct Iso639LanguageDescriptor {
83    /// One or more language entries.
84    pub entries: Vec<LanguageEntry>,
85}
86
87impl<'a> Parse<'a> for Iso639LanguageDescriptor {
88    type Error = crate::error::Error;
89    fn parse(bytes: &'a [u8]) -> Result<Self> {
90        let body = descriptor_body(
91            bytes,
92            TAG,
93            "Iso639LanguageDescriptor",
94            "unexpected tag for iso_639_language_descriptor",
95        )?;
96        if body.len() % ENTRY_LEN != 0 {
97            return Err(Error::InvalidDescriptor {
98                tag: TAG,
99                reason: "iso_639_language_descriptor length not a multiple of 4",
100            });
101        }
102        let mut entries = Vec::with_capacity(body.len() / ENTRY_LEN);
103        for chunk in body.chunks_exact(ENTRY_LEN) {
104            entries.push(LanguageEntry {
105                language_code: LangCode([chunk[0], chunk[1], chunk[2]]),
106                audio_type: AudioType::from_u8(chunk[3]),
107            });
108        }
109        Ok(Self { entries })
110    }
111}
112
113impl Serialize for Iso639LanguageDescriptor {
114    type Error = crate::error::Error;
115    fn serialized_len(&self) -> usize {
116        HEADER_LEN + self.entries.len() * ENTRY_LEN
117    }
118
119    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
120        let len = self.serialized_len();
121        if buf.len() < len {
122            return Err(Error::OutputBufferTooSmall {
123                need: len,
124                have: buf.len(),
125            });
126        }
127        buf[0] = TAG;
128        buf[1] = (self.entries.len() * ENTRY_LEN) as u8;
129        let mut pos = HEADER_LEN;
130        for e in &self.entries {
131            buf[pos..pos + 3].copy_from_slice(&e.language_code.0);
132            buf[pos + 3] = e.audio_type.to_u8();
133            pos += ENTRY_LEN;
134        }
135        Ok(len)
136    }
137}
138impl<'a> crate::traits::DescriptorDef<'a> for Iso639LanguageDescriptor {
139    const TAG: u8 = TAG;
140    const NAME: &'static str = "ISO_639_LANGUAGE";
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn parse_single_language_entry() {
149        let bytes = [TAG, 4, b'e', b'n', b'g', 0x00];
150        let d = Iso639LanguageDescriptor::parse(&bytes).unwrap();
151        assert_eq!(d.entries.len(), 1);
152        assert_eq!(d.entries[0].language_code, LangCode(*b"eng"));
153        assert_eq!(d.entries[0].audio_type, AudioType::Undefined);
154    }
155
156    #[test]
157    fn parse_multiple_entries() {
158        let bytes = [TAG, 8, b'e', b'n', b'g', 1, b'f', b'r', b'a', 2];
159        let d = Iso639LanguageDescriptor::parse(&bytes).unwrap();
160        assert_eq!(d.entries.len(), 2);
161        assert_eq!(d.entries[1].language_code, LangCode(*b"fra"));
162        assert_eq!(d.entries[1].audio_type, AudioType::HearingImpaired);
163    }
164
165    #[test]
166    fn parse_rejects_wrong_tag() {
167        let err = Iso639LanguageDescriptor::parse(&[0x0B, 0]).unwrap_err();
168        assert!(matches!(err, Error::InvalidDescriptor { tag: 0x0B, .. }));
169    }
170
171    #[test]
172    fn parse_rejects_short_header() {
173        let err = Iso639LanguageDescriptor::parse(&[TAG]).unwrap_err();
174        assert!(matches!(err, Error::BufferTooShort { .. }));
175    }
176
177    #[test]
178    fn parse_rejects_length_not_multiple_of_4() {
179        let bytes = [TAG, 5, b'e', b'n', b'g', 0, 0];
180        let err = Iso639LanguageDescriptor::parse(&bytes).unwrap_err();
181        assert!(matches!(err, Error::InvalidDescriptor { .. }));
182    }
183
184    #[test]
185    fn serialize_round_trip() {
186        let d = Iso639LanguageDescriptor {
187            entries: vec![
188                LanguageEntry {
189                    language_code: LangCode(*b"eng"),
190                    audio_type: AudioType::Undefined,
191                },
192                LanguageEntry {
193                    language_code: LangCode(*b"fra"),
194                    audio_type: AudioType::CleanEffects,
195                },
196            ],
197        };
198        let mut buf = vec![0u8; d.serialized_len()];
199        d.serialize_into(&mut buf).unwrap();
200        let re = Iso639LanguageDescriptor::parse(&buf).unwrap();
201        assert_eq!(d, re);
202    }
203
204    #[test]
205    fn descriptor_length_matches_payload() {
206        let d = Iso639LanguageDescriptor {
207            entries: vec![LanguageEntry {
208                language_code: LangCode(*b"eng"),
209                audio_type: AudioType::Undefined,
210            }],
211        };
212        assert_eq!(d.serialized_len() - 2, 4);
213    }
214
215    #[test]
216    fn audio_type_full_range_round_trip() {
217        for b in 0..=0xFF_u8 {
218            let at = AudioType::from_u8(b);
219            assert_eq!(at.to_u8(), b, "round-trip failed for byte 0x{b:02X}");
220        }
221    }
222
223    #[test]
224    fn audio_type_name_for_known() {
225        assert_eq!(AudioType::Undefined.name(), "undefined");
226        assert_eq!(AudioType::CleanEffects.name(), "clean effects");
227        assert_eq!(AudioType::HearingImpaired.name(), "hearing impaired");
228        assert_eq!(
229            AudioType::VisualImpairedCommentary.name(),
230            "visual impaired commentary"
231        );
232        assert_eq!(AudioType::Reserved(0x55).name(), "reserved");
233    }
234}