ox 1.0.4 → 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.
Files changed (230) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/core/Base64.d.ts +10 -21
  3. package/dist/core/Base64.d.ts.map +1 -1
  4. package/dist/core/Base64.js +8 -127
  5. package/dist/core/Base64.js.map +1 -1
  6. package/dist/core/BinaryStateTree.d.ts.map +1 -1
  7. package/dist/core/BinaryStateTree.js +2 -2
  8. package/dist/core/BinaryStateTree.js.map +1 -1
  9. package/dist/core/Bls.d.ts.map +1 -1
  10. package/dist/core/Bls.js +19 -44
  11. package/dist/core/Bls.js.map +1 -1
  12. package/dist/core/Ed25519.d.ts.map +1 -1
  13. package/dist/core/Ed25519.js +7 -6
  14. package/dist/core/Ed25519.js.map +1 -1
  15. package/dist/core/Engine.d.ts +124 -0
  16. package/dist/core/Engine.d.ts.map +1 -0
  17. package/dist/core/Engine.js +180 -0
  18. package/dist/core/Engine.js.map +1 -0
  19. package/dist/core/Hash.d.ts +4 -4
  20. package/dist/core/Hash.d.ts.map +1 -1
  21. package/dist/core/Hash.js +9 -12
  22. package/dist/core/Hash.js.map +1 -1
  23. package/dist/core/Keystore.d.ts.map +1 -1
  24. package/dist/core/Keystore.js +15 -10
  25. package/dist/core/Keystore.js.map +1 -1
  26. package/dist/core/Mnemonic.d.ts.map +1 -1
  27. package/dist/core/Mnemonic.js +3 -2
  28. package/dist/core/Mnemonic.js.map +1 -1
  29. package/dist/core/P256.d.ts.map +1 -1
  30. package/dist/core/P256.js +8 -9
  31. package/dist/core/P256.js.map +1 -1
  32. package/dist/core/Secp256k1.d.ts.map +1 -1
  33. package/dist/core/Secp256k1.js +8 -9
  34. package/dist/core/Secp256k1.js.map +1 -1
  35. package/dist/core/WebCryptoP256.d.ts.map +1 -1
  36. package/dist/core/WebCryptoP256.js +2 -2
  37. package/dist/core/WebCryptoP256.js.map +1 -1
  38. package/dist/core/X25519.d.ts.map +1 -1
  39. package/dist/core/X25519.js +4 -3
  40. package/dist/core/X25519.js.map +1 -1
  41. package/dist/core/internal/bls.d.ts +12 -0
  42. package/dist/core/internal/bls.d.ts.map +1 -0
  43. package/dist/core/internal/bls.js +79 -0
  44. package/dist/core/internal/bls.js.map +1 -0
  45. package/dist/core/internal/codec/base64.d.ts +84 -0
  46. package/dist/core/internal/codec/base64.d.ts.map +1 -0
  47. package/dist/core/internal/codec/base64.js +177 -0
  48. package/dist/core/internal/codec/base64.js.map +1 -0
  49. package/dist/core/internal/codec/hex.d.ts +49 -4
  50. package/dist/core/internal/codec/hex.d.ts.map +1 -1
  51. package/dist/core/internal/codec/hex.js +109 -35
  52. package/dist/core/internal/codec/hex.js.map +1 -1
  53. package/dist/core/internal/curves.d.ts +17 -0
  54. package/dist/core/internal/curves.d.ts.map +1 -0
  55. package/dist/core/internal/curves.js +17 -0
  56. package/dist/core/internal/curves.js.map +1 -0
  57. package/dist/core/internal/ed25519.d.ts +14 -0
  58. package/dist/core/internal/ed25519.d.ts.map +1 -0
  59. package/dist/core/internal/ed25519.js +29 -0
  60. package/dist/core/internal/ed25519.js.map +1 -0
  61. package/dist/core/internal/engine.d.ts +278 -0
  62. package/dist/core/internal/engine.d.ts.map +1 -0
  63. package/dist/core/internal/engine.js +131 -0
  64. package/dist/core/internal/engine.js.map +1 -0
  65. package/dist/core/internal/hash.d.ts +12 -0
  66. package/dist/core/internal/hash.d.ts.map +1 -0
  67. package/dist/core/internal/hash.js +30 -0
  68. package/dist/core/internal/hash.js.map +1 -0
  69. package/dist/core/internal/keystore.d.ts +14 -0
  70. package/dist/core/internal/keystore.d.ts.map +1 -0
  71. package/dist/core/internal/keystore.js +32 -0
  72. package/dist/core/internal/keystore.js.map +1 -0
  73. package/dist/core/internal/mnemonic.d.ts +4 -0
  74. package/dist/core/internal/mnemonic.d.ts.map +1 -0
  75. package/dist/core/internal/mnemonic.js +14 -0
  76. package/dist/core/internal/mnemonic.js.map +1 -0
  77. package/dist/core/internal/p256.d.ts +14 -0
  78. package/dist/core/internal/p256.d.ts.map +1 -0
  79. package/dist/core/internal/p256.js +36 -0
  80. package/dist/core/internal/p256.js.map +1 -0
  81. package/dist/core/internal/secp256k1.d.ts +14 -0
  82. package/dist/core/internal/secp256k1.d.ts.map +1 -0
  83. package/dist/core/internal/secp256k1.js +36 -0
  84. package/dist/core/internal/secp256k1.js.map +1 -0
  85. package/dist/core/internal/x25519.d.ts +8 -0
  86. package/dist/core/internal/x25519.d.ts.map +1 -0
  87. package/dist/core/internal/x25519.js +20 -0
  88. package/dist/core/internal/x25519.js.map +1 -0
  89. package/dist/index.d.ts +35 -0
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.docs.d.ts +2 -0
  92. package/dist/index.docs.d.ts.map +1 -1
  93. package/dist/index.docs.js +2 -0
  94. package/dist/index.docs.js.map +1 -1
  95. package/dist/index.js +35 -0
  96. package/dist/index.js.map +1 -1
  97. package/dist/node/Engine.d.ts +55 -0
  98. package/dist/node/Engine.d.ts.map +1 -0
  99. package/dist/node/Engine.js +51 -0
  100. package/dist/node/Engine.js.map +1 -0
  101. package/dist/node/Hash.d.ts +39 -0
  102. package/dist/node/Hash.d.ts.map +1 -0
  103. package/dist/node/Hash.js +37 -0
  104. package/dist/node/Hash.js.map +1 -0
  105. package/dist/node/index.d.ts +39 -0
  106. package/dist/node/index.d.ts.map +1 -0
  107. package/dist/node/index.js +39 -0
  108. package/dist/node/index.js.map +1 -0
  109. package/dist/tempo/VirtualMaster.d.ts +2 -0
  110. package/dist/tempo/VirtualMaster.d.ts.map +1 -1
  111. package/dist/tempo/VirtualMaster.js +19 -5
  112. package/dist/tempo/VirtualMaster.js.map +1 -1
  113. package/dist/tempo/ZoneId.d.ts +35 -17
  114. package/dist/tempo/ZoneId.d.ts.map +1 -1
  115. package/dist/tempo/ZoneId.js +41 -16
  116. package/dist/tempo/ZoneId.js.map +1 -1
  117. package/dist/tempo/index.d.ts +5 -6
  118. package/dist/tempo/index.d.ts.map +1 -1
  119. package/dist/tempo/index.js +5 -6
  120. package/dist/tempo/index.js.map +1 -1
  121. package/dist/tempo/internal/mine.wasm.d.ts +2 -4
  122. package/dist/tempo/internal/mine.wasm.d.ts.map +1 -1
  123. package/dist/tempo/internal/mine.wasm.js +7 -12
  124. package/dist/tempo/internal/mine.wasm.js.map +1 -1
  125. package/dist/tempo/internal/virtualMasterPool.js +4 -1
  126. package/dist/tempo/internal/virtualMasterPool.js.map +1 -1
  127. package/dist/wasm/Engine.d.ts +62 -0
  128. package/dist/wasm/Engine.d.ts.map +1 -0
  129. package/dist/wasm/Engine.js +57 -0
  130. package/dist/wasm/Engine.js.map +1 -0
  131. package/dist/wasm/Hash.d.ts +49 -0
  132. package/dist/wasm/Hash.d.ts.map +1 -0
  133. package/dist/wasm/Hash.js +70 -0
  134. package/dist/wasm/Hash.js.map +1 -0
  135. package/dist/wasm/index.d.ts +46 -0
  136. package/dist/wasm/index.d.ts.map +1 -0
  137. package/dist/wasm/index.js +46 -0
  138. package/dist/wasm/index.js.map +1 -0
  139. package/dist/wasm/internal/hash.d.ts +9 -0
  140. package/dist/wasm/internal/hash.d.ts.map +1 -0
  141. package/dist/wasm/internal/hash.js +26 -0
  142. package/dist/wasm/internal/hash.js.map +1 -0
  143. package/dist/wasm/internal/hashes.wasm.d.ts +5 -0
  144. package/dist/wasm/internal/hashes.wasm.d.ts.map +1 -0
  145. package/dist/wasm/internal/hashes.wasm.js +12 -0
  146. package/dist/wasm/internal/hashes.wasm.js.map +1 -0
  147. package/dist/wasm/internal/instantiate.d.ts +56 -0
  148. package/dist/wasm/internal/instantiate.d.ts.map +1 -0
  149. package/dist/wasm/internal/instantiate.js +92 -0
  150. package/dist/wasm/internal/instantiate.js.map +1 -0
  151. package/dist/zod/TypedData.d.ts +12 -12
  152. package/package.json +36 -1
  153. package/src/core/Base64.ts +19 -166
  154. package/src/core/BinaryStateTree.ts +2 -3
  155. package/src/core/Bls.ts +27 -77
  156. package/src/core/Ed25519.ts +7 -7
  157. package/src/core/Engine.ts +235 -0
  158. package/src/core/Hash.ts +9 -12
  159. package/src/core/Keystore.ts +17 -17
  160. package/src/core/Mnemonic.ts +3 -6
  161. package/src/core/P256.ts +15 -24
  162. package/src/core/Rlp_tx.bench.ts +2 -4
  163. package/src/core/Secp256k1.ts +9 -14
  164. package/src/core/TxEnvelopeEip1559.bench.ts +0 -5
  165. package/src/core/WebCryptoP256.ts +2 -2
  166. package/src/core/X25519.ts +4 -3
  167. package/src/core/_test/AbiParameters.adversarial.fuzz.ts +1 -2
  168. package/src/core/_test/AbiParameters.fuzz.ts +1 -2
  169. package/src/core/_test/Base.fuzz.ts +1 -2
  170. package/src/core/_test/Cbor.fuzz.ts +1 -2
  171. package/src/core/_test/Engine.fuzz.ts +293 -0
  172. package/src/core/_test/Engine.test-d.ts +84 -0
  173. package/src/core/_test/Engine.test.ts +523 -0
  174. package/src/core/_test/Hash.vectors.test.ts +56 -0
  175. package/src/core/_test/Hex.fuzz.ts +1 -2
  176. package/src/core/_test/Rlp.fuzz.ts +1 -2
  177. package/src/core/_test/Signature.fuzz.ts +1 -2
  178. package/src/core/_test/curves.test.ts +7 -0
  179. package/src/core/_test/index.test.ts +1 -0
  180. package/src/core/internal/bls.ts +121 -0
  181. package/src/core/internal/codec/_test/base64.conformance.ts +156 -0
  182. package/src/core/internal/codec/_test/hex.conformance.ts +204 -0
  183. package/src/core/internal/codec/_test/hex.fuzz.ts +234 -0
  184. package/src/core/internal/codec/_test/hex.test.ts +124 -0
  185. package/src/core/internal/codec/base64.ts +222 -0
  186. package/src/core/internal/codec/hex.ts +115 -34
  187. package/src/core/internal/curves.ts +18 -0
  188. package/src/core/internal/ed25519.ts +64 -0
  189. package/src/core/internal/engine.ts +410 -0
  190. package/src/core/internal/hash.ts +48 -0
  191. package/src/core/internal/keystore.ts +107 -0
  192. package/src/core/internal/mnemonic.ts +18 -0
  193. package/src/core/internal/p256.ts +91 -0
  194. package/src/core/internal/secp256k1.ts +91 -0
  195. package/src/core/internal/x25519.ts +40 -0
  196. package/src/index.docs.ts +2 -0
  197. package/src/index.ts +36 -0
  198. package/src/node/Engine.ts +69 -0
  199. package/src/node/Hash.ts +56 -0
  200. package/src/node/_test/Engine.test.ts +62 -0
  201. package/src/node/_test/Hash.test-d.ts +26 -0
  202. package/src/node/_test/Hash.test.ts +102 -0
  203. package/src/node/index.ts +40 -0
  204. package/src/tempo/VirtualMaster.test.ts +75 -0
  205. package/src/tempo/VirtualMaster.ts +28 -6
  206. package/src/tempo/ZoneId.test-d.ts +16 -0
  207. package/src/tempo/ZoneId.test.ts +48 -13
  208. package/src/tempo/ZoneId.ts +55 -19
  209. package/src/tempo/index.ts +5 -6
  210. package/src/tempo/internal/mine.wasm.ts +7 -13
  211. package/src/tempo/internal/virtualMasterPool.ts +4 -1
  212. package/src/tsconfig.json +2 -1
  213. package/src/version.ts +1 -1
  214. package/src/wasm/Engine.ts +76 -0
  215. package/src/wasm/Hash.bench.ts +60 -0
  216. package/src/wasm/Hash.ts +109 -0
  217. package/src/wasm/_test/Engine.test.ts +62 -0
  218. package/src/wasm/_test/Hash.browser.test.ts +50 -0
  219. package/src/wasm/_test/Hash.fuzz.ts +188 -0
  220. package/src/wasm/_test/Hash.test-d.ts +28 -0
  221. package/src/wasm/_test/Hash.test.ts +211 -0
  222. package/src/wasm/_test/Hash.vectors.test.ts +117 -0
  223. package/src/wasm/_test/Runtime.test.ts +21 -0
  224. package/src/wasm/_test/runtime.wasm.ts +11 -0
  225. package/src/wasm/index.ts +47 -0
  226. package/src/wasm/internal/hash.ts +50 -0
  227. package/src/wasm/internal/hashes.wasm.ts +14 -0
  228. package/src/wasm/internal/instantiate.ts +135 -0
  229. package/src/zod/tempo/_test/ZoneId.test.ts +2 -2
  230. 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
+ }