From 2c502a09e6d641acc91fcf19abcc7fd6505a58ac Mon Sep 17 00:00:00 2001 From: Dan Dumont Date: Thu, 4 Apr 2024 11:34:05 -0400 Subject: [PATCH] useful headers for rocket --- core/http/src/header/accept_encoding.rs | 369 ++++++++++++++++++ core/http/src/header/content_coding.rs | 309 +++++++++++++++ core/http/src/header/content_encoding.rs | 187 +++++++++ core/http/src/header/known_content_codings.rs | 10 + core/http/src/header/media_type.rs | 2 +- core/http/src/header/mod.rs | 8 + core/http/src/parse/accept_encoding.rs | 75 ++++ core/http/src/parse/content_coding.rs | 119 ++++++ core/http/src/parse/mod.rs | 4 + 9 files changed, 1082 insertions(+), 1 deletion(-) create mode 100644 core/http/src/header/accept_encoding.rs create mode 100644 core/http/src/header/content_coding.rs create mode 100644 core/http/src/header/content_encoding.rs create mode 100644 core/http/src/header/known_content_codings.rs create mode 100644 core/http/src/parse/accept_encoding.rs create mode 100644 core/http/src/parse/content_coding.rs diff --git a/core/http/src/header/accept_encoding.rs b/core/http/src/header/accept_encoding.rs new file mode 100644 index 0000000000..ef3eb9c32e --- /dev/null +++ b/core/http/src/header/accept_encoding.rs @@ -0,0 +1,369 @@ +use std::borrow::Cow; +use std::ops::Deref; +use std::str::FromStr; +use std::fmt; + +use crate::{ContentCoding, Header}; +use crate::parse::parse_accept_encoding; + +/// The HTTP Accept-Encoding header. +/// +/// An `AcceptEncoding` header is composed of zero or more content codings, each of which +/// may have an optional weight value (a [`QContentCoding`]). The header is sent by +/// an HTTP client to describe the formats it accepts as well as the order in +/// which it prefers different formats. +/// +/// # Usage +/// +/// The Accept-Encoding header of an incoming request can be retrieved via the +/// [`Request::accept_encoding()`] method. The [`preferred()`] method can be used to +/// retrieve the client's preferred content coding. +/// +/// [`Request::accept_encoding()`]: rocket::Request::accept_encoding() +/// [`preferred()`]: AcceptEncoding::preferred() +/// +/// An `AcceptEncoding` type with a single, common content coding can be easily constructed +/// via provided associated constants. +/// +/// ## Example +/// +/// Construct an `AcceptEncoding` header with a single `gzip` content coding: +/// +/// ```rust +/// # extern crate rocket; +/// use rocket::http::AcceptEncoding; +/// +/// # #[allow(unused_variables)] +/// let accept_gzip = AcceptEncoding::GZIP; +/// ``` +/// +/// # Header +/// +/// `AcceptEncoding` implements `Into
`. As such, it can be used in any context +/// where an `Into
` is expected: +/// +/// ```rust +/// # extern crate rocket; +/// use rocket::http::AcceptEncoding; +/// use rocket::response::Response; +/// +/// let response = Response::build().header(AcceptEncoding::GZIP).finalize(); +/// ``` +#[derive(Debug, Clone)] +pub struct AcceptEncoding(pub(crate) Cow<'static, [QContentCoding]>); + +/// A `ContentCoding` with an associated quality value. +#[derive(Debug, Clone, PartialEq)] +pub struct QContentCoding(pub ContentCoding, pub Option); + +macro_rules! accept_encoding_constructor { + ($($name:ident ($check:ident): $str:expr, $c:expr,)+) => { + $( + #[doc="An `AcceptEncoding` header with the single content coding for"] + #[doc=concat!("**", $str, "**: ", "_", $c, "_")] + #[allow(non_upper_case_globals)] + pub const $name: AcceptEncoding = AcceptEncoding({ + const INNER: &[QContentCoding] = &[QContentCoding(ContentCoding::$name, None)]; + Cow::Borrowed(INNER) + }); + )+ + }; +} + +impl AcceptEncoding { + /// Constructs a new `AcceptEncoding` header from one or more media types. + /// + /// The `items` parameter may be of type `QContentCoding`, `[QContentCoding]`, + /// `&[QContentCoding]` or `Vec`. To prevent additional allocations, + /// prefer to provide inputs of type `QContentCoding`, `[QContentCoding]`, or + /// `Vec`. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::{QContentCoding, ContentCoding, AcceptEncoding}; + /// + /// // Construct an `Accept` via a `Vec`. + /// let gzip_then_deflate = vec![ContentCoding::GZIP, ContentCoding::DEFLATE]; + /// let accept = AcceptEncoding::new(gzip_then_deflate); + /// assert_eq!(accept.preferred().media_type(), &ContentCoding::GZIP); + /// + /// // Construct an `Accept` via an `[QMediaType]`. + /// let accept = Accept::new([MediaType::JSON.into(), MediaType::HTML.into()]); + /// assert_eq!(accept.preferred().media_type(), &MediaType::JSON); + /// + /// // Construct an `Accept` via a `QMediaType`. + /// let accept = Accept::new(QMediaType(MediaType::JSON, None)); + /// assert_eq!(accept.preferred().media_type(), &MediaType::JSON); + /// ``` + #[inline(always)] + pub fn new, M: Into>(items: T) -> AcceptEncoding { + AcceptEncoding(items.into_iter().map(|v| v.into()).collect()) + } + + // TODO: Implement this. + // #[inline(always)] + // pub fn add>(&mut self, content_coding: M) { + // self.0.push(content_coding.into()); + // } + + /// Retrieve the client's preferred content coding. This method follows [RFC + /// 7231 5.3.4]. If the list of content codings is empty, this method returns a + /// content coding of any with no quality value: (`*`). + /// + /// [RFC 7231 5.3.4]: https://tools.ietf.org/html/rfc7231#section-5.3.4 + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::{QContentCoding, ContentCoding, AcceptEncoding}; + /// + /// let qcontent_codings = vec![ + /// QContentCoding(MediaType::DEFLATE, Some(0.3)), + /// QContentCoding(MediaType::GZIP, Some(0.9)), + /// ]; + /// + /// let accept = AcceptEncoding::new(qcontent_codings); + /// assert_eq!(accept.preferred().content_coding(), &MediaType::GZIP); + /// ``` + pub fn preferred(&self) -> &QContentCoding { + static ANY: QContentCoding = QContentCoding(ContentCoding::Any, None); + + // See https://tools.ietf.org/html/rfc7231#section-5.3.4. + let mut all = self.iter(); + let mut preferred = all.next().unwrap_or(&ANY); + for content_coding in all { + if content_coding.weight().is_none() && preferred.weight().is_some() { + // Content coding without a `q` parameter are preferred. + preferred = content_coding; + } else if content_coding.weight_or(0.0) > preferred.weight_or(1.0) { + // Prefer content coding with a greater weight, but if one doesn't + // have a weight, prefer the one we already have. + preferred = content_coding; + } + } + + preferred + } + + /// Retrieve the first media type in `self`, if any. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::{QContentCoding, ContentCoding, AcceptEncoding}; + /// + /// let accept_encoding = AcceptEncoding::new(QContentCoding(ContentCoding::GZIP, None)); + /// assert_eq!(accept_encoding.first(), Some(&ContentCoding::GZIP.into())); + /// ``` + #[inline(always)] + pub fn first(&self) -> Option<&QContentCoding> { + self.iter().next() + } + + /// Returns an iterator over all of the (quality) media types in `self`. + /// Media types are returned in the order in which they appear in the + /// header. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::{QContentCoding, ContentCoding, AcceptEncoding}; + /// + /// let qcontent_codings = vec![ + /// QContentCoding(MediaType::DEFLATE, Some(0.3)) + /// QContentCoding(MediaType::GZIP, Some(0.9)), + /// ]; + /// + /// let accept_encoding = AcceptEncoding::new(qcontent_codings.clone()); + /// + /// let mut iter = accept.iter(); + /// assert_eq!(iter.next(), Some(&qcontent_codings[0])); + /// assert_eq!(iter.next(), Some(&qcontent_codings[1])); + /// assert_eq!(iter.next(), None); + /// ``` + #[inline(always)] + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter() + } + + /// Returns an iterator over all of the (bare) media types in `self`. Media + /// types are returned in the order in which they appear in the header. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::{QMediaType, MediaType, Accept}; + /// + /// let qmedia_types = vec![ + /// QMediaType(MediaType::JSON, Some(0.3)), + /// QMediaType(MediaType::HTML, Some(0.9)) + /// ]; + /// + /// let accept = Accept::new(qmedia_types.clone()); + /// + /// let mut iter = accept.media_types(); + /// assert_eq!(iter.next(), Some(qmedia_types[0].media_type())); + /// assert_eq!(iter.next(), Some(qmedia_types[1].media_type())); + /// assert_eq!(iter.next(), None); + /// ``` + #[inline(always)] + pub fn content_codings(&self) -> impl Iterator + '_ { + self.iter().map(|weighted_cc| weighted_cc.content_coding()) + } + + known_content_codings!(accept_encoding_constructor); +} + +impl> From for AcceptEncoding { + #[inline(always)] + fn from(items: T) -> AcceptEncoding { + AcceptEncoding::new(items.into_iter().map(QContentCoding::from)) + } +} + +impl PartialEq for AcceptEncoding { + fn eq(&self, other: &AcceptEncoding) -> bool { + self.iter().eq(other.iter()) + } +} + +impl fmt::Display for AcceptEncoding { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, content_coding) in self.iter().enumerate() { + if i >= 1 { + write!(f, ", {}", content_coding.0)?; + } else { + write!(f, "{}", content_coding.0)?; + } + } + + Ok(()) + } +} + +impl FromStr for AcceptEncoding { + // Ideally we'd return a `ParseError`, but that requires a lifetime. + type Err = String; + + #[inline] + fn from_str(raw: &str) -> Result { + parse_accept_encoding(raw).map_err(|e| e.to_string()) + } +} + +/// Creates a new `Header` with name `Accept-Encoding` and the value set to the HTTP +/// rendering of this `Accept` header. +impl From for Header<'static> { + #[inline(always)] + fn from(val: AcceptEncoding) -> Self { + Header::new("Accept-Encoding", val.to_string()) + } +} + +impl QContentCoding { + /// Retrieve the weight of the media type, if there is any. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::{ContentCoding, QContentCoding}; + /// + /// let q_coding = QContentCoding(ContentCoding::GZIP, Some(0.3)); + /// assert_eq!(q_coding.weight(), Some(0.3)); + /// ``` + #[inline(always)] + pub fn weight(&self) -> Option { + self.1 + } + + /// Retrieve the weight of the media type or a given default value. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::{ContentCoding, QContentCoding}; + /// + /// let q_coding = QContentCoding(ContentCoding::GZIP, Some(0.3)); + /// assert_eq!(q_coding.weight_or(0.9), 0.3); + /// + /// let q_coding = QContentCoding(ContentCoding::GZIP, None); + /// assert_eq!(q_coding.weight_or(0.9), 0.9); + /// ``` + #[inline(always)] + pub fn weight_or(&self, default: f32) -> f32 { + self.1.unwrap_or(default) + } + + /// Borrow the internal `MediaType`. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::{ContentCoding, QContentCoding}; + /// + /// let q_coding = QContentCoding(ContentCoding::GZIP, Some(0.3)); + /// assert_eq!(q_coding.content_coding(), &ContentCoding::GZIP); + /// ``` + #[inline(always)] + pub fn content_coding(&self) -> &ContentCoding { + &self.0 + } +} + +impl From for QContentCoding { + #[inline(always)] + fn from(content_coding: ContentCoding) -> QContentCoding { + QContentCoding(content_coding, None) + } +} + +impl Deref for QContentCoding { + type Target = ContentCoding; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +mod test { + use crate::{AcceptEncoding, ContentCoding}; + + #[track_caller] + fn assert_preference(string: &str, expect: &str) { + let ae: AcceptEncoding = string.parse().expect("accept_encoding string parse"); + let expected: ContentCoding = expect.parse().expect("content coding parse"); + let preferred = ae.preferred(); + let actual = preferred.content_coding(); + if *actual != expected { + panic!("mismatch for {}: expected {}, got {}", string, expected, actual) + } + } + + #[test] + fn test_preferred() { + assert_preference("deflate", "deflate"); + assert_preference("gzip, deflate", "gzip"); + assert_preference("deflate; q=0.1, gzip", "gzip"); + assert_preference("gzip; q=1, gzip", "gzip"); + + assert_preference("gzip, deflate; q=1", "gzip"); + assert_preference("deflate; q=1, gzip", "gzip"); + + assert_preference("gzip; q=0.1, gzip; q=0.2", "gzip; q=0.2"); + assert_preference("rar; q=0.1, compress; q=0.2", "compress; q=0.2"); + assert_preference("rar; q=0.5, compress; q=0.2", "rar; q=0.5"); + + assert_preference("rar; q=0.5, compress; q=0.2, nonsense", "nonsense"); + } +} diff --git a/core/http/src/header/content_coding.rs b/core/http/src/header/content_coding.rs new file mode 100644 index 0000000000..135da33385 --- /dev/null +++ b/core/http/src/header/content_coding.rs @@ -0,0 +1,309 @@ +use core::f32; +use std::borrow::Cow; +use std::str::FromStr; +use std::fmt; +use std::hash::{Hash, Hasher}; + +use crate::uncased::UncasedStr; +use crate::parse::{Indexed, IndexedStr, parse_content_coding}; +use crate::Source; + +/// An HTTP content coding. +/// +/// # Usage +/// +/// A `ContentCoding` should rarely be used directly. Instead, one is typically used +/// indirectly via types like [`Accept-Encoding`](crate::Accept-Encoding) and +/// [`ContentEncoding`](crate::ContentEncoding), which internally contain `ContentCoding`s. +/// Nonetheless, a `ContentCoding` can be created via the [`ContentCoding::new()`] +/// and [`ContentCoding::with_weight()`]. +/// The preferred method, however, is to create a `ContentCoding` via an associated +/// constant. +/// +/// ## Example +/// +/// A content coding of `gzip` can be instantiated via the +/// [`ContentCoding::GZIP`] constant: +/// +/// ```rust +/// # extern crate rocket; +/// use rocket::http::ContentCoding; +/// +/// let gzip = ContentCoding::GZIP; +/// assert_eq!(gzip.coding(), "gzip"); +/// +/// let gzip = ContentCoding::new("gzip"); +/// assert_eq!(ContentCoding::GZIP, gzip); +/// ``` +/// +/// # Comparison and Hashing +/// +/// The `PartialEq` and `Hash` implementations for `ContentCoding` _do not_ take +/// into account parameters. This means that a content coding of `gzip` is +/// equal to a content coding of `gzip; q=1`, for instance. This is +/// typically the comparison that is desired. +/// +/// If an exact comparison is desired that takes into account parameters, the +/// [`exact_eq()`](ContentCoding::exact_eq()) method can be used. +#[derive(Debug, Clone)] +pub struct ContentCoding { + /// InitCell for the entire content codding string. + pub(crate) source: Source, + /// The top-level type. + pub(crate) coding: IndexedStr<'static>, + /// The parameters, if any. + pub(crate) weight: Option, +} + +macro_rules! content_codings { + // ($($name:ident ($check:ident): $str:expr, $t:expr, $s:expr $(; $k:expr => $v:expr)*,)+) + ($($name:ident ($check:ident): $str:expr, $c:expr,)+) => { + $( + /// Content Coding for + #[doc = concat!("**", $str, "**: ")] + #[doc = concat!("`", $c, "`")] + #[allow(non_upper_case_globals)] + pub const $name: ContentCoding = ContentCoding::new_known( + $c, + $c, + None, + ); + )+ + + /// Returns `true` if this ContentCoding is known to Rocket. In other words, + /// returns `true` if there is an associated constant for `self`. + pub fn is_known(&self) -> bool { + if let Source::Known(_) = self.source { + return true; + } + + $(if self.$check() { return true })+ + false + } + + $( + /// Returns `true` if the top-level and sublevel types of + /// `self` are the same as those of + #[doc = concat!("`ContentCoding::", stringify!($name), "`, ")] + /// i.e + #[doc = concat!("`", $c, "`.")] + #[inline(always)] + pub fn $check(&self) -> bool { + *self == ContentCoding::$name + } + )+ + } +} + +impl ContentCoding { + /// Creates a new `ContentCoding` for `coding`. + /// This should _only_ be used to construct uncommon or custom content codings. + /// Use an associated constant for everything else. + /// + /// # Example + /// + /// Create a custom `rar` content coding: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::ContentCoding; + /// + /// let custom = ContentCoding::new("rar"); + /// assert_eq!(custom.coding(), "rar"); + /// ``` + #[inline] + pub fn new(coding: C) -> ContentCoding + where C: Into> + { + ContentCoding { + source: Source::None, + coding: Indexed::Concrete(coding.into()), + weight: None, + } + } + + /// Sets the weight `weight` on `self`. + /// + /// # Example + /// + /// Create a custom `rar; q=1` content coding: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::ContentCoding; + /// + /// let id = ContentCoding::new("rar").with_weight(1); + /// assert_eq!(id.to_string(), "rar; q=1".to_string()); + /// ``` + pub fn with_weight(mut self, p: f32) -> ContentCoding + { + self.weight = Some(p); + self + } + + /// A `const` variant of [`ContentCoding::with_params()`]. Creates a new + /// `ContentCoding` with coding `coding`, and weight + /// `weight`, which may be empty. + /// + /// # Example + /// + /// Create a custom `rar` content coding: + /// + /// ```rust + /// use rocket::http::ContentCoding; + /// + /// let custom = ContentCoding::const_new("rar", None); + /// assert_eq!(custom.coding(), "rar"); + /// assert_eq!(custom.weight(), None); + /// ``` + #[inline] + pub const fn const_new( + coding: &'static str, + weight: Option, + ) -> ContentCoding { + ContentCoding { + source: Source::None, + coding: Indexed::Concrete(Cow::Borrowed(coding)), + weight: weight, + } + } + + #[inline] + pub(crate) const fn new_known( + source: &'static str, + coding: &'static str, + weight: Option, + ) -> ContentCoding { + ContentCoding { + source: Source::Known(source), + coding: Indexed::Concrete(Cow::Borrowed(coding)), + weight: weight, + } + } + + pub(crate) fn known_source(&self) -> Option<&'static str> { + match self.source { + Source::Known(string) => Some(string), + Source::Custom(Cow::Borrowed(string)) => Some(string), + _ => None + } + } + + /// Returns the coding for this ContentCoding. The return type, + /// `UncasedStr`, has caseless equality comparison and hashing. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::ContentCoding; + /// + /// let gzip = ContentCoding::GZIP; + /// assert_eq!(gzip.coding(), "gzip"); + /// assert_eq!(gzip.top(), "GZIP"); + /// assert_eq!(gzip.top(), "Gzip"); + /// ``` + #[inline] + pub fn coding(&self) -> &UncasedStr { + self.coding.from_source(self.source.as_str()).into() + } + + /// Compares `self` with `other` and returns `true` if `self` and `other` + /// are exactly equal to each other, including with respect to their + /// weight. + /// + /// This is different from the `PartialEq` implementation in that it + /// considers parameters. In particular, `Eq` implies `PartialEq` but + /// `PartialEq` does not imply `Eq`. That is, if `PartialEq` returns false, + /// this function is guaranteed to return false. Similarly, if `exact_eq` + /// returns `true`, `PartialEq` is guaranteed to return true. However, if + /// `PartialEq` returns `true`, `exact_eq` function may or may not return + /// `true`. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::ContentCoding; + /// + /// let gzip = ContentCoding::GZIP; + /// let gzip2 = ContentCoding::new("gzip").with_weight(1); + /// let just_plain = ContentCoding::new("gzip"); + /// + /// // The `PartialEq` implementation doesn't consider parameters. + /// assert!(plain == just_plain); + /// assert!(just_plain == plain2); + /// assert!(plain == plain2); + /// + /// // While `exact_eq` does. + /// assert!(plain.exact_eq(&just_plain)); + /// assert!(!plain2.exact_eq(&just_plain)); + /// assert!(!plain.exact_eq(&plain2)); + /// ``` + pub fn exact_eq(&self, other: &ContentCoding) -> bool { + self == other && self.weight().eq(other.weight()) + } + + /// Returns the weight content coding. + /// + /// # Example + /// + /// The `ContentCoding::GZIP` type has no specified weight: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::ContentCoding; + /// + /// let gzip = ContentCoding::GZIP; + /// let weight = gzip.weight(); + /// assert_eq!(weight, None); + /// ``` + #[inline] + pub fn weight(&self) -> &Option { + &self.weight + } + + known_content_codings!(content_codings); +} + +impl FromStr for ContentCoding { + // Ideally we'd return a `ParseError`, but that requires a lifetime. + type Err = String; + + #[inline] + fn from_str(raw: &str) -> Result { + parse_content_coding(raw).map_err(|e| e.to_string()) + } +} + +impl PartialEq for ContentCoding { + #[inline(always)] + fn eq(&self, other: &ContentCoding) -> bool { + self.coding() == other.coding() + } +} + +impl Eq for ContentCoding { } + +impl Hash for ContentCoding { + #[inline] + fn hash(&self, state: &mut H) { + self.coding().hash(state); + } +} + +impl fmt::Display for ContentCoding { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(src) = self.known_source() { + src.fmt(f) + } else { + write!(f, "{}", self.coding())?; + if let Some(weight) = self.weight() { + write!(f, "; q={}", weight)?; + } + + Ok(()) + } + } +} diff --git a/core/http/src/header/content_encoding.rs b/core/http/src/header/content_encoding.rs new file mode 100644 index 0000000000..13521f4d47 --- /dev/null +++ b/core/http/src/header/content_encoding.rs @@ -0,0 +1,187 @@ +use std::borrow::Cow; +use std::ops::Deref; +use std::str::FromStr; +use std::fmt; + +use crate::header::Header; +use crate::ContentCoding; + +/// Representation of HTTP Content-Encoding. +/// +/// # Usage +/// +/// `ContentEncoding`s should rarely be created directly. Instead, an associated +/// constant should be used; one is declared for most commonly used content +/// types. +/// +/// ## Example +/// +/// A Content-Encoding of `gzip` can be instantiated via the +/// `GZIP` constant: +/// +/// ```rust +/// # extern crate rocket; +/// use rocket::http::ContentEncoding; +/// +/// # #[allow(unused_variables)] +/// let html = ContentEncoding::GZIP; +/// ``` +/// +/// # Header +/// +/// `ContentEncoding` implements `Into
`. As such, it can be used in any +/// context where an `Into
` is expected: +/// +/// ```rust +/// # extern crate rocket; +/// use rocket::http::ContentEncoding; +/// use rocket::response::Response; +/// +/// let response = Response::build().header(ContentEncoding::GZIP).finalize(); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ContentEncoding(pub ContentCoding); + +macro_rules! content_encodings { + ($($name:ident ($check:ident): $str:expr, $c:expr,)+) => { + $( + + /// Content Encoding for + #[doc = concat!("**", $str, "**: ")] + #[doc = concat!("`", $c, "`")] + + #[allow(non_upper_case_globals)] + pub const $name: ContentEncoding = ContentEncoding(ContentCoding::$name); + )+ +}} + +impl ContentEncoding { + /// Creates a new `ContentEncoding` with `coding`. + /// This should _only_ be used to construct uncommon or custom content + /// types. Use an associated constant for everything else. + /// + /// # Example + /// + /// Create a custom `foo` content encoding: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::ContentEncoding; + /// + /// let custom = ContentEncoding::new("foo"); + /// assert_eq!(custom.content_coding(), "foo"); + /// ``` + #[inline(always)] + pub fn new(coding: S) -> ContentEncoding + where S: Into> + { + ContentEncoding(ContentCoding::new(coding)) + } + + /// Borrows the inner `ContentCoding` of `self`. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::{ContentEncoding, ContentCoding}; + /// + /// let http = ContentEncoding::GZIP; + /// let content_coding = http.content_coding(); + /// ``` + #[inline(always)] + pub fn content_coding(&self) -> &ContentCoding { + &self.0 + } + + known_content_codings!(content_encodings); +} + +impl Default for ContentEncoding { + /// Returns a ContentEncoding of `Any`, or `*`. + #[inline(always)] + fn default() -> ContentEncoding { + ContentEncoding::Any + } +} + +impl Deref for ContentEncoding { + type Target = ContentCoding; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for ContentEncoding { + type Err = String; + + /// Parses a `ContentEncoding` from a given Content-Encoding header value. + /// + /// # Examples + /// + /// Parsing a `gzip`: + /// + /// ```rust + /// # extern crate rocket; + /// use std::str::FromStr; + /// use rocket::http::ContentEncoding; + /// + /// let gzip = ContentEncoding::from_str("gzip").unwrap(); + /// assert!(gzip.is_known()); + /// assert_eq!(gzip, ContentEncoding::GZIP); + /// ``` + /// + /// Parsing an invalid Content-Encoding value: + /// + /// ```rust + /// # extern crate rocket; + /// use std::str::FromStr; + /// use rocket::http::ContentEncoding; + /// + /// let custom = ContentEncoding::from_str("12ec/.322r"); + /// assert!(custom.is_err()); + /// ``` + #[inline(always)] + fn from_str(raw: &str) -> Result { + ContentCoding::from_str(raw).map(ContentEncoding) + } +} + +impl From for ContentEncoding { + fn from(content_coding: ContentCoding) -> Self { + ContentEncoding(content_coding) + } +} + +impl fmt::Display for ContentEncoding { + /// Formats the ContentEncoding as an HTTP Content-Encoding value. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::ContentEncoding; + /// + /// let cc = format!("{}", ContentEncoding::GZIP); + /// assert_eq!(cc, "gzip"); + /// ``` + #[inline(always)] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Creates a new `Header` with name `Content-Encoding` and the value set to the +/// HTTP rendering of this Content-Encoding. +impl From for Header<'static> { + #[inline(always)] + fn from(content_encoding: ContentEncoding) -> Self { + if let Some(src) = content_encoding.known_source() { + Header::new("Content-Encoding", src) + } else { + Header::new("Content-Encoding", content_encoding.to_string()) + } + } +} diff --git a/core/http/src/header/known_content_codings.rs b/core/http/src/header/known_content_codings.rs new file mode 100644 index 0000000000..f8ee6b2f20 --- /dev/null +++ b/core/http/src/header/known_content_codings.rs @@ -0,0 +1,10 @@ +macro_rules! known_content_codings { + ($cont:ident) => ($cont! { + Any (is_any): "any content coding", "*", + // BR (is_br): "Brotli Compressed Data Format", "br", + // COMPRESS (is_compress): "UNIX \"compress\" data format", "compress", + // DEFLATE (is_deflate): "\"deflate\" compressed data inside the \"zlib\" data format", "deflate", + GZIP (is_gzip): "GZIP file format", "gzip", + IDENTITY (is_identity): "Reserved", "identity", + }) +} diff --git a/core/http/src/header/media_type.rs b/core/http/src/header/media_type.rs index 691ac24db7..b6a3ca5a15 100644 --- a/core/http/src/header/media_type.rs +++ b/core/http/src/header/media_type.rs @@ -621,7 +621,7 @@ impl Extend<(IndexedStr<'static>, IndexedStr<'static>)> for MediaParams { impl Source { #[inline] - fn as_str(&self) -> Option<&str> { + pub(crate) fn as_str(&self) -> Option<&str> { match *self { Source::Known(s) => Some(s), Source::Custom(ref s) => Some(s.borrow()), diff --git a/core/http/src/header/mod.rs b/core/http/src/header/mod.rs index 653b786348..3e37d5bed8 100644 --- a/core/http/src/header/mod.rs +++ b/core/http/src/header/mod.rs @@ -1,13 +1,21 @@ #[macro_use] mod known_media_types; +#[macro_use] +mod known_content_codings; mod media_type; +mod content_coding; mod content_type; mod accept; +mod accept_encoding; +mod content_encoding; mod header; mod proxy_proto; pub use self::content_type::ContentType; +pub use self::content_encoding::ContentEncoding; pub use self::accept::{Accept, QMediaType}; +pub use self::accept_encoding::{AcceptEncoding, QContentCoding}; +pub use self::content_coding::ContentCoding; pub use self::media_type::MediaType; pub use self::header::{Header, HeaderMap}; pub use self::proxy_proto::ProxyProto; diff --git a/core/http/src/parse/accept_encoding.rs b/core/http/src/parse/accept_encoding.rs new file mode 100644 index 0000000000..3698e1f4a0 --- /dev/null +++ b/core/http/src/parse/accept_encoding.rs @@ -0,0 +1,75 @@ +use pear::macros::parser; +use pear::combinators::{series, surrounded}; + +use crate::{AcceptEncoding, QContentCoding}; +use crate::parse::checkers::is_whitespace; +use crate::parse::content_coding::content_coding; + +type Input<'a> = pear::input::Pear>; +type Result<'a, T> = pear::input::Result>; + +#[parser] +fn weighted_content_coding<'a>(input: &mut Input<'a>) -> Result<'a, QContentCoding> { + let content_coding = content_coding()?; + let weight = match content_coding.weight() { + Some(v) => Some(*v), + _ => None + }; + + QContentCoding(content_coding, weight) +} + +#[parser] +fn accept_encoding<'a>(input: &mut Input<'a>) -> Result<'a, AcceptEncoding> { + let vec = series(|i| surrounded(i, weighted_content_coding, is_whitespace), ',')?; + AcceptEncoding(std::borrow::Cow::Owned(vec)) +} + +pub fn parse_accept_encoding(input: &str) -> Result<'_, AcceptEncoding> { + parse!(accept_encoding: Input::new(input)) +} + +#[cfg(test)] +mod test { + use crate::ContentCoding; + use super::parse_accept_encoding; + + macro_rules! assert_parse { + ($string:expr) => ({ + match parse_accept_encoding($string) { + Ok(ae) => ae, + Err(e) => panic!("{:?} failed to parse: {}", $string, e) + } + }); + } + + macro_rules! assert_parse_eq { + ($string:expr, [$($cc:expr),*]) => ({ + let expected = vec![$($cc),*]; + let result = assert_parse!($string); + for (i, wcc) in result.iter().enumerate() { + assert_eq!(wcc.content_coding(), &expected[i]); + } + }); + } + + #[test] + fn check_does_parse() { + assert_parse!("gzip"); + assert_parse!("gzip; q=1"); + assert_parse!("*, gzip; q=1.0, rar, deflate"); + assert_parse!("rar, deflate"); + assert_parse!("deflate;q=0.3, gzip;q=0.7, rar;q=0.4, *;q=0.5"); + } + + #[test] + fn check_parse_eq() { + assert_parse_eq!("gzip", [ContentCoding::GZIP]); + assert_parse_eq!("gzip, deflate", + [ContentCoding::GZIP, ContentCoding::new("deflate")]); + assert_parse_eq!("gzip; q=1, deflate", + [ContentCoding::GZIP, ContentCoding::new("deflate")]); + assert_parse_eq!("gzip, gzip; q=0.1, gzip; q=0.2", + [ContentCoding::GZIP, ContentCoding::GZIP, ContentCoding::GZIP]); + } +} diff --git a/core/http/src/parse/content_coding.rs b/core/http/src/parse/content_coding.rs new file mode 100644 index 0000000000..b96c85821f --- /dev/null +++ b/core/http/src/parse/content_coding.rs @@ -0,0 +1,119 @@ +use std::borrow::Cow; + +use pear::input::Extent; +use pear::macros::{parser, parse}; +use pear::parsers::*; +use pear::combinators::surrounded; + +use crate::header::{ContentCoding, Source}; +use crate::parse::checkers::{is_valid_token, is_whitespace}; + +type Input<'a> = pear::input::Pear>; +type Result<'a, T> = pear::input::Result>; + +#[parser] +fn coding_param<'a>(input: &mut Input<'a>) -> Result<'a, Extent<&'a str>> { + let _ = (take_some_while_until(|c| matches!(c, 'Q' | 'q'), '=')?, eat('=')?).0; + let value = take_some_while_until(|c| matches!(c, '0'..='9' | '.'), ';')?; + + value +} + +#[parser] +pub fn content_coding<'a>(input: &mut Input<'a>) -> Result<'a, ContentCoding> { + let (coding, weight) = { + let coding = take_some_while_until(is_valid_token, ';')?; + let weight = match eat(input, ';') { + Ok(_) => surrounded(coding_param, is_whitespace)?, + Err(_) => Extent {start: 0, end: 0, values: ""}, + }; + + (coding, weight) + }; + + let weight = match weight.len() { + len if len > 0 && len <= 5 => match weight.parse::().ok() { + Some(q) if q > 1. => parse_error!("q value must be <= 1")?, + Some(q) if q < 0. => parse_error!("q value must be > 0")?, + Some(q) => Some(q), + None => parse_error!("invalid content coding weight")? + }, + _ => None, + }; + + ContentCoding { + weight: weight, + source: Source::Custom(Cow::Owned(input.start.to_string())), + coding: coding.into(), + } +} + +pub fn parse_content_coding(input: &str) -> Result<'_, ContentCoding> { + parse!(content_coding: Input::new(input)) +} + +#[cfg(test)] +mod test { + use crate::ContentCoding; + use super::parse_content_coding; + + macro_rules! assert_no_parse { + ($string:expr) => ({ + let result: Result<_, _> = parse_content_coding($string).into(); + if result.is_ok() { + panic!("{:?} parsed unexpectedly.", $string) + } + }); + } + + macro_rules! assert_parse { + ($string:expr) => ({ + match parse_content_coding($string) { + Ok(content_coding) => content_coding, + Err(e) => panic!("{:?} failed to parse: {}", $string, e) + } + }); + } + + macro_rules! assert_parse_eq { + (@full $string:expr, $result:expr, $weight:expr) => ({ + let result = assert_parse!($string); + assert_eq!(result, $result); + + assert_eq!(*result.weight(), $weight); + }); + + (from: $string:expr, into: $result:expr) + => (assert_parse_eq!(@full $string, $result, None)); + (from: $string:expr, into: $result:expr, weight: $weight:literal) + => (assert_parse_eq!(@full $string, $result, Some($weight))); + } + + #[test] + fn check_does_parse() { + assert_parse!("*"); + assert_parse!("rar"); + assert_parse!("gzip"); + assert_parse!("identity"); + } + + #[test] + fn check_parse_eq() { + assert_parse_eq!(from: "gzip", into: ContentCoding::GZIP); + assert_parse_eq!(from: "gzip; q=1", into: ContentCoding::GZIP, weight: 1f32); + + assert_parse_eq!(from: "*", into: ContentCoding::Any); + assert_parse_eq!(from: "rar", into: ContentCoding::new("rar")); + } + + #[test] + fn check_params_do_parse() { + assert_parse!("*; q=1"); + } + + #[test] + fn test_bad_parses() { + assert_no_parse!("*; q=1;"); + assert_no_parse!("*; q=1; q=2"); + } +} diff --git a/core/http/src/parse/mod.rs b/core/http/src/parse/mod.rs index 3c0a1f8705..66aba3d94b 100644 --- a/core/http/src/parse/mod.rs +++ b/core/http/src/parse/mod.rs @@ -1,10 +1,14 @@ mod media_type; mod accept; +mod accept_encoding; +mod content_coding; mod checkers; mod indexed; pub use self::media_type::*; pub use self::accept::*; +pub use self::accept_encoding::*; +pub use self::content_coding::*; pub mod uri;