v2 / vlib / rand / cuid2 / cuid2.v
155 lines · 136 sloc · 4.48 KB · 37255767290c243b71f0e78d77c3bd5e875748e6
Raw
1module cuid2
2
3import rand
4import time
5import strconv
6import crypto.sha3
7import math.big
8import os
9
10const default_id_length = 24
11const min_id_length = 2
12const max_id_length = 32
13// ~22k hosts before 50% chance of initial counter collision
14const max_session_count = 476782367
15
16// Cuid2Generator can be used to get secure, collision-resistant ids optimized for horizontal scaling and performance. Next generation UUIDs.
17pub struct Cuid2Generator {
18mut:
19 // A counter that will be used to affect the entropy of
20 // successive id generation calls
21 session_counter i64
22 // A unique string that will be used by the Cuid generator
23 // to help prevent collisions when generating Cuids in a
24 // distributed system.
25 fingerprint string
26pub mut:
27 // A PRNG that has a PRNG interface
28 prng &rand.PRNG = rand.get_current_rng()
29 // Length of the generated Cuid, min = 2, max = 32, default = 24
30 length int = default_id_length
31}
32
33@[params]
34pub struct Cuid2Param {
35pub mut:
36 // A PRNG that has a PRNG interface
37 prng &rand.PRNG = rand.get_current_rng()
38 // Length of the generated Cuid, min = 2, max = 32, default = 24
39 length int = default_id_length
40}
41
42// new Create a cuid2 UUID generator.
43pub fn new(param Cuid2Param) Cuid2Generator {
44 return Cuid2Generator{
45 prng: param.prng
46 length: param.length
47 }
48}
49
50// generate Generate a new cuid2 UUID.
51// It is an alias to function `cuid2()`
52pub fn (mut g Cuid2Generator) generate() string {
53 return g.cuid2()
54}
55
56// cuid2 generates a random (cuid2) UUID.
57// Secure, collision-resistant ids optimized for horizontal
58// scaling and performance. Next generation UUIDs.
59// Ported from https://github.com/paralleldrive/cuid2
60pub fn (mut g Cuid2Generator) cuid2() string {
61 if g.length < min_id_length || g.length > max_id_length {
62 panic('cuid2 length(${g.length}) out of range: min=${min_id_length}, max=${max_id_length}')
63 }
64
65 mut prng := g.prng
66 first_letter := prng.string(1).to_lower()
67 now := strconv.format_int(time.now().unix_milli(), 36)
68 if g.session_counter == 0 {
69 // First call, init session counter, fingerprint.
70 g.session_counter = i64(prng.f64() * max_session_count)
71 g.fingerprint = create_fingerprint(mut prng, get_environment_key_string())
72 }
73 g.session_counter = g.session_counter + 1
74 count := strconv.format_int(g.session_counter, 36)
75
76 // The salt should be long enough to be globally unique
77 // across the full length of the hash. For simplicity,
78 // we use the same length as the intended id output.
79 salt := create_entropy(g.length, mut prng)
80 hash_input := now + salt + count + g.fingerprint
81 hash_digest := first_letter + hash(hash_input)[1..g.length]
82 return hash_digest
83}
84
85// next generates a new cuid2 UUID.
86// It is an alias to function `cuid2()`
87pub fn (mut g Cuid2Generator) next() ?string {
88 return g.cuid2()
89}
90
91// is_cuid checks whether a given `cuid` has a valid form and length.
92pub fn is_cuid(cuid string) bool {
93 if cuid.len < min_id_length || cuid.len > max_id_length {
94 return false
95 }
96
97 // first letter should in [a..z]
98 if cuid[0] < u8(`a`) || cuid[0] > u8(`z`) {
99 return false
100 }
101
102 // other letter should in [a..z,0..9]
103 for letter in cuid[1..] {
104 if (letter >= u8(`a`) && letter <= u8(`z`)) || (letter >= u8(`0`) && letter <= u8(`9`)) {
105 continue
106 }
107 return false
108 }
109 return true
110}
111
112fn create_entropy(length int, mut prng rand.PRNG) string {
113 mut entropy := ''
114 for entropy.len < length {
115 randomness := i64(prng.f64() * 36)
116 entropy += strconv.format_int(randomness, 36)
117 }
118 return entropy
119}
120
121// create_fingerprint This is a fingerprint of the host environment.
122// It is used to help prevent collisions when generating ids in a
123// distributed system. If no global object is available, you can
124// pass in your own, or fall back on a random string.
125fn create_fingerprint(mut prng rand.PRNG, env_key_string string) string {
126 mut source_string := create_entropy(max_id_length, mut prng)
127 if env_key_string.len > 0 {
128 source_string += env_key_string
129 }
130 source_string_hash := hash(source_string)
131 return source_string_hash[1..]
132}
133
134fn hash(input string) string {
135 mut hash := sha3.new512() or { panic(err) }
136 hash.write(input.bytes()) or { panic(err) }
137 hash_digest := hash.checksum()
138
139 // Drop the first character because it will bias
140 // the histogram to the left.
141 return big.integer_from_bytes(hash_digest).radix_str(36)[1..]
142}
143
144fn get_environment_key_string() string {
145 env := os.environ()
146 mut keys := []string{}
147
148 // Discard values of environment variables
149 for _, variable in env {
150 index := variable.index('=') or { variable.len }
151 key := variable[..index]
152 keys << key
153 }
154 return keys.join('')
155}
156