Skip to main content

rmt_flute/
lct_ext.rs

1//! LCT Header-Extension Type registry + the EXT_TIME typed extension
2//! (RFC 5651 §5.2.1, §5.2.2, §9.2).
3
4use alloc::vec::Vec;
5
6use crate::error::{Error, Result};
7use crate::ext::{HeaderExtension, WORD};
8
9/// HET for EXT_NOP (No-Operation) — RFC 5651 §5.2.1.
10pub const HET_EXT_NOP: u8 = 0;
11/// HET for EXT_AUTH (Packet Authentication) — RFC 5651 §5.2.1.
12pub const HET_EXT_AUTH: u8 = 1;
13/// HET for EXT_TIME (Timing information) — RFC 5651 §5.2.2.
14pub const HET_EXT_TIME: u8 = 2;
15
16/// A known LCT Header Extension Type (RFC 5651 §5.2.1 / §9.2).
17///
18/// Covers the three base LCT-defined HET values; protocol-instantiation HETs
19/// (`EXT_FTI` 64, `EXT_FDT` 192, `EXT_CENC` 193, …) live in their own modules
20/// and fall under [`LctExtType::Other`] here.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize))]
23#[non_exhaustive]
24pub enum LctExtType {
25    /// EXT_NOP (HET 0) — content ignored by receivers.
26    Nop,
27    /// EXT_AUTH (HET 1) — packet authentication; format out-of-band.
28    Auth,
29    /// EXT_TIME (HET 2) — timing info (SCT/ERT/SLC).
30    Time,
31    /// Any other HET value (protocol-instantiation or unassigned).
32    Other(u8),
33}
34
35impl LctExtType {
36    /// Decode a HET value.
37    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    /// The HET value for this type.
47    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    /// Spec label.
57    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
69// EXT_TIME Use-field bit masks (RFC 5651 §5.2.2, Figure 4). The Use field is
70// the low 16 bits of the first 32-bit word; SCT-High is the MSB.
71/// Use-field bit: Sender Current Time, high 32 bits present.
72pub const USE_SCT_HIGH: u16 = 0x8000;
73/// Use-field bit: Sender Current Time, low 32 bits present.
74pub const USE_SCT_LOW: u16 = 0x4000;
75/// Use-field bit: Expected Residual Time present.
76pub const USE_ERT: u16 = 0x2000;
77/// Use-field bit: Session Last Changed time present.
78pub const USE_SLC: u16 = 0x1000;
79
80// EXT_TIME Use-field sub-masks (RFC 5651 §5.2.2).
81/// Mask for the PI-specific (protocol-instantiation) low 8 bits of the Use field.
82const USE_PI_SPECIFIC_MASK: u16 = 0x00FF;
83/// Mask for the reserved-by-LCT bits in the Use field (bits 8..=11).
84const USE_RESERVED_MASK: u16 = 0x0F00;
85
86/// A decoded EXT_TIME header extension (RFC 5651 §5.2.2, HET = 2).
87///
88/// Carries 0..4 32-bit time values selected by the 16-bit `Use` bit field. When
89/// present they appear in the fixed order SCT-High, SCT-Low, ERT, SLC; each
90/// `Some` value contributes one 32-bit word. The PI-specific low 8 bits of the
91/// `Use` field are preserved verbatim in `pi_specific`.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize))]
94pub struct ExtTime {
95    /// Sender Current Time, MS 32 bits (NTP seconds).
96    pub sct_high: Option<u32>,
97    /// Sender Current Time, LS 32 bits (NTP fraction). If set, `sct_high` MUST
98    /// also be set.
99    pub sct_low: Option<u32>,
100    /// Expected Residual Time, seconds.
101    pub ert: Option<u32>,
102    /// Session Last Changed time, seconds.
103    pub slc: Option<u32>,
104    /// PI-specific low 8 bits of the Use field (out of scope of RFC 5651).
105    pub pi_specific: u8,
106}
107
108impl ExtTime {
109    /// Build the 16-bit Use field from the present values + PI-specific byte.
110    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    /// Number of 32-bit time values that follow the first word.
128    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    /// Total serialized length in bytes (first word + 4 bytes per value).
136    pub fn serialized_len(&self) -> usize {
137        WORD + WORD * self.value_count()
138    }
139
140    /// Decode an EXT_TIME from the *content* of a [`HeaderExtension`] whose HET
141    /// is [`HET_EXT_TIME`] (the content is everything after HET+HEL: the 2-byte
142    /// Use field followed by the time values).
143    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        // Reserved-by-LCT bits (Use & USE_RESERVED_MASK) MUST be 0.
154        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    /// Encode the EXT_TIME content (Use field + present time values) into a
203    /// freshly allocated buffer suitable for [`HeaderExtension::content`].
204    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        // Pad the Use field's word to 4 bytes? No — RFC 5651 packs the Use into
208        // the same word as HET+HEL; the content here starts at the Use field
209        // and the leading 2 HET/HEL bytes belong to the HeaderExtension. So the
210        // first word is HET|HEL|Use, and our content begins at Use (2 bytes),
211        // then 4 bytes per value — total content = 2 + 4*n, +2 (HET/HEL) = 4*(n+1).
212        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    /// Build a generic [`HeaderExtension`] (HET = 2) carrying this EXT_TIME,
222    /// borrowing from `scratch` (which must outlive the returned extension).
223    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        // Use = SCT_HIGH | SCT_LOW = 0xC000. Content = 2 + 8 = 10 bytes.
255        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        // As a whole extension: HET=2, HEL = (2 + 2 + 8)/4 = 3.
266        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        // Values follow in order.
287        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        // Use = SCT_LOW only (0x4000) + one value.
296        let content = [0x40u8, 0x00, 0x00, 0x00, 0x00, 0x01];
297        assert!(matches!(
298            ExtTime::parse(&content),
299            Err(Error::InvalidField { .. })
300        ));
301    }
302}