rust_web_server/compression/
dictionary.rs1use crate::header::Header;
29use crate::request::Request;
30use crate::response::Response;
31
32use super::Level;
33
34pub const DCZ_MAGIC: [u8; 8] = [0x5e, 0x2a, 0x4d, 0x18, 0x20, 0x00, 0x00, 0x00];
36pub const DCB_MAGIC: [u8; 4] = [0xff, 0x44, 0x43, 0x42];
38pub const MAX_SIZE: usize = 1024 * 1024;
41
42pub type DictionaryHash = [u8; 32];
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
47pub enum DictionaryEncoding {
48 Dcz,
50 Dcb,
52}
53
54impl DictionaryEncoding {
55 pub fn token(self) -> &'static str {
57 match self {
58 DictionaryEncoding::Dcz => "dcz",
59 DictionaryEncoding::Dcb => "dcb",
60 }
61 }
62
63 pub fn available(self) -> bool {
65 match self {
66 DictionaryEncoding::Dcz => cfg!(feature = "zstd"),
67 DictionaryEncoding::Dcb => cfg!(feature = "brotli"),
68 }
69 }
70
71 fn magic(self) -> &'static [u8] {
72 match self {
73 DictionaryEncoding::Dcz => &DCZ_MAGIC,
74 DictionaryEncoding::Dcb => &DCB_MAGIC,
75 }
76 }
77
78 pub fn header_len(self) -> usize {
80 self.magic().len() + 32
81 }
82}
83
84pub const PREFERENCE: &[DictionaryEncoding] = &[DictionaryEncoding::Dcz, DictionaryEncoding::Dcb];
87
88pub fn hash(dictionary: &[u8]) -> DictionaryHash {
90 crate::core::sha256::sha256(dictionary)
91}
92
93pub fn parse_available_dictionary(value: &str) -> Option<DictionaryHash> {
96 let rest = value.trim().strip_prefix(':')?;
97 let end = rest.find(':')?;
98 let (encoded, after) = (&rest[..end], &rest[end + 1..]);
99 if !(after.is_empty() || after.starts_with(';')) || encoded.len() != 44 {
100 return None;
101 }
102 if !encoded.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'=')) {
103 return None;
104 }
105 let bytes = crate::core::base64::Base64::decode(encoded.to_string()).ok()?;
106 bytes.try_into().ok()
107}
108
109pub fn available_dictionary(request: &Request) -> Option<DictionaryHash> {
111 parse_available_dictionary(&request.header("Available-Dictionary")?.value)
112}
113
114pub fn negotiate(request: &Request) -> Option<DictionaryEncoding> {
118 let header = request.header(Header::_ACCEPT_ENCODING)?;
119 let mut best: Option<(DictionaryEncoding, f32)> = None;
120 for &encoding in PREFERENCE.iter().filter(|e| e.available()) {
121 let listed = header.value.split(',').any(|c| c.split(';').next().unwrap_or("").trim().eq_ignore_ascii_case(encoding.token()));
122 let q = if listed { super::accepted_weight(&header.value, encoding.token()) } else { 0.0 };
123 if q > 0.0 && best.is_none_or(|(_, best_q)| q > best_q) {
124 best = Some((encoding, q));
125 }
126 }
127 best.map(|(encoding, _)| encoding)
128}
129
130fn window_log(bytes: usize) -> u32 {
132 (usize::BITS - bytes.max(1).saturating_sub(1).leading_zeros()).max(10)
133}
134
135pub fn encode(encoding: DictionaryEncoding, body: &[u8], dictionary: &[u8], level: Level) -> Option<Vec<u8>> {
139 if body.len() > MAX_SIZE || dictionary.len() > MAX_SIZE || !encoding.available() {
140 return None;
141 }
142 let mut out = Vec::with_capacity(encoding.header_len() + body.len() / 4 + 64);
143 out.extend_from_slice(encoding.magic());
144 out.extend_from_slice(&hash(dictionary));
145 let window = window_log(dictionary.len() + body.len() + 16);
148 match encoding {
149 #[cfg(feature = "zstd")]
150 DictionaryEncoding::Dcz => {
151 let mut compressor = zstd::bulk::Compressor::new(super::ZSTD_LEVEL[level.index()]).ok()?;
152 compressor.set_parameter(zstd::zstd_safe::CParameter::WindowLog(window)).ok()?;
153 compressor.context_mut().ref_prefix(dictionary).ok()?;
154 out.extend_from_slice(&compressor.compress(body).ok()?);
155 }
156 #[cfg(feature = "brotli")]
157 DictionaryEncoding::Dcb => {
158 let params = brotli::enc::BrotliEncoderParams {
159 quality: super::BROTLI_QUALITY[level.index()] as i32,
160 lgwin: window.min(24) as i32,
161 ..Default::default()
162 };
163 let mut nop = |_: &mut brotli::interface::PredictionModeContextMap<brotli::InputReferenceMut>,
164 _: &mut [brotli::interface::StaticCommand],
165 _: brotli::InputPair,
166 _: &mut brotli::enc::StandardAlloc| ();
167 brotli::BrotliCompressCustomIoCustomDict(
168 &mut brotli::enc::IoReaderWrapper(&mut &body[..]),
169 &mut brotli::enc::IoWriterWrapper(&mut out),
170 &mut [0u8; 4096],
171 &mut [0u8; 4096],
172 ¶ms,
173 brotli::enc::StandardAlloc::default(),
174 &mut nop,
175 dictionary,
176 std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected end of input"),
177 )
178 .ok()?;
179 }
180 #[allow(unreachable_patterns)]
181 _ => {
182 let _ = (level, window);
183 return None;
184 }
185 }
186 Some(out)
187}
188
189pub fn decode(encoding: DictionaryEncoding, data: &[u8], dictionary: &[u8], max_len: usize) -> Option<Vec<u8>> {
193 use std::io::Read;
194 let header = encoding.header_len();
195 if data.len() < header || !data.starts_with(encoding.magic()) || data[encoding.magic().len()..header] != hash(dictionary) {
196 return None;
197 }
198 let stream = &data[header..];
199 let mut out = Vec::new();
200 match encoding {
201 #[cfg(feature = "zstd")]
202 DictionaryEncoding::Dcz => {
203 let mut decoder = zstd::stream::read::Decoder::with_ref_prefix(stream, dictionary).ok()?;
204 decoder.window_log_max(23).ok()?;
205 decoder.take(max_len as u64 + 1).read_to_end(&mut out).ok()?;
206 }
207 #[cfg(feature = "brotli")]
208 DictionaryEncoding::Dcb => {
209 use brotli::{Allocator, SliceWrapperMut};
210 let mut alloc = brotli::enc::StandardAlloc::default();
211 let mut dict = <brotli::enc::StandardAlloc as Allocator<u8>>::alloc_cell(&mut alloc, dictionary.len());
212 dict.slice_mut().copy_from_slice(dictionary);
213 let mut writer = LimitedWriter { out: &mut out, max: max_len + 1 };
214 brotli::BrotliDecompressCustomIoCustomDict(
215 &mut brotli::enc::IoReaderWrapper(&mut &stream[..]),
216 &mut brotli::enc::IoWriterWrapper(&mut writer),
217 &mut [0u8; 4096],
218 &mut [0u8; 4096],
219 brotli::enc::StandardAlloc::default(),
220 brotli::enc::StandardAlloc::default(),
221 brotli::enc::StandardAlloc::default(),
222 dict,
223 std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected end of input"),
224 )
225 .ok()?;
226 }
227 #[allow(unreachable_patterns)]
228 _ => return None,
229 }
230 (out.len() <= max_len).then_some(out)
231}
232
233#[cfg(feature = "brotli")]
235struct LimitedWriter<'a> {
236 out: &'a mut Vec<u8>,
237 max: usize,
238}
239
240#[cfg(feature = "brotli")]
241impl std::io::Write for LimitedWriter<'_> {
242 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
243 if self.out.len() + buf.len() > self.max {
244 return Err(std::io::Error::other("decoded body too large"));
245 }
246 self.out.extend_from_slice(buf);
247 Ok(buf.len())
248 }
249
250 fn flush(&mut self) -> std::io::Result<()> {
251 Ok(())
252 }
253}
254
255pub fn mark_encoded(response: &mut Response, encoding: DictionaryEncoding) {
258 response.headers.push(Header { name: Header::_CONTENT_ENCODING.into(), value: encoding.token().into() });
259 add_vary(response, "Accept-Encoding");
260 add_vary(response, "Available-Dictionary");
261}
262
263pub(crate) fn add_vary(response: &mut Response, name: &'static str) {
265 if let Some(vary) = response.headers.iter_mut().find(|h| h.name.eq_ignore_ascii_case("vary")) {
266 if !vary.value.split(',').any(|v| v.trim().eq_ignore_ascii_case(name) || v.trim() == "*") {
267 vary.value = format!("{}, {name}", vary.value).into();
268 }
269 } else {
270 response.headers.push(Header { name: "Vary".into(), value: name.into() });
271 }
272}
273
274#[cfg(test)]
275mod tests;