use std::io::{Read, Write};
use flate2::read::DeflateDecoder;
use flate2::write::DeflateEncoder;
use flate2::Compression;
use crate::codec::Frame;
use crate::error::WsError;
pub const EXTENSION_NAME: &str = "x-ws-kit-deflate";
const FLAG_COMPRESSED: u8 = 0x01;
const FLAG_TEXT: u8 = 0x02;
const FLAG_KNOWN: u8 = FLAG_COMPRESSED | FLAG_TEXT;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompressionConfig {
pub level: u8,
pub min_size: usize,
pub max_size: usize,
}
impl Default for CompressionConfig {
fn default() -> Self {
Self {
level: 6,
min_size: 64,
max_size: 1024 * 1024,
}
}
}
impl CompressionConfig {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone)]
pub struct FrameCompressor {
config: CompressionConfig,
}
impl FrameCompressor {
pub fn new(config: CompressionConfig) -> Self {
Self { config }
}
pub fn with_defaults() -> Self {
Self::new(CompressionConfig::default())
}
pub fn config(&self) -> &CompressionConfig {
&self.config
}
pub fn compress(&self, data: &[u8]) -> Result<Vec<u8>, WsError> {
let mut flags = 0u8;
let body: Vec<u8> = if data.len() >= self.config.min_size {
let mut encoder = DeflateEncoder::new(
Vec::with_capacity(data.len() / 2),
Compression::new(u32::from(self.config.level)),
);
encoder.write_all(data).map_err(|_| WsError::Compression)?;
let compressed = encoder.finish().map_err(|_| WsError::Compression)?;
if compressed.len() < data.len() {
flags |= FLAG_COMPRESSED;
compressed
} else {
data.to_vec()
}
} else {
data.to_vec()
};
let mut out = Vec::with_capacity(body.len() + 1);
out.push(flags);
out.extend_from_slice(&body);
Ok(out)
}
pub fn decompress(&self, enveloped: &[u8]) -> Result<Vec<u8>, WsError> {
let (&flags, body) = enveloped.split_first().ok_or(WsError::Compression)?;
if flags & !FLAG_KNOWN != 0 {
return Err(WsError::Compression);
}
if flags & FLAG_COMPRESSED == 0 {
if body.len() > self.config.max_size {
return Err(WsError::PayloadTooLarge);
}
return Ok(body.to_vec());
}
let mut decoder = DeflateDecoder::new(body).take(self.config.max_size as u64 + 1);
let mut out = Vec::new();
decoder
.read_to_end(&mut out)
.map_err(|_| WsError::Compression)?;
if out.len() > self.config.max_size {
return Err(WsError::PayloadTooLarge);
}
Ok(out)
}
pub fn compress_frame(&self, frame: Frame) -> Result<Frame, WsError> {
let mut flags = 0u8;
if frame.is_text() {
flags |= FLAG_TEXT;
}
let payload = self.compress(frame.as_bytes())?;
let mut out = Vec::with_capacity(payload.len() + 1);
out.push(flags);
out.extend_from_slice(&payload);
Ok(Frame::Binary(out.into()))
}
pub fn decompress_frame(&self, frame: Frame) -> Result<Frame, WsError> {
let bytes = frame.into_bytes();
let (&flags, body) = bytes.split_first().ok_or(WsError::Compression)?;
if flags & !FLAG_KNOWN != 0 {
return Err(WsError::Compression);
}
let payload = self.decompress(body)?;
if flags & FLAG_TEXT != 0 {
let s = String::from_utf8(payload).map_err(|_| WsError::Compression)?;
Ok(Frame::Text(s))
} else {
Ok(Frame::Binary(payload.into()))
}
}
}
pub fn extension_accepted(header: Option<&str>) -> bool {
header.is_some_and(|value| {
value
.split(',')
.any(|token| token.trim().eq_ignore_ascii_case(EXTENSION_NAME))
})
}
pub fn offer_extension() -> &'static str {
EXTENSION_NAME
}
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::expect_used,
clippy::panic
)]
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
fn repetitive(n: usize) -> Vec<u8> {
let pattern: Vec<u8> = (0..=63u8).collect();
pattern.repeat(n / 64 + 1).into_iter().take(n).collect()
}
#[test]
fn roundtrip_text_and_binary() {
let c = FrameCompressor::with_defaults();
let text = Frame::from("chat line ".repeat(40));
let wire = c.compress_frame(text.clone()).unwrap();
assert!(wire.is_binary());
let restored = c.decompress_frame(wire).unwrap();
assert_eq!(restored, text);
assert!(restored.is_text());
let bin = Frame::from(repetitive(4096));
let wire = c.compress_frame(bin.clone()).unwrap();
let restored = c.decompress_frame(wire).unwrap();
assert_eq!(restored, bin);
assert!(restored.is_binary());
}
#[test]
fn small_payloads_stay_identity() {
let c = FrameCompressor::with_defaults(); let small = b"tiny".to_vec();
let env = c.compress(&small).unwrap();
assert_eq!(env[0] & FLAG_COMPRESSED, 0, "below min_size: identity");
assert_eq!(&env[1..], &small[..]);
assert_eq!(c.decompress(&env).unwrap(), small);
}
#[test]
fn repetitive_payload_compresses_smaller() {
let c = FrameCompressor::with_defaults();
let data = repetitive(4096);
let env = c.compress(&data).unwrap();
assert_eq!(env[0] & FLAG_COMPRESSED, FLAG_COMPRESSED);
assert!(
env.len() < data.len() / 4,
"repetitive data should crush: env={} raw={}",
env.len(),
data.len()
);
assert_eq!(c.decompress(&env).unwrap(), data);
}
#[test]
fn incompressible_payload_falls_back_to_identity() {
let c = FrameCompressor::with_defaults();
let mut data = vec![0u8; 4096];
let mut x: u64 = 0x9E3779B97F4A7C15;
for slot in data.iter_mut() {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*slot = (x >> 32) as u8;
}
let env = c.compress(&data).unwrap();
assert_eq!(env[0] & FLAG_COMPRESSED, 0, "must not grow on the wire");
assert_eq!(c.decompress(&env).unwrap(), data);
}
#[test]
fn empty_payload_roundtrips() {
let c = FrameCompressor::with_defaults();
let env = c.compress(&[]).unwrap();
assert!(c.decompress(&env).unwrap().is_empty());
let wire = c.compress_frame(Frame::from("")).unwrap();
assert_eq!(c.decompress_frame(wire).unwrap(), Frame::from(""));
}
#[test]
fn bomb_guard_caps_decompression() {
let small = FrameCompressor::new(CompressionConfig {
level: 6,
min_size: 64,
max_size: 1024,
});
let bomb_input = vec![0u8; 1 << 20]; let env = small.compress(&bomb_input).unwrap();
assert_eq!(
small.decompress(&env).unwrap_err(),
WsError::PayloadTooLarge
);
let big_identity = small.compress(&vec![7u8; 2048]).unwrap();
assert_eq!(
small.decompress(&big_identity).unwrap_err(),
WsError::PayloadTooLarge
);
}
#[test]
fn unknown_flags_fail_closed() {
let c = FrameCompressor::with_defaults();
let err = c.decompress(&[0b1000_0000, 1, 2, 3]).unwrap_err();
assert_eq!(err, WsError::Compression);
let err = c
.decompress_frame(Frame::from(vec![0b0000_0100u8, 1]))
.unwrap_err();
assert_eq!(err, WsError::Compression);
assert_eq!(c.decompress(&[]).unwrap_err(), WsError::Compression);
assert_eq!(
c.decompress_frame(Frame::from(Bytes::new())).unwrap_err(),
WsError::Compression
);
}
#[test]
fn identity_envelope_roundtrips_through_decompress() {
let c = FrameCompressor::with_defaults();
let env = [0b0000_0000, b'o', b'k'];
assert_eq!(c.decompress(&env).unwrap(), b"ok".to_vec());
let big = vec![0u8; c.config.max_size + 1];
let mut oversized = vec![0b0000_0000];
oversized.extend_from_slice(&big);
assert_eq!(
c.decompress(&oversized).unwrap_err(),
WsError::PayloadTooLarge
);
}
#[test]
fn corrupt_deflate_stream_is_an_error() {
let c = FrameCompressor::with_defaults();
let garbage_env = [FLAG_COMPRESSED, 0xDE, 0xAD, 0xBE, 0xEF];
assert_eq!(
c.decompress(&garbage_env).unwrap_err(),
WsError::Compression
);
}
#[test]
fn text_frame_with_invalid_utf8_flag_rejected() {
let c = FrameCompressor::with_defaults();
let evil = [FLAG_TEXT, 0xFF, 0xFE, 0xFF];
assert_eq!(
c.decompress_frame(Frame::from(evil.to_vec())).unwrap_err(),
WsError::Compression
);
}
#[test]
fn multibyte_utf8_text_survives_exactly() {
let c = FrameCompressor::with_defaults();
let text = Frame::from("héllo wörld — ünïcode 🌐 ".repeat(20));
let restored = c
.decompress_frame(c.compress_frame(text.clone()).unwrap())
.unwrap();
assert_eq!(restored, text);
}
#[test]
fn negotiation_tokens() {
assert!(extension_accepted(Some(EXTENSION_NAME)));
assert!(extension_accepted(Some("X-WS-KIT-DEFLATE")));
assert!(extension_accepted(Some(
" permessage-deflate, x-ws-kit-deflate "
)));
assert!(!extension_accepted(Some("permessage-deflate")));
assert!(!extension_accepted(Some("x-ws-kit-deflate2")));
assert!(!extension_accepted(Some("")));
assert!(!extension_accepted(None));
assert_eq!(offer_extension(), "x-ws-kit-deflate");
}
#[test]
fn config_level_bounds_and_accessors() {
let cfg = CompressionConfig {
level: 9,
..CompressionConfig::default()
};
let c = FrameCompressor::new(cfg.clone());
assert_eq!(c.config(), &cfg);
assert_eq!(CompressionConfig::new(), CompressionConfig::default());
}
}