use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Opcode {
Continuation,
Text,
Binary,
Close,
Ping,
Pong,
Reserved(u8),
}
impl Opcode {
#[must_use]
pub const fn from_bits(v: u8) -> Self {
match v & 0x0F {
0x0 => Self::Continuation,
0x1 => Self::Text,
0x2 => Self::Binary,
0x8 => Self::Close,
0x9 => Self::Ping,
0xA => Self::Pong,
other => Self::Reserved(other),
}
}
#[must_use]
pub const fn to_bits(self) -> u8 {
match self {
Self::Continuation => 0x0,
Self::Text => 0x1,
Self::Binary => 0x2,
Self::Close => 0x8,
Self::Ping => 0x9,
Self::Pong => 0xA,
Self::Reserved(v) => v & 0x0F,
}
}
#[must_use]
pub const fn is_control(self) -> bool {
match self {
Self::Close | Self::Ping | Self::Pong => true,
Self::Reserved(v) => (v & 0x08) != 0,
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame {
pub fin: bool,
pub rsv1: bool,
pub rsv2: bool,
pub rsv3: bool,
pub opcode: Opcode,
pub masking_key: Option<[u8; 4]>,
pub payload: Vec<u8>,
}
impl Frame {
#[must_use]
pub fn text(s: impl Into<alloc::string::String>) -> Self {
Self {
fin: true,
rsv1: false,
rsv2: false,
rsv3: false,
opcode: Opcode::Text,
masking_key: None,
payload: s.into().into_bytes(),
}
}
#[must_use]
pub const fn binary(payload: Vec<u8>) -> Self {
Self {
fin: true,
rsv1: false,
rsv2: false,
rsv3: false,
opcode: Opcode::Binary,
masking_key: None,
payload,
}
}
#[must_use]
pub const fn ping(payload: Vec<u8>) -> Self {
Self {
fin: true,
rsv1: false,
rsv2: false,
rsv3: false,
opcode: Opcode::Ping,
masking_key: None,
payload,
}
}
#[must_use]
pub const fn pong(payload: Vec<u8>) -> Self {
Self {
fin: true,
rsv1: false,
rsv2: false,
rsv3: false,
opcode: Opcode::Pong,
masking_key: None,
payload,
}
}
#[must_use]
pub fn close(status: u16, reason: &str) -> Self {
let mut payload = Vec::with_capacity(2 + reason.len());
payload.extend_from_slice(&status.to_be_bytes());
payload.extend_from_slice(reason.as_bytes());
Self {
fin: true,
rsv1: false,
rsv2: false,
rsv3: false,
opcode: Opcode::Close,
masking_key: None,
payload,
}
}
#[must_use]
pub const fn with_mask(mut self, key: [u8; 4]) -> Self {
self.masking_key = Some(key);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opcode_round_trip_via_bits() {
for v in 0..16u8 {
let op = Opcode::from_bits(v);
assert_eq!(op.to_bits(), v);
}
}
#[test]
fn opcode_well_known_values_match_spec() {
assert_eq!(Opcode::Continuation.to_bits(), 0x0);
assert_eq!(Opcode::Text.to_bits(), 0x1);
assert_eq!(Opcode::Binary.to_bits(), 0x2);
assert_eq!(Opcode::Close.to_bits(), 0x8);
assert_eq!(Opcode::Ping.to_bits(), 0x9);
assert_eq!(Opcode::Pong.to_bits(), 0xA);
}
#[test]
fn opcode_is_control_predicate() {
assert!(Opcode::Close.is_control());
assert!(Opcode::Ping.is_control());
assert!(Opcode::Pong.is_control());
assert!(!Opcode::Text.is_control());
assert!(!Opcode::Binary.is_control());
assert!(!Opcode::Continuation.is_control());
assert!(Opcode::Reserved(0xB).is_control());
assert!(!Opcode::Reserved(0x3).is_control());
}
#[test]
fn text_frame_constructor_sets_fin_and_opcode() {
let f = Frame::text("hello");
assert!(f.fin);
assert_eq!(f.opcode, Opcode::Text);
assert!(f.masking_key.is_none());
assert_eq!(f.payload, alloc::vec![b'h', b'e', b'l', b'l', b'o']);
}
#[test]
fn close_frame_includes_status_code_in_be_payload() {
let f = Frame::close(1000, "");
assert_eq!(&f.payload[..2], &1000u16.to_be_bytes());
}
#[test]
fn close_frame_with_reason_carries_utf8_bytes() {
let f = Frame::close(1001, "Going Away");
assert_eq!(&f.payload[..2], &1001u16.to_be_bytes());
assert_eq!(&f.payload[2..], b"Going Away");
}
#[test]
fn with_mask_sets_masking_key() {
let f = Frame::text("hi").with_mask([1, 2, 3, 4]);
assert_eq!(f.masking_key, Some([1, 2, 3, 4]));
}
}