use crate::{Bytes, Error, Result};
pub const MAX_INFLATED_BYTES: u64 = 1 << 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum ContentEncoding {
Gzip,
}
impl ContentEncoding {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
ContentEncoding::Gzip => "gzip",
}
}
pub fn parse(token: &str) -> Result<Option<Self>> {
let t = token.trim();
if t.is_empty() || t.eq_ignore_ascii_case("identity") {
Ok(None)
} else if t.eq_ignore_ascii_case("gzip") {
Ok(Some(ContentEncoding::Gzip))
} else {
Err(unsupported(t))
}
}
}
impl std::fmt::Display for ContentEncoding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
fn unsupported(token: &str) -> Error {
Error::UnsupportedEncoding { encoding: token.chars().take(64).collect() }
}
#[cfg_attr(not(feature = "gzip"), allow(clippy::unnecessary_wraps))]
pub(crate) fn encode(encoding: ContentEncoding, body: &[u8]) -> Result<Bytes> {
#[cfg(feature = "gzip")]
{
use std::io::Write;
let ContentEncoding::Gzip = encoding;
let mut enc = flate2::write::GzEncoder::new(
Vec::with_capacity(body.len() / 4 + 64),
flate2::Compression::default(),
);
enc.write_all(body)?;
Ok(Bytes::from(enc.finish()?))
}
#[cfg(not(feature = "gzip"))]
{
let _ = body;
Err(unsupported(encoding.as_str()))
}
}
pub(crate) fn decode(encoding: ContentEncoding, stored: &[u8], limit: u64) -> Result<Bytes> {
#[cfg(feature = "gzip")]
{
use std::io::Read;
let ContentEncoding::Gzip = encoding;
let isize_hint = stored
.len()
.checked_sub(4)
.and_then(|at| stored.get(at..))
.map_or(0, |t| u64::from(u32::from_le_bytes([t[0], t[1], t[2], t[3]])));
let ceiling = (stored.len() as u64).saturating_mul(1032);
let hint = isize_hint.min(ceiling).min(limit);
let mut out = Vec::with_capacity(usize::try_from(hint).unwrap_or(0));
let mut dec = flate2::read::MultiGzDecoder::new(stored).take(limit.saturating_add(1));
dec.read_to_end(&mut out).map_err(|e| Error::corrupt(format!("gzip stream: {e}")))?;
if out.len() as u64 > limit {
return Err(Error::TooLarge { limit });
}
Ok(Bytes::from(out))
}
#[cfg(not(feature = "gzip"))]
{
let _ = (stored, limit);
Err(unsupported(encoding.as_str()))
}
}
#[doc(hidden)]
pub fn fuzz(stored: &[u8]) {
const LIMIT: u64 = 1 << 20;
if let Ok(b) = decode(ContentEncoding::Gzip, stored, LIMIT) {
assert!(b.len() as u64 <= LIMIT);
if let Ok(again) = encode(ContentEncoding::Gzip, &b) {
assert_eq!(decode(ContentEncoding::Gzip, &again, LIMIT).ok().as_ref(), Some(&b));
}
}
let _ = ContentEncoding::parse(&String::from_utf8_lossy(stored));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tokens_parse_and_print() {
assert_eq!(ContentEncoding::parse("gzip").unwrap(), Some(ContentEncoding::Gzip));
assert_eq!(ContentEncoding::parse(" GZip ").unwrap(), Some(ContentEncoding::Gzip));
assert_eq!(ContentEncoding::parse("identity").unwrap(), None);
assert_eq!(ContentEncoding::parse("").unwrap(), None);
for bad in ["br", "deflate", "gzip, br", "x-gzip"] {
assert!(
matches!(ContentEncoding::parse(bad), Err(Error::UnsupportedEncoding { .. })),
"{bad}"
);
}
assert_eq!(ContentEncoding::Gzip.to_string(), "gzip");
let long = "z".repeat(10_000);
let Err(Error::UnsupportedEncoding { encoding }) = ContentEncoding::parse(&long) else {
panic!("unknown token accepted")
};
assert_eq!(encoding.len(), 64, "a hostile token is bounded in the error");
}
#[cfg(feature = "gzip")]
#[test]
fn round_trip_bounds_and_damage() {
let body: Vec<u8> = b"the same line, again and again\n".repeat(1000);
let gz = encode(ContentEncoding::Gzip, &body).unwrap();
assert!(gz.len() * 10 < body.len(), "{} -> {}", body.len(), gz.len());
assert_eq!(gz, encode(ContentEncoding::Gzip, &body).unwrap(), "deterministic");
assert_eq!(decode(ContentEncoding::Gzip, &gz, body.len() as u64).unwrap(), body);
assert!(matches!(
decode(ContentEncoding::Gzip, &gz, body.len() as u64 - 1),
Err(Error::TooLarge { .. })
));
assert!(matches!(
decode(ContentEncoding::Gzip, &gz[..gz.len() - 3], u64::MAX),
Err(Error::Corrupt { .. })
));
let mut bad = gz.to_vec();
let mid = bad.len() / 2;
bad[mid] ^= 0x55;
assert!(decode(ContentEncoding::Gzip, &bad, u64::MAX).is_err());
assert!(matches!(decode(ContentEncoding::Gzip, b"", 10), Err(Error::Corrupt { .. })));
assert!(matches!(
decode(ContentEncoding::Gzip, b"plain text", 10),
Err(Error::Corrupt { .. })
));
let mut two = encode(ContentEncoding::Gzip, b"ab").unwrap().to_vec();
two.extend_from_slice(&encode(ContentEncoding::Gzip, b"cd").unwrap());
assert_eq!(decode(ContentEncoding::Gzip, &two, 10).unwrap(), &b"abcd"[..]);
let empty = encode(ContentEncoding::Gzip, b"").unwrap();
assert!(decode(ContentEncoding::Gzip, &empty, 0).unwrap().is_empty());
}
#[cfg(not(feature = "gzip"))]
#[test]
fn without_the_codec_encoded_io_is_refused() {
assert!(matches!(
encode(ContentEncoding::Gzip, b"x"),
Err(Error::UnsupportedEncoding { .. })
));
assert!(matches!(
decode(ContentEncoding::Gzip, b"x", 1),
Err(Error::UnsupportedEncoding { .. })
));
}
}