code wiki / (root) / crockford32.nx

crockford32.nx source

↩ module page · 197 lines · 6567 B

1// crockford32.nx -- Crockford Base32 encoding. 2// 3// Douglas Crockford 2002: a base32 variant designed for human 4// transcription. Removes I, L, O, U (visually similar to 1, 1, 5// 0, V respectively) from the alphabet. Accepts mixed case + 6// hyphens on decode (so \"4CQ-5M4D\" and \"4cq5m4d\" both decode to 7// the same bytes). 8// 9// Alphabet: 0123456789ABCDEFGHJKMNPQRSTVWXYZ 10// 0 1 11// 0123456789012345678901234567890 1 12// 13// Used by: 14// - ULID (Universal Unique Lexicographically-sortable Identifier) 15// -- 26-char ID, Crockford-32 over 128 bits 16// - License keys, short-URL tokens, password reset tokens 17// - Voicemail PINs, serial numbers for hardware 18// 19// Not RFC-standardised (RFC 4648 base32 is similar but uses a 20// different alphabet and doesn't allow hyphens/mixed case). We 21// ship both: base32.nx (RFC 4648) and this module (Crockford). 22// 23// Invariants: 24// C1 Encoder output is upper-case only, no padding. 25// C2 Decoder accepts mixed case + '-' between groups. 26// C3 Round-trip exact: decode(encode(x)) == x. 27 28import "syscalls.nx" 29 30const C32_ERR_SHORT: i64 = -1 31const C32_ERR_CHAR: i64 = -2 32 33// Encode 5 bits (0..31) as one Crockford ASCII char. 34func c32_enc_char(n: i64) -> i64 { 35 if n < 10 { return 0x30 + n } // '0'..'9' 36 let m: i64 = n - 10 37 // 0..9 map to A..J (skipping I at index 8) 38 // Actually Crockford: 10='A', 11='B', 12='C', 13='D', 14='E', 39 // 15='F', 16='G', 17='H', 18='J', 19='K', 40 // 20='M', 21='N', 22='P', 23='Q', 24='R', 41 // 25='S', 26='T', 27='V', 28='W', 29='X', 42 // 30='Y', 31='Z'. 43 if m < 8 { return 0x41 + m } // A..H for 10..17 44 if m == 8 { return 0x4A } // 18 -> J (skip I) 45 if m == 9 { return 0x4B } // 19 -> K 46 if m == 10 { return 0x4D } // 20 -> M (skip L) 47 if m == 11 { return 0x4E } // 21 -> N 48 if m == 12 { return 0x50 } // 22 -> P (skip O) 49 if m == 13 { return 0x51 } // 23 -> Q 50 if m == 14 { return 0x52 } // 24 -> R 51 if m == 15 { return 0x53 } // 25 -> S 52 if m == 16 { return 0x54 } // 26 -> T 53 if m == 17 { return 0x56 } // 27 -> V (skip U) 54 if m == 18 { return 0x57 } // 28 -> W 55 if m == 19 { return 0x58 } // 29 -> X 56 if m == 20 { return 0x59 } // 30 -> Y 57 return 0x5A // 31 -> Z 58} 59 60// Decode one Crockford char. Returns 0..31 or C32_ERR_CHAR. 61// Accepts both cases + lenient I/L/O mapping to 1/1/0 per 62// Crockford spec ยง3 \"friendly decoding\". 63func c32_dec_char(c: i64) -> i64 { 64 // Uppercase the letter half. 65 var u: i64 = c 66 if u >= 0x61 { 67 if u <= 0x7A { u = u - 0x20 } 68 } 69 // Digits 0..9. 70 if u >= 0x30 { 71 if u <= 0x39 { return u - 0x30 } 72 } 73 // Skip hyphens (just error, caller pre-strips them). 74 if u == 0x49 { return 1 } // I -> 1 75 if u == 0x4C { return 1 } // L -> 1 76 if u == 0x4F { return 0 } // O -> 0 77 78 if u == 0x41 { return 10 } // A 79 if u == 0x42 { return 11 } 80 if u == 0x43 { return 12 } 81 if u == 0x44 { return 13 } 82 if u == 0x45 { return 14 } 83 if u == 0x46 { return 15 } 84 if u == 0x47 { return 16 } 85 if u == 0x48 { return 17 } 86 if u == 0x4A { return 18 } 87 if u == 0x4B { return 19 } 88 if u == 0x4D { return 20 } 89 if u == 0x4E { return 21 } 90 if u == 0x50 { return 22 } 91 if u == 0x51 { return 23 } 92 if u == 0x52 { return 24 } 93 if u == 0x53 { return 25 } 94 if u == 0x54 { return 26 } 95 if u == 0x56 { return 27 } 96 if u == 0x57 { return 28 } 97 if u == 0x58 { return 29 } 98 if u == 0x59 { return 30 } 99 if u == 0x5A { return 31 } 100 return C32_ERR_CHAR 101} 102 103// Encode `n` bytes of `in_bytes` as Crockford base32. Output 104// length = ceil(n * 8 / 5). No padding. 105func c32_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 { 106 var buf: i64 = 0 107 var bits: i64 = 0 108 var out_pos: i64 = 0 109 var i: i64 = 0 110 while i < n { 111 buf = (buf << 8) | in_bytes[i] 112 bits = bits + 8 113 while bits >= 5 { 114 bits = bits - 5 115 let chunk: i64 = (buf >> bits) & 0x1F 116 out[out_pos] = c32_enc_char(chunk) 117 out_pos = out_pos + 1 118 } 119 i = i + 1 120 } 121 if bits > 0 { 122 let chunk: i64 = (buf << (5 - bits)) & 0x1F 123 out[out_pos] = c32_enc_char(chunk) 124 out_pos = out_pos + 1 125 } 126 return out_pos 127} 128 129// Decode. Skips '-' and whitespace. Returns bytes written or 130// negative. 131func c32_decode(chars: *u8, n: i64, out: *u8) -> i64 { 132 var buf: i64 = 0 133 var bits: i64 = 0 134 var out_pos: i64 = 0 135 var i: i64 = 0 136 while i < n { 137 let c: i64 = chars[i] 138 if c == 0x2D { 139 i = i + 1 140 continue 141 } 142 if c == 0x20 { 143 i = i + 1 144 continue 145 } 146 let v: i64 = c32_dec_char(c) 147 if v < 0 { return v } 148 buf = (buf << 5) | v 149 bits = bits + 5 150 if bits >= 8 { 151 bits = bits - 8 152 out[out_pos] = (buf >> bits) & 0xFF 153 out_pos = out_pos + 1 154 } 155 i = i + 1 156 } 157 return out_pos 158} 159 160// Compile-only smoke: round-trip + skip hyphens on decode. 161func main() -> i64 { 162 let out: *u8 = sys_mmap(64) 163 let n: i64 = c32_encode("hello", 5, out) 164 // 5 bytes * 8 = 40 bits / 5 = 8 chars. 165 if n != 8 { return 1 } 166 167 // Round trip. 168 let back: *u8 = sys_mmap(64) 169 let n2: i64 = c32_decode(out, n, back) 170 if n2 != 5 { return 2 } 171 let expected: *u8 = "hello" 172 var i: i64 = 0 173 while i < 5 { 174 if back[i] != expected[i] { return 3 } 175 i = i + 1 176 } 177 178 // Lenient decode: hyphens + lowercase + I/O -> 1/0. 179 // Encode \"hello\" produced uppercase C32. Insert a hyphen: 180 // e.g. out[4] kept, then hyphen, then rest. 181 let chopped: *u8 = sys_mmap(16) 182 i = 0 183 while i < 4 { 184 chopped[i] = out[i] 185 i = i + 1 186 } 187 chopped[4] = 0x2D // '-' 188 i = 4 189 while i < n { 190 chopped[i + 1] = out[i] 191 i = i + 1 192 } 193 let n3: i64 = c32_decode(chopped, n + 1, back) 194 if n3 != 5 { return 4 } 195 196 return 0 197}