use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
use rand::Rng;
use super::random::generate_random_string;
const SESSION_PREFIX_DEFAULT_LEN: usize = 12;
const SESSION_ID_RANDOM_SUFFIX_LEN: usize = 5;
fn session_prefix_storage() -> &'static Mutex<String> {
static PREFIX: OnceLock<Mutex<String>> = OnceLock::new();
PREFIX.get_or_init(|| Mutex::new(generate_random_string(SESSION_PREFIX_DEFAULT_LEN)))
}
pub fn set_session_prefix(prefix: &str) {
if prefix.is_empty() {
return;
}
let mut guard = session_prefix_storage()
.lock()
.expect("session-prefix mutex poisoned");
*guard = prefix.to_string();
}
pub fn generate_session_id() -> String {
let prefix = session_prefix_storage()
.lock()
.expect("session-prefix mutex poisoned")
.clone();
let ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let suffix = generate_random_string(SESSION_ID_RANDOM_SUFFIX_LEN);
format!("{prefix}{ms}{suffix}")
}
pub fn make_uid_postfix() -> String {
let ms: u128 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let r: u128 = rand::thread_rng().gen_range(0..1000);
format!("{:x}", ms * 1000 + r)
}
pub fn make_user_id(appid: &str, uid: &str, cuid: &str) -> String {
format!("connect.{appid}.{uid}.{cuid}")
}
pub fn make_db_key(uid: &str) -> String {
format!("connect.{uid}")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserId {
pub id: String,
pub appid: String,
pub uid: String,
pub cuid: String,
}
pub fn parse_user_id(user_id: &str) -> Option<UserId> {
let items: Vec<&str> = user_id.split('.').collect();
if items.len() < 4 {
return None;
}
Some(UserId {
id: user_id.to_string(),
appid: items[1].to_string(),
uid: items[2].to_string(),
cuid: items[3].to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn prefix_test_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
fn lock_prefix_state<'a>() -> std::sync::MutexGuard<'a, ()> {
prefix_test_lock().lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn session_id_default_prefix_shape() {
let _g = lock_prefix_state();
let sid = generate_session_id();
assert!(sid.len() >= SESSION_PREFIX_DEFAULT_LEN + 13 + SESSION_ID_RANDOM_SUFFIX_LEN);
}
#[test]
fn session_id_with_custom_prefix() {
let _g = lock_prefix_state();
set_session_prefix("test_prefix_alpha");
let sid = generate_session_id();
assert!(sid.starts_with("test_prefix_alpha"));
}
#[test]
fn session_id_empty_prefix_is_ignored() {
let _g = lock_prefix_state();
set_session_prefix("known_prefix_beta");
set_session_prefix("");
let sid = generate_session_id();
assert!(sid.starts_with("known_prefix_beta"));
}
#[test]
fn session_ids_are_unique() {
let a = generate_session_id();
let b = generate_session_id();
assert_ne!(a, b);
}
#[test]
fn uid_postfix_is_valid_hex() {
let p = make_uid_postfix();
assert!(!p.is_empty());
assert!(p.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn uid_postfix_increases_over_time() {
let a = u128::from_str_radix(&make_uid_postfix(), 16).unwrap();
std::thread::sleep(std::time::Duration::from_millis(2));
let b = u128::from_str_radix(&make_uid_postfix(), 16).unwrap();
assert!(a < b);
}
#[test]
fn make_user_id_format() {
assert_eq!(make_user_id("a", "u", "c"), "connect.a.u.c");
}
#[test]
fn parse_user_id_round_trip() {
let s = make_user_id("app1", "user42", "client99");
let parsed = parse_user_id(&s).unwrap();
assert_eq!(parsed.id, s);
assert_eq!(parsed.appid, "app1");
assert_eq!(parsed.uid, "user42");
assert_eq!(parsed.cuid, "client99");
}
#[test]
fn parse_user_id_rejects_too_few_segments() {
assert!(parse_user_id("connect.a.b").is_none());
assert!(parse_user_id("nope").is_none());
assert!(parse_user_id("").is_none());
}
#[test]
fn parse_user_id_accepts_extra_trailing_segments() {
let parsed = parse_user_id("connect.app1.uid1.cid1.extra").unwrap();
assert_eq!(parsed.appid, "app1");
assert_eq!(parsed.uid, "uid1");
assert_eq!(parsed.cuid, "cid1");
}
#[test]
fn make_db_key_format() {
assert_eq!(make_db_key("user42"), "connect.user42");
}
}