1use std::sync::{Mutex, OnceLock};
9use std::time::{SystemTime, UNIX_EPOCH};
10
11use rand::Rng;
12
13use super::random::generate_random_string;
14
15const SESSION_PREFIX_DEFAULT_LEN: usize = 12;
16const SESSION_ID_RANDOM_SUFFIX_LEN: usize = 5;
17
18fn session_prefix_storage() -> &'static Mutex<String> {
19 static PREFIX: OnceLock<Mutex<String>> = OnceLock::new();
20 PREFIX.get_or_init(|| Mutex::new(generate_random_string(SESSION_PREFIX_DEFAULT_LEN)))
21}
22
23pub fn set_session_prefix(prefix: &str) {
36 if prefix.is_empty() {
37 return;
38 }
39 let mut guard = session_prefix_storage()
40 .lock()
41 .expect("session-prefix mutex poisoned");
42 *guard = prefix.to_string();
43}
44
45pub fn generate_session_id() -> String {
52 let prefix = session_prefix_storage()
53 .lock()
54 .expect("session-prefix mutex poisoned")
55 .clone();
56 let ms = SystemTime::now()
57 .duration_since(UNIX_EPOCH)
58 .map(|d| d.as_millis())
59 .unwrap_or(0);
60 let suffix = generate_random_string(SESSION_ID_RANDOM_SUFFIX_LEN);
61 format!("{prefix}{ms}{suffix}")
62}
63
64pub fn make_uid_postfix() -> String {
73 let ms: u128 = SystemTime::now()
74 .duration_since(UNIX_EPOCH)
75 .map(|d| d.as_millis())
76 .unwrap_or(0);
77 let r: u128 = rand::thread_rng().gen_range(0..1000);
78 format!("{:x}", ms * 1000 + r)
79}
80
81pub fn make_user_id(appid: &str, uid: &str, cuid: &str) -> String {
85 format!("connect.{appid}.{uid}.{cuid}")
86}
87
88pub fn make_db_key(uid: &str) -> String {
92 format!("connect.{uid}")
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct UserId {
98 pub id: String,
100 pub appid: String,
102 pub uid: String,
104 pub cuid: String,
106}
107
108pub fn parse_user_id(user_id: &str) -> Option<UserId> {
121 let items: Vec<&str> = user_id.split('.').collect();
122 if items.len() < 4 {
123 return None;
124 }
125 Some(UserId {
126 id: user_id.to_string(),
127 appid: items[1].to_string(),
128 uid: items[2].to_string(),
129 cuid: items[3].to_string(),
130 })
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 fn prefix_test_lock() -> &'static Mutex<()> {
141 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
142 LOCK.get_or_init(|| Mutex::new(()))
143 }
144
145 fn lock_prefix_state<'a>() -> std::sync::MutexGuard<'a, ()> {
146 prefix_test_lock().lock().unwrap_or_else(|e| e.into_inner())
148 }
149
150 #[test]
151 fn session_id_default_prefix_shape() {
152 let _g = lock_prefix_state();
153 let sid = generate_session_id();
154 assert!(sid.len() >= SESSION_PREFIX_DEFAULT_LEN + 13 + SESSION_ID_RANDOM_SUFFIX_LEN);
155 }
156
157 #[test]
158 fn session_id_with_custom_prefix() {
159 let _g = lock_prefix_state();
160 set_session_prefix("test_prefix_alpha");
161 let sid = generate_session_id();
162 assert!(sid.starts_with("test_prefix_alpha"));
163 }
164
165 #[test]
166 fn session_id_empty_prefix_is_ignored() {
167 let _g = lock_prefix_state();
168 set_session_prefix("known_prefix_beta");
169 set_session_prefix("");
170 let sid = generate_session_id();
171 assert!(sid.starts_with("known_prefix_beta"));
172 }
173
174 #[test]
175 fn session_ids_are_unique() {
176 let a = generate_session_id();
177 let b = generate_session_id();
178 assert_ne!(a, b);
179 }
180
181 #[test]
182 fn uid_postfix_is_valid_hex() {
183 let p = make_uid_postfix();
184 assert!(!p.is_empty());
185 assert!(p.chars().all(|c| c.is_ascii_hexdigit()));
186 }
187
188 #[test]
189 fn uid_postfix_increases_over_time() {
190 let a = u128::from_str_radix(&make_uid_postfix(), 16).unwrap();
194 std::thread::sleep(std::time::Duration::from_millis(2));
195 let b = u128::from_str_radix(&make_uid_postfix(), 16).unwrap();
196 assert!(a < b);
197 }
198
199 #[test]
200 fn make_user_id_format() {
201 assert_eq!(make_user_id("a", "u", "c"), "connect.a.u.c");
202 }
203
204 #[test]
205 fn parse_user_id_round_trip() {
206 let s = make_user_id("app1", "user42", "client99");
207 let parsed = parse_user_id(&s).unwrap();
208 assert_eq!(parsed.id, s);
209 assert_eq!(parsed.appid, "app1");
210 assert_eq!(parsed.uid, "user42");
211 assert_eq!(parsed.cuid, "client99");
212 }
213
214 #[test]
215 fn parse_user_id_rejects_too_few_segments() {
216 assert!(parse_user_id("connect.a.b").is_none());
217 assert!(parse_user_id("nope").is_none());
218 assert!(parse_user_id("").is_none());
219 }
220
221 #[test]
222 fn parse_user_id_accepts_extra_trailing_segments() {
223 let parsed = parse_user_id("connect.app1.uid1.cid1.extra").unwrap();
225 assert_eq!(parsed.appid, "app1");
226 assert_eq!(parsed.uid, "uid1");
227 assert_eq!(parsed.cuid, "cid1");
228 }
229
230 #[test]
231 fn make_db_key_format() {
232 assert_eq!(make_db_key("user42"), "connect.user42");
233 }
234}