castrum 0.9.0 → 0.9.1
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/README.md +153 -630
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1374 -1772
- package/dist/index.js +5570 -4169
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +106 -14
- package/docs/BENCHMARKS.md +83 -47
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +8 -6
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +15 -32
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +5 -4
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +46 -26
- package/docs/adr/0003-bun-builtins-delegation.md +6 -5
- package/docs/bun-builtins-decision-matrix.md +25 -18
- package/index.ts +9 -16
- package/package.json +36 -24
- package/src/ingress/body.ts +167 -27
- package/src/ingress/handlers.ts +91 -15
- package/src/ingress/health.ts +1 -0
- package/src/ingress/index.ts +39 -27
- package/src/ingress/packing/gather-raw-headers.ts +85 -21
- package/src/ingress/packing/header-packing.ts +5 -5
- package/src/ingress/packing/input-packer.ts +17 -3
- package/src/ingress/packing/select-headers.ts +12 -6
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +42 -1
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +3 -13
- package/src/ingress/routes/index.ts +5 -4
- package/src/ingress/routes/json-write.ts +7 -31
- package/src/ingress/routes/options.ts +2 -12
- package/src/ingress/routes/read.ts +3 -13
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +6 -6
- package/src/ingress/server.ts +45 -13
- package/src/ingress/types.ts +35 -1
- package/src/integration/pipeline.ts +3 -0
- package/src/integration/streaming.ts +2 -3
- package/src/integration/websocket.ts +4 -3
- package/src/loader/batch.ts +4 -0
- package/src/loader/cost.ts +1 -0
- package/src/loader/index.ts +43 -228
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +729 -0
- package/src/native/ffi/types.ts +474 -0
- package/src/native/ffi.ts +1063 -829
- package/src/native/loader.ts +52 -6
- package/src/native/types/instances.ts +274 -0
- package/src/native/types.ts +41 -238
- package/src/runtime/builtins.ts +131 -0
- package/src/runtime/codec.ts +147 -0
- package/src/runtime/detect.ts +36 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/native.ts +35 -0
- package/src/runtime/server.ts +31 -0
- package/src/runtime/transport.ts +64 -0
- package/src/runtime/types.ts +173 -0
- package/src/runtime/uuid.ts +45 -0
- package/src/runtime/websocket.ts +30 -0
- package/src/rust-ffi/addon.ts +1 -1
- package/src/rust-ffi/batch/build.ts +155 -0
- package/src/rust-ffi/batch/index.ts +8 -0
- package/src/rust-ffi/batch/types.ts +92 -0
- package/src/rust-ffi/client.ts +5 -6
- package/src/rust-ffi/context.ts +39 -10
- package/src/rust-ffi/index.ts +28 -17
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +1 -1
- package/src/rust-ffi/packed.ts +2 -2
- package/src/rust-ffi/proven.ts +16 -30
- package/src/rust-ffi/scalar/crypto.ts +188 -107
- package/src/rust-ffi/scalar/factories.ts +253 -27
- package/src/rust-ffi/scalar/hashing.ts +43 -41
- package/src/rust-ffi/scalar/http.ts +208 -77
- package/src/rust-ffi/scalar/index.ts +3 -3
- package/src/rust-ffi/scalar/interface.ts +88 -213
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +28 -12
- package/src/rust-ffi/scalar/payload.ts +159 -42
- package/src/rust-ffi/text.ts +31 -5
- package/src/selection.ts +27 -31
- package/src/shared/adaptive.ts +1 -2
- package/src/shared/buffer-pool.ts +72 -17
- package/src/shared/bytes.ts +75 -6
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +4 -4
- package/src/shared/log.ts +1 -0
- package/src/shared/packed/index.ts +10 -0
- package/src/shared/packed/parsers.ts +31 -0
- package/src/shared/packed/schema.ts +23 -0
- package/src/shared/{packed.ts → packed/wire.ts} +31 -39
- package/src/shared/proven.ts +189 -496
- package/src/shared/runtime.ts +20 -25
- package/src/shared/uuid.ts +12 -28
- package/src/data/json-rows.ts +0 -33
- package/src/rust-ffi/batch.ts +0 -431
- package/src/shared/bench-classify.ts +0 -209
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
// src/native/ffi/selftest.ts — bind-time self-test for the bun:ffi transport.
|
|
2
|
+
//
|
|
3
|
+
// Every bound function is checked against known-good vectors at bind time. If
|
|
4
|
+
// ANY check fails (ABI mismatch, platform quirk, a future Bun regression), the
|
|
5
|
+
// whole ffi layer is disabled and the caller falls back to the napi addon —
|
|
6
|
+
// the public API never sees a wrong result. `bun:ffi` is experimental; this is
|
|
7
|
+
// the safety net. New C-ABI symbols MUST be added to `selfTest` too.
|
|
8
|
+
|
|
9
|
+
import { decodeJsonPacked } from '../../rust-ffi/scalar/json-packed'
|
|
10
|
+
import { decodeUtf8, encodeUtf8 } from '../../shared/codec'
|
|
11
|
+
import { SELFTEST_HEX, SELFTEST_JSON } from './constants'
|
|
12
|
+
import type { BunFFI } from './types'
|
|
13
|
+
|
|
14
|
+
/** Verify every bound function against known-good results; false disables ffi. */
|
|
15
|
+
export function selfTest(b: BunFFI): boolean {
|
|
16
|
+
// Codec-backed codecs — on Bun these use native transfer (Bun.ArrayBufferSink
|
|
17
|
+
// / bun:ffi CString) so no TextEncoder/TextDecoder runs on the Bun path.
|
|
18
|
+
const enc = { encode: encodeUtf8 }
|
|
19
|
+
const dec = { decode: decodeUtf8 }
|
|
20
|
+
|
|
21
|
+
if (b.crc32(enc.encode('123456789')) !== 0xcbf4_3926) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
if (b.fnv1a64(enc.encode('foobar')) !== 0x8594_4171_f739_67e8n) {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
// XXH3-64 of empty input = 0x2d06800538d394c2 (standard reference vector).
|
|
28
|
+
if (b.xxh3(new Uint8Array(0)) !== 0x2d06800538d394c2n) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
if (b.jsonValid(SELFTEST_JSON) !== true || b.jsonValid(enc.encode('{not json')) !== false) {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
// UTF-8 validity probe (native replacement for a fatal TextDecoder on the
|
|
35
|
+
// Bun path — used by the urlDecode wrapper).
|
|
36
|
+
if (!b.utf8Valid(enc.encode('héllo')) || b.utf8Valid(new Uint8Array([0xff, 0xfe]))) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
const hexOut = new Uint8Array(SELFTEST_HEX.length * 2)
|
|
40
|
+
if (b.hexEncodeInto(SELFTEST_HEX, hexOut) !== 10 || dec.decode(hexOut) !== '68656c6c6f') {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
const urlInput = enc.encode('a b/c')
|
|
44
|
+
const urlOut = new Uint8Array(9)
|
|
45
|
+
if (b.urlEncodeInto(urlInput, urlOut) !== 9 || dec.decode(urlOut) !== 'a%20b%2Fc') {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Ingress layout blob (38 × u32 LE). The pinned values catch a reordered
|
|
50
|
+
// `#[repr(C)] IngressLayout` (drift → self-test fails → napi fallback); the
|
|
51
|
+
// Rust unit test `ingress_layout_c_abi_matches_output_source` pins every
|
|
52
|
+
// field against output.rs. Slot order mirrors the struct field order.
|
|
53
|
+
const layoutBuf = new Uint8Array(38 * 4)
|
|
54
|
+
b.ingressLayout(layoutBuf)
|
|
55
|
+
const layoutView = new DataView(layoutBuf.buffer, layoutBuf.byteOffset, layoutBuf.byteLength)
|
|
56
|
+
if (
|
|
57
|
+
layoutView.getUint32(0, true) !== 0 || // OUT_VERDICT
|
|
58
|
+
layoutView.getUint32(2 * 4, true) !== 2 || // OUT_STATUS
|
|
59
|
+
layoutView.getUint32(12 * 4, true) !== 48 || // OUT_DATA_START
|
|
60
|
+
layoutView.getUint32(13 * 4, true) !== 1 || // FLAG_HAS_COOKIES
|
|
61
|
+
layoutView.getUint32(28 * 4, true) !== 32 || // HV_COUNT
|
|
62
|
+
layoutView.getUint32(37 * 4, true) !== 8 // ERR_INTERNAL
|
|
63
|
+
) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Ingress pipeline C-ABI: with a null (0) inner handle the Rust side returns
|
|
68
|
+
// 0 immediately and the wrapper throws. This exercises the symbol's ABI (arg
|
|
69
|
+
// count/types/return) at bind time — a signature drift would surface here
|
|
70
|
+
// instead of crashing under load. Real frame→output parity is covered by
|
|
71
|
+
// ffi.test.ts against a live napi instance.
|
|
72
|
+
try {
|
|
73
|
+
b.ingressHandlePacked(0, enc.encode('/'), null, new Uint8Array(64))
|
|
74
|
+
return false // a null handle must throw, not return
|
|
75
|
+
} catch {
|
|
76
|
+
// expected: null inner handle → 0 → throw
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Ingress raw-components C-ABI: with a null (0) inner handle the Rust side
|
|
80
|
+
// returns 0 immediately and the wrapper throws — exercises the 12-arg ABI
|
|
81
|
+
// (incl. the two `cstring` slots for url/ip) at bind time. Real frame→output
|
|
82
|
+
// parity is covered by ffi.test.ts against a live napi instance.
|
|
83
|
+
try {
|
|
84
|
+
b.ingressHandleComponents(
|
|
85
|
+
0,
|
|
86
|
+
0, // GET
|
|
87
|
+
'/',
|
|
88
|
+
'',
|
|
89
|
+
enc.encode('rid'),
|
|
90
|
+
new Uint8Array(2), // empty packed headers [u16 0]
|
|
91
|
+
null,
|
|
92
|
+
new Uint8Array(64),
|
|
93
|
+
)
|
|
94
|
+
return false // a null handle must throw, not return
|
|
95
|
+
} catch {
|
|
96
|
+
// expected: null inner handle → 0 → throw
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Per-route native stack (castrum_route_*): compile a parseQuery-only
|
|
100
|
+
// descriptor, run one frame, assert the packed verdict. Exercises all three
|
|
101
|
+
// symbols' ABI at bind time (handle return, run needed-size, destroy).
|
|
102
|
+
const routeDesc = new Uint8Array(33)
|
|
103
|
+
const rd = new DataView(routeDesc.buffer)
|
|
104
|
+
rd.setUint32(0, 0x524f5554, true) // ROUTE_DESC_MAGIC "ROUT"
|
|
105
|
+
rd.setUint32(4, 3, true) // ROUTE_DESC_VERSION
|
|
106
|
+
rd.setUint32(8, 2 * 1024 * 1024, true) // maxBodyBytes
|
|
107
|
+
rd.setUint32(12, 8192, true) // maxQueryBytes
|
|
108
|
+
rd.setUint32(16, 8192, true) // maxCookieBytes
|
|
109
|
+
rd.setUint32(20, 0, true) // maxPairs
|
|
110
|
+
rd.setUint32(24, 1, true) // stageCount
|
|
111
|
+
routeDesc[28] = 0 // parseQuery
|
|
112
|
+
rd.setUint32(29, 0, true) // schemaCount
|
|
113
|
+
const routeHandle = b.routeCompile(routeDesc)
|
|
114
|
+
if (routeHandle === 0) {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
const routeFrame = new Uint8Array(15)
|
|
118
|
+
const rf = new DataView(routeFrame.buffer)
|
|
119
|
+
rf.setUint32(0, 0, true) // flags (no body)
|
|
120
|
+
rf.setUint32(4, 3, true) // qLen
|
|
121
|
+
routeFrame.set(enc.encode('a=1'), 8)
|
|
122
|
+
rf.setUint32(11, 0, true) // cLen
|
|
123
|
+
const routeOut = new Uint8Array(64)
|
|
124
|
+
const routeW = b.routeRun(routeHandle, routeFrame, routeOut)
|
|
125
|
+
if (routeW <= 8) {
|
|
126
|
+
return false // header + a query pair section must exceed 8 bytes
|
|
127
|
+
}
|
|
128
|
+
const rv = new DataView(routeOut.buffer)
|
|
129
|
+
const rFlags = rv.getUint32(0, true)
|
|
130
|
+
if ((rFlags & 0b1) === 0 || (rFlags & 0b100) === 0) {
|
|
131
|
+
return false // OK + QUERY_VALID bits must be set
|
|
132
|
+
}
|
|
133
|
+
b.routeDestroy(routeHandle)
|
|
134
|
+
|
|
135
|
+
// ── New bindings ───────────────────────────────────────────────────
|
|
136
|
+
// Validators take `cstring` ARGs (the engine transcodes the JS string).
|
|
137
|
+
if (
|
|
138
|
+
!b.validateEmail('a@b.com') ||
|
|
139
|
+
!b.validateUuid('550e8400-e29b-41d4-a716-446655440000') ||
|
|
140
|
+
!b.validateIpv4('192.168.0.1') ||
|
|
141
|
+
!b.validateIpv6('2001:db8::1') ||
|
|
142
|
+
b.validateEmail('not-an-email') ||
|
|
143
|
+
b.validateUuid('not-a-uuid')
|
|
144
|
+
) {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
if (b.jsonSumIds(enc.encode(`[{"id":1},{"id":2}]`)) !== 3n) {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
// The packed [u8 ok][i64 sum LE] ABI: a legit zero-sum is ok, invalid input throws.
|
|
151
|
+
if (b.jsonSumIds(enc.encode(`[{"id":0},{"id":0}]`)) !== 0n) {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
let sumInvalidThrew = false
|
|
155
|
+
try {
|
|
156
|
+
b.jsonSumIds(enc.encode('nope'))
|
|
157
|
+
} catch {
|
|
158
|
+
sumInvalidThrew = true
|
|
159
|
+
}
|
|
160
|
+
if (!sumInvalidThrew) {
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// HMAC RFC 4231 test case 1 (0x0b × 20 key, "Hi There" data).
|
|
165
|
+
const hmacKey = new Uint8Array(20).fill(0x0b)
|
|
166
|
+
const hmacData = enc.encode('Hi There')
|
|
167
|
+
const hmacSig = enc.encode('b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7')
|
|
168
|
+
const hmacHex = b.hmacSha256(hmacKey, hmacData)
|
|
169
|
+
if (hmacHex !== 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7') {
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
if (!b.hmacSha256Verify(hmacKey, hmacData, hmacSig)) {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Decoders round-trip.
|
|
177
|
+
const decoded = b.hexDecode(enc.encode('68656c6c6f'))
|
|
178
|
+
if (
|
|
179
|
+
dec.decode(decoded) !== 'hello' ||
|
|
180
|
+
dec.decode(b.urlDecode(enc.encode('a%20b%2Fc'))) !== 'a b/c'
|
|
181
|
+
) {
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// WebSocket accept key (RFC 6455 sample) — `key` is a `cstring` ARG.
|
|
186
|
+
if (b.wsAcceptKey('dGhlIHNhbXBsZSBub25jZQ==') !== 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=') {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ETag: strong = 10 bytes, weak = 12 bytes.
|
|
191
|
+
if (b.etag(SELFTEST_HEX).length !== 10 || b.etag(SELFTEST_HEX, true).length !== 12) {
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
// ConditionalRequest opaque-handle: a null (0) handle must return false (the
|
|
195
|
+
// C side never dereferences freed state) — exercises the symbol's ABI. Real
|
|
196
|
+
// verdict parity is covered by test/unit/features/etag.test.ts.
|
|
197
|
+
if (b.conditionalIsNotModified(0, null, null) !== false) {
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
// Phase 6 opaque-handle instances: null (0) handles must fail SAFELY (false /
|
|
201
|
+
// null / throw — never dereference freed state). Real verdict parity is
|
|
202
|
+
// covered by the per-instance JS tests.
|
|
203
|
+
if (b.mediaTypeMatcherMatches(0, enc.encode('x')) !== false) return false
|
|
204
|
+
if (b.acceptNegotiatorNegotiate(0, enc.encode('gzip')) !== null) return false
|
|
205
|
+
if (b.schemaValidatorValidate(0, enc.encode('{}')) !== false) return false
|
|
206
|
+
if (b.jwtSignerVerify(0, enc.encode('a.b.c'), 0) !== null) return false
|
|
207
|
+
let phase6Threw = false
|
|
208
|
+
try {
|
|
209
|
+
b.jwtSignerSign(0, enc.encode('{}'), 0)
|
|
210
|
+
} catch {
|
|
211
|
+
phase6Threw = true
|
|
212
|
+
}
|
|
213
|
+
if (!phase6Threw) return false
|
|
214
|
+
phase6Threw = false
|
|
215
|
+
try {
|
|
216
|
+
b.templateRender(0, enc.encode('{}'))
|
|
217
|
+
} catch {
|
|
218
|
+
phase6Threw = true
|
|
219
|
+
}
|
|
220
|
+
if (!phase6Threw) return false
|
|
221
|
+
phase6Threw = false
|
|
222
|
+
try {
|
|
223
|
+
b.rateLimiterCheck(0, 'k', 0)
|
|
224
|
+
} catch {
|
|
225
|
+
phase6Threw = true
|
|
226
|
+
}
|
|
227
|
+
if (!phase6Threw) return false
|
|
228
|
+
|
|
229
|
+
// base64.
|
|
230
|
+
if (b.base64Encode(SELFTEST_HEX) !== 'aGVsbG8=') {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
if (dec.decode(b.base64Decode(enc.encode('aGVsbG8='))) !== 'hello') {
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Signed cookie round-trip.
|
|
238
|
+
const secret = enc.encode('s3cr3t-secret')
|
|
239
|
+
const signed = b.signCookie(SELFTEST_HEX, secret)
|
|
240
|
+
const verified = b.verifyCookie(enc.encode(signed), secret)
|
|
241
|
+
if (verified === null || verified !== 'hello') {
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
if (b.verifyCookie(enc.encode('tampered.0000'), secret) !== null) {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// CSRF token round-trip (issued token verifies against the same secret).
|
|
249
|
+
const csrfTokenStr = b.csrfToken(secret)
|
|
250
|
+
if (csrfTokenStr.length !== 129 || !b.csrfVerify(enc.encode(csrfTokenStr), secret)) {
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Argon2id round-trip at minimum cost (fast) — full defaults would take ~50ms.
|
|
255
|
+
const pw = enc.encode('correct horse battery staple')
|
|
256
|
+
const salt = enc.encode('salty-salt-16b')
|
|
257
|
+
const phc = b.passwordHash(pw, salt, 8, 1, 1, 16)
|
|
258
|
+
if (phc.length === 0 || !b.passwordVerify(pw, enc.encode(phc))) {
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// bcrypt round-trip at minimum cost (fast) — `phc` is a `cstring` ARG.
|
|
263
|
+
const bcryptPhc = b.passwordHashBcrypt(pw, 4)
|
|
264
|
+
if (bcryptPhc.length === 0 || !b.passwordVerifyBcrypt(pw, bcryptPhc)) {
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// PBKDF2-HMAC-SHA256: password="password", salt="salt", c=1, dkLen=32.
|
|
269
|
+
// The C ABI writes the RAW derived key; hex-encode before comparing.
|
|
270
|
+
const dk = b.pbkdf2Sha256(enc.encode('password'), enc.encode('salt'), 1, 32)
|
|
271
|
+
if (b.hexEncode(dk) !== '120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b') {
|
|
272
|
+
return false
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// AEAD AES-256-GCM round-trip (key 32B, nonce 12B).
|
|
276
|
+
const aeadKey = new Uint8Array(32).fill(0x42)
|
|
277
|
+
const nonce = new Uint8Array(12).fill(0x07)
|
|
278
|
+
const ct = b.aeadEncrypt(aeadKey, nonce, SELFTEST_HEX, 0)
|
|
279
|
+
const pt = b.aeadDecrypt(aeadKey, nonce, ct, 0)
|
|
280
|
+
if (pt === null || dec.decode(pt) !== 'hello') {
|
|
281
|
+
return false
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// WebSocket frame: text frame, FIN, no mask → first byte 0x81.
|
|
285
|
+
const frame = b.wsFrameEncode(1, SELFTEST_HEX, false, true)
|
|
286
|
+
if (frame.length === 0 || frame[0] !== 0x81) {
|
|
287
|
+
return false
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// JSON patch: add a key.
|
|
291
|
+
const patched = b.jsonPatch(
|
|
292
|
+
enc.encode(`{"a":"b"}`),
|
|
293
|
+
enc.encode(`[{"op":"add","path":"/c","value":"d"}]`),
|
|
294
|
+
)
|
|
295
|
+
if (!patched.includes(`"c":"d"`)) {
|
|
296
|
+
return false
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// gzip / brotli round-trips.
|
|
300
|
+
const gz = b.gzipCompress(SELFTEST_HEX)
|
|
301
|
+
if (dec.decode(b.gzipDecompress(gz)) !== 'hello') {
|
|
302
|
+
return false
|
|
303
|
+
}
|
|
304
|
+
const br = b.brotliCompress(SELFTEST_HEX)
|
|
305
|
+
if (dec.decode(b.brotliDecompress(br)) !== 'hello') {
|
|
306
|
+
return false
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Needed-size convention: invalid compressed input throws IMMEDIATELY (the C
|
|
310
|
+
// ABI returns 0 = real error, so the JS wrapper does NOT grow-retry re-runs
|
|
311
|
+
// or allocate up to the 64 MiB decompression cap per bad input).
|
|
312
|
+
let decompressThrew = false
|
|
313
|
+
try {
|
|
314
|
+
b.gzipDecompress(enc.encode('not-a-gzip-stream'))
|
|
315
|
+
} catch {
|
|
316
|
+
decompressThrew = true
|
|
317
|
+
}
|
|
318
|
+
if (!decompressThrew) {
|
|
319
|
+
return false
|
|
320
|
+
}
|
|
321
|
+
decompressThrew = false
|
|
322
|
+
try {
|
|
323
|
+
b.brotliDecompress(enc.encode('not-brotli-stream'))
|
|
324
|
+
} catch {
|
|
325
|
+
decompressThrew = true
|
|
326
|
+
}
|
|
327
|
+
if (!decompressThrew) {
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Packed parsers (non-empty output). Packed output is LARGER than input (each
|
|
332
|
+
// component gets a u32 length prefix), so size with the Rust allocator's
|
|
333
|
+
// conservative upper bound (`input.len() * 9 + 16` in query_parser.rs).
|
|
334
|
+
const req = enc.encode('GET /a?b=1 HTTP/1.1\r\nHost: example.com\r\n\r\n')
|
|
335
|
+
const reqOut = new Uint8Array(req.length * 9 + 16)
|
|
336
|
+
if (b.httpParseRequestPackedInto(req, reqOut) === 0) {
|
|
337
|
+
return false
|
|
338
|
+
}
|
|
339
|
+
const qIn = enc.encode('a=1&b=2')
|
|
340
|
+
const qOut = new Uint8Array(qIn.length * 9 + 16)
|
|
341
|
+
if (b.queryParsePackedInto(qIn, qOut) === 0) {
|
|
342
|
+
return false
|
|
343
|
+
}
|
|
344
|
+
const cIn = enc.encode('a=1; b=2')
|
|
345
|
+
const cOut = new Uint8Array(cIn.length * 9 + 16)
|
|
346
|
+
if (b.cookieParsePackedInto(cIn, cOut) === 0) {
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Excluded-surface additions ────────────────────────────────────
|
|
351
|
+
// form parse shares the query core → 2 pairs.
|
|
352
|
+
const fIn = enc.encode('a=1&b=2')
|
|
353
|
+
const fOut = new Uint8Array(fIn.length * 9 + 16)
|
|
354
|
+
if (b.formParsePackedInto(fIn, fOut) === 0 || fOut[0] !== 2) {
|
|
355
|
+
return false
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// multipart parse → 1 part named "field".
|
|
359
|
+
const boundary = enc.encode('----boundary')
|
|
360
|
+
// Wire format is `--{boundary}` — boundary is `----boundary`, so the body
|
|
361
|
+
// must open with `------boundary`.
|
|
362
|
+
const mBody = enc.encode(
|
|
363
|
+
'------boundary\r\nContent-Disposition: form-data; name="field"\r\n\r\nvalue\r\n------boundary--',
|
|
364
|
+
)
|
|
365
|
+
const mOut = b.multipartParsePacked(mBody, boundary)
|
|
366
|
+
if (mOut[0] !== 1) {
|
|
367
|
+
return false
|
|
368
|
+
}
|
|
369
|
+
// Packed layout: [u32 count][u32 name_len][name]... → name_len at offset 4.
|
|
370
|
+
const mNameLen =
|
|
371
|
+
(mOut[4] ?? 0) | ((mOut[5] ?? 0) << 8) | ((mOut[6] ?? 0) << 16) | ((mOut[7] ?? 0) << 24)
|
|
372
|
+
if (dec.decode(mOut.subarray(8, 8 + mNameLen)) !== 'field') {
|
|
373
|
+
return false
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// WS frame decode: encode("hello") → decode → fin=1, opcode=1, payload="hello".
|
|
377
|
+
const wf = b.wsFrameEncode(1, SELFTEST_HEX, true, true)
|
|
378
|
+
const wd = b.wsFrameDecodePacked(wf)
|
|
379
|
+
if (wd === null || wd[0] !== 1 || wd[1] !== 1 || dec.decode(wd.subarray(6)) !== 'hello') {
|
|
380
|
+
return false
|
|
381
|
+
}
|
|
382
|
+
if (b.wsFrameDecodePacked(enc.encode('\x80')) !== null) {
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
// Pooled sibling: same decode into a caller buffer; malformed → null.
|
|
386
|
+
const wdInto = new Uint8Array(wf.length + 6)
|
|
387
|
+
const wdW = b.wsFrameDecodePackedInto(wf, wdInto)
|
|
388
|
+
if (
|
|
389
|
+
wdW === null ||
|
|
390
|
+
wdInto[0] !== 1 ||
|
|
391
|
+
wdInto[1] !== 1 ||
|
|
392
|
+
dec.decode(wdInto.subarray(6, wdW)) !== 'hello'
|
|
393
|
+
) {
|
|
394
|
+
return false
|
|
395
|
+
}
|
|
396
|
+
if (b.wsFrameDecodePackedInto(enc.encode('\x80'), wdInto) !== null) {
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Random token pooled Into: writes byteLen*2 hex chars into the buffer.
|
|
401
|
+
const rtOut = new Uint8Array(32)
|
|
402
|
+
const rtW = b.randomTokenInto(16, rtOut)
|
|
403
|
+
if (rtW !== 32 || !/^[0-9a-f]{32}$/.test(dec.decode(rtOut))) {
|
|
404
|
+
return false
|
|
405
|
+
}
|
|
406
|
+
// Too-small buffer → throw (needed-size convention).
|
|
407
|
+
try {
|
|
408
|
+
b.randomTokenInto(16, new Uint8Array(8))
|
|
409
|
+
return false
|
|
410
|
+
} catch {
|
|
411
|
+
// expected
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Fixed-size writers pooled Into: ws_accept_key + etag + csrf_token match the
|
|
415
|
+
// cstring path byte-for-byte.
|
|
416
|
+
const waOut = new Uint8Array(28)
|
|
417
|
+
const waW = b.wsAcceptKeyInto(enc.encode('dGhlIHNhbXBsZSBub25jZQ=='), waOut)
|
|
418
|
+
const waExpected = b.wsAcceptKey('dGhlIHNhbXBsZSBub25jZQ==')
|
|
419
|
+
if (waW !== 28 || dec.decode(waOut) !== waExpected) {
|
|
420
|
+
return false
|
|
421
|
+
}
|
|
422
|
+
const etagData = enc.encode('hello')
|
|
423
|
+
const etagOut = new Uint8Array(16)
|
|
424
|
+
const etagW = b.etagInto(etagData, etagOut)
|
|
425
|
+
if (etagW !== 10 || dec.decode(etagOut.subarray(0, etagW)) !== b.etag(etagData)) {
|
|
426
|
+
return false
|
|
427
|
+
}
|
|
428
|
+
const csrfOut = new Uint8Array(129)
|
|
429
|
+
const csrfW = b.csrfTokenInto(enc.encode('csrf-secret'), csrfOut)
|
|
430
|
+
if (csrfW !== 129 || csrfOut[64] !== 46 /* '.' */) {
|
|
431
|
+
return false
|
|
432
|
+
}
|
|
433
|
+
// sign/verify cookie pooled Into: round-trip value bytes.
|
|
434
|
+
const ckVal = enc.encode('session-value')
|
|
435
|
+
const ckSec = enc.encode('s3cr3t-secret')
|
|
436
|
+
const ckOut = new Uint8Array(256)
|
|
437
|
+
const ckW = b.signCookieInto(ckVal, ckSec, ckOut)
|
|
438
|
+
if (ckW !== ckVal.length + 65) {
|
|
439
|
+
return false
|
|
440
|
+
}
|
|
441
|
+
const ckVerifyOut = new Uint8Array(256)
|
|
442
|
+
const ckV = b.verifyCookieInto(ckOut.subarray(0, ckW), ckSec, ckVerifyOut)
|
|
443
|
+
if (ckV !== ckVal.length || dec.decode(ckVerifyOut.subarray(0, ckV)) !== 'session-value') {
|
|
444
|
+
return false
|
|
445
|
+
}
|
|
446
|
+
// JWT sign pooled Into: same token as the cstring path.
|
|
447
|
+
const jwtClaims = enc.encode('{"sub":"user-1"}')
|
|
448
|
+
const jwtSecret2 = enc.encode('my-secret')
|
|
449
|
+
const jwtOut = new Uint8Array(512)
|
|
450
|
+
const jwtW = b.jwtSignBytesInto(jwtClaims, jwtSecret2, 0, 0, jwtOut)
|
|
451
|
+
if (jwtW === 0 || dec.decode(jwtOut.subarray(0, jwtW)).split('.').length !== 3) {
|
|
452
|
+
return false
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Multipart parse Into (pooled): 1 part named "field".
|
|
456
|
+
const mOutInto = new Uint8Array(mBody.length + boundary.length + 64)
|
|
457
|
+
const mW = b.multipartParsePackedInto(mBody, boundary, mOutInto)
|
|
458
|
+
if (mW === 0 || mOutInto[0] !== 1) {
|
|
459
|
+
return false
|
|
460
|
+
}
|
|
461
|
+
const mNameLenInto =
|
|
462
|
+
(mOutInto[4] ?? 0) |
|
|
463
|
+
((mOutInto[5] ?? 0) << 8) |
|
|
464
|
+
((mOutInto[6] ?? 0) << 16) |
|
|
465
|
+
((mOutInto[7] ?? 0) << 24)
|
|
466
|
+
if (dec.decode(mOutInto.subarray(8, 8 + mNameLenInto)) !== 'field') {
|
|
467
|
+
return false
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// JWT sign with ttl=0 (deterministic — no iat/exp), then verify the
|
|
471
|
+
// signature with the FFI HMAC to prove the binding is real.
|
|
472
|
+
const jwtSecret = enc.encode('my-secret')
|
|
473
|
+
const jwt = b.jwtSignBytes(enc.encode('{"sub":"user-1"}'), jwtSecret, 0, 0)
|
|
474
|
+
const jwtStr = jwt
|
|
475
|
+
const segs = jwtStr.split('.')
|
|
476
|
+
if (segs.length !== 3 || segs[0] === '' || segs[1] === '' || segs[2] === '') {
|
|
477
|
+
return false
|
|
478
|
+
}
|
|
479
|
+
const signingInput = enc.encode(`${segs[0]}.${segs[1]}`)
|
|
480
|
+
const sigHex = b.hmacSha256(jwtSecret, signingInput)
|
|
481
|
+
const sigBytes = b.base64Decode(enc.encode(segs[2] ?? ''), true, false)
|
|
482
|
+
if (sigBytes === null || b.hexEncode(sigBytes) !== sigHex) {
|
|
483
|
+
return false
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// HTTP-date Into: Sun, 06 Nov 1994 08:49:37 GMT (fixed 29 bytes).
|
|
487
|
+
const dateOut = new Uint8Array(32)
|
|
488
|
+
const dateW = b.httpDateInto(784111777, dateOut)
|
|
489
|
+
if (dateW !== 29 || dec.decode(dateOut.subarray(0, dateW)) !== 'Sun, 06 Nov 1994 08:49:37 GMT') {
|
|
490
|
+
return false
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// SSE encode Into: event/id/retry present + null-omission parity.
|
|
494
|
+
const sseData = enc.encode('hello')
|
|
495
|
+
const sse1 = dec.decode(b.sseEncodeEvent('update', sseData, '42', 3000))
|
|
496
|
+
if (sse1 !== 'id: 42\nevent: update\nretry: 3000\ndata: hello\n\n') {
|
|
497
|
+
return false
|
|
498
|
+
}
|
|
499
|
+
const sse2 = dec.decode(b.sseEncodeEvent(null, sseData, null, null))
|
|
500
|
+
if (sse2 !== 'data: hello\n\n') {
|
|
501
|
+
return false
|
|
502
|
+
}
|
|
503
|
+
// Present-but-empty event string emits the line (Option parity vs napi).
|
|
504
|
+
const sse3 = dec.decode(b.sseEncodeEvent('', sseData, null, null))
|
|
505
|
+
if (sse3 !== 'event: \ndata: hello\n\n') {
|
|
506
|
+
return false
|
|
507
|
+
}
|
|
508
|
+
// Pooled sibling: same bytes into a caller buffer; too-small throws.
|
|
509
|
+
const ssePool = new Uint8Array(128)
|
|
510
|
+
const sseW = b.sseEncodeEventInto('update', sseData, '42', 3000, ssePool)
|
|
511
|
+
if (sseW !== sse1.length || dec.decode(ssePool.subarray(0, sseW)) !== sse1) {
|
|
512
|
+
return false
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
b.sseEncodeEventInto('update', sseData, '42', 3000, new Uint8Array(4))
|
|
516
|
+
return false
|
|
517
|
+
} catch {
|
|
518
|
+
// expected
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ── Wired WIP surface: JWT verify ──
|
|
522
|
+
// jwtVerify: sign (ttl=0 → no iat/exp) then verify → claims JSON; tampered → null.
|
|
523
|
+
const vjwtSecret = enc.encode('verify-secret')
|
|
524
|
+
const vjwt = b.jwtSignBytes(enc.encode('{"sub":"u-1"}'), vjwtSecret, 0, 0)
|
|
525
|
+
const vjwtClaims = b.jwtVerify(enc.encode(vjwt), vjwtSecret, 0)
|
|
526
|
+
if (vjwtClaims === null || !vjwtClaims.includes('"sub":"u-1"')) {
|
|
527
|
+
return false
|
|
528
|
+
}
|
|
529
|
+
if (b.jwtVerify(enc.encode('tampered.token.value'), vjwtSecret, 0) !== null) {
|
|
530
|
+
return false
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ── Phase 3 — stateless napi-only scalars ─────────────────────────
|
|
534
|
+
// jsonParsePacked: packed token stream decodes (via the REAL public decoder)
|
|
535
|
+
// to the same value as JSON.parse, with NO second text parse; invalid JSON
|
|
536
|
+
// throws (napi parity). Also verifies the needed-size retry path.
|
|
537
|
+
{
|
|
538
|
+
const packed = b.jsonParsePacked(enc.encode('{"a":1,"b":[true,null,"x"],"n":{"v":2.5}}'))
|
|
539
|
+
const v = decodeJsonPacked(packed) as { a: number; b: unknown[]; n: { v: number } }
|
|
540
|
+
if (v.a !== 1 || v.b[0] !== true || v.b[1] !== null || v.b[2] !== 'x' || v.n.v !== 2.5) {
|
|
541
|
+
return false
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
b.jsonParsePacked(enc.encode('nope'))
|
|
545
|
+
return false
|
|
546
|
+
} catch {
|
|
547
|
+
// expected: invalid JSON → growExact throws
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// parseMediaType: packed verdict, mediaType at [4..4+len].
|
|
552
|
+
const mt = b.parseMediaType(enc.encode('application/json; charset=utf-8'))
|
|
553
|
+
const mtLen = new DataView(mt.buffer, mt.byteOffset, mt.byteLength).getUint32(0, true)
|
|
554
|
+
if (mtLen === 0 || dec.decode(mt.subarray(4, 4 + mtLen)) !== 'application/json') {
|
|
555
|
+
return false
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// parseHttpDate: RFC 7231 vector → epoch; malformed → null.
|
|
559
|
+
if (b.parseHttpDate(enc.encode('Sun, 06 Nov 1994 08:49:37 GMT')) !== 784111777n) {
|
|
560
|
+
return false
|
|
561
|
+
}
|
|
562
|
+
if (b.parseHttpDate(enc.encode('not a date')) !== null) {
|
|
563
|
+
return false
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// parseAcceptEncoding: count, first encoding + q (f32 LE at offset 8+encLen).
|
|
567
|
+
const ae = b.parseAcceptEncoding(enc.encode('gzip, deflate;q=0.5'))
|
|
568
|
+
const aeView = new DataView(ae.buffer, ae.byteOffset, ae.byteLength)
|
|
569
|
+
if (aeView.getUint32(0, true) !== 2) {
|
|
570
|
+
return false
|
|
571
|
+
}
|
|
572
|
+
const aeLen = aeView.getUint32(4, true)
|
|
573
|
+
if (dec.decode(ae.subarray(8, 8 + aeLen)) !== 'gzip') {
|
|
574
|
+
return false
|
|
575
|
+
}
|
|
576
|
+
if (aeView.getFloat32(8 + aeLen, true) !== 1.0) {
|
|
577
|
+
return false
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// urlEncodeQuery (packed pairs → sorted query text). Build the packed pairs
|
|
581
|
+
// inline — no shared/packed import on the bind-time critical path.
|
|
582
|
+
const qpPacked = new Uint8Array(4 + 2 * (4 + 1 + 4 + 1))
|
|
583
|
+
const qpView = new DataView(qpPacked.buffer)
|
|
584
|
+
qpView.setUint32(0, 2, true)
|
|
585
|
+
let qpOff = 4
|
|
586
|
+
for (const [k, v] of [
|
|
587
|
+
['a', '1'],
|
|
588
|
+
['b', '2'],
|
|
589
|
+
] as const) {
|
|
590
|
+
qpView.setUint32(qpOff, k.length, true)
|
|
591
|
+
qpOff += 4
|
|
592
|
+
qpPacked.set(enc.encode(k), qpOff)
|
|
593
|
+
qpOff += k.length
|
|
594
|
+
qpView.setUint32(qpOff, v.length, true)
|
|
595
|
+
qpOff += 4
|
|
596
|
+
qpPacked.set(enc.encode(v), qpOff)
|
|
597
|
+
qpOff += v.length
|
|
598
|
+
}
|
|
599
|
+
if (b.urlEncodeQuery(qpPacked) !== 'a=1&b=2') {
|
|
600
|
+
return false
|
|
601
|
+
}
|
|
602
|
+
// urlResolve: RFC 3986 §5.4.1.
|
|
603
|
+
if (b.urlResolve(enc.encode('http://a/b/c/d;p?q'), enc.encode('g')) !== 'http://a/b/c/g') {
|
|
604
|
+
return false
|
|
605
|
+
}
|
|
606
|
+
// UrlBuilder opaque-handle: a null (0) handle must throw (the C side never
|
|
607
|
+
// dereferences freed state) — exercises the symbol's ABI. Real resolve
|
|
608
|
+
// parity is covered by test/unit/features/url-join.test.ts.
|
|
609
|
+
let urlBuilderThrew = false
|
|
610
|
+
try {
|
|
611
|
+
b.urlBuilderResolve(0, enc.encode('g'))
|
|
612
|
+
} catch {
|
|
613
|
+
urlBuilderThrew = true
|
|
614
|
+
}
|
|
615
|
+
if (!urlBuilderThrew) {
|
|
616
|
+
return false
|
|
617
|
+
}
|
|
618
|
+
// mimeFromExtension: known + unknown fallback.
|
|
619
|
+
if (b.mimeFromExtension('.js') !== 'text/javascript') {
|
|
620
|
+
return false
|
|
621
|
+
}
|
|
622
|
+
if (b.mimeFromExtension('nope') !== 'application/octet-stream') {
|
|
623
|
+
return false
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ── Bind-time coverage for the remaining pooled/string wrappers ──
|
|
627
|
+
// (closes the guard gap — every bound BunFFI method is now exercised here,
|
|
628
|
+
// matching the "every bound function is checked at bind time" header claim).
|
|
629
|
+
|
|
630
|
+
// urlEncode string form.
|
|
631
|
+
if (b.urlEncode(enc.encode('a b/c')) !== 'a%20b%2Fc') {
|
|
632
|
+
return false
|
|
633
|
+
}
|
|
634
|
+
// hexDecodeInto.
|
|
635
|
+
const hdInto = new Uint8Array(8)
|
|
636
|
+
if (
|
|
637
|
+
b.hexDecodeInto(enc.encode('68656c6c6f'), hdInto) !== 5 ||
|
|
638
|
+
dec.decode(hdInto.subarray(0, 5)) !== 'hello'
|
|
639
|
+
) {
|
|
640
|
+
return false
|
|
641
|
+
}
|
|
642
|
+
// urlDecodeInto.
|
|
643
|
+
const udInto = new Uint8Array(16)
|
|
644
|
+
if (
|
|
645
|
+
b.urlDecodeInto(enc.encode('a%20b%2Fc'), udInto) !== 5 ||
|
|
646
|
+
dec.decode(udInto.subarray(0, 5)) !== 'a b/c'
|
|
647
|
+
) {
|
|
648
|
+
return false
|
|
649
|
+
}
|
|
650
|
+
// base64EncodeInto.
|
|
651
|
+
const b64Into = new Uint8Array(16)
|
|
652
|
+
if (
|
|
653
|
+
b.base64EncodeInto(SELFTEST_HEX, b64Into) !== 8 ||
|
|
654
|
+
dec.decode(b64Into.subarray(0, 8)) !== 'aGVsbG8='
|
|
655
|
+
) {
|
|
656
|
+
return false
|
|
657
|
+
}
|
|
658
|
+
// base64DecodeInto.
|
|
659
|
+
const bdInto = new Uint8Array(8)
|
|
660
|
+
if (
|
|
661
|
+
b.base64DecodeInto(enc.encode('aGVsbG8='), bdInto) !== 5 ||
|
|
662
|
+
dec.decode(bdInto.subarray(0, 5)) !== 'hello'
|
|
663
|
+
) {
|
|
664
|
+
return false
|
|
665
|
+
}
|
|
666
|
+
// rateLimiterCheckKey: null (0) handle → throw (ABI exercise).
|
|
667
|
+
let rlKeyThrew = false
|
|
668
|
+
try {
|
|
669
|
+
b.rateLimiterCheckKey(0, 12345, 0)
|
|
670
|
+
} catch {
|
|
671
|
+
rlKeyThrew = true
|
|
672
|
+
}
|
|
673
|
+
if (!rlKeyThrew) {
|
|
674
|
+
return false
|
|
675
|
+
}
|
|
676
|
+
// randomToken string form.
|
|
677
|
+
if (!/^[0-9a-f]{32}$/.test(b.randomToken(16))) {
|
|
678
|
+
return false
|
|
679
|
+
}
|
|
680
|
+
// hmacSha256Into (RFC 4231 test case 1 hex vector).
|
|
681
|
+
const hmacInto = new Uint8Array(64)
|
|
682
|
+
if (
|
|
683
|
+
b.hmacSha256Into(hmacKey, hmacData, hmacInto) !== 64 ||
|
|
684
|
+
dec.decode(hmacInto) !== 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'
|
|
685
|
+
) {
|
|
686
|
+
return false
|
|
687
|
+
}
|
|
688
|
+
// aeadEncryptInto → decrypt round-trip.
|
|
689
|
+
const ctInto = new Uint8Array(64)
|
|
690
|
+
const ctW = b.aeadEncryptInto(aeadKey, nonce, SELFTEST_HEX, ctInto, 0)
|
|
691
|
+
if (ctW === 0 || b.aeadDecrypt(aeadKey, nonce, ctInto.subarray(0, ctW), 0) === null) {
|
|
692
|
+
return false
|
|
693
|
+
}
|
|
694
|
+
// wsFrameEncodeInto.
|
|
695
|
+
const frameInto = new Uint8Array(64)
|
|
696
|
+
if (b.wsFrameEncodeInto(1, SELFTEST_HEX, false, true, frameInto) === 0 || frameInto[0] !== 0x81) {
|
|
697
|
+
return false
|
|
698
|
+
}
|
|
699
|
+
// gzipCompressInto → decompress round-trip.
|
|
700
|
+
const gzInto = new Uint8Array(64)
|
|
701
|
+
const gzW = b.gzipCompressInto(SELFTEST_HEX, gzInto)
|
|
702
|
+
if (gzW === 0 || dec.decode(b.gzipDecompress(gzInto.subarray(0, gzW))) !== 'hello') {
|
|
703
|
+
return false
|
|
704
|
+
}
|
|
705
|
+
// brotliCompressInto → decompress round-trip.
|
|
706
|
+
const brInto = new Uint8Array(64)
|
|
707
|
+
const brW = b.brotliCompressInto(SELFTEST_HEX, brInto)
|
|
708
|
+
if (brW === 0 || dec.decode(b.brotliDecompress(brInto.subarray(0, brW))) !== 'hello') {
|
|
709
|
+
return false
|
|
710
|
+
}
|
|
711
|
+
// gzipDecompressInto → decompress into a CALLER buffer round-trip.
|
|
712
|
+
const gzDst = new Uint8Array(16)
|
|
713
|
+
const gzDW = b.gzipDecompressInto(gzInto.subarray(0, gzW), gzDst)
|
|
714
|
+
if (gzDW === 0 || dec.decode(gzDst.subarray(0, gzDW)) !== 'hello') {
|
|
715
|
+
return false
|
|
716
|
+
}
|
|
717
|
+
// brotliDecompressInto → decompress into a CALLER buffer round-trip.
|
|
718
|
+
const brDst = new Uint8Array(16)
|
|
719
|
+
const brDW = b.brotliDecompressInto(brInto.subarray(0, brW), brDst)
|
|
720
|
+
if (brDW === 0 || dec.decode(brDst.subarray(0, brDW)) !== 'hello') {
|
|
721
|
+
return false
|
|
722
|
+
}
|
|
723
|
+
// httpDate string form (RFC 7231 vector).
|
|
724
|
+
if (b.httpDate(784111777) !== 'Sun, 06 Nov 1994 08:49:37 GMT') {
|
|
725
|
+
return false
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return true
|
|
729
|
+
}
|