castrum 0.9.0 → 0.9.2

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 (120) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1653 -1817
  4. package/dist/index.js +6172 -4294
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +111 -17
  7. package/docs/BENCHMARKS.md +117 -49
  8. package/docs/CASE_STUDY.md +243 -0
  9. package/docs/ENVIRONMENT.md +24 -7
  10. package/docs/FFI_BUN_GUIDE.md +335 -0
  11. package/docs/GETTING_STARTED.md +18 -34
  12. package/docs/HOW_TO_ADD_AN_OP.md +180 -0
  13. package/docs/INGRESS-ROUTER.md +82 -0
  14. package/docs/INGRESS.md +11 -7
  15. package/docs/NATIVE-ROUTE.md +77 -0
  16. package/docs/REPO_MAP.md +103 -53
  17. package/docs/adr/0001-two-wire-formats.md +1 -1
  18. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  19. package/docs/ai/LOCAL_DEV.md +81 -0
  20. package/docs/ai/TREE.md +1070 -0
  21. package/docs/bun-builtins-decision-matrix.md +25 -18
  22. package/index.ts +9 -16
  23. package/package.json +41 -28
  24. package/src/ingress/body.ts +167 -27
  25. package/src/ingress/decode/result-base.ts +10 -2
  26. package/src/ingress/handlers.ts +116 -285
  27. package/src/ingress/headers/security.ts +41 -0
  28. package/src/ingress/health.ts +1 -0
  29. package/src/ingress/index.ts +45 -169
  30. package/src/ingress/native-route.ts +182 -0
  31. package/src/ingress/packing/gather-raw-headers.ts +110 -22
  32. package/src/ingress/packing/header-packing.ts +5 -5
  33. package/src/ingress/packing/input-packer.ts +17 -3
  34. package/src/ingress/packing/route-wire.ts +197 -0
  35. package/src/ingress/packing/select-headers.ts +20 -7
  36. package/src/ingress/path-matcher.ts +115 -0
  37. package/src/ingress/response/baked-response.ts +302 -0
  38. package/src/ingress/router.ts +277 -0
  39. package/src/ingress/routes/common.ts +50 -1
  40. package/src/ingress/routes/fallback.ts +2 -2
  41. package/src/ingress/routes/head.ts +3 -13
  42. package/src/ingress/routes/index.ts +5 -4
  43. package/src/ingress/routes/json-write.ts +26 -47
  44. package/src/ingress/routes/native.ts +130 -0
  45. package/src/ingress/routes/options.ts +2 -12
  46. package/src/ingress/routes/read.ts +3 -13
  47. package/src/ingress/routes/responder.ts +159 -0
  48. package/src/ingress/server-node.ts +7 -7
  49. package/src/ingress/server.ts +83 -112
  50. package/src/ingress/sync.ts +151 -0
  51. package/src/ingress/types.ts +44 -1
  52. package/src/integration/pipeline.ts +3 -0
  53. package/src/integration/streaming.ts +2 -3
  54. package/src/integration/websocket.ts +4 -3
  55. package/src/loader/batch.ts +4 -0
  56. package/src/loader/cost.ts +1 -0
  57. package/src/loader/index.ts +43 -228
  58. package/src/loader/state.ts +84 -0
  59. package/src/loader/types.ts +145 -0
  60. package/src/native/ffi/build/codecs.ts +736 -0
  61. package/src/native/ffi/build/compress.ts +248 -0
  62. package/src/native/ffi/build/instances.ts +598 -0
  63. package/src/native/ffi/build/parse.ts +539 -0
  64. package/src/native/ffi/build/util.ts +140 -0
  65. package/src/native/ffi/build.ts +94 -0
  66. package/src/native/ffi/constants.ts +58 -0
  67. package/src/native/ffi/selftest.ts +22 -0
  68. package/src/native/ffi/types.ts +499 -0
  69. package/src/native/ffi.ts +307 -1357
  70. package/src/native/loader.ts +52 -6
  71. package/src/native/types/instances.ts +279 -0
  72. package/src/native/types.ts +41 -238
  73. package/src/runtime/builtins.ts +131 -0
  74. package/src/runtime/codec.ts +147 -0
  75. package/src/runtime/detect.ts +36 -0
  76. package/src/runtime/index.ts +28 -0
  77. package/src/runtime/native.ts +35 -0
  78. package/src/runtime/server.ts +31 -0
  79. package/src/runtime/transport.ts +64 -0
  80. package/src/runtime/types.ts +173 -0
  81. package/src/runtime/uuid.ts +45 -0
  82. package/src/runtime/websocket.ts +30 -0
  83. package/src/rust-ffi/addon.ts +1 -1
  84. package/src/rust-ffi/batch/build.ts +155 -0
  85. package/src/rust-ffi/batch/index.ts +8 -0
  86. package/src/rust-ffi/batch/types.ts +92 -0
  87. package/src/rust-ffi/client.ts +5 -6
  88. package/src/rust-ffi/context.ts +39 -10
  89. package/src/rust-ffi/index.ts +28 -17
  90. package/src/rust-ffi/into.ts +21 -0
  91. package/src/rust-ffi/options.ts +1 -1
  92. package/src/rust-ffi/packed.ts +40 -2
  93. package/src/rust-ffi/proven.ts +16 -30
  94. package/src/rust-ffi/scalar/crypto.ts +188 -107
  95. package/src/rust-ffi/scalar/factories.ts +257 -27
  96. package/src/rust-ffi/scalar/hashing.ts +43 -41
  97. package/src/rust-ffi/scalar/http.ts +208 -77
  98. package/src/rust-ffi/scalar/index.ts +3 -3
  99. package/src/rust-ffi/scalar/interface.ts +88 -213
  100. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  101. package/src/rust-ffi/scalar/json.ts +28 -12
  102. package/src/rust-ffi/scalar/payload.ts +159 -42
  103. package/src/rust-ffi/text.ts +31 -5
  104. package/src/selection.ts +27 -31
  105. package/src/shared/adaptive.ts +1 -2
  106. package/src/shared/buffer-pool.ts +72 -17
  107. package/src/shared/bytes.ts +75 -6
  108. package/src/shared/codec.ts +78 -0
  109. package/src/shared/env.ts +4 -4
  110. package/src/shared/log.ts +1 -0
  111. package/src/shared/packed/index.ts +10 -0
  112. package/src/shared/packed/parsers.ts +31 -0
  113. package/src/shared/packed/schema.ts +23 -0
  114. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  115. package/src/shared/proven.ts +189 -496
  116. package/src/shared/runtime.ts +20 -25
  117. package/src/shared/uuid.ts +12 -28
  118. package/src/data/json-rows.ts +0 -33
  119. package/src/rust-ffi/batch.ts +0 -431
  120. package/src/shared/bench-classify.ts +0 -209
package/src/native/ffi.ts CHANGED
@@ -1,271 +1,58 @@
1
- // src/native/ffi.ts — Bun-only C-ABI fast path via `bun:ffi`.
1
+ // src/native/ffi.ts — Bun-only C-ABI fast path via `bun:ffi` (transport core).
2
2
  //
3
3
  // Bun JIT-compiles `bun:ffi` calls down to direct native calls (~10-20ns
4
4
  // crossing), versus ~100-350ns for a Node-API call. The same cdylib that Node
5
- // loads through napi-rs ALSO exports `extern "C"` symbols (`rust/ffi.rs`), so
6
- // under Bun we can `dlopen` it and call the hot scalar functions directly —
7
- // skipping the N-API marshaling that dominates the sub-µs operations.
5
+ // loads through napi-rs ALSO exports `extern "C"` symbols (`rust/ffi/`), so
6
+ // under Bun we can `dlopen` it and call the hot scalar functions directly.
7
+ //
8
+ // Structure: this file is the transport CORE (transport selection + probe +
9
+ // dlopen + symbol binding + bind-time self-test gating). The per-call wrapper
10
+ // surface lives in `build.ts` (delegating to `build/{codecs,compress,parse,
11
+ // instances}.ts`), and the pure pieces beside it: `ffi/types.ts` (BunFFI /
12
+ // FfiMode / Raw signatures), `ffi/constants.ts` (caps + sizing + self-test
13
+ // vectors), `ffi/selftest.ts` (the bind-time self-test). This file re-exports
14
+ // `getBunFFI` + `BunFFI` so existing importers keep working unchanged.
8
15
  //
9
16
  // Safety / correctness strategy:
10
17
  // - Lazily bound (no addon/ffi work until first use).
11
- // - A one-time SELF-TEST runs at bind time: every bound function is checked
12
- // against known-good vectors. If ANY check fails (ABI mismatch, platform
13
- // quirk, a future Bun regression), the whole ffi layer is disabled and the
14
- // caller falls back to the napi addon — the public API never sees a wrong
15
- // result. `bun:ffi` is experimental; this is the safety net.
16
- // - Decoders that can fail (hexDecode / urlDecode / base64Decode) ARE exposed
17
- // here with parity error semantics: a `0` write on non-empty input throws
18
- // (the napi decoders throw rather than return a short buffer). The only
19
- // unavoidable divergence is decompressing a stream that yields exactly 0
20
- // bytes, which the C ABI can't distinguish from "too small" (napi returns
21
- // an empty buffer; ffi throws) — degenerate and not exercised by tests.
18
+ // - A one-time SELF-TEST runs at bind time (ffi/selftest.ts); any failure
19
+ // disables ffi and falls back to the napi addon.
20
+ // - Decoders that can fail ARE exposed with parity error semantics: a `0`
21
+ // write on non-empty input throws (the napi decoders throw rather than
22
+ // return a short buffer).
22
23
  // - Never called under Node (falls back to napi immediately).
23
24
  //
24
25
  // Buffer ABI: input/out pointers are `(ptr, len)` pairs; the JS wrapper passes
25
26
  // `(view, view.length)` and bun:ffi converts the TypedArray to its pointer.
26
27
 
27
- // Type-only: erased at compile time (no runtime import of `bun:ffi` — that
28
- // stays lazy inside `bind()`). `FFITypeOrString` is the dlopen ABI-string union.
29
28
  import type { FFITypeOrString } from 'bun:ffi'
30
29
  import { resolveEnvVar } from '../shared/env'
31
30
  import { isBun } from '../shared/runtime'
31
+ import { build } from './ffi/build'
32
+ import { selfTest } from './ffi/selftest'
33
+ import type { BunFFI, FfiMode } from './ffi/types'
32
34
  // Static import: loader.ts has no side effects (path resolution only — no
33
35
  // dlopen), so this adds zero import-time cost while letting `bind()` reuse the
34
36
  // exact same resolved `.node` path the napi fallback uses (the shared seam).
35
37
  import { getAddonPath } from './loader'
36
38
 
37
- const encoder = new TextEncoder()
38
-
39
- /** The set of C-ABI functions this layer can accelerate on Bun. */
40
- export interface BunFFI {
41
- crc32(input: Uint8Array): number
42
- fnv1a64(input: Uint8Array): bigint
43
- xxh3(input: Uint8Array): bigint
44
- jsonValid(input: Uint8Array): boolean
45
- /** Lowercase-hex encode into a fresh `Uint8Array` (size `input.length * 2`). */
46
- hexEncode(input: Uint8Array): Uint8Array
47
- /** Lowercase-hex encode into `output`; returns bytes written. */
48
- hexEncodeInto(input: Uint8Array, output: Uint8Array): number
49
- /** RFC 3986 percent-encode into a fresh buffer (size `input.length * 3`). */
50
- urlEncode(input: Uint8Array): Uint8Array
51
- /** RFC 3986 percent-encode into `output`; returns bytes written. */
52
- urlEncodeInto(input: Uint8Array, output: Uint8Array): number
53
-
54
- // ── Validators (u8 → boolean) ─────────────────────────────────────
55
- validateEmail(input: Uint8Array): boolean
56
- validateUuid(input: Uint8Array): boolean
57
- validateIpv4(input: Uint8Array): boolean
58
- validateIpv6(input: Uint8Array): boolean
59
- /** Sum of `id` fields across a JSON array → bigint (throws on non-array input). */
60
- jsonSumIds(input: Uint8Array): bigint
61
-
62
- // ── Constant-time verify (u8 → boolean) ───────────────────────────
63
- hmacSha256Verify(key: Uint8Array, data: Uint8Array, signature: Uint8Array): boolean
64
- csrfVerify(token: Uint8Array, secret: Uint8Array): boolean
65
- passwordVerify(password: Uint8Array, phc: Uint8Array): boolean
66
- passwordVerifyBcrypt(password: Uint8Array, phc: Uint8Array): boolean
67
-
68
- // ── Decoders (throw on malformed input — napi `Result` parity) ──
69
- /** Hex-decode into a fresh buffer (size `input.length / 2`). */
70
- hexDecode(input: Uint8Array): Uint8Array
71
- /** Hex-decode into `output`; returns bytes written (throws if too small). */
72
- hexDecodeInto(input: Uint8Array, output: Uint8Array): number
73
- /** Percent-decode into a fresh buffer (size `input.length`). */
74
- urlDecode(input: Uint8Array): Uint8Array
75
- /** Percent-decode into `output`; returns bytes written (throws if too small). */
76
- urlDecodeInto(input: Uint8Array, output: Uint8Array): number
77
- /** base64-decode into a fresh buffer (size `ceil(len/4)*3`). */
78
- base64Decode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array
79
- /** base64-decode into `output`; returns bytes written (throws if too small). */
80
- base64DecodeInto(
81
- input: Uint8Array,
82
- output: Uint8Array,
83
- urlSafe?: boolean,
84
- padding?: boolean,
85
- ): number
86
-
87
- // ── Fixed-size output writers ─────────────────────────────────────
88
- /** RFC 6455 Sec-WebSocket-Accept (28 bytes) into a fresh buffer. */
89
- wsAcceptKey(key: Uint8Array): Uint8Array
90
- /** crc32 ETag (10 strong / 12 weak bytes) into a fresh buffer. */
91
- etag(data: Uint8Array, weak?: boolean): Uint8Array
92
- /** crc32 ETag into `output`; returns bytes written. */
93
- etagInto(data: Uint8Array, output: Uint8Array, weak?: boolean): number
94
- /** `byteLen` random bytes → `byteLen * 2` hex chars. */
95
- randomToken(byteLen: number): Uint8Array
96
- /** base64-encode into a fresh buffer (size `ceil(len/3)*4`). */
97
- base64Encode(input: Uint8Array, urlSafe?: boolean, padding?: boolean): Uint8Array
98
- /** base64-encode into `output`; returns bytes written. */
99
- base64EncodeInto(
100
- input: Uint8Array,
101
- output: Uint8Array,
102
- urlSafe?: boolean,
103
- padding?: boolean,
104
- ): number
105
- /** HMAC-SHA256 hex (64 chars) into a fresh buffer. */
106
- hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array
107
- /** HMAC-SHA256 hex into `output`; returns bytes written (throws if too small). */
108
- hmacSha256Into(key: Uint8Array, data: Uint8Array, output: Uint8Array): number
109
- /** Sign cookie `value` as `value.<64-hex>` into a fresh buffer. */
110
- signCookie(value: Uint8Array, secret: Uint8Array): Uint8Array
111
- /** Sign cookie into `output`; returns bytes written (throws if too small). */
112
- signCookieInto(value: Uint8Array, secret: Uint8Array, output: Uint8Array): number
113
- /** Verify a signed cookie → value bytes, or `null` on bad signature. */
114
- verifyCookie(signed: Uint8Array, secret: Uint8Array): Uint8Array | null
115
- /** CSRF token (129 bytes: 64 rnd-hex + '.' + 64 sig-hex). */
116
- csrfToken(secret: Uint8Array): Uint8Array
117
- /** Argon2id PHC hash bytes (m/t/p/out_len params). */
118
- passwordHash(
119
- password: Uint8Array,
120
- salt: Uint8Array,
121
- mCost: number,
122
- tCost: number,
123
- pCost: number,
124
- outLen: number,
125
- ): Uint8Array
126
- /** bcrypt `$2b$` PHC string bytes (cost clamped 4..=31). */
127
- passwordHashBcrypt(password: Uint8Array, cost: number): Uint8Array
128
- /** PBKDF2-HMAC-SHA256 → `dkLen` bytes (rounds, dkLen clamped). */
129
- pbkdf2Sha256(password: Uint8Array, salt: Uint8Array, rounds: number, dkLen: number): Uint8Array
130
- /** AEAD encrypt (0 = AES-256-GCM, 1 = ChaCha20-Poly1305) → ct+tag. */
131
- aeadEncrypt(
132
- key: Uint8Array,
133
- nonce: Uint8Array,
134
- plaintext: Uint8Array,
135
- algorithm?: number,
136
- ): Uint8Array
137
- /** AEAD encrypt into `output` (ct + 16-byte tag); returns bytes written. */
138
- aeadEncryptInto(
139
- key: Uint8Array,
140
- nonce: Uint8Array,
141
- plaintext: Uint8Array,
142
- output: Uint8Array,
143
- algorithm?: number,
144
- ): number
145
- /** AEAD decrypt → plaintext, or `null` on auth failure. */
146
- aeadDecrypt(
147
- key: Uint8Array,
148
- nonce: Uint8Array,
149
- ciphertext: Uint8Array,
150
- algorithm?: number,
151
- ): Uint8Array | null
152
-
153
- // ── Frame / patch / compression (variable-size → grow-retry) ─────
154
- /** RFC 6455 frame encode into a fresh buffer. */
155
- wsFrameEncode(opcode: number, payload: Uint8Array, mask: boolean, fin: boolean): Uint8Array
156
- /** RFC 6455 frame encode into `output`; returns bytes written (throws if too small). */
157
- wsFrameEncodeInto(
158
- opcode: number,
159
- payload: Uint8Array,
160
- mask: boolean,
161
- fin: boolean,
162
- output: Uint8Array,
163
- ): number
164
- /** RFC 6902 JSON patch into a fresh buffer. */
165
- jsonPatch(doc: Uint8Array, patch: Uint8Array): Uint8Array
166
- /** gzip-compress into a fresh buffer (level clamped 0..=9, default 6). */
167
- gzipCompress(data: Uint8Array, level?: number): Uint8Array
168
- /** gzip-compress into `output`; returns bytes written (throws if too small). */
169
- gzipCompressInto(data: Uint8Array, output: Uint8Array, level?: number): number
170
- /** gzip-decompress into a fresh buffer (capped by `maxDecompressed`). */
171
- gzipDecompress(data: Uint8Array, maxDecompressed?: number): Uint8Array
172
- /** brotli-compress into a fresh buffer (quality clamped 0..=11, default 5). */
173
- brotliCompress(data: Uint8Array, quality?: number): Uint8Array
174
- /** brotli-compress into `output`; returns bytes written (throws if too small). */
175
- brotliCompressInto(data: Uint8Array, output: Uint8Array, quality?: number): number
176
- /** brotli-decompress into a fresh buffer (capped by `maxDecompressed`). */
177
- brotliDecompress(data: Uint8Array, maxDecompressed?: number): Uint8Array
178
-
179
- // ── Packed parsers (into caller buffers) ─────────────────────────
180
- /** HTTP request parse → packed output into `output`; returns bytes written. */
181
- httpParseRequestPackedInto(input: Uint8Array, output: Uint8Array): number
182
- /** Query string parse → packed output into `output`; returns bytes written. */
183
- queryParsePackedInto(input: Uint8Array, output: Uint8Array): number
184
- /** Cookie header parse → packed output into `output`; returns bytes written. */
185
- cookieParsePackedInto(input: Uint8Array, output: Uint8Array): number
186
-
187
- // ── Excluded-surface additions (packed / opaque-handle) ─────────
188
- /** Sign JWT (HS256) from pre-serialized claim JSON (ttl<=0 → no iat/exp). */
189
- jwtSignBytes(claimsJson: Uint8Array, secret: Uint8Array, ttl: number, now: number): Uint8Array
190
- /** Decode a WS frame into packed `[flags][opcode][u32 len][payload]`; null on malformed. */
191
- wsFrameDecodePacked(data: Uint8Array): Uint8Array | null
192
- /** Parse multipart/form-data into the packed parts layout. */
193
- multipartParsePacked(body: Uint8Array, boundary: Uint8Array): Uint8Array
194
- /** Parse x-www-form-urlencoded into packed pairs into `output`. */
195
- formParsePackedInto(input: Uint8Array, output: Uint8Array): number
196
- /**
197
- * Run the ingress pipeline on a packed frame via the opaque inner handle from
198
- * `Ingress.ingressInnerPtr()` (valid only while the instance is alive — the
199
- * caller must hold it). Returns bytes written; throws on error / too-small.
200
- */
201
- ingressHandlePacked(
202
- inner: number,
203
- input: Uint8Array,
204
- body: Uint8Array | null,
205
- output: Uint8Array,
206
- ): number
207
- /**
208
- * Write the ingress binary-layout constants (38 × u32 LE — `rust/ffi.rs`
209
- * `IngressLayout`, numeric source `rust/ingress/output.rs`) into `output`;
210
- * returns bytes written. Lets `src/ingress/constants.ts` read the layout via
211
- * bun:ffi on Bun so importing the package does NOT dlopen the napi addon.
212
- */
213
- ingressLayout(out: Uint8Array): number
214
- }
215
-
216
- // ── Caps mirrored from the Rust napi layer ────────────────────────
217
- // rust/payload/compress.rs DEFAULT_MAX_DECOMPRESSED (64 MiB decompression bomb
218
- // guard — must stay in sync; do not raise without benchmarking).
219
- const MAX_DECOMPRESSED = 64 * 1024 * 1024
220
- // rust/json/json_patch_ops.rs MAX_JSON_PATCH_OUTPUT (128 MiB).
221
- const MAX_JSON_PATCH_OUTPUT = 128 * 1024 * 1024
222
-
223
- // ── Output-buffer sizing heuristics (measured) ─────────────────────
224
- // These are ALLOCATION caps only, not correctness bounds: `growExact` covers
225
- // any residual miss with at most one exact-size retry (no re-run loop), so a
226
- // guess that is too small costs one extra native pass, never a wrong result.
227
- //
228
- // Compress: a 75 KiB input compresses to <1 KiB, so a 16 KiB initial is
229
- // plenty and single-pass; the 1 MiB ceiling bounds incompressible data.
230
- const COMPRESS_INITIAL_CAP = 16 * 1024
231
- const COMPRESS_MAX_CAP = 1024 * 1024
232
- const COMPRESS_HEADROOM = 64 // small-input slack above `data.length`
233
- // Decompress (no trailer / expensive trailer): typical JSON/text ratios.
234
- const DECOMPRESS_GUESS_MULTIPLIER_GZIP = 16
235
- const DECOMPRESS_GUESS_MULTIPLIER_BROTLI = 32
236
- const DECOMPRESS_FALLBACK_CAP = 4 * 1024 * 1024 // 4 MiB over-alloc bound
237
- const DECOMPRESS_MIN_INITIAL = 1024
238
- // JWT: token ≈ header(~36) + payload(≈4/3× claims) + sig(~43) + 2 dots. The
239
- // old `claims.length + 128` under-sized a typical token (measured 168 B for a
240
- // 30 B claim → grow-retry double-run, making ffi slower than napi). 2×+128
241
- // with a 256-byte floor covers typical claims in one pass.
242
- const JWT_INITIAL_MULTIPLIER = 2
243
- const JWT_INITIAL_EXTRA = 128
244
- const JWT_INITIAL_FLOOR = 256
245
-
246
- // ── Known-good vectors for the bind-time self-test ───────────────
247
- // These mirror the Rust `#[cfg(test)]` vectors in rust/ffi.rs.
248
- const SELFTEST_HEX = encoder.encode('hello') // -> 68656c6c6f
249
- const SELFTEST_JSON = encoder.encode('{"a":1}')
250
-
251
- // Empty view for `null` body slots in (ptr, len) pairs — the C side treats
252
- // len 0 as "no body" regardless of the pointer value.
253
- const EMPTY_VIEW = new Uint8Array(0)
39
+ // Re-export the type surface so `import type { BunFFI } from '../native/ffi'`
40
+ // keeps working (existing call sites).
41
+ export type { BunFFI, FfiMode } from './ffi/types'
254
42
 
255
43
  let cached: BunFFI | null | undefined
44
+ /**
45
+ * Resolved buffer ABI mode, set once by `bind()`. `null` = ffi unavailable
46
+ * (Node / `CASTRUM_FFI_MODE=napi` / failed self-test), `'buffer-pair'` = the
47
+ * engine-native `buffer`/`buffer_length` pair is live, `'ptr-len'` = the
48
+ * explicit `(ptr, usize)` fallback.
49
+ */
50
+ let bufferAbiMode: 'buffer-pair' | 'ptr-len' | null = null
256
51
 
257
52
  // ── Transport selection (CASTRUM_FFI_MODE) ───────────────────────
258
- // auto (default): use bun:ffi on Bun, silently fall back to napi when the
259
- // bind or self-test fails (and always on Node).
260
- // ffi : force bun:ffi on Bun — throw a clear error if it can't bind or the
261
- // self-test fails (use in benches/CI that MUST run ffi).
262
- // napi : never bind — every call goes through the napi addon (the fallback;
263
- // useful for exercising the fallback path on Bun).
264
- // The legacy alias `RUST_FFI_MODE` is accepted too (see src/shared/env.ts).
265
- export type FfiMode = 'auto' | 'ffi' | 'napi'
266
53
 
267
54
  function resolveFfiMode(): FfiMode {
268
- const raw = resolveEnvVar('CASTRUM_FFI_MODE', ['RUST_FFI_MODE'])
55
+ const raw = resolveEnvVar('CASTRUM_FFI_MODE')
269
56
  switch (raw) {
270
57
  case 'ffi':
271
58
  return 'ffi'
@@ -292,6 +79,23 @@ export function getBunFFI(): BunFFI | null {
292
79
  return cached
293
80
  }
294
81
 
82
+ /**
83
+ * Which buffer ABI the live `bun:ffi` binding uses, or `null` when the
84
+ * transport is unavailable (Node, `CASTRUM_FFI_MODE=napi`, or a failed
85
+ * self-test). `'buffer-pair'` = the engine-native `buffer`/`buffer_length`
86
+ * pair (atomic ptr+byteLength snapshot) is in use; `'ptr-len'` = the explicit
87
+ * `(ptr, usize)` fallback (older Bun / probe failure). Lazy — triggers the
88
+ * same one-time bind as {@link getBunFFI}. Under `CASTRUM_FFI_MODE=ffi` on Bun
89
+ * this is guaranteed non-null; a bind/self-test failure THROWS, matching
90
+ * `getBunFFI`.
91
+ */
92
+ export function ffiBufferMode(): 'buffer-pair' | 'ptr-len' | null {
93
+ if (cached === undefined) {
94
+ getBunFFI()
95
+ }
96
+ return bufferAbiMode
97
+ }
98
+
295
99
  /**
296
100
  * Probe whether this Bun build accepts the `buffer`/`buffer_length` ABI pair in
297
101
  * `dlopen`. Bun's docs list `buffer_length` as engine-native (dlopen-supported),
@@ -324,6 +128,7 @@ function probeBufferLength(dlopen: typeof import('bun:ffi')['dlopen'], path: str
324
128
  }
325
129
 
326
130
  function bind(): BunFFI | null {
131
+ bufferAbiMode = null // reset — the mode below is only valid for a live bind
327
132
  const mode = resolveFfiMode()
328
133
  if (!isBun()) {
329
134
  if (mode === 'ffi') {
@@ -349,125 +154,319 @@ function bind(): BunFFI | null {
349
154
  // TypedArray at call time — an atomic snapshot) once; when supported we use
350
155
  // it for the input-only hot fns (one JS arg instead of two), else `(ptr,len)`.
351
156
  const useBufferLength = probeBufferLength(dlopen, path)
157
+ bufferAbiMode = useBufferLength ? 'buffer-pair' : 'ptr-len'
352
158
 
353
159
  // String ABI specs ("ptr"/"usize"/"u32"...): accepted by bun:ffi and typed
354
160
  // via `FFITypeOrString` in bun-types. usize == uint64 on the host ABI.
355
161
  // Symbol keys must be the exact `#[no_mangle] extern "C"` names in
356
- // rust/ffi.rs. Every `(ptr,len)` pair is passed as `(view, view.length)`.
162
+ // rust/ffi/. Every `(ptr,len)` pair is passed as `(view, view.length)`.
357
163
  // The 8 input-only single-buffer fns use `inputAbi`; everything else keeps
358
164
  // `(ptr,len)` because the output length is the caller's write cap.
359
165
  const inputAbi: readonly FFITypeOrString[] = useBufferLength
360
166
  ? (['buffer', 'buffer_length'] as unknown as readonly FFITypeOrString[])
361
167
  : ['ptr', 'usize']
168
+ // Phase 1.1: extend `buffer`/`buffer_length` to EVERY `(ptr,len)` pair, not
169
+ // just the input-only fns. The engine reads ptr + byteLength off the SAME
170
+ // view at call time (atomic snapshot), so call sites pass the view twice
171
+ // (see `lenOrView` below). Conversion is POSITIONAL and pair-aware: each
172
+ // `ptr` consumes its following `usize` as a length. Scalar args that happen
173
+ // to be `usize` (the opaque `inner` handle of `castrum_ingress_handle_packed`
174
+ // and the `max` param of gzip/brotli decompress) are passed through
175
+ // unchanged — a blind `usize`→`buffer_length` map would corrupt them.
176
+ const abi = (shape: readonly string[]): readonly FFITypeOrString[] => {
177
+ if (!useBufferLength) return shape as readonly FFITypeOrString[]
178
+ const out: FFITypeOrString[] = []
179
+ for (let i = 0; i < shape.length; i++) {
180
+ const t = shape[i]
181
+ if (t === 'ptr') {
182
+ out.push('buffer' as unknown as FFITypeOrString)
183
+ out.push('buffer_length' as unknown as FFITypeOrString)
184
+ i++ // consume the paired `usize`
185
+ } else {
186
+ out.push(t as FFITypeOrString)
187
+ }
188
+ }
189
+ return out
190
+ }
191
+ // `u64_fast` returns a plain `number` when the value fits (< 2^53) instead
192
+ // of a BigInt — bun-types lacks the literal, so bind it via a typed const
193
+ // (same cast pattern as the `buffer_length` probe above). Every bytes-written
194
+ // return fits in a double, so this removes the per-call BigInt boxing on the
195
+ // hot scalar / `*_into` / packed-parser / ingress calls.
196
+ const U64_FAST = 'u64_fast' as unknown as FFITypeOrString
362
197
  const { symbols } = dlopen(path, {
363
198
  castrum_crc32: { args: inputAbi, returns: 'u32' },
364
199
  castrum_fnv1a64: { args: inputAbi, returns: 'u64' },
365
200
  castrum_xxh3: { args: inputAbi, returns: 'u64' },
366
201
  castrum_json_valid: { args: inputAbi, returns: 'u8' },
367
- castrum_hex_encode: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
368
- castrum_hex_decode: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
369
- castrum_url_encode: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
370
- castrum_url_decode: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
371
- castrum_validate_email: { args: inputAbi, returns: 'u8' },
372
- castrum_validate_uuid: { args: inputAbi, returns: 'u8' },
373
- castrum_validate_ipv4: { args: inputAbi, returns: 'u8' },
374
- castrum_validate_ipv6: { args: inputAbi, returns: 'u8' },
375
- castrum_json_sum_ids: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
202
+ castrum_utf8_valid: { args: inputAbi, returns: 'u8' },
203
+ castrum_hex_encode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
204
+ castrum_hex_decode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
205
+ castrum_url_encode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
206
+ castrum_url_decode: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
207
+ // String-input validators: `cstring` ARG (the engine transcodes the JS
208
+ // string in-engine — no JS-side encode; Rust borrows via CStr::from_ptr).
209
+ castrum_validate_email: { args: ['cstring'], returns: 'u8' },
210
+ castrum_validate_uuid: { args: ['cstring'], returns: 'u8' },
211
+ castrum_validate_ipv4: { args: ['cstring'], returns: 'u8' },
212
+ castrum_validate_ipv6: { args: ['cstring'], returns: 'u8' },
213
+ castrum_json_sum_ids: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
376
214
  castrum_hmac_sha256_verify: {
377
- args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
215
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
216
+ returns: 'u8',
217
+ },
218
+ castrum_csrf_verify: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'u8' },
219
+ castrum_password_verify: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'u8' },
220
+ castrum_password_verify_bcrypt: {
221
+ // `phc` crosses as a `cstring` ARG; `password` stays a `(ptr,len)` pair.
222
+ args: abi(['ptr', 'usize', 'cstring']),
378
223
  returns: 'u8',
379
224
  },
380
- castrum_csrf_verify: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'u8' },
381
- castrum_password_verify: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'u8' },
382
- castrum_password_verify_bcrypt: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'u8' },
383
- castrum_ws_accept_key: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
384
- castrum_etag: { args: ['ptr', 'usize', 'ptr', 'usize', 'u8'], returns: 'usize' },
385
- castrum_random_token: { args: ['u32', 'ptr', 'usize'], returns: 'usize' },
225
+ castrum_ws_accept_key: { args: ['cstring'], returns: 'cstring' },
226
+ castrum_ws_accept_key_into: {
227
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
228
+ returns: U64_FAST,
229
+ },
230
+ castrum_etag: { args: abi(['ptr', 'usize', 'u8']), returns: 'cstring' },
231
+ castrum_etag_into: { args: abi(['ptr', 'usize', 'u8', 'ptr', 'usize']), returns: U64_FAST },
232
+ castrum_conditional_is_not_modified: {
233
+ args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize', 'u8']),
234
+ returns: 'u8',
235
+ },
236
+ castrum_media_type_matcher_matches: {
237
+ args: abi(['usize', 'ptr', 'usize']),
238
+ returns: 'u8',
239
+ },
240
+ castrum_accept_negotiator_negotiate: {
241
+ args: abi(['usize', 'ptr', 'usize']),
242
+ returns: 'cstring',
243
+ },
244
+ // Server-preference sibling (RFC 7231 server semantics): `header` is a
245
+ // `cstring` ARG — the engine transcodes the JS header string in-engine.
246
+ castrum_accept_negotiator_negotiate_server: {
247
+ args: ['usize', 'cstring'],
248
+ returns: 'cstring',
249
+ },
250
+ castrum_jwt_signer_sign: {
251
+ args: abi(['usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
252
+ returns: U64_FAST,
253
+ },
254
+ castrum_jwt_signer_verify: {
255
+ args: abi(['usize', 'ptr', 'usize', 'i64', 'ptr', 'usize']),
256
+ returns: U64_FAST,
257
+ },
258
+ castrum_template_render: {
259
+ args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
260
+ returns: U64_FAST,
261
+ },
262
+ castrum_schema_validator_validate: {
263
+ args: abi(['usize', 'ptr', 'usize']),
264
+ returns: 'u8',
265
+ },
266
+ castrum_rate_limiter_check: {
267
+ // `key` crosses as a `cstring` ARG (text, no NUL in the intended use).
268
+ args: abi(['usize', 'cstring', 'i64', 'ptr', 'usize']),
269
+ returns: U64_FAST,
270
+ },
271
+ castrum_rate_limiter_check_key: {
272
+ args: abi(['usize', 'i64', 'i64', 'ptr', 'usize']),
273
+ returns: U64_FAST,
274
+ },
275
+ castrum_random_token: { args: ['u32'], returns: 'cstring' },
276
+ castrum_random_token_into: { args: ['u32', ...abi(['ptr', 'usize'])], returns: U64_FAST },
386
277
  castrum_base64_encode: {
387
- args: ['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8'],
388
- returns: 'usize',
278
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8']),
279
+ returns: U64_FAST,
389
280
  },
390
281
  castrum_base64_decode: {
391
- args: ['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8'],
392
- returns: 'usize',
282
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'u8', 'u8']),
283
+ returns: U64_FAST,
393
284
  },
394
285
  castrum_hmac_sha256: {
395
- args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
396
- returns: 'usize',
286
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
287
+ returns: U64_FAST,
397
288
  },
398
- castrum_sign_cookie: {
399
- args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
400
- returns: 'usize',
289
+ castrum_sign_cookie: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'cstring' },
290
+ castrum_sign_cookie_into: {
291
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
292
+ returns: U64_FAST,
401
293
  },
402
- castrum_verify_cookie: {
403
- args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
404
- returns: 'usize',
294
+ castrum_verify_cookie: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: 'cstring' },
295
+ castrum_verify_cookie_into: {
296
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
297
+ returns: U64_FAST,
405
298
  },
406
- castrum_csrf_token: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
299
+ castrum_csrf_token: { args: abi(['ptr', 'usize']), returns: 'cstring' },
300
+ castrum_csrf_token_into: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
407
301
  castrum_password_hash: {
408
- args: ['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'u32', 'u32', 'ptr', 'usize'],
409
- returns: 'usize',
302
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'u32', 'u32', 'ptr', 'usize']),
303
+ returns: U64_FAST,
410
304
  },
411
305
  castrum_password_hash_bcrypt: {
412
- args: ['ptr', 'usize', 'u32', 'ptr', 'usize'],
413
- returns: 'usize',
306
+ args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
307
+ returns: U64_FAST,
414
308
  },
415
309
  castrum_pbkdf2_sha256: {
416
- args: ['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'ptr', 'usize'],
417
- returns: 'usize',
310
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'u32', 'u32', 'ptr', 'usize']),
311
+ returns: U64_FAST,
418
312
  },
419
313
  castrum_aead_encrypt: {
420
- args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize'],
421
- returns: 'usize',
314
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize']),
315
+ returns: U64_FAST,
422
316
  },
423
317
  castrum_aead_decrypt: {
424
- args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize'],
425
- returns: 'usize',
318
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'ptr', 'usize']),
319
+ returns: U64_FAST,
426
320
  },
427
321
  castrum_ws_frame_encode: {
428
- args: ['u8', 'ptr', 'usize', 'u8', 'u8', 'ptr', 'usize'],
429
- returns: 'usize',
322
+ args: abi(['u8', 'ptr', 'usize', 'u8', 'u8', 'ptr', 'usize']),
323
+ returns: U64_FAST,
430
324
  },
431
325
  castrum_json_patch: {
432
- args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
433
- returns: 'usize',
326
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
327
+ returns: U64_FAST,
328
+ },
329
+ castrum_gzip_compress: {
330
+ args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
331
+ returns: U64_FAST,
434
332
  },
435
- castrum_gzip_compress: { args: ['ptr', 'usize', 'u32', 'ptr', 'usize'], returns: 'usize' },
436
333
  castrum_gzip_decompress: {
437
- args: ['ptr', 'usize', 'usize', 'ptr', 'usize'],
438
- returns: 'usize',
334
+ args: abi(['ptr', 'usize', 'usize', 'ptr', 'usize']),
335
+ returns: U64_FAST,
336
+ },
337
+ castrum_brotli_compress: {
338
+ args: abi(['ptr', 'usize', 'u32', 'ptr', 'usize']),
339
+ returns: U64_FAST,
439
340
  },
440
- castrum_brotli_compress: { args: ['ptr', 'usize', 'u32', 'ptr', 'usize'], returns: 'usize' },
441
341
  castrum_brotli_decompress: {
442
- args: ['ptr', 'usize', 'usize', 'ptr', 'usize'],
443
- returns: 'usize',
342
+ args: abi(['ptr', 'usize', 'usize', 'ptr', 'usize']),
343
+ returns: U64_FAST,
444
344
  },
445
- castrum_gzip_isize: { args: ['ptr', 'usize'], returns: 'u32' },
345
+ castrum_gzip_isize: { args: abi(['ptr', 'usize']), returns: 'u32' },
446
346
  castrum_http_parse_request_packed: {
447
- args: ['ptr', 'usize', 'ptr', 'usize'],
448
- returns: 'usize',
347
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
348
+ returns: U64_FAST,
349
+ },
350
+ castrum_query_parse_packed: {
351
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
352
+ returns: U64_FAST,
353
+ },
354
+ castrum_cookie_parse_packed: {
355
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
356
+ returns: U64_FAST,
357
+ },
358
+ castrum_http_date_into: { args: ['f64', ...abi(['ptr', 'usize'])], returns: U64_FAST },
359
+ castrum_sse_encode_into: {
360
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize', 'u8', 'u32', 'ptr', 'usize']),
361
+ returns: U64_FAST,
449
362
  },
450
- castrum_query_parse_packed: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
451
- castrum_cookie_parse_packed: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
452
363
  // ── Excluded-surface additions ──
453
364
  castrum_jwt_sign_bytes: {
454
- args: ['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64', 'ptr', 'usize'],
455
- returns: 'usize',
365
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64']),
366
+ returns: 'cstring',
367
+ },
368
+ castrum_jwt_sign_bytes_into: {
369
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64', 'ptr', 'usize']),
370
+ returns: U64_FAST,
371
+ },
372
+ // ── Ed25519 / EdDSA JWT (RBAC auth) — bound + self-tested; the public
373
+ // surface is deferred to the auth-module consumer ────────────────
374
+ castrum_ed25519_generate_keypair: {
375
+ args: abi(['ptr', 'usize']),
376
+ returns: U64_FAST,
377
+ },
378
+ castrum_ed25519_sign: {
379
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
380
+ returns: U64_FAST,
381
+ },
382
+ castrum_ed25519_verify: {
383
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
384
+ returns: 'u32',
385
+ },
386
+ castrum_jwt_eddsa_sign: {
387
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64', 'i64']),
388
+ returns: 'cstring',
389
+ },
390
+ castrum_jwt_eddsa_verify: {
391
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64']),
392
+ returns: 'cstring',
456
393
  },
457
394
  castrum_ws_frame_decode_packed: {
458
- args: ['ptr', 'usize', 'ptr', 'usize'],
459
- returns: 'usize',
395
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
396
+ returns: U64_FAST,
460
397
  },
461
398
  castrum_multipart_parse_packed: {
462
- args: ['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
463
- returns: 'usize',
399
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
400
+ returns: U64_FAST,
401
+ },
402
+ castrum_form_parse_packed: { args: abi(['ptr', 'usize', 'ptr', 'usize']), returns: U64_FAST },
403
+ // ── Phase 3 — stateless napi-only scalars (now FFI) ──────────
404
+ // Structural parse: parses ONCE, emits a packed token stream with a
405
+ // deduped string table so JS assembles the value with NO second parse.
406
+ castrum_json_parse_packed: {
407
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
408
+ returns: U64_FAST,
409
+ },
410
+ castrum_parse_media_type: {
411
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
412
+ returns: U64_FAST,
413
+ },
414
+ castrum_parse_http_date: {
415
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
416
+ returns: U64_FAST,
417
+ },
418
+ castrum_parse_accept_encoding: {
419
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
420
+ returns: U64_FAST,
421
+ },
422
+ castrum_url_encode_query: { args: abi(['ptr', 'usize']), returns: 'cstring' },
423
+ castrum_url_resolve: {
424
+ args: abi(['ptr', 'usize', 'ptr', 'usize']),
425
+ returns: 'cstring',
426
+ },
427
+ castrum_url_builder_resolve: {
428
+ args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
429
+ returns: U64_FAST,
430
+ },
431
+ castrum_mime_from_extension: { args: ['cstring'], returns: 'cstring' },
432
+ castrum_jwt_verify: {
433
+ args: abi(['ptr', 'usize', 'ptr', 'usize', 'i64']),
434
+ returns: 'cstring',
464
435
  },
465
- castrum_form_parse_packed: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
466
436
  castrum_ingress_handle_packed: {
467
- args: ['usize', 'ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize'],
468
- returns: 'usize',
437
+ args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize', 'ptr', 'usize']),
438
+ returns: U64_FAST,
439
+ },
440
+ // Raw-components sibling of `castrum_ingress_handle_packed`: `url`/`ip`
441
+ // are `cstring` ARGs (Bun transcodes the JS strings to call-scoped
442
+ // NUL-terminated buffers in-engine — no JS-side Buffer.write encode);
443
+ // rid/headers/body are `(ptr,len)` byte slices; out is `(ptr,len)`.
444
+ castrum_ingress_handle_components: {
445
+ args: abi([
446
+ 'usize',
447
+ 'u8',
448
+ 'cstring',
449
+ 'cstring',
450
+ 'ptr',
451
+ 'usize',
452
+ 'ptr',
453
+ 'usize',
454
+ 'ptr',
455
+ 'usize',
456
+ 'ptr',
457
+ 'usize',
458
+ ]),
459
+ returns: U64_FAST,
460
+ },
461
+ castrum_ingress_layout: { args: abi(['ptr', 'usize']), returns: U64_FAST },
462
+ // Per-route native stack (`@ignex/native` also dlopens these exact names
463
+ // itself; castrum binds them for the bind-time self-test + parity guard).
464
+ castrum_route_compile: { args: abi(['ptr', 'usize']), returns: U64_FAST },
465
+ castrum_route_run: {
466
+ args: abi(['usize', 'ptr', 'usize', 'ptr', 'usize']),
467
+ returns: U64_FAST,
469
468
  },
470
- castrum_ingress_layout: { args: ['ptr', 'usize'], returns: 'usize' },
469
+ castrum_route_destroy: { args: ['usize'], returns: 'void' },
471
470
  })
472
471
 
473
472
  const bindings = build(symbols as Record<string, (...a: unknown[]) => unknown>, useBufferLength)
@@ -490,1052 +489,3 @@ function bind(): BunFFI | null {
490
489
  return null
491
490
  }
492
491
  }
493
-
494
- // Types for the raw dlopen'd functions (bun:ffi converts Uint8Array -> ptr for
495
- // `ptr` args; `usize`/`u64`/`i64` returns surface as BigInt).
496
- type Raw2 = (a: unknown, b: unknown) => number | bigint
497
- type Raw3 = (a: unknown, b: unknown, c: unknown) => number | bigint
498
- type Raw4 = (a: unknown, b: unknown, c: unknown, d: unknown) => number | bigint
499
- type Raw5 = (a: unknown, b: unknown, c: unknown, d: unknown, e: unknown) => number | bigint
500
- type Raw6 = (
501
- a: unknown,
502
- b: unknown,
503
- c: unknown,
504
- d: unknown,
505
- e: unknown,
506
- f: unknown,
507
- ) => number | bigint
508
- type Raw7 = (
509
- a: unknown,
510
- b: unknown,
511
- c: unknown,
512
- d: unknown,
513
- e: unknown,
514
- f: unknown,
515
- g: unknown,
516
- ) => number | bigint
517
- type Raw8 = (
518
- a: unknown,
519
- b: unknown,
520
- c: unknown,
521
- d: unknown,
522
- e: unknown,
523
- f: unknown,
524
- g: unknown,
525
- h: unknown,
526
- ) => number | bigint
527
- type Raw9 = (
528
- a: unknown,
529
- b: unknown,
530
- c: unknown,
531
- d: unknown,
532
- e: unknown,
533
- f: unknown,
534
- g: unknown,
535
- h: unknown,
536
- i: unknown,
537
- ) => number | bigint
538
- type Raw10 = (
539
- a: unknown,
540
- b: unknown,
541
- c: unknown,
542
- d: unknown,
543
- e: unknown,
544
- f: unknown,
545
- g: unknown,
546
- h: unknown,
547
- i: unknown,
548
- j: unknown,
549
- ) => number | bigint
550
-
551
- const flag = (v?: boolean): number => (v ? 1 : 0)
552
-
553
- /**
554
- * Write with the C ABI's "needed" convention. The C side returns the EXACT
555
- * total bytes the result requires: `0` = real error (throw immediately — no
556
- * re-run, no grow-to-max allocation storm on invalid input), `w > out.length`
557
- * = buffer too small with `w` the exact required size (allocate once, retry),
558
- * else `w` is the written count. A miss therefore costs at most ONE retry with
559
- * an exact buffer — never a doubling loop that re-runs the whole native op
560
- * (which is what made FFI gzip/brotli/jsonPatch slower than napi).
561
- */
562
- function growExact(
563
- write: (out: Uint8Array) => number,
564
- initial: number,
565
- max: number,
566
- error: string,
567
- ): Uint8Array {
568
- let cap = Math.min(Math.max(initial, 16), max)
569
- for (;;) {
570
- const out = new Uint8Array(cap)
571
- const w = Number(write(out))
572
- if (w === 0) throw new Error(error)
573
- if (w <= out.length) return out.subarray(0, w)
574
- if (w > max) throw new Error(error)
575
- cap = Math.min(w, max)
576
- }
577
- }
578
-
579
- /** Decoder parity: a `0` write on non-empty input throws (napi `Result`). */
580
- function writeOrThrow(w: number, inputLen: number, label: string): number {
581
- if (w === 0 && inputLen !== 0) {
582
- throw new Error(`${label}: invalid input or output buffer too small`)
583
- }
584
- return w
585
- }
586
-
587
- /** base64 (no-pad) length of `n` bytes. */
588
- const b64Len = (n: number): number => (n === 0 ? 0 : Math.ceil(n / 3) * 4 - ((3 - (n % 3)) % 3))
589
-
590
- /**
591
- * Exact length of an argon2id PHC string for the given params (m/t/p/out_len).
592
- * Format: `$argon2id$v=19$m=<m>,t=<t>,p=<p>$<22-char salt b64>$<hash b64>`.
593
- * The salt is always 16 bytes → 22 base64 chars (SaltString::encode_b64).
594
- * Pre-sizing exactly makes `passwordHash` a single native pass instead of a
595
- * grow-retry (which would re-run the whole ~tens-of-ms hash on a miss).
596
- */
597
- function argon2PhcLength(m: number, t: number, p: number, outLen: number): number {
598
- return (
599
- 15 + // "$argon2id$v=19$"
600
- 2 + // "m="
601
- String(m).length +
602
- 3 + // ",t="
603
- String(t).length +
604
- 3 + // ",p="
605
- String(p).length +
606
- 1 + // "$" before the salt
607
- 22 + // salt b64 (16 bytes)
608
- 1 + // "$" before the hash
609
- b64Len(outLen)
610
- )
611
- }
612
-
613
- function build(
614
- sym: Record<string, (...a: unknown[]) => unknown>,
615
- useBufferLength: boolean,
616
- ): BunFFI {
617
- const crc32 = sym.castrum_crc32 as (...a: unknown[]) => number | bigint
618
- const fnv = sym.castrum_fnv1a64 as (...a: unknown[]) => number | bigint
619
- const xxh = sym.castrum_xxh3 as (...a: unknown[]) => number | bigint
620
- const jsonValid = sym.castrum_json_valid as (...a: unknown[]) => number | bigint
621
- const hexEncode = sym.castrum_hex_encode as Raw4
622
- const hexDecode = sym.castrum_hex_decode as Raw4
623
- const urlEncode = sym.castrum_url_encode as Raw4
624
- const urlDecode = sym.castrum_url_decode as Raw4
625
- const validateEmail = sym.castrum_validate_email as (...a: unknown[]) => number | bigint
626
- const validateUuid = sym.castrum_validate_uuid as (...a: unknown[]) => number | bigint
627
- const validateIpv4 = sym.castrum_validate_ipv4 as (...a: unknown[]) => number | bigint
628
- const validateIpv6 = sym.castrum_validate_ipv6 as (...a: unknown[]) => number | bigint
629
- const jsonSumRaw = sym.castrum_json_sum_ids as Raw4
630
-
631
- // Single-buffer input fns: with the probe-gated `buffer/buffer_length` ABI the
632
- // engine reads ptr + byteLength off the SAME TypedArray at call time (an
633
- // atomic snapshot — pass the view twice, per Bun's docs); with `(ptr,len)` we
634
- // pass the length explicitly. The branch is on a bind-time constant, so the
635
- // JIT folds it away at the call site.
636
- const oneArg = (raw: (...a: unknown[]) => number | bigint, v: Uint8Array): number | bigint =>
637
- useBufferLength ? raw(v, v) : raw(v, v.length)
638
- const hmacVerify = sym.castrum_hmac_sha256_verify as Raw6
639
- const csrfVerify = sym.castrum_csrf_verify as Raw4
640
- const passwordVerify = sym.castrum_password_verify as Raw4
641
- const passwordVerifyBcrypt = sym.castrum_password_verify_bcrypt as Raw4
642
- const wsAcceptKey = sym.castrum_ws_accept_key as Raw4
643
- const etag = sym.castrum_etag as Raw5
644
- const randomToken = sym.castrum_random_token as Raw3
645
- const base64Encode = sym.castrum_base64_encode as Raw6
646
- const base64Decode = sym.castrum_base64_decode as Raw6
647
- const hmacSha256 = sym.castrum_hmac_sha256 as Raw6
648
- const signCookie = sym.castrum_sign_cookie as Raw6
649
- const verifyCookie = sym.castrum_verify_cookie as Raw6
650
- const csrfToken = sym.castrum_csrf_token as Raw4
651
- const passwordHash = sym.castrum_password_hash as Raw10
652
- const passwordHashBcrypt = sym.castrum_password_hash_bcrypt as Raw5
653
- const pbkdf2 = sym.castrum_pbkdf2_sha256 as Raw8
654
- const aeadEncrypt = sym.castrum_aead_encrypt as Raw9
655
- const aeadDecrypt = sym.castrum_aead_decrypt as Raw9
656
- const wsFrameEncode = sym.castrum_ws_frame_encode as Raw7
657
- const jsonPatch = sym.castrum_json_patch as Raw6
658
- const gzipCompress = sym.castrum_gzip_compress as Raw5
659
- const gzipDecompress = sym.castrum_gzip_decompress as Raw5
660
- const brotliCompress = sym.castrum_brotli_compress as Raw5
661
- const brotliDecompress = sym.castrum_brotli_decompress as Raw5
662
- const gzipIsize = sym.castrum_gzip_isize as Raw2
663
- const httpParsePacked = sym.castrum_http_parse_request_packed as Raw4
664
- const queryParsePacked = sym.castrum_query_parse_packed as Raw4
665
- const cookieParsePacked = sym.castrum_cookie_parse_packed as Raw4
666
- const jwtSignBytes = sym.castrum_jwt_sign_bytes as Raw8
667
- const wsFrameDecodePacked = sym.castrum_ws_frame_decode_packed as Raw4
668
- const multipartParsePacked = sym.castrum_multipart_parse_packed as Raw6
669
- const formParsePacked = sym.castrum_form_parse_packed as Raw4
670
- const ingressHandlePacked = sym.castrum_ingress_handle_packed as Raw7
671
- const ingressLayoutSym = sym.castrum_ingress_layout as Raw2
672
-
673
- // ── Encoder / decoder `Into` helpers ──────────────────────────────
674
- const hexEncodeInto = (input: Uint8Array, output: Uint8Array): number => {
675
- // Mirror napi error semantics: a too-small buffer throws, not returns 0.
676
- // Hex encode always writes exactly `input.length * 2` bytes on success.
677
- if (output.length < input.length * 2) {
678
- throw new Error('hex encode: output buffer too small')
679
- }
680
- return Number(hexEncode(input, input.length, output, output.length))
681
- }
682
-
683
- const urlEncodeInto = (input: Uint8Array, output: Uint8Array): number => {
684
- const w = Number(urlEncode(input, input.length, output, output.length))
685
- // Every input byte encodes to >= 1 output byte, so a 0 write on non-empty
686
- // input means the buffer was too small (napi throws there too). Empty
687
- // input legitimately writes 0.
688
- if (w === 0 && input.length !== 0) {
689
- throw new Error('url encode: output buffer too small')
690
- }
691
- return w
692
- }
693
-
694
- const hexDecodeInto = (input: Uint8Array, output: Uint8Array): number => {
695
- const w = Number(hexDecode(input, input.length, output, output.length))
696
- return writeOrThrow(w, input.length, 'hex decode')
697
- }
698
-
699
- const urlDecodeInto = (input: Uint8Array, output: Uint8Array): number => {
700
- const w = Number(urlDecode(input, input.length, output, output.length))
701
- return writeOrThrow(w, input.length, 'url decode')
702
- }
703
-
704
- const base64DecodeInto = (
705
- input: Uint8Array,
706
- output: Uint8Array,
707
- urlSafe?: boolean,
708
- padding?: boolean,
709
- ): number => {
710
- // napi defaults: urlSafe=false, padding=true (rust/crypto/base64.rs).
711
- const w = Number(
712
- base64Decode(
713
- input,
714
- input.length,
715
- output,
716
- output.length,
717
- flag(urlSafe),
718
- flag(padding ?? true),
719
- ),
720
- )
721
- return writeOrThrow(w, input.length, 'base64 decode')
722
- }
723
-
724
- const base64EncodeInto = (
725
- input: Uint8Array,
726
- output: Uint8Array,
727
- urlSafe?: boolean,
728
- padding?: boolean,
729
- ): number => {
730
- // napi defaults: urlSafe=false, padding=true.
731
- const w = Number(
732
- base64Encode(
733
- input,
734
- input.length,
735
- output,
736
- output.length,
737
- flag(urlSafe),
738
- flag(padding ?? true),
739
- ),
740
- )
741
- // Empty input legitimately writes 0 bytes — only a 0 write on NON-empty
742
- // input is a real error (same convention as the decode/etag paths).
743
- return writeOrThrow(w, input.length, 'base64 encode')
744
- }
745
-
746
- const etagInto = (data: Uint8Array, output: Uint8Array, weak?: boolean): number => {
747
- const w = Number(etag(data, data.length, output, output.length, flag(weak)))
748
- if (w === 0 && data.length !== 0) {
749
- throw new Error('etag: output buffer too small')
750
- }
751
- return w
752
- }
753
-
754
- return {
755
- crc32: (input) => Number(oneArg(crc32, input)) >>> 0,
756
- fnv1a64: (input) => BigInt(oneArg(fnv, input)),
757
- xxh3: (input) => BigInt(oneArg(xxh, input)),
758
- jsonValid: (input) => Number(oneArg(jsonValid, input)) === 1,
759
- hexEncode(input) {
760
- const out = new Uint8Array(input.length * 2)
761
- const w = hexEncodeInto(input, out)
762
- return w === input.length * 2 ? out : out.subarray(0, w)
763
- },
764
- hexEncodeInto,
765
- urlEncode(input) {
766
- // RFC 3986 worst case is 3 bytes per input byte (`%XX`).
767
- const out = new Uint8Array(input.length * 3)
768
- const w = urlEncodeInto(input, out)
769
- return out.subarray(0, w)
770
- },
771
- urlEncodeInto,
772
-
773
- validateEmail: (input) => Number(oneArg(validateEmail, input)) === 1,
774
- validateUuid: (input) => Number(oneArg(validateUuid, input)) === 1,
775
- validateIpv4: (input) => Number(oneArg(validateIpv4, input)) === 1,
776
- validateIpv6: (input) => Number(oneArg(validateIpv6, input)) === 1,
777
- jsonSumIds: (input) => {
778
- // Packed [u8 ok][i64 sum LE] output (9 B): ok=1 → valid array (the sum
779
- // may be 0); ok=0 → invalid input. Bytes written: 9/1/0 (0 = real error).
780
- const out = new Uint8Array(9)
781
- const w = Number(jsonSumRaw(input, input.length, out, out.length))
782
- if (w === 0) {
783
- throw new Error('json sum ids: output buffer too small')
784
- }
785
- if (out[0] === 0) {
786
- // Mirrors the napi error phrasing (serde: "expected an array of objects
787
- // with numeric ids") so both transports throw the same message.
788
- throw new Error('json sum ids: expected an array of objects with numeric ids')
789
- }
790
- return new DataView(out.buffer, out.byteOffset, out.byteLength).getBigInt64(1, true)
791
- },
792
- hmacSha256Verify: (key, data, signature) =>
793
- Number(hmacVerify(key, key.length, data, data.length, signature, signature.length)) === 1,
794
- csrfVerify: (token, secret) =>
795
- Number(csrfVerify(token, token.length, secret, secret.length)) === 1,
796
- passwordVerify: (password, phc) =>
797
- Number(passwordVerify(password, password.length, phc, phc.length)) === 1,
798
- passwordVerifyBcrypt: (password, phc) =>
799
- Number(passwordVerifyBcrypt(password, password.length, phc, phc.length)) === 1,
800
-
801
- hexDecode(input) {
802
- const out = new Uint8Array(Math.floor(input.length / 2))
803
- const w = hexDecodeInto(input, out)
804
- return out.subarray(0, w)
805
- },
806
- hexDecodeInto,
807
- urlDecode(input) {
808
- const out = new Uint8Array(input.length)
809
- const w = urlDecodeInto(input, out)
810
- return out.subarray(0, w)
811
- },
812
- urlDecodeInto,
813
- base64Decode(input, urlSafe, padding) {
814
- const out = new Uint8Array(Math.ceil((input.length * 3) / 4))
815
- const w = base64DecodeInto(input, out, urlSafe, padding)
816
- return out.subarray(0, w)
817
- },
818
- base64DecodeInto,
819
-
820
- wsAcceptKey(key) {
821
- const out = new Uint8Array(28)
822
- const w = Number(wsAcceptKey(key, key.length, out, out.length))
823
- if (w === 0) {
824
- throw new Error('ws accept key: bad key or output buffer too small')
825
- }
826
- return out.subarray(0, w)
827
- },
828
- etag(data, weak) {
829
- const out = new Uint8Array(12)
830
- const w = etagInto(data, out, weak)
831
- return out.subarray(0, w)
832
- },
833
- etagInto,
834
- randomToken(byteLen) {
835
- const out = new Uint8Array(byteLen * 2)
836
- const w = Number(randomToken(byteLen, out, out.length))
837
- // byteLen 0 legitimately yields an empty token (napi returns empty too);
838
- // any other 0 is a real error (buffer too small / random source failed).
839
- if (w === 0 && byteLen !== 0) {
840
- throw new Error('random token: output buffer too small or random source failed')
841
- }
842
- return out.subarray(0, w)
843
- },
844
- base64Encode(input, urlSafe, padding) {
845
- const out = new Uint8Array(Math.ceil(input.length / 3) * 4)
846
- const w = base64EncodeInto(input, out, urlSafe, padding)
847
- return out.subarray(0, w)
848
- },
849
- base64EncodeInto,
850
- hmacSha256(key, data) {
851
- const out = new Uint8Array(64)
852
- const w = Number(hmacSha256(key, key.length, data, data.length, out, out.length))
853
- if (w === 0) {
854
- throw new Error('hmac sha256: output buffer too small')
855
- }
856
- return out.subarray(0, w)
857
- },
858
- hmacSha256Into(key, data, output) {
859
- if (output.length < 64) {
860
- throw new Error('hmac sha256: output buffer too small')
861
- }
862
- const w = Number(hmacSha256(key, key.length, data, data.length, output, output.length))
863
- if (w === 0) {
864
- throw new Error('hmac sha256: output buffer too small')
865
- }
866
- return w
867
- },
868
- signCookie(value, secret) {
869
- // `value.<64-hex>` → value.length + 1 + 64.
870
- const out = new Uint8Array(value.length + 65)
871
- const w = Number(signCookie(value, value.length, secret, secret.length, out, out.length))
872
- if (w === 0) {
873
- throw new Error('sign cookie: output buffer too small')
874
- }
875
- return out.subarray(0, w)
876
- },
877
- signCookieInto(value, secret, output) {
878
- const need = value.length + 65
879
- if (output.length < need) {
880
- throw new Error('sign cookie: output buffer too small')
881
- }
882
- const w = Number(
883
- signCookie(value, value.length, secret, secret.length, output, output.length),
884
- )
885
- if (w === 0) {
886
- throw new Error('sign cookie: output buffer too small')
887
- }
888
- return w
889
- },
890
- verifyCookie(signed, secret) {
891
- const out = new Uint8Array(signed.length)
892
- const w = Number(verifyCookie(signed, signed.length, secret, secret.length, out, out.length))
893
- return w === 0 ? null : out.subarray(0, w)
894
- },
895
- csrfToken(secret) {
896
- const out = new Uint8Array(129)
897
- const w = Number(csrfToken(secret, secret.length, out, out.length))
898
- if (w === 0) {
899
- throw new Error('csrf token: output buffer too small or random source failed')
900
- }
901
- return out.subarray(0, w)
902
- },
903
- passwordHash(password, salt, mCost, tCost, pCost, outLen) {
904
- // Pre-size with the EXACT PHC string length (computable from the params),
905
- // so the hash runs once — a grow-retry would re-run the whole argon2
906
- // hash on a miss. growExact remains the safety net.
907
- return growExact(
908
- (out) =>
909
- Number(
910
- passwordHash(
911
- password,
912
- password.length,
913
- salt,
914
- salt.length,
915
- mCost,
916
- tCost,
917
- pCost,
918
- outLen,
919
- out,
920
- out.length,
921
- ),
922
- ),
923
- argon2PhcLength(mCost, tCost, pCost, outLen),
924
- 2 * 1024 * 1024,
925
- 'password hash: output buffer too small',
926
- )
927
- },
928
- passwordHashBcrypt(password, cost) {
929
- // `$2b$CC$` + 22 salt chars + 31 hash chars = 60 chars.
930
- const out = new Uint8Array(64)
931
- const w = Number(passwordHashBcrypt(password, password.length, cost, out, out.length))
932
- if (w === 0) {
933
- throw new Error('password hash bcrypt: output buffer too small')
934
- }
935
- return out.subarray(0, w)
936
- },
937
- pbkdf2Sha256(password, salt, rounds, dkLen) {
938
- // Rust clamps dkLen to [1, 1MiB] (PBKDF2_MIN_LEN/MAX_LEN) AFTER sizing its
939
- // own buffer — so pre-clamp here so dkLen 0 still yields a 1-byte result.
940
- const dk = Math.min(Math.max(dkLen, 1), 1024 * 1024)
941
- const out = new Uint8Array(dk)
942
- const w = Number(
943
- pbkdf2(password, password.length, salt, salt.length, rounds, dkLen, out, out.length),
944
- )
945
- if (w === 0) {
946
- throw new Error('pbkdf2: output buffer too small')
947
- }
948
- return out.subarray(0, w)
949
- },
950
- aeadEncrypt(key, nonce, plaintext, algorithm = 0) {
951
- // ciphertext + 16-byte auth tag.
952
- const out = new Uint8Array(plaintext.length + 16)
953
- const w = Number(
954
- aeadEncrypt(
955
- key,
956
- key.length,
957
- nonce,
958
- nonce.length,
959
- plaintext,
960
- plaintext.length,
961
- algorithm,
962
- out,
963
- out.length,
964
- ),
965
- )
966
- if (w === 0) {
967
- throw new Error('aead encrypt: output buffer too small or bad parameters')
968
- }
969
- return out.subarray(0, w)
970
- },
971
- aeadEncryptInto(key, nonce, plaintext, output, algorithm = 0) {
972
- const need = plaintext.length + 16
973
- if (output.length < need) {
974
- throw new Error('aead encrypt: output buffer too small')
975
- }
976
- const w = Number(
977
- aeadEncrypt(
978
- key,
979
- key.length,
980
- nonce,
981
- nonce.length,
982
- plaintext,
983
- plaintext.length,
984
- algorithm,
985
- output,
986
- output.length,
987
- ),
988
- )
989
- if (w === 0) {
990
- throw new Error('aead encrypt: output buffer too small or bad parameters')
991
- }
992
- return w
993
- },
994
- aeadDecrypt(key, nonce, ciphertext, algorithm = 0) {
995
- const out = new Uint8Array(ciphertext.length)
996
- const w = Number(
997
- aeadDecrypt(
998
- key,
999
- key.length,
1000
- nonce,
1001
- nonce.length,
1002
- ciphertext,
1003
- ciphertext.length,
1004
- algorithm,
1005
- out,
1006
- out.length,
1007
- ),
1008
- )
1009
- return w === 0 ? null : out.subarray(0, w)
1010
- },
1011
-
1012
- wsFrameEncode(opcode, payload, mask, fin) {
1013
- // Header (max 10) + payload + mask key (4).
1014
- const out = new Uint8Array(payload.length + 14)
1015
- const w = Number(
1016
- wsFrameEncode(opcode, payload, payload.length, flag(mask), flag(fin), out, out.length),
1017
- )
1018
- if (w === 0) {
1019
- throw new Error('ws frame encode: output buffer too small')
1020
- }
1021
- return out.subarray(0, w)
1022
- },
1023
- wsFrameEncodeInto(opcode, payload, mask, fin, output) {
1024
- // Header (max 10) + payload + mask key (4).
1025
- const need = payload.length + 14
1026
- if (output.length < need) {
1027
- throw new Error('ws frame encode: output buffer too small')
1028
- }
1029
- const w = Number(
1030
- wsFrameEncode(
1031
- opcode,
1032
- payload,
1033
- payload.length,
1034
- flag(mask),
1035
- flag(fin),
1036
- output,
1037
- output.length,
1038
- ),
1039
- )
1040
- if (w === 0) {
1041
- throw new Error('ws frame encode: output buffer too small')
1042
- }
1043
- return w
1044
- },
1045
- jsonPatch(doc, patch) {
1046
- return growExact(
1047
- (out) => Number(jsonPatch(doc, doc.length, patch, patch.length, out, out.length)),
1048
- Math.min(Math.max(doc.length, patch.length) + 16, 64 * 1024),
1049
- MAX_JSON_PATCH_OUTPUT,
1050
- 'json patch: output buffer too small or patch inapplicable',
1051
- )
1052
- },
1053
- gzipCompress(data, level = 6) {
1054
- // The C ABI streams the compressed output directly into `out` (no internal
1055
- // Vec — see rust/payload/compress.rs gzip_compress_into). Cap the initial
1056
- // so large compressible inputs don't pay a full-size allocation (measured:
1057
- // a 75 KiB input compressed to <1 KiB — COMPRESS_INITIAL_CAP is plenty and
1058
- // single-pass). growExact handles incompressible data with at most one
1059
- // exact retry (no re-run loop).
1060
- return growExact(
1061
- (out) => Number(gzipCompress(data, data.length, Math.min(level, 9), out, out.length)),
1062
- Math.min(data.length + COMPRESS_HEADROOM, COMPRESS_INITIAL_CAP),
1063
- Math.max(data.length * 2 + COMPRESS_HEADROOM, COMPRESS_MAX_CAP),
1064
- 'gzip compress: output buffer too small',
1065
- )
1066
- },
1067
- gzipCompressInto(data, output, level = 6) {
1068
- const w = Number(gzipCompress(data, data.length, Math.min(level, 9), output, output.length))
1069
- // Needed-size convention: w > output.length = too small; w === 0 = real error.
1070
- if (w === 0) {
1071
- throw new Error('gzip compress: invalid input')
1072
- }
1073
- if (w > output.length) {
1074
- throw new Error('gzip compress: output buffer too small')
1075
- }
1076
- return w
1077
- },
1078
- gzipDecompress(data, maxDecompressed) {
1079
- const max = maxDecompressed ?? MAX_DECOMPRESSED
1080
- // Pre-size from the gzip ISIZE trailer via the C probe (exact for
1081
- // single-member streams) so the happy path is a single native pass; fall
1082
- // back to a multiplier guess. growExact handles any residual miss with
1083
- // one exact retry, and invalid input throws immediately (no 64 MiB
1084
- // grow-to-max allocation storm).
1085
- const isize = Number(gzipIsize(data, data.length))
1086
- const initial =
1087
- isize !== 0
1088
- ? Math.min(isize, max)
1089
- : Math.min(data.length * DECOMPRESS_GUESS_MULTIPLIER_GZIP, DECOMPRESS_FALLBACK_CAP)
1090
- return growExact(
1091
- (out) => Number(gzipDecompress(data, data.length, max, out, out.length)),
1092
- initial,
1093
- max,
1094
- 'gzip decompress: invalid stream or exceeded max decompressed size',
1095
- )
1096
- },
1097
- brotliCompress(data, quality = 5) {
1098
- // Same cap rationale as gzipCompress (streaming core); growExact for
1099
- // incompressible data.
1100
- return growExact(
1101
- (out) => Number(brotliCompress(data, data.length, Math.min(quality, 11), out, out.length)),
1102
- Math.min(data.length + COMPRESS_HEADROOM, COMPRESS_INITIAL_CAP),
1103
- Math.max(data.length * 2 + COMPRESS_HEADROOM, COMPRESS_MAX_CAP),
1104
- 'brotli compress: output buffer too small',
1105
- )
1106
- },
1107
- brotliCompressInto(data, output, quality = 5) {
1108
- const w = Number(
1109
- brotliCompress(data, data.length, Math.min(quality, 11), output, output.length),
1110
- )
1111
- // Needed-size convention: w > output.length = too small; w === 0 = real error.
1112
- if (w === 0) {
1113
- throw new Error('brotli compress: invalid input')
1114
- }
1115
- if (w > output.length) {
1116
- throw new Error('brotli compress: output buffer too small')
1117
- }
1118
- return w
1119
- },
1120
- brotliDecompress(data, maxDecompressed) {
1121
- const max = maxDecompressed ?? MAX_DECOMPRESSED
1122
- // Brotli has no cheap trailer size: DECOMPRESS_GUESS_MULTIPLIER_BROTLI×
1123
- // initial covers typical JSON/text ratios (~10-30x) while the fallback
1124
- // cap bounds over-allocation on large streams; growExact handles
1125
- // higher-ratio streams with one exact retry.
1126
- return growExact(
1127
- (out) => Number(brotliDecompress(data, data.length, max, out, out.length)),
1128
- Math.min(
1129
- Math.max(data.length * DECOMPRESS_GUESS_MULTIPLIER_BROTLI, DECOMPRESS_MIN_INITIAL),
1130
- DECOMPRESS_FALLBACK_CAP,
1131
- ),
1132
- max,
1133
- 'brotli decompress: invalid stream or exceeded max decompressed size',
1134
- )
1135
- },
1136
-
1137
- httpParseRequestPackedInto(input, output) {
1138
- const w = Number(httpParsePacked(input, input.length, output, output.length))
1139
- if (w === 0 && input.length !== 0) {
1140
- throw new Error('http parse: output buffer too small or malformed request')
1141
- }
1142
- return w
1143
- },
1144
- queryParsePackedInto(input, output) {
1145
- const w = Number(queryParsePacked(input, input.length, output, output.length))
1146
- if (w === 0 && input.length !== 0) {
1147
- throw new Error('query parse: output buffer too small')
1148
- }
1149
- return w
1150
- },
1151
- cookieParsePackedInto(input, output) {
1152
- const w = Number(cookieParsePacked(input, input.length, output, output.length))
1153
- if (w === 0 && input.length !== 0) {
1154
- throw new Error('cookie parse: output buffer too small')
1155
- }
1156
- return w
1157
- },
1158
-
1159
- // ── Excluded-surface additions ──────────────────────────────────
1160
- jwtSignBytes(claimsJson, secret, ttl, now) {
1161
- // Token = header(~36) + payload (claims + iat/exp, base64url ≈ 4/3×) +
1162
- // signature (~43) + 2 dots. The old `claims.length + 128` under-sized a
1163
- // typical token (measured 168 B for a 30 B claim → grow-retry double-run,
1164
- // which made ffi slower than napi). 2×+128 with a 256-byte floor covers
1165
- // typical claims in one pass; growExact handles huge claims with one
1166
- // exact retry (no re-run loop).
1167
- return growExact(
1168
- (out) =>
1169
- Number(
1170
- jwtSignBytes(
1171
- claimsJson,
1172
- claimsJson.length,
1173
- secret,
1174
- secret.length,
1175
- ttl,
1176
- now,
1177
- out,
1178
- out.length,
1179
- ),
1180
- ),
1181
- Math.min(
1182
- Math.max(
1183
- claimsJson.length * JWT_INITIAL_MULTIPLIER + JWT_INITIAL_EXTRA,
1184
- JWT_INITIAL_FLOOR,
1185
- ),
1186
- 1024 * 1024,
1187
- ),
1188
- 1024 * 1024,
1189
- 'jwt sign: output buffer too small',
1190
- )
1191
- },
1192
- wsFrameDecodePacked(data) {
1193
- // Max packed output = 6-byte header + payload.
1194
- const out = new Uint8Array(data.length + 6)
1195
- const w = Number(wsFrameDecodePacked(data, data.length, out, out.length))
1196
- return w === 0 ? null : out.subarray(0, w)
1197
- },
1198
- multipartParsePacked(body, boundary) {
1199
- return growExact(
1200
- (out) =>
1201
- Number(
1202
- multipartParsePacked(body, body.length, boundary, boundary.length, out, out.length),
1203
- ),
1204
- Math.min(body.length + boundary.length + 64, 64 * 1024),
1205
- 128 * 1024 * 1024,
1206
- 'multipart parse: output buffer too small',
1207
- )
1208
- },
1209
- formParsePackedInto(input, output) {
1210
- const w = Number(formParsePacked(input, input.length, output, output.length))
1211
- if (w === 0 && input.length !== 0) {
1212
- throw new Error('form parse: output buffer too small')
1213
- }
1214
- return w
1215
- },
1216
- ingressHandlePacked(inner, input, body, output) {
1217
- const w = Number(
1218
- ingressHandlePacked(
1219
- inner,
1220
- input,
1221
- input.length,
1222
- body ?? EMPTY_VIEW,
1223
- body?.length ?? 0,
1224
- output,
1225
- output.length,
1226
- ),
1227
- )
1228
- if (w === 0) {
1229
- throw new Error('ingress handle: output buffer too small or pipeline error')
1230
- }
1231
- return w
1232
- },
1233
- ingressLayout(out) {
1234
- const w = Number(ingressLayoutSym(out, out.length))
1235
- if (w !== out.length) {
1236
- throw new Error('ingress layout: output buffer too small')
1237
- }
1238
- return w
1239
- },
1240
- }
1241
- }
1242
-
1243
- /** Verify every bound function against known-good results; false disables ffi. */
1244
- function selfTest(b: BunFFI): boolean {
1245
- const dec = new TextDecoder()
1246
- const enc = encoder
1247
-
1248
- if (b.crc32(enc.encode('123456789')) !== 0xcbf4_3926) {
1249
- return false
1250
- }
1251
- if (b.fnv1a64(enc.encode('foobar')) !== 0x8594_4171_f739_67e8n) {
1252
- return false
1253
- }
1254
- // XXH3-64 of empty input = 0x2d06800538d394c2 (standard reference vector).
1255
- if (b.xxh3(new Uint8Array(0)) !== 0x2d06800538d394c2n) {
1256
- return false
1257
- }
1258
- if (b.jsonValid(SELFTEST_JSON) !== true || b.jsonValid(enc.encode('{not json')) !== false) {
1259
- return false
1260
- }
1261
- const hexOut = new Uint8Array(SELFTEST_HEX.length * 2)
1262
- if (b.hexEncodeInto(SELFTEST_HEX, hexOut) !== 10 || dec.decode(hexOut) !== '68656c6c6f') {
1263
- return false
1264
- }
1265
- const urlInput = enc.encode('a b/c')
1266
- const urlOut = new Uint8Array(9)
1267
- if (b.urlEncodeInto(urlInput, urlOut) !== 9 || dec.decode(urlOut) !== 'a%20b%2Fc') {
1268
- return false
1269
- }
1270
-
1271
- // Ingress layout blob (38 × u32 LE). The pinned values catch a reordered
1272
- // `#[repr(C)] IngressLayout` (drift → self-test fails → napi fallback); the
1273
- // Rust unit test `ingress_layout_c_abi_matches_output_source` pins every
1274
- // field against output.rs. Slot order mirrors the struct field order.
1275
- const layoutBuf = new Uint8Array(38 * 4)
1276
- b.ingressLayout(layoutBuf)
1277
- const layoutView = new DataView(layoutBuf.buffer, layoutBuf.byteOffset, layoutBuf.byteLength)
1278
- if (
1279
- layoutView.getUint32(0, true) !== 0 || // OUT_VERDICT
1280
- layoutView.getUint32(2 * 4, true) !== 2 || // OUT_STATUS
1281
- layoutView.getUint32(12 * 4, true) !== 48 || // OUT_DATA_START
1282
- layoutView.getUint32(13 * 4, true) !== 1 || // FLAG_HAS_COOKIES
1283
- layoutView.getUint32(28 * 4, true) !== 32 || // HV_COUNT
1284
- layoutView.getUint32(37 * 4, true) !== 8 // ERR_INTERNAL
1285
- ) {
1286
- return false
1287
- }
1288
-
1289
- // Ingress pipeline C-ABI: with a null (0) inner handle the Rust side returns
1290
- // 0 immediately and the wrapper throws. This exercises the symbol's ABI (arg
1291
- // count/types/return) at bind time — a signature drift would surface here
1292
- // instead of crashing under load. Real frame→output parity is covered by
1293
- // ffi.test.ts against a live napi instance.
1294
- try {
1295
- b.ingressHandlePacked(0, enc.encode('/'), null, new Uint8Array(64))
1296
- return false // a null handle must throw, not return
1297
- } catch {
1298
- // expected: null inner handle → 0 → throw
1299
- }
1300
-
1301
- // ── New bindings ───────────────────────────────────────────────────
1302
- const a = enc.encode('a@b.com')
1303
- const uuid = enc.encode('550e8400-e29b-41d4-a716-446655440000')
1304
- if (
1305
- !b.validateEmail(a) ||
1306
- !b.validateUuid(uuid) ||
1307
- !b.validateIpv4(enc.encode('192.168.0.1')) ||
1308
- !b.validateIpv6(enc.encode('2001:db8::1')) ||
1309
- b.validateEmail(enc.encode('not-an-email')) ||
1310
- b.validateUuid(enc.encode('not-a-uuid'))
1311
- ) {
1312
- return false
1313
- }
1314
- if (b.jsonSumIds(enc.encode(`[{"id":1},{"id":2}]`)) !== 3n) {
1315
- return false
1316
- }
1317
- // The packed [u8 ok][i64 sum LE] ABI: a legit zero-sum is ok, invalid input throws.
1318
- if (b.jsonSumIds(enc.encode(`[{"id":0},{"id":0}]`)) !== 0n) {
1319
- return false
1320
- }
1321
- let sumInvalidThrew = false
1322
- try {
1323
- b.jsonSumIds(enc.encode('nope'))
1324
- } catch {
1325
- sumInvalidThrew = true
1326
- }
1327
- if (!sumInvalidThrew) {
1328
- return false
1329
- }
1330
-
1331
- // HMAC RFC 4231 test case 1 (0x0b × 20 key, "Hi There" data).
1332
- const hmacKey = new Uint8Array(20).fill(0x0b)
1333
- const hmacData = enc.encode('Hi There')
1334
- const hmacSig = enc.encode('b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7')
1335
- const hmacHex = b.hmacSha256(hmacKey, hmacData)
1336
- if (dec.decode(hmacHex) !== 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7') {
1337
- return false
1338
- }
1339
- if (!b.hmacSha256Verify(hmacKey, hmacData, hmacSig)) {
1340
- return false
1341
- }
1342
-
1343
- // Decoders round-trip.
1344
- const decoded = b.hexDecode(enc.encode('68656c6c6f'))
1345
- if (
1346
- dec.decode(decoded) !== 'hello' ||
1347
- dec.decode(b.urlDecode(enc.encode('a%20b%2Fc'))) !== 'a b/c'
1348
- ) {
1349
- return false
1350
- }
1351
-
1352
- // WebSocket accept key (RFC 6455 sample).
1353
- if (
1354
- dec.decode(b.wsAcceptKey(enc.encode('dGhlIHNhbXBsZSBub25jZQ=='))) !==
1355
- 's3pPLMBiTxaQ9kYGzzhZRbK+xOo='
1356
- ) {
1357
- return false
1358
- }
1359
-
1360
- // ETag: strong = 10 bytes, weak = 12 bytes.
1361
- if (b.etag(SELFTEST_HEX).length !== 10 || b.etag(SELFTEST_HEX, true).length !== 12) {
1362
- return false
1363
- }
1364
-
1365
- // base64.
1366
- if (dec.decode(b.base64Encode(SELFTEST_HEX)) !== 'aGVsbG8=') {
1367
- return false
1368
- }
1369
- if (dec.decode(b.base64Decode(enc.encode('aGVsbG8='))) !== 'hello') {
1370
- return false
1371
- }
1372
-
1373
- // Signed cookie round-trip.
1374
- const secret = enc.encode('s3cr3t-secret')
1375
- const signed = b.signCookie(SELFTEST_HEX, secret)
1376
- const verified = b.verifyCookie(signed, secret)
1377
- if (verified === null || dec.decode(verified) !== 'hello') {
1378
- return false
1379
- }
1380
- if (b.verifyCookie(enc.encode('tampered.0000'), secret) !== null) {
1381
- return false
1382
- }
1383
-
1384
- // CSRF token round-trip (issued token verifies against the same secret).
1385
- const csrfTokenBytes = b.csrfToken(secret)
1386
- if (csrfTokenBytes.length !== 129 || !b.csrfVerify(csrfTokenBytes, secret)) {
1387
- return false
1388
- }
1389
-
1390
- // Argon2id round-trip at minimum cost (fast) — full defaults would take ~50ms.
1391
- const pw = enc.encode('correct horse battery staple')
1392
- const salt = enc.encode('salty-salt-16b')
1393
- const phc = b.passwordHash(pw, salt, 8, 1, 1, 16)
1394
- if (phc.length === 0 || !b.passwordVerify(pw, phc)) {
1395
- return false
1396
- }
1397
-
1398
- // bcrypt round-trip at minimum cost (fast).
1399
- const bcryptPhc = b.passwordHashBcrypt(pw, 4)
1400
- if (bcryptPhc.length === 0 || !b.passwordVerifyBcrypt(pw, bcryptPhc)) {
1401
- return false
1402
- }
1403
-
1404
- // PBKDF2-HMAC-SHA256: password="password", salt="salt", c=1, dkLen=32.
1405
- // The C ABI writes the RAW derived key; hex-encode before comparing.
1406
- const dk = b.pbkdf2Sha256(enc.encode('password'), enc.encode('salt'), 1, 32)
1407
- if (
1408
- dec.decode(b.hexEncode(dk)) !==
1409
- '120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b'
1410
- ) {
1411
- return false
1412
- }
1413
-
1414
- // AEAD AES-256-GCM round-trip (key 32B, nonce 12B).
1415
- const aeadKey = new Uint8Array(32).fill(0x42)
1416
- const nonce = new Uint8Array(12).fill(0x07)
1417
- const ct = b.aeadEncrypt(aeadKey, nonce, SELFTEST_HEX, 0)
1418
- const pt = b.aeadDecrypt(aeadKey, nonce, ct, 0)
1419
- if (pt === null || dec.decode(pt) !== 'hello') {
1420
- return false
1421
- }
1422
-
1423
- // WebSocket frame: text frame, FIN, no mask → first byte 0x81.
1424
- const frame = b.wsFrameEncode(1, SELFTEST_HEX, false, true)
1425
- if (frame.length === 0 || frame[0] !== 0x81) {
1426
- return false
1427
- }
1428
-
1429
- // JSON patch: add a key.
1430
- const patched = b.jsonPatch(
1431
- enc.encode(`{"a":"b"}`),
1432
- enc.encode(`[{"op":"add","path":"/c","value":"d"}]`),
1433
- )
1434
- if (!dec.decode(patched).includes(`"c":"d"`)) {
1435
- return false
1436
- }
1437
-
1438
- // gzip / brotli round-trips.
1439
- const gz = b.gzipCompress(SELFTEST_HEX)
1440
- if (dec.decode(b.gzipDecompress(gz)) !== 'hello') {
1441
- return false
1442
- }
1443
- const br = b.brotliCompress(SELFTEST_HEX)
1444
- if (dec.decode(b.brotliDecompress(br)) !== 'hello') {
1445
- return false
1446
- }
1447
-
1448
- // Needed-size convention: invalid compressed input throws IMMEDIATELY (the C
1449
- // ABI returns 0 = real error, so the JS wrapper does NOT grow-retry re-runs
1450
- // or allocate up to the 64 MiB decompression cap per bad input).
1451
- let decompressThrew = false
1452
- try {
1453
- b.gzipDecompress(enc.encode('not-a-gzip-stream'))
1454
- } catch {
1455
- decompressThrew = true
1456
- }
1457
- if (!decompressThrew) {
1458
- return false
1459
- }
1460
- decompressThrew = false
1461
- try {
1462
- b.brotliDecompress(enc.encode('not-brotli-stream'))
1463
- } catch {
1464
- decompressThrew = true
1465
- }
1466
- if (!decompressThrew) {
1467
- return false
1468
- }
1469
-
1470
- // Packed parsers (non-empty output). Packed output is LARGER than input (each
1471
- // component gets a u32 length prefix), so size with the Rust allocator's
1472
- // conservative upper bound (`input.len() * 9 + 16` in query_parser.rs).
1473
- const req = enc.encode('GET /a?b=1 HTTP/1.1\r\nHost: example.com\r\n\r\n')
1474
- const reqOut = new Uint8Array(req.length * 9 + 16)
1475
- if (b.httpParseRequestPackedInto(req, reqOut) === 0) {
1476
- return false
1477
- }
1478
- const qIn = enc.encode('a=1&b=2')
1479
- const qOut = new Uint8Array(qIn.length * 9 + 16)
1480
- if (b.queryParsePackedInto(qIn, qOut) === 0) {
1481
- return false
1482
- }
1483
- const cIn = enc.encode('a=1; b=2')
1484
- const cOut = new Uint8Array(cIn.length * 9 + 16)
1485
- if (b.cookieParsePackedInto(cIn, cOut) === 0) {
1486
- return false
1487
- }
1488
-
1489
- // ── Excluded-surface additions ────────────────────────────────────
1490
- // form parse shares the query core → 2 pairs.
1491
- const fIn = enc.encode('a=1&b=2')
1492
- const fOut = new Uint8Array(fIn.length * 9 + 16)
1493
- if (b.formParsePackedInto(fIn, fOut) === 0 || fOut[0] !== 2) {
1494
- return false
1495
- }
1496
-
1497
- // multipart parse → 1 part named "field".
1498
- const boundary = enc.encode('----boundary')
1499
- // Wire format is `--{boundary}` — boundary is `----boundary`, so the body
1500
- // must open with `------boundary`.
1501
- const mBody = enc.encode(
1502
- '------boundary\r\nContent-Disposition: form-data; name="field"\r\n\r\nvalue\r\n------boundary--',
1503
- )
1504
- const mOut = b.multipartParsePacked(mBody, boundary)
1505
- if (mOut[0] !== 1) {
1506
- return false
1507
- }
1508
- // Packed layout: [u32 count][u32 name_len][name]... → name_len at offset 4.
1509
- const mNameLen = (mOut[4] ?? 0) | ((mOut[5] ?? 0) << 8) | ((mOut[6] ?? 0) << 16) | ((mOut[7] ?? 0) << 24)
1510
- if (dec.decode(mOut.subarray(8, 8 + mNameLen)) !== 'field') {
1511
- return false
1512
- }
1513
-
1514
- // WS frame decode: encode("hello") → decode → fin=1, opcode=1, payload="hello".
1515
- const wf = b.wsFrameEncode(1, SELFTEST_HEX, true, true)
1516
- const wd = b.wsFrameDecodePacked(wf)
1517
- if (wd === null || wd[0] !== 1 || wd[1] !== 1 || dec.decode(wd.subarray(6)) !== 'hello') {
1518
- return false
1519
- }
1520
- if (b.wsFrameDecodePacked(enc.encode('\x80')) !== null) {
1521
- return false
1522
- }
1523
-
1524
- // JWT sign with ttl=0 (deterministic — no iat/exp), then verify the
1525
- // signature with the FFI HMAC to prove the binding is real.
1526
- const jwtSecret = enc.encode('my-secret')
1527
- const jwt = b.jwtSignBytes(enc.encode('{"sub":"user-1"}'), jwtSecret, 0, 0)
1528
- const jwtStr = dec.decode(jwt)
1529
- const segs = jwtStr.split('.')
1530
- if (segs.length !== 3 || segs[0] === '' || segs[1] === '' || segs[2] === '') {
1531
- return false
1532
- }
1533
- const signingInput = enc.encode(`${segs[0]}.${segs[1]}`)
1534
- const sigHex = dec.decode(b.hmacSha256(jwtSecret, signingInput))
1535
- const sigBytes = b.base64Decode(enc.encode(segs[2]), true, false)
1536
- if (sigBytes === null || dec.decode(b.hexEncode(sigBytes)) !== sigHex) {
1537
- return false
1538
- }
1539
-
1540
- return true
1541
- }