use std::time::{SystemTime, UNIX_EPOCH};
use crate::session::types::SessionId;
const RANDOM_SUFFIX_LEN: usize = 6;
const ALPHABET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ23456789";
#[aristo::intent(
"Session ids start with a sortable UTC timestamp (YYYYMMDDTHHMMSSZ) \
so `ls .aristo/sessions/active/` and `aristo session list` order \
chronologically without an index. A refactor that put random bytes \
first would break `aristo session list`'s expected newest-last \
ordering and would force a per-session sort on every read.",
verify = "neural",
id = "session_id_timestamp_prefix_is_load_bearing_for_ordering"
)]
pub fn mint_session_id() -> SessionId {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock is post-1970");
let stamp = format_utc_compact(now.as_secs());
let mut bytes = [0u8; RANDOM_SUFFIX_LEN];
getrandom::getrandom(&mut bytes)
.expect("OS RNG failed; aborting rather than emitting a low-entropy id");
let mut suffix = String::with_capacity(RANDOM_SUFFIX_LEN);
for b in bytes {
suffix.push(ALPHABET[(b as usize) % ALPHABET.len()] as char);
}
SessionId::from_string(format!("{stamp}-{suffix}"))
}
pub(crate) fn format_utc_compact(secs: u64) -> String {
let (y, mo, d, h, m, s) = utc_components(secs);
format!("{y:04}{mo:02}{d:02}T{h:02}{m:02}{s:02}Z")
}
pub(crate) fn format_rfc3339(secs: u64) -> String {
let (y, mo, d, h, m, s) = utc_components(secs);
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
}
fn utc_components(secs: u64) -> (i64, u32, u32, u32, u32, u32) {
let days = (secs / 86_400) as i64;
let secs_of_day = (secs % 86_400) as u32;
let h = secs_of_day / 3600;
let m = (secs_of_day % 3600) / 60;
let s = secs_of_day % 60;
let (y, mo, d) = civil_from_days(days);
(y, mo, d, h, m, s)
}
fn civil_from_days(days: i64) -> (i64, u32, u32) {
let z = days + 719_468; let era = z.div_euclid(146_097);
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + 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 u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mint_produces_expected_shape() {
let id = mint_session_id();
let s = id.as_str();
assert_eq!(s.len(), 23, "unexpected id length for {s}");
assert!(s.chars().nth(8).unwrap() == 'T', "missing T separator: {s}");
assert!(s.contains('-'));
}
#[test]
fn ids_minted_in_sequence_sort_chronologically() {
let earlier = format_utc_compact(1_700_000_000); let later = format_utc_compact(1_700_000_001);
assert!(earlier < later, "lex order should match time order");
let across_day = format_utc_compact(1_700_086_400); assert!(later < across_day);
}
#[test]
fn format_utc_compact_known_values() {
assert_eq!(format_utc_compact(1_767_225_600), "20260101T000000Z");
assert_eq!(format_utc_compact(1_779_447_600), "20260522T110000Z");
assert_eq!(format_utc_compact(1_709_208_000), "20240229T120000Z");
}
#[test]
fn format_rfc3339_uses_dashes_and_colons() {
assert_eq!(format_rfc3339(1_767_225_600), "2026-01-01T00:00:00Z");
}
#[test]
fn ids_minted_concurrently_are_distinct() {
let a = mint_session_id();
let b = mint_session_id();
assert_ne!(a.as_str(), b.as_str());
}
}