Skip to main content

elara_wire/
header.rs

1//! Fixed header for ELARA wire protocol
2//!
3//! Fixed header is 30 bytes:
4//! - Byte 0: Version (4 bits) + Crypto Suite (4 bits)
5//! - Byte 1: Flags
6//! - Bytes 2-3: Header length (LE)
7//! - Bytes 4-11: Session ID (LE)
8//! - Bytes 12-19: Node ID (LE)
9//! - Byte 20: Packet class
10//! - Byte 21: Representation profile
11//! - Bytes 22-25: Time hint (LE, signed)
12//! - Bytes 26-29: Seq/Window (LE)
13
14use elara_core::{ElaraError, ElaraResult, NodeId, PacketClass, RepresentationProfile, SessionId};
15
16use crate::FrameFlags;
17
18/// Fixed header size in bytes
19pub const FIXED_HEADER_SIZE: usize = 30;
20
21/// Current wire protocol version
22pub const WIRE_VERSION: u8 = 0;
23
24/// Crypto suite identifiers
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
26#[repr(u8)]
27pub enum CryptoSuite {
28    /// X25519 + ChaCha20-Poly1305 + Ed25519
29    #[default]
30    Suite0 = 0,
31    /// X25519 + AES-256-GCM + Ed25519
32    Suite1 = 1,
33    /// Reserved for post-quantum
34    Suite2 = 2,
35}
36
37impl CryptoSuite {
38    pub fn from_nibble(n: u8) -> Option<Self> {
39        match n {
40            0 => Some(CryptoSuite::Suite0),
41            1 => Some(CryptoSuite::Suite1),
42            2 => Some(CryptoSuite::Suite2),
43            _ => None,
44        }
45    }
46
47    #[inline]
48    pub fn to_nibble(self) -> u8 {
49        self as u8
50    }
51}
52
53/// Fixed header structure
54#[derive(Clone, Debug)]
55pub struct FixedHeader {
56    /// Wire protocol version (4 bits, 0-15)
57    pub version: u8,
58    /// Crypto suite identifier (4 bits, 0-15)
59    pub crypto_suite: CryptoSuite,
60    /// Frame flags
61    pub flags: FrameFlags,
62    /// Total header length (fixed + extensions)
63    pub header_len: u16,
64    /// Session ID
65    pub session_id: SessionId,
66    /// Sender node ID
67    pub node_id: NodeId,
68    /// Packet class
69    pub class: PacketClass,
70    /// Representation profile hint
71    pub profile: RepresentationProfile,
72    /// Time hint (offset relative to τs in 100μs units)
73    pub time_hint: i32,
74    /// Sequence number (upper 16 bits) + window bitmap (lower 16 bits)
75    pub seq_window: u32,
76}
77
78impl FixedHeader {
79    /// Create a new header with default values
80    pub fn new(session_id: SessionId, node_id: NodeId) -> Self {
81        FixedHeader {
82            version: WIRE_VERSION,
83            crypto_suite: CryptoSuite::default(),
84            flags: FrameFlags::NONE,
85            header_len: FIXED_HEADER_SIZE as u16,
86            session_id,
87            node_id,
88            class: PacketClass::Core,
89            profile: RepresentationProfile::Textual,
90            time_hint: 0,
91            seq_window: 0,
92        }
93    }
94
95    /// Get sequence number (upper 16 bits)
96    #[inline]
97    pub fn seq(&self) -> u16 {
98        (self.seq_window >> 16) as u16
99    }
100
101    /// Get window bitmap (lower 16 bits)
102    #[inline]
103    pub fn window(&self) -> u16 {
104        (self.seq_window & 0xFFFF) as u16
105    }
106
107    /// Set sequence number
108    #[inline]
109    pub fn set_seq(&mut self, seq: u16) {
110        self.seq_window = ((seq as u32) << 16) | (self.seq_window & 0xFFFF);
111    }
112
113    /// Set window bitmap
114    #[inline]
115    pub fn set_window(&mut self, window: u16) {
116        self.seq_window = (self.seq_window & 0xFFFF0000) | (window as u32);
117    }
118
119    /// Parse header from bytes
120    pub fn parse(buf: &[u8]) -> ElaraResult<Self> {
121        if buf.len() < FIXED_HEADER_SIZE {
122            return Err(ElaraError::BufferTooShort {
123                expected: FIXED_HEADER_SIZE,
124                actual: buf.len(),
125            });
126        }
127
128        // Byte 0: Version + Crypto Suite
129        let version = buf[0] >> 4;
130        let crypto_suite = CryptoSuite::from_nibble(buf[0] & 0x0F)
131            .ok_or_else(|| ElaraError::InvalidWireFormat("Unknown crypto suite".into()))?;
132
133        // Byte 1: Flags
134        let flags = FrameFlags::new(buf[1]);
135
136        // Bytes 2-3: Header length
137        let header_len = u16::from_le_bytes([buf[2], buf[3]]);
138
139        // Bytes 4-11: Session ID
140        let session_id = SessionId::from_bytes(buf[4..12].try_into().unwrap());
141
142        // Bytes 12-19: Node ID
143        let node_id = NodeId::from_bytes(buf[12..20].try_into().unwrap());
144
145        // Byte 20: Class
146        let class = PacketClass::from_byte(buf[20])
147            .ok_or_else(|| ElaraError::UnknownPacketClass(buf[20]))?;
148
149        // Byte 21: Profile
150        let profile = RepresentationProfile::from_byte(buf[21]);
151
152        // Bytes 22-25: Time hint
153        let time_hint = i32::from_le_bytes(buf[22..26].try_into().unwrap());
154
155        // Bytes 26-29: Seq/Window
156        let seq_window = u32::from_le_bytes(buf[26..30].try_into().unwrap());
157
158        Ok(FixedHeader {
159            version,
160            crypto_suite,
161            flags,
162            header_len,
163            session_id,
164            node_id,
165            class,
166            profile,
167            time_hint,
168            seq_window,
169        })
170    }
171
172    /// Serialize header to bytes
173    pub fn serialize(&self, buf: &mut [u8]) -> ElaraResult<()> {
174        if buf.len() < FIXED_HEADER_SIZE {
175            return Err(ElaraError::BufferTooShort {
176                expected: FIXED_HEADER_SIZE,
177                actual: buf.len(),
178            });
179        }
180
181        // Byte 0: Version + Crypto Suite
182        buf[0] = (self.version << 4) | self.crypto_suite.to_nibble();
183
184        // Byte 1: Flags
185        buf[1] = self.flags.0;
186
187        // Bytes 2-3: Header length
188        buf[2..4].copy_from_slice(&self.header_len.to_le_bytes());
189
190        // Bytes 4-11: Session ID
191        buf[4..12].copy_from_slice(&self.session_id.to_bytes());
192
193        // Bytes 12-19: Node ID
194        buf[12..20].copy_from_slice(&self.node_id.to_bytes());
195
196        // Byte 20: Class
197        buf[20] = self.class.to_byte();
198
199        // Byte 21: Profile
200        buf[21] = self.profile.to_byte();
201
202        // Bytes 22-25: Time hint
203        buf[22..26].copy_from_slice(&self.time_hint.to_le_bytes());
204
205        // Bytes 26-29: Seq/Window
206        buf[26..30].copy_from_slice(&self.seq_window.to_le_bytes());
207
208        Ok(())
209    }
210
211    /// Serialize header to a new Vec
212    pub fn to_bytes(&self) -> Vec<u8> {
213        let mut buf = vec![0u8; FIXED_HEADER_SIZE];
214        self.serialize(&mut buf).unwrap();
215        buf
216    }
217}
218
219impl Default for FixedHeader {
220    fn default() -> Self {
221        FixedHeader::new(SessionId::ZERO, NodeId::ZERO)
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_header_roundtrip() {
231        let header = FixedHeader {
232            version: WIRE_VERSION,
233            crypto_suite: CryptoSuite::Suite0,
234            flags: FrameFlags(FrameFlags::MULTIPATH | FrameFlags::PRIORITY),
235            header_len: 30,
236            session_id: SessionId::new(0xDEADBEEF_CAFEBABE),
237            node_id: NodeId::new(0x12345678_9ABCDEF0),
238            class: PacketClass::Perceptual,
239            profile: RepresentationProfile::VoiceMinimal,
240            time_hint: -12345,
241            seq_window: 0x00010002,
242        };
243
244        let bytes = header.to_bytes();
245        assert_eq!(bytes.len(), FIXED_HEADER_SIZE);
246
247        let parsed = FixedHeader::parse(&bytes).unwrap();
248
249        assert_eq!(parsed.version, header.version);
250        assert_eq!(parsed.crypto_suite, header.crypto_suite);
251        assert_eq!(parsed.flags, header.flags);
252        assert_eq!(parsed.header_len, header.header_len);
253        assert_eq!(parsed.session_id, header.session_id);
254        assert_eq!(parsed.node_id, header.node_id);
255        assert_eq!(parsed.class, header.class);
256        assert_eq!(parsed.profile, header.profile);
257        assert_eq!(parsed.time_hint, header.time_hint);
258        assert_eq!(parsed.seq_window, header.seq_window);
259    }
260
261    #[test]
262    fn test_seq_window_accessors() {
263        let mut header = FixedHeader::default();
264
265        header.set_seq(0x1234);
266        header.set_window(0x5678);
267
268        assert_eq!(header.seq(), 0x1234);
269        assert_eq!(header.window(), 0x5678);
270        assert_eq!(header.seq_window, 0x12345678);
271    }
272
273    #[test]
274    fn test_header_too_short() {
275        let buf = [0u8; 20]; // Too short
276        let result = FixedHeader::parse(&buf);
277        assert!(matches!(result, Err(ElaraError::BufferTooShort { .. })));
278    }
279
280    #[test]
281    fn test_header_size() {
282        assert_eq!(FIXED_HEADER_SIZE, 30);
283    }
284}