1use crate::error::{Error, Result};
7use crate::text::LangCode;
8use crate::traits::Descriptor;
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 if bytes.len() < HEADER_LEN {
146 return Err(Error::BufferTooShort {
147 need: HEADER_LEN,
148 have: bytes.len(),
149 what: "LocalTimeOffsetDescriptor header",
150 });
151 }
152 if bytes[0] != TAG {
153 return Err(Error::InvalidDescriptor {
154 tag: bytes[0],
155 reason: "unexpected tag for local_time_offset_descriptor",
156 });
157 }
158 let length = bytes[1] as usize;
159 if length % ENTRY_LEN != 0 {
160 return Err(Error::InvalidDescriptor {
161 tag: TAG,
162 reason: "descriptor_length must be a multiple of 13",
163 });
164 }
165 let body_start = HEADER_LEN;
166 let body_end = body_start + length;
167 if bytes.len() < body_end {
168 return Err(Error::BufferTooShort {
169 need: body_end,
170 have: bytes.len(),
171 what: "LocalTimeOffsetDescriptor body",
172 });
173 }
174 let mut entries = Vec::with_capacity(length / ENTRY_LEN);
175 let mut offset = body_start;
176 while offset < body_end {
177 let country_code = LangCode([bytes[offset], bytes[offset + 1], bytes[offset + 2]]);
178 let flags = bytes[offset + 3];
179 let country_region_id = (flags & REGION_ID_MASK) >> 2;
182 let local_time_offset_negative = flags & POLARITY_MASK != 0;
183 let local_time_offset_bcd = u16::from_be_bytes([bytes[offset + 4], bytes[offset + 5]]);
184 let mut time_of_change_raw = [0u8; 5];
185 time_of_change_raw.copy_from_slice(&bytes[offset + 6..offset + 11]);
186 let next_time_offset_bcd = u16::from_be_bytes([bytes[offset + 11], bytes[offset + 12]]);
187 entries.push(LocalTimeOffsetEntry {
188 country_code,
189 country_region_id,
190 local_time_offset_negative,
191 local_time_offset_bcd,
192 time_of_change_raw,
193 next_time_offset_bcd,
194 });
195 offset += ENTRY_LEN;
196 }
197 Ok(Self { entries })
198 }
199}
200
201impl Serialize for LocalTimeOffsetDescriptor {
202 type Error = crate::error::Error;
203 fn serialized_len(&self) -> usize {
204 HEADER_LEN + ENTRY_LEN * self.entries.len()
205 }
206
207 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
208 let len = self.serialized_len();
209 if buf.len() < len {
210 return Err(Error::OutputBufferTooSmall {
211 need: len,
212 have: buf.len(),
213 });
214 }
215 buf[0] = TAG;
216 buf[1] = (len - HEADER_LEN) as u8;
217 let mut offset = HEADER_LEN;
218 for entry in &self.entries {
219 buf[offset..offset + 3].copy_from_slice(&entry.country_code.0);
220 let flags = ((entry.country_region_id << 2) & REGION_ID_MASK)
221 | RESERVED_BIT_MASK
222 | if entry.local_time_offset_negative {
223 POLARITY_MASK
224 } else {
225 0
226 };
227 buf[offset + 3] = flags;
228 buf[offset + 4..offset + 6].copy_from_slice(&entry.local_time_offset_bcd.to_be_bytes());
229 buf[offset + 6..offset + 11].copy_from_slice(&entry.time_of_change_raw);
230 buf[offset + 11..offset + 13]
231 .copy_from_slice(&entry.next_time_offset_bcd.to_be_bytes());
232 offset += ENTRY_LEN;
233 }
234 Ok(len)
235 }
236}
237
238impl<'a> Descriptor<'a> for LocalTimeOffsetDescriptor {
239 const TAG: u8 = TAG;
240 fn descriptor_length(&self) -> u8 {
241 (self.serialized_len() - HEADER_LEN) as u8
242 }
243}
244
245impl<'a> crate::traits::DescriptorDef<'a> for LocalTimeOffsetDescriptor {
246 const TAG: u8 = TAG;
247 const NAME: &'static str = "LOCAL_TIME_OFFSET";
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn parse_single_entry() {
256 let bytes = [
257 TAG, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
258 ];
259 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
260 assert_eq!(d.entries.len(), 1);
261 assert_eq!(d.entries[0].country_code, LangCode([0x46, 0x52, 0x41]));
262 assert_eq!(d.entries[0].country_region_id, 0);
263 assert!(!d.entries[0].local_time_offset_negative);
264 assert_eq!(d.entries[0].local_time_offset_bcd, 0x0100);
265 assert_eq!(
266 d.entries[0].time_of_change_raw,
267 [0xAB, 0xCD, 0xEF, 0x12, 0x34]
268 );
269 assert_eq!(d.entries[0].next_time_offset_bcd, 0x0200);
270 }
271
272 #[test]
273 fn parse_multiple_entries_preserves_order() {
274 let bytes = [
275 TAG, 26, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
276 0x47, 0x42, 0x52, 0x06, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x01, 0x00,
277 ];
278 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
279 assert_eq!(d.entries.len(), 2);
280 assert_eq!(d.entries[0].country_code, LangCode([0x46, 0x52, 0x41]));
281 assert_eq!(d.entries[1].country_code, LangCode([0x47, 0x42, 0x52]));
282 }
283
284 #[test]
285 fn parse_extracts_polarity_negative() {
286 let bytes = [
287 TAG, 13, 0x46, 0x52, 0x41, 0x03, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
288 ];
289 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
290 assert!(d.entries[0].local_time_offset_negative);
291 }
292
293 #[test]
294 fn parse_extracts_country_region_id() {
295 let bytes = [
296 TAG, 13, 0x46, 0x52, 0x41, 0x1A, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
297 ];
298 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
299 assert_eq!(d.entries[0].country_region_id, 6);
300 }
301
302 #[test]
303 fn parse_rejects_wrong_tag() {
304 let err = LocalTimeOffsetDescriptor::parse(&[
305 0x59, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
306 ])
307 .unwrap_err();
308 assert!(matches!(err, Error::InvalidDescriptor { tag: 0x59, .. }));
309 }
310
311 #[test]
312 fn parse_rejects_length_not_multiple_of_13() {
313 let bytes = [
314 TAG, 14, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
315 0xFF,
316 ];
317 let err = LocalTimeOffsetDescriptor::parse(&bytes).unwrap_err();
318 assert!(matches!(err, Error::InvalidDescriptor { tag: TAG, .. }));
319 }
320
321 #[test]
322 fn parse_ignores_reserved_bit_not_set() {
323 let bytes = [
325 TAG, 13, 0x46, 0x52, 0x41, 0x00, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
326 ];
327 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
328 assert_eq!(d.entries.len(), 1);
329 assert!(!d.entries[0].local_time_offset_negative);
330 }
331
332 #[test]
333 fn serialize_round_trip() {
334 let d = LocalTimeOffsetDescriptor {
335 entries: vec![LocalTimeOffsetEntry {
336 country_code: LangCode([0x46, 0x52, 0x41]),
337 country_region_id: 0,
338 local_time_offset_negative: false,
339 local_time_offset_bcd: 0x0100,
340 time_of_change_raw: [0xAB, 0xCD, 0xEF, 0x12, 0x34],
341 next_time_offset_bcd: 0x0200,
342 }],
343 };
344 let mut buf = vec![0u8; d.serialized_len()];
345 d.serialize_into(&mut buf).unwrap();
346 let re = LocalTimeOffsetDescriptor::parse(&buf).unwrap();
347 assert_eq!(d, re);
348 }
349
350 #[test]
351 fn empty_descriptor_valid() {
352 let bytes = [TAG, 0];
353 let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
354 assert!(d.entries.is_empty());
355 }
356}