From e255f88dd2824be5d9d2de932583a5bdd6e3f6a8 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Sun, 30 Nov 2014 15:47:02 -0800 Subject: [PATCH 1/3] feat(headers): add Expires header --- src/header/common/date.rs | 45 +++++------------------------------- src/header/common/expires.rs | 42 +++++++++++++++++++++++++++++++++ src/header/common/mod.rs | 3 +++ src/header/common/util.rs | 32 +++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 39 deletions(-) create mode 100644 src/header/common/expires.rs diff --git a/src/header/common/date.rs b/src/header/common/date.rs index ffa0a548..7a283c0e 100644 --- a/src/header/common/date.rs +++ b/src/header/common/date.rs @@ -1,8 +1,8 @@ -use header::{Header, HeaderFormat}; use std::fmt::{mod, Show}; -use super::util::from_one_raw_str; use std::str::FromStr; -use time::{Tm, strptime}; +use time::Tm; +use header::{Header, HeaderFormat}; +use super::util::{from_one_raw_str, tm_from_str}; // Egh, replace as soon as something better than time::Tm exists. /// The `Date` header field. @@ -21,17 +21,10 @@ impl Header for Date { } } + impl HeaderFormat for Date { fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - self.fmt(fmt) - } -} - -impl fmt::Show for Date { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - let Date(ref tm) = *self; - // bummer that tm.strftime allocates a string. It would nice if it - // returned a Show instead, since I don't need the String here + let tm = **self; match tm.tm_utcoff { 0 => tm.rfc822().fmt(fmt), _ => tm.to_utc().rfc822().fmt(fmt) @@ -40,34 +33,8 @@ impl fmt::Show for Date { } impl FromStr for Date { - // Prior to 1995, there were three different formats commonly used by - // servers to communicate timestamps. For compatibility with old - // implementations, all three are defined here. The preferred format is - // a fixed-length and single-zone subset of the date and time - // specification used by the Internet Message Format [RFC5322]. - // - // HTTP-date = IMF-fixdate / obs-date - // - // An example of the preferred format is - // - // Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate - // - // Examples of the two obsolete formats are - // - // Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format - // Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format - // - // A recipient that parses a timestamp value in an HTTP header field - // MUST accept all three HTTP-date formats. When a sender generates a - // header field that contains one or more timestamps defined as - // HTTP-date, the sender MUST generate those timestamps in the - // IMF-fixdate format. fn from_str(s: &str) -> Option { - strptime(s, "%a, %d %b %Y %T %Z").or_else(|_| { - strptime(s, "%A, %d-%b-%y %T %Z") - }).or_else(|_| { - strptime(s, "%c") - }).ok().map(|tm| Date(tm)) + tm_from_str(s).map(Date) } } diff --git a/src/header/common/expires.rs b/src/header/common/expires.rs new file mode 100644 index 00000000..b11e08fe --- /dev/null +++ b/src/header/common/expires.rs @@ -0,0 +1,42 @@ +use std::fmt::{mod, Show}; +use std::str::FromStr; +use time::Tm; +use header::{Header, HeaderFormat}; +use super::util::{from_one_raw_str, tm_from_str}; + +/// The `Expires` header field. +#[deriving(PartialEq, Clone)] +pub struct Expires(pub Tm); + +deref!(Expires -> Tm) + +impl Header for Expires { + fn header_name(_: Option) -> &'static str { + "Expires" + } + + fn parse_header(raw: &[Vec]) -> Option { + from_one_raw_str(raw) + } +} + + +impl HeaderFormat for Expires { + fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let tm = **self; + match tm.tm_utcoff { + 0 => tm.rfc822().fmt(fmt), + _ => tm.to_utc().rfc822().fmt(fmt) + } + } +} + +impl FromStr for Expires { + fn from_str(s: &str) -> Option { + tm_from_str(s).map(Expires) + } +} + +bench_header!(imf_fixdate, Expires, { vec![b"Sun, 07 Nov 1994 08:48:37 GMT".to_vec()] }) +bench_header!(rfc_850, Expires, { vec![b"Sunday, 06-Nov-94 08:49:37 GMT".to_vec()] }) +bench_header!(asctime, Expires, { vec![b"Sun Nov 6 08:49:37 1994".to_vec()] }) diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs index 664966d1..dd3a0d9f 100644 --- a/src/header/common/mod.rs +++ b/src/header/common/mod.rs @@ -96,6 +96,9 @@ pub mod content_type; /// Exposes the Date header. pub mod date; +/// Exposes the Expires header. +pub mod expires; + /// Exposes the Host header. pub mod host; diff --git a/src/header/common/util.rs b/src/header/common/util.rs index a6063975..0d4f60f9 100644 --- a/src/header/common/util.rs +++ b/src/header/common/util.rs @@ -2,6 +2,7 @@ use std::str::{FromStr, from_utf8}; use std::fmt::{mod, Show}; +use time::{Tm, strptime}; /// Reads a single raw string when parsing a header pub fn from_one_raw_str(raw: &[Vec]) -> Option { @@ -43,3 +44,34 @@ pub fn fmt_comma_delimited(fmt: &mut fmt::Formatter, parts: &[T]) -> fm } Ok(()) } + +/// Get a Tm from HTTP date formats. +// Prior to 1995, there were three different formats commonly used by +// servers to communicate timestamps. For compatibility with old +// implementations, all three are defined here. The preferred format is +// a fixed-length and single-zone subset of the date and time +// specification used by the Internet Message Format [RFC5322]. +// +// HTTP-date = IMF-fixdate / obs-date +// +// An example of the preferred format is +// +// Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate +// +// Examples of the two obsolete formats are +// +// Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format +// Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format +// +// A recipient that parses a timestamp value in an HTTP header field +// MUST accept all three HTTP-date formats. When a sender generates a +// header field that contains one or more timestamps defined as +// HTTP-date, the sender MUST generate those timestamps in the +// IMF-fixdate format. +pub fn tm_from_str(s: &str) -> Option { + strptime(s, "%a, %d %b %Y %T %Z").or_else(|_| { + strptime(s, "%A, %d-%b-%y %T %Z") + }).or_else(|_| { + strptime(s, "%c") + }).ok() +} From 0297147dd1d379869eac6049f8ecbe2483bb3ca1 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Sun, 30 Nov 2014 15:59:40 -0800 Subject: [PATCH 2/3] feat(headers): add LastModified header --- src/header/common/last_modified.rs | 42 ++++++++++++++++++++++++++++++ src/header/common/mod.rs | 5 ++++ 2 files changed, 47 insertions(+) create mode 100644 src/header/common/last_modified.rs diff --git a/src/header/common/last_modified.rs b/src/header/common/last_modified.rs new file mode 100644 index 00000000..83e19ad1 --- /dev/null +++ b/src/header/common/last_modified.rs @@ -0,0 +1,42 @@ +use std::fmt::{mod, Show}; +use std::str::FromStr; +use time::Tm; +use header::{Header, HeaderFormat}; +use super::util::{from_one_raw_str, tm_from_str}; + +/// The `LastModified` header field. +#[deriving(PartialEq, Clone)] +pub struct LastModified(pub Tm); + +deref!(LastModified -> Tm) + +impl Header for LastModified { + fn header_name(_: Option) -> &'static str { + "Last-Modified" + } + + fn parse_header(raw: &[Vec]) -> Option { + from_one_raw_str(raw) + } +} + + +impl HeaderFormat for LastModified { + fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let tm = **self; + match tm.tm_utcoff { + 0 => tm.rfc822().fmt(fmt), + _ => tm.to_utc().rfc822().fmt(fmt) + } + } +} + +impl FromStr for LastModified { + fn from_str(s: &str) -> Option { + tm_from_str(s).map(LastModified) + } +} + +bench_header!(imf_fixdate, LastModified, { vec![b"Sun, 07 Nov 1994 08:48:37 GMT".to_vec()] }) +bench_header!(rfc_850, LastModified, { vec![b"Sunday, 06-Nov-94 08:49:37 GMT".to_vec()] }) +bench_header!(asctime, LastModified, { vec![b"Sun Nov 6 08:49:37 1994".to_vec()] }) diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs index dd3a0d9f..f262622d 100644 --- a/src/header/common/mod.rs +++ b/src/header/common/mod.rs @@ -13,7 +13,9 @@ pub use self::connection::Connection; pub use self::content_length::ContentLength; pub use self::content_type::ContentType; pub use self::date::Date; +pub use self::expires::Expires; pub use self::host::Host; +pub use self::last_modified::LastModified; pub use self::location::Location; pub use self::transfer_encoding::TransferEncoding; pub use self::upgrade::Upgrade; @@ -102,6 +104,9 @@ pub mod expires; /// Exposes the Host header. pub mod host; +/// Exposes the LastModified header. +pub mod last_modified; + /// Exposes the Location header. pub mod location; From f182f53210ec8f7ec0c9f342b28f71f0efa1141b Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 1 Dec 2014 19:58:07 -0800 Subject: [PATCH 3/3] feat(headers): add CacheControl header --- src/header/common/cache_control.rs | 165 +++++++++++++++++++++++++ src/header/common/mod.rs | 4 + src/header/common/transfer_encoding.rs | 5 +- src/header/common/util.rs | 10 +- 4 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 src/header/common/cache_control.rs diff --git a/src/header/common/cache_control.rs b/src/header/common/cache_control.rs new file mode 100644 index 00000000..f4563d49 --- /dev/null +++ b/src/header/common/cache_control.rs @@ -0,0 +1,165 @@ +use std::fmt; +use std::str::FromStr; +use header::{Header, HeaderFormat}; +use super::util::{from_one_comma_delimited, fmt_comma_delimited}; + +/// The Cache-Control header. +#[deriving(PartialEq, Clone, Show)] +pub struct CacheControl(pub Vec); + +deref!(CacheControl -> Vec) + +impl Header for CacheControl { + fn header_name(_: Option) -> &'static str { + "Cache-Control" + } + + fn parse_header(raw: &[Vec]) -> Option { + let directives = raw.iter() + .filter_map(|line| from_one_comma_delimited(line[])) + .collect::>>() + .concat_vec(); + if directives.len() > 0 { + Some(CacheControl(directives)) + } else { + None + } + } +} + +impl HeaderFormat for CacheControl { + fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt_comma_delimited(fmt, self[]) + } +} + +/// CacheControl contains a list of these directives. +#[deriving(PartialEq, Clone)] +pub enum CacheDirective { + /// "no-cache" + NoCache, + /// "no-store" + NoStore, + /// "no-transform" + NoTransform, + /// "only-if-cached" + OnlyIfCached, + + // request directives + /// "max-age=delta" + MaxAge(uint), + /// "max-stale=delta" + MaxStale(uint), + /// "min-fresh=delta" + MinFresh(uint), + + // response directives + /// "must-revalidate" + MustRevalidate, + /// "public" + Public, + /// "private" + Private, + /// "proxy-revalidate" + ProxyRevalidate, + /// "s-maxage=delta" + SMaxAge(uint), + + /// Extension directives. Optionally include an argument. + Extension(String, Option) +} + +impl fmt::Show for CacheDirective { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::CacheDirective::*; + match *self { + NoCache => "no-cache", + NoStore => "no-store", + NoTransform => "no-transform", + OnlyIfCached => "only-if-cached", + + MaxAge(secs) => return write!(f, "max-age={}", secs), + MaxStale(secs) => return write!(f, "max-stale={}", secs), + MinFresh(secs) => return write!(f, "min-fresh={}", secs), + + MustRevalidate => "must-revalidate", + Public => "public", + Private => "private", + ProxyRevalidate => "proxy-revalidate", + SMaxAge(secs) => return write!(f, "s-maxage={}", secs), + + Extension(ref name, None) => name[], + Extension(ref name, Some(ref arg)) => return write!(f, "{}={}", name, arg), + + }.fmt(f) + } +} + +impl FromStr for CacheDirective { + fn from_str(s: &str) -> Option { + use self::CacheDirective::*; + match s { + "no-cache" => Some(NoCache), + "no-store" => Some(NoStore), + "no-transform" => Some(NoTransform), + "only-if-cached" => Some(OnlyIfCached), + "must-revalidate" => Some(MustRevalidate), + "public" => Some(Public), + "private" => Some(Private), + "proxy-revalidate" => Some(ProxyRevalidate), + "" => None, + _ => match s.find('=') { + Some(idx) if idx+1 < s.len() => match (s[..idx], s[idx+1..].trim_chars('"')) { + ("max-age" , secs) => from_str::(secs).map(MaxAge), + ("max-stale", secs) => from_str::(secs).map(MaxStale), + ("min-fresh", secs) => from_str::(secs).map(MinFresh), + ("s-maxage", secs) => from_str::(secs).map(SMaxAge), + (left, right) => Some(Extension(left.into_string(), Some(right.into_string()))) + }, + Some(_) => None, + None => Some(Extension(s.into_string(), None)) + } + } + } +} + +#[cfg(test)] +mod tests { + use header::Header; + use super::*; + + #[test] + fn test_parse_multiple_headers() { + let cache = Header::parse_header(&[b"no-cache".to_vec(), b"private".to_vec()]); + assert_eq!(cache, Some(CacheControl(vec![CacheDirective::NoCache, + CacheDirective::Private]))) + } + + #[test] + fn test_parse_argument() { + let cache = Header::parse_header(&[b"max-age=100, private".to_vec()]); + assert_eq!(cache, Some(CacheControl(vec![CacheDirective::MaxAge(100), + CacheDirective::Private]))) + } + + #[test] + fn test_parse_quote_form() { + let cache = Header::parse_header(&[b"max-age=\"200\"".to_vec()]); + assert_eq!(cache, Some(CacheControl(vec![CacheDirective::MaxAge(200)]))) + } + + #[test] + fn test_parse_extension() { + let cache = Header::parse_header(&[b"foo, bar=baz".to_vec()]); + assert_eq!(cache, Some(CacheControl(vec![CacheDirective::Extension("foo".to_string(), None), + CacheDirective::Extension("bar".to_string(), Some("baz".to_string()))]))) + } + + #[test] + fn test_parse_bad_syntax() { + let cache: Option = Header::parse_header(&[b"foo=".to_vec()]); + assert_eq!(cache, None) + } +} + +bench_header!(normal, CacheControl, { vec![b"no-cache, private".to_vec(), b"max-age=100".to_vec()] }) diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs index f262622d..4c019ff0 100644 --- a/src/header/common/mod.rs +++ b/src/header/common/mod.rs @@ -8,6 +8,7 @@ pub use self::accept::Accept; pub use self::authorization::Authorization; +pub use self::cache_control::CacheControl; pub use self::cookie::Cookies; pub use self::connection::Connection; pub use self::content_length::ContentLength; @@ -83,6 +84,9 @@ pub mod accept; /// Exposes the Authorization header. pub mod authorization; +/// Exposes the CacheControl header. +pub mod cache_control; + /// Exposes the Cookie header. pub mod cookie; diff --git a/src/header/common/transfer_encoding.rs b/src/header/common/transfer_encoding.rs index ac628e0b..d3db256e 100644 --- a/src/header/common/transfer_encoding.rs +++ b/src/header/common/transfer_encoding.rs @@ -76,14 +76,13 @@ impl Header for TransferEncoding { } fn parse_header(raw: &[Vec]) -> Option { - from_comma_delimited(raw).map(|vec| TransferEncoding(vec)) + from_comma_delimited(raw).map(TransferEncoding) } } impl HeaderFormat for TransferEncoding { fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - let TransferEncoding(ref parts) = *self; - fmt_comma_delimited(fmt, parts[]) + fmt_comma_delimited(fmt, self[]) } } diff --git a/src/header/common/util.rs b/src/header/common/util.rs index 0d4f60f9..b6bb4d3c 100644 --- a/src/header/common/util.rs +++ b/src/header/common/util.rs @@ -16,13 +16,19 @@ pub fn from_one_raw_str(raw: &[Vec]) -> Option { } } -/// Reads a comma-delimited raw string into a Vec. +/// Reads a comma-delimited raw header into a Vec. +#[inline] pub fn from_comma_delimited(raw: &[Vec]) -> Option> { if raw.len() != 1 { return None; } // we JUST checked that raw.len() == 1, so raw[0] WILL exist. - match from_utf8(unsafe { raw.as_slice().unsafe_get(0).as_slice() }) { + from_one_comma_delimited(unsafe { raw.as_slice().unsafe_get(0).as_slice() }) +} + +/// Reads a comma-delimited raw string into a Vec. +pub fn from_one_comma_delimited(raw: &[u8]) -> Option> { + match from_utf8(raw) { Some(s) => { Some(s.as_slice() .split([',', ' '].as_slice())