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.
Files changed (144) hide show
  1. package/README.md +186 -17
  2. package/dist/cjs/bases.d.ts +189 -0
  3. package/dist/cjs/bases.d.ts.map +1 -0
  4. package/dist/cjs/bases.js +134 -0
  5. package/dist/cjs/bases.js.map +1 -0
  6. package/dist/cjs/bufferbase.d.ts +22 -45
  7. package/dist/cjs/bufferbase.d.ts.map +1 -1
  8. package/dist/cjs/bufferbase.js +26 -120
  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 +38 -0
  27. package/dist/cjs/codec.d.ts.map +1 -0
  28. package/dist/cjs/codec.js +69 -0
  29. package/dist/cjs/codec.js.map +1 -0
  30. package/dist/cjs/codecs.d.ts +20 -0
  31. package/dist/cjs/codecs.d.ts.map +1 -0
  32. package/dist/cjs/codecs.js +19 -0
  33. package/dist/cjs/codecs.js.map +1 -0
  34. package/dist/cjs/errors.d.ts +93 -0
  35. package/dist/cjs/errors.d.ts.map +1 -0
  36. package/dist/cjs/errors.js +130 -0
  37. package/dist/cjs/errors.js.map +1 -0
  38. package/dist/cjs/example.js +49 -33
  39. package/dist/cjs/example.js.map +1 -1
  40. package/dist/cjs/functions.d.ts +58 -0
  41. package/dist/cjs/functions.d.ts.map +1 -0
  42. package/dist/cjs/functions.js +71 -0
  43. package/dist/cjs/functions.js.map +1 -0
  44. package/dist/cjs/index.d.ts +10 -1
  45. package/dist/cjs/index.d.ts.map +1 -1
  46. package/dist/cjs/index.js +35 -4
  47. package/dist/cjs/index.js.map +1 -1
  48. package/dist/cjs/package.json +17 -12
  49. package/dist/cjs/types.d.ts +144 -0
  50. package/dist/cjs/types.d.ts.map +1 -0
  51. package/dist/cjs/types.js +3 -0
  52. package/dist/cjs/types.js.map +1 -0
  53. package/dist/esm/bases.d.ts +189 -0
  54. package/dist/esm/bases.d.ts.map +1 -0
  55. package/dist/esm/bases.js +129 -0
  56. package/dist/esm/bases.js.map +1 -0
  57. package/dist/esm/bufferbase.d.ts +22 -45
  58. package/dist/esm/bufferbase.d.ts.map +1 -1
  59. package/dist/esm/bufferbase.js +21 -115
  60. package/dist/esm/bufferbase.js.map +1 -1
  61. package/dist/esm/codec-base.d.ts +56 -0
  62. package/dist/esm/codec-base.d.ts.map +1 -0
  63. package/dist/esm/codec-base.js +127 -0
  64. package/dist/esm/codec-base.js.map +1 -0
  65. package/dist/esm/codec-block.d.ts +47 -0
  66. package/dist/esm/codec-block.d.ts.map +1 -0
  67. package/dist/esm/codec-block.js +209 -0
  68. package/dist/esm/codec-block.js.map +1 -0
  69. package/dist/esm/codec-block85.d.ts +56 -0
  70. package/dist/esm/codec-block85.d.ts.map +1 -0
  71. package/dist/esm/codec-block85.js +267 -0
  72. package/dist/esm/codec-block85.js.map +1 -0
  73. package/dist/esm/codec-radix.d.ts +46 -0
  74. package/dist/esm/codec-radix.d.ts.map +1 -0
  75. package/dist/esm/codec-radix.js +177 -0
  76. package/dist/esm/codec-radix.js.map +1 -0
  77. package/dist/esm/codec.d.ts +38 -0
  78. package/dist/esm/codec.d.ts.map +1 -0
  79. package/dist/esm/codec.js +60 -0
  80. package/dist/esm/codec.js.map +1 -0
  81. package/dist/esm/codecs.d.ts +20 -0
  82. package/dist/esm/codecs.d.ts.map +1 -0
  83. package/dist/esm/codecs.js +16 -0
  84. package/dist/esm/codecs.js.map +1 -0
  85. package/dist/esm/errors.d.ts +93 -0
  86. package/dist/esm/errors.d.ts.map +1 -0
  87. package/dist/esm/errors.js +119 -0
  88. package/dist/esm/errors.js.map +1 -0
  89. package/dist/esm/example.js +48 -32
  90. package/dist/esm/example.js.map +1 -1
  91. package/dist/esm/functions.d.ts +58 -0
  92. package/dist/esm/functions.d.ts.map +1 -0
  93. package/dist/esm/functions.js +65 -0
  94. package/dist/esm/functions.js.map +1 -0
  95. package/dist/esm/index.d.ts +10 -1
  96. package/dist/esm/index.d.ts.map +1 -1
  97. package/dist/esm/index.js +12 -1
  98. package/dist/esm/index.js.map +1 -1
  99. package/dist/esm/package.json +17 -12
  100. package/dist/esm/types.d.ts +144 -0
  101. package/dist/esm/types.d.ts.map +1 -0
  102. package/dist/esm/types.js +2 -0
  103. package/dist/esm/types.js.map +1 -0
  104. package/package.json +19 -14
  105. package/src/algorithms.test.ts +442 -0
  106. package/src/bases.ts +142 -0
  107. package/src/bufferbase.test.ts +44 -27
  108. package/src/bufferbase.ts +32 -117
  109. package/src/codec-base.ts +181 -0
  110. package/src/codec-block.ts +258 -0
  111. package/src/codec-block85.ts +315 -0
  112. package/src/codec-radix.ts +206 -0
  113. package/src/codec.test.ts +328 -0
  114. package/src/codec.ts +67 -0
  115. package/src/codecs.ts +24 -0
  116. package/src/errors.ts +127 -0
  117. package/src/example.ts +49 -34
  118. package/src/functions.ts +73 -0
  119. package/src/index.ts +39 -6
  120. package/src/types.ts +156 -0
  121. package/dist/cjs/bufferbase.d.mts +0 -60
  122. package/dist/cjs/bufferbase.d.mts.map +0 -1
  123. package/dist/cjs/bufferbase.mjs +0 -150
  124. package/dist/cjs/bufferbase.mjs.map +0 -1
  125. package/dist/cjs/example.d.mts +0 -2
  126. package/dist/cjs/example.d.mts.map +0 -1
  127. package/dist/cjs/example.mjs +0 -39
  128. package/dist/cjs/example.mjs.map +0 -1
  129. package/dist/cjs/index.d.mts +0 -2
  130. package/dist/cjs/index.d.mts.map +0 -1
  131. package/dist/cjs/index.mjs +0 -11
  132. package/dist/cjs/index.mjs.map +0 -1
  133. package/dist/esm/bufferbase.d.mts +0 -60
  134. package/dist/esm/bufferbase.d.mts.map +0 -1
  135. package/dist/esm/bufferbase.mjs +0 -141
  136. package/dist/esm/bufferbase.mjs.map +0 -1
  137. package/dist/esm/example.d.mts +0 -2
  138. package/dist/esm/example.d.mts.map +0 -1
  139. package/dist/esm/example.mjs +0 -37
  140. package/dist/esm/example.mjs.map +0 -1
  141. package/dist/esm/index.d.mts +0 -2
  142. package/dist/esm/index.d.mts.map +0 -1
  143. package/dist/esm/index.mjs +0 -2
  144. package/dist/esm/index.mjs.map +0 -1
@@ -1,53 +1,49 @@
1
1
  import { Chars, BufferEncoder } from './bufferbase.js';
2
2
  import { Buffer } from 'node:buffer';
3
- import test from 'node:test'; // ①
4
- import assert from 'node:assert';
3
+ import { describe, test, expect } from 'bun:test';
5
4
 
6
- test('BufferEncoder', async (t) => {
7
- await t.test(
8
- 'encodes and decodes a string correctly with base58chars',
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
- const encoded = encoder.encode(buffer);
14
- const decoded = encoder.decode(encoded);
10
+ const encoded = encoder.encode(buffer);
11
+ const decoded = encoder.decode(encoded);
15
12
 
16
- assert.strictEqual(decoded.toString('utf8'), 'Hello, World!');
17
- }
18
- );
13
+ expect(decoded.toString('utf8')).toBe('Hello, World!');
14
+ });
19
15
 
20
- await t.test('handles empty buffer correctly', () => {
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
- assert.strictEqual(decoded.toString('utf8'), '');
23
+ expect(decoded.toString('utf8')).toBe('');
28
24
  });
29
25
 
30
- await t.test('handles long buffer correctly', () => {
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
- assert.strictEqual(decoded.toString('utf8'), 'Hello, World!'.repeat(100));
33
+ expect(decoded.toString('utf8')).toBe('Hello, World!'.repeat(100));
38
34
  });
39
35
 
40
- await t.test('handles bufferSize argument correctly', () => {
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
- assert.strictEqual(decoded.toString('hex'), '00000000');
43
+ expect(decoded.toString('hex')).toBe('00000000');
48
44
  });
49
45
 
50
- await t.test('encode and decode various chars correctly', () => {
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
- assert.strictEqual(decoded.toString('utf8'), 'Hello, World!');
54
+ expect(decoded.toString('utf8')).toBe('Hello, World!');
59
55
  }
60
56
  });
61
57
 
62
- await t.test('handles binary powers from 1 to 63 correctly', () => {
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
- console.log(encoded, result);
72
- assert.strictEqual(result, num);
67
+ expect(result).toBe(num);
73
68
  }
74
69
  });
75
- await t.test('throws error on invalid characters in decode', () => {
76
- assert.throws(() => {
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
- }, new Error('Invalid character found'));
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
- * A collection of common bases.
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
- export class InvalidCharacterError extends Error {
28
- message = 'Invalid character found';
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
- export class BufferSizeError extends Error {
32
- message = 'Buffer size exceeds specified size';
33
- }
16
+ /** @deprecated Import `Chars` from the package root. */
17
+ export { Chars } from './bases.js';
34
18
 
35
- /**
36
- * Encodes and decodes buffers to and from a base.
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
- constructor(private baseChars: string) {}
24
+ private readonly codec: RadixCodec;
40
25
 
41
- /**
42
- * Encodes a buffer to a string.
43
- */
44
- encode(buffer: Buffer): string {
45
- let result = [];
46
- for (const byte of buffer) {
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
- let result = Buffer.alloc(0);
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
- decorder: BufferEncoder;
56
+ private readonly codec: RadixCodec;
130
57
 
131
58
  constructor(inputBase: string) {
132
- this.decorder = new BufferEncoder(inputBase);
59
+ this.codec = new RadixCodec({ alphabet: inputBase, algorithm: 'radix' });
133
60
  }
134
61
 
135
62
  validate(input: string): boolean {
136
- try {
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
+ }