fly_accept_encoding/
lib.rs

1#![forbid(unsafe_code, future_incompatible)]
2#![forbid(rust_2018_idioms, rust_2018_compatibility)]
3#![deny(missing_debug_implementations, bad_style)]
4#![deny(missing_docs)]
5#![cfg_attr(test, deny(warnings))]
6
7//! ## Examples
8//! ```rust
9//! use fly_accept_encoding::{Encoding,Error};
10//! use http::header::{HeaderMap, HeaderValue, ACCEPT_ENCODING};
11//!
12//! # fn main () -> Result<(), Error> {
13//! let mut headers = HeaderMap::new();
14//! headers.insert(ACCEPT_ENCODING, HeaderValue::from_str("gzip, deflate, br").unwrap());
15//!
16//! let encoding = fly_accept_encoding::parse(&headers)?;
17//! assert_eq!(encoding, Some(Encoding::Gzip));
18//! # Ok(())}
19//! ```
20//!
21//! ```rust
22//! use fly_accept_encoding::{Encoding,Error};
23//! use http::header::{HeaderMap, HeaderValue, ACCEPT_ENCODING};
24//!
25//! # fn main () -> Result<(), Error> {
26//! let mut headers = HeaderMap::new();
27//! headers.insert(ACCEPT_ENCODING, HeaderValue::from_str("gzip;q=0.5, deflate;q=0.9, br;q=1.0").unwrap());
28//!
29//! let encoding = fly_accept_encoding::parse(&headers)?;
30//! assert_eq!(encoding, Some(Encoding::Brotli));
31//! # Ok(())}
32//! ```
33
34mod error;
35
36pub use crate::error::{Error, Result};
37use http::header::{HeaderMap, HeaderValue, ACCEPT_ENCODING};
38use itertools::Itertools;
39
40/// Encodings to use.
41#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
42pub enum Encoding {
43    /// The Gzip encoding.
44    Gzip,
45    /// The Deflate encoding.
46    Deflate,
47    /// The Brotli encoding.
48    Brotli,
49    /// The Zstd encoding.
50    Zstd,
51    /// No encoding.
52    Identity,
53}
54
55impl Encoding {
56    /// Parses a given string into its corresponding encoding.
57    fn parse(s: &str) -> Result<Option<Encoding>> {
58        match s {
59            "gzip" => Ok(Some(Encoding::Gzip)),
60            "deflate" => Ok(Some(Encoding::Deflate)),
61            "br" => Ok(Some(Encoding::Brotli)),
62            "zstd" => Ok(Some(Encoding::Zstd)),
63            "identity" => Ok(Some(Encoding::Identity)),
64            "*" => Ok(None),
65            _ => Err(Error::UnknownEncoding),
66        }
67    }
68
69    /// Converts the encoding into its' corresponding header value.
70    pub fn to_header_value(self) -> HeaderValue {
71        match self {
72            Encoding::Gzip => HeaderValue::from_str("gzip").unwrap(),
73            Encoding::Deflate => HeaderValue::from_str("deflate").unwrap(),
74            Encoding::Brotli => HeaderValue::from_str("br").unwrap(),
75            Encoding::Zstd => HeaderValue::from_str("zstd").unwrap(),
76            Encoding::Identity => HeaderValue::from_str("identity").unwrap(),
77        }
78    }
79}
80
81/// Parse a set of HTTP headers into a single option yielding an `Encoding` that the client prefers.
82///
83/// If you're looking for an easy way to determine the best encoding for the client and support every [`Encoding`] listed, this is likely what you want.
84///
85/// Note that a result of `None` indicates there preference is expressed on which encoding to use.
86/// Either the `Accept-Encoding` header is not present, or `*` is set as the most preferred encoding.
87pub fn parse(headers: &HeaderMap) -> Result<Option<Encoding>> {
88    preferred(encodings_iter(headers))
89}
90
91/// Select the encoding with the largest qval or the first with qval ~= 1
92pub fn preferred(
93    encodings: impl Iterator<Item = Result<(Option<Encoding>, f32)>>,
94) -> Result<Option<Encoding>> {
95    let mut preferred_encoding = None;
96    let mut max_qval = 0.0;
97
98    for r in encodings {
99        let (encoding, qval) = r?;
100        if (qval - 1.0f32).abs() < 0.01 {
101            return Ok(encoding);
102        } else if qval > max_qval {
103            preferred_encoding = encoding;
104            max_qval = qval;
105        }
106    }
107
108    Ok(preferred_encoding)
109}
110
111/// Parse a set of HTTP headers into a vector containing tuples of options containing encodings and their corresponding q-values.
112///
113/// If you're looking for more fine-grained control over what encoding to choose for the client, or if you don't support every [`Encoding`] listed, this is likely what you want.
114///
115/// Note that a result of `None` indicates there preference is expressed on which encoding to use.
116/// Either the `Accept-Encoding` header is not present, or `*` is set as the most preferred encoding.
117/// ## Examples
118/// ```rust
119/// use fly_accept_encoding::{Encoding,Error};
120/// use http::header::{HeaderMap, HeaderValue, ACCEPT_ENCODING};
121///
122/// # fn main () -> Result<(), Error> {
123/// let mut headers = HeaderMap::new();
124/// headers.insert(ACCEPT_ENCODING, HeaderValue::from_str("zstd;q=1.0, deflate;q=0.8, br;q=0.9").unwrap());
125///
126/// let encodings = fly_accept_encoding::encodings(&headers)?;
127/// for (encoding, qval) in encodings {
128///     println!("{:?} {}", encoding, qval);
129/// }
130/// # Ok(())}
131/// ```
132pub fn encodings(headers: &HeaderMap) -> Result<Vec<(Option<Encoding>, f32)>> {
133    encodings_iter(headers).collect()
134}
135
136/// Parse a set of HTTP headers into an iterator containing tuples of options containing encodings and their corresponding q-values.
137pub fn encodings_iter(
138    headers: &HeaderMap,
139) -> impl Iterator<Item = Result<(Option<Encoding>, f32)>> + '_ {
140    headers
141        .get_all(ACCEPT_ENCODING)
142        .iter()
143        .map(|hval| hval.to_str().map_err(|_| Error::InvalidEncoding))
144        .map_ok(|s| s.split(',').map(str::trim))
145        .flatten_ok()
146        .filter_map_ok(|v| {
147            let (e, q) = match v.split_once(";q=") {
148                Some((e, q)) => (e, q),
149                None => return Some(Ok((Encoding::parse(v).ok()?, 1.0f32))),
150            };
151            let encoding = Encoding::parse(e).ok()?; // ignore unknown encodings
152            let qval = match q.parse() {
153                Ok(f) if f > 1.0 => return Some(Err(Error::InvalidEncoding)), // q-values over 1 are unacceptable,
154                Ok(f) => f,
155                Err(_) => return Some(Err(Error::InvalidEncoding)),
156            };
157            Some(Ok((encoding, qval)))
158        })
159        .map(|r| r?) // flatten Result<Result<...
160}