Skip to main content

dvb_si/descriptors/
local_time_offset.rs

1//! Local Time Offset Descriptor — ETSI EN 300 468 §6.2.20 (tag 0x58).
2//!
3//! Carried inside the TOT (Time Offset Table) on PID 0x0014. Signals per-
4//! country offsets from UTC plus any upcoming DST transition.
5
6use crate::error::{Error, Result};
7use crate::traits::Descriptor;
8use dvb_common::{Parse, Serialize};
9
10/// Descriptor tag for local_time_offset_descriptor.
11pub const TAG: u8 = 0x58;
12const HEADER_LEN: usize = 2;
13const ENTRY_LEN: usize = 13;
14const POLARITY_MASK: u8 = 0x01;
15const REGION_ID_MASK: u8 = 0xFC;
16const RESERVED_BIT_MASK: u8 = 0x02;
17
18/// One per-country offset entry.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct LocalTimeOffsetEntry {
22    /// ISO 3166 alpha country code.
23    pub country_code: [u8; 3],
24    /// 6-bit country_region_id for sub-national regions.
25    pub country_region_id: u8,
26    /// Polarity: false = offset is positive (local = UTC + offset),
27    /// true = offset is negative (local = UTC − offset).
28    pub local_time_offset_negative: bool,
29    /// 16-bit BCD HHMM local time offset.
30    pub local_time_offset_bcd: u16,
31    /// 40-bit MJD+UTC raw bytes of the DST/offset transition moment.
32    pub time_of_change_raw: [u8; 5],
33    /// 16-bit BCD HHMM next offset (applied after `time_of_change`).
34    pub next_time_offset_bcd: u16,
35}
36
37/// Local Time Offset Descriptor.
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct LocalTimeOffsetDescriptor {
41    /// Entries in wire order.
42    pub entries: Vec<LocalTimeOffsetEntry>,
43}
44
45impl<'a> Parse<'a> for LocalTimeOffsetDescriptor {
46    type Error = crate::error::Error;
47    fn parse(bytes: &'a [u8]) -> Result<Self> {
48        if bytes.len() < HEADER_LEN {
49            return Err(Error::BufferTooShort {
50                need: HEADER_LEN,
51                have: bytes.len(),
52                what: "LocalTimeOffsetDescriptor header",
53            });
54        }
55        if bytes[0] != TAG {
56            return Err(Error::InvalidDescriptor {
57                tag: bytes[0],
58                reason: "unexpected tag for local_time_offset_descriptor",
59            });
60        }
61        let length = bytes[1] as usize;
62        if length % ENTRY_LEN != 0 {
63            return Err(Error::InvalidDescriptor {
64                tag: TAG,
65                reason: "descriptor_length must be a multiple of 13",
66            });
67        }
68        let body_start = HEADER_LEN;
69        let body_end = body_start + length;
70        if bytes.len() < body_end {
71            return Err(Error::BufferTooShort {
72                need: body_end,
73                have: bytes.len(),
74                what: "LocalTimeOffsetDescriptor body",
75            });
76        }
77        let mut entries = Vec::with_capacity(length / ENTRY_LEN);
78        let mut offset = body_start;
79        while offset < body_end {
80            let country_code = [bytes[offset], bytes[offset + 1], bytes[offset + 2]];
81            let flags = bytes[offset + 3];
82            // The reserved bit is ignored on parse (EN 300 468 §5.1: decoders
83            // shall ignore reserved bits).
84            let country_region_id = (flags & REGION_ID_MASK) >> 2;
85            let local_time_offset_negative = flags & POLARITY_MASK != 0;
86            let local_time_offset_bcd = u16::from_be_bytes([bytes[offset + 4], bytes[offset + 5]]);
87            let mut time_of_change_raw = [0u8; 5];
88            time_of_change_raw.copy_from_slice(&bytes[offset + 6..offset + 11]);
89            let next_time_offset_bcd = u16::from_be_bytes([bytes[offset + 11], bytes[offset + 12]]);
90            entries.push(LocalTimeOffsetEntry {
91                country_code,
92                country_region_id,
93                local_time_offset_negative,
94                local_time_offset_bcd,
95                time_of_change_raw,
96                next_time_offset_bcd,
97            });
98            offset += ENTRY_LEN;
99        }
100        Ok(Self { entries })
101    }
102}
103
104impl Serialize for LocalTimeOffsetDescriptor {
105    type Error = crate::error::Error;
106    fn serialized_len(&self) -> usize {
107        HEADER_LEN + ENTRY_LEN * self.entries.len()
108    }
109
110    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
111        let len = self.serialized_len();
112        if buf.len() < len {
113            return Err(Error::OutputBufferTooSmall {
114                need: len,
115                have: buf.len(),
116            });
117        }
118        buf[0] = TAG;
119        buf[1] = (len - HEADER_LEN) as u8;
120        let mut offset = HEADER_LEN;
121        for entry in &self.entries {
122            buf[offset..offset + 3].copy_from_slice(&entry.country_code);
123            let flags = ((entry.country_region_id << 2) & REGION_ID_MASK)
124                | RESERVED_BIT_MASK
125                | if entry.local_time_offset_negative {
126                    POLARITY_MASK
127                } else {
128                    0
129                };
130            buf[offset + 3] = flags;
131            buf[offset + 4..offset + 6].copy_from_slice(&entry.local_time_offset_bcd.to_be_bytes());
132            buf[offset + 6..offset + 11].copy_from_slice(&entry.time_of_change_raw);
133            buf[offset + 11..offset + 13]
134                .copy_from_slice(&entry.next_time_offset_bcd.to_be_bytes());
135            offset += ENTRY_LEN;
136        }
137        Ok(len)
138    }
139}
140
141impl<'a> Descriptor<'a> for LocalTimeOffsetDescriptor {
142    const TAG: u8 = TAG;
143    fn descriptor_length(&self) -> u8 {
144        (self.serialized_len() - HEADER_LEN) as u8
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn parse_single_entry() {
154        let bytes = [
155            TAG, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
156        ];
157        let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
158        assert_eq!(d.entries.len(), 1);
159        assert_eq!(d.entries[0].country_code, [0x46, 0x52, 0x41]);
160        assert_eq!(d.entries[0].country_region_id, 0);
161        assert!(!d.entries[0].local_time_offset_negative);
162        assert_eq!(d.entries[0].local_time_offset_bcd, 0x0100);
163        assert_eq!(
164            d.entries[0].time_of_change_raw,
165            [0xAB, 0xCD, 0xEF, 0x12, 0x34]
166        );
167        assert_eq!(d.entries[0].next_time_offset_bcd, 0x0200);
168    }
169
170    #[test]
171    fn parse_multiple_entries_preserves_order() {
172        let bytes = [
173            TAG, 26, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
174            0x47, 0x42, 0x52, 0x06, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x01, 0x00,
175        ];
176        let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
177        assert_eq!(d.entries.len(), 2);
178        assert_eq!(d.entries[0].country_code, [0x46, 0x52, 0x41]);
179        assert_eq!(d.entries[1].country_code, [0x47, 0x42, 0x52]);
180    }
181
182    #[test]
183    fn parse_extracts_polarity_negative() {
184        let bytes = [
185            TAG, 13, 0x46, 0x52, 0x41, 0x03, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
186        ];
187        let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
188        assert!(d.entries[0].local_time_offset_negative);
189    }
190
191    #[test]
192    fn parse_extracts_country_region_id() {
193        let bytes = [
194            TAG, 13, 0x46, 0x52, 0x41, 0x1A, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
195        ];
196        let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
197        assert_eq!(d.entries[0].country_region_id, 6);
198    }
199
200    #[test]
201    fn parse_rejects_wrong_tag() {
202        let err = LocalTimeOffsetDescriptor::parse(&[
203            0x59, 13, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
204        ])
205        .unwrap_err();
206        assert!(matches!(err, Error::InvalidDescriptor { tag: 0x59, .. }));
207    }
208
209    #[test]
210    fn parse_rejects_length_not_multiple_of_13() {
211        let bytes = [
212            TAG, 14, 0x46, 0x52, 0x41, 0x02, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
213            0xFF,
214        ];
215        let err = LocalTimeOffsetDescriptor::parse(&bytes).unwrap_err();
216        assert!(matches!(err, Error::InvalidDescriptor { tag: TAG, .. }));
217    }
218
219    #[test]
220    fn parse_ignores_reserved_bit_not_set() {
221        // Reserved bit clear must be ignored, not rejected (EN 300 468 §5.1).
222        let bytes = [
223            TAG, 13, 0x46, 0x52, 0x41, 0x00, 0x01, 0x00, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x02, 0x00,
224        ];
225        let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
226        assert_eq!(d.entries.len(), 1);
227        assert!(!d.entries[0].local_time_offset_negative);
228    }
229
230    #[test]
231    fn serialize_round_trip() {
232        let d = LocalTimeOffsetDescriptor {
233            entries: vec![LocalTimeOffsetEntry {
234                country_code: [0x46, 0x52, 0x41],
235                country_region_id: 0,
236                local_time_offset_negative: false,
237                local_time_offset_bcd: 0x0100,
238                time_of_change_raw: [0xAB, 0xCD, 0xEF, 0x12, 0x34],
239                next_time_offset_bcd: 0x0200,
240            }],
241        };
242        let mut buf = vec![0u8; d.serialized_len()];
243        d.serialize_into(&mut buf).unwrap();
244        let re = LocalTimeOffsetDescriptor::parse(&buf).unwrap();
245        assert_eq!(d, re);
246    }
247
248    #[test]
249    fn empty_descriptor_valid() {
250        let bytes = [TAG, 0];
251        let d = LocalTimeOffsetDescriptor::parse(&bytes).unwrap();
252        assert!(d.entries.is_empty());
253    }
254}