code wiki / (root) / nx_nanoid.nx

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}