Skip to main content

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}