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
|
@@ -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
|
+
}
|
package/src/rust-ffi/text.ts
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
// src/rust-ffi/text.ts — String-oriented FFI namespace (text in → text out).
|
|
2
2
|
//
|
|
3
3
|
// `rust.text.*` is the ergonomic string API over the byte-level native
|
|
4
|
-
// functions.
|
|
4
|
+
// functions. MOST of it is FFI-first with `cstring` args/returns: the engine
|
|
5
|
+
// transcodes a JS string in-call, so there is no JS-side encode at all (and
|
|
6
|
+
// byte-arg ops are not involved). Encode/decode wrappers exist only for the
|
|
7
|
+
// napi fallback and for ops with no cstring form — plus a NUL guard wherever a
|
|
8
|
+
// `cstring` ARG could truncate a verdict or a byte-exact value
|
|
9
|
+
// (docs/FFI_BUN_GUIDE.md §6.1).
|
|
5
10
|
//
|
|
6
11
|
// Runtime dispatch is centralized in the adapter (`ctx.runtime`): the Bun
|
|
7
12
|
// built-in delegations (urlEncode/urlDecode) come from `builtins.has(op)` and
|
|
8
13
|
// the native call from `transport.ffi` / `transport.resolve` (bun:ffi first,
|
|
9
14
|
// napi fallback) — no inline `isBun()` / `getBunFFI()`.
|
|
10
15
|
|
|
11
|
-
import { decoder, encoder } from '../shared/bytes'
|
|
16
|
+
import { decoder, encoder, hasNul } from '../shared/bytes'
|
|
12
17
|
import { type RustClientContext, resolveNative } from './context'
|
|
13
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Scratch for the byte-path `Sec-WebSocket-Accept` fallback (28 chars; the
|
|
21
|
+
* NUL case only). Decoded synchronously before returning, so reuse is safe.
|
|
22
|
+
*/
|
|
23
|
+
const WS_ACCEPT_OUT = new Uint8Array(64)
|
|
24
|
+
|
|
14
25
|
/** String-oriented FFI namespace. */
|
|
15
26
|
export interface RustText {
|
|
16
27
|
/**
|
|
@@ -91,8 +102,19 @@ export function buildText(ctx: RustClientContext): RustText {
|
|
|
91
102
|
// FFI-first: `cstring` ARG + cstring return (native transfer both ways —
|
|
92
103
|
// the engine transcodes the key in-engine, no JS encode). napi keeps the
|
|
93
104
|
// encode/decode path.
|
|
105
|
+
//
|
|
106
|
+
// Exception: a NUL would truncate the `cstring` ARG, so the accept value
|
|
107
|
+
// would be computed for a PREFIX of the key. Encode and take the byte path
|
|
108
|
+
// in that (practically unreachable) case: still a wrong key for a client
|
|
109
|
+
// that sent a NUL, but wrong for the bytes the caller actually passed.
|
|
94
110
|
const f = transport.ffi
|
|
95
|
-
if (f)
|
|
111
|
+
if (f) {
|
|
112
|
+
if (!hasNul(key)) return f.wsAcceptKey(key)
|
|
113
|
+
const w = f.wsAcceptKeyInto(encoder.encode(key), WS_ACCEPT_OUT)
|
|
114
|
+
return w > 0 && w <= WS_ACCEPT_OUT.length
|
|
115
|
+
? decoder.decode(WS_ACCEPT_OUT.subarray(0, w))
|
|
116
|
+
: ''
|
|
117
|
+
}
|
|
96
118
|
return decoder.decode(resolveNative(ctx, 'wsAcceptKey')(encoder.encode(key)) as Uint8Array)
|
|
97
119
|
},
|
|
98
120
|
validateEmail(input) {
|
package/src/selection.ts
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
// live wiring, so it can't drift.
|
|
19
19
|
|
|
20
20
|
import { getAddon, lazyAddon } from './native'
|
|
21
|
-
import { provenImpl } from './shared/proven'
|
|
22
21
|
import { runtimeNative } from './runtime/native'
|
|
22
|
+
import { provenImpl } from './shared/proven'
|
|
23
23
|
|
|
24
24
|
/** Recommended implementation for an operation. */
|
|
25
25
|
export type OpImpl = 'native' | 'js'
|
package/src/shared/bytes.ts
CHANGED
|
@@ -4,10 +4,11 @@ import { decodeUtf8, encodeUtf8, encodeUtf8Into } from './codec'
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Shared UTF-8 encoder (encode / encodeInto), backed by the runtime-native
|
|
7
|
-
* codec (`src/shared/codec.ts`):
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* codec (`src/shared/codec.ts`): a shared native `TextEncoder` singleton on
|
|
8
|
+
* both runtimes (on Bun 1.4 the engine-native implementation measures ~3x
|
|
9
|
+
* faster than the old `Bun.ArrayBufferSink` path) plus a zero-copy `Buffer`
|
|
10
|
+
* view write for `encodeInto`. Reusing a single object avoids allocation
|
|
11
|
+
* overhead in hot paths.
|
|
11
12
|
*
|
|
12
13
|
* `encode` accepts the string-or-bytes union the text-returning `rust.*` ops
|
|
13
14
|
* surface (Bun returns strings, Node returns bytes) and normalizes to bytes —
|
|
@@ -133,3 +134,22 @@ export function toPlainBuffer(bytes: Uint8Array): Uint8Array {
|
|
|
133
134
|
copy.set(bytes)
|
|
134
135
|
return copy
|
|
135
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Does this string contain a NUL (`U+0000`)?
|
|
140
|
+
*
|
|
141
|
+
* `bun:ffi` transcodes a JS string passed to a `cstring` ARG into a
|
|
142
|
+
* NUL-TERMINATED UTF-8 buffer, so an embedded NUL SILENTLY TRUNCATES the value
|
|
143
|
+
* natively (Bun's docs describe the transcode; the truncation is the C
|
|
144
|
+
* convention it inherits). That is fine for developer-supplied config, and a
|
|
145
|
+
* real bug wherever the callee answers with a VERDICT (a truncated prefix
|
|
146
|
+
* passes where the full input fails) or where the exact bytes ARE the result.
|
|
147
|
+
*
|
|
148
|
+
* Cheap: one `indexOf` scan, ~4 ns for typical inputs.
|
|
149
|
+
*
|
|
150
|
+
* @param s - The string to check.
|
|
151
|
+
* @returns `true` when a NUL is present.
|
|
152
|
+
*/
|
|
153
|
+
export function hasNul(s: string): boolean {
|
|
154
|
+
return s.indexOf('\u0000') !== -1
|
|
155
|
+
}
|
package/src/shared/codec.ts
CHANGED
|
@@ -12,9 +12,10 @@ import { runtimeCodec } from '../runtime/codec'
|
|
|
12
12
|
/**
|
|
13
13
|
* Encode a string to UTF-8 bytes.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* The implementation is
|
|
15
|
+
* Both runtimes use a shared native `TextEncoder` singleton (on Bun 1.4 the
|
|
16
|
+
* engine-native implementation measures ~3x faster than the previous
|
|
17
|
+
* `Bun.ArrayBufferSink` start/write/flush path). The implementation is
|
|
18
|
+
* selected once at load via the runtime adapter.
|
|
18
19
|
*
|
|
19
20
|
* @param s - The string to encode to UTF-8 bytes.
|
|
20
21
|
* @returns A fresh `Uint8Array` of the UTF-8 encoding of `s`.
|
|
@@ -64,6 +65,20 @@ export const encodeUtf8Into: (s: string, dest: Uint8Array, offset?: number) => n
|
|
|
64
65
|
*/
|
|
65
66
|
export const decodeUtf8: (bytes: Uint8Array) => string = runtimeCodec.decodeUtf8
|
|
66
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Decode the byte RANGE `[start, end)` of `bytes` to a string (replacement
|
|
70
|
+
* mode on invalid). Zero-copy ranged decode for packed-wire unpackers — no
|
|
71
|
+
* per-field `subarray` view. ASCII ranges take a latin1 fast path (~2x on
|
|
72
|
+
* Bun); multi-byte ranges fall back to real UTF-8 decoding of that range.
|
|
73
|
+
*
|
|
74
|
+
* @param bytes - The backing byte buffer.
|
|
75
|
+
* @param start - Range start (inclusive, relative to the VIEW).
|
|
76
|
+
* @param end - Range end (exclusive, relative to the VIEW).
|
|
77
|
+
* @returns The decoded string.
|
|
78
|
+
*/
|
|
79
|
+
export const decodeUtf8Range: (bytes: Uint8Array, start: number, end: number) => string =
|
|
80
|
+
runtimeCodec.decodeUtf8Range
|
|
81
|
+
|
|
67
82
|
/**
|
|
68
83
|
* Decode UTF-8 bytes to a string, throwing on invalid UTF-8.
|
|
69
84
|
*
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/task/index.ts — public barrel for the off-thread task runtime.
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
TASK_OP,
|
|
5
|
+
TASK_STATUS_CANCELLED,
|
|
6
|
+
TASK_STATUS_ERROR,
|
|
7
|
+
TASK_STATUS_OK,
|
|
8
|
+
brotliCapacityGuess,
|
|
9
|
+
encodeArgon2VerifyArgs,
|
|
10
|
+
encodeBrotliDecompressArgs,
|
|
11
|
+
encodeGzipCompressHeader,
|
|
12
|
+
encodeGzipDecompressArgs,
|
|
13
|
+
encodePbkdf2Args,
|
|
14
|
+
gzipCompressUpperBound,
|
|
15
|
+
} from './op'
|
|
16
|
+
export type { TaskOpName } from './op'
|
|
17
|
+
export { createTaskRuntime } from './runtime'
|
|
18
|
+
export type {
|
|
19
|
+
Pbkdf2RunOptions,
|
|
20
|
+
TaskRunOptions,
|
|
21
|
+
TaskRuntime,
|
|
22
|
+
TaskRuntimeOptions,
|
|
23
|
+
TaskStats,
|
|
24
|
+
} from './runtime'
|
package/src/task/op.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// src/task/op.ts — pure task-op registry: numeric ids + packed-arg encoders.
|
|
2
|
+
//
|
|
3
|
+
// PURE by design (no addon, no module state) so the wire encoding is directly
|
|
4
|
+
// unit-testable. The ids MUST match the `OP_*` constants in `rust/task/ops.rs`;
|
|
5
|
+
// `test/unit/task/runtime.test.ts` pins the round-trip.
|
|
6
|
+
|
|
7
|
+
/** Numeric op ids — mirror `rust/task/ops.rs`. */
|
|
8
|
+
export const TASK_OP = {
|
|
9
|
+
/** args `[u32 maxDecompressed LE][gzip bytes]` (0 = native 64 MiB default). */
|
|
10
|
+
gzipDecompress: 1,
|
|
11
|
+
/** args `[u32 rounds][u32 dkLen][u32 saltLen][salt][password]`. */
|
|
12
|
+
pbkdf2Sha256: 2,
|
|
13
|
+
/** `gzipDecompress` writing into a caller buffer (zero-copy output). */
|
|
14
|
+
gzipDecompressInto: 3,
|
|
15
|
+
/** args `[u32 pwLen][u32 phcLen][password][phc]` → body is one byte (1 = match). */
|
|
16
|
+
argon2Verify: 4,
|
|
17
|
+
/** args `[u32 maxDecompressed LE][brotli bytes]` (0 = native 64 MiB default). */
|
|
18
|
+
brotliDecompress: 5,
|
|
19
|
+
/** `brotliDecompress` writing into a caller buffer (zero-copy output). */
|
|
20
|
+
brotliDecompressInto: 6,
|
|
21
|
+
/** args `[u32 level][gzip bytes]` (level clamped to 0-9 natively). */
|
|
22
|
+
gzipCompress: 7,
|
|
23
|
+
} as const
|
|
24
|
+
|
|
25
|
+
/** A task op name (`gzipDecompress`). */
|
|
26
|
+
export type TaskOpName = keyof typeof TASK_OP
|
|
27
|
+
|
|
28
|
+
/** Completion status codes (mirror `rust/task/ops.rs`). */
|
|
29
|
+
export const TASK_STATUS_OK = 0
|
|
30
|
+
/** The op returned an error; the body is the UTF-8 error message. */
|
|
31
|
+
export const TASK_STATUS_ERROR = 1
|
|
32
|
+
/** The task was cancelled before/while it ran; the body is empty. */
|
|
33
|
+
export const TASK_STATUS_CANCELLED = 2
|
|
34
|
+
/** The zero-copy destination was too small; the body is the exact size (u64 LE). */
|
|
35
|
+
export const TASK_STATUS_TOO_SMALL = 3
|
|
36
|
+
/** The op has no zero-copy form; fall back to the copy path. */
|
|
37
|
+
export const TASK_STATUS_UNSUPPORTED = 4
|
|
38
|
+
|
|
39
|
+
/** Shared zero-length payload (header-only ops). */
|
|
40
|
+
const EMPTY = new Uint8Array(0)
|
|
41
|
+
|
|
42
|
+
// Shared `[u32 header][bytes]` packing for the single-header codec ops (the
|
|
43
|
+
// `maxDecompressed` cap for decompress, the level for compress).
|
|
44
|
+
function packHeaderAndBytes(header: number, data: Uint8Array, out?: Uint8Array): Uint8Array {
|
|
45
|
+
const need = 4 + data.length
|
|
46
|
+
const dest = out !== undefined && out.length >= need ? out : new Uint8Array(need)
|
|
47
|
+
// View-aware: `out` may be a subarray with a non-zero byteOffset.
|
|
48
|
+
new DataView(dest.buffer, dest.byteOffset, need).setUint32(0, header >>> 0, true)
|
|
49
|
+
dest.set(data, 4)
|
|
50
|
+
return dest.subarray(0, need)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pack the `gzip.decompress` task args: `[u32 maxDecompressed LE][gzip bytes]`.
|
|
55
|
+
*
|
|
56
|
+
* @param data Compressed gzip bytes.
|
|
57
|
+
* @param maxDecompressed Output cap in bytes; `undefined`/`0` selects the
|
|
58
|
+
* native 64 MiB decompression-bomb default.
|
|
59
|
+
* @param out Optional caller-owned destination (>= `4 + data.length`),
|
|
60
|
+
* returned as a view. Native copies args on submit, so the
|
|
61
|
+
* same buffer may be reused for the next submission — but
|
|
62
|
+
* only while the caller stays SYNCHRONOUS (a zero-copy
|
|
63
|
+
* retry re-submits after an await and must own its memory).
|
|
64
|
+
* @returns A packed args buffer.
|
|
65
|
+
*/
|
|
66
|
+
export function encodeGzipDecompressArgs(
|
|
67
|
+
data: Uint8Array,
|
|
68
|
+
maxDecompressed?: number,
|
|
69
|
+
out?: Uint8Array,
|
|
70
|
+
): Uint8Array {
|
|
71
|
+
return packHeaderAndBytes(maxDecompressed ?? 0, data, out)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Pack the `brotli.decompress` task args: `[u32 maxDecompressed LE][bytes]`.
|
|
76
|
+
*
|
|
77
|
+
* @param data Compressed brotli bytes.
|
|
78
|
+
* @param maxDecompressed Output cap in bytes (`undefined`/`0` → native 64 MiB).
|
|
79
|
+
* @param out Optional caller-owned destination (see the gzip form).
|
|
80
|
+
* @returns A packed args buffer.
|
|
81
|
+
*/
|
|
82
|
+
export function encodeBrotliDecompressArgs(
|
|
83
|
+
data: Uint8Array,
|
|
84
|
+
maxDecompressed?: number,
|
|
85
|
+
out?: Uint8Array,
|
|
86
|
+
): Uint8Array {
|
|
87
|
+
return packHeaderAndBytes(maxDecompressed ?? 0, data, out)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pack the `gzip.compress` task HEADER: `[u32 level]`. The payload is passed
|
|
92
|
+
* separately so it can be read in place (zero-copy input) instead of copied.
|
|
93
|
+
*
|
|
94
|
+
* @param level Deflate level (`0`-`9`, clamped natively).
|
|
95
|
+
* @param out Optional caller-owned destination (>= 4 bytes).
|
|
96
|
+
* @returns The 4-byte header.
|
|
97
|
+
*/
|
|
98
|
+
export function encodeGzipCompressHeader(level = 6, out?: Uint8Array): Uint8Array {
|
|
99
|
+
return packHeaderAndBytes(level, EMPTY, out)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pack the `argon2id.verify` task args:
|
|
104
|
+
* `[u32 pwLen][u32 phcLen][password][phc]`. No `out` parameter on purpose: a
|
|
105
|
+
* shared scratch would keep a plaintext password alive far longer than a
|
|
106
|
+
* per-call allocation, which is the wrong trade for a secret.
|
|
107
|
+
*
|
|
108
|
+
* @param password Candidate password bytes.
|
|
109
|
+
* @param phc PHC string bytes (`$argon2id$...`).
|
|
110
|
+
* @returns A packed args buffer owned by the caller (Rust copies it on submit).
|
|
111
|
+
*/
|
|
112
|
+
export function encodeArgon2VerifyArgs(password: Uint8Array, phc: Uint8Array): Uint8Array {
|
|
113
|
+
const out = new Uint8Array(8 + password.length + phc.length)
|
|
114
|
+
const view = new DataView(out.buffer)
|
|
115
|
+
view.setUint32(0, password.length >>> 0, true)
|
|
116
|
+
view.setUint32(4, phc.length >>> 0, true)
|
|
117
|
+
out.set(password, 8)
|
|
118
|
+
out.set(phc, 8 + password.length)
|
|
119
|
+
return out
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Safe upper bound on the gzip size of `n` input bytes: deflate's worst case is
|
|
124
|
+
* stored blocks (5 bytes per block) plus the 18-byte gzip wrapper. The 16 KiB
|
|
125
|
+
* block assumption is deliberately conservative (real blocks are 64 KiB), so a
|
|
126
|
+
* destination sized with this can never be too small — no retry round trip.
|
|
127
|
+
*
|
|
128
|
+
* @param n Uncompressed input length in bytes.
|
|
129
|
+
* @returns An upper bound in bytes.
|
|
130
|
+
*/
|
|
131
|
+
export function gzipCompressUpperBound(n: number): number {
|
|
132
|
+
return n + 5 * (Math.ceil(n / 16384) + 1) + 18
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Starting capacity for a brotli zero-copy destination. Brotli carries no
|
|
137
|
+
* ISIZE trailer, so there is no exact hint: this is a ratio guess, and the
|
|
138
|
+
* needed-size retry lands the exact size if it falls short.
|
|
139
|
+
*
|
|
140
|
+
* @param compressedLen Compressed input length in bytes.
|
|
141
|
+
* @returns A capacity guess in bytes (floored at the runtime's zero-copy size).
|
|
142
|
+
*/
|
|
143
|
+
export function brotliCapacityGuess(compressedLen: number): number {
|
|
144
|
+
return Math.max(64 * 1024, compressedLen * 8)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Pack the `pbkdf2.sha256` task args:
|
|
149
|
+
* `[u32 rounds][u32 dkLen][u32 saltLen][salt][password]`.
|
|
150
|
+
*
|
|
151
|
+
* @param password Password bytes (the PBKDF2 secret).
|
|
152
|
+
* @param salt Salt bytes.
|
|
153
|
+
* @param rounds Iteration count (clamped to >= 1 natively).
|
|
154
|
+
* @param dkLen Derived-key length in bytes (default 32, capped at 1 MiB).
|
|
155
|
+
* @returns A packed args buffer owned by the caller (Rust copies it on submit).
|
|
156
|
+
*/
|
|
157
|
+
export function encodePbkdf2Args(
|
|
158
|
+
password: Uint8Array,
|
|
159
|
+
salt: Uint8Array,
|
|
160
|
+
rounds: number,
|
|
161
|
+
dkLen = 32,
|
|
162
|
+
): Uint8Array {
|
|
163
|
+
const out = new Uint8Array(12 + salt.length + password.length)
|
|
164
|
+
const view = new DataView(out.buffer)
|
|
165
|
+
view.setUint32(0, rounds >>> 0, true)
|
|
166
|
+
view.setUint32(4, dkLen >>> 0, true)
|
|
167
|
+
view.setUint32(8, salt.length >>> 0, true)
|
|
168
|
+
out.set(salt, 12)
|
|
169
|
+
out.set(password, 12 + salt.length)
|
|
170
|
+
return out
|
|
171
|
+
}
|