Skip to main content

dvb_si/tables/
downloadable_font_info.rs

1//! Downloadable Font Information Section (DFIS) — ETSI EN 303 560 v1.1.1
2//! §5.3.2.3.1 (table_id 0x7C).
3//!
4//! The DFIS conveys download location and font metadata for a single font or
5//! font family. Sections of a Downloadable Font Information Table (DFIT) are
6//! carried together on one PID, signalled in the PMT by a `data_broadcast_id`
7//! descriptor with `data_broadcast_id` 0x000D (§5.3.2.3.1) — there is no
8//! well-known PID, so [`PID`] follows the `dsmcc.rs` "no fixed PID" convention.
9//!
10//! ## table_id
11//!
12//! EN 303 560 v1.1.1 §5.3.2.3.1 says `table_id` 0x4C — an acknowledged
13//! allocation accident: 0x4C was already the INT. EN 300 468 V1.19.1 Table 2
14//! NOTE 2 ("table_id 0x4C was previously accidentally assigned to both of
15//! these two DVB specifications, this has now been corrected") reassigns the
16//! DFIS to **0x7C**, which is what [`TABLE_ID`] and the crate registry use.
17//!
18//! ## The 0x02 conditional (resolved against the PDF, pp. 30-31)
19//!
20//! Table 22's syntax has two consecutive conditionals that both fire for
21//! `font_info_type == 0x02`:
22//!
23//! ```text
24//! if (font_info_type == 0x02) { font_size (16) }
25//! if (font_info_type >= 0x02) { font_info_length (8) + text_char ... }
26//! ```
27//!
28//! This is **not** a typo and **not** double-handling: a type-0x02 entry
29//! carries the 16-bit `font_size` *followed by* the length-prefixed string
30//! block. Every `font_info_type >= 0x02` is length-delimited by
31//! `font_info_length` (the 0x02 case additionally prefixes the 2-byte
32//! `font_size`). This makes types 0x03 (`font_family`) and all reserved types
33//! 0x04..=0xFF safely skippable, so they round-trip as
34//! [`FontInfo::LengthDelimited`]. Verified against EN 303 560 v1.1.1 PDF
35//! pp. 30-31 (Table 22) and p. 32 (Table 23 type allocation).
36//!
37//! Per crate contract this parser does NOT verify CRC_32 (use
38//! `Section::validate_crc`). Reserved bits are ignored on parse; spec-mandated
39//! zero fields (`font_id_extension`, `reserved_zero_future_use`) are emitted 0.
40
41use crate::error::{Error, Result};
42use crate::traits::Table;
43use dvb_common::{Parse, Serialize};
44
45/// table_id for the DFIS — the crate registry value (see module docs re. spec 0x4C).
46pub const TABLE_ID: u8 = 0x7C;
47/// DFIS has no well-known PID; carried on the PID signalled by the
48/// `data_broadcast_id` (0x000D) descriptor in the PMT (§5.3.2.3.1).
49pub const PID: u16 = 0x0000;
50
51/// `font_info_type` for style/weight (§5.3.2.3.2.1 Table 23).
52pub const FONT_INFO_TYPE_STYLE_WEIGHT: u8 = 0x00;
53/// `font_info_type` for a font file URI (Table 23).
54pub const FONT_INFO_TYPE_FILE_URI: u8 = 0x01;
55/// `font_info_type` for font size in pixels (Table 23).
56pub const FONT_INFO_TYPE_FONT_SIZE: u8 = 0x02;
57
58/// table_id(1) + section_length(2) + font_id_extension/font_id(2)
59/// + version/cni(1) + section_number(1) + last_section_number(1) = 8-byte header.
60const HEADER_LEN: usize = 8;
61/// `section_length` counts from just after the field (byte 3) to end of section.
62const SECTION_LENGTH_PREFIX: usize = 3;
63/// CRC_32 trailer.
64const CRC_LEN: usize = 4;
65
66/// One entry in the DFIS font_info loop (§5.3.2.3.1 Table 22).
67///
68/// Variant is selected by `font_info_type` (Table 23). Reserved types
69/// (0x04..=0xFF) are length-delimited and round-trip via [`FontInfo::LengthDelimited`].
70#[derive(Debug, Clone, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub enum FontInfo<'a> {
73    /// `font_info_type == 0x00`: style(3) + weight(4) + reserved(1).
74    StyleWeight {
75        /// `font_style` (§5.3.2.3.2.2 Table 24): 0 undefined, 1 normal, 2 italic, 3 oblique.
76        style: u8,
77        /// `font_weight` (Table 25): 0 undefined, 1 normal, 2 bold.
78        weight: u8,
79    },
80    /// `font_info_type == 0x01`: reserved(4) + font_file_format(4) + uri_length(8) + uri.
81    FileUri {
82        /// `font_file_format` (§5.3.2.3.2.3 Table 26): 0 = OFF, 1 = WOFF.
83        format: u8,
84        /// DVB URI string (UTF-8), `uri_length` bytes.
85        #[cfg_attr(feature = "serde", serde(borrow))]
86        uri: &'a [u8],
87    },
88    /// `font_info_type == 0x02`: font_size(16) followed by the length-delimited block.
89    FontSize {
90        /// `font_size` — font height in pixels.
91        size: u16,
92        /// `text_char` block following `font_info_length` (UTF-8).
93        #[cfg_attr(feature = "serde", serde(borrow))]
94        info: &'a [u8],
95    },
96    /// `font_info_type >= 0x03` (incl. reserved): font_info_length(8) + text_char block.
97    LengthDelimited {
98        /// The `font_info_type` byte as parsed (0x03 = font_family, else reserved).
99        font_info_type: u8,
100        /// `text_char` block, `font_info_length` bytes (UTF-8 for defined types).
101        #[cfg_attr(feature = "serde", serde(borrow))]
102        info: &'a [u8],
103    },
104}
105
106/// Downloadable Font Information Section (EN 303 560 §5.3.2.3.1, Table 22).
107#[derive(Debug, Clone, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
110pub struct DownloadableFontInfoSection<'a> {
111    /// 9-bit `font_id_extension` — spec-mandated all-zero; together with
112    /// `font_id` forms the 16-bit table_id_extension.
113    pub font_id_extension: u16,
114    /// 7-bit `font_id` identifying the sub_table (one font/family).
115    pub font_id: u8,
116    /// 5-bit version_number.
117    pub version_number: u8,
118    /// current_next_indicator bit.
119    pub current_next_indicator: bool,
120    /// section_number.
121    pub section_number: u8,
122    /// last_section_number.
123    pub last_section_number: u8,
124    /// font_info loop entries in wire order.
125    #[cfg_attr(feature = "serde", serde(borrow))]
126    pub font_info: Vec<FontInfo<'a>>,
127}
128
129impl<'a> Parse<'a> for DownloadableFontInfoSection<'a> {
130    type Error = crate::error::Error;
131    fn parse(bytes: &'a [u8]) -> Result<Self> {
132        let min_len = HEADER_LEN + CRC_LEN;
133        if bytes.len() < min_len {
134            return Err(Error::BufferTooShort {
135                need: min_len,
136                have: bytes.len(),
137                what: "DownloadableFontInfoSection",
138            });
139        }
140        if bytes[0] != TABLE_ID {
141            return Err(Error::UnexpectedTableId {
142                table_id: bytes[0],
143                what: "DownloadableFontInfoSection",
144                expected: &[TABLE_ID],
145            });
146        }
147        let section_length = (((bytes[1] & 0x0F) as usize) << 8) | bytes[2] as usize;
148        let total = SECTION_LENGTH_PREFIX + section_length;
149        if bytes.len() < total || total < HEADER_LEN + CRC_LEN {
150            return Err(Error::SectionLengthOverflow {
151                declared: section_length,
152                available: bytes.len().saturating_sub(SECTION_LENGTH_PREFIX),
153            });
154        }
155
156        // bytes[3..5] = font_id_extension(9) | font_id(7).
157        let id_word = u16::from_be_bytes([bytes[3], bytes[4]]);
158        let font_id_extension = id_word >> 7;
159        let font_id = (id_word & 0x7F) as u8;
160        let version_number = (bytes[5] >> 1) & 0x1F;
161        let current_next_indicator = bytes[5] & 0x01 != 0;
162        let section_number = bytes[6];
163        let last_section_number = bytes[7];
164
165        let loop_end = total - CRC_LEN;
166        let mut font_info = Vec::new();
167        let mut pos = HEADER_LEN;
168        while pos < loop_end {
169            let font_info_type = bytes[pos];
170            pos += 1;
171            match font_info_type {
172                FONT_INFO_TYPE_STYLE_WEIGHT => {
173                    if pos + 1 > loop_end {
174                        return Err(Error::SectionLengthOverflow {
175                            declared: 1,
176                            available: loop_end - pos,
177                        });
178                    }
179                    let b = bytes[pos];
180                    pos += 1;
181                    font_info.push(FontInfo::StyleWeight {
182                        style: b >> 5,
183                        weight: (b >> 1) & 0x0F,
184                    });
185                }
186                FONT_INFO_TYPE_FILE_URI => {
187                    if pos + 2 > loop_end {
188                        return Err(Error::SectionLengthOverflow {
189                            declared: 2,
190                            available: loop_end - pos,
191                        });
192                    }
193                    let format = bytes[pos] & 0x0F;
194                    let uri_length = bytes[pos + 1] as usize;
195                    let uri_start = pos + 2;
196                    let uri_end = uri_start + uri_length;
197                    if uri_end > loop_end {
198                        return Err(Error::SectionLengthOverflow {
199                            declared: uri_length,
200                            available: loop_end - uri_start,
201                        });
202                    }
203                    font_info.push(FontInfo::FileUri {
204                        format,
205                        uri: &bytes[uri_start..uri_end],
206                    });
207                    pos = uri_end;
208                }
209                FONT_INFO_TYPE_FONT_SIZE => {
210                    // font_size(16) then font_info_length(8) + block (Table 22, type >= 0x02).
211                    if pos + 3 > loop_end {
212                        return Err(Error::SectionLengthOverflow {
213                            declared: 3,
214                            available: loop_end - pos,
215                        });
216                    }
217                    let size = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]);
218                    let info_length = bytes[pos + 2] as usize;
219                    let info_start = pos + 3;
220                    let info_end = info_start + info_length;
221                    if info_end > loop_end {
222                        return Err(Error::SectionLengthOverflow {
223                            declared: info_length,
224                            available: loop_end - info_start,
225                        });
226                    }
227                    font_info.push(FontInfo::FontSize {
228                        size,
229                        info: &bytes[info_start..info_end],
230                    });
231                    pos = info_end;
232                }
233                _ => {
234                    // font_info_type >= 0x03: font_info_length(8) + text_char block.
235                    if pos + 1 > loop_end {
236                        return Err(Error::SectionLengthOverflow {
237                            declared: 1,
238                            available: loop_end - pos,
239                        });
240                    }
241                    let info_length = bytes[pos] as usize;
242                    let info_start = pos + 1;
243                    let info_end = info_start + info_length;
244                    if info_end > loop_end {
245                        return Err(Error::SectionLengthOverflow {
246                            declared: info_length,
247                            available: loop_end - info_start,
248                        });
249                    }
250                    font_info.push(FontInfo::LengthDelimited {
251                        font_info_type,
252                        info: &bytes[info_start..info_end],
253                    });
254                    pos = info_end;
255                }
256            }
257        }
258
259        Ok(DownloadableFontInfoSection {
260            font_id_extension,
261            font_id,
262            version_number,
263            current_next_indicator,
264            section_number,
265            last_section_number,
266            font_info,
267        })
268    }
269}
270
271impl Serialize for DownloadableFontInfoSection<'_> {
272    type Error = crate::error::Error;
273    fn serialized_len(&self) -> usize {
274        let loop_bytes: usize = self
275            .font_info
276            .iter()
277            .map(|f| match f {
278                FontInfo::StyleWeight { .. } => 2, // type + 1 packed byte
279                FontInfo::FileUri { uri, .. } => 1 + 2 + uri.len(), // type + (fmt|len) + uri
280                FontInfo::FontSize { info, .. } => 1 + 2 + 1 + info.len(), // type + size + len + info
281                FontInfo::LengthDelimited { info, .. } => 1 + 1 + info.len(), // type + len + info
282            })
283            .sum();
284        HEADER_LEN + loop_bytes + CRC_LEN
285    }
286    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
287        let len = self.serialized_len();
288        if buf.len() < len {
289            return Err(Error::OutputBufferTooSmall {
290                need: len,
291                have: buf.len(),
292            });
293        }
294        let section_length = (len - SECTION_LENGTH_PREFIX) as u16;
295        buf[0] = TABLE_ID;
296        // section_syntax_indicator=1, reserved_future_use=0, reserved=11, section_length hi nibble.
297        buf[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
298        buf[2] = (section_length & 0xFF) as u8;
299        // font_id_extension(9) | font_id(7); spec mandates extension all-zero.
300        let id_word = ((self.font_id_extension & 0x01FF) << 7) | (self.font_id as u16 & 0x7F);
301        buf[3..5].copy_from_slice(&id_word.to_be_bytes());
302        // reserved(2)=11, version_number(5), current_next_indicator(1).
303        buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
304        buf[6] = self.section_number;
305        buf[7] = self.last_section_number;
306
307        // 8-bit length prefixes error on over-range payloads rather than
308        // silently truncating (the crate's strict serialize idiom).
309        let guard_u8 = |len: usize| -> Result<()> {
310            if len > u8::MAX as usize {
311                return Err(Error::SectionLengthOverflow {
312                    declared: len,
313                    available: u8::MAX as usize,
314                });
315            }
316            Ok(())
317        };
318
319        let mut pos = HEADER_LEN;
320        for f in &self.font_info {
321            match f {
322                FontInfo::StyleWeight { style, weight } => {
323                    buf[pos] = FONT_INFO_TYPE_STYLE_WEIGHT;
324                    // style(3) | weight(4) | reserved_zero_future_use(1)=0.
325                    buf[pos + 1] = ((style & 0x07) << 5) | ((weight & 0x0F) << 1);
326                    pos += 2;
327                }
328                FontInfo::FileUri { format, uri } => {
329                    guard_u8(uri.len())?;
330                    buf[pos] = FONT_INFO_TYPE_FILE_URI;
331                    // reserved_zero_future_use(4)=0 | font_file_format(4).
332                    buf[pos + 1] = format & 0x0F;
333                    buf[pos + 2] = uri.len() as u8;
334                    let s = pos + 3;
335                    buf[s..s + uri.len()].copy_from_slice(uri);
336                    pos = s + uri.len();
337                }
338                FontInfo::FontSize { size, info } => {
339                    guard_u8(info.len())?;
340                    buf[pos] = FONT_INFO_TYPE_FONT_SIZE;
341                    buf[pos + 1..pos + 3].copy_from_slice(&size.to_be_bytes());
342                    buf[pos + 3] = info.len() as u8;
343                    let s = pos + 4;
344                    buf[s..s + info.len()].copy_from_slice(info);
345                    pos = s + info.len();
346                }
347                FontInfo::LengthDelimited {
348                    font_info_type,
349                    info,
350                } => {
351                    guard_u8(info.len())?;
352                    buf[pos] = *font_info_type;
353                    buf[pos + 1] = info.len() as u8;
354                    let s = pos + 2;
355                    buf[s..s + info.len()].copy_from_slice(info);
356                    pos = s + info.len();
357                }
358            }
359        }
360
361        let crc = dvb_common::crc32_mpeg2::compute(&buf[..pos]);
362        buf[pos..len].copy_from_slice(&crc.to_be_bytes());
363        Ok(len)
364    }
365}
366
367impl<'a> Table<'a> for DownloadableFontInfoSection<'a> {
368    const TABLE_ID: u8 = TABLE_ID;
369    const PID: u16 = PID;
370}
371
372impl<'a> crate::traits::TableDef<'a> for DownloadableFontInfoSection<'a> {
373    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
374    const NAME: &'static str = "DOWNLOADABLE_FONT_INFO";
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    /// Wrap a font_info loop body in the 8-byte common header + placeholder CRC.
382    fn build_section(font_id: u8, version: u8, loop_body: &[u8]) -> Vec<u8> {
383        let section_length =
384            (HEADER_LEN - SECTION_LENGTH_PREFIX + loop_body.len() + CRC_LEN) as u16;
385        // font_id_extension = 0 (spec-mandated), font_id in low 7 bits.
386        let id_word = (font_id as u16) & 0x7F;
387        let mut v = vec![
388            TABLE_ID,
389            0xB0 | ((section_length >> 8) as u8 & 0x0F),
390            (section_length & 0xFF) as u8,
391            (id_word >> 8) as u8,
392            (id_word & 0xFF) as u8,
393            0xC0 | (version << 1) | 0x01,
394            0x00,
395            0x00,
396        ];
397        v.extend_from_slice(loop_body);
398        v.extend_from_slice(&[0, 0, 0, 0]);
399        v
400    }
401
402    /// Build a font_info loop with one of each variant.
403    fn mixed_loop() -> Vec<u8> {
404        let uri = b"https://f.example/Droid.otf";
405        let family = b"Droid Sans";
406        let mut b = vec![
407            FONT_INFO_TYPE_STYLE_WEIGHT, // type 0x00
408            (2u8 << 5) | (2u8 << 1),     // style=2 (italic), weight=2 (bold)
409            FONT_INFO_TYPE_FILE_URI,     // type 0x01
410            0x01,                        // format=1 (WOFF)
411            uri.len() as u8,             // uri_length
412        ];
413        b.extend_from_slice(uri);
414        // type 0x02 — font_size=24, info block "px"
415        b.push(FONT_INFO_TYPE_FONT_SIZE);
416        b.extend_from_slice(&24u16.to_be_bytes());
417        b.push(2);
418        b.extend_from_slice(b"px");
419        // type 0x03 — font_family
420        b.push(0x03);
421        b.push(family.len() as u8);
422        b.extend_from_slice(family);
423        b
424    }
425
426    #[test]
427    fn parse_header_fields() {
428        let bytes = build_section(0x42, 9, &[]);
429        let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
430        assert_eq!(sec.font_id, 0x42);
431        assert_eq!(sec.font_id_extension, 0);
432        assert_eq!(sec.version_number, 9);
433        assert!(sec.current_next_indicator);
434        assert!(sec.font_info.is_empty());
435    }
436
437    #[test]
438    fn parse_all_variants() {
439        let bytes = build_section(1, 0, &mixed_loop());
440        let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
441        assert_eq!(sec.font_info.len(), 4);
442        assert_eq!(
443            sec.font_info[0],
444            FontInfo::StyleWeight {
445                style: 2,
446                weight: 2
447            }
448        );
449        match &sec.font_info[1] {
450            FontInfo::FileUri { format, uri } => {
451                assert_eq!(*format, 1);
452                assert_eq!(*uri, b"https://f.example/Droid.otf");
453            }
454            other => panic!("expected FileUri, got {other:?}"),
455        }
456        match &sec.font_info[2] {
457            FontInfo::FontSize { size, info } => {
458                assert_eq!(*size, 24);
459                assert_eq!(*info, b"px");
460            }
461            other => panic!("expected FontSize, got {other:?}"),
462        }
463        match &sec.font_info[3] {
464            FontInfo::LengthDelimited {
465                font_info_type,
466                info,
467            } => {
468                assert_eq!(*font_info_type, 0x03);
469                assert_eq!(*info, b"Droid Sans");
470            }
471            other => panic!("expected LengthDelimited, got {other:?}"),
472        }
473    }
474
475    #[test]
476    fn reserved_type_round_trips_as_length_delimited() {
477        // type 0x77 (reserved) is length-delimited and skippable.
478        let mut body = vec![0x77u8, 0x03];
479        body.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
480        let bytes = build_section(1, 0, &body);
481        let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
482        assert_eq!(
483            sec.font_info[0],
484            FontInfo::LengthDelimited {
485                font_info_type: 0x77,
486                info: &[0xAA, 0xBB, 0xCC]
487            }
488        );
489    }
490
491    #[test]
492    fn parse_rejects_wrong_tag() {
493        let mut bytes = build_section(1, 0, &mixed_loop());
494        bytes[0] = 0x4C; // INT table_id
495        assert!(matches!(
496            DownloadableFontInfoSection::parse(&bytes).unwrap_err(),
497            Error::UnexpectedTableId { table_id: 0x4C, .. }
498        ));
499    }
500
501    #[test]
502    fn rejects_short_buffer() {
503        assert!(matches!(
504            DownloadableFontInfoSection::parse(&[0x7C, 0xB0]).unwrap_err(),
505            Error::BufferTooShort {
506                what: "DownloadableFontInfoSection",
507                ..
508            }
509        ));
510    }
511
512    #[test]
513    fn uri_length_overflow_rejected() {
514        // type 0x01, uri_length 0x20 but no uri bytes present.
515        let body = vec![FONT_INFO_TYPE_FILE_URI, 0x01, 0x20];
516        let bytes = build_section(1, 0, &body);
517        assert!(matches!(
518            DownloadableFontInfoSection::parse(&bytes).unwrap_err(),
519            Error::SectionLengthOverflow { .. }
520        ));
521    }
522
523    #[test]
524    fn round_trip_all_variants() {
525        let bytes = build_section(0x33, 4, &mixed_loop());
526        let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
527        let mut buf = vec![0u8; sec.serialized_len()];
528        sec.serialize_into(&mut buf).unwrap();
529        let re = DownloadableFontInfoSection::parse(&buf).unwrap();
530        assert_eq!(sec, re);
531    }
532
533    #[test]
534    fn table_trait_constants() {
535        assert_eq!(<DownloadableFontInfoSection as Table>::TABLE_ID, 0x7C);
536        assert_eq!(<DownloadableFontInfoSection as Table>::PID, 0x0000);
537    }
538
539    #[test]
540    #[cfg(feature = "serde")]
541    fn serde_json_round_trip() {
542        let bytes = build_section(1, 0, &mixed_loop());
543        let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
544        let j = serde_json::to_string(&sec).unwrap();
545        // The borrowed `uri`/`info` `&[u8]` fields cannot be JSON-deserialized
546        // zero-copy (serde_json renders them as number sequences, not borrowed
547        // byte arrays) — the crate-wide constraint affecting every
548        // borrowed-slice table (cf. mpe.rs). Exercise the derive through the
549        // WIRE form: a re-parse must serialize to byte-identical JSON.
550        let reparsed = DownloadableFontInfoSection::parse(&bytes).unwrap();
551        assert_eq!(serde_json::to_string(&reparsed).unwrap(), j);
552        assert!(j.contains("\"font_id\":1"));
553    }
554}