Skip to content

Commit

Permalink
feat: add base45 multibase
Browse files Browse the repository at this point in the history
  • Loading branch information
rvagg committed Jun 6, 2024
1 parent dba6462 commit eef3821
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 7 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ import the ones you need yourself.
| `base64`, `base64pad`, `base64url`, `base64urlpad` | `multiformats/bases/base64` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) |
| `base58btc`, `base58flick4` | `multiformats/bases/base58` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) |

Other (less useful) bases implemented in [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) include: `base2`, `base8`, `base10`, `base36` and `base256emoji`.
Other (less useful) bases implemented in [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) include: `base2`, `base8`, `base10`, `base36`, `base45` and `base256emoji`.

### Multihash hashers

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
"types": "./dist/src/bases/base36.d.ts",
"import": "./dist/src/bases/base36.js"
},
"./bases/base45": {
"types": "./dist/src/bases/base45.d.ts",
"import": "./dist/src/bases/base45.js"
},
"./bases/base58": {
"types": "./dist/src/bases/base58.d.ts",
"import": "./dist/src/bases/base58.js"
Expand Down
51 changes: 51 additions & 0 deletions src/bases/base45.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { from } from './base.js'

const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'

function indexOf (char: string) {
const index = alphabet.indexOf(char)
if (index === -1) {
throw new Error(`Non-base45 character: ${char}`)
}
return index
}

export const base45 = from({
name: 'base45',
prefix: 'R',
encode: (input: Uint8Array): string => {
var ret = ''
for (let i = 0; i < input.length; i += 2) {
if (i + 1 === input.length) {
const v = input[i]
const a = v / 45 | 0
const b = v % 45 | 0
ret += alphabet[b] + alphabet[a]
break
}
const v = input[i] << 8 | input[i + 1]
const a = v / 45 ** 2 | 0
const b = v / 45 % 45 | 0
const c = v % 45
ret += alphabet[c] + alphabet[b] + alphabet[a]
}
return ret
},
decode: (input: string): Uint8Array => {
if ((input.length * 2) % 3 === 2) {
throw new Error('Unexpected end of data')
}
const out = new Uint8Array(Math.floor(input.length * 2 / 3))
for (let i = 0; i < input.length; i += 3) {
if (i + 2 === input.length) {
const v = indexOf(input[i]) + indexOf(input[i + 1]) * 45
out[i / 3 * 2] = v
break
}
const v = indexOf(input[i]) + indexOf(input[i + 1]) * 45 + indexOf(input[i + 2]) * 45 ** 2
out[i / 3 * 2] = v >> 8
out[i / 3 * 2 + 1] = v & 0xff
}
return out
}
})
3 changes: 2 additions & 1 deletion src/basics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as base2 from './bases/base2.js'
import * as base256emoji from './bases/base256emoji.js'
import * as base32 from './bases/base32.js'
import * as base36 from './bases/base36.js'
import * as base45 from './bases/base45.js'
import * as base58 from './bases/base58.js'
import * as base64 from './bases/base64.js'
import * as base8 from './bases/base8.js'
Expand All @@ -14,7 +15,7 @@ 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, ...base58, ...base64, ...base256emoji }
export const bases = { ...identityBase, ...base2, ...base8, ...base10, ...base16, ...base32, ...base36, ...base45, ...base58, ...base64, ...base256emoji }
export const hashes = { ...sha2, ...identity }
export const codecs = { raw, json }

Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
* | `base64`, `base64pad`, `base64url`, `base64urlpad` | `multiformats/bases/base64` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) |
* | `base58btc`, `base58flick4` | `multiformats/bases/base58` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) |
*
* Other (less useful) bases implemented in [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) include: `base2`, `base8`, `base10`, `base36` and `base256emoji`.
* Other (less useful) bases implemented in [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) include: `base2`, `base8`, `base10`, `base36`, `base45` and `base256emoji`.
*
* ### Multihash hashers
*
Expand Down
21 changes: 18 additions & 3 deletions test/test-multibase-spec.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const encoded = [
['base32z', 'het1sg3mqqt3gn5djxj11y3msci3817depfzgqejb'],
['base36', 'k343ixo7d49hqj1ium15pgy1wzww5fxrid21td7l'],
['base36upper', 'K343IXO7D49HQJ1IUM15PGY1WZWW5FXRID21TD7L'],
['base45', 'R4T8KPCG/DVKEXVDDLFD44O/EALEAWEZEDV1DX0'],
['base58flickr', 'Ztwe7gVTeK8wswS1gf8hrgAua9fcw9reboD'],
['base58btc', 'zUXE7GvtEk8XTXs1GF8HSGbVA9FCX9SEBPe'],
['base64', 'mRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchIQ'],
Expand Down Expand Up @@ -54,6 +55,7 @@ const encoded = [
['base32z', 'hxf1zgedpcfzg1ebb'],
['base36', 'k2lcpzo5yikidynfl'],
['base36upper', 'K2LCPZO5YIKIDYNFL'],
['base45', 'RRFF.OEB$D5/DZ24'],
['base58flickr', 'Z7Pznk19XTTzBtx'],
['base58btc', 'z7paNL19xttacUY'],
['base64', 'meWVzIG1hbmkgIQ'],
Expand Down Expand Up @@ -83,6 +85,7 @@ const encoded = [
['base32z', 'hpb1sa5dxrb5s6hucco'],
['base36', 'kfuvrsivvnfrbjwajo'],
['base36upper', 'KFUVRSIVVNFRBJWAJO'],
['base45', 'R+8D VD82EK4F.KEA2'],
['base58flickr', 'ZrTu1dk6cWsRYjYu'],
['base58btc', 'zStV1DL6CwTryKyV'],
['base64', 'maGVsbG8gd29ybGQ'],
Expand Down Expand Up @@ -112,6 +115,7 @@ const encoded = [
['base32z', 'hybhskh3ypiosh4jyrr'],
['base36', 'k02lcpzo5yikidynfl'],
['base36upper', 'K02LCPZO5YIKIDYNFL'],
['base45', 'RV206$CL44CEC2DDX0'],
['base58flickr', 'Z17Pznk19XTTzBtx'],
['base58btc', 'z17paNL19xttacUY'],
['base64', 'mAHllcyBtYW5pICE'],
Expand Down Expand Up @@ -141,6 +145,7 @@ const encoded = [
['base32z', 'hyyy813murbssn5ujryoo'],
['base36', 'k002lcpzo5yikidynfl'],
['base36upper', 'K002LCPZO5YIKIDYNFL'],
['base45', 'R000RFF.OEB$D5/DZ24'],
['base58flickr', 'Z117Pznk19XTTzBtx'],
['base58btc', 'z117paNL19xttacUY'],
['base64', 'mAAB5ZXMgbWFuaSAh'],
Expand All @@ -149,7 +154,12 @@ const encoded = [
['base64urlpad', 'UAAB5ZXMgbWFuaSAh'],
['base256emoji', '🚀🚀🚀🏃✋🌈😅🌷🤤😻🌟😅👏']
]
}
},
{ 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']] },
]

describe('spec test', () => {
Expand All @@ -160,12 +170,12 @@ describe('spec test', () => {
const base = bases[name as keyof typeof bases]

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

it('should decode string', () => {
it(`should decode from string [${input}]`, () => {
assert.deepStrictEqual(base.decode(output), fromString(input))
})
})
Expand All @@ -182,4 +192,9 @@ describe('spec test', () => {
assert.throws(() => base.decode(base.prefix + '^!@$%!#$%@#y'), `Non-${base.name} character`)
})
}

it('base45 should fail with invalid input', () => {
// 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')
})
})
7 changes: 6 additions & 1 deletion test/test-multibase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as b16 from '../src/bases/base16.js'
import * as b2 from '../src/bases/base2.js'
import * as b32 from '../src/bases/base32.js'
import * as b36 from '../src/bases/base36.js'
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'
Expand Down Expand Up @@ -64,7 +65,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 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): void => {
for (const base of Object.values(bases)) {
if (((base as { name: string })?.name) !== '') {
it(`encode/decode ${base.name}`, () => {
Expand Down Expand Up @@ -110,6 +111,10 @@ describe('multibase', () => {
baseTest(b36)
})

describe('base45', () => {
baseTest(b45)
})

describe('base58', () => {
baseTest(b58)
})
Expand Down
1 change: 1 addition & 0 deletions typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"./src/bases/base256emoji.ts",
"./src/bases/base32.ts",
"./src/bases/base36.ts",
"./src/bases/base45.ts",
"./src/bases/base58.ts",
"./src/bases/base64.ts",
"./src/bases/base8.ts",
Expand Down

0 comments on commit eef3821

Please sign in to comment.