1use alloc::vec::Vec;
20
21use crate::error::{Error, Result};
22use crate::ext::{self, HeaderExtension, WORD};
23
24pub const LCT_VERSION: u8 = 1;
26pub const FIXED_HEADER_LEN: usize = 4;
28
29const FLAG_A: u16 = 0x0002;
32const FLAG_B: u16 = 0x0001;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize))]
42pub struct LctHeader<'a> {
43 pub version: u8,
45 pub psi: u8,
47 pub close_session: bool,
49 pub close_object: bool,
51 pub codepoint: u8,
53 pub cci: &'a [u8],
56 pub tsi: &'a [u8],
58 pub toi: &'a [u8],
60 pub extensions: Vec<HeaderExtension<'a>>,
62}
63
64fn flags_from_lengths(cci: usize, tsi: usize, toi: usize) -> Result<(u8, u8, u8, u8)> {
69 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 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 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 fn cci_len(c: u8) -> usize {
136 WORD * (c as usize + 1)
137 }
138 fn tsi_len(s: u8, h: u8) -> usize {
140 WORD * s as usize + 2 * h as usize
141 }
142 fn toi_len(o: u8, h: u8) -> usize {
144 WORD * o as usize + 2 * h as usize
145 }
146
147 pub fn c_flag(&self) -> u8 {
149 (self.cci.len() / WORD).saturating_sub(1) as u8
150 }
151 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 pub fn s_flag(&self) -> u8 {
163 (self.tsi.len() / WORD) as u8
164 }
165 pub fn o_flag(&self) -> u8 {
167 (self.toi.len() / WORD) as u8
168 }
169
170 fn base_len(&self) -> usize {
172 FIXED_HEADER_LEN + self.cci.len() + self.tsi.len() + self.toi.len()
173 }
174
175 pub fn serialized_len(&self) -> usize {
177 self.base_len() + ext::chain_len(&self.extensions)
178 }
179
180 pub fn hdr_len(&self) -> usize {
182 self.serialized_len() / WORD
183 }
184
185 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 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 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 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 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 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 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 #[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 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 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 #[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]; 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]; let toi = [0x10u8, 0x20, 0x30, 0x40, 0x50, 0x60]; 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 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 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); assert_eq!(out[3], 0x42); let (re, used) = LctHeader::parse(&out).unwrap();
445 assert_eq!(used, 24);
446 assert_eq!(re, hdr);
447 assert_eq!(re.cci.len(), 8);
449 assert_eq!(re.tsi.len(), 6);
450 assert_eq!(re.toi.len(), 6);
451 }
452
453 #[test]
455 fn shared_h_bit_feeds_both_tsi_and_toi() {
456 let cci = [0u8; 4];
457 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 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 #[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 #[test]
512 fn header_with_extension_chain() {
513 let cci = [0u8; 4];
514 let tsi = [0x00u8, 0x00, 0x00, 0x05]; let nop = [0u8; 2]; let ext_content = [0xAAu8, 0xBB, 0xCC]; 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 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]; 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 let cci = [0u8; 4];
568 let tsi = [0u8; 2]; let toi = [0u8; 4]; 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}