castrum 0.9.7 → 0.9.8

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 (84) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/castrum.darwin-arm64.node +0 -0
  4. package/castrum.darwin-x64.node +0 -0
  5. package/castrum.linux-arm64-gnu.node +0 -0
  6. package/castrum.linux-arm64-musl.node +0 -0
  7. package/castrum.linux-x64-gnu.node +0 -0
  8. package/castrum.linux-x64-musl.node +0 -0
  9. package/castrum.linux-x64-v3-gnu.node +0 -0
  10. package/castrum.win32-x64-msvc.node +0 -0
  11. package/dist/index.d.ts +501 -5
  12. package/dist/index.js +1648 -1325
  13. package/docs/BENCHMARKS.md +13 -0
  14. package/docs/CASE_STUDY.md +1 -1
  15. package/docs/ENVIRONMENT.md +1 -1
  16. package/docs/FFI_BUN_GUIDE.md +1 -1
  17. package/docs/INGRESS.md +4 -4
  18. package/docs/REPO_MAP.md +7 -5
  19. package/docs/superpowers/plans/2026-09-18-castrum-hardening-cleanup.md +617 -0
  20. package/index.ts +1 -0
  21. package/package.json +3 -2
  22. package/rust/crypto/aead.rs +44 -0
  23. package/rust/crypto/argon2.rs +32 -2
  24. package/rust/crypto/jwt/tests.rs +19 -0
  25. package/rust/crypto/jwt/token.rs +43 -28
  26. package/rust/crypto/session.rs +6 -0
  27. package/rust/ffi/crypto.rs +63 -32
  28. package/rust/ffi/hashing.rs +48 -33
  29. package/rust/ffi/http.rs +62 -36
  30. package/rust/ffi/metrics.rs +6 -2
  31. package/rust/ffi/payload.rs +47 -27
  32. package/rust/ffi/task.rs +51 -32
  33. package/rust/ffi/tests.rs +24 -0
  34. package/rust/ffi/util.rs +40 -0
  35. package/rust/http/headers.rs +13 -0
  36. package/rust/http/multipart.rs +139 -0
  37. package/rust/ingress/native_route.rs +21 -3
  38. package/rust/task/completion.rs +7 -0
  39. package/rust/task/mod.rs +1 -1
  40. package/rust/task/tests.rs +30 -0
  41. package/src/ingress/decode/baked-result.ts +13 -5
  42. package/src/ingress/decode/fast-result.ts +1 -1
  43. package/src/ingress/decode/result-base.ts +16 -2
  44. package/src/ingress/handlers.ts +103 -31
  45. package/src/ingress/headers/memoized-headers.ts +40 -0
  46. package/src/ingress/native-route.ts +25 -6
  47. package/src/ingress/options.ts +26 -2
  48. package/src/ingress/packing/gather-raw-headers.ts +24 -8
  49. package/src/ingress/response/baked-response.ts +118 -28
  50. package/src/ingress/router.ts +14 -2
  51. package/src/ingress/routes/common.ts +26 -10
  52. package/src/ingress/routes/echo.ts +50 -7
  53. package/src/ingress/routes/json-write.ts +21 -8
  54. package/src/ingress/routes/responder.ts +4 -4
  55. package/src/ingress/server-node.ts +33 -14
  56. package/src/ingress/server.ts +3 -4
  57. package/src/ingress/shared.ts +1 -1
  58. package/src/ingress/types.ts +15 -0
  59. package/src/loader/ops.ts +1 -1
  60. package/src/native/ffi/build/codecs-selftest.ts +305 -0
  61. package/src/native/ffi/build/codecs.ts +0 -296
  62. package/src/native/ffi/build/instances-selftest.ts +264 -0
  63. package/src/native/ffi/build/instances.ts +10 -259
  64. package/src/native/ffi/build/parse-selftest.ts +238 -0
  65. package/src/native/ffi/build/parse.ts +11 -234
  66. package/src/native/ffi/build.ts +5 -1
  67. package/src/native/ffi/selftest.ts +3 -3
  68. package/src/native/ffi/symbols.ts +460 -0
  69. package/src/native/ffi.ts +2 -434
  70. package/src/native/ingress-binding.ts +39 -0
  71. package/src/runtime/codec.ts +24 -0
  72. package/src/runtime/types.ts +11 -0
  73. package/src/rust-ffi/batch/build.ts +18 -8
  74. package/src/rust-ffi/batch/types.ts +15 -0
  75. package/src/rust-ffi/options.ts +19 -0
  76. package/src/rust-ffi/scalar/crypto.ts +8 -0
  77. package/src/rust-ffi/scalar/factories.ts +47 -20
  78. package/src/rust-ffi/scalar/http.ts +60 -10
  79. package/src/rust-ffi/scalar/interface.ts +4 -1
  80. package/src/rust-ffi/scalar/payload.ts +27 -1
  81. package/src/shared/buffer-pool.ts +44 -9
  82. package/src/shared/codec.ts +26 -0
  83. package/src/shared/packed/parsers.ts +64 -7
  84. package/src/task/runtime.ts +29 -28
@@ -0,0 +1,238 @@
1
+ // src/native/ffi/build/parse-selftest.ts — bind-time self-test for the
2
+ // parser/wire-format BunFFI surface.
3
+ //
4
+ // Extracted from parse.ts so the method builders and the known-answer probes
5
+ // stay separate modules; `selftest.ts` ANDs this with the other per-domain
6
+ // probes.
7
+
8
+ import { decodeJsonPacked } from '../../../rust-ffi/scalar/json-packed'
9
+ import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
10
+ import { SELFTEST_HEX } from '../constants'
11
+ import type { BunFFI } from '../types'
12
+
13
+ /**
14
+ * Bind-time self-test for the parser/wire-format surface (the methods built
15
+ * in `buildParse`). `false` disables the ffi layer and forces the napi
16
+ * fallback.
17
+ */
18
+ export function selfTestParse(b: BunFFI): boolean {
19
+ const enc = { encode: encodeUtf8 }
20
+ const dec = { decode: decodeUtf8 }
21
+
22
+ // WebSocket frame: text frame, FIN, no mask → first byte 0x81.
23
+ const frame = b.wsFrameEncode(1, SELFTEST_HEX, false, true)
24
+ if (frame.length === 0 || frame[0] !== 0x81) {
25
+ return false
26
+ }
27
+
28
+ // Packed parsers (non-empty output). Packed output is LARGER than input
29
+ // (each component gets a u32 length prefix), so size with the Rust
30
+ // allocator's conservative upper bound (`input.len() * 9 + 16` in
31
+ // query_parser.rs).
32
+ const req = enc.encode('GET /a?b=1 HTTP/1.1\r\nHost: example.com\r\n\r\n')
33
+ const reqOut = new Uint8Array(req.length * 9 + 16)
34
+ if (b.httpParseRequestPackedInto(req, reqOut) === 0) {
35
+ return false
36
+ }
37
+ const qIn = enc.encode('a=1&b=2')
38
+ const qOut = new Uint8Array(qIn.length * 9 + 16)
39
+ if (b.queryParsePackedInto(qIn, qOut) === 0) {
40
+ return false
41
+ }
42
+ const cIn = enc.encode('a=1; b=2')
43
+ const cOut = new Uint8Array(cIn.length * 9 + 16)
44
+ if (b.cookieParsePackedInto(cIn, cOut) === 0) {
45
+ return false
46
+ }
47
+
48
+ // form parse shares the query core → 2 pairs.
49
+ const fIn = enc.encode('a=1&b=2')
50
+ const fOut = new Uint8Array(fIn.length * 9 + 16)
51
+ if (b.formParsePackedInto(fIn, fOut) === 0 || fOut[0] !== 2) {
52
+ return false
53
+ }
54
+
55
+ // multipart parse → 1 part named "field".
56
+ const boundary = enc.encode('----boundary')
57
+ // Wire format is `--{boundary}` — boundary is `----boundary`, so the body
58
+ // must open with `------boundary`.
59
+ const mBody = enc.encode(
60
+ '------boundary\r\nContent-Disposition: form-data; name="field"\r\n\r\nvalue\r\n------boundary--',
61
+ )
62
+ const mOut = b.multipartParsePacked(mBody, boundary)
63
+ if (mOut[0] !== 1) {
64
+ return false
65
+ }
66
+ // Packed layout: [u32 count][u32 name_len][name]... → name_len at offset 4.
67
+ const mNameLen =
68
+ (mOut[4] ?? 0) | ((mOut[5] ?? 0) << 8) | ((mOut[6] ?? 0) << 16) | ((mOut[7] ?? 0) << 24)
69
+ if (dec.decode(mOut.subarray(8, 8 + mNameLen)) !== 'field') {
70
+ return false
71
+ }
72
+
73
+ // WS frame decode: encode("hello") → decode → fin=1, opcode=1, payload="hello".
74
+ const wf = b.wsFrameEncode(1, SELFTEST_HEX, true, true)
75
+ const wd = b.wsFrameDecodePacked(wf)
76
+ if (wd === null || wd[0] !== 1 || wd[1] !== 1 || dec.decode(wd.subarray(6)) !== 'hello') {
77
+ return false
78
+ }
79
+ if (b.wsFrameDecodePacked(enc.encode('\x80')) !== null) {
80
+ return false
81
+ }
82
+ // Pooled sibling: same decode into a caller buffer; malformed → null.
83
+ const wdInto = new Uint8Array(wf.length + 6)
84
+ const wdW = b.wsFrameDecodePackedInto(wf, wdInto)
85
+ if (
86
+ wdW === null ||
87
+ wdInto[0] !== 1 ||
88
+ wdInto[1] !== 1 ||
89
+ dec.decode(wdInto.subarray(6, wdW)) !== 'hello'
90
+ ) {
91
+ return false
92
+ }
93
+ if (b.wsFrameDecodePackedInto(enc.encode('\x80'), wdInto) !== null) {
94
+ return false
95
+ }
96
+
97
+ // Multipart parse Into (pooled): 1 part named "field".
98
+ const mOutInto = new Uint8Array(mBody.length + boundary.length + 64)
99
+ const mW = b.multipartParsePackedInto(mBody, boundary, mOutInto)
100
+ if (mW === 0 || mOutInto[0] !== 1) {
101
+ return false
102
+ }
103
+ const mNameLenInto =
104
+ (mOutInto[4] ?? 0) |
105
+ ((mOutInto[5] ?? 0) << 8) |
106
+ ((mOutInto[6] ?? 0) << 16) |
107
+ ((mOutInto[7] ?? 0) << 24)
108
+ if (dec.decode(mOutInto.subarray(8, 8 + mNameLenInto)) !== 'field') {
109
+ return false
110
+ }
111
+
112
+ // HTTP-date Into: Sun, 06 Nov 1994 08:49:37 GMT (fixed 29 bytes).
113
+ const dateOut = new Uint8Array(32)
114
+ const dateW = b.httpDateInto(784111777, dateOut)
115
+ if (dateW !== 29 || dec.decode(dateOut.subarray(0, dateW)) !== 'Sun, 06 Nov 1994 08:49:37 GMT') {
116
+ return false
117
+ }
118
+
119
+ // SSE encode Into: event/id/retry present + null-omission parity.
120
+ const sseData = enc.encode('hello')
121
+ const sse1 = dec.decode(b.sseEncodeEvent('update', sseData, '42', 3000))
122
+ if (sse1 !== 'id: 42\nevent: update\nretry: 3000\ndata: hello\n\n') {
123
+ return false
124
+ }
125
+ const sse2 = dec.decode(b.sseEncodeEvent(null, sseData, null, null))
126
+ if (sse2 !== 'data: hello\n\n') {
127
+ return false
128
+ }
129
+ // Present-but-empty event string emits the line (Option parity vs napi).
130
+ const sse3 = dec.decode(b.sseEncodeEvent('', sseData, null, null))
131
+ if (sse3 !== 'event: \ndata: hello\n\n') {
132
+ return false
133
+ }
134
+ // Pooled sibling: same bytes into a caller buffer; too-small throws.
135
+ const ssePool = new Uint8Array(128)
136
+ const sseW = b.sseEncodeEventInto('update', sseData, '42', 3000, ssePool)
137
+ if (sseW !== sse1.length || dec.decode(ssePool.subarray(0, sseW)) !== sse1) {
138
+ return false
139
+ }
140
+ try {
141
+ b.sseEncodeEventInto('update', sseData, '42', 3000, new Uint8Array(4))
142
+ return false
143
+ } catch {
144
+ // expected
145
+ }
146
+
147
+ // jsonParsePacked: packed token stream decodes (via the REAL public decoder)
148
+ // to the same value as JSON.parse, with NO second text parse; invalid JSON
149
+ // throws (napi parity). Also verifies the needed-size retry path.
150
+ {
151
+ const packed = b.jsonParsePacked(enc.encode('{"a":1,"b":[true,null,"x"],"n":{"v":2.5}}'))
152
+ const v = decodeJsonPacked(packed) as { a: number; b: unknown[]; n: { v: number } }
153
+ if (v.a !== 1 || v.b[0] !== true || v.b[1] !== null || v.b[2] !== 'x' || v.n.v !== 2.5) {
154
+ return false
155
+ }
156
+ try {
157
+ b.jsonParsePacked(enc.encode('nope'))
158
+ return false
159
+ } catch {
160
+ // expected: invalid JSON → growExact throws
161
+ }
162
+ }
163
+
164
+ // parseMediaType: packed verdict, mediaType at [4..4+len].
165
+ const mt = b.parseMediaType(enc.encode('application/json; charset=utf-8'))
166
+ const mtLen = new DataView(mt.buffer, mt.byteOffset, mt.byteLength).getUint32(0, true)
167
+ if (mtLen === 0 || dec.decode(mt.subarray(4, 4 + mtLen)) !== 'application/json') {
168
+ return false
169
+ }
170
+
171
+ // parseHttpDate: RFC 7231 vector → epoch; malformed → null.
172
+ if (b.parseHttpDate(enc.encode('Sun, 06 Nov 1994 08:49:37 GMT')) !== 784111777n) {
173
+ return false
174
+ }
175
+ if (b.parseHttpDate(enc.encode('not a date')) !== null) {
176
+ return false
177
+ }
178
+
179
+ // parseAcceptEncoding: count, first encoding + q (f32 LE at offset 8+encLen).
180
+ const ae = b.parseAcceptEncoding(enc.encode('gzip, deflate;q=0.5'))
181
+ const aeView = new DataView(ae.buffer, ae.byteOffset, ae.byteLength)
182
+ if (aeView.getUint32(0, true) !== 2) {
183
+ return false
184
+ }
185
+ const aeLen = aeView.getUint32(4, true)
186
+ if (dec.decode(ae.subarray(8, 8 + aeLen)) !== 'gzip') {
187
+ return false
188
+ }
189
+ if (aeView.getFloat32(8 + aeLen, true) !== 1.0) {
190
+ return false
191
+ }
192
+
193
+ // urlEncodeQuery (packed pairs → sorted query text). Build the packed pairs
194
+ // inline — no shared/packed import on the bind-time critical path.
195
+ const qpPacked = new Uint8Array(4 + 2 * (4 + 1 + 4 + 1))
196
+ const qpView = new DataView(qpPacked.buffer)
197
+ qpView.setUint32(0, 2, true)
198
+ let qpOff = 4
199
+ for (const [k, v] of [
200
+ ['a', '1'],
201
+ ['b', '2'],
202
+ ] as const) {
203
+ qpView.setUint32(qpOff, k.length, true)
204
+ qpOff += 4
205
+ qpPacked.set(enc.encode(k), qpOff)
206
+ qpOff += k.length
207
+ qpView.setUint32(qpOff, v.length, true)
208
+ qpOff += 4
209
+ qpPacked.set(enc.encode(v), qpOff)
210
+ qpOff += v.length
211
+ }
212
+ if (b.urlEncodeQuery(qpPacked) !== 'a=1&b=2') {
213
+ return false
214
+ }
215
+
216
+ // urlResolve: RFC 3986 §5.4.1.
217
+ if (b.urlResolve(enc.encode('http://a/b/c/d;p?q'), enc.encode('g')) !== 'http://a/b/c/g') {
218
+ return false
219
+ }
220
+ // mimeFromExtension: known + unknown fallback.
221
+ if (b.mimeFromExtension('.js') !== 'text/javascript') {
222
+ return false
223
+ }
224
+ if (b.mimeFromExtension('nope') !== 'application/octet-stream') {
225
+ return false
226
+ }
227
+ // httpDate string form (RFC 7231 vector).
228
+ if (b.httpDate(784111777) !== 'Sun, 06 Nov 1994 08:49:37 GMT') {
229
+ return false
230
+ }
231
+ // wsFrameEncodeInto.
232
+ const frameInto = new Uint8Array(64)
233
+ if (b.wsFrameEncodeInto(1, SELFTEST_HEX, false, true, frameInto) === 0 || frameInto[0] !== 0x81) {
234
+ return false
235
+ }
236
+
237
+ return true
238
+ }
@@ -6,18 +6,12 @@
6
6
  // and SSE encoding. Receives the raw dlopen'd symbols and the per-bind context
7
7
  // from `build()`.
8
8
 
9
- import { decodeJsonPacked } from '../../../rust-ffi/scalar/json-packed'
10
9
  import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
11
- import { EMPTY_VIEW, SELFTEST_HEX } from '../constants'
10
+ import { EMPTY_VIEW } from '../constants'
12
11
  import type { BunFFI, Raw3, Raw4, Raw6, Raw7, Raw10 } from '../types'
13
12
  import type { BuildCtx } from './util'
14
13
  import { allocOut, flag, growExact } from './util'
15
14
 
16
- /**
17
- * Build the parser/wire-format methods of the BunFFI surface. `ctx` is
18
- * destructured so the method bodies read exactly as the original `build()`.
19
- */
20
-
21
15
  // ── SSE event/id encode memo ────────────────────────────────────────────────
22
16
  // SSE streams repeat the same event NAME constantly ('message'/'update') and
23
17
  // often repeat ids; re-encoding them per event is pure waste (~60ns each).
@@ -34,6 +28,16 @@ function sseMemoEncode(slot: { str: string | null; bytes: Uint8Array }, s: strin
34
28
  return bytes
35
29
  }
36
30
 
31
+ /**
32
+ * Build the parser/wire-format methods of the BunFFI surface (HTTP/query/
33
+ * cookie/form/multipart/ws-frame parsers, media-type/http-date/accept
34
+ * verdicts, MIME lookup, URL resolve, and SSE encode). `ctx` is destructured so
35
+ * the method bodies read exactly as the original `build()`.
36
+ *
37
+ * @param sym - Raw dlopen'd symbols for this domain.
38
+ * @param ctx - Per-bind shared scratch/context from `build()`.
39
+ * @returns The parser/wire-format slice of the `BunFFI` surface.
40
+ */
37
41
  export function buildParse(
38
42
  sym: Record<string, (...a: unknown[]) => unknown>,
39
43
  ctx: BuildCtx,
@@ -346,230 +350,3 @@ export function buildParse(
346
350
  },
347
351
  }
348
352
  }
349
-
350
- /**
351
- * Bind-time self-test for the parser/wire-format surface (the methods built
352
- * in `buildParse`). `false` disables the ffi layer and forces the napi
353
- * fallback.
354
- */
355
- export function selfTestParse(b: BunFFI): boolean {
356
- const enc = { encode: encodeUtf8 }
357
- const dec = { decode: decodeUtf8 }
358
-
359
- // WebSocket frame: text frame, FIN, no mask → first byte 0x81.
360
- const frame = b.wsFrameEncode(1, SELFTEST_HEX, false, true)
361
- if (frame.length === 0 || frame[0] !== 0x81) {
362
- return false
363
- }
364
-
365
- // Packed parsers (non-empty output). Packed output is LARGER than input
366
- // (each component gets a u32 length prefix), so size with the Rust
367
- // allocator's conservative upper bound (`input.len() * 9 + 16` in
368
- // query_parser.rs).
369
- const req = enc.encode('GET /a?b=1 HTTP/1.1\r\nHost: example.com\r\n\r\n')
370
- const reqOut = new Uint8Array(req.length * 9 + 16)
371
- if (b.httpParseRequestPackedInto(req, reqOut) === 0) {
372
- return false
373
- }
374
- const qIn = enc.encode('a=1&b=2')
375
- const qOut = new Uint8Array(qIn.length * 9 + 16)
376
- if (b.queryParsePackedInto(qIn, qOut) === 0) {
377
- return false
378
- }
379
- const cIn = enc.encode('a=1; b=2')
380
- const cOut = new Uint8Array(cIn.length * 9 + 16)
381
- if (b.cookieParsePackedInto(cIn, cOut) === 0) {
382
- return false
383
- }
384
-
385
- // form parse shares the query core → 2 pairs.
386
- const fIn = enc.encode('a=1&b=2')
387
- const fOut = new Uint8Array(fIn.length * 9 + 16)
388
- if (b.formParsePackedInto(fIn, fOut) === 0 || fOut[0] !== 2) {
389
- return false
390
- }
391
-
392
- // multipart parse → 1 part named "field".
393
- const boundary = enc.encode('----boundary')
394
- // Wire format is `--{boundary}` — boundary is `----boundary`, so the body
395
- // must open with `------boundary`.
396
- const mBody = enc.encode(
397
- '------boundary\r\nContent-Disposition: form-data; name="field"\r\n\r\nvalue\r\n------boundary--',
398
- )
399
- const mOut = b.multipartParsePacked(mBody, boundary)
400
- if (mOut[0] !== 1) {
401
- return false
402
- }
403
- // Packed layout: [u32 count][u32 name_len][name]... → name_len at offset 4.
404
- const mNameLen =
405
- (mOut[4] ?? 0) | ((mOut[5] ?? 0) << 8) | ((mOut[6] ?? 0) << 16) | ((mOut[7] ?? 0) << 24)
406
- if (dec.decode(mOut.subarray(8, 8 + mNameLen)) !== 'field') {
407
- return false
408
- }
409
-
410
- // WS frame decode: encode("hello") → decode → fin=1, opcode=1, payload="hello".
411
- const wf = b.wsFrameEncode(1, SELFTEST_HEX, true, true)
412
- const wd = b.wsFrameDecodePacked(wf)
413
- if (wd === null || wd[0] !== 1 || wd[1] !== 1 || dec.decode(wd.subarray(6)) !== 'hello') {
414
- return false
415
- }
416
- if (b.wsFrameDecodePacked(enc.encode('\x80')) !== null) {
417
- return false
418
- }
419
- // Pooled sibling: same decode into a caller buffer; malformed → null.
420
- const wdInto = new Uint8Array(wf.length + 6)
421
- const wdW = b.wsFrameDecodePackedInto(wf, wdInto)
422
- if (
423
- wdW === null ||
424
- wdInto[0] !== 1 ||
425
- wdInto[1] !== 1 ||
426
- dec.decode(wdInto.subarray(6, wdW)) !== 'hello'
427
- ) {
428
- return false
429
- }
430
- if (b.wsFrameDecodePackedInto(enc.encode('\x80'), wdInto) !== null) {
431
- return false
432
- }
433
-
434
- // Multipart parse Into (pooled): 1 part named "field".
435
- const mOutInto = new Uint8Array(mBody.length + boundary.length + 64)
436
- const mW = b.multipartParsePackedInto(mBody, boundary, mOutInto)
437
- if (mW === 0 || mOutInto[0] !== 1) {
438
- return false
439
- }
440
- const mNameLenInto =
441
- (mOutInto[4] ?? 0) |
442
- ((mOutInto[5] ?? 0) << 8) |
443
- ((mOutInto[6] ?? 0) << 16) |
444
- ((mOutInto[7] ?? 0) << 24)
445
- if (dec.decode(mOutInto.subarray(8, 8 + mNameLenInto)) !== 'field') {
446
- return false
447
- }
448
-
449
- // HTTP-date Into: Sun, 06 Nov 1994 08:49:37 GMT (fixed 29 bytes).
450
- const dateOut = new Uint8Array(32)
451
- const dateW = b.httpDateInto(784111777, dateOut)
452
- if (dateW !== 29 || dec.decode(dateOut.subarray(0, dateW)) !== 'Sun, 06 Nov 1994 08:49:37 GMT') {
453
- return false
454
- }
455
-
456
- // SSE encode Into: event/id/retry present + null-omission parity.
457
- const sseData = enc.encode('hello')
458
- const sse1 = dec.decode(b.sseEncodeEvent('update', sseData, '42', 3000))
459
- if (sse1 !== 'id: 42\nevent: update\nretry: 3000\ndata: hello\n\n') {
460
- return false
461
- }
462
- const sse2 = dec.decode(b.sseEncodeEvent(null, sseData, null, null))
463
- if (sse2 !== 'data: hello\n\n') {
464
- return false
465
- }
466
- // Present-but-empty event string emits the line (Option parity vs napi).
467
- const sse3 = dec.decode(b.sseEncodeEvent('', sseData, null, null))
468
- if (sse3 !== 'event: \ndata: hello\n\n') {
469
- return false
470
- }
471
- // Pooled sibling: same bytes into a caller buffer; too-small throws.
472
- const ssePool = new Uint8Array(128)
473
- const sseW = b.sseEncodeEventInto('update', sseData, '42', 3000, ssePool)
474
- if (sseW !== sse1.length || dec.decode(ssePool.subarray(0, sseW)) !== sse1) {
475
- return false
476
- }
477
- try {
478
- b.sseEncodeEventInto('update', sseData, '42', 3000, new Uint8Array(4))
479
- return false
480
- } catch {
481
- // expected
482
- }
483
-
484
- // jsonParsePacked: packed token stream decodes (via the REAL public decoder)
485
- // to the same value as JSON.parse, with NO second text parse; invalid JSON
486
- // throws (napi parity). Also verifies the needed-size retry path.
487
- {
488
- const packed = b.jsonParsePacked(enc.encode('{"a":1,"b":[true,null,"x"],"n":{"v":2.5}}'))
489
- const v = decodeJsonPacked(packed) as { a: number; b: unknown[]; n: { v: number } }
490
- if (v.a !== 1 || v.b[0] !== true || v.b[1] !== null || v.b[2] !== 'x' || v.n.v !== 2.5) {
491
- return false
492
- }
493
- try {
494
- b.jsonParsePacked(enc.encode('nope'))
495
- return false
496
- } catch {
497
- // expected: invalid JSON → growExact throws
498
- }
499
- }
500
-
501
- // parseMediaType: packed verdict, mediaType at [4..4+len].
502
- const mt = b.parseMediaType(enc.encode('application/json; charset=utf-8'))
503
- const mtLen = new DataView(mt.buffer, mt.byteOffset, mt.byteLength).getUint32(0, true)
504
- if (mtLen === 0 || dec.decode(mt.subarray(4, 4 + mtLen)) !== 'application/json') {
505
- return false
506
- }
507
-
508
- // parseHttpDate: RFC 7231 vector → epoch; malformed → null.
509
- if (b.parseHttpDate(enc.encode('Sun, 06 Nov 1994 08:49:37 GMT')) !== 784111777n) {
510
- return false
511
- }
512
- if (b.parseHttpDate(enc.encode('not a date')) !== null) {
513
- return false
514
- }
515
-
516
- // parseAcceptEncoding: count, first encoding + q (f32 LE at offset 8+encLen).
517
- const ae = b.parseAcceptEncoding(enc.encode('gzip, deflate;q=0.5'))
518
- const aeView = new DataView(ae.buffer, ae.byteOffset, ae.byteLength)
519
- if (aeView.getUint32(0, true) !== 2) {
520
- return false
521
- }
522
- const aeLen = aeView.getUint32(4, true)
523
- if (dec.decode(ae.subarray(8, 8 + aeLen)) !== 'gzip') {
524
- return false
525
- }
526
- if (aeView.getFloat32(8 + aeLen, true) !== 1.0) {
527
- return false
528
- }
529
-
530
- // urlEncodeQuery (packed pairs → sorted query text). Build the packed pairs
531
- // inline — no shared/packed import on the bind-time critical path.
532
- const qpPacked = new Uint8Array(4 + 2 * (4 + 1 + 4 + 1))
533
- const qpView = new DataView(qpPacked.buffer)
534
- qpView.setUint32(0, 2, true)
535
- let qpOff = 4
536
- for (const [k, v] of [
537
- ['a', '1'],
538
- ['b', '2'],
539
- ] as const) {
540
- qpView.setUint32(qpOff, k.length, true)
541
- qpOff += 4
542
- qpPacked.set(enc.encode(k), qpOff)
543
- qpOff += k.length
544
- qpView.setUint32(qpOff, v.length, true)
545
- qpOff += 4
546
- qpPacked.set(enc.encode(v), qpOff)
547
- qpOff += v.length
548
- }
549
- if (b.urlEncodeQuery(qpPacked) !== 'a=1&b=2') {
550
- return false
551
- }
552
-
553
- // urlResolve: RFC 3986 §5.4.1.
554
- if (b.urlResolve(enc.encode('http://a/b/c/d;p?q'), enc.encode('g')) !== 'http://a/b/c/g') {
555
- return false
556
- }
557
- // mimeFromExtension: known + unknown fallback.
558
- if (b.mimeFromExtension('.js') !== 'text/javascript') {
559
- return false
560
- }
561
- if (b.mimeFromExtension('nope') !== 'application/octet-stream') {
562
- return false
563
- }
564
- // httpDate string form (RFC 7231 vector).
565
- if (b.httpDate(784111777) !== 'Sun, 06 Nov 1994 08:49:37 GMT') {
566
- return false
567
- }
568
- // wsFrameEncodeInto.
569
- const frameInto = new Uint8Array(64)
570
- if (b.wsFrameEncodeInto(1, SELFTEST_HEX, false, true, frameInto) === 0 || frameInto[0] !== 0x81) {
571
- return false
572
- }
573
-
574
- return true
575
- }
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // `build(sym, useBufferLength)` sets up the shared per-bind state — the
4
4
  // probe-gated `lenOrView`/`oneArg` argument adapters and the pooled scratch
5
- // buffers — then delegates to the four domain builders (each receives the raw
5
+ // buffers — then delegates to the domain builders (each receives the raw
6
6
  // dlopen'd symbols + this context) and merges their method objects into the
7
7
  // final `BunFFI`. Extracted from the former monolithic `ffi.ts` so the
8
8
  // per-call wrapper surface is navigable by domain:
@@ -10,6 +10,10 @@
10
10
  // - `build/compress.ts` gzip / brotli / json patch
11
11
  // - `build/parse.ts` packed parsers / media-type / http-date / sse
12
12
  // - `build/instances.ts` opaque-handle evals + ingress + route stack
13
+ // - `build/metrics.ts` metrics-registry handle surface
14
+ // - `build/task.ts` off-thread task-runtime symbols
15
+ // Bind-time value probes live in `build/*-selftest.ts` (e.g.
16
+ // `build/instances-selftest.ts`) and are ANDed by `selftest.ts`.
13
17
 
14
18
  import { buildCodecs } from './build/codecs'
15
19
  import { buildCompress } from './build/compress'
@@ -10,11 +10,11 @@
10
10
  // `selfTestInstances` / `selfTestMetrics`), and this entry point ANDs them
11
11
  // together so any single failure disables the whole layer.
12
12
 
13
- import { selfTestCodecs } from './build/codecs'
13
+ import { selfTestCodecs } from './build/codecs-selftest'
14
14
  import { selfTestCompress } from './build/compress'
15
- import { selfTestInstances } from './build/instances'
15
+ import { selfTestInstances } from './build/instances-selftest'
16
16
  import { selfTestMetrics } from './build/metrics'
17
- import { selfTestParse } from './build/parse'
17
+ import { selfTestParse } from './build/parse-selftest'
18
18
  import { selfTestTask } from './build/task'
19
19
  import type { BunFFI } from './types'
20
20