diff --git a/src/lib.rs b/src/lib.rs index c332fb54..3123d7cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,6 +126,9 @@ pub mod mime; pub mod other; pub mod proxies; pub mod server; +pub mod trace; +pub mod transfer; +pub mod upgrade; mod body; mod error; @@ -139,9 +142,6 @@ mod status; mod status_code; mod version; -pub mod trace; -pub mod upgrade; - pub use body::Body; pub use error::{Error, Result}; pub use method::Method; diff --git a/src/transfer/encoding.rs b/src/transfer/encoding.rs new file mode 100644 index 00000000..c64b3a45 --- /dev/null +++ b/src/transfer/encoding.rs @@ -0,0 +1,64 @@ +use crate::headers::HeaderValue; +use std::fmt::{self, Display}; + +/// Available compression algorithms. +/// +/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding#Directives) +#[non_exhaustive] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Encoding { + /// Send a series of chunks. + Chunked, + /// 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. + pub(crate) fn from_str(s: &str) -> Option { + let s = s.trim(); + + // We're dealing with an empty string. + if s.is_empty() { + return None; + } + + match s { + "chunked" => Some(Encoding::Chunked), + "gzip" => Some(Encoding::Gzip), + "deflate" => Some(Encoding::Deflate), + "br" => Some(Encoding::Brotli), + "zstd" => Some(Encoding::Zstd), + "identity" => Some(Encoding::Identity), + _ => None, + } + } +} + +impl Display for Encoding { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Encoding::Gzip => write!(f, "gzip"), + Encoding::Deflate => write!(f, "deflate"), + Encoding::Brotli => write!(f, "br"), + Encoding::Zstd => write!(f, "zstd"), + Encoding::Identity => write!(f, "identity"), + Encoding::Chunked => write!(f, "chunked"), + } + } +} + +impl From for HeaderValue { + fn from(directive: Encoding) -> Self { + let s = directive.to_string(); + unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) } + } +} diff --git a/src/transfer/encoding_proposal.rs b/src/transfer/encoding_proposal.rs new file mode 100644 index 00000000..0961b84b --- /dev/null +++ b/src/transfer/encoding_proposal.rs @@ -0,0 +1,147 @@ +use crate::ensure; +use crate::headers::HeaderValue; +use crate::transfer::Encoding; +use crate::utils::parse_weight; + +use std::cmp::{Ordering, PartialEq}; +use std::ops::{Deref, DerefMut}; + +/// A proposed `Encoding` in `AcceptEncoding`. +/// +/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/TE#Directives) +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct EncodingProposal { + /// The proposed encoding. + pub(crate) encoding: Encoding, + + /// The weight of the proposal. + /// + /// This is a number between 0.0 and 1.0, and is max 3 decimal points. + weight: Option, +} + +impl EncodingProposal { + /// Create a new instance of `EncodingProposal`. + pub fn new(encoding: impl Into, weight: Option) -> crate::Result { + if let Some(weight) = weight { + ensure!( + weight.is_sign_positive() && weight <= 1.0, + "EncodingProposal should have a weight between 0.0 and 1.0" + ) + } + + Ok(Self { + encoding: encoding.into(), + weight, + }) + } + + /// Get the proposed encoding. + pub fn encoding(&self) -> &Encoding { + &self.encoding + } + + /// Get the weight of the proposal. + pub fn weight(&self) -> Option { + self.weight + } + + pub(crate) fn from_str(s: &str) -> crate::Result> { + let mut parts = s.split(';'); + let encoding = match Encoding::from_str(parts.next().unwrap()) { + Some(encoding) => encoding, + None => return Ok(None), + }; + let weight = parts.next().map(parse_weight).transpose()?; + + Ok(Some(Self::new(encoding, weight)?)) + } +} + +impl From for EncodingProposal { + fn from(encoding: Encoding) -> Self { + Self { + encoding, + weight: None, + } + } +} + +impl PartialEq for EncodingProposal { + fn eq(&self, other: &Encoding) -> bool { + self.encoding == *other + } +} + +impl PartialEq for &EncodingProposal { + fn eq(&self, other: &Encoding) -> bool { + self.encoding == *other + } +} + +impl Deref for EncodingProposal { + type Target = Encoding; + fn deref(&self) -> &Self::Target { + &self.encoding + } +} + +impl DerefMut for EncodingProposal { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.encoding + } +} + +// NOTE: Firefox populates Accept-Encoding as `gzip, deflate, br`. This means +// when parsing encodings we should choose the last value in the list under +// equal weights. This impl doesn't know which value was passed later, so that +// behavior needs to be handled separately. +// +// NOTE: This comparison does not include a notion of `*` (any value is valid). +// that needs to be handled separately. +impl PartialOrd for EncodingProposal { + fn partial_cmp(&self, other: &Self) -> Option { + match (self.weight, other.weight) { + (Some(left), Some(right)) => left.partial_cmp(&right), + (Some(_), None) => Some(Ordering::Greater), + (None, Some(_)) => Some(Ordering::Less), + (None, None) => None, + } + } +} + +impl From for HeaderValue { + fn from(entry: EncodingProposal) -> HeaderValue { + let s = match entry.weight { + Some(weight) => format!("{};q={:.3}", entry.encoding, weight), + None => entry.encoding.to_string(), + }; + unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn smoke() -> crate::Result<()> { + let _ = EncodingProposal::new(Encoding::Gzip, Some(0.0)).unwrap(); + let _ = EncodingProposal::new(Encoding::Gzip, Some(0.5)).unwrap(); + let _ = EncodingProposal::new(Encoding::Gzip, Some(1.0)).unwrap(); + Ok(()) + } + + #[test] + fn error_code_500() -> crate::Result<()> { + let err = EncodingProposal::new(Encoding::Gzip, Some(1.1)).unwrap_err(); + assert_eq!(err.status(), 500); + + let err = EncodingProposal::new(Encoding::Gzip, Some(-0.1)).unwrap_err(); + assert_eq!(err.status(), 500); + + let err = EncodingProposal::new(Encoding::Gzip, Some(-0.0)).unwrap_err(); + assert_eq!(err.status(), 500); + Ok(()) + } +} diff --git a/src/transfer/mod.rs b/src/transfer/mod.rs new file mode 100644 index 00000000..38cc6753 --- /dev/null +++ b/src/transfer/mod.rs @@ -0,0 +1,13 @@ +//! HTTP transfer headers. +//! +//! [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Transfer_coding) + +mod encoding; +mod encoding_proposal; +mod te; +mod transfer_encoding; + +pub use encoding::Encoding; +pub use encoding_proposal::EncodingProposal; +pub use te::TE; +pub use transfer_encoding::TransferEncoding; diff --git a/src/transfer/te.rs b/src/transfer/te.rs new file mode 100644 index 00000000..7be659e6 --- /dev/null +++ b/src/transfer/te.rs @@ -0,0 +1,430 @@ +use crate::headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, ACCEPT_ENCODING}; +use crate::transfer::{Encoding, EncodingProposal, TransferEncoding}; +use crate::utils::sort_by_weight; +use crate::{Error, StatusCode}; + +use std::fmt::{self, Debug, Write}; +use std::option; +use std::slice; + +/// Client header advertising the transfer encodings the user agent is willing to +/// accept. +/// +/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/TE) +/// +/// # Specifications +/// +/// - [RFC 7230, section 4.3: TE](https://tools.ietf.org/html/rfc7230#section-4.3) +/// +/// # Examples +/// +/// ``` +/// # fn main() -> http_types::Result<()> { +/// # +/// use http_types::transfer::{TE, TransferEncoding, Encoding, EncodingProposal}; +/// use http_types::Response; +/// +/// let mut te = TE::new(); +/// te.push(EncodingProposal::new(Encoding::Brotli, Some(0.8))?); +/// te.push(EncodingProposal::new(Encoding::Gzip, Some(0.4))?); +/// te.push(EncodingProposal::new(Encoding::Identity, None)?); +/// +/// let mut res = Response::new(200); +/// let encoding = te.negotiate(&[Encoding::Brotli, Encoding::Gzip])?; +/// encoding.apply(&mut res); +/// +/// assert_eq!(res["Content-Encoding"], "br"); +/// # +/// # Ok(()) } +/// ``` +pub struct TE { + wildcard: bool, + entries: Vec, +} + +impl TE { + /// Create a new instance of `TE`. + pub fn new() -> Self { + Self { + entries: vec![], + wildcard: false, + } + } + + /// Create an instance of `TE` from a `Headers` instance. + pub fn from_headers(headers: impl AsRef) -> crate::Result> { + let mut entries = vec![]; + let headers = match headers.as_ref().get(ACCEPT_ENCODING) { + Some(headers) => headers, + None => return Ok(None), + }; + + let mut wildcard = false; + + for value in headers { + for part in value.as_str().trim().split(',') { + let part = part.trim(); + + // Handle empty strings, and wildcard directives. + if part.is_empty() { + continue; + } else if part == "*" { + wildcard = true; + continue; + } + + // Try and parse a directive from a str. If the directive is + // unkown we skip it. + if let Some(entry) = EncodingProposal::from_str(part)? { + entries.push(entry); + } + } + } + + Ok(Some(Self { entries, wildcard })) + } + + /// Push a directive into the list of entries. + pub fn push(&mut self, prop: impl Into) { + self.entries.push(prop.into()); + } + + /// Returns `true` if a wildcard directive was passed. + pub fn wildcard(&self) -> bool { + self.wildcard + } + + /// Set the wildcard directive. + pub fn set_wildcard(&mut self, wildcard: bool) { + self.wildcard = wildcard + } + + /// Sort the header directives by weight. + /// + /// Headers with a higher `q=` value will be returned first. If two + /// directives have the same weight, the directive that was declared later + /// will be returned first. + pub fn sort(&mut self) { + sort_by_weight(&mut self.entries); + } + + /// Determine the most suitable `Content-Type` encoding. + /// + /// # Errors + /// + /// If no suitable encoding is found, an error with the status of `406` will be returned. + pub fn negotiate(&mut self, available: &[Encoding]) -> crate::Result { + // Start by ordering the encodings. + self.sort(); + + // Try and find the first encoding that matches. + for encoding in &self.entries { + if available.contains(&encoding) { + return Ok(encoding.into()); + } + } + + // If no encoding matches and wildcard is set, send whichever encoding we got. + if self.wildcard { + if let Some(encoding) = available.iter().next() { + return Ok(encoding.into()); + } + } + + let mut err = Error::new_adhoc("No suitable ContentEncoding found"); + err.set_status(StatusCode::NotAcceptable); + Err(err) + } + + /// Sets the `Accept-Encoding` header. + pub fn apply(&self, mut headers: impl AsMut) { + headers.as_mut().insert(ACCEPT_ENCODING, self.value()); + } + + /// Get the `HeaderName`. + pub fn name(&self) -> HeaderName { + ACCEPT_ENCODING + } + + /// Get the `HeaderValue`. + pub fn value(&self) -> HeaderValue { + let mut output = String::new(); + for (n, directive) in self.entries.iter().enumerate() { + let directive: HeaderValue = directive.clone().into(); + match n { + 0 => write!(output, "{}", directive).unwrap(), + _ => write!(output, ", {}", directive).unwrap(), + }; + } + + if self.wildcard { + match output.len() { + 0 => write!(output, "*").unwrap(), + _ => write!(output, ", *").unwrap(), + } + } + + // SAFETY: the internal string is validated to be ASCII. + unsafe { HeaderValue::from_bytes_unchecked(output.into()) } + } + + /// An iterator visiting all entries. + pub fn iter(&self) -> Iter<'_> { + Iter { + inner: self.entries.iter(), + } + } + + /// An iterator visiting all entries. + pub fn iter_mut(&mut self) -> IterMut<'_> { + IterMut { + inner: self.entries.iter_mut(), + } + } +} + +impl IntoIterator for TE { + type Item = EncodingProposal; + type IntoIter = IntoIter; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + IntoIter { + inner: self.entries.into_iter(), + } + } +} + +impl<'a> IntoIterator for &'a TE { + type Item = &'a EncodingProposal; + type IntoIter = Iter<'a>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a> IntoIterator for &'a mut TE { + type Item = &'a mut EncodingProposal; + type IntoIter = IterMut<'a>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +/// A borrowing iterator over entries in `TE`. +#[derive(Debug)] +pub struct IntoIter { + inner: std::vec::IntoIter, +} + +impl Iterator for IntoIter { + type Item = EncodingProposal; + + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +/// A lending iterator over entries in `TE`. +#[derive(Debug)] +pub struct Iter<'a> { + inner: slice::Iter<'a, EncodingProposal>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = &'a EncodingProposal; + + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +/// A mutable iterator over entries in `TE`. +#[derive(Debug)] +pub struct IterMut<'a> { + inner: slice::IterMut<'a, EncodingProposal>, +} + +impl<'a> Iterator for IterMut<'a> { + type Item = &'a mut EncodingProposal; + + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl ToHeaderValues for TE { + type Iter = option::IntoIter; + fn to_header_values(&self) -> crate::Result { + // A HeaderValue will always convert into itself. + Ok(self.value().to_header_values().unwrap()) + } +} + +impl Debug for TE { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut list = f.debug_list(); + for directive in &self.entries { + list.entry(directive); + } + list.finish() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::transfer::Encoding; + use crate::Response; + + #[test] + fn smoke() -> crate::Result<()> { + let mut accept = TE::new(); + accept.push(Encoding::Gzip); + + let mut headers = Response::new(200); + accept.apply(&mut headers); + + let accept = TE::from_headers(headers)?.unwrap(); + assert_eq!(accept.iter().next().unwrap(), Encoding::Gzip); + Ok(()) + } + + #[test] + fn wildcard() -> crate::Result<()> { + let mut accept = TE::new(); + accept.set_wildcard(true); + + let mut headers = Response::new(200); + accept.apply(&mut headers); + + let accept = TE::from_headers(headers)?.unwrap(); + assert!(accept.wildcard()); + Ok(()) + } + + #[test] + fn wildcard_and_header() -> crate::Result<()> { + let mut accept = TE::new(); + accept.push(Encoding::Gzip); + accept.set_wildcard(true); + + let mut headers = Response::new(200); + accept.apply(&mut headers); + + let accept = TE::from_headers(headers)?.unwrap(); + assert!(accept.wildcard()); + assert_eq!(accept.iter().next().unwrap(), Encoding::Gzip); + Ok(()) + } + + #[test] + fn iter() -> crate::Result<()> { + let mut accept = TE::new(); + accept.push(Encoding::Gzip); + accept.push(Encoding::Brotli); + + let mut headers = Response::new(200); + accept.apply(&mut headers); + + let accept = TE::from_headers(headers)?.unwrap(); + let mut accept = accept.iter(); + assert_eq!(accept.next().unwrap(), Encoding::Gzip); + assert_eq!(accept.next().unwrap(), Encoding::Brotli); + Ok(()) + } + + #[test] + fn reorder_based_on_weight() -> crate::Result<()> { + let mut accept = TE::new(); + accept.push(EncodingProposal::new(Encoding::Gzip, Some(0.4))?); + accept.push(EncodingProposal::new(Encoding::Identity, None)?); + accept.push(EncodingProposal::new(Encoding::Brotli, Some(0.8))?); + + let mut headers = Response::new(200); + accept.apply(&mut headers); + + let mut accept = TE::from_headers(headers)?.unwrap(); + accept.sort(); + let mut accept = accept.iter(); + assert_eq!(accept.next().unwrap(), Encoding::Brotli); + assert_eq!(accept.next().unwrap(), Encoding::Gzip); + assert_eq!(accept.next().unwrap(), Encoding::Identity); + Ok(()) + } + + #[test] + fn reorder_based_on_weight_and_location() -> crate::Result<()> { + let mut accept = TE::new(); + accept.push(EncodingProposal::new(Encoding::Identity, None)?); + accept.push(EncodingProposal::new(Encoding::Gzip, None)?); + accept.push(EncodingProposal::new(Encoding::Brotli, Some(0.8))?); + + let mut res = Response::new(200); + accept.apply(&mut res); + + let mut accept = TE::from_headers(res)?.unwrap(); + accept.sort(); + let mut accept = accept.iter(); + assert_eq!(accept.next().unwrap(), Encoding::Brotli); + assert_eq!(accept.next().unwrap(), Encoding::Gzip); + assert_eq!(accept.next().unwrap(), Encoding::Identity); + Ok(()) + } + + #[test] + fn negotiate() -> crate::Result<()> { + let mut accept = TE::new(); + accept.push(EncodingProposal::new(Encoding::Brotli, Some(0.8))?); + accept.push(EncodingProposal::new(Encoding::Gzip, Some(0.4))?); + accept.push(EncodingProposal::new(Encoding::Identity, None)?); + + assert_eq!( + accept.negotiate(&[Encoding::Brotli, Encoding::Gzip])?, + Encoding::Brotli, + ); + Ok(()) + } + + #[test] + fn negotiate_not_acceptable() -> crate::Result<()> { + let mut accept = TE::new(); + let err = accept.negotiate(&[Encoding::Gzip]).unwrap_err(); + assert_eq!(err.status(), 406); + + let mut accept = TE::new(); + accept.push(EncodingProposal::new(Encoding::Brotli, Some(0.8))?); + let err = accept.negotiate(&[Encoding::Gzip]).unwrap_err(); + assert_eq!(err.status(), 406); + Ok(()) + } + + #[test] + fn negotiate_wildcard() -> crate::Result<()> { + let mut accept = TE::new(); + accept.push(EncodingProposal::new(Encoding::Brotli, Some(0.8))?); + accept.set_wildcard(true); + + assert_eq!(accept.negotiate(&[Encoding::Gzip])?, Encoding::Gzip); + Ok(()) + } +} diff --git a/src/transfer/transfer_encoding.rs b/src/transfer/transfer_encoding.rs new file mode 100644 index 00000000..2f3eb7ca --- /dev/null +++ b/src/transfer/transfer_encoding.rs @@ -0,0 +1,148 @@ +use crate::headers::{HeaderName, HeaderValue, Headers, ToHeaderValues, CONTENT_ENCODING}; +use crate::transfer::{Encoding, EncodingProposal}; + +use std::fmt::{self, Debug}; +use std::ops::{Deref, DerefMut}; +use std::option; + +/// The form of encoding used to safely transfer the payload body to the user. +/// +/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding) +/// +/// # Specifications +/// +/// - [RFC 7230, section 3.3.1: Transfer-Encoding](https://tools.ietf.org/html/rfc7230#section-3.3.1) +/// +/// # Examples +/// +/// ``` +/// # fn main() -> http_types::Result<()> { +/// # +/// use http_types::Response; +/// use http_types::transfer::{TransferEncoding, Encoding}; +/// let mut encoding = TransferEncoding::new(Encoding::Chunked); +/// +/// let mut res = Response::new(200); +/// encoding.apply(&mut res); +/// +/// let encoding = TransferEncoding::from_headers(res)?.unwrap(); +/// assert_eq!(encoding, &Encoding::Chunked); +/// # +/// # Ok(()) } +/// ``` +pub struct TransferEncoding { + inner: Encoding, +} + +impl TransferEncoding { + /// Create a new instance of `CacheControl`. + pub fn new(encoding: Encoding) -> Self { + Self { inner: encoding } + } + + /// Create a new instance from headers. + pub fn from_headers(headers: impl AsRef) -> crate::Result> { + let headers = match headers.as_ref().get(CONTENT_ENCODING) { + Some(headers) => headers, + None => return Ok(None), + }; + + let mut inner = None; + + for value in headers { + if let Some(entry) = Encoding::from_str(value.as_str()) { + inner = Some(entry); + } + } + + let inner = inner.expect("Headers instance with no entries found"); + Ok(Some(Self { inner })) + } + + /// Sets the `Content-Encoding` header. + pub fn apply(&self, mut headers: impl AsMut) { + headers.as_mut().insert(CONTENT_ENCODING, self.value()); + } + + /// Get the `HeaderName`. + pub fn name(&self) -> HeaderName { + CONTENT_ENCODING + } + + /// Get the `HeaderValue`. + pub fn value(&self) -> HeaderValue { + self.inner.into() + } + + /// Access the encoding kind. + pub fn encoding(&self) -> Encoding { + self.inner + } +} + +impl ToHeaderValues for TransferEncoding { + type Iter = option::IntoIter; + fn to_header_values(&self) -> crate::Result { + // A HeaderValue will always convert into itself. + Ok(self.value().to_header_values().unwrap()) + } +} + +impl Deref for TransferEncoding { + type Target = Encoding; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for TransferEncoding { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl PartialEq for TransferEncoding { + fn eq(&self, other: &Encoding) -> bool { + &self.inner == other + } +} + +impl PartialEq<&Encoding> for TransferEncoding { + fn eq(&self, other: &&Encoding) -> bool { + &&self.inner == other + } +} + +impl From for TransferEncoding { + fn from(encoding: Encoding) -> Self { + Self { inner: encoding } + } +} + +impl From<&Encoding> for TransferEncoding { + fn from(encoding: &Encoding) -> Self { + Self { inner: *encoding } + } +} + +impl From for TransferEncoding { + fn from(encoding: EncodingProposal) -> Self { + Self { + inner: encoding.encoding, + } + } +} + +impl From<&EncodingProposal> for TransferEncoding { + fn from(encoding: &EncodingProposal) -> Self { + Self { + inner: encoding.encoding, + } + } +} + +impl Debug for TransferEncoding { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +}