use crate::env_source::{EnvSource, ProcessEnvSource};
use chrono::{DateTime, Utc};
pub fn resolve_now_with_env<E: EnvSource + ?Sized>(env: &E) -> DateTime<Utc> {
env.var("SOURCE_DATE_EPOCH")
.and_then(|s| s.parse::<i64>().ok())
.and_then(|secs| DateTime::<Utc>::from_timestamp(secs, 0))
.unwrap_or_else(Utc::now)
}
pub fn resolve_now() -> DateTime<Utc> {
resolve_now_with_env(&ProcessEnvSource)
}
pub fn source_date_epoch_with_env<E: EnvSource + ?Sized>(env: &E) -> Option<DateTime<Utc>> {
env.var("SOURCE_DATE_EPOCH")
.and_then(|s| s.parse::<i64>().ok())
.and_then(|secs| DateTime::<Utc>::from_timestamp(secs, 0))
}
pub fn source_date_epoch() -> Option<DateTime<Utc>> {
source_date_epoch_with_env(&ProcessEnvSource)
}
pub fn rfc3339_utc_from_epoch(secs: i64) -> Option<String> {
DateTime::<Utc>::from_timestamp(secs, 0).map(|dt| dt.to_rfc3339())
}
pub fn rfc2822_utc_from_epoch(secs: i64) -> Option<String> {
DateTime::<Utc>::from_timestamp(secs, 0)
.map(|dt| dt.format("%a, %d %b %Y %H:%M:%S +0000").to_string())
}
pub fn zip_datetime_fields(epoch_secs: u64) -> Option<(u16, u8, u8, u8, u8, u8)> {
use chrono::{Datelike as _, Timelike as _};
let dt = DateTime::<Utc>::from_timestamp(epoch_secs as i64, 0)?;
let year = u16::try_from(dt.year()).ok()?.clamp(1980, 2107);
Some((
year,
dt.month() as u8,
dt.day() as u8,
dt.hour() as u8,
dt.minute() as u8,
dt.second() as u8,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::env_source::MapEnvSource;
#[test]
fn zip_datetime_fields_clamps_out_of_range_years() {
let (y, ..) = zip_datetime_fields(0).expect("epoch 0 fields");
assert_eq!(y, 1980);
let fields = zip_datetime_fields(1_700_000_000).expect("fields");
assert_eq!(fields, (2023, 11, 14, 22, 13, 20));
}
#[test]
fn resolve_now_honors_source_date_epoch() {
let env = MapEnvSource::new().with("SOURCE_DATE_EPOCH", "1715000000");
let now = resolve_now_with_env(&env);
assert_eq!(now.timestamp(), 1715000000);
}
#[test]
fn resolve_now_ignores_malformed_sde() {
let env = MapEnvSource::new().with("SOURCE_DATE_EPOCH", "not-a-number");
let now = resolve_now_with_env(&env);
assert!(now.timestamp() > 1_577_836_800);
}
#[test]
fn rfc2822_matches_zsyncmake_format() {
assert_eq!(
rfc2822_utc_from_epoch(1_700_000_000).as_deref(),
Some("Tue, 14 Nov 2023 22:13:20 +0000")
);
assert_eq!(
rfc2822_utc_from_epoch(1_751_355_036).as_deref(),
Some("Tue, 01 Jul 2025 07:30:36 +0000")
);
}
#[test]
fn resolve_now_falls_back_when_unset() {
let env = MapEnvSource::new();
let now = resolve_now_with_env(&env);
assert!(now.timestamp() > 1_577_836_800);
}
#[test]
fn source_date_epoch_returns_some_when_set() {
let env = MapEnvSource::new().with("SOURCE_DATE_EPOCH", "1715000000");
let dt = source_date_epoch_with_env(&env);
assert_eq!(dt.map(|d| d.timestamp()), Some(1715000000));
}
#[test]
fn source_date_epoch_returns_none_when_unset() {
let env = MapEnvSource::new();
assert!(source_date_epoch_with_env(&env).is_none());
}
#[test]
fn source_date_epoch_returns_none_when_malformed() {
let env = MapEnvSource::new().with("SOURCE_DATE_EPOCH", "not-a-number");
assert!(source_date_epoch_with_env(&env).is_none());
}
}