dvb_si/descriptors/
parental_rating.rs1use super::descriptor_body;
6use crate::error::{Error, Result};
7use crate::text::LangCode;
8use dvb_common::{Parse, Serialize};
9
10pub const TAG: u8 = 0x55;
12const HEADER_LEN: usize = 2;
13const ENTRY_LEN: usize = 4;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize))]
18pub struct RatingEntry {
19 pub country_code: LangCode,
21 pub rating: u8,
23}
24
25impl RatingEntry {
26 #[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#[derive(Debug, Clone, PartialEq, Eq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize))]
39pub struct ParentalRatingDescriptor {
40 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}