Skip to content

Commit

Permalink
refactor: replace headers module fork by an in-tree handler for the A…
Browse files Browse the repository at this point in the history
…ccept-Encoding header (#354)

* Replaced fork of the headers module by an in-tree handler for the Accept-Encoding header

* Added test for various allowed whitespace within the Accept-Encoding header

* Add tests for ContentCoding::ANY

* Fixed links in documentation

* Removed CargoCoding::ANY for now

* Added license headers to imported files

* Remove unused AcceptEncoding::gzip method

* Make headers_ext module internal

This required removing some unused code and code examples. Also, additional clippy warnings had to be addressed.

* refactor: reduce the visibility of headers_ext and its methods

---------

Co-authored-by: Jose Quintana <1700322+joseluisq@users.noreply.github.com>
  • Loading branch information
palant and joseluisq committed Apr 21, 2024
1 parent 941f692 commit a13f496
Show file tree
Hide file tree
Showing 9 changed files with 457 additions and 39 deletions.
24 changes: 4 additions & 20 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ clap = { version = "4.5", features = ["derive", "env"] }
form_urlencoded = "1.2"
futures-util = { version = "0.3", default-features = false }
globset = { version = "0.4", features = ["serde1"] }
headers = { package = "headers-accept-encoding", version = "=1.0" }
headers = "0.3"
http = "0.2"
http-serde = "1.1"
humansize = { version = "2.1", features = ["impl_style"], optional = true }
Expand Down
32 changes: 18 additions & 14 deletions src/compression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use async_compression::tokio::bufread::ZstdEncoder;

use bytes::Bytes;
use futures_util::Stream;
use headers::{AcceptEncoding, ContentCoding, ContentType, HeaderMap, HeaderMapExt, HeaderValue};
use headers::{ContentType, HeaderMap, HeaderMapExt, HeaderValue};
use hyper::{
header::{CONTENT_ENCODING, CONTENT_LENGTH},
Body, Method, Response,
Expand All @@ -30,7 +30,11 @@ use std::pin::Pin;
use std::task::{Context, Poll};
use tokio_util::io::{ReaderStream, StreamReader};

use crate::{http_ext::MethodExt, Result};
use crate::{
headers_ext::{AcceptEncoding, ContentCoding},
http_ext::MethodExt,
Result,
};

/// Contains a fixed list of common text-based MIME types in order to apply compression.
pub const TEXT_MIME_TYPES: [&str; 24] = [
Expand Down Expand Up @@ -75,9 +79,9 @@ pub fn auto(
}

// Compress response based on Accept-Encoding header
if let Some(encoding) = get_prefered_encoding(headers) {
if let Some(encoding) = get_preferred_encoding(headers) {
tracing::trace!(
"prefered encoding selected from the accept-ecoding header: {:?}",
"preferred encoding selected from the accept-encoding header: {:?}",
encoding
);

Expand Down Expand Up @@ -113,7 +117,7 @@ pub fn auto(
return Ok(zstd(head, body.into()));
}

tracing::trace!("no compression feature matched the prefered encoding, probably not enabled or unsupported");
tracing::trace!("no compression feature matched the preferred encoding, probably not enabled or unsupported");
}

Ok(resp)
Expand Down Expand Up @@ -211,21 +215,21 @@ pub fn zstd(
pub fn create_encoding_header(existing: Option<HeaderValue>, coding: ContentCoding) -> HeaderValue {
if let Some(val) = existing {
if let Ok(str_val) = val.to_str() {
return HeaderValue::from_str(&[str_val, ", ", coding.to_static()].concat())
return HeaderValue::from_str(&[str_val, ", ", coding.as_str()].concat())
.unwrap_or_else(|_| coding.into());
}
}
coding.into()
}

/// Try to get the prefered `content-encoding` via the `accept-encoding` header.
/// Try to get the preferred `content-encoding` via the `accept-encoding` header.
#[inline(always)]
pub fn get_prefered_encoding(headers: &HeaderMap<HeaderValue>) -> Option<ContentCoding> {
pub fn get_preferred_encoding(headers: &HeaderMap<HeaderValue>) -> Option<ContentCoding> {
if let Some(ref accept_encoding) = headers.typed_get::<AcceptEncoding>() {
tracing::trace!("request with accept-ecoding header: {:?}", accept_encoding);

let encoding = accept_encoding.prefered_encoding();
if let Some(prefered_enc) = encoding {
let encoding = accept_encoding.preferred_encoding();
if let Some(preferred_enc) = encoding {
let mut feature_formats = Vec::<ContentCoding>::with_capacity(5);
if cfg!(feature = "compression-deflate") {
feature_formats.push(ContentCoding::DEFLATE);
Expand All @@ -241,19 +245,19 @@ pub fn get_prefered_encoding(headers: &HeaderMap<HeaderValue>) -> Option<Content
}

// If there is only one Cargo compression feature enabled
// then re-evaluate the prefered encoding and return
// then re-evaluate the preferred encoding and return
// that feature compression algorithm as `ContentCoding` only
// if was contained in the `AcceptEncoding` value.
if feature_formats.len() == 1 {
let feature_enc = *feature_formats.first().unwrap();
if feature_enc != prefered_enc
if feature_enc != preferred_enc
&& accept_encoding
.sorted_encodings()
.any(|enc| enc == feature_enc)
{
tracing::trace!(
"prefered encoding {:?} is re-evalated to {:?} because is the only compression feature enabled that matches the `accept-encoding` header",
prefered_enc,
"preferred encoding {:?} is re-evalated to {:?} because is the only compression feature enabled that matches the `accept-encoding` header",
preferred_enc,
feature_enc
);
return Some(feature_enc);
Expand Down
8 changes: 4 additions & 4 deletions src/compression_static.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
//! Compression static module to serve compressed files directly from the file system.
//!

use headers::{ContentCoding, HeaderMap, HeaderValue};
use headers::{HeaderMap, HeaderValue};
use std::{
ffi::OsStr,
fs::Metadata,
path::{Path, PathBuf},
};

use crate::{compression, static_files::file_metadata};
use crate::{compression, headers_ext::ContentCoding, static_files::file_metadata};

/// It defines the pre-compressed file variant metadata of a particular file path.
pub struct CompressedFileVariant<'a> {
Expand All @@ -35,8 +35,8 @@ pub async fn precompressed_variant<'a>(
file_path.display()
);

// Determine prefered-encoding extension if available
let comp_ext = match compression::get_prefered_encoding(headers) {
// Determine preferred-encoding extension if available
let comp_ext = match compression::get_preferred_encoding(headers) {
// https://zlib.net/zlib_faq.html#faq39
#[cfg(any(feature = "compression", feature = "compression-gzip"))]
Some(ContentCoding::GZIP | ContentCoding::DEFLATE) => "gz",
Expand Down
83 changes: 83 additions & 0 deletions src/headers_ext/accept_encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
// Original code by Parker Timmerman
// Original code sourced from https://github.com/hyperium/headers/pull/70

use headers::{Error, Header};
use hyper::header::{HeaderName, HeaderValue, ACCEPT_ENCODING};

use super::{ContentCoding, QualityValue};

/// `Accept-Encoding` header, defined in
/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.4)
///
/// The `Accept-Encoding` header field can be used by user agents to
/// indicate what response content-codings are acceptable in the response.
/// An "identity" token is used as a synonym for "no encoding" in
/// order to communicate when no encoding is preferred.
///
/// # ABNF
///
/// ```text
/// Accept-Encoding = #( codings [ weight ] )
/// codings = content-coding / "identity" / "*"
/// ```
///
/// # Example Values
///
/// * `gzip`
/// * `br;q=1.0, gzip;q=0.8`
///
#[derive(Clone, Debug)]
pub(crate) struct AcceptEncoding(QualityValue);

impl Header for AcceptEncoding {
fn name() -> &'static HeaderName {
&ACCEPT_ENCODING
}

fn decode<'i, I>(values: &mut I) -> Result<Self, Error>
where
I: Iterator<Item = &'i HeaderValue>,
{
QualityValue::try_from_values(values).map(Self)
}

fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
values.extend(std::iter::once((&self.0).into()))
}
}

impl AcceptEncoding {
/// Returns the most preferred encoding that is specified by the header,
/// if one is specified.
pub(crate) fn preferred_encoding(&self) -> Option<ContentCoding> {
self.0.iter().next().map(ContentCoding::from)
}

/// Returns a quality sorted iterator of the `ContentCoding`
pub(crate) fn sorted_encodings(&self) -> impl Iterator<Item = ContentCoding> + '_ {
self.0.iter().map(ContentCoding::from)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn from_static() {
let val = HeaderValue::from_static("deflate, gzip;q=1.0, br;q=0.9");
let accept_enc = AcceptEncoding(val.into());

assert_eq!(
accept_enc.preferred_encoding(),
Some(ContentCoding::DEFLATE)
);

let mut encodings = accept_enc.sorted_encodings();
assert_eq!(encodings.next(), Some(ContentCoding::DEFLATE));
assert_eq!(encodings.next(), Some(ContentCoding::GZIP));
assert_eq!(encodings.next(), Some(ContentCoding::BROTLI));
assert_eq!(encodings.next(), None);
}
}
122 changes: 122 additions & 0 deletions src/headers_ext/content_coding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
// Original code by Parker Timmerman
// Original code sourced from https://github.com/hyperium/headers/pull/70

// Derives an enum to represent content codings and some helpful impls
macro_rules! define_content_coding {
($($coding:ident; $str:expr,)+) => {
use hyper::header::HeaderValue;
use std::str::FromStr;

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
/// Values that are used with headers like [`Content-Encoding`](headers::ContentEncoding) or
/// [`Accept-Encoding`](super::AcceptEncoding)
///
/// [RFC7231](https://www.iana.org/assignments/http-parameters/http-parameters.xhtml)
pub enum ContentCoding {
$(
#[allow(clippy::upper_case_acronyms)]
#[doc = $str]
$coding,
)+
}

impl ContentCoding {
/// Returns a `&'static str` for a `ContentCoding`
#[inline]
pub(crate) fn as_str(&self) -> &'static str {
match *self {
$(ContentCoding::$coding => $str,)+
}
}
}

impl From<&str> for ContentCoding {
/// Given a `&str` returns a `ContentCoding`
///
/// Note this will never fail, in the case of `&str` being an invalid content coding,
/// will return `ContentCoding::IDENTITY` because `'identity'` is generally always an
/// accepted coding.
#[inline]
fn from(s: &str) -> Self {
ContentCoding::from_str(s).unwrap_or_else(|_| ContentCoding::IDENTITY)
}
}

impl FromStr for ContentCoding {
type Err = ();

/// Given a `&str` will try to return a `ContentCoding`
///
/// Different from `ContentCoding::from(&str)`, if `&str` is an invalid content
/// coding, it will return `Err(())`
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
$(
stringify!($coding)
| $str => Ok(Self::$coding),
)+
_ => Err(())
}
}
}

impl std::fmt::Display for ContentCoding {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{}", match *self {
$(ContentCoding::$coding => $str.to_string(),)+
})
}
}

impl From<ContentCoding> for HeaderValue {
fn from(coding: ContentCoding) -> HeaderValue {
match coding {
$(ContentCoding::$coding => HeaderValue::from_static($str),)+
}
}
}
}
}

define_content_coding! {
BROTLI; "br",
COMPRESS; "compress",
DEFLATE; "deflate",
GZIP; "gzip",
IDENTITY; "identity",
ZSTD; "zstd",
}

#[cfg(test)]
mod tests {
use super::ContentCoding;
use std::str::FromStr;

#[test]
fn as_str() {
assert_eq!(ContentCoding::GZIP.as_str(), "gzip");
}

#[test]
fn to_string() {
assert_eq!(ContentCoding::DEFLATE.to_string(), "deflate".to_string());
}

#[test]
fn from() {
assert_eq!(ContentCoding::from("br"), ContentCoding::BROTLI);
assert_eq!(ContentCoding::from("GZIP"), ContentCoding::GZIP);
assert_eq!(ContentCoding::from("zstd"), ContentCoding::ZSTD);
assert_eq!(ContentCoding::from("blah blah"), ContentCoding::IDENTITY);
}

#[test]
fn from_str() {
assert_eq!(ContentCoding::from_str("br"), Ok(ContentCoding::BROTLI));
assert_eq!(ContentCoding::from_str("zstd"), Ok(ContentCoding::ZSTD));
assert_eq!(ContentCoding::from_str("blah blah"), Err(()));
}
}
Loading

0 comments on commit a13f496

Please sign in to comment.