use core::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::timeutil::{civil_to_unix, month_index};
#[must_use]
pub fn parse_retry_after(value: &str) -> Option<Duration> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX));
parse_retry_after_at(value, now)
}
#[must_use]
pub fn parse_retry_after_at(value: &str, now_unix_secs: i64) -> Option<Duration> {
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
if trimmed.bytes().all(|b| b.is_ascii_digit()) {
return trimmed.parse::<u64>().ok().map(Duration::from_secs);
}
let target = parse_http_date(trimmed)?;
let delta = target.saturating_sub(now_unix_secs);
Some(Duration::from_secs(
u64::try_from(delta.max(0)).unwrap_or(0),
))
}
fn parse_http_date(value: &str) -> Option<i64> {
let tokens: Vec<&str> = value.split_whitespace().collect();
match tokens.as_slice() {
[_dow, day, month, year, time, "GMT"] => {
let day = day.parse::<u32>().ok()?;
let month = month_index(month)?;
let year = year.parse::<i64>().ok()?;
let (h, m, s) = parse_hms(time)?;
civil_to_unix(year, month, day, h, m, s)
}
[_dow, month, day, time, year] => {
let day = day.parse::<u32>().ok()?;
let month = month_index(month)?;
let year = year.parse::<i64>().ok()?;
let (h, m, s) = parse_hms(time)?;
civil_to_unix(year, month, day, h, m, s)
}
[_dow, date, time, "GMT"] => {
let mut parts = date.split('-');
let day = parts.next()?.parse::<u32>().ok()?;
let month = month_index(parts.next()?)?;
let yy = parts.next()?.parse::<i64>().ok()?;
if parts.next().is_some() {
return None;
}
let year = if yy >= 70 { 1900 + yy } else { 2000 + yy };
let (h, m, s) = parse_hms(time)?;
civil_to_unix(year, month, day, h, m, s)
}
_ => None,
}
}
fn parse_hms(value: &str) -> Option<(u32, u32, u32)> {
let mut parts = value.split(':');
let h = parts.next()?.parse::<u32>().ok()?;
let m = parts.next()?.parse::<u32>().ok()?;
let s = parts.next()?.parse::<u32>().ok()?;
if parts.next().is_some() || h > 23 || m > 59 || s > 60 {
return None;
}
Some((h, m, s))
}
#[cfg(test)]
mod tests {
use super::parse_retry_after_at;
use core::time::Duration;
#[test]
fn test_seconds_form() {
assert_eq!(parse_retry_after_at("0", 999), Some(Duration::ZERO));
assert_eq!(
parse_retry_after_at("120", 0),
Some(Duration::from_secs(120))
);
assert_eq!(
parse_retry_after_at(" 45 ", 0),
Some(Duration::from_secs(45))
);
}
#[test]
fn test_malformed_is_none() {
assert_eq!(parse_retry_after_at("", 0), None);
assert_eq!(parse_retry_after_at("soon", 0), None);
assert_eq!(parse_retry_after_at("-5", 0), None);
assert_eq!(parse_retry_after_at("12.5", 0), None);
assert_eq!(
parse_retry_after_at("Mon, 99 Zzz 2026 99:99:99 GMT", 0),
None
);
}
#[test]
fn test_imf_fixdate_form() {
let target = 784_111_777;
let header = "Sun, 06 Nov 1994 08:49:37 GMT";
assert_eq!(
parse_retry_after_at(header, target - 100),
Some(Duration::from_secs(100))
);
assert_eq!(
parse_retry_after_at(header, target + 50),
Some(Duration::ZERO)
);
}
#[test]
fn test_asctime_and_rfc850_forms_agree() {
let target = 784_111_777; let asctime = "Sun Nov 6 08:49:37 1994";
let rfc850 = "Sunday, 06-Nov-94 08:49:37 GMT";
assert_eq!(
parse_retry_after_at(asctime, target - 10),
Some(Duration::from_secs(10))
);
assert_eq!(
parse_retry_after_at(rfc850, target - 10),
Some(Duration::from_secs(10))
);
}
#[test]
fn test_case_insensitive_month() {
let header = "Thu, 01 jan 2026 00:00:00 GMT";
assert_eq!(
parse_retry_after_at(header, 1_767_225_600),
Some(Duration::ZERO)
);
}
}