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}