From 9cb54132e4bb7c24c07599d9029c556f29d8cdb8 Mon Sep 17 00:00:00 2001 From: grey Date: Thu, 9 May 2019 12:58:21 -0700 Subject: [PATCH 1/6] refactor some stuff adds support for q-values per spec at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding#Directives adds extra values and potentially helpful functions to encoding new tests changes rustfmt.toml to look similar to Tide's --- Cargo.toml | 2 +- rustfmt.toml | 4 +- src/error.rs | 64 +++++++++++++-------------- src/lib.rs | 120 ++++++++++++++++++++++++++++++++++++++------------ tests/test.rs | 75 ++++++++++++++++++++++++++++++- 5 files changed, 200 insertions(+), 65 deletions(-) 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 index 4c1eefa..a855209 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,2 @@ -max_width = 80 -tab_spaces = 2 +edition = "2018" +tab_spaces = 4 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..74fd723 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,110 @@ //! 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)] 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 v: Vec<&str> = v.splitn(2, ";q=").collect(); + let encoding = v[0]; + + match Encoding::parse(encoding) { + Ok(encoding) => { + if v.len() > 1 { + let qval = match v[1].parse::() { + Ok(f) => f, + Err(_) => continue, // skip malformed q values + }; + if (qval - 1.0f32).abs() < 0.01 { + preferred_encoding = encoding; + break; + } else if qval > 1.0 { + // q-values over 1 are unacceptable + continue; + } 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(()) } From 0aa135852944d08bbf8c02aab87da575293d95fa Mon Sep 17 00:00:00 2001 From: grey Date: Thu, 9 May 2019 13:27:12 -0700 Subject: [PATCH 2/6] delete rustfmt.toml --- rustfmt.toml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index a855209..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -edition = "2018" -tab_spaces = 4 From 661180664385332240c6dcc8df83fa0816a5df9f Mon Sep 17 00:00:00 2001 From: grey Date: Thu, 9 May 2019 13:34:49 -0700 Subject: [PATCH 3/6] return invalid encoding errors on malformed q-values --- src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 74fd723..85d295a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,14 +97,13 @@ pub fn parse(headers: &HeaderMap) -> Result { if v.len() > 1 { let qval = match v[1].parse::() { Ok(f) => f, - Err(_) => continue, // skip malformed q values + Err(_) => return Err(ErrorKind::InvalidEncoding)?, }; if (qval - 1.0f32).abs() < 0.01 { preferred_encoding = encoding; break; } else if qval > 1.0 { - // q-values over 1 are unacceptable - continue; + return Err(ErrorKind::InvalidEncoding)?; // q-values over 1 are unacceptable } else if qval > max_qval { preferred_encoding = encoding; max_qval = qval; From e4ef187f30fa530fbb67cdce85fee0e42aea8360 Mon Sep 17 00:00:00 2001 From: grey Date: Thu, 9 May 2019 15:13:27 -0700 Subject: [PATCH 4/6] add helpful traits --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 85d295a..4bb5b2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,7 @@ 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 most preferred encoding present. Gzip, From 99d59e3dd9bf2ada10fa4fd81f39fc98b03dcde3 Mon Sep 17 00:00:00 2001 From: grey Date: Thu, 9 May 2019 15:39:20 -0700 Subject: [PATCH 5/6] fix clippy warnings (pass by value) --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4bb5b2e..afab08b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,8 +70,8 @@ impl Encoding { /// /// 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 { + 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(), From bf535efa99c0f1ce0defb0b0cd4d15097278922b Mon Sep 17 00:00:00 2001 From: grey Date: Thu, 9 May 2019 23:26:56 -0700 Subject: [PATCH 6/6] keep the iterator --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index afab08b..01c6d1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,13 +89,13 @@ pub fn parse(headers: &HeaderMap) -> Result { 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 v: Vec<&str> = v.splitn(2, ";q=").collect(); - let encoding = v[0]; + let mut v = v.splitn(2, ";q="); + let encoding = v.next().unwrap(); match Encoding::parse(encoding) { Ok(encoding) => { - if v.len() > 1 { - let qval = match v[1].parse::() { + if let Some(qval) = v.next() { + let qval = match qval.parse::() { Ok(f) => f, Err(_) => return Err(ErrorKind::InvalidEncoding)?, };