use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::ext::{self, HeaderExtension, WORD};
pub const LCT_VERSION: u8 = 1;
pub const FIXED_HEADER_LEN: usize = 4;
const FLAG_A: u16 = 0x0002;
const FLAG_B: u16 = 0x0001;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct LctHeader<'a> {
pub version: u8,
pub psi: u8,
pub close_session: bool,
pub close_object: bool,
pub codepoint: u8,
pub cci: &'a [u8],
pub tsi: &'a [u8],
pub toi: &'a [u8],
pub extensions: Vec<HeaderExtension<'a>>,
}
fn flags_from_lengths(cci: usize, tsi: usize, toi: usize) -> Result<(u8, u8, u8, u8)> {
if cci == 0 || cci % WORD != 0 {
return Err(Error::InvalidField {
what: "CCI",
reason: "CCI length must be a non-zero multiple of 4 bytes",
});
}
let words = cci / WORD;
if !(1..=4).contains(&words) {
return Err(Error::InvalidField {
what: "CCI",
reason: "CCI length must be 4, 8, 12 or 16 bytes (C in 0..=3)",
});
}
let c = (words - 1) as u8;
let h_tsi = (tsi % WORD) != 0;
let h_toi = (toi % WORD) != 0;
if h_tsi != h_toi {
return Err(Error::InvalidField {
what: "H",
reason: "TSI and TOI must agree on the shared half-word (H) bit",
});
}
if tsi % 2 != 0 || toi % 2 != 0 {
return Err(Error::InvalidField {
what: "TSI/TOI",
reason: "TSI and TOI lengths must be a whole number of 16-bit half-words",
});
}
let h = u8::from(h_tsi);
let s_bytes = tsi - (2 * h as usize);
let o_bytes = toi - (2 * h as usize);
let s = (s_bytes / WORD) as u8;
let o = (o_bytes / WORD) as u8;
if s > 1 {
return Err(Error::InvalidField {
what: "S",
reason: "TSI 32-bit-word count (S) must be 0 or 1",
});
}
if o > 7 {
return Err(Error::InvalidField {
what: "O",
reason: "TOI 32-bit-word count (O) must be 0..=7",
});
}
Ok((c, s, o, h))
}
impl<'a> LctHeader<'a> {
fn cci_len(c: u8) -> usize {
WORD * (c as usize + 1)
}
fn tsi_len(s: u8, h: u8) -> usize {
WORD * s as usize + 2 * h as usize
}
fn toi_len(o: u8, h: u8) -> usize {
WORD * o as usize + 2 * h as usize
}
pub fn c_flag(&self) -> u8 {
(self.cci.len() / WORD).saturating_sub(1) as u8
}
pub fn h_flag(&self) -> u8 {
u8::from((self.tsi.len() % WORD) != 0 && (self.toi.len() % WORD) != 0)
}
pub fn s_flag(&self) -> u8 {
(self.tsi.len() / WORD) as u8
}
pub fn o_flag(&self) -> u8 {
(self.toi.len() / WORD) as u8
}
fn base_len(&self) -> usize {
FIXED_HEADER_LEN + self.cci.len() + self.tsi.len() + self.toi.len()
}
pub fn serialized_len(&self) -> usize {
self.base_len() + ext::chain_len(&self.extensions)
}
pub fn hdr_len(&self) -> usize {
self.serialized_len() / WORD
}
pub fn parse(data: &'a [u8]) -> Result<(Self, usize)> {
if data.len() < FIXED_HEADER_LEN {
return Err(Error::BufferTooShort {
need: FIXED_HEADER_LEN,
have: data.len(),
what: "LCT fixed header",
});
}
let w = u16::from_be_bytes([data[0], data[1]]);
let version = (w >> 12) as u8 & 0x0F;
let c = (w >> 10) as u8 & 0x03;
let psi = (w >> 8) as u8 & 0x03;
let s = (w >> 7) as u8 & 0x01;
let o = (w >> 5) as u8 & 0x03;
let h = (w >> 4) as u8 & 0x01;
let close_session = (w & FLAG_A) != 0;
let close_object = (w & FLAG_B) != 0;
let hdr_len = data[2];
let codepoint = data[3];
let total = hdr_len as usize * WORD;
if total < FIXED_HEADER_LEN {
return Err(Error::InconsistentLength {
length: hdr_len,
reason: "HDR_LEN smaller than the fixed header word",
});
}
if data.len() < total {
return Err(Error::BufferTooShort {
need: total,
have: data.len(),
what: "LCT header (per HDR_LEN)",
});
}
let cci_len = Self::cci_len(c);
let tsi_len = Self::tsi_len(s, h);
let toi_len = Self::toi_len(o, h);
let base = FIXED_HEADER_LEN + cci_len + tsi_len + toi_len;
if base > total {
return Err(Error::InconsistentLength {
length: hdr_len,
reason: "HDR_LEN too small for the flag-derived CCI/TSI/TOI fields",
});
}
let mut off = FIXED_HEADER_LEN;
let cci = &data[off..off + cci_len];
off += cci_len;
let tsi = &data[off..off + tsi_len];
off += tsi_len;
let toi = &data[off..off + toi_len];
off += toi_len;
let extensions = ext::parse_chain(&data[off..total])?;
Ok((
LctHeader {
version,
psi,
close_session,
close_object,
codepoint,
cci,
tsi,
toi,
extensions,
},
total,
))
}
pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
let total = self.serialized_len();
if out.len() < total {
return Err(Error::OutputBufferTooSmall {
need: total,
have: out.len(),
});
}
if self.version > 0x0F {
return Err(Error::FieldTooWide {
what: "version",
value: self.version as u64,
bits: 4,
});
}
if self.psi > 0x03 {
return Err(Error::FieldTooWide {
what: "PSI",
value: self.psi as u64,
bits: 2,
});
}
let (c, s, o, h) = flags_from_lengths(self.cci.len(), self.tsi.len(), self.toi.len())?;
let words = total / WORD;
if total % WORD != 0 {
return Err(Error::InvalidField {
what: "HDR_LEN",
reason: "total LCT header length is not a multiple of 4 bytes",
});
}
if words > u8::MAX as usize {
return Err(Error::FieldTooWide {
what: "HDR_LEN",
value: words as u64,
bits: 8,
});
}
let mut w: u16 = 0;
w |= (self.version as u16 & 0x0F) << 12;
w |= (c as u16 & 0x03) << 10;
w |= (self.psi as u16 & 0x03) << 8;
w |= (s as u16 & 0x01) << 7;
w |= (o as u16 & 0x03) << 5;
w |= (h as u16 & 0x01) << 4;
if self.close_session {
w |= FLAG_A;
}
if self.close_object {
w |= FLAG_B;
}
out[0..2].copy_from_slice(&w.to_be_bytes());
out[2] = words as u8;
out[3] = self.codepoint;
let mut off = FIXED_HEADER_LEN;
out[off..off + self.cci.len()].copy_from_slice(self.cci);
off += self.cci.len();
out[off..off + self.tsi.len()].copy_from_slice(self.tsi);
off += self.tsi.len();
out[off..off + self.toi.len()].copy_from_slice(self.toi);
off += self.toi.len();
off += ext::serialize_chain(&self.extensions, &mut out[off..])?;
Ok(off)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn minimal_header_exact_wire_bytes() {
let cci = [0x00u8, 0x00, 0x00, 0x01];
let hdr = LctHeader {
version: LCT_VERSION,
psi: 0,
close_session: false,
close_object: false,
codepoint: 0x00,
cci: &cci,
tsi: &[],
toi: &[],
extensions: vec![],
};
assert_eq!(hdr.hdr_len(), 2);
let mut out = vec![0u8; hdr.serialized_len()];
let n = hdr.serialize_into(&mut out).unwrap();
assert_eq!(n, 8);
assert_eq!(&out[0..4], &[0x10, 0x00, 0x02, 0x00]);
assert_eq!(&out[4..8], &cci);
let (re, used) = LctHeader::parse(&out).unwrap();
assert_eq!(used, 8);
assert_eq!(re, hdr);
}
#[test]
fn flag_dependent_widths_round_trip() {
let cci = [0xAAu8, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11];
let tsi = [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06]; let toi = [0x10u8, 0x20, 0x30, 0x40, 0x50, 0x60]; let hdr = LctHeader {
version: LCT_VERSION,
psi: 0b10,
close_session: true,
close_object: false,
codepoint: 0x42,
cci: &cci,
tsi: &tsi,
toi: &toi,
extensions: vec![],
};
assert_eq!(hdr.c_flag(), 1);
assert_eq!(hdr.s_flag(), 1);
assert_eq!(hdr.o_flag(), 1);
assert_eq!(hdr.h_flag(), 1);
assert_eq!(hdr.serialized_len(), 24);
assert_eq!(hdr.hdr_len(), 6);
let mut out = vec![0u8; hdr.serialized_len()];
let n = hdr.serialize_into(&mut out).unwrap();
assert_eq!(n, 24);
let expect = 0x1000 | 0x0400 | 0x0200 | 0x0080 | 0x0020 | 0x0010 | 0x0002;
assert_eq!(u16::from_be_bytes([out[0], out[1]]), expect);
assert_eq!(out[2], 6); assert_eq!(out[3], 0x42);
let (re, used) = LctHeader::parse(&out).unwrap();
assert_eq!(used, 24);
assert_eq!(re, hdr);
assert_eq!(re.cci.len(), 8);
assert_eq!(re.tsi.len(), 6);
assert_eq!(re.toi.len(), 6);
}
#[test]
fn shared_h_bit_feeds_both_tsi_and_toi() {
let cci = [0u8; 4];
let tsi = [0xABu8, 0xCD];
let toi = [0x12u8, 0x34];
let hdr = LctHeader {
version: LCT_VERSION,
psi: 0,
close_session: false,
close_object: false,
codepoint: 0,
cci: &cci,
tsi: &tsi,
toi: &toi,
extensions: vec![],
};
assert_eq!(hdr.h_flag(), 1);
assert_eq!(hdr.s_flag(), 0);
assert_eq!(hdr.o_flag(), 0);
assert_eq!(hdr.hdr_len(), 3);
let mut out = vec![0u8; hdr.serialized_len()];
hdr.serialize_into(&mut out).unwrap();
let (re, _) = LctHeader::parse(&out).unwrap();
assert_eq!(re, hdr);
}
#[test]
fn mutating_codepoint_changes_wire() {
let cci = [0u8; 4];
let mk = |cp: u8| {
let mut out = vec![0u8; 8];
LctHeader {
version: LCT_VERSION,
psi: 0,
close_session: false,
close_object: false,
codepoint: cp,
cci: &cci,
tsi: &[],
toi: &[],
extensions: vec![],
}
.serialize_into(&mut out)
.unwrap();
out
};
let a = mk(0x00);
let b = mk(0x7F);
assert_ne!(a, b);
assert_eq!(a[3], 0x00);
assert_eq!(b[3], 0x7F);
}
#[test]
fn header_with_extension_chain() {
let cci = [0u8; 4];
let tsi = [0x00u8, 0x00, 0x00, 0x05]; let nop = [0u8; 2]; let ext_content = [0xAAu8, 0xBB, 0xCC]; let exts = vec![
HeaderExtension::new(0, &nop),
HeaderExtension::new(200, &ext_content),
];
let hdr = LctHeader {
version: LCT_VERSION,
psi: 0,
close_session: false,
close_object: false,
codepoint: 0,
cci: &cci,
tsi: &tsi,
toi: &[],
extensions: exts,
};
assert_eq!(hdr.serialized_len(), 20);
assert_eq!(hdr.hdr_len(), 5);
let mut out = vec![0u8; hdr.serialized_len()];
hdr.serialize_into(&mut out).unwrap();
let (re, used) = LctHeader::parse(&out).unwrap();
assert_eq!(used, 20);
assert_eq!(re, hdr);
assert_eq!(re.extensions.len(), 2);
}
#[test]
fn rejects_bad_cci_length() {
let cci = [0u8; 3]; let hdr = LctHeader {
version: LCT_VERSION,
psi: 0,
close_session: false,
close_object: false,
codepoint: 0,
cci: &cci,
tsi: &[],
toi: &[],
extensions: vec![],
};
let mut out = vec![0u8; 32];
assert!(matches!(
hdr.serialize_into(&mut out),
Err(Error::InvalidField { .. })
));
}
#[test]
fn rejects_mismatched_h() {
let cci = [0u8; 4];
let tsi = [0u8; 2]; let toi = [0u8; 4]; let hdr = LctHeader {
version: LCT_VERSION,
psi: 0,
close_session: false,
close_object: false,
codepoint: 0,
cci: &cci,
tsi: &tsi,
toi: &toi,
extensions: vec![],
};
let mut out = vec![0u8; 32];
assert!(matches!(
hdr.serialize_into(&mut out),
Err(Error::InvalidField { what: "H", .. })
));
}
}