rust_web_server/compression/
mod.rs1#[cfg(test)]
2mod tests;
3
4use flate2::Compression;
5use flate2::write::GzEncoder;
6use std::io::Write;
7use crate::header::Header;
8use crate::request::Request;
9use crate::response::Response;
10
11const COMPRESSIBLE: &[&str] = &[
13 "text/html",
14 "text/css",
15 "text/javascript",
16 "text/plain",
17 "text/xml",
18 "application/json",
19 "application/javascript",
20 "application/xml",
21 "application/xhtml+xml",
22 "image/svg+xml",
23];
24
25pub const MIN_COMPRESS_SIZE: usize = 512;
29
30pub fn apply_gzip(request: &Request, response: &mut Response) {
37 if response.content_range_list.is_empty() {
38 return;
39 }
40 if response.status_code == 206 || response.content_range_list.len() > 1 {
44 return;
45 }
46 if response.headers.iter().any(|h| h.name.eq_ignore_ascii_case(Header::_CONTENT_ENCODING)) {
47 return;
48 }
49 let original_size: usize = response.content_range_list.iter().map(|cr| cr.body.len()).sum();
50 if original_size < MIN_COMPRESS_SIZE {
51 return;
52 }
53 if !accepts_gzip(request) {
54 return;
55 }
56 let content_type = response
57 .content_range_list
58 .first()
59 .map(|cr| cr.content_type.as_str())
60 .unwrap_or_default();
61 if !is_compressible(content_type) {
62 return;
63 }
64
65 let mut compressed_bodies = Vec::with_capacity(response.content_range_list.len());
66 for cr in &response.content_range_list {
67 match gzip(&cr.body) {
68 Some(compressed) => compressed_bodies.push(compressed),
69 None => return,
70 }
71 }
72 let compressed_size: usize = compressed_bodies.iter().map(Vec::len).sum();
73 if compressed_size >= original_size {
74 return;
75 }
76 for (cr, compressed) in response.content_range_list.iter_mut().zip(compressed_bodies) {
77 let new_len = compressed.len() as u64;
78 cr.body = compressed;
79 cr.range.end = new_len;
80 cr.size = new_len.to_string();
81 }
82 mark_gzipped(response);
83}
84
85pub(crate) fn accepts_gzip(request: &Request) -> bool {
88 request
89 .header(Header::_ACCEPT_ENCODING)
90 .map(|h| h.value.split(',').any(|coding| {
91 let mut parts = coding.split(';');
92 let name_ok = parts.next().unwrap_or("").trim().eq_ignore_ascii_case("gzip");
93 let refused = parts.any(|p| {
94 let p = p.trim();
95 p.len() >= 2 && p[..2].eq_ignore_ascii_case("q=") && p[2..].trim().parse::<f32>().is_ok_and(|q| q == 0.0)
96 });
97 name_ok && !refused
98 }))
99 .unwrap_or(false)
100}
101
102pub(crate) fn is_compressible(content_type: &str) -> bool {
105 COMPRESSIBLE.iter().any(|mime| {
106 content_type.len() >= mime.len() && content_type.as_bytes()[..mime.len()].eq_ignore_ascii_case(mime.as_bytes())
107 })
108}
109
110pub(crate) fn gzip(body: &[u8]) -> Option<Vec<u8>> {
112 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
113 encoder.write_all(body).ok()?;
114 encoder.finish().ok()
115}
116
117pub(crate) fn mark_gzipped(response: &mut Response) {
120 response.headers.push(Header {
121 name: Header::_CONTENT_ENCODING.into(),
122 value: "gzip".into(),
123 });
124
125 let vary_pos = response.headers.iter().position(|h| h.name == Header::_VARY);
126 if let Some(i) = vary_pos {
127 let current = response.headers[i].value.clone();
128 if !current.to_lowercase().contains("accept-encoding") {
129 response.headers[i].value = format!("{}, Accept-Encoding", current).into();
130 }
131 } else {
132 response.headers.push(Header {
133 name: Header::_VARY.into(),
134 value: "Accept-Encoding".into(),
135 });
136 }
137}