Skip to main content

dvb_ci/objects/
date_time.rs

1//! Date-Time objects — ETSI EN 50221 §8.5.2, Tables 31-32 (PDF p. 35).
2//!
3//! - `date_time_enq` (`9F 84 40`, Table 31) — one-byte `response_interval`.
4//! - `date_time` (`9F 84 41`, Table 32) — 5-byte MJD+BCD UTC time, plus an
5//!   optional 16-bit signed `local_offset` (length_field = 5 or 7).
6//!
7//! (The PDF mislabels both tables "Date-Time Enquiry"; Table 32 is the
8//! `date_time()` object — see `docs/en_50221/datetime.md`.) The `UTC_time` is
9//! carried as the verbatim 5 wire bytes (MJD+BCD, EN 300 468 Annex C) so it
10//! round-trips without needing the `chrono` feature.
11
12use crate::error::{Error, Result};
13use crate::tag::{self, ApduTag};
14use crate::traits::ApduDef;
15use broadcast_common::{Parse, Serialize};
16
17/// Length of the `UTC_time` field (40 bits = 5 bytes).
18pub const UTC_TIME_LEN: usize = 5;
19
20/// `date_time_enq()` — Date-Time Enquiry (Table 31).
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize))]
23pub struct DateTimeEnq {
24    /// `response_interval` — if 0, the host replies once; otherwise it replies
25    /// immediately then every `response_interval` seconds.
26    pub response_interval: u8,
27}
28
29/// `date_time()` — current UTC date/time with optional local offset (Table 32).
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize))]
32pub struct DateTime {
33    /// `UTC_time` — 5 bytes, MJD (2) + BCD HH:MM:SS (3), as in TDT/TOT.
34    pub utc_time: [u8; UTC_TIME_LEN],
35    /// `local_offset` — optional signed minutes (`Local = UTC + offset`); present
36    /// only when the host has reliable knowledge (length_field = 7 vs 5).
37    pub local_offset: Option<i16>,
38}
39
40impl<'a> Parse<'a> for DateTimeEnq {
41    type Error = Error;
42    fn parse(bytes: &'a [u8]) -> Result<Self> {
43        let body = super::parse_apdu_header(bytes, tag::DATE_TIME_ENQ, "date_time_enq")?;
44        let &[response_interval] = body else {
45            return Err(Error::InvalidObject {
46                what: "date_time_enq",
47                reason: "body must be exactly 1 byte (response_interval)",
48            });
49        };
50        Ok(Self { response_interval })
51    }
52}
53
54impl Serialize for DateTimeEnq {
55    type Error = Error;
56    fn serialized_len(&self) -> usize {
57        super::apdu_len(1)
58    }
59    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
60        let pos = super::write_apdu_header(tag::DATE_TIME_ENQ, 1, buf)?;
61        buf[pos] = self.response_interval;
62        Ok(pos + 1)
63    }
64}
65
66impl ApduDef<'_> for DateTimeEnq {
67    const TAG: ApduTag = tag::DATE_TIME_ENQ;
68    const NAME: &'static str = "DATE_TIME_ENQ";
69}
70
71impl<'a> Parse<'a> for DateTime {
72    type Error = Error;
73    fn parse(bytes: &'a [u8]) -> Result<Self> {
74        let body = super::parse_apdu_header(bytes, tag::DATE_TIME, "date_time")?;
75        let utc_time: [u8; UTC_TIME_LEN] = match body.len() {
76            UTC_TIME_LEN | 7 => body[..UTC_TIME_LEN].try_into().unwrap(),
77            _ => {
78                return Err(Error::InvalidObject {
79                    what: "date_time",
80                    reason: "body must be 5 or 7 bytes",
81                });
82            }
83        };
84        let local_offset = if body.len() == 7 {
85            Some(i16::from_be_bytes([body[5], body[6]]))
86        } else {
87            None
88        };
89        Ok(Self {
90            utc_time,
91            local_offset,
92        })
93    }
94}
95
96impl Serialize for DateTime {
97    type Error = Error;
98    fn serialized_len(&self) -> usize {
99        let body = if self.local_offset.is_some() { 7 } else { 5 };
100        super::apdu_len(body)
101    }
102    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
103        let body_len = if self.local_offset.is_some() { 7 } else { 5 };
104        let mut pos = super::write_apdu_header(tag::DATE_TIME, body_len, buf)?;
105        buf[pos..pos + UTC_TIME_LEN].copy_from_slice(&self.utc_time);
106        pos += UTC_TIME_LEN;
107        if let Some(off) = self.local_offset {
108            buf[pos..pos + 2].copy_from_slice(&off.to_be_bytes());
109            pos += 2;
110        }
111        Ok(pos)
112    }
113}
114
115impl ApduDef<'_> for DateTime {
116    const TAG: ApduTag = tag::DATE_TIME;
117    const NAME: &'static str = "DATE_TIME";
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn enq_round_trip() {
126        let e = DateTimeEnq {
127            response_interval: 10,
128        };
129        let bytes = e.to_bytes();
130        assert_eq!(bytes, [0x9F, 0x84, 0x40, 0x01, 0x0A]);
131        assert_eq!(DateTimeEnq::parse(&bytes).unwrap(), e);
132    }
133
134    #[test]
135    fn date_time_no_offset_round_trip() {
136        let dt = DateTime {
137            utc_time: [0xC0, 0x79, 0x12, 0x34, 0x56],
138            local_offset: None,
139        };
140        let bytes = dt.to_bytes();
141        assert_eq!(&bytes[..4], &[0x9F, 0x84, 0x41, 0x05]); // len 5
142        assert_eq!(DateTime::parse(&bytes).unwrap(), dt);
143    }
144
145    #[test]
146    fn date_time_with_offset_round_trip() {
147        let dt = DateTime {
148            utc_time: [0xC0, 0x79, 0x12, 0x34, 0x56],
149            local_offset: Some(-60),
150        };
151        let bytes = dt.to_bytes();
152        assert_eq!(&bytes[..4], &[0x9F, 0x84, 0x41, 0x07]); // len 7
153        let parsed = DateTime::parse(&bytes).unwrap();
154        assert_eq!(parsed, dt);
155        assert_eq!(parsed.local_offset, Some(-60));
156    }
157
158    #[test]
159    fn mutating_offset_changes_bytes_and_length() {
160        let dt = DateTime {
161            utc_time: [0; 5],
162            local_offset: None,
163        };
164        let a = dt.to_bytes();
165        let mut other = dt;
166        other.local_offset = Some(120);
167        let b = other.to_bytes();
168        assert_ne!(a, b);
169        assert_eq!(a.len(), 9);
170        assert_eq!(b.len(), 11);
171    }
172}