bufferbase 1.3.0 → 3.0.0
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.
- package/README.md +186 -17
- package/dist/cjs/bases.d.ts +189 -0
- package/dist/cjs/bases.d.ts.map +1 -0
- package/dist/cjs/bases.js +134 -0
- package/dist/cjs/bases.js.map +1 -0
- package/dist/cjs/bufferbase.d.ts +22 -45
- package/dist/cjs/bufferbase.d.ts.map +1 -1
- package/dist/cjs/bufferbase.js +26 -120
- package/dist/cjs/bufferbase.js.map +1 -1
- package/dist/cjs/codec-base.d.ts +56 -0
- package/dist/cjs/codec-base.d.ts.map +1 -0
- package/dist/cjs/codec-base.js +132 -0
- package/dist/cjs/codec-base.js.map +1 -0
- package/dist/cjs/codec-block.d.ts +47 -0
- package/dist/cjs/codec-block.d.ts.map +1 -0
- package/dist/cjs/codec-block.js +213 -0
- package/dist/cjs/codec-block.js.map +1 -0
- package/dist/cjs/codec-block85.d.ts +56 -0
- package/dist/cjs/codec-block85.d.ts.map +1 -0
- package/dist/cjs/codec-block85.js +271 -0
- package/dist/cjs/codec-block85.js.map +1 -0
- package/dist/cjs/codec-radix.d.ts +46 -0
- package/dist/cjs/codec-radix.d.ts.map +1 -0
- package/dist/cjs/codec-radix.js +181 -0
- package/dist/cjs/codec-radix.js.map +1 -0
- package/dist/cjs/codec.d.ts +38 -0
- package/dist/cjs/codec.d.ts.map +1 -0
- package/dist/cjs/codec.js +69 -0
- package/dist/cjs/codec.js.map +1 -0
- package/dist/cjs/codecs.d.ts +20 -0
- package/dist/cjs/codecs.d.ts.map +1 -0
- package/dist/cjs/codecs.js +19 -0
- package/dist/cjs/codecs.js.map +1 -0
- package/dist/cjs/errors.d.ts +93 -0
- package/dist/cjs/errors.d.ts.map +1 -0
- package/dist/cjs/errors.js +130 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/example.js +49 -33
- package/dist/cjs/example.js.map +1 -1
- package/dist/cjs/functions.d.ts +58 -0
- package/dist/cjs/functions.d.ts.map +1 -0
- package/dist/cjs/functions.js +71 -0
- package/dist/cjs/functions.js.map +1 -0
- package/dist/cjs/index.d.ts +10 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +35 -4
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/package.json +17 -12
- package/dist/cjs/types.d.ts +144 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/bases.d.ts +189 -0
- package/dist/esm/bases.d.ts.map +1 -0
- package/dist/esm/bases.js +129 -0
- package/dist/esm/bases.js.map +1 -0
- package/dist/esm/bufferbase.d.ts +22 -45
- package/dist/esm/bufferbase.d.ts.map +1 -1
- package/dist/esm/bufferbase.js +21 -115
- package/dist/esm/bufferbase.js.map +1 -1
- package/dist/esm/codec-base.d.ts +56 -0
- package/dist/esm/codec-base.d.ts.map +1 -0
- package/dist/esm/codec-base.js +127 -0
- package/dist/esm/codec-base.js.map +1 -0
- package/dist/esm/codec-block.d.ts +47 -0
- package/dist/esm/codec-block.d.ts.map +1 -0
- package/dist/esm/codec-block.js +209 -0
- package/dist/esm/codec-block.js.map +1 -0
- package/dist/esm/codec-block85.d.ts +56 -0
- package/dist/esm/codec-block85.d.ts.map +1 -0
- package/dist/esm/codec-block85.js +267 -0
- package/dist/esm/codec-block85.js.map +1 -0
- package/dist/esm/codec-radix.d.ts +46 -0
- package/dist/esm/codec-radix.d.ts.map +1 -0
- package/dist/esm/codec-radix.js +177 -0
- package/dist/esm/codec-radix.js.map +1 -0
- package/dist/esm/codec.d.ts +38 -0
- package/dist/esm/codec.d.ts.map +1 -0
- package/dist/esm/codec.js +60 -0
- package/dist/esm/codec.js.map +1 -0
- package/dist/esm/codecs.d.ts +20 -0
- package/dist/esm/codecs.d.ts.map +1 -0
- package/dist/esm/codecs.js +16 -0
- package/dist/esm/codecs.js.map +1 -0
- package/dist/esm/errors.d.ts +93 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +119 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/example.js +48 -32
- package/dist/esm/example.js.map +1 -1
- package/dist/esm/functions.d.ts +58 -0
- package/dist/esm/functions.d.ts.map +1 -0
- package/dist/esm/functions.js +65 -0
- package/dist/esm/functions.js.map +1 -0
- package/dist/esm/index.d.ts +10 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +12 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/package.json +17 -12
- package/dist/esm/types.d.ts +144 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/package.json +19 -14
- package/src/algorithms.test.ts +442 -0
- package/src/bases.ts +142 -0
- package/src/bufferbase.test.ts +44 -27
- package/src/bufferbase.ts +32 -117
- package/src/codec-base.ts +181 -0
- package/src/codec-block.ts +258 -0
- package/src/codec-block85.ts +315 -0
- package/src/codec-radix.ts +206 -0
- package/src/codec.test.ts +328 -0
- package/src/codec.ts +67 -0
- package/src/codecs.ts +24 -0
- package/src/errors.ts +127 -0
- package/src/example.ts +49 -34
- package/src/functions.ts +73 -0
- package/src/index.ts +39 -6
- package/src/types.ts +156 -0
- package/dist/cjs/bufferbase.d.mts +0 -60
- package/dist/cjs/bufferbase.d.mts.map +0 -1
- package/dist/cjs/bufferbase.mjs +0 -150
- package/dist/cjs/bufferbase.mjs.map +0 -1
- package/dist/cjs/example.d.mts +0 -2
- package/dist/cjs/example.d.mts.map +0 -1
- package/dist/cjs/example.mjs +0 -39
- package/dist/cjs/example.mjs.map +0 -1
- package/dist/cjs/index.d.mts +0 -2
- package/dist/cjs/index.d.mts.map +0 -1
- package/dist/cjs/index.mjs +0 -11
- package/dist/cjs/index.mjs.map +0 -1
- package/dist/esm/bufferbase.d.mts +0 -60
- package/dist/esm/bufferbase.d.mts.map +0 -1
- package/dist/esm/bufferbase.mjs +0 -141
- package/dist/esm/bufferbase.mjs.map +0 -1
- package/dist/esm/example.d.mts +0 -2
- package/dist/esm/example.d.mts.map +0 -1
- package/dist/esm/example.mjs +0 -37
- package/dist/esm/example.mjs.map +0 -1
- package/dist/esm/index.d.mts +0 -2
- package/dist/esm/index.d.mts.map +0 -1
- package/dist/esm/index.mjs +0 -2
- package/dist/esm/index.mjs.map +0 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import type { BufferLike, DecodeOptions, RadixSpec } from './types.js';
|
|
3
|
+
import { BaseCodec, toBuffer } from './codec-base.js';
|
|
4
|
+
import {
|
|
5
|
+
BufferSizeError,
|
|
6
|
+
CheckSymbolError,
|
|
7
|
+
InvalidCharacterError,
|
|
8
|
+
InvalidCodecError,
|
|
9
|
+
InvalidLengthError,
|
|
10
|
+
} from './errors.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reads the buffer as one big integer and rewrites it in base
|
|
14
|
+
* `alphabet.length`.
|
|
15
|
+
*
|
|
16
|
+
* This is the Base58 family of encodings. It accepts any alphabet and any
|
|
17
|
+
* input length, but it is not RFC 4648: the output length is not a fixed
|
|
18
|
+
* function of the input length, so it cannot be streamed, and encoding is
|
|
19
|
+
* O(n²) in the input size.
|
|
20
|
+
*
|
|
21
|
+
* Leading zero bytes carry no value in an integer, so they are recorded
|
|
22
|
+
* separately as leading `alphabet[0]` characters.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { RadixCodec } from 'bufferbase';
|
|
27
|
+
*
|
|
28
|
+
* const binary = new RadixCodec({ alphabet: '01', algorithm: 'radix' });
|
|
29
|
+
* binary.encode(Uint8Array.of(5)); // '101'
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class RadixCodec extends BaseCodec {
|
|
33
|
+
readonly algorithm = 'radix' as const;
|
|
34
|
+
|
|
35
|
+
/** Alphabet plus the extra check symbols, or `null` when unchecked. */
|
|
36
|
+
private readonly checkChars: string | null;
|
|
37
|
+
|
|
38
|
+
/** Value of every character a check symbol may be written as. */
|
|
39
|
+
private readonly checkValues: ReadonlyMap<string, number>;
|
|
40
|
+
|
|
41
|
+
constructor(spec: RadixSpec) {
|
|
42
|
+
super(spec);
|
|
43
|
+
|
|
44
|
+
const extra = spec.checkSymbols;
|
|
45
|
+
if (extra === undefined || extra === '') {
|
|
46
|
+
this.checkChars = null;
|
|
47
|
+
this.checkValues = new Map();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const combined = spec.alphabet + extra;
|
|
51
|
+
if (new Set(combined).size !== combined.length) {
|
|
52
|
+
throw new InvalidCodecError('checkSymbols repeat a character of the alphabet');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The symbol is matched by value, so that a case-insensitive codec accepts
|
|
56
|
+
// it in either case just as it does the rest of the string.
|
|
57
|
+
const values = new Map<string, number>();
|
|
58
|
+
for (let i = 0; i < combined.length; i++) {
|
|
59
|
+
const char = combined[i];
|
|
60
|
+
values.set(char, i);
|
|
61
|
+
if (spec.caseInsensitive === true) {
|
|
62
|
+
values.set(char.toLowerCase(), i);
|
|
63
|
+
values.set(char.toUpperCase(), i);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.checkChars = combined;
|
|
67
|
+
this.checkValues = values;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
encode(input: BufferLike): string {
|
|
71
|
+
const buffer = toBuffer(input);
|
|
72
|
+
const base = this.chars.length;
|
|
73
|
+
const digits: number[] = [];
|
|
74
|
+
|
|
75
|
+
for (const byte of buffer) {
|
|
76
|
+
let carry = byte;
|
|
77
|
+
for (let i = 0; i < digits.length; i++) {
|
|
78
|
+
carry += digits[i] * 256;
|
|
79
|
+
digits[i] = carry % base;
|
|
80
|
+
carry = Math.floor(carry / base);
|
|
81
|
+
}
|
|
82
|
+
while (carry > 0) {
|
|
83
|
+
digits.push(carry % base);
|
|
84
|
+
carry = Math.floor(carry / base);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < buffer.length && buffer[i] === 0; i++) {
|
|
89
|
+
digits.push(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let out = '';
|
|
93
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
94
|
+
out += this.chars[digits[i]];
|
|
95
|
+
}
|
|
96
|
+
if (this.checkChars !== null) {
|
|
97
|
+
out += this.checkChars[this.remainderOf(buffer, this.checkChars.length)];
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
decode(encoded: string, options?: DecodeOptions): Buffer {
|
|
103
|
+
const bytes = this.read(this.strip(encoded));
|
|
104
|
+
|
|
105
|
+
const size = options?.size;
|
|
106
|
+
if (size === undefined) {
|
|
107
|
+
return Buffer.from(bytes);
|
|
108
|
+
}
|
|
109
|
+
if (bytes.length > size) {
|
|
110
|
+
throw new BufferSizeError(bytes.length, size);
|
|
111
|
+
}
|
|
112
|
+
const padded = Buffer.alloc(size);
|
|
113
|
+
Buffer.from(bytes).copy(padded, size - bytes.length);
|
|
114
|
+
return padded;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
validate(input: string): boolean {
|
|
118
|
+
const stripped = this.strip(input);
|
|
119
|
+
if (this.checkChars === null) {
|
|
120
|
+
// Every string over the alphabet decodes, so the characters settle it.
|
|
121
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
122
|
+
if (this.valueAt(stripped, i) < 0) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
this.read(stripped);
|
|
130
|
+
return true;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Decodes the body, verifying the check symbol when the codec carries one.
|
|
138
|
+
*
|
|
139
|
+
* Everything `decode` can reject other than `DecodeOptions.size` is checked
|
|
140
|
+
* here, so `validate` and `decode` always agree.
|
|
141
|
+
*/
|
|
142
|
+
private read(input: string): number[] {
|
|
143
|
+
let body = input;
|
|
144
|
+
|
|
145
|
+
if (this.checkChars !== null) {
|
|
146
|
+
if (body.length === 0) {
|
|
147
|
+
throw new InvalidLengthError(0, 'a checked encoding always carries a check symbol');
|
|
148
|
+
}
|
|
149
|
+
const found = body.slice(-1);
|
|
150
|
+
body = body.slice(0, -1);
|
|
151
|
+
const bytes = this.toBytes(body);
|
|
152
|
+
const expected = this.remainderOf(bytes, this.checkChars.length);
|
|
153
|
+
if (this.checkValues.get(found) !== expected) {
|
|
154
|
+
throw new CheckSymbolError(found, this.checkChars[expected]);
|
|
155
|
+
}
|
|
156
|
+
return bytes;
|
|
157
|
+
}
|
|
158
|
+
return this.toBytes(input);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Rewrites the digits of `body` as bytes, most significant first. */
|
|
162
|
+
private toBytes(body: string): number[] {
|
|
163
|
+
const base = this.chars.length;
|
|
164
|
+
const bytes: number[] = [];
|
|
165
|
+
let leadingZeros = 0;
|
|
166
|
+
let stillLeading = true;
|
|
167
|
+
|
|
168
|
+
for (let at = 0; at < body.length; at++) {
|
|
169
|
+
const value = this.valueAt(body, at);
|
|
170
|
+
if (value < 0) {
|
|
171
|
+
throw new InvalidCharacterError(body[at]);
|
|
172
|
+
}
|
|
173
|
+
if (stillLeading && value === 0) {
|
|
174
|
+
leadingZeros++;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
stillLeading = false;
|
|
178
|
+
|
|
179
|
+
let carry = value;
|
|
180
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
181
|
+
carry += bytes[i] * base;
|
|
182
|
+
bytes[i] = carry % 256;
|
|
183
|
+
carry = Math.floor(carry / 256);
|
|
184
|
+
}
|
|
185
|
+
while (carry > 0) {
|
|
186
|
+
bytes.push(carry % 256);
|
|
187
|
+
carry = Math.floor(carry / 256);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (let i = 0; i < leadingZeros; i++) {
|
|
192
|
+
bytes.push(0);
|
|
193
|
+
}
|
|
194
|
+
bytes.reverse();
|
|
195
|
+
return bytes;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The value the bytes denote, modulo `divisor`. */
|
|
199
|
+
private remainderOf(bytes: Iterable<number>, divisor: number): number {
|
|
200
|
+
let remainder = 0;
|
|
201
|
+
for (const byte of bytes) {
|
|
202
|
+
remainder = (remainder * 256 + byte) % divisor;
|
|
203
|
+
}
|
|
204
|
+
return remainder;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { describe, test, expect } from 'bun:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
encode,
|
|
6
|
+
decode,
|
|
7
|
+
convert,
|
|
8
|
+
validate,
|
|
9
|
+
createCodec,
|
|
10
|
+
Codecs,
|
|
11
|
+
Chars,
|
|
12
|
+
InvalidCharacterError,
|
|
13
|
+
BufferSizeError,
|
|
14
|
+
UnknownBaseError,
|
|
15
|
+
} from './index.js';
|
|
16
|
+
|
|
17
|
+
describe('Function API', () => {
|
|
18
|
+
test('encode/decode round-trip with base name', () => {
|
|
19
|
+
const buffer = Buffer.from('Hello, World!', 'utf8');
|
|
20
|
+
const encoded = encode(buffer, 'base58');
|
|
21
|
+
const decoded = decode(encoded, 'base58');
|
|
22
|
+
expect(decoded.toString('utf8')).toBe('Hello, World!');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('encode/decode with custom character set', () => {
|
|
26
|
+
const spec = { alphabet: Chars.Base58, algorithm: 'radix' } as const;
|
|
27
|
+
const buffer = Buffer.from('Test', 'utf8');
|
|
28
|
+
const encoded = encode(buffer, spec);
|
|
29
|
+
const decoded = decode(encoded, spec);
|
|
30
|
+
expect(decoded.toString('utf8')).toBe('Test');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('a bare alphabet is no longer accepted as a base', () => {
|
|
34
|
+
expect(() => encode(Buffer.from('Test'), Chars.Base58 as never)).toThrow(UnknownBaseError);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('convert between bases', () => {
|
|
38
|
+
const buffer = Buffer.from('Hello', 'utf8');
|
|
39
|
+
const base58Encoded = encode(buffer, 'base58');
|
|
40
|
+
const converted = convert(base58Encoded, 'base58', 'base64url');
|
|
41
|
+
const expected = encode(buffer, 'base64url');
|
|
42
|
+
expect(converted).toBe(expected);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('validate returns true for valid input', () => {
|
|
46
|
+
expect(validate('9Ajdvz', 'base58')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('validate returns false for invalid input', () => {
|
|
50
|
+
expect(validate('0Ajdvz', 'base58')).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('decode with size option pads buffer', () => {
|
|
54
|
+
const buffer = Buffer.from([1, 2, 3]);
|
|
55
|
+
const encoded = encode(buffer, 'base58');
|
|
56
|
+
const decoded = decode(encoded, 'base58', { size: 8 });
|
|
57
|
+
expect(decoded.length).toBe(8);
|
|
58
|
+
expect(decoded).toEqual(Buffer.from([0, 0, 0, 0, 0, 1, 2, 3]));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('decode with size option throws on overflow', () => {
|
|
62
|
+
const buffer = Buffer.from([1, 2, 3, 4, 5]);
|
|
63
|
+
const encoded = encode(buffer, 'base58');
|
|
64
|
+
expect(() => {
|
|
65
|
+
decode(encoded, 'base58', { size: 3 });
|
|
66
|
+
}).toThrow(BufferSizeError);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('throws UnknownBaseError for invalid base name', () => {
|
|
70
|
+
expect(() => {
|
|
71
|
+
encode(Buffer.from('test'), 'invalid');
|
|
72
|
+
}).toThrow(UnknownBaseError);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Codecs object', () => {
|
|
77
|
+
test('Codecs.base58.encode/decode round-trip', () => {
|
|
78
|
+
const buffer = Buffer.from('Hello, World!', 'utf8');
|
|
79
|
+
const encoded = Codecs.base58.encode(buffer);
|
|
80
|
+
const decoded = Codecs.base58.decode(encoded);
|
|
81
|
+
expect(decoded.toString('utf8')).toBe('Hello, World!');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('Codecs.base64url.encode/decode round-trip', () => {
|
|
85
|
+
const buffer = Buffer.from('Hello, World!', 'utf8');
|
|
86
|
+
const encoded = Codecs.base64url.encode(buffer);
|
|
87
|
+
const decoded = Codecs.base64url.decode(encoded);
|
|
88
|
+
expect(decoded.toString('utf8')).toBe('Hello, World!');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('Codecs.base58.convertTo', () => {
|
|
92
|
+
const buffer = Buffer.from('Test', 'utf8');
|
|
93
|
+
const base58Encoded = Codecs.base58.encode(buffer);
|
|
94
|
+
const converted = Codecs.base58.convertTo(Codecs.base64url, base58Encoded);
|
|
95
|
+
expect(converted).toBe(Codecs.base64url.encode(buffer));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('Codecs.base58.validate', () => {
|
|
99
|
+
expect(Codecs.base58.validate('123abc')).toBe(true);
|
|
100
|
+
expect(Codecs.base58.validate('0abc')).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('createCodec', () => {
|
|
105
|
+
test('creates custom codec', () => {
|
|
106
|
+
const binary = createCodec('01');
|
|
107
|
+
const buffer = Buffer.from([5]);
|
|
108
|
+
const encoded = binary.encode(buffer);
|
|
109
|
+
expect(encoded).toBe('101');
|
|
110
|
+
const decoded = binary.decode(encoded);
|
|
111
|
+
expect(decoded).toEqual(Buffer.from([5]));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('Edge cases', () => {
|
|
116
|
+
test('empty buffer', () => {
|
|
117
|
+
const buffer = Buffer.alloc(0);
|
|
118
|
+
const encoded = encode(buffer, 'base58');
|
|
119
|
+
expect(encoded).toBe('');
|
|
120
|
+
const decoded = decode(encoded, 'base58');
|
|
121
|
+
expect(decoded.length).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('empty string with size option', () => {
|
|
125
|
+
const decoded = decode('', 'base58', { size: 4 });
|
|
126
|
+
expect(decoded).toEqual(Buffer.from([0, 0, 0, 0]));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('leading zero bytes preserved', () => {
|
|
130
|
+
const buffer = Buffer.from([0, 0, 1, 2, 3]);
|
|
131
|
+
const encoded = encode(buffer, 'base58');
|
|
132
|
+
const decoded = decode(encoded, 'base58');
|
|
133
|
+
expect(decoded).toEqual(buffer);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('multiple leading zeros', () => {
|
|
137
|
+
const buffer = Buffer.from([0, 0, 0, 0, 255]);
|
|
138
|
+
const encoded = encode(buffer, 'base58');
|
|
139
|
+
const decoded = decode(encoded, 'base58');
|
|
140
|
+
expect(decoded).toEqual(buffer);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('all zeros', () => {
|
|
144
|
+
const buffer = Buffer.from([0, 0, 0]);
|
|
145
|
+
const encoded = encode(buffer, 'base58');
|
|
146
|
+
const decoded = decode(encoded, 'base58');
|
|
147
|
+
expect(decoded).toEqual(buffer);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('single byte values', () => {
|
|
151
|
+
for (let i = 0; i < 256; i++) {
|
|
152
|
+
const buffer = Buffer.from([i]);
|
|
153
|
+
const encoded = encode(buffer, 'base58');
|
|
154
|
+
const decoded = decode(encoded, 'base58');
|
|
155
|
+
expect(decoded).toEqual(buffer);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('long buffer', () => {
|
|
160
|
+
const buffer = Buffer.from('Hello, World!'.repeat(100), 'utf8');
|
|
161
|
+
const encoded = encode(buffer, 'base58');
|
|
162
|
+
const decoded = decode(encoded, 'base58');
|
|
163
|
+
expect(decoded.toString('utf8')).toBe('Hello, World!'.repeat(100));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('binary powers from 0 to 63', () => {
|
|
167
|
+
for (let i = 0; i < 64; i++) {
|
|
168
|
+
const buffer = Buffer.alloc(8);
|
|
169
|
+
const num = BigInt(2) ** BigInt(i) - BigInt(1);
|
|
170
|
+
buffer.writeBigInt64BE(num);
|
|
171
|
+
const encoded = encode(buffer, 'base58');
|
|
172
|
+
const decoded = decode(encoded, 'base58', { size: 8 });
|
|
173
|
+
const result = decoded.readBigInt64BE();
|
|
174
|
+
expect(result).toBe(num);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('All character sets', () => {
|
|
180
|
+
const baseNames = [
|
|
181
|
+
'decimal',
|
|
182
|
+
'base16',
|
|
183
|
+
'hex',
|
|
184
|
+
'base32',
|
|
185
|
+
'base32crockford',
|
|
186
|
+
'base36',
|
|
187
|
+
'base52',
|
|
188
|
+
'base58',
|
|
189
|
+
'base64',
|
|
190
|
+
'base64url',
|
|
191
|
+
'base32hex',
|
|
192
|
+
'ascii85',
|
|
193
|
+
'z85',
|
|
194
|
+
] as const;
|
|
195
|
+
|
|
196
|
+
// 12 bytes, so that z85 (whole 4-byte blocks only) is covered by the loop.
|
|
197
|
+
const payload = 'Hello, World';
|
|
198
|
+
|
|
199
|
+
for (const base of baseNames) {
|
|
200
|
+
test(`${base} encode/decode round-trip`, () => {
|
|
201
|
+
const buffer = Buffer.from(payload, 'utf8');
|
|
202
|
+
const encoded = encode(buffer, base);
|
|
203
|
+
const decoded = decode(encoded, base);
|
|
204
|
+
expect(decoded.toString('utf8')).toBe(payload);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('Error handling', () => {
|
|
210
|
+
test('InvalidCharacterError on decode', () => {
|
|
211
|
+
expect(() => {
|
|
212
|
+
decode('Invalid!String', 'base58');
|
|
213
|
+
}).toThrow(InvalidCharacterError);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('InvalidCharacterError includes character', () => {
|
|
217
|
+
try {
|
|
218
|
+
decode('abc!def', 'base58');
|
|
219
|
+
expect(true).toBe(false);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
expect(e).toBeInstanceOf(InvalidCharacterError);
|
|
222
|
+
expect((e as Error).message).toContain('!');
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('Uint8Array support', () => {
|
|
228
|
+
test('encode Uint8Array with function API', () => {
|
|
229
|
+
const uint8Array = new Uint8Array([72, 101, 108, 108, 111]); // 'Hello'
|
|
230
|
+
const buffer = Buffer.from('Hello', 'utf8');
|
|
231
|
+
|
|
232
|
+
const encodedFromUint8 = encode(uint8Array, 'base58');
|
|
233
|
+
const encodedFromBuffer = encode(buffer, 'base58');
|
|
234
|
+
|
|
235
|
+
expect(encodedFromUint8).toBe(encodedFromBuffer);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('encode Uint8Array with Codecs object', () => {
|
|
239
|
+
const uint8Array = new Uint8Array([72, 101, 108, 108, 111]); // 'Hello'
|
|
240
|
+
const buffer = Buffer.from('Hello', 'utf8');
|
|
241
|
+
|
|
242
|
+
const encodedFromUint8 = Codecs.base58.encode(uint8Array);
|
|
243
|
+
const encodedFromBuffer = Codecs.base58.encode(buffer);
|
|
244
|
+
|
|
245
|
+
expect(encodedFromUint8).toBe(encodedFromBuffer);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('encode empty Uint8Array', () => {
|
|
249
|
+
const uint8Array = new Uint8Array(0);
|
|
250
|
+
const encoded = encode(uint8Array, 'base58');
|
|
251
|
+
expect(encoded).toBe('');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('encode Uint8Array with leading zeros', () => {
|
|
255
|
+
const uint8Array = new Uint8Array([0, 0, 1, 2, 3]);
|
|
256
|
+
const encoded = encode(uint8Array, 'base58');
|
|
257
|
+
const decoded = decode(encoded, 'base58');
|
|
258
|
+
expect(Array.from(decoded)).toEqual([0, 0, 1, 2, 3]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('encode Uint8Array all zeros', () => {
|
|
262
|
+
const uint8Array = new Uint8Array([0, 0, 0]);
|
|
263
|
+
const encoded = encode(uint8Array, 'base58');
|
|
264
|
+
const decoded = decode(encoded, 'base58');
|
|
265
|
+
expect(Array.from(decoded)).toEqual([0, 0, 0]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('encode Uint8Array across all base encodings', () => {
|
|
269
|
+
const uint8Array = new Uint8Array([1, 2, 3, 4]);
|
|
270
|
+
const buffer = Buffer.from([1, 2, 3, 4]);
|
|
271
|
+
|
|
272
|
+
const baseNames = [
|
|
273
|
+
'decimal', 'base16', 'hex', 'base32', 'base32hex', 'base32crockford',
|
|
274
|
+
'base36', 'base52', 'base58', 'base64', 'base64url',
|
|
275
|
+
'ascii85', 'z85',
|
|
276
|
+
] as const;
|
|
277
|
+
|
|
278
|
+
for (const base of baseNames) {
|
|
279
|
+
const encodedFromUint8 = encode(uint8Array, base);
|
|
280
|
+
const encodedFromBuffer = encode(buffer, base);
|
|
281
|
+
expect(encodedFromUint8).toBe(encodedFromBuffer);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('custom codec with Uint8Array', () => {
|
|
286
|
+
const binary = createCodec('01');
|
|
287
|
+
const uint8Array = new Uint8Array([5]);
|
|
288
|
+
const encoded = binary.encode(uint8Array);
|
|
289
|
+
expect(encoded).toBe('101');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('Backward compatibility', () => {
|
|
294
|
+
test('BufferEncoder works', async () => {
|
|
295
|
+
const { BufferEncoder, Chars } = await import('./bufferbase.js');
|
|
296
|
+
const encoder = new BufferEncoder(Chars.Base58);
|
|
297
|
+
const buffer = Buffer.from('Hello', 'utf8');
|
|
298
|
+
const encoded = encoder.encode(buffer);
|
|
299
|
+
const decoded = encoder.decode(encoded);
|
|
300
|
+
expect(decoded.toString('utf8')).toBe('Hello');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('BufferEncoder with bufferSize', async () => {
|
|
304
|
+
const { BufferEncoder, Chars } = await import('./bufferbase.js');
|
|
305
|
+
const encoder = new BufferEncoder(Chars.Base58);
|
|
306
|
+
const buffer = Buffer.alloc(0);
|
|
307
|
+
const encoded = encoder.encode(buffer);
|
|
308
|
+
const decoded = encoder.decode(encoded, 4);
|
|
309
|
+
expect(decoded.toString('hex')).toBe('00000000');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('Converter works', async () => {
|
|
313
|
+
const { BufferEncoder, Converter, Chars } = await import('./bufferbase.js');
|
|
314
|
+
const converter = new Converter(Chars.Base58, Chars.Base64);
|
|
315
|
+
const buffer = Buffer.from('Test', 'utf8');
|
|
316
|
+
const base58Encoded = new BufferEncoder(Chars.Base58).encode(buffer);
|
|
317
|
+
const converted = converter.convert(base58Encoded);
|
|
318
|
+
const expected = new BufferEncoder(Chars.Base64).encode(buffer);
|
|
319
|
+
expect(converted).toBe(expected);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('Validator works', async () => {
|
|
323
|
+
const { Validator, Chars } = await import('./bufferbase.js');
|
|
324
|
+
const validator = new Validator(Chars.Base58);
|
|
325
|
+
expect(validator.validate('123abc')).toBe(true);
|
|
326
|
+
expect(validator.validate('0abc')).toBe(false);
|
|
327
|
+
});
|
|
328
|
+
});
|
package/src/codec.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { CodecSpec, ICodec } from './types.js';
|
|
2
|
+
import { RadixCodec } from './codec-radix.js';
|
|
3
|
+
import { BlockCodec } from './codec-block.js';
|
|
4
|
+
import { Block85Codec } from './codec-block85.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds the codec described by a spec.
|
|
8
|
+
*
|
|
9
|
+
* A bare string is read as a custom alphabet for a radix conversion, which is
|
|
10
|
+
* the only algorithm that works with an alphabet of any length.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { createCodec } from 'bufferbase';
|
|
15
|
+
*
|
|
16
|
+
* const binary = createCodec('01');
|
|
17
|
+
* binary.encode(Uint8Array.of(5)); // '101'
|
|
18
|
+
*
|
|
19
|
+
* const paddedHex = createCodec({ alphabet: '0123456789abcdef', algorithm: 'block' });
|
|
20
|
+
* paddedHex.encode(Uint8Array.of(0, 1)); // '0001'
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function createCodec(spec: CodecSpec | string): ICodec {
|
|
24
|
+
const resolved: CodecSpec =
|
|
25
|
+
typeof spec === 'string' ? { alphabet: spec, algorithm: 'radix' } : spec;
|
|
26
|
+
|
|
27
|
+
switch (resolved.algorithm) {
|
|
28
|
+
case 'radix':
|
|
29
|
+
return new RadixCodec(resolved);
|
|
30
|
+
case 'block':
|
|
31
|
+
return new BlockCodec(resolved);
|
|
32
|
+
case 'block85':
|
|
33
|
+
return new Block85Codec(resolved);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const cache = new WeakMap<CodecSpec, ICodec>();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns the codec for a spec, building it once per spec object.
|
|
41
|
+
*
|
|
42
|
+
* Named bases always resolve to the same spec object, so they are built once
|
|
43
|
+
* for the lifetime of the process.
|
|
44
|
+
*/
|
|
45
|
+
export function getCodec(spec: CodecSpec): ICodec {
|
|
46
|
+
const cached = cache.get(spec);
|
|
47
|
+
if (cached !== undefined) {
|
|
48
|
+
return cached;
|
|
49
|
+
}
|
|
50
|
+
const codec = createCodec(spec);
|
|
51
|
+
cache.set(spec, codec);
|
|
52
|
+
return codec;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { RadixCodec } from './codec-radix.js';
|
|
56
|
+
export { BlockCodec } from './codec-block.js';
|
|
57
|
+
export { Block85Codec } from './codec-block85.js';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @deprecated Use `createCodec({ alphabet, algorithm: 'radix' })`, or
|
|
61
|
+
* `createCodec(alphabet)` for the same thing.
|
|
62
|
+
*/
|
|
63
|
+
export class Codec extends RadixCodec {
|
|
64
|
+
constructor(chars: string) {
|
|
65
|
+
super({ alphabet: chars, algorithm: 'radix' });
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/codecs.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ICodec } from './types.js';
|
|
2
|
+
import { Bases, type BaseName } from './bases.js';
|
|
3
|
+
import { getCodec } from './codec.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A ready-made codec for every name in `Bases`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { Codecs } from 'bufferbase';
|
|
11
|
+
*
|
|
12
|
+
* Codecs.base58.encode(Buffer.from('Hello')); // '9Ajdvzr'
|
|
13
|
+
* Codecs.base64.encode(Buffer.from('fo')); // 'Zm8='
|
|
14
|
+
* Codecs.base58.convertTo(Codecs.base64url, '9Ajdvzr');
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export const Codecs = Object.fromEntries(
|
|
18
|
+
Object.entries(Bases).map(([name, spec]) => [name, getCodec(spec)]),
|
|
19
|
+
) as Record<BaseName, ICodec>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Type representing valid codec names in the `Codecs` object.
|
|
23
|
+
*/
|
|
24
|
+
export type CodecName = BaseName;
|