castrum 0.9.3 → 0.9.6
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 +2242 -0
- package/Cargo.toml +155 -0
- package/README.md +14 -6
- package/build.rs +5 -0
- 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 +1508 -1291
- package/dist/index.js +3391 -2069
- package/docs/API.md +2 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +5 -3
- package/docs/ENVIRONMENT.md +25 -2
- package/docs/FFI_BUN_GUIDE.md +242 -14
- package/docs/HOW_TO_ADD_AN_OP.md +10 -0
- package/docs/REPO_MAP.md +17 -6
- package/docs/RND-CONCURRENCY.md +772 -0
- package/docs/bun-builtins-decision-matrix.md +4 -1
- package/index.ts +24 -25
- package/package.json +173 -156
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +105 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +69 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/task.rs +172 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +136 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +211 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +770 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +567 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +883 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +73 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/task/completion.rs +119 -0
- package/rust/task/mod.rs +65 -0
- package/rust/task/ops.rs +503 -0
- package/rust/task/runtime.rs +220 -0
- package/rust/task/tests.rs +231 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +594 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/health.ts +1 -2
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +130 -12
- package/src/native/ffi/build/compress.ts +22 -10
- package/src/native/ffi/build/instances.ts +46 -4
- package/src/native/ffi/build/metrics.ts +247 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/task.ts +79 -0
- package/src/native/ffi/build/util.ts +32 -1
- package/src/native/ffi/build.ts +4 -0
- package/src/native/ffi/selftest.ts +12 -3
- package/src/native/ffi/types.ts +151 -9
- package/src/native/ffi.ts +160 -15
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +55 -5
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +71 -27
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/rust-ffi/text.ts +25 -3
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +24 -4
- package/src/shared/codec.ts +18 -3
- package/src/task/index.ts +24 -0
- package/src/task/op.ts +171 -0
- package/src/task/runtime.ts +568 -0
package/src/integration/index.ts
CHANGED
|
@@ -8,22 +8,22 @@
|
|
|
8
8
|
// - sseResponse → server-sent-events response framed on the fast path
|
|
9
9
|
|
|
10
10
|
export {
|
|
11
|
+
runMany,
|
|
12
|
+
runOne,
|
|
13
|
+
validateCount,
|
|
14
|
+
validateMany,
|
|
15
|
+
} from './batch'
|
|
16
|
+
export {
|
|
17
|
+
type CreatePipelineOptions,
|
|
11
18
|
createPipeline,
|
|
12
19
|
type IngressPipeline,
|
|
13
|
-
type PipelineResult,
|
|
14
20
|
type PipelineContext,
|
|
21
|
+
type PipelineResult,
|
|
15
22
|
type PreprocessOutcome,
|
|
16
|
-
type CreatePipelineOptions,
|
|
17
23
|
} from './pipeline'
|
|
24
|
+
export { type SseEvent, sseResponse } from './streaming'
|
|
18
25
|
export {
|
|
19
26
|
createWebSocketUpgrade,
|
|
20
27
|
type WebSocketUpgradeOptions,
|
|
21
28
|
type WebSocketUpgradeResult,
|
|
22
29
|
} from './websocket'
|
|
23
|
-
export { sseResponse, type SseEvent } from './streaming'
|
|
24
|
-
export {
|
|
25
|
-
validateMany,
|
|
26
|
-
validateCount,
|
|
27
|
-
runMany,
|
|
28
|
-
runOne,
|
|
29
|
-
} from './batch'
|
|
@@ -7,18 +7,18 @@
|
|
|
7
7
|
// context to the app's own handler. `handleRequest` is a fetch-compatible
|
|
8
8
|
// convenience that serves the terminal response or a rendered OK response.
|
|
9
9
|
|
|
10
|
+
import { readBodyWithLimit } from '../ingress/body'
|
|
11
|
+
import type { BakedIngressResult } from '../ingress/decode/baked-result'
|
|
10
12
|
import {
|
|
11
|
-
createIngressHandler,
|
|
12
13
|
type BakedIngressRuntime,
|
|
14
|
+
createIngressHandler,
|
|
13
15
|
type OptimizedIngressHandler,
|
|
14
16
|
} from '../ingress/handlers'
|
|
15
|
-
import type { BakedContext } from '../ingress/types'
|
|
16
17
|
import type { IngressHandlerOptions } from '../ingress/options'
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { generateRequestId } from '../shared/request-id'
|
|
18
|
+
import { DEFAULT_BODY_TIMEOUT_MS, DEFAULT_MAX_BODY_BYTES } from '../ingress/shared'
|
|
19
|
+
import type { BakedContext } from '../ingress/types'
|
|
20
20
|
import { decoder } from '../shared/bytes'
|
|
21
|
-
import
|
|
21
|
+
import { generateRequestId } from '../shared/request-id'
|
|
22
22
|
|
|
23
23
|
/** Readonly snapshot of the ingress decision, safe after `run()` returns. */
|
|
24
24
|
export interface PipelineResult {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/integration/websocket.ts — WebSocket upgrade handshake helper.
|
|
2
2
|
|
|
3
|
-
import { isBun } from '../shared/runtime'
|
|
4
3
|
import { rust } from '../rust-ffi'
|
|
5
|
-
|
|
4
|
+
import { isBun } from '../shared/runtime'
|
|
5
|
+
/** Options for `createWebSocketUpgrade` (supported subprotocols). */ export interface WebSocketUpgradeOptions {
|
|
6
6
|
/** Subprotocols this server supports; the first client-requested match wins. */
|
|
7
7
|
protocols?: ReadonlyArray<string>
|
|
8
8
|
}
|
package/src/loader/index.ts
CHANGED
|
@@ -27,11 +27,7 @@
|
|
|
27
27
|
import { rust } from '../rust-ffi'
|
|
28
28
|
import { encoder, viewForArrayBuffer } from '../shared/bytes'
|
|
29
29
|
import type { SchemaValidator } from '../shared/packed'
|
|
30
|
-
import {
|
|
31
|
-
createLruCache,
|
|
32
|
-
createTickCoalescer,
|
|
33
|
-
type LoadRequest,
|
|
34
|
-
} from './batch'
|
|
30
|
+
import { createLruCache, createTickCoalescer, type LoadRequest } from './batch'
|
|
35
31
|
import { createCostModel, nowNs } from './cost'
|
|
36
32
|
import {
|
|
37
33
|
fnvBatchKeysInto,
|
package/src/loader/ops.ts
CHANGED
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
// the loader never pays a conversion cost on the hot path. This module is the
|
|
20
20
|
// single source of truth for what the loader can dispatch.
|
|
21
21
|
|
|
22
|
+
import type { PasswordHashOptions } from '../native'
|
|
22
23
|
import { rust } from '../rust-ffi'
|
|
23
24
|
import { addon } from '../rust-ffi/addon'
|
|
24
25
|
import { decoder } from '../shared/bytes'
|
|
25
|
-
import type { PasswordHashOptions } from '../native'
|
|
26
26
|
import type { SchemaValidator } from '../shared/packed'
|
|
27
27
|
|
|
28
28
|
/** Empty bytes sentinel for skip-on-error scalar parity (e.g. AEAD decrypt). */
|
|
@@ -9,7 +9,7 @@ import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
|
|
|
9
9
|
import { SELFTEST_HEX, SELFTEST_JSON } from '../constants'
|
|
10
10
|
import type { BunFFI, Raw3, Raw4, Raw5, Raw6, Raw8, Raw9, Raw10, RawCStr } from '../types'
|
|
11
11
|
import type { BuildCtx } from './util'
|
|
12
|
-
import { argon2PhcLength, cstr, flag, growExact, writeOrThrow } from './util'
|
|
12
|
+
import { allocOut, argon2PhcLength, cstr, flag, growExact, hasNul, writeOrThrow } from './util'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Build the codec/crypto/auth methods of the BunFFI surface. `ctx` is
|
|
@@ -34,7 +34,17 @@ export function buildCodecs(
|
|
|
34
34
|
const validateUuid = sym.castrum_validate_uuid as (...a: unknown[]) => number | bigint
|
|
35
35
|
const validateIpv4 = sym.castrum_validate_ipv4 as (...a: unknown[]) => number | bigint
|
|
36
36
|
const validateIpv6 = sym.castrum_validate_ipv6 as (...a: unknown[]) => number | bigint
|
|
37
|
+
const validateEmailBytes = sym.castrum_validate_email_bytes as (
|
|
38
|
+
...a: unknown[]
|
|
39
|
+
) => number | bigint
|
|
40
|
+
const validateUuidBytes = sym.castrum_validate_uuid_bytes as (...a: unknown[]) => number | bigint
|
|
41
|
+
const validateIpv4Bytes = sym.castrum_validate_ipv4_bytes as (...a: unknown[]) => number | bigint
|
|
42
|
+
const validateIpv6Bytes = sym.castrum_validate_ipv6_bytes as (...a: unknown[]) => number | bigint
|
|
37
43
|
const jsonSumRaw = sym.castrum_json_sum_ids as Raw4
|
|
44
|
+
const hexBatchRaw = sym.castrum_hex_validate_batch as Raw5
|
|
45
|
+
const hexBatchStrRaw = sym.castrum_hex_validate_batch_str as Raw4
|
|
46
|
+
const regexEscapeRaw = sym.castrum_regex_escape as Raw4
|
|
47
|
+
const regexEscapeStrRaw = sym.castrum_regex_escape_str as RawCStr
|
|
38
48
|
const hmacVerify = sym.castrum_hmac_sha256_verify as Raw6
|
|
39
49
|
const csrfVerify = sym.castrum_csrf_verify as Raw4
|
|
40
50
|
const passwordVerify = sym.castrum_password_verify as Raw4
|
|
@@ -172,10 +182,67 @@ export function buildCodecs(
|
|
|
172
182
|
},
|
|
173
183
|
urlEncodeInto,
|
|
174
184
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
185
|
+
// Verdict guards: the `cstring` form would truncate at an embedded NUL, so
|
|
186
|
+
// `"a@b.com\0<script>"` would be reported VALID — a verdict callers act on.
|
|
187
|
+
// No address / UUID / IP contains U+0000, so short-circuit without an FFI
|
|
188
|
+
// call (one `indexOf` scan, far cheaper than the encode fallback).
|
|
189
|
+
validateEmail: (input) => !hasNul(input) && Number(validateEmail(input)) === 1,
|
|
190
|
+
validateUuid: (input) => !hasNul(input) && Number(validateUuid(input)) === 1,
|
|
191
|
+
validateIpv4: (input) => !hasNul(input) && Number(validateIpv4(input)) === 1,
|
|
192
|
+
validateIpv6: (input) => !hasNul(input) && Number(validateIpv6(input)) === 1,
|
|
193
|
+
// Byte-input validators: `(ptr,len)` — zero transcode (no CString decode
|
|
194
|
+
// + engine re-encode like the cstring-ARG string forms above).
|
|
195
|
+
validateEmailBytes: (input) => Number(oneArg(validateEmailBytes, input)) === 1,
|
|
196
|
+
validateUuidBytes: (input) => Number(oneArg(validateUuidBytes, input)) === 1,
|
|
197
|
+
validateIpv4Bytes: (input) => Number(oneArg(validateIpv4Bytes, input)) === 1,
|
|
198
|
+
validateIpv6Bytes: (input) => Number(oneArg(validateIpv6Bytes, input)) === 1,
|
|
199
|
+
hexValidateBatchInto(input, width, output) {
|
|
200
|
+
// Batch fixed-width hex validation: NEWLINE-separated lines in; one
|
|
201
|
+
// verdict byte (1/0) per line out. Needed-size convention: `0` = bad
|
|
202
|
+
// width / null pointers (real error → throw); `> output.length` = the
|
|
203
|
+
// exact required size (caller grows once and retries). Width is
|
|
204
|
+
// validated here so an empty input can't mask it.
|
|
205
|
+
if (width === 0 || width > 4096) {
|
|
206
|
+
throw new Error('hex validate batch: width must be 1..=4096')
|
|
207
|
+
}
|
|
208
|
+
const w = Number(hexBatchRaw(input, lenOrView(input), width >>> 0, output, lenOrView(output)))
|
|
209
|
+
return w
|
|
210
|
+
},
|
|
211
|
+
regexEscapeInto(input, output) {
|
|
212
|
+
const w = Number(regexEscapeRaw(input, lenOrView(input), output, lenOrView(output)))
|
|
213
|
+
if (w === 0) {
|
|
214
|
+
throw new Error('regex escape: output buffer too small')
|
|
215
|
+
}
|
|
216
|
+
return w
|
|
217
|
+
},
|
|
218
|
+
// Zero-copy text path: cstring ARG in (engine-transcoded), cstring return
|
|
219
|
+
// out (engine-cloned) — the JS side does zero encode AND zero decode. A NUL
|
|
220
|
+
// would truncate the INPUT, so that case routes to the `(ptr,len)` sibling
|
|
221
|
+
// (exact length) instead of returning a silently shortened string.
|
|
222
|
+
regexEscapeStr: (input) => {
|
|
223
|
+
if (!hasNul(input)) return regexEscapeStrRaw(input)
|
|
224
|
+
const bytes = encodeUtf8(input)
|
|
225
|
+
const escaped = growExact(
|
|
226
|
+
(out) => Number(regexEscapeRaw(bytes, lenOrView(bytes), out, lenOrView(out))),
|
|
227
|
+
16,
|
|
228
|
+
1 << 20,
|
|
229
|
+
'regex escape: output buffer too small',
|
|
230
|
+
)
|
|
231
|
+
return decodeUtf8(escaped)
|
|
232
|
+
},
|
|
233
|
+
hexValidateBatchStr(ids, width, output) {
|
|
234
|
+
if (width === 0 || width > 4096) {
|
|
235
|
+
throw new Error('hex validate batch: width must be 1..=4096')
|
|
236
|
+
}
|
|
237
|
+
// A NUL in the joined ids would truncate the line list, so a bad id could
|
|
238
|
+
// pass as its own prefix. Fall back to the byte form (exact length).
|
|
239
|
+
if (hasNul(ids)) {
|
|
240
|
+
const bytes = encodeUtf8(ids)
|
|
241
|
+
return Number(hexBatchRaw(bytes, lenOrView(bytes), width >>> 0, output, lenOrView(output)))
|
|
242
|
+
}
|
|
243
|
+
const w = Number(hexBatchStrRaw(ids, width >>> 0, output, lenOrView(output)))
|
|
244
|
+
return w
|
|
245
|
+
},
|
|
179
246
|
jsonSumIds: (input) => {
|
|
180
247
|
// Packed [u8 ok][i64 sum LE] output (9 B): ok=1 → valid array (the sum
|
|
181
248
|
// may be 0); ok=0 → invalid input. Bytes written: 9/1/0 (0 = real error).
|
|
@@ -202,19 +269,19 @@ export function buildCodecs(
|
|
|
202
269
|
Number(passwordVerifyBcrypt(password, lenOrView(password), phc)) === 1,
|
|
203
270
|
|
|
204
271
|
hexDecode(input) {
|
|
205
|
-
const out =
|
|
272
|
+
const out = allocOut(Math.floor(input.length / 2))
|
|
206
273
|
const w = hexDecodeInto(input, out)
|
|
207
274
|
return out.subarray(0, w)
|
|
208
275
|
},
|
|
209
276
|
hexDecodeInto,
|
|
210
277
|
urlDecode(input) {
|
|
211
|
-
const out =
|
|
278
|
+
const out = allocOut(input.length)
|
|
212
279
|
const w = urlDecodeInto(input, out)
|
|
213
280
|
return out.subarray(0, w)
|
|
214
281
|
},
|
|
215
282
|
urlDecodeInto,
|
|
216
283
|
base64Decode(input, urlSafe, padding) {
|
|
217
|
-
const out =
|
|
284
|
+
const out = allocOut(Math.ceil((input.length * 3) / 4))
|
|
218
285
|
const w = base64DecodeInto(input, out, urlSafe, padding)
|
|
219
286
|
return out.subarray(0, w)
|
|
220
287
|
},
|
|
@@ -398,7 +465,7 @@ export function buildCodecs(
|
|
|
398
465
|
},
|
|
399
466
|
passwordHashBcrypt(password, cost) {
|
|
400
467
|
// `$2b$CC$` + 22 salt chars + 31 hash chars = 60 chars.
|
|
401
|
-
const out =
|
|
468
|
+
const out = allocOut(64)
|
|
402
469
|
const w = Number(passwordHashBcrypt(password, lenOrView(password), cost, out, lenOrView(out)))
|
|
403
470
|
if (w === 0) {
|
|
404
471
|
throw new Error('password hash bcrypt: output buffer too small')
|
|
@@ -409,7 +476,7 @@ export function buildCodecs(
|
|
|
409
476
|
// Rust clamps dkLen to [1, 1MiB] (PBKDF2_MIN_LEN/MAX_LEN) AFTER sizing its
|
|
410
477
|
// own buffer — so pre-clamp here so dkLen 0 still yields a 1-byte result.
|
|
411
478
|
const dk = Math.min(Math.max(dkLen, 1), 1024 * 1024)
|
|
412
|
-
const out =
|
|
479
|
+
const out = allocOut(dk)
|
|
413
480
|
const w = Number(
|
|
414
481
|
pbkdf2(
|
|
415
482
|
password,
|
|
@@ -429,7 +496,7 @@ export function buildCodecs(
|
|
|
429
496
|
},
|
|
430
497
|
aeadEncrypt(key, nonce, plaintext, algorithm = 0) {
|
|
431
498
|
// ciphertext + 16-byte auth tag.
|
|
432
|
-
const out =
|
|
499
|
+
const out = allocOut(plaintext.length + 16)
|
|
433
500
|
const w = Number(
|
|
434
501
|
aeadEncrypt(
|
|
435
502
|
key,
|
|
@@ -472,7 +539,7 @@ export function buildCodecs(
|
|
|
472
539
|
return w
|
|
473
540
|
},
|
|
474
541
|
aeadDecrypt(key, nonce, ciphertext, algorithm = 0) {
|
|
475
|
-
const out =
|
|
542
|
+
const out = allocOut(ciphertext.length)
|
|
476
543
|
const w = Number(
|
|
477
544
|
aeadDecrypt(
|
|
478
545
|
key,
|
|
@@ -534,6 +601,17 @@ export function selfTestCodecs(b: BunFFI): boolean {
|
|
|
534
601
|
) {
|
|
535
602
|
return false
|
|
536
603
|
}
|
|
604
|
+
// Byte-input validator siblings: `(ptr,len)` parity with the string forms.
|
|
605
|
+
if (
|
|
606
|
+
!b.validateEmailBytes(enc.encode('a@b.com')) ||
|
|
607
|
+
!b.validateUuidBytes(enc.encode('550e8400-e29b-41d4-a716-446655440000')) ||
|
|
608
|
+
!b.validateIpv4Bytes(enc.encode('192.168.0.1')) ||
|
|
609
|
+
!b.validateIpv6Bytes(enc.encode('2001:db8::1')) ||
|
|
610
|
+
b.validateEmailBytes(enc.encode('not-an-email')) ||
|
|
611
|
+
b.validateIpv6Bytes(enc.encode('999'))
|
|
612
|
+
) {
|
|
613
|
+
return false
|
|
614
|
+
}
|
|
537
615
|
// Packed `[u8 ok][i64 sum LE]` ABI: legit zero-sum is ok, invalid input throws.
|
|
538
616
|
if (b.jsonSumIds(enc.encode(`[{"id":1},{"id":2}]`)) !== 3n) return false
|
|
539
617
|
if (b.jsonSumIds(enc.encode(`[{"id":0},{"id":0}]`)) !== 0n) return false
|
|
@@ -545,6 +623,46 @@ export function selfTestCodecs(b: BunFFI): boolean {
|
|
|
545
623
|
}
|
|
546
624
|
if (!sumInvalidThrew) return false
|
|
547
625
|
|
|
626
|
+
// Batch fixed-width hex validation (needed-size convention + verdicts).
|
|
627
|
+
{
|
|
628
|
+
const ids = enc.encode('507f1f77bcf86cd799439011\nzz\n507F1F77BCF86CD799439012')
|
|
629
|
+
const tiny = new Uint8Array(2)
|
|
630
|
+
const needed = b.hexValidateBatchInto(ids, 24, tiny)
|
|
631
|
+
if (needed !== 3) return false // exact required size on a too-small buffer
|
|
632
|
+
const out = new Uint8Array(needed)
|
|
633
|
+
if (b.hexValidateBatchInto(ids, 24, out) !== 3) return false
|
|
634
|
+
if (out[0] !== 1 || out[1] !== 0 || out[2] !== 1) return false
|
|
635
|
+
let widthThrew = false
|
|
636
|
+
try {
|
|
637
|
+
b.hexValidateBatchInto(ids, 0, new Uint8Array(3))
|
|
638
|
+
} catch {
|
|
639
|
+
widthThrew = true
|
|
640
|
+
}
|
|
641
|
+
if (!widthThrew) return false
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// RegExp escaping: metachars get backslashes; plain text is untouched.
|
|
645
|
+
{
|
|
646
|
+
const out = new Uint8Array(64)
|
|
647
|
+
const w = b.regexEscapeInto(enc.encode('a.c(x)'), out)
|
|
648
|
+
if (w !== 9 || dec.decode(out.subarray(0, w)) !== 'a\\.c\\(x\\)') return false
|
|
649
|
+
const plain = new Uint8Array(16)
|
|
650
|
+
const wp = b.regexEscapeInto(enc.encode('hello'), plain)
|
|
651
|
+
if (wp !== 5 || dec.decode(plain.subarray(0, wp)) !== 'hello') return false
|
|
652
|
+
// too-small buffer reports the exact required size
|
|
653
|
+
if (b.regexEscapeInto(enc.encode('a.b'), new Uint8Array(2)) !== 4) return false
|
|
654
|
+
// zero-copy str sibling must produce the identical escaped string
|
|
655
|
+
if (b.regexEscapeStr('a.c(x)') !== 'a\\.c\\(x\\)') return false
|
|
656
|
+
if (b.regexEscapeStr('plain text') !== 'plain text') return false
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// String-input batch hex validation (cstring ARG sibling).
|
|
660
|
+
{
|
|
661
|
+
const out = new Uint8Array(8)
|
|
662
|
+
const w = b.hexValidateBatchStr('507f1f77bcf86cd799439011\nzz', 24, out)
|
|
663
|
+
if (w !== 2 || out[0] !== 1 || out[1] !== 0) return false
|
|
664
|
+
}
|
|
665
|
+
|
|
548
666
|
// HMAC RFC 4231 test case 1 (0x0b × 20 key, "Hi There" data).
|
|
549
667
|
const hmacKey = new Uint8Array(20).fill(0x0b)
|
|
550
668
|
const hmacData = enc.encode('Hi There')
|
|
@@ -29,7 +29,7 @@ export function buildCompress(
|
|
|
29
29
|
sym: Record<string, (...a: unknown[]) => unknown>,
|
|
30
30
|
ctx: BuildCtx,
|
|
31
31
|
): Partial<BunFFI> {
|
|
32
|
-
const { lenOrView } = ctx
|
|
32
|
+
const { lenOrView, scratchFor } = ctx
|
|
33
33
|
|
|
34
34
|
const gzipCompress = sym.castrum_gzip_compress as Raw5
|
|
35
35
|
const gzipDecompress = sym.castrum_gzip_decompress as Raw5
|
|
@@ -40,15 +40,19 @@ export function buildCompress(
|
|
|
40
40
|
|
|
41
41
|
return {
|
|
42
42
|
jsonPatch(doc, patch) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
),
|
|
51
|
-
|
|
43
|
+
// Pooled scratch — the patched doc is decoded to an immutable string
|
|
44
|
+
// synchronously (same call-scope contract as the cstring returns), so
|
|
45
|
+
// the bytes never need a fresh buffer. One exact retry on miss.
|
|
46
|
+
let out = scratchFor(Math.min(Math.max(doc.length, patch.length) + 16, 64 * 1024))
|
|
47
|
+
let w = Number(jsonPatch(doc, lenOrView(doc), patch, lenOrView(patch), out, lenOrView(out)))
|
|
48
|
+
if (w > out.length && w <= MAX_JSON_PATCH_OUTPUT) {
|
|
49
|
+
out = new Uint8Array(w)
|
|
50
|
+
w = Number(jsonPatch(doc, lenOrView(doc), patch, lenOrView(patch), out, lenOrView(out)))
|
|
51
|
+
}
|
|
52
|
+
if (w === 0 || w > out.length) {
|
|
53
|
+
throw new Error('json patch: output buffer too small or patch inapplicable')
|
|
54
|
+
}
|
|
55
|
+
return decodeUtf8(out.subarray(0, w))
|
|
52
56
|
},
|
|
53
57
|
gzipCompress(data, level = 6) {
|
|
54
58
|
// The C ABI streams the compressed output directly into `out` (no internal
|
|
@@ -113,6 +117,14 @@ export function buildCompress(
|
|
|
113
117
|
}
|
|
114
118
|
return w
|
|
115
119
|
},
|
|
120
|
+
/**
|
|
121
|
+
* gzip ISIZE probe → uncompressed size for a single-member stream, or `0`
|
|
122
|
+
* when unknown. Used to PRE-SIZE a destination buffer for the zero-copy
|
|
123
|
+
* task path (one pass, no intermediate buffer, no retry in the common case).
|
|
124
|
+
*/
|
|
125
|
+
gzipIsize(data) {
|
|
126
|
+
return Number(gzipIsize(data, lenOrView(data)))
|
|
127
|
+
},
|
|
116
128
|
brotliCompress(data, quality = 5) {
|
|
117
129
|
// Same cap rationale as gzipCompress (streaming core); growExact for
|
|
118
130
|
// incompressible data.
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
|
|
10
10
|
import { EMPTY_VIEW } from '../constants'
|
|
11
|
-
import type { BunFFI, Raw2, Raw3, Raw5, Raw6, Raw7, Raw8, Raw12, RawCStr } from '../types'
|
|
11
|
+
import type { BunFFI, Raw2, Raw3, Raw4, Raw5, Raw6, Raw7, Raw8, Raw12, RawCStr } from '../types'
|
|
12
12
|
import type { BuildCtx } from './util'
|
|
13
|
-
import { cstr, growExact, unpackRateCheck } from './util'
|
|
13
|
+
import { cstr, growExact, hasNul, unpackRateCheck } from './util'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Build the opaque-handle methods of the BunFFI surface. `ctx` is destructured
|
|
@@ -25,8 +25,9 @@ export function buildInstances(
|
|
|
25
25
|
const conditionalIsNotModifiedRaw = sym.castrum_conditional_is_not_modified as Raw6
|
|
26
26
|
const mediaTypeMatcherMatchesRaw = sym.castrum_media_type_matcher_matches as Raw3
|
|
27
27
|
const acceptNegotiatorNegotiateRaw = sym.castrum_accept_negotiator_negotiate as RawCStr
|
|
28
|
-
const acceptNegotiatorNegotiateServerRaw = sym
|
|
29
|
-
|
|
28
|
+
const acceptNegotiatorNegotiateServerRaw = sym.castrum_accept_negotiator_negotiate_server as (
|
|
29
|
+
...a: unknown[]
|
|
30
|
+
) => string | null
|
|
30
31
|
const ed25519GenerateKeypairRaw = sym.castrum_ed25519_generate_keypair as Raw2
|
|
31
32
|
const ed25519SignRaw = sym.castrum_ed25519_sign as Raw6
|
|
32
33
|
const ed25519VerifyRaw = sym.castrum_ed25519_verify as Raw6
|
|
@@ -36,6 +37,10 @@ export function buildInstances(
|
|
|
36
37
|
const jwtSignerVerifyRaw = sym.castrum_jwt_signer_verify as Raw6
|
|
37
38
|
const templateRenderRaw = sym.castrum_template_render as Raw5
|
|
38
39
|
const schemaValidatorValidateRaw = sym.castrum_schema_validator_validate as Raw3
|
|
40
|
+
const queryValidateSym = sym.castrum_query_validate as (...a: unknown[]) => number | bigint
|
|
41
|
+
const cookieValidateSym = sym.castrum_cookie_validate as (...a: unknown[]) => number | bigint
|
|
42
|
+
const sessionSealSym = sym.castrum_session_seal as (...a: unknown[]) => string | null
|
|
43
|
+
const sessionOpenSym = sym.castrum_session_open as Raw4
|
|
39
44
|
const rateLimiterCheckRaw = sym.castrum_rate_limiter_check as Raw5
|
|
40
45
|
const rateLimiterCheckKeyRaw = sym.castrum_rate_limiter_check_key as Raw5
|
|
41
46
|
const jwtSignBytes = sym.castrum_jwt_sign_bytes as RawCStr
|
|
@@ -138,6 +143,27 @@ export function buildInstances(
|
|
|
138
143
|
schemaValidatorValidate(inner, doc) {
|
|
139
144
|
return Number(schemaValidatorValidateRaw(inner, doc, lenOrView(doc))) === 1
|
|
140
145
|
},
|
|
146
|
+
queryValidate(inner, qs) {
|
|
147
|
+
// `qs` is a cstring ARG — engine-transcoded (zero JS encode). A raw NUL is
|
|
148
|
+
// not a valid query byte, and it would TRUNCATE the arg so only a prefix
|
|
149
|
+
// got validated: a gate that says "this query passed" must not be foolable.
|
|
150
|
+
if (hasNul(qs)) return false
|
|
151
|
+
return Number(queryValidateSym(inner, qs)) === 1
|
|
152
|
+
},
|
|
153
|
+
cookieValidate(inner, header) {
|
|
154
|
+
// Same contract as `queryValidate`: a NUL would validate only a prefix.
|
|
155
|
+
if (hasNul(header)) return false
|
|
156
|
+
return Number(cookieValidateSym(inner, header)) === 1
|
|
157
|
+
},
|
|
158
|
+
sessionSeal(id, dataJson, expSecs, secret) {
|
|
159
|
+
// exp is an i64 C arg → BigInt.
|
|
160
|
+
return sessionSealSym(id, dataJson, BigInt(expSecs), secret)
|
|
161
|
+
},
|
|
162
|
+
sessionOpen(token, secret, output) {
|
|
163
|
+
// Needed-size convention; 0 = bad signature / malformed.
|
|
164
|
+
const w = Number(sessionOpenSym(token, secret, output, lenOrView(output)))
|
|
165
|
+
return w
|
|
166
|
+
},
|
|
141
167
|
rateLimiterCheck(inner, key, nowMs) {
|
|
142
168
|
// Packed [u8 allowed][u32 remaining LE][i64 reset_ms LE] (13 bytes).
|
|
143
169
|
// Reused scratch + cached DataView (no per-call allocs). `key` is a
|
|
@@ -583,6 +609,22 @@ export function selfTestInstances(b: BunFFI): boolean {
|
|
|
583
609
|
if (!urlBuilderThrew) {
|
|
584
610
|
return false
|
|
585
611
|
}
|
|
612
|
+
// Wire-validate: null inner (0) → false (ABI exercise); real parity lives
|
|
613
|
+
// in ffi.test.ts with a live compiled instance.
|
|
614
|
+
if (b.queryValidate(0, 'route=/a') !== false) return false
|
|
615
|
+
if (b.cookieValidate(0, 'route=/a') !== false) return false
|
|
616
|
+
|
|
617
|
+
// Session seal/open round trip through the C ABI.
|
|
618
|
+
{
|
|
619
|
+
const tok = b.sessionSeal('sess-9', '{"n":1}', 1234567, 'sekrit')
|
|
620
|
+
if (tok === null || !tok.includes('.') || !tok.startsWith('{"id":"sess-9"')) return false
|
|
621
|
+
const out = new Uint8Array(256)
|
|
622
|
+
const w = b.sessionOpen(tok, 'sekrit', out)
|
|
623
|
+
if (w <= 13 || out[0] !== 1) return false
|
|
624
|
+
// bad signature → 0
|
|
625
|
+
if (b.sessionOpen(tok, 'wrong', out) !== 0) return false
|
|
626
|
+
}
|
|
627
|
+
|
|
586
628
|
// rateLimiterCheckKey: null (0) handle → throw (ABI exercise).
|
|
587
629
|
let rlKeyThrew = false
|
|
588
630
|
try {
|