| 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 can be used to get secure, collision-resistant ids optimized for horizontal scaling and performance. Next generation UUIDs. |
| 17 | pub struct Cuid2Generator { |
| 18 | mut: |
| 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 |
| 26 | pub 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] |
| 34 | pub struct Cuid2Param { |
| 35 | pub 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. |
| 43 | pub 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()` |
| 52 | pub 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 |
| 60 | pub 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()` |
| 87 | pub 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. |
| 92 | pub 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 | |
| 112 | fn 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. |
| 125 | fn 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 | |
| 134 | fn hash(input string) string { |
| 135 | mut h := sha3.new512() or { panic(err) } |
| 136 | h.write(input.bytes()) or { panic(err) } |
| 137 | hash_digest := h.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 | |
| 144 | fn 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 | |