Skip to content

Commit 6ba35cc

Browse files
committed
feat: add proquint multibase
1 parent b962767 commit 6ba35cc

File tree

5 files changed

+133
-9
lines changed

5 files changed

+133
-9
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@
9090
"types": "./dist/src/bases/identity.d.ts",
9191
"import": "./dist/src/bases/identity.js"
9292
},
93+
"./bases/proquint": {
94+
"types": "./dist/src/bases/proquint.d.ts",
95+
"import": "./dist/src/bases/proquint.js"
96+
},
9397
"./bases/interface": {
9498
"types": "./dist/src/bases/interface.d.ts",
9599
"import": "./dist/src/bases/interface.js"

src/bases/proquint.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { from } from './base.js'
2+
3+
const consonants = 'bdfghjklmnprstvz'
4+
const vowels = 'aiou'
5+
6+
function consonantIndex (c: string): number {
7+
const idx = consonants.indexOf(c)
8+
if (idx === -1) {
9+
throw new Error(`Non-proquint character: ${c}`)
10+
}
11+
return idx
12+
}
13+
14+
function vowelIndex (v: string): number {
15+
const idx = vowels.indexOf(v)
16+
if (idx === -1) {
17+
throw new Error(`Non-proquint character: ${v}`)
18+
}
19+
return idx
20+
}
21+
22+
export const proquint = from({
23+
name: 'proquint',
24+
prefix: 'p',
25+
encode: (input: Uint8Array): string => {
26+
// blocks of 16 bits in the pattern:
27+
// 4 bits = consonant
28+
// 2 bits = vowel
29+
// 4 bits = consonant
30+
// 2 bits = vowel
31+
// 4 bits = consonant
32+
// '-'
33+
let ret = ''
34+
for (let i = 0; i < input.length; i += 2) {
35+
let y = input[i] << 8
36+
if (i + 1 !== input.length) {
37+
y |= input[i + 1]
38+
}
39+
ret += consonants[y >> 12 & 0xf]
40+
ret += vowels[(y >> 10) & 0x03]
41+
ret += consonants[(y >> 6) & 0x0f]
42+
if (i + 1 !== input.length) {
43+
ret += vowels[(y >> 4) & 0x03]
44+
ret += consonants[y & 0x0f]
45+
}
46+
if (i + 2 < input.length) {
47+
ret += '-'
48+
}
49+
}
50+
51+
return ret
52+
},
53+
decode: (input: string): Uint8Array => {
54+
const out = []
55+
let i = 0
56+
while (i < input.length) {
57+
const hasFive = input.length - i >= 5
58+
// must have at least 3
59+
if (!hasFive && input.length - i < 3) {
60+
throw new Error('Unexpected end of data')
61+
}
62+
let y = consonantIndex(input[i++]) << 12
63+
y |= vowelIndex(input[i++]) << 10
64+
y |= consonantIndex(input[i++]) << 6
65+
if (hasFive) {
66+
y |= vowelIndex(input[i++]) << 4
67+
y |= consonantIndex(input[i++])
68+
}
69+
out.push(y >> 8)
70+
if (hasFive) {
71+
out.push(y & 0xff)
72+
if (input[i] === '-') {
73+
if (i + 1 === input.length) {
74+
throw new Error('Unexpected end of data')
75+
}
76+
i++
77+
}
78+
}
79+
}
80+
81+
return Uint8Array.from(out)
82+
}
83+
})

src/basics.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import * as base58 from './bases/base58.js'
99
import * as base64 from './bases/base64.js'
1010
import * as base8 from './bases/base8.js'
1111
import * as identityBase from './bases/identity.js'
12+
import * as proquint from './bases/proquint.js'
1213
import * as json from './codecs/json.js'
1314
import * as raw from './codecs/raw.js'
1415
import * as identity from './hashes/identity.js'
1516
import * as sha2 from './hashes/sha2.js'
1617
import { CID, hasher, digest, varint, bytes } from './index.js'
1718

18-
export const bases = { ...identityBase, ...base2, ...base8, ...base10, ...base16, ...base32, ...base36, ...base45, ...base58, ...base64, ...base256emoji }
19+
export const bases = { ...identityBase, ...base2, ...base8, ...base10, ...base16, ...base32, ...base36, ...base45, ...base58, ...base64, ...base256emoji, ...proquint }
1920
export const hashes = { ...sha2, ...identity }
2021
export const codecs = { raw, json }
2122

test/test-multibase-spec.spec.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ const encoded = [
3232
['base64pad', 'MRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchIQ=='],
3333
['base64url', 'uRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchIQ'],
3434
['base64urlpad', 'URGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchIQ=='],
35-
['base256emoji', '🚀💛✋💃✋😻😈🥺🤤🍀🌟💐✋😅✋💦✋🥺🏃😈😴🌟😻😝👏👏']
35+
['base256emoji', '🚀💛✋💃✋😻😈🥺🤤🍀🌟💐✋😅✋💦✋🥺🏃😈😴🌟😻😝👏👏'],
36+
['proquint', 'phidoj-katoj-kunuh-lanod-kudon-lonoj-fadoj-linoj-lanun-lidom-kojov-kisod-fah']
3637
]
3738
},
3839
{
@@ -62,7 +63,8 @@ const encoded = [
6263
['base64pad', 'MeWVzIG1hbmkgIQ=='],
6364
['base64url', 'ueWVzIG1hbmkgIQ'],
6465
['base64urlpad', 'UeWVzIG1hbmkgIQ=='],
65-
['base256emoji', '🚀🏃✋🌈😅🌷🤤😻🌟😅👏']
66+
['base256emoji', '🚀🏃✋🌈😅🌷🤤😻🌟😅👏'],
67+
['proquint', 'plojoj-lasob-kujod-kunon-fabod']
6668
]
6769
},
6870
{
@@ -92,7 +94,8 @@ const encoded = [
9294
['base64pad', 'MaGVsbG8gd29ybGQ='],
9395
['base64url', 'uaGVsbG8gd29ybGQ'],
9496
['base64urlpad', 'UaGVsbG8gd29ybGQ='],
95-
['base256emoji', '🚀😴✋🍀🍀😓😅✔😓🥺🍀😳']
97+
['base256emoji', '🚀😴✋🍀🍀😓😅✔😓🥺🍀😳'],
98+
['proquint', 'pkodoj-kudos-kusob-litoz-lanos-kib']
9699
]
97100
},
98101
{
@@ -122,7 +125,8 @@ const encoded = [
122125
['base64pad', 'MAHllcyBtYW5pICE='],
123126
['base64url', 'uAHllcyBtYW5pICE'],
124127
['base64urlpad', 'UAHllcyBtYW5pICE='],
125-
['base256emoji', '🚀🚀🏃✋🌈😅🌷🤤😻🌟😅👏']
128+
['base256emoji', '🚀🚀🏃✋🌈😅🌷🤤😻🌟😅👏'],
129+
['proquint', 'pbadun-kijug-fadot-kajov-kohob-fah']
126130
]
127131
},
128132
{
@@ -152,13 +156,31 @@ const encoded = [
152156
['base64pad', 'MAAB5ZXMgbWFuaSAh'],
153157
['base64url', 'uAAB5ZXMgbWFuaSAh'],
154158
['base64urlpad', 'UAAB5ZXMgbWFuaSAh'],
155-
['base256emoji', '🚀🚀🚀🏃✋🌈😅🌷🤤😻🌟😅👏']
159+
['base256emoji', '🚀🚀🚀🏃✋🌈😅🌷🤤😻🌟😅👏'],
160+
['proquint', 'pbabab-lojoj-lasob-kujod-kunon-fabod']
156161
]
157162
},
163+
164+
// RFC9285 examples
158165
{ input: 'AB', tests: [['base45', 'RBB8']] },
159166
{ input: 'Hello!!', tests: [['base45', 'R%69 VD92EX0']] },
160167
{ input: 'base-45', tests: [['base45', 'RUJCLQE7W581']] },
161168
{ input: 'ietf!', tests: [['base45', 'RQED8WEX0']] }
169+
170+
// proquint spec examples, IPv4 addresses
171+
{ input: Uint8Array.from([127, 0, 0, 1]), tests: [['proquint', 'plusab-babad']] }, // 127.0.0.1
172+
{ input: Uint8Array.from([63, 84, 220, 193]), tests: [['proquint', 'pgutih-tugad']] }, // 63.84.220.193
173+
{ input: Uint8Array.from([63, 118, 7, 35]), tests: [['proquint', 'pgutuk-bisog']] }, // 63.118.7.35
174+
{ input: Uint8Array.from([140, 98, 193, 141]), tests: [['proquint', 'pmudof-sakat']] }, // 140.98.193.141
175+
{ input: Uint8Array.from([64, 255, 6, 200]), tests: [['proquint', 'phaguz-biram']] }, // 64.255.6.200
176+
{ input: Uint8Array.from([128, 30, 52, 45]), tests: [['proquint', 'pmabiv-gibot']] }, // 128.30.52.45
177+
{ input: Uint8Array.from([147, 67, 119, 2]), tests: [['proquint', 'pnatag-lisaf']] }, // 147.67.119.2
178+
{ input: Uint8Array.from([212, 58, 253, 68]), tests: [['proquint', 'ptibup-zujah']] }, // 212.58.253.68
179+
{ input: Uint8Array.from([216, 35, 68, 215]), tests: [['proquint', 'ptobog-higil']] }, // 216.35.68.215
180+
{ input: Uint8Array.from([216, 68, 232, 21]), tests: [['proquint', 'ptodah-vobij']] }, // 216.68.232.21
181+
{ input: Uint8Array.from([198, 81, 129, 136]), tests: [['proquint', 'psinid-makam']] }, // 198.81.129.136
182+
{ input: Uint8Array.from([12, 110, 110, 204]), tests: [['proquint', 'pbudov-kuras']] } // 12.110.110.204
183+
>>>>>>> bc54e9a (feat: add proquint multibase)
162184
]
163185

164186
describe('spec test', () => {
@@ -169,13 +191,15 @@ describe('spec test', () => {
169191
const base = bases[name as keyof typeof bases]
170192

171193
describe(name, () => {
194+
const byteInput = typeof input === 'string' ? fromString(input) : input
195+
172196
it(`should encode from buffer [${input}]`, () => {
173-
const out = base.encode(fromString(input))
197+
const out = base.encode(byteInput)
174198
assert.deepStrictEqual(out, output)
175199
})
176200

177201
it(`should decode from string [${input}]`, () => {
178-
assert.deepStrictEqual(base.decode(output), fromString(input))
202+
assert.deepStrictEqual(base.decode(output), byteInput)
179203
})
180204
})
181205
}
@@ -196,4 +220,11 @@ describe('spec test', () => {
196220
// not enough input chars, should be multiple of 3 or multiple of 3 + 2
197221
assert.throws(() => bases.base45.decode('R%69 VD92EX'), 'Unexpected end of data')
198222
})
223+
224+
it('proquint should fail with invalid input', () => {
225+
assert.throws(() => bases.proquint.decode('plojoj-lasob-kujod-kunon-'), 'Unexpected end of data')
226+
assert.throws(() => bases.proquint.decode('plojoj-lasob-kujod-kunon-f'), 'Unexpected end of data')
227+
assert.throws(() => bases.proquint.decode('plojoj-lasob-kujod-kunon-fa'), 'Unexpected end of data')
228+
assert.throws(() => bases.proquint.decode('plojoj-lasob-kujod-kunon-fabo'), 'Unexpected end of data')
229+
})
199230
})

test/test-multibase.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as b45 from '../src/bases/base45.js'
1010
import * as b58 from '../src/bases/base58.js'
1111
import * as b64 from '../src/bases/base64.js'
1212
import * as b8 from '../src/bases/base8.js'
13+
import * as proquint from '../src/bases/proquint.js'
1314
import * as bytes from '../src/bytes.js'
1415

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

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

127+
describe('proquint', () => {
128+
baseTest(proquint)
129+
})
130+
126131
it('multibase mismatch', () => {
127132
const b64 = base64.encode(bytes.fromString('test'))
128133
const msg = `Unable to decode multibase string "${b64}", base32 decoder only supports inputs prefixed with ${base32.prefix}`

0 commit comments

Comments
 (0)