Skip to main content

broadcast_common/
http_date.rs

1//! HTTP `Date` and `Retry-After` date readers (RFC 9110 §5.6.7).
2//!
3//! Calendar arithmetic is jiff's `strptime`; nothing here reads a clock.
4
5use thiserror::Error;
6
7const HTTP_DATE_RFC850: &str = "%A, %d-%b-%y %H:%M:%S GMT";
8const HTTP_DATE_ASCTIME: &str = "%a %b %e %H:%M:%S %Y";
9
10/// Why an HTTP date was rejected.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
12#[non_exhaustive]
13pub enum HttpDateError {
14    /// Not an HTTP `Date` value.
15    #[error("not an HTTP Date value")]
16    HttpDate,
17}
18
19/// Parse an HTTP-date (RFC 9110 §5.6.7) to Unix microseconds: the preferred
20/// IMF-fixdate and the two obsolete forms a recipient must still accept, the
21/// RFC 850 form (two-digit years follow jiff's POSIX window: 69-99 are 19xx,
22/// 00-68 are 20xx, so no clock is needed) and the asctime form (always UTC).
23/// Resolution is whole seconds. Used for the `Date` and `Retry-After` headers.
24///
25/// # Errors
26/// [`HttpDateError::HttpDate`] when `text` is not a date.
27pub fn parse_http_date(text: &str) -> Result<i64, HttpDateError> {
28    let text = text.trim();
29    if let Ok(t) = jiff::fmt::rfc2822::DateTimeParser::new().parse_timestamp(text) {
30        return Ok(t.as_microsecond());
31    }
32    [HTTP_DATE_RFC850, HTTP_DATE_ASCTIME]
33        .iter()
34        .find_map(|format| {
35            jiff::fmt::strtime::parse(format, text)
36                .and_then(|broken| broken.to_datetime())
37                .and_then(|datetime| datetime.to_zoned(jiff::tz::TimeZone::UTC))
38                .ok()
39        })
40        .map(|zoned| zoned.timestamp().as_microsecond())
41        .ok_or(HttpDateError::HttpDate)
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn the_three_forms_agree() {
50        let expected = 784_111_777 * 1_000_000;
51        for text in [
52            "Sun, 06 Nov 1994 08:49:37 GMT",
53            "Sunday, 06-Nov-94 08:49:37 GMT",
54            "Sun Nov  6 08:49:37 1994",
55        ] {
56            assert_eq!(parse_http_date(text), Ok(expected), "{text}");
57        }
58    }
59
60    #[test]
61    fn garbage_is_rejected() {
62        assert_eq!(parse_http_date("yesterday"), Err(HttpDateError::HttpDate));
63    }
64}