dhttp/util/
httpdate.rs

1//! Utilities to format an HTTP date
2//! # Example
3//! ```
4//! # use dhttp::util::httpdate;
5//! # use dhttp::reqres::HttpResponse;
6//! # let mut res = HttpResponse::new();
7//! let your_time;
8//! # your_time = std::fs::metadata("src/util/httpdate.rs").unwrap().modified().unwrap();
9//! if let Some(date) = httpdate::from_systime(your_time) {
10//!     res.add_header("Last-Modified", date);
11//! }
12//! ```
13
14use std::time::{SystemTime, UNIX_EPOCH};
15
16use chrono_lite::{time_t, Tm, gmtime, timegm, time};
17
18const WEEKDAYS: &[&str] = &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
19const MONTHS: &[&str] = &["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
20
21fn httpdate(tm: Tm) -> String {
22    let Tm { tm_wday, tm_mday, tm_mon, tm_year, tm_hour, tm_min, tm_sec, .. } = tm;
23    let weekday = WEEKDAYS[tm_wday as usize];
24    let month = MONTHS[tm_mon as usize];
25    let year = tm_year + 1900;
26    // example output: Tue, 25 Feb 2025 21:05:51 GMT
27    format!("{weekday}, {tm_mday} {month} {year} {tm_hour:02}:{tm_min:02}:{tm_sec:02} GMT")
28}
29
30/// Formats an HTTP date from a [`SystemTime`]
31///
32/// Returns `None` when date formatting fails (i. e. when provided timestamp was invalid)
33pub fn from_systime(systime: SystemTime) -> Option<String> {
34    let time = systime.duration_since(UNIX_EPOCH).ok()?.as_secs();
35    let tm = gmtime(time as time_t)?;
36
37    Some(httpdate(tm))
38}
39
40/// Returns the current time in an HTTP date
41///
42/// May return `None` on Windows after 31 Dec, 3000
43pub fn now() -> Option<String> {
44    let tm = gmtime(time())?;
45    Some(httpdate(tm))
46}
47
48/// Parses an HTTP date
49pub fn parse(mut date: &str) -> Option<time_t> {
50    // It looks terrible, I know
51    // Could be better if I had sscanf in Rust
52    date = date.get(5..)?; // "Sat, "
53
54    let mdaylen = if date.chars().nth(1)? == ' ' { 1 } else { 2 };
55    let tm_mday = date.get(0..mdaylen)?.parse().ok()?; // "03"
56    date = date.get(mdaylen+1..)?; // "03 "
57
58    let month = date.get(0..3)?; // "Jan"
59    let month = MONTHS.iter().position(|&i| i == month)?;
60    date = date.get(4..)?; // "Jan "
61    let year: i32 = date.get(0..4)?.parse().ok()?; // "2026"
62    date = date.get(5..)?; // "2026 "
63    let tm_hour = date.get(0..2)?.parse().ok()?; // "17"
64    date = date.get(3..)?; // "17:"
65    let tm_min = date.get(0..2)?.parse().ok()?; // "49"
66    date = date.get(3..)?; // "49:"
67    let tm_sec = date.get(0..2)?.parse().ok()?; // "29"
68    date = date.get(3..)?; // "29 "
69    if date != "GMT" { return None; } // "GMT"
70
71    let tm = Tm {
72        tm_year: year - 1900,
73        tm_mon: month as i32,
74        tm_mday,
75        tm_hour,
76        tm_min,
77        tm_sec,
78        ..Default::default()
79    };
80    timegm(tm)
81}
82
83#[cfg(test)]
84mod test {
85    use super::*;
86
87    #[test]
88    fn test_httpdate() {
89        assert_eq!("Wed, 26 Feb 2025 22:10:59 GMT", &httpdate(gmtime(1740607859).unwrap()));
90    }
91
92    #[test]
93    fn test_parse() {
94        assert_eq!(1767462569, parse("Sat, 03 Jan 2026 17:49:29 GMT").unwrap());
95        assert_eq!(1767484771, parse("Sat, 3 Jan 2026 23:59:31 GMT").unwrap());
96    }
97
98    #[test]
99    fn test_panic() {
100        assert_eq!(None, parse("🐉🐉🐉🐉🐉"));
101    }
102}