castrum 0.9.3 → 0.9.4
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 +2259 -0
- package/Cargo.toml +138 -0
- package/README.md +6 -0
- package/build.rs +5 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1348 -1295
- package/dist/index.js +4307 -3776
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +16 -0
- package/docs/FFI_BUN_GUIDE.md +89 -6
- package/docs/REPO_MAP.md +2 -2
- package/index.ts +19 -25
- package/package.json +9 -1
- 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 +96 -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 +68 -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/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 +132 -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 +180 -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 +779 -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 +566 -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 +881 -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 +72 -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/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 +407 -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/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 +104 -8
- package/src/native/ffi/build/compress.ts +14 -10
- package/src/native/ffi/build/instances.ts +40 -3
- package/src/native/ffi/build/metrics.ts +224 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/util.ts +14 -1
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +10 -3
- package/src/native/ffi/types.ts +111 -9
- package/src/native/ffi.ts +87 -1
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +2 -2
- 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 +17 -11
- 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/selection.ts +1 -1
- package/src/shared/bytes.ts +5 -4
- package/src/shared/codec.ts +18 -3
package/src/runtime/codec.ts
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The UTF-8 codec branches (formerly in `src/shared/codec.ts`) are centralized
|
|
4
4
|
// here and selected ONCE at module load:
|
|
5
|
-
// - encode:
|
|
5
|
+
// - encode: shared `TextEncoder` singleton (Bun's native implementation
|
|
6
|
+
// measures ~3x faster than the old `Bun.ArrayBufferSink` start/write/flush
|
|
7
|
+
// path on Bun 1.4 — 34-50ns vs 188-215ns for small strings) vs `TextEncoder`
|
|
8
|
+
// under Node.
|
|
6
9
|
// - encodeInto: zero-copy `Buffer` view `.write` vs `TextEncoder.encodeInto`.
|
|
7
10
|
// - decode: `new CString(ptr, 0, len)` from `bun:ffi` vs `TextDecoder`.
|
|
8
11
|
// - decodeFatal: replacement-mode (Bun) vs fatal `TextDecoder` (Node).
|
|
@@ -36,7 +39,6 @@ function getNodeFatalDecoder(): TextDecoder {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
// ── Bun codec ─────────────────────────────────────────────────────
|
|
39
|
-
let bunSink: Bun.ArrayBufferSink | null = null
|
|
40
42
|
let bunFfiCodec: typeof import('bun:ffi') | null | undefined
|
|
41
43
|
|
|
42
44
|
function getBunFfiCodec(): typeof import('bun:ffi') | null {
|
|
@@ -60,15 +62,13 @@ function createBunCodec(): Utf8Codec {
|
|
|
60
62
|
}
|
|
61
63
|
return getNodeDecoder().decode(bytes)
|
|
62
64
|
}
|
|
65
|
+
// Shared encoder singleton — Bun's native TextEncoder implementation
|
|
66
|
+
// allocates the exact-size buffer in-engine; measured ~3x faster than the
|
|
67
|
+
// previous ArrayBufferSink start/write/flush path on Bun 1.4 (see header).
|
|
68
|
+
const bunEncoder = new TextEncoder()
|
|
63
69
|
return {
|
|
64
70
|
encodeUtf8(s: string): Uint8Array {
|
|
65
|
-
|
|
66
|
-
const sink = bunSink
|
|
67
|
-
// `stream` resets the buffer on flush (reusable singleton); `flush()`
|
|
68
|
-
// returns the written data as a `Uint8Array`.
|
|
69
|
-
sink.start({ asUint8Array: true, stream: true })
|
|
70
|
-
sink.write(s)
|
|
71
|
-
return sink.flush() as Uint8Array
|
|
71
|
+
return bunEncoder.encode(s)
|
|
72
72
|
},
|
|
73
73
|
encodeUtf8Into(s: string, dest: Uint8Array, offset = 0): number {
|
|
74
74
|
return bunEncodeUtf8Into(s, dest, offset)
|
|
@@ -78,6 +78,7 @@ function createBunCodec(): Utf8Codec {
|
|
|
78
78
|
// `castrum_utf8_valid` probe (callers check it first via the transport's
|
|
79
79
|
// ffi surface). This fallback matches the historical behavior.
|
|
80
80
|
decodeUtf8Fatal: decodeUtf8Bun,
|
|
81
|
+
decodeUtf8Range: decodeUtf8RangeBun,
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -120,6 +121,39 @@ function bunEncodeUtf8Into(s: string, dest: Uint8Array, offset = 0): number {
|
|
|
120
121
|
return view.write(s, start, 'utf8')
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
// Shared cached full-backing view for the ranged DECODE fast path (same
|
|
125
|
+
// WeakMap pattern as the encode views above — one Buffer per ArrayBuffer,
|
|
126
|
+
// never per call).
|
|
127
|
+
const bunDecodeViewCache = new WeakMap<ArrayBufferLike, Buffer>()
|
|
128
|
+
|
|
129
|
+
function bunDecodeView(bytes: Uint8Array): Buffer {
|
|
130
|
+
const backing = bytes.buffer
|
|
131
|
+
let view = bunDecodeViewCache.get(backing)
|
|
132
|
+
if (view === undefined || view.byteLength !== backing.byteLength) {
|
|
133
|
+
view = Buffer.from(backing)
|
|
134
|
+
bunDecodeViewCache.set(backing, view)
|
|
135
|
+
}
|
|
136
|
+
return view
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Ranged decode: ASCII-only ranges take the latin1 `toString` path (JSC
|
|
141
|
+
* builds the string without the UTF-8 decoder machinery — measured ~2x the
|
|
142
|
+
* CString path on Bun 1.4); any high byte falls back to a real UTF-8 decode
|
|
143
|
+
* of exactly that range. Replacement-mode parity with `decodeUtf8` holds for
|
|
144
|
+
* both branches (latin1 is byte-identical to UTF-8 when no byte exceeds 0x7F).
|
|
145
|
+
*/
|
|
146
|
+
function decodeUtf8RangeBun(bytes: Uint8Array, start: number, end: number): string {
|
|
147
|
+
const view = bunDecodeView(bytes)
|
|
148
|
+
const base = bytes.byteOffset
|
|
149
|
+
for (let i = start; i < end; i++) {
|
|
150
|
+
if ((bytes[i] ?? 0) >= 0x80) {
|
|
151
|
+
return view.toString('utf8', base + start, base + end)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return view.toString('latin1', base + start, base + end)
|
|
155
|
+
}
|
|
156
|
+
|
|
123
157
|
// ── Node codec ────────────────────────────────────────────────────
|
|
124
158
|
function createNodeCodec(): Utf8Codec {
|
|
125
159
|
return {
|
|
@@ -135,6 +169,16 @@ function createNodeCodec(): Utf8Codec {
|
|
|
135
169
|
decodeUtf8Fatal(bytes: Uint8Array): string {
|
|
136
170
|
return getNodeFatalDecoder().decode(bytes)
|
|
137
171
|
},
|
|
172
|
+
decodeUtf8Range(bytes: Uint8Array, start: number, end: number): string {
|
|
173
|
+
// Node: ranged Buffer toString with the same ASCII fast path.
|
|
174
|
+
const view = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
175
|
+
for (let i = start; i < end; i++) {
|
|
176
|
+
if ((bytes[i] ?? 0) >= 0x80) {
|
|
177
|
+
return view.toString('utf8', start, end)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return view.toString('latin1', start, end)
|
|
181
|
+
},
|
|
138
182
|
}
|
|
139
183
|
}
|
|
140
184
|
|
package/src/runtime/types.ts
CHANGED
|
@@ -31,6 +31,13 @@ export interface Utf8Codec {
|
|
|
31
31
|
decodeUtf8(bytes: Uint8Array): string
|
|
32
32
|
/** Decode UTF-8 bytes to a string, throwing on invalid UTF-8. */
|
|
33
33
|
decodeUtf8Fatal(bytes: Uint8Array): string
|
|
34
|
+
/**
|
|
35
|
+
* Decode the byte RANGE `[start, end)` of `bytes` to a string (replacement
|
|
36
|
+
* mode on invalid). Zero-copy ranged decode for packed-wire unpackers —
|
|
37
|
+
* avoids the per-field `subarray` view allocation. ASCII-only ranges take
|
|
38
|
+
* a latin1 fast path (~2x on Bun); multi-byte ranges fall back to UTF-8.
|
|
39
|
+
*/
|
|
40
|
+
decodeUtf8Range(bytes: Uint8Array, start: number, end: number): string
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
/** UUIDv7 generation (Bun built-in vs crypto.randomUUID on Node). */
|
|
@@ -82,8 +82,7 @@ export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
|
82
82
|
via(ctx, 'signCookieBatchPacked', unpackByteResults, [secret])(items),
|
|
83
83
|
verifyCookie: (items, secret) =>
|
|
84
84
|
via(ctx, 'verifyCookieBatchPacked', unpackBitset, [secret])(items),
|
|
85
|
-
csrfVerify: (items, secret) =>
|
|
86
|
-
via(ctx, 'csrfVerifyBatchPacked', unpackBitset, [secret])(items),
|
|
85
|
+
csrfVerify: (items, secret) => via(ctx, 'csrfVerifyBatchPacked', unpackBitset, [secret])(items),
|
|
87
86
|
httpParseRequest: via(ctx, 'httpParseRequestBatchPacked', unpackByteResults),
|
|
88
87
|
fnv1a64: via(ctx, 'fnv1A64BatchPacked', unpackU64ArrayAsBigInt),
|
|
89
88
|
etag: (items, weak) => via(ctx, 'etagBatchPacked', unpackByteResults, [weak ?? null])(items),
|
|
@@ -96,13 +95,18 @@ export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
|
96
95
|
via(ctx, 'base64EncodeBatchPacked', unpackByteResults, [true, false])(items),
|
|
97
96
|
base64UrlDecode: (items) =>
|
|
98
97
|
via(ctx, 'base64DecodeBatchPacked', unpackByteResults, [true, false])(items),
|
|
99
|
-
jsonPatch: (docs, patches) =>
|
|
98
|
+
jsonPatch: (docs, patches) =>
|
|
99
|
+
via2(ctx, 'jsonPatchBatchPacked', unpackByteResults)(docs, patches),
|
|
100
100
|
hexEncode: via(ctx, 'hexEncodeBatchPacked', unpackByteResults),
|
|
101
101
|
hexDecode: via(ctx, 'hexDecodeBatchPacked', unpackByteResults),
|
|
102
102
|
base64Encode: (items, urlSafe, padding) =>
|
|
103
|
-
via(ctx, 'base64EncodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(
|
|
103
|
+
via(ctx, 'base64EncodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(
|
|
104
|
+
items,
|
|
105
|
+
),
|
|
104
106
|
base64Decode: (items, urlSafe, padding) =>
|
|
105
|
-
via(ctx, 'base64DecodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(
|
|
107
|
+
via(ctx, 'base64DecodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(
|
|
108
|
+
items,
|
|
109
|
+
),
|
|
106
110
|
schemaValidate(validator, items) {
|
|
107
111
|
return schemaValidateBatch(validator, items)
|
|
108
112
|
},
|
|
@@ -131,7 +135,8 @@ export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
|
131
135
|
hmacSha256Verify: (items, sigs, key) =>
|
|
132
136
|
via2(ctx, 'hmacSha256VerifyBatchPacked', unpackBitset, [key])(items, sigs),
|
|
133
137
|
// ── Two-list ──
|
|
134
|
-
passwordVerify: (items, phcs) =>
|
|
138
|
+
passwordVerify: (items, phcs) =>
|
|
139
|
+
via2(ctx, 'passwordVerifyBatchPacked', unpackBitset)(items, phcs),
|
|
135
140
|
urlResolve: (bases, references) =>
|
|
136
141
|
via2(ctx, 'urlResolveBatchPacked', unpackByteResults)(bases, references),
|
|
137
142
|
templateRender: (source, contexts) =>
|
|
@@ -143,9 +148,11 @@ export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
|
143
148
|
nowSeconds ?? Math.floor(Date.now() / 1000),
|
|
144
149
|
])(items),
|
|
145
150
|
sseEncode: (items, event, id, retry) =>
|
|
146
|
-
via(ctx, 'sseEncodeBatchPacked', unpackByteResults, [
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
via(ctx, 'sseEncodeBatchPacked', unpackByteResults, [
|
|
152
|
+
event ?? null,
|
|
153
|
+
id ?? null,
|
|
154
|
+
retry ?? null,
|
|
155
|
+
])(items),
|
|
149
156
|
wsFrameEncode: (items, opcode, mask, fin) =>
|
|
150
157
|
via(ctx, 'wsFrameEncodeBatchPacked', unpackByteResults, [opcode, mask, fin])(items),
|
|
151
158
|
wsFrameDecode: via(ctx, 'wsFrameDecodeBatchPacked', unpackByteResults),
|
package/src/rust-ffi/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ export type {
|
|
|
37
37
|
HmacSignerInstance,
|
|
38
38
|
MediaTypeParserInstance,
|
|
39
39
|
MediaTypeResult,
|
|
40
|
+
MetricsRegistryInstance,
|
|
40
41
|
MultipartPart,
|
|
41
42
|
PasswordHashOptions,
|
|
42
43
|
SchemaError,
|
|
@@ -46,20 +47,20 @@ export type {
|
|
|
46
47
|
WsFrame,
|
|
47
48
|
} from '../native'
|
|
48
49
|
export type { SchemaValidator } from '../shared/packed'
|
|
49
|
-
export type {
|
|
50
|
-
export type { RustClient } from './client'
|
|
51
|
-
export { createRust, rust } from './client'
|
|
52
|
-
export { proven } from './proven'
|
|
50
|
+
export type { ProvenEntry, ProvenImpl, ProvenStatus } from '../shared/proven'
|
|
53
51
|
export {
|
|
54
|
-
PROVEN_SELECTION,
|
|
55
52
|
isProven,
|
|
53
|
+
PROVEN_SELECTION,
|
|
56
54
|
provenEntry,
|
|
57
55
|
provenImpl,
|
|
58
56
|
provenStatus,
|
|
59
57
|
provenSummary,
|
|
60
58
|
provenSurface,
|
|
61
59
|
} from '../shared/proven'
|
|
62
|
-
export type {
|
|
60
|
+
export type { RustBatch } from './batch'
|
|
61
|
+
export type { RustClient } from './client'
|
|
62
|
+
export { createRust, rust } from './client'
|
|
63
63
|
export type { RustOptions } from './options'
|
|
64
64
|
export type { RustPacked } from './packed'
|
|
65
|
+
export { proven } from './proven'
|
|
65
66
|
export type { RustText } from './text'
|
package/src/rust-ffi/options.ts
CHANGED
|
@@ -50,9 +50,7 @@ export function resolveRayonThreads(explicit?: number): number {
|
|
|
50
50
|
return Math.max(1, Math.min(cores, Math.floor(explicit)))
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const envThreads = Number(
|
|
54
|
-
resolveEnvVar('CASTRUM_RAYON_THREADS'),
|
|
55
|
-
)
|
|
53
|
+
const envThreads = Number(resolveEnvVar('CASTRUM_RAYON_THREADS'))
|
|
56
54
|
|
|
57
55
|
if (Number.isFinite(envThreads) && envThreads > 0) {
|
|
58
56
|
return Math.max(1, Math.min(cores, Math.floor(envThreads)))
|
package/src/rust-ffi/proven.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// live benchmark audit and no `@deprecated` JSDoc machinery (the old
|
|
9
9
|
// `check:proven` / `check:annotate` friction, intentionally not re-added).
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { type RustClient, rust } from './client'
|
|
12
12
|
|
|
13
13
|
/** The full `rust.*` surface — identical to `rust`; the proven wiring is baked (see {@link ../shared/proven}). */
|
|
14
14
|
export type ProvenClient = RustClient
|
|
@@ -16,13 +16,13 @@ export type ProvenClient = RustClient
|
|
|
16
16
|
/** The full `rust.*` surface, exposed for audit. Same object as `rust`. */
|
|
17
17
|
export const proven: ProvenClient = rust
|
|
18
18
|
|
|
19
|
+
export type { ProvenEntry, ProvenImpl, ProvenStatus } from '../shared/proven'
|
|
19
20
|
export {
|
|
20
|
-
PROVEN_SELECTION,
|
|
21
21
|
isProven,
|
|
22
|
+
PROVEN_SELECTION,
|
|
22
23
|
provenEntry,
|
|
23
24
|
provenImpl,
|
|
24
25
|
provenStatus,
|
|
25
26
|
provenSummary,
|
|
26
27
|
provenSurface,
|
|
27
28
|
} from '../shared/proven'
|
|
28
|
-
export type { ProvenEntry, ProvenImpl, ProvenStatus } from '../shared/proven'
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import type { PasswordHashOptions } from '../../native'
|
|
12
12
|
import { encoder } from '../../shared/bytes'
|
|
13
|
-
import { writeInto } from '../into'
|
|
14
13
|
import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
|
|
14
|
+
import { writeInto } from '../into'
|
|
15
15
|
|
|
16
16
|
/** Auth / crypto scalar methods (`Pick<RustScalar, ...>`). */
|
|
17
17
|
export function buildCrypto(ctx: RustClientContext) {
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
JwtSignerInstance,
|
|
17
17
|
MediaTypeMatcherInstance,
|
|
18
18
|
MediaTypeParserInstance,
|
|
19
|
+
MetricsRegistryInstance,
|
|
19
20
|
PasswordHashOptions,
|
|
20
21
|
RateLimiterInstance,
|
|
21
22
|
SchemaValidatorInstance,
|
|
@@ -329,6 +330,71 @@ function ffiFormParser(ffi: BunFFI): FormParserInstance {
|
|
|
329
330
|
}
|
|
330
331
|
}
|
|
331
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Caller-owned-handle `MetricsRegistry`: on Bun the whole surface runs through
|
|
335
|
+
* the C-ABI (`castrum_metrics_*`) with a native handle — no napi instance is
|
|
336
|
+
* involved at all (the route-stack ownership model). Label values cross as a
|
|
337
|
+
* JOINED `\u001f` string via the cstring-ARG `_str` symbols: the engine
|
|
338
|
+
* transcodes them in-engine, so the per-event JS cost is just
|
|
339
|
+
* `values.join('\u001f')` — ZERO TextEncoder work (Bun 1.4 zero-copy text).
|
|
340
|
+
*/
|
|
341
|
+
function ffiMetricsRegistry(ffi: BunFFI): MetricsRegistryInstance {
|
|
342
|
+
const handle = ffi.metricsCreate()
|
|
343
|
+
return {
|
|
344
|
+
counter(name, labelKeys) {
|
|
345
|
+
return ffi.metricsCounter(handle, name, (labelKeys ?? []).join('\u001f'))
|
|
346
|
+
},
|
|
347
|
+
gauge(name, labelKeys) {
|
|
348
|
+
return ffi.metricsGauge(handle, name, (labelKeys ?? []).join('\u001f'))
|
|
349
|
+
},
|
|
350
|
+
histogram(name, labelKeys, buckets) {
|
|
351
|
+
return ffi.metricsHistogram(
|
|
352
|
+
handle,
|
|
353
|
+
name,
|
|
354
|
+
(labelKeys ?? []).join('\u001f'),
|
|
355
|
+
(buckets ?? []).join(','),
|
|
356
|
+
)
|
|
357
|
+
},
|
|
358
|
+
record(series, values, amount) {
|
|
359
|
+
const joined = (values ?? []).join('\u001f')
|
|
360
|
+
if (!ffi.metricsRecordStr(handle, series, joined, amount ?? 1)) {
|
|
361
|
+
throw new Error('metrics record: unknown series / arity mismatch / invalid amount')
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
gaugeSet(series, values, value) {
|
|
365
|
+
const joined = (values ?? []).join('\u001f')
|
|
366
|
+
if (!ffi.metricsGaugeSetStr(handle, series, joined, value)) {
|
|
367
|
+
throw new Error('metrics gauge set: unknown series / arity mismatch')
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
render() {
|
|
371
|
+
// Probe large enough for typical registries (one native pass), then
|
|
372
|
+
// grow exactly once if ever exceeded (needed-size convention).
|
|
373
|
+
let out = new Uint8Array(8192)
|
|
374
|
+
let w = ffi.metricsRender(handle, out)
|
|
375
|
+
if (w > out.length) {
|
|
376
|
+
out = new Uint8Array(w)
|
|
377
|
+
w = ffi.metricsRender(handle, out)
|
|
378
|
+
}
|
|
379
|
+
return decodeUtf8(out.subarray(0, w))
|
|
380
|
+
},
|
|
381
|
+
destroy() {
|
|
382
|
+
ffi.metricsDestroy(handle)
|
|
383
|
+
},
|
|
384
|
+
snapshot() {
|
|
385
|
+
// Packed v1 dump — decoded by consumers (the @ignex/native metrics
|
|
386
|
+
// wrapper); probe large, grow exactly once.
|
|
387
|
+
let out = new Uint8Array(4096)
|
|
388
|
+
let w = ffi.metricsSnapshot(handle, out)
|
|
389
|
+
if (w > out.length) {
|
|
390
|
+
out = new Uint8Array(w)
|
|
391
|
+
w = ffi.metricsSnapshot(handle, out)
|
|
392
|
+
}
|
|
393
|
+
return out.subarray(0, w)
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
332
398
|
/** Compiled-once factory + runtime-control methods (`Pick<RustScalar, ...>`). */
|
|
333
399
|
export function buildFactories(ctx: RustClientContext) {
|
|
334
400
|
const { transport } = ctx.runtime
|
|
@@ -450,6 +516,13 @@ export function buildFactories(ctx: RustClientContext) {
|
|
|
450
516
|
}
|
|
451
517
|
return new addon.RateLimiter(limit, windowMs, maxEntries ?? undefined)
|
|
452
518
|
},
|
|
519
|
+
createMetricsRegistry(): MetricsRegistryInstance {
|
|
520
|
+
// Fully C-ABI-backed on Bun (native handle owned by the wrapper — no
|
|
521
|
+
// napi instance involved); napi class on Node / fallback.
|
|
522
|
+
const ffi = transport.ffi
|
|
523
|
+
if (ffi) return ffiMetricsRegistry(ffi)
|
|
524
|
+
return new addon.MetricsRegistry()
|
|
525
|
+
},
|
|
453
526
|
initThreadPool(threads?: number): void {
|
|
454
527
|
// Explicit user call also establishes the pool state locally.
|
|
455
528
|
ctx.markPoolInitialized()
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// (bun:ffi first, napi fallback) — no inline `isBun()` / `getBunFFI()`.
|
|
10
10
|
|
|
11
11
|
import { toBytes } from '../../shared/bytes'
|
|
12
|
-
import { writeInto } from '../into'
|
|
13
12
|
import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
|
|
13
|
+
import { writeInto } from '../into'
|
|
14
14
|
import { asBigInt, asNumber } from '../options'
|
|
15
15
|
|
|
16
16
|
/** Hashing scalar methods (`Pick<RustScalar, ...>`). */
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
import type { EncodingPrefResult, MediaTypeResult } from '../../native'
|
|
13
13
|
import type { BunFFI } from '../../native/ffi'
|
|
14
14
|
import { decoder } from '../../shared/bytes'
|
|
15
|
-
import { decodeUtf8Fatal } from '../../shared/codec'
|
|
15
|
+
import { decodeUtf8Fatal, decodeUtf8Range } from '../../shared/codec'
|
|
16
16
|
import { packPairs } from '../../shared/packed'
|
|
17
|
-
import { writeInto } from '../into'
|
|
18
17
|
import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
|
|
18
|
+
import { writeInto } from '../into'
|
|
19
19
|
|
|
20
20
|
// Mirror napi `url_decode`'s UTF-8 validation (rust/http/url_codec.rs): only
|
|
21
21
|
// validate when a high byte is present (the ASCII fast path skips it — same as
|
|
@@ -60,23 +60,25 @@ function u32LE(b: Uint8Array, off: number): number {
|
|
|
60
60
|
* napi `HashMap` (which keeps every param including charset/boundary).
|
|
61
61
|
*/
|
|
62
62
|
function unpackMediaType(packed: Uint8Array): MediaTypeResult {
|
|
63
|
+
// Ranged decode straight off the (pooled) packed buffer — no per-field
|
|
64
|
+
// subarray views. ASCII fields take the latin1 fast path.
|
|
63
65
|
let off = 0
|
|
64
66
|
const mtLen = u32LE(packed, off)
|
|
65
67
|
off += 4
|
|
66
|
-
const mediaType =
|
|
68
|
+
const mediaType = decodeUtf8Range(packed, off, off + mtLen)
|
|
67
69
|
off += mtLen
|
|
68
70
|
let charset: string | null = null
|
|
69
71
|
const csLen = u32LE(packed, off)
|
|
70
72
|
off += 4
|
|
71
73
|
if (csLen !== 0xffffffff) {
|
|
72
|
-
charset =
|
|
74
|
+
charset = decodeUtf8Range(packed, off, off + csLen)
|
|
73
75
|
off += csLen
|
|
74
76
|
}
|
|
75
77
|
let boundary: string | null = null
|
|
76
78
|
const bLen = u32LE(packed, off)
|
|
77
79
|
off += 4
|
|
78
80
|
if (bLen !== 0xffffffff) {
|
|
79
|
-
boundary =
|
|
81
|
+
boundary = decodeUtf8Range(packed, off, off + bLen)
|
|
80
82
|
off += bLen
|
|
81
83
|
}
|
|
82
84
|
const count = u32LE(packed, off)
|
|
@@ -85,11 +87,11 @@ function unpackMediaType(packed: Uint8Array): MediaTypeResult {
|
|
|
85
87
|
for (let i = 0; i < count; i++) {
|
|
86
88
|
const kLen = u32LE(packed, off)
|
|
87
89
|
off += 4
|
|
88
|
-
const key =
|
|
90
|
+
const key = decodeUtf8Range(packed, off, off + kLen)
|
|
89
91
|
off += kLen
|
|
90
92
|
const vLen = u32LE(packed, off)
|
|
91
93
|
off += 4
|
|
92
|
-
const val =
|
|
94
|
+
const val = decodeUtf8Range(packed, off, off + vLen)
|
|
93
95
|
off += vLen
|
|
94
96
|
params[key] = val
|
|
95
97
|
}
|
|
@@ -97,18 +99,22 @@ function unpackMediaType(packed: Uint8Array): MediaTypeResult {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/** Unpack the `castrum_parse_accept_encoding` verdict (f32 q-values) into the
|
|
100
|
-
* napi-shaped array. Layout: `[u32 count]{[u32 encLen][enc][f32 q][u32 order]}`.
|
|
102
|
+
* napi-shaped array. Layout: `[u32 count]{[u32 encLen][enc][f32 q][u32 order]}`.
|
|
103
|
+
* Ranged decode off the pooled buffer + shared f32 bit-reinterpret views —
|
|
104
|
+
* no per-field subarrays, no per-call DataView. */
|
|
105
|
+
const f32Bits = new Float32Array(1)
|
|
106
|
+
const f32BitsU32 = new Uint32Array(f32Bits.buffer)
|
|
101
107
|
function unpackAcceptEncoding(packed: Uint8Array): EncodingPrefResult[] {
|
|
102
108
|
const count = u32LE(packed, 0)
|
|
103
109
|
let off = 4
|
|
104
110
|
const out: EncodingPrefResult[] = []
|
|
105
|
-
const view = new DataView(packed.buffer, packed.byteOffset, packed.byteLength)
|
|
106
111
|
for (let i = 0; i < count; i++) {
|
|
107
112
|
const encLen = u32LE(packed, off)
|
|
108
113
|
off += 4
|
|
109
|
-
const encoding =
|
|
114
|
+
const encoding = decodeUtf8Range(packed, off, off + encLen)
|
|
110
115
|
off += encLen
|
|
111
|
-
|
|
116
|
+
f32BitsU32[0] = u32LE(packed, off)
|
|
117
|
+
const q: number = f32Bits[0] ?? 0
|
|
112
118
|
off += 4
|
|
113
119
|
const order = u32LE(packed, off)
|
|
114
120
|
off += 4
|
|
@@ -15,6 +15,7 @@ import { buildHashing } from './hashing'
|
|
|
15
15
|
import { buildHttp } from './http'
|
|
16
16
|
import { buildJson } from './json'
|
|
17
17
|
import { buildPayload } from './payload'
|
|
18
|
+
import { buildValidation } from './validation'
|
|
18
19
|
|
|
19
20
|
export type { RustScalar } from './interface'
|
|
20
21
|
|
|
@@ -26,6 +27,7 @@ export function buildScalar(ctx: RustClientContext): import('./interface').RustS
|
|
|
26
27
|
...buildHttp(ctx),
|
|
27
28
|
...buildCrypto(ctx),
|
|
28
29
|
...buildPayload(ctx),
|
|
30
|
+
...buildValidation(ctx),
|
|
29
31
|
...buildFactories(ctx),
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
MediaTypeMatcherInstance,
|
|
20
20
|
MediaTypeParserInstance,
|
|
21
21
|
MediaTypeResult,
|
|
22
|
+
MetricsRegistryInstance,
|
|
22
23
|
MultipartPart,
|
|
23
24
|
PasswordHashOptions,
|
|
24
25
|
RateLimiterInstance,
|
|
@@ -67,6 +68,18 @@ export interface RustScalar {
|
|
|
67
68
|
validateUuid(input: Uint8Array): boolean
|
|
68
69
|
validateIpv4(input: Uint8Array): boolean
|
|
69
70
|
validateIpv6(input: Uint8Array): boolean
|
|
71
|
+
/**
|
|
72
|
+
* Batch fixed-width hex validation: NEWLINE-separated lines in → one
|
|
73
|
+
* verdict byte (1/0) per line. One native call replaces per-item
|
|
74
|
+
* `/^[0-9a-f]{width}$/` regex loops (ObjectIds = width 24). Accepts bytes,
|
|
75
|
+
* a joined string, or an array of id strings (joined internally).
|
|
76
|
+
*/
|
|
77
|
+
hexValidateBatch(input: Uint8Array | readonly string[], width: number): Uint8Array
|
|
78
|
+
/**
|
|
79
|
+
* Escape JS-RegExp metacharacters so `new RegExp(escaped, flags)` matches
|
|
80
|
+
* the input literally — the safe literal-substring-search primitive.
|
|
81
|
+
*/
|
|
82
|
+
regexEscape(input: Uint8Array | string): string
|
|
70
83
|
wsAcceptKey(key: Uint8Array): Uint8Array | string
|
|
71
84
|
wsAcceptKeyInto(key: Uint8Array, output: Uint8Array): number
|
|
72
85
|
|
|
@@ -212,6 +225,13 @@ export interface RustScalar {
|
|
|
212
225
|
windowMs: number,
|
|
213
226
|
maxEntries?: number | null,
|
|
214
227
|
): RateLimiterInstance
|
|
228
|
+
/**
|
|
229
|
+
* Create a sharded metrics registry (counters / gauges / histograms +
|
|
230
|
+
* Prometheus render). Fully C-ABI-backed on Bun (caller-owned native
|
|
231
|
+
* handle); the napi class on Node. Call `destroy()` on ffi-backed
|
|
232
|
+
* registries at shutdown.
|
|
233
|
+
*/
|
|
234
|
+
createMetricsRegistry(): MetricsRegistryInstance
|
|
215
235
|
initThreadPool(rayonThreads?: number): void
|
|
216
236
|
rayonNumThreads(): number
|
|
217
237
|
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import type { MultipartPart, WsFrame } from '../../native'
|
|
12
12
|
import { decoder } from '../../shared/bytes'
|
|
13
|
-
import { writeInto } from '../into'
|
|
14
13
|
import { memoizeFfi, type RustClientContext, resolveNative } from '../context'
|
|
14
|
+
import { writeInto } from '../into'
|
|
15
15
|
|
|
16
16
|
/** Read an unsigned little-endian u32 at `off`. */
|
|
17
17
|
function u32LE(b: Uint8Array, off: number): number {
|
|
@@ -102,7 +102,11 @@ export function buildPayload(ctx: RustClientContext) {
|
|
|
102
102
|
// it via the `_into` core; keeps the 64 MiB decompression-bomb cap).
|
|
103
103
|
const f = ffi()
|
|
104
104
|
if (f) return f.gzipDecompressInto(data, output, maxDecompressed ?? undefined)
|
|
105
|
-
return writeInto(
|
|
105
|
+
return writeInto(
|
|
106
|
+
'gzip decompress',
|
|
107
|
+
output,
|
|
108
|
+
addon.gzipDecompress(data, maxDecompressed ?? null),
|
|
109
|
+
)
|
|
106
110
|
},
|
|
107
111
|
brotliCompress(data: Uint8Array, quality?: number | null): Uint8Array {
|
|
108
112
|
const f = ffi()
|
|
@@ -127,7 +131,11 @@ export function buildPayload(ctx: RustClientContext) {
|
|
|
127
131
|
// Pooled sibling — caller-owned output buffer (keeps the 64 MiB cap).
|
|
128
132
|
const f = ffi()
|
|
129
133
|
if (f) return f.brotliDecompressInto(data, output, maxDecompressed ?? undefined)
|
|
130
|
-
return writeInto(
|
|
134
|
+
return writeInto(
|
|
135
|
+
'brotli decompress',
|
|
136
|
+
output,
|
|
137
|
+
addon.brotliDecompress(data, maxDecompressed ?? null),
|
|
138
|
+
)
|
|
131
139
|
},
|
|
132
140
|
multipartParse(body: Uint8Array, boundary: Uint8Array): MultipartPart[] {
|
|
133
141
|
// FFI-first: packed parts (castrum_multipart_parse_packed) → unpack to
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/rust-ffi/scalar/validation.ts — Validation & text-utility scalar methods.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors rust/util/validation.rs (batch fixed-width hex validation) and
|
|
4
|
+
// rust/util/text.rs (JS-RegExp metacharacter escaping). Stateless; on Bun the
|
|
5
|
+
// string paths cross as cstring ARGs (engine-transcoded — zero TextEncoder)
|
|
6
|
+
// and regexEscape returns via a cstring return (engine-cloned — zero decode).
|
|
7
|
+
|
|
8
|
+
import { encoder, toBytes } from '../../shared/bytes'
|
|
9
|
+
import { decodeUtf8 } from '../../shared/codec'
|
|
10
|
+
import type { RustClientContext } from '../context'
|
|
11
|
+
import { memoizeFfi, resolveNative } from '../context'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Non-zeroing output allocation for the batch verdicts (`Buffer.allocUnsafe`
|
|
15
|
+
* skips the 4 KB memset a plain `new Uint8Array` pays; every byte is
|
|
16
|
+
* overwritten by the native write anyway).
|
|
17
|
+
*/
|
|
18
|
+
const allocOut = (n: number): Uint8Array =>
|
|
19
|
+
typeof Buffer !== 'undefined' && typeof Buffer.allocUnsafe === 'function'
|
|
20
|
+
? Buffer.allocUnsafe(n)
|
|
21
|
+
: new Uint8Array(n)
|
|
22
|
+
|
|
23
|
+
/** Validation / text-utility methods (`Pick<RustScalar, ...>`). */
|
|
24
|
+
export function buildValidation(ctx: RustClientContext) {
|
|
25
|
+
const { transport } = ctx.runtime
|
|
26
|
+
const n = (name: string) => resolveNative(ctx, name)
|
|
27
|
+
// Lazy-memoized ffi surface: binds on first call (no eager dlopen at import).
|
|
28
|
+
const ffi = memoizeFfi(transport)
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
/**
|
|
32
|
+
* Validate NEWLINE-separated strings as fixed-width hex (e.g. Mongo
|
|
33
|
+
* ObjectIds at width 24) in ONE native call → one verdict byte per line.
|
|
34
|
+
* Replaces the per-item `/^[0-9a-f]{24}$/` regex loops list endpoints
|
|
35
|
+
* scatter everywhere. Accepts an array of id strings directly — the join
|
|
36
|
+
* crosses as a `cstring` ARG, so the JS side never touches an encoder.
|
|
37
|
+
*
|
|
38
|
+
* @param input Newline-separated candidates as bytes, or an array of
|
|
39
|
+
* candidate strings (joined internally with `\n`)
|
|
40
|
+
* @param width Exact expected length of each line (1..=4096)
|
|
41
|
+
* @returns Uint8Array of 1/0 verdicts, one per line (trailing newline
|
|
42
|
+
* does not add an extra verdict)
|
|
43
|
+
* @throws On `width` outside 1..=4096
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* rust.hexValidateBatch(['507f1f77bcf86cd799439011', 'nope'], 24) // [1, 0]
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
hexValidateBatch(input: Uint8Array | readonly string[], width: number): Uint8Array {
|
|
50
|
+
if (width === 0 || width > 4096 || !Number.isInteger(width)) {
|
|
51
|
+
throw new Error('hex validate batch: width must be an integer 1..=4096')
|
|
52
|
+
}
|
|
53
|
+
const f = ffi()
|
|
54
|
+
if (f) {
|
|
55
|
+
// Verdict count ≤ line count ≤ input length — one native pass.
|
|
56
|
+
const joined =
|
|
57
|
+
typeof input === 'string'
|
|
58
|
+
? input
|
|
59
|
+
: Array.isArray(input)
|
|
60
|
+
? (input as readonly string[]).join('\n')
|
|
61
|
+
: null
|
|
62
|
+
let out = allocOut(Math.min(joined !== null ? joined.length + 1 : input.length + 1, 4096))
|
|
63
|
+
let w =
|
|
64
|
+
joined !== null
|
|
65
|
+
? f.hexValidateBatchStr(joined, width, out)
|
|
66
|
+
: f.hexValidateBatchInto(input as Uint8Array, width, out)
|
|
67
|
+
if (w > out.length) {
|
|
68
|
+
out = new Uint8Array(w)
|
|
69
|
+
w =
|
|
70
|
+
joined !== null
|
|
71
|
+
? f.hexValidateBatchStr(joined, width, out)
|
|
72
|
+
: f.hexValidateBatchInto(input as Uint8Array, width, out)
|
|
73
|
+
}
|
|
74
|
+
return out.subarray(0, w)
|
|
75
|
+
}
|
|
76
|
+
const bytes = Array.isArray(input)
|
|
77
|
+
? encoder.encode((input as readonly string[]).join('\n'))
|
|
78
|
+
: (input as Uint8Array)
|
|
79
|
+
return n('hexValidateBatch')(bytes, width) as Uint8Array
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Escape JS-RegExp metacharacters (`\ . * + ? ^ $ { } | ( ) [ ]`) so a
|
|
84
|
+
* pattern built with `new RegExp(rust.regexEscape(userInput), 'i')`
|
|
85
|
+
* matches the input LITERALLY. Under Bun the whole call is the FFI
|
|
86
|
+
* crossing + the escape: the input crosses as a `cstring` ARG (engine-
|
|
87
|
+
* transcoded, zero encode) and the result returns as a `cstring`
|
|
88
|
+
* (engine-cloned, zero decode).
|
|
89
|
+
*
|
|
90
|
+
* @param input Arbitrary text or bytes (multi-byte UTF-8 passes through)
|
|
91
|
+
* @returns The escaped pattern string
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* const re = new RegExp(rust.regexEscape('a.c*(x)'), 'i')
|
|
95
|
+
* re.test('A.C(X)') // true — literal match, not wildcard semantics
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
regexEscape(input: Uint8Array | string): string {
|
|
99
|
+
const f = ffi()
|
|
100
|
+
if (f) {
|
|
101
|
+
if (typeof input === 'string') {
|
|
102
|
+
const escaped = f.regexEscapeStr(input)
|
|
103
|
+
if (escaped === null) throw new Error('regex escape failed')
|
|
104
|
+
return escaped
|
|
105
|
+
}
|
|
106
|
+
// Bytes path: needed-size convention with a generous first guess.
|
|
107
|
+
const bytes = toBytes(input)
|
|
108
|
+
let out = new Uint8Array(bytes.length + 16)
|
|
109
|
+
let w = f.regexEscapeInto(bytes, out)
|
|
110
|
+
if (w > out.length) {
|
|
111
|
+
out = new Uint8Array(w)
|
|
112
|
+
w = f.regexEscapeInto(bytes, out)
|
|
113
|
+
}
|
|
114
|
+
return decodeUtf8(out.subarray(0, w))
|
|
115
|
+
}
|
|
116
|
+
// napi takes/returns strings directly.
|
|
117
|
+
const s = typeof input === 'string' ? input : decodeUtf8(toBytes(input))
|
|
118
|
+
return n('regexEscape')(s) as string
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
}
|