1use crate::file::File;
4use anyhow::{Error, bail};
5use http::{HeaderMap, HeaderValue, header};
6use std::cell::Cell;
7
8#[derive(PartialEq, Eq, Debug)]
15pub struct EncodingAccepted {
16 pub gzip: bool,
18 pub brotli: bool,
20}
21impl EncodingAccepted {
22 pub fn none() -> Self {
25 Self {
26 gzip: false,
27 brotli: false,
28 }
29 }
30
31 pub fn from_headers(headers: &HeaderMap) -> Result<Self, Error> {
35 let accept_encoding = match headers.get(header::ACCEPT_ENCODING) {
36 Some(accept_encoding) => accept_encoding,
37 None => return Ok(Self::none()),
38 };
39
40 let self_ = Self::from_accept_encoding_header_raw(accept_encoding)?;
41
42 Ok(self_)
43 }
44 pub fn from_accept_encoding_header_raw(accept_encoding: &HeaderValue) -> Result<Self, Error> {
47 let accept_encoding = match accept_encoding.to_str() {
48 Ok(accept_encoding) => accept_encoding,
49 Err(_) => bail!("unable to parse accept encoding as string"),
50 };
51
52 let self_ = Self::from_accept_encoding_header_str(accept_encoding);
53
54 Ok(self_)
55 }
56 pub fn from_accept_encoding_header_str(accept_encoding: &str) -> Self {
58 let mut gzip = false;
59 let mut brotli = false;
60
61 accept_encoding
64 .split(",")
65 .map(|accept_encoding| accept_encoding.trim())
66 .map(Self::extract_algorithm_from_value)
67 .for_each(|accept_encoding| match accept_encoding {
68 "gzip" => {
69 gzip = true;
70 }
71 "br" => {
72 brotli = true;
73 }
74 _ => {}
75 });
76
77 Self { gzip, brotli }
78 }
79
80 pub fn extract_algorithm_from_value(mut value: &str) -> &str {
83 if let Some((algorithm, _)) = value.split_once(";q=") {
84 value = algorithm;
85 }
86 value
87 }
88}
89
90#[cfg(test)]
91mod test_encoding_accepted {
92 use super::EncodingAccepted;
93 use http::{HeaderMap, HeaderName, HeaderValue};
94 use test_case::test_case;
95
96 #[test_case(&[], Some(EncodingAccepted::none()))]
97 #[test_case(&[("accept-encoding", "gzip")], Some(EncodingAccepted { gzip: true, brotli: false }))]
98 fn from_headers_returns_expected(
99 headers: &[(&'static str, &'static str)],
100 expected: Option<EncodingAccepted>,
101 ) {
102 let headers_map = headers
103 .iter()
104 .copied()
105 .map(|(key, value)| {
106 (
107 HeaderName::from_static(key),
108 HeaderValue::from_static(value),
109 )
110 })
111 .collect::<HeaderMap>();
112
113 assert_eq!(EncodingAccepted::from_headers(&headers_map).ok(), expected);
114 }
115
116 #[test_case(HeaderValue::from_bytes(b"\xff").unwrap(), None)]
117 #[test_case(HeaderValue::from_static(""), Some(EncodingAccepted { gzip: false, brotli: false }))]
118 #[test_case(HeaderValue::from_static("gzip, compress, br"), Some(EncodingAccepted { gzip: true, brotli: true }))]
119 fn from_accept_encoding_header_raw_returns_expected(
120 header_value: HeaderValue,
121 expected: Option<EncodingAccepted>,
122 ) {
123 assert_eq!(
124 EncodingAccepted::from_accept_encoding_header_raw(&header_value).ok(),
125 expected
126 );
127 }
128
129 #[test_case("", EncodingAccepted { gzip: false, brotli: false })]
130 #[test_case("gzip", EncodingAccepted { gzip: true, brotli: false })]
131 #[test_case("br", EncodingAccepted { gzip: false, brotli: true })]
132 #[test_case("deflate, gzip;q=1.0", EncodingAccepted { gzip: true, brotli: false })]
133 #[test_case("br,gzip", EncodingAccepted { gzip: true, brotli: true })]
134 fn from_accept_encoding_header_str_returns_expected(
135 accept_encoding: &str,
136 expected: EncodingAccepted,
137 ) {
138 assert_eq!(
139 EncodingAccepted::from_accept_encoding_header_str(accept_encoding),
140 expected
141 );
142 }
143
144 #[test_case("", "")]
145 #[test_case("gzip", "gzip")]
146 #[test_case("gzip;q=1.0", "gzip")]
147 fn extract_algorithm_from_value_returns_expected(
148 value: &str,
149 expected: &str,
150 ) {
151 assert_eq!(
152 EncodingAccepted::extract_algorithm_from_value(value),
153 expected
154 );
155 }
156}
157
158#[derive(PartialEq, Eq, Debug)]
162pub struct ContentContentEncoding<'c> {
163 pub content: &'c [u8],
165 pub content_encoding: HeaderValue,
167}
168impl<'c> ContentContentEncoding<'c> {
169 pub fn resolve(
172 encoding_accepted: &EncodingAccepted,
173 file: &'c impl File,
174 ) -> Self {
175 let mut best = Cell::new(ContentContentEncoding {
176 content: file.content(),
177 content_encoding: HeaderValue::from_static("identity"),
178 });
179
180 if encoding_accepted.gzip
182 && let Some(content_gzip) = file.content_gzip()
183 && content_gzip.len() <= best.get_mut().content.len()
184 {
185 best.set(ContentContentEncoding {
186 content: content_gzip,
187 content_encoding: HeaderValue::from_static("gzip"),
188 });
189 }
190
191 if encoding_accepted.brotli
193 && let Some(content_brotli) = file.content_brotli()
194 && content_brotli.len() <= best.get_mut().content.len()
195 {
196 best.set(ContentContentEncoding {
197 content: content_brotli,
198 content_encoding: HeaderValue::from_static("br"),
199 });
200 }
201
202 best.into_inner()
203 }
204}
205
206#[cfg(test)]
207mod test_content_content_encoding {
208 use super::{ContentContentEncoding, EncodingAccepted};
209 use crate::{cache_control::CacheControl, file::File};
210 use http::HeaderValue;
211 use test_case::test_case;
212
213 #[derive(Debug)]
214 pub struct FileMock {
215 pub content: &'static [u8],
216 pub content_gzip: Option<&'static [u8]>,
217 pub content_brotli: Option<&'static [u8]>,
218 }
219 impl File for FileMock {
220 fn content(&self) -> &[u8] {
221 self.content
222 }
223 fn content_gzip(&self) -> Option<&[u8]> {
224 self.content_gzip
225 }
226 fn content_brotli(&self) -> Option<&[u8]> {
227 self.content_brotli
228 }
229
230 fn content_type(&self) -> HeaderValue {
231 unimplemented!()
232 }
233
234 fn etag(&self) -> HeaderValue {
235 unimplemented!()
236 }
237
238 fn cache_control(&self) -> CacheControl {
239 unimplemented!()
240 }
241 }
242
243 #[test_case(
244 EncodingAccepted { gzip: false, brotli: false },
245 FileMock { content: b"content-identity", content_gzip: None, content_brotli: None },
246 ContentContentEncoding {content: b"content-identity", content_encoding: HeaderValue::from_static("identity") } ;
247 "nothing provided, nothing accepted"
248 )]
249 #[test_case(
250 EncodingAccepted { gzip: false, brotli: false },
251 FileMock { content: b"content-identity", content_gzip: Some(b"content-gzip"), content_brotli: Some(b"content-brotli") },
252 ContentContentEncoding {content: b"content-identity", content_encoding: HeaderValue::from_static("identity") } ;
253 "all provided, nothing accepted"
254 )]
255 #[test_case(
256 EncodingAccepted { gzip: true, brotli: true },
257 FileMock { content: b"content-identity", content_gzip: None, content_brotli: None },
258 ContentContentEncoding {content: b"content-identity", content_encoding: HeaderValue::from_static("identity") } ;
259 "all accepted, nothing provided"
260 )]
261 #[test_case(
262 EncodingAccepted { gzip: true, brotli: true },
263 FileMock { content: b"content-aaa", content_gzip: Some(b"content-bb"), content_brotli: Some(b"content-c") },
264 ContentContentEncoding {content: b"content-c", content_encoding: HeaderValue::from_static("br") } ;
265 "brotli should win as the shortest"
266 )]
267 fn resolve_returns_expected(
268 encoding_accepted: EncodingAccepted,
269 content: FileMock,
270 expected: ContentContentEncoding,
271 ) {
272 assert_eq!(
273 ContentContentEncoding::resolve(&encoding_accepted, &content),
274 expected
275 );
276 }
277}