use crate::header::Header;
use crate::request::Request;
use crate::response::Response;
use super::Level;
pub const DCZ_MAGIC: [u8; 8] = [0x5e, 0x2a, 0x4d, 0x18, 0x20, 0x00, 0x00, 0x00];
pub const DCB_MAGIC: [u8; 4] = [0xff, 0x44, 0x43, 0x42];
pub const MAX_SIZE: usize = 1024 * 1024;
pub type DictionaryHash = [u8; 32];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DictionaryEncoding {
Dcz,
Dcb,
}
impl DictionaryEncoding {
pub fn token(self) -> &'static str {
match self {
DictionaryEncoding::Dcz => "dcz",
DictionaryEncoding::Dcb => "dcb",
}
}
pub fn available(self) -> bool {
match self {
DictionaryEncoding::Dcz => cfg!(feature = "zstd"),
DictionaryEncoding::Dcb => cfg!(feature = "brotli"),
}
}
fn magic(self) -> &'static [u8] {
match self {
DictionaryEncoding::Dcz => &DCZ_MAGIC,
DictionaryEncoding::Dcb => &DCB_MAGIC,
}
}
pub fn header_len(self) -> usize {
self.magic().len() + 32
}
}
pub const PREFERENCE: &[DictionaryEncoding] = &[DictionaryEncoding::Dcz, DictionaryEncoding::Dcb];
pub fn hash(dictionary: &[u8]) -> DictionaryHash {
crate::core::sha256::sha256(dictionary)
}
pub fn parse_available_dictionary(value: &str) -> Option<DictionaryHash> {
let rest = value.trim().strip_prefix(':')?;
let end = rest.find(':')?;
let (encoded, after) = (&rest[..end], &rest[end + 1..]);
if !(after.is_empty() || after.starts_with(';')) || encoded.len() != 44 {
return None;
}
if !encoded.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'=')) {
return None;
}
let bytes = crate::core::base64::Base64::decode(encoded.to_string()).ok()?;
bytes.try_into().ok()
}
pub fn available_dictionary(request: &Request) -> Option<DictionaryHash> {
parse_available_dictionary(&request.header("Available-Dictionary")?.value)
}
pub fn negotiate(request: &Request) -> Option<DictionaryEncoding> {
let header = request.header(Header::_ACCEPT_ENCODING)?;
let mut best: Option<(DictionaryEncoding, f32)> = None;
for &encoding in PREFERENCE.iter().filter(|e| e.available()) {
let listed = header.value.split(',').any(|c| c.split(';').next().unwrap_or("").trim().eq_ignore_ascii_case(encoding.token()));
let q = if listed { super::accepted_weight(&header.value, encoding.token()) } else { 0.0 };
if q > 0.0 && best.is_none_or(|(_, best_q)| q > best_q) {
best = Some((encoding, q));
}
}
best.map(|(encoding, _)| encoding)
}
fn window_log(bytes: usize) -> u32 {
(usize::BITS - bytes.max(1).saturating_sub(1).leading_zeros()).max(10)
}
pub fn encode(encoding: DictionaryEncoding, body: &[u8], dictionary: &[u8], level: Level) -> Option<Vec<u8>> {
if body.len() > MAX_SIZE || dictionary.len() > MAX_SIZE || !encoding.available() {
return None;
}
let mut out = Vec::with_capacity(encoding.header_len() + body.len() / 4 + 64);
out.extend_from_slice(encoding.magic());
out.extend_from_slice(&hash(dictionary));
let window = window_log(dictionary.len() + body.len() + 16);
match encoding {
#[cfg(feature = "zstd")]
DictionaryEncoding::Dcz => {
let mut compressor = zstd::bulk::Compressor::new(super::ZSTD_LEVEL[level.index()]).ok()?;
compressor.set_parameter(zstd::zstd_safe::CParameter::WindowLog(window)).ok()?;
compressor.context_mut().ref_prefix(dictionary).ok()?;
out.extend_from_slice(&compressor.compress(body).ok()?);
}
#[cfg(feature = "brotli")]
DictionaryEncoding::Dcb => {
let params = brotli::enc::BrotliEncoderParams {
quality: super::BROTLI_QUALITY[level.index()] as i32,
lgwin: window.min(24) as i32,
..Default::default()
};
let mut nop = |_: &mut brotli::interface::PredictionModeContextMap<brotli::InputReferenceMut>,
_: &mut [brotli::interface::StaticCommand],
_: brotli::InputPair,
_: &mut brotli::enc::StandardAlloc| ();
brotli::BrotliCompressCustomIoCustomDict(
&mut brotli::enc::IoReaderWrapper(&mut &body[..]),
&mut brotli::enc::IoWriterWrapper(&mut out),
&mut [0u8; 4096],
&mut [0u8; 4096],
¶ms,
brotli::enc::StandardAlloc::default(),
&mut nop,
dictionary,
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected end of input"),
)
.ok()?;
}
#[allow(unreachable_patterns)]
_ => {
let _ = (level, window);
return None;
}
}
Some(out)
}
pub fn decode(encoding: DictionaryEncoding, data: &[u8], dictionary: &[u8], max_len: usize) -> Option<Vec<u8>> {
use std::io::Read;
let header = encoding.header_len();
if data.len() < header || !data.starts_with(encoding.magic()) || data[encoding.magic().len()..header] != hash(dictionary) {
return None;
}
let stream = &data[header..];
let mut out = Vec::new();
match encoding {
#[cfg(feature = "zstd")]
DictionaryEncoding::Dcz => {
let mut decoder = zstd::stream::read::Decoder::with_ref_prefix(stream, dictionary).ok()?;
decoder.window_log_max(23).ok()?;
decoder.take(max_len as u64 + 1).read_to_end(&mut out).ok()?;
}
#[cfg(feature = "brotli")]
DictionaryEncoding::Dcb => {
use brotli::{Allocator, SliceWrapperMut};
let mut alloc = brotli::enc::StandardAlloc::default();
let mut dict = <brotli::enc::StandardAlloc as Allocator<u8>>::alloc_cell(&mut alloc, dictionary.len());
dict.slice_mut().copy_from_slice(dictionary);
let mut writer = LimitedWriter { out: &mut out, max: max_len + 1 };
brotli::BrotliDecompressCustomIoCustomDict(
&mut brotli::enc::IoReaderWrapper(&mut &stream[..]),
&mut brotli::enc::IoWriterWrapper(&mut writer),
&mut [0u8; 4096],
&mut [0u8; 4096],
brotli::enc::StandardAlloc::default(),
brotli::enc::StandardAlloc::default(),
brotli::enc::StandardAlloc::default(),
dict,
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected end of input"),
)
.ok()?;
}
#[allow(unreachable_patterns)]
_ => return None,
}
(out.len() <= max_len).then_some(out)
}
#[cfg(feature = "brotli")]
struct LimitedWriter<'a> {
out: &'a mut Vec<u8>,
max: usize,
}
#[cfg(feature = "brotli")]
impl std::io::Write for LimitedWriter<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if self.out.len() + buf.len() > self.max {
return Err(std::io::Error::other("decoded body too large"));
}
self.out.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub fn mark_encoded(response: &mut Response, encoding: DictionaryEncoding) {
response.headers.push(Header { name: Header::_CONTENT_ENCODING.into(), value: encoding.token().into() });
add_vary(response, "Accept-Encoding");
add_vary(response, "Available-Dictionary");
}
pub(crate) fn add_vary(response: &mut Response, name: &'static str) {
if let Some(vary) = response.headers.iter_mut().find(|h| h.name.eq_ignore_ascii_case("vary")) {
if !vary.value.split(',').any(|v| v.trim().eq_ignore_ascii_case(name) || v.trim() == "*") {
vary.value = format!("{}, {name}", vary.value).into();
}
} else {
response.headers.push(Header { name: "Vary".into(), value: name.into() });
}
}
#[cfg(test)]
mod tests;