use std::time::{SystemTime, UNIX_EPOCH};
#[must_use]
pub fn parse_utc_seconds(text: &str) -> Option<i64> {
let bytes = text.as_bytes();
if bytes.len() != 24 {
return None;
}
for (index, expected) in [
(4, b'-'),
(7, b'-'),
(10, b'T'),
(13, b':'),
(16, b':'),
(19, b'.'),
] {
if bytes.get(index) != Some(&expected) {
return None;
}
}
if bytes.last() != Some(&b'Z') {
return None;
}
let year = number(text.get(0..4)?)?;
let month = number(text.get(5..7)?)?;
let day = number(text.get(8..10)?)?;
let hour = number(text.get(11..13)?)?;
let minute = number(text.get(14..16)?)?;
let second = number(text.get(17..19)?)?;
let _milliseconds = number(text.get(20..23)?)?;
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
if hour > 23 || minute > 59 || second > 60 {
return None;
}
if day > days_in_month(year, month) {
return None;
}
let days = days_from_civil(year, month, day);
Some(days * 86_400 + hour * 3_600 + minute * 60 + second)
}
#[must_use]
pub fn has_expired(expires_at: &str, now: SystemTime) -> bool {
let Some(deadline) = parse_utc_seconds(expires_at) else {
return true;
};
let Ok(elapsed) = now.duration_since(UNIX_EPOCH) else {
return true;
};
let seconds = i64::try_from(elapsed.as_secs()).unwrap_or(i64::MAX);
seconds > deadline
}
#[must_use]
pub fn format_utc(seconds: i64) -> String {
let days = seconds.div_euclid(86_400);
let rest = seconds.rem_euclid(86_400);
let (year, month, day) = civil_from_days(days);
let (hour, minute, second) = (rest / 3_600, (rest % 3_600) / 60, rest % 60);
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.000Z")
}
#[must_use]
pub fn deadline_in(seconds: u64, now: SystemTime) -> String {
let Ok(elapsed) = now.duration_since(UNIX_EPOCH) else {
return format_utc(0);
};
let base = i64::try_from(elapsed.as_secs()).unwrap_or(i64::MAX);
format_utc(base.saturating_add(i64::try_from(seconds).unwrap_or(0)))
}
const fn civil_from_days(days: i64) -> (i64, i64, i64) {
let shifted = days + 719_468;
let era = if shifted >= 0 {
shifted
} else {
shifted - 146_096
} / 146_097;
let day_of_era = shifted - era * 146_097;
let year_of_era =
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let shifted_month = (5 * day_of_year + 2) / 153;
let day = day_of_year - (153 * shifted_month + 2) / 5 + 1;
let month = if shifted_month < 10 {
shifted_month + 3
} else {
shifted_month - 9
};
(if month <= 2 { year + 1 } else { year }, month, day)
}
fn number(text: &str) -> Option<i64> {
if !text.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
text.parse().ok()
}
const fn is_leap(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
const 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 is_leap(year) => 29,
2 => 28,
_ => 0,
}
}
const 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;
let month_shift = if month > 2 { month - 3 } else { month + 9 };
let day_of_year = (153 * month_shift + 2) / 5 + day - 1;
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 {
#![allow(clippy::unwrap_used, clippy::panic)]
use std::time::Duration;
use super::*;
#[test]
fn the_epoch_itself_is_zero() {
assert_eq!(parse_utc_seconds("1970-01-01T00:00:00.000Z"), Some(0));
}
#[test]
fn a_known_instant_matches_its_unix_seconds() {
assert_eq!(
parse_utc_seconds("2026-08-23T15:04:05.123Z"),
Some(1_787_497_445)
);
}
#[test]
fn leap_days_are_accepted_only_in_leap_years() {
assert!(parse_utc_seconds("2024-02-29T00:00:00.000Z").is_some());
assert!(parse_utc_seconds("2026-02-29T00:00:00.000Z").is_none());
assert!(parse_utc_seconds("2000-02-29T00:00:00.000Z").is_some());
assert!(parse_utc_seconds("1900-02-29T00:00:00.000Z").is_none());
}
#[test]
fn any_shape_but_the_consumers_is_refused_rather_than_interpreted() {
for hostile in [
"2026-08-23T15:04:05Z", "2026-08-23T15:04:05.123+00:00", "2026-08-23 15:04:05.123Z", "2026-13-01T00:00:00.000Z", "2026-08-32T00:00:00.000Z", "2026-08-23T24:00:00.000Z", "2026-08-23T15:60:00.000Z", "not-a-timestamp---------", "",
] {
assert!(parse_utc_seconds(hostile).is_none(), "accepted {hostile:?}");
}
}
#[test]
fn an_unparseable_expiry_counts_as_expired() {
assert!(has_expired("whenever", SystemTime::UNIX_EPOCH));
assert!(has_expired("", SystemTime::now()));
}
#[test]
fn a_deadline_in_the_future_has_not_passed_and_one_in_the_past_has() {
let now = UNIX_EPOCH + Duration::from_secs(1_787_497_445);
assert!(!has_expired("2026-08-23T15:04:05.123Z", now));
assert!(!has_expired("2026-08-23T15:04:06.000Z", now));
assert!(has_expired("2026-08-23T15:04:04.999Z", now));
}
#[test]
fn what_this_writes_is_what_it_parses() {
for seconds in [0_i64, 1_787_497_445, 4_102_444_800, 1_709_164_800] {
let text = format_utc(seconds);
assert_eq!(
parse_utc_seconds(&text),
Some(seconds),
"round trip failed for {text}"
);
}
}
#[test]
fn a_deadline_lands_the_requested_distance_ahead() {
let now = UNIX_EPOCH + Duration::from_secs(1_787_497_445);
let text = deadline_in(600, now);
assert_eq!(parse_utc_seconds(&text), Some(1_787_498_045));
assert!(!has_expired(&text, now));
}
#[test]
fn the_boundary_second_is_still_valid() {
let now = UNIX_EPOCH + Duration::from_secs(1_787_497_445);
assert!(!has_expired("2026-08-23T15:04:05.000Z", now));
}
}