├── .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 | [](https://github.com/SasLuca/nanoid-zig/master/LICENSE)
4 | [](https://github.com/SasLuca/zig-nanoid/actions/workflows/cross-build.yml)
5 | 
6 |
7 |
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 |
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 | }
--------------------------------------------------------------------------------