md_codec/header.rs
1//! Single-payload header (5 bits) per SPEC v0.30 §2.1.
2//!
3//! bit 4: divergent-paths flag (0 = shared origin path, 1 = divergent)
4//! bits 3..0: 4-bit version field (v0.30 = 4; usable WF-redesign set {4, 8, 12} per §2.4)
5//!
6//! The chunk header (`chunk.rs`, SPEC v0.30 §2.2) is a separate 37-bit form
7//! with a different first-symbol layout — chunked-flag relocated to bit 0 of
8//! the first 5-bit symbol enabling in-band auto-dispatch per SPEC §2.3. v0.x
9//! single-payload (version=0) and v0.x chunked-misread-as-version=2 are both
10//! rejected with `Error::WireVersionMismatch` per the SPEC §2.5 trace.
11
12use crate::bitstream::{BitReader, BitWriter};
13use crate::error::Error;
14
15/// 5-bit single-payload header per SPEC v0.30 §2.1.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Header {
18 /// Wire-format generation (4 bits). v0.30 = 4.
19 pub version: u8,
20 /// Bit 4: false = shared origin path, true = divergent per-`@N` paths.
21 pub divergent_paths: bool,
22}
23
24impl Header {
25 /// Wire-format version constant for v0.30 (the redesign cycle).
26 /// Usable WF-redesign version set per SPEC §2.4: {4, 8, 12}.
27 pub const WF_REDESIGN_VERSION: u8 = 4;
28
29 /// Encode the 5-bit header into the bit stream.
30 pub fn write(&self, w: &mut BitWriter) {
31 let bits = (u64::from(self.divergent_paths) << 4) | u64::from(self.version & 0b1111);
32 w.write_bits(bits, 5);
33 }
34
35 /// Decode the 5-bit header from the bit stream. Rejects inputs whose
36 /// version field ≠ `WF_REDESIGN_VERSION` (4 in this release) with
37 /// `Error::WireVersionMismatch` per SPEC §2.5.
38 pub fn read(r: &mut BitReader) -> Result<Self, Error> {
39 let bits = r.read_bits(5)?;
40 let divergent_paths = (bits >> 4) & 1 != 0;
41 let version = (bits & 0b1111) as u8;
42 if version != Self::WF_REDESIGN_VERSION {
43 return Err(Error::WireVersionMismatch { got: version });
44 }
45 Ok(Self {
46 version,
47 divergent_paths,
48 })
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn header_round_trip_shared() {
58 let h = Header {
59 version: Header::WF_REDESIGN_VERSION,
60 divergent_paths: false,
61 };
62 let mut w = BitWriter::new();
63 h.write(&mut w);
64 let bytes = w.into_bytes();
65 let mut r = BitReader::new(&bytes);
66 assert_eq!(Header::read(&mut r).unwrap(), h);
67 }
68
69 #[test]
70 fn header_round_trip_divergent() {
71 let h = Header {
72 version: Header::WF_REDESIGN_VERSION,
73 divergent_paths: true,
74 };
75 let mut w = BitWriter::new();
76 h.write(&mut w);
77 let bytes = w.into_bytes();
78 let mut r = BitReader::new(&bytes);
79 assert_eq!(Header::read(&mut r).unwrap(), h);
80 }
81
82 /// SPEC v0.30 §2.5 v0.x rejection trace. Two arms cover (a) v0.x
83 /// single-payload (version=0) and (b) v0.x chunked read as single-
84 /// payload (auto-dispatch read by `decode.rs` per §2.3 routes to
85 /// `Header::read` and the embedded chunked-flag bit is parsed as
86 /// version bit v0, yielding got=2). Both arms reject cleanly with
87 /// `Error::WireVersionMismatch`.
88 #[test]
89 fn header_rejects_version_mismatch() {
90 // Arm 1: v0.x single-payload (paths=0, version=0)
91 // first 5 bits MSB-first = [0][0][0][0][0] = 0b00000
92 // packed MSB-aligned byte = 0b00000_000 = 0x00
93 let bytes = vec![0x00];
94 let mut r = BitReader::new(&bytes);
95 assert!(matches!(
96 Header::read(&mut r),
97 Err(Error::WireVersionMismatch { got: 0 })
98 ));
99
100 // Arm 2: v0.x chunked-misread-as-single-payload per SPEC §2.5
101 // v0.x chunked first 5 bits = [v_msb=0][v=0][v_lsb=0][chunked=1][reserved=0]
102 // = 0b00010 (numeric value 2). Read as v0.30 single-payload:
103 // bit 4 = paths = 0; bits 3..0 = version = 0b0010 = 2.
104 // packed MSB-aligned byte = 0b00010_000 = 0x10
105 let bytes = vec![0x10];
106 let mut r = BitReader::new(&bytes);
107 assert!(matches!(
108 Header::read(&mut r),
109 Err(Error::WireVersionMismatch { got: 2 })
110 ));
111 }
112
113 #[test]
114 fn header_common_case_byte_value() {
115 // Common case: version=4 (v0.30), divergent_paths=false ⇒
116 // first 5 bits = [paths=0][v3=0][v2=1][v1=0][v0=0] = 0b00100 = 0x04
117 // packed MSB-aligned byte = 0b00100_000 = 0x20
118 let h = Header {
119 version: Header::WF_REDESIGN_VERSION,
120 divergent_paths: false,
121 };
122 let mut w = BitWriter::new();
123 h.write(&mut w);
124 assert_eq!(w.into_bytes(), vec![0x20]);
125 }
126}