Skip to content

Commit a1de8db

Browse files
authored
vlib: add new rand.cuid2 module (#23181)
1 parent 87e017e commit a1de8db

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed

vlib/rand/cuid2/cuid2.v

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

vlib/rand/cuid2/cuid2_test.v

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
module cuid2
2+
3+
import rand.musl
4+
import rand.mt19937
5+
6+
fn test_cuid2() {
7+
// default prng(wyrand), default id length = 24
8+
mut g24 := new()
9+
uuid24 := g24.generate()
10+
assert uuid24.len == 24
11+
assert is_cuid(uuid24)
12+
13+
// default prng(wyrand), id length = 2
14+
mut g2 := new(length: 2)
15+
uuid2 := g2.generate()
16+
assert uuid2.len == 2
17+
assert is_cuid(uuid2)
18+
19+
// default prng(wyrand), id length = 32
20+
mut g32 := new(length: 32)
21+
uuid32 := g32.generate()
22+
assert uuid32.len == 32
23+
assert is_cuid(uuid32)
24+
25+
// musl prng, id length = 28
26+
mut g_musl := new(prng: &musl.MuslRNG{}, length: 28)
27+
uuid_musl := g_musl.generate()
28+
assert uuid_musl.len == 28
29+
assert is_cuid(uuid_musl)
30+
31+
// mt19937 prng, default id length = 24
32+
mut g_mt19937 := new(prng: &mt19937.MT19937RNG{})
33+
uuid_mt19937 := g_mt19937.generate()
34+
assert uuid_mt19937.len == 24
35+
assert is_cuid(uuid_mt19937)
36+
37+
// successive calls
38+
// default prng(wyrand), default id length = 24
39+
mut g := new()
40+
mut ids := []string{}
41+
for id in g {
42+
eprintln(id)
43+
// id length should be default length(24)
44+
assert id.len == 24
45+
assert is_cuid(id)
46+
47+
ids << id
48+
if ids.len == 5 {
49+
break
50+
}
51+
}
52+
53+
// successive calls to g.next() in a row should be unique
54+
assert ids[0] != ids[1]
55+
assert ids[0] != ids[2]
56+
assert ids[0] != ids[3]
57+
assert ids[0] != ids[4]
58+
assert ids[1] != ids[2]
59+
assert ids[1] != ids[3]
60+
assert ids[1] != ids[4]
61+
assert ids[2] != ids[3]
62+
assert ids[2] != ids[4]
63+
assert ids[3] != ids[4]
64+
}

0 commit comments

Comments
 (0)