Skip to content

Commit

Permalink
feat: add proquint multibase
Browse files Browse the repository at this point in the history
  • Loading branch information
rvagg committed Jun 6, 2024
1 parent b962767 commit 7fe7a7a
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 10 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@
"types": "./dist/src/bases/identity.d.ts",
"import": "./dist/src/bases/identity.js"
},
"./bases/proquint": {
"types": "./dist/src/bases/proquint.d.ts",
"import": "./dist/src/bases/proquint.js"
},
"./bases/interface": {
"types": "./dist/src/bases/interface.d.ts",
"import": "./dist/src/bases/interface.js"
Expand Down
87 changes: 87 additions & 0 deletions src/bases/proquint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { from } from './base.js'

const consonants = 'bdfghjklmnprstvz'
const vowels = 'aiou'

function consonantIndex (c: string): number {
const idx = consonants.indexOf(c)
if (idx === -1) {
throw new Error(`Non-proquint character: ${c}`)
}
return idx
}

function vowelIndex (v: string): number {
const idx = vowels.indexOf(v)
if (idx === -1) {
throw new Error(`Non-proquint character: ${v}`)
}

Check warning on line 18 in src/bases/proquint.ts

View check run for this annotation

Codecov / codecov/patch

src/bases/proquint.ts#L17-L18

Added lines #L17 - L18 were not covered by tests
return idx
}

export const proquint = from({
name: 'proquint',
prefix: 'p',
encode: (input: Uint8Array): string => {
// blocks of 16 bits in the pattern:
// 4 bits = consonant
// 2 bits = vowel
// 4 bits = consonant
// 2 bits = vowel
// 4 bits = consonant
// '-'
let ret = 'ro-'
for (let i = 0; i < input.length; i += 2) {
let y = input[i] << 8
if (i + 1 !== input.length) {
y |= input[i + 1]
}
ret += consonants[y >> 12 & 0xf]
ret += vowels[(y >> 10) & 0x03]
ret += consonants[(y >> 6) & 0x0f]
if (i + 1 !== input.length) {
ret += vowels[(y >> 4) & 0x03]
ret += consonants[y & 0x0f]
}
if (i + 2 < input.length) {
ret += '-'
}
}

return ret
},
decode: (input: string): Uint8Array => {
if (!input.startsWith('ro-')) {
throw new Error('Invalid proquint string')
}
input = input.slice(3)
const out = []
let i = 0
while (i < input.length) {
const hasFive = input.length - i >= 5
// must have at least 3
if (!hasFive && input.length - i < 3) {
throw new Error('Unexpected end of data')
}
let y = consonantIndex(input[i++]) << 12
y |= vowelIndex(input[i++]) << 10
y |= consonantIndex(input[i++]) << 6
if (hasFive) {
y |= vowelIndex(input[i++]) << 4
y |= consonantIndex(input[i++])
}
out.push(y >> 8)
if (hasFive) {
out.push(y & 0xff)
if (input[i] === '-') {
if (i + 1 === input.length) {
throw new Error('Unexpected end of data')
}
i++
}
}
}

return Uint8Array.from(out)
}
})
3 changes: 2 additions & 1 deletion src/basics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import * as base58 from './bases/base58.js'
import * as base64 from './bases/base64.js'
import * as base8 from './bases/base8.js'
import * as identityBase from './bases/identity.js'
import * as proquint from './bases/proquint.js'
import * as json from './codecs/json.js'
import * as raw from './codecs/raw.js'
import * as identity from './hashes/identity.js'
import * as sha2 from './hashes/sha2.js'
import { CID, hasher, digest, varint, bytes } from './index.js'

export const bases = { ...identityBase, ...base2, ...base8, ...base10, ...base16, ...base32, ...base36, ...base45, ...base58, ...base64, ...base256emoji }
export const bases = { ...identityBase, ...base2, ...base8, ...base10, ...base16, ...base32, ...base36, ...base45, ...base58, ...base64, ...base256emoji, ...proquint }
export const hashes = { ...sha2, ...identity }
export const codecs = { raw, json }

Expand Down
53 changes: 45 additions & 8 deletions test/test-multibase-spec.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ const encoded = [
['base64pad', 'MRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchIQ=='],
['base64url', 'uRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchIQ'],
['base64urlpad', 'URGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchIQ=='],
['base256emoji', '🚀💛✋💃✋😻😈🥺🤤🍀🌟💐✋😅✋💦✋🥺🏃😈😴🌟😻😝👏👏']
['base256emoji', '🚀💛✋💃✋😻😈🥺🤤🍀🌟💐✋😅✋💦✋🥺🏃😈😴🌟😻😝👏👏'],
['proquint', 'pro-hidoj-katoj-kunuh-lanod-kudon-lonoj-fadoj-linoj-lanun-lidom-kojov-kisod-fah']
]
},
{
Expand Down Expand Up @@ -62,7 +63,8 @@ const encoded = [
['base64pad', 'MeWVzIG1hbmkgIQ=='],
['base64url', 'ueWVzIG1hbmkgIQ'],
['base64urlpad', 'UeWVzIG1hbmkgIQ=='],
['base256emoji', '🚀🏃✋🌈😅🌷🤤😻🌟😅👏']
['base256emoji', '🚀🏃✋🌈😅🌷🤤😻🌟😅👏'],
['proquint', 'pro-lojoj-lasob-kujod-kunon-fabod']
]
},
{
Expand Down Expand Up @@ -92,7 +94,8 @@ const encoded = [
['base64pad', 'MaGVsbG8gd29ybGQ='],
['base64url', 'uaGVsbG8gd29ybGQ'],
['base64urlpad', 'UaGVsbG8gd29ybGQ='],
['base256emoji', '🚀😴✋🍀🍀😓😅✔😓🥺🍀😳']
['base256emoji', '🚀😴✋🍀🍀😓😅✔😓🥺🍀😳'],
['proquint', 'pro-kodoj-kudos-kusob-litoz-lanos-kib']
]
},
{
Expand Down Expand Up @@ -122,7 +125,8 @@ const encoded = [
['base64pad', 'MAHllcyBtYW5pICE='],
['base64url', 'uAHllcyBtYW5pICE'],
['base64urlpad', 'UAHllcyBtYW5pICE='],
['base256emoji', '🚀🚀🏃✋🌈😅🌷🤤😻🌟😅👏']
['base256emoji', '🚀🚀🏃✋🌈😅🌷🤤😻🌟😅👏'],
['proquint', 'pro-badun-kijug-fadot-kajov-kohob-fah']
]
},
{
Expand Down Expand Up @@ -152,13 +156,30 @@ const encoded = [
['base64pad', 'MAAB5ZXMgbWFuaSAh'],
['base64url', 'uAAB5ZXMgbWFuaSAh'],
['base64urlpad', 'UAAB5ZXMgbWFuaSAh'],
['base256emoji', '🚀🚀🚀🏃✋🌈😅🌷🤤😻🌟😅👏']
['base256emoji', '🚀🚀🚀🏃✋🌈😅🌷🤤😻🌟😅👏'],
['proquint', 'pro-babab-lojoj-lasob-kujod-kunon-fabod']
]
},

// RFC9285 examples
{ input: 'AB', tests: [['base45', 'RBB8']] },
{ input: 'Hello!!', tests: [['base45', 'R%69 VD92EX0']] },
{ input: 'base-45', tests: [['base45', 'RUJCLQE7W581']] },
{ input: 'ietf!', tests: [['base45', 'RQED8WEX0']] }
{ input: 'ietf!', tests: [['base45', 'RQED8WEX0']] },

// proquint spec examples, IPv4 addresses
{ input: Uint8Array.from([127, 0, 0, 1]), tests: [['proquint', 'pro-lusab-babad']] }, // 127.0.0.1
{ input: Uint8Array.from([63, 84, 220, 193]), tests: [['proquint', 'pro-gutih-tugad']] }, // 63.84.220.193
{ input: Uint8Array.from([63, 118, 7, 35]), tests: [['proquint', 'pro-gutuk-bisog']] }, // 63.118.7.35
{ input: Uint8Array.from([140, 98, 193, 141]), tests: [['proquint', 'pro-mudof-sakat']] }, // 140.98.193.141
{ input: Uint8Array.from([64, 255, 6, 200]), tests: [['proquint', 'pro-haguz-biram']] }, // 64.255.6.200
{ input: Uint8Array.from([128, 30, 52, 45]), tests: [['proquint', 'pro-mabiv-gibot']] }, // 128.30.52.45
{ input: Uint8Array.from([147, 67, 119, 2]), tests: [['proquint', 'pro-natag-lisaf']] }, // 147.67.119.2
{ input: Uint8Array.from([212, 58, 253, 68]), tests: [['proquint', 'pro-tibup-zujah']] }, // 212.58.253.68
{ input: Uint8Array.from([216, 35, 68, 215]), tests: [['proquint', 'pro-tobog-higil']] }, // 216.35.68.215
{ input: Uint8Array.from([216, 68, 232, 21]), tests: [['proquint', 'pro-todah-vobij']] }, // 216.68.232.21
{ input: Uint8Array.from([198, 81, 129, 136]), tests: [['proquint', 'pro-sinid-makam']] }, // 198.81.129.136
{ input: Uint8Array.from([12, 110, 110, 204]), tests: [['proquint', 'pro-budov-kuras']] } // 12.110.110.204
]

describe('spec test', () => {
Expand All @@ -169,13 +190,15 @@ describe('spec test', () => {
const base = bases[name as keyof typeof bases]

describe(name, () => {
const byteInput = typeof input === 'string' ? fromString(input) : input

it(`should encode from buffer [${input}]`, () => {
const out = base.encode(fromString(input))
const out = base.encode(byteInput)
assert.deepStrictEqual(out, output)
})

it(`should decode from string [${input}]`, () => {
assert.deepStrictEqual(base.decode(output), fromString(input))
assert.deepStrictEqual(base.decode(output), byteInput)
})
})
}
Expand All @@ -187,6 +210,10 @@ describe('spec test', () => {
if (base.name === 'identity') {
return this.skip()
}
if (base.name === 'proquint') {
assert.throws(() => base.decode('pro-^!@$%!#$%@#y'), `Non-${base.name} character`)
return
}

assert.throws(() => base.decode(base.prefix + '^!@$%!#$%@#y'), `Non-${base.name} character`)
})
Expand All @@ -196,4 +223,14 @@ describe('spec test', () => {
// not enough input chars, should be multiple of 3 or multiple of 3 + 2
assert.throws(() => bases.base45.decode('R%69 VD92EX'), 'Unexpected end of data')
})

it('proquint should fail with invalid input', () => {
assert.throws(() => bases.proquint.decode('pro-lojoj-lasob-kujod-kunon-'), 'Unexpected end of data')
assert.throws(() => bases.proquint.decode('pro-lojoj-lasob-kujod-kunon-f'), 'Unexpected end of data')
assert.throws(() => bases.proquint.decode('pro-lojoj-lasob-kujod-kunon-fa'), 'Unexpected end of data')
assert.throws(() => bases.proquint.decode('pro-lojoj-lasob-kujod-kunon-fabo'), 'Unexpected end of data')
assert.throws(() => bases.proquint.decode('plojoj-lasob-kujod-kunon-fabod'), 'Invalid proquint string')
assert.throws(() => bases.proquint.decode('prlojoj-lasob-kujod-kunon-fabod'), 'Invalid proquint string')
assert.throws(() => bases.proquint.decode('prolojoj-lasob-kujod-kunon-fabod'), 'Invalid proquint string')
})
})
7 changes: 6 additions & 1 deletion test/test-multibase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as b45 from '../src/bases/base45.js'
import * as b58 from '../src/bases/base58.js'
import * as b64 from '../src/bases/base64.js'
import * as b8 from '../src/bases/base8.js'
import * as proquint from '../src/bases/proquint.js'
import * as bytes from '../src/bytes.js'

const { base16, base32, base58btc, base64 } = { ...b16, ...b32, ...b58, ...b64 }
Expand Down Expand Up @@ -65,7 +66,7 @@ describe('multibase', () => {
const buff = bytes.fromString('test')
const nonPrintableBuff = Uint8Array.from([239, 250, 254])

const baseTest = (bases: typeof b2 | typeof b8 | typeof b10 | typeof b16 | typeof b32 | typeof b36 | typeof b45 | typeof b58 | typeof b64): void => {
const baseTest = (bases: typeof b2 | typeof b8 | typeof b10 | typeof b16 | typeof b32 | typeof b36 | typeof b45 | typeof b58 | typeof b64 | typeof proquint): void => {
for (const base of Object.values(bases)) {
if (((base as { name: string })?.name) !== '') {
it(`encode/decode ${base.name}`, () => {
Expand Down Expand Up @@ -123,6 +124,10 @@ describe('multibase', () => {
baseTest(b64)
})

describe('proquint', () => {
baseTest(proquint)
})

it('multibase mismatch', () => {
const b64 = base64.encode(bytes.fromString('test'))
const msg = `Unable to decode multibase string "${b64}", base32 decoder only supports inputs prefixed with ${base32.prefix}`
Expand Down

0 comments on commit 7fe7a7a

Please sign in to comment.