Skip to main content

tower_http/
content_encoding.rs

1pub(crate) trait SupportedEncodings: Copy {
2    fn gzip(&self) -> bool;
3    fn deflate(&self) -> bool;
4    fn br(&self) -> bool;
5    fn zstd(&self) -> bool;
6}
7
8// This enum's variants are ordered from least to most preferred.
9#[derive(Copy, Clone, Debug, Ord, PartialOrd, PartialEq, Eq)]
10pub(crate) enum Encoding {
11    #[allow(dead_code)]
12    Identity,
13    #[cfg(any(feature = "fs", feature = "compression-deflate"))]
14    Deflate,
15    #[cfg(any(feature = "fs", feature = "compression-gzip"))]
16    Gzip,
17    #[cfg(any(feature = "fs", feature = "compression-br"))]
18    Brotli,
19    #[cfg(any(feature = "fs", feature = "compression-zstd"))]
20    Zstd,
21}
22
23impl Encoding {
24    #[allow(dead_code)]
25    fn to_str(self) -> &'static str {
26        match self {
27            #[cfg(any(feature = "fs", feature = "compression-gzip"))]
28            Encoding::Gzip => "gzip",
29            #[cfg(any(feature = "fs", feature = "compression-deflate"))]
30            Encoding::Deflate => "deflate",
31            #[cfg(any(feature = "fs", feature = "compression-br"))]
32            Encoding::Brotli => "br",
33            #[cfg(any(feature = "fs", feature = "compression-zstd"))]
34            Encoding::Zstd => "zstd",
35            Encoding::Identity => "identity",
36        }
37    }
38
39    #[cfg(feature = "fs")]
40    pub(crate) fn to_file_extension(self) -> Option<&'static std::ffi::OsStr> {
41        match self {
42            Encoding::Gzip => Some(std::ffi::OsStr::new(".gz")),
43            Encoding::Deflate => Some(std::ffi::OsStr::new(".zz")),
44            Encoding::Brotli => Some(std::ffi::OsStr::new(".br")),
45            Encoding::Zstd => Some(std::ffi::OsStr::new(".zst")),
46            Encoding::Identity => None,
47        }
48    }
49
50    #[allow(dead_code)]
51    pub(crate) fn into_header_value(self) -> http::HeaderValue {
52        http::HeaderValue::from_static(self.to_str())
53    }
54
55    #[cfg(any(
56        feature = "compression-gzip",
57        feature = "compression-br",
58        feature = "compression-deflate",
59        feature = "compression-zstd",
60        feature = "fs",
61    ))]
62    fn parse(s: &str, _supported_encoding: impl SupportedEncodings) -> Option<Encoding> {
63        #[cfg(any(feature = "fs", feature = "compression-gzip"))]
64        if (s.eq_ignore_ascii_case("gzip") || s.eq_ignore_ascii_case("x-gzip"))
65            && _supported_encoding.gzip()
66        {
67            return Some(Encoding::Gzip);
68        }
69
70        #[cfg(any(feature = "fs", feature = "compression-deflate"))]
71        if s.eq_ignore_ascii_case("deflate") && _supported_encoding.deflate() {
72            return Some(Encoding::Deflate);
73        }
74
75        #[cfg(any(feature = "fs", feature = "compression-br"))]
76        if s.eq_ignore_ascii_case("br") && _supported_encoding.br() {
77            return Some(Encoding::Brotli);
78        }
79
80        #[cfg(any(feature = "fs", feature = "compression-zstd"))]
81        if s.eq_ignore_ascii_case("zstd") && _supported_encoding.zstd() {
82            return Some(Encoding::Zstd);
83        }
84
85        if s.eq_ignore_ascii_case("identity") {
86            return Some(Encoding::Identity);
87        }
88
89        None
90    }
91
92    #[cfg(any(
93        feature = "compression-gzip",
94        feature = "compression-br",
95        feature = "compression-zstd",
96        feature = "compression-deflate",
97    ))]
98    // based on https://github.com/http-rs/accept-encoding
99    //
100    // Returns `Some(encoding)` for the best acceptable encoding, or `None` if the client's
101    // preferences cannot be satisfied (406 Not Acceptable per RFC 9110 §12.5.3).
102    pub(crate) fn from_headers(
103        headers: &http::HeaderMap,
104        supported_encoding: impl SupportedEncodings,
105    ) -> Option<Self> {
106        preferred_encoding_with_wildcard(headers, supported_encoding)
107    }
108
109    #[cfg(any(
110        feature = "compression-gzip",
111        feature = "compression-br",
112        feature = "compression-zstd",
113        feature = "compression-deflate",
114        feature = "fs",
115    ))]
116    pub(crate) fn preferred_encoding(
117        accepted_encodings: impl Iterator<Item = (Encoding, QValue)>,
118    ) -> Option<Self> {
119        accepted_encodings
120            .filter(|(_, qvalue)| qvalue.0 > 0)
121            .max_by_key(|&(encoding, qvalue)| (qvalue, encoding))
122            .map(|(encoding, _)| encoding)
123    }
124}
125
126// Allowed q-values are numbers between 0 and 1 with at most 3 digits in the fractional part. They
127// are presented here as an unsigned integer between 0 and 1000.
128#[cfg(any(
129    feature = "compression-gzip",
130    feature = "compression-br",
131    feature = "compression-zstd",
132    feature = "compression-deflate",
133    feature = "fs",
134))]
135#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
136pub(crate) struct QValue(u16);
137
138#[cfg(any(
139    feature = "compression-gzip",
140    feature = "compression-br",
141    feature = "compression-zstd",
142    feature = "compression-deflate",
143    feature = "fs",
144))]
145impl QValue {
146    #[inline]
147    pub(crate) fn one() -> Self {
148        Self(1000)
149    }
150
151    // Parse a q-value as specified in RFC 7231 section 5.3.1.
152    fn parse(s: &str) -> Option<Self> {
153        let mut c = s.chars();
154        // Parse "q=" (case-insensitively).
155        match c.next() {
156            Some('q' | 'Q') => (),
157            _ => return None,
158        };
159        match c.next() {
160            Some('=') => (),
161            _ => return None,
162        };
163
164        // Parse leading digit. Since valid q-values are between 0.000 and 1.000, only "0" and "1"
165        // are allowed.
166        let mut value = match c.next() {
167            Some('0') => 0,
168            Some('1') => 1000,
169            _ => return None,
170        };
171
172        // Parse optional decimal point.
173        match c.next() {
174            Some('.') => (),
175            None => return Some(Self(value)),
176            _ => return None,
177        };
178
179        // Parse optional fractional digits. The value of each digit is multiplied by `factor`.
180        // Since the q-value is represented as an integer between 0 and 1000, `factor` is `100` for
181        // the first digit, `10` for the next, and `1` for the digit after that.
182        let mut factor = 100;
183        loop {
184            match c.next() {
185                Some(n @ '0'..='9') => {
186                    // If `factor` is less than `1`, three digits have already been parsed. A
187                    // q-value having more than 3 fractional digits is invalid.
188                    if factor < 1 {
189                        return None;
190                    }
191                    // Add the digit's value multiplied by `factor` to `value`.
192                    value += factor * (n as u16 - '0' as u16);
193                }
194                None => {
195                    // No more characters to parse. Check that the value representing the q-value is
196                    // in the valid range.
197                    return if value <= 1000 {
198                        Some(Self(value))
199                    } else {
200                        None
201                    };
202                }
203                _ => return None,
204            };
205            factor /= 10;
206        }
207    }
208}
209
210#[cfg(any(
211    feature = "compression-gzip",
212    feature = "compression-br",
213    feature = "compression-zstd",
214    feature = "compression-deflate",
215    feature = "fs",
216))]
217// based on https://github.com/http-rs/accept-encoding
218pub(crate) fn encodings<'a>(
219    headers: &'a http::HeaderMap,
220    supported_encoding: impl SupportedEncodings + 'a,
221) -> impl Iterator<Item = (Encoding, QValue)> + 'a {
222    headers
223        .get_all(http::header::ACCEPT_ENCODING)
224        .iter()
225        .filter_map(|hval| hval.to_str().ok())
226        .flat_map(|s| s.split(','))
227        .filter_map(move |v| {
228            let mut v = v.splitn(2, ';');
229
230            let encoding = match Encoding::parse(v.next().unwrap().trim(), supported_encoding) {
231                Some(encoding) => encoding,
232                None => return None, // ignore unknown encodings
233            };
234
235            let qval = if let Some(qval) = v.next() {
236                QValue::parse(qval.trim())?
237            } else {
238                QValue::one()
239            };
240
241            Some((encoding, qval))
242        })
243}
244
245/// Extracts the q-value for the `*` wildcard from Accept-Encoding headers.
246/// Returns `None` if no wildcard is present.
247#[cfg(any(
248    feature = "compression-gzip",
249    feature = "compression-br",
250    feature = "compression-zstd",
251    feature = "compression-deflate",
252))]
253fn wildcard_qvalue(headers: &http::HeaderMap) -> Option<QValue> {
254    headers
255        .get_all(http::header::ACCEPT_ENCODING)
256        .iter()
257        .filter_map(|hval| hval.to_str().ok())
258        .flat_map(|s| s.split(','))
259        .find_map(|v| {
260            let mut v = v.splitn(2, ';');
261            let coding = v.next().unwrap().trim();
262            if coding != "*" {
263                return None;
264            }
265            let qval = if let Some(qval) = v.next() {
266                QValue::parse(qval.trim())?
267            } else {
268                QValue::one()
269            };
270            Some(qval)
271        })
272}
273
274/// Selects the preferred encoding considering the `*` wildcard per RFC 9110 §12.5.3.
275///
276/// The wildcard applies its q-value to any encoding not explicitly listed. If all acceptable
277/// encodings (including identity) are excluded, returns `None` to signal 406 Not Acceptable.
278#[cfg(any(
279    feature = "compression-gzip",
280    feature = "compression-br",
281    feature = "compression-zstd",
282    feature = "compression-deflate",
283))]
284fn preferred_encoding_with_wildcard(
285    headers: &http::HeaderMap,
286    supported_encoding: impl SupportedEncodings,
287) -> Option<Encoding> {
288    let explicit: Vec<(Encoding, QValue)> = encodings(headers, supported_encoding).collect();
289    let wildcard_q = wildcard_qvalue(headers);
290
291    // If there is no wildcard, use only the explicitly listed encodings.
292    // Per RFC 9110 §12.5.3, if identity is excluded (q=0) and no other encoding is
293    // acceptable, the server SHOULD respond with 406.
294    let wildcard_q = match wildcard_q {
295        Some(q) => q,
296        None => {
297            let identity_rejected = explicit
298                .iter()
299                .any(|(enc, q)| *enc == Encoding::Identity && q.0 == 0);
300            return match Encoding::preferred_encoding(explicit.into_iter()) {
301                Some(enc) => Some(enc),
302                None => {
303                    if identity_rejected {
304                        None
305                    } else {
306                        Some(Encoding::Identity)
307                    }
308                }
309            };
310        }
311    };
312
313    // Build the effective set of (encoding, qvalue) for all supported encodings.
314    // For each supported encoding, use its explicit q-value if listed, otherwise the wildcard
315    // q-value.
316    let all_supported = all_supported_encodings(supported_encoding);
317
318    let effective = all_supported.iter().filter_map(|e| *e).map(|enc| {
319        let q = explicit
320            .iter()
321            .find(|(e, _)| *e == enc)
322            .map(|(_, q)| *q)
323            .unwrap_or(wildcard_q);
324        (enc, q)
325    });
326
327    Encoding::preferred_encoding(effective)
328}
329
330/// Returns all encodings the server supports (including Identity) in a fixed-capacity array.
331#[cfg(any(
332    feature = "compression-gzip",
333    feature = "compression-br",
334    feature = "compression-zstd",
335    feature = "compression-deflate",
336))]
337fn all_supported_encodings(supported_encoding: impl SupportedEncodings) -> [Option<Encoding>; 5] {
338    let mut out: [Option<Encoding>; 5] = [None; 5];
339    let mut n = 0;
340
341    macro_rules! push {
342        ($enc:expr) => {
343            out[n] = Some($enc);
344            n += 1;
345        };
346    }
347
348    push!(Encoding::Identity);
349
350    #[cfg(any(feature = "fs", feature = "compression-gzip"))]
351    if supported_encoding.gzip() {
352        push!(Encoding::Gzip);
353    }
354
355    #[cfg(any(feature = "fs", feature = "compression-deflate"))]
356    if supported_encoding.deflate() {
357        push!(Encoding::Deflate);
358    }
359
360    #[cfg(any(feature = "fs", feature = "compression-br"))]
361    if supported_encoding.br() {
362        push!(Encoding::Brotli);
363    }
364
365    #[cfg(any(feature = "fs", feature = "compression-zstd"))]
366    if supported_encoding.zstd() {
367        push!(Encoding::Zstd);
368    }
369
370    let _ = n;
371    out
372}
373
374#[cfg(all(
375    test,
376    feature = "compression-gzip",
377    feature = "compression-deflate",
378    feature = "compression-br",
379    feature = "compression-zstd",
380))]
381mod tests {
382    use super::*;
383
384    #[derive(Copy, Clone, Default)]
385    struct SupportedEncodingsAll;
386
387    impl SupportedEncodings for SupportedEncodingsAll {
388        fn gzip(&self) -> bool {
389            true
390        }
391
392        fn deflate(&self) -> bool {
393            true
394        }
395
396        fn br(&self) -> bool {
397            true
398        }
399
400        fn zstd(&self) -> bool {
401            true
402        }
403    }
404
405    #[test]
406    fn no_accept_encoding_header() {
407        let encoding = Encoding::from_headers(&http::HeaderMap::new(), SupportedEncodingsAll);
408        assert_eq!(Some(Encoding::Identity), encoding);
409    }
410
411    #[test]
412    fn accept_encoding_header_single_encoding() {
413        let mut headers = http::HeaderMap::new();
414        headers.append(
415            http::header::ACCEPT_ENCODING,
416            http::HeaderValue::from_static("gzip"),
417        );
418        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
419        assert_eq!(Some(Encoding::Gzip), encoding);
420    }
421
422    #[test]
423    fn accept_encoding_header_two_encodings() {
424        let mut headers = http::HeaderMap::new();
425        headers.append(
426            http::header::ACCEPT_ENCODING,
427            http::HeaderValue::from_static("gzip,br"),
428        );
429        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
430        assert_eq!(Some(Encoding::Brotli), encoding);
431    }
432
433    #[test]
434    fn accept_encoding_header_gzip_x_gzip() {
435        let mut headers = http::HeaderMap::new();
436        headers.append(
437            http::header::ACCEPT_ENCODING,
438            http::HeaderValue::from_static("gzip,x-gzip"),
439        );
440        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
441        assert_eq!(Some(Encoding::Gzip), encoding);
442    }
443
444    #[test]
445    fn accept_encoding_header_x_gzip_deflate() {
446        let mut headers = http::HeaderMap::new();
447        headers.append(
448            http::header::ACCEPT_ENCODING,
449            http::HeaderValue::from_static("deflate,x-gzip"),
450        );
451        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
452        assert_eq!(Some(Encoding::Gzip), encoding);
453    }
454
455    #[test]
456    fn accept_encoding_header_three_encodings() {
457        let mut headers = http::HeaderMap::new();
458        headers.append(
459            http::header::ACCEPT_ENCODING,
460            http::HeaderValue::from_static("gzip,deflate,br"),
461        );
462        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
463        assert_eq!(Some(Encoding::Brotli), encoding);
464    }
465
466    #[test]
467    fn accept_encoding_header_two_encodings_with_one_qvalue() {
468        let mut headers = http::HeaderMap::new();
469        headers.append(
470            http::header::ACCEPT_ENCODING,
471            http::HeaderValue::from_static("gzip;q=0.5,br"),
472        );
473        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
474        assert_eq!(Some(Encoding::Brotli), encoding);
475    }
476
477    #[test]
478    fn accept_encoding_header_three_encodings_with_one_qvalue() {
479        let mut headers = http::HeaderMap::new();
480        headers.append(
481            http::header::ACCEPT_ENCODING,
482            http::HeaderValue::from_static("gzip;q=0.5,deflate,br"),
483        );
484        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
485        assert_eq!(Some(Encoding::Brotli), encoding);
486    }
487
488    #[test]
489    fn two_accept_encoding_headers_with_one_qvalue() {
490        let mut headers = http::HeaderMap::new();
491        headers.append(
492            http::header::ACCEPT_ENCODING,
493            http::HeaderValue::from_static("gzip;q=0.5"),
494        );
495        headers.append(
496            http::header::ACCEPT_ENCODING,
497            http::HeaderValue::from_static("br"),
498        );
499        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
500        assert_eq!(Some(Encoding::Brotli), encoding);
501    }
502
503    #[test]
504    fn two_accept_encoding_headers_three_encodings_with_one_qvalue() {
505        let mut headers = http::HeaderMap::new();
506        headers.append(
507            http::header::ACCEPT_ENCODING,
508            http::HeaderValue::from_static("gzip;q=0.5,deflate"),
509        );
510        headers.append(
511            http::header::ACCEPT_ENCODING,
512            http::HeaderValue::from_static("br"),
513        );
514        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
515        assert_eq!(Some(Encoding::Brotli), encoding);
516    }
517
518    #[test]
519    fn three_accept_encoding_headers_with_one_qvalue() {
520        let mut headers = http::HeaderMap::new();
521        headers.append(
522            http::header::ACCEPT_ENCODING,
523            http::HeaderValue::from_static("gzip;q=0.5"),
524        );
525        headers.append(
526            http::header::ACCEPT_ENCODING,
527            http::HeaderValue::from_static("deflate"),
528        );
529        headers.append(
530            http::header::ACCEPT_ENCODING,
531            http::HeaderValue::from_static("br"),
532        );
533        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
534        assert_eq!(Some(Encoding::Brotli), encoding);
535    }
536
537    #[test]
538    fn accept_encoding_header_two_encodings_with_two_qvalues() {
539        let mut headers = http::HeaderMap::new();
540        headers.append(
541            http::header::ACCEPT_ENCODING,
542            http::HeaderValue::from_static("gzip;q=0.5,br;q=0.8"),
543        );
544        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
545        assert_eq!(Some(Encoding::Brotli), encoding);
546
547        let mut headers = http::HeaderMap::new();
548        headers.append(
549            http::header::ACCEPT_ENCODING,
550            http::HeaderValue::from_static("gzip;q=0.8,br;q=0.5"),
551        );
552        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
553        assert_eq!(Some(Encoding::Gzip), encoding);
554
555        let mut headers = http::HeaderMap::new();
556        headers.append(
557            http::header::ACCEPT_ENCODING,
558            http::HeaderValue::from_static("gzip;q=0.995,br;q=0.999"),
559        );
560        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
561        assert_eq!(Some(Encoding::Brotli), encoding);
562    }
563
564    #[test]
565    fn accept_encoding_header_three_encodings_with_three_qvalues() {
566        let mut headers = http::HeaderMap::new();
567        headers.append(
568            http::header::ACCEPT_ENCODING,
569            http::HeaderValue::from_static("gzip;q=0.5,deflate;q=0.6,br;q=0.8"),
570        );
571        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
572        assert_eq!(Some(Encoding::Brotli), encoding);
573
574        let mut headers = http::HeaderMap::new();
575        headers.append(
576            http::header::ACCEPT_ENCODING,
577            http::HeaderValue::from_static("gzip;q=0.8,deflate;q=0.6,br;q=0.5"),
578        );
579        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
580        assert_eq!(Some(Encoding::Gzip), encoding);
581
582        let mut headers = http::HeaderMap::new();
583        headers.append(
584            http::header::ACCEPT_ENCODING,
585            http::HeaderValue::from_static("gzip;q=0.6,deflate;q=0.8,br;q=0.5"),
586        );
587        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
588        assert_eq!(Some(Encoding::Deflate), encoding);
589
590        let mut headers = http::HeaderMap::new();
591        headers.append(
592            http::header::ACCEPT_ENCODING,
593            http::HeaderValue::from_static("gzip;q=0.995,deflate;q=0.997,br;q=0.999"),
594        );
595        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
596        assert_eq!(Some(Encoding::Brotli), encoding);
597    }
598
599    #[test]
600    fn accept_encoding_header_invalid_encdoing() {
601        let mut headers = http::HeaderMap::new();
602        headers.append(
603            http::header::ACCEPT_ENCODING,
604            http::HeaderValue::from_static("invalid,gzip"),
605        );
606        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
607        assert_eq!(Some(Encoding::Gzip), encoding);
608    }
609
610    #[test]
611    fn accept_encoding_header_with_qvalue_zero() {
612        let mut headers = http::HeaderMap::new();
613        headers.append(
614            http::header::ACCEPT_ENCODING,
615            http::HeaderValue::from_static("gzip;q=0"),
616        );
617        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
618        assert_eq!(Some(Encoding::Identity), encoding);
619
620        let mut headers = http::HeaderMap::new();
621        headers.append(
622            http::header::ACCEPT_ENCODING,
623            http::HeaderValue::from_static("gzip;q=0."),
624        );
625        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
626        assert_eq!(Some(Encoding::Identity), encoding);
627
628        let mut headers = http::HeaderMap::new();
629        headers.append(
630            http::header::ACCEPT_ENCODING,
631            http::HeaderValue::from_static("gzip;q=0,br;q=0.5"),
632        );
633        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
634        assert_eq!(Some(Encoding::Brotli), encoding);
635    }
636
637    #[test]
638    fn accept_encoding_header_with_uppercase_letters() {
639        let mut headers = http::HeaderMap::new();
640        headers.append(
641            http::header::ACCEPT_ENCODING,
642            http::HeaderValue::from_static("gZiP"),
643        );
644        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
645        assert_eq!(Some(Encoding::Gzip), encoding);
646
647        let mut headers = http::HeaderMap::new();
648        headers.append(
649            http::header::ACCEPT_ENCODING,
650            http::HeaderValue::from_static("gzip;q=0.5,br;Q=0.8"),
651        );
652        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
653        assert_eq!(Some(Encoding::Brotli), encoding);
654    }
655
656    #[test]
657    fn accept_encoding_header_with_allowed_spaces() {
658        let mut headers = http::HeaderMap::new();
659        headers.append(
660            http::header::ACCEPT_ENCODING,
661            http::HeaderValue::from_static(" gzip\t; q=0.5 ,\tbr ;\tq=0.8\t"),
662        );
663        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
664        assert_eq!(Some(Encoding::Brotli), encoding);
665    }
666
667    #[test]
668    fn accept_encoding_header_with_invalid_spaces() {
669        let mut headers = http::HeaderMap::new();
670        headers.append(
671            http::header::ACCEPT_ENCODING,
672            http::HeaderValue::from_static("gzip;q =0.5"),
673        );
674        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
675        assert_eq!(Some(Encoding::Identity), encoding);
676
677        let mut headers = http::HeaderMap::new();
678        headers.append(
679            http::header::ACCEPT_ENCODING,
680            http::HeaderValue::from_static("gzip;q= 0.5"),
681        );
682        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
683        assert_eq!(Some(Encoding::Identity), encoding);
684    }
685
686    #[test]
687    fn accept_encoding_header_with_invalid_quvalues() {
688        let mut headers = http::HeaderMap::new();
689        headers.append(
690            http::header::ACCEPT_ENCODING,
691            http::HeaderValue::from_static("gzip;q=-0.1"),
692        );
693        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
694        assert_eq!(Some(Encoding::Identity), encoding);
695
696        let mut headers = http::HeaderMap::new();
697        headers.append(
698            http::header::ACCEPT_ENCODING,
699            http::HeaderValue::from_static("gzip;q=00.5"),
700        );
701        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
702        assert_eq!(Some(Encoding::Identity), encoding);
703
704        let mut headers = http::HeaderMap::new();
705        headers.append(
706            http::header::ACCEPT_ENCODING,
707            http::HeaderValue::from_static("gzip;q=0.5000"),
708        );
709        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
710        assert_eq!(Some(Encoding::Identity), encoding);
711
712        let mut headers = http::HeaderMap::new();
713        headers.append(
714            http::header::ACCEPT_ENCODING,
715            http::HeaderValue::from_static("gzip;q=.5"),
716        );
717        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
718        assert_eq!(Some(Encoding::Identity), encoding);
719
720        let mut headers = http::HeaderMap::new();
721        headers.append(
722            http::header::ACCEPT_ENCODING,
723            http::HeaderValue::from_static("gzip;q=1.01"),
724        );
725        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
726        assert_eq!(Some(Encoding::Identity), encoding);
727
728        let mut headers = http::HeaderMap::new();
729        headers.append(
730            http::header::ACCEPT_ENCODING,
731            http::HeaderValue::from_static("gzip;q=1.001"),
732        );
733        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
734        assert_eq!(Some(Encoding::Identity), encoding);
735    }
736
737    #[test]
738    fn wildcard_alone_picks_best_supported() {
739        let mut headers = http::HeaderMap::new();
740        headers.append(
741            http::header::ACCEPT_ENCODING,
742            http::HeaderValue::from_static("*"),
743        );
744        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
745        // * with q=1 means all encodings are acceptable; picks the highest-priority supported
746        assert_eq!(Some(Encoding::Zstd), encoding);
747    }
748
749    #[test]
750    fn wildcard_q_zero_with_nothing_else_returns_not_satisfiable() {
751        let mut headers = http::HeaderMap::new();
752        headers.append(
753            http::header::ACCEPT_ENCODING,
754            http::HeaderValue::from_static("*;q=0"),
755        );
756        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
757        // *;q=0 rejects everything, including identity
758        assert_eq!(None, encoding);
759    }
760
761    #[test]
762    fn wildcard_q_zero_with_gzip_picks_gzip() {
763        let mut headers = http::HeaderMap::new();
764        headers.append(
765            http::header::ACCEPT_ENCODING,
766            http::HeaderValue::from_static("*;q=0,gzip"),
767        );
768        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
769        assert_eq!(Some(Encoding::Gzip), encoding);
770    }
771
772    #[test]
773    fn identity_q_zero_alone_returns_not_satisfiable() {
774        let mut headers = http::HeaderMap::new();
775        headers.append(
776            http::header::ACCEPT_ENCODING,
777            http::HeaderValue::from_static("identity;q=0"),
778        );
779        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
780        // identity;q=0 with no other encoding explicitly listed: the server cannot
781        // determine what the client accepts, so 406 per RFC 9110 §12.5.3
782        assert_eq!(None, encoding);
783    }
784
785    #[test]
786    fn identity_q_zero_with_gzip_picks_gzip() {
787        let mut headers = http::HeaderMap::new();
788        headers.append(
789            http::header::ACCEPT_ENCODING,
790            http::HeaderValue::from_static("identity;q=0,gzip"),
791        );
792        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
793        assert_eq!(Some(Encoding::Gzip), encoding);
794    }
795
796    #[test]
797    fn wildcard_q_zero_identity_q_zero_no_compression_returns_not_satisfiable() {
798        // *;q=0,identity;q=0 with no explicit compression listed
799        let mut headers = http::HeaderMap::new();
800        headers.append(
801            http::header::ACCEPT_ENCODING,
802            http::HeaderValue::from_static("*;q=0,identity;q=0"),
803        );
804        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
805        // Both wildcard and identity are q=0, and no explicit encoding is listed with q>0
806        assert_eq!(None, encoding);
807    }
808
809    #[test]
810    fn wildcard_with_low_qvalue() {
811        let mut headers = http::HeaderMap::new();
812        headers.append(
813            http::header::ACCEPT_ENCODING,
814            http::HeaderValue::from_static("*;q=0.5,gzip;q=1"),
815        );
816        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
817        // gzip is explicitly q=1, everything else gets q=0.5 from wildcard
818        assert_eq!(Some(Encoding::Gzip), encoding);
819    }
820
821    #[test]
822    fn wildcard_q_zero_with_identity_picks_identity() {
823        let mut headers = http::HeaderMap::new();
824        headers.append(
825            http::header::ACCEPT_ENCODING,
826            http::HeaderValue::from_static("*;q=0,identity"),
827        );
828        let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll);
829        // *;q=0 rejects all, but identity is explicitly listed with q=1
830        assert_eq!(Some(Encoding::Identity), encoding);
831    }
832
833    #[derive(Copy, Clone)]
834    struct SupportedGzipOnly;
835
836    impl SupportedEncodings for SupportedGzipOnly {
837        fn gzip(&self) -> bool {
838            true
839        }
840        fn deflate(&self) -> bool {
841            false
842        }
843        fn br(&self) -> bool {
844            false
845        }
846        fn zstd(&self) -> bool {
847            false
848        }
849    }
850
851    #[test]
852    fn wildcard_with_partial_server_support_picks_best_available() {
853        let mut headers = http::HeaderMap::new();
854        headers.append(
855            http::header::ACCEPT_ENCODING,
856            http::HeaderValue::from_static("*"),
857        );
858        let encoding = Encoding::from_headers(&headers, SupportedGzipOnly);
859        // Server only supports gzip, so * should pick gzip (not zstd/br)
860        assert_eq!(Some(Encoding::Gzip), encoding);
861    }
862
863    #[test]
864    fn wildcard_q_zero_with_unsupported_encoding_returns_not_satisfiable() {
865        let mut headers = http::HeaderMap::new();
866        headers.append(
867            http::header::ACCEPT_ENCODING,
868            http::HeaderValue::from_static("*;q=0,br"),
869        );
870        let encoding = Encoding::from_headers(&headers, SupportedGzipOnly);
871        // Client wants br, but server only supports gzip. br is not in the
872        // supported set so it's ignored by encodings(). Wildcard rejects
873        // everything else. Result: 406.
874        assert_eq!(None, encoding);
875    }
876}