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 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))]
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))]
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        let body = descriptor_body(
48            bytes,
49            TAG,
50            "ParentalRatingDescriptor",
51            "unexpected tag for parental_rating_descriptor",
52        )?;
53        if body.len() % ENTRY_LEN != 0 {
54            return Err(Error::InvalidDescriptor {
55                tag: TAG,
56                reason: "descriptor_body_length is not a multiple of 4",
57            });
58        }
59        let num_entries = body.len() / ENTRY_LEN;
60        let mut entries = Vec::with_capacity(num_entries);
61        for chunk in body.chunks_exact(ENTRY_LEN) {
62            entries.push(RatingEntry {
63                country_code: LangCode([chunk[0], chunk[1], chunk[2]]),
64                rating: chunk[3],
65            });
66        }
67        Ok(Self { entries })
68    }
69}
70
71impl Serialize for ParentalRatingDescriptor {
72    type Error = crate::error::Error;
73    fn serialized_len(&self) -> usize {
74        HEADER_LEN + self.entries.len() * ENTRY_LEN
75    }
76
77    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
78        let len = self.serialized_len();
79        if buf.len() < len {
80            return Err(Error::OutputBufferTooSmall {
81                need: len,
82                have: buf.len(),
83            });
84        }
85        buf[0] = TAG;
86        buf[1] = (len - HEADER_LEN) as u8;
87        for (i, entry) in self.entries.iter().enumerate() {
88            let entry_start = HEADER_LEN + i * ENTRY_LEN;
89            buf[entry_start..entry_start + 3].copy_from_slice(&entry.country_code.0);
90            buf[entry_start + 3] = entry.rating;
91        }
92        Ok(len)
93    }
94}
95impl<'a> crate::traits::DescriptorDef<'a> for ParentalRatingDescriptor {
96    const TAG: u8 = TAG;
97    const NAME: &'static str = "PARENTAL_RATING";
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn parse_single_entry_extracts_country_and_rating() {
106        let bytes = [TAG, 4, b'F', b'R', b'A', 0x05];
107        let d = ParentalRatingDescriptor::parse(&bytes).unwrap();
108        assert_eq!(d.entries.len(), 1);
109        assert_eq!(d.entries[0].country_code, LangCode(*b"FRA"));
110        assert_eq!(d.entries[0].rating, 0x05);
111    }
112
113    #[test]
114    fn parse_multiple_entries_preserves_order() {
115        let bytes = [TAG, 8, b'G', b'B', b'R', 0x01, b'U', b'S', b'A', 0x10];
116        let d = ParentalRatingDescriptor::parse(&bytes).unwrap();
117        assert_eq!(d.entries.len(), 2);
118        assert_eq!(d.entries[0].country_code, LangCode(*b"GBR"));
119        assert_eq!(d.entries[0].rating, 0x01);
120        assert_eq!(d.entries[1].country_code, LangCode(*b"USA"));
121        assert_eq!(d.entries[1].rating, 0x10);
122    }
123
124    #[test]
125    fn parse_rejects_wrong_tag() {
126        let err = ParentalRatingDescriptor::parse(&[0x4E, 0]).unwrap_err();
127        assert!(matches!(err, Error::InvalidDescriptor { tag: 0x4E, .. }));
128    }
129
130    #[test]
131    fn parse_rejects_length_not_multiple_of_4() {
132        let bytes = [TAG, 3, b'F', b'R', b'A'];
133        let err = ParentalRatingDescriptor::parse(&bytes).unwrap_err();
134        assert!(matches!(err, Error::InvalidDescriptor { .. }));
135    }
136
137    #[test]
138    fn parse_rejects_truncated_body() {
139        let bytes = [TAG, 4, b'F', b'R'];
140        let err = ParentalRatingDescriptor::parse(&bytes).unwrap_err();
141        assert!(matches!(err, Error::BufferTooShort { .. }));
142    }
143
144    #[test]
145    fn minimum_age_maps_0x01_to_4_years() {
146        let entry = RatingEntry {
147            country_code: LangCode(*b"FRA"),
148            rating: 0x01,
149        };
150        assert_eq!(entry.minimum_age(), Some(4));
151    }
152
153    #[test]
154    fn minimum_age_returns_none_for_rating_0x00() {
155        let entry = RatingEntry {
156            country_code: LangCode(*b"USA"),
157            rating: 0x00,
158        };
159        assert!(entry.minimum_age().is_none());
160    }
161
162    #[test]
163    fn minimum_age_returns_none_for_rating_0x10_and_above() {
164        let entry = RatingEntry {
165            country_code: LangCode(*b"GBR"),
166            rating: 0x10,
167        };
168        assert!(entry.minimum_age().is_none());
169        let entry2 = RatingEntry {
170            country_code: LangCode(*b"JPN"),
171            rating: 0xFF,
172        };
173        assert!(entry2.minimum_age().is_none());
174    }
175
176    #[test]
177    fn serialize_round_trip() {
178        let d = ParentalRatingDescriptor {
179            entries: vec![
180                RatingEntry {
181                    country_code: LangCode(*b"FRA"),
182                    rating: 0x05,
183                },
184                RatingEntry {
185                    country_code: LangCode(*b"GBR"),
186                    rating: 0x01,
187                },
188            ],
189        };
190        let mut buf = vec![0u8; d.serialized_len()];
191        d.serialize_into(&mut buf).unwrap();
192        let re = ParentalRatingDescriptor::parse(&buf).unwrap();
193        assert_eq!(d, re);
194    }
195}