#![allow(clippy::cast_possible_truncation, clippy::cast_lossless)]
use core::convert::TryFrom;
use thiserror::Error;
pub const MAX_HEADER_LEN: usize = 14;
pub const MAX_CONTROL_PAYLOAD: u64 = 125;
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OpCode {
Continuation = 0x0,
Text = 0x1,
Binary = 0x2,
Close = 0x8,
Ping = 0x9,
Pong = 0xA,
}
impl OpCode {
#[inline]
#[must_use]
pub const fn is_control(self) -> bool {
matches!(self, Self::Close | Self::Ping | Self::Pong)
}
#[inline]
#[must_use]
pub const fn is_data(self) -> bool {
matches!(self, Self::Continuation | Self::Text | Self::Binary)
}
}
impl TryFrom<u8> for OpCode {
type Error = FrameError;
#[inline]
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x0 => Ok(Self::Continuation),
0x1 => Ok(Self::Text),
0x2 => Ok(Self::Binary),
0x8 => Ok(Self::Close),
0x9 => Ok(Self::Ping),
0xA => Ok(Self::Pong),
other => Err(FrameError::InvalidOpCode(other)),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FrameHeader {
pub fin: bool,
pub opcode: OpCode,
pub mask: Option<[u8; 4]>,
pub payload_len: u64,
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
pub enum FrameError {
#[error("RSV1/2/3 bits set but no extension negotiated (RFC §5.2)")]
RsvBitsSet,
#[error("invalid or reserved opcode: 0x{0:X}")]
InvalidOpCode(u8),
#[error("control frame must have FIN=1 (RFC §5.4)")]
ControlFrameFragmented,
#[error("control frame payload > 125 bytes (RFC §5.5)")]
ControlFrameTooLarge,
#[error("payload length 64-bit MSB set (RFC §5.2)")]
PayloadTooLarge,
#[error("payload length used a non-minimal encoding (RFC §5.2)")]
NonMinimalPayloadLength,
#[error("server sent masked frame (RFC §5.1 violation)")]
ServerSentMaskedFrame,
}
pub fn parse_header(buf: &[u8]) -> Result<Option<(FrameHeader, usize)>, FrameError> {
if buf.len() < 2 {
return Ok(None);
}
let b0 = buf[0];
let b1 = buf[1];
let fin = (b0 & 0x80) != 0;
if (b0 & 0x70) != 0 {
return Err(FrameError::RsvBitsSet);
}
let opcode = OpCode::try_from(b0 & 0x0F)?;
let masked = (b1 & 0x80) != 0;
let len7 = b1 & 0x7F;
let (payload_len, len_field_size) = match len7 {
0..=125 => (u64::from(len7), 0_usize),
126 => {
if buf.len() < 4 {
return Ok(None);
}
let val = u16::from_be_bytes([buf[2], buf[3]]);
if val < 126 {
return Err(FrameError::NonMinimalPayloadLength);
}
(u64::from(val), 2)
}
127 => {
if buf.len() < 10 {
return Ok(None);
}
let bytes = [
buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8], buf[9],
];
let val = u64::from_be_bytes(bytes);
if val & 0x8000_0000_0000_0000 != 0 {
return Err(FrameError::PayloadTooLarge);
}
if val <= 0xFFFF {
return Err(FrameError::NonMinimalPayloadLength);
}
(val, 8)
}
_ => unreachable!("len7 is masked to 7 bits"),
};
if opcode.is_control() {
if !fin {
return Err(FrameError::ControlFrameFragmented);
}
if payload_len > MAX_CONTROL_PAYLOAD {
return Err(FrameError::ControlFrameTooLarge);
}
}
let mask_field_size = if masked { 4 } else { 0 };
let header_size = 2 + len_field_size + mask_field_size;
if buf.len() < header_size {
return Ok(None);
}
let mask = if masked {
let off = 2 + len_field_size;
Some([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
} else {
None
};
Ok(Some((
FrameHeader {
fin,
opcode,
mask,
payload_len,
},
header_size,
)))
}
pub fn encode_header(
dst: &mut [u8],
fin: bool,
opcode: OpCode,
mask: Option<[u8; 4]>,
payload_len: u64,
) -> usize {
debug_assert!(dst.len() >= MAX_HEADER_LEN);
let mut b0 = opcode as u8;
if fin {
b0 |= 0x80;
}
dst[0] = b0;
let masked_bit: u8 = if mask.is_some() { 0x80 } else { 0 };
let mut idx = if payload_len <= 125 {
dst[1] = masked_bit | (payload_len as u8);
2
} else if payload_len <= 0xFFFF {
dst[1] = masked_bit | 126;
dst[2..4].copy_from_slice(&(payload_len as u16).to_be_bytes());
4
} else {
dst[1] = masked_bit | 127;
dst[2..10].copy_from_slice(&payload_len.to_be_bytes());
10
};
if let Some(m) = mask {
dst[idx..idx + 4].copy_from_slice(&m);
idx += 4;
}
idx
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
mod tests {
use super::*;
#[test]
fn small_text_unmasked_roundtrip() {
let mut buf = [0u8; 14];
let n = encode_header(&mut buf, true, OpCode::Text, None, 5);
assert_eq!(n, 2);
let (h, c) = parse_header(&buf[..n]).unwrap().unwrap();
assert_eq!(c, 2);
assert!(h.fin);
assert_eq!(h.opcode, OpCode::Text);
assert_eq!(h.payload_len, 5);
assert!(h.mask.is_none());
}
#[test]
fn binary_masked_extended_roundtrip() {
let mut buf = [0u8; 14];
let mask = [0x12, 0x34, 0x56, 0x78];
let n = encode_header(&mut buf, true, OpCode::Binary, Some(mask), 12_345);
assert_eq!(n, 8); let (h, c) = parse_header(&buf[..n]).unwrap().unwrap();
assert_eq!(c, 8);
assert_eq!(h.payload_len, 12_345);
assert_eq!(h.mask, Some(mask));
}
#[test]
fn binary_unmasked_64bit_len() {
let mut buf = [0u8; 14];
let n = encode_header(&mut buf, false, OpCode::Binary, None, 1_000_000);
assert_eq!(n, 10);
let (h, c) = parse_header(&buf[..n]).unwrap().unwrap();
assert_eq!(c, 10);
assert_eq!(h.payload_len, 1_000_000);
assert!(!h.fin);
}
#[test]
fn ping_with_mask_full_header() {
let mut buf = [0u8; 14];
let mask = [0xDE, 0xAD, 0xBE, 0xEF];
let n = encode_header(&mut buf, true, OpCode::Ping, Some(mask), 4);
assert_eq!(n, 6); let (h, _) = parse_header(&buf[..n]).unwrap().unwrap();
assert!(h.opcode.is_control());
assert_eq!(h.mask, Some(mask));
}
#[test]
fn fin_zero_control_rejected() {
let buf = [0x09_u8, 0x00];
let err = parse_header(&buf).unwrap_err();
assert_eq!(err, FrameError::ControlFrameFragmented);
}
#[test]
fn rsv_bits_rejected() {
let buf = [0xC1_u8, 0x00];
let err = parse_header(&buf).unwrap_err();
assert_eq!(err, FrameError::RsvBitsSet);
}
#[test]
fn reserved_opcode_rejected() {
let buf = [0x83_u8, 0x00];
let err = parse_header(&buf).unwrap_err();
assert_eq!(err, FrameError::InvalidOpCode(0x3));
let buf2 = [0x8B_u8, 0x00];
let err2 = parse_header(&buf2).unwrap_err();
assert_eq!(err2, FrameError::InvalidOpCode(0xB));
}
#[test]
fn control_oversize_rejected() {
let buf = [0x89_u8, 0x7E, 0x01, 0x00];
let err = parse_header(&buf).unwrap_err();
assert_eq!(err, FrameError::ControlFrameTooLarge);
}
#[test]
fn payload_64bit_msb_rejected() {
let buf = [0x82_u8, 0x7F, 0x80, 0, 0, 0, 0, 0, 0, 0];
let err = parse_header(&buf).unwrap_err();
assert_eq!(err, FrameError::PayloadTooLarge);
}
#[test]
fn non_minimal_16bit_len_rejected() {
let buf = [0x82_u8, 0x7E, 0x00, 0x7D];
let err = parse_header(&buf).unwrap_err();
assert_eq!(err, FrameError::NonMinimalPayloadLength);
}
#[test]
fn non_minimal_64bit_len_rejected() {
let buf = [0x82_u8, 0x7F, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF];
let err = parse_header(&buf).unwrap_err();
assert_eq!(err, FrameError::NonMinimalPayloadLength);
}
#[test]
fn partial_header_returns_none() {
assert!(parse_header(&[0x82_u8]).unwrap().is_none());
assert!(parse_header(&[0x82_u8, 0x7E]).unwrap().is_none());
assert!(parse_header(&[0x82_u8, 0x85]).unwrap().is_none());
}
}