1#![allow(missing_docs)]
29
30pub const MAGIC: [u8; 4] = *b"BRN0";
32
33pub const WIRE_VERSION: u8 = 1;
35
36pub const HEADER_SIZE: usize = 32;
38
39pub const FLAG_EOS: u8 = 0x80;
41pub const FLAG_MPL: u8 = 0x40;
42pub const FLAG_CMP: u8 = 0x20;
43
44pub const FLAGS_DEFINED_MASK: u8 = FLAG_EOS | FLAG_MPL | FLAG_CMP;
48
49pub const OPCODE_NS_SUBSTRATE: u8 = 0x00;
51pub const OPCODE_NS_TYPED_GRAPH: u8 = 0x01;
52
53pub const OFF_MAGIC: usize = 0;
55pub const OFF_VERSION: usize = 4;
56pub const OFF_OPCODE: usize = 5;
57pub const OFF_FLAGS: usize = 7;
58pub const OFF_HEADER_CRC: usize = 8;
59pub const OFF_STREAM_ID: usize = 12;
60pub const OFF_PAYLOAD_LEN: usize = 16;
61pub const OFF_RESERVED_A: usize = 19;
62pub const OFF_PAYLOAD_CRC: usize = 20;
63pub const OFF_RESERVED_B: usize = 24;
64
65pub const MAX_PAYLOAD_BYTES: usize = 0xff_ff_ff;
67
68#[derive(Debug, thiserror::Error, PartialEq, Eq)]
72pub enum FrameError {
73 #[error("bad frame magic")]
75 BadMagic,
76 #[error("unsupported wire version: got {got}, expected {expected}")]
78 BadVersion { got: u8, expected: u8 },
79 #[error("header CRC mismatch")]
81 BadHeaderCrc,
82 #[error("payload CRC mismatch")]
84 BadPayloadCrc,
85 #[error("payload length {len} exceeds maximum {max}")]
89 OversizePayload { len: u32, max: u32 },
90 #[error("reserved field or flag bit was non-zero")]
92 ReservedNonZero,
93 #[error("truncated frame: have {have} bytes, need {need}")]
96 Truncated { have: usize, need: usize },
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct Frame {
103 pub opcode: u16,
104 pub flags: u8,
105 pub stream_id: u32,
106 pub payload: Vec<u8>,
107}
108
109fn compute_header_crc(header: &[u8; HEADER_SIZE]) -> u32 {
113 let mut buf = [0u8; HEADER_SIZE - 4];
114 buf[..OFF_HEADER_CRC].copy_from_slice(&header[..OFF_HEADER_CRC]);
115 buf[OFF_HEADER_CRC..].copy_from_slice(&header[OFF_STREAM_ID..]);
116 crc32c::crc32c(&buf)
117}
118
119impl Frame {
120 #[must_use]
122 pub fn new(opcode: u16, flags: u8, stream_id: u32, payload: Vec<u8>) -> Self {
123 Self {
124 opcode,
125 flags,
126 stream_id,
127 payload,
128 }
129 }
130
131 pub fn encode(&self) -> Result<Vec<u8>, FrameError> {
153 if self.payload.len() > MAX_PAYLOAD_BYTES {
154 return Err(FrameError::OversizePayload {
155 #[allow(clippy::cast_possible_truncation)]
156 len: self.payload.len() as u32,
157 #[allow(clippy::cast_possible_truncation)]
158 max: MAX_PAYLOAD_BYTES as u32,
159 });
160 }
161
162 let mut out = vec![0u8; HEADER_SIZE + self.payload.len()];
163 {
164 let header = &mut out[..HEADER_SIZE];
165 header[OFF_MAGIC..OFF_MAGIC + 4].copy_from_slice(&MAGIC);
166 header[OFF_VERSION] = WIRE_VERSION;
167 header[OFF_OPCODE..OFF_OPCODE + 2].copy_from_slice(&self.opcode.to_be_bytes());
168 header[OFF_FLAGS] = self.flags;
169 header[OFF_STREAM_ID..OFF_STREAM_ID + 4].copy_from_slice(&self.stream_id.to_be_bytes());
171 #[allow(clippy::cast_possible_truncation)]
172 let len_be = (self.payload.len() as u32).to_be_bytes();
173 header[OFF_PAYLOAD_LEN..OFF_PAYLOAD_LEN + 3].copy_from_slice(&len_be[1..]);
174 let payload_crc = crc32c::crc32c(&self.payload);
178 header[OFF_PAYLOAD_CRC..OFF_PAYLOAD_CRC + 4]
179 .copy_from_slice(&payload_crc.to_be_bytes());
180 }
181
182 let header_arr: [u8; HEADER_SIZE] = out[..HEADER_SIZE]
183 .try_into()
184 .expect("invariant: out starts with exactly HEADER_SIZE header bytes");
185 let header_crc = compute_header_crc(&header_arr);
186 out[OFF_HEADER_CRC..OFF_HEADER_CRC + 4].copy_from_slice(&header_crc.to_be_bytes());
187
188 out[HEADER_SIZE..].copy_from_slice(&self.payload);
189 Ok(out)
190 }
191
192 pub fn decode(bytes: &[u8]) -> Result<(Frame, &[u8]), FrameError> {
205 if bytes.len() < HEADER_SIZE {
206 return Err(FrameError::Truncated {
207 have: bytes.len(),
208 need: HEADER_SIZE,
209 });
210 }
211
212 let header: [u8; HEADER_SIZE] = bytes[..HEADER_SIZE]
213 .try_into()
214 .expect("invariant: slice is exactly HEADER_SIZE bytes after the length check");
215
216 if header[OFF_MAGIC..OFF_MAGIC + 4] != MAGIC {
217 return Err(FrameError::BadMagic);
218 }
219 if header[OFF_VERSION] != WIRE_VERSION {
220 return Err(FrameError::BadVersion {
221 got: header[OFF_VERSION],
222 expected: WIRE_VERSION,
223 });
224 }
225 if header[OFF_RESERVED_A] != 0 || header[OFF_RESERVED_B..HEADER_SIZE] != [0u8; 8] {
226 return Err(FrameError::ReservedNonZero);
227 }
228 let flags = header[OFF_FLAGS];
229 if flags & !FLAGS_DEFINED_MASK != 0 {
230 return Err(FrameError::ReservedNonZero);
231 }
232
233 let stored_header_crc = u32::from_be_bytes([
234 header[OFF_HEADER_CRC],
235 header[OFF_HEADER_CRC + 1],
236 header[OFF_HEADER_CRC + 2],
237 header[OFF_HEADER_CRC + 3],
238 ]);
239 if stored_header_crc != compute_header_crc(&header) {
240 return Err(FrameError::BadHeaderCrc);
241 }
242
243 let payload_len = u32::from_be_bytes([
244 0,
245 header[OFF_PAYLOAD_LEN],
246 header[OFF_PAYLOAD_LEN + 1],
247 header[OFF_PAYLOAD_LEN + 2],
248 ]);
249 #[allow(clippy::cast_possible_truncation)]
252 if payload_len as usize > MAX_PAYLOAD_BYTES {
253 return Err(FrameError::OversizePayload {
254 len: payload_len,
255 max: MAX_PAYLOAD_BYTES as u32,
256 });
257 }
258
259 let need = HEADER_SIZE + payload_len as usize;
260 if bytes.len() < need {
261 return Err(FrameError::Truncated {
262 have: bytes.len(),
263 need,
264 });
265 }
266 let payload_slice = &bytes[HEADER_SIZE..need];
267
268 let stored_payload_crc = u32::from_be_bytes([
269 header[OFF_PAYLOAD_CRC],
270 header[OFF_PAYLOAD_CRC + 1],
271 header[OFF_PAYLOAD_CRC + 2],
272 header[OFF_PAYLOAD_CRC + 3],
273 ]);
274 if stored_payload_crc != crc32c::crc32c(payload_slice) {
275 return Err(FrameError::BadPayloadCrc);
276 }
277
278 let frame = Frame {
279 opcode: u16::from_be_bytes([header[OFF_OPCODE], header[OFF_OPCODE + 1]]),
280 flags,
281 stream_id: u32::from_be_bytes([
282 header[OFF_STREAM_ID],
283 header[OFF_STREAM_ID + 1],
284 header[OFF_STREAM_ID + 2],
285 header[OFF_STREAM_ID + 3],
286 ]),
287 payload: payload_slice.to_vec(),
288 };
289 Ok((frame, &bytes[need..]))
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 fn sample() -> Frame {
298 Frame::new(0x0021, FLAG_EOS, 7, b"hello brain".to_vec())
299 }
300
301 #[test]
302 fn encode_decode_round_trip() {
303 let original = sample();
304 let bytes = original.encode().expect("frame fits");
305 assert_eq!(bytes.len(), HEADER_SIZE + original.payload.len());
306 let (decoded, rest) = Frame::decode(&bytes).expect("decode");
307 assert_eq!(rest, &[] as &[u8]);
308 assert_eq!(decoded, original);
309 }
310
311 #[test]
312 fn empty_payload_has_zero_payload_crc() {
313 let frame = Frame::new(0x0010, 0, 0, Vec::new());
314 let bytes = frame.encode().expect("frame fits");
315 assert_eq!(bytes.len(), HEADER_SIZE);
316 assert_eq!(&bytes[OFF_PAYLOAD_CRC..OFF_PAYLOAD_CRC + 4], &[0u8; 4]);
317 let (decoded, rest) = Frame::decode(&bytes).expect("decode empty");
318 assert_eq!(rest, &[] as &[u8]);
319 assert_eq!(decoded.payload, Vec::<u8>::new());
320 }
321
322 #[test]
323 fn decode_returns_unconsumed_tail() {
324 let mut bytes = sample().encode().expect("frame fits");
325 bytes.extend_from_slice(b"TAIL");
326 let (_frame, rest) = Frame::decode(&bytes).expect("decode");
327 assert_eq!(rest, b"TAIL");
328 }
329
330 #[test]
331 fn rejects_bad_magic() {
332 let mut bytes = sample().encode().expect("frame fits");
333 bytes[0] = b'X';
334 assert_eq!(Frame::decode(&bytes), Err(FrameError::BadMagic));
335 }
336
337 #[test]
338 fn rejects_bad_version() {
339 let mut bytes = sample().encode().expect("frame fits");
340 bytes[OFF_VERSION] = 99;
341 assert_eq!(
342 Frame::decode(&bytes),
343 Err(FrameError::BadVersion {
344 got: 99,
345 expected: 1
346 })
347 );
348 }
349
350 #[test]
351 fn rejects_bad_header_crc() {
352 let mut bytes = sample().encode().expect("frame fits");
353 bytes[OFF_HEADER_CRC] ^= 0xFF;
354 assert_eq!(Frame::decode(&bytes), Err(FrameError::BadHeaderCrc));
355 }
356
357 #[test]
358 fn rejects_bad_payload_crc() {
359 let mut bytes = sample().encode().expect("frame fits");
360 bytes[HEADER_SIZE] ^= 0xFF;
361 assert_eq!(Frame::decode(&bytes), Err(FrameError::BadPayloadCrc));
362 }
363
364 #[test]
365 fn rejects_reserved_a_nonzero() {
366 let mut bytes = sample().encode().expect("frame fits");
367 bytes[OFF_RESERVED_A] = 1;
368 let mut header: [u8; HEADER_SIZE] = bytes[..HEADER_SIZE].try_into().unwrap();
371 let crc = compute_header_crc(&{
372 let mut h = header;
373 h[OFF_HEADER_CRC..OFF_HEADER_CRC + 4].copy_from_slice(&[0; 4]);
374 h
375 });
376 header[OFF_HEADER_CRC..OFF_HEADER_CRC + 4].copy_from_slice(&crc.to_be_bytes());
377 bytes[..HEADER_SIZE].copy_from_slice(&header);
378 assert_eq!(Frame::decode(&bytes), Err(FrameError::ReservedNonZero));
379 }
380
381 #[test]
382 fn rejects_reserved_flag_bit() {
383 let frame = Frame::new(0x0010, 0x01, 0, Vec::new());
386 let bytes = frame.encode().expect("frame fits");
387 assert_eq!(Frame::decode(&bytes), Err(FrameError::ReservedNonZero));
388 }
389
390 #[test]
391 fn rejects_truncated_header() {
392 let bytes = sample().encode().expect("frame fits");
393 let truncated = &bytes[..10];
394 assert_eq!(
395 Frame::decode(truncated),
396 Err(FrameError::Truncated {
397 have: 10,
398 need: HEADER_SIZE
399 })
400 );
401 }
402
403 #[test]
404 fn rejects_truncated_payload() {
405 let bytes = sample().encode().expect("frame fits");
406 let truncated = &bytes[..bytes.len() - 3];
407 assert!(matches!(
408 Frame::decode(truncated),
409 Err(FrameError::Truncated { .. })
410 ));
411 }
412
413 #[test]
415 fn crc32c_check_vector() {
416 assert_eq!(crc32c::crc32c(b"123456789"), 0xE306_9283);
417 }
418}