bufferbase 2.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +162 -63
  2. package/dist/cjs/bases.d.ts +152 -45
  3. package/dist/cjs/bases.d.ts.map +1 -1
  4. package/dist/cjs/bases.js +90 -77
  5. package/dist/cjs/bases.js.map +1 -1
  6. package/dist/cjs/bufferbase.d.ts +21 -27
  7. package/dist/cjs/bufferbase.d.ts.map +1 -1
  8. package/dist/cjs/bufferbase.js +21 -31
  9. package/dist/cjs/bufferbase.js.map +1 -1
  10. package/dist/cjs/codec-base.d.ts +56 -0
  11. package/dist/cjs/codec-base.d.ts.map +1 -0
  12. package/dist/cjs/codec-base.js +132 -0
  13. package/dist/cjs/codec-base.js.map +1 -0
  14. package/dist/cjs/codec-block.d.ts +47 -0
  15. package/dist/cjs/codec-block.d.ts.map +1 -0
  16. package/dist/cjs/codec-block.js +213 -0
  17. package/dist/cjs/codec-block.js.map +1 -0
  18. package/dist/cjs/codec-block85.d.ts +56 -0
  19. package/dist/cjs/codec-block85.d.ts.map +1 -0
  20. package/dist/cjs/codec-block85.js +271 -0
  21. package/dist/cjs/codec-block85.js.map +1 -0
  22. package/dist/cjs/codec-radix.d.ts +46 -0
  23. package/dist/cjs/codec-radix.d.ts.map +1 -0
  24. package/dist/cjs/codec-radix.js +181 -0
  25. package/dist/cjs/codec-radix.js.map +1 -0
  26. package/dist/cjs/codec.d.ts +25 -111
  27. package/dist/cjs/codec.d.ts.map +1 -1
  28. package/dist/cjs/codec.js +50 -228
  29. package/dist/cjs/codec.js.map +1 -1
  30. package/dist/cjs/codecs.d.ts +8 -59
  31. package/dist/cjs/codecs.d.ts.map +1 -1
  32. package/dist/cjs/codecs.js +6 -79
  33. package/dist/cjs/codecs.js.map +1 -1
  34. package/dist/cjs/errors.d.ts +65 -48
  35. package/dist/cjs/errors.d.ts.map +1 -1
  36. package/dist/cjs/errors.js +89 -51
  37. package/dist/cjs/errors.js.map +1 -1
  38. package/dist/cjs/example.js +44 -21
  39. package/dist/cjs/example.js.map +1 -1
  40. package/dist/cjs/functions.d.ts +27 -31
  41. package/dist/cjs/functions.d.ts.map +1 -1
  42. package/dist/cjs/functions.js +25 -47
  43. package/dist/cjs/functions.js.map +1 -1
  44. package/dist/cjs/index.d.ts +7 -5
  45. package/dist/cjs/index.d.ts.map +1 -1
  46. package/dist/cjs/index.js +17 -7
  47. package/dist/cjs/index.js.map +1 -1
  48. package/dist/cjs/package.json +12 -7
  49. package/dist/cjs/types.d.ts +108 -71
  50. package/dist/cjs/types.d.ts.map +1 -1
  51. package/dist/esm/bases.d.ts +152 -45
  52. package/dist/esm/bases.d.ts.map +1 -1
  53. package/dist/esm/bases.js +88 -75
  54. package/dist/esm/bases.js.map +1 -1
  55. package/dist/esm/bufferbase.d.ts +21 -27
  56. package/dist/esm/bufferbase.d.ts.map +1 -1
  57. package/dist/esm/bufferbase.js +18 -30
  58. package/dist/esm/bufferbase.js.map +1 -1
  59. package/dist/esm/codec-base.d.ts +56 -0
  60. package/dist/esm/codec-base.d.ts.map +1 -0
  61. package/dist/esm/codec-base.js +127 -0
  62. package/dist/esm/codec-base.js.map +1 -0
  63. package/dist/esm/codec-block.d.ts +47 -0
  64. package/dist/esm/codec-block.d.ts.map +1 -0
  65. package/dist/esm/codec-block.js +209 -0
  66. package/dist/esm/codec-block.js.map +1 -0
  67. package/dist/esm/codec-block85.d.ts +56 -0
  68. package/dist/esm/codec-block85.d.ts.map +1 -0
  69. package/dist/esm/codec-block85.js +267 -0
  70. package/dist/esm/codec-block85.js.map +1 -0
  71. package/dist/esm/codec-radix.d.ts +46 -0
  72. package/dist/esm/codec-radix.d.ts.map +1 -0
  73. package/dist/esm/codec-radix.js +177 -0
  74. package/dist/esm/codec-radix.js.map +1 -0
  75. package/dist/esm/codec.d.ts +25 -111
  76. package/dist/esm/codec.d.ts.map +1 -1
  77. package/dist/esm/codec.js +44 -226
  78. package/dist/esm/codec.js.map +1 -1
  79. package/dist/esm/codecs.d.ts +8 -59
  80. package/dist/esm/codecs.d.ts.map +1 -1
  81. package/dist/esm/codecs.js +7 -80
  82. package/dist/esm/codecs.js.map +1 -1
  83. package/dist/esm/errors.d.ts +65 -48
  84. package/dist/esm/errors.d.ts.map +1 -1
  85. package/dist/esm/errors.js +83 -50
  86. package/dist/esm/errors.js.map +1 -1
  87. package/dist/esm/example.js +45 -22
  88. package/dist/esm/example.js.map +1 -1
  89. package/dist/esm/functions.d.ts +27 -31
  90. package/dist/esm/functions.d.ts.map +1 -1
  91. package/dist/esm/functions.js +26 -48
  92. package/dist/esm/functions.js.map +1 -1
  93. package/dist/esm/index.d.ts +7 -5
  94. package/dist/esm/index.d.ts.map +1 -1
  95. package/dist/esm/index.js +6 -6
  96. package/dist/esm/index.js.map +1 -1
  97. package/dist/esm/package.json +12 -7
  98. package/dist/esm/types.d.ts +108 -71
  99. package/dist/esm/types.d.ts.map +1 -1
  100. package/package.json +12 -7
  101. package/src/algorithms.test.ts +442 -0
  102. package/src/bases.ts +98 -76
  103. package/src/bufferbase.ts +26 -36
  104. package/src/codec-base.ts +181 -0
  105. package/src/codec-block.ts +258 -0
  106. package/src/codec-block85.ts +315 -0
  107. package/src/codec-radix.ts +206 -0
  108. package/src/codec.test.ts +17 -9
  109. package/src/codec.ts +48 -247
  110. package/src/codecs.ts +11 -105
  111. package/src/errors.ts +90 -50
  112. package/src/example.ts +47 -22
  113. package/src/functions.ts +34 -54
  114. package/src/index.ts +27 -7
  115. package/src/types.ts +113 -86
@@ -0,0 +1,442 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { describe, test, expect } from 'bun:test';
3
+
4
+ import {
5
+ Chars,
6
+ Codecs,
7
+ convert,
8
+ createCodec,
9
+ decode,
10
+ encode,
11
+ validate,
12
+ BufferSizeError,
13
+ CheckSymbolError,
14
+ InvalidCodecError,
15
+ InvalidLengthError,
16
+ NonCanonicalError,
17
+ ValueRangeError,
18
+ type Base,
19
+ type CodecSpec,
20
+ } from './index.js';
21
+
22
+ /** The test vectors published in RFC 4648 §10. */
23
+ const RFC_INPUTS = ['', 'f', 'fo', 'foo', 'foob', 'fooba', 'foobar'];
24
+ const RFC_VECTORS = {
25
+ base16: ['', '66', '666F', '666F6F', '666F6F62', '666F6F6261', '666F6F626172'],
26
+ base32: ['', 'MY======', 'MZXQ====', 'MZXW6===', 'MZXW6YQ=', 'MZXW6YTB', 'MZXW6YTBOI======'],
27
+ base32hex: ['', 'CO======', 'CPNG====', 'CPNMU===', 'CPNMUOG=', 'CPNMUOJ1', 'CPNMUOJ1E8======'],
28
+ base64: ['', 'Zg==', 'Zm8=', 'Zm9v', 'Zm9vYg==', 'Zm9vYmE=', 'Zm9vYmFy'],
29
+ } as const;
30
+
31
+ describe('RFC 4648', () => {
32
+ for (const [base, expected] of Object.entries(RFC_VECTORS)) {
33
+ test(`${base} matches the published vectors`, () => {
34
+ expect(RFC_INPUTS.map((s) => encode(Buffer.from(s), base as Base))).toEqual([...expected]);
35
+ });
36
+
37
+ test(`${base} decodes the published vectors`, () => {
38
+ expect(expected.map((s) => decode(s, base as Base).toString())).toEqual(RFC_INPUTS);
39
+ });
40
+ }
41
+
42
+ test('base64url is unpadded, as JWT expects', () => {
43
+ expect(encode(Buffer.from([0xfb, 0xff]), 'base64url')).toBe('-_8');
44
+ expect(decode('-_8', 'base64url')).toEqual(Buffer.from([0xfb, 0xff]));
45
+ });
46
+
47
+ test('hex agrees with Node for every leading byte', () => {
48
+ for (let first = 0; first < 256; first++) {
49
+ const buffer = Buffer.from([first, 0x00, 0x9f]);
50
+ expect(encode(buffer, 'hex')).toBe(buffer.toString('hex').toUpperCase());
51
+ }
52
+ });
53
+
54
+ test('base64 agrees with Node for every input length', () => {
55
+ for (let length = 0; length < 24; length++) {
56
+ const buffer = Buffer.from(Array.from({ length }, (_, i) => (i * 37) % 256));
57
+ expect(encode(buffer, 'base64')).toBe(buffer.toString('base64'));
58
+ }
59
+ });
60
+
61
+ test('the native fast path agrees with the generic one', () => {
62
+ // A pad character outside the spec keeps these codecs off the
63
+ // Buffer.toString path, so the two implementations can be compared.
64
+ const cases = [
65
+ { fast: 'base64' as const, generic: { alphabet: Chars.Base64, algorithm: 'block', pad: '.' } },
66
+ { fast: 'hex' as const, generic: { alphabet: Chars.Base16, algorithm: 'block', pad: '.' } },
67
+ {
68
+ fast: 'base64url' as const,
69
+ generic: { alphabet: Chars.Base64Url, algorithm: 'block', pad: '.' },
70
+ },
71
+ ];
72
+ for (const { fast, generic } of cases) {
73
+ const codec = createCodec(generic as CodecSpec);
74
+ for (let length = 0; length < 24; length++) {
75
+ const buffer = Buffer.from(Array.from({ length }, (_, i) => (i * 91) % 256));
76
+ const viaGeneric = codec.encode(buffer).replaceAll('.', fast === 'base64' ? '=' : '');
77
+ expect([fast, length, viaGeneric]).toEqual([fast, length, encode(buffer, fast)]);
78
+ }
79
+ }
80
+ });
81
+ });
82
+
83
+ describe('canonical encodings', () => {
84
+ test('spare bits that are not zero are rejected', () => {
85
+ expect(() => decode('Zm9=', 'base64')).toThrow(NonCanonicalError);
86
+ expect(() => decode('Zh==', 'base64')).toThrow(NonCanonicalError);
87
+ expect(validate('Zm9=', 'base64')).toBe(false);
88
+ });
89
+
90
+ test('the canonical form of the same bytes is accepted', () => {
91
+ expect(decode('Zm8=', 'base64').toString()).toBe('fo');
92
+ expect(decode('Zg==', 'base64').toString()).toBe('f');
93
+ });
94
+
95
+ test('strict: false accepts them, at the cost of injectivity', () => {
96
+ const lenient = createCodec({ alphabet: Chars.Base64, algorithm: 'block', strict: false });
97
+ expect(lenient.decode('Zm9=').toString()).toBe('fo');
98
+ expect(lenient.decode('Zm8=').toString()).toBe('fo');
99
+ });
100
+
101
+ test('lengths that cannot be whole bytes are rejected', () => {
102
+ for (const bad of ['Z', 'Zm9vd', 'Zm9vYmFyZ']) {
103
+ expect(validate(bad, 'base64')).toBe(false);
104
+ }
105
+ // RFC 4648 allows 2, 4, 5, 7 and 8 data characters in a base32 block.
106
+ for (const count of [1, 3, 6]) {
107
+ const input = 'A'.repeat(count).padEnd(8, '=');
108
+ expect(validate(input, 'base32')).toBe(false);
109
+ }
110
+ for (const count of [2, 4, 5, 7, 8]) {
111
+ const input = 'A'.repeat(count).padEnd(8, '=');
112
+ expect(validate(input, 'base32')).toBe(true);
113
+ }
114
+ });
115
+
116
+ test('padding must be present and only at the end', () => {
117
+ expect(() => decode('Zm8', 'base64')).toThrow(InvalidLengthError);
118
+ expect(validate('=Zm8', 'base64')).toBe(false);
119
+ expect(validate('Zm=8', 'base64')).toBe(false);
120
+ expect(validate('Zg======', 'base64')).toBe(false);
121
+ });
122
+
123
+ test('an Ascii85 group above 2^32 encodes nothing', () => {
124
+ // 's8W-!' is exactly 2^32 - 1, the largest group that does encode 4 bytes.
125
+ expect(decode('s8W-!', 'ascii85')).toEqual(Buffer.from('ffffffff', 'hex'));
126
+ expect(() => decode('s8W-"', 'ascii85')).toThrow(ValueRangeError);
127
+ });
128
+
129
+ test('a longer-than-needed Ascii85 tail is rejected', () => {
130
+ const canonical = encode(Buffer.from('ab'), 'ascii85');
131
+ expect(decode(canonical, 'ascii85').toString()).toBe('ab');
132
+ expect(validate(canonical.slice(0, -1) + Chars.Ascii85[84], 'ascii85')).toBe(false);
133
+ });
134
+ });
135
+
136
+ describe('validate agrees with decode', () => {
137
+ const bases = ['base64', 'base64url', 'base32', 'base16', 'base58', 'ascii85', 'z85'] as const;
138
+ const pieces = ['A', 'Z', 'm', '9', '=', '!', 'z', 'y', '~', '1', '0', 'u', '/', '-'];
139
+
140
+ for (const base of bases) {
141
+ test(base, () => {
142
+ let checked = 0;
143
+ for (let seed = 0; seed < 400; seed++) {
144
+ let input = '';
145
+ for (let n = seed % 9; n > 0; n--) {
146
+ input += pieces[(seed * 7 + n * 13) % pieces.length];
147
+ }
148
+ let decodes = true;
149
+ try {
150
+ decode(input, base);
151
+ } catch {
152
+ decodes = false;
153
+ }
154
+ expect([input, validate(input, base)]).toEqual([input, decodes]);
155
+ checked++;
156
+ }
157
+ expect(checked).toBe(400);
158
+ });
159
+ }
160
+ });
161
+
162
+ describe('Ascii85 and Z85', () => {
163
+ test('the default matches Python a85encode', () => {
164
+ expect(encode(Buffer.from('Man '), 'ascii85')).toBe('9jqo^');
165
+ expect(encode(Buffer.alloc(4), 'ascii85')).toBe('z');
166
+ expect(encode(Buffer.from(' '), 'ascii85')).toBe('+<VdL');
167
+ });
168
+
169
+ test('shortcuts and framing are independent options', () => {
170
+ const btoa = createCodec({
171
+ alphabet: Chars.Ascii85,
172
+ algorithm: 'block85',
173
+ zeroShortcut: true,
174
+ spaceShortcut: true,
175
+ });
176
+ const adobe = createCodec({
177
+ alphabet: Chars.Ascii85,
178
+ algorithm: 'block85',
179
+ zeroShortcut: true,
180
+ delimiters: true,
181
+ });
182
+ const plain = createCodec({ alphabet: Chars.Ascii85, algorithm: 'block85' });
183
+
184
+ expect(btoa.encode(Buffer.from(' '))).toBe('y');
185
+ expect(adobe.encode(Buffer.from(' '))).toBe('<~+<VdL~>');
186
+ expect(adobe.encode(Buffer.alloc(4))).toBe('<~z~>');
187
+ expect(plain.encode(Buffer.alloc(4))).toBe('!!!!!');
188
+
189
+ for (const codec of [btoa, adobe, plain]) {
190
+ const bytes = Buffer.from(' \0\0\0\0ab');
191
+ expect(codec.decode(codec.encode(bytes))).toEqual(bytes);
192
+ }
193
+ });
194
+
195
+ test('framing is required when configured', () => {
196
+ const adobe = createCodec({
197
+ alphabet: Chars.Ascii85,
198
+ algorithm: 'block85',
199
+ delimiters: true,
200
+ });
201
+ expect(adobe.validate('9jqo^')).toBe(false);
202
+ expect(adobe.validate('<~9jqo^~>')).toBe(true);
203
+ });
204
+
205
+ test('a shortcut may not appear inside a group', () => {
206
+ expect(validate('9jz', 'ascii85')).toBe(false);
207
+ });
208
+
209
+ test('z85 matches the ZeroMQ RFC 32 vector', () => {
210
+ const bytes = Buffer.from('864FD26FB559F75B', 'hex');
211
+ expect(encode(bytes, 'z85')).toBe('HelloWorld');
212
+ expect(decode('HelloWorld', 'z85')).toEqual(bytes);
213
+ });
214
+
215
+ test('z85 encodes whole blocks only', () => {
216
+ expect(() => encode(Buffer.from('abc'), 'z85')).toThrow(InvalidLengthError);
217
+ expect(() => decode('abcdef', 'z85')).toThrow(InvalidLengthError);
218
+ });
219
+
220
+ test('a trailing group needs at least two characters', () => {
221
+ const encoded = encode(Buffer.from('Man x'), 'ascii85');
222
+ expect(encoded).toBe('9jqo^GQ'); // 5 characters for the block, 2 for the byte
223
+ expect(validate(encoded, 'ascii85')).toBe(true);
224
+ expect(validate(encoded.slice(0, 6), 'ascii85')).toBe(false);
225
+ });
226
+ });
227
+
228
+ describe("Crockford's Base32", () => {
229
+ test('reads the symbols a reader is likely to confuse', () => {
230
+ const encoded = encode(Buffer.from('Hello'), 'base32crockford');
231
+ expect(encoded).toBe('91JPRV3F');
232
+ for (const written of [encoded.toLowerCase(), encoded.replaceAll('1', 'I'), encoded.replaceAll('1', 'l')]) {
233
+ expect([written, decode(written, 'base32crockford').toString()]).toEqual([written, 'Hello']);
234
+ }
235
+ const zeros = encode(Buffer.from([0, 0, 5]), 'base32crockford');
236
+ expect(decode(zeros.replaceAll('0', 'O'), 'base32crockford')).toEqual(Buffer.from([0, 0, 5]));
237
+ });
238
+
239
+ test('ignores hyphens inserted for readability', () => {
240
+ const encoded = encode(Buffer.from('Hello'), 'base32crockford');
241
+ expect(decode(`${encoded.slice(0, 3)}-${encoded.slice(3)}`, 'base32crockford').toString()).toBe(
242
+ 'Hello',
243
+ );
244
+ expect(validate('-91-JP-RV-3F-', 'base32crockford')).toBe(true);
245
+ });
246
+
247
+ test('encoding emits only the alphabet', () => {
248
+ for (let n = 0; n < 200; n++) {
249
+ const encoded = encode(Buffer.from([n, (n * 7) % 256]), 'base32crockford');
250
+ expect([n, /^[0-9A-HJKMNP-TV-Z]*$/.test(encoded)]).toEqual([n, true]);
251
+ }
252
+ });
253
+
254
+ test('the check symbol catches a changed character', () => {
255
+ const encoded = encode(Buffer.from('Hello'), 'base32crockfordcheck');
256
+ expect(encoded).toBe('91JPRV3FG');
257
+ expect(decode(encoded, 'base32crockfordcheck').toString()).toBe('Hello');
258
+
259
+ for (let at = 0; at < encoded.length - 1; at++) {
260
+ const other = encoded[at] === '1' ? '2' : '1';
261
+ const changed = encoded.slice(0, at) + other + encoded.slice(at + 1);
262
+ expect([at, validate(changed, 'base32crockfordcheck')]).toEqual([at, false]);
263
+ }
264
+ expect(validate(`${encoded.slice(0, -1)}9`, 'base32crockfordcheck')).toBe(false);
265
+ expect(() => decode(`${encoded.slice(0, -1)}9`, 'base32crockfordcheck')).toThrow(
266
+ CheckSymbolError,
267
+ );
268
+ });
269
+
270
+ test('the check symbol is read in either case, like the rest', () => {
271
+ const encoded = encode(Buffer.from('Hello, World!'), 'base32crockfordcheck');
272
+ expect(decode(encoded.toLowerCase(), 'base32crockfordcheck').toString()).toBe('Hello, World!');
273
+ const withU = encode(Buffer.from([0x00, 0x24]), 'base32crockfordcheck');
274
+ expect(withU.slice(-1)).toBe('U');
275
+ expect(decode(withU.toLowerCase(), 'base32crockfordcheck')).toEqual(Buffer.from([0x00, 0x24]));
276
+ });
277
+
278
+ test('the check symbol uses the five extra symbols for 32 to 36', () => {
279
+ for (const [value, expected] of [
280
+ [0x20, '*'],
281
+ [0x21, '~'],
282
+ [0x22, '$'],
283
+ [0x23, '='],
284
+ [0x24, 'U'],
285
+ ] as const) {
286
+ const bytes = Buffer.from([0x00, value]);
287
+ const encoded = encode(bytes, 'base32crockfordcheck');
288
+ expect([value, encoded.slice(-1)]).toEqual([value, expected]);
289
+ expect([value, decode(encoded, 'base32crockfordcheck')]).toEqual([value, bytes]);
290
+ }
291
+ });
292
+
293
+ test('a checked encoding always carries its symbol', () => {
294
+ expect(validate('', 'base32crockfordcheck')).toBe(false);
295
+ expect(encode(Buffer.alloc(0), 'base32crockfordcheck')).toBe('0');
296
+ expect(decode('0', 'base32crockfordcheck')).toEqual(Buffer.alloc(0));
297
+ });
298
+
299
+ test('every checked string validates exactly when it decodes', () => {
300
+ const pieces = ['0', '1', 'I', 'O', 'l', '-', 'Z', '*', 'U', '9', 'G'];
301
+ for (let seed = 0; seed < 500; seed++) {
302
+ let input = '';
303
+ for (let n = seed % 7; n > 0; n--) {
304
+ input += pieces[(seed * 5 + n * 3) % pieces.length];
305
+ }
306
+ let decodes = true;
307
+ try {
308
+ decode(input, 'base32crockfordcheck');
309
+ } catch {
310
+ decodes = false;
311
+ }
312
+ expect([input, validate(input, 'base32crockfordcheck')]).toEqual([input, decodes]);
313
+ }
314
+ });
315
+ });
316
+
317
+ describe('aliases and ignored characters', () => {
318
+ test('an alias must point at a character of the alphabet', () => {
319
+ expect(() =>
320
+ createCodec({ alphabet: '01', algorithm: 'radix', aliases: { O: '9' } }),
321
+ ).toThrow(InvalidCodecError);
322
+ });
323
+
324
+ test('an alias may not already be in the alphabet', () => {
325
+ expect(() =>
326
+ createCodec({ alphabet: '012', algorithm: 'radix', aliases: { 2: '0' } }),
327
+ ).toThrow(InvalidCodecError);
328
+ });
329
+
330
+ test('an ignored character may not also be decodable', () => {
331
+ expect(() => createCodec({ alphabet: '01', algorithm: 'radix', ignore: '1' })).toThrow(
332
+ InvalidCodecError,
333
+ );
334
+ });
335
+
336
+ test('check symbols may not repeat the alphabet', () => {
337
+ expect(() =>
338
+ createCodec({ alphabet: '0123456789', algorithm: 'radix', checkSymbols: '9*' }),
339
+ ).toThrow(InvalidCodecError);
340
+ });
341
+ });
342
+
343
+ describe('size option', () => {
344
+ test('a radix codec pads to the requested length', () => {
345
+ const encoded = encode(Buffer.from([0x01, 0x02]), 'base58');
346
+ expect(decode(encoded, 'base58', { size: 8 })).toEqual(
347
+ Buffer.from([0, 0, 0, 0, 0, 0, 0x01, 0x02]),
348
+ );
349
+ });
350
+
351
+ test('a radix codec rejects a result that is too long', () => {
352
+ const encoded = encode(Buffer.from('a longer payload'), 'base58');
353
+ expect(() => decode(encoded, 'base58', { size: 4 })).toThrow(BufferSizeError);
354
+ });
355
+
356
+ test('a block codec checks the length it derives', () => {
357
+ expect(decode('Zm9v', 'base64', { size: 3 })).toEqual(Buffer.from('foo'));
358
+ expect(() => decode('Zm9v', 'base64', { size: 4 })).toThrow(BufferSizeError);
359
+ expect(() => decode('HelloWorld', 'z85', { size: 5 })).toThrow(BufferSizeError);
360
+ });
361
+ });
362
+
363
+ describe('case insensitivity', () => {
364
+ test('base32 and hex accept either case', () => {
365
+ expect(decode('mzxw6ytb', 'base32').toString()).toBe('fooba');
366
+ expect(decode('666f6f', 'hex').toString()).toBe('foo');
367
+ expect(validate('mzxw6ytb', 'base32')).toBe(true);
368
+ });
369
+
370
+ test('base64 stays case sensitive, because its alphabet uses both', () => {
371
+ expect(decode('Zm9v', 'base64').toString()).toBe('foo');
372
+ expect(decode('ZM9V', 'base64').toString()).not.toBe('foo');
373
+ });
374
+ });
375
+
376
+ describe('migrating data written by bufferbase 2', () => {
377
+ test('radix64 reads it and base64 rewrites it', () => {
378
+ const bytes = Buffer.from('Hello, world!');
379
+ const legacy = encode(bytes, 'radix64');
380
+ expect(legacy).toBe('BIZWxsbywgd29ybGQh');
381
+ expect(convert(legacy, 'radix64', 'base64')).toBe(bytes.toString('base64'));
382
+ });
383
+
384
+ test('the radix aliases keep the old algorithm', () => {
385
+ for (const [modern, legacy] of [
386
+ ['base16', 'radix16'],
387
+ ['base32', 'radix32'],
388
+ ['base64', 'radix64'],
389
+ ['base64url', 'radix64url'],
390
+ ] as const) {
391
+ const bytes = Buffer.from([0x00, 0x0f, 0x2a]);
392
+ expect(encode(bytes, legacy)).not.toBe(encode(bytes, modern));
393
+ expect(decode(encode(bytes, legacy), legacy)).toEqual(bytes);
394
+ }
395
+ });
396
+ });
397
+
398
+ describe('codec specs that cannot work', () => {
399
+ test('block needs a power-of-two alphabet', () => {
400
+ expect(() => createCodec({ alphabet: Chars.Base58, algorithm: 'block' })).toThrow(
401
+ InvalidCodecError,
402
+ );
403
+ });
404
+
405
+ test('block85 needs exactly 85 characters', () => {
406
+ expect(() => createCodec({ alphabet: Chars.Base64, algorithm: 'block85' })).toThrow(
407
+ InvalidCodecError,
408
+ );
409
+ });
410
+
411
+ test('an alphabet may not repeat a character', () => {
412
+ expect(() => createCodec('0101')).toThrow(InvalidCodecError);
413
+ });
414
+
415
+ test('case folding needs an alphabet of one case', () => {
416
+ expect(() =>
417
+ createCodec({ alphabet: Chars.Base64, algorithm: 'block', caseInsensitive: true }),
418
+ ).toThrow(InvalidCodecError);
419
+ });
420
+
421
+ test('the pad character may not be part of the alphabet', () => {
422
+ expect(() =>
423
+ createCodec({ alphabet: Chars.Base16, algorithm: 'block', pad: 'A' }),
424
+ ).toThrow(InvalidCodecError);
425
+ });
426
+
427
+ test('a shortcut character may not be part of the alphabet', () => {
428
+ expect(() =>
429
+ createCodec({ alphabet: Chars.Z85, algorithm: 'block85', zeroShortcut: true }),
430
+ ).toThrow(InvalidCodecError);
431
+ });
432
+ });
433
+
434
+ describe('every named base round-trips', () => {
435
+ test('through Codecs, for a payload each one accepts', () => {
436
+ const bytes = Buffer.from('Hello, World');
437
+ for (const [name, codec] of Object.entries(Codecs)) {
438
+ expect([name, codec.decode(codec.encode(bytes))]).toEqual([name, bytes]);
439
+ expect([name, codec.validate(codec.encode(bytes))]).toEqual([name, true]);
440
+ }
441
+ });
442
+ });
package/src/bases.ts CHANGED
@@ -1,27 +1,21 @@
1
- import type { BaseName } from './types.js';
1
+ import type { CodecSpec } from './types.js';
2
2
  import { UnknownBaseError } from './errors.js';
3
3
 
4
4
  /**
5
- * Character sets for common base encodings.
5
+ * Character sets, ordered by value.
6
6
  *
7
- * Use these with `createCodec()` or the deprecated `BufferEncoder` class
8
- * when you need direct access to character sets.
9
- *
10
- * @example
11
- * ```typescript
12
- * import { Chars, createCodec } from 'bufferbase';
13
- *
14
- * const codec = createCodec(Chars.Base58);
15
- * const encoded = codec.encode(Buffer.from('Hello'));
16
- * ```
7
+ * A character set does not by itself describe an encoding — see `Bases` for
8
+ * the alphabet/algorithm pairs that do.
17
9
  */
18
10
  export const Chars = {
19
11
  /** Base 10: 0-9 */
20
12
  Decimal: '0123456789',
21
- /** Hexadecimal: 0-9A-F */
13
+ /** RFC 4648 Table 5 — Base16: 0-9A-F */
22
14
  Base16: '0123456789ABCDEF',
23
- /** RFC 4648 Base32: A-Z2-7 */
15
+ /** RFC 4648 Table 3 — Base32: A-Z2-7 */
24
16
  Base32: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
17
+ /** RFC 4648 Table 4 — Base32 with extended hex alphabet: 0-9A-V */
18
+ Base32Hex: '0123456789ABCDEFGHIJKLMNOPQRSTUV',
25
19
  /** Crockford's Base32: 0-9A-HJKMNP-TV-Z (no I, L, O, U) */
26
20
  Base32Crockford: '0123456789ABCDEFGHJKMNPQRSTVWXYZ',
27
21
  /** Alphanumeric: 0-9A-Z */
@@ -30,91 +24,119 @@ export const Chars = {
30
24
  Base52: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
31
25
  /** Bitcoin alphabet: 1-9A-HJ-NP-Za-km-z (no 0, O, I, l) */
32
26
  Base58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
33
- /** Standard Base64: A-Za-z0-9+/ */
27
+ /** RFC 4648 Table 1 — Base64: A-Za-z0-9+/ */
34
28
  Base64: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
35
- /** URL-safe Base64: A-Za-z0-9-_ */
29
+ /** RFC 4648 Table 2 — URL-safe Base64: A-Za-z0-9-_ */
36
30
  Base64Url: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
37
31
  /** XML token Base64: A-Za-z0-9._ */
38
32
  Base64Xml: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._',
39
33
  /** XML name Base64: A-Za-z0-9_: */
40
34
  Base64XmlName: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_:',
41
- /** ASCII85 encoding */
42
- Ascii85: '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstu',
43
- /** Base85 encoding */
44
- Base85: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&()*+-;<=>?@^_`{|}~',
45
- /** ZeroMQ Z85 encoding */
35
+ /** Ascii85: the 85 printable characters from `!` (0x21) to `u` (0x75) */
36
+ Ascii85: '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstu',
37
+ /** ZeroMQ Z85 (RFC 32): avoids the quote and backslash characters */
46
38
  Z85: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#',
39
+ /** The five symbols Crockford's Base32 adds for its check symbol, giving a modulus of 37 */
40
+ CrockfordCheck: '*~$=U',
47
41
  } as const;
48
42
 
49
- /**
50
- * Internal mapping from base names to character sets.
51
- * @internal
52
- */
53
- const baseNameToChars: Record<BaseName, string> = {
54
- decimal: Chars.Decimal,
55
- base16: Chars.Base16,
56
- hex: Chars.Base16,
57
- base32: Chars.Base32,
58
- base32crockford: Chars.Base32Crockford,
59
- base36: Chars.Base36,
60
- base52: Chars.Base52,
61
- base58: Chars.Base58,
62
- base64: Chars.Base64,
63
- base64url: Chars.Base64Url,
64
- base64xml: Chars.Base64Xml,
65
- base64xmlname: Chars.Base64XmlName,
66
- ascii85: Chars.Ascii85,
67
- base85: Chars.Base85,
68
- z85: Chars.Z85,
69
- };
43
+ /** The characters Crockford's Base32 reads as the digit they resemble. */
44
+ const CROCKFORD_ALIASES = { I: '1', L: '1', O: '0' } as const;
70
45
 
71
46
  /**
72
- * Resolves a base name or character set string to a character set.
47
+ * The encodings this package knows by name.
73
48
  *
74
- * This function is used internally by the function API to convert
75
- * base names (like `'base58'`) to their character sets.
76
- *
77
- * @param base - Base name (e.g., `'base58'`) or custom character set string
78
- * @returns The character set string
79
- * @throws {UnknownBaseError} If base name is not recognized and string length <= 15
49
+ * Each entry pairs an alphabet with the algorithm that reads it, because the
50
+ * alphabet alone is ambiguous: `base64` and `radix64` share Table 1 but
51
+ * produce different strings for the same bytes.
80
52
  *
81
53
  * @example
82
54
  * ```typescript
83
- * import { resolveChars } from 'bufferbase';
55
+ * import { encode, Bases } from 'bufferbase';
84
56
  *
85
- * resolveChars('base58'); // Returns Bitcoin alphabet
86
- * resolveChars('0123456789ABCDEF'); // Returns the string as-is (custom charset)
87
- * resolveChars('invalid'); // Throws UnknownBaseError
57
+ * encode(Buffer.from('fo'), 'base64'); // 'Zm8=' — RFC 4648
58
+ * encode(Buffer.from('fo'), 'radix64'); // 'GZv' — radix conversion
88
59
  * ```
89
60
  */
90
- export function resolveChars(base: BaseName | string): string {
91
- const normalized = base.toLowerCase() as BaseName;
92
- if (normalized in baseNameToChars) {
93
- return baseNameToChars[normalized];
94
- }
95
- // If not a known base name, treat as custom character set
96
- // (must be longer than 15 chars to distinguish from typos)
97
- if (base.length > 15) {
61
+ export const Bases = {
62
+ // Radix conversion. No external standard fixes these, so nothing here can
63
+ // disagree with one.
64
+ decimal: { alphabet: Chars.Decimal, algorithm: 'radix' },
65
+ base36: { alphabet: Chars.Base36, algorithm: 'radix', caseInsensitive: true },
66
+ base52: { alphabet: Chars.Base52, algorithm: 'radix' },
67
+ base58: { alphabet: Chars.Base58, algorithm: 'radix' },
68
+ /**
69
+ * Crockford's Base32, which specifies a notation for numbers rather than for
70
+ * byte streams, so a radix conversion is the right reading of it.
71
+ *
72
+ * Decoding accepts either case, reads `I` and `L` as `1` and `O` as `0`, and
73
+ * ignores hyphens.
74
+ */
75
+ base32crockford: {
76
+ alphabet: Chars.Base32Crockford,
77
+ algorithm: 'radix',
78
+ caseInsensitive: true,
79
+ aliases: CROCKFORD_ALIASES,
80
+ ignore: '-',
81
+ },
82
+ /** Crockford's Base32 carrying its trailing check symbol. */
83
+ base32crockfordcheck: {
84
+ alphabet: Chars.Base32Crockford,
85
+ algorithm: 'radix',
86
+ caseInsensitive: true,
87
+ aliases: CROCKFORD_ALIASES,
88
+ ignore: '-',
89
+ checkSymbols: Chars.CrockfordCheck,
90
+ },
91
+
92
+ // Bit-block encoding, RFC 4648.
93
+ base16: { alphabet: Chars.Base16, algorithm: 'block', pad: false, caseInsensitive: true },
94
+ hex: { alphabet: Chars.Base16, algorithm: 'block', pad: false, caseInsensitive: true },
95
+ base32: { alphabet: Chars.Base32, algorithm: 'block', caseInsensitive: true },
96
+ base32hex: { alphabet: Chars.Base32Hex, algorithm: 'block', caseInsensitive: true },
97
+ base64: { alphabet: Chars.Base64, algorithm: 'block' },
98
+ /** Unpadded, as used by JWT and other URL-facing formats. */
99
+ base64url: { alphabet: Chars.Base64Url, algorithm: 'block', pad: false },
100
+ base64xml: { alphabet: Chars.Base64Xml, algorithm: 'block', pad: false },
101
+ base64xmlname: { alphabet: Chars.Base64XmlName, algorithm: 'block', pad: false },
102
+
103
+ // Four bytes to five characters.
104
+ /** Ascii85 with the zero-block shortcut, matching Python's `a85encode`. */
105
+ ascii85: { alphabet: Chars.Ascii85, algorithm: 'block85', zeroShortcut: true },
106
+ /** ZeroMQ Z85, which has no padding and so encodes whole blocks only. */
107
+ z85: { alphabet: Chars.Z85, algorithm: 'block85', requireFullBlocks: true },
108
+
109
+ // Radix conversion over the RFC alphabets.
110
+ radix16: { alphabet: Chars.Base16, algorithm: 'radix' },
111
+ radix32: { alphabet: Chars.Base32, algorithm: 'radix' },
112
+ radix64: { alphabet: Chars.Base64, algorithm: 'radix' },
113
+ radix64url: { alphabet: Chars.Base64Url, algorithm: 'radix' },
114
+ } as const satisfies Record<string, CodecSpec>;
115
+
116
+ /**
117
+ * The names accepted wherever a base is expected.
118
+ */
119
+ export type BaseName = keyof typeof Bases;
120
+
121
+ /**
122
+ * Resolves a base name to its codec spec, and passes a spec through unchanged.
123
+ *
124
+ * @throws {UnknownBaseError} If the name is not one of `Bases`
125
+ */
126
+ export function resolveSpec(base: BaseName | CodecSpec): CodecSpec {
127
+ if (typeof base !== 'string') {
98
128
  return base;
99
129
  }
100
- throw new UnknownBaseError(base);
130
+ const spec: CodecSpec | undefined = Bases[base.toLowerCase() as BaseName];
131
+ if (spec === undefined) {
132
+ throw new UnknownBaseError(base);
133
+ }
134
+ return spec;
101
135
  }
102
136
 
103
137
  /**
104
- * Checks if a string is a valid base name.
105
- *
106
- * @param value - The string to check
107
- * @returns `true` if the value is a valid `BaseName`, `false` otherwise
108
- *
109
- * @example
110
- * ```typescript
111
- * import { isBaseName } from 'bufferbase';
112
- *
113
- * isBaseName('base58'); // true
114
- * isBaseName('BASE58'); // true (case-insensitive)
115
- * isBaseName('unknown'); // false
116
- * ```
138
+ * Checks whether a string names one of the encodings in `Bases`.
117
139
  */
118
140
  export function isBaseName(value: string): value is BaseName {
119
- return value.toLowerCase() in baseNameToChars;
141
+ return value.toLowerCase() in Bases;
120
142
  }