use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub(crate) fn format_rfc3339_secs(t: SystemTime) -> String {
let epoch_secs = t
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs() as i64;
let (y, mo, d, h, mi, s) = epoch_secs_to_calendar(epoch_secs);
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z")
}
pub(crate) fn parse_rfc3339_to_unix_secs(s: &str) -> Option<i64> {
let b = s.as_bytes();
if b.len() < 20 {
return None;
}
if b[4] != b'-' || b[7] != b'-' || b[13] != b':' || b[16] != b':' {
return None;
}
let sep = b[10];
if sep != b'T' && sep != b't' {
return None;
}
let year = parse_fixed::<4>(&b[0..4])? as i64;
let month = parse_fixed::<2>(&b[5..7])? as i64;
let day = parse_fixed::<2>(&b[8..10])? as i64;
let hour = parse_fixed::<2>(&b[11..13])? as i64;
let min = parse_fixed::<2>(&b[14..16])? as i64;
let sec = parse_fixed::<2>(&b[17..19])? as i64;
let mut pos = 19usize;
if b.get(pos) == Some(&b'.') {
pos += 1;
while pos < b.len() && b[pos].is_ascii_digit() {
pos += 1;
}
}
let offset_secs: i64 = match b.get(pos) {
Some(&b'Z') | Some(&b'z') => 0,
Some(&b'+') | Some(&b'-') => {
if b.len() < pos + 6 {
return None;
}
let sign: i64 = if b[pos] == b'+' { 1 } else { -1 };
let oh = parse_fixed::<2>(&b[pos + 1..pos + 3])? as i64;
if b.get(pos + 3) != Some(&b':') {
return None;
}
let om = parse_fixed::<2>(&b[pos + 4..pos + 6])? as i64;
if oh > 23 || om > 59 {
return None;
}
sign * (oh * 3600 + om * 60)
}
_ => return None,
};
if !(1..=9999).contains(&year) || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
if hour > 23 || min > 59 || sec > 60 {
return None;
}
let days = calendar_to_days(year, month, day)?;
let utc_secs = days * 86400 + hour * 3600 + min * 60 + sec - offset_secs;
Some(utc_secs)
}
fn calendar_to_days(y: i64, m: i64, d: i64) -> Option<i64> {
let y = if m <= 2 { y - 1 } else { y };
let m = if m <= 2 { m + 9 } else { m - 3 };
let era = y.div_euclid(400);
let yoe = y.rem_euclid(400); let doy = (153 * m + 2) / 5 + d - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; let z = era * 146097 + doe;
Some(z - 719468)
}
fn epoch_secs_to_calendar(secs: i64) -> (i32, u8, u8, u8, u8, u8) {
let z = secs.div_euclid(86400) + 719468;
let era = z.div_euclid(146097) as i32;
let doe = z.rem_euclid(146097) as u64;
let yoe = (doe - doe / 1461 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i32 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u8;
let mo = if mp < 10 { mp as u8 + 3 } else { mp as u8 - 9 };
let y = if mo <= 2 { y + 1 } else { y };
let tod = secs.rem_euclid(86400) as u64;
let h = (tod / 3600) as u8;
let mi = ((tod % 3600) / 60) as u8;
let s = (tod % 60) as u8;
(y, mo, d, h, mi, s)
}
#[inline]
fn parse_fixed<const N: usize>(b: &[u8]) -> Option<u64> {
if b.len() != N {
return None;
}
let mut n: u64 = 0;
for &c in b {
if !c.is_ascii_digit() {
return None;
}
n = n * 10 + (c - b'0') as u64;
}
Some(n)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unix_epoch_formats_correctly() {
assert_eq!(format_rfc3339_secs(UNIX_EPOCH), "1970-01-01T00:00:00Z");
}
#[test]
fn known_date_formats_correctly() {
let t = UNIX_EPOCH + Duration::from_secs(10957 * 86400);
assert_eq!(format_rfc3339_secs(t), "2000-01-01T00:00:00Z");
}
#[test]
fn format_rfc3339_round_trip() {
let secs = 1_748_000_000u64; let t = UNIX_EPOCH + Duration::from_secs(secs);
let s = format_rfc3339_secs(t);
let parsed = parse_rfc3339_to_unix_secs(&s).expect("should parse back");
assert_eq!(parsed, secs as i64);
}
#[test]
fn parse_utc_z_suffix() {
let secs = parse_rfc3339_to_unix_secs("2026-05-27T12:34:56Z").unwrap();
assert_eq!(
format_rfc3339_secs(UNIX_EPOCH + Duration::from_secs(secs as u64)),
"2026-05-27T12:34:56Z"
);
}
#[test]
fn parse_positive_offset() {
let a = parse_rfc3339_to_unix_secs("2026-05-27T14:34:56+02:00").unwrap();
let b = parse_rfc3339_to_unix_secs("2026-05-27T12:34:56Z").unwrap();
assert_eq!(a, b);
}
#[test]
fn parse_negative_offset() {
let a = parse_rfc3339_to_unix_secs("2026-05-27T07:34:56-05:00").unwrap();
let b = parse_rfc3339_to_unix_secs("2026-05-27T12:34:56Z").unwrap();
assert_eq!(a, b);
}
#[test]
fn parse_fractional_seconds_discarded() {
let a = parse_rfc3339_to_unix_secs("2026-05-27T12:34:56.789Z").unwrap();
let b = parse_rfc3339_to_unix_secs("2026-05-27T12:34:56Z").unwrap();
assert_eq!(a, b);
}
#[test]
fn parse_known_unix_epoch_2000() {
let secs = parse_rfc3339_to_unix_secs("2000-01-01T00:00:00Z").unwrap();
assert_eq!(secs, 10957 * 86400);
}
#[test]
fn reject_too_short() {
assert!(parse_rfc3339_to_unix_secs("2026-05-27").is_none());
assert!(parse_rfc3339_to_unix_secs("").is_none());
}
#[test]
fn reject_missing_timezone() {
assert!(parse_rfc3339_to_unix_secs("2026-05-27T12:34:56").is_none());
}
#[test]
fn reject_invalid_month() {
assert!(parse_rfc3339_to_unix_secs("2026-13-01T00:00:00Z").is_none());
assert!(parse_rfc3339_to_unix_secs("2026-00-01T00:00:00Z").is_none());
}
#[test]
fn reject_invalid_separators() {
assert!(parse_rfc3339_to_unix_secs("2026/05/27T12:34:56Z").is_none());
assert!(parse_rfc3339_to_unix_secs("2026-05-27X12:34:56Z").is_none());
}
#[test]
fn reject_non_ascii_digit_fields() {
assert!(parse_rfc3339_to_unix_secs("20XY-05-27T12:34:56Z").is_none());
}
#[test]
fn lowercase_t_accepted() {
let a = parse_rfc3339_to_unix_secs("2026-05-27t12:34:56Z").unwrap();
let b = parse_rfc3339_to_unix_secs("2026-05-27T12:34:56Z").unwrap();
assert_eq!(a, b);
}
}