Skip to content

Commit a13f496

Browse files
palantjoseluisq
andauthored
refactor: replace headers module fork by an in-tree handler for the Accept-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>
1 parent 941f692 commit a13f496

File tree

9 files changed

+457
-39
lines changed

9 files changed

+457
-39
lines changed

Cargo.lock

Lines changed: 4 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ clap = { version = "4.5", features = ["derive", "env"] }
6868
form_urlencoded = "1.2"
6969
futures-util = { version = "0.3", default-features = false }
7070
globset = { version = "0.4", features = ["serde1"] }
71-
headers = { package = "headers-accept-encoding", version = "=1.0" }
71+
headers = "0.3"
7272
http = "0.2"
7373
http-serde = "1.1"
7474
humansize = { version = "2.1", features = ["impl_style"], optional = true }

src/compression.rs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use async_compression::tokio::bufread::ZstdEncoder;
1919

2020
use bytes::Bytes;
2121
use futures_util::Stream;
22-
use headers::{AcceptEncoding, ContentCoding, ContentType, HeaderMap, HeaderMapExt, HeaderValue};
22+
use headers::{ContentType, HeaderMap, HeaderMapExt, HeaderValue};
2323
use hyper::{
2424
header::{CONTENT_ENCODING, CONTENT_LENGTH},
2525
Body, Method, Response,
@@ -30,7 +30,11 @@ use std::pin::Pin;
3030
use std::task::{Context, Poll};
3131
use tokio_util::io::{ReaderStream, StreamReader};
3232

33-
use crate::{http_ext::MethodExt, Result};
33+
use crate::{
34+
headers_ext::{AcceptEncoding, ContentCoding},
35+
http_ext::MethodExt,
36+
Result,
37+
};
3438

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

7781
// Compress response based on Accept-Encoding header
78-
if let Some(encoding) = get_prefered_encoding(headers) {
82+
if let Some(encoding) = get_preferred_encoding(headers) {
7983
tracing::trace!(
80-
"prefered encoding selected from the accept-ecoding header: {:?}",
84+
"preferred encoding selected from the accept-encoding header: {:?}",
8185
encoding
8286
);
8387

@@ -113,7 +117,7 @@ pub fn auto(
113117
return Ok(zstd(head, body.into()));
114118
}
115119

116-
tracing::trace!("no compression feature matched the prefered encoding, probably not enabled or unsupported");
120+
tracing::trace!("no compression feature matched the preferred encoding, probably not enabled or unsupported");
117121
}
118122

119123
Ok(resp)
@@ -211,21 +215,21 @@ pub fn zstd(
211215
pub fn create_encoding_header(existing: Option<HeaderValue>, coding: ContentCoding) -> HeaderValue {
212216
if let Some(val) = existing {
213217
if let Ok(str_val) = val.to_str() {
214-
return HeaderValue::from_str(&[str_val, ", ", coding.to_static()].concat())
218+
return HeaderValue::from_str(&[str_val, ", ", coding.as_str()].concat())
215219
.unwrap_or_else(|_| coding.into());
216220
}
217221
}
218222
coding.into()
219223
}
220224

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

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

243247
// If there is only one Cargo compression feature enabled
244-
// then re-evaluate the prefered encoding and return
248+
// then re-evaluate the preferred encoding and return
245249
// that feature compression algorithm as `ContentCoding` only
246250
// if was contained in the `AcceptEncoding` value.
247251
if feature_formats.len() == 1 {
248252
let feature_enc = *feature_formats.first().unwrap();
249-
if feature_enc != prefered_enc
253+
if feature_enc != preferred_enc
250254
&& accept_encoding
251255
.sorted_encodings()
252256
.any(|enc| enc == feature_enc)
253257
{
254258
tracing::trace!(
255-
"prefered encoding {:?} is re-evalated to {:?} because is the only compression feature enabled that matches the `accept-encoding` header",
256-
prefered_enc,
259+
"preferred encoding {:?} is re-evalated to {:?} because is the only compression feature enabled that matches the `accept-encoding` header",
260+
preferred_enc,
257261
feature_enc
258262
);
259263
return Some(feature_enc);

src/compression_static.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
//! Compression static module to serve compressed files directly from the file system.
77
//!
88
9-
use headers::{ContentCoding, HeaderMap, HeaderValue};
9+
use headers::{HeaderMap, HeaderValue};
1010
use std::{
1111
ffi::OsStr,
1212
fs::Metadata,
1313
path::{Path, PathBuf},
1414
};
1515

16-
use crate::{compression, static_files::file_metadata};
16+
use crate::{compression, headers_ext::ContentCoding, static_files::file_metadata};
1717

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

38-
// Determine prefered-encoding extension if available
39-
let comp_ext = match compression::get_prefered_encoding(headers) {
38+
// Determine preferred-encoding extension if available
39+
let comp_ext = match compression::get_preferred_encoding(headers) {
4040
// https://zlib.net/zlib_faq.html#faq39
4141
#[cfg(any(feature = "compression", feature = "compression-gzip"))]
4242
Some(ContentCoding::GZIP | ContentCoding::DEFLATE) => "gz",

src/headers_ext/accept_encoding.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
// Original code by Parker Timmerman
3+
// Original code sourced from https://github.com/hyperium/headers/pull/70
4+
5+
use headers::{Error, Header};
6+
use hyper::header::{HeaderName, HeaderValue, ACCEPT_ENCODING};
7+
8+
use super::{ContentCoding, QualityValue};
9+
10+
/// `Accept-Encoding` header, defined in
11+
/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.4)
12+
///
13+
/// The `Accept-Encoding` header field can be used by user agents to
14+
/// indicate what response content-codings are acceptable in the response.
15+
/// An "identity" token is used as a synonym for "no encoding" in
16+
/// order to communicate when no encoding is preferred.
17+
///
18+
/// # ABNF
19+
///
20+
/// ```text
21+
/// Accept-Encoding = #( codings [ weight ] )
22+
/// codings = content-coding / "identity" / "*"
23+
/// ```
24+
///
25+
/// # Example Values
26+
///
27+
/// * `gzip`
28+
/// * `br;q=1.0, gzip;q=0.8`
29+
///
30+
#[derive(Clone, Debug)]
31+
pub(crate) struct AcceptEncoding(QualityValue);
32+
33+
impl Header for AcceptEncoding {
34+
fn name() -> &'static HeaderName {
35+
&ACCEPT_ENCODING
36+
}
37+
38+
fn decode<'i, I>(values: &mut I) -> Result<Self, Error>
39+
where
40+
I: Iterator<Item = &'i HeaderValue>,
41+
{
42+
QualityValue::try_from_values(values).map(Self)
43+
}
44+
45+
fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
46+
values.extend(std::iter::once((&self.0).into()))
47+
}
48+
}
49+
50+
impl AcceptEncoding {
51+
/// Returns the most preferred encoding that is specified by the header,
52+
/// if one is specified.
53+
pub(crate) fn preferred_encoding(&self) -> Option<ContentCoding> {
54+
self.0.iter().next().map(ContentCoding::from)
55+
}
56+
57+
/// Returns a quality sorted iterator of the `ContentCoding`
58+
pub(crate) fn sorted_encodings(&self) -> impl Iterator<Item = ContentCoding> + '_ {
59+
self.0.iter().map(ContentCoding::from)
60+
}
61+
}
62+
63+
#[cfg(test)]
64+
mod tests {
65+
use super::*;
66+
67+
#[test]
68+
fn from_static() {
69+
let val = HeaderValue::from_static("deflate, gzip;q=1.0, br;q=0.9");
70+
let accept_enc = AcceptEncoding(val.into());
71+
72+
assert_eq!(
73+
accept_enc.preferred_encoding(),
74+
Some(ContentCoding::DEFLATE)
75+
);
76+
77+
let mut encodings = accept_enc.sorted_encodings();
78+
assert_eq!(encodings.next(), Some(ContentCoding::DEFLATE));
79+
assert_eq!(encodings.next(), Some(ContentCoding::GZIP));
80+
assert_eq!(encodings.next(), Some(ContentCoding::BROTLI));
81+
assert_eq!(encodings.next(), None);
82+
}
83+
}

src/headers_ext/content_coding.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
// Original code by Parker Timmerman
3+
// Original code sourced from https://github.com/hyperium/headers/pull/70
4+
5+
// Derives an enum to represent content codings and some helpful impls
6+
macro_rules! define_content_coding {
7+
($($coding:ident; $str:expr,)+) => {
8+
use hyper::header::HeaderValue;
9+
use std::str::FromStr;
10+
11+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
12+
/// Values that are used with headers like [`Content-Encoding`](headers::ContentEncoding) or
13+
/// [`Accept-Encoding`](super::AcceptEncoding)
14+
///
15+
/// [RFC7231](https://www.iana.org/assignments/http-parameters/http-parameters.xhtml)
16+
pub enum ContentCoding {
17+
$(
18+
#[allow(clippy::upper_case_acronyms)]
19+
#[doc = $str]
20+
$coding,
21+
)+
22+
}
23+
24+
impl ContentCoding {
25+
/// Returns a `&'static str` for a `ContentCoding`
26+
#[inline]
27+
pub(crate) fn as_str(&self) -> &'static str {
28+
match *self {
29+
$(ContentCoding::$coding => $str,)+
30+
}
31+
}
32+
}
33+
34+
impl From<&str> for ContentCoding {
35+
/// Given a `&str` returns a `ContentCoding`
36+
///
37+
/// Note this will never fail, in the case of `&str` being an invalid content coding,
38+
/// will return `ContentCoding::IDENTITY` because `'identity'` is generally always an
39+
/// accepted coding.
40+
#[inline]
41+
fn from(s: &str) -> Self {
42+
ContentCoding::from_str(s).unwrap_or_else(|_| ContentCoding::IDENTITY)
43+
}
44+
}
45+
46+
impl FromStr for ContentCoding {
47+
type Err = ();
48+
49+
/// Given a `&str` will try to return a `ContentCoding`
50+
///
51+
/// Different from `ContentCoding::from(&str)`, if `&str` is an invalid content
52+
/// coding, it will return `Err(())`
53+
#[inline]
54+
fn from_str(s: &str) -> Result<Self, Self::Err> {
55+
match s {
56+
$(
57+
stringify!($coding)
58+
| $str => Ok(Self::$coding),
59+
)+
60+
_ => Err(())
61+
}
62+
}
63+
}
64+
65+
impl std::fmt::Display for ContentCoding {
66+
#[inline]
67+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
68+
write!(f, "{}", match *self {
69+
$(ContentCoding::$coding => $str.to_string(),)+
70+
})
71+
}
72+
}
73+
74+
impl From<ContentCoding> for HeaderValue {
75+
fn from(coding: ContentCoding) -> HeaderValue {
76+
match coding {
77+
$(ContentCoding::$coding => HeaderValue::from_static($str),)+
78+
}
79+
}
80+
}
81+
}
82+
}
83+
84+
define_content_coding! {
85+
BROTLI; "br",
86+
COMPRESS; "compress",
87+
DEFLATE; "deflate",
88+
GZIP; "gzip",
89+
IDENTITY; "identity",
90+
ZSTD; "zstd",
91+
}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
use super::ContentCoding;
96+
use std::str::FromStr;
97+
98+
#[test]
99+
fn as_str() {
100+
assert_eq!(ContentCoding::GZIP.as_str(), "gzip");
101+
}
102+
103+
#[test]
104+
fn to_string() {
105+
assert_eq!(ContentCoding::DEFLATE.to_string(), "deflate".to_string());
106+
}
107+
108+
#[test]
109+
fn from() {
110+
assert_eq!(ContentCoding::from("br"), ContentCoding::BROTLI);
111+
assert_eq!(ContentCoding::from("GZIP"), ContentCoding::GZIP);
112+
assert_eq!(ContentCoding::from("zstd"), ContentCoding::ZSTD);
113+
assert_eq!(ContentCoding::from("blah blah"), ContentCoding::IDENTITY);
114+
}
115+
116+
#[test]
117+
fn from_str() {
118+
assert_eq!(ContentCoding::from_str("br"), Ok(ContentCoding::BROTLI));
119+
assert_eq!(ContentCoding::from_str("zstd"), Ok(ContentCoding::ZSTD));
120+
assert_eq!(ContentCoding::from_str("blah blah"), Err(()));
121+
}
122+
}

0 commit comments

Comments
 (0)