kleio_rs/lib.rs
1use std::time::{Duration, SystemTime, UNIX_EPOCH};
2
3use rand::prelude::*;
4
5// Sunday, June 23, 2024 Midnight UTC
6const KLEIO_EPOCH_SINCE_UNIX_EPOCH: Duration = Duration::from_secs(1719100800);
7
8#[cfg(not(test))]
9pub fn now() -> SystemTime {
10 SystemTime::now()
11}
12
13///
14/// Generate a new key
15///
16/// This function generates a new key using the algorithm outlined in [`generate_id`] and then encodes it
17/// using base62 encoding, to display it as a string. This function is meant to be used when generating
18/// keys for external use like in urls.
19///
20pub fn generate_key() -> String {
21 base62::encode(generate_id())
22}
23
24///
25/// Generate a new id
26///
27/// This function generates a new key using the following algorithm:
28/// 1. Generate a random 16-bit number
29/// 2. Generate a 48-bit number representing the number of milliseconds since the Kleio Epoch
30/// 3. Combine the two numbers into a 64-bit number
31///
32/// This guarantees that the key is unique, granted there are no collisions in the random number generation
33/// at time x. There is a 50% possiblity of a collision if 110000 keys are generated at the same millisecond, assuming
34/// a uniform distribution of random numbers.
35///
36/// For any reasonable number of keys generated, the probability of a collision is negligible.
37///
38pub fn generate_id() -> u64 {
39 generate_id_from_rng(&mut thread_rng())
40}
41
42///
43/// Generate a new id from rng
44///
45/// This function generates a new key using the following algorithm:
46/// 1. Generate a random 16-bit number
47/// 2. Generate a 48-bit number representing the number of milliseconds since the Kleio Epoch
48/// 3. Combine the two numbers into a 64-bit number
49///
50/// This guarantees that the key is unique, granted there are no collisions in the random number generation
51/// at time x. There is a 50% possiblity of a collision if 110000 keys are generated at the same millisecond, assuming
52/// a uniform distribution of random numbers.
53///
54/// For any reasonable number of keys generated, the probability of a collision is negligible.
55///
56pub fn generate_id_from_rng(rng: &mut impl Rng) -> u64 {
57 let entropy = (rng.next_u32() as u64) << 48;
58 let ms_since_epoch = ((now().duration_since(UNIX_EPOCH + KLEIO_EPOCH_SINCE_UNIX_EPOCH).unwrap().as_millis() as u64) << 16) >> 16;
59
60 entropy | ms_since_epoch
61}
62
63#[cfg(test)]
64pub mod mock_time {
65 use super::*;
66 use std::cell::RefCell;
67
68 thread_local! {
69 static MOCK_TIME: RefCell<Option<SystemTime>> = RefCell::new(None);
70 }
71
72 pub fn now() -> SystemTime {
73 MOCK_TIME.with(|cell| {
74 cell.borrow()
75 .as_ref()
76 .cloned()
77 .unwrap_or_else(SystemTime::now)
78 })
79 }
80
81 pub fn set_mock_time(time: SystemTime) {
82 MOCK_TIME.with(|cell| *cell.borrow_mut() = Some(time));
83 }
84
85 pub fn clear_mock_time() {
86 MOCK_TIME.with(|cell| *cell.borrow_mut() = None);
87 }
88}
89
90#[cfg(test)]
91pub use mock_time::now;
92
93#[cfg(test)]
94mod tests {
95 use mock_time::set_mock_time;
96 use rand::rngs::mock::StepRng;
97
98 use super::*;
99
100 #[test]
101 fn generate_id_from_rng_correctly_uses_rng_and_time() {
102 // Set mock random to 1
103 let mut step_rng = StepRng::new(0b1011001110001111, 1);
104 // Set mock time to 0b1010101 milliseconds after the epoch
105 set_mock_time(UNIX_EPOCH + KLEIO_EPOCH_SINCE_UNIX_EPOCH + Duration::from_millis(0b010011000111000111100001111100000111111000000111));
106
107 let id = generate_id_from_rng(&mut step_rng);
108 assert_eq!(id, 0b1011001110001111010011000111000111100001111100000111111000000111);
109 }
110
111 #[test]
112 fn generate_id_produces_unique_ids() {
113 let (id1, id2) = (generate_id(), generate_id());
114
115 assert_ne!(id1, id2);
116 }
117
118 #[test]
119 fn generate_key_produces_unique_ids() {
120 let (id1, id2) = (generate_key(), generate_key());
121
122 assert_ne!(id1, id2);
123 }
124}