aboutsummaryrefslogtreecommitdiff
path: root/backend/src/lib/nanoid.zig
diff options
context:
space:
mode:
Diffstat (limited to 'backend/src/lib/nanoid.zig')
-rw-r--r--backend/src/lib/nanoid.zig287
1 files changed, 287 insertions, 0 deletions
diff --git a/backend/src/lib/nanoid.zig b/backend/src/lib/nanoid.zig
new file mode 100644
index 0000000..64ab9bd
--- /dev/null
+++ b/backend/src/lib/nanoid.zig
@@ -0,0 +1,287 @@
+// Copied from:
+// https://github.com/SasLuca/zig-nanoid/blob/91e0a9a8890984f3dcdd98c99002a05a83d0ee89/src/nanoid.zig
+// As it doesn’t support ZON build yet.
+
+const std = @import("std");
+
+/// A collection of useful alphabets that can be used to generate ids.
+pub const alphabets = struct {
+ /// Numbers from 0 to 9.
+ pub const numbers = "0123456789";
+
+ /// English hexadecimal with lowercase characters.
+ pub const hexadecimal_lowercase = numbers ++ "abcdef";
+
+ /// English hexadecimal with uppercase characters.
+ pub const hexadecimal_uppercase = numbers ++ "ABCDEF";
+
+ /// Lowercase English letters.
+ pub const lowercase = "abcdefghijklmnopqrstuvwxyz";
+
+ /// Uppercase English letters.
+ pub const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ /// Numbers and english letters without lookalikes: 1, l, I, 0, O, o, u, v, 5, S, s, 2, Z.
+ pub const no_look_alikes = "346789ABCDEFGHJKLMNPQRTUVWXYabcdefghijkmnpqrtwxyz";
+
+ /// Same as nolookalikes but with removed vowels and following letters: 3, 4, x, X, V.
+ /// This list should protect you from accidentally getting obscene words in generated strings.
+ pub const no_look_alikes_safe = "6789BCDFGHJKLMNPQRTWbcdfghjkmnpqrtwz";
+
+ /// Combination of all the lowercase, uppercase characters and numbers from 0 to 9.
+ /// Does not include any symbols or special characters.
+ pub const alphanumeric = numbers ++ lowercase ++ uppercase;
+
+ /// URL friendly characters used by the default generate procedure.
+ pub const default = "_-" ++ alphanumeric;
+
+ /// An array of all the alphabets.
+ pub const all = [_][]const u8{
+ numbers,
+ hexadecimal_lowercase,
+ hexadecimal_uppercase,
+ lowercase,
+ uppercase,
+ no_look_alikes,
+ no_look_alikes_safe,
+ alphanumeric,
+ default,
+ };
+};
+
+/// The default length for nanoids.
+pub const default_id_len = 21;
+
+/// The mask for the default alphabet length.
+pub const default_mask = computeMask(alphabets.default.len);
+
+/// This should be enough memory for an rng step buffer when generating an id of default length regardless of alphabet length.
+/// It can be used for allocating your rng step buffer if you know the length of your id is `<= default_id_len`.
+pub const rng_step_buffer_len_sufficient_for_default_length_ids = computeSufficientRngStepBufferLengthFor(default_id_len);
+
+/// The maximum length of the alphabet accepted by the nanoid algorithm.
+pub const max_alphabet_len: u8 = std.math.maxInt(u8);
+
+/// Computes the mask necessary for the nanoid algorithm given an alphabet length.
+/// The mask is used to transform a random byte into an index into an array of length `alphabet_len`.
+///
+/// Parameters:
+/// - `alphabet_len`: the length of the alphabet used. The alphabet length must be in the range `(0, max_alphabet_len]`.
+pub fn computeMask(alphabet_len: u8) u8 {
+ std.debug.assert(alphabet_len > 0);
+
+ const clz: u5 = @clz(@as(u31, (alphabet_len - 1) | 1));
+ const mask = (@as(u32, 2) << (31 - clz)) - 1;
+ const result: u8 = @truncate(mask);
+ return result;
+}
+
+/// Computes the length necessary for a buffer which can hold the random byte in a step of a the nanoid generation algorithm given a
+/// certain alphabet length.
+///
+/// Parameters:
+/// - `id_len`: the length of the id you will generate. Can be any value.
+///
+/// - `alphabet_len`: the length of the alphabet used. The alphabet length must be in the range `(0, max_alphabet_len]`.
+pub fn computeRngStepBufferLength(id_len: usize, alphabet_len: u8) usize {
+ // @Note:
+ // Original dev notes regarding this algorithm.
+ // Source: https://github.com/ai/nanoid/blob/0454333dee4612d2c2e163d271af6cc3ce1e5aa4/index.js#L45
+ //
+ // "Next, a step determines how many random bytes to generate.
+ // The number of random bytes gets decided upon the ID length, mask,
+ // alphabet length, and magic number 1.6 (using 1.6 peaks at performance
+ // according to benchmarks)."
+
+ const id_len_f: f64 = @as(f64, @floatFromInt(id_len));
+ const mask_f: f64 = @as(f64, @floatFromInt(computeMask(alphabet_len)));
+ const alphabet_len_f: f64 = @as(f64, @floatFromInt(alphabet_len));
+ const step_buffer_len: f64 = @ceil(1.6 * mask_f * id_len_f / alphabet_len_f);
+ const result = @as(usize, @intFromFloat(step_buffer_len));
+
+ return result;
+}
+
+/// This function computes the biggest possible rng step buffer length necessary
+/// to compute an id with a max length of `max_id_len` regardless of the alphabet length.
+///
+/// Parameters:
+/// - `max_id_len`: The biggest id length for which the step buffer length needs to be sufficient.
+pub fn computeSufficientRngStepBufferLengthFor(max_id_len: usize) usize {
+ @setEvalBranchQuota(2500);
+ var max_step_buffer_len: usize = 0;
+ var i: u9 = 1;
+ while (i <= max_alphabet_len) : (i += 1) {
+ const alphabet_len: u8 = @truncate(i);
+ const step_buffer_len = computeRngStepBufferLength(max_id_len, alphabet_len);
+
+ if (step_buffer_len > max_step_buffer_len) {
+ max_step_buffer_len = step_buffer_len;
+ }
+ }
+
+ return max_step_buffer_len;
+}
+
+/// Generates a nanoid inside `result_buffer` and returns it back to the caller.
+///
+/// Parameters:
+/// - `rng`: a random number generator.
+/// Provide a secure one such as `std.Random.DefaultCsprng` and seed it properly if you have security concerns.
+/// See `Regarding RNGs` in `readme.md` for more information.
+///
+/// - `alphabet`: an array of the bytes that will be used in the id, its length must be in the range `(0, max_alphabet_len]`.
+/// Consider the options from `nanoid.alphabets`.
+///
+/// - `result_buffer`: is an output buffer that will be filled *completely* with random bytes from `alphabet`, thus generating an id of
+/// length `result_buffer.len`. This buffer will be returned at the end of the function.
+///
+/// - `step_buffer`: The buffer will be filled with random bytes using `rng.bytes()`.
+/// Must be at least `computeRngStepBufferLength(computeMask(@truncate(alphabet.len)), result_buffer.len, alphabet.len)` bytes.
+pub fn generateEx(rng: std.Random, alphabet: []const u8, result_buffer: []u8, step_buffer: []u8) []u8 {
+ std.debug.assert(alphabet.len > 0 and alphabet.len <= max_alphabet_len);
+
+ const alphabet_len: u8 = @truncate(alphabet.len);
+ const mask = computeMask(alphabet_len);
+ const necessary_step_buffer_len = computeRngStepBufferLength(result_buffer.len, alphabet_len);
+ const actual_step_buffer = step_buffer[0..necessary_step_buffer_len];
+
+ var result_iter: usize = 0;
+ while (true) {
+ rng.bytes(actual_step_buffer);
+
+ for (actual_step_buffer) |it| {
+ const alphabet_index = it & mask;
+
+ if (alphabet_index >= alphabet_len) {
+ continue;
+ }
+
+ result_buffer[result_iter] = alphabet[alphabet_index];
+
+ if (result_iter == result_buffer.len - 1) {
+ return result_buffer;
+ } else {
+ result_iter += 1;
+ }
+ }
+ }
+}
+
+/// Generates a nanoid inside `result_buffer` and returns it back to the caller.
+///
+/// This function will use `rng.int` instead of `rng.bytes` thus avoiding the need for a step buffer.
+/// Depending on your choice of rng this can be useful, since you avoid the need for a step buffer,
+/// but repeated calls to `rng.int` might be slower than a single call `rng.bytes`.
+///
+/// Parameters:
+/// - `rng`: a random number generator.
+/// Provide a secure one such as `std.Random.DefaultCsprng` and seed it properly if you have security concerns.
+/// See `Regarding RNGs` in `readme.md` for more information.
+///
+/// - `alphabet`: an array of the bytes that will be used in the id, its length must be in the range `(0, max_alphabet_len]`.
+/// Consider the options from `nanoid.alphabets`.
+///
+/// - `result_buffer` is an output buffer that will be filled *completely* with random bytes from `alphabet`, thus generating an id of
+/// length `result_buffer.len`. This buffer will be returned at the end of the function.
+pub fn generateExWithIterativeRng(rng: std.Random, alphabet: []const u8, result_buffer: []u8) []u8 {
+ std.debug.assert(result_buffer.len > 0);
+ std.debug.assert(alphabet.len > 0 and alphabet.len <= max_alphabet_len);
+
+ const alphabet_len: u8 = @truncate(alphabet.len);
+ const mask = computeMask(alphabet_len);
+
+ var result_iter: usize = 0;
+ while (true) {
+ const random_byte = rng.int(u8);
+
+ const alphabet_index = random_byte & mask;
+
+ if (alphabet_index >= alphabet_len) {
+ continue;
+ }
+
+ result_buffer[result_iter] = alphabet[alphabet_index];
+
+ if (result_iter == result_buffer.len - 1) {
+ return result_buffer;
+ } else {
+ result_iter += 1;
+ }
+ }
+
+ return result_buffer;
+}
+
+/// Generates a nanoid using the provided alphabet.
+///
+/// Parameters:
+///
+/// - `rng`: a random number generator.
+/// Provide a secure one such as `std.Random.DefaultCsprng` and seed it properly if you have security concerns.
+/// See `Regarding RNGs` in `README.md` for more information.
+///
+/// - `alphabet`: an array of the bytes that will be used in the id, its length must be in the range `(0, max_alphabet_len]`.
+pub fn generateWithAlphabet(rng: std.Random, alphabet: []const u8) [default_id_len]u8 {
+ var nanoid: [default_id_len]u8 = undefined;
+ var step_buffer: [rng_step_buffer_len_sufficient_for_default_length_ids]u8 = undefined;
+ _ = generateEx(rng, alphabet, &nanoid, &step_buffer);
+ return nanoid;
+}
+
+/// Generates a nanoid using the default alphabet.
+///
+/// Parameters:
+///
+/// - `rng`: a random number generator.
+/// Provide a secure one such as `std.Random.DefaultCsprng` and seed it properly if you have security concerns.
+/// See `Regarding RNGs` in `README.md` for more information.
+pub fn generate(rng: std.Random) [default_id_len]u8 {
+ const result = generateWithAlphabet(rng, alphabets.default);
+ return result;
+}
+
+/// Non public utility functions used mostly in unit tests.
+const internal_utils = struct {
+ fn makeDefaultCsprng() std.Random.DefaultCsprng {
+ // Generate seed
+ var seed: [std.Random.DefaultCsprng.secret_seed_length]u8 = undefined;
+ std.crypto.random.bytes(&seed);
+
+ // Initialize the rng and allocator
+ const rng = std.Random.DefaultCsprng.init(seed);
+ return rng;
+ }
+
+ fn makeDefaultPrngWithConstantSeed() std.Random.DefaultPrng {
+ const rng = std.Random.DefaultPrng.init(0);
+ return rng;
+ }
+
+ fn makeDefaultCsprngWithConstantSeed() std.Random.DefaultCsprng {
+ // Generate seed
+ const seed: [std.Random.DefaultCsprng.secret_seed_length]u8 = undefined;
+ for (seed) |*it| it.* = 'a';
+
+ // Initialize the rng and allocator
+ const rng = std.Random.DefaultCsprng.init(seed);
+ return rng;
+ }
+
+ /// Taken from https://github.com/codeyu/nanoid-net/blob/445f4d363e0079e151ea414dab1a9f9961679e7e/test/Nanoid.Test/NanoidTest.cs#L145
+ fn toBeCloseTo(actual: f64, expected: f64, precision: f64) bool {
+ const pass = @abs(expected - actual) < std.math.pow(f64, 10, -precision) / 2;
+ return pass;
+ }
+
+ /// Checks if all elements in `array` are present in `includedIn`.
+ fn allIn(comptime T: type, array: []const T, includedIn: []const T) bool {
+ for (array) |it| {
+ if (std.mem.indexOfScalar(u8, includedIn, it) == null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+};