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('') }