roughtime 0.1.0

A no_std-capable Roughtime secure time-sync client with pluggable crypto backends
Documentation
//! A minimal, dependency-free RFC 1123-ish HTTP date parser.
//!
//! Useful for kernel/bootloader consumers that need to parse a `Date` header (e.g. from a TLS
//! handshake) without pulling in a full date/time crate.

/// Parses an HTTP date of the form `"Mon, 01 Jun 2026 12:41:36 GMT"` into a Unix timestamp
/// (seconds).
///
/// Returns `None` if the string does not match the expected format.
#[must_use]
pub fn parse_http_date(date_str: &str) -> Option<u64> {
    let mut parts = date_str.split_whitespace();
    let _day_name = parts.next()?;
    let day_str = parts.next()?;
    let month_str = parts.next()?;
    let year_str = parts.next()?;
    let time_str = parts.next()?;

    let day = day_str.parse::<u64>().ok()?;
    let year = year_str.parse::<u64>().ok()?;

    let month = match month_str {
        "Jan" => 1,
        "Feb" => 2,
        "Mar" => 3,
        "Apr" => 4,
        "May" => 5,
        "Jun" => 6,
        "Jul" => 7,
        "Aug" => 8,
        "Sep" => 9,
        "Oct" => 10,
        "Nov" => 11,
        "Dec" => 12,
        _ => return None,
    };

    let mut time_parts = time_str.split(':');
    let hour = time_parts.next()?.parse::<u64>().ok()?;
    let minute = time_parts.next()?.parse::<u64>().ok()?;
    let second = time_parts.next()?.parse::<u64>().ok()?;

    let mut days = 0u64;
    for y in 1970..year {
        days += if is_leap_year(y) { 366 } else { 365 };
    }

    let month_days = if is_leap_year(year) {
        [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    } else {
        [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    };

    let month_index = usize::try_from(month).ok()?;
    days += month_days
        .iter()
        .take(month_index)
        .skip(1)
        .sum::<u64>();
    days += day - 1;

    Some(days * 86_400 + hour * 3_600 + minute * 60 + second)
}

/// Returns whether `year` is a leap year in the Gregorian calendar.
#[must_use]
pub const fn is_leap_year(year: u64) -> bool {
    (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
}

#[cfg(test)]
mod tests {
    #![allow(clippy::expect_used)]

    use super::{is_leap_year, parse_http_date};

    #[test]
    fn leap_years() {
        assert!(is_leap_year(2000));
        assert!(is_leap_year(2004));
        assert!(!is_leap_year(1900));
        assert!(!is_leap_year(2021));
        assert!(is_leap_year(2024));
    }

    #[test]
    fn parses_valid_dates() {
        let ts = parse_http_date("Mon, 01 Jun 2026 12:41:36 GMT").expect("parse June 1 2026");
        assert_eq!(ts, 1_780_317_696);

        let ts2 = parse_http_date("Tue, 02 Jun 2026 00:00:00 GMT").expect("parse June 2 2026");
        assert_eq!(ts2, 1_780_358_400);
    }

    #[test]
    fn rejects_invalid_dates() {
        assert!(parse_http_date("Mon, 01 Invalid 2026 12:41:36 GMT").is_none());
        assert!(parse_http_date("Mon, 01 Jun 2026 12:41 GMT").is_none());
        assert!(parse_http_date("").is_none());
    }
}