Skip to content

Commit c57af93

Browse files
committed
fix: address review feedback
1 parent 14fb384 commit c57af93

3 files changed

Lines changed: 47 additions & 20 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@
8383
},
8484
"./bases/base45": {
8585
"types": "./dist/src/bases/base45.d.ts",
86-
"import": "./dist/src/bases/base45.js"
86+
"import": "./dist/src/bases/base45.js",
87+
"module-sync": "./dist/src/bases/base45.js"
8788
},
8889
"./bases/base58": {
8990
"types": "./dist/src/bases/base58.d.ts",

src/bases/base45.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import { from } from './base.ts'
22

33
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
4+
const INVALID = 0xff
5+
const decodeTable = (() => {
6+
const table = new Uint8Array(256).fill(INVALID)
7+
for (let i = 0; i < alphabet.length; i++) {
8+
table[alphabet.charCodeAt(i)] = i
9+
}
10+
return table
11+
})()
412

5-
function indexOf (char: string): number {
6-
const index = alphabet.indexOf(char)
7-
if (index === -1) {
8-
throw new Error(`Non-base45 character: ${char}`)
13+
function decodeChar (input: string, i: number): number {
14+
const v = decodeTable[input.charCodeAt(i)]
15+
if (v === INVALID) {
16+
throw new Error(`Non-base45 character: ${input[i]}`)
917
}
10-
return index
18+
return v
1119
}
1220

1321
export const base45 = from({
@@ -18,33 +26,35 @@ export const base45 = from({
1826
for (let i = 0; i < input.length; i += 2) {
1927
if (i + 1 === input.length) {
2028
const v = input[i]
21-
const a = v / 45 | 0
22-
const b = v % 45 | 0
23-
ret += alphabet[b] + alphabet[a]
29+
ret += alphabet[v % 45] + alphabet[(v / 45) | 0]
2430
break
2531
}
26-
const v = input[i] << 8 | input[i + 1]
27-
const a = v / 45 ** 2 | 0
28-
const b = v / 45 % 45 | 0
29-
const c = v % 45
30-
ret += alphabet[c] + alphabet[b] + alphabet[a]
32+
const v = (input[i] << 8) | input[i + 1]
33+
ret += alphabet[v % 45] + alphabet[((v / 45) | 0) % 45] + alphabet[(v / 2025) | 0]
3134
}
3235
return ret
3336
},
3437
decode: (input: string): Uint8Array<ArrayBuffer> => {
3538
if ((input.length * 2) % 3 === 2) {
3639
throw new Error('Unexpected end of data')
3740
}
38-
const out = new Uint8Array(Math.floor(input.length * 2 / 3))
41+
const out = new Uint8Array(((input.length * 2) / 3) | 0)
42+
let o = 0
3943
for (let i = 0; i < input.length; i += 3) {
4044
if (i + 2 === input.length) {
41-
const v = indexOf(input[i]) + indexOf(input[i + 1]) * 45
42-
out[i / 3 * 2] = v
45+
const v = decodeChar(input, i) + decodeChar(input, i + 1) * 45
46+
if (v > 0xff) {
47+
throw new Error('Invalid base45 encoding: trailing chunk out of range')
48+
}
49+
out[o++] = v
4350
break
4451
}
45-
const v = indexOf(input[i]) + indexOf(input[i + 1]) * 45 + indexOf(input[i + 2]) * 45 ** 2
46-
out[i / 3 * 2] = v >> 8
47-
out[i / 3 * 2 + 1] = v & 0xff
52+
const v = decodeChar(input, i) + decodeChar(input, i + 1) * 45 + decodeChar(input, i + 2) * 2025
53+
if (v > 0xffff) {
54+
throw new Error('Invalid base45 encoding: chunk out of range')
55+
}
56+
out[o++] = v >> 8
57+
out[o++] = v & 0xff
4858
}
4959
return out
5060
}

test/test-multibase-spec.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,20 @@ describe('spec test', () => {
194194
// not enough input chars, should be multiple of 3 or multiple of 3 + 2
195195
assert.throws(() => bases.base45.decode('R%69 VD92EX'), 'Unexpected end of data')
196196
})
197+
198+
// RFC9285 section 6: encoded chunks must lie within the byte ranges they
199+
// represent; 3-char chunks decode to a 16-bit value, 2-char trailers to 8-bit.
200+
for (const input of ['RGGW', 'R:::', 'R000V5', 'R000::']) {
201+
it(`base45 should fail with out-of-range encoding [${input}]`, () => {
202+
assert.throws(() => bases.base45.decode(input), 'Invalid base45 encoding')
203+
})
204+
}
205+
206+
// RFC9285 section 6 boundary: "FGW" is 65535 (0xFFFF), the largest valid
207+
// 16-bit value; the contrasting "GGW" (above) is 65536 and must fail.
208+
it('base45 should round-trip the 0xFFFF boundary [FGW]', () => {
209+
const bytes = Uint8Array.from([0xff, 0xff])
210+
assert.deepStrictEqual(bases.base45.encode(bytes), 'RFGW')
211+
assert.deepStrictEqual(bases.base45.decode('RFGW'), bytes)
212+
})
197213
})

0 commit comments

Comments
 (0)