ox 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/dist/core/Base64.d.ts +10 -21
- package/dist/core/Base64.d.ts.map +1 -1
- package/dist/core/Base64.js +8 -127
- package/dist/core/Base64.js.map +1 -1
- package/dist/core/BinaryStateTree.d.ts.map +1 -1
- package/dist/core/BinaryStateTree.js +2 -2
- package/dist/core/BinaryStateTree.js.map +1 -1
- package/dist/core/Bls.d.ts.map +1 -1
- package/dist/core/Bls.js +19 -44
- package/dist/core/Bls.js.map +1 -1
- package/dist/core/Ed25519.d.ts.map +1 -1
- package/dist/core/Ed25519.js +7 -6
- package/dist/core/Ed25519.js.map +1 -1
- package/dist/core/Engine.d.ts +124 -0
- package/dist/core/Engine.d.ts.map +1 -0
- package/dist/core/Engine.js +180 -0
- package/dist/core/Engine.js.map +1 -0
- package/dist/core/Hash.d.ts +4 -4
- package/dist/core/Hash.d.ts.map +1 -1
- package/dist/core/Hash.js +9 -12
- package/dist/core/Hash.js.map +1 -1
- package/dist/core/Keystore.d.ts.map +1 -1
- package/dist/core/Keystore.js +15 -10
- package/dist/core/Keystore.js.map +1 -1
- package/dist/core/Mnemonic.d.ts.map +1 -1
- package/dist/core/Mnemonic.js +3 -2
- package/dist/core/Mnemonic.js.map +1 -1
- package/dist/core/P256.d.ts.map +1 -1
- package/dist/core/P256.js +8 -9
- package/dist/core/P256.js.map +1 -1
- package/dist/core/Secp256k1.d.ts.map +1 -1
- package/dist/core/Secp256k1.js +8 -9
- package/dist/core/Secp256k1.js.map +1 -1
- package/dist/core/WebCryptoP256.d.ts.map +1 -1
- package/dist/core/WebCryptoP256.js +2 -2
- package/dist/core/WebCryptoP256.js.map +1 -1
- package/dist/core/X25519.d.ts.map +1 -1
- package/dist/core/X25519.js +4 -3
- package/dist/core/X25519.js.map +1 -1
- package/dist/core/internal/bls.d.ts +12 -0
- package/dist/core/internal/bls.d.ts.map +1 -0
- package/dist/core/internal/bls.js +79 -0
- package/dist/core/internal/bls.js.map +1 -0
- package/dist/core/internal/codec/base64.d.ts +84 -0
- package/dist/core/internal/codec/base64.d.ts.map +1 -0
- package/dist/core/internal/codec/base64.js +177 -0
- package/dist/core/internal/codec/base64.js.map +1 -0
- package/dist/core/internal/codec/hex.d.ts +49 -4
- package/dist/core/internal/codec/hex.d.ts.map +1 -1
- package/dist/core/internal/codec/hex.js +109 -35
- package/dist/core/internal/codec/hex.js.map +1 -1
- package/dist/core/internal/curves.d.ts +17 -0
- package/dist/core/internal/curves.d.ts.map +1 -0
- package/dist/core/internal/curves.js +17 -0
- package/dist/core/internal/curves.js.map +1 -0
- package/dist/core/internal/ed25519.d.ts +14 -0
- package/dist/core/internal/ed25519.d.ts.map +1 -0
- package/dist/core/internal/ed25519.js +29 -0
- package/dist/core/internal/ed25519.js.map +1 -0
- package/dist/core/internal/engine.d.ts +278 -0
- package/dist/core/internal/engine.d.ts.map +1 -0
- package/dist/core/internal/engine.js +131 -0
- package/dist/core/internal/engine.js.map +1 -0
- package/dist/core/internal/hash.d.ts +12 -0
- package/dist/core/internal/hash.d.ts.map +1 -0
- package/dist/core/internal/hash.js +30 -0
- package/dist/core/internal/hash.js.map +1 -0
- package/dist/core/internal/keystore.d.ts +14 -0
- package/dist/core/internal/keystore.d.ts.map +1 -0
- package/dist/core/internal/keystore.js +32 -0
- package/dist/core/internal/keystore.js.map +1 -0
- package/dist/core/internal/mnemonic.d.ts +4 -0
- package/dist/core/internal/mnemonic.d.ts.map +1 -0
- package/dist/core/internal/mnemonic.js +14 -0
- package/dist/core/internal/mnemonic.js.map +1 -0
- package/dist/core/internal/p256.d.ts +14 -0
- package/dist/core/internal/p256.d.ts.map +1 -0
- package/dist/core/internal/p256.js +36 -0
- package/dist/core/internal/p256.js.map +1 -0
- package/dist/core/internal/secp256k1.d.ts +14 -0
- package/dist/core/internal/secp256k1.d.ts.map +1 -0
- package/dist/core/internal/secp256k1.js +36 -0
- package/dist/core/internal/secp256k1.js.map +1 -0
- package/dist/core/internal/x25519.d.ts +8 -0
- package/dist/core/internal/x25519.d.ts.map +1 -0
- package/dist/core/internal/x25519.js +20 -0
- package/dist/core/internal/x25519.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.docs.d.ts +2 -0
- package/dist/index.docs.d.ts.map +1 -1
- package/dist/index.docs.js +2 -0
- package/dist/index.docs.js.map +1 -1
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -1
- package/dist/node/Engine.d.ts +55 -0
- package/dist/node/Engine.d.ts.map +1 -0
- package/dist/node/Engine.js +51 -0
- package/dist/node/Engine.js.map +1 -0
- package/dist/node/Hash.d.ts +39 -0
- package/dist/node/Hash.d.ts.map +1 -0
- package/dist/node/Hash.js +37 -0
- package/dist/node/Hash.js.map +1 -0
- package/dist/node/index.d.ts +39 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +39 -0
- package/dist/node/index.js.map +1 -0
- package/dist/tempo/VirtualMaster.d.ts +2 -0
- package/dist/tempo/VirtualMaster.d.ts.map +1 -1
- package/dist/tempo/VirtualMaster.js +19 -5
- package/dist/tempo/VirtualMaster.js.map +1 -1
- package/dist/tempo/internal/mine.wasm.d.ts +2 -4
- package/dist/tempo/internal/mine.wasm.d.ts.map +1 -1
- package/dist/tempo/internal/mine.wasm.js +7 -12
- package/dist/tempo/internal/mine.wasm.js.map +1 -1
- package/dist/tempo/internal/virtualMasterPool.js +4 -1
- package/dist/tempo/internal/virtualMasterPool.js.map +1 -1
- package/dist/wasm/Engine.d.ts +62 -0
- package/dist/wasm/Engine.d.ts.map +1 -0
- package/dist/wasm/Engine.js +57 -0
- package/dist/wasm/Engine.js.map +1 -0
- package/dist/wasm/Hash.d.ts +49 -0
- package/dist/wasm/Hash.d.ts.map +1 -0
- package/dist/wasm/Hash.js +70 -0
- package/dist/wasm/Hash.js.map +1 -0
- package/dist/wasm/index.d.ts +46 -0
- package/dist/wasm/index.d.ts.map +1 -0
- package/dist/wasm/index.js +46 -0
- package/dist/wasm/index.js.map +1 -0
- package/dist/wasm/internal/hash.d.ts +9 -0
- package/dist/wasm/internal/hash.d.ts.map +1 -0
- package/dist/wasm/internal/hash.js +26 -0
- package/dist/wasm/internal/hash.js.map +1 -0
- package/dist/wasm/internal/hashes.wasm.d.ts +5 -0
- package/dist/wasm/internal/hashes.wasm.d.ts.map +1 -0
- package/dist/wasm/internal/hashes.wasm.js +12 -0
- package/dist/wasm/internal/hashes.wasm.js.map +1 -0
- package/dist/wasm/internal/instantiate.d.ts +56 -0
- package/dist/wasm/internal/instantiate.d.ts.map +1 -0
- package/dist/wasm/internal/instantiate.js +92 -0
- package/dist/wasm/internal/instantiate.js.map +1 -0
- package/dist/zod/TypedData.d.ts +12 -12
- package/package.json +36 -1
- package/src/core/Base64.ts +19 -166
- package/src/core/BinaryStateTree.ts +2 -3
- package/src/core/Bls.ts +27 -77
- package/src/core/Ed25519.ts +7 -7
- package/src/core/Engine.ts +235 -0
- package/src/core/Hash.ts +9 -12
- package/src/core/Keystore.ts +17 -17
- package/src/core/Mnemonic.ts +3 -6
- package/src/core/P256.ts +15 -24
- package/src/core/Rlp_tx.bench.ts +2 -4
- package/src/core/Secp256k1.ts +9 -14
- package/src/core/TxEnvelopeEip1559.bench.ts +0 -5
- package/src/core/WebCryptoP256.ts +2 -2
- package/src/core/X25519.ts +4 -3
- package/src/core/_test/AbiParameters.adversarial.fuzz.ts +1 -2
- package/src/core/_test/AbiParameters.fuzz.ts +1 -2
- package/src/core/_test/Base.fuzz.ts +1 -2
- package/src/core/_test/Cbor.fuzz.ts +1 -2
- package/src/core/_test/Engine.fuzz.ts +293 -0
- package/src/core/_test/Engine.test-d.ts +84 -0
- package/src/core/_test/Engine.test.ts +523 -0
- package/src/core/_test/Hash.vectors.test.ts +56 -0
- package/src/core/_test/Hex.fuzz.ts +1 -2
- package/src/core/_test/Rlp.fuzz.ts +1 -2
- package/src/core/_test/Signature.fuzz.ts +1 -2
- package/src/core/_test/curves.test.ts +7 -0
- package/src/core/_test/index.test.ts +1 -0
- package/src/core/internal/bls.ts +121 -0
- package/src/core/internal/codec/_test/base64.conformance.ts +156 -0
- package/src/core/internal/codec/_test/hex.conformance.ts +204 -0
- package/src/core/internal/codec/_test/hex.fuzz.ts +234 -0
- package/src/core/internal/codec/_test/hex.test.ts +124 -0
- package/src/core/internal/codec/base64.ts +222 -0
- package/src/core/internal/codec/hex.ts +115 -34
- package/src/core/internal/curves.ts +18 -0
- package/src/core/internal/ed25519.ts +64 -0
- package/src/core/internal/engine.ts +410 -0
- package/src/core/internal/hash.ts +48 -0
- package/src/core/internal/keystore.ts +107 -0
- package/src/core/internal/mnemonic.ts +18 -0
- package/src/core/internal/p256.ts +91 -0
- package/src/core/internal/secp256k1.ts +91 -0
- package/src/core/internal/x25519.ts +40 -0
- package/src/index.docs.ts +2 -0
- package/src/index.ts +36 -0
- package/src/node/Engine.ts +69 -0
- package/src/node/Hash.ts +56 -0
- package/src/node/_test/Engine.test.ts +62 -0
- package/src/node/_test/Hash.test-d.ts +26 -0
- package/src/node/_test/Hash.test.ts +102 -0
- package/src/node/index.ts +40 -0
- package/src/tempo/VirtualMaster.test.ts +75 -0
- package/src/tempo/VirtualMaster.ts +28 -6
- package/src/tempo/internal/mine.wasm.ts +7 -13
- package/src/tempo/internal/virtualMasterPool.ts +4 -1
- package/src/tsconfig.json +2 -1
- package/src/version.ts +1 -1
- package/src/wasm/Engine.ts +76 -0
- package/src/wasm/Hash.bench.ts +60 -0
- package/src/wasm/Hash.ts +109 -0
- package/src/wasm/_test/Engine.test.ts +62 -0
- package/src/wasm/_test/Hash.browser.test.ts +50 -0
- package/src/wasm/_test/Hash.fuzz.ts +188 -0
- package/src/wasm/_test/Hash.test-d.ts +28 -0
- package/src/wasm/_test/Hash.test.ts +211 -0
- package/src/wasm/_test/Hash.vectors.test.ts +117 -0
- package/src/wasm/_test/Runtime.test.ts +21 -0
- package/src/wasm/_test/runtime.wasm.ts +11 -0
- package/src/wasm/index.ts +47 -0
- package/src/wasm/internal/hash.ts +50 -0
- package/src/wasm/internal/hashes.wasm.ts +14 -0
- package/src/wasm/internal/instantiate.ts +135 -0
- package/src/tempo/internal/mine.c +0 -182
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { fc, test } from '@fast-check/vitest'
|
|
2
|
+
import { describe, expect } from 'vp/test'
|
|
3
|
+
import {
|
|
4
|
+
bytesToHex,
|
|
5
|
+
bytesToHexLoop,
|
|
6
|
+
hexToBytes,
|
|
7
|
+
hexToBytesLoop,
|
|
8
|
+
} from '../hex.js'
|
|
9
|
+
import { numRuns } from '../../../../../test/fuzz/numRuns.js'
|
|
10
|
+
|
|
11
|
+
// The implementations as they stood before the tiering change, vendored so the
|
|
12
|
+
// suite has an oracle independent of the code under test. Any disagreement is
|
|
13
|
+
// either a regression or a deliberate behavior change that should be argued for
|
|
14
|
+
// in review -- not something to discover in the wild.
|
|
15
|
+
|
|
16
|
+
const referenceHexes = Array.from({ length: 256 }, (_v, i) =>
|
|
17
|
+
i.toString(16).padStart(2, '0'),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
function referenceEncode(value: Uint8Array): string {
|
|
21
|
+
const length = value.length
|
|
22
|
+
const parts = Array.from<string>({ length })
|
|
23
|
+
for (let i = 0; i < length; i++) parts[i] = referenceHexes[value[i]!]!
|
|
24
|
+
return `0x${parts.join('')}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether the previous decoder can be reconstructed here at all. It was built
|
|
29
|
+
* on `Buffer`, so on a runtime without one there is nothing to compare against
|
|
30
|
+
* -- and nothing to regress, since such a runtime always used the JS loop.
|
|
31
|
+
*/
|
|
32
|
+
const hasReference = typeof globalThis.Buffer !== 'undefined'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The previous decoder: `Buffer` at every size, with the JS loop reachable only
|
|
36
|
+
* where `Buffer` was absent. Throws a bare `Error` rather than ox's error
|
|
37
|
+
* classes, so properties compare *whether* it threw, not what it threw.
|
|
38
|
+
*/
|
|
39
|
+
function referenceDecode(value: string): Uint8Array {
|
|
40
|
+
if (
|
|
41
|
+
typeof value !== 'string' ||
|
|
42
|
+
value.length < 2 ||
|
|
43
|
+
value.charCodeAt(0) !== 48 ||
|
|
44
|
+
value.charCodeAt(1) !== 120
|
|
45
|
+
)
|
|
46
|
+
throw new Error('invalid')
|
|
47
|
+
const body = value.length === 2 ? '' : value.slice(2)
|
|
48
|
+
if ((body.length & 1) !== 0) throw new Error('odd')
|
|
49
|
+
if (body.length > 0) {
|
|
50
|
+
const expected = body.length >> 1
|
|
51
|
+
const buf = Buffer.from(body, 'hex')
|
|
52
|
+
if (buf.length !== expected) throw new Error('invalid')
|
|
53
|
+
const out = new Uint8Array(buf.byteLength)
|
|
54
|
+
out.set(buf)
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
return new Uint8Array(0)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Runs `fn`, reporting either its value or the fact that it threw. */
|
|
61
|
+
function outcome<value>(
|
|
62
|
+
fn: () => value,
|
|
63
|
+
): { ok: true; value: value } | { ok: false } {
|
|
64
|
+
try {
|
|
65
|
+
return { ok: true, value: fn() }
|
|
66
|
+
} catch {
|
|
67
|
+
return { ok: false }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Sizes cluster at the tier boundary (`loopDecodeMaxBytes` is 32 or 64
|
|
72
|
+
// depending on the runtime), where an off-by-one is likeliest to hide.
|
|
73
|
+
const arbitraryBoundaryBytes = fc.oneof(
|
|
74
|
+
fc.uint8Array({ minLength: 0, maxLength: 4 }),
|
|
75
|
+
fc.uint8Array({ minLength: 30, maxLength: 34 }),
|
|
76
|
+
fc.uint8Array({ minLength: 62, maxLength: 66 }),
|
|
77
|
+
fc.uint8Array({ minLength: 0, maxLength: 300 }),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A single character: the troublemakers test262 enumerates, plus any code point
|
|
82
|
+
* at all -- including lone surrogates and astral characters, which sit far
|
|
83
|
+
* above the Latin-1 lookup table the decoder reads through.
|
|
84
|
+
*/
|
|
85
|
+
const arbitraryChar = fc.oneof(
|
|
86
|
+
fc.constantFrom(
|
|
87
|
+
'.',
|
|
88
|
+
'^',
|
|
89
|
+
' ',
|
|
90
|
+
'\t',
|
|
91
|
+
'\n',
|
|
92
|
+
'\r',
|
|
93
|
+
'\u00a0',
|
|
94
|
+
'\u2009',
|
|
95
|
+
'\u2028',
|
|
96
|
+
'\u0000',
|
|
97
|
+
'\u00ff',
|
|
98
|
+
'\u0100',
|
|
99
|
+
'\ud800',
|
|
100
|
+
'\u{1f600}',
|
|
101
|
+
'g',
|
|
102
|
+
'G',
|
|
103
|
+
'z',
|
|
104
|
+
'Z',
|
|
105
|
+
'+',
|
|
106
|
+
'-',
|
|
107
|
+
'/',
|
|
108
|
+
),
|
|
109
|
+
fc.integer({ min: 0, max: 0x10ffff }).map((c) => String.fromCodePoint(c)),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
/** Valid hex with exactly one character swapped for an arbitrary one. */
|
|
113
|
+
const arbitraryCorruptedHex = fc
|
|
114
|
+
.tuple(
|
|
115
|
+
fc.uint8Array({ minLength: 1, maxLength: 80 }),
|
|
116
|
+
fc.nat(),
|
|
117
|
+
arbitraryChar,
|
|
118
|
+
)
|
|
119
|
+
.map(([bytes, index, char]) => {
|
|
120
|
+
const body = referenceEncode(bytes).slice(2)
|
|
121
|
+
const at = index % body.length
|
|
122
|
+
return `0x${body.slice(0, at)}${char}${body.slice(at + 1)}`
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe.runIf(hasReference)('encode: new vs previous implementation', () => {
|
|
126
|
+
test.prop({ bytes: arbitraryBoundaryBytes }, { numRuns })(
|
|
127
|
+
'bytesToHexLoop agrees with the previous loop',
|
|
128
|
+
({ bytes }) => {
|
|
129
|
+
expect(bytesToHexLoop(bytes)).toBe(referenceEncode(bytes))
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
test.prop({ bytes: arbitraryBoundaryBytes }, { numRuns })(
|
|
134
|
+
'the dispatched encoder agrees with the previous loop',
|
|
135
|
+
({ bytes }) => {
|
|
136
|
+
expect(bytesToHex(bytes)).toBe(referenceEncode(bytes))
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe.runIf(hasReference)('decode: new vs previous implementation', () => {
|
|
142
|
+
// The previous decoder had a bug: `Buffer.from(…, 'hex')` masks UTF-16 code
|
|
143
|
+
// units to 8 bits, so a character above U+00FF could alias a hex digit and
|
|
144
|
+
// decode instead of being refused. That is now rejected, which is the one
|
|
145
|
+
// intended difference between the two. For pure-ASCII input -- everything a
|
|
146
|
+
// caller could legitimately pass -- they must still agree exactly.
|
|
147
|
+
const isAscii = (value: string) => {
|
|
148
|
+
for (let i = 0; i < value.length; i++)
|
|
149
|
+
if (value.charCodeAt(i) > 0x7f) return false
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
test.prop({ bytes: arbitraryBoundaryBytes }, { numRuns })(
|
|
154
|
+
'valid input decodes identically',
|
|
155
|
+
({ bytes }) => {
|
|
156
|
+
const hex = referenceEncode(bytes)
|
|
157
|
+
expect(hexToBytes(hex)).toEqual(referenceDecode(hex))
|
|
158
|
+
expect(hexToBytesLoop(hex, bytes.length)).toEqual(referenceDecode(hex))
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
test.prop({ hex: arbitraryCorruptedHex }, { numRuns })(
|
|
163
|
+
'pure-ASCII corruption is accepted or rejected identically',
|
|
164
|
+
({ hex }) => {
|
|
165
|
+
fc.pre(isAscii(hex))
|
|
166
|
+
expect(outcome(() => hexToBytes(hex))).toEqual(
|
|
167
|
+
outcome(() => referenceDecode(hex)),
|
|
168
|
+
)
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
test.prop({ value: fc.string({ unit: 'grapheme' }) }, { numRuns })(
|
|
173
|
+
'pure-ASCII strings are accepted or rejected identically',
|
|
174
|
+
({ value }) => {
|
|
175
|
+
fc.pre(isAscii(value))
|
|
176
|
+
const hex = `0x${value}`
|
|
177
|
+
expect(outcome(() => hexToBytes(hex))).toEqual(
|
|
178
|
+
outcome(() => referenceDecode(hex)),
|
|
179
|
+
)
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
test.prop({ hex: arbitraryCorruptedHex }, { numRuns })(
|
|
184
|
+
'non-ASCII input is always rejected',
|
|
185
|
+
({ hex }) => {
|
|
186
|
+
fc.pre(!isAscii(hex))
|
|
187
|
+
expect(() => hexToBytes(hex)).toThrow()
|
|
188
|
+
const length = (hex.length - 2) >> 1
|
|
189
|
+
if ((hex.length - 2) % 2 === 0)
|
|
190
|
+
expect(() => hexToBytesLoop(hex, length)).toThrow()
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('decode: tiers agree with each other', () => {
|
|
196
|
+
// Prefixing with valid hex pushes the same body across the size threshold and
|
|
197
|
+
// through a different tier. The prefix cannot change whether the body is
|
|
198
|
+
// valid, so the two must agree on outcome, and on value up to the prefix.
|
|
199
|
+
const prefix = 'ab'.repeat(96)
|
|
200
|
+
const prefixBytes = hexToBytes(`0x${prefix}`)
|
|
201
|
+
|
|
202
|
+
test.prop({ hex: arbitraryCorruptedHex }, { numRuns })(
|
|
203
|
+
'the tier chosen by size does not change the verdict',
|
|
204
|
+
({ hex }) => {
|
|
205
|
+
const body = hex.slice(2)
|
|
206
|
+
const short = outcome(() => hexToBytes(`0x${body}`))
|
|
207
|
+
const long = outcome(() => hexToBytes(`0x${prefix}${body}`))
|
|
208
|
+
expect(long.ok).toBe(short.ok)
|
|
209
|
+
if (short.ok && long.ok)
|
|
210
|
+
expect(long.value).toEqual(
|
|
211
|
+
Uint8Array.from([...prefixBytes, ...short.value]),
|
|
212
|
+
)
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
test.prop({ bytes: arbitraryBoundaryBytes }, { numRuns })(
|
|
217
|
+
'the loop agrees with whichever tier size selects',
|
|
218
|
+
({ bytes }) => {
|
|
219
|
+
const hex = referenceEncode(bytes)
|
|
220
|
+
expect(hexToBytesLoop(hex, bytes.length)).toEqual(hexToBytes(hex))
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe('round-trip', () => {
|
|
226
|
+
test.prop({ bytes: arbitraryBoundaryBytes }, { numRuns })(
|
|
227
|
+
'decode(encode(b)) is b, through every tier',
|
|
228
|
+
({ bytes }) => {
|
|
229
|
+
expect(hexToBytes(bytesToHex(bytes))).toEqual(bytes)
|
|
230
|
+
expect(hexToBytes(bytesToHexLoop(bytes))).toEqual(bytes)
|
|
231
|
+
expect(hexToBytesLoop(bytesToHexLoop(bytes), bytes.length)).toEqual(bytes)
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Bytes, Hex } from 'ox'
|
|
2
|
+
import { describe, expect, test } from 'vp/test'
|
|
3
|
+
import {
|
|
4
|
+
bytesToHex,
|
|
5
|
+
bytesToHexLoop,
|
|
6
|
+
hexToBytes,
|
|
7
|
+
hexToBytesLoop,
|
|
8
|
+
} from '../hex.js'
|
|
9
|
+
|
|
10
|
+
// The dispatched `bytesToHex` / `hexToBytes` pick a native method or `Buffer`
|
|
11
|
+
// when the runtime offers one, so the JS loops underneath them are unreachable
|
|
12
|
+
// on Node. They back every browser without `Uint8Array.prototype.toHex`, so
|
|
13
|
+
// they are exercised directly here rather than left to whichever tier the test
|
|
14
|
+
// runtime happens to take.
|
|
15
|
+
|
|
16
|
+
const sizes = [0, 1, 2, 19, 20, 31, 32, 33, 48, 64, 127, 128, 1024]
|
|
17
|
+
|
|
18
|
+
describe('bytesToHexLoop', () => {
|
|
19
|
+
test('agrees with the dispatched encoder at every size', () => {
|
|
20
|
+
for (const size of sizes) {
|
|
21
|
+
const value = Bytes.random(size)
|
|
22
|
+
expect(bytesToHexLoop(value)).toBe(bytesToHex(value))
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('behavior: empty input', () => {
|
|
27
|
+
expect(bytesToHexLoop(new Uint8Array(0))).toMatchInlineSnapshot(`"0x"`)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('behavior: pads each byte to two nibbles', () => {
|
|
31
|
+
expect(
|
|
32
|
+
bytesToHexLoop(Uint8Array.of(0, 1, 15, 16, 255)),
|
|
33
|
+
).toMatchInlineSnapshot(`"0x00010f10ff"`)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('behavior: reads through a subarray offset', () => {
|
|
37
|
+
const backing = Uint8Array.of(0xde, 0xad, 0xbe, 0xef)
|
|
38
|
+
expect(bytesToHexLoop(backing.subarray(1, 3))).toMatchInlineSnapshot(
|
|
39
|
+
`"0xadbe"`,
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('hexToBytesLoop', () => {
|
|
45
|
+
test('agrees with the dispatched decoder at every size', () => {
|
|
46
|
+
for (const size of sizes) {
|
|
47
|
+
const hex = Hex.fromBytes(Bytes.random(size))
|
|
48
|
+
expect(hexToBytesLoop(hex, size)).toEqual(hexToBytes(hex))
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('behavior: empty input', () => {
|
|
53
|
+
expect(hexToBytesLoop('0x', 0)).toMatchInlineSnapshot(`Uint8Array []`)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('behavior: accepts uppercase nibbles', () => {
|
|
57
|
+
expect(hexToBytesLoop('0xDEADBEEF', 4)).toEqual(hexToBytes('0xdeadbeef'))
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('error: invalid character in the high nibble', () => {
|
|
61
|
+
expect(() => hexToBytesLoop('0xzz', 1)).toThrowErrorMatchingInlineSnapshot(
|
|
62
|
+
`
|
|
63
|
+
[Hex.InvalidHexValueError: Value \`0xzz\` is an invalid hex value.
|
|
64
|
+
|
|
65
|
+
Hex values must start with \`"0x"\` and contain only hexadecimal characters (0-9, a-f, A-F).]
|
|
66
|
+
`,
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('error: invalid character in the low nibble', () => {
|
|
71
|
+
expect(() => hexToBytesLoop('0x0g', 1)).toThrowError(
|
|
72
|
+
Hex.InvalidHexValueError,
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('error: invalid character in a trailing byte', () => {
|
|
77
|
+
// The sentinel is accumulated across the loop, so a late bad nibble must
|
|
78
|
+
// still be caught rather than masked by the valid ones before it.
|
|
79
|
+
expect(() => hexToBytesLoop('0xdeadbe$f', 4)).toThrowError(
|
|
80
|
+
Hex.InvalidHexValueError,
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('error: characters beyond Latin-1', () => {
|
|
85
|
+
// These index past `nibbleTable`, so they read `undefined` rather than the
|
|
86
|
+
// sentinel; unmapped, they would OR in as zero and decode to `0x00`.
|
|
87
|
+
for (const value of ['0x€€', '0xĀĀ', '0xǰ0'])
|
|
88
|
+
expect(() => hexToBytesLoop(value, 1)).toThrowError(
|
|
89
|
+
Hex.InvalidHexValueError,
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('hexToBytes', () => {
|
|
95
|
+
// `loopDecodeMaxBytes` is 32; both sides of it must behave identically.
|
|
96
|
+
test('behavior: matches across the loop/Buffer threshold', () => {
|
|
97
|
+
for (const size of [31, 32, 33, 34]) {
|
|
98
|
+
const bytes = Bytes.random(size)
|
|
99
|
+
expect(hexToBytes(Hex.fromBytes(bytes))).toEqual(bytes)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('error: invalid character above the threshold', () => {
|
|
104
|
+
const hex = `0x${'ab'.repeat(40)}zz` as const
|
|
105
|
+
expect(() => hexToBytes(hex)).toThrowError(Hex.InvalidHexValueError)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('error: odd length', () => {
|
|
109
|
+
expect(() => hexToBytes('0xabc')).toThrowError(Hex.InvalidLengthError)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('error: characters beyond Latin-1, both sides of the threshold', () => {
|
|
113
|
+
for (const value of ['0x€€', `0x${'ab'.repeat(40)}€€`])
|
|
114
|
+
expect(() => hexToBytes(value)).toThrowError(Hex.InvalidHexValueError)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('behavior: result does not alias a pooled buffer', () => {
|
|
118
|
+
// Above the threshold the decoder goes through `Buffer`, which allocates
|
|
119
|
+
// from a shared pool; the returned bytes must own their backing store.
|
|
120
|
+
const bytes = hexToBytes(Hex.fromBytes(Bytes.random(64)))
|
|
121
|
+
expect(bytes.byteOffset).toBe(0)
|
|
122
|
+
expect(bytes.buffer.byteLength).toBe(64)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 codec tiers, and the tables they share.
|
|
3
|
+
*
|
|
4
|
+
* Split out of the public `Base64` module for the same reason the hex codec is:
|
|
5
|
+
* a runtime selects one tier at import, so the others are unreachable from a
|
|
6
|
+
* test unless they can be called directly. Keeping them here lets the
|
|
7
|
+
* conformance suite compare every tier a runtime can execute, without widening
|
|
8
|
+
* `Base64`'s public surface.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as Errors from '../../Errors.js'
|
|
14
|
+
import type * as Bytes from '../../Bytes.js'
|
|
15
|
+
import { decoder } from './utf8.js'
|
|
16
|
+
|
|
17
|
+
/** @internal */
|
|
18
|
+
export type Options = {
|
|
19
|
+
/** Whether to pad the encoded string. */
|
|
20
|
+
pad?: boolean | undefined
|
|
21
|
+
/** Whether to use the URL-safe alphabet. */
|
|
22
|
+
url?: boolean | undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Standard Base64 alphabet (RFC 4648 section 4) and URL-safe alphabet (section 5).
|
|
26
|
+
const stdAlphabet =
|
|
27
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
28
|
+
|
|
29
|
+
// Char-code -> 6-bit integer lookup. Sentinel `0xff` = invalid character.
|
|
30
|
+
// Standard alphabet plus URL-safe `-`/`_` (decoders are alphabet-agnostic).
|
|
31
|
+
const characterToInteger = /*#__PURE__*/ (() => {
|
|
32
|
+
const table = new Uint8Array(256).fill(0xff)
|
|
33
|
+
for (let i = 0; i < stdAlphabet.length; i++)
|
|
34
|
+
table[stdAlphabet.charCodeAt(i)] = i
|
|
35
|
+
// URL-safe alternates: `-` for `+` (62), `_` for `/` (63).
|
|
36
|
+
table[45 /* '-' */] = 62
|
|
37
|
+
table[95 /* '_' */] = 63
|
|
38
|
+
return table
|
|
39
|
+
})()
|
|
40
|
+
|
|
41
|
+
// Integer -> char-code table for the standard alphabet, indexed by 6-bit value.
|
|
42
|
+
const integerToCharacter = /*#__PURE__*/ (() => {
|
|
43
|
+
const table = new Uint8Array(64)
|
|
44
|
+
for (let i = 0; i < stdAlphabet.length; i++)
|
|
45
|
+
table[i] = stdAlphabet.charCodeAt(i)
|
|
46
|
+
return table
|
|
47
|
+
})()
|
|
48
|
+
|
|
49
|
+
// Native fast-path detection. `Uint8Array.prototype.toBase64` ships in
|
|
50
|
+
// Chromium 145, Safari 18.2+, Firefox 133+ and Bun, but not in Node 24.
|
|
51
|
+
//
|
|
52
|
+
// Only encoding delegates to it; see `toBytes` for why decoding cannot.
|
|
53
|
+
const nativeToBase64:
|
|
54
|
+
| ((
|
|
55
|
+
this: Uint8Array,
|
|
56
|
+
options?: { alphabet?: 'base64' | 'base64url'; omitPadding?: boolean },
|
|
57
|
+
) => string)
|
|
58
|
+
| undefined = (
|
|
59
|
+
Uint8Array.prototype as Uint8Array & {
|
|
60
|
+
toBase64?: (options?: {
|
|
61
|
+
alphabet?: 'base64' | 'base64url'
|
|
62
|
+
omitPadding?: boolean
|
|
63
|
+
}) => string
|
|
64
|
+
}
|
|
65
|
+
).toBase64
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Encodes via `Uint8Array.prototype.toBase64`, where the runtime has it.
|
|
69
|
+
* `undefined` otherwise, so the conformance suite can ask whether this tier
|
|
70
|
+
* exists rather than inferring it from the runtime.
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
export const fromBytesNative:
|
|
75
|
+
| ((value: Bytes.Bytes, options?: Options) => string)
|
|
76
|
+
| undefined =
|
|
77
|
+
nativeToBase64 &&
|
|
78
|
+
((value, options = {}) => {
|
|
79
|
+
const { pad = true, url = false } = options
|
|
80
|
+
// `omitPadding` governs both alphabets, so `base64url` is padded here too
|
|
81
|
+
// when asked. Appending padding on top of that double-pads it.
|
|
82
|
+
return nativeToBase64.call(value, {
|
|
83
|
+
alphabet: url ? 'base64url' : 'base64',
|
|
84
|
+
omitPadding: !pad,
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Encodes in plain JavaScript, for runtimes without the native method.
|
|
90
|
+
*
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
export function fromBytesLoop(
|
|
94
|
+
value: Bytes.Bytes,
|
|
95
|
+
options: Options = {},
|
|
96
|
+
): string {
|
|
97
|
+
const { pad = true, url = false } = options
|
|
98
|
+
|
|
99
|
+
const encoded = new Uint8Array(Math.ceil(value.length / 3) * 4)
|
|
100
|
+
|
|
101
|
+
for (let i = 0, j = 0; j < value.length; i += 4, j += 3) {
|
|
102
|
+
const y = (value[j]! << 16) + (value[j + 1]! << 8) + (value[j + 2]! | 0)
|
|
103
|
+
encoded[i] = integerToCharacter[y >> 18]!
|
|
104
|
+
encoded[i + 1] = integerToCharacter[(y >> 12) & 0x3f]!
|
|
105
|
+
encoded[i + 2] = integerToCharacter[(y >> 6) & 0x3f]!
|
|
106
|
+
encoded[i + 3] = integerToCharacter[y & 0x3f]!
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const k = value.length % 3
|
|
110
|
+
const end = Math.floor(value.length / 3) * 4 + (k && k + 1)
|
|
111
|
+
let base64 = decoder.decode(new Uint8Array(encoded.buffer, 0, end))
|
|
112
|
+
if (pad && k === 1) base64 += '=='
|
|
113
|
+
if (pad && k === 2) base64 += '='
|
|
114
|
+
if (url) base64 = base64.replaceAll('+', '-').replaceAll('/', '_')
|
|
115
|
+
return base64
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Encodes with whichever tier this runtime provides.
|
|
120
|
+
*
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
123
|
+
export function fromBytes(value: Bytes.Bytes, options: Options = {}): string {
|
|
124
|
+
if (fromBytesNative) return fromBytesNative(value, options)
|
|
125
|
+
return fromBytesLoop(value, options)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* `Uint8Array.fromBase64` is deliberately unused.
|
|
130
|
+
*
|
|
131
|
+
* Its `alphabet` option is strict and mutually exclusive -- `base64` refuses
|
|
132
|
+
* `-`/`_`, `base64url` refuses `+`/`/` -- while ox decodes either alphabet
|
|
133
|
+
* transparently. There is no single native call with that behavior, so matching
|
|
134
|
+
* it would mean normalizing the string first or trying both alphabets and
|
|
135
|
+
* throwing on one. Both cost more than they would save, and neither has been
|
|
136
|
+
* measured; the loop is the only decode tier until one is.
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Decodes in plain JavaScript, for runtimes without the native method.
|
|
141
|
+
*
|
|
142
|
+
* @internal
|
|
143
|
+
*/
|
|
144
|
+
export function toBytesLoop(value: string, bodyEnd: number): Bytes.Bytes {
|
|
145
|
+
const size = bodyEnd
|
|
146
|
+
|
|
147
|
+
// Validate characters: every char must be in the standard or URL-safe
|
|
148
|
+
// alphabet, and '=' may not appear in the body (only stripped trailing
|
|
149
|
+
// padding).
|
|
150
|
+
const decoded = new Uint8Array(
|
|
151
|
+
(size >> 2) * 3 + (size % 4 ? (size % 4) - 1 : 0),
|
|
152
|
+
)
|
|
153
|
+
let acc = 0
|
|
154
|
+
let bits = 0
|
|
155
|
+
let n = 0
|
|
156
|
+
for (let i = 0; i < size; i++) {
|
|
157
|
+
const code = value.charCodeAt(i)
|
|
158
|
+
if (code === 61 /* '=' */)
|
|
159
|
+
throw new InvalidCharacterError({ character: '=' })
|
|
160
|
+
// `characterToInteger` only spans Latin-1, so a code unit above it reads
|
|
161
|
+
// `undefined` and has to be mapped to the sentinel. Without that, `undefined`
|
|
162
|
+
// fails the `=== 0xff` test, then coerces to zero in the shift below, and a
|
|
163
|
+
// non-ASCII character silently decodes as `A`.
|
|
164
|
+
const v = characterToInteger[code] ?? 0xff
|
|
165
|
+
if (v === 0xff) throw new InvalidCharacterError({ character: value[i]! })
|
|
166
|
+
acc = (acc << 6) | v
|
|
167
|
+
bits += 6
|
|
168
|
+
if (bits >= 8) {
|
|
169
|
+
bits -= 8
|
|
170
|
+
decoded[n++] = (acc >>> bits) & 0xff
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return decoded
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Decodes with whichever tier this runtime provides.
|
|
178
|
+
*
|
|
179
|
+
* @internal
|
|
180
|
+
*/
|
|
181
|
+
export function toBytes(value: string): Bytes.Bytes {
|
|
182
|
+
// Strip trailing '=' padding (only at the very end).
|
|
183
|
+
let bodyEnd = value.length
|
|
184
|
+
let pad = 0
|
|
185
|
+
while (bodyEnd > 0 && value.charCodeAt(bodyEnd - 1) === 61 /* '=' */) {
|
|
186
|
+
bodyEnd--
|
|
187
|
+
pad++
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Reject impossible lengths and excessive padding.
|
|
191
|
+
if (bodyEnd % 4 === 1) throw new InvalidLengthError({ length: value.length })
|
|
192
|
+
if (pad > 2) throw new InvalidPaddingError({ padding: pad })
|
|
193
|
+
|
|
194
|
+
return toBytesLoop(value, bodyEnd)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Thrown when a Base64 string contains an invalid character. */
|
|
198
|
+
export class InvalidCharacterError extends Errors.BaseError {
|
|
199
|
+
override readonly name = 'Base64.InvalidCharacterError'
|
|
200
|
+
|
|
201
|
+
constructor({ character }: { character: string }) {
|
|
202
|
+
super(`Invalid Base64 character: "${character}".`)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Thrown when a Base64 string has an impossible length. */
|
|
207
|
+
export class InvalidLengthError extends Errors.BaseError {
|
|
208
|
+
override readonly name = 'Base64.InvalidLengthError'
|
|
209
|
+
|
|
210
|
+
constructor({ length }: { length: number }) {
|
|
211
|
+
super(`Invalid Base64 input length \`${length}\`.`)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Thrown when a Base64 string contains too many trailing `=` padding characters. */
|
|
216
|
+
export class InvalidPaddingError extends Errors.BaseError {
|
|
217
|
+
override readonly name = 'Base64.InvalidPaddingError'
|
|
218
|
+
|
|
219
|
+
constructor({ padding }: { padding: number }) {
|
|
220
|
+
super(`Invalid Base64 padding length \`${padding}\` (must be 0, 1, or 2).`)
|
|
221
|
+
}
|
|
222
|
+
}
|