Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions src/bases/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ export function from <Base extends string, Prefix extends string> ({ name, prefi
return new Codec(name, prefix, encode, decode)
}

export function baseX <Base extends string, Prefix extends string> ({ name, prefix, alphabet }: { name: Base, prefix: Prefix, alphabet: string }): Codec<Base, Prefix> {
const { encode, decode } = basex(alphabet, name)
export function baseX <Base extends string, Prefix extends string> ({ name, prefix, alphabet, caseInsensitive = false }: { name: Base, prefix: Prefix, alphabet: string, caseInsensitive?: boolean }): Codec<Base, Prefix> {
const { encode, decode } = basex(alphabet, name, caseInsensitive)
return from({
prefix,
name,
Expand Down Expand Up @@ -214,20 +214,32 @@ function encode (data: Uint8Array, alphabet: string, bitsPerChar: number): strin
return out
}

function createAlphabetIdx (alphabet: string): Record<string, number> {
function createAlphabetIdx (alphabet: string, caseInsensitive: boolean): Record<string, number> {
// Build the character lookup table:
const alphabetIdx: Record<string, number> = {}
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
}

/**
* RFC4648 Factory
*/
export function rfc4648 <Base extends string, Prefix extends string> ({ name, prefix, bitsPerChar, alphabet }: { name: Base, prefix: Prefix, bitsPerChar: number, alphabet: string }): Codec<Base, Prefix> {
const alphabetIdx = createAlphabetIdx(alphabet)
export function rfc4648 <Base extends string, Prefix extends string> ({ name, prefix, bitsPerChar, alphabet, caseInsensitive = false }: { name: Base, prefix: Prefix, bitsPerChar: number, alphabet: string, caseInsensitive?: boolean }): Codec<Base, Prefix> {
const alphabetIdx = createAlphabetIdx(alphabet, caseInsensitive)
return from({
prefix,
name,
Expand Down
6 changes: 4 additions & 2 deletions src/bases/base16.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
24 changes: 16 additions & 8 deletions src/bases/base32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
6 changes: 4 additions & 2 deletions src/bases/base36.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
2 changes: 1 addition & 1 deletion src/vendor/base-x.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 10 additions & 1 deletion src/vendor/base-x.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -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);
Expand Down
28 changes: 28 additions & 0 deletions test/test-multibase-spec.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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'))
})
}
})
})
Loading