Skip to content

Commit 334cf40

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

File tree

2 files changed

+44
-118
lines changed

2 files changed

+44
-118
lines changed

index.ts

Lines changed: 41 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,6 @@ function abytes(b: Uint8Array | undefined): void {
1818
if (!isBytes(b)) throw new Error('Uint8Array expected');
1919
}
2020

21-
function isArrayOf(isString: boolean, arr: any[]) {
22-
if (!Array.isArray(arr)) return false;
23-
if (arr.length === 0) return true;
24-
if (isString) {
25-
return arr.every((item) => typeof item === 'string');
26-
} else {
27-
return arr.every((item) => Number.isSafeInteger(item));
28-
}
29-
}
30-
3121
function afn(input: Function): input is Function {
3222
if (typeof input !== 'function') throw new Error('function expected');
3323
return true;
@@ -42,13 +32,6 @@ function anumber(n: number): void {
4232
if (!Number.isSafeInteger(n)) throw new Error(`invalid integer: ${n}`);
4333
}
4434

45-
function aArr(input: any[]) {
46-
if (!Array.isArray(input)) throw new Error('array expected');
47-
}
48-
function astrArr(label: string, input: string[]) {
49-
if (!isArrayOf(true, input)) throw new Error(`${label}: array of strings expected`);
50-
}
51-
5235
// TODO: some recusive type inference so it would check correct order of input/output inside rest?
5336
// like <string, number>, <number, bytes>, <bytes, float>
5437
type Chain = [Coder<any, any>, ...Coder<any, any>[]];
@@ -80,15 +63,16 @@ function chain<T extends Chain & AsChain<T>>(...args: T): Coder<Input<First<T>>,
8063
}
8164

8265
/**
83-
* Encodes integer radix representation to array of strings using alphabet and back.
84-
* Could also be array of strings.
66+
* Encodes integer radix representation in Uint8Array to a string in alphabet and back, with optional padding
8567
* @__NO_SIDE_EFFECTS__
8668
*/
87-
function alphabet(letters: string | string[]): Coder<Uint8Array, string[]> {
69+
function alphabet(letters: string, paddingBits: number = 0, paddingChr = '='): Coder<Uint8Array, string> {
8870
// mapping 1 to "b"
89-
const lettersA = typeof letters === 'string' ? letters.split('') : letters;
71+
astr('alphabet', letters);
72+
const lettersA = letters.split('');
9073
const len = lettersA.length;
91-
astrArr('alphabet', lettersA);
74+
const paddingCode = paddingChr.codePointAt(0)!;
75+
if (paddingChr.length !== 1 || paddingCode > 128) throw new Error('Wrong padding char');
9276

9377
// mapping "b" to 1
9478
const indexes = new Int8Array(256).fill(-1);
@@ -98,79 +82,46 @@ function alphabet(letters: string | string[]): Coder<Uint8Array, string[]> {
9882
indexes[code] = i;
9983
});
10084
return {
101-
encode: (digits: Uint8Array): string[] => {
85+
encode: (digits: Uint8Array): string => {
10286
abytes(digits);
10387
const out = []
10488
for (const i of digits) {
10589
if (i >= len)
10690
throw new Error(
10791
`alphabet.encode: digit index outside alphabet "${i}". Allowed: ${letters}`
10892
);
109-
out.push(letters[i]!);
93+
out.push(lettersA[i]!);
11094
}
111-
return out;
95+
if (paddingBits > 0) {
96+
while ((out.length * paddingBits) % 8) out.push(paddingChr);
97+
}
98+
return out.join('');
11299
},
113-
decode: (input: string[]): Uint8Array => {
114-
aArr(input);
115-
const out = new Uint8Array(input.length);
100+
decode: (str: string): Uint8Array => {
101+
astr('alphabet.decode', str);
102+
let end = str.length;
103+
if (paddingBits > 0) {
104+
if ((end * paddingBits) % 8)
105+
throw new Error('padding: invalid, string should have whole number of bytes');
106+
for (; end > 0 && str.charCodeAt(end - 1) === paddingCode; end--) {
107+
const last = end - 1;
108+
const byte = last * paddingBits;
109+
if (byte % 8 === 0) throw new Error('padding: invalid, string has too much padding');
110+
}
111+
}
112+
const out = new Uint8Array(end);
116113
let at = 0
117-
for (const letter of input) {
118-
astr('alphabet.decode', letter);
119-
const c = letter.codePointAt(0)!;
114+
for (let j = 0; j < end; j++) {
115+
const c = str.charCodeAt(j)!;
120116
const i = indexes[c]!;
121-
if (letter.length !== 1 || c > 127 || i < 0) throw new Error(`Unknown letter: "${letter}". Allowed: ${letters}`);
117+
if (c > 127 || i < 0) throw new Error(`Unknown letter: "${String.fromCharCode(c)}". Allowed: ${letters}`);
122118
out[at++] = i;
123119
}
124120
return out;
125121
},
126122
};
127123
}
128124

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-
174125
/**
175126
* @__NO_SIDE_EFFECTS__
176127
*/
@@ -347,8 +298,8 @@ function checksum(
347298
}
348299

349300
// 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,
301+
export const utils: { alphabet: typeof alphabet; chain: typeof chain; checksum: typeof checksum; convertRadix: typeof convertRadix; convertRadix2: typeof convertRadix2; radix: typeof radix; radix2: typeof radix2; } = {
302+
alphabet, chain, checksum, convertRadix, convertRadix2, radix, radix2,
352303
};
353304

354305
// RFC 4648 aka RFC 3548
@@ -362,7 +313,7 @@ export const utils: { alphabet: typeof alphabet; chain: typeof chain; checksum:
362313
* // => '12AB'
363314
* ```
364315
*/
365-
export const base16: BytesCoder = chain(radix2(4), alphabet('0123456789ABCDEF'), join(''));
316+
export const base16: BytesCoder = chain(radix2(4), alphabet('0123456789ABCDEF'));
366317

367318
/**
368319
* base32 encoding from RFC 4648. Has padding.
@@ -378,9 +329,7 @@ export const base16: BytesCoder = chain(radix2(4), alphabet('0123456789ABCDEF'),
378329
*/
379330
export const base32: BytesCoder = chain(
380331
radix2(5),
381-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'),
382-
padding(5),
383-
join('')
332+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', 5)
384333
);
385334

386335
/**
@@ -397,8 +346,7 @@ export const base32: BytesCoder = chain(
397346
*/
398347
export const base32nopad: BytesCoder = chain(
399348
radix2(5),
400-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'),
401-
join('')
349+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
402350
);
403351
/**
404352
* base32 encoding from RFC 4648. Padded. Compared to ordinary `base32`, slightly different alphabet.
@@ -413,9 +361,7 @@ export const base32nopad: BytesCoder = chain(
413361
*/
414362
export const base32hex: BytesCoder = chain(
415363
radix2(5),
416-
alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'),
417-
padding(5),
418-
join('')
364+
alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV', 5)
419365
);
420366

421367
/**
@@ -431,8 +377,7 @@ export const base32hex: BytesCoder = chain(
431377
*/
432378
export const base32hexnopad: BytesCoder = chain(
433379
radix2(5),
434-
alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'),
435-
join('')
380+
alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV')
436381
);
437382
/**
438383
* base32 encoding from RFC 4648. Doug Crockford's version.
@@ -448,7 +393,6 @@ export const base32hexnopad: BytesCoder = chain(
448393
export const base32crockford: BytesCoder = chain(
449394
radix2(5),
450395
alphabet('0123456789ABCDEFGHJKMNPQRSTVWXYZ'),
451-
join(''),
452396
normalize((s: string) => s.toUpperCase().replace(/O/g, '0').replace(/[IL]/g, '1'))
453397
);
454398

@@ -489,9 +433,7 @@ export const base64: BytesCoder = hasBase64Builtin ? {
489433
decode(s) { return decodeBase64Builtin(s, false); },
490434
} : chain(
491435
radix2(6),
492-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'),
493-
padding(6),
494-
join('')
436+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 6)
495437
);
496438
/**
497439
* base64 from RFC 4648. No padding.
@@ -506,8 +448,7 @@ export const base64: BytesCoder = hasBase64Builtin ? {
506448
*/
507449
export const base64nopad: BytesCoder = chain(
508450
radix2(6),
509-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'),
510-
join('')
451+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/')
511452
);
512453

513454
/**
@@ -528,9 +469,7 @@ export const base64url: BytesCoder = hasBase64Builtin ? {
528469
decode(s) { return decodeBase64Builtin(s, true); },
529470
} : chain(
530471
radix2(6),
531-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'),
532-
padding(6),
533-
join('')
472+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', 6)
534473
);
535474

536475
/**
@@ -546,14 +485,13 @@ export const base64url: BytesCoder = hasBase64Builtin ? {
546485
*/
547486
export const base64urlnopad: BytesCoder = chain(
548487
radix2(6),
549-
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'),
550-
join('')
488+
alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_')
551489
);
552490

553491
// base58 code
554492
// -----------
555493
const genBase58 = /* @__NO_SIDE_EFFECTS__ */ (abc: string) =>
556-
chain(radix(58), alphabet(abc), join(''));
494+
chain(radix(58), alphabet(abc));
557495

558496
/**
559497
* base58: base64 without ambigous characters +, /, 0, O, I, l.
@@ -642,8 +580,7 @@ export interface Bech32DecodedWithArray<Prefix extends string = string> {
642580
}
643581

644582
const BECH_ALPHABET: Coder<Uint8Array, string> = chain(
645-
alphabet('qpzry9x8gf2tvdw0s3jn54khce6mua7l'),
646-
join('')
583+
alphabet('qpzry9x8gf2tvdw0s3jn54khce6mua7l')
647584
);
648585

649586
const POLYMOD_GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
@@ -822,7 +759,6 @@ export const hex: BytesCoder = hasHexBuiltin
822759
: chain(
823760
radix2(4),
824761
alphabet('0123456789abcdef'),
825-
join(''),
826762
normalize((s: string) => {
827763
if (typeof s !== 'string' || s.length % 2 !== 0)
828764
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)