Skip to content

Commit 71b3ca0

Browse files
committed
split convert into array.js and hex.js
1 parent 5f121a1 commit 71b3ca0

11 files changed

Lines changed: 208 additions & 111 deletions

File tree

array.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { assertTypedArray } from './assert.js'
2+
3+
const { Buffer } = globalThis // Buffer is optional
4+
5+
export function fromTypedArray(arr, format = 'uint8') {
6+
assertTypedArray(arr)
7+
switch (format) {
8+
case 'uint8':
9+
if (arr.constructor === Uint8Array) return arr // fast path
10+
return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength)
11+
case 'buffer':
12+
if (arr.constructor === Buffer && Buffer.isBuffer(arr)) return arr
13+
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength)
14+
}
15+
16+
throw new TypeError('Unexpected format')
17+
}

assert.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ export function assertEmptyRest(rest) {
88

99
const makeMessage = (name, extra) => `Expected${name ? ` ${name} to be` : ''} an Uint8Array${extra}`
1010

11+
const TypedArray = Object.getPrototypeOf(Uint8Array)
12+
13+
export function assertTypedArray(arr) {
14+
assert(arr instanceof TypedArray, 'Expected a TypedArray instance')
15+
}
16+
1117
export function assertUint8(arr, { name, length, ...rest } = {}) {
1218
assertEmptyRest(rest)
1319
if (arr instanceof Uint8Array && (length === undefined || arr.length === length)) return

base64.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { assert, assertUint8 } from './assert.js'
2-
import { fromTypedArray } from './convert.js'
2+
import { fromTypedArray } from './array.js'
33

44
// See https://datatracker.ietf.org/doc/html/rfc4648
55

benchmarks/hex.bench.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as exodus from '@exodus/bytes/hex.js'
2+
import { hex as scureHex } from '@scure/base'
3+
import buffer from 'buffer/index.js'
4+
5+
import { bufs } from './random.js'
6+
7+
if (!globalThis.Buffer) globalThis.Buffer = buffer.Buffer
8+
Buffer.TYPED_ARRAY_SUPPORT = true
9+
const exodusPure = await import('../hex.js?pure')
10+
delete Buffer.TYPED_ARRAY_SUPPORT
11+
12+
const val = exodus.toHex(bufs[0])
13+
if (scureHex.encode(bufs[0]) !== val) throw new Error('scure.hex')
14+
if (exodusPure.toHex(bufs[0]) !== val) throw new Error('exodus pure')
15+
16+
for (let i = 0; i < 5; i++) {
17+
console.time('@exodus/bytes/hex.js')
18+
for (const buf of bufs) exodus.toHex(buf)
19+
console.timeEnd('@exodus/bytes/hex.js')
20+
21+
console.time('@exodus/bytes/hex.js, pure')
22+
for (const buf of bufs) exodusPure.toHex(buf)
23+
console.timeEnd('@exodus/bytes/hex.js, pure')
24+
25+
console.time('@scure/base')
26+
for (const buf of bufs) scureHex.encode(buf)
27+
console.timeEnd('@scure/base')
28+
29+
console.time('Buffer.from')
30+
for (const buf of bufs) Buffer.from(buf).toString('hex')
31+
console.timeEnd('Buffer.from')
32+
33+
console.time('buffer/Buffer.from')
34+
for (const buf of bufs) buffer.Buffer.from(buf).toString('hex')
35+
console.timeEnd('buffer/Buffer.from')
36+
}

benchmarks/hex.from.bench.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as exodus from '@exodus/bytes/hex.js'
2+
import { hex as scureHex } from '@scure/base'
3+
import buffer from 'buffer/index.js'
4+
5+
import { bufs } from './random.js'
6+
7+
if (!globalThis.Buffer) globalThis.Buffer = buffer.Buffer
8+
Buffer.TYPED_ARRAY_SUPPORT = true
9+
const exodusPure = await import('../hex.js?pure')
10+
delete Buffer.TYPED_ARRAY_SUPPORT
11+
12+
const strings = bufs.map(x => exodus.fromTypedArray(x, 'hex'))
13+
14+
if (Buffer.compare(exodus.fromHex(strings[0]), bufs[0]) !== 0) throw new Error('exodus')
15+
if (Buffer.compare(exodusPure.fromHex(strings[0]), bufs[0]) !== 0) throw new Error('exodus pure')
16+
if (Buffer.compare(scureHex.decode(strings[0]), bufs[0]) !== 0) throw new Error('scure.hex')
17+
18+
for (let i = 0; i < 5; i++) {
19+
console.time('@exodus/bytes/hex.js')
20+
for (const str of strings) exodus.fromHex(str)
21+
console.timeEnd('@exodus/bytes/hex.js')
22+
23+
console.time('@exodus/bytes/hex.js, pure')
24+
for (const str of strings) exodusPure.fromHex(str)
25+
console.timeEnd('@exodus/bytes/hex.js, pure')
26+
27+
console.time('@scure/base')
28+
for (const str of strings) scureHex.decode(str)
29+
console.timeEnd('@scure/base')
30+
31+
console.time('Buffer.from')
32+
for (const str of strings) Buffer.from(str, 'hex')
33+
console.timeEnd('Buffer.from')
34+
35+
console.time('buffer/Buffer.from')
36+
for (const str of strings) buffer.Buffer.from(str, 'hex')
37+
console.timeEnd('buffer/Buffer.from')
38+
}

convert.js

Lines changed: 0 additions & 63 deletions
This file was deleted.

hex.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { assertTypedArray, assert } from './assert.js'
2+
import { fromTypedArray } from './array.js'
3+
4+
const { Buffer } = globalThis // Buffer is optional, only used when native
5+
const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
6+
7+
let hexArray
8+
let dehexArray
9+
10+
export function toHex(arr) {
11+
assertTypedArray(arr)
12+
const u8 = arr instanceof Uint8Array ? arr : new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength)
13+
if (Uint8Array.prototype.toHex && u8.toHex === Uint8Array.prototype.toHex) return u8.toHex()
14+
if (haveNativeBuffer) {
15+
if (arr.constructor === Buffer && Buffer.isBuffer(arr)) return arr.toString('hex')
16+
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString('hex')
17+
}
18+
if (!hexArray) hexArray = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'))
19+
let out = ''
20+
for (let i = 0; i < arr.length; i++) out += hexArray[u8[i]]
21+
return out
22+
}
23+
24+
// Unlike Buffer.from(), throws on invalid input
25+
export function fromHex(arg, format = 'uint8') {
26+
if (Uint8Array.fromHex) return fromTypedArray(Uint8Array.fromHex(arg), format)
27+
if (typeof arg !== 'string') throw new TypeError('Input is not a string')
28+
assert(arg.length % 2 === 0, 'Input is not a hex string')
29+
if (haveNativeBuffer) {
30+
assert(!/[^0-9a-f]/ui.test(arg), 'Input is not a hex string')
31+
return fromTypedArray(Buffer.from(arg, 'hex'), format)
32+
}
33+
34+
if (!dehexArray) {
35+
dehexArray = new Array(103) // f is 102
36+
for (let i = 0; i < 16; i++) {
37+
const s = i.toString(16)
38+
dehexArray[s.charCodeAt(0)] = dehexArray[s.toUpperCase().charCodeAt(0)] = i
39+
}
40+
}
41+
42+
const arr = new Uint8Array(arg.length / 2)
43+
let j = 0
44+
for (let i = 0; i < arr.length; i++) {
45+
const a = dehexArray[arg.charCodeAt(j++)] * 16 + dehexArray[arg.charCodeAt(j++)]
46+
if (!a && Number.isNaN(a)) throw new Error('Input is not a hex string')
47+
arr[i] = a
48+
}
49+
50+
return fromTypedArray(arr, format)
51+
}

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@
3939
"files": [
4040
"/assert.js",
4141
"/base64.js",
42-
"/convert.js"
42+
"/array.js",
43+
"/hex.js"
4344
],
4445
"exports": {
4546
"./assert.js": "./assert.js",
4647
"./base64.js": "./base64.js",
47-
"./convert.js": "./convert.js"
48+
"./array.js": "./array.js",
49+
"./hex.js": "./hex.js"
4850
},
4951
"dependencies": {},
5052
"devDependencies": {
Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fromTypedArray, fromHex } from '@exodus/bytes/convert.js'
1+
import { fromTypedArray } from '@exodus/bytes/array.js'
22
import { describe, test } from 'node:test'
33

44
const raw = [new Uint8Array(), new Uint8Array([0]), new Uint8Array([1]), new Uint8Array([255])]
@@ -16,7 +16,7 @@ describe('fromTypedArray', () => {
1616
test('invalid input', (t) => {
1717
for (const input of [null, undefined, [], [1,2], 'string']) {
1818
t.assert.throws(() => fromTypedArray(input))
19-
for (const form of ['uint8', 'buffer', 'hex']) {
19+
for (const form of ['uint8', 'buffer']) {
2020
t.assert.throws(() => fromTypedArray(input, form))
2121
}
2222
}
@@ -43,40 +43,4 @@ describe('fromTypedArray', () => {
4343
t.assert.strictEqual(a.buffer, uint8.buffer)
4444
}
4545
})
46-
47-
test('hex', (t) => {
48-
for (const { uint8, buffer, hex} of pool) {
49-
t.assert.strictEqual(fromTypedArray(uint8, 'hex'), hex)
50-
t.assert.strictEqual(fromTypedArray(buffer, 'hex'), hex)
51-
}
52-
})
53-
})
54-
55-
describe('fromHex', () => {
56-
test('invalid input', (t) => {
57-
for (const input of [null, undefined, [], [1,2], ['00'], new Uint8Array(), 'a', '0x00', 'ag']) {
58-
if (Uint8Array.fromHex) t.assert.throws(() => Uint8Array.fromHex(input))
59-
t.assert.throws(() => fromHex(input))
60-
for (const form of ['uint8', 'buffer', 'hex']) {
61-
t.assert.throws(() => fromHex(input, form))
62-
}
63-
}
64-
})
65-
66-
test('uint8', (t) => {
67-
for (const { hex, uint8 } of pool) {
68-
t.assert.deepStrictEqual(fromHex(hex), uint8)
69-
t.assert.deepStrictEqual(fromHex(hex, 'uint8'), uint8)
70-
}
71-
})
72-
73-
test('buffer', (t) => {
74-
for (const { hex, buffer } of pool) {
75-
t.assert.deepStrictEqual(fromHex(hex, 'buffer'), buffer)
76-
}
77-
})
78-
79-
test('hex', (t) => {
80-
for (const { hex } of pool) t.assert.strictEqual(fromHex(hex, 'hex'), hex)
81-
})
8246
})

tests/base64.test.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,4 @@ describe('fromBase64', () => {
9999
t.assert.deepStrictEqual(fromBase64url(base64url, 'buffer'), buffer)
100100
}
101101
})
102-
103-
test('hex', (t) => {
104-
for (const { hex, base64, base64url } of pool) {
105-
t.assert.strictEqual(fromBase64(base64, 'hex'), hex)
106-
t.assert.strictEqual(fromBase64url(base64url, 'hex'), hex)
107-
}
108-
})
109102
})

0 commit comments

Comments
 (0)