diff --git a/Cargo.toml b/Cargo.toml index d5df575..88cdb1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,8 @@ readme = "README.md" edition = "2018" [dependencies] +derive_is_enum_variant = "0.1.1" failure = "0.1.3" http = "0.1.13" -derive_is_enum_variant = "0.1.1" [dev-dependencies] diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 4c1eefa..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -max_width = 80 -tab_spaces = 2 diff --git a/src/error.rs b/src/error.rs index 21b26ab..494be65 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,15 +21,15 @@ pub type Result = result::Result; /// [`Error`]: std.struct.Error.html #[derive(Debug, Fail)] pub enum ErrorKind { - /// Invalid header encoding. - #[fail(display = "Invalid header encoding.")] - InvalidEncoding, - /// The encoding scheme is unknown. - #[fail(display = "Uknown encoding scheme.")] - UnknownEncoding, - /// Any error not part of this list. - #[fail(display = "Generic error.")] - Other, + /// Invalid header encoding. + #[fail(display = "Invalid header encoding.")] + InvalidEncoding, + /// The encoding scheme is unknown. + #[fail(display = "Unknown encoding scheme.")] + UnknownEncoding, + /// Any error not part of this list. + #[fail(display = "Generic error.")] + Other, } /// A specialized [`Error`] type for this crate's operations. @@ -37,43 +37,43 @@ pub enum ErrorKind { /// [`Error`]: https://doc.rust-lang.org/nightly/std/error/trait.Error.html #[derive(Debug)] pub struct Error { - inner: Context, + inner: Context, } impl Error { - /// Access the [`ErrorKind`] member. - /// - /// [`ErrorKind`]: enum.ErrorKind.html - pub fn kind(&self) -> &ErrorKind { - &*self.inner.get_context() - } + /// Access the [`ErrorKind`] member. + /// + /// [`ErrorKind`]: enum.ErrorKind.html + pub fn kind(&self) -> &ErrorKind { + &*self.inner.get_context() + } } impl Fail for Error { - fn cause(&self) -> Option<&dyn Fail> { - self.inner.cause() - } + fn cause(&self) -> Option<&dyn Fail> { + self.inner.cause() + } - fn backtrace(&self) -> Option<&Backtrace> { - self.inner.backtrace() - } + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } } impl Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(&self.inner, f) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.inner, f) + } } impl From for Error { - fn from(kind: ErrorKind) -> Error { - let inner = Context::new(kind); - Error { inner } - } + fn from(kind: ErrorKind) -> Error { + let inner = Context::new(kind); + Error { inner } + } } impl From> for Error { - fn from(inner: Context) -> Error { - Error { inner } - } + fn from(inner: Context) -> Error { + Error { inner } + } } diff --git a/src/lib.rs b/src/lib.rs index 018dd5d..01c6d1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ #![deny(missing_docs)] #![cfg_attr(test, deny(warnings))] -//! ## Example +//! ## Examples //! ```rust //! # use failure::Error; //! use http::header::{HeaderMap, HeaderValue, ACCEPT_ENCODING}; @@ -14,46 +14,109 @@ //! headers.insert(ACCEPT_ENCODING, HeaderValue::from_str("gzip, deflate, br")?); //! //! let encoding = accept_encoding::parse(&headers)?; +//! assert!(encoding.is_gzip()); +//! # Ok(())} +//! ``` +//! +//! ```rust +//! # use failure::Error; +//! use http::header::{HeaderMap, HeaderValue, ACCEPT_ENCODING}; +//! +//! # fn main () -> Result<(), failure::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")?); +//! +//! let encoding = accept_encoding::parse(&headers)?; //! assert!(encoding.is_brotli()); //! # Ok(())} //! ``` mod error; +pub use crate::error::{Error, ErrorKind, Result}; use derive_is_enum_variant::is_enum_variant; -use http::header::{ACCEPT_ENCODING, HeaderMap}; -use failure::{ResultExt}; -pub use crate::error::{Error, Result, ErrorKind}; +use failure::ResultExt; +use http::header::{HeaderMap, HeaderValue, ACCEPT_ENCODING}; /// Encoding levels. -#[derive(Debug, Clone, is_enum_variant)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, is_enum_variant)] pub enum Encoding { - /// Gzip is the best encoding present. - Gzip, - /// Deflate is the best encoding present. - Deflate, - /// Brotli is the best encoding is present. - Brotli, - /// No encoding is present. - None, + /// Gzip is the most preferred encoding present. + Gzip, + /// Deflate is the most preferred encoding present. + Deflate, + /// Brotli is the most preferred encoding present. + Brotli, + /// No encoding is preferred. + Identity, + /// No preference is expressed on which encoding to use. Either the `Accept-Encoding` header is not present, or `*` is set as the most preferred encoding. + None, +} + +impl Encoding { + /// Parses a given string into its corresponding encoding. + fn parse(s: &str) -> Result { + match s { + "gzip" => Ok(Encoding::Gzip), + "deflate" => Ok(Encoding::Deflate), + "br" => Ok(Encoding::Brotli), + "identity" => Ok(Encoding::Identity), + "*" => Ok(Encoding::None), + _ => Err(ErrorKind::UnknownEncoding)?, + } + } + + /// Converts the encoding into its' corresponding header value. + /// + /// Note that [`Encoding::None`] will return a HeaderValue with the content `*`. + /// This is likely not what you want if you are using this to generate the `Content-Encoding` header to be included in an encoded response. + 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::Identity => HeaderValue::from_str("identity").unwrap(), + Encoding::None => HeaderValue::from_str("*").unwrap(), + } + } } /// Parse a set of HTTP headers into an `Encoding`. pub fn parse(headers: &HeaderMap) -> Result { - let header = match headers.get(ACCEPT_ENCODING) { - Some(header) => header, - None => return Ok(Encoding::None), - }; - - let string = header.to_str().context(ErrorKind::InvalidEncoding)?; - - if string.contains("br") { - Ok(Encoding::Brotli) - } else if string.contains("deflate") { - Ok(Encoding::Deflate) - } else if string.contains("gzip") { - Ok(Encoding::Gzip) - } else { - Err(ErrorKind::UnknownEncoding)? - } + let mut preferred_encoding = Encoding::None; + let mut max_qval = 0.0; + + for header_value in headers.get_all(ACCEPT_ENCODING).iter() { + let header_value = header_value.to_str().context(ErrorKind::InvalidEncoding)?; + for v in header_value.split(',').map(str::trim) { + let mut v = v.splitn(2, ";q="); + let encoding = v.next().unwrap(); + + match Encoding::parse(encoding) { + Ok(encoding) => { + if let Some(qval) = v.next() { + let qval = match qval.parse::() { + Ok(f) => f, + Err(_) => return Err(ErrorKind::InvalidEncoding)?, + }; + if (qval - 1.0f32).abs() < 0.01 { + preferred_encoding = encoding; + break; + } else if qval > 1.0 { + return Err(ErrorKind::InvalidEncoding)?; // q-values over 1 are unacceptable + } else if qval > max_qval { + preferred_encoding = encoding; + max_qval = qval; + } + } else { + preferred_encoding = encoding; + break; + } + } + Err(_) => continue, // ignore unknown encodings for now + } + } + } + + Ok(preferred_encoding) } diff --git a/tests/test.rs b/tests/test.rs index deb24c7..7bae96d 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -2,8 +2,79 @@ extern crate accept_encoding; extern crate failure; use failure::Error; +use http::header::{HeaderMap, HeaderValue, ACCEPT_ENCODING}; #[test] -fn should_work() -> Result<(), Error> { - Ok(()) +fn single_encoding() -> Result<(), Error> { + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT_ENCODING, HeaderValue::from_str("gzip")?); + + let encoding = accept_encoding::parse(&headers)?; + assert!(encoding.is_gzip()); + + Ok(()) +} + +#[test] +fn multiple_encodings() -> Result<(), Error> { + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT_ENCODING, HeaderValue::from_str("gzip, deflate, br")?); + + let encoding = accept_encoding::parse(&headers)?; + assert!(encoding.is_gzip()); + + Ok(()) +} + +#[test] +fn single_encoding_with_qval() -> Result<(), Error> { + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT_ENCODING, HeaderValue::from_str("deflate;q=1.0")?); + + let encoding = accept_encoding::parse(&headers)?; + assert!(encoding.is_deflate()); + + Ok(()) +} + +#[test] +fn multiple_encodings_with_qval_1() -> Result<(), Error> { + let mut headers = HeaderMap::new(); + headers.insert( + ACCEPT_ENCODING, + HeaderValue::from_str("deflate, gzip;q=1.0, *;q=0.5")?, + ); + + let encoding = accept_encoding::parse(&headers)?; + assert!(encoding.is_deflate()); + + Ok(()) +} + +#[test] +fn multiple_encodings_with_qval_2() -> Result<(), Error> { + let mut headers = HeaderMap::new(); + headers.insert( + ACCEPT_ENCODING, + HeaderValue::from_str("gzip;q=0.5, deflate;q=1.0, *;q=0.5")?, + ); + + let encoding = accept_encoding::parse(&headers)?; + assert!(encoding.is_deflate()); + + Ok(()) +} + +#[test] +fn multiple_encodings_with_qval_3() -> Result<(), Error> { + let mut headers = HeaderMap::new(); + headers.insert( + ACCEPT_ENCODING, + HeaderValue::from_str("gzip;q=0.5, deflate;q=0.75, *;q=1.0")?, + ); + + let encoding = accept_encoding::parse(&headers)?; + assert!(encoding.is_none()); + + Ok(()) }