Add `Transfer-Encoding` and `TE` headers

This commit is contained in:
Yoshua Wuyts 2020-12-18 22:37:51 +01:00
parent ef5d1c84c0
commit 3eda9dfbe8
6 changed files with 805 additions and 3 deletions

View File

@ -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;

64
src/transfer/encoding.rs Normal file
View File

@ -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<Encoding> {
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<Encoding> for HeaderValue {
fn from(directive: Encoding) -> Self {
let s = directive.to_string();
unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) }
}
}

View File

@ -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<f32>,
}
impl EncodingProposal {
/// Create a new instance of `EncodingProposal`.
pub fn new(encoding: impl Into<Encoding>, weight: Option<f32>) -> crate::Result<Self> {
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<f32> {
self.weight
}
pub(crate) fn from_str(s: &str) -> crate::Result<Option<Self>> {
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<Encoding> for EncodingProposal {
fn from(encoding: Encoding) -> Self {
Self {
encoding,
weight: None,
}
}
}
impl PartialEq<Encoding> for EncodingProposal {
fn eq(&self, other: &Encoding) -> bool {
self.encoding == *other
}
}
impl PartialEq<Encoding> 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<std::cmp::Ordering> {
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<EncodingProposal> 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(())
}
}

13
src/transfer/mod.rs Normal file
View File

@ -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;

430
src/transfer/te.rs Normal file
View File

@ -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<EncodingProposal>,
}
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<Headers>) -> crate::Result<Option<Self>> {
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<EncodingProposal>) {
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<TransferEncoding> {
// 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>) {
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<EncodingProposal>,
}
impl Iterator for IntoIter {
type Item = EncodingProposal;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
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::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
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::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl ToHeaderValues for TE {
type Iter = option::IntoIter<HeaderValue>;
fn to_header_values(&self) -> crate::Result<Self::Iter> {
// 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(())
}
}

View File

@ -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<Headers>) -> crate::Result<Option<Self>> {
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>) {
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<HeaderValue>;
fn to_header_values(&self) -> crate::Result<Self::Iter> {
// 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<Encoding> 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<Encoding> 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<EncodingProposal> 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)
}
}