1use super::descriptor_body;
7use crate::error::{Error, Result};
8use crate::text::LangCode;
9use alloc::vec::Vec;
10use dvb_common::{Parse, Serialize};
11
12pub const TAG: u8 = 0x58;
14const HEADER_LEN: usize = 2;
15const ENTRY_LEN: usize = 13;
16const POLARITY_MASK: u8 = 0x01;
17const REGION_ID_MASK: u8 = 0xFC;
18const RESERVED_BIT_MASK: u8 = 0x02;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize))]
23pub struct LocalTimeOffsetEntry {
24 pub country_code: LangCode,
26 pub country_region_id: u8,
28 pub local_time_offset_negative: bool,
31 pub local_time_offset_bcd: u16,
33 time_of_change_raw: [u8; 5],
37 pub next_time_offset_bcd: u16,
39}
40
41impl LocalTimeOffsetEntry {
42 #[must_use]
44 pub fn new(
45 country_code: LangCode,
46 country_region_id: u8,
47 local_time_offset_negative: bool,
48 local_time_offset_bcd: u16,
49 time_of_change_raw: [u8; 5],
50 next_time_offset_bcd: u16,
51 ) -> Self {
52 Self {
53 country_code,
54 country_region_id,
55 local_time_offset_negative,
56 local_time_offset_bcd,
57 time_of_change_raw,
58 next_time_offset_bcd,
59 }
60 }
61
62 #[must_use]
68 pub fn time_of_change_parts(&self) -> (u16, Option<u8>, Option<u8>, Option<u8>) {
69 let mjd = u16::from_be_bytes([self.time_of_change_raw[0], self.time_of_change_raw[1]]);
70 let h = dvb_common::bcd::from_bcd_byte(self.time_of_change_raw[2]);
71 let m = dvb_common::bcd::from_bcd_byte(self.time_of_change_raw[3]);
72 let s = dvb_common::bcd::from_bcd_byte(self.time_of_change_raw[4]);
73 (mjd, h, m, s)
74 }
75
76 #[must_use]
78 pub fn time_of_change_raw(&self) -> [u8; 5] {
79 self.time_of_change_raw
80 }
81
82 pub fn set_time_of_change_raw(&mut self, raw: [u8; 5]) {
84 self.time_of_change_raw = raw;
85 }
86}
87
88#[cfg(feature = "chrono")]
91fn decode_hhmm(bcd: u16, negative: bool) -> Option<chrono::Duration> {
92 let h = dvb_common::bcd::from_bcd_byte((bcd >> 8) as u8)?;
93 let m = dvb_common::bcd::from_bcd_byte((bcd & 0xFF) as u8)?;
94 let mins = i64::from(h) * 60 + i64::from(m);
95 Some(chrono::Duration::minutes(if negative {
96 -mins
97 } else {
98 mins
99 }))
100}
101
102#[cfg(feature = "chrono")]
105fn encode_hhmm(offset: chrono::Duration) -> Option<(bool, u16)> {
106 let negative = offset < chrono::Duration::zero();
107 let total_min = offset.num_minutes().unsigned_abs();
108 let h = total_min / 60;
109 let m = total_min % 60;
110 if h > 99 {
111 return None;
112 }
113 let hb = dvb_common::bcd::to_bcd_byte(h as u8)?;
114 let mb = dvb_common::bcd::to_bcd_byte(m as u8)?;
115 Some((negative, (u16::from(hb) << 8) | u16::from(mb)))
116}
117
118#[cfg(feature = "chrono")]
119impl LocalTimeOffsetEntry {
120 #[must_use]
124 pub fn local_time_offset(&self) -> Option<chrono::Duration> {
125 decode_hhmm(self.local_time_offset_bcd, self.local_time_offset_negative)
126 }
127
128 #[must_use]
132 pub fn next_time_offset(&self) -> Option<chrono::Duration> {
133 decode_hhmm(self.next_time_offset_bcd, self.local_time_offset_negative)
134 }
135
136 #[must_use]
139 pub fn time_of_change(&self) -> Option<chrono::DateTime<chrono::Utc>> {
140 dvb_common::time::decode_mjd_bcd_utc(self.time_of_change_raw)
141 }
142
143 pub fn set_time_of_change(&mut self, dt: chrono::DateTime<chrono::Utc>) -> Result<()> {
149 self.time_of_change_raw =
150 dvb_common::time::encode_mjd_bcd_utc(dt).ok_or(Error::ValueOutOfRange {
151 field: "LocalTimeOffsetEntry::time_of_change",
152 reason: "date not representable in 16-bit MJD",
153 })?;
154 Ok(())
155 }
156
157 pub fn set_offsets(&mut self, local: chrono::Duration, next: chrono::Duration) -> Result<()> {
166 let oor = |reason| Error::ValueOutOfRange {
167 field: "LocalTimeOffsetEntry offsets",
168 reason,
169 };
170 let local_neg = local < chrono::Duration::zero();
171 let next_neg = next < chrono::Duration::zero();
172 if local_neg != next_neg && !local.is_zero() && !next.is_zero() {
173 return Err(oor("local and next offsets must share a sign"));
174 }
175 let (lneg, lbcd) = encode_hhmm(local).ok_or(oor("local offset magnitude too large"))?;
176 let (nneg, nbcd) = encode_hhmm(next).ok_or(oor("next offset magnitude too large"))?;
177 self.local_time_offset_negative = lneg || nneg;
178 self.local_time_offset_bcd = lbcd;
179 self.next_time_offset_bcd = nbcd;
180 Ok(())
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize))]
187pub struct LocalTimeOffsetDescriptor {
188 pub entries: Vec<LocalTimeOffsetEntry>,
190}
191
192impl<'a> Parse<'a> for LocalTimeOffsetDescriptor {
193 type Error = crate::error::Error;
194 fn parse(bytes: &'a [u8]) -> Result<Self> {
195 let body = descriptor_body(
196 bytes,
197 TAG,
198 "LocalTimeOffsetDescriptor",
199 "unexpected tag for local_time_offset_descriptor",
200 )?;
201 if body.len() % ENTRY_LEN != 0 {
202 return Err(Error::InvalidDescriptor {
203 tag: TAG,
204 reason: "descriptor_length must be a multiple of 13",
205 });
206 }
207 let mut entries = Vec::with_capacity(body.len() / ENTRY_LEN);
208 for chunk in body.chunks_exact(ENTRY_LEN) {
209 let country_code = LangCode([chunk[0], chunk[1], chunk[2]]);
211 let flags = chunk[3];
212 let country_region_id = (flags & REGION_ID_MASK) >> 2;
213 let local_time_offset_negative = flags & POLARITY_MASK != 0;
214 let lto_bytes = chunk[4..6].first_chunk::<2>().unwrap();
215 let local_time_offset_bcd = u16::from_be_bytes(*lto_bytes);
216 let mut time_of_change_raw = [0u8; 5];
217 time_of_change_raw.copy_from_slice(&chunk[6..11]);
218 let nto_bytes = chunk[11..13].first_chunk::<2>().unwrap();
219 let next_time_offset_bcd = u16::from_be_bytes(*nto_bytes);
220 entries.push(LocalTimeOffsetEntry {
221 country_code,
222 country_region_id,
223 local_time_offset_negative,
224 local_time_offset_bcd,
225 time_of_change_raw,
226 next_time_offset_bcd,
227 });
228 }
229 Ok(Self { entries })
230 }
231}
232
233impl Serialize for LocalTimeOffsetDescriptor {
234 type Error = crate::error::Error;
235 fn serialized_len(&self) -> usize {
236 HEADER_LEN + ENTRY_LEN * self.entries.len()
237 }
238
239 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
240 let len = self.serialized_len();
241 if buf.len() < len {
242 return Err(Error::OutputBufferTooSmall {
243 need: len,
244 have: buf.len(),
245 });
246 }
247 buf[0] = TAG;
248 buf[1] = (len - HEADER_LEN) as u8;
249 let mut offset = HEADER_LEN;
250 for entry in &self.entries {
251 buf[offset..offset + 3].copy_from_slice(&entry.country_code.0);
252 let flags = ((entry.country_region_id << 2) & REGION_ID_MASK)
253 | RESERVED_BIT_MASK
254 | if entry.local_time_offset_negative {
255 POLARITY_MASK
256 } else {
257 0
258 };
259 buf[offset + 3] = flags;
260 buf[offset + 4..offset + 6].copy_from_slice(&entry.local_time_offset_bcd.to_be_bytes());
261 buf[offset + 6..offset + 11].copy_from_slice(&entry.time_of_change_raw);
262 buf[offset + 11..offset + 13]
263 .copy_from_slice(&entry.next_time_offset_bcd.to_be_bytes());
264 offset += ENTRY_LEN;
265 }
266 Ok(len)
267 }
268}
269impl<'a> crate::traits::DescriptorDef<'a> for LocalTimeOffsetDescriptor {
270 const TAG: u8 = TAG;
271 const NAME: &'static str = "LOCAL_TIME_OFFSET";
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn parse_single_entry() {
280 let bytes = [
281 TAG, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
282 ];
283 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
284 assert_eq!(d.entries.len(), 1);
285 assert_eq!(d.entries[0].country_code, LangCode([0x46, 0x52, 0x41]));
286 assert_eq!(d.entries[0].country_region_id, 0);
287 assert!(!d.entries[0].local_time_offset_negative);
288 assert_eq!(d.entries[0].local_time_offset_bcd, 0x0100);
289 assert_eq!(
290 d.entries[0].time_of_change_raw(),
291 [0xAB, 0xCD, 0xEF, 0x12, 0x34]
292 );
293 assert_eq!(d.entries[0].next_time_offset_bcd, 0x0200);
294 }
295
296 #[test]
297 fn time_of_change_parts_decoded() {
298 let bytes = [
299 TAG, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
300 ];
301 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
302 let (mjd, h, m, s) = d.entries[0].time_of_change_parts();
303 assert_eq!(mjd, 0xABCD);
304 assert_eq!(h, None);
305 assert_eq!(m, Some(12));
306 assert_eq!(s, Some(34));
307 }
308
309 #[test]
310 fn time_of_change_parts_valid_bcd() {
311 let entry = LocalTimeOffsetEntry {
312 country_code: LangCode([0x47, 0x42, 0x52]),
313 country_region_id: 0,
314 local_time_offset_negative: false,
315 local_time_offset_bcd: 0x0000,
316 time_of_change_raw: [0xC0, 0x3E, 0x12, 0x30, 0x00],
317 next_time_offset_bcd: 0x0100,
318 };
319 let (mjd, h, m, s) = entry.time_of_change_parts();
320 assert_eq!(mjd, 0xC03E);
321 assert_eq!(h, Some(12));
322 assert_eq!(m, Some(30));
323 assert_eq!(s, Some(0));
324 }
325
326 #[test]
327 fn parse_multiple_entries_preserves_order() {
328 let bytes = [
329 TAG, 26, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
330 0x47, 0x42, 0x52, 0x06, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x01, 0x00,
331 ];
332 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
333 assert_eq!(d.entries.len(), 2);
334 assert_eq!(d.entries[0].country_code, LangCode([0x46, 0x52, 0x41]));
335 assert_eq!(d.entries[1].country_code, LangCode([0x47, 0x42, 0x52]));
336 }
337
338 #[test]
339 fn parse_extracts_polarity_negative() {
340 let bytes = [
341 TAG, 13, 0x46, 0x52, 0x41, 0x03, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
342 ];
343 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
344 assert!(d.entries[0].local_time_offset_negative);
345 }
346
347 #[test]
348 fn parse_extracts_country_region_id() {
349 let bytes = [
350 TAG, 13, 0x46, 0x52, 0x41, 0x1A, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
351 ];
352 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
353 assert_eq!(d.entries[0].country_region_id, 6);
354 }
355
356 #[test]
357 fn parse_rejects_wrong_tag() {
358 let err = LocalTimeOffsetDescriptor::parse(&[
359 0x59, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
360 ])
361 .unwrap_err();
362 assert!(matches!(err, Error::InvalidDescriptor { tag: 0x59, .. }));
363 }
364
365 #[test]
366 fn parse_rejects_length_not_multiple_of_13() {
367 let bytes = [
368 TAG, 14, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
369 0xFF,
370 ];
371 let err = LocalTimeOffsetDescriptor::parse(&bytes).unwrap_err();
372 assert!(matches!(err, Error::InvalidDescriptor { tag: TAG, .. }));
373 }
374
375 #[test]
376 fn parse_ignores_reserved_bit_not_set() {
377 let bytes = [
378 TAG, 13, 0x46, 0x52, 0x41, 0x00, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
379 ];
380 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
381 assert_eq!(d.entries.len(), 1);
382 assert!(!d.entries[0].local_time_offset_negative);
383 }
384
385 #[test]
386 fn serialize_round_trip() {
387 let e = LocalTimeOffsetEntry {
388 country_code: LangCode([0x46, 0x52, 0x41]),
389 country_region_id: 0,
390 local_time_offset_negative: false,
391 local_time_offset_bcd: 0x0100,
392 time_of_change_raw: [0xAB, 0xCD, 0xEF, 0x12, 0x34],
393 next_time_offset_bcd: 0x0200,
394 };
395 let d = LocalTimeOffsetDescriptor { entries: vec![e] };
396 let mut buf = vec![0u8; d.serialized_len()];
397 d.serialize_into(&mut buf).unwrap();
398 let re = LocalTimeOffsetDescriptor::parse(&buf).unwrap();
399 assert_eq!(d, re);
400 }
401
402 #[test]
403 fn set_time_of_change_raw_updates_field() {
404 let mut e = LocalTimeOffsetEntry {
405 country_code: LangCode([0x46, 0x52, 0x41]),
406 country_region_id: 0,
407 local_time_offset_negative: false,
408 local_time_offset_bcd: 0x0100,
409 time_of_change_raw: [0; 5],
410 next_time_offset_bcd: 0x0200,
411 };
412 e.set_time_of_change_raw([0xAB, 0xCD, 0xEF, 0x12, 0x34]);
413 assert_eq!(e.time_of_change_raw(), [0xAB, 0xCD, 0xEF, 0x12, 0x34]);
414 }
415
416 #[test]
417 fn empty_descriptor_valid() {
418 let bytes = [TAG, 0];
419 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
420 assert!(d.entries.is_empty());
421 }
422}