use std::fmt;
use std::io;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContentEncoding {
Identity,
Gzip,
Deflate,
Brotli,
}
impl ContentEncoding {
#[must_use]
pub fn from_token(token: &str) -> Option<Self> {
match token.trim().to_ascii_lowercase().as_str() {
"identity" => Some(Self::Identity),
"gzip" | "x-gzip" => Some(Self::Gzip),
"deflate" => Some(Self::Deflate),
"br" => Some(Self::Brotli),
_ => None,
}
}
#[must_use]
#[inline]
pub const fn as_token(&self) -> &'static str {
match self {
Self::Identity => "identity",
Self::Gzip => "gzip",
Self::Deflate => "deflate",
Self::Brotli => "br",
}
}
}
impl fmt::Display for ContentEncoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_token())
}
}
#[derive(Debug, Clone, PartialEq)]
struct QualityValue {
encoding: String,
quality: f32,
}
fn parse_accept_encoding(header: &str) -> Vec<QualityValue> {
header
.split(',')
.filter_map(|part| {
let part = part.trim();
if part.is_empty() {
return None;
}
let mut pieces = part.splitn(2, ';');
let encoding = pieces.next()?.trim().to_ascii_lowercase();
let quality = if let Some(q_part) = pieces.next() {
let q_part = q_part.trim();
let q_str = q_part
.strip_prefix("q=")
.or_else(|| q_part.strip_prefix("Q="))?;
let q = q_str.trim().parse::<f32>().ok()?;
if !q.is_finite() || !(0.0..=1.0).contains(&q) {
return None;
}
q
} else {
1.0
};
Some(QualityValue { encoding, quality })
})
.collect()
}
#[must_use]
pub fn negotiate_encoding(
accept_encoding: Option<&str>,
supported: &[ContentEncoding],
) -> Option<ContentEncoding> {
let Some(accept_encoding) = accept_encoding else {
return if supported.contains(&ContentEncoding::Identity) {
Some(ContentEncoding::Identity)
} else {
supported.first().copied()
};
};
if accept_encoding.trim().is_empty() {
return supported
.contains(&ContentEncoding::Identity)
.then_some(ContentEncoding::Identity);
}
let preferences = parse_accept_encoding(accept_encoding);
let wildcard_quality = preferences
.iter()
.find(|q| q.encoding == "*")
.map(|q| q.quality);
let mut best: Option<(ContentEncoding, f32)> = None;
for &encoding in supported {
let enc_token = encoding.as_token();
let explicit_quality = preferences
.iter()
.find(|q| q.encoding == enc_token)
.map(|q| q.quality);
let quality = match encoding {
ContentEncoding::Identity => explicit_quality.unwrap_or(match wildcard_quality {
Some(q) if q <= 0.0 => 0.0,
_ => 1.0,
}),
_ => explicit_quality.or(wildcard_quality).unwrap_or(0.0),
};
if quality <= 0.0 {
continue;
}
match best {
Some((_, best_q)) if quality <= best_q => {}
_ => best = Some((encoding, quality)),
}
}
best.map(|(enc, _)| enc)
}
pub trait Compressor: Send {
fn compress(&mut self, input: &[u8], output: &mut Vec<u8>) -> io::Result<()>;
fn finish(&mut self, output: &mut Vec<u8>) -> io::Result<()>;
fn encoding(&self) -> ContentEncoding;
}
pub trait Decompressor: Send {
fn decompress(&mut self, input: &[u8], output: &mut Vec<u8>) -> io::Result<()>;
fn finish(&mut self, output: &mut Vec<u8>) -> io::Result<()>;
fn encoding(&self) -> ContentEncoding;
}
#[derive(Debug, Default)]
pub struct IdentityCompressor;
impl Compressor for IdentityCompressor {
fn compress(&mut self, input: &[u8], output: &mut Vec<u8>) -> io::Result<()> {
output.extend_from_slice(input);
Ok(())
}
fn finish(&mut self, _output: &mut Vec<u8>) -> io::Result<()> {
Ok(())
}
fn encoding(&self) -> ContentEncoding {
ContentEncoding::Identity
}
}
#[derive(Debug, Default)]
pub struct IdentityDecompressor {
max_size: Option<usize>,
total: usize,
}
impl IdentityDecompressor {
#[must_use]
pub const fn new(max_size: Option<usize>) -> Self {
Self { max_size, total: 0 }
}
}
impl Decompressor for IdentityDecompressor {
fn decompress(&mut self, input: &[u8], output: &mut Vec<u8>) -> io::Result<()> {
update_decompressed_total(&mut self.total, input.len(), self.max_size)?;
output.extend_from_slice(input);
Ok(())
}
fn finish(&mut self, _output: &mut Vec<u8>) -> io::Result<()> {
Ok(())
}
fn encoding(&self) -> ContentEncoding {
ContentEncoding::Identity
}
}
#[derive(Debug, Default)]
#[cfg(feature = "compression")]
#[allow(dead_code)]
struct LimitedWriter {
inner: Vec<u8>,
max_size: Option<usize>,
}
#[cfg(feature = "compression")]
#[allow(dead_code)]
impl LimitedWriter {
fn new(max_size: Option<usize>) -> Self {
Self {
inner: Vec::new(),
max_size,
}
}
}
#[cfg(feature = "compression")]
impl io::Write for LimitedWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if let Some(max) = self.max_size {
if self.inner.len().saturating_add(buf.len()) > max {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"decompressed size exceeds limit",
));
}
}
self.inner.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[cfg(feature = "compression")]
const BROTLI_BUFFER_SIZE: usize = 4096;
#[cfg(feature = "compression")]
const BROTLI_DEFAULT_QUALITY: u32 = 5;
#[cfg(feature = "compression")]
const BROTLI_DEFAULT_LGWIN: u32 = 22;
#[cfg(feature = "compression")]
pub struct GzipCompressor {
encoder: flate2::write::GzEncoder<Vec<u8>>,
finished: bool,
}
#[cfg(feature = "compression")]
impl GzipCompressor {
#[must_use]
pub fn new() -> Self {
Self::with_level(flate2::Compression::default())
}
#[must_use]
pub fn with_level(level: flate2::Compression) -> Self {
Self {
encoder: flate2::write::GzEncoder::new(Vec::new(), level),
finished: false,
}
}
}
#[cfg(feature = "compression")]
impl Default for GzipCompressor {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "compression")]
impl Compressor for GzipCompressor {
fn compress(&mut self, input: &[u8], output: &mut Vec<u8>) -> io::Result<()> {
use io::Write;
self.encoder.write_all(input)?;
let buf = self.encoder.get_mut();
if !buf.is_empty() {
output.extend_from_slice(buf);
buf.clear();
}
Ok(())
}
fn finish(&mut self, output: &mut Vec<u8>) -> io::Result<()> {
use io::Write;
if self.finished {
return Ok(());
}
self.encoder.flush()?;
let inner = std::mem::replace(
&mut self.encoder,
flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::none()),
);
let finished = inner.finish()?;
output.extend_from_slice(&finished);
self.finished = true;
Ok(())
}
fn encoding(&self) -> ContentEncoding {
ContentEncoding::Gzip
}
}
#[cfg(feature = "compression")]
pub struct GzipDecompressor {
max_size: Option<usize>,
total: usize,
decoder: flate2::write::GzDecoder<LimitedWriter>,
}
#[cfg(feature = "compression")]
impl GzipDecompressor {
#[must_use]
pub fn new(max_size: Option<usize>) -> Self {
Self {
max_size,
total: 0,
decoder: flate2::write::GzDecoder::new(LimitedWriter::new(max_size)),
}
}
}
#[cfg(feature = "compression")]
impl Decompressor for GzipDecompressor {
fn decompress(&mut self, input: &[u8], output: &mut Vec<u8>) -> io::Result<()> {
use io::Write;
let remaining = self.max_size.map(|m| m.saturating_sub(self.total));
self.decoder.get_mut().max_size = remaining;
self.decoder.write_all(input)?;
self.decoder.flush()?;
let mut buf = std::mem::take(&mut self.decoder.get_mut().inner);
update_decompressed_total(&mut self.total, buf.len(), self.max_size)?;
output.append(&mut buf);
Ok(())
}
fn finish(&mut self, output: &mut Vec<u8>) -> io::Result<()> {
let mut dummy = flate2::write::GzDecoder::new(LimitedWriter::new(None));
std::mem::swap(&mut self.decoder, &mut dummy);
dummy.get_mut().max_size = self.max_size.map(|m| m.saturating_sub(self.total));
let mut buf = dummy.finish()?.inner;
update_decompressed_total(&mut self.total, buf.len(), self.max_size)?;
output.append(&mut buf);
Ok(())
}
fn encoding(&self) -> ContentEncoding {
ContentEncoding::Gzip
}
}
#[cfg(feature = "compression")]
pub struct DeflateCompressor {
encoder: flate2::write::DeflateEncoder<Vec<u8>>,
finished: bool,
}
#[cfg(feature = "compression")]
impl DeflateCompressor {
#[must_use]
pub fn new() -> Self {
Self::with_level(flate2::Compression::default())
}
#[must_use]
pub fn with_level(level: flate2::Compression) -> Self {
Self {
encoder: flate2::write::DeflateEncoder::new(Vec::new(), level),
finished: false,
}
}
}
#[cfg(feature = "compression")]
impl Default for DeflateCompressor {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "compression")]
impl Compressor for DeflateCompressor {
fn compress(&mut self, input: &[u8], output: &mut Vec<u8>) -> io::Result<()> {
use io::Write;
self.encoder.write_all(input)?;
let buf = self.encoder.get_mut();
if !buf.is_empty() {
output.extend_from_slice(buf);
buf.clear();
}
Ok(())
}
fn finish(&mut self, output: &mut Vec<u8>) -> io::Result<()> {
if self.finished {
return Ok(());
}
let inner = std::mem::replace(
&mut self.encoder,
flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::none()),
);
let finished = inner.finish()?;
output.extend_from_slice(&finished);
self.finished = true;
Ok(())
}
fn encoding(&self) -> ContentEncoding {
ContentEncoding::Deflate
}
}
#[cfg(feature = "compression")]
pub struct DeflateDecompressor {
max_size: Option<usize>,
total: usize,
decoder: flate2::write::DeflateDecoder<LimitedWriter>,
}
#[cfg(feature = "compression")]
impl DeflateDecompressor {
#[must_use]
pub fn new(max_size: Option<usize>) -> Self {
Self {
max_size,
total: 0,
decoder: flate2::write::DeflateDecoder::new(LimitedWriter::new(max_size)),
}
}
}
#[cfg(feature = "compression")]
impl Decompressor for DeflateDecompressor {
fn decompress(&mut self, input: &[u8], output: &mut Vec<u8>) -> io::Result<()> {
use io::Write;
let remaining = self.max_size.map(|m| m.saturating_sub(self.total));
self.decoder.get_mut().max_size = remaining;
self.decoder.write_all(input)?;
self.decoder.flush()?;
let mut buf = std::mem::take(&mut self.decoder.get_mut().inner);
update_decompressed_total(&mut self.total, buf.len(), self.max_size)?;
output.append(&mut buf);
Ok(())
}
fn finish(&mut self, output: &mut Vec<u8>) -> io::Result<()> {
let mut dummy = flate2::write::DeflateDecoder::new(LimitedWriter::new(None));
std::mem::swap(&mut self.decoder, &mut dummy);
dummy.get_mut().max_size = self.max_size.map(|m| m.saturating_sub(self.total));
let mut buf = dummy.finish()?.inner;
update_decompressed_total(&mut self.total, buf.len(), self.max_size)?;
output.append(&mut buf);
Ok(())
}
fn encoding(&self) -> ContentEncoding {
ContentEncoding::Deflate
}
}
#[cfg(feature = "compression")]
pub struct BrotliCompressor {
encoder: brotli::CompressorWriter<Vec<u8>>,
finished: bool,
}
#[cfg(feature = "compression")]
impl BrotliCompressor {
#[must_use]
pub fn new() -> Self {
Self::with_params(BROTLI_DEFAULT_QUALITY, BROTLI_DEFAULT_LGWIN)
}
#[must_use]
pub fn with_params(quality: u32, lgwin: u32) -> Self {
Self {
encoder: brotli::CompressorWriter::new(Vec::new(), BROTLI_BUFFER_SIZE, quality, lgwin),
finished: false,
}
}
}
#[cfg(feature = "compression")]
impl Default for BrotliCompressor {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "compression")]
impl Compressor for BrotliCompressor {
fn compress(&mut self, input: &[u8], output: &mut Vec<u8>) -> io::Result<()> {
use io::Write;
self.encoder.write_all(input)?;
self.encoder.flush()?;
let buf = self.encoder.get_mut();
if !buf.is_empty() {
output.extend_from_slice(buf);
buf.clear();
}
Ok(())
}
fn finish(&mut self, output: &mut Vec<u8>) -> io::Result<()> {
use io::Write;
if self.finished {
return Ok(());
}
self.encoder.flush()?;
let finished = std::mem::replace(
&mut self.encoder,
brotli::CompressorWriter::new(
Vec::new(),
BROTLI_BUFFER_SIZE,
BROTLI_DEFAULT_QUALITY,
BROTLI_DEFAULT_LGWIN,
),
)
.into_inner();
output.extend_from_slice(&finished);
self.finished = true;
Ok(())
}
fn encoding(&self) -> ContentEncoding {
ContentEncoding::Brotli
}
}
#[cfg(feature = "compression")]
pub struct BrotliDecompressor {
max_size: Option<usize>,
total: usize,
decoder: brotli::DecompressorWriter<LimitedWriter>,
finished: bool,
}
#[cfg(feature = "compression")]
impl BrotliDecompressor {
#[must_use]
pub fn new(max_size: Option<usize>) -> Self {
Self {
max_size,
total: 0,
decoder: brotli::DecompressorWriter::new(
LimitedWriter::new(max_size),
BROTLI_BUFFER_SIZE,
),
finished: false,
}
}
}
#[cfg(feature = "compression")]
impl Decompressor for BrotliDecompressor {
fn decompress(&mut self, input: &[u8], output: &mut Vec<u8>) -> io::Result<()> {
use io::Write;
let remaining = self.max_size.map(|m| m.saturating_sub(self.total));
self.decoder.get_mut().max_size = remaining;
self.decoder.write_all(input)?;
self.decoder.flush()?;
let mut buf = std::mem::take(&mut self.decoder.get_mut().inner);
update_decompressed_total(&mut self.total, buf.len(), self.max_size)?;
output.append(&mut buf);
Ok(())
}
fn finish(&mut self, output: &mut Vec<u8>) -> io::Result<()> {
use io::Write;
if self.finished {
return Ok(());
}
let remaining = self.max_size.map(|m| m.saturating_sub(self.total));
self.decoder.get_mut().max_size = remaining;
self.decoder.flush()?;
self.decoder.close()?;
let mut buf = std::mem::take(&mut self.decoder.get_mut().inner);
update_decompressed_total(&mut self.total, buf.len(), self.max_size)?;
output.append(&mut buf);
self.finished = true;
Ok(())
}
fn encoding(&self) -> ContentEncoding {
ContentEncoding::Brotli
}
}
#[must_use]
pub fn make_compressor(encoding: ContentEncoding) -> Option<Box<dyn Compressor>> {
match encoding {
ContentEncoding::Identity => Some(Box::new(IdentityCompressor)),
#[cfg(feature = "compression")]
ContentEncoding::Gzip => Some(Box::new(GzipCompressor::new())),
#[cfg(feature = "compression")]
ContentEncoding::Deflate => Some(Box::new(DeflateCompressor::new())),
#[cfg(feature = "compression")]
ContentEncoding::Brotli => Some(Box::new(BrotliCompressor::new())),
#[cfg(not(feature = "compression"))]
ContentEncoding::Gzip | ContentEncoding::Deflate | ContentEncoding::Brotli => None,
}
}
#[must_use]
pub fn content_encoding_from_headers(headers: &[(String, String)]) -> Option<ContentEncoding> {
headers
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case("content-encoding"))
.and_then(|(_, value)| ContentEncoding::from_token(value))
}
#[must_use]
pub fn accept_encoding_from_headers(headers: &[(String, String)]) -> Option<&str> {
headers
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case("accept-encoding"))
.map(|(_, value)| value.as_str())
}
fn update_decompressed_total(
total: &mut usize,
added: usize,
max_size: Option<usize>,
) -> io::Result<()> {
let next_total = total.checked_add(added).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"decompressed size exceeds limit",
)
})?;
if let Some(max) = max_size {
if next_total > max {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"decompressed size exceeds limit",
));
}
}
*total = next_total;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_quality(actual: f32, expected: f32) {
let delta = (actual - expected).abs();
assert!(
delta <= f32::EPSILON,
"quality mismatch: expected {expected}, got {actual}"
);
}
#[test]
fn encoding_from_token() {
assert_eq!(
ContentEncoding::from_token("gzip"),
Some(ContentEncoding::Gzip)
);
assert_eq!(
ContentEncoding::from_token("x-gzip"),
Some(ContentEncoding::Gzip)
);
assert_eq!(
ContentEncoding::from_token("GZIP"),
Some(ContentEncoding::Gzip)
);
assert_eq!(
ContentEncoding::from_token("deflate"),
Some(ContentEncoding::Deflate)
);
assert_eq!(
ContentEncoding::from_token("br"),
Some(ContentEncoding::Brotli)
);
assert_eq!(
ContentEncoding::from_token("identity"),
Some(ContentEncoding::Identity)
);
assert_eq!(ContentEncoding::from_token("unknown"), None);
}
#[test]
fn encoding_roundtrip() {
for enc in [
ContentEncoding::Identity,
ContentEncoding::Gzip,
ContentEncoding::Deflate,
ContentEncoding::Brotli,
] {
let token = enc.as_token();
assert_eq!(ContentEncoding::from_token(token), Some(enc));
}
}
#[test]
fn encoding_display() {
assert_eq!(ContentEncoding::Gzip.to_string(), "gzip");
assert_eq!(ContentEncoding::Brotli.to_string(), "br");
}
#[test]
fn parse_simple_accept_encoding() {
let parsed = parse_accept_encoding("gzip, deflate");
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0].encoding, "gzip");
assert_quality(parsed[0].quality, 1.0);
assert_eq!(parsed[1].encoding, "deflate");
assert_quality(parsed[1].quality, 1.0);
}
#[test]
fn parse_accept_encoding_with_quality() {
let parsed = parse_accept_encoding("gzip;q=1.0, deflate;q=0.5, *;q=0");
assert_eq!(parsed.len(), 3);
assert_quality(parsed[0].quality, 1.0);
assert_quality(parsed[1].quality, 0.5);
assert_eq!(parsed[2].encoding, "*");
assert_quality(parsed[2].quality, 0.0);
}
#[test]
fn parse_accept_encoding_empty() {
let parsed = parse_accept_encoding("");
assert!(parsed.is_empty());
}
#[test]
fn parse_accept_encoding_whitespace() {
let parsed = parse_accept_encoding(" gzip ; q=0.8 , br ");
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0].encoding, "gzip");
assert_quality(parsed[0].quality, 0.8);
assert_eq!(parsed[1].encoding, "br");
assert_quality(parsed[1].quality, 1.0);
}
#[test]
fn parse_accept_encoding_rejects_malformed_q() {
let parsed =
parse_accept_encoding("gzip;q=1.5, deflate;q=-0.1, br;q=abc, identity;q=NaN, *;q=1.0");
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].encoding, "*");
assert_quality(parsed[0].quality, 1.0);
}
#[test]
fn negotiate_prefers_highest_quality() {
let supported = &[
ContentEncoding::Gzip,
ContentEncoding::Deflate,
ContentEncoding::Identity,
];
let best = negotiate_encoding(Some("gzip;q=0.5, deflate;q=1.0"), supported);
assert_eq!(best, Some(ContentEncoding::Deflate));
}
#[test]
fn negotiate_server_order_breaks_ties() {
let supported = &[ContentEncoding::Gzip, ContentEncoding::Deflate];
let best = negotiate_encoding(Some("gzip, deflate"), supported);
assert_eq!(best, Some(ContentEncoding::Gzip));
}
#[test]
fn negotiate_wildcard() {
let supported = &[ContentEncoding::Brotli, ContentEncoding::Identity];
let best = negotiate_encoding(Some("*"), supported);
assert_eq!(best, Some(ContentEncoding::Brotli));
}
#[test]
fn negotiate_wildcard_with_explicit_reject() {
let supported = &[
ContentEncoding::Gzip,
ContentEncoding::Deflate,
ContentEncoding::Identity,
];
let best = negotiate_encoding(Some("gzip;q=0, *;q=0.5"), supported);
assert_eq!(best, Some(ContentEncoding::Identity));
}
#[test]
fn negotiate_all_rejected() {
let supported = &[ContentEncoding::Gzip];
let best = negotiate_encoding(Some("gzip;q=0, *;q=0"), supported);
assert_eq!(best, None);
}
#[test]
fn negotiate_absent_accept_encoding_prefers_identity() {
let supported = &[ContentEncoding::Gzip, ContentEncoding::Identity];
let best = negotiate_encoding(None, supported);
assert_eq!(best, Some(ContentEncoding::Identity));
}
#[test]
fn negotiate_absent_accept_encoding_uses_first_supported_when_identity_missing() {
let supported = &[ContentEncoding::Gzip];
let best = negotiate_encoding(None, supported);
assert_eq!(best, Some(ContentEncoding::Gzip));
}
#[test]
fn negotiate_empty_accept_encoding_only_accepts_identity() {
let with_identity = &[ContentEncoding::Gzip, ContentEncoding::Identity];
assert_eq!(
negotiate_encoding(Some(""), with_identity),
Some(ContentEncoding::Identity)
);
let gzip_only = &[ContentEncoding::Gzip];
assert_eq!(negotiate_encoding(Some(""), gzip_only), None);
}
#[test]
fn negotiate_whitespace_only_accept_encoding_matches_explicit_empty_header() {
let with_identity = &[ContentEncoding::Gzip, ContentEncoding::Identity];
assert_eq!(
negotiate_encoding(Some(" "), with_identity),
Some(ContentEncoding::Identity)
);
let gzip_only = &[ContentEncoding::Gzip];
assert_eq!(negotiate_encoding(Some(" "), gzip_only), None);
}
#[test]
fn negotiate_identity_implicit_acceptable() {
let supported = &[ContentEncoding::Identity, ContentEncoding::Gzip];
let best = negotiate_encoding(Some("gzip;q=0.5"), supported);
assert_eq!(best, Some(ContentEncoding::Identity));
}
#[test]
fn negotiate_identity_explicitly_rejected() {
let supported = &[ContentEncoding::Identity, ContentEncoding::Gzip];
let best = negotiate_encoding(Some("identity;q=0, gzip;q=1.0"), supported);
assert_eq!(best, Some(ContentEncoding::Gzip));
}
#[test]
fn negotiate_identity_default_preferred_over_wildcard_quality() {
let supported = &[ContentEncoding::Brotli, ContentEncoding::Identity];
let best = negotiate_encoding(Some("*;q=0.5"), supported);
assert_eq!(best, Some(ContentEncoding::Identity));
}
#[test]
fn negotiate_wildcard_only_identity_keeps_default() {
let supported = &[ContentEncoding::Gzip, ContentEncoding::Identity];
let best = negotiate_encoding(Some("*;q=0.5"), supported);
assert_eq!(best, Some(ContentEncoding::Identity));
}
#[test]
fn negotiate_zero_wildcard_rejects_implicit_identity() {
let supported = &[ContentEncoding::Identity];
let best = negotiate_encoding(Some("*;q=0"), supported);
assert_eq!(best, None);
}
#[test]
fn negotiate_identity_default_without_wildcard() {
let supported = &[ContentEncoding::Gzip, ContentEncoding::Identity];
let best = negotiate_encoding(Some("gzip;q=0.8"), supported);
assert_eq!(best, Some(ContentEncoding::Identity));
}
#[test]
fn negotiate_explicit_identity_overrides_wildcard() {
let supported = &[ContentEncoding::Gzip, ContentEncoding::Identity];
let best = negotiate_encoding(Some("identity;q=1.0, *;q=0.5"), supported);
assert_eq!(best, Some(ContentEncoding::Identity));
}
#[test]
fn negotiate_wildcard_does_not_lower_identity_default() {
let supported = &[ContentEncoding::Identity];
let best = negotiate_encoding(Some("*;q=0.3"), supported);
assert_eq!(best, Some(ContentEncoding::Identity));
let supported2 = &[ContentEncoding::Gzip, ContentEncoding::Identity];
let best2 = negotiate_encoding(Some("gzip;q=0.5, *;q=0.3"), supported2);
assert_eq!(best2, Some(ContentEncoding::Identity));
}
#[test]
fn identity_compressor_passthrough() {
let mut comp = IdentityCompressor;
let mut output = Vec::new();
comp.compress(b"hello", &mut output).unwrap();
comp.compress(b" world", &mut output).unwrap();
comp.finish(&mut output).unwrap();
assert_eq!(output, b"hello world");
assert_eq!(comp.encoding(), ContentEncoding::Identity);
}
#[test]
fn identity_decompressor_passthrough() {
let mut dec = IdentityDecompressor::new(None);
let mut output = Vec::new();
dec.decompress(b"hello", &mut output).unwrap();
dec.decompress(b" world", &mut output).unwrap();
dec.finish(&mut output).unwrap();
assert_eq!(output, b"hello world");
assert_eq!(dec.encoding(), ContentEncoding::Identity);
}
#[test]
fn identity_decompressor_size_limit() {
let mut dec = IdentityDecompressor::new(Some(10));
let mut output = Vec::new();
dec.decompress(b"hello", &mut output).unwrap();
let result = dec.decompress(b"123456", &mut output);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
}
#[test]
fn identity_decompressor_exact_limit() {
let mut dec = IdentityDecompressor::new(Some(10));
let mut output = Vec::new();
dec.decompress(b"1234567890", &mut output).unwrap();
assert_eq!(output.len(), 10);
let result = dec.decompress(b"x", &mut output);
assert!(result.is_err());
}
#[test]
fn identity_decompressor_overflow_is_rejected() {
let mut dec = IdentityDecompressor {
max_size: None,
total: usize::MAX,
};
let mut output = Vec::new();
let result = dec.decompress(b"x", &mut output);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
assert!(output.is_empty());
}
#[test]
fn content_encoding_header_extraction() {
let headers = vec![
("Content-Type".to_owned(), "text/html".to_owned()),
("Content-Encoding".to_owned(), "gzip".to_owned()),
];
assert_eq!(
content_encoding_from_headers(&headers),
Some(ContentEncoding::Gzip)
);
}
#[test]
fn content_encoding_header_case_insensitive() {
let headers = vec![("content-encoding".to_owned(), "BR".to_owned())];
assert_eq!(
content_encoding_from_headers(&headers),
Some(ContentEncoding::Brotli)
);
}
#[test]
fn content_encoding_header_missing() {
let headers: Vec<(String, String)> = vec![];
assert_eq!(content_encoding_from_headers(&headers), None);
}
#[test]
fn accept_encoding_header_extraction() {
let headers = vec![("Accept-Encoding".to_owned(), "gzip, deflate, br".to_owned())];
assert_eq!(
accept_encoding_from_headers(&headers),
Some("gzip, deflate, br")
);
}
#[test]
fn accept_encoding_header_missing() {
let headers: Vec<(String, String)> = vec![];
assert_eq!(accept_encoding_from_headers(&headers), None);
}
#[test]
fn content_encoding_debug_clone_copy_hash_eq() {
use std::collections::HashSet;
let gz = ContentEncoding::Gzip;
let dbg = format!("{gz:?}");
assert!(dbg.contains("Gzip"), "{dbg}");
let copied: ContentEncoding = gz;
let cloned = gz;
assert_eq!(copied, cloned);
assert_eq!(gz, ContentEncoding::Gzip);
assert_ne!(gz, ContentEncoding::Brotli);
let mut set = HashSet::new();
set.insert(ContentEncoding::Identity);
set.insert(ContentEncoding::Gzip);
set.insert(ContentEncoding::Deflate);
set.insert(ContentEncoding::Brotli);
assert_eq!(set.len(), 4);
assert!(set.contains(&ContentEncoding::Gzip));
}
#[test]
fn identity_compressor_debug_default() {
let c = IdentityCompressor;
let dbg = format!("{c:?}");
assert!(dbg.contains("IdentityCompressor"), "{dbg}");
}
#[test]
fn identity_decompressor_debug_default() {
let d = IdentityDecompressor::default();
let dbg = format!("{d:?}");
assert!(dbg.contains("IdentityDecompressor"), "{dbg}");
}
#[test]
fn make_compressor_identity() {
let comp = make_compressor(ContentEncoding::Identity);
assert!(comp.is_some());
assert_eq!(comp.unwrap().encoding(), ContentEncoding::Identity);
}
#[cfg(feature = "compression")]
#[test]
fn make_compressor_brotli() {
let comp = make_compressor(ContentEncoding::Brotli);
assert!(comp.is_some());
assert_eq!(comp.unwrap().encoding(), ContentEncoding::Brotli);
}
#[cfg(not(feature = "compression"))]
#[test]
fn make_compressor_brotli_unsupported() {
let comp = make_compressor(ContentEncoding::Brotli);
assert!(comp.is_none());
}
#[cfg(feature = "compression")]
#[test]
fn make_compressor_gzip() {
let comp = make_compressor(ContentEncoding::Gzip);
assert!(comp.is_some());
assert_eq!(comp.unwrap().encoding(), ContentEncoding::Gzip);
}
#[cfg(feature = "compression")]
#[test]
fn make_compressor_deflate() {
let comp = make_compressor(ContentEncoding::Deflate);
assert!(comp.is_some());
assert_eq!(comp.unwrap().encoding(), ContentEncoding::Deflate);
}
#[cfg(feature = "compression")]
#[test]
fn gzip_decompressor_state_across_chunks() {
let input = b"Hello, World! Here is some data to compress and decompress in chunks.";
let mut compressor = GzipCompressor::new();
let mut compressed = Vec::new();
compressor.compress(input, &mut compressed).unwrap();
compressor.finish(&mut compressed).unwrap();
let mut decompressor = GzipDecompressor::new(None);
let mut decompressed = Vec::new();
for chunk in compressed.chunks(5) {
decompressor.decompress(chunk, &mut decompressed).unwrap();
}
decompressor.finish(&mut decompressed).unwrap();
assert_eq!(decompressed, input);
}
#[cfg(feature = "compression")]
#[test]
fn gzip_compress_decompress_roundtrip() {
let input = b"Hello, World! This is a test of gzip compression.";
let mut comp = GzipCompressor::new();
let mut compressed = Vec::new();
comp.compress(input, &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
assert!(!compressed.is_empty());
let mut dec = GzipDecompressor::new(None);
let mut decompressed = Vec::new();
dec.decompress(&compressed, &mut decompressed).unwrap();
dec.finish(&mut decompressed).unwrap();
assert_eq!(&decompressed, input);
}
#[cfg(feature = "compression")]
#[test]
fn gzip_empty_input() {
let mut comp = GzipCompressor::new();
let mut compressed = Vec::new();
comp.compress(b"", &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
let mut dec = GzipDecompressor::new(None);
let mut decompressed = Vec::new();
dec.decompress(&compressed, &mut decompressed).unwrap();
assert!(decompressed.is_empty());
}
#[cfg(feature = "compression")]
#[test]
fn gzip_compressor_default() {
let comp = GzipCompressor::default();
assert_eq!(comp.encoding(), ContentEncoding::Gzip);
}
#[cfg(feature = "compression")]
#[test]
fn gzip_decompressor_size_limit() {
let input = b"Hello, World! This is a test of gzip compression.";
let mut comp = GzipCompressor::new();
let mut compressed = Vec::new();
comp.compress(input, &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
let mut dec = GzipDecompressor::new(Some(10));
let mut decompressed = Vec::new();
let result = dec.decompress(&compressed, &mut decompressed);
assert!(result.is_err());
}
#[cfg(feature = "compression")]
#[test]
fn gzip_decompressor_overflow_is_rejected() {
let mut comp = GzipCompressor::new();
let mut compressed = Vec::new();
comp.compress(b"x", &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
let mut dec = GzipDecompressor {
max_size: None,
total: usize::MAX,
decoder: flate2::write::GzDecoder::new(LimitedWriter::new(None)),
};
let mut decompressed = Vec::new();
let result = dec.decompress(&compressed, &mut decompressed);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
assert!(decompressed.is_empty());
}
#[cfg(feature = "compression")]
#[test]
fn deflate_decompressor_state_across_chunks() {
let input = b"Hello, World! Here is some data to compress and decompress in chunks.";
let mut compressor = DeflateCompressor::new();
let mut compressed = Vec::new();
compressor.compress(input, &mut compressed).unwrap();
compressor.finish(&mut compressed).unwrap();
let mut decompressor = DeflateDecompressor::new(None);
let mut decompressed = Vec::new();
for chunk in compressed.chunks(5) {
decompressor.decompress(chunk, &mut decompressed).unwrap();
}
decompressor.finish(&mut decompressed).unwrap();
assert_eq!(decompressed, input);
}
#[cfg(feature = "compression")]
#[test]
fn deflate_compress_decompress_roundtrip() {
let input = b"Hello, World! This is a test of deflate compression.";
let mut comp = DeflateCompressor::new();
let mut compressed = Vec::new();
comp.compress(input, &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
assert!(!compressed.is_empty());
let mut dec = DeflateDecompressor::new(None);
let mut decompressed = Vec::new();
dec.decompress(&compressed, &mut decompressed).unwrap();
dec.finish(&mut decompressed).unwrap();
assert_eq!(&decompressed, input);
}
#[cfg(feature = "compression")]
#[test]
fn deflate_streaming_output_matches_reference_encoder() {
use flate2::Compression;
use flate2::read::DeflateDecoder as ReferenceDeflateDecoder;
use flate2::write::DeflateEncoder as ReferenceDeflateEncoder;
use std::io::{Read, Write};
let input = b"RFC 1951 differential vector: repeated repeated repeated payload.";
let mut ours = DeflateCompressor::with_level(Compression::default());
let mut streamed = Vec::new();
for chunk in input.chunks(7) {
ours.compress(chunk, &mut streamed).unwrap();
}
ours.finish(&mut streamed).unwrap();
let mut reference = ReferenceDeflateEncoder::new(Vec::new(), Compression::default());
reference.write_all(input).unwrap();
let reference_bytes = reference.finish().unwrap();
assert_eq!(
streamed, reference_bytes,
"streaming wrapper must match canonical RFC 1951 deflate bytes for the same payload"
);
let mut ours_dec = DeflateDecompressor::new(None);
let mut ours_plain = Vec::new();
for chunk in reference_bytes.chunks(5) {
ours_dec.decompress(chunk, &mut ours_plain).unwrap();
}
ours_dec.finish(&mut ours_plain).unwrap();
assert_eq!(ours_plain, input);
let mut reference_plain = Vec::new();
ReferenceDeflateDecoder::new(&streamed[..])
.read_to_end(&mut reference_plain)
.unwrap();
assert_eq!(reference_plain, input);
}
#[cfg(feature = "compression")]
#[test]
fn deflate_empty_input() {
let mut comp = DeflateCompressor::new();
let mut compressed = Vec::new();
comp.compress(b"", &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
let mut dec = DeflateDecompressor::new(None);
let mut decompressed = Vec::new();
dec.decompress(&compressed, &mut decompressed).unwrap();
assert!(decompressed.is_empty());
}
#[cfg(feature = "compression")]
#[test]
fn deflate_empty_stream_matches_rfc1951_empty_final_block_vector() {
let mut comp = DeflateCompressor::new();
let mut compressed = Vec::new();
comp.finish(&mut compressed).unwrap();
assert_eq!(
compressed,
vec![0x03, 0x00],
"empty raw DEFLATE stream should be a final empty block"
);
let mut dec = DeflateDecompressor::new(None);
let mut decompressed = Vec::new();
dec.decompress(&compressed, &mut decompressed).unwrap();
dec.finish(&mut decompressed).unwrap();
assert!(decompressed.is_empty());
}
#[cfg(feature = "compression")]
#[test]
fn deflate_compressor_default() {
let comp = DeflateCompressor::default();
assert_eq!(comp.encoding(), ContentEncoding::Deflate);
}
#[cfg(feature = "compression")]
#[test]
fn deflate_decompressor_size_limit() {
let input = b"Hello, World! This is a test of deflate compression.";
let mut comp = DeflateCompressor::new();
let mut compressed = Vec::new();
comp.compress(input, &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
let mut dec = DeflateDecompressor::new(Some(10));
let mut decompressed = Vec::new();
let result = dec.decompress(&compressed, &mut decompressed);
assert!(result.is_err());
}
#[cfg(feature = "compression")]
#[test]
fn deflate_decompressor_overflow_is_rejected() {
let mut comp = DeflateCompressor::new();
let mut compressed = Vec::new();
comp.compress(b"x", &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
let mut dec = DeflateDecompressor {
max_size: None,
total: usize::MAX,
decoder: flate2::write::DeflateDecoder::new(LimitedWriter::new(None)),
};
let mut decompressed = Vec::new();
let result = dec.decompress(&compressed, &mut decompressed);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
assert!(decompressed.is_empty());
}
#[cfg(feature = "compression")]
#[test]
fn gzip_compresses_repetitive_data() {
let input: Vec<u8> = "aaaa".repeat(1000).into_bytes();
let mut comp = GzipCompressor::new();
let mut compressed = Vec::new();
comp.compress(&input, &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
assert!(
compressed.len() < input.len() / 2,
"gzip should compress repetitive data: {} -> {}",
input.len(),
compressed.len()
);
}
#[cfg(feature = "compression")]
#[test]
fn deflate_compresses_repetitive_data() {
let input: Vec<u8> = "bbbb".repeat(1000).into_bytes();
let mut comp = DeflateCompressor::new();
let mut compressed = Vec::new();
comp.compress(&input, &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
assert!(
compressed.len() < input.len() / 2,
"deflate should compress repetitive data: {} -> {}",
input.len(),
compressed.len()
);
}
#[cfg(feature = "compression")]
#[test]
fn gzip_compresses_repetitive_data_chunked() {
let input: Vec<u8> = "aaaa".repeat(1000).into_bytes();
let mut comp = GzipCompressor::new();
let mut compressed = Vec::new();
for chunk in input.chunks(10) {
comp.compress(chunk, &mut compressed).unwrap();
}
comp.finish(&mut compressed).unwrap();
assert!(
compressed.len() < input.len() / 2,
"gzip should compress chunked repetitive data efficiently: {} -> {}",
input.len(),
compressed.len()
);
}
#[cfg(feature = "compression")]
#[test]
fn gzip_double_finish_is_idempotent() {
let mut comp = GzipCompressor::new();
let mut out = Vec::new();
comp.compress(b"hello", &mut out).unwrap();
comp.finish(&mut out).unwrap();
let len_after_first = out.len();
comp.finish(&mut out).unwrap();
assert_eq!(
out.len(),
len_after_first,
"second finish must not append extra bytes"
);
}
#[cfg(feature = "compression")]
#[test]
fn deflate_double_finish_is_idempotent() {
let mut comp = DeflateCompressor::new();
let mut out = Vec::new();
comp.compress(b"hello", &mut out).unwrap();
comp.finish(&mut out).unwrap();
let len_after_first = out.len();
comp.finish(&mut out).unwrap();
assert_eq!(
out.len(),
len_after_first,
"second finish must not append extra bytes"
);
}
#[cfg(feature = "compression")]
#[test]
fn brotli_decompressor_state_across_chunks() {
let input = b"Hello, World! Here is some data to compress and decompress in chunks.";
let mut compressor = BrotliCompressor::new();
let mut compressed = Vec::new();
compressor.compress(input, &mut compressed).unwrap();
compressor.finish(&mut compressed).unwrap();
let mut decompressor = BrotliDecompressor::new(None);
let mut decompressed = Vec::new();
for chunk in compressed.chunks(5) {
decompressor.decompress(chunk, &mut decompressed).unwrap();
}
decompressor.finish(&mut decompressed).unwrap();
assert_eq!(decompressed, input);
}
#[cfg(feature = "compression")]
#[test]
fn brotli_compress_decompress_roundtrip() {
let input = b"Hello, World! This is a test of brotli compression.";
let mut comp = BrotliCompressor::new();
let mut compressed = Vec::new();
comp.compress(input, &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
assert!(!compressed.is_empty());
let mut dec = BrotliDecompressor::new(None);
let mut decompressed = Vec::new();
dec.decompress(&compressed, &mut decompressed).unwrap();
dec.finish(&mut decompressed).unwrap();
assert_eq!(&decompressed, input);
}
#[cfg(feature = "compression")]
#[test]
fn brotli_empty_input() {
let mut comp = BrotliCompressor::new();
let mut compressed = Vec::new();
comp.compress(b"", &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
let mut dec = BrotliDecompressor::new(None);
let mut decompressed = Vec::new();
dec.decompress(&compressed, &mut decompressed).unwrap();
dec.finish(&mut decompressed).unwrap();
assert!(decompressed.is_empty());
}
#[cfg(feature = "compression")]
#[test]
fn brotli_compressor_default() {
let comp = BrotliCompressor::default();
assert_eq!(comp.encoding(), ContentEncoding::Brotli);
}
#[cfg(feature = "compression")]
#[test]
fn brotli_decompressor_size_limit() {
let input = b"Hello, World! This is a test of brotli compression.";
let mut comp = BrotliCompressor::new();
let mut compressed = Vec::new();
comp.compress(input, &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
let mut dec = BrotliDecompressor::new(Some(10));
let mut decompressed = Vec::new();
let result = dec.decompress(&compressed, &mut decompressed);
assert!(result.is_err());
}
#[cfg(feature = "compression")]
#[test]
fn brotli_decompressor_overflow_is_rejected() {
let mut comp = BrotliCompressor::new();
let mut compressed = Vec::new();
comp.compress(b"x", &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
let mut dec = BrotliDecompressor {
max_size: None,
total: usize::MAX,
decoder: brotli::DecompressorWriter::new(LimitedWriter::new(None), BROTLI_BUFFER_SIZE),
finished: false,
};
let mut decompressed = Vec::new();
let result = dec.decompress(&compressed, &mut decompressed);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
assert!(decompressed.is_empty());
}
#[cfg(feature = "compression")]
#[test]
fn brotli_compresses_repetitive_data() {
let input: Vec<u8> = "cccc".repeat(1000).into_bytes();
let mut comp = BrotliCompressor::new();
let mut compressed = Vec::new();
comp.compress(&input, &mut compressed).unwrap();
comp.finish(&mut compressed).unwrap();
assert!(
compressed.len() < input.len() / 2,
"brotli should compress repetitive data: {} -> {}",
input.len(),
compressed.len()
);
}
#[cfg(feature = "compression")]
#[test]
fn brotli_double_finish_is_idempotent() {
let mut comp = BrotliCompressor::new();
let mut out = Vec::new();
comp.compress(b"hello", &mut out).unwrap();
comp.finish(&mut out).unwrap();
let len_after_first = out.len();
comp.finish(&mut out).unwrap();
assert_eq!(
out.len(),
len_after_first,
"second finish must not append extra bytes"
);
}
}