diff --git a/Cargo.toml b/Cargo.toml index a88f18e7..4a108ff4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ headers-core = { version = "0.2", path = "./headers-core" } base64 = "0.12" bitflags = "1.0" bytes = "0.5" +itertools = "0.9" mime = "0.3.14" sha-1 = "0.8" time = "0.1.34" diff --git a/src/common/accept_encoding.rs b/src/common/accept_encoding.rs new file mode 100644 index 00000000..245054cc --- /dev/null +++ b/src/common/accept_encoding.rs @@ -0,0 +1,156 @@ +use std::convert::TryFrom; + +use {ContentCoding, HeaderValue}; +use util::{QualityValue, TryFromValues}; + +/// `Accept-Encoding` header, defined in +/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.4) +/// +/// The `Accept-Encoding` header field can be used by user agents to +/// indicate what response content-codings are acceptable in the response. +/// An "identity" token is used as a synonym for "no encoding" in +/// order to communicate when no encoding is preferred. +/// +/// # ABNF +/// +/// ```text +/// Accept-Encoding = #( codings [ weight ] ) +/// codings = content-coding / "identity" / "*" +/// ``` +/// +/// # Example Values +/// +/// * `gzip` +/// * `br;q=1.0, gzip;q=0.8` +/// +#[derive(Clone, Debug)] +pub struct AcceptEncoding(pub QualityValue); + +derive_header! { + AcceptEncoding(_), + name: ACCEPT_ENCODING +} + +impl AcceptEncoding { + /// Convience method to create an `Accept-Encoding: gzip` header + #[inline] + pub fn gzip() -> AcceptEncoding { + AcceptEncoding(HeaderValue::from_static("gzip").into()) + } + + /// A convience method to create an Accept-Encoding header from pairs of values and qualities + /// + /// # Example + /// + /// ``` + /// use headers::AcceptEncoding; + /// + /// let pairs = vec![("gzip", 1.0), ("deflate", 0.8)]; + /// let header = AcceptEncoding::from_quality_pairs(&mut pairs.into_iter()); + /// ``` + pub fn from_quality_pairs<'i, I>(pairs: &mut I) -> Result + where + I: Iterator, + { + let values: Vec = pairs + .map(|pair| { + QualityValue::try_from(pair).map(|qual: QualityValue| HeaderValue::from(qual)) + }) + .collect::, ::Error>>()?; + let value = QualityValue::try_from_values(&mut values.iter())?; + Ok(AcceptEncoding(value)) + } + + /// Returns the most prefered encoding that is specified by the header, + /// if one is specified. + /// + /// Note: This peeks at the underlying iter, not modifying it. + /// + /// # Example + /// + /// ``` + /// use headers::{AcceptEncoding, ContentCoding}; + /// + /// let pairs = vec![("gzip", 1.0), ("deflate", 0.8)]; + /// let accept_enc = AcceptEncoding::from_quality_pairs(&mut pairs.into_iter()).unwrap(); + /// let mut encodings = accept_enc.sorted_encodings(); + /// + /// assert_eq!(accept_enc.prefered_encoding(), Some(ContentCoding::GZIP)); + /// ``` + pub fn prefered_encoding(&self) -> Option { + self.0.iter().peekable().peek().map(|s| ContentCoding::from_str(*s)) + } + + /// Returns a quality sorted iterator of the `ContentCoding` + /// + /// # Example + /// + /// ``` + /// use headers::{AcceptEncoding, ContentCoding, HeaderValue}; + /// + /// let val = HeaderValue::from_static("deflate, gzip;q=1.0, br;q=0.8"); + /// let accept_enc = AcceptEncoding(val.into()); + /// let mut encodings = accept_enc.sorted_encodings(); + /// + /// assert_eq!(encodings.next(), Some(ContentCoding::DEFLATE)); + /// assert_eq!(encodings.next(), Some(ContentCoding::GZIP)); + /// assert_eq!(encodings.next(), Some(ContentCoding::BROTLI)); + /// assert_eq!(encodings.next(), None); + /// ``` + pub fn sorted_encodings<'a>(&'a self) -> impl Iterator + 'a { + self.0.iter().map(|s| ContentCoding::from_str(s)) + } + + /// Returns a quality sorted iterator of values + /// + /// # Example + /// + /// ``` + /// use headers::{AcceptEncoding, ContentCoding, HeaderValue}; + /// + /// let val = HeaderValue::from_static("deflate, gzip;q=1.0, br;q=0.8"); + /// let accept_enc = AcceptEncoding(val.into()); + /// let mut encodings = accept_enc.sorted_values(); + /// + /// assert_eq!(encodings.next(), Some("deflate")); + /// assert_eq!(encodings.next(), Some("gzip")); + /// assert_eq!(encodings.next(), Some("br")); + /// assert_eq!(encodings.next(), None); + /// ``` + pub fn sorted_values(&self) -> impl Iterator { + self.0.iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use {ContentCoding, HeaderValue}; + + #[test] + fn from_static() { + let val = HeaderValue::from_static("deflate, gzip;q=1.0, br;q=0.9"); + let accept_enc = AcceptEncoding(val.into()); + + assert_eq!(accept_enc.prefered_encoding(), Some(ContentCoding::DEFLATE)); + + let mut encodings = accept_enc.sorted_encodings(); + assert_eq!(encodings.next(), Some(ContentCoding::DEFLATE)); + assert_eq!(encodings.next(), Some(ContentCoding::GZIP)); + assert_eq!(encodings.next(), Some(ContentCoding::BROTLI)); + assert_eq!(encodings.next(), None); + } + + #[test] + fn from_pairs() { + let pairs = vec![("gzip", 1.0), ("br", 0.9)]; + let accept_enc = AcceptEncoding::from_quality_pairs(&mut pairs.into_iter()).unwrap(); + + assert_eq!(accept_enc.prefered_encoding(), Some(ContentCoding::GZIP)); + + let mut encodings = accept_enc.sorted_encodings(); + assert_eq!(encodings.next(), Some(ContentCoding::GZIP)); + assert_eq!(encodings.next(), Some(ContentCoding::BROTLI)); + assert_eq!(encodings.next(), None); + } +} diff --git a/src/common/content_coding.rs b/src/common/content_coding.rs new file mode 100644 index 00000000..777b414d --- /dev/null +++ b/src/common/content_coding.rs @@ -0,0 +1,139 @@ +use HeaderValue; + +// Derives an enum to represent content codings and some helpful impls +macro_rules! define_content_coding { + ($($coding:ident; $str:expr,)+) => { + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + /// Values that are used with headers like [`Content-Encoding`](self::ContentEncoding) or + /// [`Accept-Encoding`](self::AcceptEncoding) + /// + /// [RFC7231](https://www.iana.org/assignments/http-parameters/http-parameters.xhtml) + pub enum ContentCoding { + $( + #[doc = $str] + $coding, + )+ + } + + impl ContentCoding { + /// Returns a `&'static str` for a `ContentCoding` + /// + /// # Example + /// + /// ``` + /// use headers::ContentCoding; + /// + /// let coding = ContentCoding::BROTLI; + /// assert_eq!(coding.to_static(), "br"); + /// ``` + #[inline] + pub fn to_static(&self) -> &'static str { + match *self { + $(ContentCoding::$coding => $str,)+ + } + } + + /// Given a `&str` returns a `ContentCoding` + /// + /// Note this will never fail, in the case of `&str` being an invalid content coding, + /// will return `ContentCoding::IDENTITY` because `'identity'` is generally always an + /// accepted coding. + /// + /// # Example + /// + /// ``` + /// use headers::ContentCoding; + /// + /// let invalid = ContentCoding::from_str("not a valid coding"); + /// assert_eq!(invalid, ContentCoding::IDENTITY); + /// + /// let valid = ContentCoding::from_str("gzip"); + /// assert_eq!(valid, ContentCoding::GZIP); + /// ``` + #[inline] + pub fn from_str(s: &str) -> Self { + ContentCoding::try_from_str(s).unwrap_or_else(|_| ContentCoding::IDENTITY) + } + + /// Given a `&str` will try to return a `ContentCoding` + /// + /// Different from `ContentCoding::from_str(&str)`, if `&str` is an invalid content + /// coding, it will return `Err(())` + /// + /// # Example + /// + /// ``` + /// use headers::ContentCoding; + /// + /// let invalid = ContentCoding::try_from_str("not a valid coding"); + /// assert!(invalid.is_err()); + /// + /// let valid = ContentCoding::try_from_str("gzip"); + /// assert_eq!(valid.unwrap(), ContentCoding::GZIP); + /// ``` + #[inline] + pub fn try_from_str(s: &str) -> Result { + match s { + $( + stringify!($coding) + | $str => Ok(ContentCoding::$coding), + )+ + _ => Err(()) + } + } + } + + impl std::string::ToString for ContentCoding { + #[inline] + fn to_string(&self) -> String { + match *self { + $(ContentCoding::$coding => $str.to_string(),)+ + } + } + } + + impl From for HeaderValue { + fn from(coding: ContentCoding) -> HeaderValue { + match coding { + $(ContentCoding::$coding => HeaderValue::from_static($str),)+ + } + } + } + } +} + +define_content_coding! { + BROTLI; "br", + COMPRESS; "compress", + DEFLATE; "deflate", + GZIP; "gzip", + IDENTITY; "identity", +} + +#[cfg(test)] +mod tests { + use super::ContentCoding; + + #[test] + fn to_static() { + assert_eq!(ContentCoding::GZIP.to_static(), "gzip"); + } + + #[test] + fn to_string() { + assert_eq!(ContentCoding::DEFLATE.to_string(), "deflate".to_string()); + } + + #[test] + fn from_str() { + assert_eq!(ContentCoding::from_str("br"), ContentCoding::BROTLI); + assert_eq!(ContentCoding::from_str("GZIP"), ContentCoding::GZIP); + assert_eq!(ContentCoding::from_str("blah blah"), ContentCoding::IDENTITY); + } + + #[test] + fn try_from_str() { + assert_eq!(ContentCoding::try_from_str("br"), Ok(ContentCoding::BROTLI)); + assert_eq!(ContentCoding::try_from_str("blah blah"), Err(())); + } +} \ No newline at end of file diff --git a/src/common/mod.rs b/src/common/mod.rs index 3a1e9c0f..c17cd74b 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -7,7 +7,7 @@ //! is used, such as `ContentType(pub Mime)`. //pub use self::accept_charset::AcceptCharset; -//pub use self::accept_encoding::AcceptEncoding; +pub use self::accept_encoding::AcceptEncoding; //pub use self::accept_language::AcceptLanguage; pub use self::accept_ranges::AcceptRanges; //pub use self::accept::Accept; @@ -23,6 +23,7 @@ pub use self::allow::Allow; pub use self::authorization::Authorization; pub use self::cache_control::CacheControl; pub use self::connection::Connection; +pub use self::content_coding::ContentCoding; pub use self::content_disposition::ContentDisposition; pub use self::content_encoding::ContentEncoding; //pub use self::content_language::ContentLanguage; @@ -127,7 +128,7 @@ macro_rules! bench_header { //mod accept; //mod accept_charset; -//mod accept_encoding; +mod accept_encoding; //mod accept_language; mod accept_ranges; mod access_control_allow_credentials; @@ -142,6 +143,7 @@ mod allow; pub mod authorization; mod cache_control; mod connection; +mod content_coding; mod content_disposition; mod content_encoding; //mod content_language; diff --git a/src/lib.rs b/src/lib.rs index bf05e9fa..c1a48da5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,7 @@ extern crate bitflags; extern crate bytes; extern crate headers_core; extern crate http; +extern crate itertools; extern crate mime; extern crate sha1; #[cfg(all(test, feature = "nightly"))] diff --git a/src/util/mod.rs b/src/util/mod.rs index 07fddbfb..58b7715a 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -8,7 +8,7 @@ pub(crate) use self::fmt::fmt; pub(crate) use self::http_date::HttpDate; pub(crate) use self::iter::IterExt; //pub use language_tags::LanguageTag; -//pub use self::quality_value::{Quality, QualityValue}; +pub(crate) use self::quality_value::QualityValue; pub(crate) use self::seconds::Seconds; pub(crate) use self::value_string::HeaderValueString; @@ -20,7 +20,7 @@ mod flat_csv; mod fmt; mod http_date; mod iter; -//mod quality_value; +mod quality_value; mod seconds; mod value_string; diff --git a/src/util/quality_value.rs b/src/util/quality_value.rs new file mode 100644 index 00000000..abb0362d --- /dev/null +++ b/src/util/quality_value.rs @@ -0,0 +1,231 @@ +use self::sealed::SemiQ; +use std::marker::PhantomData; +use util::FlatCsv; + +/// A CSV list that respects the Quality Values syntax defined in +/// [RFC7321](https://tools.ietf.org/html/rfc7231#section-5.3.1) +/// +/// Many of the request header fields for proactive negotiation use a +/// common parameter, named "q" (case-insensitive), to assign a relative +/// "weight" to the preference for that associated kind of content. This +/// weight is referred to as a "quality value" (or "qvalue") because the +/// same parameter name is often used within server configurations to +/// assign a weight to the relative quality of the various +/// representations that can be selected for a resource. +/// +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct QualityValue { + csv: FlatCsv, + _marker: PhantomData, +} + +mod sealed { + use super::QualityValue; + use std::cmp::Ordering; + use std::convert::{From, TryFrom}; + use std::marker::PhantomData; + + use itertools::Itertools; + use util::{FlatCsv, TryFromValues}; + use HeaderValue; + + pub trait QualityDelimiter { + const STR: &'static str; + } + + /// enum that represents the ';q=' delimiter + #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub enum SemiQ {} + + impl QualityDelimiter for SemiQ { + const STR: &'static str = ";q="; + } + + /// enum that represents the ';level=' delimiter (extremely rare) + #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub enum SemiLevel {} + + impl QualityDelimiter for SemiLevel { + const STR: &'static str = ";level="; + } + + #[derive(Clone, Debug, PartialEq, Eq)] + struct QualityMeta<'a, Sep = SemiQ> { + pub data: &'a str, + pub quality: u16, + _marker: PhantomData, + } + + impl Ord for QualityMeta<'_, Delm> { + fn cmp(&self, other: &Self) -> Ordering { + other.quality.cmp(&self.quality) + } + } + + impl PartialOrd for QualityMeta<'_, Delm> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl<'a, Delm: QualityDelimiter> TryFrom<&'a str> for QualityMeta<'a, Delm> { + type Error = ::Error; + + fn try_from(val: &'a str) -> Result { + let mut parts: Vec<&str> = val.split(Delm::STR).collect(); + + match (parts.pop(), parts.pop()) { + (Some(qual), Some(data)) => { + let parsed: f32 = qual.parse().map_err(|_| ::Error::invalid())?; + let quality = (parsed * 1000_f32) as u16; + + Ok(QualityMeta { + data, + quality, + _marker: PhantomData, + }) + } + // No deliter present, assign a quality value of 1 + (Some(data), None) => Ok(QualityMeta { + data, + quality: 1000_u16, + _marker: PhantomData, + }), + _ => Err(::Error::invalid()), + } + } + } + + impl QualityValue { + pub(crate) fn iter(&self) -> impl Iterator { + self.csv + .iter() + .map(|v| QualityMeta::::try_from(v).unwrap()) + .into_iter() + .sorted() + .map(|pair| pair.data) + .into_iter() + } + } + + impl From for QualityValue { + fn from(csv: FlatCsv) -> Self { + QualityValue { + csv, + _marker: PhantomData, + } + } + } + + impl> TryFrom<(&str, F)> for QualityValue { + type Error = ::Error; + + fn try_from(pair: (&str, F)) -> Result { + let value = HeaderValue::try_from(format!("{}{}{}", pair.0, Delm::STR, pair.1.into())) + .map_err(|_e| ::Error::invalid())?; + Ok(QualityValue { + csv: value.into(), + _marker: PhantomData, + }) + } + } + + impl From for QualityValue { + fn from(value: HeaderValue) -> Self { + QualityValue { + csv: value.into(), + _marker: PhantomData, + } + } + } + + impl<'a, Delm> From<&'a QualityValue> for HeaderValue { + fn from(qual: &'a QualityValue) -> HeaderValue { + qual.csv.value.clone() + } + } + + impl From> for HeaderValue { + fn from(qual: QualityValue) -> HeaderValue { + qual.csv.value + } + } + + impl TryFromValues for QualityValue { + fn try_from_values<'i, I>(values: &mut I) -> Result + where + I: Iterator, + { + let flat: FlatCsv = values.collect(); + Ok(QualityValue::from(flat)) + } + } +} + +#[cfg(test)] +mod tests { + use super::{ + sealed::{SemiLevel, SemiQ}, + QualityValue, + }; + use HeaderValue; + + #[test] + fn multiple_qualities() { + let val = HeaderValue::from_static("gzip;q=1, br;q=0.8"); + let qual = QualityValue::::from(val); + + let mut values = qual.iter(); + assert_eq!(values.next(), Some("gzip")); + assert_eq!(values.next(), Some("br")); + assert_eq!(values.next(), None); + } + + #[test] + fn multiple_qualities_wrong_order() { + let val = HeaderValue::from_static("br;q=0.8, gzip;q=1.0"); + let qual = QualityValue::::from(val); + + let mut values = qual.iter(); + assert_eq!(values.next(), Some("gzip")); + assert_eq!(values.next(), Some("br")); + assert_eq!(values.next(), None); + } + + #[test] + fn multiple_values() { + let val = HeaderValue::from_static("deflate, gzip;q=1, br;q=0.8"); + let qual = QualityValue::::from(val); + + let mut values = qual.iter(); + assert_eq!(values.next(), Some("deflate")); + assert_eq!(values.next(), Some("gzip")); + assert_eq!(values.next(), Some("br")); + assert_eq!(values.next(), None); + } + + #[test] + fn multiple_values_wrong_order() { + let val = HeaderValue::from_static("deflate, br;q=0.8, gzip;q=1, *;q=0.1"); + let qual = QualityValue::::from(val); + + let mut values = qual.iter(); + assert_eq!(values.next(), Some("deflate")); + assert_eq!(values.next(), Some("gzip")); + assert_eq!(values.next(), Some("br")); + assert_eq!(values.next(), Some("*")); + assert_eq!(values.next(), None); + } + + #[test] + fn alternate_delimiter() { + let val = HeaderValue::from_static("deflate, br;level=0.8, gzip;level=1"); + let qual = QualityValue::::from(val); + + let mut values = qual.iter(); + assert_eq!(values.next(), Some("deflate")); + assert_eq!(values.next(), Some("gzip")); + assert_eq!(values.next(), Some("br")); + assert_eq!(values.next(), None); + } +}