bufferbase 2.0.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 +151 -63
- package/dist/cjs/bases.d.ts +152 -45
- package/dist/cjs/bases.d.ts.map +1 -1
- package/dist/cjs/bases.js +90 -77
- package/dist/cjs/bases.js.map +1 -1
- package/dist/cjs/bufferbase.d.ts +21 -27
- package/dist/cjs/bufferbase.d.ts.map +1 -1
- package/dist/cjs/bufferbase.js +21 -31
- 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 +25 -111
- package/dist/cjs/codec.d.ts.map +1 -1
- package/dist/cjs/codec.js +50 -228
- package/dist/cjs/codec.js.map +1 -1
- package/dist/cjs/codecs.d.ts +8 -59
- package/dist/cjs/codecs.d.ts.map +1 -1
- package/dist/cjs/codecs.js +6 -79
- package/dist/cjs/codecs.js.map +1 -1
- package/dist/cjs/errors.d.ts +65 -48
- package/dist/cjs/errors.d.ts.map +1 -1
- package/dist/cjs/errors.js +89 -51
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/example.js +44 -21
- package/dist/cjs/example.js.map +1 -1
- package/dist/cjs/functions.d.ts +27 -31
- package/dist/cjs/functions.d.ts.map +1 -1
- package/dist/cjs/functions.js +25 -47
- package/dist/cjs/functions.js.map +1 -1
- package/dist/cjs/index.d.ts +7 -5
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +17 -7
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/package.json +13 -7
- package/dist/cjs/types.d.ts +108 -71
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/esm/bases.d.ts +152 -45
- package/dist/esm/bases.d.ts.map +1 -1
- package/dist/esm/bases.js +88 -75
- package/dist/esm/bases.js.map +1 -1
- package/dist/esm/bufferbase.d.ts +21 -27
- package/dist/esm/bufferbase.d.ts.map +1 -1
- package/dist/esm/bufferbase.js +18 -30
- 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 +25 -111
- package/dist/esm/codec.d.ts.map +1 -1
- package/dist/esm/codec.js +44 -226
- package/dist/esm/codec.js.map +1 -1
- package/dist/esm/codecs.d.ts +8 -59
- package/dist/esm/codecs.d.ts.map +1 -1
- package/dist/esm/codecs.js +7 -80
- package/dist/esm/codecs.js.map +1 -1
- package/dist/esm/errors.d.ts +65 -48
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +83 -50
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/example.js +45 -22
- package/dist/esm/example.js.map +1 -1
- package/dist/esm/functions.d.ts +27 -31
- package/dist/esm/functions.d.ts.map +1 -1
- package/dist/esm/functions.js +26 -48
- package/dist/esm/functions.js.map +1 -1
- package/dist/esm/index.d.ts +7 -5
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -6
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/package.json +13 -7
- package/dist/esm/types.d.ts +108 -71
- package/dist/esm/types.d.ts.map +1 -1
- package/package.json +13 -7
- package/src/algorithms.test.ts +442 -0
- package/src/bases.ts +98 -76
- package/src/bufferbase.ts +26 -36
- 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 +17 -9
- package/src/codec.ts +48 -247
- package/src/codecs.ts +11 -105
- package/src/errors.ts +90 -50
- package/src/example.ts +47 -22
- package/src/functions.ts +34 -54
- package/src/index.ts +27 -7
- package/src/types.ts +113 -86
package/src/bufferbase.ts
CHANGED
|
@@ -1,46 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @deprecated Use the
|
|
3
|
-
* - `encode(buffer, 'base58')` or `base58.encode(buffer)`
|
|
4
|
-
* - `decode(encoded, 'base58')` or `base58.decode(encoded)`
|
|
5
|
-
* - `convert(input, 'base58', 'base64')` or `base58.convertTo(
|
|
6
|
-
* - `validate(input, 'base58')` or `base58.validate(input)`
|
|
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.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
|
-
import { Buffer } from 'node:buffer';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
+
import type { Buffer } from 'node:buffer';
|
|
13
|
+
import type { BufferLike } from './types.js';
|
|
14
|
+
import { RadixCodec } from './codec-radix.js';
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
/** @deprecated Import `Chars` from the package root. */
|
|
14
17
|
export { Chars } from './bases.js';
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
export {
|
|
19
|
+
/** @deprecated Import these from the package root. */
|
|
20
|
+
export { BufferSizeError, InvalidCharacterError } from './errors.js';
|
|
18
21
|
|
|
19
|
-
/**
|
|
20
|
-
* @deprecated Use `Codec` or `createCodec()` instead.
|
|
21
|
-
*/
|
|
22
|
+
/** @deprecated Use `createCodec(alphabet)`. */
|
|
22
23
|
export class BufferEncoder {
|
|
23
|
-
private codec:
|
|
24
|
+
private readonly codec: RadixCodec;
|
|
24
25
|
|
|
25
|
-
constructor(
|
|
26
|
-
this.codec = new
|
|
26
|
+
constructor(baseChars: string) {
|
|
27
|
+
this.codec = new RadixCodec({ alphabet: baseChars, algorithm: 'radix' });
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
encode(buffer:
|
|
30
|
+
encode(buffer: BufferLike): string {
|
|
30
31
|
return this.codec.encode(buffer);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
decode(encoded: string, bufferSize?: number): Buffer {
|
|
34
|
-
return this.codec.decode(encoded, bufferSize
|
|
35
|
+
return this.codec.decode(encoded, bufferSize === undefined ? undefined : { size: bufferSize });
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/**
|
|
39
|
-
* @deprecated Use `convert()` function or `codec.convertTo()` instead.
|
|
40
|
-
*/
|
|
39
|
+
/** @deprecated Use `convert()` or `codec.convertTo()`. */
|
|
41
40
|
export class Converter {
|
|
42
|
-
decoder: BufferEncoder;
|
|
43
|
-
encoder: BufferEncoder;
|
|
41
|
+
readonly decoder: BufferEncoder;
|
|
42
|
+
readonly encoder: BufferEncoder;
|
|
44
43
|
|
|
45
44
|
constructor(inputBase: string, outputBase: string) {
|
|
46
45
|
this.decoder = new BufferEncoder(inputBase);
|
|
@@ -52,24 +51,15 @@ export class Converter {
|
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
/**
|
|
56
|
-
* @deprecated Use `validate()` function or `codec.validate()` instead.
|
|
57
|
-
*/
|
|
54
|
+
/** @deprecated Use `validate()` or `codec.validate()`. */
|
|
58
55
|
export class Validator {
|
|
59
|
-
private codec:
|
|
56
|
+
private readonly codec: RadixCodec;
|
|
60
57
|
|
|
61
58
|
constructor(inputBase: string) {
|
|
62
|
-
this.codec = new
|
|
59
|
+
this.codec = new RadixCodec({ alphabet: inputBase, algorithm: 'radix' });
|
|
63
60
|
}
|
|
64
61
|
|
|
65
62
|
validate(input: string): boolean {
|
|
66
63
|
return this.codec.validate(input);
|
|
67
64
|
}
|
|
68
65
|
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* @deprecated Use `validate()` from the main module instead.
|
|
72
|
-
*/
|
|
73
|
-
export const validate = (input: string, base: string): boolean => {
|
|
74
|
-
return new Validator(base).validate(input);
|
|
75
|
-
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import type { BlockSpec, BufferLike, DecodeOptions } from './types.js';
|
|
3
|
+
import { BaseCodec, toBuffer } from './codec-base.js';
|
|
4
|
+
import {
|
|
5
|
+
BufferSizeError,
|
|
6
|
+
InvalidCharacterError,
|
|
7
|
+
InvalidCodecError,
|
|
8
|
+
InvalidLengthError,
|
|
9
|
+
NonCanonicalError,
|
|
10
|
+
} from './errors.js';
|
|
11
|
+
|
|
12
|
+
const RFC_BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
13
|
+
const RFC_BASE64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
14
|
+
const HEX_LOWER = '0123456789abcdef';
|
|
15
|
+
const HEX_UPPER = '0123456789ABCDEF';
|
|
16
|
+
|
|
17
|
+
/** A Node encoding that produces exactly what this codec would, and whether it needs upper-casing. */
|
|
18
|
+
type NativeEncoder = { readonly encoding: 'base64' | 'base64url' | 'hex'; readonly upper: boolean };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Finds the Node encoding equivalent to a spec, if there is one.
|
|
22
|
+
*
|
|
23
|
+
* Node's encoders are 100-250x faster than the loop below and always emit
|
|
24
|
+
* canonical output, so an exact match is handed off. Decoding is never handed
|
|
25
|
+
* off: Node's decoders are lenient, and rejecting non-canonical input is the
|
|
26
|
+
* point of this codec.
|
|
27
|
+
*/
|
|
28
|
+
function nativeEncoderFor(alphabet: string, pad: string | false): NativeEncoder | null {
|
|
29
|
+
if (alphabet === RFC_BASE64 && pad === '=') {
|
|
30
|
+
return { encoding: 'base64', upper: false };
|
|
31
|
+
}
|
|
32
|
+
if (alphabet === RFC_BASE64URL && pad === false) {
|
|
33
|
+
return { encoding: 'base64url', upper: false };
|
|
34
|
+
}
|
|
35
|
+
if (pad === false && (alphabet === HEX_LOWER || alphabet === HEX_UPPER)) {
|
|
36
|
+
return { encoding: 'hex', upper: alphabet === HEX_UPPER };
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b));
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Regroups the bit stream into fixed-width chunks, as specified by RFC 4648.
|
|
45
|
+
*
|
|
46
|
+
* Each character carries `log2(alphabet.length)` bits, so the alphabet must
|
|
47
|
+
* hold 2, 4, 8, 16, 32, or 64 characters. Unlike a radix conversion the output
|
|
48
|
+
* length is a fixed function of the input length, which is what makes Base64
|
|
49
|
+
* and Base32 splittable and streamable.
|
|
50
|
+
*
|
|
51
|
+
* A trailing partial block leaves spare bits in the final character. RFC 4648
|
|
52
|
+
* §3.5 requires those bits to be zero for the encoding to be canonical; input
|
|
53
|
+
* that sets them is rejected unless the spec passes `strict: false`.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* import { Codecs } from 'bufferbase';
|
|
58
|
+
*
|
|
59
|
+
* Codecs.base64.encode(Buffer.from('fo')); // 'Zm8='
|
|
60
|
+
* Codecs.base64.validate('Zm9='); // false — spare bits are not zero
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export class BlockCodec extends BaseCodec {
|
|
64
|
+
readonly algorithm = 'block' as const;
|
|
65
|
+
|
|
66
|
+
/** Bits carried by one character. */
|
|
67
|
+
private readonly bitsPerChar: number;
|
|
68
|
+
/** Characters in a whole block. */
|
|
69
|
+
private readonly charsPerBlock: number;
|
|
70
|
+
private readonly padChar: string | false;
|
|
71
|
+
private readonly native: NativeEncoder | null;
|
|
72
|
+
|
|
73
|
+
constructor(spec: BlockSpec) {
|
|
74
|
+
super(spec);
|
|
75
|
+
|
|
76
|
+
const bits = Math.log2(spec.alphabet.length);
|
|
77
|
+
if (!Number.isInteger(bits)) {
|
|
78
|
+
throw new InvalidCodecError(
|
|
79
|
+
`'block' needs an alphabet of 2, 4, 8, 16, 32, or 64 characters, got ${spec.alphabet.length}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pad = spec.pad ?? '=';
|
|
84
|
+
if (pad !== false) {
|
|
85
|
+
if (pad.length !== 1) {
|
|
86
|
+
throw new InvalidCodecError(`pad must be a single character, got '${pad}'`);
|
|
87
|
+
}
|
|
88
|
+
if (this.valueOf(pad) !== undefined) {
|
|
89
|
+
throw new InvalidCodecError(`pad character '${pad}' is part of the alphabet`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.bitsPerChar = bits;
|
|
94
|
+
this.charsPerBlock = 8 / gcd(8, bits);
|
|
95
|
+
this.padChar = pad;
|
|
96
|
+
this.native = nativeEncoderFor(spec.alphabet, pad);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
encode(input: BufferLike): string {
|
|
100
|
+
const buffer = toBuffer(input);
|
|
101
|
+
if (this.native !== null) {
|
|
102
|
+
const encoded = buffer.toString(this.native.encoding);
|
|
103
|
+
return this.native.upper ? encoded.toUpperCase() : encoded;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const bits = this.bitsPerChar;
|
|
107
|
+
const mask = this.chars.length - 1;
|
|
108
|
+
const codes = this.codes;
|
|
109
|
+
if (codes === null) {
|
|
110
|
+
return this.encodeAsText(buffer);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const dataChars = Math.ceil((buffer.length * 8) / bits);
|
|
114
|
+
const total =
|
|
115
|
+
this.padChar === false
|
|
116
|
+
? dataChars
|
|
117
|
+
: Math.ceil(dataChars / this.charsPerBlock) * this.charsPerBlock;
|
|
118
|
+
const written = new Uint8Array(total);
|
|
119
|
+
let at = 0;
|
|
120
|
+
let acc = 0;
|
|
121
|
+
let held = 0;
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
124
|
+
acc = ((acc << 8) | buffer[i]) & 0xffff;
|
|
125
|
+
held += 8;
|
|
126
|
+
while (held >= bits) {
|
|
127
|
+
held -= bits;
|
|
128
|
+
written[at++] = codes[(acc >>> held) & mask];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (held > 0) {
|
|
132
|
+
written[at++] = codes[(acc << (bits - held)) & mask];
|
|
133
|
+
}
|
|
134
|
+
if (this.padChar !== false) {
|
|
135
|
+
const padCode = this.padChar.charCodeAt(0);
|
|
136
|
+
while (at < total) {
|
|
137
|
+
written[at++] = padCode;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return this.textFrom(written, at);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The character-by-character path, for an alphabet that is not single-byte. */
|
|
144
|
+
private encodeAsText(buffer: Uint8Array): string {
|
|
145
|
+
const bits = this.bitsPerChar;
|
|
146
|
+
const mask = this.chars.length - 1;
|
|
147
|
+
let out = '';
|
|
148
|
+
let acc = 0;
|
|
149
|
+
let held = 0;
|
|
150
|
+
|
|
151
|
+
for (const byte of buffer) {
|
|
152
|
+
acc = ((acc << 8) | byte) & 0xffff;
|
|
153
|
+
held += 8;
|
|
154
|
+
while (held >= bits) {
|
|
155
|
+
held -= bits;
|
|
156
|
+
out += this.chars[(acc >>> held) & mask];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (held > 0) {
|
|
160
|
+
out += this.chars[(acc << (bits - held)) & mask];
|
|
161
|
+
}
|
|
162
|
+
if (this.padChar !== false) {
|
|
163
|
+
while (out.length % this.charsPerBlock !== 0) {
|
|
164
|
+
out += this.padChar;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
decode(encoded: string, options?: DecodeOptions): Buffer {
|
|
171
|
+
const data = this.checkStructure(this.strip(encoded));
|
|
172
|
+
const bits = this.bitsPerChar;
|
|
173
|
+
const bytes = Buffer.alloc(Math.floor((data.length * bits) / 8));
|
|
174
|
+
let at = 0;
|
|
175
|
+
let acc = 0;
|
|
176
|
+
let held = 0;
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < data.length; i++) {
|
|
179
|
+
const value = this.valueAt(data, i);
|
|
180
|
+
if (value < 0) {
|
|
181
|
+
throw new InvalidCharacterError(data[i]);
|
|
182
|
+
}
|
|
183
|
+
acc = ((acc << bits) | value) & 0xffff;
|
|
184
|
+
held += bits;
|
|
185
|
+
if (held >= 8) {
|
|
186
|
+
held -= 8;
|
|
187
|
+
bytes[at++] = (acc >>> held) & 0xff;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const size = options?.size;
|
|
192
|
+
if (size !== undefined && bytes.length !== size) {
|
|
193
|
+
throw new BufferSizeError(bytes.length, size);
|
|
194
|
+
}
|
|
195
|
+
return bytes;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
validate(input: string): boolean {
|
|
199
|
+
try {
|
|
200
|
+
this.checkStructure(this.strip(input));
|
|
201
|
+
return true;
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Strips and checks the padding, then verifies that what remains can be the
|
|
209
|
+
* encoding of some byte sequence. Returns the data characters.
|
|
210
|
+
*
|
|
211
|
+
* Everything `decode` can reject other than `DecodeOptions.size` is checked
|
|
212
|
+
* here, so `validate` and `decode` always agree.
|
|
213
|
+
*/
|
|
214
|
+
private checkStructure(input: string): string {
|
|
215
|
+
let data = input;
|
|
216
|
+
|
|
217
|
+
if (this.padChar !== false) {
|
|
218
|
+
let dataLength = input.length;
|
|
219
|
+
while (dataLength > 0 && input[dataLength - 1] === this.padChar) {
|
|
220
|
+
dataLength--;
|
|
221
|
+
}
|
|
222
|
+
data = input.slice(0, dataLength);
|
|
223
|
+
if (input.length % this.charsPerBlock !== 0) {
|
|
224
|
+
throw new InvalidLengthError(
|
|
225
|
+
input.length,
|
|
226
|
+
`padded input must be a multiple of ${this.charsPerBlock} characters`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (input.length - dataLength >= this.charsPerBlock) {
|
|
230
|
+
throw new InvalidLengthError(input.length, 'too many padding characters');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const bits = this.bitsPerChar;
|
|
235
|
+
const spare = (data.length * bits) % 8;
|
|
236
|
+
if (spare >= bits) {
|
|
237
|
+
throw new InvalidLengthError(
|
|
238
|
+
data.length,
|
|
239
|
+
`${data.length} characters carry ${data.length * bits} bits, which is not a whole number of bytes`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let last = -1;
|
|
244
|
+
for (let i = 0; i < data.length; i++) {
|
|
245
|
+
last = this.valueAt(data, i);
|
|
246
|
+
if (last < 0) {
|
|
247
|
+
throw new InvalidCharacterError(data[i]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (this.strict && spare > 0 && last >= 0 && (last & ((1 << spare) - 1)) !== 0) {
|
|
252
|
+
throw new NonCanonicalError(
|
|
253
|
+
`the final character carries ${spare} spare bits that must be zero`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return data;
|
|
257
|
+
}
|
|
258
|
+
}
|