Skip to main content

rama_http_headers/encoding/
mod.rs

1//! Utility types and functions that can be used in context of encoding headers.
2
3use rama_utils::macros::match_ignore_ascii_case_str;
4use std::fmt;
5
6mod accept_encoding;
7pub use accept_encoding::AcceptEncoding;
8
9use super::specifier::{Quality, QualityValue};
10
11pub trait SupportedEncodings: Copy {
12    fn gzip(&self) -> bool;
13    fn deflate(&self) -> bool;
14    fn br(&self) -> bool;
15    fn zstd(&self) -> bool;
16}
17
18impl SupportedEncodings for bool {
19    fn gzip(&self) -> bool {
20        *self
21    }
22
23    fn deflate(&self) -> bool {
24        *self
25    }
26
27    fn br(&self) -> bool {
28        *self
29    }
30
31    fn zstd(&self) -> bool {
32        *self
33    }
34}
35
36#[derive(Copy, Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Hash)]
37/// This enum's variants are ordered from least to most preferred.
38pub enum Encoding {
39    Identity,
40    Deflate,
41    Gzip,
42    Brotli,
43    Zstd,
44}
45
46impl fmt::Display for Encoding {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(f, "{}", self.as_str())
49    }
50}
51
52impl From<Encoding> for rama_http_types::HeaderValue {
53    #[inline]
54    fn from(encoding: Encoding) -> Self {
55        Self::from_static(encoding.as_str())
56    }
57}
58
59impl Encoding {
60    fn as_str(self) -> &'static str {
61        match self {
62            Self::Identity => "identity",
63            Self::Gzip => "gzip",
64            Self::Deflate => "deflate",
65            Self::Brotli => "br",
66            Self::Zstd => "zstd",
67        }
68    }
69
70    #[must_use]
71    pub fn to_file_extension(self) -> Option<&'static std::ffi::OsStr> {
72        match self {
73            Self::Gzip => Some(std::ffi::OsStr::new(".gz")),
74            Self::Deflate => Some(std::ffi::OsStr::new(".zz")),
75            Self::Brotli => Some(std::ffi::OsStr::new(".br")),
76            Self::Zstd => Some(std::ffi::OsStr::new(".zst")),
77            Self::Identity => None,
78        }
79    }
80
81    fn parse(s: &str, supported_encoding: impl SupportedEncodings) -> Option<Self> {
82        match_ignore_ascii_case_str! {
83            match (s) {
84                "gzip" | "x-gzip" if supported_encoding.gzip() => Some(Self::Gzip),
85                "deflate" if supported_encoding.deflate() => Some(Self::Deflate),
86                "br" if supported_encoding.br() => Some(Self::Brotli),
87                "zstd" if supported_encoding.zstd() => Some(Self::Zstd),
88                "identity" => Some(Self::Identity),
89                _ => None,
90            }
91        }
92    }
93
94    pub fn maybe_from_content_encoding_directive(
95        encoding: &crate::ContentEncodingDirective,
96        supported_encoding: impl SupportedEncodings,
97    ) -> Option<Self> {
98        match encoding {
99            crate::ContentEncodingDirective::Gzip if supported_encoding.gzip() => Some(Self::Gzip),
100            crate::ContentEncodingDirective::Deflate if supported_encoding.deflate() => {
101                Some(Self::Deflate)
102            }
103            crate::ContentEncodingDirective::Brotli if supported_encoding.br() => {
104                Some(Self::Brotli)
105            }
106            crate::ContentEncodingDirective::ZStandard if supported_encoding.zstd() => {
107                Some(Self::Zstd)
108            }
109            _ => None,
110        }
111    }
112
113    pub fn maybe_from_content_encoding_header(
114        headers: &rama_http_types::HeaderMap,
115        supported_encoding: impl SupportedEncodings,
116    ) -> Option<Self> {
117        headers
118            .get(rama_http_types::header::CONTENT_ENCODING)
119            .and_then(|hval| hval.to_str().ok())
120            .and_then(|s| Self::parse(s, supported_encoding))
121    }
122
123    #[inline]
124    pub fn from_content_encoding_header(
125        headers: &rama_http_types::HeaderMap,
126        supported_encoding: impl SupportedEncodings,
127    ) -> Self {
128        Self::maybe_from_content_encoding_header(headers, supported_encoding)
129            .unwrap_or(Self::Identity)
130    }
131
132    pub fn maybe_from_accept_encoding_headers(
133        headers: &rama_http_types::HeaderMap,
134        supported_encoding: impl SupportedEncodings,
135    ) -> Option<Self> {
136        Self::maybe_preferred_encoding(parse_accept_encoding_headers(headers, supported_encoding))
137    }
138
139    #[inline]
140    pub fn from_accept_encoding_headers(
141        headers: &rama_http_types::HeaderMap,
142        supported_encoding: impl SupportedEncodings,
143    ) -> Self {
144        Self::maybe_from_accept_encoding_headers(headers, supported_encoding)
145            .unwrap_or(Self::Identity)
146    }
147
148    pub fn maybe_preferred_encoding(
149        accepted_encodings: impl Iterator<Item = QualityValue<Self>>,
150    ) -> Option<Self> {
151        accepted_encodings
152            .filter(|qval| qval.quality.as_u16() > 0)
153            .max_by_key(|qval| (qval.quality, qval.value))
154            .map(|qval| qval.value)
155    }
156}
157
158/// based on <https://github.com/http-rs/accept-encoding>
159pub fn parse_accept_encoding_headers<'a>(
160    headers: &'a rama_http_types::HeaderMap,
161    supported_encoding: impl SupportedEncodings + 'a,
162) -> impl Iterator<Item = QualityValue<Encoding>> + 'a {
163    headers
164        .get_all(rama_http_types::header::ACCEPT_ENCODING)
165        .iter()
166        .filter_map(|hval| hval.to_str().ok())
167        .flat_map(|s| s.split(','))
168        .filter_map(move |v| {
169            let mut v = v.splitn(2, ';');
170
171            // ignore unknown encodings
172            let encoding = Encoding::parse(v.next()?.trim(), supported_encoding)?;
173
174            let qval = if let Some(qval) = v.next() {
175                qval.trim().parse::<Quality>().ok()?
176            } else {
177                Quality::one()
178            };
179
180            Some(QualityValue::new(encoding, qval))
181        })
182}
183
184/// Extracts the quality value of the `*` wildcard from `Accept-Encoding` headers.
185///
186/// Returns `None` if no wildcard is present. A bare `*` (without a `q=` part) defaults
187/// to a quality of `1`.
188pub fn parse_accept_encoding_wildcard_quality(
189    headers: &rama_http_types::HeaderMap,
190) -> Option<Quality> {
191    headers
192        .get_all(rama_http_types::header::ACCEPT_ENCODING)
193        .iter()
194        .filter_map(|hval| hval.to_str().ok())
195        .flat_map(|s| s.split(','))
196        .find_map(|v| {
197            let mut v = v.splitn(2, ';');
198            if v.next()?.trim() != "*" {
199                return None;
200            }
201            Some(match v.next() {
202                Some(qval) => qval.trim().parse::<Quality>().ok()?,
203                None => Quality::one(),
204            })
205        })
206}
207
208/// Selects the preferred encoding while honoring the `*` wildcard per RFC 9110 §12.5.3.
209///
210/// `accepted` is the list of explicitly listed (and supported) encodings with their quality,
211/// as produced by [`parse_accept_encoding_headers`]. `wildcard_quality` is the quality of the
212/// `*` wildcard if present (see [`parse_accept_encoding_wildcard_quality`]). The wildcard
213/// quality applies to every supported encoding not explicitly listed.
214///
215/// Returns `Some(encoding)` (possibly [`Encoding::Identity`]) for the best acceptable encoding,
216/// or `None` when the client rejects every available representation (e.g. `*;q=0` or a lone
217/// `identity;q=0`). A `None` result signals that the server should respond with
218/// `406 Not Acceptable`.
219pub fn maybe_preferred_encoding_with_wildcard(
220    accepted: &[QualityValue<Encoding>],
221    wildcard_quality: Option<Quality>,
222    supported: impl SupportedEncodings,
223) -> Option<Encoding> {
224    let Some(wildcard_quality) = wildcard_quality else {
225        // No wildcard: use only the explicitly listed encodings. Per RFC 9110 §12.5.3, if
226        // identity is excluded (q=0) and nothing else is acceptable, the server should 406.
227        let identity_rejected = accepted
228            .iter()
229            .any(|qval| qval.value == Encoding::Identity && qval.quality.as_u16() == 0);
230        return match Encoding::maybe_preferred_encoding(accepted.iter().copied()) {
231            Some(enc) => Some(enc),
232            None if identity_rejected => None,
233            None => Some(Encoding::Identity),
234        };
235    };
236
237    // Wildcard present: build the effective (encoding, quality) set over every supported
238    // encoding, using the explicit quality where listed and the wildcard quality otherwise.
239    let effective = all_supported_encodings(supported)
240        .into_iter()
241        .flatten()
242        .map(|enc| {
243            let quality = accepted
244                .iter()
245                .find(|qval| qval.value == enc)
246                .map_or(wildcard_quality, |qval| qval.quality);
247            QualityValue::new(enc, quality)
248        });
249    Encoding::maybe_preferred_encoding(effective)
250}
251
252/// Returns every encoding the server supports (always including [`Encoding::Identity`]).
253fn all_supported_encodings(supported: impl SupportedEncodings) -> [Option<Encoding>; 5] {
254    [
255        Some(Encoding::Identity),
256        supported.gzip().then_some(Encoding::Gzip),
257        supported.deflate().then_some(Encoding::Deflate),
258        supported.br().then_some(Encoding::Brotli),
259        supported.zstd().then_some(Encoding::Zstd),
260    ]
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[derive(Copy, Clone, Default)]
268    struct SupportedEncodingsAll;
269
270    impl SupportedEncodings for SupportedEncodingsAll {
271        fn gzip(&self) -> bool {
272            true
273        }
274
275        fn deflate(&self) -> bool {
276            true
277        }
278
279        fn br(&self) -> bool {
280            true
281        }
282
283        fn zstd(&self) -> bool {
284            true
285        }
286    }
287
288    #[test]
289    fn no_accept_encoding_header() {
290        let encoding = Encoding::from_accept_encoding_headers(
291            &rama_http_types::HeaderMap::new(),
292            SupportedEncodingsAll,
293        );
294        assert_eq!(Encoding::Identity, encoding);
295    }
296
297    #[test]
298    fn accept_encoding_header_single_encoding() {
299        let mut headers = rama_http_types::HeaderMap::new();
300        headers.append(
301            rama_http_types::header::ACCEPT_ENCODING,
302            rama_http_types::HeaderValue::from_static("gzip"),
303        );
304        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
305        assert_eq!(Encoding::Gzip, encoding);
306    }
307
308    #[test]
309    fn accept_encoding_header_two_encodings() {
310        let mut headers = rama_http_types::HeaderMap::new();
311        headers.append(
312            rama_http_types::header::ACCEPT_ENCODING,
313            rama_http_types::HeaderValue::from_static("gzip,br"),
314        );
315        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
316        assert_eq!(Encoding::Brotli, encoding);
317    }
318
319    #[test]
320    fn accept_encoding_header_gzip_x_gzip() {
321        let mut headers = rama_http_types::HeaderMap::new();
322        headers.append(
323            rama_http_types::header::ACCEPT_ENCODING,
324            rama_http_types::HeaderValue::from_static("gzip,x-gzip"),
325        );
326        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
327        assert_eq!(Encoding::Gzip, encoding);
328    }
329
330    #[test]
331    fn accept_encoding_header_x_gzip_deflate() {
332        let mut headers = rama_http_types::HeaderMap::new();
333        headers.append(
334            rama_http_types::header::ACCEPT_ENCODING,
335            rama_http_types::HeaderValue::from_static("deflate,x-gzip"),
336        );
337        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
338        assert_eq!(Encoding::Gzip, encoding);
339    }
340
341    #[test]
342    fn accept_encoding_header_three_encodings() {
343        let mut headers = rama_http_types::HeaderMap::new();
344        headers.append(
345            rama_http_types::header::ACCEPT_ENCODING,
346            rama_http_types::HeaderValue::from_static("gzip,deflate,br"),
347        );
348        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
349        assert_eq!(Encoding::Brotli, encoding);
350    }
351
352    #[test]
353    fn accept_encoding_header_two_encodings_with_one_qvalue() {
354        let mut headers = rama_http_types::HeaderMap::new();
355        headers.append(
356            rama_http_types::header::ACCEPT_ENCODING,
357            rama_http_types::HeaderValue::from_static("gzip;q=0.5,br"),
358        );
359        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
360        assert_eq!(Encoding::Brotli, encoding);
361    }
362
363    #[test]
364    fn accept_encoding_header_three_encodings_with_one_qvalue() {
365        let mut headers = rama_http_types::HeaderMap::new();
366        headers.append(
367            rama_http_types::header::ACCEPT_ENCODING,
368            rama_http_types::HeaderValue::from_static("gzip;q=0.5,deflate,br"),
369        );
370        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
371        assert_eq!(Encoding::Brotli, encoding);
372    }
373
374    #[test]
375    fn two_accept_encoding_headers_with_one_qvalue() {
376        let mut headers = rama_http_types::HeaderMap::new();
377        headers.append(
378            rama_http_types::header::ACCEPT_ENCODING,
379            rama_http_types::HeaderValue::from_static("gzip;q=0.5"),
380        );
381        headers.append(
382            rama_http_types::header::ACCEPT_ENCODING,
383            rama_http_types::HeaderValue::from_static("br"),
384        );
385        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
386        assert_eq!(Encoding::Brotli, encoding);
387    }
388
389    #[test]
390    fn two_accept_encoding_headers_three_encodings_with_one_qvalue() {
391        let mut headers = rama_http_types::HeaderMap::new();
392        headers.append(
393            rama_http_types::header::ACCEPT_ENCODING,
394            rama_http_types::HeaderValue::from_static("gzip;q=0.5,deflate"),
395        );
396        headers.append(
397            rama_http_types::header::ACCEPT_ENCODING,
398            rama_http_types::HeaderValue::from_static("br"),
399        );
400        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
401        assert_eq!(Encoding::Brotli, encoding);
402    }
403
404    #[test]
405    fn three_accept_encoding_headers_with_one_qvalue() {
406        let mut headers = rama_http_types::HeaderMap::new();
407        headers.append(
408            rama_http_types::header::ACCEPT_ENCODING,
409            rama_http_types::HeaderValue::from_static("gzip;q=0.5"),
410        );
411        headers.append(
412            rama_http_types::header::ACCEPT_ENCODING,
413            rama_http_types::HeaderValue::from_static("deflate"),
414        );
415        headers.append(
416            rama_http_types::header::ACCEPT_ENCODING,
417            rama_http_types::HeaderValue::from_static("br"),
418        );
419        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
420        assert_eq!(Encoding::Brotli, encoding);
421    }
422
423    #[test]
424    fn accept_encoding_header_two_encodings_with_two_qvalues() {
425        let mut headers = rama_http_types::HeaderMap::new();
426        headers.append(
427            rama_http_types::header::ACCEPT_ENCODING,
428            rama_http_types::HeaderValue::from_static("gzip;q=0.5,br;q=0.8"),
429        );
430        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
431        assert_eq!(Encoding::Brotli, encoding);
432
433        let mut headers = rama_http_types::HeaderMap::new();
434        headers.append(
435            rama_http_types::header::ACCEPT_ENCODING,
436            rama_http_types::HeaderValue::from_static("gzip;q=0.8,br;q=0.5"),
437        );
438        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
439        assert_eq!(Encoding::Gzip, encoding);
440
441        let mut headers = rama_http_types::HeaderMap::new();
442        headers.append(
443            rama_http_types::header::ACCEPT_ENCODING,
444            rama_http_types::HeaderValue::from_static("gzip;q=0.995,br;q=0.999"),
445        );
446        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
447        assert_eq!(Encoding::Brotli, encoding);
448    }
449
450    #[test]
451    fn accept_encoding_header_three_encodings_with_three_qvalues() {
452        let mut headers = rama_http_types::HeaderMap::new();
453        headers.append(
454            rama_http_types::header::ACCEPT_ENCODING,
455            rama_http_types::HeaderValue::from_static("gzip;q=0.5,deflate;q=0.6,br;q=0.8"),
456        );
457        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
458        assert_eq!(Encoding::Brotli, encoding);
459
460        let mut headers = rama_http_types::HeaderMap::new();
461        headers.append(
462            rama_http_types::header::ACCEPT_ENCODING,
463            rama_http_types::HeaderValue::from_static("gzip;q=0.8,deflate;q=0.6,br;q=0.5"),
464        );
465        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
466        assert_eq!(Encoding::Gzip, encoding);
467
468        let mut headers = rama_http_types::HeaderMap::new();
469        headers.append(
470            rama_http_types::header::ACCEPT_ENCODING,
471            rama_http_types::HeaderValue::from_static("gzip;q=0.6,deflate;q=0.8,br;q=0.5"),
472        );
473        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
474        assert_eq!(Encoding::Deflate, encoding);
475
476        let mut headers = rama_http_types::HeaderMap::new();
477        headers.append(
478            rama_http_types::header::ACCEPT_ENCODING,
479            rama_http_types::HeaderValue::from_static("gzip;q=0.995,deflate;q=0.997,br;q=0.999"),
480        );
481        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
482        assert_eq!(Encoding::Brotli, encoding);
483    }
484
485    #[test]
486    fn accept_encoding_header_invalid_encdoing() {
487        let mut headers = rama_http_types::HeaderMap::new();
488        headers.append(
489            rama_http_types::header::ACCEPT_ENCODING,
490            rama_http_types::HeaderValue::from_static("invalid,gzip"),
491        );
492        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
493        assert_eq!(Encoding::Gzip, encoding);
494    }
495
496    #[test]
497    fn accept_encoding_header_with_qvalue_zero() {
498        let mut headers = rama_http_types::HeaderMap::new();
499        headers.append(
500            rama_http_types::header::ACCEPT_ENCODING,
501            rama_http_types::HeaderValue::from_static("gzip;q=0"),
502        );
503        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
504        assert_eq!(Encoding::Identity, encoding);
505
506        let mut headers = rama_http_types::HeaderMap::new();
507        headers.append(
508            rama_http_types::header::ACCEPT_ENCODING,
509            rama_http_types::HeaderValue::from_static("gzip;q=0."),
510        );
511        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
512        assert_eq!(Encoding::Identity, encoding);
513
514        let mut headers = rama_http_types::HeaderMap::new();
515        headers.append(
516            rama_http_types::header::ACCEPT_ENCODING,
517            rama_http_types::HeaderValue::from_static("gzip;q=0,br;q=0.5"),
518        );
519        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
520        assert_eq!(Encoding::Brotli, encoding);
521    }
522
523    #[test]
524    fn accept_encoding_header_with_uppercase_letters() {
525        let mut headers = rama_http_types::HeaderMap::new();
526        headers.append(
527            rama_http_types::header::ACCEPT_ENCODING,
528            rama_http_types::HeaderValue::from_static("gZiP"),
529        );
530        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
531        assert_eq!(Encoding::Gzip, encoding);
532
533        let mut headers = rama_http_types::HeaderMap::new();
534        headers.append(
535            rama_http_types::header::ACCEPT_ENCODING,
536            rama_http_types::HeaderValue::from_static("gzip;q=0.5,br;Q=0.8"),
537        );
538        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
539        assert_eq!(Encoding::Brotli, encoding);
540    }
541
542    #[test]
543    fn accept_encoding_header_with_allowed_spaces() {
544        let mut headers = rama_http_types::HeaderMap::new();
545        headers.append(
546            rama_http_types::header::ACCEPT_ENCODING,
547            rama_http_types::HeaderValue::from_static(" gzip\t; q=0.5 ,\tbr ;\tq=0.8\t"),
548        );
549        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
550        assert_eq!(Encoding::Brotli, encoding);
551    }
552
553    #[test]
554    fn accept_encoding_header_with_invalid_spaces() {
555        let mut headers = rama_http_types::HeaderMap::new();
556        headers.append(
557            rama_http_types::header::ACCEPT_ENCODING,
558            rama_http_types::HeaderValue::from_static("gzip;q =0.5"),
559        );
560        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
561        assert_eq!(Encoding::Identity, encoding);
562
563        let mut headers = rama_http_types::HeaderMap::new();
564        headers.append(
565            rama_http_types::header::ACCEPT_ENCODING,
566            rama_http_types::HeaderValue::from_static("gzip;q= 0.5"),
567        );
568        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
569        assert_eq!(Encoding::Identity, encoding);
570    }
571
572    #[test]
573    fn accept_encoding_header_with_invalid_quvalues() {
574        let mut headers = rama_http_types::HeaderMap::new();
575        headers.append(
576            rama_http_types::header::ACCEPT_ENCODING,
577            rama_http_types::HeaderValue::from_static("gzip;q=-0.1"),
578        );
579        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
580        assert_eq!(Encoding::Identity, encoding);
581
582        let mut headers = rama_http_types::HeaderMap::new();
583        headers.append(
584            rama_http_types::header::ACCEPT_ENCODING,
585            rama_http_types::HeaderValue::from_static("gzip;q=00.5"),
586        );
587        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
588        assert_eq!(Encoding::Identity, encoding);
589
590        let mut headers = rama_http_types::HeaderMap::new();
591        headers.append(
592            rama_http_types::header::ACCEPT_ENCODING,
593            rama_http_types::HeaderValue::from_static("gzip;q=0.5000"),
594        );
595        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
596        assert_eq!(Encoding::Identity, encoding);
597
598        let mut headers = rama_http_types::HeaderMap::new();
599        headers.append(
600            rama_http_types::header::ACCEPT_ENCODING,
601            rama_http_types::HeaderValue::from_static("gzip;q=.5"),
602        );
603        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
604        assert_eq!(Encoding::Identity, encoding);
605
606        let mut headers = rama_http_types::HeaderMap::new();
607        headers.append(
608            rama_http_types::header::ACCEPT_ENCODING,
609            rama_http_types::HeaderValue::from_static("gzip;q=1.01"),
610        );
611        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
612        assert_eq!(Encoding::Identity, encoding);
613
614        let mut headers = rama_http_types::HeaderMap::new();
615        headers.append(
616            rama_http_types::header::ACCEPT_ENCODING,
617            rama_http_types::HeaderValue::from_static("gzip;q=1.001"),
618        );
619        let encoding = Encoding::from_accept_encoding_headers(&headers, SupportedEncodingsAll);
620        assert_eq!(Encoding::Identity, encoding);
621    }
622
623    #[derive(Copy, Clone)]
624    struct SupportedGzipOnly;
625
626    impl SupportedEncodings for SupportedGzipOnly {
627        fn gzip(&self) -> bool {
628            true
629        }
630        fn deflate(&self) -> bool {
631            false
632        }
633        fn br(&self) -> bool {
634            false
635        }
636        fn zstd(&self) -> bool {
637            false
638        }
639    }
640
641    fn negotiate(
642        headers: &rama_http_types::HeaderMap,
643        supported: impl SupportedEncodings,
644    ) -> Option<Encoding> {
645        let accepted: Vec<_> = parse_accept_encoding_headers(headers, supported).collect();
646        let wildcard = parse_accept_encoding_wildcard_quality(headers);
647        maybe_preferred_encoding_with_wildcard(&accepted, wildcard, supported)
648    }
649
650    fn accept_encoding(value: &'static str) -> rama_http_types::HeaderMap {
651        let mut headers = rama_http_types::HeaderMap::new();
652        headers.append(
653            rama_http_types::header::ACCEPT_ENCODING,
654            rama_http_types::HeaderValue::from_static(value),
655        );
656        headers
657    }
658
659    #[test]
660    fn wildcard_alone_picks_best_supported() {
661        let headers = accept_encoding("*");
662        // `*` with q=1 means all encodings are acceptable; pick the highest-priority supported.
663        assert_eq!(
664            Some(Encoding::Zstd),
665            negotiate(&headers, SupportedEncodingsAll)
666        );
667    }
668
669    #[test]
670    fn wildcard_q_zero_with_nothing_else_returns_not_satisfiable() {
671        // `*;q=0` rejects everything, including identity.
672        let headers = accept_encoding("*;q=0");
673        assert_eq!(None, negotiate(&headers, SupportedEncodingsAll));
674    }
675
676    #[test]
677    fn wildcard_q_zero_with_gzip_picks_gzip() {
678        let headers = accept_encoding("*;q=0,gzip");
679        assert_eq!(
680            Some(Encoding::Gzip),
681            negotiate(&headers, SupportedEncodingsAll)
682        );
683    }
684
685    #[test]
686    fn identity_q_zero_alone_returns_not_satisfiable() {
687        // `identity;q=0` with no other encoding explicitly listed: the server cannot
688        // determine what the client accepts, so 406 per RFC 9110 §12.5.3.
689        let headers = accept_encoding("identity;q=0");
690        assert_eq!(None, negotiate(&headers, SupportedEncodingsAll));
691    }
692
693    #[test]
694    fn identity_q_zero_with_gzip_picks_gzip() {
695        let headers = accept_encoding("identity;q=0,gzip");
696        assert_eq!(
697            Some(Encoding::Gzip),
698            negotiate(&headers, SupportedEncodingsAll)
699        );
700    }
701
702    #[test]
703    fn wildcard_q_zero_identity_q_zero_no_compression_returns_not_satisfiable() {
704        // Both wildcard and identity are q=0, and no explicit encoding is listed with q>0.
705        let headers = accept_encoding("*;q=0,identity;q=0");
706        assert_eq!(None, negotiate(&headers, SupportedEncodingsAll));
707    }
708
709    #[test]
710    fn wildcard_with_low_qvalue() {
711        // gzip is explicitly q=1, everything else gets q=0.5 from the wildcard.
712        let headers = accept_encoding("*;q=0.5,gzip;q=1");
713        assert_eq!(
714            Some(Encoding::Gzip),
715            negotiate(&headers, SupportedEncodingsAll)
716        );
717    }
718
719    #[test]
720    fn wildcard_q_zero_with_identity_picks_identity() {
721        // `*;q=0` rejects all, but identity is explicitly listed with q=1.
722        let headers = accept_encoding("*;q=0,identity");
723        assert_eq!(
724            Some(Encoding::Identity),
725            negotiate(&headers, SupportedEncodingsAll)
726        );
727    }
728
729    #[test]
730    fn wildcard_with_partial_server_support_picks_best_available() {
731        // Server only supports gzip, so `*` should pick gzip (not zstd/br).
732        let headers = accept_encoding("*");
733        assert_eq!(Some(Encoding::Gzip), negotiate(&headers, SupportedGzipOnly));
734    }
735
736    #[test]
737    fn wildcard_q_zero_with_unsupported_encoding_returns_not_satisfiable() {
738        // Client wants br, but server only supports gzip. br is not in the supported set so
739        // it's ignored; the wildcard rejects everything else. Result: 406.
740        let headers = accept_encoding("*;q=0,br");
741        assert_eq!(None, negotiate(&headers, SupportedGzipOnly));
742    }
743}