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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +501 -5
- package/dist/index.js +1648 -1325
- package/docs/BENCHMARKS.md +13 -0
- package/docs/CASE_STUDY.md +1 -1
- package/docs/ENVIRONMENT.md +1 -1
- package/docs/FFI_BUN_GUIDE.md +1 -1
- package/docs/INGRESS.md +4 -4
- package/docs/REPO_MAP.md +7 -5
- package/docs/superpowers/plans/2026-09-18-castrum-hardening-cleanup.md +617 -0
- package/index.ts +1 -0
- package/package.json +3 -2
- package/rust/crypto/aead.rs +44 -0
- package/rust/crypto/argon2.rs +32 -2
- package/rust/crypto/jwt/tests.rs +19 -0
- package/rust/crypto/jwt/token.rs +43 -28
- package/rust/crypto/session.rs +6 -0
- package/rust/ffi/crypto.rs +63 -32
- package/rust/ffi/hashing.rs +48 -33
- package/rust/ffi/http.rs +62 -36
- package/rust/ffi/metrics.rs +6 -2
- package/rust/ffi/payload.rs +47 -27
- package/rust/ffi/task.rs +51 -32
- package/rust/ffi/tests.rs +24 -0
- package/rust/ffi/util.rs +40 -0
- package/rust/http/headers.rs +13 -0
- package/rust/http/multipart.rs +139 -0
- package/rust/ingress/native_route.rs +21 -3
- package/rust/task/completion.rs +7 -0
- package/rust/task/mod.rs +1 -1
- package/rust/task/tests.rs +30 -0
- package/src/ingress/decode/baked-result.ts +13 -5
- package/src/ingress/decode/fast-result.ts +1 -1
- package/src/ingress/decode/result-base.ts +16 -2
- package/src/ingress/handlers.ts +103 -31
- package/src/ingress/headers/memoized-headers.ts +40 -0
- package/src/ingress/native-route.ts +25 -6
- package/src/ingress/options.ts +26 -2
- package/src/ingress/packing/gather-raw-headers.ts +24 -8
- package/src/ingress/response/baked-response.ts +118 -28
- package/src/ingress/router.ts +14 -2
- package/src/ingress/routes/common.ts +26 -10
- package/src/ingress/routes/echo.ts +50 -7
- package/src/ingress/routes/json-write.ts +21 -8
- package/src/ingress/routes/responder.ts +4 -4
- package/src/ingress/server-node.ts +33 -14
- package/src/ingress/server.ts +3 -4
- package/src/ingress/shared.ts +1 -1
- package/src/ingress/types.ts +15 -0
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs-selftest.ts +305 -0
- package/src/native/ffi/build/codecs.ts +0 -296
- package/src/native/ffi/build/instances-selftest.ts +264 -0
- package/src/native/ffi/build/instances.ts +10 -259
- package/src/native/ffi/build/parse-selftest.ts +238 -0
- package/src/native/ffi/build/parse.ts +11 -234
- package/src/native/ffi/build.ts +5 -1
- package/src/native/ffi/selftest.ts +3 -3
- package/src/native/ffi/symbols.ts +460 -0
- package/src/native/ffi.ts +2 -434
- package/src/native/ingress-binding.ts +39 -0
- package/src/runtime/codec.ts +24 -0
- package/src/runtime/types.ts +11 -0
- package/src/rust-ffi/batch/build.ts +18 -8
- package/src/rust-ffi/batch/types.ts +15 -0
- package/src/rust-ffi/options.ts +19 -0
- package/src/rust-ffi/scalar/crypto.ts +8 -0
- package/src/rust-ffi/scalar/factories.ts +47 -20
- package/src/rust-ffi/scalar/http.ts +60 -10
- package/src/rust-ffi/scalar/interface.ts +4 -1
- package/src/rust-ffi/scalar/payload.ts +27 -1
- package/src/shared/buffer-pool.ts +44 -9
- package/src/shared/codec.ts +26 -0
- package/src/shared/packed/parsers.ts +64 -7
- 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
|
|
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
|
-
}
|
package/src/native/ffi/build.ts
CHANGED
|
@@ -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
|
|
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
|
|