nx_nanoid.nx source
↩ module page · 107 lines · 3337 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
28// nx_safety_envelope:
29// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
30// sil_target: SIL1
31// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
32// verdict: NOT_YET_EVALUATED
33
34import "nx_syscalls.nx"
35import "nx_rand.nx"
36
37const NANOID_DEFAULT_LEN: i64 = 21
38
39// Alphabet: 64 chars = 6-bit index.
40// 0..25 = A-Z, 26..51 = a-z, 52..61 = 0-9, 62 = _, 63 = -
41func nano_char(idx: i64) -> i64 {
42 if idx < 26 { return 0x41 + idx }
43 if idx < 52 { return 0x61 + (idx - 26) }
44 if idx < 62 { return 0x30 + (idx - 52) }
45 if idx == 62 { return 0x5F } // '_'
46 return 0x2D // '-'
47}
48
49// Generate a nanoid of `n` characters. Uses rejection sampling
50// on 8-bit random bytes: mask 6 bits + retry any byte whose
51// low 6 bits would map past the alphabet (never happens because
52// alphabet is exactly 64 = 2^6; rejection is a no-op here but
53// kept structurally for when custom alphabets are added later).
54func nanoid(out: *u8, n: i64) -> i64 {
55 // Pull n random bytes.
56 rand_bytes(out, n)
57 // Map each byte's low 6 bits into the alphabet (in place).
58 var i: i64 = 0
59 while i < n {
60 let b: i64 = out[i] & 0x3F
61 out[i] = nano_char(b)
62 i = i + 1
63 }
64 return n
65}
66
67// Default 21-char nanoid (most common use).
68func nanoid_default(out: *u8) -> i64 {
69 return nanoid(out, NANOID_DEFAULT_LEN)
70}
71
72// Compile-only smoke.
73func main() -> i64 {
74 let a: *u8 = sys_mmap(32)
75 nanoid_default(a)
76 // All 21 chars must be in the URL-safe alphabet.
77 var i: i64 = 0
78 while i < NANOID_DEFAULT_LEN {
79 let c: i64 = a[i]
80 var ok: i64 = 0
81 if c >= 0x41 {
82 if c <= 0x5A { ok = 1 }
83 }
84 if c >= 0x61 {
85 if c <= 0x7A { ok = 1 }
86 }
87 if c >= 0x30 {
88 if c <= 0x39 { ok = 1 }
89 }
90 if c == 0x5F { ok = 1 }
91 if c == 0x2D { ok = 1 }
92 if ok == 0 { return 1 }
93 i = i + 1
94 }
95
96 // Two ids should differ.
97 let b: *u8 = sys_mmap(32)
98 nanoid_default(b)
99 var diff: i64 = 0
100 i = 0
101 while i < NANOID_DEFAULT_LEN {
102 if a[i] != b[i] { diff = 1; break }
103 i = i + 1
104 }
105 if diff != 1 { return 2 }
106 return 0
107}