1use crate::error::{Error, Result};
42use crate::traits::Table;
43use dvb_common::{Parse, Serialize};
44
45pub const TABLE_ID: u8 = 0x7C;
47pub const PID: u16 = 0x0000;
50
51pub const FONT_INFO_TYPE_STYLE_WEIGHT: u8 = 0x00;
53pub const FONT_INFO_TYPE_FILE_URI: u8 = 0x01;
55pub const FONT_INFO_TYPE_FONT_SIZE: u8 = 0x02;
57
58const HEADER_LEN: usize = 8;
61const SECTION_LENGTH_PREFIX: usize = 3;
63const CRC_LEN: usize = 4;
65
66#[derive(Debug, Clone, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize))]
72pub enum FontInfo<'a> {
73 StyleWeight {
75 style: u8,
77 weight: u8,
79 },
80 FileUri {
82 format: u8,
84 uri: &'a [u8],
86 },
87 FontSize {
89 size: u16,
91 info: &'a [u8],
93 },
94 LengthDelimited {
96 font_info_type: u8,
98 info: &'a [u8],
100 },
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize))]
106#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
107pub struct DownloadableFontInfoSection<'a> {
108 pub font_id_extension: u16,
111 pub font_id: u8,
113 pub version_number: u8,
115 pub current_next_indicator: bool,
117 pub section_number: u8,
119 pub last_section_number: u8,
121 pub font_info: Vec<FontInfo<'a>>,
123}
124
125impl<'a> Parse<'a> for DownloadableFontInfoSection<'a> {
126 type Error = crate::error::Error;
127 fn parse(bytes: &'a [u8]) -> Result<Self> {
128 let min_len = HEADER_LEN + CRC_LEN;
129 if bytes.len() < min_len {
130 return Err(Error::BufferTooShort {
131 need: min_len,
132 have: bytes.len(),
133 what: "DownloadableFontInfoSection",
134 });
135 }
136 if bytes[0] != TABLE_ID {
137 return Err(Error::UnexpectedTableId {
138 table_id: bytes[0],
139 what: "DownloadableFontInfoSection",
140 expected: &[TABLE_ID],
141 });
142 }
143 let section_length = (((bytes[1] & 0x0F) as usize) << 8) | bytes[2] as usize;
144 let total = SECTION_LENGTH_PREFIX + section_length;
145 if bytes.len() < total || total < HEADER_LEN + CRC_LEN {
146 return Err(Error::SectionLengthOverflow {
147 declared: section_length,
148 available: bytes.len().saturating_sub(SECTION_LENGTH_PREFIX),
149 });
150 }
151
152 let id_word = u16::from_be_bytes([bytes[3], bytes[4]]);
154 let font_id_extension = id_word >> 7;
155 let font_id = (id_word & 0x7F) as u8;
156 let version_number = (bytes[5] >> 1) & 0x1F;
157 let current_next_indicator = bytes[5] & 0x01 != 0;
158 let section_number = bytes[6];
159 let last_section_number = bytes[7];
160
161 let loop_end = total - CRC_LEN;
162 let mut font_info = Vec::new();
163 let mut pos = HEADER_LEN;
164 while pos < loop_end {
165 let font_info_type = bytes[pos];
166 pos += 1;
167 match font_info_type {
168 FONT_INFO_TYPE_STYLE_WEIGHT => {
169 if pos + 1 > loop_end {
170 return Err(Error::SectionLengthOverflow {
171 declared: 1,
172 available: loop_end - pos,
173 });
174 }
175 let b = bytes[pos];
176 pos += 1;
177 font_info.push(FontInfo::StyleWeight {
178 style: b >> 5,
179 weight: (b >> 1) & 0x0F,
180 });
181 }
182 FONT_INFO_TYPE_FILE_URI => {
183 if pos + 2 > loop_end {
184 return Err(Error::SectionLengthOverflow {
185 declared: 2,
186 available: loop_end - pos,
187 });
188 }
189 let format = bytes[pos] & 0x0F;
190 let uri_length = bytes[pos + 1] as usize;
191 let uri_start = pos + 2;
192 let uri_end = uri_start + uri_length;
193 if uri_end > loop_end {
194 return Err(Error::SectionLengthOverflow {
195 declared: uri_length,
196 available: loop_end - uri_start,
197 });
198 }
199 font_info.push(FontInfo::FileUri {
200 format,
201 uri: &bytes[uri_start..uri_end],
202 });
203 pos = uri_end;
204 }
205 FONT_INFO_TYPE_FONT_SIZE => {
206 if pos + 3 > loop_end {
208 return Err(Error::SectionLengthOverflow {
209 declared: 3,
210 available: loop_end - pos,
211 });
212 }
213 let size = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]);
214 let info_length = bytes[pos + 2] as usize;
215 let info_start = pos + 3;
216 let info_end = info_start + info_length;
217 if info_end > loop_end {
218 return Err(Error::SectionLengthOverflow {
219 declared: info_length,
220 available: loop_end - info_start,
221 });
222 }
223 font_info.push(FontInfo::FontSize {
224 size,
225 info: &bytes[info_start..info_end],
226 });
227 pos = info_end;
228 }
229 _ => {
230 if pos + 1 > loop_end {
232 return Err(Error::SectionLengthOverflow {
233 declared: 1,
234 available: loop_end - pos,
235 });
236 }
237 let info_length = bytes[pos] as usize;
238 let info_start = pos + 1;
239 let info_end = info_start + info_length;
240 if info_end > loop_end {
241 return Err(Error::SectionLengthOverflow {
242 declared: info_length,
243 available: loop_end - info_start,
244 });
245 }
246 font_info.push(FontInfo::LengthDelimited {
247 font_info_type,
248 info: &bytes[info_start..info_end],
249 });
250 pos = info_end;
251 }
252 }
253 }
254
255 Ok(DownloadableFontInfoSection {
256 font_id_extension,
257 font_id,
258 version_number,
259 current_next_indicator,
260 section_number,
261 last_section_number,
262 font_info,
263 })
264 }
265}
266
267impl Serialize for DownloadableFontInfoSection<'_> {
268 type Error = crate::error::Error;
269 fn serialized_len(&self) -> usize {
270 let loop_bytes: usize = self
271 .font_info
272 .iter()
273 .map(|f| match f {
274 FontInfo::StyleWeight { .. } => 2, FontInfo::FileUri { uri, .. } => 1 + 2 + uri.len(), FontInfo::FontSize { info, .. } => 1 + 2 + 1 + info.len(), FontInfo::LengthDelimited { info, .. } => 1 + 1 + info.len(), })
279 .sum();
280 HEADER_LEN + loop_bytes + CRC_LEN
281 }
282 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
283 let len = self.serialized_len();
284 if buf.len() < len {
285 return Err(Error::OutputBufferTooSmall {
286 need: len,
287 have: buf.len(),
288 });
289 }
290 let section_length = (len - SECTION_LENGTH_PREFIX) as u16;
291 buf[0] = TABLE_ID;
292 buf[1] = 0xB0 | ((section_length >> 8) as u8 & 0x0F);
294 buf[2] = (section_length & 0xFF) as u8;
295 let id_word = ((self.font_id_extension & 0x01FF) << 7) | (self.font_id as u16 & 0x7F);
297 buf[3..5].copy_from_slice(&id_word.to_be_bytes());
298 buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
300 buf[6] = self.section_number;
301 buf[7] = self.last_section_number;
302
303 let guard_u8 = |len: usize| -> Result<()> {
306 if len > u8::MAX as usize {
307 return Err(Error::SectionLengthOverflow {
308 declared: len,
309 available: u8::MAX as usize,
310 });
311 }
312 Ok(())
313 };
314
315 let mut pos = HEADER_LEN;
316 for f in &self.font_info {
317 match f {
318 FontInfo::StyleWeight { style, weight } => {
319 buf[pos] = FONT_INFO_TYPE_STYLE_WEIGHT;
320 buf[pos + 1] = ((style & 0x07) << 5) | ((weight & 0x0F) << 1);
322 pos += 2;
323 }
324 FontInfo::FileUri { format, uri } => {
325 guard_u8(uri.len())?;
326 buf[pos] = FONT_INFO_TYPE_FILE_URI;
327 buf[pos + 1] = format & 0x0F;
329 buf[pos + 2] = uri.len() as u8;
330 let s = pos + 3;
331 buf[s..s + uri.len()].copy_from_slice(uri);
332 pos = s + uri.len();
333 }
334 FontInfo::FontSize { size, info } => {
335 guard_u8(info.len())?;
336 buf[pos] = FONT_INFO_TYPE_FONT_SIZE;
337 buf[pos + 1..pos + 3].copy_from_slice(&size.to_be_bytes());
338 buf[pos + 3] = info.len() as u8;
339 let s = pos + 4;
340 buf[s..s + info.len()].copy_from_slice(info);
341 pos = s + info.len();
342 }
343 FontInfo::LengthDelimited {
344 font_info_type,
345 info,
346 } => {
347 guard_u8(info.len())?;
348 buf[pos] = *font_info_type;
349 buf[pos + 1] = info.len() as u8;
350 let s = pos + 2;
351 buf[s..s + info.len()].copy_from_slice(info);
352 pos = s + info.len();
353 }
354 }
355 }
356
357 let crc = dvb_common::crc32_mpeg2::compute(&buf[..pos]);
358 buf[pos..len].copy_from_slice(&crc.to_be_bytes());
359 Ok(len)
360 }
361}
362
363impl<'a> Table<'a> for DownloadableFontInfoSection<'a> {
364 const TABLE_ID: u8 = TABLE_ID;
365 const PID: u16 = PID;
366}
367
368impl<'a> crate::traits::TableDef<'a> for DownloadableFontInfoSection<'a> {
369 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
370 const NAME: &'static str = "DOWNLOADABLE_FONT_INFO";
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 fn build_section(font_id: u8, version: u8, loop_body: &[u8]) -> Vec<u8> {
379 let section_length =
380 (HEADER_LEN - SECTION_LENGTH_PREFIX + loop_body.len() + CRC_LEN) as u16;
381 let id_word = (font_id as u16) & 0x7F;
383 let mut v = vec![
384 TABLE_ID,
385 0xB0 | ((section_length >> 8) as u8 & 0x0F),
386 (section_length & 0xFF) as u8,
387 (id_word >> 8) as u8,
388 (id_word & 0xFF) as u8,
389 0xC0 | (version << 1) | 0x01,
390 0x00,
391 0x00,
392 ];
393 v.extend_from_slice(loop_body);
394 v.extend_from_slice(&[0, 0, 0, 0]);
395 v
396 }
397
398 fn mixed_loop() -> Vec<u8> {
400 let uri = b"https://f.example/Droid.otf";
401 let family = b"Droid Sans";
402 let mut b = vec![
403 FONT_INFO_TYPE_STYLE_WEIGHT, (2u8 << 5) | (2u8 << 1), FONT_INFO_TYPE_FILE_URI, 0x01, uri.len() as u8, ];
409 b.extend_from_slice(uri);
410 b.push(FONT_INFO_TYPE_FONT_SIZE);
412 b.extend_from_slice(&24u16.to_be_bytes());
413 b.push(2);
414 b.extend_from_slice(b"px");
415 b.push(0x03);
417 b.push(family.len() as u8);
418 b.extend_from_slice(family);
419 b
420 }
421
422 #[test]
423 fn parse_header_fields() {
424 let bytes = build_section(0x42, 9, &[]);
425 let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
426 assert_eq!(sec.font_id, 0x42);
427 assert_eq!(sec.font_id_extension, 0);
428 assert_eq!(sec.version_number, 9);
429 assert!(sec.current_next_indicator);
430 assert!(sec.font_info.is_empty());
431 }
432
433 #[test]
434 fn parse_all_variants() {
435 let bytes = build_section(1, 0, &mixed_loop());
436 let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
437 assert_eq!(sec.font_info.len(), 4);
438 assert_eq!(
439 sec.font_info[0],
440 FontInfo::StyleWeight {
441 style: 2,
442 weight: 2
443 }
444 );
445 match &sec.font_info[1] {
446 FontInfo::FileUri { format, uri } => {
447 assert_eq!(*format, 1);
448 assert_eq!(*uri, b"https://f.example/Droid.otf");
449 }
450 other => panic!("expected FileUri, got {other:?}"),
451 }
452 match &sec.font_info[2] {
453 FontInfo::FontSize { size, info } => {
454 assert_eq!(*size, 24);
455 assert_eq!(*info, b"px");
456 }
457 other => panic!("expected FontSize, got {other:?}"),
458 }
459 match &sec.font_info[3] {
460 FontInfo::LengthDelimited {
461 font_info_type,
462 info,
463 } => {
464 assert_eq!(*font_info_type, 0x03);
465 assert_eq!(*info, b"Droid Sans");
466 }
467 other => panic!("expected LengthDelimited, got {other:?}"),
468 }
469 }
470
471 #[test]
472 fn reserved_type_round_trips_as_length_delimited() {
473 let mut body = vec![0x77u8, 0x03];
475 body.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
476 let bytes = build_section(1, 0, &body);
477 let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
478 assert_eq!(
479 sec.font_info[0],
480 FontInfo::LengthDelimited {
481 font_info_type: 0x77,
482 info: &[0xAA, 0xBB, 0xCC]
483 }
484 );
485 }
486
487 #[test]
488 fn parse_rejects_wrong_tag() {
489 let mut bytes = build_section(1, 0, &mixed_loop());
490 bytes[0] = 0x4C; assert!(matches!(
492 DownloadableFontInfoSection::parse(&bytes).unwrap_err(),
493 Error::UnexpectedTableId { table_id: 0x4C, .. }
494 ));
495 }
496
497 #[test]
498 fn rejects_short_buffer() {
499 assert!(matches!(
500 DownloadableFontInfoSection::parse(&[0x7C, 0xB0]).unwrap_err(),
501 Error::BufferTooShort {
502 what: "DownloadableFontInfoSection",
503 ..
504 }
505 ));
506 }
507
508 #[test]
509 fn uri_length_overflow_rejected() {
510 let body = vec![FONT_INFO_TYPE_FILE_URI, 0x01, 0x20];
512 let bytes = build_section(1, 0, &body);
513 assert!(matches!(
514 DownloadableFontInfoSection::parse(&bytes).unwrap_err(),
515 Error::SectionLengthOverflow { .. }
516 ));
517 }
518
519 #[test]
520 fn round_trip_all_variants() {
521 let bytes = build_section(0x33, 4, &mixed_loop());
522 let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
523 let mut buf = vec![0u8; sec.serialized_len()];
524 sec.serialize_into(&mut buf).unwrap();
525 let re = DownloadableFontInfoSection::parse(&buf).unwrap();
526 assert_eq!(sec, re);
527 }
528
529 #[test]
530 fn table_trait_constants() {
531 assert_eq!(<DownloadableFontInfoSection as Table>::TABLE_ID, 0x7C);
532 assert_eq!(<DownloadableFontInfoSection as Table>::PID, 0x0000);
533 }
534
535 #[test]
536 #[cfg(feature = "serde")]
537 fn serde_json_round_trip() {
538 let bytes = build_section(1, 0, &mixed_loop());
539 let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
540 let j = serde_json::to_string(&sec).unwrap();
541 let reparsed = DownloadableFontInfoSection::parse(&bytes).unwrap();
547 assert_eq!(serde_json::to_string(&reparsed).unwrap(), j);
548 assert!(j.contains("\"font_id\":1"));
549 }
550}