Skip to content

Commit

Permalink
Add Transfer-Encoding and TE headers
Browse files Browse the repository at this point in the history
  • Loading branch information
yoshuawuyts committed Dec 18, 2020
1 parent ef5d1c8 commit 3eda9df
Show file tree
Hide file tree
Showing 6 changed files with 805 additions and 3 deletions.
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
64 changes: 64 additions & 0 deletions src/transfer/encoding.rs
Original file line number Diff line number Diff line change
@@ -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()) }
}
}
147 changes: 147 additions & 0 deletions src/transfer/encoding_proposal.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions src/transfer/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 3eda9df

Please sign in to comment.