|
| 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 | +} |
0 commit comments