Skip to main content

dvb_si/descriptors/
teletext.rs

1//! Teletext Descriptor — ETSI EN 300 468 §6.2.44 (tag 0x56).
2//!
3//! Carried inside PMT's ES_info loop. Enumerates teletext components: one
4//! entry per 3-char language code + type/magazine/page triple (5 bytes).
5
6use super::descriptor_body;
7use crate::error::{Error, Result};
8use crate::text::LangCode;
9use dvb_common::{Parse, Serialize};
10
11/// Descriptor tag for teletext_descriptor.
12pub const TAG: u8 = 0x56;
13const HEADER_LEN: usize = 2;
14const ENTRY_LEN: usize = 5;
15const LANG_LEN: usize = 3;
16
17/// Teletext type — ETSI EN 300 468 Table 102.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize))]
20#[non_exhaustive]
21pub enum TeletextType {
22    /// 0x01 — initial teletext page.
23    InitialPage,
24    /// 0x02 — teletext subtitle page.
25    SubtitlePage,
26    /// 0x03 — additional information page.
27    AdditionalInformationPage,
28    /// 0x04 — programme schedule page.
29    ProgrammeSchedulePage,
30    /// 0x05 — teletext subtitle page for hearing impaired people.
31    HearingImpairedSubtitlePage,
32    /// Reserved/unallocated wire value, preserved verbatim for round-trip.
33    Reserved(u8),
34}
35
36impl TeletextType {
37    #[must_use]
38    /// Creates a value from a wire byte, preserving every possible
39    /// byte value for lossless round-trip.
40    pub fn from_u8(v: u8) -> Self {
41        match v {
42            0x01 => Self::InitialPage,
43            0x02 => Self::SubtitlePage,
44            0x03 => Self::AdditionalInformationPage,
45            0x04 => Self::ProgrammeSchedulePage,
46            0x05 => Self::HearingImpairedSubtitlePage,
47            v => Self::Reserved(v),
48        }
49    }
50
51    #[must_use]
52    /// Returns the wire byte for this value.
53    pub fn to_u8(self) -> u8 {
54        match self {
55            Self::InitialPage => 0x01,
56            Self::SubtitlePage => 0x02,
57            Self::AdditionalInformationPage => 0x03,
58            Self::ProgrammeSchedulePage => 0x04,
59            Self::HearingImpairedSubtitlePage => 0x05,
60            Self::Reserved(v) => v,
61        }
62    }
63
64    #[must_use]
65    /// Returns a human-readable spec name for this value.
66    pub fn name(self) -> &'static str {
67        match self {
68            Self::InitialPage => "initial teletext page",
69            Self::SubtitlePage => "teletext subtitle page",
70            Self::AdditionalInformationPage => "additional information page",
71            Self::ProgrammeSchedulePage => "programme schedule page",
72            Self::HearingImpairedSubtitlePage => {
73                "teletext subtitle page for hearing impaired people"
74            }
75            Self::Reserved(_) => "reserved",
76        }
77    }
78}
79
80/// One teletext component.
81#[derive(Debug, Clone, PartialEq, Eq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize))]
83pub struct TeletextEntry {
84    /// ISO 639-2 language code of this teletext service.
85    pub language_code: LangCode,
86    /// 5-bit teletext_type (ETSI Table 102).
87    pub teletext_type: TeletextType,
88    /// 3-bit teletext_magazine_number.
89    pub magazine_number: u8,
90    /// 8-bit BCD teletext_page_number.
91    pub page_number: u8,
92}
93
94/// Teletext Descriptor.
95#[derive(Debug, Clone, PartialEq, Eq)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize))]
97pub struct TeletextDescriptor {
98    /// Teletext components listed in wire order.
99    pub entries: Vec<TeletextEntry>,
100}
101
102impl<'a> Parse<'a> for TeletextDescriptor {
103    type Error = crate::error::Error;
104    fn parse(bytes: &'a [u8]) -> Result<Self> {
105        let body = descriptor_body(
106            bytes,
107            TAG,
108            "TeletextDescriptor",
109            "unexpected tag for teletext_descriptor",
110        )?;
111        if body.len() % ENTRY_LEN != 0 {
112            return Err(Error::InvalidDescriptor {
113                tag: TAG,
114                reason: "teletext_descriptor length must be a multiple of 5",
115            });
116        }
117        let mut entries = Vec::with_capacity(body.len() / ENTRY_LEN);
118        for chunk in body.chunks_exact(ENTRY_LEN) {
119            let language_code = LangCode([chunk[0], chunk[1], chunk[2]]);
120            let type_and_mag = chunk[LANG_LEN];
121            let teletext_type = TeletextType::from_u8((type_and_mag >> 3) & 0x1F);
122            let magazine_number = type_and_mag & 0x07;
123            let page_number = chunk[LANG_LEN + 1];
124            entries.push(TeletextEntry {
125                language_code,
126                teletext_type,
127                magazine_number,
128                page_number,
129            });
130        }
131        Ok(Self { entries })
132    }
133}
134
135impl Serialize for TeletextDescriptor {
136    type Error = crate::error::Error;
137    fn serialized_len(&self) -> usize {
138        HEADER_LEN + self.entries.len() * ENTRY_LEN
139    }
140
141    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
142        let len = self.serialized_len();
143        if buf.len() < len {
144            return Err(Error::OutputBufferTooSmall {
145                need: len,
146                have: buf.len(),
147            });
148        }
149        buf[0] = TAG;
150        buf[1] = (self.entries.len() * ENTRY_LEN) as u8;
151        let mut pos = HEADER_LEN;
152        for e in &self.entries {
153            buf[pos..pos + LANG_LEN].copy_from_slice(&e.language_code.0);
154            buf[pos + LANG_LEN] =
155                ((e.teletext_type.to_u8() & 0x1F) << 3) | (e.magazine_number & 0x07);
156            buf[pos + LANG_LEN + 1] = e.page_number;
157            pos += ENTRY_LEN;
158        }
159        Ok(len)
160    }
161}
162impl<'a> crate::traits::DescriptorDef<'a> for TeletextDescriptor {
163    const TAG: u8 = TAG;
164    const NAME: &'static str = "TELETEXT";
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn parse_single_entry() {
173        // lang=eng, type=1, mag=2, page=0x10
174        let bytes = [TAG, 5, b'e', b'n', b'g', (1 << 3) | 2, 0x10];
175        let d = TeletextDescriptor::parse(&bytes).unwrap();
176        assert_eq!(d.entries.len(), 1);
177        assert_eq!(d.entries[0].language_code, LangCode(*b"eng"));
178        assert_eq!(d.entries[0].teletext_type, TeletextType::InitialPage);
179        assert_eq!(d.entries[0].magazine_number, 2);
180        assert_eq!(d.entries[0].page_number, 0x10);
181    }
182
183    #[test]
184    fn parse_multiple_entries() {
185        let bytes = [
186            TAG,
187            10,
188            b'e',
189            b'n',
190            b'g',
191            (1 << 3) | 1,
192            0x10,
193            b'f',
194            b'r',
195            b'a',
196            (2 << 3) | 1,
197            0x20,
198        ];
199        let d = TeletextDescriptor::parse(&bytes).unwrap();
200        assert_eq!(d.entries.len(), 2);
201        assert_eq!(d.entries[1].teletext_type, TeletextType::SubtitlePage);
202    }
203
204    #[test]
205    fn parse_rejects_wrong_tag() {
206        assert!(matches!(
207            TeletextDescriptor::parse(&[0x57, 0]).unwrap_err(),
208            Error::InvalidDescriptor { tag: 0x57, .. }
209        ));
210    }
211
212    #[test]
213    fn parse_rejects_length_not_multiple_of_5() {
214        let bytes = [TAG, 4, 0, 0, 0, 0];
215        assert!(matches!(
216            TeletextDescriptor::parse(&bytes).unwrap_err(),
217            Error::InvalidDescriptor { .. }
218        ));
219    }
220
221    #[test]
222    fn serialize_round_trip() {
223        let d = TeletextDescriptor {
224            entries: vec![TeletextEntry {
225                language_code: LangCode(*b"fra"),
226                teletext_type: TeletextType::SubtitlePage,
227                magazine_number: 8 & 0x07,
228                page_number: 0x88,
229            }],
230        };
231        let mut buf = vec![0u8; d.serialized_len()];
232        d.serialize_into(&mut buf).unwrap();
233        assert_eq!(TeletextDescriptor::parse(&buf).unwrap(), d);
234    }
235
236    #[test]
237    fn empty_descriptor_valid() {
238        let bytes = [TAG, 0];
239        let d = TeletextDescriptor::parse(&bytes).unwrap();
240        assert_eq!(d.entries.len(), 0);
241    }
242
243    #[test]
244    fn teletext_type_full_range_round_trip() {
245        for b in 0..=0xFF_u8 {
246            let tt = TeletextType::from_u8(b);
247            assert_eq!(tt.to_u8(), b, "round-trip failed for byte 0x{b:02X}");
248        }
249    }
250
251    #[test]
252    fn teletext_type_name_for_known() {
253        assert_eq!(TeletextType::InitialPage.name(), "initial teletext page");
254        assert_eq!(
255            TeletextType::HearingImpairedSubtitlePage.name(),
256            "teletext subtitle page for hearing impaired people"
257        );
258        assert_eq!(TeletextType::Reserved(0x06).name(), "reserved");
259    }
260}