1use super::descriptor_body;
7use crate::error::{Error, Result};
8use crate::text::LangCode;
9use dvb_common::{Parse, Serialize};
10
11pub const TAG: u8 = 0x58;
13const HEADER_LEN: usize = 2;
14const ENTRY_LEN: usize = 13;
15const POLARITY_MASK: u8 = 0x01;
16const REGION_ID_MASK: u8 = 0xFC;
17const RESERVED_BIT_MASK: u8 = 0x02;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize))]
22pub struct LocalTimeOffsetEntry {
23 pub country_code: LangCode,
25 pub country_region_id: u8,
27 pub local_time_offset_negative: bool,
30 pub local_time_offset_bcd: u16,
32 pub time_of_change_raw: [u8; 5],
34 pub next_time_offset_bcd: u16,
36}
37
38#[cfg(feature = "chrono")]
41fn decode_hhmm(bcd: u16, negative: bool) -> Option<chrono::Duration> {
42 let h = dvb_common::bcd::from_bcd_byte((bcd >> 8) as u8)?;
43 let m = dvb_common::bcd::from_bcd_byte((bcd & 0xFF) as u8)?;
44 let mins = i64::from(h) * 60 + i64::from(m);
45 Some(chrono::Duration::minutes(if negative {
46 -mins
47 } else {
48 mins
49 }))
50}
51
52#[cfg(feature = "chrono")]
55fn encode_hhmm(offset: chrono::Duration) -> Option<(bool, u16)> {
56 let negative = offset < chrono::Duration::zero();
57 let total_min = offset.num_minutes().unsigned_abs();
58 let h = total_min / 60;
59 let m = total_min % 60;
60 if h > 99 {
61 return None;
62 }
63 let hb = dvb_common::bcd::to_bcd_byte(h as u8)?;
64 let mb = dvb_common::bcd::to_bcd_byte(m as u8)?;
65 Some((negative, (u16::from(hb) << 8) | u16::from(mb)))
66}
67
68#[cfg(feature = "chrono")]
69impl LocalTimeOffsetEntry {
70 #[must_use]
74 pub fn local_time_offset(&self) -> Option<chrono::Duration> {
75 decode_hhmm(self.local_time_offset_bcd, self.local_time_offset_negative)
76 }
77
78 #[must_use]
82 pub fn next_time_offset(&self) -> Option<chrono::Duration> {
83 decode_hhmm(self.next_time_offset_bcd, self.local_time_offset_negative)
84 }
85
86 #[must_use]
89 pub fn time_of_change(&self) -> Option<chrono::DateTime<chrono::Utc>> {
90 dvb_common::time::decode_mjd_bcd_utc(self.time_of_change_raw)
91 }
92
93 pub fn set_time_of_change(&mut self, dt: chrono::DateTime<chrono::Utc>) -> Result<()> {
99 self.time_of_change_raw =
100 dvb_common::time::encode_mjd_bcd_utc(dt).ok_or(Error::ValueOutOfRange {
101 field: "LocalTimeOffsetEntry::time_of_change",
102 reason: "date not representable in 16-bit MJD",
103 })?;
104 Ok(())
105 }
106
107 pub fn set_offsets(&mut self, local: chrono::Duration, next: chrono::Duration) -> Result<()> {
116 let oor = |reason| Error::ValueOutOfRange {
117 field: "LocalTimeOffsetEntry offsets",
118 reason,
119 };
120 let local_neg = local < chrono::Duration::zero();
121 let next_neg = next < chrono::Duration::zero();
122 if local_neg != next_neg && !local.is_zero() && !next.is_zero() {
123 return Err(oor("local and next offsets must share a sign"));
124 }
125 let (lneg, lbcd) = encode_hhmm(local).ok_or(oor("local offset magnitude too large"))?;
126 let (nneg, nbcd) = encode_hhmm(next).ok_or(oor("next offset magnitude too large"))?;
127 self.local_time_offset_negative = lneg || nneg;
128 self.local_time_offset_bcd = lbcd;
129 self.next_time_offset_bcd = nbcd;
130 Ok(())
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize))]
137pub struct LocalTimeOffsetDescriptor {
138 pub entries: Vec<LocalTimeOffsetEntry>,
140}
141
142impl<'a> Parse<'a> for LocalTimeOffsetDescriptor {
143 type Error = crate::error::Error;
144 fn parse(bytes: &'a [u8]) -> Result<Self> {
145 let body = descriptor_body(
146 bytes,
147 TAG,
148 "LocalTimeOffsetDescriptor",
149 "unexpected tag for local_time_offset_descriptor",
150 )?;
151 if body.len() % ENTRY_LEN != 0 {
152 return Err(Error::InvalidDescriptor {
153 tag: TAG,
154 reason: "descriptor_length must be a multiple of 13",
155 });
156 }
157 let mut entries = Vec::with_capacity(body.len() / ENTRY_LEN);
158 let mut offset = 0;
159 while offset < body.len() {
160 let country_code = LangCode([body[offset], body[offset + 1], body[offset + 2]]);
161 let flags = body[offset + 3];
162 let country_region_id = (flags & REGION_ID_MASK) >> 2;
163 let local_time_offset_negative = flags & POLARITY_MASK != 0;
164 let local_time_offset_bcd = u16::from_be_bytes([body[offset + 4], body[offset + 5]]);
165 let mut time_of_change_raw = [0u8; 5];
166 time_of_change_raw.copy_from_slice(&body[offset + 6..offset + 11]);
167 let next_time_offset_bcd = u16::from_be_bytes([body[offset + 11], body[offset + 12]]);
168 entries.push(LocalTimeOffsetEntry {
169 country_code,
170 country_region_id,
171 local_time_offset_negative,
172 local_time_offset_bcd,
173 time_of_change_raw,
174 next_time_offset_bcd,
175 });
176 offset += ENTRY_LEN;
177 }
178 Ok(Self { entries })
179 }
180}
181
182impl Serialize for LocalTimeOffsetDescriptor {
183 type Error = crate::error::Error;
184 fn serialized_len(&self) -> usize {
185 HEADER_LEN + ENTRY_LEN * self.entries.len()
186 }
187
188 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
189 let len = self.serialized_len();
190 if buf.len() < len {
191 return Err(Error::OutputBufferTooSmall {
192 need: len,
193 have: buf.len(),
194 });
195 }
196 buf[0] = TAG;
197 buf[1] = (len - HEADER_LEN) as u8;
198 let mut offset = HEADER_LEN;
199 for entry in &self.entries {
200 buf[offset..offset + 3].copy_from_slice(&entry.country_code.0);
201 let flags = ((entry.country_region_id << 2) & REGION_ID_MASK)
202 | RESERVED_BIT_MASK
203 | if entry.local_time_offset_negative {
204 POLARITY_MASK
205 } else {
206 0
207 };
208 buf[offset + 3] = flags;
209 buf[offset + 4..offset + 6].copy_from_slice(&entry.local_time_offset_bcd.to_be_bytes());
210 buf[offset + 6..offset + 11].copy_from_slice(&entry.time_of_change_raw);
211 buf[offset + 11..offset + 13]
212 .copy_from_slice(&entry.next_time_offset_bcd.to_be_bytes());
213 offset += ENTRY_LEN;
214 }
215 Ok(len)
216 }
217}
218impl<'a> crate::traits::DescriptorDef<'a> for LocalTimeOffsetDescriptor {
219 const TAG: u8 = TAG;
220 const NAME: &'static str = "LOCAL_TIME_OFFSET";
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn parse_single_entry() {
229 let bytes = [
230 TAG, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
231 ];
232 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
233 assert_eq!(d.entries.len(), 1);
234 assert_eq!(d.entries[0].country_code, LangCode([0x46, 0x52, 0x41]));
235 assert_eq!(d.entries[0].country_region_id, 0);
236 assert!(!d.entries[0].local_time_offset_negative);
237 assert_eq!(d.entries[0].local_time_offset_bcd, 0x0100);
238 assert_eq!(
239 d.entries[0].time_of_change_raw,
240 [0xAB, 0xCD, 0xEF, 0x12, 0x34]
241 );
242 assert_eq!(d.entries[0].next_time_offset_bcd, 0x0200);
243 }
244
245 #[test]
246 fn parse_multiple_entries_preserves_order() {
247 let bytes = [
248 TAG, 26, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
249 0x47, 0x42, 0x52, 0x06, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x01, 0x00,
250 ];
251 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
252 assert_eq!(d.entries.len(), 2);
253 assert_eq!(d.entries[0].country_code, LangCode([0x46, 0x52, 0x41]));
254 assert_eq!(d.entries[1].country_code, LangCode([0x47, 0x42, 0x52]));
255 }
256
257 #[test]
258 fn parse_extracts_polarity_negative() {
259 let bytes = [
260 TAG, 13, 0x46, 0x52, 0x41, 0x03, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
261 ];
262 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
263 assert!(d.entries[0].local_time_offset_negative);
264 }
265
266 #[test]
267 fn parse_extracts_country_region_id() {
268 let bytes = [
269 TAG, 13, 0x46, 0x52, 0x41, 0x1A, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
270 ];
271 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
272 assert_eq!(d.entries[0].country_region_id, 6);
273 }
274
275 #[test]
276 fn parse_rejects_wrong_tag() {
277 let err = LocalTimeOffsetDescriptor::parse(&[
278 0x59, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
279 ])
280 .unwrap_err();
281 assert!(matches!(err, Error::InvalidDescriptor { tag: 0x59, .. }));
282 }
283
284 #[test]
285 fn parse_rejects_length_not_multiple_of_13() {
286 let bytes = [
287 TAG, 14, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
288 0xFF,
289 ];
290 let err = LocalTimeOffsetDescriptor::parse(&bytes).unwrap_err();
291 assert!(matches!(err, Error::InvalidDescriptor { tag: TAG, .. }));
292 }
293
294 #[test]
295 fn parse_ignores_reserved_bit_not_set() {
296 let bytes = [
298 TAG, 13, 0x46, 0x52, 0x41, 0x00, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
299 ];
300 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
301 assert_eq!(d.entries.len(), 1);
302 assert!(!d.entries[0].local_time_offset_negative);
303 }
304
305 #[test]
306 fn serialize_round_trip() {
307 let d = LocalTimeOffsetDescriptor {
308 entries: vec![LocalTimeOffsetEntry {
309 country_code: LangCode([0x46, 0x52, 0x41]),
310 country_region_id: 0,
311 local_time_offset_negative: false,
312 local_time_offset_bcd: 0x0100,
313 time_of_change_raw: [0xAB, 0xCD, 0xEF, 0x12, 0x34],
314 next_time_offset_bcd: 0x0200,
315 }],
316 };
317 let mut buf = vec![0u8; d.serialized_len()];
318 d.serialize_into(&mut buf).unwrap();
319 let re = LocalTimeOffsetDescriptor::parse(&buf).unwrap();
320 assert_eq!(d, re);
321 }
322
323 #[test]
324 fn empty_descriptor_valid() {
325 let bytes = [TAG, 0];
326 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
327 assert!(d.entries.is_empty());
328 }
329}