use crate::error::DomainError;
use serde::{Deserialize, Serialize};
use std::cell::Cell;
thread_local! {
static RNG_STATE: Cell<u64> = const { Cell::new(0) };
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SessionId(String);
impl SessionId {
#[allow(dead_code)] pub(crate) fn new() -> Self {
let value = next_random_u64();
let hex = format!("{:012x}", value & 0xFFFF_FFFF_FFFF);
Self(format!("sess_{hex}"))
}
pub fn from_raw(s: &str) -> Result<Self, DomainError> {
if is_valid_session_id(s) {
Ok(Self(s.to_string()))
} else {
Err(DomainError::InvalidSessionId(s.to_string()))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn to_marker(&self) -> String {
format!("\n<!-- smos:{} -->", self.0)
}
}
fn is_valid_session_id(s: &str) -> bool {
let Some(hex) = s.strip_prefix("sess_") else {
return false;
};
hex.len() == 12
&& hex
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
}
#[allow(dead_code)] fn next_random_u64() -> u64 {
RNG_STATE.with(|cell| {
let mut state = cell.get();
if state == 0 {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(KNUTH_GOLDEN_GAMMA);
let addr_salt = &cell as *const _ as u64;
state = nanos ^ addr_salt.wrapping_mul(ADDR_SALT_MULTIPLIER);
if state == 0 {
state = KNUTH_GOLDEN_GAMMA;
}
}
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
cell.set(state);
state
})
}
#[allow(dead_code)] const KNUTH_GOLDEN_GAMMA: u64 = 0x9E37_79B9_7F4A_7C15;
#[allow(dead_code)] const ADDR_SALT_MULTIPLIER: u64 = 0x517C_C1B7_2722_0A95;
impl std::fmt::Display for SessionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::DomainError;
#[test]
fn new_returns_well_formed_id() {
let id = SessionId::new();
let s = id.as_str();
assert!(s.starts_with("sess_"));
let hex = &s["sess_".len()..];
assert_eq!(hex.len(), 12);
assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn new_generates_distinct_ids_in_a_loop() {
let mut ids = std::collections::HashSet::new();
for _ in 0..32 {
ids.insert(SessionId::new().as_str().to_string());
}
assert!(ids.len() > 28, "got {} distinct ids", ids.len());
}
#[test]
fn new_produces_canonical_shape() {
let id = SessionId::new();
assert!(id.as_str().starts_with("sess_"));
}
#[test]
fn from_raw_accepts_well_formed_id() {
let parsed = SessionId::from_raw("sess_abcdef012345").unwrap();
assert_eq!(parsed.as_str(), "sess_abcdef012345");
}
#[test]
fn from_raw_rejects_missing_prefix() {
assert!(matches!(
SessionId::from_raw("abcdef012345"),
Err(DomainError::InvalidSessionId(_))
));
}
#[test]
fn from_raw_rejects_wrong_length() {
assert!(SessionId::from_raw("sess_abc").is_err());
assert!(SessionId::from_raw("sess_abcdef0123456789").is_err());
}
#[test]
fn from_raw_rejects_uppercase() {
assert!(SessionId::from_raw("sess_ABCDEF012345").is_err());
}
#[test]
fn from_raw_rejects_non_hex() {
assert!(SessionId::from_raw("sess_zzzzzzzzzzzz").is_err());
}
#[test]
fn display_returns_raw_string() {
let id = SessionId::from_raw("sess_abcdef012345").unwrap();
assert_eq!(id.to_string(), "sess_abcdef012345");
}
#[test]
fn to_marker_renders_template_with_session_id() {
let id = SessionId::from_raw("sess_abcdef012345").unwrap();
assert_eq!(id.to_marker(), "\n<!-- smos:sess_abcdef012345 -->");
}
}