Skip to main content

dvb_si/descriptors/
parental_rating.rs

1//! Parental Rating Descriptor — ETSI EN 300 468 §6.2.30 (tag 0x55).
2//!
3//! Carried inside EIT. Per-country minimum-age rating for the event.
4
5use super::descriptor_body;
6use crate::error::{Error, Result};
7use crate::text::LangCode;
8use alloc::vec::Vec;
9use dvb_common::{Parse, Serialize};
10
11/// Descriptor tag for parental_rating_descriptor.
12pub const TAG: u8 = 0x55;
13const HEADER_LEN: usize = 2;
14const ENTRY_LEN: usize = 4;
15
16/// One parental rating entry.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize))]
19pub struct RatingEntry {
20    /// ISO 3166 alpha country code (e.g. `LangCode(*b"FRA")`, `LangCode(*b"GBR")`).
21    pub country_code: LangCode,
22    /// Rating byte per §6.2.30 Table 83.
23    pub rating: u8,
24}
25
26impl RatingEntry {
27    /// Minimum age if `rating` falls in the numeric range, else None.
28    #[must_use]
29    pub fn minimum_age(&self) -> Option<u8> {
30        match self.rating {
31            0x01..=0x0F => Some(self.rating + 3),
32            _ => None,
33        }
34    }
35}
36
37/// Parental Rating Descriptor.
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize))]
40pub struct ParentalRatingDescriptor {
41    /// Entries in wire order.
42    pub entries: Vec<RatingEntry>,
43}
44
45impl<'a> Parse<'a> for ParentalRatingDescriptor {
46    type Error = crate::error::Error;
47    fn parse(bytes: &'a [u8]) -> Result<Self> {
48        let body = descriptor_body(
49            bytes,
50            TAG,
51            "ParentalRatingDescriptor",
52            "unexpected tag for parental_rating_descriptor",
53        )?;
54        if body.len() % ENTRY_LEN != 0 {
55            return Err(Error::InvalidDescriptor {
56                tag: TAG,
57                reason: "descriptor_body_length is not a multiple of 4",
58            });
59        }
60        let num_entries = body.len() / ENTRY_LEN;
61        let mut entries = Vec::with_capacity(num_entries);
62        for chunk in body.chunks_exact(ENTRY_LEN) {
63            entries.push(RatingEntry {
64                country_code: LangCode([chunk[0], chunk[1], chunk[2]]),
65                rating: chunk[3],
66            });
67        }
68        Ok(Self { entries })
69    }
70}
71
72impl Serialize for ParentalRatingDescriptor {
73    type Error = crate::error::Error;
74    fn serialized_len(&self) -> usize {
75        HEADER_LEN + self.entries.len() * ENTRY_LEN
76    }
77
78    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
79        let len = self.serialized_len();
80        if buf.len() < len {
81            return Err(Error::OutputBufferTooSmall {
82                need: len,
83                have: buf.len(),
84            });
85        }
86        buf[0] = TAG;
87        buf[1] = (len - HEADER_LEN) as u8;
88        for (i, entry) in self.entries.iter().enumerate() {
89            let entry_start = HEADER_LEN + i * ENTRY_LEN;
90            buf[entry_start..entry_start + 3].copy_from_slice(&entry.country_code.0);
91            buf[entry_start + 3] = entry.rating;
92        }
93        Ok(len)
94    }
95}
96impl<'a> crate::traits::DescriptorDef<'a> for ParentalRatingDescriptor {
97    const TAG: u8 = TAG;
98    const NAME: &'static str = "PARENTAL_RATING";
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn parse_single_entry_extracts_country_and_rating() {
107        let bytes = [TAG, 4, b'F', b'R', b'A', 0x05];
108        let d = ParentalRatingDescriptor::parse(&bytes).unwrap();
109        assert_eq!(d.entries.len(), 1);
110        assert_eq!(d.entries[0].country_code, LangCode(*b"FRA"));
111        assert_eq!(d.entries[0].rating, 0x05);
112    }
113
114    #[test]
115    fn parse_multiple_entries_preserves_order() {
116        let bytes = [TAG, 8, b'G', b'B', b'R', 0x01, b'U', b'S', b'A', 0x10];
117        let d = ParentalRatingDescriptor::parse(&bytes).unwrap();
118        assert_eq!(d.entries.len(), 2);
119        assert_eq!(d.entries[0].country_code, LangCode(*b"GBR"));
120        assert_eq!(d.entries[0].rating, 0x01);
121        assert_eq!(d.entries[1].country_code, LangCode(*b"USA"));
122        assert_eq!(d.entries[1].rating, 0x10);
123    }
124
125    #[test]
126    fn parse_rejects_wrong_tag() {
127        let err = ParentalRatingDescriptor::parse(&[0x4E, 0]).unwrap_err();
128        assert!(matches!(err, Error::InvalidDescriptor { tag: 0x4E, .. }));
129    }
130
131    #[test]
132    fn parse_rejects_length_not_multiple_of_4() {
133        let bytes = [TAG, 3, b'F', b'R', b'A'];
134        let err = ParentalRatingDescriptor::parse(&bytes).unwrap_err();
135        assert!(matches!(err, Error::InvalidDescriptor { .. }));
136    }
137
138    #[test]
139    fn parse_rejects_truncated_body() {
140        let bytes = [TAG, 4, b'F', b'R'];
141        let err = ParentalRatingDescriptor::parse(&bytes).unwrap_err();
142        assert!(matches!(err, Error::BufferTooShort { .. }));
143    }
144
145    #[test]
146    fn minimum_age_maps_0x01_to_4_years() {
147        let entry = RatingEntry {
148            country_code: LangCode(*b"FRA"),
149            rating: 0x01,
150        };
151        assert_eq!(entry.minimum_age(), Some(4));
152    }
153
154    #[test]
155    fn minimum_age_returns_none_for_rating_0x00() {
156        let entry = RatingEntry {
157            country_code: LangCode(*b"USA"),
158            rating: 0x00,
159        };
160        assert!(entry.minimum_age().is_none());
161    }
162
163    #[test]
164    fn minimum_age_returns_none_for_rating_0x10_and_above() {
165        let entry = RatingEntry {
166            country_code: LangCode(*b"GBR"),
167            rating: 0x10,
168        };
169        assert!(entry.minimum_age().is_none());
170        let entry2 = RatingEntry {
171            country_code: LangCode(*b"JPN"),
172            rating: 0xFF,
173        };
174        assert!(entry2.minimum_age().is_none());
175    }
176
177    #[test]
178    fn serialize_round_trip() {
179        let d = ParentalRatingDescriptor {
180            entries: vec![
181                RatingEntry {
182                    country_code: LangCode(*b"FRA"),
183                    rating: 0x05,
184                },
185                RatingEntry {
186                    country_code: LangCode(*b"GBR"),
187                    rating: 0x01,
188                },
189            ],
190        };
191        let mut buf = vec![0u8; d.serialized_len()];
192        d.serialize_into(&mut buf).unwrap();
193        let re = ParentalRatingDescriptor::parse(&buf).unwrap();
194        assert_eq!(d, re);
195    }
196}