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,121 @@
1
+ import { bls12_381 as bls } from '@noble/curves/bls12-381.js'
2
+ import { type Bls, type Complete, overrides } from './engine.js'
3
+
4
+ /**
5
+ * Resolvers for the `Bls` slot, and ox's defaults for it, backed by
6
+ * `@noble/curves`.
7
+ *
8
+ * Declaring the defaults against the slot contract is what keeps them honest: a
9
+ * default that goes missing, or whose signature drifts, fails to compile rather
10
+ * than failing at the call site.
11
+ */
12
+
13
+ // Branching on the group rather than selecting a curve into a variable: the
14
+ // two `Point` types are unrelated, so a union of them has no common `add`.
15
+ const aggregateDefault: Complete<Bls>['aggregate'] = (points, group) => {
16
+ if (group === 'G1') {
17
+ let point = bls.G1.Point.ZERO
18
+ for (const value of points) point = point.add(bls.G1.Point.fromBytes(value))
19
+ return point.toBytes()
20
+ }
21
+ let point = bls.G2.Point.ZERO
22
+ for (const value of points) point = point.add(bls.G2.Point.fromBytes(value))
23
+ return point.toBytes()
24
+ }
25
+
26
+ const getPublicKeyDefault: Complete<Bls>['getPublicKey'] = (
27
+ privateKey,
28
+ group,
29
+ ) => {
30
+ const curve = group === 'G1' ? bls.G1 : bls.G2
31
+ return curve.Point.BASE.multiply(
32
+ curve.Point.Fn.fromBytes(privateKey),
33
+ ).toBytes()
34
+ }
35
+
36
+ const randomSecretKeyDefault: Complete<Bls>['randomSecretKey'] = () =>
37
+ bls.utils.randomSecretKey()
38
+
39
+ const signDefault: Complete<Bls>['sign'] = (payload, privateKey, options) => {
40
+ const { dst, group } = options
41
+ const signatureCurve = group === 'G1' ? bls.G1 : bls.G2
42
+ // The scalar comes from the group the public key lives in, which is the one
43
+ // the signature does not.
44
+ const privateKeyCurve = group === 'G1' ? bls.G2 : bls.G1
45
+ const payloadPoint = signatureCurve.hashToCurve(
46
+ payload,
47
+ dst ? { DST: dst } : undefined,
48
+ )
49
+ return payloadPoint
50
+ .multiply(privateKeyCurve.Point.Fn.fromBytes(privateKey))
51
+ .toBytes()
52
+ }
53
+
54
+ const verifyDefault: Complete<Bls>['verify'] = (
55
+ signature,
56
+ payload,
57
+ publicKey,
58
+ options,
59
+ ) => {
60
+ const { dst, signatureGroup } = options
61
+ const shortSignature = signatureGroup === 'G1'
62
+ const curve = shortSignature ? bls.G1 : bls.G2
63
+ const payloadPoint = curve.hashToCurve(
64
+ payload,
65
+ dst ? { DST: dst } : undefined,
66
+ )
67
+ const pairing = shortSignature
68
+ ? bls.pairingBatch([
69
+ {
70
+ g1: payloadPoint as InstanceType<typeof bls.G1.Point>,
71
+ g2: bls.G2.Point.fromBytes(publicKey),
72
+ },
73
+ {
74
+ g1: bls.G1.Point.fromBytes(signature),
75
+ g2: bls.G2.Point.BASE.negate(),
76
+ },
77
+ ])
78
+ : bls.pairingBatch([
79
+ {
80
+ g1: bls.G1.Point.fromBytes(publicKey).negate(),
81
+ g2: payloadPoint as InstanceType<typeof bls.G2.Point>,
82
+ },
83
+ {
84
+ g1: bls.G1.Point.BASE,
85
+ g2: bls.G2.Point.fromBytes(signature),
86
+ },
87
+ ])
88
+ return bls.fields.Fp12.eql(pairing, bls.fields.Fp12.ONE)
89
+ }
90
+
91
+ /** @internal */
92
+ export const aggregate: Complete<Bls>['aggregate'] = (points, group) =>
93
+ (overrides.Bls?.aggregate ?? aggregateDefault)(points, group)
94
+
95
+ /** @internal */
96
+ export const getPublicKey: Complete<Bls>['getPublicKey'] = (
97
+ privateKey,
98
+ group,
99
+ ) => (overrides.Bls?.getPublicKey ?? getPublicKeyDefault)(privateKey, group)
100
+
101
+ /** @internal */
102
+ export const randomSecretKey: Complete<Bls>['randomSecretKey'] = () =>
103
+ (overrides.Bls?.randomSecretKey ?? randomSecretKeyDefault)()
104
+
105
+ /** @internal */
106
+ export const sign: Complete<Bls>['sign'] = (payload, privateKey, options) =>
107
+ (overrides.Bls?.sign ?? signDefault)(payload, privateKey, options)
108
+
109
+ /** @internal */
110
+ export const verify: Complete<Bls>['verify'] = (
111
+ signature,
112
+ payload,
113
+ publicKey,
114
+ options,
115
+ ) =>
116
+ (overrides.Bls?.verify ?? verifyDefault)(
117
+ signature,
118
+ payload,
119
+ publicKey,
120
+ options,
121
+ )
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Base64 codec conformance, written once and run by every runtime.
3
+ *
4
+ * Declares the tiers this runtime can actually execute, pins them to published
5
+ * vectors, and compares them against each other. Coverage widens on its own
6
+ * where more tiers exist -- Node runs the loop, a browser the loop and
7
+ * its native method -- without this file changing or naming a runtime.
8
+ *
9
+ * Collected by both the `core` and `browser` projects, via their
10
+ * `*.conformance.ts` globs.
11
+ *
12
+ * @internal
13
+ */
14
+
15
+ import { describe, expect, test } from 'vp/test'
16
+ import {
17
+ corrupted,
18
+ expectTiersAgree,
19
+ expectTiersMatch,
20
+ expectTiersReject,
21
+ sampleBytes,
22
+ type Tier,
23
+ } from '../../../../../test/conformance.js'
24
+ import * as base64 from '../base64.js'
25
+
26
+ /**
27
+ * Published vectors, transcribed verbatim rather than regenerated, so a change
28
+ * to ox cannot quietly move the expected values.
29
+ *
30
+ * - RFC 4648 section 10 specifies Base64 and gives the `""` through `"foobar"`
31
+ * progression. https://www.rfc-editor.org/rfc/rfc4648#section-10
32
+ * - test262 pins which characters the native codec must reject.
33
+ * https://github.com/tc39/test262/tree/main/test/built-ins/Uint8Array/fromBase64
34
+ */
35
+
36
+ /** Bytes and their Base64, from RFC 4648 section 10. */
37
+ const roundTrip = ['', 'f', 'fo', 'foo', 'foob', 'fooba', 'foobar'].map(
38
+ (value, i) => ({
39
+ bytes: Uint8Array.from(value, (c) => c.charCodeAt(0)),
40
+ encoded: ['', 'Zg==', 'Zm8=', 'Zm9v', 'Zm9vYg==', 'Zm9vYmE=', 'Zm9vYmFy'][
41
+ i
42
+ ]!,
43
+ }),
44
+ )
45
+
46
+ /**
47
+ * Strings a decoder must reject, from test262
48
+ * `fromBase64/illegal-characters.js`.
49
+ *
50
+ * U+2212 and U+FF0B are the interesting ones: they are homoglyphs for `-` and
51
+ * `+`, both of which *are* Base64 alphabet characters, and U+FF0B sits above
52
+ * the Latin-1 table. Before the guard it decoded silently as `A` rather than
53
+ * being refused.
54
+ */
55
+ const illegal = [
56
+ 'Zm.9v',
57
+ 'Zm9v^',
58
+ 'Zg==&',
59
+ 'Z\u2212==', // minus sign, homoglyph for '-'
60
+ 'Z\uff0b==', // fullwidth plus sign, homoglyph for '+'
61
+ 'Zg\u00a0==', // no-break space
62
+ 'Zg\u2009==', // thin space
63
+ 'Zg\u2028==', // line separator
64
+ ]
65
+
66
+ describe('base64', () => {
67
+ const options = [
68
+ { pad: true, url: false },
69
+ { pad: false, url: false },
70
+ { pad: true, url: true },
71
+ { pad: false, url: true },
72
+ ] as const
73
+
74
+ type EncodeInput = { bytes: Uint8Array; options: base64.Options }
75
+ const encodeTiers: Tier<EncodeInput, string>[] = [
76
+ {
77
+ name: 'loop',
78
+ run: ({ bytes, options }) => base64.fromBytesLoop(bytes, options),
79
+ },
80
+ ...(base64.fromBytesNative
81
+ ? [
82
+ {
83
+ name: 'native',
84
+ run: ({ bytes, options }: EncodeInput) =>
85
+ base64.fromBytesNative!(bytes, options),
86
+ },
87
+ ]
88
+ : []),
89
+ ]
90
+
91
+ // `toBytes` strips padding before handing the body to a tier, so the suite
92
+ // has to do the same to compare them.
93
+ const bodyEnd = (value: string) => {
94
+ let end = value.length
95
+ while (end > 0 && value.charCodeAt(end - 1) === 61) end--
96
+ return end
97
+ }
98
+ // Only one decode tier: `Uint8Array.fromBase64` cannot match ox's
99
+ // alphabet-agnostic decoding, so the loop is all there is. The suite still
100
+ // pins it to the published
101
+ const decodeTiers: Tier<string, Uint8Array>[] = [
102
+ { name: 'loop', run: (v) => base64.toBytesLoop(v, bodyEnd(v)) },
103
+ ]
104
+
105
+ test(`behavior: exercises ${encodeTiers.length} encode / ${decodeTiers.length} decode tiers`, () => {
106
+ expect(encodeTiers.length).toBeGreaterThan(0)
107
+ expect(decodeTiers.length).toBe(1)
108
+ })
109
+
110
+ test('vectors: RFC 4648', () => {
111
+ expectTiersMatch(
112
+ encodeTiers,
113
+ roundTrip.map(({ bytes, encoded }) => ({
114
+ input: { bytes, options: { pad: true, url: false } },
115
+ output: encoded,
116
+ })),
117
+ { describe: ({ bytes }) => `${bytes.length} bytes` },
118
+ )
119
+ expectTiersMatch(
120
+ decodeTiers,
121
+ roundTrip.map(({ bytes, encoded }) => ({
122
+ input: encoded,
123
+ output: bytes,
124
+ })),
125
+ )
126
+ })
127
+
128
+ test('vectors: illegal characters are rejected by every tier', () => {
129
+ expectTiersReject(decodeTiers, illegal)
130
+ })
131
+
132
+ test('differential: tiers agree on valid input', () => {
133
+ for (const option of options)
134
+ expectTiersAgree(
135
+ encodeTiers,
136
+ [...sampleBytes(200)].map((bytes) => ({ bytes, options: option })),
137
+ {
138
+ describe: ({ bytes }) =>
139
+ `${bytes.length} bytes, ${JSON.stringify(option)}`,
140
+ },
141
+ )
142
+ expectTiersAgree(
143
+ decodeTiers,
144
+ [...sampleBytes(400)].map((b) => base64.fromBytesLoop(b)),
145
+ { describe: (v) => `${v.slice(0, 20)}… (${v.length})` },
146
+ )
147
+ })
148
+
149
+ test('differential: tiers agree on corrupted input', () => {
150
+ expectTiersAgree(
151
+ decodeTiers,
152
+ [...corrupted((b) => base64.fromBytesLoop(b), 400)],
153
+ { describe: (v) => JSON.stringify(v.slice(0, 24)) },
154
+ )
155
+ })
156
+ })
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Hex codec conformance, written once and run by every runtime.
3
+ *
4
+ * Declares the tiers this runtime can actually execute, pins them to published
5
+ * vectors, and compares them against each other. Coverage widens on its own
6
+ * where more tiers exist -- Node runs the loop and `Buffer`, a browser the loop and
7
+ * its native method -- without this file changing or naming a runtime.
8
+ *
9
+ * Collected by both the `core` and `browser` projects, via their
10
+ * `*.conformance.ts` globs.
11
+ *
12
+ * @internal
13
+ */
14
+
15
+ import { describe, expect, test } from 'vp/test'
16
+ import {
17
+ corrupted,
18
+ expectTiersAgree,
19
+ expectTiersMatch,
20
+ expectTiersReject,
21
+ sampleBytes,
22
+ type Tier,
23
+ } from '../../../../../test/conformance.js'
24
+ import * as hex from '../hex.js'
25
+
26
+ /**
27
+ * Published vectors, transcribed verbatim rather than regenerated, so a change
28
+ * to ox cannot quietly move the expected values.
29
+ *
30
+ * - RFC 4648 section 10 specifies Base16 and gives the `""` through `"foobar"`
31
+ * progression. Section 8 defines it as case-insensitive on decode; ox emits
32
+ * lowercase. https://www.rfc-editor.org/rfc/rfc4648#section-10
33
+ * - test262 pins the native codec ox delegates to, including which characters
34
+ * it must reject.
35
+ * https://github.com/tc39/test262/tree/main/test/built-ins/Uint8Array/fromHex
36
+ */
37
+
38
+ /** The RFC 4648 section 10 inputs, as bytes. */
39
+ const foobar = ['', 'f', 'fo', 'foo', 'foob', 'fooba', 'foobar'].map((value) =>
40
+ Uint8Array.from(value, (c) => c.charCodeAt(0)),
41
+ )
42
+
43
+ /**
44
+ * Bytes and their lowercase hex, from RFC 4648 section 10 and test262
45
+ * `fromHex/results.js` / `toHex/results.js`. The two agree except on case: the
46
+ * RFC writes uppercase, the ECMAScript methods emit lowercase.
47
+ */
48
+ const roundTrip = foobar.map((bytes, i) => ({
49
+ bytes,
50
+ encoded: [
51
+ '',
52
+ '66',
53
+ '666f',
54
+ '666f6f',
55
+ '666f6f62',
56
+ '666f6f6261',
57
+ '666f6f626172',
58
+ ][i]!,
59
+ }))
60
+
61
+ /**
62
+ * Uppercase and mixed-case bodies that must decode to the same bytes as their
63
+ * lowercase form, from test262 `fromHex/results.js` and the uppercase spellings
64
+ * RFC 4648 section 10 uses.
65
+ */
66
+ const decodeCaseInsensitive = [
67
+ { bytes: Uint8Array.of(102, 111), encoded: '666F' },
68
+ { bytes: Uint8Array.of(102, 111, 111), encoded: '666F6f' },
69
+ { bytes: foobar[6]!, encoded: '666F6F626172' },
70
+ ]
71
+
72
+ /**
73
+ * Characters a decoder must reject, from test262
74
+ * `fromHex/illegal-characters.js`: punctuation, every flavor of ASCII
75
+ * whitespace, and three non-ASCII spaces.
76
+ *
77
+ * The last two sit above the Latin-1 lookup table ox decodes through, so a
78
+ * table read for them yields `undefined` rather than the invalid sentinel --
79
+ * the exact shape of the bug this suite exists to catch.
80
+ */
81
+ const illegalCharacters = [
82
+ '.',
83
+ '^',
84
+ ' ',
85
+ '\t',
86
+ '\u000a', // line feed
87
+ '\u000c', // form feed
88
+ '\u000d', // carriage return
89
+ '\u00a0', // no-break space -- inside the Latin-1 table
90
+ '\u2009', // thin space -- above it
91
+ '\u2028', // line separator -- above it
92
+ ]
93
+
94
+ /**
95
+ * Bodies that Node's `Buffer` hex decoder accepted, wrongly, before ox guarded
96
+ * against it: it masks each UTF-16 code unit to 8 bits, so a character above
97
+ * U+00FF can alias a hex digit. U+C230 masks to `0x30`, the digit `0`, and the
98
+ * surrogate pair for U+1D330 masks to `40`.
99
+ */
100
+ const maskedNonAscii = [
101
+ { body: '\uc2300', note: 'U+C230 masks to the digit 0' },
102
+ { body: '\u{1d330}', note: 'surrogate pair masks to 4 and 0' },
103
+ ]
104
+
105
+ describe('hex', () => {
106
+ const encodeTiers: Tier<Uint8Array, string>[] = [
107
+ { name: 'loop', run: hex.bytesToHexLoop },
108
+ ...(hex.bytesToHexNative
109
+ ? [{ name: 'native', run: hex.bytesToHexNative }]
110
+ : []),
111
+ ...(hex.bytesToHexBuffer
112
+ ? [{ name: 'buffer', run: hex.bytesToHexBuffer }]
113
+ : []),
114
+ ]
115
+
116
+ const byteLength = (value: string) => (value.length - 2) >> 1
117
+ // No native decode tier: `Uint8Array.fromHex` cannot be trusted to reject
118
+ // non-ASCII across the browser versions ox supports.
119
+ const decodeTiers: Tier<string, Uint8Array>[] = [
120
+ { name: 'loop', run: (v) => hex.hexToBytesLoop(v, byteLength(v)) },
121
+ ...(hex.hexToBytesBuffer
122
+ ? [
123
+ {
124
+ name: 'buffer',
125
+ run: (v: string) => hex.hexToBytesBuffer!(v, byteLength(v)),
126
+ },
127
+ ]
128
+ : []),
129
+ ]
130
+
131
+ test(`behavior: exercises ${encodeTiers.length} encode / ${decodeTiers.length} decode tiers`, () => {
132
+ // Named so the report says which tiers a runtime actually covered, and so
133
+ // a tier silently disappearing is visible rather than quietly shrinking
134
+ // coverage. Encoding has one more tier than decoding: the native method is
135
+ // trusted to encode but not to validate.
136
+ expect(encodeTiers.length).toBeGreaterThan(0)
137
+ expect(decodeTiers.length).toBeGreaterThan(0)
138
+ })
139
+
140
+ test('vectors: RFC 4648 and test262', () => {
141
+ expectTiersMatch(
142
+ encodeTiers,
143
+ roundTrip.map(({ bytes, encoded }) => ({
144
+ input: bytes,
145
+ output: `0x${encoded}`,
146
+ })),
147
+ )
148
+ expectTiersMatch(
149
+ decodeTiers,
150
+ roundTrip.map(({ bytes, encoded }) => ({
151
+ input: `0x${encoded}`,
152
+ output: bytes,
153
+ })),
154
+ )
155
+ expectTiersMatch(
156
+ decodeTiers,
157
+ decodeCaseInsensitive.map(({ bytes, encoded }) => ({
158
+ input: `0x${encoded}`,
159
+ output: bytes,
160
+ })),
161
+ )
162
+ })
163
+
164
+ test('vectors: illegal characters are rejected by every tier', () => {
165
+ const pad = 'ab'.repeat(96)
166
+ expectTiersReject(
167
+ decodeTiers,
168
+ illegalCharacters.flatMap((char) => [
169
+ `0xaa${char}a`,
170
+ `0x${pad}aa${char}a`,
171
+ ]),
172
+ )
173
+ })
174
+
175
+ test('vectors: characters Buffer would mask into a digit are rejected', () => {
176
+ const pad = 'ab'.repeat(96)
177
+ expectTiersReject(
178
+ decodeTiers,
179
+ maskedNonAscii.flatMap(({ body }) => [`0x${body}`, `0x${pad}${body}`]),
180
+ )
181
+ })
182
+
183
+ test('differential: tiers agree on valid input', () => {
184
+ expectTiersAgree(encodeTiers, sampleBytes(400), {
185
+ describe: (b) => `${b.length} bytes`,
186
+ })
187
+ expectTiersAgree(
188
+ decodeTiers,
189
+ [...sampleBytes(400)].map((b) => hex.bytesToHexLoop(b)),
190
+ { describe: (v) => `${v.slice(0, 20)}… (${v.length})` },
191
+ )
192
+ })
193
+
194
+ test('differential: tiers agree on corrupted input', () => {
195
+ expectTiersAgree(
196
+ decodeTiers,
197
+ [...corrupted((b) => hex.bytesToHexLoop(b), 400)].filter(
198
+ // Odd bodies never reach a tier; the dispatcher rejects them first.
199
+ (v) => (v.length - 2) % 2 === 0,
200
+ ),
201
+ { describe: (v) => JSON.stringify(v.slice(0, 24)) },
202
+ )
203
+ })
204
+ })