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