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
package/src/bufferbase.test.ts
CHANGED
|
@@ -1,53 +1,49 @@
|
|
|
1
1
|
import { Chars, BufferEncoder } from './bufferbase.js';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import test from '
|
|
4
|
-
import assert from 'node:assert';
|
|
3
|
+
import { describe, test, expect } from 'bun:test';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
(
|
|
10
|
-
const encoder = new BufferEncoder(Chars.Base58);
|
|
11
|
-
const buffer = Buffer.from('Hello, World!', 'utf8');
|
|
5
|
+
describe('BufferEncoder', () => {
|
|
6
|
+
test('encodes and decodes a string correctly with base58chars', () => {
|
|
7
|
+
const encoder = new BufferEncoder(Chars.Base58);
|
|
8
|
+
const buffer = Buffer.from('Hello, World!', 'utf8');
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const encoded = encoder.encode(buffer);
|
|
11
|
+
const decoded = encoder.decode(encoded);
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
);
|
|
13
|
+
expect(decoded.toString('utf8')).toBe('Hello, World!');
|
|
14
|
+
});
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
test('handles empty buffer correctly', () => {
|
|
21
17
|
const encoder = new BufferEncoder(Chars.Base58);
|
|
22
18
|
const buffer = Buffer.alloc(0);
|
|
23
19
|
|
|
24
20
|
const encoded = encoder.encode(buffer);
|
|
25
21
|
const decoded = encoder.decode(encoded);
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
expect(decoded.toString('utf8')).toBe('');
|
|
28
24
|
});
|
|
29
25
|
|
|
30
|
-
|
|
26
|
+
test('handles long buffer correctly', () => {
|
|
31
27
|
const encoder = new BufferEncoder(Chars.Base58);
|
|
32
28
|
const buffer = Buffer.from('Hello, World!'.repeat(100), 'utf8');
|
|
33
29
|
|
|
34
30
|
const encoded = encoder.encode(buffer);
|
|
35
31
|
const decoded = encoder.decode(encoded);
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
expect(decoded.toString('utf8')).toBe('Hello, World!'.repeat(100));
|
|
38
34
|
});
|
|
39
35
|
|
|
40
|
-
|
|
36
|
+
test('handles bufferSize argument correctly', () => {
|
|
41
37
|
const encoder = new BufferEncoder(Chars.Base58);
|
|
42
38
|
const buffer = Buffer.alloc(0);
|
|
43
39
|
|
|
44
40
|
const encoded = encoder.encode(buffer);
|
|
45
41
|
const decoded = encoder.decode(encoded, 4);
|
|
46
42
|
|
|
47
|
-
|
|
43
|
+
expect(decoded.toString('hex')).toBe('00000000');
|
|
48
44
|
});
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
test('encode and decode various chars correctly', () => {
|
|
51
47
|
for (const charTable of Object.values(Chars)) {
|
|
52
48
|
const encoder = new BufferEncoder(charTable);
|
|
53
49
|
const buffer = Buffer.from('Hello, World!', 'utf8');
|
|
@@ -55,11 +51,11 @@ test('BufferEncoder', async (t) => {
|
|
|
55
51
|
const encoded = encoder.encode(buffer);
|
|
56
52
|
const decoded = encoder.decode(encoded);
|
|
57
53
|
|
|
58
|
-
|
|
54
|
+
expect(decoded.toString('utf8')).toBe('Hello, World!');
|
|
59
55
|
}
|
|
60
56
|
});
|
|
61
57
|
|
|
62
|
-
|
|
58
|
+
test('handles binary powers from 1 to 63 correctly', () => {
|
|
63
59
|
const encoder = new BufferEncoder(Chars.Base58);
|
|
64
60
|
for (let i = 0; i < 64; i++) {
|
|
65
61
|
const buffer = Buffer.alloc(8);
|
|
@@ -68,14 +64,35 @@ test('BufferEncoder', async (t) => {
|
|
|
68
64
|
const encoded = encoder.encode(buffer);
|
|
69
65
|
const decoded = encoder.decode(encoded, 8);
|
|
70
66
|
const result = decoded.readBigInt64BE();
|
|
71
|
-
|
|
72
|
-
assert.strictEqual(result, num);
|
|
67
|
+
expect(result).toBe(num);
|
|
73
68
|
}
|
|
74
69
|
});
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
|
|
71
|
+
test('throws error on invalid characters in decode', () => {
|
|
72
|
+
expect(() => {
|
|
77
73
|
const encoder = new BufferEncoder(Chars.Base58);
|
|
78
74
|
encoder.decode('InvalidString!');
|
|
79
|
-
}
|
|
75
|
+
}).toThrow(/Invalid character found/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('encodes Uint8Array correctly', () => {
|
|
79
|
+
const encoder = new BufferEncoder(Chars.Base58);
|
|
80
|
+
const uint8Array = new Uint8Array([72, 101, 108, 108, 111]); // 'Hello'
|
|
81
|
+
const buffer = Buffer.from('Hello', 'utf8');
|
|
82
|
+
|
|
83
|
+
const encodedFromUint8 = encoder.encode(uint8Array);
|
|
84
|
+
const encodedFromBuffer = encoder.encode(buffer);
|
|
85
|
+
|
|
86
|
+
expect(encodedFromUint8).toBe(encodedFromBuffer);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('encodes Uint8Array with leading zeros correctly', () => {
|
|
90
|
+
const encoder = new BufferEncoder(Chars.Base58);
|
|
91
|
+
const uint8Array = new Uint8Array([0, 0, 1, 2, 3]);
|
|
92
|
+
|
|
93
|
+
const encoded = encoder.encode(uint8Array);
|
|
94
|
+
const decoded = encoder.decode(encoded);
|
|
95
|
+
|
|
96
|
+
expect(Array.from(decoded)).toEqual([0, 0, 1, 2, 3]);
|
|
80
97
|
});
|
|
81
98
|
});
|
package/src/bufferbase.ts
CHANGED
|
@@ -1,150 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @deprecated Use the current API instead:
|
|
3
|
+
* - `encode(buffer, 'base58')` or `Codecs.base58.encode(buffer)`
|
|
4
|
+
* - `decode(encoded, 'base58')` or `Codecs.base58.decode(encoded)`
|
|
5
|
+
* - `convert(input, 'base58', 'base64')` or `Codecs.base58.convertTo(...)`
|
|
6
|
+
* - `validate(input, 'base58')` or `Codecs.base58.validate(input)`
|
|
7
|
+
*
|
|
8
|
+
* These classes take a bare alphabet, which always meant a radix conversion,
|
|
9
|
+
* so they keep behaving exactly as they did.
|
|
3
10
|
*/
|
|
4
|
-
export const Chars = {
|
|
5
|
-
Decimal: '0123456789',
|
|
6
|
-
Base16: '0123456789ABCDEF',
|
|
7
|
-
Base32: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
|
|
8
|
-
Base32Crockford: '0123456789ABCDEFGHJKMNPQRSTVWXYZ',
|
|
9
|
-
Base36: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
10
|
-
Base52: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
|
11
|
-
Base58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
|
|
12
|
-
Base64_STD:
|
|
13
|
-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
|
|
14
|
-
Base64_URL_SAFE:
|
|
15
|
-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
|
|
16
|
-
Base64_XML_NMTOKEN:
|
|
17
|
-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._',
|
|
18
|
-
Base64_XML_NAME:
|
|
19
|
-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_:',
|
|
20
|
-
Ascii85:
|
|
21
|
-
'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstu',
|
|
22
|
-
Base85:
|
|
23
|
-
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&()*+-;<=>?@^_`{|}~',
|
|
24
|
-
Z85: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#',
|
|
25
|
-
};
|
|
26
11
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
12
|
+
import type { Buffer } from 'node:buffer';
|
|
13
|
+
import type { BufferLike } from './types.js';
|
|
14
|
+
import { RadixCodec } from './codec-radix.js';
|
|
30
15
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
16
|
+
/** @deprecated Import `Chars` from the package root. */
|
|
17
|
+
export { Chars } from './bases.js';
|
|
34
18
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
19
|
+
/** @deprecated Import these from the package root. */
|
|
20
|
+
export { BufferSizeError, InvalidCharacterError } from './errors.js';
|
|
21
|
+
|
|
22
|
+
/** @deprecated Use `createCodec(alphabet)`. */
|
|
38
23
|
export class BufferEncoder {
|
|
39
|
-
|
|
24
|
+
private readonly codec: RadixCodec;
|
|
40
25
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let carry = byte;
|
|
48
|
-
for (let j = 0; j < result.length; j++) {
|
|
49
|
-
carry += result[j] * 256;
|
|
50
|
-
result[j] = carry % this.baseChars.length;
|
|
51
|
-
carry = Math.floor(carry / this.baseChars.length);
|
|
52
|
-
}
|
|
53
|
-
while (carry > 0) {
|
|
54
|
-
result.push(carry % this.baseChars.length);
|
|
55
|
-
carry = Math.floor(carry / this.baseChars.length);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
for (const byte of buffer) {
|
|
59
|
-
if (byte === 0) {
|
|
60
|
-
result.push(0);
|
|
61
|
-
} else {
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return result
|
|
66
|
-
.reverse()
|
|
67
|
-
.map((index) => this.baseChars[index])
|
|
68
|
-
.join('');
|
|
26
|
+
constructor(baseChars: string) {
|
|
27
|
+
this.codec = new RadixCodec({ alphabet: baseChars, algorithm: 'radix' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
encode(buffer: BufferLike): string {
|
|
31
|
+
return this.codec.encode(buffer);
|
|
69
32
|
}
|
|
70
33
|
|
|
71
|
-
/**
|
|
72
|
-
* Decodes a string to a buffer.
|
|
73
|
-
*/
|
|
74
34
|
decode(encoded: string, bufferSize?: number): Buffer {
|
|
75
|
-
|
|
76
|
-
for (const char of encoded) {
|
|
77
|
-
const value = this.baseChars.indexOf(char);
|
|
78
|
-
if (value === -1) throw new InvalidCharacterError();
|
|
79
|
-
let carry = value;
|
|
80
|
-
let tempResult = Buffer.alloc(result.length);
|
|
81
|
-
let i;
|
|
82
|
-
for (i = 0; i < result.length; i++) {
|
|
83
|
-
carry += result[i] * this.baseChars.length;
|
|
84
|
-
tempResult[i] = carry % 256;
|
|
85
|
-
carry = Math.floor(carry / 256);
|
|
86
|
-
}
|
|
87
|
-
while (carry > 0) {
|
|
88
|
-
tempResult = Buffer.concat([tempResult, Buffer.from([carry % 256])]);
|
|
89
|
-
carry = Math.floor(carry / 256);
|
|
90
|
-
}
|
|
91
|
-
result = tempResult;
|
|
92
|
-
}
|
|
93
|
-
// buffer size
|
|
94
|
-
if (bufferSize && result.length < bufferSize) {
|
|
95
|
-
result = Buffer.concat([
|
|
96
|
-
result,
|
|
97
|
-
Buffer.from(Array(bufferSize - result.length).fill(0)),
|
|
98
|
-
]);
|
|
99
|
-
}
|
|
100
|
-
if (bufferSize && result.length > bufferSize) {
|
|
101
|
-
throw new BufferSizeError();
|
|
102
|
-
}
|
|
103
|
-
result.reverse();
|
|
104
|
-
return result;
|
|
35
|
+
return this.codec.decode(encoded, bufferSize === undefined ? undefined : { size: bufferSize });
|
|
105
36
|
}
|
|
106
37
|
}
|
|
107
38
|
|
|
108
|
-
/**
|
|
109
|
-
* Creates a converter function that can convert between two bases.
|
|
110
|
-
*/
|
|
39
|
+
/** @deprecated Use `convert()` or `codec.convertTo()`. */
|
|
111
40
|
export class Converter {
|
|
112
|
-
decoder: BufferEncoder;
|
|
113
|
-
encoder: BufferEncoder;
|
|
41
|
+
readonly decoder: BufferEncoder;
|
|
42
|
+
readonly encoder: BufferEncoder;
|
|
114
43
|
|
|
115
44
|
constructor(inputBase: string, outputBase: string) {
|
|
116
45
|
this.decoder = new BufferEncoder(inputBase);
|
|
117
46
|
this.encoder = new BufferEncoder(outputBase);
|
|
118
47
|
}
|
|
119
48
|
|
|
120
|
-
/**
|
|
121
|
-
* Converts a string from the input base to the output base.
|
|
122
|
-
*/
|
|
123
49
|
convert(input: string): string {
|
|
124
50
|
return this.encoder.encode(this.decoder.decode(input));
|
|
125
51
|
}
|
|
126
52
|
}
|
|
127
53
|
|
|
54
|
+
/** @deprecated Use `validate()` or `codec.validate()`. */
|
|
128
55
|
export class Validator {
|
|
129
|
-
|
|
56
|
+
private readonly codec: RadixCodec;
|
|
130
57
|
|
|
131
58
|
constructor(inputBase: string) {
|
|
132
|
-
this.
|
|
59
|
+
this.codec = new RadixCodec({ alphabet: inputBase, algorithm: 'radix' });
|
|
133
60
|
}
|
|
134
61
|
|
|
135
62
|
validate(input: string): boolean {
|
|
136
|
-
|
|
137
|
-
this.decorder.decode(input);
|
|
138
|
-
return true;
|
|
139
|
-
} catch (e) {
|
|
140
|
-
if (e instanceof InvalidCharacterError) {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
throw e;
|
|
144
|
-
}
|
|
63
|
+
return this.codec.validate(input);
|
|
145
64
|
}
|
|
146
65
|
}
|
|
147
|
-
|
|
148
|
-
export const validate = (input: string, base: string): boolean => {
|
|
149
|
-
return new Validator(base).validate(input);
|
|
150
|
-
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import type { Algorithm, BufferLike, CodecSpec, DecodeOptions, ICodec } from './types.js';
|
|
3
|
+
import { InvalidCodecError } from './errors.js';
|
|
4
|
+
|
|
5
|
+
/** Returns one byte per character, or `null` if the alphabet needs more. */
|
|
6
|
+
function singleByteCodes(alphabet: string): Uint8Array | null {
|
|
7
|
+
const codes = new Uint8Array(alphabet.length);
|
|
8
|
+
for (let i = 0; i < alphabet.length; i++) {
|
|
9
|
+
const code = alphabet.charCodeAt(i);
|
|
10
|
+
if (code > 0xff) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
codes[i] = code;
|
|
14
|
+
}
|
|
15
|
+
return codes;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Indexes the decodable characters by code unit, or `null` if any needs more than a byte. */
|
|
19
|
+
function codeTable(charToIndex: ReadonlyMap<string, number>): Int16Array | null {
|
|
20
|
+
const table = new Int16Array(256).fill(-1);
|
|
21
|
+
for (const [char, value] of charToIndex) {
|
|
22
|
+
const code = char.charCodeAt(0);
|
|
23
|
+
if (char.length !== 1 || code > 0xff) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
table[code] = value;
|
|
27
|
+
}
|
|
28
|
+
return table;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Converts a `BufferLike` to a `Buffer` without copying when possible. */
|
|
32
|
+
export function toBuffer(input: BufferLike): Buffer {
|
|
33
|
+
return Buffer.isBuffer(input) ? input : Buffer.from(input.buffer, input.byteOffset, input.length);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Shared alphabet handling for the three codec implementations.
|
|
38
|
+
*
|
|
39
|
+
* Subclasses provide the mapping between bytes and characters; this class owns
|
|
40
|
+
* the alphabet, the character lookup, optional case folding, and `convertTo`.
|
|
41
|
+
*/
|
|
42
|
+
export abstract class BaseCodec implements ICodec {
|
|
43
|
+
/** Character set used for encoding. */
|
|
44
|
+
readonly chars: string;
|
|
45
|
+
|
|
46
|
+
/** The algorithm this codec applies to `chars`. */
|
|
47
|
+
abstract readonly algorithm: Algorithm;
|
|
48
|
+
|
|
49
|
+
/** Whether non-canonical input is rejected. */
|
|
50
|
+
protected readonly strict: boolean;
|
|
51
|
+
|
|
52
|
+
private readonly charToIndex: Map<string, number>;
|
|
53
|
+
|
|
54
|
+
/** Characters dropped from the input before decoding. */
|
|
55
|
+
private readonly ignored: ReadonlySet<string>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The alphabet as one byte per character, or `null` when some character does
|
|
59
|
+
* not fit in a byte.
|
|
60
|
+
*
|
|
61
|
+
* Assembling output as bytes and decoding it once is several times faster
|
|
62
|
+
* than concatenating characters, and every alphabet in practical use is
|
|
63
|
+
* single-byte.
|
|
64
|
+
*/
|
|
65
|
+
protected readonly codes: Uint8Array | null;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The value of every decodable character, indexed by code unit, or `-1`.
|
|
69
|
+
* `null` when some character does not fit in a byte.
|
|
70
|
+
*
|
|
71
|
+
* Reading a character by index and looking it up here avoids allocating a
|
|
72
|
+
* string and hashing it for every character of the input.
|
|
73
|
+
*/
|
|
74
|
+
private readonly valueByCode: Int16Array | null;
|
|
75
|
+
|
|
76
|
+
protected constructor(spec: CodecSpec) {
|
|
77
|
+
const { alphabet, strict = true, caseInsensitive = false, aliases, ignore } = spec;
|
|
78
|
+
|
|
79
|
+
if (alphabet.length < 2) {
|
|
80
|
+
throw new InvalidCodecError(`alphabet needs at least 2 characters, got ${alphabet.length}`);
|
|
81
|
+
}
|
|
82
|
+
if (new Set(alphabet).size !== alphabet.length) {
|
|
83
|
+
throw new InvalidCodecError('alphabet contains duplicate characters');
|
|
84
|
+
}
|
|
85
|
+
if ([...alphabet].some((char) => char.length !== 1)) {
|
|
86
|
+
throw new InvalidCodecError('every alphabet character must be a single code unit');
|
|
87
|
+
}
|
|
88
|
+
if (caseInsensitive && new Set(alphabet.toLowerCase()).size !== alphabet.length) {
|
|
89
|
+
throw new InvalidCodecError(
|
|
90
|
+
'caseInsensitive needs an alphabet that does not contain both cases of a letter',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.chars = alphabet;
|
|
95
|
+
this.strict = strict;
|
|
96
|
+
this.charToIndex = new Map();
|
|
97
|
+
|
|
98
|
+
const accept = (char: string, value: number): void => {
|
|
99
|
+
this.charToIndex.set(char, value);
|
|
100
|
+
if (caseInsensitive) {
|
|
101
|
+
this.charToIndex.set(char.toLowerCase(), value);
|
|
102
|
+
this.charToIndex.set(char.toUpperCase(), value);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < alphabet.length; i++) {
|
|
107
|
+
accept(alphabet[i], i);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const [from, to] of Object.entries(aliases ?? {})) {
|
|
111
|
+
if (from.length !== 1) {
|
|
112
|
+
throw new InvalidCodecError(`alias '${from}' must be a single character`);
|
|
113
|
+
}
|
|
114
|
+
const value = this.charToIndex.get(to);
|
|
115
|
+
if (value === undefined) {
|
|
116
|
+
throw new InvalidCodecError(`alias '${from}' points at '${to}', which is not in the alphabet`);
|
|
117
|
+
}
|
|
118
|
+
if (alphabet.includes(from)) {
|
|
119
|
+
throw new InvalidCodecError(`alias '${from}' is already in the alphabet`);
|
|
120
|
+
}
|
|
121
|
+
accept(from, value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.codes = singleByteCodes(alphabet);
|
|
125
|
+
this.valueByCode = codeTable(this.charToIndex);
|
|
126
|
+
this.ignored = new Set(ignore ?? '');
|
|
127
|
+
for (const char of this.ignored) {
|
|
128
|
+
if (this.charToIndex.has(char)) {
|
|
129
|
+
throw new InvalidCodecError(`ignored character '${char}' is also decodable`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Reads the first `length` bytes of `written` back as text. */
|
|
135
|
+
protected textFrom(written: Uint8Array, length: number): string {
|
|
136
|
+
return Buffer.from(written.buffer, written.byteOffset, length).toString('latin1');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Drops the characters this codec ignores, such as readability hyphens. */
|
|
140
|
+
protected strip(input: string): string {
|
|
141
|
+
if (this.ignored.size === 0) {
|
|
142
|
+
return input;
|
|
143
|
+
}
|
|
144
|
+
let out = '';
|
|
145
|
+
for (let i = 0; i < input.length; i++) {
|
|
146
|
+
const char = input[i];
|
|
147
|
+
if (!this.ignored.has(char)) {
|
|
148
|
+
out += char;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Returns the value of a character, or `undefined` if it is not in the alphabet. */
|
|
155
|
+
protected valueOf(char: string): number | undefined {
|
|
156
|
+
return this.charToIndex.get(char);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns the value of the character at `index`, or `-1` if it is not
|
|
161
|
+
* decodable. This is the hot path of every decoder.
|
|
162
|
+
*/
|
|
163
|
+
protected valueAt(input: string, index: number): number {
|
|
164
|
+
const table = this.valueByCode;
|
|
165
|
+
if (table === null) {
|
|
166
|
+
const value = this.charToIndex.get(input[index]);
|
|
167
|
+
return value === undefined ? -1 : value;
|
|
168
|
+
}
|
|
169
|
+
const code = input.charCodeAt(index);
|
|
170
|
+
return code > 0xff ? -1 : table[code];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
abstract encode(buffer: BufferLike): string;
|
|
174
|
+
abstract decode(encoded: string, options?: DecodeOptions): Buffer;
|
|
175
|
+
abstract validate(input: string): boolean;
|
|
176
|
+
|
|
177
|
+
/** Decodes with this codec, then re-encodes with `target`. */
|
|
178
|
+
convertTo(target: ICodec, input: string): string {
|
|
179
|
+
return target.encode(this.decode(input));
|
|
180
|
+
}
|
|
181
|
+
}
|