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