From 6213fe78a1f6860f64dc7a53a6ad8e62c328f172 Mon Sep 17 00:00:00 2001 From: Nikita Skovoroda Date: Tue, 14 Oct 2025 10:40:27 +0300 Subject: [PATCH 1/4] feat: use Uint8Array in alphabet() --- index.ts | 127 +++++++++++++++++++++----------------------- test/bases.test.ts | 26 ++++----- test/bech32.test.ts | 12 ++++- test/bip173.test.ts | 6 ++- 4 files changed, 86 insertions(+), 85 deletions(-) diff --git a/index.ts b/index.ts index 9a48a7a..95f6a30 100644 --- a/index.ts +++ b/index.ts @@ -48,9 +48,6 @@ function aArr(input: any[]) { function astrArr(label: string, input: string[]) { if (!isArrayOf(true, input)) throw new Error(`${label}: array of strings expected`); } -function anumArr(label: string, input: number[]) { - if (!isArrayOf(false, input)) throw new Error(`${label}: array of numbers expected`); -} // TODO: some recusive type inference so it would check correct order of input/output inside rest? // like , , @@ -87,7 +84,7 @@ function chain>(...args: T): Coder>, * Could also be array of strings. * @__NO_SIDE_EFFECTS__ */ -function alphabet(letters: string | string[]): Coder { +function alphabet(letters: string | string[]): Coder { // mapping 1 to "b" const lettersA = typeof letters === 'string' ? letters.split('') : letters; const len = lettersA.length; @@ -96,19 +93,19 @@ function alphabet(letters: string | string[]): Coder { // mapping "b" to 1 const indexes = new Map(lettersA.map((l, i) => [l, i])); return { - encode: (digits: number[]) => { - aArr(digits); - return digits.map((i) => { - if (!Number.isSafeInteger(i) || i < 0 || i >= len) + encode: (digits: Uint8Array): string[] => { + abytes(digits); + return Array.from(digits, (i) => { + if (i >= len) throw new Error( `alphabet.encode: digit index outside alphabet "${i}". Allowed: ${letters}` ); - return lettersA[i]!; + return letters[i]!; }); }, - decode: (input: string[]): number[] => { + decode: (input: string[]): Uint8Array => { aArr(input); - return input.map((letter) => { + return Uint8Array.from(input, (letter) => { astr('alphabet.decode', letter); const i = indexes.get(letter); if (i === undefined) throw new Error(`Unknown letter: "${letter}". Allowed: ${letters}`); @@ -174,20 +171,24 @@ function normalize(fn: (val: T) => T): Coder { /** * Slow: O(n^2) time complexity */ -function convertRadix(data: number[], from: number, to: number): number[] { +function convertRadix(data: Uint8Array, from: number, to: number): Uint8Array { // base 1 is impossible - if (from < 2) throw new Error(`convertRadix: invalid from=${from}, base cannot be less than 2`); - if (to < 2) throw new Error(`convertRadix: invalid to=${to}, base cannot be less than 2`); - aArr(data); - if (!data.length) return []; + if (from < 2 || from > 256) throw new Error(`convertRadix: wrong from=${from}`); + if (to < 2 || to > 256) throw new Error(`convertRadix: wrong to=${to}`); + abytes(data); + if (!data.length) return new Uint8Array(); let pos = 0; - const res = []; - const digits = Array.from(data, (d) => { - anumber(d); - if (d < 0 || d >= from) throw new Error(`invalid integer: ${d}`); + const digits = Uint8Array.from(data, (d) => { + if (d >= from) throw new Error(`invalid integer: ${d}`); return d; }); const dlen = digits.length; + let zeros = 0 + while (zeros < dlen && data[zeros] === 0) zeros++ + if (zeros === dlen) return new Uint8Array(zeros); + const significant = dlen - zeros; + const res = new Uint8Array(zeros + 1 + Math.ceil(significant * Math.log(from) / Math.log(to))); // + 1 to overshoot due to float calculation + let writtenLow = res.length; while (true) { let carry = 0; let done = true; @@ -212,16 +213,14 @@ function convertRadix(data: number[], from: number, to: number): number[] { else if (!rounded) pos = i; else done = false; } - res.push(carry); + res[--writtenLow] = carry; if (done) break; } - for (let i = 0; i < data.length - 1 && data[i] === 0; i++) res.push(0); - return res.reverse(); + writtenLow -= zeros; + if (writtenLow < 0) throw new Error('convertRadix: data did not fit'); // unreachable + return res.subarray(writtenLow); } -const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b)); -const radix2carry = /* @__NO_SIDE_EFFECTS__ */ (from: number, to: number) => - from + (to - gcd(from, to)); const powers: number[] = /* @__PURE__ */ (() => { let res = []; for (let i = 0; i < 40; i++) res.push(2 ** i); @@ -230,27 +229,25 @@ const powers: number[] = /* @__PURE__ */ (() => { /** * Implemented with numbers, because BigInt is 5x slower */ -function convertRadix2(data: number[], from: number, to: number, padding: boolean): number[] { - aArr(data); - if (from <= 0 || from > 32) throw new Error(`convertRadix2: wrong from=${from}`); - if (to <= 0 || to > 32) throw new Error(`convertRadix2: wrong to=${to}`); - if (radix2carry(from, to) > 32) { - throw new Error( - `convertRadix2: carry overflow from=${from} to=${to} carryBits=${radix2carry(from, to)}` - ); - } +function convertRadix2(data: Uint8Array, from: number, to: number, padding: boolean): Uint8Array { + abytes(data); + if (from <= 0 || from > 8) throw new Error(`convertRadix2: wrong from=${from}`); + if (to <= 0 || to > 8) throw new Error(`convertRadix2: wrong to=${to}`); let carry = 0; let pos = 0; // bitwise position in current element const max = powers[from]!; const mask = powers[to]! - 1; - const res: number[] = []; + const dataLength = data.length; + const bits = dataLength * from; + if (!Number.isSafeInteger(bits)) throw new Error('Input too large'); // math safeguard, nothing below 32 TiB should trigger this + const res = new Uint8Array(Math.ceil(bits / to)); + let written = 0; for (const n of data) { - anumber(n); if (n >= max) throw new Error(`convertRadix2: invalid data word=${n} from=${from}`); carry = (carry << from) | n; if (pos + from > 32) throw new Error(`convertRadix2: carry overflow pos=${pos} from=${from}`); pos += from; - for (; pos >= to; pos -= to) res.push(((carry >> (pos - to)) & mask) >>> 0); + for (; pos >= to; pos -= to) res[written++] = ((carry >> (pos - to)) & mask) >>> 0; const pow = powers[pos]; if (pow === undefined) throw new Error('invalid carry'); carry &= pow - 1; // clean carry, otherwise it will cause overflow @@ -258,24 +255,25 @@ function convertRadix2(data: number[], from: number, to: number, padding: boolea carry = (carry << (to - pos)) & mask; if (!padding && pos >= from) throw new Error('Excess padding'); if (!padding && carry > 0) throw new Error(`Non-zero padding: ${carry}`); - if (padding && pos > 0) res.push(carry >>> 0); - return res; + if (padding && pos > 0) res[written++] = carry >>> 0; + if (written > res.length) throw new Error('convertRadix2: data did not fit'); // unreachable + return res.subarray(0, written); } /** * @__NO_SIDE_EFFECTS__ */ -function radix(num: number): Coder { +function radix(num: number): Coder { anumber(num); const _256 = 2 ** 8; return { encode: (bytes: Uint8Array) => { if (!isBytes(bytes)) throw new Error('radix.encode input should be Uint8Array'); - return convertRadix(Array.from(bytes), _256, num); + return convertRadix(bytes, _256, num); }, - decode: (digits: number[]) => { - anumArr('radix.decode', digits); - return Uint8Array.from(convertRadix(digits, num, _256)); + decode: (digits: Uint8Array) => { + if (!isBytes(digits)) throw new Error('radix.decode input should be Uint8Array'); + return convertRadix(digits, num, _256); }, }; } @@ -285,19 +283,17 @@ function radix(num: number): Coder { * there is a linear algorithm. For now we have implementation for power-of-two bases only. * @__NO_SIDE_EFFECTS__ */ -function radix2(bits: number, revPadding = false): Coder { +function radix2(bits: number, revPadding = false): Coder { anumber(bits); - if (bits <= 0 || bits > 32) throw new Error('radix2: bits should be in (0..32]'); - if (radix2carry(8, bits) > 32 || radix2carry(bits, 8) > 32) - throw new Error('radix2: carry overflow'); + if (bits <= 0 || bits > 8) throw new Error('radix2: bits should be in (0..8]'); return { encode: (bytes: Uint8Array) => { if (!isBytes(bytes)) throw new Error('radix2.encode input should be Uint8Array'); - return convertRadix2(Array.from(bytes), 8, bits, !revPadding); + return convertRadix2(bytes, 8, bits, !revPadding); }, - decode: (digits: number[]) => { - anumArr('radix2.decode', digits); - return Uint8Array.from(convertRadix2(digits, bits, 8, revPadding)); + decode: (digits: Uint8Array) => { + if (!isBytes(digits)) throw new Error('radix2.decode input should be Uint8Array'); + return convertRadix2(digits, bits, 8, revPadding); }, }; } @@ -626,15 +622,15 @@ export const base58check: (sha256: (data: Uint8Array) => Uint8Array) => BytesCod // ----------- export interface Bech32Decoded { prefix: Prefix; - words: number[]; + words: Uint8Array; } export interface Bech32DecodedWithArray { prefix: Prefix; - words: number[]; + words: Uint8Array; bytes: Uint8Array; } -const BECH_ALPHABET: Coder = chain( +const BECH_ALPHABET: Coder = chain( alphabet('qpzry9x8gf2tvdw0s3jn54khce6mua7l'), join('') ); @@ -649,7 +645,7 @@ function bech32Polymod(pre: number): number { return chk; } -function bechChecksum(prefix: string, words: number[], encodingConst = 1): string { +function bechChecksum(prefix: string, words: Uint8Array, encodingConst = 1): string { const len = prefix.length; let chk = 1; for (let i = 0; i < len; i++) { @@ -662,13 +658,15 @@ function bechChecksum(prefix: string, words: number[], encodingConst = 1): strin for (let v of words) chk = bech32Polymod(chk) ^ v; for (let i = 0; i < 6; i++) chk = bech32Polymod(chk); chk ^= encodingConst; - return BECH_ALPHABET.encode(convertRadix2([chk % powers[30]!], 30, 5, false)); + const v = chk & 0x3fffffff; // 30 bits, we need to convert to 6 5-bit values + const v5bit = Uint8Array.of(v >>> 25, (v >>> 20) & 0x1f, (v >>> 15) & 0x1f, (v >>> 10) & 0x1f, (v >>> 5) & 0x1f, v & 0x1f); + return BECH_ALPHABET.encode(v5bit); } export interface Bech32 { encode( prefix: Prefix, - words: number[] | Uint8Array, + words: Uint8Array, limit?: number | false ): `${Lowercase}1${string}`; decode( @@ -679,9 +677,9 @@ export interface Bech32 { encodeFromBytes(prefix: string, bytes: Uint8Array): string; decodeToBytes(str: string): Bech32DecodedWithArray; decodeUnsafe(str: string, limit?: number | false): void | Bech32Decoded; - fromWords(to: number[]): Uint8Array; - fromWordsUnsafe(to: number[]): void | Uint8Array; - toWords(from: Uint8Array): number[]; + fromWords(to: Uint8Array): Uint8Array; + fromWordsUnsafe(to: Uint8Array): void | Uint8Array; + toWords(from: Uint8Array): Uint8Array; } /** * @__NO_SIDE_EFFECTS__ @@ -695,12 +693,11 @@ function genBech32(encoding: 'bech32' | 'bech32m'): Bech32 { function encode( prefix: Prefix, - words: number[] | Uint8Array, + words: Uint8Array, limit: number | false = 90 ): `${Lowercase}1${string}` { astr('bech32.encode prefix', prefix); - if (isBytes(words)) words = Array.from(words); - anumArr('bech32.encode', words); + if (!isBytes(words)) throw new Error('bech32.encode: input should be Uint8Array'); const plen = prefix.length; if (plen === 0) throw new TypeError(`Invalid prefix length ${plen}`); const actualLength = plen + 7 + words.length; diff --git a/test/bases.test.ts b/test/bases.test.ts index 9007e4a..d52687f 100644 --- a/test/bases.test.ts +++ b/test/bases.test.ts @@ -125,13 +125,8 @@ should('utils: radix2', () => { ); }; throws(() => t(0)); - for (let i = 1; i < 27; i++) t(i); - throws(() => t(27)); // 34 bits - t(28); - throws(() => t(29)); // 36 bits - throws(() => t(30)); // 36 bits - throws(() => t(31)); // 38 bits - t(32); // ok + for (let i = 1; i < 8; i++) t(i); + for (let i = 9; i < 40; i++) throws(() => t(i)); // true is not a number throws(() => utils.radix2(4).decode([1, true, 1, 1])); }); @@ -149,13 +144,10 @@ should('utils: radix', () => { ); }; throws(() => t(1)); - for (let i = 1; i < 46; i++) t(2 ** i); - for (let i = 2; i < 46; i++) t(2 ** i - 1); - for (let i = 1; i < 46; i++) t(2 ** i + 1); - // carry overflows here - t(35195299949887); - throws(() => t(35195299949887 + 1)); - throws(() => t(2 ** i)); + for (let i = 1; i <= 8; i++) t(2 ** i); + for (let i = 2; i <= 8; i++) t(2 ** i - 1); + for (let i = 1; i < 8; i++) t(2 ** i + 1); + throws(() => t(2 ** 8 + 1)); // true is not a number throws(() => utils.radix(2 ** 4).decode([1, true, 1, 1])); }); @@ -163,9 +155,9 @@ should('utils: radix', () => { should('utils: alphabet', () => { const a = utils.alphabet('12345'); const ab = utils.alphabet(['11', '2', '3', '4', '5']); - eql(a.encode([1]), ['2']); - eql(ab.encode([0]), ['11']); - eql(a.encode([2]), ab.encode([2])); + eql(a.encode(Uint8Array.of(1)), ['2']); + eql(ab.encode(Uint8Array.of(0)), ['11']); + eql(a.encode(Uint8Array.of(2)), ab.encode(Uint8Array.of(2))); throws(() => a.encode([1, 2, true, 3])); throws(() => a.decode(['1', 2, true])); throws(() => a.decode(['1', 2])); diff --git a/test/bech32.test.ts b/test/bech32.test.ts index 01ea2c8..f426f08 100644 --- a/test/bech32.test.ts +++ b/test/bech32.test.ts @@ -276,6 +276,15 @@ const VALID_WORDS = [ }, ]; +// Convert fixtures to Uint8Array +for (const fixture of [BECH32_VALID, BECH32_INVALID_ENCODE, BECH32M_VALID, BECH32M_INVALID_ENCODE, VALID_WORDS]) { + for (const v of fixture) { + const u8 = Uint8Array.from(v.words) + eql(v.words, Array.from(u8)) + v.words = u8 + } +} + for (let v of BECH32M_VALID) { should(`encode ${v.prefix} ${v.words}`, () => { eql(bech32m.encode(v.prefix, v.words, v.limit), v.string.toLowerCase()); @@ -367,6 +376,7 @@ for (let v of VALID_WORDS) { for (let v of INVALID_WORDS) { should(`throw om fromWords`, () => { + v = Uint8Array.from(v) eql(bech32.fromWordsUnsafe(v), undefined); throws(() => bech32.fromWords(v)); }); @@ -375,7 +385,7 @@ for (let v of INVALID_WORDS) { should('toWords/toWordsUnsafe accept Uint8Array', () => { const bytes = new Uint8Array([0x00, 0x11, 0x22, 0x33, 0xff]); const words = bech32.toWords(bytes); - eql(words, [0, 0, 8, 18, 4, 12, 31, 31]); + eql(words, Uint8Array.of(0, 0, 8, 18, 4, 12, 31, 31)); }); should('encode accepts Uint8Array', () => { diff --git a/test/bip173.test.ts b/test/bip173.test.ts index 68cda7e..b01ca8c 100644 --- a/test/bip173.test.ts +++ b/test/bip173.test.ts @@ -51,7 +51,8 @@ function decodeBtc(address) { // with actual witnes program words in rest // BIP-141: The value of the first push is called the "version byte". // The following byte vector pushed is called the "witness program". - const [ver, ...dataW] = decoded.words; + const [ver] = decoded.words; + const dataW = decoded.words.subarray(1) // MUST verify that the first decoded data value (the witness version) // is between 0 and 16, inclusive. if (ver < 0 || ver > 16) throw new Error('wrong version'); @@ -186,7 +187,8 @@ function decodeBtc350(address) { if (!decoded) throw err; // The human-readable part "bc"[7] for mainnet, and "tb"[8] for testnet. if (!['bc', 'tb'].includes(decoded.prefix)) throw new Error('Invalid prefix'); - const [ver, ...dataW] = decoded.words; + const [ver] = decoded.words; + const dataW = decoded.words.subarray(1); if (isb32m && ver === 0) throw new Error('Witness program version 0 should use bech32'); if (!isb32m && ver >= 1) throw new Error('Witness program with version >=1 should use bech32m'); // MUST verify that the first decoded data value (the witness version) From e95be95fd893058f52d9e6a023c911d8d2e45fd2 Mon Sep 17 00:00:00 2001 From: Nikita Skovoroda Date: Tue, 14 Oct 2025 10:42:30 +0300 Subject: [PATCH 2/4] perf: use for loop in alphabet --- index.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/index.ts b/index.ts index 95f6a30..1797c5f 100644 --- a/index.ts +++ b/index.ts @@ -95,22 +95,27 @@ function alphabet(letters: string | string[]): Coder { return { encode: (digits: Uint8Array): string[] => { abytes(digits); - return Array.from(digits, (i) => { + const out = [] + for (const i of digits) { if (i >= len) throw new Error( `alphabet.encode: digit index outside alphabet "${i}". Allowed: ${letters}` ); - return letters[i]!; - }); + out.push(letters[i]!); + } + return out; }, decode: (input: string[]): Uint8Array => { aArr(input); - return Uint8Array.from(input, (letter) => { + const out = new Uint8Array(input.length); + let at = 0 + for (const letter of input) { astr('alphabet.decode', letter); const i = indexes.get(letter); if (i === undefined) throw new Error(`Unknown letter: "${letter}". Allowed: ${letters}`); - return i; - }); + out[at++] = i; + } + return out; }, }; } From 7c6de0a2e3474aa04d1d32836b49c08ecd19f1b2 Mon Sep 17 00:00:00 2001 From: Nikita Skovoroda Date: Tue, 14 Oct 2025 10:49:37 +0300 Subject: [PATCH 3/4] perf: use Int8Array instead of Map in alphabet.decode --- index.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index 1797c5f..81e9711 100644 --- a/index.ts +++ b/index.ts @@ -91,7 +91,12 @@ function alphabet(letters: string | string[]): Coder { astrArr('alphabet', lettersA); // mapping "b" to 1 - const indexes = new Map(lettersA.map((l, i) => [l, i])); + const indexes = new Int8Array(256).fill(-1); + lettersA.forEach((l, i) => { + const code = l.codePointAt(0)!; + if (code > 127 || indexes[code] !== -1) throw new Error(`Non-ascii or duplicate symbol: "${l}"`); + indexes[code] = i; + }); return { encode: (digits: Uint8Array): string[] => { abytes(digits); @@ -111,8 +116,9 @@ function alphabet(letters: string | string[]): Coder { let at = 0 for (const letter of input) { astr('alphabet.decode', letter); - const i = indexes.get(letter); - if (i === undefined) throw new Error(`Unknown letter: "${letter}". Allowed: ${letters}`); + const c = letter.codePointAt(0)!; + const i = indexes[c]!; + if (letter.length !== 1 || c > 127 || i < 0) throw new Error(`Unknown letter: "${letter}". Allowed: ${letters}`); out[at++] = i; } return out; From 334cf40d78ae780a62bc58f3e901d67687559afc Mon Sep 17 00:00:00 2001 From: Nikita Skovoroda Date: Tue, 14 Oct 2025 14:46:24 +0300 Subject: [PATCH 4/4] feat!: unite alphabet, join, padding --- index.ts | 146 +++++++++++++-------------------------------- test/bases.test.ts | 16 +---- 2 files changed, 44 insertions(+), 118 deletions(-) diff --git a/index.ts b/index.ts index 81e9711..4267da3 100644 --- a/index.ts +++ b/index.ts @@ -18,16 +18,6 @@ function abytes(b: Uint8Array | undefined): void { if (!isBytes(b)) throw new Error('Uint8Array expected'); } -function isArrayOf(isString: boolean, arr: any[]) { - if (!Array.isArray(arr)) return false; - if (arr.length === 0) return true; - if (isString) { - return arr.every((item) => typeof item === 'string'); - } else { - return arr.every((item) => Number.isSafeInteger(item)); - } -} - function afn(input: Function): input is Function { if (typeof input !== 'function') throw new Error('function expected'); return true; @@ -42,13 +32,6 @@ function anumber(n: number): void { if (!Number.isSafeInteger(n)) throw new Error(`invalid integer: ${n}`); } -function aArr(input: any[]) { - if (!Array.isArray(input)) throw new Error('array expected'); -} -function astrArr(label: string, input: string[]) { - if (!isArrayOf(true, input)) throw new Error(`${label}: array of strings expected`); -} - // TODO: some recusive type inference so it would check correct order of input/output inside rest? // like , , type Chain = [Coder, ...Coder[]]; @@ -80,15 +63,16 @@ function chain>(...args: T): Coder>, } /** - * Encodes integer radix representation to array of strings using alphabet and back. - * Could also be array of strings. + * Encodes integer radix representation in Uint8Array to a string in alphabet and back, with optional padding * @__NO_SIDE_EFFECTS__ */ -function alphabet(letters: string | string[]): Coder { +function alphabet(letters: string, paddingBits: number = 0, paddingChr = '='): Coder { // mapping 1 to "b" - const lettersA = typeof letters === 'string' ? letters.split('') : letters; + astr('alphabet', letters); + const lettersA = letters.split(''); const len = lettersA.length; - astrArr('alphabet', lettersA); + const paddingCode = paddingChr.codePointAt(0)!; + if (paddingChr.length !== 1 || paddingCode > 128) throw new Error('Wrong padding char'); // mapping "b" to 1 const indexes = new Int8Array(256).fill(-1); @@ -98,7 +82,7 @@ function alphabet(letters: string | string[]): Coder { indexes[code] = i; }); return { - encode: (digits: Uint8Array): string[] => { + encode: (digits: Uint8Array): string => { abytes(digits); const out = [] for (const i of digits) { @@ -106,19 +90,31 @@ function alphabet(letters: string | string[]): Coder { throw new Error( `alphabet.encode: digit index outside alphabet "${i}". Allowed: ${letters}` ); - out.push(letters[i]!); + out.push(lettersA[i]!); } - return out; + if (paddingBits > 0) { + while ((out.length * paddingBits) % 8) out.push(paddingChr); + } + return out.join(''); }, - decode: (input: string[]): Uint8Array => { - aArr(input); - const out = new Uint8Array(input.length); + decode: (str: string): Uint8Array => { + astr('alphabet.decode', str); + let end = str.length; + if (paddingBits > 0) { + if ((end * paddingBits) % 8) + throw new Error('padding: invalid, string should have whole number of bytes'); + for (; end > 0 && str.charCodeAt(end - 1) === paddingCode; end--) { + const last = end - 1; + const byte = last * paddingBits; + if (byte % 8 === 0) throw new Error('padding: invalid, string has too much padding'); + } + } + const out = new Uint8Array(end); let at = 0 - for (const letter of input) { - astr('alphabet.decode', letter); - const c = letter.codePointAt(0)!; + for (let j = 0; j < end; j++) { + const c = str.charCodeAt(j)!; const i = indexes[c]!; - if (letter.length !== 1 || c > 127 || i < 0) throw new Error(`Unknown letter: "${letter}". Allowed: ${letters}`); + if (c > 127 || i < 0) throw new Error(`Unknown letter: "${String.fromCharCode(c)}". Allowed: ${letters}`); out[at++] = i; } return out; @@ -126,51 +122,6 @@ function alphabet(letters: string | string[]): Coder { }; } -/** - * @__NO_SIDE_EFFECTS__ - */ -function join(separator = ''): Coder { - astr('join', separator); - return { - encode: (from) => { - astrArr('join.decode', from); - return from.join(separator); - }, - decode: (to) => { - astr('join.decode', to); - return to.split(separator); - }, - }; -} - -/** - * Pad strings array so it has integer number of bits - * @__NO_SIDE_EFFECTS__ - */ -function padding(bits: number, chr = '='): Coder { - anumber(bits); - astr('padding', chr); - return { - encode(data: string[]): string[] { - astrArr('padding.encode', data); - while ((data.length * bits) % 8) data.push(chr); - return data; - }, - decode(input: string[]): string[] { - astrArr('padding.decode', input); - let end = input.length; - if ((end * bits) % 8) - throw new Error('padding: invalid, string should have whole number of bytes'); - for (; end > 0 && input[end - 1] === chr; end--) { - const last = end - 1; - const byte = last * bits; - if (byte % 8 === 0) throw new Error('padding: invalid, string has too much padding'); - } - return input.slice(0, end); - }, - }; -} - /** * @__NO_SIDE_EFFECTS__ */ @@ -347,8 +298,8 @@ function checksum( } // prettier-ignore -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; } = { - alphabet, chain, checksum, convertRadix, convertRadix2, radix, radix2, join, padding, +export const utils: { alphabet: typeof alphabet; chain: typeof chain; checksum: typeof checksum; convertRadix: typeof convertRadix; convertRadix2: typeof convertRadix2; radix: typeof radix; radix2: typeof radix2; } = { + alphabet, chain, checksum, convertRadix, convertRadix2, radix, radix2, }; // RFC 4648 aka RFC 3548 @@ -362,7 +313,7 @@ export const utils: { alphabet: typeof alphabet; chain: typeof chain; checksum: * // => '12AB' * ``` */ -export const base16: BytesCoder = chain(radix2(4), alphabet('0123456789ABCDEF'), join('')); +export const base16: BytesCoder = chain(radix2(4), alphabet('0123456789ABCDEF')); /** * base32 encoding from RFC 4648. Has padding. @@ -378,9 +329,7 @@ export const base16: BytesCoder = chain(radix2(4), alphabet('0123456789ABCDEF'), */ export const base32: BytesCoder = chain( radix2(5), - alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), - padding(5), - join('') + alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', 5) ); /** @@ -397,8 +346,7 @@ export const base32: BytesCoder = chain( */ export const base32nopad: BytesCoder = chain( radix2(5), - alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), - join('') + alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567') ); /** * base32 encoding from RFC 4648. Padded. Compared to ordinary `base32`, slightly different alphabet. @@ -413,9 +361,7 @@ export const base32nopad: BytesCoder = chain( */ export const base32hex: BytesCoder = chain( radix2(5), - alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'), - padding(5), - join('') + alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV', 5) ); /** @@ -431,8 +377,7 @@ export const base32hex: BytesCoder = chain( */ export const base32hexnopad: BytesCoder = chain( radix2(5), - alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'), - join('') + alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV') ); /** * base32 encoding from RFC 4648. Doug Crockford's version. @@ -448,7 +393,6 @@ export const base32hexnopad: BytesCoder = chain( export const base32crockford: BytesCoder = chain( radix2(5), alphabet('0123456789ABCDEFGHJKMNPQRSTVWXYZ'), - join(''), normalize((s: string) => s.toUpperCase().replace(/O/g, '0').replace(/[IL]/g, '1')) ); @@ -489,9 +433,7 @@ export const base64: BytesCoder = hasBase64Builtin ? { decode(s) { return decodeBase64Builtin(s, false); }, } : chain( radix2(6), - alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'), - padding(6), - join('') + alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 6) ); /** * base64 from RFC 4648. No padding. @@ -506,8 +448,7 @@ export const base64: BytesCoder = hasBase64Builtin ? { */ export const base64nopad: BytesCoder = chain( radix2(6), - alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'), - join('') + alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') ); /** @@ -528,9 +469,7 @@ export const base64url: BytesCoder = hasBase64Builtin ? { decode(s) { return decodeBase64Builtin(s, true); }, } : chain( radix2(6), - alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'), - padding(6), - join('') + alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', 6) ); /** @@ -546,14 +485,13 @@ export const base64url: BytesCoder = hasBase64Builtin ? { */ export const base64urlnopad: BytesCoder = chain( radix2(6), - alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'), - join('') + alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_') ); // base58 code // ----------- const genBase58 = /* @__NO_SIDE_EFFECTS__ */ (abc: string) => - chain(radix(58), alphabet(abc), join('')); + chain(radix(58), alphabet(abc)); /** * base58: base64 without ambigous characters +, /, 0, O, I, l. @@ -642,8 +580,7 @@ export interface Bech32DecodedWithArray { } const BECH_ALPHABET: Coder = chain( - alphabet('qpzry9x8gf2tvdw0s3jn54khce6mua7l'), - join('') + alphabet('qpzry9x8gf2tvdw0s3jn54khce6mua7l') ); const POLYMOD_GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; @@ -822,7 +759,6 @@ export const hex: BytesCoder = hasHexBuiltin : chain( radix2(4), alphabet('0123456789abcdef'), - join(''), normalize((s: string) => { if (typeof s !== 'string' || s.length % 2 !== 0) throw new TypeError( diff --git a/test/bases.test.ts b/test/bases.test.ts index d52687f..33c40e8 100644 --- a/test/bases.test.ts +++ b/test/bases.test.ts @@ -154,9 +154,9 @@ should('utils: radix', () => { should('utils: alphabet', () => { const a = utils.alphabet('12345'); - const ab = utils.alphabet(['11', '2', '3', '4', '5']); - eql(a.encode(Uint8Array.of(1)), ['2']); - eql(ab.encode(Uint8Array.of(0)), ['11']); + const ab = utils.alphabet('A2345'); + eql(a.encode(Uint8Array.of(1)), '2'); + eql(ab.encode(Uint8Array.of(0)), 'A'); eql(a.encode(Uint8Array.of(2)), ab.encode(Uint8Array.of(2))); throws(() => a.encode([1, 2, true, 3])); throws(() => a.decode(['1', 2, true])); @@ -164,15 +164,5 @@ should('utils: alphabet', () => { throws(() => a.decode(['toString'])); }); -should('utils: join', () => { - throws(() => utils.join('1').encode(['1', 1, true])); -}); - -should('utils: padding', () => { - const coder = utils.padding(4, '='); - throws(() => coder.encode(['1', 1, true])); - throws(() => coder.decode(['1', 1, true, '='])); -}); - export { CODERS }; should.runWhen(import.meta.url);