use core::fmt;
const MAX_SECONDS: u64 = 253_402_300_799;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct HttpDate([u8; 29]);
impl HttpDate {
#[must_use]
pub const fn from_unix_seconds(secs: u64) -> Self {
const WEEKDAYS: [&[u8; 3]; 7] = [b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat", b"Sun"];
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 secs = if secs > MAX_SECONDS {
MAX_SECONDS
} else {
secs
};
let days = (secs / 86_400) as i64;
let rem = secs % 86_400;
let (year, month, day) = parse_net::date::civil_from_days(days);
let weekday = parse_net::date::weekday_from_days(days);
let mut out = *b"Thu, 01 Jan 1970 00:00:00 GMT";
copy3(&mut out, 0, WEEKDAYS[(weekday - 1) as usize]);
two_digits(&mut out, 5, day as u64);
copy3(&mut out, 8, MONTHS[(month - 1) as usize]);
two_digits(&mut out, 12, year as u64 / 100);
two_digits(&mut out, 14, year as u64 % 100);
two_digits(&mut out, 17, rem / 3600);
two_digits(&mut out, 20, rem % 3600 / 60);
two_digits(&mut out, 23, rem % 60);
Self(out)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 29] {
&self.0
}
#[must_use]
pub fn as_str(&self) -> &str {
core::str::from_utf8(&self.0).unwrap_or("")
}
}
impl fmt::Debug for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("HttpDate").field(&self.as_str()).finish()
}
}
impl fmt::Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DateCache {
second: Option<u64>,
date: HttpDate,
}
impl Default for DateCache {
fn default() -> Self {
Self::new()
}
}
impl DateCache {
#[must_use]
pub const fn new() -> Self {
Self {
second: None,
date: HttpDate::from_unix_seconds(0),
}
}
pub fn update(&mut self, unix_seconds: u64) -> &HttpDate {
if self.second != Some(unix_seconds) {
self.date = HttpDate::from_unix_seconds(unix_seconds);
self.second = Some(unix_seconds);
}
&self.date
}
#[must_use]
pub const fn current(&self) -> &HttpDate {
&self.date
}
}
const fn copy3(dst: &mut [u8; 29], at: usize, src: &[u8; 3]) {
dst[at] = src[0];
dst[at + 1] = src[1];
dst[at + 2] = src[2];
}
const fn two_digits(dst: &mut [u8; 29], at: usize, v: u64) {
dst[at] = b'0' + (v / 10 % 10) as u8;
dst[at + 1] = b'0' + (v % 10) as u8;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_http_date_known_values() {
for (secs, text) in [
(0, "Thu, 01 Jan 1970 00:00:00 GMT"),
(784_111_777, "Sun, 06 Nov 1994 08:49:37 GMT"),
(951_782_400, "Tue, 29 Feb 2000 00:00:00 GMT"),
(1_791_072_000, "Sun, 04 Oct 2026 00:00:00 GMT"),
(4_107_542_400, "Mon, 01 Mar 2100 00:00:00 GMT"),
(MAX_SECONDS, "Fri, 31 Dec 9999 23:59:59 GMT"),
(MAX_SECONDS + 1, "Fri, 31 Dec 9999 23:59:59 GMT"),
] {
assert_eq!(HttpDate::from_unix_seconds(secs).as_str(), text, "{secs}");
}
}
#[test]
fn test_civil_from_days_matches_a_day_by_day_walk() {
let (mut y, mut m, mut d) = (1970u64, 1u64, 1u64);
for days in 0..157_000u64 {
let (py, pm, pd) = parse_net::date::civil_from_days(days as i64);
assert_eq!((py as u64, pm as u64, pd as u64), (y, m, d), "day {days}");
let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
let len = match m {
2 if leap => 29,
2 => 28,
4 | 6 | 9 | 11 => 30,
_ => 31,
};
d += 1;
if d > len {
d = 1;
m += 1;
if m > 12 {
m = 1;
y += 1;
}
}
}
}
mod old {
use super::super::{MAX_SECONDS, copy3, two_digits};
pub(super) const fn from_unix_seconds(secs: u64) -> [u8; 29] {
const WEEKDAYS: [&[u8; 3]; 7] =
[b"Thu", b"Fri", b"Sat", b"Sun", b"Mon", b"Tue", b"Wed"];
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 secs = if secs > MAX_SECONDS {
MAX_SECONDS
} else {
secs
};
let days = secs / 86_400;
let rem = secs % 86_400;
let (year, month, day) = civil_from_days(days);
let mut out = *b"Thu, 01 Jan 1970 00:00:00 GMT";
copy3(&mut out, 0, WEEKDAYS[(days % 7) as usize]);
two_digits(&mut out, 5, day);
copy3(&mut out, 8, MONTHS[(month - 1) as usize]);
two_digits(&mut out, 12, year / 100);
two_digits(&mut out, 14, year % 100);
two_digits(&mut out, 17, rem / 3600);
two_digits(&mut out, 20, rem % 3600 / 60);
two_digits(&mut out, 23, rem % 60);
out
}
pub(super) const fn civil_from_days(days: u64) -> (u64, u64, u64) {
let z = days + 719_468;
let era = z / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = yoe + era * 400 + (month <= 2) as u64;
(year, month, day)
}
}
#[test]
fn test_formatter_matches_07_on_every_day_to_9999() {
let last_day = MAX_SECONDS / 86_400;
for day in 0..=last_day {
let secs = day * 86_400 + (day * 7_919) % 86_400;
let new = *HttpDate::from_unix_seconds(secs).as_bytes();
assert_eq!(new, old::from_unix_seconds(secs), "{secs}");
let pn = parse_net::HttpDate::from_unix(secs as i64).map(|d| d.format());
assert_eq!(pn, Ok(new), "{secs}");
}
for secs in [
0,
1,
86_399,
MAX_SECONDS - 1,
MAX_SECONDS,
MAX_SECONDS + 1,
u64::MAX / 2,
u64::MAX,
] {
assert_eq!(
*HttpDate::from_unix_seconds(secs).as_bytes(),
old::from_unix_seconds(secs),
"{secs}"
);
}
for secs in 0..86_400 * 2 {
assert_eq!(
*HttpDate::from_unix_seconds(secs).as_bytes(),
old::from_unix_seconds(secs),
"{secs}"
);
}
}
#[test]
fn test_date_cache_reformats_only_on_a_new_second() {
let mut cache = DateCache::new();
let a = *cache.update(10);
assert_eq!(cache.second, Some(10));
assert_eq!(*cache.update(10), a);
let b = *cache.update(11);
assert_ne!(a, b);
assert_eq!(*cache.update(10), a);
assert_eq!(cache.current(), &a);
}
}