Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Transfer-Encoding and TE headers #301

Merged
merged 1 commit into from
Dec 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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