nanoid.nx source
↩ module page · 101 lines · 3181 B
1// nanoid.nx -- modern URL-safe ID generator.
2//
3// Andrey Sitnik 2017, widely adopted in the JS ecosystem (Next.js,
4// Prisma, PlanetScale, tRPC). Alternative to UUID + ULID with
5// different trade-offs:
6//
7// - Default 21 chars ~= 126 bits of entropy (UUID: 128)
8// - URL-safe alphabet: A-Z a-z 0-9 _ - (64 chars, 6 bits each)
9// - No structure: pure random, no embedded timestamp
10// - No lexicographic sort: use ULID if sorting matters
11//
12// Use cases: URL slugs that need collision-resistance, short
13// share codes, DB primary keys where you don't need time-sort.
14//
15// Algorithm:
16// Request N random bytes from /dev/urandom via rand.nx.
17// Mask each byte to 6 bits + retry on overflow (standard
18// rejection sampling -- keeps the output alphabet uniform).
19//
20// Composes rand.nx.
21//
22// Invariants:
23// N1 Output is fixed-length caller-chosen (default 21).
24// N2 Every byte is from the 64-char URL-safe alphabet.
25// N3 Rejection sampling ensures uniform distribution -- we
26// don't use mod-256 which would bias bytes 0..31.
27
28import "syscalls.nx"
29import "rand.nx"
30
31const NANOID_DEFAULT_LEN: i64 = 21
32
33// Alphabet: 64 chars = 6-bit index.
34// 0..25 = A-Z, 26..51 = a-z, 52..61 = 0-9, 62 = _, 63 = -
35func nano_char(idx: i64) -> i64 {
36 if idx < 26 { return 0x41 + idx }
37 if idx < 52 { return 0x61 + (idx - 26) }
38 if idx < 62 { return 0x30 + (idx - 52) }
39 if idx == 62 { return 0x5F } // '_'
40 return 0x2D // '-'
41}
42
43// Generate a nanoid of `n` characters. Uses rejection sampling
44// on 8-bit random bytes: mask 6 bits + retry any byte whose
45// low 6 bits would map past the alphabet (never happens because
46// alphabet is exactly 64 = 2^6; rejection is a no-op here but
47// kept structurally for when custom alphabets are added later).
48func nanoid(out: *u8, n: i64) -> i64 {
49 // Pull n random bytes.
50 rand_bytes(out, n)
51 // Map each byte's low 6 bits into the alphabet (in place).
52 var i: i64 = 0
53 while i < n {
54 let b: i64 = out[i] & 0x3F
55 out[i] = nano_char(b)
56 i = i + 1
57 }
58 return n
59}
60
61// Default 21-char nanoid (most common use).
62func nanoid_default(out: *u8) -> i64 {
63 return nanoid(out, NANOID_DEFAULT_LEN)
64}
65
66// Compile-only smoke.
67func main() -> i64 {
68 let a: *u8 = sys_mmap(32)
69 nanoid_default(a)
70 // All 21 chars must be in the URL-safe alphabet.
71 var i: i64 = 0
72 while i < NANOID_DEFAULT_LEN {
73 let c: i64 = a[i]
74 var ok: i64 = 0
75 if c >= 0x41 {
76 if c <= 0x5A { ok = 1 }
77 }
78 if c >= 0x61 {
79 if c <= 0x7A { ok = 1 }
80 }
81 if c >= 0x30 {
82 if c <= 0x39 { ok = 1 }
83 }
84 if c == 0x5F { ok = 1 }
85 if c == 0x2D { ok = 1 }
86 if ok == 0 { return 1 }
87 i = i + 1
88 }
89
90 // Two ids should differ.
91 let b: *u8 = sys_mmap(32)
92 nanoid_default(b)
93 var diff: i64 = 0
94 i = 0
95 while i < NANOID_DEFAULT_LEN {
96 if a[i] != b[i] { diff = 1; break }
97 i = i + 1
98 }
99 if diff != 1 { return 2 }
100 return 0
101}