From 0c629d5b9516d2b0be3d37ec448641d6396b1336 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Wed, 24 Jun 2026 01:58:31 +0100 Subject: [PATCH] fix: case-insensitive decode for base16/base32/base36 (multibase spec) The multibase registry marks base16, base32 (and all 8 variants) and base36 as case-insensitive, and the spec's case_insensitivity.csv fixture requires their decoders to accept differently cased input "without errors". RFC 4648 section 3.4 backs this for base16/base32, and go-multibase decodes these codecs case-insensitively. Currently mixed-case input throws "Non-baseXX character". Thread a caseInsensitive flag from the rfc4648 and baseX factories so the decode lookup resolves both cases for only the spec-designated codecs. Encode output stays canonical and case-sensitive codecs are unaffected. --- src/bases/base.ts | 22 +++++++++++++++++----- src/bases/base16.ts | 6 ++++-- src/bases/base32.ts | 24 ++++++++++++++++-------- src/bases/base36.ts | 6 ++++-- src/vendor/base-x.d.ts | 2 +- src/vendor/base-x.js | 11 ++++++++++- test/test-multibase-spec.spec.ts | 28 ++++++++++++++++++++++++++++ 7 files changed, 80 insertions(+), 19 deletions(-) diff --git a/src/bases/base.ts b/src/bases/base.ts index afe6425c..54cea234 100644 --- a/src/bases/base.ts +++ b/src/bases/base.ts @@ -130,8 +130,8 @@ export function from ({ name, prefi return new Codec(name, prefix, encode, decode) } -export function baseX ({ name, prefix, alphabet }: { name: Base, prefix: Prefix, alphabet: string }): Codec { - const { encode, decode } = basex(alphabet, name) +export function baseX ({ name, prefix, alphabet, caseInsensitive = false }: { name: Base, prefix: Prefix, alphabet: string, caseInsensitive?: boolean }): Codec { + const { encode, decode } = basex(alphabet, name, caseInsensitive) return from({ prefix, name, @@ -214,11 +214,23 @@ function encode (data: Uint8Array, alphabet: string, bitsPerChar: number): strin return out } -function createAlphabetIdx (alphabet: string): Record { +function createAlphabetIdx (alphabet: string, caseInsensitive: boolean): Record { // Build the character lookup table: const alphabetIdx: Record = {} for (let i = 0; i < alphabet.length; ++i) { alphabetIdx[alphabet[i]] = i + // For case-insensitive codecs, map the opposite case to the same index so + // differently cased input decodes without errors (multibase spec). + if (caseInsensitive) { + const lower = alphabet[i].toLowerCase() + const upper = alphabet[i].toUpperCase() + if (lower !== alphabet[i]) { + alphabetIdx[lower] = i + } + if (upper !== alphabet[i]) { + alphabetIdx[upper] = i + } + } } return alphabetIdx } @@ -226,8 +238,8 @@ function createAlphabetIdx (alphabet: string): Record { /** * RFC4648 Factory */ -export function rfc4648 ({ name, prefix, bitsPerChar, alphabet }: { name: Base, prefix: Prefix, bitsPerChar: number, alphabet: string }): Codec { - const alphabetIdx = createAlphabetIdx(alphabet) +export function rfc4648 ({ name, prefix, bitsPerChar, alphabet, caseInsensitive = false }: { name: Base, prefix: Prefix, bitsPerChar: number, alphabet: string, caseInsensitive?: boolean }): Codec { + const alphabetIdx = createAlphabetIdx(alphabet, caseInsensitive) return from({ prefix, name, diff --git a/src/bases/base16.ts b/src/bases/base16.ts index 4797518f..5745cfef 100644 --- a/src/bases/base16.ts +++ b/src/bases/base16.ts @@ -4,12 +4,14 @@ export const base16 = rfc4648({ prefix: 'f', name: 'base16', alphabet: '0123456789abcdef', - bitsPerChar: 4 + bitsPerChar: 4, + caseInsensitive: true }) export const base16upper = rfc4648({ prefix: 'F', name: 'base16upper', alphabet: '0123456789ABCDEF', - bitsPerChar: 4 + bitsPerChar: 4, + caseInsensitive: true }) diff --git a/src/bases/base32.ts b/src/bases/base32.ts index f3717d9b..e4932bde 100644 --- a/src/bases/base32.ts +++ b/src/bases/base32.ts @@ -4,56 +4,64 @@ export const base32 = rfc4648({ prefix: 'b', name: 'base32', alphabet: 'abcdefghijklmnopqrstuvwxyz234567', - bitsPerChar: 5 + bitsPerChar: 5, + caseInsensitive: true }) export const base32upper = rfc4648({ prefix: 'B', name: 'base32upper', alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', - bitsPerChar: 5 + bitsPerChar: 5, + caseInsensitive: true }) export const base32pad = rfc4648({ prefix: 'c', name: 'base32pad', alphabet: 'abcdefghijklmnopqrstuvwxyz234567=', - bitsPerChar: 5 + bitsPerChar: 5, + caseInsensitive: true }) export const base32padupper = rfc4648({ prefix: 'C', name: 'base32padupper', alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=', - bitsPerChar: 5 + bitsPerChar: 5, + caseInsensitive: true }) export const base32hex = rfc4648({ prefix: 'v', name: 'base32hex', alphabet: '0123456789abcdefghijklmnopqrstuv', - bitsPerChar: 5 + bitsPerChar: 5, + caseInsensitive: true }) export const base32hexupper = rfc4648({ prefix: 'V', name: 'base32hexupper', alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUV', - bitsPerChar: 5 + bitsPerChar: 5, + caseInsensitive: true }) export const base32hexpad = rfc4648({ prefix: 't', name: 'base32hexpad', alphabet: '0123456789abcdefghijklmnopqrstuv=', - bitsPerChar: 5 + bitsPerChar: 5, + caseInsensitive: true }) export const base32hexpadupper = rfc4648({ prefix: 'T', name: 'base32hexpadupper', alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUV=', - bitsPerChar: 5 + bitsPerChar: 5, + caseInsensitive: true }) export const base32z = rfc4648({ diff --git a/src/bases/base36.ts b/src/bases/base36.ts index bb087871..67c448a2 100644 --- a/src/bases/base36.ts +++ b/src/bases/base36.ts @@ -3,11 +3,13 @@ import { baseX } from './base.ts' export const base36 = baseX({ prefix: 'k', name: 'base36', - alphabet: '0123456789abcdefghijklmnopqrstuvwxyz' + alphabet: '0123456789abcdefghijklmnopqrstuvwxyz', + caseInsensitive: true }) export const base36upper = baseX({ prefix: 'K', name: 'base36upper', - alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ', + caseInsensitive: true }) diff --git a/src/vendor/base-x.d.ts b/src/vendor/base-x.d.ts index 94ada8af..bc5069a6 100644 --- a/src/vendor/base-x.d.ts +++ b/src/vendor/base-x.d.ts @@ -6,4 +6,4 @@ export interface BaseConverter { decode(string: string): Uint8Array; } -export default function base(ALPHABET: string, name: string): BaseConverter +export default function base(ALPHABET: string, name: string, caseInsensitive?: boolean): BaseConverter diff --git a/src/vendor/base-x.js b/src/vendor/base-x.js index dd396ab2..38efc6a2 100644 --- a/src/vendor/base-x.js +++ b/src/vendor/base-x.js @@ -7,8 +7,9 @@ /** * @param {string} ALPHABET * @param {any} name + * @param {boolean} [caseInsensitive] */ -function base (ALPHABET, name) { +function base (ALPHABET, name, caseInsensitive) { if (ALPHABET.length >= 255) { throw new TypeError('Alphabet too long') } var BASE_MAP = new Uint8Array(256); for (var j = 0; j < BASE_MAP.length; j++) { @@ -19,6 +20,14 @@ function base (ALPHABET, name) { var xc = x.charCodeAt(0); if (BASE_MAP[xc] !== 255) { throw new TypeError(x + ' is ambiguous') } BASE_MAP[xc] = i; + // For case-insensitive codecs, map the opposite case to the same index so + // differently cased input decodes without errors (multibase spec). + if (caseInsensitive) { + var xl = x.toLowerCase().charCodeAt(0); + var xu = x.toUpperCase().charCodeAt(0); + if (xl !== xc) { BASE_MAP[xl] = i; } + if (xu !== xc) { BASE_MAP[xu] = i; } + } } var BASE = ALPHABET.length; var LEADER = ALPHABET.charAt(0); diff --git a/test/test-multibase-spec.spec.ts b/test/test-multibase-spec.spec.ts index 63d5acf5..d971bfbb 100644 --- a/test/test-multibase-spec.spec.ts +++ b/test/test-multibase-spec.spec.ts @@ -150,6 +150,24 @@ const encoded = [ } ] +// Differently cased than the canonical encoding. Per the multibase spec +// `tests/case_insensitivity.csv` fixture, these must decode without errors. +// Every value below encodes 'hello world'. +const caseInsensitive: Array<[string, string]> = [ + ['base16', 'f68656c6c6f20776F726C64'], + ['base16upper', 'F68656c6c6f20776F726C64'], + ['base32', 'bnbswy3dpeB3W64TMMQ'], + ['base32upper', 'Bnbswy3dpeB3W64TMMQ'], + ['base32hex', 'vd1imor3f41RMUSJCCG'], + ['base32hexupper', 'Vd1imor3f41RMUSJCCG'], + ['base32pad', 'cnbswy3dpeB3W64TMMQ======'], + ['base32padupper', 'Cnbswy3dpeB3W64TMMQ======'], + ['base32hexpad', 'td1imor3f41RMUSJCCG======'], + ['base32hexpadupper', 'Td1imor3f41RMUSJCCG======'], + ['base36', 'kfUvrsIvVnfRbjWaJo'], + ['base36upper', 'KfUVrSIVVnFRbJWAJo'] +] + describe('spec test', () => { let index = 0 for (const { input, tests } of encoded) { @@ -180,4 +198,14 @@ describe('spec test', () => { assert.throws(() => base.decode(base.prefix + '^!@$%!#$%@#y'), `Non-${base.name} character`) }) } + + describe('case insensitivity', () => { + for (const [name, output] of caseInsensitive) { + const base = bases[name as keyof typeof bases] + + it(`${name} should decode differently cased input`, () => { + assert.deepStrictEqual(base.decode(output), fromString('hello world')) + }) + } + }) })