1use rand::rngs::OsRng;
2use rand::{Rng, SeedableRng, TryRngCore};
3use rand_chacha::ChaCha20Rng;
4use sha3::{Digest, Sha3_512};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8pub const DEFAULT_LENGTH: usize = 24;
10pub const MAX_LENGTH: usize = 32;
12pub const MIN_LENGTH: usize = 2;
14const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
16static COUNTER: AtomicU64 = AtomicU64::new(0);
18
19#[derive(Debug)]
21pub enum CuidError {
22 InvalidLength(usize, usize, usize),
23 SystemTimeError(std::time::SystemTimeError),
24 RandChaChaError(rand_chacha::rand_core::OsError),
25}
26
27impl std::fmt::Display for CuidError {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 CuidError::InvalidLength(len, min, max) => {
31 write!(
32 f,
33 "Invalid CUID length: {}, expected between {} and {}",
34 len, min, max
35 )
36 }
37 CuidError::SystemTimeError(err) => {
38 write!(f, "System time error: {}", err)
39 }
40 CuidError::RandChaChaError(err) => {
41 write!(f, "ChaCha RNG error: {}", err)
42 }
43 }
44 }
45}
46
47impl std::error::Error for CuidError {}
48
49impl From<std::time::SystemTimeError> for CuidError {
50 fn from(err: std::time::SystemTimeError) -> Self {
51 CuidError::SystemTimeError(err)
52 }
53}
54
55impl From<rand_chacha::rand_core::OsError> for CuidError {
56 fn from(err: rand_chacha::rand_core::OsError) -> Self {
57 CuidError::RandChaChaError(err)
58 }
59}
60
61pub type Result<T> = std::result::Result<T, CuidError>;
63
64fn generate_entropy(length: usize) -> Result<String> {
66 let seed = OsRng.try_next_u64()?;
68 let mut rng = ChaCha20Rng::seed_from_u64(seed);
69
70 Ok((0..length)
71 .map(|_| char::from_digit(rng.random_range(0..36) as u32, 36).unwrap())
72 .collect())
73}
74
75fn compute_hash(input: &str, length: usize) -> String {
77 let mut hasher = Sha3_512::new();
78 hasher.update(input.as_bytes());
79 let result = hasher.finalize();
80 let hash_str = hex::encode(result);
81 hash_str[..length].to_string()
82}
83
84fn generate_random_letter() -> Result<char> {
86 let seed = OsRng.try_next_u64()?;
88 let mut rng = ChaCha20Rng::seed_from_u64(seed);
89
90 Ok(ALPHABET[rng.random_range(0..ALPHABET.len())] as char)
91}
92
93fn generate_fingerprint() -> Result<String> {
95 let entropy = generate_entropy(MAX_LENGTH)?;
96 Ok(compute_hash(&entropy, MAX_LENGTH))
97}
98
99pub fn generate_cuid(length: usize) -> Result<String> {
115 if !(MIN_LENGTH..=MAX_LENGTH).contains(&length) {
116 return Err(CuidError::InvalidLength(length, MIN_LENGTH, MAX_LENGTH));
117 }
118
119 let first_letter = generate_random_letter()?;
120 let timestamp = SystemTime::now()
121 .duration_since(UNIX_EPOCH)?
122 .as_millis()
123 .to_string();
124 let counter_value = COUNTER.fetch_add(1, Ordering::SeqCst).to_string();
125 let salt = generate_entropy(length)?;
126 let fingerprint = generate_fingerprint()?;
127
128 let hash_input = format!("{}{}{}{}", timestamp, salt, counter_value, fingerprint);
129 let hashed = compute_hash(&hash_input, length);
130
131 Ok(format!("{}{}", first_letter, &hashed[1..length]))
132}
133
134pub fn generate() -> Result<String> {
147 generate_cuid(DEFAULT_LENGTH)
148}
149
150pub fn is_valid_cuid(id: &str, min_length: usize, max_length: usize) -> bool {
168 if id.is_empty() {
169 return false;
170 }
171
172 let first_char = id.chars().next().unwrap();
173 let starts_with_letter = first_char.is_ascii_lowercase();
174 let valid_format = id
175 .chars()
176 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit());
177 let valid_length = id.len() >= min_length && id.len() <= max_length;
178
179 starts_with_letter && valid_format && valid_length
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_generate_cuid() {
188 let id = generate_cuid(DEFAULT_LENGTH).unwrap();
189 assert!(is_valid_cuid(&id, MIN_LENGTH, MAX_LENGTH));
190 assert_eq!(id.len(), DEFAULT_LENGTH);
191 }
192
193 #[test]
194 fn test_generate_default() {
195 let id = generate().unwrap();
196 assert!(is_valid_cuid(&id, MIN_LENGTH, MAX_LENGTH));
197 assert_eq!(id.len(), DEFAULT_LENGTH);
198 }
199
200 #[test]
201 fn test_is_valid_cuid() {
202 let id = generate_cuid(DEFAULT_LENGTH).unwrap();
203 assert!(is_valid_cuid(&id, MIN_LENGTH, MAX_LENGTH));
204 }
205
206 #[test]
207 fn test_invalid_cuid_length() {
208 assert!(!is_valid_cuid("a", MIN_LENGTH, MAX_LENGTH));
209 assert!(!is_valid_cuid(
210 "a123456789012345678901234567890123",
211 MIN_LENGTH,
212 MAX_LENGTH
213 ));
214 }
215
216 #[test]
217 fn test_invalid_cuid_format() {
218 assert!(!is_valid_cuid("1abc123", MIN_LENGTH, MAX_LENGTH)); assert!(!is_valid_cuid("abc-123", MIN_LENGTH, MAX_LENGTH)); }
221
222 #[test]
223 fn test_generate_entropy() {
224 let entropy = generate_entropy(10).unwrap();
225 assert_eq!(entropy.len(), 10);
226 assert!(entropy.chars().all(|c| c.is_ascii_alphanumeric()));
227 }
228
229 #[test]
230 fn test_generate_random_letter() {
231 let letter = generate_random_letter().unwrap();
232 assert!(ALPHABET.contains(&(letter as u8)));
233 }
234
235 #[test]
236 fn test_compute_hash() {
237 let input = "test_string";
238 let hashed = compute_hash(input, 16);
239 assert_eq!(hashed.len(), 16);
240 }
241
242 #[test]
243 fn test_invalid_length_error() {
244 let result = generate_cuid(MAX_LENGTH + 1);
245 assert!(result.is_err());
246 match result {
247 Err(CuidError::InvalidLength(len, min, max)) => {
248 assert_eq!(len, MAX_LENGTH + 1);
249 assert_eq!(min, MIN_LENGTH);
250 assert_eq!(max, MAX_LENGTH);
251 }
252 _ => panic!("Expected InvalidLength error"),
253 }
254 }
255
256 #[test]
257 fn test_first_char_is_letter() {
258 for _ in 0..100 {
259 let id = generate().unwrap();
260 assert!(id.chars().next().unwrap().is_ascii_lowercase());
261 }
262 }
263}