http_types/utils/
date.rs

1use std::fmt::{self, Display, Formatter};
2use std::str::{from_utf8, FromStr};
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5use crate::StatusCode;
6use crate::{bail, ensure, format_err};
7
8const IMF_FIXDATE_LENGTH: usize = 29;
9const RFC850_MAX_LENGTH: usize = 23;
10const ASCTIME_LENGTH: usize = 24;
11
12const YEAR_9999_SECONDS: u64 = 253402300800;
13const SECONDS_IN_DAY: u64 = 86400;
14const SECONDS_IN_HOUR: u64 = 3600;
15
16/// Format using the `Display` trait.
17/// Convert timestamp into/from `SytemTime` to use.
18/// Supports comparison and sorting.
19#[derive(Copy, Clone, Debug, Eq)]
20pub(crate) struct HttpDate {
21    /// 0...59
22    second: u8,
23    /// 0...59
24    minute: u8,
25    /// 0...23
26    hour: u8,
27    /// 1...31
28    day: u8,
29    /// 1...12
30    month: u8,
31    /// 1970...9999
32    year: u16,
33    /// 1...7
34    week_day: u8,
35}
36
37/// Parse a date from an HTTP header field.
38///
39/// Supports the preferred IMF-fixdate and the legacy RFC 805 and
40/// ascdate formats. Two digit years are mapped to dates between
41/// 1970 and 2069.
42pub(crate) fn parse_http_date(s: &str) -> crate::Result<SystemTime> {
43    s.parse::<HttpDate>().map(|d| d.into()).map_err(|mut e| {
44        e.set_status(StatusCode::BadRequest);
45        e
46    })
47}
48
49/// Format a date to be used in a HTTP header field.
50///
51/// Dates are formatted as IMF-fixdate: `Fri, 15 May 2015 15:34:21 GMT`.
52pub(crate) fn fmt_http_date(d: SystemTime) -> String {
53    format!("{}", HttpDate::from(d))
54}
55
56impl HttpDate {
57    fn is_valid(self) -> bool {
58        self.second < 60
59            && self.minute < 60
60            && self.hour < 24
61            && self.day > 0
62            && self.day < 32
63            && self.month > 0
64            && self.month <= 12
65            && self.year >= 1970
66            && self.year <= 9999
67            && self.week_day >= 1
68            && self.week_day < 8
69    }
70}
71
72fn parse_imf_fixdate(s: &[u8]) -> crate::Result<HttpDate> {
73    // Example: `Sun, 06 Nov 1994 08:49:37 GMT`
74    if s.len() != IMF_FIXDATE_LENGTH
75        || &s[25..] != b" GMT"
76        || s[16] != b' '
77        || s[19] != b':'
78        || s[22] != b':'
79    {
80        bail!("Date time not in imf fixdate format");
81    }
82    Ok(HttpDate {
83        second: from_utf8(&s[23..25])?.parse()?,
84        minute: from_utf8(&s[20..22])?.parse()?,
85        hour: from_utf8(&s[17..19])?.parse()?,
86        day: from_utf8(&s[5..7])?.parse()?,
87        month: match &s[7..12] {
88            b" Jan " => 1,
89            b" Feb " => 2,
90            b" Mar " => 3,
91            b" Apr " => 4,
92            b" May " => 5,
93            b" Jun " => 6,
94            b" Jul " => 7,
95            b" Aug " => 8,
96            b" Sep " => 9,
97            b" Oct " => 10,
98            b" Nov " => 11,
99            b" Dec " => 12,
100            _ => bail!("Invalid Month"),
101        },
102        year: from_utf8(&s[12..16])?.parse()?,
103        week_day: match &s[..5] {
104            b"Mon, " => 1,
105            b"Tue, " => 2,
106            b"Wed, " => 3,
107            b"Thu, " => 4,
108            b"Fri, " => 5,
109            b"Sat, " => 6,
110            b"Sun, " => 7,
111            _ => bail!("Invalid Day"),
112        },
113    })
114}
115
116fn parse_rfc850_date(s: &[u8]) -> crate::Result<HttpDate> {
117    // Example: `Sunday, 06-Nov-94 08:49:37 GMT`
118    ensure!(
119        s.len() >= RFC850_MAX_LENGTH,
120        "Date time not in rfc850 format"
121    );
122
123    fn week_day<'a>(s: &'a [u8], week_day: u8, name: &'static [u8]) -> Option<(u8, &'a [u8])> {
124        if &s[0..name.len()] == name {
125            return Some((week_day, &s[name.len()..]));
126        }
127        None
128    }
129    let (week_day, s) = week_day(s, 1, b"Monday, ")
130        .or_else(|| week_day(s, 2, b"Tuesday, "))
131        .or_else(|| week_day(s, 3, b"Wednesday, "))
132        .or_else(|| week_day(s, 4, b"Thursday, "))
133        .or_else(|| week_day(s, 5, b"Friday, "))
134        .or_else(|| week_day(s, 6, b"Saturday, "))
135        .or_else(|| week_day(s, 7, b"Sunday, "))
136        .ok_or_else(|| format_err!("Invalid day"))?;
137    if s.len() != 22 || s[12] != b':' || s[15] != b':' || &s[18..22] != b" GMT" {
138        bail!("Date time not in rfc950 fmt");
139    }
140    let mut year = from_utf8(&s[7..9])?.parse::<u16>()?;
141    if year < 70 {
142        year += 2000;
143    } else {
144        year += 1900;
145    }
146    Ok(HttpDate {
147        second: from_utf8(&s[16..18])?.parse()?,
148        minute: from_utf8(&s[13..15])?.parse()?,
149        hour: from_utf8(&s[10..12])?.parse()?,
150        day: from_utf8(&s[0..2])?.parse()?,
151        month: match &s[2..7] {
152            b"-Jan-" => 1,
153            b"-Feb-" => 2,
154            b"-Mar-" => 3,
155            b"-Apr-" => 4,
156            b"-May-" => 5,
157            b"-Jun-" => 6,
158            b"-Jul-" => 7,
159            b"-Aug-" => 8,
160            b"-Sep-" => 9,
161            b"-Oct-" => 10,
162            b"-Nov-" => 11,
163            b"-Dec-" => 12,
164            _ => bail!("Invalid month"),
165        },
166        year,
167        week_day,
168    })
169}
170
171fn parse_asctime(s: &[u8]) -> crate::Result<HttpDate> {
172    // Example: `Sun Nov  6 08:49:37 1994`
173    if s.len() != ASCTIME_LENGTH || s[10] != b' ' || s[13] != b':' || s[16] != b':' || s[19] != b' '
174    {
175        bail!("Date time not in asctime format");
176    }
177    Ok(HttpDate {
178        second: from_utf8(&s[17..19])?.parse()?,
179        minute: from_utf8(&s[14..16])?.parse()?,
180        hour: from_utf8(&s[11..13])?.parse()?,
181        day: {
182            let x = &s[8..10];
183            from_utf8(if x[0] == b' ' { &x[1..2] } else { x })?.parse()?
184        },
185        month: match &s[4..8] {
186            b"Jan " => 1,
187            b"Feb " => 2,
188            b"Mar " => 3,
189            b"Apr " => 4,
190            b"May " => 5,
191            b"Jun " => 6,
192            b"Jul " => 7,
193            b"Aug " => 8,
194            b"Sep " => 9,
195            b"Oct " => 10,
196            b"Nov " => 11,
197            b"Dec " => 12,
198            _ => bail!("Invalid month"),
199        },
200        year: from_utf8(&s[20..24])?.parse()?,
201        week_day: match &s[0..4] {
202            b"Mon " => 1,
203            b"Tue " => 2,
204            b"Wed " => 3,
205            b"Thu " => 4,
206            b"Fri " => 5,
207            b"Sat " => 6,
208            b"Sun " => 7,
209            _ => bail!("Invalid day"),
210        },
211    })
212}
213
214impl From<SystemTime> for HttpDate {
215    fn from(system_time: SystemTime) -> Self {
216        let dur = system_time
217            .duration_since(UNIX_EPOCH)
218            .expect("all times should be after the epoch");
219        let secs_since_epoch = dur.as_secs();
220
221        if secs_since_epoch >= YEAR_9999_SECONDS {
222            // year 9999
223            panic!("date must be before year 9999");
224        }
225
226        /* 2000-03-01 (mod 400 year, immediately after feb29 */
227        const LEAPOCH: i64 = 11017;
228        const DAYS_PER_400Y: i64 = 365 * 400 + 97;
229        const DAYS_PER_100Y: i64 = 365 * 100 + 24;
230        const DAYS_PER_4Y: i64 = 365 * 4 + 1;
231
232        let days = (secs_since_epoch / SECONDS_IN_DAY) as i64 - LEAPOCH;
233        let secs_of_day = secs_since_epoch % SECONDS_IN_DAY;
234
235        let mut qc_cycles = days / DAYS_PER_400Y;
236        let mut remdays = days % DAYS_PER_400Y;
237
238        if remdays < 0 {
239            remdays += DAYS_PER_400Y;
240            qc_cycles -= 1;
241        }
242
243        let mut c_cycles = remdays / DAYS_PER_100Y;
244        if c_cycles == 4 {
245            c_cycles -= 1;
246        }
247        remdays -= c_cycles * DAYS_PER_100Y;
248
249        let mut q_cycles = remdays / DAYS_PER_4Y;
250        if q_cycles == 25 {
251            q_cycles -= 1;
252        }
253        remdays -= q_cycles * DAYS_PER_4Y;
254
255        let mut remyears = remdays / 365;
256        if remyears == 4 {
257            remyears -= 1;
258        }
259        remdays -= remyears * 365;
260
261        let mut year = 2000 + remyears + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles;
262
263        let months = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
264        let mut month = 0;
265        for month_len in months.iter() {
266            month += 1;
267            if remdays < *month_len {
268                break;
269            }
270            remdays -= *month_len;
271        }
272        let mday = remdays + 1;
273        let month = if month + 2 > 12 {
274            year += 1;
275            month - 10
276        } else {
277            month + 2
278        };
279
280        let mut week_day = (3 + days) % 7;
281        if week_day <= 0 {
282            week_day += 7
283        };
284
285        HttpDate {
286            second: (secs_of_day % 60) as u8,
287            minute: ((secs_of_day % SECONDS_IN_HOUR) / 60) as u8,
288            hour: (secs_of_day / SECONDS_IN_HOUR) as u8,
289            day: mday as u8,
290            month: month as u8,
291            year: year as u16,
292            week_day: week_day as u8,
293        }
294    }
295}
296
297impl From<HttpDate> for SystemTime {
298    fn from(http_date: HttpDate) -> Self {
299        let leap_years = ((http_date.year - 1) - 1968) / 4 - ((http_date.year - 1) - 1900) / 100
300            + ((http_date.year - 1) - 1600) / 400;
301        let mut ydays = match http_date.month {
302            1 => 0,
303            2 => 31,
304            3 => 59,
305            4 => 90,
306            5 => 120,
307            6 => 151,
308            7 => 181,
309            8 => 212,
310            9 => 243,
311            10 => 273,
312            11 => 304,
313            12 => 334,
314            _ => unreachable!(),
315        } + http_date.day as u64
316            - 1;
317        if is_leap_year(http_date.year) && http_date.month > 2 {
318            ydays += 1;
319        }
320        let days = (http_date.year as u64 - 1970) * 365 + leap_years as u64 + ydays;
321        UNIX_EPOCH
322            + Duration::from_secs(
323                http_date.second as u64
324                    + http_date.minute as u64 * 60
325                    + http_date.hour as u64 * SECONDS_IN_HOUR
326                    + days * SECONDS_IN_DAY,
327            )
328    }
329}
330
331impl FromStr for HttpDate {
332    type Err = crate::Error;
333
334    fn from_str(s: &str) -> Result<Self, Self::Err> {
335        ensure!(s.is_ascii(), "String slice is not valid ASCII");
336        let x = s.trim().as_bytes();
337        let date = parse_imf_fixdate(x)
338            .or_else(|_| parse_rfc850_date(x))
339            .or_else(|_| parse_asctime(x))?;
340        ensure!(date.is_valid(), "Invalid date time");
341        Ok(date)
342    }
343}
344
345impl Display for HttpDate {
346    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
347        let week_day = match self.week_day {
348            1 => b"Mon",
349            2 => b"Tue",
350            3 => b"Wed",
351            4 => b"Thu",
352            5 => b"Fri",
353            6 => b"Sat",
354            7 => b"Sun",
355            _ => unreachable!(),
356        };
357        let month = match self.month {
358            1 => b"Jan",
359            2 => b"Feb",
360            3 => b"Mar",
361            4 => b"Apr",
362            5 => b"May",
363            6 => b"Jun",
364            7 => b"Jul",
365            8 => b"Aug",
366            9 => b"Sep",
367            10 => b"Oct",
368            11 => b"Nov",
369            12 => b"Dec",
370            _ => unreachable!(),
371        };
372        let mut buf: [u8; 29] = [
373            // Too long to write as: b"Thu, 01 Jan 1970 00:00:00 GMT"
374            b' ', b' ', b' ', b',', b' ', b'0', b'0', b' ', b' ', b' ', b' ', b' ', b'0', b'0',
375            b'0', b'0', b' ', b'0', b'0', b':', b'0', b'0', b':', b'0', b'0', b' ', b'G', b'M',
376            b'T',
377        ];
378        buf[0] = week_day[0];
379        buf[1] = week_day[1];
380        buf[2] = week_day[2];
381        buf[5] = b'0' + (self.day / 10) as u8;
382        buf[6] = b'0' + (self.day % 10) as u8;
383        buf[8] = month[0];
384        buf[9] = month[1];
385        buf[10] = month[2];
386        buf[12] = b'0' + (self.year / 1000) as u8;
387        buf[13] = b'0' + (self.year / 100 % 10) as u8;
388        buf[14] = b'0' + (self.year / 10 % 10) as u8;
389        buf[15] = b'0' + (self.year % 10) as u8;
390        buf[17] = b'0' + (self.hour / 10) as u8;
391        buf[18] = b'0' + (self.hour % 10) as u8;
392        buf[20] = b'0' + (self.minute / 10) as u8;
393        buf[21] = b'0' + (self.minute % 10) as u8;
394        buf[23] = b'0' + (self.second / 10) as u8;
395        buf[24] = b'0' + (self.second % 10) as u8;
396        f.write_str(from_utf8(&buf[..]).unwrap())
397    }
398}
399
400impl PartialEq for HttpDate {
401    fn eq(&self, other: &HttpDate) -> bool {
402        SystemTime::from(*self) == SystemTime::from(*other)
403    }
404}
405
406impl PartialOrd for HttpDate {
407    fn partial_cmp(&self, other: &HttpDate) -> Option<std::cmp::Ordering> {
408        SystemTime::from(*self).partial_cmp(&SystemTime::from(*other))
409    }
410}
411
412fn is_leap_year(year: u16) -> bool {
413    year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
414}
415
416#[cfg(test)]
417mod tests {
418    use std::time::{Duration, UNIX_EPOCH};
419
420    use super::{fmt_http_date, parse_http_date, HttpDate, SECONDS_IN_DAY, SECONDS_IN_HOUR};
421
422    #[test]
423    fn test_rfc_example() {
424        let d = UNIX_EPOCH + Duration::from_secs(784111777);
425        assert_eq!(
426            d,
427            parse_http_date("Sun, 06 Nov 1994 08:49:37 GMT").expect("#1")
428        );
429        assert_eq!(
430            d,
431            parse_http_date("Sunday, 06-Nov-94 08:49:37 GMT").expect("#2")
432        );
433        assert_eq!(d, parse_http_date("Sun Nov  6 08:49:37 1994").expect("#3"));
434    }
435
436    #[test]
437    fn test2() {
438        let d = UNIX_EPOCH + Duration::from_secs(1475419451);
439        assert_eq!(
440            d,
441            parse_http_date("Sun, 02 Oct 2016 14:44:11 GMT").expect("#1")
442        );
443        assert!(parse_http_date("Sun Nov 10 08:00:00 1000").is_err());
444        assert!(parse_http_date("Sun Nov 10 08*00:00 2000").is_err());
445        assert!(parse_http_date("Sunday, 06-Nov-94 08+49:37 GMT").is_err());
446    }
447
448    #[test]
449    fn test3() {
450        let mut d = UNIX_EPOCH;
451        assert_eq!(d, parse_http_date("Thu, 01 Jan 1970 00:00:00 GMT").unwrap());
452        d += Duration::from_secs(SECONDS_IN_HOUR);
453        assert_eq!(d, parse_http_date("Thu, 01 Jan 1970 01:00:00 GMT").unwrap());
454        d += Duration::from_secs(SECONDS_IN_DAY);
455        assert_eq!(d, parse_http_date("Fri, 02 Jan 1970 01:00:00 GMT").unwrap());
456        d += Duration::from_secs(2592000);
457        assert_eq!(d, parse_http_date("Sun, 01 Feb 1970 01:00:00 GMT").unwrap());
458        d += Duration::from_secs(2592000);
459        assert_eq!(d, parse_http_date("Tue, 03 Mar 1970 01:00:00 GMT").unwrap());
460        d += Duration::from_secs(31536005);
461        assert_eq!(d, parse_http_date("Wed, 03 Mar 1971 01:00:05 GMT").unwrap());
462        d += Duration::from_secs(15552000);
463        assert_eq!(d, parse_http_date("Mon, 30 Aug 1971 01:00:05 GMT").unwrap());
464        d += Duration::from_secs(6048000);
465        assert_eq!(d, parse_http_date("Mon, 08 Nov 1971 01:00:05 GMT").unwrap());
466        d += Duration::from_secs(864000000);
467        assert_eq!(d, parse_http_date("Fri, 26 Mar 1999 01:00:05 GMT").unwrap());
468    }
469
470    #[test]
471    fn test_fmt() {
472        let d = UNIX_EPOCH;
473        assert_eq!(fmt_http_date(d), "Thu, 01 Jan 1970 00:00:00 GMT");
474        let d = UNIX_EPOCH + Duration::from_secs(1475419451);
475        assert_eq!(fmt_http_date(d), "Sun, 02 Oct 2016 14:44:11 GMT");
476    }
477
478    #[test]
479    fn size_of() {
480        assert_eq!(::std::mem::size_of::<HttpDate>(), 8);
481    }
482}