1use alloc::vec::Vec;
5
6use crate::error::{Error, Result};
7use crate::ext::{HeaderExtension, WORD};
8
9pub const HET_EXT_NOP: u8 = 0;
11pub const HET_EXT_AUTH: u8 = 1;
13pub const HET_EXT_TIME: u8 = 2;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize))]
23#[non_exhaustive]
24pub enum LctExtType {
25 Nop,
27 Auth,
29 Time,
31 Other(u8),
33}
34
35impl LctExtType {
36 pub fn from_het(het: u8) -> Self {
38 match het {
39 HET_EXT_NOP => LctExtType::Nop,
40 HET_EXT_AUTH => LctExtType::Auth,
41 HET_EXT_TIME => LctExtType::Time,
42 other => LctExtType::Other(other),
43 }
44 }
45
46 pub fn het(self) -> u8 {
48 match self {
49 LctExtType::Nop => HET_EXT_NOP,
50 LctExtType::Auth => HET_EXT_AUTH,
51 LctExtType::Time => HET_EXT_TIME,
52 LctExtType::Other(v) => v,
53 }
54 }
55
56 pub fn name(&self) -> &'static str {
58 match self {
59 LctExtType::Nop => "EXT_NOP",
60 LctExtType::Auth => "EXT_AUTH",
61 LctExtType::Time => "EXT_TIME",
62 LctExtType::Other(_) => "other",
63 }
64 }
65}
66
67broadcast_common::impl_spec_display!(LctExtType, Other);
68
69pub const USE_SCT_HIGH: u16 = 0x8000;
73pub const USE_SCT_LOW: u16 = 0x4000;
75pub const USE_ERT: u16 = 0x2000;
77pub const USE_SLC: u16 = 0x1000;
79
80const USE_PI_SPECIFIC_MASK: u16 = 0x00FF;
83const USE_RESERVED_MASK: u16 = 0x0F00;
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize))]
94pub struct ExtTime {
95 pub sct_high: Option<u32>,
97 pub sct_low: Option<u32>,
100 pub ert: Option<u32>,
102 pub slc: Option<u32>,
104 pub pi_specific: u8,
106}
107
108impl ExtTime {
109 pub fn use_field(&self) -> u16 {
111 let mut u = self.pi_specific as u16;
112 if self.sct_high.is_some() {
113 u |= USE_SCT_HIGH;
114 }
115 if self.sct_low.is_some() {
116 u |= USE_SCT_LOW;
117 }
118 if self.ert.is_some() {
119 u |= USE_ERT;
120 }
121 if self.slc.is_some() {
122 u |= USE_SLC;
123 }
124 u
125 }
126
127 fn value_count(&self) -> usize {
129 self.sct_high.is_some() as usize
130 + self.sct_low.is_some() as usize
131 + self.ert.is_some() as usize
132 + self.slc.is_some() as usize
133 }
134
135 pub fn serialized_len(&self) -> usize {
137 WORD + WORD * self.value_count()
138 }
139
140 pub fn parse(content: &[u8]) -> Result<Self> {
144 if content.len() < 2 {
145 return Err(Error::BufferTooShort {
146 need: 2,
147 have: content.len(),
148 what: "EXT_TIME Use field",
149 });
150 }
151 let use_field = u16::from_be_bytes([content[0], content[1]]);
152 let pi_specific = (use_field & USE_PI_SPECIFIC_MASK) as u8;
153 if use_field & USE_RESERVED_MASK != 0 {
155 return Err(Error::InvalidField {
156 what: "EXT_TIME Use reserved",
157 reason: "reserved-by-LCT Use bits must be zero",
158 });
159 }
160 if (use_field & USE_SCT_LOW != 0) && (use_field & USE_SCT_HIGH == 0) {
161 return Err(Error::InvalidField {
162 what: "EXT_TIME Use",
163 reason: "SCT-Low set without SCT-High",
164 });
165 }
166
167 let mut off = 2;
168 let mut take = |present: bool| -> Result<Option<u32>> {
169 if !present {
170 return Ok(None);
171 }
172 if content.len() < off + WORD {
173 return Err(Error::BufferTooShort {
174 need: off + WORD,
175 have: content.len(),
176 what: "EXT_TIME time value",
177 });
178 }
179 let v = u32::from_be_bytes([
180 content[off],
181 content[off + 1],
182 content[off + 2],
183 content[off + 3],
184 ]);
185 off += WORD;
186 Ok(Some(v))
187 };
188 let sct_high = take(use_field & USE_SCT_HIGH != 0)?;
189 let sct_low = take(use_field & USE_SCT_LOW != 0)?;
190 let ert = take(use_field & USE_ERT != 0)?;
191 let slc = take(use_field & USE_SLC != 0)?;
192
193 Ok(ExtTime {
194 sct_high,
195 sct_low,
196 ert,
197 slc,
198 pi_specific,
199 })
200 }
201
202 pub fn to_content(&self) -> Vec<u8> {
205 let mut out = Vec::with_capacity(self.serialized_len());
206 out.extend_from_slice(&self.use_field().to_be_bytes());
207 for v in [self.sct_high, self.sct_low, self.ert, self.slc]
213 .into_iter()
214 .flatten()
215 {
216 out.extend_from_slice(&v.to_be_bytes());
217 }
218 out
219 }
220
221 pub fn to_extension<'a>(&self, scratch: &'a mut Vec<u8>) -> HeaderExtension<'a> {
224 *scratch = self.to_content();
225 HeaderExtension::new(HET_EXT_TIME, scratch)
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use alloc::string::ToString;
233 use alloc::vec;
234
235 #[test]
236 fn ext_type_round_trip() {
237 for het in [0u8, 1, 2, 64, 192, 255] {
238 assert_eq!(LctExtType::from_het(het).het(), het);
239 }
240 assert_eq!(LctExtType::from_het(64), LctExtType::Other(64));
241 assert_eq!(LctExtType::Time.to_string(), "EXT_TIME");
242 assert_eq!(LctExtType::Other(64).to_string(), "other(0x40)");
243 }
244
245 #[test]
246 fn ext_time_sct_high_low_round_trip() {
247 let t = ExtTime {
248 sct_high: Some(0x1122_3344),
249 sct_low: Some(0x5566_7788),
250 ert: None,
251 slc: None,
252 pi_specific: 0,
253 };
254 assert_eq!(t.use_field(), 0xC000);
256 let content = t.to_content();
257 assert_eq!(content.len(), 10);
258 assert_eq!(&content[0..2], &[0xC0, 0x00]);
259 assert_eq!(&content[2..6], &[0x11, 0x22, 0x33, 0x44]);
260 assert_eq!(&content[6..10], &[0x55, 0x66, 0x77, 0x88]);
261
262 let re = ExtTime::parse(&content).unwrap();
263 assert_eq!(re, t);
264
265 let mut scratch = vec![];
267 let ext = t.to_extension(&mut scratch);
268 assert_eq!(ext.het, 2);
269 assert_eq!(ext.serialized_len(), 12);
270 assert_eq!(ext.hel(), 3);
271 }
272
273 #[test]
274 fn ext_time_all_four_values_in_order() {
275 let t = ExtTime {
276 sct_high: Some(1),
277 sct_low: Some(2),
278 ert: Some(3),
279 slc: Some(4),
280 pi_specific: 0xAB,
281 };
282 assert_eq!(t.use_field(), 0xF000 | 0x00AB);
283 let content = t.to_content();
284 let re = ExtTime::parse(&content).unwrap();
285 assert_eq!(re, t);
286 assert_eq!(&content[2..6], &1u32.to_be_bytes());
288 assert_eq!(&content[6..10], &2u32.to_be_bytes());
289 assert_eq!(&content[10..14], &3u32.to_be_bytes());
290 assert_eq!(&content[14..18], &4u32.to_be_bytes());
291 }
292
293 #[test]
294 fn ext_time_rejects_sct_low_without_high() {
295 let content = [0x40u8, 0x00, 0x00, 0x00, 0x00, 0x01];
297 assert!(matches!(
298 ExtTime::parse(&content),
299 Err(Error::InvalidField { .. })
300 ));
301 }
302}