diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..50856fd1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: [push, pull_request] + +env: + minrust: 1.56.0 + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + strategy: + matrix: + rust: [stable, beta, nightly] + + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + components: rustfmt + - run: cargo test --workspace + - if: matrix.rust == 'nightly' + run: cargo test --benches + - name: Check minimal versions + if: matrix.rust == 'nightly' + run: | + cargo clean + cargo update -Z minimal-versions + cargo check + - run: cargo fmt --all --check + + MSRV: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install rust ${{ env.minrust }} + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.minrust }} + - run: cargo build diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 89f5bfcc..00000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: rust -sudo: false - -cache: cargo - -matrix: - include: - - rust: stable - - rust: beta - - rust: nightly - # minimum rustc version - - rust: 1.39.0 - script: cargo build - -script: - - cargo test --all - - 'if [ "$TRAVIS_RUST_VERSION" == "nightly" ]; then cargo test --benches; fi' - -notifications: - email: - on_success: never diff --git a/Cargo.toml b/Cargo.toml index a88f18e7..95ae3e9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,30 +1,26 @@ [package] -name = "headers" -version = "0.3.2" # don't forget to update html_root_url -description = "typed HTTP headers" +name = "headers-accept-encoding" +version = "1.1.0" # don't forget to update html_root_url +description = "Hypper typed HTTP headers with Accept-Encoding + zstd support" license = "MIT" readme = "README.md" homepage = "https://hyper.rs" -repository = "https://github.com/hyperium/headers" -authors = ["Sean McArthur "] +repository = "https://github.com/static-web-server/headers-accept-encoding" +authors = ["Sean McArthur ", "Jose Quintana "] keywords = ["http", "headers", "hyper", "hyperium"] categories = ["web-programming"] -[workspace] -members = [ - "./", - "headers-core", -] +rust-version = "1.56" [dependencies] -http = "0.2.0" -headers-core = { version = "0.2", path = "./headers-core" } -base64 = "0.12" -bitflags = "1.0" -bytes = "0.5" +http = "1.0.0" +headers-core = { version = "0.3" } +base64 = "0.21.5" +bytes = "1" mime = "0.3.14" -sha-1 = "0.8" -time = "0.1.34" +sha1 = "0.10" +httpdate = "1" +itertools = "0.11" [features] nightly = [] diff --git a/LICENSE b/LICENSE index aa33b8e7..985a757d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2019 Sean McArthur +Copyright (c) 2014-2023 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d68f3df9..c81c6dae 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# rust http headers +# Rust HTTP headers with Accept-Encoding + zstd Content Coding support -[![Build Status](https://travis-ci.org/hyperium/headers.svg?branch=master)](https://travis-ci.org/hyperium/header) +[![Build Status](https://github.com/hyperium/headers/workflows/CI/badge.svg)](https://github.com/hyperium/headers/actions?query=workflow%3ACI) -Typed HTTP headers. +A [headers crate fork](https://github.com/ParkMyCar/headers) with `Accept-Encoding` + zstd Content Coding support and synced with the upstream [hyperium/headers](https://github.com/hyperium/headers). + +This fork is used by [static-web-server](https://github.com/static-web-server/static-web-server). diff --git a/headers-core/Cargo.toml b/headers-core/Cargo.toml index 22c011f1..60ecec29 100644 --- a/headers-core/Cargo.toml +++ b/headers-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "headers-core" -version = "0.2.0" # don't forget to update html_root_url +version = "0.3.0" # don't forget to update html_root_url description = "typed HTTP headers core trait" license = "MIT" readme = "README.md" @@ -10,4 +10,4 @@ authors = ["Sean McArthur "] keywords = ["http", "headers", "hyper", "hyperium"] [dependencies] -http = "0.2.0" +http = "1.0.0" diff --git a/headers-core/src/lib.rs b/headers-core/src/lib.rs index 92e3d15e..5692b65a 100644 --- a/headers-core/src/lib.rs +++ b/headers-core/src/lib.rs @@ -1,7 +1,7 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![cfg_attr(test, deny(warnings))] -#![doc(html_root_url = "https://docs.rs/headers-core/0.2.0")] +#![doc(html_root_url = "https://docs.rs/headers-core/0.3.0")] //! # headers-core //! diff --git a/src/common/accept_encoding.rs b/src/common/accept_encoding.rs new file mode 100644 index 00000000..436a39d4 --- /dev/null +++ b/src/common/accept_encoding.rs @@ -0,0 +1,160 @@ +use std::convert::TryFrom; + +use util::{QualityValue, TryFromValues}; +use {ContentCoding, HeaderValue}; + +/// `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 struct AcceptEncoding(pub QualityValue); + +derive_header! { + AcceptEncoding(_), + name: ACCEPT_ENCODING +} + +impl AcceptEncoding { + /// Convience method to create an `Accept-Encoding: gzip` header + #[inline] + pub fn gzip() -> AcceptEncoding { + AcceptEncoding(HeaderValue::from_static("gzip").into()) + } + + /// A convience method to create an Accept-Encoding header from pairs of values and qualities + /// + /// # Example + /// + /// ``` + /// use headers::AcceptEncoding; + /// + /// let pairs = vec![("gzip", 1.0), ("deflate", 0.8)]; + /// let header = AcceptEncoding::from_quality_pairs(&mut pairs.into_iter()); + /// ``` + pub fn from_quality_pairs<'i, I>(pairs: &mut I) -> Result + where + I: Iterator, + { + let values: Vec = pairs + .map(|pair| { + QualityValue::try_from(pair).map(|qual: QualityValue| HeaderValue::from(qual)) + }) + .collect::, ::Error>>()?; + let value = QualityValue::try_from_values(&mut values.iter())?; + Ok(AcceptEncoding(value)) + } + + /// Returns the most prefered encoding that is specified by the header, + /// if one is specified. + /// + /// Note: This peeks at the underlying iter, not modifying it. + /// + /// # Example + /// + /// ``` + /// use headers::{AcceptEncoding, ContentCoding}; + /// + /// let pairs = vec![("gzip", 1.0), ("deflate", 0.8)]; + /// let accept_enc = AcceptEncoding::from_quality_pairs(&mut pairs.into_iter()).unwrap(); + /// let mut encodings = accept_enc.sorted_encodings(); + /// + /// assert_eq!(accept_enc.prefered_encoding(), Some(ContentCoding::GZIP)); + /// ``` + pub fn prefered_encoding(&self) -> Option { + self.0 + .iter() + .peekable() + .peek() + .map(|s| ContentCoding::from_str(*s)) + } + + /// Returns a quality sorted iterator of the `ContentCoding` + /// + /// # Example + /// + /// ``` + /// use headers::{AcceptEncoding, ContentCoding, HeaderValue}; + /// + /// let val = HeaderValue::from_static("deflate, gzip;q=1.0, br;q=0.8"); + /// let accept_enc = AcceptEncoding(val.into()); + /// 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); + /// ``` + pub fn sorted_encodings<'a>(&'a self) -> impl Iterator + 'a { + self.0.iter().map(|s| ContentCoding::from_str(s)) + } + + /// Returns a quality sorted iterator of values + /// + /// # Example + /// + /// ``` + /// use headers::{AcceptEncoding, ContentCoding, HeaderValue}; + /// + /// let val = HeaderValue::from_static("deflate, gzip;q=1.0, br;q=0.8"); + /// let accept_enc = AcceptEncoding(val.into()); + /// let mut encodings = accept_enc.sorted_values(); + /// + /// assert_eq!(encodings.next(), Some("deflate")); + /// assert_eq!(encodings.next(), Some("gzip")); + /// assert_eq!(encodings.next(), Some("br")); + /// assert_eq!(encodings.next(), None); + /// ``` + pub fn sorted_values(&self) -> impl Iterator { + self.0.iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use {ContentCoding, HeaderValue}; + + #[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.prefered_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); + } + + #[test] + fn from_pairs() { + let pairs = vec![("gzip", 1.0), ("br", 0.9)]; + let accept_enc = AcceptEncoding::from_quality_pairs(&mut pairs.into_iter()).unwrap(); + + assert_eq!(accept_enc.prefered_encoding(), Some(ContentCoding::GZIP)); + + let mut encodings = accept_enc.sorted_encodings(); + assert_eq!(encodings.next(), Some(ContentCoding::GZIP)); + assert_eq!(encodings.next(), Some(ContentCoding::BROTLI)); + assert_eq!(encodings.next(), None); + } +} diff --git a/src/common/access_control_allow_origin.rs b/src/common/access_control_allow_origin.rs index c048bf83..8d38bf17 100644 --- a/src/common/access_control_allow_origin.rs +++ b/src/common/access_control_allow_origin.rs @@ -1,3 +1,5 @@ +use std::convert::TryFrom; + use super::origin::Origin; use util::{IterExt, TryFromValues}; use HeaderValue; @@ -25,9 +27,11 @@ use HeaderValue; /// ``` /// # extern crate headers; /// use headers::AccessControlAllowOrigin; +/// use std::convert::TryFrom; /// /// let any_origin = AccessControlAllowOrigin::ANY; /// let null_origin = AccessControlAllowOrigin::NULL; +/// let origin = AccessControlAllowOrigin::try_from("http://web-platform.test:8000"); /// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct AccessControlAllowOrigin(OriginOrAny); @@ -60,6 +64,26 @@ impl AccessControlAllowOrigin { } } +impl TryFrom<&str> for AccessControlAllowOrigin { + type Error = ::Error; + + fn try_from(s: &str) -> Result { + let header_value = HeaderValue::from_str(s).map_err(|_| ::Error::invalid())?; + let origin = OriginOrAny::try_from(&header_value)?; + Ok(Self(origin)) + } +} + +impl TryFrom<&HeaderValue> for OriginOrAny { + type Error = ::Error; + + fn try_from(header_value: &HeaderValue) -> Result { + Origin::try_from_value(header_value) + .map(OriginOrAny::Origin) + .ok_or_else(::Error::invalid) + } +} + impl TryFromValues for OriginOrAny { fn try_from_values<'i, I>(values: &mut I) -> Result where @@ -89,12 +113,14 @@ impl<'a> From<&'a OriginOrAny> for HeaderValue { #[cfg(test)] mod tests { + use super::super::{test_decode, test_encode}; use super::*; #[test] fn origin() { let s = "http://web-platform.test:8000"; + let allow_origin = test_decode::(&[s]).unwrap(); { let origin = allow_origin.origin().unwrap(); @@ -107,6 +133,22 @@ mod tests { assert_eq!(headers["access-control-allow-origin"], s); } + #[test] + fn try_from_origin() { + let s = "http://web-platform.test:8000"; + + let allow_origin = AccessControlAllowOrigin::try_from(s).unwrap(); + { + let origin = allow_origin.origin().unwrap(); + assert_eq!(origin.scheme(), "http"); + assert_eq!(origin.hostname(), "web-platform.test"); + assert_eq!(origin.port(), Some(8000)); + } + + let headers = test_encode(allow_origin); + assert_eq!(headers["access-control-allow-origin"], s); + } + #[test] fn any() { let allow_origin = test_decode::(&["*"]).unwrap(); diff --git a/src/common/age.rs b/src/common/age.rs new file mode 100644 index 00000000..11f1f646 --- /dev/null +++ b/src/common/age.rs @@ -0,0 +1,69 @@ +use std::time::Duration; + +use util::Seconds; + +/// `Age` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.1) +/// +/// The "Age" header field conveys the sender's estimate of the amount of +/// time since the response was generated or successfully validated at +/// the origin server. Age values are calculated as specified in +/// [Section 4.2.3](https://tools.ietf.org/html/rfc7234#section-4.2.3). +/// +/// ## ABNF +/// +/// ```text +/// Age = delta-seconds +/// ``` +/// +/// The Age field-value is a non-negative integer, representing time in +/// seconds (see [Section 1.2.1](https://tools.ietf.org/html/rfc7234#section-1.2.1)). +/// +/// The presence of an Age header field implies that the response was not +/// generated or validated by the origin server for this request. +/// However, lack of an Age header field does not imply the origin was +/// contacted, since the response might have been received from an +/// HTTP/1.0 cache that does not implement Age. +/// +/// ## Example values +/// +/// * `3600` +/// +/// # Example +/// +/// ``` +/// # extern crate headers; +/// use headers::Age; +/// +/// let len = Age::from_secs(60); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Age(Seconds); + +derive_header! { + Age(_), + name: AGE +} + +impl Age { + /// Creates a new `Age` header from the specified number of whole seconds. + pub fn from_secs(secs: u64) -> Self { + Self(Seconds::from_secs(secs)) + } + + /// Returns the number of seconds for this `Age` header. + pub fn as_secs(&self) -> u64 { + self.0.as_u64() + } +} + +impl From for Age { + fn from(dur: Duration) -> Self { + Age(Seconds::from(dur)) + } +} + +impl From for Duration { + fn from(age: Age) -> Self { + age.0.into() + } +} diff --git a/src/common/authorization.rs b/src/common/authorization.rs index 6442a5fd..bb2c35ec 100644 --- a/src/common/authorization.rs +++ b/src/common/authorization.rs @@ -1,6 +1,7 @@ //! Authorization header and types. -use base64; +use base64::engine::general_purpose::STANDARD as ENGINE; +use base64::Engine; use bytes::Bytes; use util::HeaderValueString; @@ -45,6 +46,16 @@ impl Authorization { Authorization(Basic { decoded, colon_pos }) } + + /// View the decoded username. + pub fn username(&self) -> &str { + self.0.username() + } + + /// View the decoded password. + pub fn password(&self) -> &str { + self.0.password() + } } impl Authorization { @@ -54,6 +65,11 @@ impl Authorization { .map(|val| Authorization(Bearer(val))) .ok_or_else(|| InvalidBearerToken { _inner: () }) } + + /// View the token part as a `&str`. + pub fn token(&self) -> &str { + self.0.token() + } } impl ::Header for Authorization { @@ -66,9 +82,9 @@ impl ::Header for Authorization { .next() .and_then(|val| { let slice = val.as_bytes(); - if slice.starts_with(C::SCHEME.as_bytes()) - && slice.len() > C::SCHEME.len() + if slice.len() > C::SCHEME.len() && slice[C::SCHEME.len()] == b' ' + && slice[..C::SCHEME.len()].eq_ignore_ascii_case(C::SCHEME.as_bytes()) { C::decode(val).map(Authorization) } else { @@ -79,7 +95,8 @@ impl ::Header for Authorization { } fn encode>(&self, values: &mut E) { - let value = self.0.encode(); + let mut value = self.0.encode(); + value.set_sensitive(true); debug_assert!( value.as_bytes().starts_with(C::SCHEME.as_bytes()), "Credentials::encode should include its scheme: scheme = {:?}, encoded = {:?}", @@ -134,7 +151,7 @@ impl Credentials for Basic { fn decode(value: &HeaderValue) -> Option { debug_assert!( - value.as_bytes().starts_with(b"Basic "), + value.as_bytes()[..Self::SCHEME.len()].eq_ignore_ascii_case(Self::SCHEME.as_bytes()), "HeaderValue to decode should start with \"Basic ..\", received = {:?}", value, ); @@ -142,7 +159,8 @@ impl Credentials for Basic { let bytes = &value.as_bytes()["Basic ".len()..]; let non_space_pos = bytes.iter().position(|b| *b != b' ')?; let bytes = &bytes[non_space_pos..]; - let bytes = base64::decode(bytes).ok()?; + + let bytes = ENGINE.decode(bytes).ok()?; let decoded = String::from_utf8(bytes).ok()?; @@ -153,10 +171,11 @@ impl Credentials for Basic { fn encode(&self) -> HeaderValue { let mut encoded = String::from("Basic "); - base64::encode_config_buf(&self.decoded, base64::STANDARD, &mut encoded); + ENGINE.encode_string(&self.decoded, &mut encoded); let bytes = Bytes::from(encoded); - HeaderValue::from_maybe_shared(bytes).expect("base64 encoding is always a valid HeaderValue") + HeaderValue::from_maybe_shared(bytes) + .expect("base64 encoding is always a valid HeaderValue") } } @@ -167,7 +186,7 @@ pub struct Bearer(HeaderValueString); impl Bearer { /// View the token part as a `&str`. pub fn token(&self) -> &str { - &self.0.as_str()["Bearer ".len()..] + self.0.as_str()["Bearer ".len()..].trim_start() } } @@ -176,7 +195,7 @@ impl Credentials for Bearer { fn decode(value: &HeaderValue) -> Option { debug_assert!( - value.as_bytes().starts_with(b"Bearer "), + value.as_bytes()[..Self::SCHEME.len()].eq_ignore_ascii_case(Self::SCHEME.as_bytes()), "HeaderValue to decode should start with \"Bearer ..\", received = {:?}", value, ); @@ -233,6 +252,22 @@ mod tests { assert_eq!(auth.0.password(), "open sesame"); } + #[test] + fn basic_decode_case_insensitive() { + let auth: Authorization = + test_decode(&["basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="]).unwrap(); + assert_eq!(auth.0.username(), "Aladdin"); + assert_eq!(auth.0.password(), "open sesame"); + } + + #[test] + fn basic_decode_extra_whitespaces() { + let auth: Authorization = + test_decode(&["Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="]).unwrap(); + assert_eq!(auth.0.username(), "Aladdin"); + assert_eq!(auth.0.password(), "open sesame"); + } + #[test] fn basic_decode_no_password() { let auth: Authorization = test_decode(&["Basic QWxhZGRpbjo="]).unwrap(); @@ -254,6 +289,18 @@ mod tests { let auth: Authorization = test_decode(&["Bearer fpKL54jvWmEGVoRdCNjG"]).unwrap(); assert_eq!(auth.0.token().as_bytes(), b"fpKL54jvWmEGVoRdCNjG"); } + + #[test] + fn bearer_decode_case_insensitive() { + let auth: Authorization = test_decode(&["bearer fpKL54jvWmEGVoRdCNjG"]).unwrap(); + assert_eq!(auth.0.token().as_bytes(), b"fpKL54jvWmEGVoRdCNjG"); + } + + #[test] + fn bearer_decode_extra_whitespaces() { + let auth: Authorization = test_decode(&["Bearer fpKL54jvWmEGVoRdCNjG"]).unwrap(); + assert_eq!(auth.0.token().as_bytes(), b"fpKL54jvWmEGVoRdCNjG"); + } } //bench_header!(raw, Authorization, { vec![b"foo bar baz".to_vec()] }); diff --git a/src/common/cache_control.rs b/src/common/cache_control.rs index 305361d3..7d5db055 100644 --- a/src/common/cache_control.rs +++ b/src/common/cache_control.rs @@ -7,6 +7,7 @@ use util::{self, csv, Seconds}; use HeaderValue; /// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2) +/// with extensions in [RFC8246](https://www.rfc-editor.org/rfc/rfc8246) /// /// The `Cache-Control` header field is used to specify directives for /// caches along the request/response chain. Such cache directives are @@ -43,16 +44,33 @@ pub struct CacheControl { s_max_age: Option, } -bitflags! { - struct Flags: u32 { - const NO_CACHE = 0b00000001; - const NO_STORE = 0b00000010; - const NO_TRANSFORM = 0b00000100; - const ONLY_IF_CACHED = 0b00001000; - const MUST_REVALIDATE = 0b00010000; - const PUBLIC = 0b00100000; - const PRIVATE = 0b01000000; - const PROXY_REVALIDATE = 0b10000000; +#[derive(Debug, Clone, PartialEq)] +struct Flags { + bits: u64, +} + +impl Flags { + const NO_CACHE: Self = Self { bits: 0b000000001 }; + const NO_STORE: Self = Self { bits: 0b000000010 }; + const NO_TRANSFORM: Self = Self { bits: 0b000000100 }; + const ONLY_IF_CACHED: Self = Self { bits: 0b000001000 }; + const MUST_REVALIDATE: Self = Self { bits: 0b000010000 }; + const PUBLIC: Self = Self { bits: 0b000100000 }; + const PRIVATE: Self = Self { bits: 0b001000000 }; + const PROXY_REVALIDATE: Self = Self { bits: 0b010000000 }; + const IMMUTABLE: Self = Self { bits: 0b100000000 }; + const MUST_UNDERSTAND: Self = Self { bits: 0b1000000000 }; + + fn empty() -> Self { + Self { bits: 0 } + } + + fn contains(&self, flag: Self) -> bool { + (self.bits & flag.bits) != 0 + } + + fn insert(&mut self, flag: Self) { + self.bits |= flag.bits; } } @@ -100,6 +118,15 @@ impl CacheControl { self.flags.contains(Flags::PRIVATE) } + /// Check if the `immutable` directive is set. + pub fn immutable(&self) -> bool { + self.flags.contains(Flags::IMMUTABLE) + } + /// Check if the `must_understand` directive is set. + pub fn must_understand(&self) -> bool { + self.flags.contains(Flags::MUST_UNDERSTAND) + } + /// Get the value of the `max-age` directive if set. pub fn max_age(&self) -> Option { self.max_age.map(Into::into) @@ -158,27 +185,38 @@ impl CacheControl { self } + /// Set the `immutable` directive. + pub fn with_immutable(mut self) -> Self { + self.flags.insert(Flags::IMMUTABLE); + self + } + + /// Set the `must_understand` directive. + pub fn with_must_understand(mut self) -> Self { + self.flags.insert(Flags::MUST_UNDERSTAND); + self + } /// Set the `max-age` directive. - pub fn with_max_age(mut self, seconds: Duration) -> Self { - self.max_age = Some(seconds.into()); + pub fn with_max_age(mut self, duration: Duration) -> Self { + self.max_age = Some(duration.into()); self } /// Set the `max-stale` directive. - pub fn with_max_stale(mut self, seconds: Duration) -> Self { - self.max_stale = Some(seconds.into()); + pub fn with_max_stale(mut self, duration: Duration) -> Self { + self.max_stale = Some(duration.into()); self } /// Set the `min-fresh` directive. - pub fn with_min_fresh(mut self, seconds: Duration) -> Self { - self.min_fresh = Some(seconds.into()); + pub fn with_min_fresh(mut self, duration: Duration) -> Self { + self.min_fresh = Some(duration.into()); self } /// Set the `s-maxage` directive. - pub fn with_s_max_age(mut self, seconds: Duration) -> Self { - self.s_max_age = Some(seconds.into()); + pub fn with_s_max_age(mut self, duration: Duration) -> Self { + self.s_max_age = Some(duration.into()); self } } @@ -230,12 +268,18 @@ impl FromIterator for FromIter { Directive::MustRevalidate => { cc.flags.insert(Flags::MUST_REVALIDATE); } + Directive::MustUnderstand => { + cc.flags.insert(Flags::MUST_UNDERSTAND); + } Directive::Public => { cc.flags.insert(Flags::PUBLIC); } Directive::Private => { cc.flags.insert(Flags::PRIVATE); } + Directive::Immutable => { + cc.flags.insert(Flags::IMMUTABLE); + } Directive::ProxyRevalidate => { cc.flags.insert(Flags::PROXY_REVALIDATE); } @@ -278,6 +322,8 @@ impl<'a> fmt::Display for Fmt<'a> { if_flag(Flags::MUST_REVALIDATE, Directive::MustRevalidate), if_flag(Flags::PUBLIC, Directive::Public), if_flag(Flags::PRIVATE, Directive::Private), + if_flag(Flags::IMMUTABLE, Directive::Immutable), + if_flag(Flags::MUST_UNDERSTAND, Directive::MustUnderstand), if_flag(Flags::PROXY_REVALIDATE, Directive::ProxyRevalidate), self.0 .max_age @@ -323,8 +369,10 @@ enum Directive { // response directives MustRevalidate, + MustUnderstand, Public, Private, + Immutable, ProxyRevalidate, SMaxAge(u64), } @@ -343,8 +391,10 @@ impl fmt::Display for Directive { Directive::MinFresh(secs) => return write!(f, "min-fresh={}", secs), Directive::MustRevalidate => "must-revalidate", + Directive::MustUnderstand => "must-understand", Directive::Public => "public", Directive::Private => "private", + Directive::Immutable => "immutable", Directive::ProxyRevalidate => "proxy-revalidate", Directive::SMaxAge(secs) => return write!(f, "s-maxage={}", secs), }, @@ -364,6 +414,8 @@ impl FromStr for KnownDirective { "must-revalidate" => Directive::MustRevalidate, "public" => Directive::Public, "private" => Directive::Private, + "immutable" => Directive::Immutable, + "must-understand" => Directive::MustUnderstand, "proxy-revalidate" => Directive::ProxyRevalidate, "" => return Err(()), _ => match s.find('=') { @@ -428,9 +480,30 @@ mod tests { ); } + #[test] + fn test_immutable() { + let cc = CacheControl::new().with_immutable(); + let headers = test_encode(cc.clone()); + assert_eq!(headers["cache-control"], "immutable"); + assert_eq!(test_decode::(&["immutable"]).unwrap(), cc); + assert!(cc.immutable()); + } + + #[test] + fn test_must_understand() { + let cc = CacheControl::new().with_must_understand(); + let headers = test_encode(cc.clone()); + assert_eq!(headers["cache-control"], "must-understand"); + assert_eq!( + test_decode::(&["must-understand"]).unwrap(), + cc + ); + assert!(cc.must_understand()); + } + #[test] fn test_parse_bad_syntax() { - assert_eq!(test_decode::(&["max-age=lolz"]), None,); + assert_eq!(test_decode::(&["max-age=lolz"]), None); } #[test] diff --git a/src/common/content_coding.rs b/src/common/content_coding.rs new file mode 100644 index 00000000..aedf9a4f --- /dev/null +++ b/src/common/content_coding.rs @@ -0,0 +1,145 @@ +use HeaderValue; + +// Derives an enum to represent content codings and some helpful impls +macro_rules! define_content_coding { + ($($coding:ident; $str:expr,)+) => { + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + /// Values that are used with headers like [`Content-Encoding`](self::ContentEncoding) or + /// [`Accept-Encoding`](self::AcceptEncoding) + /// + /// [RFC7231](https://www.iana.org/assignments/http-parameters/http-parameters.xhtml) + pub enum ContentCoding { + $( + #[doc = $str] + $coding, + )+ + } + + impl ContentCoding { + /// Returns a `&'static str` for a `ContentCoding` + /// + /// # Example + /// + /// ``` + /// use headers::ContentCoding; + /// + /// let coding = ContentCoding::BROTLI; + /// assert_eq!(coding.to_static(), "br"); + /// ``` + #[inline] + pub fn to_static(&self) -> &'static str { + match *self { + $(ContentCoding::$coding => $str,)+ + } + } + + /// 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. + /// + /// # Example + /// + /// ``` + /// use headers::ContentCoding; + /// + /// let invalid = ContentCoding::from_str("not a valid coding"); + /// assert_eq!(invalid, ContentCoding::IDENTITY); + /// + /// let valid = ContentCoding::from_str("gzip"); + /// assert_eq!(valid, ContentCoding::GZIP); + /// ``` + #[inline] + pub fn from_str(s: &str) -> Self { + ContentCoding::try_from_str(s).unwrap_or_else(|_| ContentCoding::IDENTITY) + } + + /// Given a `&str` will try to return a `ContentCoding` + /// + /// Different from `ContentCoding::from_str(&str)`, if `&str` is an invalid content + /// coding, it will return `Err(())` + /// + /// # Example + /// + /// ``` + /// use headers::ContentCoding; + /// + /// let invalid = ContentCoding::try_from_str("not a valid coding"); + /// assert!(invalid.is_err()); + /// + /// let valid = ContentCoding::try_from_str("gzip"); + /// assert_eq!(valid.unwrap(), ContentCoding::GZIP); + /// ``` + #[inline] + pub fn try_from_str(s: &str) -> Result { + match s { + $( + stringify!($coding) + | $str => Ok(ContentCoding::$coding), + )+ + _ => Err(()) + } + } + } + + impl std::string::ToString for ContentCoding { + #[inline] + fn to_string(&self) -> String { + match *self { + $(ContentCoding::$coding => $str.to_string(),)+ + } + } + } + + impl From 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; + + #[test] + fn to_static() { + assert_eq!(ContentCoding::GZIP.to_static(), "gzip"); + } + + #[test] + fn to_string() { + assert_eq!(ContentCoding::DEFLATE.to_string(), "deflate".to_string()); + } + + #[test] + fn from_str() { + assert_eq!(ContentCoding::from_str("br"), ContentCoding::BROTLI); + assert_eq!(ContentCoding::from_str("GZIP"), ContentCoding::GZIP); + assert_eq!(ContentCoding::from_str("zstd"), ContentCoding::ZSTD); + assert_eq!( + ContentCoding::from_str("blah blah"), + ContentCoding::IDENTITY + ); + } + + #[test] + fn try_from_str() { + assert_eq!(ContentCoding::try_from_str("br"), Ok(ContentCoding::BROTLI)); + assert_eq!(ContentCoding::try_from_str("zstd"), Ok(ContentCoding::ZSTD)); + assert_eq!(ContentCoding::try_from_str("blah blah"), Err(())); + } +} diff --git a/src/common/content_range.rs b/src/common/content_range.rs index 7ed2b200..65cd7965 100644 --- a/src/common/content_range.rs +++ b/src/common/content_range.rs @@ -178,22 +178,23 @@ fn split_in_two(s: &str, separator: char) -> Option<(&str, &str)> { } /* - test_header!(test_bytes, - vec![b"bytes 0-499/500"], - Some(ContentRange(ContentRangeSpec::Bytes { - range: Some((0, 499)), - complete_length: Some(500) - }))); - - test_header!(test_bytes_unknown_len, - vec![b"bytes 0-499/*"], - Some(ContentRange(ContentRangeSpec::Bytes { - range: Some((0, 499)), - complete_length: None - }))); - - test_header!(test_bytes_unknown_range, - vec![b"bytes */500"], +test_header!(test_bytes, + vec![b"bytes 0-499/500"], + Some(ContentRange(ContentRangeSpec::Bytes { + range: Some((0, 499)), + complete_length: Some(500) + }))); + +test_header!(test_bytes_unknown_len, + vec![b"bytes 0-499/*"], + Some(ContentRange(ContentRangeSpec::Bytes { + range: Some((0, 499)), + complete_length: None + }))); + +test_header!(test_bytes_unknown_range, + vec![b"bytes */ +500"], Some(ContentRange(ContentRangeSpec::Bytes { range: None, complete_length: Some(500) diff --git a/src/common/content_type.rs b/src/common/content_type.rs index bfe56527..1ae15c2e 100644 --- a/src/common/content_type.rs +++ b/src/common/content_type.rs @@ -135,6 +135,16 @@ impl fmt::Display for ContentType { } } +impl std::str::FromStr for ContentType { + type Err = ::Error; + + fn from_str(s: &str) -> Result { + s.parse::() + .map(|m| m.into()) + .map_err(|_| ::Error::invalid()) + } +} + #[cfg(test)] mod tests { use super::super::test_decode; @@ -148,6 +158,15 @@ mod tests { ); } + #[test] + fn from_str() { + assert_eq!( + "application/json".parse::().unwrap(), + ContentType::json(), + ); + assert!("invalid-mimetype".parse::().is_err()); + } + bench_header!(bench_plain, ContentType, "text/plain"); bench_header!(bench_json, ContentType, "application/json"); bench_header!( diff --git a/src/common/etag.rs b/src/common/etag.rs index 1bbd0740..25846b76 100644 --- a/src/common/etag.rs +++ b/src/common/etag.rs @@ -50,9 +50,7 @@ error_type!(InvalidETag); impl FromStr for ETag { type Err = InvalidETag; fn from_str(src: &str) -> Result { - let val = src - .parse() - .map_err(|_| InvalidETag { _inner: () })?; + let val = src.parse().map_err(|_| InvalidETag { _inner: () })?; EntityTag::from_owned(val) .map(ETag) diff --git a/src/common/host.rs b/src/common/host.rs index a5c41b1d..7c0d7acd 100644 --- a/src/common/host.rs +++ b/src/common/host.rs @@ -1,5 +1,5 @@ -use std::fmt; use std::convert::TryFrom; +use std::fmt; use http::uri::Authority; diff --git a/src/common/if_range.rs b/src/common/if_range.rs index 38480bbe..e2675b43 100644 --- a/src/common/if_range.rs +++ b/src/common/if_range.rs @@ -64,7 +64,9 @@ impl IfRange { pub fn is_modified(&self, etag: Option<&ETag>, last_modified: Option<&LastModified>) -> bool { match self.0 { IfRange_::Date(since) => last_modified.map(|time| since < time.0).unwrap_or(true), - IfRange_::EntityTag(ref entity) => etag.map(|etag| !etag.0.strong_eq(entity)).unwrap_or(true), + IfRange_::EntityTag(ref entity) => { + etag.map(|etag| !etag.0.strong_eq(entity)).unwrap_or(true) + } } } } diff --git a/src/common/mod.rs b/src/common/mod.rs index 3a1e9c0f..60e27402 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -7,7 +7,7 @@ //! is used, such as `ContentType(pub Mime)`. //pub use self::accept_charset::AcceptCharset; -//pub use self::accept_encoding::AcceptEncoding; +pub use self::accept_encoding::AcceptEncoding; //pub use self::accept_language::AcceptLanguage; pub use self::accept_ranges::AcceptRanges; //pub use self::accept::Accept; @@ -19,10 +19,12 @@ pub use self::access_control_expose_headers::AccessControlExposeHeaders; pub use self::access_control_max_age::AccessControlMaxAge; pub use self::access_control_request_headers::AccessControlRequestHeaders; pub use self::access_control_request_method::AccessControlRequestMethod; +pub use self::age::Age; pub use self::allow::Allow; pub use self::authorization::Authorization; pub use self::cache_control::CacheControl; pub use self::connection::Connection; +pub use self::content_coding::ContentCoding; pub use self::content_disposition::ContentDisposition; pub use self::content_encoding::ContentEncoding; //pub use self::content_language::ContentLanguage; @@ -127,7 +129,7 @@ macro_rules! bench_header { //mod accept; //mod accept_charset; -//mod accept_encoding; +mod accept_encoding; //mod accept_language; mod accept_ranges; mod access_control_allow_credentials; @@ -138,10 +140,12 @@ mod access_control_expose_headers; mod access_control_max_age; mod access_control_request_headers; mod access_control_request_method; +mod age; mod allow; pub mod authorization; mod cache_control; mod connection; +mod content_coding; mod content_disposition; mod content_encoding; //mod content_language; diff --git a/src/common/origin.rs b/src/common/origin.rs index 09349f5e..6d4a022e 100644 --- a/src/common/origin.rs +++ b/src/common/origin.rs @@ -1,5 +1,5 @@ -use std::fmt; use std::convert::TryFrom; +use std::fmt; use bytes::Bytes; use http::uri::{self, Authority, Scheme, Uri}; diff --git a/src/common/range.rs b/src/common/range.rs index 29cc79d3..6d35936d 100644 --- a/src/common/range.rs +++ b/src/common/range.rs @@ -48,29 +48,52 @@ impl Range { /// Creates a `Range` header from bounds. pub fn bytes(bounds: impl RangeBounds) -> Result { let v = match (bounds.start_bound(), bounds.end_bound()) { - (Bound::Unbounded, Bound::Included(end)) => format!("bytes=-{}", end), - (Bound::Unbounded, Bound::Excluded(&end)) => format!("bytes=-{}", end - 1), (Bound::Included(start), Bound::Included(end)) => format!("bytes={}-{}", start, end), (Bound::Included(start), Bound::Excluded(&end)) => { format!("bytes={}-{}", start, end - 1) } (Bound::Included(start), Bound::Unbounded) => format!("bytes={}-", start), + // These do not directly translate. + //(Bound::Unbounded, Bound::Included(end)) => format!("bytes=-{}", end), + //(Bound::Unbounded, Bound::Excluded(&end)) => format!("bytes=-{}", end - 1), _ => return Err(InvalidRange { _inner: () }), }; Ok(Range(::HeaderValue::from_str(&v).unwrap())) } - /// Iterate the range sets as a tuple of bounds. - pub fn iter<'a>(&'a self) -> impl Iterator, Bound)> + 'a { + /// Iterate the range sets as a tuple of bounds, if valid with length. + /// + /// The length of the content is passed as an argument, and all ranges + /// that can be satisfied will be iterated. + pub fn satisfiable_ranges<'a>( + &'a self, + len: u64, + ) -> impl Iterator, Bound)> + 'a { let s = self .0 .to_str() .expect("valid string checked in Header::decode()"); - s["bytes=".len()..].split(',').filter_map(|spec| { + s["bytes=".len()..].split(',').filter_map(move |spec| { let mut iter = spec.trim().splitn(2, '-'); - Some((parse_bound(iter.next()?)?, parse_bound(iter.next()?)?)) + let start = parse_bound(iter.next()?)?; + let end = parse_bound(iter.next()?)?; + + // Unbounded ranges in HTTP are actually a suffix + // For example, `-100` means the last 100 bytes. + if let Bound::Unbounded = start { + if let Bound::Included(end) = end { + if len < end { + // Last N bytes is larger than available! + return None; + } + return Some((Bound::Included(len - end), Bound::Unbounded)); + } + // else fall through + } + + Some((start, end)) }) } } @@ -416,3 +439,17 @@ fn test_byte_range_spec_to_satisfiable_range() { bench_header!(bytes_multi, Range, { vec![b"bytes=1-1001,2001-3001,10001-".to_vec()]}); bench_header!(custom_unit, Range, { vec![b"other=0-100000".to_vec()]}); */ + +#[test] +fn test_to_satisfiable_range_suffix() { + let range = super::test_decode::(&["bytes=-100"]).unwrap(); + let bounds = range.satisfiable_ranges(350).next().unwrap(); + assert_eq!(bounds, (Bound::Included(250), Bound::Unbounded)); +} + +#[test] +fn test_to_unsatisfiable_range_suffix() { + let range = super::test_decode::(&["bytes=-350"]).unwrap(); + let bounds = range.satisfiable_ranges(100).next(); + assert_eq!(bounds, None); +} diff --git a/src/common/referer.rs b/src/common/referer.rs index 864cbd72..c85973b9 100644 --- a/src/common/referer.rs +++ b/src/common/referer.rs @@ -5,7 +5,7 @@ use http::header::HeaderValue; /// `Referer` header, defined in /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.5.2) /// -/// The `Referer` [sic] header field allows the user agent to specify a +/// The `Referer` \[sic\] header field allows the user agent to specify a /// URI reference for the resource from which the target URI was obtained /// (i.e., the "referrer", though the field name is misspelled). A user /// agent MUST NOT include the fragment and userinfo components of the diff --git a/src/common/sec_websocket_accept.rs b/src/common/sec_websocket_accept.rs index b5152479..89ec7c07 100644 --- a/src/common/sec_websocket_accept.rs +++ b/src/common/sec_websocket_accept.rs @@ -1,4 +1,5 @@ -use base64; +use base64::engine::general_purpose::STANDARD as ENGINE; +use base64::Engine; use bytes::Bytes; use sha1::{Digest, Sha1}; @@ -37,9 +38,9 @@ impl From for SecWebsocketAccept { fn sign(key: &[u8]) -> SecWebsocketAccept { let mut sha1 = Sha1::default(); - sha1.input(key); - sha1.input(&b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"[..]); - let b64 = Bytes::from(base64::encode(&sha1.result())); + sha1.update(key); + sha1.update(&b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"[..]); + let b64 = Bytes::from(ENGINE.encode(&sha1.finalize())); let val = ::HeaderValue::from_maybe_shared(b64).expect("base64 is a valid value"); diff --git a/src/lib.rs b/src/lib.rs index bf05e9fa..dc809be9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ #![deny(missing_debug_implementations)] #![cfg_attr(test, deny(warnings))] #![cfg_attr(all(test, feature = "nightly"), feature(test))] -#![doc(html_root_url = "https://docs.rs/headers/0.3.2")] +#![doc(html_root_url = "https://docs.rs/headers/0.4.0")] //! # Typed HTTP Headers //! @@ -73,16 +73,15 @@ //! ``` extern crate base64; -#[macro_use] -extern crate bitflags; extern crate bytes; extern crate headers_core; extern crate http; +extern crate httpdate; +extern crate itertools; extern crate mime; extern crate sha1; #[cfg(all(test, feature = "nightly"))] extern crate test; -extern crate time; pub use headers_core::{Error, Header}; diff --git a/src/util/entity.rs b/src/util/entity.rs index c966b6fe..67604be4 100644 --- a/src/util/entity.rs +++ b/src/util/entity.rs @@ -167,9 +167,7 @@ impl EntityTag { } pub(crate) fn from_val(val: &HeaderValue) -> Option { - EntityTag::parse(val.as_bytes()).map(|_entity| { - EntityTag(val.clone()) - }) + EntityTag::parse(val.as_bytes()).map(|_entity| EntityTag(val.clone())) } } @@ -239,11 +237,10 @@ impl EntityTagRange { { match *self { EntityTagRange::Any => true, - EntityTagRange::Tags(ref tags) => { - tags.iter() - .flat_map(EntityTag::<&str>::parse) - .any(|tag| func(&tag, entity)) - }, + EntityTagRange::Tags(ref tags) => tags + .iter() + .flat_map(EntityTag::<&str>::parse) + .any(|tag| func(&tag, entity)), } } } diff --git a/src/util/flat_csv.rs b/src/util/flat_csv.rs index 099b0342..7be56c87 100644 --- a/src/util/flat_csv.rs +++ b/src/util/flat_csv.rs @@ -120,8 +120,8 @@ impl<'a, Sep: Separator> FromIterator<&'a HeaderValue> for FlatCsv { buf.extend_from_slice(val.as_bytes()); } - let val = - HeaderValue::from_maybe_shared(buf.freeze()).expect("comma separated HeaderValues are valid"); + let val = HeaderValue::from_maybe_shared(buf.freeze()) + .expect("comma separated HeaderValues are valid"); val.into() } @@ -151,8 +151,8 @@ impl FromIterator for FlatCsv { buf.extend_from_slice(val.as_bytes()); } - let val = - HeaderValue::from_maybe_shared(buf.freeze()).expect("comma separated HeaderValues are valid"); + let val = HeaderValue::from_maybe_shared(buf.freeze()) + .expect("comma separated HeaderValues are valid"); val.into() } diff --git a/src/util/http_date.rs b/src/util/http_date.rs index f2d8f7af..da3f8396 100644 --- a/src/util/http_date.rs +++ b/src/util/http_date.rs @@ -1,10 +1,10 @@ use std::fmt; use std::str::FromStr; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::SystemTime; use bytes::Bytes; use http::header::HeaderValue; -use time; +use httpdate; use super::IterExt; @@ -32,7 +32,7 @@ use super::IterExt; // HTTP-date, the sender MUST generate those timestamps in the // IMF-fixdate format. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct HttpDate(time::Tm); +pub(crate) struct HttpDate(httpdate::HttpDate); impl HttpDate { pub(crate) fn from_val(val: &HeaderValue) -> Option { @@ -74,96 +74,74 @@ impl<'a> From<&'a HttpDate> for HeaderValue { impl FromStr for HttpDate { type Err = Error; fn from_str(s: &str) -> Result { - time::strptime(s, "%a, %d %b %Y %T %Z") - .or_else(|_| time::strptime(s, "%A, %d-%b-%y %T %Z")) - .or_else(|_| time::strptime(s, "%c")) - .map(HttpDate) - .map_err(|_| Error(())) + Ok(HttpDate(s.parse().map_err(|_| Error(()))?)) } } impl fmt::Debug for HttpDate { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0.to_utc().rfc822(), f) + fmt::Display::fmt(&self.0, f) } } impl fmt::Display for HttpDate { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0.to_utc().rfc822(), f) + fmt::Display::fmt(&self.0, f) } } impl From for HttpDate { fn from(sys: SystemTime) -> HttpDate { - let tmspec = match sys.duration_since(UNIX_EPOCH) { - Ok(dur) => { - // subsec nanos always dropped - time::Timespec::new(dur.as_secs() as i64, 0) - } - Err(err) => { - let neg = err.duration(); - // subsec nanos always dropped - time::Timespec::new(-(neg.as_secs() as i64), 0) - } - }; - HttpDate(time::at_utc(tmspec)) + HttpDate(sys.into()) } } impl From for SystemTime { fn from(date: HttpDate) -> SystemTime { - let spec = date.0.to_timespec(); - if spec.sec >= 0 { - UNIX_EPOCH + Duration::new(spec.sec as u64, spec.nsec as u32) - } else { - UNIX_EPOCH - Duration::new(spec.sec as u64, spec.nsec as u32) - } + SystemTime::from(date.0) } } #[cfg(test)] mod tests { use super::HttpDate; - use time::Tm; - - const NOV_07: HttpDate = HttpDate(Tm { - tm_nsec: 0, - tm_sec: 37, - tm_min: 48, - tm_hour: 8, - tm_mday: 7, - tm_mon: 10, - tm_year: 94, - tm_wday: 0, - tm_isdst: 0, - tm_yday: 0, - tm_utcoff: 0, - }); + + use std::time::{Duration, UNIX_EPOCH}; + + // The old tests had Sunday, but 1994-11-07 is a Monday. + // See https://github.com/pyfisch/httpdate/pull/6#issuecomment-846881001 + fn nov_07() -> HttpDate { + HttpDate((UNIX_EPOCH + Duration::new(784198117, 0)).into()) + } + + #[test] + fn test_display_is_imf_fixdate() { + assert_eq!("Mon, 07 Nov 1994 08:48:37 GMT", &nov_07().to_string()); + } #[test] fn test_imf_fixdate() { assert_eq!( - "Sun, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), - NOV_07 + "Mon, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), + nov_07() ); } #[test] fn test_rfc_850() { assert_eq!( - "Sunday, 07-Nov-94 08:48:37 GMT" + "Monday, 07-Nov-94 08:48:37 GMT" .parse::() .unwrap(), - NOV_07 + nov_07() ); } #[test] fn test_asctime() { assert_eq!( - "Sun Nov 7 08:48:37 1994".parse::().unwrap(), - NOV_07 + "Mon Nov 7 08:48:37 1994".parse::().unwrap(), + nov_07() ); } diff --git a/src/util/mod.rs b/src/util/mod.rs index 07fddbfb..58b7715a 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -8,7 +8,7 @@ pub(crate) use self::fmt::fmt; pub(crate) use self::http_date::HttpDate; pub(crate) use self::iter::IterExt; //pub use language_tags::LanguageTag; -//pub use self::quality_value::{Quality, QualityValue}; +pub(crate) use self::quality_value::QualityValue; pub(crate) use self::seconds::Seconds; pub(crate) use self::value_string::HeaderValueString; @@ -20,7 +20,7 @@ mod flat_csv; mod fmt; mod http_date; mod iter; -//mod quality_value; +mod quality_value; mod seconds; mod value_string; diff --git a/src/util/quality_value.rs b/src/util/quality_value.rs new file mode 100644 index 00000000..abb0362d --- /dev/null +++ b/src/util/quality_value.rs @@ -0,0 +1,231 @@ +use self::sealed::SemiQ; +use std::marker::PhantomData; +use util::FlatCsv; + +/// A CSV list that respects the Quality Values syntax defined in +/// [RFC7321](https://tools.ietf.org/html/rfc7231#section-5.3.1) +/// +/// Many of the request header fields for proactive negotiation use a +/// common parameter, named "q" (case-insensitive), to assign a relative +/// "weight" to the preference for that associated kind of content. This +/// weight is referred to as a "quality value" (or "qvalue") because the +/// same parameter name is often used within server configurations to +/// assign a weight to the relative quality of the various +/// representations that can be selected for a resource. +/// +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct QualityValue { + csv: FlatCsv, + _marker: PhantomData, +} + +mod sealed { + use super::QualityValue; + use std::cmp::Ordering; + use std::convert::{From, TryFrom}; + use std::marker::PhantomData; + + use itertools::Itertools; + use util::{FlatCsv, TryFromValues}; + use HeaderValue; + + pub trait QualityDelimiter { + const STR: &'static str; + } + + /// enum that represents the ';q=' delimiter + #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub enum SemiQ {} + + impl QualityDelimiter for SemiQ { + const STR: &'static str = ";q="; + } + + /// enum that represents the ';level=' delimiter (extremely rare) + #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub enum SemiLevel {} + + impl QualityDelimiter for SemiLevel { + const STR: &'static str = ";level="; + } + + #[derive(Clone, Debug, PartialEq, Eq)] + struct QualityMeta<'a, Sep = SemiQ> { + pub data: &'a str, + pub quality: u16, + _marker: PhantomData, + } + + impl Ord for QualityMeta<'_, Delm> { + fn cmp(&self, other: &Self) -> Ordering { + other.quality.cmp(&self.quality) + } + } + + impl PartialOrd for QualityMeta<'_, Delm> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl<'a, Delm: QualityDelimiter> TryFrom<&'a str> for QualityMeta<'a, Delm> { + type Error = ::Error; + + fn try_from(val: &'a str) -> Result { + let mut parts: Vec<&str> = val.split(Delm::STR).collect(); + + match (parts.pop(), parts.pop()) { + (Some(qual), Some(data)) => { + let parsed: f32 = qual.parse().map_err(|_| ::Error::invalid())?; + let quality = (parsed * 1000_f32) as u16; + + Ok(QualityMeta { + data, + quality, + _marker: PhantomData, + }) + } + // No deliter present, assign a quality value of 1 + (Some(data), None) => Ok(QualityMeta { + data, + quality: 1000_u16, + _marker: PhantomData, + }), + _ => Err(::Error::invalid()), + } + } + } + + impl QualityValue { + pub(crate) fn iter(&self) -> impl Iterator { + self.csv + .iter() + .map(|v| QualityMeta::::try_from(v).unwrap()) + .into_iter() + .sorted() + .map(|pair| pair.data) + .into_iter() + } + } + + impl From for QualityValue { + fn from(csv: FlatCsv) -> Self { + QualityValue { + csv, + _marker: PhantomData, + } + } + } + + impl> TryFrom<(&str, F)> for QualityValue { + type Error = ::Error; + + fn try_from(pair: (&str, F)) -> Result { + let value = HeaderValue::try_from(format!("{}{}{}", pair.0, Delm::STR, pair.1.into())) + .map_err(|_e| ::Error::invalid())?; + Ok(QualityValue { + csv: value.into(), + _marker: PhantomData, + }) + } + } + + impl From for QualityValue { + fn from(value: HeaderValue) -> Self { + QualityValue { + csv: value.into(), + _marker: PhantomData, + } + } + } + + impl<'a, Delm> From<&'a QualityValue> for HeaderValue { + fn from(qual: &'a QualityValue) -> HeaderValue { + qual.csv.value.clone() + } + } + + impl From> for HeaderValue { + fn from(qual: QualityValue) -> HeaderValue { + qual.csv.value + } + } + + impl TryFromValues for QualityValue { + fn try_from_values<'i, I>(values: &mut I) -> Result + where + I: Iterator, + { + let flat: FlatCsv = values.collect(); + Ok(QualityValue::from(flat)) + } + } +} + +#[cfg(test)] +mod tests { + use super::{ + sealed::{SemiLevel, SemiQ}, + QualityValue, + }; + use HeaderValue; + + #[test] + fn multiple_qualities() { + let val = HeaderValue::from_static("gzip;q=1, br;q=0.8"); + let qual = QualityValue::::from(val); + + let mut values = qual.iter(); + assert_eq!(values.next(), Some("gzip")); + assert_eq!(values.next(), Some("br")); + assert_eq!(values.next(), None); + } + + #[test] + fn multiple_qualities_wrong_order() { + let val = HeaderValue::from_static("br;q=0.8, gzip;q=1.0"); + let qual = QualityValue::::from(val); + + let mut values = qual.iter(); + assert_eq!(values.next(), Some("gzip")); + assert_eq!(values.next(), Some("br")); + assert_eq!(values.next(), None); + } + + #[test] + fn multiple_values() { + let val = HeaderValue::from_static("deflate, gzip;q=1, br;q=0.8"); + let qual = QualityValue::::from(val); + + let mut values = qual.iter(); + assert_eq!(values.next(), Some("deflate")); + assert_eq!(values.next(), Some("gzip")); + assert_eq!(values.next(), Some("br")); + assert_eq!(values.next(), None); + } + + #[test] + fn multiple_values_wrong_order() { + let val = HeaderValue::from_static("deflate, br;q=0.8, gzip;q=1, *;q=0.1"); + let qual = QualityValue::::from(val); + + let mut values = qual.iter(); + assert_eq!(values.next(), Some("deflate")); + assert_eq!(values.next(), Some("gzip")); + assert_eq!(values.next(), Some("br")); + assert_eq!(values.next(), Some("*")); + assert_eq!(values.next(), None); + } + + #[test] + fn alternate_delimiter() { + let val = HeaderValue::from_static("deflate, br;level=0.8, gzip;level=1"); + let qual = QualityValue::::from(val); + + let mut values = qual.iter(); + assert_eq!(values.next(), Some("deflate")); + assert_eq!(values.next(), Some("gzip")); + assert_eq!(values.next(), Some("br")); + assert_eq!(values.next(), None); + } +} diff --git a/src/util/seconds.rs b/src/util/seconds.rs index 8cf7b6da..a1a9194b 100644 --- a/src/util/seconds.rs +++ b/src/util/seconds.rs @@ -11,7 +11,11 @@ impl Seconds { pub(crate) fn from_val(val: &HeaderValue) -> Option { let secs = val.to_str().ok()?.parse().ok()?; - Some(Seconds(Duration::from_secs(secs))) + Some(Self::from_secs(secs)) + } + + pub(crate) fn from_secs(secs: u64) -> Self { + Self::from(Duration::from_secs(secs)) } pub(crate) fn as_u64(&self) -> u64 {