Skip to content

Commit 960528f

Browse files
committed
feat!: unite alphabet, join, padding
1 parent 7c6de0a commit 960528f

File tree

2 files changed

+42
-102
lines changed

2 files changed

+42
-102
lines changed

index.ts

Lines changed: 39 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,6 @@ function anumber(n: number): void {
4242
if (!Number.isSafeInteger(n)) throw new Error(`invalid integer: ${n}`);
4343
}
4444

45-
function aArr(input: any[]) {
46-
if (!Array.isArray(input)) throw new Error('array expected');
47-
}
4845
function astrArr(label: string, input: string[]) {
4946
if (!isArrayOf(true, input)) throw new Error(`${label}: array of strings expected`);
5047
}
@@ -80,15 +77,16 @@ function chain<T extends Chain & AsChain<T>>(...args: T): Coder<Input<First<T>>,
8077
}
8178

8279
/**
83-
* Encodes integer radix representation to array of strings using alphabet and back.
84-
* Could also be array of strings.
80+
* Encodes integer radix representation to Uint8Array of indexes in alphabet and back, with optional padding
8581
* @__NO_SIDE_EFFECTS__
8682
*/
87-
function alphabet(letters: string | string[]): Coder<Uint8Array, string[]> {
83+
function alphabet(letters: string, paddingBits: number = 0, paddingChr = '='): Coder<Uint8Array, string> {
8884
// mapping 1 to "b"
85+
astr('alphabet', letters);
8986
const lettersA = typeof letters === 'string' ? letters.split('') : letters;
9087
const len = lettersA.length;
91-
astrArr('alphabet', lettersA);
88+
const paddingCode = paddingChr.codePointAt(0)!;
89+
if (paddingChr.length !== 1 || paddingCode > 128) throw new Error('Wrong padding char');
9290

9391
// mapping "b" to 1
9492
const indexes = new Int8Array(256).fill(-1);
@@ -98,7 +96,7 @@ function alphabet(letters: string | string[]): Coder<Uint8Array, string[]> {
9896
indexes[code] = i;
9997
});
10098
return {
101-
encode: (digits: Uint8Array): string[] => {
99+
encode: (digits: Uint8Array): string => {
102100
abytes(digits);
103101
const out = []
104102
for (const i of digits) {
@@ -108,69 +106,36 @@ function alphabet(letters: string | string[]): Coder<Uint8Array, string[]> {
108106
);
109107
out.push(letters[i]!);
110108
}
111-
return out;
109+
if (paddingBits > 0) {
110+
while ((out.length * paddingBits) % 8) out.push(paddingChr);
111+
}
112+
return out.join('');
112113
},
113-
decode: (input: string[]): Uint8Array => {
114-
aArr(input);
115-
const out = new Uint8Array(input.length);
114+
decode: (str: string): Uint8Array => {
115+
astr('alphabet.decode', str);
116+
let end = str.length;
117+
if (paddingBits > 0) {
118+
if ((end * paddingBits) % 8)
119+
throw new Error('padding: invalid, string should have whole number of bytes');
120+
for (; end > 0 && str.charCodeAt(end - 1) === paddingCode; end--) {
121+
const last = end - 1;
122+
const byte = last * paddingBits;
123+
if (byte % 8 === 0) throw new Error('padding: invalid, string has too much padding');
124+
}
125+
}
126+
const out = new Uint8Array(end);
116127
let at = 0
117-
for (const letter of input) {
118-
astr('alphabet.decode', letter);
119-
const c = letter.codePointAt(0)!;
128+
for (let j = 0; j < end; j++) {
129+
const c = str.charCodeAt(j)!;
120130
const i = indexes[c]!;
121-
if (letter.length !== 1 || c > 127 || i < 0) throw new Error(`Unknown letter: "${letter}". Allowed: ${letters}`);
131+
if (c > 127 || i < 0) throw new Error(`Unknown letter: "${String.fromCharCode(c)}". Allowed: ${letters}`);
122132
out[at++] = i;
123133
}
124134
return out;
125135
},
126136
};
127137
}
128138

129-
/**
130-
* @__NO_SIDE_EFFECTS__
131-
*/
132-
function join(separator = ''): Coder<string[], string> {
133-
astr('join', separator);
134-
return {
135-
encode: (from) => {
136-
astrArr('join.decode', from);
137-
return from.join(separator);
138-
},
139-
decode: (to) => {
140-
astr('join.decode', to);
141-
return to.split(separator);
142-
},
143-
};
144-
}
145-
146-
/**
147-
* Pad strings array so it has integer number of bits
148-
* @__NO_SIDE_EFFECTS__
149-
*/
150-
function padding(bits: number, chr = '='): Coder<string[], string[]> {
151-
anumber(bits);
152-
astr('padding', chr);
153-
return {
154-
encode(data: string[]): string[] {
155-
astrArr('padding.encode', data);
156-
while ((data.length * bits) % 8) data.push(chr);
157-
return data;
158-
},
159-
decode(input: string[]): string[] {
160-
astrArr('padding.decode', input);
161-
let end = input.length;
162-
if ((end * bits) % 8)
163-
throw new Error('padding: invalid, string should have whole number of bytes');
164-
for (; end > 0 && input[end - 1] === chr; end--) {
165-
const last = end - 1;
166-
const byte = last * bits;
167-
if (byte % 8 === 0) throw new Error('padding: invalid, string has too much padding');
168-
}
169-
return input.slice(0, end);
170-
},
171-
};
172-
}
173-
174139
/**
175140
* @__NO_SIDE_EFFECTS__
176141
*/
@@ -347,8 +312,8 @@ function checksum(
347312
}
348313

349314
// prettier-ignore
350-
export const utils: { alphabet: typeof alphabet; chain: typeof chain; checksum: typeof checksum; convertRadix: typeof convertRadix; convertRadix2: typeof convertRadix2; radix: typeof radix; radix2: typeof radix2; join: typeof join; padding: typeof padding; } = {
351-
alphabet, chain, checksum, convertRadix, convertRadix2, radix, radix2, join, padding,
315+
export const utils: { alphabet: typeof alphabet; chain: typeof chain; checksum: typeof checksum; convertRadix: typeof convertRadix; convertRadix2: typeof convertRadix2; radix: typeof radix; radix2: typeof radix2; } = {
316+
alphabet, chain, checksum, convertRadix, convertRadix2, radix, radix2,
352317
};
353318

354319
// RFC 4648 aka RFC 3548
@@ -362,7 +327,7 @@ export const utils: { alphabet: typeof alphabet; chain: typeof chain; checksum:
362327
* // => '12AB'
363328
* ```
364329
*/
365-
export const base16: BytesCoder = chain(radix2(4), alphabet('0123456789ABCDEF'), join(''));
330+
export const base16: BytesCoder = chain(radix2(4), alphabet('0123456789ABCDEF'));
366331

367332
/**
368333
* base32 encoding from RFC 4648. Has padding.
@@ -378,9 +343,7 @@ export const base16: BytesCoder = chain(radix2(4), alphabet('0123456789ABCDEF'),
378343
*/
379344
export const base32: BytesCoder = chain(
380345
radix2(5),
381-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'),
382-
padding(5),
383-
join('')
346+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', 5)
384347
);
385348

386349
/**
@@ -397,8 +360,7 @@ export const base32: BytesCoder = chain(
397360
*/
398361
export const base32nopad: BytesCoder = chain(
399362
radix2(5),
400-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'),
401-
join('')
363+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
402364
);
403365
/**
404366
* base32 encoding from RFC 4648. Padded. Compared to ordinary `base32`, slightly different alphabet.
@@ -413,9 +375,7 @@ export const base32nopad: BytesCoder = chain(
413375
*/
414376
export const base32hex: BytesCoder = chain(
415377
radix2(5),
416-
alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'),
417-
padding(5),
418-
join('')
378+
alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV', 5)
419379
);
420380

421381
/**
@@ -431,8 +391,7 @@ export const base32hex: BytesCoder = chain(
431391
*/
432392
export const base32hexnopad: BytesCoder = chain(
433393
radix2(5),
434-
alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'),
435-
join('')
394+
alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV')
436395
);
437396
/**
438397
* base32 encoding from RFC 4648. Doug Crockford's version.
@@ -448,7 +407,6 @@ export const base32hexnopad: BytesCoder = chain(
448407
export const base32crockford: BytesCoder = chain(
449408
radix2(5),
450409
alphabet('0123456789ABCDEFGHJKMNPQRSTVWXYZ'),
451-
join(''),
452410
normalize((s: string) => s.toUpperCase().replace(/O/g, '0').replace(/[IL]/g, '1'))
453411
);
454412

@@ -489,9 +447,7 @@ export const base64: BytesCoder = hasBase64Builtin ? {
489447
decode(s) { return decodeBase64Builtin(s, false); },
490448
} : chain(
491449
radix2(6),
492-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'),
493-
padding(6),
494-
join('')
450+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 6)
495451
);
496452
/**
497453
* base64 from RFC 4648. No padding.
@@ -506,8 +462,7 @@ export const base64: BytesCoder = hasBase64Builtin ? {
506462
*/
507463
export const base64nopad: BytesCoder = chain(
508464
radix2(6),
509-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'),
510-
join('')
465+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/')
511466
);
512467

513468
/**
@@ -528,9 +483,7 @@ export const base64url: BytesCoder = hasBase64Builtin ? {
528483
decode(s) { return decodeBase64Builtin(s, true); },
529484
} : chain(
530485
radix2(6),
531-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'),
532-
padding(6),
533-
join('')
486+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', 6)
534487
);
535488

536489
/**
@@ -546,14 +499,13 @@ export const base64url: BytesCoder = hasBase64Builtin ? {
546499
*/
547500
export const base64urlnopad: BytesCoder = chain(
548501
radix2(6),
549-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'),
550-
join('')
502+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_')
551503
);
552504

553505
// base58 code
554506
// -----------
555507
const genBase58 = /* @__NO_SIDE_EFFECTS__ */ (abc: string) =>
556-
chain(radix(58), alphabet(abc), join(''));
508+
chain(radix(58), alphabet(abc));
557509

558510
/**
559511
* base58: base64 without ambigous characters +, /, 0, O, I, l.
@@ -642,8 +594,7 @@ export interface Bech32DecodedWithArray<Prefix extends string = string> {
642594
}
643595

644596
const BECH_ALPHABET: Coder<Uint8Array, string> = chain(
645-
alphabet('qpzry9x8gf2tvdw0s3jn54khce6mua7l'),
646-
join('')
597+
alphabet('qpzry9x8gf2tvdw0s3jn54khce6mua7l')
647598
);
648599

649600
const POLYMOD_GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
@@ -822,7 +773,6 @@ export const hex: BytesCoder = hasHexBuiltin
822773
: chain(
823774
radix2(4),
824775
alphabet('0123456789abcdef'),
825-
join(''),
826776
normalize((s: string) => {
827777
if (typeof s !== 'string' || s.length % 2 !== 0)
828778
throw new TypeError(

test/bases.test.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -154,25 +154,15 @@ should('utils: radix', () => {
154154

155155
should('utils: alphabet', () => {
156156
const a = utils.alphabet('12345');
157-
const ab = utils.alphabet(['11', '2', '3', '4', '5']);
158-
eql(a.encode(Uint8Array.of(1)), ['2']);
159-
eql(ab.encode(Uint8Array.of(0)), ['11']);
157+
const ab = utils.alphabet('A2345');
158+
eql(a.encode(Uint8Array.of(1)), '2');
159+
eql(ab.encode(Uint8Array.of(0)), 'A');
160160
eql(a.encode(Uint8Array.of(2)), ab.encode(Uint8Array.of(2)));
161161
throws(() => a.encode([1, 2, true, 3]));
162162
throws(() => a.decode(['1', 2, true]));
163163
throws(() => a.decode(['1', 2]));
164164
throws(() => a.decode(['toString']));
165165
});
166166

167-
should('utils: join', () => {
168-
throws(() => utils.join('1').encode(['1', 1, true]));
169-
});
170-
171-
should('utils: padding', () => {
172-
const coder = utils.padding(4, '=');
173-
throws(() => coder.encode(['1', 1, true]));
174-
throws(() => coder.decode(['1', 1, true, '=']));
175-
});
176-
177167
export { CODERS };
178168
should.runWhen(import.meta.url);

0 commit comments

Comments
 (0)