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}