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