From a1de8dbd106caa90679ad5416b22bfc20ffd6d0b Mon Sep 17 00:00:00 2001 From: kbkpbot Date: Tue, 17 Dec 2024 22:32:24 +0800 Subject: [PATCH] vlib: add new `rand.cuid2` module (#23181) --- vlib/rand/cuid2/cuid2.v | 156 +++++++++++++++++++++++++++++++++++ vlib/rand/cuid2/cuid2_test.v | 64 ++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 vlib/rand/cuid2/cuid2.v create mode 100644 vlib/rand/cuid2/cuid2_test.v diff --git a/vlib/rand/cuid2/cuid2.v b/vlib/rand/cuid2/cuid2.v new file mode 100644 index 000000000..28c333f53 --- /dev/null +++ b/vlib/rand/cuid2/cuid2.v @@ -0,0 +1,156 @@ +module cuid2 + +import rand +import time +import strconv +import crypto.sha3 +import math.big +import os + +const default_id_length = 24 +const min_id_length = 2 +const max_id_length = 32 +// ~22k hosts before 50% chance of initial counter collision +const max_session_count = 476782367 + +// Cuid2Generator Secure, collision-resistant ids optimized for horizontal +// scaling and performance. Next generation UUIDs. +pub struct Cuid2Generator { +mut: + // A counter that will be used to affect the entropy of + // successive id generation calls + session_counter i64 + // A unique string that will be used by the Cuid generator + // to help prevent collisions when generating Cuids in a + // distributed system. + fingerprint string +pub mut: + // A PRNG that has a PRNG interface + prng &rand.PRNG = rand.get_current_rng() + // Length of the generated Cuid, min = 2, max = 32, default = 24 + length int = default_id_length +} + +@[params] +pub struct Cuid2Param { +pub mut: + // A PRNG that has a PRNG interface + prng &rand.PRNG = rand.get_current_rng() + // Length of the generated Cuid, min = 2, max = 32, default = 24 + length int = default_id_length +} + +// new Create a cuid2 UUID generator. +pub fn new(param Cuid2Param) Cuid2Generator { + return Cuid2Generator{ + prng: param.prng + length: param.length + } +} + +// generate Generate a new cuid2 UUID. +// It is an alias to function `cuid2()` +pub fn (mut g Cuid2Generator) generate() string { + return g.cuid2() +} + +// cuid2 generates a random (cuid2) UUID. +// Secure, collision-resistant ids optimized for horizontal +// scaling and performance. Next generation UUIDs. +// Ported from https://github.com/paralleldrive/cuid2 +pub fn (mut g Cuid2Generator) cuid2() string { + if g.length < min_id_length || g.length > max_id_length { + panic('cuid2 length(${g.length}) out of range: min=${min_id_length}, max=${max_id_length}') + } + + mut prng := g.prng + first_letter := prng.string(1).to_lower() + now := strconv.format_int(time.now().unix_milli(), 36) + if g.session_counter == 0 { + // First call, init session counter, fingerprint. + g.session_counter = i64(prng.f64() * max_session_count) + g.fingerprint = create_fingerprint(mut prng, get_environment_key_string()) + } + g.session_counter = g.session_counter + 1 + count := strconv.format_int(g.session_counter, 36) + + // The salt should be long enough to be globally unique + // across the full length of the hash. For simplicity, + // we use the same length as the intended id output. + salt := create_entropy(g.length, mut prng) + hash_input := now + salt + count + g.fingerprint + hash_digest := first_letter + hash(hash_input)[1..g.length] + return hash_digest +} + +// next Generate a new cuid2 UUID. +// It is an alias to function `cuid2()` +pub fn (mut g Cuid2Generator) next() ?string { + return g.cuid2() +} + +// is_cuid Checks whether a given `cuid` has a valid form and length +pub fn is_cuid(cuid string) bool { + if cuid.len < min_id_length || cuid.len > max_id_length { + return false + } + + // first letter should in [a..z] + if cuid[0] < u8(`a`) || cuid[0] > u8(`z`) { + return false + } + + // other letter should in [a..z,0..9] + for letter in cuid[1..] { + if (letter >= u8(`a`) && letter <= u8(`z`)) || (letter >= u8(`0`) && letter <= u8(`9`)) { + continue + } + return false + } + return true +} + +fn create_entropy(length int, mut prng rand.PRNG) string { + mut entropy := '' + for entropy.len < length { + randomness := i64(prng.f64() * 36) + entropy += strconv.format_int(randomness, 36) + } + return entropy +} + +// create_fingerprint This is a fingerprint of the host environment. +// It is used to help prevent collisions when generating ids in a +// distributed system. If no global object is available, you can +// pass in your own, or fall back on a random string. +fn create_fingerprint(mut prng rand.PRNG, env_key_string string) string { + mut source_string := create_entropy(max_id_length, mut prng) + if env_key_string.len > 0 { + source_string += env_key_string + } + source_string_hash := hash(source_string) + return source_string_hash[1..] +} + +fn hash(input string) string { + mut hash := sha3.new512() or { panic(err) } + hash.write(input.bytes()) or { panic(err) } + hash_digest := hash.checksum() + + // Drop the first character because it will bias + // the histogram to the left. + return big.integer_from_bytes(hash_digest).radix_str(36)[1..] +} + +fn get_environment_key_string() string { + env := os.environ() + mut keys := []string{} + + // Discard values of environment variables + for _, variable in env { + index := variable.index('=') or { variable.len } + key := variable[..index] + keys << key + } + return keys.join('') +} diff --git a/vlib/rand/cuid2/cuid2_test.v b/vlib/rand/cuid2/cuid2_test.v new file mode 100644 index 000000000..bf03cef33 --- /dev/null +++ b/vlib/rand/cuid2/cuid2_test.v @@ -0,0 +1,64 @@ +module cuid2 + +import rand.musl +import rand.mt19937 + +fn test_cuid2() { + // default prng(wyrand), default id length = 24 + mut g24 := new() + uuid24 := g24.generate() + assert uuid24.len == 24 + assert is_cuid(uuid24) + + // default prng(wyrand), id length = 2 + mut g2 := new(length: 2) + uuid2 := g2.generate() + assert uuid2.len == 2 + assert is_cuid(uuid2) + + // default prng(wyrand), id length = 32 + mut g32 := new(length: 32) + uuid32 := g32.generate() + assert uuid32.len == 32 + assert is_cuid(uuid32) + + // musl prng, id length = 28 + mut g_musl := new(prng: &musl.MuslRNG{}, length: 28) + uuid_musl := g_musl.generate() + assert uuid_musl.len == 28 + assert is_cuid(uuid_musl) + + // mt19937 prng, default id length = 24 + mut g_mt19937 := new(prng: &mt19937.MT19937RNG{}) + uuid_mt19937 := g_mt19937.generate() + assert uuid_mt19937.len == 24 + assert is_cuid(uuid_mt19937) + + // successive calls + // default prng(wyrand), default id length = 24 + mut g := new() + mut ids := []string{} + for id in g { + eprintln(id) + // id length should be default length(24) + assert id.len == 24 + assert is_cuid(id) + + ids << id + if ids.len == 5 { + break + } + } + + // successive calls to g.next() in a row should be unique + assert ids[0] != ids[1] + assert ids[0] != ids[2] + assert ids[0] != ids[3] + assert ids[0] != ids[4] + assert ids[1] != ids[2] + assert ids[1] != ids[3] + assert ids[1] != ids[4] + assert ids[2] != ids[3] + assert ids[2] != ids[4] + assert ids[3] != ids[4] +} -- 2.39.5