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