#![allow(clippy::cast_possible_truncation, clippy::cast_lossless)]
use super::frame::{FrameError, FrameHeader, MAX_HEADER_LEN, parse_header};
#[derive(Clone, Copy, Debug)]
pub enum FrameEvent<'a> {
FrameStart(FrameHeader),
PayloadChunk(&'a [u8]),
FrameEnd,
}
#[derive(Debug)]
pub enum FeedOutcome<'a> {
NeedMore { consumed: usize },
Event {
consumed: usize,
event: FrameEvent<'a>,
},
}
#[derive(Debug)]
enum ParserState {
Idle,
HeaderPartial {
partial: [u8; MAX_HEADER_LEN],
filled: u8,
},
Payload {
remaining: u64,
},
}
#[derive(Debug)]
pub struct FrameParser {
state: ParserState,
}
impl Default for FrameParser {
fn default() -> Self {
Self::new()
}
}
impl FrameParser {
#[must_use]
pub const fn new() -> Self {
Self {
state: ParserState::Idle,
}
}
#[inline]
#[must_use]
pub const fn is_idle(&self) -> bool {
matches!(self.state, ParserState::Idle)
}
pub fn feed_one<'a>(&mut self, mut bytes: &'a [u8]) -> Result<FeedOutcome<'a>, FrameError> {
let mut total_consumed: usize = 0;
loop {
match &mut self.state {
ParserState::Idle => {
self.state = ParserState::HeaderPartial {
partial: [0; MAX_HEADER_LEN],
filled: 0,
};
}
ParserState::HeaderPartial { partial, filled } => {
let min_needed = if *filled < 2 {
2
} else {
header_size_from_byte1(partial[1])
};
if (*filled as usize) >= min_needed {
let snapshot = *partial;
let len = *filled as usize;
let (header, _hsize) =
parse_header(&snapshot[..len])?.unwrap_or_else(|| {
unreachable!("filled >= min_needed: parse_header must yield Some")
});
if header.mask.is_some() {
return Err(FrameError::ServerSentMaskedFrame);
}
self.state = ParserState::Payload {
remaining: header.payload_len,
};
return Ok(FeedOutcome::Event {
consumed: total_consumed,
event: FrameEvent::FrameStart(header),
});
}
let want = min_needed - *filled as usize;
let take = want.min(bytes.len());
if take == 0 {
return Ok(FeedOutcome::NeedMore {
consumed: total_consumed,
});
}
let start = *filled as usize;
partial[start..start + take].copy_from_slice(&bytes[..take]);
*filled += take as u8;
bytes = &bytes[take..];
total_consumed += take;
}
ParserState::Payload { remaining } => {
if *remaining == 0 {
self.state = ParserState::Idle;
return Ok(FeedOutcome::Event {
consumed: total_consumed,
event: FrameEvent::FrameEnd,
});
}
if bytes.is_empty() {
return Ok(FeedOutcome::NeedMore {
consumed: total_consumed,
});
}
let take = bytes.len().min(*remaining as usize);
*remaining -= take as u64;
let chunk = &bytes[..take];
return Ok(FeedOutcome::Event {
consumed: total_consumed + take,
event: FrameEvent::PayloadChunk(chunk),
});
}
}
}
}
}
#[inline]
fn header_size_from_byte1(b1: u8) -> usize {
let masked = (b1 & 0x80) != 0;
let len7 = b1 & 0x7F;
let len_field = match len7 {
0..=125 => 0,
126 => 2,
127 => 8,
_ => unreachable!("len7 = b1 & 0x7F is 0..=127"),
};
let mask_field = if masked { 4 } else { 0 };
2 + len_field + mask_field
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing, clippy::panic)]
mod tests {
use super::*;
use crate::ws::frame::OpCode;
fn drain_all<'a>(p: &mut FrameParser, mut bytes: &'a [u8]) -> Vec<DrainEvent> {
let mut out = Vec::new();
loop {
match p.feed_one(bytes).unwrap() {
FeedOutcome::NeedMore { .. } => {
out.push(DrainEvent::NeedMore);
return out;
}
FeedOutcome::Event { consumed, event } => {
out.push(match event {
FrameEvent::FrameStart(h) => DrainEvent::Start(h),
FrameEvent::PayloadChunk(c) => DrainEvent::Chunk(c.to_vec()),
FrameEvent::FrameEnd => DrainEvent::End,
});
bytes = &bytes[consumed..];
}
}
}
}
#[derive(Debug)]
enum DrainEvent {
Start(FrameHeader),
Chunk(Vec<u8>),
End,
NeedMore,
}
impl DrainEvent {
fn is_start(&self) -> bool {
matches!(self, Self::Start(_))
}
fn is_end(&self) -> bool {
matches!(self, Self::End)
}
}
#[test]
fn complete_small_text_one_call() {
let frame = b"\x81\x05Hello";
let mut p = FrameParser::new();
let events = drain_all(&mut p, frame);
assert!(events[0].is_start());
match &events[1] {
DrainEvent::Chunk(c) => assert_eq!(c, b"Hello"),
other => panic!("expected Chunk, got {other:?}"),
}
assert!(events[2].is_end());
assert!(matches!(events[3], DrainEvent::NeedMore));
}
#[test]
fn byte_by_byte_drip_feed() {
let frame = b"\x81\x05Hello";
let mut p = FrameParser::new();
let mut all_events = Vec::new();
for chunk in frame.chunks(1) {
let mut remaining = chunk;
loop {
match p.feed_one(remaining).unwrap() {
FeedOutcome::NeedMore { consumed } => {
remaining = &remaining[consumed..];
if remaining.is_empty() {
break;
}
}
FeedOutcome::Event { consumed, event } => {
all_events.push(format!("{event:?}"));
remaining = &remaining[consumed..];
}
}
}
}
assert!(all_events.iter().any(|s| s.starts_with("FrameStart")));
assert!(all_events.iter().any(|s| s == "FrameEnd"));
let chunk_count = all_events
.iter()
.filter(|s| s.starts_with("PayloadChunk"))
.count();
assert_eq!(chunk_count, 5);
}
#[test]
fn extended_16bit_len_frame() {
let mut frame: Vec<u8> = vec![0x82, 0x7E, 0x01, 0x2C];
frame.extend(std::iter::repeat_n(0xAB, 300));
let mut p = FrameParser::new();
let events = drain_all(&mut p, &frame);
match &events[0] {
DrainEvent::Start(h) => {
assert_eq!(h.opcode, OpCode::Binary);
assert_eq!(h.payload_len, 300);
}
other => panic!("{other:?}"),
}
let total_payload: usize = events
.iter()
.filter_map(|e| match e {
DrainEvent::Chunk(c) => Some(c.len()),
_ => None,
})
.sum();
assert_eq!(total_payload, 300);
assert!(events.iter().any(|e| e.is_end()));
}
#[test]
fn split_header_across_calls() {
let mut p = FrameParser::new();
let r1 = p.feed_one(&[0x81]).unwrap();
assert!(matches!(r1, FeedOutcome::NeedMore { consumed: 1 }));
let r2 = p.feed_one(&[0x03]).unwrap();
match r2 {
FeedOutcome::Event {
consumed: 1,
event: FrameEvent::FrameStart(h),
} => {
assert_eq!(h.payload_len, 3);
}
other => panic!("expected FrameStart, got {other:?}"),
}
let r3 = p.feed_one(b"abc").unwrap();
match r3 {
FeedOutcome::Event {
consumed: 3,
event: FrameEvent::PayloadChunk(c),
} => assert_eq!(c, b"abc"),
other => panic!("{other:?}"),
}
let r4 = p.feed_one(&[]).unwrap();
assert!(matches!(
r4,
FeedOutcome::Event {
consumed: 0,
event: FrameEvent::FrameEnd
}
));
}
#[test]
fn empty_payload_frame() {
let frame = b"\x89\x00";
let mut p = FrameParser::new();
let events = drain_all(&mut p, frame);
assert!(events[0].is_start());
assert!(events[1].is_end()); }
#[test]
fn rsv_bits_rejected() {
let frame = b"\xC1\x00"; let mut p = FrameParser::new();
let err = p.feed_one(frame).unwrap_err();
assert_eq!(err, FrameError::RsvBitsSet);
}
#[test]
fn masked_server_frame_rejected() {
let frame = b"\x81\x85\x12\x34\x56\x78Hello"; let mut p = FrameParser::new();
let err = p.feed_one(frame).unwrap_err();
assert_eq!(err, FrameError::ServerSentMaskedFrame);
}
#[test]
fn two_back_to_back_frames() {
let mut data: Vec<u8> = vec![];
data.extend_from_slice(b"\x81\x02hi");
data.extend_from_slice(b"\x81\x03foo");
let mut p = FrameParser::new();
let events = drain_all(&mut p, &data);
let start_count = events.iter().filter(|e| e.is_start()).count();
let end_count = events.iter().filter(|e| e.is_end()).count();
assert_eq!(start_count, 2);
assert_eq!(end_count, 2);
}
}