Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/estate-clock/src/date.rs

//! RFC 7231 IMF-fixdate → epoch second. Pure: no socket, no clock.
//!
//! HTTP dates are GMT and second-resolution, which against a **300 second**
//! credential window is three hundred times the precision the detector needs.

// ── the date on the wire ────────────────────────────────────────────────────

/// Parse an RFC 7231 **IMF-fixdate** into an epoch second.
///
/// ```text
///   Sun, 06 Nov 1994 08:49:37 GMT
///   0123456789...                   exactly 29 bytes, fixed width, GMT only
/// ```
///
/// The two obsolete forms RFC 7231 §7.1.1.1 also lists — RFC 850's
/// `Sunday, 06-Nov-94 08:49:37 GMT` and asctime's `Sun Nov  6 08:49:37 1994` —
/// are **not** accepted, and that is a decision rather than an omission. The
/// same section requires every sender to emit IMF-fixdate; the three sources in
/// sources this was written against were measured doing so; and a two-digit year is a thing
/// this module would have to guess a century for, which is precisely the class
/// of guess a clock must not make.
///
/// The day-of-week is parsed and **ignored**: it is redundant with the date, and
/// a sender that disagrees with itself about which is authoritative should not
/// get to pick.
///
/// Returns `None` on anything it cannot read exactly.
pub fn parse_http_date(raw: &str) -> Option<i64> {
    let bytes = raw.trim().as_bytes();
    if bytes.len() != 29 {
        return None;
    }
    if bytes[3] != b',' || bytes[4] != b' ' || bytes[7] != b' ' || bytes[11] != b' ' {
        return None;
    }
    if bytes[16] != b' ' || bytes[19] != b':' || bytes[22] != b':' {
        return None;
    }
    if &bytes[25..29] != b" GMT" {
        return None;
    }

    let digits = |range: std::ops::Range<usize>| -> Option<i64> {
        let mut value: i64 = 0;
        for byte in &bytes[range] {
            if !byte.is_ascii_digit() {
                return None;
            }
            value = value * 10 + i64::from(byte - b'0');
        }
        Some(value)
    };

    let day = digits(5..7)?;
    let year = digits(12..16)?;
    let hour = digits(17..19)?;
    let minute = digits(20..22)?;
    let second = digits(23..25)?;

    const MONTHS: [&[u8; 3]; 12] = [
        b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov",
        b"Dec",
    ];
    let month = MONTHS
        .iter()
        .position(|name| name.as_slice() == &bytes[8..11])
        .map(|index| index as i64 + 1)?;

    // A day the month does not have is a date this cannot convert without
    // silently sliding into the next month — `31 Feb` would become `3 Mar` and
    // the answer would be wrong by days while looking perfectly well-formed.
    if day < 1 || day > days_in_month(year, month) {
        return None;
    }
    // Second 60 is a leap second and is real (RFC 7231 does not exclude it);
    // hour 24 and minute 60 are not.
    if hour > 23 || minute > 59 || second > 60 {
        return None;
    }

    Some(days_from_civil(year, month, day) * 86_400 + hour * 3_600 + minute * 60 + second)
}

/// Days in a proleptic-Gregorian month.
fn days_in_month(year: i64, month: i64) -> i64 {
    match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 => 29,
        2 => 28,
        _ => 0,
    }
}

/// Days since 1970-01-01 for a proleptic-Gregorian date.
///
/// Howard Hinnant's `days_from_civil`, which is exact for every year this will
/// ever see and is fifteen lines of integer arithmetic. The alternative was a
/// date library in the appliance's dependency graph to convert one header.
fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
    let year = if month <= 2 { year - 1 } else { year };
    let era = if year >= 0 { year } else { year - 399 } / 400;
    let year_of_era = year - era * 400; // [0, 399]
    let shifted_month = (month + 9) % 12; // Mar = 0 … Feb = 11
    let day_of_year = (153 * shifted_month + 2) / 5 + day - 1; // [0, 365]
    let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
    era * 146_097 + day_of_era - 719_468
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::decide::{is_sane, EARLIEST_SANE_UNIX, LATEST_SANE_UNIX};

    #[test]
    fn the_imf_fixdate_in_rfc_7231_parses_to_its_documented_epoch_second() {
        // The example in RFC 7231 §7.1.1.1 itself.
        assert_eq!(
            parse_http_date("Sun, 06 Nov 1994 08:49:37 GMT"),
            Some(784_111_777)
        );
        assert_eq!(parse_http_date("Thu, 01 Jan 1970 00:00:00 GMT"), Some(0));
    }

    #[test]
    fn the_date_measured_on_the_estate_parses() {
        // Read off api.stripe.com, www.cloudflare.com and www.google.com from
        // the front on 2026-09-14; all three sent this byte-for-byte.
        let parsed = parse_http_date("Mon, 14 Sep 2026 11:58:27 GMT").expect("a real Date");
        assert!(
            is_sane(parsed),
            "the measured date must be inside the window"
        );
    }

    #[test]
    fn the_sanity_bounds_are_the_dates_their_names_claim() {
        // The constants are asserted against the parser rather than trusted as
        // literals, because a mistyped epoch second is invisible by inspection.
        assert_eq!(
            parse_http_date("Thu, 01 Jan 2026 00:00:00 GMT"),
            Some(EARLIEST_SANE_UNIX)
        );
        assert_eq!(
            parse_http_date("Mon, 01 Jan 2046 00:00:00 GMT"),
            Some(LATEST_SANE_UNIX)
        );
        const { assert!(EARLIEST_SANE_UNIX < LATEST_SANE_UNIX) };
    }

    #[test]
    fn a_date_that_is_not_an_imf_fixdate_is_refused_rather_than_guessed() {
        // RFC 850 — a two-digit year this must not pick a century for.
        assert_eq!(parse_http_date("Sunday, 06-Nov-94 08:49:37 GMT"), None);
        // asctime — no zone at all.
        assert_eq!(parse_http_date("Sun Nov  6 08:49:37 1994"), None);
        // A zone that is not GMT. The offset is not applied; the date is refused.
        assert_eq!(parse_http_date("Sun, 06 Nov 1994 08:49:37 CET"), None);
        // Truncated, padded, and empty.
        assert_eq!(parse_http_date("Sun, 06 Nov 1994 08:49:37"), None);
        assert_eq!(parse_http_date(""), None);
    }

    #[test]
    fn a_date_that_is_well_formed_but_impossible_is_refused() {
        // `31 Feb` would slide into March and be wrong by days while looking
        // perfectly well-formed — the worst shape a bad date can have.
        assert_eq!(parse_http_date("Wed, 31 Feb 2026 00:00:00 GMT"), None);
        assert_eq!(parse_http_date("Wed, 00 Feb 2026 00:00:00 GMT"), None);
        assert_eq!(parse_http_date("Wed, 01 Foo 2026 00:00:00 GMT"), None);
        assert_eq!(parse_http_date("Wed, 01 Feb 2026 24:00:00 GMT"), None);
        assert_eq!(parse_http_date("Wed, 01 Feb 2026 00:60:00 GMT"), None);
    }

    #[test]
    fn leap_day_and_leap_second_are_both_real_and_both_parse() {
        assert!(parse_http_date("Sat, 29 Feb 2048 00:00:00 GMT").is_some());
        assert!(parse_http_date("Wed, 28 Feb 2046 00:00:00 GMT").is_some());
        // 2100 is not a leap year; the century rule has to be in there.
        assert_eq!(parse_http_date("Mon, 29 Feb 2100 00:00:00 GMT"), None);
        // A leap second is a real value a real server can send.
        assert!(parse_http_date("Tue, 31 Dec 2030 23:59:60 GMT").is_some());
    }

    #[test]
    fn the_day_of_week_is_ignored_rather_than_believed() {
        // Both spell the same date; one of the weekdays is wrong. A sender that
        // disagrees with itself does not get to pick which half wins.
        assert_eq!(
            parse_http_date("Mon, 06 Nov 1994 08:49:37 GMT"),
            parse_http_date("Sun, 06 Nov 1994 08:49:37 GMT")
        );
    }
}