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 a43a2e29..05dc58e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "headers" -version = "0.3.0" # don't forget to update html_root_url +version = "0.4.0" # don't forget to update html_root_url description = "typed HTTP headers" license = "MIT" readme = "README.md" @@ -9,6 +9,7 @@ repository = "https://github.com/hyperium/headers" authors = ["Sean McArthur "] keywords = ["http", "headers", "hyper", "hyperium"] categories = ["web-programming"] +rust-version = "1.56" [workspace] members = [ @@ -17,14 +18,13 @@ members = [ ] [dependencies] -http = "0.2.0" -headers-core = { version = "0.2", path = "./headers-core" } -base64 = "0.11" -bitflags = "1.0" -bytes = "0.5" -mime = "0.3" -sha-1 = "0.8" -time = "0.1" +http = "1.0.0" +headers-core = { version = "0.3", path = "./headers-core" } +base64 = "0.21.3" +bytes = "1" +mime = "0.3.14" +sha1 = "0.10" +httpdate = "1" [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..a93368fb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # rust http headers -[![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. 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/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_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 4e1aac94..25846b76 100644 --- a/src/common/etag.rs +++ b/src/common/etag.rs @@ -1,3 +1,4 @@ +use std::str::FromStr; use util::EntityTag; /// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3) @@ -26,6 +27,9 @@ use util::EntityTag; /// /// # Examples /// +/// ``` +/// let etag = "\"xyzzy\"".parse::().unwrap(); +/// ``` #[derive(Clone, Debug, PartialEq, Eq)] pub struct ETag(pub(super) EntityTag); @@ -34,6 +38,26 @@ derive_header! { name: ETAG } +impl ETag { + #[cfg(test)] + pub(crate) fn from_static(src: &'static str) -> ETag { + ETag(EntityTag::from_static(src)) + } +} + +error_type!(InvalidETag); + +impl FromStr for ETag { + type Err = InvalidETag; + fn from_str(src: &str) -> Result { + let val = src.parse().map_err(|_| InvalidETag { _inner: () })?; + + EntityTag::from_owned(val) + .map(ETag) + .ok_or_else(|| InvalidETag { _inner: () }) + } +} + /* test_etag { // From the RFC 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_match.rs b/src/common/if_match.rs index 24cb5457..5b9bdd95 100644 --- a/src/common/if_match.rs +++ b/src/common/if_match.rs @@ -1,5 +1,5 @@ use super::ETag; -use util::FlatCsv; +use util::EntityTagRange; use HeaderValue; /// `If-Match` header, defined in @@ -37,7 +37,7 @@ use HeaderValue; /// let if_match = IfMatch::any(); /// ``` #[derive(Clone, Debug, PartialEq)] -pub struct IfMatch(FlatCsv); +pub struct IfMatch(EntityTagRange); derive_header! { IfMatch(_), @@ -47,12 +47,65 @@ derive_header! { impl IfMatch { /// Create a new `If-Match: *` header. pub fn any() -> IfMatch { - IfMatch(HeaderValue::from_static("*").into()) + IfMatch(EntityTagRange::Any) + } + + /// Returns whether this is `If-Match: *`, matching any entity tag. + pub fn is_any(&self) -> bool { + match self.0 { + EntityTagRange::Any => true, + EntityTagRange::Tags(..) => false, + } + } + + /// Checks whether the `ETag` strongly matches. + pub fn precondition_passes(&self, etag: &ETag) -> bool { + self.0.matches_strong(&etag.0) } } impl From for IfMatch { fn from(etag: ETag) -> IfMatch { - IfMatch(HeaderValue::from(etag.0).into()) + IfMatch(EntityTagRange::Tags(HeaderValue::from(etag.0).into())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_any() { + assert!(IfMatch::any().is_any()); + assert!(!IfMatch::from(ETag::from_static("\"yolo\"")).is_any()); + } + + #[test] + fn precondition_fails() { + let if_match = IfMatch::from(ETag::from_static("\"foo\"")); + + let bar = ETag::from_static("\"bar\""); + let weak_foo = ETag::from_static("W/\"foo\""); + + assert!(!if_match.precondition_passes(&bar)); + assert!(!if_match.precondition_passes(&weak_foo)); + } + + #[test] + fn precondition_passes() { + let foo = ETag::from_static("\"foo\""); + + let if_match = IfMatch::from(foo.clone()); + + assert!(if_match.precondition_passes(&foo)); + } + + #[test] + fn precondition_any() { + let foo = ETag::from_static("\"foo\""); + + let if_match = IfMatch::any(); + + assert!(if_match.precondition_passes(&foo)); } } diff --git a/src/common/if_none_match.rs b/src/common/if_none_match.rs index 501c8c88..590609cb 100644 --- a/src/common/if_none_match.rs +++ b/src/common/if_none_match.rs @@ -1,5 +1,5 @@ use super::ETag; -use util::FlatCsv; +use util::EntityTagRange; use HeaderValue; /// `If-None-Match` header, defined in @@ -39,7 +39,7 @@ use HeaderValue; /// let if_none_match = IfNoneMatch::any(); /// ``` #[derive(Clone, Debug, PartialEq)] -pub struct IfNoneMatch(FlatCsv); +pub struct IfNoneMatch(EntityTagRange); derive_header! { IfNoneMatch(_), @@ -49,13 +49,18 @@ derive_header! { impl IfNoneMatch { /// Create a new `If-None-Match: *` header. pub fn any() -> IfNoneMatch { - IfNoneMatch(HeaderValue::from_static("*").into()) + IfNoneMatch(EntityTagRange::Any) + } + + /// Checks whether the ETag passes this precondition. + pub fn precondition_passes(&self, etag: &ETag) -> bool { + !self.0.matches_weak(&etag.0) } } impl From for IfNoneMatch { fn from(etag: ETag) -> IfNoneMatch { - IfNoneMatch(HeaderValue::from(etag.0).into()) + IfNoneMatch(EntityTagRange::Tags(HeaderValue::from(etag.0).into())) } } @@ -69,27 +74,38 @@ test_if_none_match { } */ -/* #[cfg(test)] mod tests { - use super::IfNoneMatch; - use Header; - use EntityTag; + use super::*; + + #[test] + fn precondition_fails() { + let foo = ETag::from_static("\"foo\""); + let weak_foo = ETag::from_static("W/\"foo\""); + + let if_none = IfNoneMatch::from(foo.clone()); + + assert!(!if_none.precondition_passes(&foo)); + assert!(!if_none.precondition_passes(&weak_foo)); + } #[test] - fn test_if_none_match() { - let mut if_none_match: ::Result; - - if_none_match = Header::parse_header(&b"*".as_ref().into()); - assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any)); - - if_none_match = Header::parse_header(&b"\"foobar\", W/\"weak-etag\"".as_ref().into()); - let mut entities: Vec = Vec::new(); - let foobar_etag = EntityTag::new(false, "foobar".to_owned()); - let weak_etag = EntityTag::new(true, "weak-etag".to_owned()); - entities.push(foobar_etag); - entities.push(weak_etag); - assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities))); + fn precondition_passes() { + let if_none = IfNoneMatch::from(ETag::from_static("\"foo\"")); + + let bar = ETag::from_static("\"bar\""); + let weak_bar = ETag::from_static("W/\"bar\""); + + assert!(if_none.precondition_passes(&bar)); + assert!(if_none.precondition_passes(&weak_bar)); + } + + #[test] + fn precondition_any() { + let foo = ETag::from_static("\"foo\""); + + let if_none = IfNoneMatch::any(); + + assert!(!if_none.precondition_passes(&foo)); } } -*/ diff --git a/src/common/if_range.rs b/src/common/if_range.rs index 787c1baa..e2675b43 100644 --- a/src/common/if_range.rs +++ b/src/common/if_range.rs @@ -61,10 +61,12 @@ impl IfRange { /// Checks if the resource has been modified, or if the range request /// can be served. - pub fn is_modified(&self, _etag: Option<&ETag>, last_modified: Option<&LastModified>) -> bool { + 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(_) => true, + IfRange_::EntityTag(ref entity) => { + etag.map(|etag| !etag.0.strong_eq(entity)).unwrap_or(true) + } } } } @@ -116,3 +118,19 @@ mod tests { test_header!(test3, vec![b"this-is-invalid"], None::); } */ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_modified_etag() { + let etag = ETag::from_static("\"xyzzy\""); + let if_range = IfRange::etag(etag.clone()); + + assert!(!if_range.is_modified(Some(&etag), None)); + + let etag = ETag::from_static("W/\"xyzzy\""); + assert!(if_range.is_modified(Some(&etag), None)); + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs index 3a1e9c0f..2237ae8e 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -19,6 +19,7 @@ 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; @@ -138,6 +139,7 @@ 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; 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/common/strict_transport_security.rs b/src/common/strict_transport_security.rs index df35fccd..628b8004 100644 --- a/src/common/strict_transport_security.rs +++ b/src/common/strict_transport_security.rs @@ -70,6 +70,18 @@ impl StrictTransportSecurity { include_subdomains: false, } } + + // getters + + /// Get whether this should include subdomains. + pub fn include_subdomains(&self) -> bool { + self.include_subdomains + } + + /// Get the max-age. + pub fn max_age(&self) -> Duration { + self.max_age.into() + } } enum Directive { diff --git a/src/lib.rs b/src/lib.rs index 1dc01acd..18c57fba 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.0")] +#![doc(html_root_url = "https://docs.rs/headers/0.4.0")] //! # Typed HTTP Headers //! @@ -73,16 +73,14 @@ //! ``` extern crate base64; -#[macro_use] -extern crate bitflags; extern crate bytes; extern crate headers_core; extern crate http; +extern crate httpdate; 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 a19b3d4e..67604be4 100644 --- a/src/util/entity.rs +++ b/src/util/entity.rs @@ -1,4 +1,6 @@ -use super::IterExt; +use std::fmt; + +use super::{FlatCsv, IterExt}; use HeaderValue; /// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) @@ -31,47 +33,21 @@ use HeaderValue; /// | `W/"1"` | `W/"2"` | no match | no match | /// | `W/"1"` | `"1"` | no match | match | /// | `"1"` | `"1"` | match | match | -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct EntityTag(HeaderValue); - -impl EntityTag { - /* - /// Constructs a new EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn new(weak: bool, tag: String) -> EntityTag { - assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); - EntityTag { weak: weak, tag: tag } - } +#[derive(Clone, Eq, PartialEq)] +pub(crate) struct EntityTag(T); - /// Constructs a new weak EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn weak(tag: String) -> EntityTag { - EntityTag::new(true, tag) - } - - /// Constructs a new strong EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn strong(tag: String) -> EntityTag { - EntityTag::new(false, tag) - } - */ +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum EntityTagRange { + Any, + Tags(FlatCsv), +} - pub fn from_static(bytes: &'static str) -> EntityTag { - let val = HeaderValue::from_static(bytes); - match EntityTag::from_val(&val) { - Some(tag) => tag, - None => { - panic!("invalid static string for EntityTag: {:?}", bytes); - } - } - } +// ===== impl EntityTag ===== +impl> EntityTag { /// Get the tag. - pub fn tag(&self) -> &[u8] { - let bytes = self.0.as_bytes(); + pub(crate) fn tag(&self) -> &[u8] { + let bytes = self.0.as_ref(); let end = bytes.len() - 1; if bytes[0] == b'W' { // W/"" @@ -83,35 +59,43 @@ impl EntityTag { } /// Return if this is a "weak" tag. - pub fn is_weak(&self) -> bool { - self.0.as_bytes()[0] == b'W' + pub(crate) fn is_weak(&self) -> bool { + self.0.as_ref()[0] == b'W' } /// For strong comparison two entity-tags are equivalent if both are not weak and their /// opaque-tags match character-by-character. - pub fn strong_eq(&self, other: &EntityTag) -> bool { + pub(crate) fn strong_eq(&self, other: &EntityTag) -> bool + where + R: AsRef<[u8]>, + { !self.is_weak() && !other.is_weak() && self.tag() == other.tag() } /// For weak comparison two entity-tags are equivalent if their /// opaque-tags match character-by-character, regardless of either or /// both being tagged as "weak". - pub fn weak_eq(&self, other: &EntityTag) -> bool { + pub(crate) fn weak_eq(&self, other: &EntityTag) -> bool + where + R: AsRef<[u8]>, + { self.tag() == other.tag() } /// The inverse of `EntityTag.strong_eq()`. - pub fn strong_ne(&self, other: &EntityTag) -> bool { + #[cfg(test)] + pub(crate) fn strong_ne(&self, other: &EntityTag) -> bool { !self.strong_eq(other) } /// The inverse of `EntityTag.weak_eq()`. - pub fn weak_ne(&self, other: &EntityTag) -> bool { + #[cfg(test)] + pub(crate) fn weak_ne(&self, other: &EntityTag) -> bool { !self.weak_eq(other) } - pub(crate) fn from_val(val: &HeaderValue) -> Option { - let slice = val.as_bytes(); + pub(crate) fn parse(src: T) -> Option { + let slice = src.as_ref(); let length = slice.len(); // Early exits if it doesn't terminate in a DQUOTE. @@ -134,13 +118,65 @@ impl EntityTag { }; if check_slice_validity(&slice[start..length - 1]) { - Some(EntityTag(val.clone())) + Some(EntityTag(src)) } else { None } } } +impl EntityTag { + /* + /// Constructs a new EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn new(weak: bool, tag: String) -> EntityTag { + assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); + EntityTag { weak: weak, tag: tag } + } + + /// Constructs a new weak EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn weak(tag: String) -> EntityTag { + EntityTag::new(true, tag) + } + + /// Constructs a new strong EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn strong(tag: String) -> EntityTag { + EntityTag::new(false, tag) + } + */ + + #[cfg(test)] + pub fn from_static(bytes: &'static str) -> EntityTag { + let val = HeaderValue::from_static(bytes); + match EntityTag::from_val(&val) { + Some(tag) => tag, + None => { + panic!("invalid static string for EntityTag: {:?}", bytes); + } + } + } + + pub(crate) fn from_owned(val: HeaderValue) -> Option { + EntityTag::parse(val.as_bytes())?; + Some(EntityTag(val)) + } + + pub(crate) fn from_val(val: &HeaderValue) -> Option { + EntityTag::parse(val.as_bytes()).map(|_entity| EntityTag(val.clone())) + } +} + +impl fmt::Debug for EntityTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + impl super::TryFromValues for EntityTag { fn try_from_values<'i, I>(values: &mut I) -> Result where @@ -184,6 +220,54 @@ fn check_slice_validity(slice: &[u8]) -> bool { }) } +// ===== impl EntityTagRange ===== + +impl EntityTagRange { + pub(crate) fn matches_strong(&self, entity: &EntityTag) -> bool { + self.matches_if(entity, |a, b| a.strong_eq(b)) + } + + pub(crate) fn matches_weak(&self, entity: &EntityTag) -> bool { + self.matches_if(entity, |a, b| a.weak_eq(b)) + } + + fn matches_if(&self, entity: &EntityTag, func: F) -> bool + where + F: Fn(&EntityTag<&str>, &EntityTag) -> bool, + { + match *self { + EntityTagRange::Any => true, + EntityTagRange::Tags(ref tags) => tags + .iter() + .flat_map(EntityTag::<&str>::parse) + .any(|tag| func(&tag, entity)), + } + } +} + +impl super::TryFromValues for EntityTagRange { + fn try_from_values<'i, I>(values: &mut I) -> Result + where + I: Iterator, + { + let flat = FlatCsv::try_from_values(values)?; + if flat.value == "*" { + Ok(EntityTagRange::Any) + } else { + Ok(EntityTagRange::Tags(flat)) + } + } +} + +impl<'a> From<&'a EntityTagRange> for HeaderValue { + fn from(tag: &'a EntityTagRange) -> HeaderValue { + match *tag { + EntityTagRange::Any => HeaderValue::from_static("*"), + EntityTagRange::Tags(ref tags) => tags.into(), + } + } +} + #[cfg(test)] mod tests { use super::*; 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 7ac789d9..07fddbfb 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -2,7 +2,7 @@ use HeaderValue; //pub use self::charset::Charset; //pub use self::encoding::Encoding; -pub(crate) use self::entity::EntityTag; +pub(crate) use self::entity::{EntityTag, EntityTagRange}; pub(crate) use self::flat_csv::{FlatCsv, SemiColon}; pub(crate) use self::fmt::fmt; pub(crate) use self::http_date::HttpDate; @@ -43,11 +43,7 @@ macro_rules! error_type { } } - impl ::std::error::Error for $name { - fn description(&self) -> &str { - stringify!($name) - } - } + impl ::std::error::Error for $name {} }; } diff --git a/src/util/seconds.rs b/src/util/seconds.rs index 0c8297b2..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 { @@ -39,6 +43,7 @@ impl<'a> From<&'a Seconds> for HeaderValue { impl From for Seconds { fn from(dur: Duration) -> Seconds { + debug_assert!(dur.subsec_nanos() == 0); Seconds(dur) } }