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 let mut offset = 0;
209 while offset < body.len() {
210 let country_code = LangCode([body[offset], body[offset + 1], body[offset + 2]]);
211 let flags = body[offset + 3];
212 let country_region_id = (flags & REGION_ID_MASK) >> 2;
213 let local_time_offset_negative = flags & POLARITY_MASK != 0;
214 let local_time_offset_bcd = u16::from_be_bytes([body[offset + 4], body[offset + 5]]);
215 let mut time_of_change_raw = [0u8; 5];
216 time_of_change_raw.copy_from_slice(&body[offset + 6..offset + 11]);
217 let next_time_offset_bcd = u16::from_be_bytes([body[offset + 11], body[offset + 12]]);
218 entries.push(LocalTimeOffsetEntry {
219 country_code,
220 country_region_id,
221 local_time_offset_negative,
222 local_time_offset_bcd,
223 time_of_change_raw,
224 next_time_offset_bcd,
225 });
226 offset += ENTRY_LEN;
227 }
228 Ok(Self { entries })
229 }
230}
231
232impl Serialize for LocalTimeOffsetDescriptor {
233 type Error = crate::error::Error;
234 fn serialized_len(&self) -> usize {
235 HEADER_LEN + ENTRY_LEN * self.entries.len()
236 }
237
238 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
239 let len = self.serialized_len();
240 if buf.len() < len {
241 return Err(Error::OutputBufferTooSmall {
242 need: len,
243 have: buf.len(),
244 });
245 }
246 buf[0] = TAG;
247 buf[1] = (len - HEADER_LEN) as u8;
248 let mut offset = HEADER_LEN;
249 for entry in &self.entries {
250 buf[offset..offset + 3].copy_from_slice(&entry.country_code.0);
251 let flags = ((entry.country_region_id << 2) & REGION_ID_MASK)
252 | RESERVED_BIT_MASK
253 | if entry.local_time_offset_negative {
254 POLARITY_MASK
255 } else {
256 0
257 };
258 buf[offset + 3] = flags;
259 buf[offset + 4..offset + 6].copy_from_slice(&entry.local_time_offset_bcd.to_be_bytes());
260 buf[offset + 6..offset + 11].copy_from_slice(&entry.time_of_change_raw);
261 buf[offset + 11..offset + 13]
262 .copy_from_slice(&entry.next_time_offset_bcd.to_be_bytes());
263 offset += ENTRY_LEN;
264 }
265 Ok(len)
266 }
267}
268impl<'a> crate::traits::DescriptorDef<'a> for LocalTimeOffsetDescriptor {
269 const TAG: u8 = TAG;
270 const NAME: &'static str = "LOCAL_TIME_OFFSET";
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn parse_single_entry() {
279 let bytes = [
280 TAG, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
281 ];
282 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
283 assert_eq!(d.entries.len(), 1);
284 assert_eq!(d.entries[0].country_code, LangCode([0x46, 0x52, 0x41]));
285 assert_eq!(d.entries[0].country_region_id, 0);
286 assert!(!d.entries[0].local_time_offset_negative);
287 assert_eq!(d.entries[0].local_time_offset_bcd, 0x0100);
288 assert_eq!(
289 d.entries[0].time_of_change_raw(),
290 [0xAB, 0xCD, 0xEF, 0x12, 0x34]
291 );
292 assert_eq!(d.entries[0].next_time_offset_bcd, 0x0200);
293 }
294
295 #[test]
296 fn time_of_change_parts_decoded() {
297 let bytes = [
298 TAG, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
299 ];
300 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
301 let (mjd, h, m, s) = d.entries[0].time_of_change_parts();
302 assert_eq!(mjd, 0xABCD);
303 assert_eq!(h, None);
304 assert_eq!(m, Some(12));
305 assert_eq!(s, Some(34));
306 }
307
308 #[test]
309 fn time_of_change_parts_valid_bcd() {
310 let entry = LocalTimeOffsetEntry {
311 country_code: LangCode([0x47, 0x42, 0x52]),
312 country_region_id: 0,
313 local_time_offset_negative: false,
314 local_time_offset_bcd: 0x0000,
315 time_of_change_raw: [0xC0, 0x3E, 0x12, 0x30, 0x00],
316 next_time_offset_bcd: 0x0100,
317 };
318 let (mjd, h, m, s) = entry.time_of_change_parts();
319 assert_eq!(mjd, 0xC03E);
320 assert_eq!(h, Some(12));
321 assert_eq!(m, Some(30));
322 assert_eq!(s, Some(0));
323 }
324
325 #[test]
326 fn parse_multiple_entries_preserves_order() {
327 let bytes = [
328 TAG, 26, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
329 0x47, 0x42, 0x52, 0x06, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x01, 0x00,
330 ];
331 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
332 assert_eq!(d.entries.len(), 2);
333 assert_eq!(d.entries[0].country_code, LangCode([0x46, 0x52, 0x41]));
334 assert_eq!(d.entries[1].country_code, LangCode([0x47, 0x42, 0x52]));
335 }
336
337 #[test]
338 fn parse_extracts_polarity_negative() {
339 let bytes = [
340 TAG, 13, 0x46, 0x52, 0x41, 0x03, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
341 ];
342 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
343 assert!(d.entries[0].local_time_offset_negative);
344 }
345
346 #[test]
347 fn parse_extracts_country_region_id() {
348 let bytes = [
349 TAG, 13, 0x46, 0x52, 0x41, 0x1A, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
350 ];
351 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
352 assert_eq!(d.entries[0].country_region_id, 6);
353 }
354
355 #[test]
356 fn parse_rejects_wrong_tag() {
357 let err = LocalTimeOffsetDescriptor::parse(&[
358 0x59, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
359 ])
360 .unwrap_err();
361 assert!(matches!(err, Error::InvalidDescriptor { tag: 0x59, .. }));
362 }
363
364 #[test]
365 fn parse_rejects_length_not_multiple_of_13() {
366 let bytes = [
367 TAG, 14, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
368 0xFF,
369 ];
370 let err = LocalTimeOffsetDescriptor::parse(&bytes).unwrap_err();
371 assert!(matches!(err, Error::InvalidDescriptor { tag: TAG, .. }));
372 }
373
374 #[test]
375 fn parse_ignores_reserved_bit_not_set() {
376 let bytes = [
377 TAG, 13, 0x46, 0x52, 0x41, 0x00, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
378 ];
379 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
380 assert_eq!(d.entries.len(), 1);
381 assert!(!d.entries[0].local_time_offset_negative);
382 }
383
384 #[test]
385 fn serialize_round_trip() {
386 let e = LocalTimeOffsetEntry {
387 country_code: LangCode([0x46, 0x52, 0x41]),
388 country_region_id: 0,
389 local_time_offset_negative: false,
390 local_time_offset_bcd: 0x0100,
391 time_of_change_raw: [0xAB, 0xCD, 0xEF, 0x12, 0x34],
392 next_time_offset_bcd: 0x0200,
393 };
394 let d = LocalTimeOffsetDescriptor { entries: vec![e] };
395 let mut buf = vec![0u8; d.serialized_len()];
396 d.serialize_into(&mut buf).unwrap();
397 let re = LocalTimeOffsetDescriptor::parse(&buf).unwrap();
398 assert_eq!(d, re);
399 }
400
401 #[test]
402 fn set_time_of_change_raw_updates_field() {
403 let mut e = LocalTimeOffsetEntry {
404 country_code: LangCode([0x46, 0x52, 0x41]),
405 country_region_id: 0,
406 local_time_offset_negative: false,
407 local_time_offset_bcd: 0x0100,
408 time_of_change_raw: [0; 5],
409 next_time_offset_bcd: 0x0200,
410 };
411 e.set_time_of_change_raw([0xAB, 0xCD, 0xEF, 0x12, 0x34]);
412 assert_eq!(e.time_of_change_raw(), [0xAB, 0xCD, 0xEF, 0x12, 0x34]);
413 }
414
415 #[test]
416 fn empty_descriptor_valid() {
417 let bytes = [TAG, 0];
418 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
419 assert!(d.entries.is_empty());
420 }
421}