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 for accept_encoding in accept_encoding.split(", ") {
62 let accept_encoding = Self::extract_algorithm_from_value(accept_encoding);
63
64 match accept_encoding {
65 "gzip" => {
66 gzip = true;
67 }
68 "br" => {
69 brotli = true;
70 }
71 _ => {}
72 }
73 }
74
75 Self { gzip, brotli }
76 }
77
78 pub fn extract_algorithm_from_value(mut value: &str) -> &str {
81 if let Some((algorithm, _)) = value.split_once(";q=") {
82 value = algorithm;
83 }
84 value
85 }
86}
87
88#[cfg(test)]
89mod test_encoding_accepted {
90 use super::EncodingAccepted;
91 use http::{HeaderMap, HeaderName, HeaderValue};
92 use test_case::test_case;
93
94 #[test_case(&[], Some(EncodingAccepted::none()))]
95 #[test_case(&[("accept-encoding", "gzip")], Some(EncodingAccepted { gzip: true, brotli: false }))]
96 fn from_headers_returns_expected(
97 headers: &[(&'static str, &'static str)],
98 expected: Option<EncodingAccepted>,
99 ) {
100 let headers_map = headers
101 .iter()
102 .copied()
103 .map(|(key, value)| {
104 (
105 HeaderName::from_static(key),
106 HeaderValue::from_static(value),
107 )
108 })
109 .collect::<HeaderMap>();
110
111 assert_eq!(EncodingAccepted::from_headers(&headers_map).ok(), expected);
112 }
113
114 #[test_case(HeaderValue::from_bytes(b"\xff").unwrap(), None)]
115 #[test_case(HeaderValue::from_static(""), Some(EncodingAccepted { gzip: false, brotli: false }))]
116 #[test_case(HeaderValue::from_static("gzip, compress, br"), Some(EncodingAccepted { gzip: true, brotli: true }))]
117 fn from_accept_encoding_header_raw_returns_expected(
118 header_value: HeaderValue,
119 expected: Option<EncodingAccepted>,
120 ) {
121 assert_eq!(
122 EncodingAccepted::from_accept_encoding_header_raw(&header_value).ok(),
123 expected
124 );
125 }
126
127 #[test_case("", EncodingAccepted { gzip: false, brotli: false })]
128 #[test_case("gzip", EncodingAccepted { gzip: true, brotli: false })]
129 #[test_case("br", EncodingAccepted { gzip: false, brotli: true })]
130 #[test_case("deflate, gzip;q=1.0", EncodingAccepted { gzip: true, brotli: false })]
131 fn from_accept_encoding_header_str_returns_expected(
132 accept_encoding: &str,
133 expected: EncodingAccepted,
134 ) {
135 assert_eq!(
136 EncodingAccepted::from_accept_encoding_header_str(accept_encoding),
137 expected
138 );
139 }
140
141 #[test_case("", "")]
142 #[test_case("gzip", "gzip")]
143 #[test_case("gzip;q=1.0", "gzip")]
144 fn extract_algorithm_from_value_returns_expected(
145 value: &str,
146 expected: &str,
147 ) {
148 assert_eq!(
149 EncodingAccepted::extract_algorithm_from_value(value),
150 expected
151 );
152 }
153}
154
155#[derive(PartialEq, Eq, Debug)]
159pub struct ContentContentEncoding<'c> {
160 pub content: &'c [u8],
162 pub content_encoding: HeaderValue,
164}
165impl<'c> ContentContentEncoding<'c> {
166 pub fn resolve(
169 encoding_accepted: &EncodingAccepted,
170 file: &'c impl File,
171 ) -> Self {
172 let mut best = Cell::new(ContentContentEncoding {
173 content: file.content(),
174 content_encoding: HeaderValue::from_static("identity"),
175 });
176
177 if encoding_accepted.gzip
179 && let Some(content_gzip) = file.content_gzip()
180 && content_gzip.len() <= best.get_mut().content.len()
181 {
182 best.set(ContentContentEncoding {
183 content: content_gzip,
184 content_encoding: HeaderValue::from_static("gzip"),
185 });
186 }
187
188 if encoding_accepted.brotli
190 && let Some(content_brotli) = file.content_brotli()
191 && content_brotli.len() <= best.get_mut().content.len()
192 {
193 best.set(ContentContentEncoding {
194 content: content_brotli,
195 content_encoding: HeaderValue::from_static("br"),
196 });
197 }
198
199 best.into_inner()
200 }
201}
202
203#[cfg(test)]
204mod test_content_content_encoding {
205 use super::{ContentContentEncoding, EncodingAccepted};
206 use crate::{cache_control::CacheControl, file::File};
207 use http::HeaderValue;
208 use test_case::test_case;
209
210 #[derive(Debug)]
211 pub struct FileMock {
212 pub content: &'static [u8],
213 pub content_gzip: Option<&'static [u8]>,
214 pub content_brotli: Option<&'static [u8]>,
215 }
216 impl File for FileMock {
217 fn content(&self) -> &[u8] {
218 self.content
219 }
220 fn content_gzip(&self) -> Option<&[u8]> {
221 self.content_gzip
222 }
223 fn content_brotli(&self) -> Option<&[u8]> {
224 self.content_brotli
225 }
226
227 fn content_type(&self) -> HeaderValue {
228 unimplemented!()
229 }
230
231 fn etag(&self) -> HeaderValue {
232 unimplemented!()
233 }
234
235 fn cache_control(&self) -> CacheControl {
236 unimplemented!()
237 }
238 }
239
240 #[test_case(
241 EncodingAccepted { gzip: false, brotli: false },
242 FileMock { content: b"content-identity", content_gzip: None, content_brotli: None },
243 ContentContentEncoding {content: b"content-identity", content_encoding: HeaderValue::from_static("identity") } ;
244 "nothing provided, nothing accepted"
245 )]
246 #[test_case(
247 EncodingAccepted { gzip: false, brotli: false },
248 FileMock { content: b"content-identity", content_gzip: Some(b"content-gzip"), content_brotli: Some(b"content-brotli") },
249 ContentContentEncoding {content: b"content-identity", content_encoding: HeaderValue::from_static("identity") } ;
250 "all provided, nothing accepted"
251 )]
252 #[test_case(
253 EncodingAccepted { gzip: true, brotli: true },
254 FileMock { content: b"content-identity", content_gzip: None, content_brotli: None },
255 ContentContentEncoding {content: b"content-identity", content_encoding: HeaderValue::from_static("identity") } ;
256 "all accepted, nothing provided"
257 )]
258 #[test_case(
259 EncodingAccepted { gzip: true, brotli: true },
260 FileMock { content: b"content-aaa", content_gzip: Some(b"content-bb"), content_brotli: Some(b"content-c") },
261 ContentContentEncoding {content: b"content-c", content_encoding: HeaderValue::from_static("br") } ;
262 "brotli should win as the shortest"
263 )]
264 fn resolve_returns_expected(
265 encoding_accepted: EncodingAccepted,
266 content: FileMock,
267 expected: ContentContentEncoding,
268 ) {
269 assert_eq!(
270 ContentContentEncoding::resolve(&encoding_accepted, &content),
271 expected
272 );
273 }
274}