Skip to main content

httpdate/
lib.rs

1//! Date and time utils for HTTP.
2//!
3//! Multiple HTTP header fields store timestamps.
4//! For example a response created on May 15, 2015 may contain the header
5//! `Date: Fri, 15 May 2015 15:34:21 GMT`. Since the timestamp does not
6//! contain any timezone or leap second information it is equvivalent to
7//! writing 1431696861 Unix time. Rust’s `SystemTime` is used to store
8//! these timestamps.
9//!
10//! This crate provides two public functions:
11//!
12//! * `parse_http_date` to parse a HTTP datetime string to a system time
13//! * `fmt_http_date` to format a system time to a IMF-fixdate
14//!
15//! In addition it exposes the `HttpDate` type that can be used to parse
16//! and format timestamps. Convert a sytem time to `HttpDate` and vice versa.
17//! The `HttpDate` (8 bytes) is smaller than `SystemTime` (16 bytes) and
18//! using the display impl avoids a temporary allocation.
19#![forbid(unsafe_code)]
20
21use std::{
22    error,
23    fmt::{self, Display, Formatter},
24    io,
25    time::SystemTime,
26};
27
28pub use date::HttpDate;
29
30mod date;
31
32/// An opaque error type for all parsing errors.
33#[derive(Debug)]
34pub struct Error(());
35
36impl error::Error for Error {}
37
38impl Display for Error {
39    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
40        f.write_str("string contains no or an invalid date")
41    }
42}
43
44impl From<Error> for io::Error {
45    fn from(e: Error) -> io::Error {
46        io::Error::new(io::ErrorKind::Other, e)
47    }
48}
49
50/// Parse a date from an HTTP header field.
51///
52/// Supports the preferred IMF-fixdate and the legacy RFC 805 and
53/// ascdate formats. Two digit years are mapped to dates between
54/// 1970 and 2069.
55pub fn parse_http_date(s: &str) -> Result<SystemTime, Error> {
56    s.parse::<HttpDate>().map(|d| d.into())
57}
58
59/// Format a date to be used in a HTTP header field.
60///
61/// Dates are formatted as IMF-fixdate: `Fri, 15 May 2015 15:34:21 GMT`.
62pub fn fmt_http_date(d: SystemTime) -> String {
63    format!("{}", HttpDate::from(d))
64}
65
66#[cfg(test)]
67mod tests {
68    use std::{
69        str,
70        time::{Duration, UNIX_EPOCH},
71    };
72
73    use super::{HttpDate, fmt_http_date, parse_http_date};
74
75    #[test]
76    fn test_rfc_example() {
77        let d = UNIX_EPOCH + Duration::from_secs(784111777);
78        assert_eq!(d, parse_http_date("Sun, 06 Nov 1994 08:49:37 GMT").expect("#1"));
79        assert_eq!(d, parse_http_date("Sunday, 06-Nov-94 08:49:37 GMT").expect("#2"));
80        assert_eq!(d, parse_http_date("Sun Nov  6 08:49:37 1994").expect("#3"));
81    }
82
83    #[test]
84    fn test2() {
85        let d = UNIX_EPOCH + Duration::from_secs(1475419451);
86        assert_eq!(d, parse_http_date("Sun, 02 Oct 2016 14:44:11 GMT").expect("#1"));
87        assert!(parse_http_date("Sun Nov 10 08:00:00 1000").is_err());
88        assert!(parse_http_date("Sun Nov 10 08*00:00 2000").is_err());
89        assert!(parse_http_date("Sunday, 06-Nov-94 08+49:37 GMT").is_err());
90    }
91
92    #[test]
93    fn test3() {
94        let mut d = UNIX_EPOCH;
95        assert_eq!(d, parse_http_date("Thu, 01 Jan 1970 00:00:00 GMT").unwrap());
96        d += Duration::from_secs(3600);
97        assert_eq!(d, parse_http_date("Thu, 01 Jan 1970 01:00:00 GMT").unwrap());
98        d += Duration::from_secs(86400);
99        assert_eq!(d, parse_http_date("Fri, 02 Jan 1970 01:00:00 GMT").unwrap());
100        d += Duration::from_secs(2592000);
101        assert_eq!(d, parse_http_date("Sun, 01 Feb 1970 01:00:00 GMT").unwrap());
102        d += Duration::from_secs(2592000);
103        assert_eq!(d, parse_http_date("Tue, 03 Mar 1970 01:00:00 GMT").unwrap());
104        d += Duration::from_secs(31536005);
105        assert_eq!(d, parse_http_date("Wed, 03 Mar 1971 01:00:05 GMT").unwrap());
106        d += Duration::from_secs(15552000);
107        assert_eq!(d, parse_http_date("Mon, 30 Aug 1971 01:00:05 GMT").unwrap());
108        d += Duration::from_secs(6048000);
109        assert_eq!(d, parse_http_date("Mon, 08 Nov 1971 01:00:05 GMT").unwrap());
110        d += Duration::from_secs(864000000);
111        assert_eq!(d, parse_http_date("Fri, 26 Mar 1999 01:00:05 GMT").unwrap());
112    }
113
114    #[test]
115    fn test_fmt() {
116        let d = UNIX_EPOCH;
117        assert_eq!(fmt_http_date(d), "Thu, 01 Jan 1970 00:00:00 GMT");
118        let d = UNIX_EPOCH + Duration::from_secs(1475419451);
119        assert_eq!(fmt_http_date(d), "Sun, 02 Oct 2016 14:44:11 GMT");
120    }
121
122    #[allow(dead_code)]
123    fn testcase(data: &[u8]) {
124        if let Ok(s) = str::from_utf8(data) {
125            println!("{:?}", s);
126            if let Ok(d) = parse_http_date(s) {
127                let o = fmt_http_date(d);
128                assert!(!o.is_empty());
129            }
130        }
131    }
132
133    #[test]
134    fn size_of() {
135        assert_eq!(::std::mem::size_of::<HttpDate>(), 8);
136    }
137
138    #[test]
139    fn test_date_comparison() {
140        let a = UNIX_EPOCH + Duration::from_secs(784111777);
141        let b = a + Duration::from_secs(30);
142        assert!(a < b);
143        let a_date: HttpDate = a.into();
144        let b_date: HttpDate = b.into();
145        assert!(a_date < b_date);
146        assert_eq!(a_date.cmp(&b_date), ::std::cmp::Ordering::Less)
147    }
148
149    #[test]
150    fn test_parse_bad_date() {
151        // 1994-11-07 is actually a Monday
152        let parsed = "Sun, 07 Nov 1994 08:48:37 GMT".parse::<HttpDate>();
153        assert!(parsed.is_err())
154    }
155}