├── .gitattributes ├── .github └── workflows │ └── cross-build.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.zig ├── examples ├── custom-alphabet-example.zig └── default-example.zig ├── gyro.zzz ├── logo.svg └── src └── nanoid.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text=auto eol=lf 2 | *.svg binary -------------------------------------------------------------------------------- /.github/workflows/cross-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "*.zig" 8 | - "src/*.zig" 9 | - "examples/*.zig" 10 | - ".github/workflows/cross-build.yml" 11 | pull_request: 12 | branches: [main] 13 | schedule: 14 | - cron: "0 5 * * *" # run at 5 AM UTC 15 | 16 | jobs: 17 | cross-build: 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | platform: [ubuntu-latest, windows-latest, macos-latest] 22 | runs-on: ${{ matrix.platform }} 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Setup Zig 28 | uses: goto-bus-stop/setup-zig@v1 29 | with: 30 | version: master 31 | 32 | - name: Run the test suite and examples 33 | run: | 34 | zig build test 35 | zig build run -Ddefault_example 36 | zig build run -Dcustom_alphabet_example 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Taken from: https://github.com/ziglang/zig/blob/master/.gitignore 2 | 3 | zig-cache/ 4 | zig-out/ 5 | /release/ 6 | /debug/ 7 | /build/ 8 | /build-*/ 9 | /docgen_tmp/ 10 | 11 | # Custom 12 | .vscode/settings.json 13 | .gyro -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run default example", 6 | "type": "cppvsdbg", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/zig-out/bin/nanoid-zig-default-example.exe", 9 | "args": [], 10 | "stopAtEntry": false, 11 | "cwd": "${workspaceFolder}", 12 | "environment": [], 13 | "preLaunchTask": "install default_example", 14 | "logging": { 15 | "exceptions": true, 16 | "moduleLoad": false, 17 | "programOutput": true, 18 | "engineLogging": false, 19 | "trace": false, 20 | "traceResponse": false, 21 | } 22 | }, 23 | { 24 | "name": "Run custom alphabet example", 25 | "type": "cppvsdbg", 26 | "request": "launch", 27 | "program": "${workspaceFolder}/zig-out/bin/nanoid-zig-custom-alphabet-example.exe", 28 | "args": [], 29 | "stopAtEntry": false, 30 | "cwd": "${workspaceFolder}", 31 | "environment": [], 32 | "preLaunchTask": "install custom_alphabet_example", 33 | "logging": { 34 | "exceptions": true, 35 | "moduleLoad": false, 36 | "programOutput": true, 37 | "engineLogging": false, 38 | "trace": false, 39 | "traceResponse": false, 40 | } 41 | }, 42 | { 43 | "name": "Run tests", 44 | "type": "cppvsdbg", 45 | "request": "launch", 46 | "program": "${workspaceFolder}/zig-out/bin/nanoid-zig-test.exe", 47 | "args": [ "zig" ], 48 | "stopAtEntry": false, 49 | "cwd": "${workspaceFolder}", 50 | "environment": [], 51 | "preLaunchTask": "install tests", 52 | "logging": { 53 | "exceptions": true, 54 | "moduleLoad": false, 55 | "programOutput": true, 56 | "engineLogging": false, 57 | "trace": false, 58 | "traceResponse": false, 59 | } 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "install default_example", 6 | "type": "shell", 7 | "command": "zig build install -Ddefault_example", 8 | "group": "build", 9 | "problemMatcher": [ "$gcc" ] 10 | }, 11 | { 12 | "label": "install custom_alphabet_example", 13 | "type": "shell", 14 | "command": "zig build install -Dcustom_alphabet_example", 15 | "group": "build", 16 | "problemMatcher": [ "$gcc" ] 17 | }, 18 | { 19 | "label": "install tests", 20 | "type": "shell", 21 | "command": "zig build install -Dtests", 22 | "group": "build", 23 | "problemMatcher": [ "$gcc" ] 24 | }, 25 | ] 26 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.3 (2022-05-29) Updated build system to work with latest zig master version. 2 | 3 | 1.0.2 (2022-05-02) Refactored API to simplify basic generation procs. 4 | 5 | 1.0.1 (2022-04-18) Finished initial API, docs and tests suite. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Luca Sas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nano ID in Zig 2 | 3 | [![License](https://img.shields.io/badge/license-MIT%20License-blue.svg)](https://github.com/SasLuca/nanoid-zig/master/LICENSE) 4 | [![cross build tests](https://github.com/SasLuca/zig-nanoid/actions/workflows/cross-build.yml/badge.svg)](https://github.com/SasLuca/zig-nanoid/actions/workflows/cross-build.yml) 5 | ![Maintenance intention for this crate](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg) 6 | 7 | Nano ID x Zig logo by Anton Lovchikov, adapted by Luca Sas 8 | 9 | A battle-tested, tiny, secure, URL-friendly, unique string ID generator. Now available in pure Zig. 10 | 11 | * **Freestanding.** zig-nanoid is entirely freestanding. 12 | * **Fast.** The algorithm is very fast and relies just on basic math, speed will mostly depend on your choice of RNG. 13 | * **Safe.** It can use any random generator you want and the library has no errors to handle. 14 | * **Short IDs.** It uses a larger alphabet than UUID (`A-Za-z0-9_-`). So ID length was reduced from 36 to 21 symbols and it is URL friendly. 15 | * **Battle Tested.** Original implementation has over 18 million weekly downloads on [npm](https://www.npmjs.com/package/nanoid). 16 | * **Portable.** Nano ID was ported to [20+ programming languages](https://github.com/ai/nanoid#other-programming-languages). 17 | 18 | ## Example 19 | 20 | Basic usage with `std.crypto.random`: 21 | ```zig 22 | const std = @import("std"); 23 | const nanoid = @import("nanoid"); 24 | 25 | pub fn main() !void 26 | { 27 | const result = nanoid.generate(std.crypto.random); 28 | 29 | std.log.info("Nanoid: {s}", .{result}); 30 | } 31 | ``` 32 | 33 | ## Comparison to UUID 34 | 35 | Nano ID is quite comparable to UUID v4 (random-based). 36 | 37 | It has a similar number of random bits in the ID (126 in Nano ID and 122 in UUID), so it has a similar collision probability. 38 | 39 | It also uses a bigger alphabet, so a similar number of random bits are packed in just 21 symbols instead of 36. 40 | 41 | For there to be a one in a billion chance of duplication, 103 trillion version 4 IDs must be generated. 42 | 43 | ## How to use 44 | 45 | ### Generating an id with the default size 46 | 47 | The simplest way to generate an id with the default alphabet and length is by using the function `generate` like so: 48 | 49 | ```zig 50 | const result = nanoid.generate(std.crypto.random); 51 | ``` 52 | 53 | If you want a custom alphabet you can use `generateWithAlphabet` and pass either a custom alphabet or one from `nanoid.alphabets`: 54 | ```zig 55 | const result = nanoid.generateWithAlphabet(std.crypto.random, nanoid.alphabets.numbers); // This id will only contain numbers 56 | ``` 57 | 58 | You can find a variety of other useful alphabets inside of `nanoid.alphabets`. 59 | 60 | The result is an array of size `default_id_len` which happens to be 21 which is returned by value. 61 | 62 | There are no errors to handle, assuming your rng object is valid everything will work. 63 | The default alphabet includes the symbols "-_", numbers and English lowercase and uppercase letters. 64 | 65 | ### Generating an id with a custom size 66 | 67 | If you want a custom alphabet and length use `generateEx` or `generateExWithIterativeRng`. 68 | 69 | The function `generateEx` takes an rng, an `alphabet`, a `result_buffer` that it will write the id to, and a `step_buffer`. 70 | The `step_buffer` is used by the algorithm to store a random bytes so it has to do less calls to the rng and `step_buffer.len` must be at 71 | least `computeRngStepBufferLength(computeMask(@truncate(u8, alphabet.len)), result_buffer.len, alphabet.len)`. 72 | 73 | The function `generateExWithIterativeRng` is the same as `generateEx` except it doesn't need a `step_buffer`. It will use `Random.int(u8)` 74 | instead of `Random.bytes()` to get a random byte at a time thus avoiding the need for a rng step buffer. Normally this will be slower but 75 | depending on your rng algorithm or other requirements it might not be, so the option is there in case you need but normally it is 76 | recommended you use `generateEx` which requires a temporary buffer that will be filled using `Random.bytes()` in order to get the best 77 | performance. 78 | 79 | Additionally you can precompute a sufficient length for the `step_buffer` and pre-allocate it as an optimization using 80 | `computeSufficientRngStepBufferLengthFor` which simply asks for the largest possible id length you want to generate. 81 | 82 | If you intend to use the `default_id_len`, you can use the constant `nanoid.rng_step_buffer_len_sufficient_for_default_length_ids`. 83 | 84 | ### Regarding RNGs 85 | 86 | You will need to provide an random number generator (rng) yourself. You can use the zig standard library ones, either `std.rand.DefaultPrng` 87 | or if you have stricter security requirements use `std.rand.DefaultCsprng` or `std.crypto.random`. 88 | 89 | When you initialize them you need to provide a seed, providing the same one every time will result in the same ids being generated every 90 | time you run the program, except for `std.crypto.random`. 91 | 92 | If you want a good secure seed you can generate one using `std.crypto.random.bytes`. 93 | 94 | Here is an example of how you would initialize and seed `std.rand.DefaultCsprng` and use it: 95 | 96 | ```zig 97 | // Generate seed 98 | var seed: [std.rand.DefaultCsprng.secret_seed_length]u8 = undefined; 99 | std.crypto.random.bytes(&seed); 100 | 101 | // Initialize the rng and allocator 102 | var rng = std.rand.DefaultCsprng.init(seed); 103 | 104 | // Generate id 105 | var id = nanoid.generate(rng.random()); 106 | ``` 107 | 108 | ## Add zig-nanoid to your project 109 | 110 | ### Manually 111 | 112 | To add the library as a package to your zig project: 113 | 1. Download the repo and put it in a folder (eg: `thirdparty`) in your project. 114 | 2. Import the library's `build.zig` in your build script (eg: `const nanoid = @import("thirdparty/nanoid-zig/build.zig");`) 115 | 3. Add the library as a package to your steps (eg: `exe.addPackage(nanoid.getPackage("nanoid"));`) 116 | 117 | Full example: 118 | ```zig 119 | // build.zig 120 | const std = @import("std"); 121 | const nanoid = @import("thirdparty/zig-nanoid/build.zig"); 122 | 123 | pub fn build(b: *std.build.Builder) void 124 | { 125 | const target = b.standardTargetOptions(.{}); 126 | const mode = b.standardReleaseOptions(); 127 | 128 | const exe = b.addExecutable("zig-nanoid-test", "src/main.zig"); 129 | exe.setTarget(target); 130 | exe.setBuildMode(mode); 131 | exe.addPackage(nanoid.getPackage("nanoid")); 132 | exe.install(); 133 | } 134 | ``` 135 | 136 | ### Using the gyro package manager 137 | 138 | We support the zig [gyro package manager](https://github.com/mattnite/gyro). 139 | Here is how to use it: 140 | 141 | 1. From your terminal initialize a gyro project and add the package `SasLuca/nanoid`. 142 | ``` 143 | gyro init 144 | gyro add SasLuca/nanoid 145 | ``` 146 | 147 | 2. In your `build.zig` do an import like so `const pkgs = @import("deps.zig").pkgs;` and call `pkgs.addAllTo(exe);` to add all libraries to your executable (or some other target). 148 | 149 | 3. Import `const nanoid = @import("nanoid");` in your `main.zig` and use it. 150 | 151 | 4. Invoke `gyro build run` which will generate `deps.zig` and other files as well as building and running your project. 152 | 153 | ## Useful links 154 | 155 | - Original implementation: https://github.com/ai/nanoid 156 | 157 | - Online Tool: https://zelark.github.io/nano-id-cc/ 158 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.build.Builder) void 4 | { 5 | const target = b.standardTargetOptions(.{}); 6 | const mode = b.standardReleaseOptions(); 7 | 8 | const test_step = b.step("test", "Run unit tests directly."); 9 | const run_step = b.step("run", "Run example."); 10 | 11 | const default_example = b.option(bool, "default_example", "A simple example of using nanoid.") orelse false; 12 | const custom_alphabet_example = b.option(bool, "custom_alphabet_example", "A simple example of using nanoid.") orelse false; 13 | const tests = b.option(bool, "tests", "The unit tests of the library.") orelse false; 14 | 15 | if (default_example) 16 | { 17 | const exe = b.addExecutable("nanoid-zig-default-example", "examples/default-example.zig"); 18 | exe.setTarget(target); 19 | exe.setBuildMode(mode); 20 | exe.addPackage(getPackage("nanoid")); 21 | exe.install(); 22 | 23 | const exe_run = exe.run(); 24 | exe_run.step.dependOn(b.getInstallStep()); 25 | run_step.dependOn(&exe_run.step); 26 | } 27 | 28 | if (custom_alphabet_example) 29 | { 30 | const exe = b.addExecutable("nanoid-zig-custom-alphabet-example", "examples/custom-alphabet-example.zig"); 31 | exe.setTarget(target); 32 | exe.setBuildMode(mode); 33 | exe.addPackage(getPackage("nanoid")); 34 | exe.install(); 35 | 36 | const exe_run = exe.run(); 37 | exe_run.step.dependOn(b.getInstallStep()); 38 | run_step.dependOn(&exe_run.step); 39 | } 40 | 41 | if (tests) 42 | { 43 | const exe = b.addTestExe("nanoid-zig-test", "src/nanoid.zig"); 44 | exe.setTarget(target); 45 | exe.setBuildMode(mode); 46 | exe.install(); 47 | 48 | const exe_run = exe.run(); 49 | exe_run.step.dependOn(b.getInstallStep()); 50 | run_step.dependOn(&exe_run.step); 51 | } 52 | 53 | // Test runner 54 | const test_runner = b.addTest("src/nanoid.zig"); 55 | test_runner.setBuildMode(mode); 56 | test_runner.setTarget(target); 57 | test_step.dependOn(&test_runner.step); 58 | } 59 | 60 | pub fn getPackage(name: []const u8) std.build.Pkg 61 | { 62 | return std.build.Pkg{ 63 | .name = name, 64 | .source = .{ .path = comptime std.fs.path.dirname(@src().file).? ++ "/src/nanoid.zig" }, 65 | .dependencies = null, // null by default, but can be set to a slice of `std.build.Pkg`s that your package depends on. 66 | }; 67 | } -------------------------------------------------------------------------------- /examples/custom-alphabet-example.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const nanoid = @import("nanoid"); 3 | 4 | pub fn main() !void 5 | { 6 | const result = nanoid.generateWithAlphabet(std.crypto.random, nanoid.alphabets.numbers); 7 | 8 | std.log.info("Nanoid: {s}", .{result}); 9 | } -------------------------------------------------------------------------------- /examples/default-example.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const nanoid = @import("nanoid"); 3 | 4 | pub fn main() !void 5 | { 6 | const result = nanoid.generate(std.crypto.random); 7 | 8 | std.log.info("Nanoid: {s}", .{result}); 9 | } -------------------------------------------------------------------------------- /gyro.zzz: -------------------------------------------------------------------------------- 1 | pkgs: 2 | nanoid: 3 | version: 1.0.4 4 | description: "A battle-tested, tiny, secure, URL-friendly, unique string ID generator. Now available in pure Zig." 5 | license: MIT 6 | source_url: "https://github.com/SasLuca/zig-nanoid" 7 | tags: 8 | url 9 | uuid 10 | random 11 | id 12 | unique-id 13 | unique-identifier 14 | uuid-generator 15 | unique-id 16 | nanoid 17 | root: src/nanoid.zig 18 | files: 19 | README.md 20 | CHANGELOG.md 21 | LICENSE -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/nanoid.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// A collection of useful alphabets that can be used to generate ids. 4 | pub const alphabets = struct 5 | { 6 | /// Numbers from 0 to 9. 7 | pub const numbers = "0123456789"; 8 | 9 | /// English hexadecimal with lowercase characters. 10 | pub const hexadecimal_lowercase = numbers ++ "abcdef"; 11 | 12 | /// English hexadecimal with uppercase characters. 13 | pub const hexadecimal_uppercase = numbers ++ "ABCDEF"; 14 | 15 | /// Lowercase English letters. 16 | pub const lowercase = "abcdefghijklmnopqrstuvwxyz"; 17 | 18 | /// Uppercase English letters. 19 | pub const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 20 | 21 | /// Numbers and english letters without lookalikes: 1, l, I, 0, O, o, u, v, 5, S, s, 2, Z. 22 | pub const no_look_alikes = "346789ABCDEFGHJKLMNPQRTUVWXYabcdefghijkmnpqrtwxyz"; 23 | 24 | /// Same as nolookalikes but with removed vowels and following letters: 3, 4, x, X, V. 25 | /// This list should protect you from accidentally getting obscene words in generated strings. 26 | pub const no_look_alikes_safe = "6789BCDFGHJKLMNPQRTWbcdfghjkmnpqrtwz"; 27 | 28 | /// Combination of all the lowercase, uppercase characters and numbers from 0 to 9. 29 | /// Does not include any symbols or special characters. 30 | pub const alphanumeric = numbers ++ lowercase ++ uppercase; 31 | 32 | /// URL friendly characters used by the default generate procedure. 33 | pub const default = "_-" ++ alphanumeric; 34 | }; 35 | 36 | /// An array of all the alphabets. 37 | pub const all_alphabets = internal_utils.collectAllConstantsInStruct(alphabets, []const u8); 38 | 39 | /// The default length for nanoids. 40 | pub const default_id_len = 21; 41 | 42 | /// The mask for the default alphabet length. 43 | pub const default_mask = computeMask(alphabets.default.len); 44 | 45 | /// This should be enough memory for an rng step buffer when generating an id of default length regardless of alphabet length. 46 | /// It can be used for allocating your rng step buffer if you know the length of your id is `<= default_id_len`. 47 | pub const rng_step_buffer_len_sufficient_for_default_length_ids = computeSufficientRngStepBufferLengthFor(default_id_len); 48 | 49 | /// The maximum length of the alphabet accepted by the nanoid algorithm. 50 | pub const max_alphabet_len: u8 = std.math.maxInt(u8); 51 | 52 | /// Computes the mask necessary for the nanoid algorithm given an alphabet length. 53 | /// The mask is used to transform a random byte into an index into an array of length `alphabet_len`. 54 | /// 55 | /// Parameters: 56 | /// - `alphabet_len`: the length of the alphabet used. The alphabet length must be in the range `(0, max_alphabet_len]`. 57 | pub fn computeMask(alphabet_len: u8) u8 58 | { 59 | std.debug.assert(alphabet_len > 0); 60 | 61 | const clz: u5 = @clz(@as(u31, (alphabet_len - 1) | 1)); 62 | const mask = (@as(u32, 2) << (31 - clz)) - 1; 63 | const result = @truncate(u8, mask); 64 | return result; 65 | } 66 | 67 | /// Computes the length necessary for a buffer which can hold the random byte in a step of a the nanoid generation algorithm given a 68 | /// certain alphabet length. 69 | /// 70 | /// Parameters: 71 | /// - `id_len`: the length of the id you will generate. Can be any value. 72 | /// 73 | /// - `alphabet_len`: the length of the alphabet used. The alphabet length must be in the range `(0, max_alphabet_len]`. 74 | pub fn computeRngStepBufferLength(id_len: usize, alphabet_len: u8) usize 75 | { 76 | // @Note: 77 | // Original dev notes regarding this algorithm. 78 | // Source: https://github.com/ai/nanoid/blob/0454333dee4612d2c2e163d271af6cc3ce1e5aa4/index.js#L45 79 | // 80 | // "Next, a step determines how many random bytes to generate. 81 | // The number of random bytes gets decided upon the ID length, mask, 82 | // alphabet length, and magic number 1.6 (using 1.6 peaks at performance 83 | // according to benchmarks)." 84 | const mask_f = @intToFloat(f64, computeMask(alphabet_len)); 85 | const id_len_f = @intToFloat(f64, id_len); 86 | const alphabet_size_f = @intToFloat(f64, alphabet_len); 87 | const step_buffer_len = @ceil(1.6 * mask_f * id_len_f / alphabet_size_f); 88 | const result = @floatToInt(usize, step_buffer_len); 89 | 90 | return result; 91 | } 92 | 93 | /// This function computes the biggest possible rng step buffer length necessary 94 | /// to compute an id with a max length of `max_id_len` regardless of the alphabet length. 95 | /// 96 | /// Parameters: 97 | /// - `max_id_len`: The biggest id length for which the step buffer length needs to be sufficient. 98 | pub fn computeSufficientRngStepBufferLengthFor(max_id_len: usize) usize 99 | { 100 | @setEvalBranchQuota(2500); 101 | var max_step_buffer_len: usize = 0; 102 | var i: u9 = 1; 103 | while (i <= max_alphabet_len) : (i += 1) 104 | { 105 | const alphabet_len = @truncate(u8, i); 106 | const step_buffer_len = computeRngStepBufferLength(max_id_len, alphabet_len); 107 | 108 | if (step_buffer_len > max_step_buffer_len) 109 | { 110 | max_step_buffer_len = step_buffer_len; 111 | } 112 | } 113 | 114 | return max_step_buffer_len; 115 | } 116 | 117 | /// Generates a nanoid inside `result_buffer` and returns it back to the caller. 118 | /// 119 | /// Parameters: 120 | /// - `rng`: a random number generator. 121 | /// Provide a secure one such as `std.rand.DefaultCsprng` and seed it properly if you have security concerns. 122 | /// See `Regarding RNGs` in `readme.md` for more information. 123 | /// 124 | /// - `alphabet`: an array of the bytes that will be used in the id, its length must be in the range `(0, max_alphabet_len]`. 125 | /// Consider the options from `nanoid.alphabets`. 126 | /// 127 | /// - `result_buffer`: is an output buffer that will be filled *completely* with random bytes from `alphabet`, thus generating an id of 128 | /// length `result_buffer.len`. This buffer will be returned at the end of the function. 129 | /// 130 | /// - `step_buffer`: The buffer will be filled with random bytes using `rng.bytes()`. 131 | /// Must be at least `computeRngStepBufferLength(computeMask(@truncate(u8, alphabet.len)), result_buffer.len, alphabet.len)` bytes. 132 | pub fn generateEx(rng: std.rand.Random, alphabet: []const u8, result_buffer: []u8, step_buffer: []u8) []u8 133 | { 134 | std.debug.assert(alphabet.len > 0 and alphabet.len <= max_alphabet_len); 135 | 136 | const alphabet_len = @truncate(u8, alphabet.len); 137 | const mask = computeMask(alphabet_len); 138 | const necessary_step_buffer_len = computeRngStepBufferLength(result_buffer.len, alphabet_len); 139 | const actual_step_buffer = step_buffer[0..necessary_step_buffer_len]; 140 | 141 | var result_iter: usize = 0; 142 | while (true) 143 | { 144 | rng.bytes(actual_step_buffer); 145 | 146 | for (actual_step_buffer) |it| 147 | { 148 | const alphabet_index = it & mask; 149 | 150 | if (alphabet_index >= alphabet_len) 151 | { 152 | continue; 153 | } 154 | 155 | result_buffer[result_iter] = alphabet[alphabet_index]; 156 | 157 | if (result_iter == result_buffer.len - 1) 158 | { 159 | return result_buffer; 160 | } 161 | else 162 | { 163 | result_iter += 1; 164 | } 165 | } 166 | } 167 | } 168 | 169 | /// Generates a nanoid inside `result_buffer` and returns it back to the caller. 170 | /// 171 | /// This function will use `rng.int` instead of `rng.bytes` thus avoiding the need for a step buffer. 172 | /// Depending on your choice of rng this can be useful, since you avoid the need for a step buffer, 173 | /// but repeated calls to `rng.int` might be slower than a single call `rng.bytes`. 174 | /// 175 | /// Parameters: 176 | /// - `rng`: a random number generator. 177 | /// Provide a secure one such as `std.rand.DefaultCsprng` and seed it properly if you have security concerns. 178 | /// See `Regarding RNGs` in `readme.md` for more information. 179 | /// 180 | /// - `alphabet`: an array of the bytes that will be used in the id, its length must be in the range `(0, max_alphabet_len]`. 181 | /// Consider the options from `nanoid.alphabets`. 182 | /// 183 | /// - `result_buffer` is an output buffer that will be filled *completely* with random bytes from `alphabet`, thus generating an id of 184 | /// length `result_buffer.len`. This buffer will be returned at the end of the function. 185 | pub fn generateExWithIterativeRng(rng: std.rand.Random, alphabet: []const u8, result_buffer: []u8) []u8 186 | { 187 | std.debug.assert(result_buffer.len > 0); 188 | std.debug.assert(alphabet.len > 0 and alphabet.len <= max_alphabet_len); 189 | 190 | const alphabet_len = @truncate(u8, alphabet.len); 191 | const mask = computeMask(alphabet_len); 192 | 193 | var result_iter: usize = 0; 194 | while (true) 195 | { 196 | const random_byte = rng.int(u8); 197 | 198 | const alphabet_index = random_byte & mask; 199 | 200 | if (alphabet_index >= alphabet_len) 201 | { 202 | continue; 203 | } 204 | 205 | result_buffer[result_iter] = alphabet[alphabet_index]; 206 | 207 | if (result_iter == result_buffer.len - 1) 208 | { 209 | return result_buffer; 210 | } 211 | else 212 | { 213 | result_iter += 1; 214 | } 215 | } 216 | 217 | return result_buffer; 218 | } 219 | 220 | /// Generates a nanoid using the provided alphabet. 221 | /// 222 | /// Parameters: 223 | /// 224 | /// - `rng`: a random number generator. 225 | /// Provide a secure one such as `std.rand.DefaultCsprng` and seed it properly if you have security concerns. 226 | /// See `Regarding RNGs` in `README.md` for more information. 227 | /// 228 | /// - `alphabet`: an array of the bytes that will be used in the id, its length must be in the range `(0, max_alphabet_len]`. 229 | pub fn generateWithAlphabet(rng: std.rand.Random, alphabet: []const u8) [default_id_len]u8 230 | { 231 | var nanoid: [default_id_len]u8 = undefined; 232 | var step_buffer: [rng_step_buffer_len_sufficient_for_default_length_ids]u8 = undefined; 233 | _ = generateEx(rng, alphabet, &nanoid, &step_buffer); 234 | return nanoid; 235 | } 236 | 237 | /// Generates a nanoid using the default alphabet. 238 | /// 239 | /// Parameters: 240 | /// 241 | /// - `rng`: a random number generator. 242 | /// Provide a secure one such as `std.rand.DefaultCsprng` and seed it properly if you have security concerns. 243 | /// See `Regarding RNGs` in `README.md` for more information. 244 | pub fn generate(rng: std.rand.Random) [default_id_len]u8 245 | { 246 | const result = generateWithAlphabet(rng, alphabets.default); 247 | return result; 248 | } 249 | 250 | /// Non public utility functions used mostly in unit tests. 251 | const internal_utils = struct 252 | { 253 | fn makeDefaultCsprng() std.rand.DefaultCsprng 254 | { 255 | // Generate seed 256 | var seed: [std.rand.DefaultCsprng.secret_seed_length]u8 = undefined; 257 | std.crypto.random.bytes(&seed); 258 | 259 | // Initialize the rng and allocator 260 | var rng = std.rand.DefaultCsprng.init(seed); 261 | return rng; 262 | } 263 | 264 | fn makeDefaultPrngWithConstantSeed() std.rand.DefaultPrng 265 | { 266 | var rng = std.rand.DefaultPrng.init(0); 267 | return rng; 268 | } 269 | 270 | fn makeDefaultCsprngWithConstantSeed() std.rand.DefaultCsprng 271 | { 272 | // Generate seed 273 | var seed: [std.rand.DefaultCsprng.secret_seed_length]u8 = undefined; 274 | for (seed) |*it| it.* = 'a'; 275 | 276 | // Initialize the rng and allocator 277 | var rng = std.rand.DefaultCsprng.init(seed); 278 | return rng; 279 | } 280 | 281 | /// Taken from https://github.com/codeyu/nanoid-net/blob/445f4d363e0079e151ea414dab1a9f9961679e7e/test/Nanoid.Test/NanoidTest.cs#L145 282 | fn toBeCloseTo(actual: f64, expected: f64, precision: f64) bool 283 | { 284 | const pass = @fabs(expected - actual) < std.math.pow(f64, 10, -precision) / 2; 285 | return pass; 286 | } 287 | 288 | /// Checks if all elements in `array` are present in `includedIn`. 289 | fn allIn(comptime T: type, array: []const T, includedIn: []const T) bool 290 | { 291 | for (array) |it| 292 | { 293 | if (std.mem.indexOfScalar(u8, includedIn, it) == null) 294 | { 295 | return false; 296 | } 297 | } 298 | 299 | return true; 300 | } 301 | 302 | /// Returns an array with all the public constants from a struct. 303 | fn collectAllConstantsInStruct(comptime namespace: type, comptime T: type) []const T 304 | { 305 | var result: []const T = &.{}; 306 | for (@typeInfo(namespace).Struct.decls) |decl| 307 | { 308 | if (!decl.is_pub) continue; 309 | 310 | const value = @field(namespace, decl.name); 311 | 312 | if (@TypeOf(value) == T) 313 | { 314 | result = result ++ [_]T{ value }; 315 | } 316 | } 317 | return result; 318 | } 319 | }; 320 | 321 | test "calling computeMask with all acceptable input" 322 | { 323 | var i: u9 = 1; 324 | while (i <= max_alphabet_len) : (i += 1) 325 | { 326 | const alphabet_len = @truncate(u8, i); 327 | const mask = computeMask(alphabet_len); 328 | try std.testing.expect(mask > 0); 329 | } 330 | } 331 | 332 | test "calling computeRngStepBufferLength with all acceptable alphabet sizes and default id length" 333 | { 334 | var i: u9 = 1; 335 | while (i <= max_alphabet_len) : (i += 1) 336 | { 337 | const alphabet_len = @truncate(u8, i); 338 | const rng_step_size = computeRngStepBufferLength(default_id_len, alphabet_len); 339 | try std.testing.expect(rng_step_size > 0); 340 | } 341 | } 342 | 343 | test "generating an id with default settings" 344 | { 345 | // Init rng 346 | var rng = internal_utils.makeDefaultCsprng(); 347 | 348 | // Generate a nanoid 349 | const result = generate(rng.random()); 350 | try std.testing.expect(internal_utils.allIn(u8, &result, alphabets.default)); 351 | } 352 | 353 | test "generating an id with a custom length" 354 | { 355 | // Init rng 356 | var rng = internal_utils.makeDefaultCsprng(); 357 | 358 | // Generate a nanoid 359 | const custom_id_len = 10; 360 | const rng_step_size = comptime computeRngStepBufferLength(custom_id_len, alphabets.default.len); 361 | 362 | var result_buffer: [custom_id_len]u8 = undefined; 363 | var step_buffer: [rng_step_size]u8 = undefined; 364 | 365 | const result = generateEx(rng.random(), alphabets.default, &result_buffer, &step_buffer); 366 | 367 | try std.testing.expect(result.len == custom_id_len); 368 | } 369 | 370 | test "generating an id with a custom alphabet" 371 | { 372 | // Initialize the rng 373 | var rng = internal_utils.makeDefaultCsprng(); 374 | 375 | // Generate a nanoid 376 | const custom_alphabet = "1234abcd"; 377 | const result = generateWithAlphabet(rng.random(), custom_alphabet); 378 | 379 | try std.testing.expect(internal_utils.allIn(u8, &result, custom_alphabet)); 380 | } 381 | 382 | test "generating an id for all alphabets" 383 | { 384 | var rng = internal_utils.makeDefaultCsprng(); 385 | 386 | for (all_alphabets) |alphabet| 387 | { 388 | const result = generateWithAlphabet(rng.random(), alphabet); 389 | 390 | try std.testing.expect(internal_utils.allIn(u8, &result, alphabet)); 391 | } 392 | } 393 | 394 | test "generating an id with a custom alphabet and length" 395 | { 396 | // Initialize the rng 397 | var rng = internal_utils.makeDefaultCsprng(); 398 | 399 | // Generate a nanoid 400 | const custom_alphabet = "1234abcd"; 401 | const custom_id_len = 7; 402 | var result_buffer: [custom_id_len]u8 = undefined; 403 | var step_buffer: [computeSufficientRngStepBufferLengthFor(custom_id_len)]u8 = undefined; 404 | const result = generateEx(rng.random(), custom_alphabet, &result_buffer, &step_buffer); 405 | 406 | try std.testing.expect(result.len == custom_id_len); 407 | 408 | for (result) |it| 409 | { 410 | try std.testing.expect(std.mem.indexOfScalar(u8, custom_alphabet, it) != null); 411 | } 412 | } 413 | 414 | test "generating an id with a single letter alphabet" 415 | { 416 | // Initialize the rng and allocator 417 | var rng = internal_utils.makeDefaultCsprng(); 418 | 419 | // Generate a nanoid 420 | const custom_alphabet = "a"; 421 | const custom_id_len = 5; 422 | var result_buffer: [custom_id_len]u8 = undefined; 423 | var step_buffer: [computeSufficientRngStepBufferLengthFor(custom_id_len)]u8 = undefined; 424 | const result = generateEx(rng.random(), custom_alphabet, &result_buffer, &step_buffer); 425 | 426 | try std.testing.expect(std.mem.eql(u8, "aaaaa", result)); 427 | } 428 | 429 | test "flat distribution of generated ids" 430 | { 431 | // Initialize the rng and allocator 432 | var rng = internal_utils.makeDefaultCsprng(); 433 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 434 | defer _ = gpa.deinit(); 435 | 436 | // Generate a nanoid 437 | const number_of_ids_to_generate = 100 * 1000; 438 | 439 | var characters_counts = std.AutoArrayHashMap(u8, usize).init(gpa.allocator()); 440 | defer characters_counts.deinit(); 441 | 442 | // Generate ids 443 | var i: usize = 0; 444 | while (i < number_of_ids_to_generate) : (i += 1) 445 | { 446 | const id = generate(rng.random()); 447 | 448 | // Count the occurence of every character across all generated ids 449 | for (id) |char| 450 | { 451 | var char_count = characters_counts.getPtr(char); 452 | if (char_count) |c| 453 | { 454 | c.* += 1; 455 | } 456 | else 457 | { 458 | try characters_counts.put(char, 0); 459 | } 460 | } 461 | } 462 | 463 | for (characters_counts.values()) |value| 464 | { 465 | const value_f = @intToFloat(f64, value); 466 | const alphabet_len_f = @intToFloat(f64, alphabets.default.len); 467 | const count_f = @intToFloat(f64, number_of_ids_to_generate); 468 | const id_len_f = @intToFloat(f64, default_id_len); 469 | const distribution = value_f * alphabet_len_f / (count_f * id_len_f); 470 | try std.testing.expect(internal_utils.toBeCloseTo(distribution, 1, 1)); 471 | } 472 | } 473 | 474 | test "flat distribution of generated ids with the iterative method" 475 | { 476 | // Initialize the rng and allocator 477 | var rng = internal_utils.makeDefaultCsprng(); 478 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 479 | defer _ = gpa.deinit(); 480 | 481 | // Generate a nanoid 482 | const number_of_ids_to_generate = 100 * 1000; 483 | 484 | var characters_counts = std.AutoArrayHashMap(u8, usize).init(gpa.allocator()); 485 | defer characters_counts.deinit(); 486 | 487 | // Generate ids 488 | var i: usize = 0; 489 | while (i < number_of_ids_to_generate) : (i += 1) 490 | { 491 | var id_buffer: [default_id_len]u8 = undefined; 492 | const id = generateExWithIterativeRng(rng.random(), alphabets.default, &id_buffer); 493 | 494 | // Count the occurence of every character across all generated ids 495 | for (id) |char| 496 | { 497 | var char_count = characters_counts.getPtr(char); 498 | if (char_count) |c| 499 | { 500 | c.* += 1; 501 | } 502 | else 503 | { 504 | try characters_counts.put(char, 0); 505 | } 506 | } 507 | } 508 | 509 | for (characters_counts.values()) |value| 510 | { 511 | const value_f = @intToFloat(f64, value); 512 | const alphabet_len_f = @intToFloat(f64, alphabets.default.len); 513 | const count_f = @intToFloat(f64, number_of_ids_to_generate); 514 | const id_len_f = @intToFloat(f64, default_id_len); 515 | const distribution = value_f * alphabet_len_f / (count_f * id_len_f); 516 | try std.testing.expect(internal_utils.toBeCloseTo(distribution, 1, 1)); 517 | } 518 | } --------------------------------------------------------------------------------