///|
/// Parses an HTTP-date (RFC 9110 §5.6.7) into Unix milliseconds: the
/// IMF-fixdate a sender generates, or one of the two obsolete forms a
/// recipient must also accept, rfc850-date and asctime-date. HTTP-date is
/// case-sensitive and the input is not trimmed. The weekday must agree with
/// the date. `now_unix_ms` places rfc850-date's two-digit year: a year more
/// than 50 years after it is taken from the century before.
///
/// ```mbt check
/// test {
/// assert_eq(
/// @http.parse_http_date("Sun, 06 Nov 1994 08:49:37 GMT", now_unix_ms=0L),
/// Some(784111777000L),
/// )
/// assert_eq(@http.parse_http_date("06 Nov 1994", now_unix_ms=0L), None)
/// }
/// ```
pub fn parse_http_date(s : String, now_unix_ms~ : Int64) -> Int64? {
match imf_fixdate(s) {
Some(ms) => Some(ms)
None =>
match rfc850_date(s, now_unix_ms) {
Some(ms) => Some(ms)
None => asctime_date(s)
}
}
}
///|
/// `Sun, 06 Nov 1994 08:49:37 GMT`
fn imf_fixdate(s : String) -> Int64? {
guard s.length() == 29 &&
s[3] == ',' &&
s[4] == ' ' &&
s[7] == ' ' &&
s[11] == ' ' &&
s[16] == ' ' &&
s.has_suffix(" GMT") else {
return None
}
guard short_day_name(s, 0) is Some(weekday) else { return None }
guard date_digits(s, 5, 2) is Some(day) else { return None }
guard month_name(s, 8) is Some(month) else { return None }
guard date_digits(s, 12, 4) is Some(year) else { return None }
guard time_of_day(s, 17) is Some(time) else { return None }
date_ms(weekday, year, month, day, time)
}
///|
/// `Sunday, 06-Nov-94 08:49:37 GMT`. The two-digit year names the latest year
/// with those digits that does not put the whole date more than 50 years
/// after `now_unix_ms` (RFC 9110 §5.6.7), so it can fall in the next century
/// as well as the previous one.
fn rfc850_date(s : String, now_unix_ms : Int64) -> Int64? {
guard s.find(", ") is Some(comma) else { return None }
guard long_day_name(s[:comma]) is Some(weekday) else { return None }
let at = comma + 2
guard s.length() == at + 22 &&
s[at + 2] == '-' &&
s[at + 6] == '-' &&
s[at + 9] == ' ' &&
s.has_suffix(" GMT") else {
return None
}
guard date_digits(s, at, 2) is Some(day) else { return None }
guard month_name(s, at + 3) is Some(month) else { return None }
guard date_digits(s, at + 7, 2) is Some(two_digits) else { return None }
guard time_of_day(s, at + 10) is Some(time) else { return None }
// The cutoff is the clock's own calendar instant 50 years on, so that a
// 29 February clock needs no day arithmetic.
let (now_year, now_month, now_day, now_ms_of_day) = civil_of(now_unix_ms)
let cutoff = (now_year + 50, now_month, now_day, now_ms_of_day)
let (hour, minute, second) = time
let ms_of_day = ((hour * 60 + minute) * 60 + second).to_int64() * 1000L
for year = now_year - now_year % 100 + two_digits + 100; ; year = year - 100 {
if (year, month, day, ms_of_day) <= cutoff {
break date_ms(weekday, year, month, day, time)
}
}
}
///|
/// `Sun Nov 6 08:49:37 1994`: the day is two digits, or a space and one.
fn asctime_date(s : String) -> Int64? {
guard s.length() == 24 &&
s[3] == ' ' &&
s[7] == ' ' &&
s[10] == ' ' &&
s[19] == ' ' else {
return None
}
guard short_day_name(s, 0) is Some(weekday) else { return None }
guard month_name(s, 4) is Some(month) else { return None }
let day = if s[8] == ' ' {
date_digits(s, 9, 1)
} else {
date_digits(s, 8, 2)
}
guard day is Some(day) else { return None }
guard time_of_day(s, 11) is Some(time) else { return None }
guard date_digits(s, 20, 4) is Some(year) else { return None }
date_ms(weekday, year, month, day, time)
}
///|
fn date_digits(s : String, start : Int32, count : Int32) -> Int32? {
let mut n = 0
for i = start; i < start + count; i = i + 1 {
guard s[i] >= '0' && s[i] <= '9' else { return None }
n = n * 10 + s[i].to_int() - 48
}
Some(n)
}
///|
/// `HH:MM:SS` at `at`, unvalidated.
fn time_of_day(s : String, at : Int32) -> (Int32, Int32, Int32)? {
guard s[at + 2] == ':' && s[at + 5] == ':' else { return None }
guard date_digits(s, at, 2) is Some(hour) else { return None }
guard date_digits(s, at + 3, 2) is Some(minute) else { return None }
guard date_digits(s, at + 6, 2) is Some(second) else { return None }
Some((hour, minute, second))
}
///|
/// `day-name` at `at`, Sunday as 0.
fn short_day_name(s : String, at : Int32) -> Int32? {
match (s[at], s[at + 1], s[at + 2]) {
('S', 'u', 'n') => Some(0)
('M', 'o', 'n') => Some(1)
('T', 'u', 'e') => Some(2)
('W', 'e', 'd') => Some(3)
('T', 'h', 'u') => Some(4)
('F', 'r', 'i') => Some(5)
('S', 'a', 't') => Some(6)
_ => None
}
}
///|
/// `day-name-l`, Sunday as 0.
fn long_day_name(name : StringView) -> Int32? {
match name {
"Sunday" => Some(0)
"Monday" => Some(1)
"Tuesday" => Some(2)
"Wednesday" => Some(3)
"Thursday" => Some(4)
"Friday" => Some(5)
"Saturday" => Some(6)
_ => None
}
}
///|
fn month_name(s : String, at : Int32) -> Int32? {
match (s[at], s[at + 1], s[at + 2]) {
('J', 'a', 'n') => Some(1)
('F', 'e', 'b') => Some(2)
('M', 'a', 'r') => Some(3)
('A', 'p', 'r') => Some(4)
('M', 'a', 'y') => Some(5)
('J', 'u', 'n') => Some(6)
('J', 'u', 'l') => Some(7)
('A', 'u', 'g') => Some(8)
('S', 'e', 'p') => Some(9)
('O', 'c', 't') => Some(10)
('N', 'o', 'v') => Some(11)
('D', 'e', 'c') => Some(12)
_ => None
}
}
///|
/// Validates the fields, the weekday included, and converts them to Unix
/// milliseconds. Second 60 is accepted and lands on the next second.
fn date_ms(
weekday : Int32,
year : Int32,
month : Int32,
day : Int32,
time : (Int32, Int32, Int32),
) -> Int64? {
let (hour, minute, second) = time
let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
let days = match month {
2 => if leap { 29 } else { 28 }
4 | 6 | 9 | 11 => 30
_ => 31
}
guard year >= 1 &&
day >= 1 &&
day <= days &&
hour < 24 &&
minute < 60 &&
second <= 60 else {
return None
}
let days = days_from_civil(year, month, day)
guard ((days + 4) % 7 + 7) % 7 == weekday else { return None }
Some(
((days.to_int64() * 24 + hour.to_int64()) * 60 + minute.to_int64()) * 60000 +
second.to_int64() * 1000,
)
}
///|
/// The Gregorian year, month, day and millisecond of the day of a Unix time.
/// The year is clamped to 0–99999 so that an extreme clock cannot overflow
/// the year arithmetic; the other fields are then those of the unclamped
/// date.
fn civil_of(unix_ms : Int64) -> (Int32, Int32, Int32, Int64) {
let day = if unix_ms >= 0L {
unix_ms / 86400000L
} else {
(unix_ms + 1L) / 86400000L - 1L
}
let ms_of_day = unix_ms - day * 86400000L
// The inverse of `days_from_civil`, on March-based eras.
let z = day + 719468L
let era = (if z >= 0L { z } else { z - 146096L }) / 146097L
let doe = z - era * 146097L
let yoe = (doe - doe / 1460L + doe / 36524L - doe / 146096L) / 365L
let doy = doe - (365L * yoe + yoe / 4L - yoe / 100L)
let march_month = (5L * doy + 2L) / 153L
let day_of_month = doy - (153L * march_month + 2L) / 5L + 1L
let month = if march_month < 10L {
march_month + 3L
} else {
march_month - 9L
}
let year = yoe + era * 400L + (if month <= 2L { 1L } else { 0L })
(
year.max(0L).min(99999L).to_int(),
month.to_int(),
day_of_month.to_int(),
ms_of_day,
)
}
///|
// March-based Gregorian eras, with 1970-01-01 as day zero.
fn days_from_civil(year : Int32, month : Int32, day : Int32) -> Int32 {
let y = year - (if month <= 2 { 1 } else { 0 })
let era = y / 400
let yoe = y - era * 400
let m = month + (if month > 2 { -3 } else { 9 })
let doy = (153 * m + 2) / 5 + day - 1
era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719468
}