Skip to main content

rmt_flute/
lct.rs

1//! LCT — Layered Coding Transport header (RFC 5651 §5).
2//!
3//! Every packet of an LCT session carries a variable-size LCT header. The fixed
4//! first 32 bits carry the version, the `C`/`PSI`/`S`/`O`/`H`/`A`/`B` flags,
5//! `HDR_LEN`, and the Codepoint. After that come three variable-length fields
6//! whose byte-sizes are driven **entirely** by the flags — this is the LCT
7//! correctness point (RFC 5651 §5.1):
8//!
9//! - **CCI** (Congestion Control Information) = `32*(C+1)` bits → `4*(C+1)`
10//!   bytes (4, 8, 12 or 16).
11//! - **TSI** (Transport Session Identifier) = `32*S + 16*H` bits.
12//! - **TOI** (Transport Object Identifier) = `32*O + 16*H` bits.
13//!
14//! The single `H` half-word flag feeds **both** the TSI and the TOI length
15//! formulas independently (so the aggregate TSI+TOI length is always a whole
16//! number of 32-bit words). After the variable fields, header extensions occupy
17//! the space up to `HDR_LEN` words ([`crate::ext`]).
18
19use alloc::vec::Vec;
20
21use crate::error::{Error, Result};
22use crate::ext::{self, HeaderExtension, WORD};
23
24/// LCT version number for RFC 5651.
25pub const LCT_VERSION: u8 = 1;
26/// Size in bytes of the fixed first word (V/C/PSI/S/O/H/Res/A/B + HDR_LEN + CP).
27pub const FIXED_HEADER_LEN: usize = 4;
28
29// First-word flag bits for the 16-bit packed field (RFC 5651 §5.1).
30/// Bit mask for the `A` (Close Session) flag in the first header word.
31const FLAG_A: u16 = 0x0002;
32/// Bit mask for the `B` (Close Object) flag in the first header word.
33const FLAG_B: u16 = 0x0001;
34
35/// A decoded LCT header (RFC 5651 §5.1).
36///
37/// The flag-driven field widths are reconstructed from the typed fields on
38/// serialize: `C` from `cci.len()`, `S`/`O`/`H` from `tsi`/`toi`, and `HDR_LEN`
39/// from the total. None of the wire length/flag bytes are stored raw.
40#[derive(Debug, Clone, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize))]
42pub struct LctHeader<'a> {
43    /// LCT version (`V`). RFC 5651 = [`LCT_VERSION`] (1).
44    pub version: u8,
45    /// Protocol-Specific Indication (`PSI`, 2 bits). Meaning is per instantiation.
46    pub psi: u8,
47    /// Close Session flag (`A`).
48    pub close_session: bool,
49    /// Close Object flag (`B`).
50    pub close_object: bool,
51    /// Codepoint (`CP`, 8 bits) — opaque codec identifier.
52    pub codepoint: u8,
53    /// Congestion Control Information. Length is `4*(C+1)` bytes; `C` is derived
54    /// as `cci.len()/4 − 1`, so it must be 4, 8, 12 or 16 bytes.
55    pub cci: &'a [u8],
56    /// Transport Session Identifier. Length `4*S + 2*H` bytes (0, 2, 4 or 6).
57    pub tsi: &'a [u8],
58    /// Transport Object Identifier. Length `4*O + 2*H` bytes (0, 2, 4, …, 14).
59    pub toi: &'a [u8],
60    /// Header-extension chain occupying the header space up to `HDR_LEN`.
61    pub extensions: Vec<HeaderExtension<'a>>,
62}
63
64/// Decode `C`/`S`/`O`/`H` flags from the requested CCI/TSI/TOI byte lengths.
65///
66/// Returns `(c, s, o, h)`. The `H` half-word contributes 2 bytes to **both**
67/// TSI and TOI, so the two lengths must agree on whether `H` is set.
68fn flags_from_lengths(cci: usize, tsi: usize, toi: usize) -> Result<(u8, u8, u8, u8)> {
69    // CCI: 4*(C+1) bytes → C in 0..=3.
70    if cci == 0 || !cci.is_multiple_of(WORD) {
71        return Err(Error::InvalidField {
72            what: "CCI",
73            reason: "CCI length must be a non-zero multiple of 4 bytes",
74        });
75    }
76    let words = cci / WORD;
77    if !(1..=4).contains(&words) {
78        return Err(Error::InvalidField {
79            what: "CCI",
80            reason: "CCI length must be 4, 8, 12 or 16 bytes (C in 0..=3)",
81        });
82    }
83    let c = (words - 1) as u8;
84
85    // TSI = 4*S + 2*H bytes; TOI = 4*O + 2*H bytes. The half-word parity (odd
86    // multiple of 2 bytes) determines H; it MUST match across TSI and TOI.
87    let h_tsi = !tsi.is_multiple_of(WORD);
88    let h_toi = !toi.is_multiple_of(WORD);
89    if h_tsi != h_toi {
90        return Err(Error::InvalidField {
91            what: "H",
92            reason: "TSI and TOI must agree on the shared half-word (H) bit",
93        });
94    }
95    if !tsi.is_multiple_of(2) || !toi.is_multiple_of(2) {
96        return Err(Error::InvalidField {
97            what: "TSI/TOI",
98            reason: "TSI and TOI lengths must be a whole number of 16-bit half-words",
99        });
100    }
101    let h = u8::from(h_tsi);
102    let s_bytes = tsi - (2 * h as usize);
103    let o_bytes = toi - (2 * h as usize);
104    let s = (s_bytes / WORD) as u8;
105    let o = (o_bytes / WORD) as u8;
106    if s > 1 {
107        return Err(Error::InvalidField {
108            what: "S",
109            reason: "TSI 32-bit-word count (S) must be 0 or 1",
110        });
111    }
112    // `O` is a TWO-bit field (RFC 5651 §5.1), at bits 5..6 of the flags word
113    // and masked `& 0x03` on serialize — exactly like `C` and `PSI`. The bound
114    // was `> 7`, which is the only one of the three that contradicted both its
115    // own field width and this struct's doc ("TOI ... 0, 2, 4, ..., 14", i.e.
116    // 4*O + 2*H with O <= 3).
117    //
118    // The consequence was silent, not loud: a TOI of 16/20/24/28/30 bytes gave
119    // o = 4..=7, passed the old check, and then serialized as `o & 0x03` — so
120    // the wire declared a SHORTER TOI than the bytes actually written, and a
121    // reparse read the surplus as a header-extension chain. That breaks the
122    // workspace's byte-exact round-trip invariant, and it is reachable by a
123    // well-behaved sender: wide TOIs are legitimate in FLUTE.
124    if o > 3 {
125        return Err(Error::InvalidField {
126            what: "O",
127            reason: "TOI 32-bit-word count (O) must be 0..=3 (2-bit field)",
128        });
129    }
130    Ok((c, s, o, h))
131}
132
133impl<'a> LctHeader<'a> {
134    /// CCI length in bytes = `4*(C+1)`.
135    fn cci_len(c: u8) -> usize {
136        WORD * (c as usize + 1)
137    }
138    /// TSI length in bytes = `4*S + 2*H`.
139    fn tsi_len(s: u8, h: u8) -> usize {
140        WORD * s as usize + 2 * h as usize
141    }
142    /// TOI length in bytes = `4*O + 2*H`.
143    fn toi_len(o: u8, h: u8) -> usize {
144        WORD * o as usize + 2 * h as usize
145    }
146
147    /// The `C` flag value (CCI words − 1), derived from the CCI length.
148    pub fn c_flag(&self) -> u8 {
149        (self.cci.len() / WORD).saturating_sub(1) as u8
150    }
151    /// The `H` half-word flag, derived from TSI/TOI parity.
152    ///
153    /// RFC 5651 §5.1: TSI and TOI **both** carry the half-word when `H` is set,
154    /// so both must agree (the `flags_from_lengths` validator enforces this on
155    /// serialize). A validly constructed header always has matching parity; we
156    /// use `&&` to reflect the RFC constraint rather than the `||` that would
157    /// mask a corrupted struct.
158    pub fn h_flag(&self) -> u8 {
159        u8::from(!self.tsi.len().is_multiple_of(WORD) && !self.toi.len().is_multiple_of(WORD))
160    }
161    /// The `S` flag (full 32-bit words in TSI).
162    pub fn s_flag(&self) -> u8 {
163        (self.tsi.len() / WORD) as u8
164    }
165    /// The `O` flag (full 32-bit words in TOI).
166    pub fn o_flag(&self) -> u8 {
167        (self.toi.len() / WORD) as u8
168    }
169
170    /// Total bytes of the fixed + CCI + TSI + TOI portion (no extensions).
171    fn base_len(&self) -> usize {
172        FIXED_HEADER_LEN + self.cci.len() + self.tsi.len() + self.toi.len()
173    }
174
175    /// Total serialized length in bytes (fixed + CCI/TSI/TOI + extensions).
176    pub fn serialized_len(&self) -> usize {
177        self.base_len() + ext::chain_len(&self.extensions)
178    }
179
180    /// `HDR_LEN` as it appears on the wire — total header length in 32-bit words.
181    pub fn hdr_len(&self) -> usize {
182        self.serialized_len() / WORD
183    }
184
185    /// Parse an LCT header from the start of `data`. Reads `HDR_LEN` to find the
186    /// end of the header (incl. extensions); trailing bytes (FEC Payload ID /
187    /// payload) are left for the caller. Returns the header and bytes consumed.
188    pub fn parse(data: &'a [u8]) -> Result<(Self, usize)> {
189        if data.len() < FIXED_HEADER_LEN {
190            return Err(Error::BufferTooShort {
191                need: FIXED_HEADER_LEN,
192                have: data.len(),
193                what: "LCT fixed header",
194            });
195        }
196        // First 16 bits: V(4) C(2) PSI(2) S(1) O(2) H(1) Res(2) A(1) B(1).
197        let w = u16::from_be_bytes([data[0], data[1]]);
198        let version = (w >> 12) as u8 & 0x0F;
199        let c = (w >> 10) as u8 & 0x03;
200        let psi = (w >> 8) as u8 & 0x03;
201        let s = (w >> 7) as u8 & 0x01;
202        let o = (w >> 5) as u8 & 0x03;
203        let h = (w >> 4) as u8 & 0x01;
204        // Res = bits 2..3 (ignored). A = bit 1, B = bit 0.
205        let close_session = (w & FLAG_A) != 0;
206        let close_object = (w & FLAG_B) != 0;
207        let hdr_len = data[2];
208        let codepoint = data[3];
209
210        let total = hdr_len as usize * WORD;
211        if total < FIXED_HEADER_LEN {
212            return Err(Error::InconsistentLength {
213                length: hdr_len,
214                reason: "HDR_LEN smaller than the fixed header word",
215            });
216        }
217        if data.len() < total {
218            return Err(Error::BufferTooShort {
219                need: total,
220                have: data.len(),
221                what: "LCT header (per HDR_LEN)",
222            });
223        }
224
225        let cci_len = Self::cci_len(c);
226        let tsi_len = Self::tsi_len(s, h);
227        let toi_len = Self::toi_len(o, h);
228        let base = FIXED_HEADER_LEN + cci_len + tsi_len + toi_len;
229        if base > total {
230            return Err(Error::InconsistentLength {
231                length: hdr_len,
232                reason: "HDR_LEN too small for the flag-derived CCI/TSI/TOI fields",
233            });
234        }
235
236        let mut off = FIXED_HEADER_LEN;
237        let cci = &data[off..off + cci_len];
238        off += cci_len;
239        let tsi = &data[off..off + tsi_len];
240        off += tsi_len;
241        let toi = &data[off..off + toi_len];
242        off += toi_len;
243
244        let extensions = ext::parse_chain(&data[off..total])?;
245
246        Ok((
247            LctHeader {
248                version,
249                psi,
250                close_session,
251                close_object,
252                codepoint,
253                cci,
254                tsi,
255                toi,
256                extensions,
257            },
258            total,
259        ))
260    }
261
262    /// Serialize the LCT header into `out`, recomputing the flag bits and
263    /// `HDR_LEN` from the typed fields. Returns bytes written.
264    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
265        let total = self.serialized_len();
266        if out.len() < total {
267            return Err(Error::OutputBufferTooSmall {
268                need: total,
269                have: out.len(),
270            });
271        }
272        if self.version > 0x0F {
273            return Err(Error::FieldTooWide {
274                what: "version",
275                value: self.version as u64,
276                bits: 4,
277            });
278        }
279        if self.psi > 0x03 {
280            return Err(Error::FieldTooWide {
281                what: "PSI",
282                value: self.psi as u64,
283                bits: 2,
284            });
285        }
286        // Validate the flag-derived widths (also yields C/S/O/H).
287        let (c, s, o, h) = flags_from_lengths(self.cci.len(), self.tsi.len(), self.toi.len())?;
288
289        let words = total / WORD;
290        if !total.is_multiple_of(WORD) {
291            return Err(Error::InvalidField {
292                what: "HDR_LEN",
293                reason: "total LCT header length is not a multiple of 4 bytes",
294            });
295        }
296        if words > u8::MAX as usize {
297            return Err(Error::FieldTooWide {
298                what: "HDR_LEN",
299                value: words as u64,
300                bits: 8,
301            });
302        }
303
304        // Pack the first 16-bit word MSB-first:
305        // V(4) C(2) PSI(2) S(1) O(2) H(1) Res(2)=0 A(1) B(1).
306        let mut w: u16 = 0;
307        w |= (self.version as u16 & 0x0F) << 12;
308        w |= (c as u16 & 0x03) << 10;
309        w |= (self.psi as u16 & 0x03) << 8;
310        w |= (s as u16 & 0x01) << 7;
311        w |= (o as u16 & 0x03) << 5;
312        w |= (h as u16 & 0x01) << 4;
313        // Res (bits 2..3) = 0.
314        if self.close_session {
315            w |= FLAG_A;
316        }
317        if self.close_object {
318            w |= FLAG_B;
319        }
320        out[0..2].copy_from_slice(&w.to_be_bytes());
321        out[2] = words as u8;
322        out[3] = self.codepoint;
323
324        let mut off = FIXED_HEADER_LEN;
325        out[off..off + self.cci.len()].copy_from_slice(self.cci);
326        off += self.cci.len();
327        out[off..off + self.tsi.len()].copy_from_slice(self.tsi);
328        off += self.tsi.len();
329        out[off..off + self.toi.len()].copy_from_slice(self.toi);
330        off += self.toi.len();
331
332        off += ext::serialize_chain(&self.extensions, &mut out[off..])?;
333        Ok(off)
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use alloc::vec;
341
342    // Minimal header: C=0 (CCI 4 bytes), S=0, O=0, H=0 (no TSI/TOI), no ext.
343    #[test]
344    fn minimal_header_exact_wire_bytes() {
345        let cci = [0x00u8, 0x00, 0x00, 0x01];
346        let hdr = LctHeader {
347            version: LCT_VERSION,
348            psi: 0,
349            close_session: false,
350            close_object: false,
351            codepoint: 0x00,
352            cci: &cci,
353            tsi: &[],
354            toi: &[],
355            extensions: vec![],
356        };
357        // HDR_LEN = (4 fixed + 4 cci) / 4 = 2.
358        assert_eq!(hdr.hdr_len(), 2);
359        let mut out = vec![0u8; hdr.serialized_len()];
360        let n = hdr.serialize_into(&mut out).unwrap();
361        assert_eq!(n, 8);
362        // First word: V=1 (0x1000), all flags 0 → 0x1000. HDR_LEN=2, CP=0.
363        assert_eq!(&out[0..4], &[0x10, 0x00, 0x02, 0x00]);
364        assert_eq!(&out[4..8], &cci);
365        let (re, used) = LctHeader::parse(&out).unwrap();
366        assert_eq!(used, 8);
367        assert_eq!(re, hdr);
368    }
369
370    // THE flag-dependent-size test: C=1 (CCI 8), S=1 + H=1 (TSI 6), O=1 + H=1
371    // (TOI 6). All three widths differ from the minimal case.
372    /// A TOI wider than the 2-bit `O` field can describe must be REJECTED,
373    /// not silently truncated.
374    ///
375    /// `O` is 2 bits (RFC 5651 §5.1) and is masked `& 0x03` on serialize, but
376    /// the bound was `o > 7`. A 16-byte TOI gives `o = 4`: it passed the check
377    /// and then encoded as `O = 0`, so the wire declared a zero-length TOI
378    /// while 16 TOI bytes were written. A reparse then read those bytes as a
379    /// header-extension chain — a byte-exact round-trip failure produced by a
380    /// perfectly well-behaved sender, since wide TOIs are legitimate in FLUTE.
381    #[test]
382    fn a_toi_too_wide_for_the_two_bit_o_field_is_rejected_not_truncated() {
383        let cci = [0u8; 4];
384        let toi = [0u8; 16]; // o = 4, which the 2-bit O field cannot express
385        let hdr = LctHeader {
386            version: LCT_VERSION,
387            psi: 0,
388            close_session: false,
389            close_object: false,
390            codepoint: 0,
391            cci: &cci,
392            tsi: &[],
393            toi: &toi,
394            extensions: vec![],
395        };
396
397        let mut out = vec![0u8; 64];
398        let err = hdr
399            .serialize_into(&mut out)
400            .expect_err("a 16-byte TOI must be refused, not encoded as O=0");
401        assert!(
402            matches!(err, Error::InvalidField { what: "O", .. }),
403            "the error must name the O field, got: {err:?}"
404        );
405    }
406
407    #[test]
408    fn flag_dependent_widths_round_trip() {
409        let cci = [0xAAu8, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11];
410        let tsi = [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06]; // 4*1 + 2*1 = 6 bytes
411        let toi = [0x10u8, 0x20, 0x30, 0x40, 0x50, 0x60]; // 4*1 + 2*1 = 6 bytes
412        let hdr = LctHeader {
413            version: LCT_VERSION,
414            psi: 0b10,
415            close_session: true,
416            close_object: false,
417            codepoint: 0x42,
418            cci: &cci,
419            tsi: &tsi,
420            toi: &toi,
421            extensions: vec![],
422        };
423        assert_eq!(hdr.c_flag(), 1);
424        assert_eq!(hdr.s_flag(), 1);
425        assert_eq!(hdr.o_flag(), 1);
426        assert_eq!(hdr.h_flag(), 1);
427
428        // base = 4 + 8 + 6 + 6 = 24 → HDR_LEN 6.
429        assert_eq!(hdr.serialized_len(), 24);
430        assert_eq!(hdr.hdr_len(), 6);
431
432        let mut out = vec![0u8; hdr.serialized_len()];
433        let n = hdr.serialize_into(&mut out).unwrap();
434        assert_eq!(n, 24);
435
436        // Verify the packed first word bit-for-bit.
437        // V=1 (1<<12=0x1000), C=1 (1<<10=0x0400), PSI=10 (2<<8=0x0200),
438        // S=1 (1<<7=0x0080), O=1 (1<<5=0x0020), H=1 (1<<4=0x0010), A=1 (0x0002).
439        let expect = 0x1000 | 0x0400 | 0x0200 | 0x0080 | 0x0020 | 0x0010 | 0x0002;
440        assert_eq!(u16::from_be_bytes([out[0], out[1]]), expect);
441        assert_eq!(out[2], 6); // HDR_LEN
442        assert_eq!(out[3], 0x42); // CP
443
444        let (re, used) = LctHeader::parse(&out).unwrap();
445        assert_eq!(used, 24);
446        assert_eq!(re, hdr);
447        // The reparse must recover the exact widths.
448        assert_eq!(re.cci.len(), 8);
449        assert_eq!(re.tsi.len(), 6);
450        assert_eq!(re.toi.len(), 6);
451    }
452
453    // H=0 vs H=1 must change BOTH TSI and TOI widths (the shared-half-word point).
454    #[test]
455    fn shared_h_bit_feeds_both_tsi_and_toi() {
456        let cci = [0u8; 4];
457        // H=1: TSI = 2 (S=0,H=1), TOI = 2 (O=0,H=1).
458        let tsi = [0xABu8, 0xCD];
459        let toi = [0x12u8, 0x34];
460        let hdr = LctHeader {
461            version: LCT_VERSION,
462            psi: 0,
463            close_session: false,
464            close_object: false,
465            codepoint: 0,
466            cci: &cci,
467            tsi: &tsi,
468            toi: &toi,
469            extensions: vec![],
470        };
471        assert_eq!(hdr.h_flag(), 1);
472        assert_eq!(hdr.s_flag(), 0);
473        assert_eq!(hdr.o_flag(), 0);
474        // base = 4 + 4 + 2 + 2 = 12 → 3 words.
475        assert_eq!(hdr.hdr_len(), 3);
476        let mut out = vec![0u8; hdr.serialized_len()];
477        hdr.serialize_into(&mut out).unwrap();
478        let (re, _) = LctHeader::parse(&out).unwrap();
479        assert_eq!(re, hdr);
480    }
481
482    // Mutation bite: changing the codepoint changes the wire byte.
483    #[test]
484    fn mutating_codepoint_changes_wire() {
485        let cci = [0u8; 4];
486        let mk = |cp: u8| {
487            let mut out = vec![0u8; 8];
488            LctHeader {
489                version: LCT_VERSION,
490                psi: 0,
491                close_session: false,
492                close_object: false,
493                codepoint: cp,
494                cci: &cci,
495                tsi: &[],
496                toi: &[],
497                extensions: vec![],
498            }
499            .serialize_into(&mut out)
500            .unwrap();
501            out
502        };
503        let a = mk(0x00);
504        let b = mk(0x7F);
505        assert_ne!(a, b);
506        assert_eq!(a[3], 0x00);
507        assert_eq!(b[3], 0x7F);
508    }
509
510    // Header WITH a 2-element extension chain round-trips and HDR_LEN grows.
511    #[test]
512    fn header_with_extension_chain() {
513        let cci = [0u8; 4];
514        let tsi = [0x00u8, 0x00, 0x00, 0x05]; // S=1, H=0
515        let nop = [0u8; 2]; // EXT_NOP HEL=1: HET+HEL+2 = 4
516        let ext_content = [0xAAu8, 0xBB, 0xCC]; // fixed ext (HET 200)
517        let exts = vec![
518            HeaderExtension::new(0, &nop),
519            HeaderExtension::new(200, &ext_content),
520        ];
521        let hdr = LctHeader {
522            version: LCT_VERSION,
523            psi: 0,
524            close_session: false,
525            close_object: false,
526            codepoint: 0,
527            cci: &cci,
528            tsi: &tsi,
529            toi: &[],
530            extensions: exts,
531        };
532        // base = 4 + 4 + 4 + 0 = 12; ext = 4 + 4 = 8; total 20 → HDR_LEN 5.
533        assert_eq!(hdr.serialized_len(), 20);
534        assert_eq!(hdr.hdr_len(), 5);
535        let mut out = vec![0u8; hdr.serialized_len()];
536        hdr.serialize_into(&mut out).unwrap();
537        let (re, used) = LctHeader::parse(&out).unwrap();
538        assert_eq!(used, 20);
539        assert_eq!(re, hdr);
540        assert_eq!(re.extensions.len(), 2);
541    }
542
543    #[test]
544    fn rejects_bad_cci_length() {
545        let cci = [0u8; 3]; // not a multiple of 4
546        let hdr = LctHeader {
547            version: LCT_VERSION,
548            psi: 0,
549            close_session: false,
550            close_object: false,
551            codepoint: 0,
552            cci: &cci,
553            tsi: &[],
554            toi: &[],
555            extensions: vec![],
556        };
557        let mut out = vec![0u8; 32];
558        assert!(matches!(
559            hdr.serialize_into(&mut out),
560            Err(Error::InvalidField { .. })
561        ));
562    }
563
564    #[test]
565    fn rejects_mismatched_h() {
566        // TSI has the half-word (odd 2-byte), TOI does not → H disagreement.
567        let cci = [0u8; 4];
568        let tsi = [0u8; 2]; // H=1
569        let toi = [0u8; 4]; // H=0
570        let hdr = LctHeader {
571            version: LCT_VERSION,
572            psi: 0,
573            close_session: false,
574            close_object: false,
575            codepoint: 0,
576            cci: &cci,
577            tsi: &tsi,
578            toi: &toi,
579            extensions: vec![],
580        };
581        let mut out = vec![0u8; 32];
582        assert!(matches!(
583            hdr.serialize_into(&mut out),
584            Err(Error::InvalidField { what: "H", .. })
585        ));
586    }
587}