castrum 0.7.0 → 0.9.0
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/LICENSE +21 -0
- package/README.md +322 -59
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +3161 -0
- package/dist/index.js +7306 -0
- package/docs/ARCHITECTURE.md +415 -0
- package/docs/BENCHMARKS.md +307 -0
- package/docs/ENVIRONMENT.md +79 -0
- package/docs/GETTING_STARTED.md +209 -0
- package/docs/INGRESS.md +745 -0
- package/docs/REPO_MAP.md +259 -0
- package/docs/adr/0001-two-wire-formats.md +39 -0
- package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
- package/docs/adr/0003-bun-builtins-delegation.md +42 -0
- package/docs/bun-builtins-decision-matrix.md +108 -0
- package/index.ts +51 -8
- package/package.json +73 -20
- package/src/data/json-rows.ts +13 -13
- package/src/ingress/body.ts +149 -0
- package/src/ingress/constants.ts +220 -48
- package/src/ingress/context.ts +136 -0
- package/src/ingress/decode/baked-result.ts +121 -0
- package/src/ingress/decode/fast-result.ts +160 -0
- package/src/ingress/decode/packed-sections.ts +56 -0
- package/src/ingress/decode/result-base.ts +172 -0
- package/src/ingress/errors.ts +72 -0
- package/src/ingress/fast.ts +152 -1021
- package/src/ingress/handlers.ts +563 -1001
- package/src/ingress/headers/baked-templates.ts +102 -0
- package/src/ingress/headers/cors.ts +44 -0
- package/src/ingress/headers/fast-templates.ts +238 -0
- package/src/ingress/headers/hsts.ts +83 -0
- package/src/ingress/health.ts +67 -0
- package/src/ingress/index.ts +130 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +75 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +150 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +96 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/routes/common.ts +38 -0
- package/src/ingress/routes/delete.ts +14 -0
- package/src/ingress/routes/echo.ts +107 -0
- package/src/ingress/routes/fallback.ts +23 -0
- package/src/ingress/routes/head.ts +37 -0
- package/src/ingress/routes/index.ts +10 -0
- package/src/ingress/routes/json-write.ts +153 -0
- package/src/ingress/routes/options.ts +36 -0
- package/src/ingress/routes/read.ts +45 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +443 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +157 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +217 -0
- package/src/integration/streaming.ts +79 -0
- package/src/integration/websocket.ts +92 -0
- package/src/loader/batch.ts +133 -0
- package/src/loader/cost.ts +104 -0
- package/src/loader/index.ts +930 -0
- package/src/loader/ops.ts +657 -0
- package/src/native/ffi.ts +1541 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +194 -0
- package/src/native/types.ts +598 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch.ts +431 -0
- package/src/rust-ffi/client.ts +88 -0
- package/src/rust-ffi/context.ts +164 -0
- package/src/rust-ffi/index.ts +40 -541
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +42 -0
- package/src/rust-ffi/scalar/crypto.ts +265 -0
- package/src/rust-ffi/scalar/factories.ts +233 -0
- package/src/rust-ffi/scalar/hashing.ts +72 -0
- package/src/rust-ffi/scalar/http.ts +206 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +450 -0
- package/src/rust-ffi/scalar/json.ts +45 -0
- package/src/rust-ffi/scalar/payload.ts +124 -0
- package/src/rust-ffi/text.ts +95 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +77 -0
- package/src/shared/adaptive.ts +68 -0
- package/src/shared/bench-classify.ts +209 -0
- package/src/shared/buffer-pool.ts +209 -0
- package/src/shared/bytes.ts +35 -5
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +74 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed.ts +350 -154
- package/src/shared/proven.ts +596 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +34 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +31 -0
- package/Cargo.toml +0 -73
- package/bench.ts +0 -3
- package/rust/batch.rs +0 -195
- package/rust/cookie_parser.rs +0 -70
- package/rust/cors.rs +0 -263
- package/rust/hashing.rs +0 -164
- package/rust/headers.rs +0 -234
- package/rust/hmac_sha256.rs +0 -113
- package/rust/http_parser.rs +0 -132
- package/rust/ingress.rs +0 -616
- package/rust/ingress_constants.rs +0 -89
- package/rust/ip_trust.rs +0 -185
- package/rust/json_ops.rs +0 -66
- package/rust/json_patch_ops.rs +0 -25
- package/rust/json_schema.rs +0 -151
- package/rust/json_ser.rs +0 -429
- package/rust/lib.rs +0 -48
- package/rust/method.rs +0 -141
- package/rust/mime_lookup.rs +0 -76
- package/rust/output.rs +0 -238
- package/rust/proxy.rs +0 -90
- package/rust/query_parser.rs +0 -141
- package/rust/random_token.rs +0 -52
- package/rust/rate_limit.rs +0 -203
- package/rust/terminal.rs +0 -95
- package/rust/unit_tests.rs +0 -843
- package/rust/url_codec.rs +0 -133
- package/rust/util.rs +0 -537
- package/rust/validation.rs +0 -107
- package/rust/websocket.rs +0 -23
- package/src/baseline/index.ts +0 -12
- package/src/baseline/tasks/cookie.ts +0 -22
- package/src/baseline/tasks/hashing.ts +0 -18
- package/src/baseline/tasks/hmac.ts +0 -38
- package/src/baseline/tasks/http.ts +0 -62
- package/src/baseline/tasks/json-patch.ts +0 -12
- package/src/baseline/tasks/json.ts +0 -50
- package/src/baseline/tasks/mime.ts +0 -5
- package/src/baseline/tasks/query.ts +0 -14
- package/src/baseline/tasks/token.ts +0 -15
- package/src/baseline/tasks/url.ts +0 -11
- package/src/baseline/tasks/validation.ts +0 -19
- package/src/baseline/tasks/websocket.ts +0 -14
- package/src/bench/assert.ts +0 -70
- package/src/bench/checks.ts +0 -227
- package/src/bench/checksum.ts +0 -27
- package/src/bench/comparisons.ts +0 -70
- package/src/bench/concurrent-worker.ts +0 -135
- package/src/bench/fixtures.ts +0 -233
- package/src/bench/index.ts +0 -2
- package/src/bench/measure.ts +0 -177
- package/src/bench/now.ts +0 -3
- package/src/bench/report.ts +0 -79
- package/src/bench/run.ts +0 -57
- package/src/bench/tasks/complex.ts +0 -232
- package/src/bench/tasks/concurrent.ts +0 -213
- package/src/bench/tasks/cookie.ts +0 -44
- package/src/bench/tasks/hashing.ts +0 -33
- package/src/bench/tasks/hmac.ts +0 -36
- package/src/bench/tasks/http.ts +0 -50
- package/src/bench/tasks/index.ts +0 -52
- package/src/bench/tasks/json-patch.ts +0 -21
- package/src/bench/tasks/json.ts +0 -33
- package/src/bench/tasks/mime.ts +0 -21
- package/src/bench/tasks/query.ts +0 -44
- package/src/bench/tasks/stress.ts +0 -142
- package/src/bench/tasks/token.ts +0 -20
- package/src/bench/tasks/url.ts +0 -34
- package/src/bench/tasks/validation.ts +0 -57
- package/src/bench/tasks/websocket.ts +0 -21
- package/src/bench/types.ts +0 -60
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// src/rust-ffi/scalar/factories.ts — Compiled-once instance factories + rayon runtime.
|
|
2
|
+
//
|
|
3
|
+
// Every `create*` here constructs a compiled-once native instance (key / schema
|
|
4
|
+
// / template precompiled at construction) plus the rayon thread-pool controls.
|
|
5
|
+
|
|
6
|
+
import { asNumber } from '../options'
|
|
7
|
+
import { getBunFFI, type BunFFI } from '../../native/ffi'
|
|
8
|
+
import type { RustClientContext } from '../context'
|
|
9
|
+
import type {
|
|
10
|
+
HmacSignerInstance,
|
|
11
|
+
SchemaValidatorInstance,
|
|
12
|
+
TemplateRendererInstance,
|
|
13
|
+
FormParserInstance,
|
|
14
|
+
MediaTypeParserInstance,
|
|
15
|
+
MediaTypeMatcherInstance,
|
|
16
|
+
ConditionalRequestInstance,
|
|
17
|
+
AcceptNegotiatorInstance,
|
|
18
|
+
Base64CodecInstance,
|
|
19
|
+
CookieSignerInstance,
|
|
20
|
+
CsrfProtectorInstance,
|
|
21
|
+
UrlBuilderInstance,
|
|
22
|
+
JwtSignerInstance,
|
|
23
|
+
AeadCipherInstance,
|
|
24
|
+
Argon2HasherInstance,
|
|
25
|
+
RateLimiterInstance,
|
|
26
|
+
PasswordHashOptions,
|
|
27
|
+
} from '../../native'
|
|
28
|
+
|
|
29
|
+
// ── FFI-backed instance wrappers ───────────────────────────────────────────
|
|
30
|
+
// On Bun, instances whose methods have a STATELESS C-ABI sibling are backed by
|
|
31
|
+
// the `bun:ffi` fast path instead of a NAPI instance — cutting the per-call
|
|
32
|
+
// NAPI crossing (~300ns) to ~20ns. The precompiled-state benefit is mostly
|
|
33
|
+
// lost (e.g. the HMAC key is rebuilt per call, ~100ns), but that is still far
|
|
34
|
+
// cheaper than a NAPI crossing, so these win under Bun. Byte-for-byte output is
|
|
35
|
+
// guaranteed by the ffi cross-check suite. NAPI instances remain the fallback
|
|
36
|
+
// (Node / CASTRUM_FFI_MODE=napi / failed self-test). Instances with genuinely
|
|
37
|
+
// stateful precompiled cores (SchemaValidator, TemplateRenderer,
|
|
38
|
+
// AcceptNegotiator, ConditionalRequest, MediaTypeMatcher/Parser, UrlBuilder,
|
|
39
|
+
// RateLimiter) stay NAPI — they need opaque handles (deferred).
|
|
40
|
+
|
|
41
|
+
function ffiHmacSigner(key: Uint8Array, ffi: BunFFI): HmacSignerInstance {
|
|
42
|
+
return {
|
|
43
|
+
sign(data) {
|
|
44
|
+
return ffi.hmacSha256(key, data)
|
|
45
|
+
},
|
|
46
|
+
verify(data, sig) {
|
|
47
|
+
return ffi.hmacSha256Verify(key, data, sig)
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ffiBase64Codec(
|
|
53
|
+
urlSafe: boolean | undefined,
|
|
54
|
+
padding: boolean | undefined,
|
|
55
|
+
ffi: BunFFI,
|
|
56
|
+
): Base64CodecInstance {
|
|
57
|
+
return {
|
|
58
|
+
encode(input) {
|
|
59
|
+
return ffi.base64Encode(input, urlSafe, padding)
|
|
60
|
+
},
|
|
61
|
+
decode(input) {
|
|
62
|
+
return ffi.base64Decode(input, urlSafe, padding)
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ffiCookieSigner(secret: Uint8Array, ffi: BunFFI): CookieSignerInstance {
|
|
68
|
+
return {
|
|
69
|
+
sign(value) {
|
|
70
|
+
return ffi.signCookie(value, secret)
|
|
71
|
+
},
|
|
72
|
+
verify(signed) {
|
|
73
|
+
return ffi.verifyCookie(signed, secret)
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ffiCsrfProtector(secret: Uint8Array, ffi: BunFFI): CsrfProtectorInstance {
|
|
79
|
+
return {
|
|
80
|
+
create() {
|
|
81
|
+
return ffi.csrfToken(secret)
|
|
82
|
+
},
|
|
83
|
+
verify(token) {
|
|
84
|
+
return ffi.csrfVerify(token, secret)
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ffiArgon2Hasher(
|
|
90
|
+
options: PasswordHashOptions | null | undefined,
|
|
91
|
+
ffi: BunFFI,
|
|
92
|
+
): Argon2HasherInstance {
|
|
93
|
+
// Resolve the napi defaults (rust/crypto/argon2.rs resolve_opts) — mirrors
|
|
94
|
+
// the scalar passwordHash FFI wrapper (crypto.ts).
|
|
95
|
+
const o = options ?? {}
|
|
96
|
+
const mCost = o.mCost ?? 19456
|
|
97
|
+
const tCost = o.tCost ?? 2
|
|
98
|
+
const pCost = o.pCost ?? 1
|
|
99
|
+
const outLen = o.outLen ?? 32
|
|
100
|
+
return {
|
|
101
|
+
hash(password, salt) {
|
|
102
|
+
return ffi.passwordHash(password, salt, mCost, tCost, pCost, outLen)
|
|
103
|
+
},
|
|
104
|
+
verify(password, phc) {
|
|
105
|
+
return ffi.passwordVerify(password, phc)
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ffiAeadCipher(
|
|
111
|
+
key: Uint8Array,
|
|
112
|
+
algorithm: string | undefined,
|
|
113
|
+
ffi: BunFFI,
|
|
114
|
+
): AeadCipherInstance {
|
|
115
|
+
// Mirror the napi constructor's validation (rust/crypto/aead.rs
|
|
116
|
+
// `resolve_algorithm`): only aes-256-gcm (default) and chacha20-poly1305 are
|
|
117
|
+
// accepted; anything else throws at construction, NOT at first use.
|
|
118
|
+
if (algorithm !== undefined && algorithm !== 'aes-256-gcm' && algorithm !== 'chacha20-poly1305') {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`unsupported aead algorithm: ${algorithm} (expected aes-256-gcm | chacha20-poly1305)`,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
const alg = algorithm === 'chacha20-poly1305' ? 1 : 0
|
|
124
|
+
return {
|
|
125
|
+
encrypt(nonce, plaintext) {
|
|
126
|
+
return ffi.aeadEncrypt(key, nonce, plaintext, alg)
|
|
127
|
+
},
|
|
128
|
+
decrypt(nonce, ciphertext) {
|
|
129
|
+
return ffi.aeadDecrypt(key, nonce, ciphertext, alg)
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ffiFormParser(ffi: BunFFI): FormParserInstance {
|
|
135
|
+
return {
|
|
136
|
+
parse(input) {
|
|
137
|
+
// Sizing matches the scalar formParsePacked path (Rust allocator bound
|
|
138
|
+
// `input.len() * 9 + 16`).
|
|
139
|
+
const out = new Uint8Array(input.length * 9 + 16)
|
|
140
|
+
const w = ffi.formParsePackedInto(input, out)
|
|
141
|
+
return out.subarray(0, w)
|
|
142
|
+
},
|
|
143
|
+
parseInto(input, output) {
|
|
144
|
+
return ffi.formParsePackedInto(input, output)
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Compiled-once factory + runtime-control methods (`Pick<RustScalar, ...>`). */
|
|
150
|
+
export function buildFactories(ctx: RustClientContext) {
|
|
151
|
+
const { addon } = ctx
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
createSchemaValidator(schema: Uint8Array): SchemaValidatorInstance {
|
|
155
|
+
return new addon.SchemaValidator(schema)
|
|
156
|
+
},
|
|
157
|
+
createHmacSigner(key: Uint8Array): HmacSignerInstance {
|
|
158
|
+
const ffi = getBunFFI()
|
|
159
|
+
if (ffi) return ffiHmacSigner(key, ffi)
|
|
160
|
+
return new addon.HmacSigner(key)
|
|
161
|
+
},
|
|
162
|
+
createTemplateRenderer(source: string): TemplateRendererInstance {
|
|
163
|
+
return new addon.TemplateRenderer(source)
|
|
164
|
+
},
|
|
165
|
+
createFormParser(capacity?: number): FormParserInstance {
|
|
166
|
+
const ffi = getBunFFI()
|
|
167
|
+
if (ffi) return ffiFormParser(ffi)
|
|
168
|
+
return new addon.FormParser(capacity)
|
|
169
|
+
},
|
|
170
|
+
createMediaTypeParser(): MediaTypeParserInstance {
|
|
171
|
+
return new addon.MediaTypeParser()
|
|
172
|
+
},
|
|
173
|
+
createConditionalRequest(
|
|
174
|
+
etagValue: Uint8Array,
|
|
175
|
+
lastModifiedSecs?: number,
|
|
176
|
+
): ConditionalRequestInstance {
|
|
177
|
+
return new addon.ConditionalRequest(etagValue, lastModifiedSecs ?? undefined)
|
|
178
|
+
},
|
|
179
|
+
createAcceptNegotiator(supported: string[]): AcceptNegotiatorInstance {
|
|
180
|
+
return new addon.AcceptNegotiator(supported)
|
|
181
|
+
},
|
|
182
|
+
createBase64Codec(urlSafe?: boolean, padding?: boolean): Base64CodecInstance {
|
|
183
|
+
const ffi = getBunFFI()
|
|
184
|
+
if (ffi) return ffiBase64Codec(urlSafe, padding, ffi)
|
|
185
|
+
return new addon.Base64Codec(urlSafe ?? undefined, padding ?? undefined)
|
|
186
|
+
},
|
|
187
|
+
createCookieSigner(secret: Uint8Array): CookieSignerInstance {
|
|
188
|
+
const ffi = getBunFFI()
|
|
189
|
+
if (ffi) return ffiCookieSigner(secret, ffi)
|
|
190
|
+
return new addon.CookieSigner(secret)
|
|
191
|
+
},
|
|
192
|
+
createCsrfProtector(secret: Uint8Array): CsrfProtectorInstance {
|
|
193
|
+
const ffi = getBunFFI()
|
|
194
|
+
if (ffi) return ffiCsrfProtector(secret, ffi)
|
|
195
|
+
return new addon.CsrfProtector(secret)
|
|
196
|
+
},
|
|
197
|
+
createUrlBuilder(base: Uint8Array): UrlBuilderInstance {
|
|
198
|
+
return new addon.UrlBuilder(base)
|
|
199
|
+
},
|
|
200
|
+
createJwtSigner(secret: Uint8Array, ttlSeconds?: number): JwtSignerInstance {
|
|
201
|
+
return new addon.JwtSigner(secret, ttlSeconds ?? undefined)
|
|
202
|
+
},
|
|
203
|
+
createAeadCipher(key: Uint8Array, algorithm?: string): AeadCipherInstance {
|
|
204
|
+
const ffi = getBunFFI()
|
|
205
|
+
if (ffi) return ffiAeadCipher(key, algorithm, ffi)
|
|
206
|
+
return new addon.AeadCipher(key, algorithm ?? undefined)
|
|
207
|
+
},
|
|
208
|
+
createArgon2Hasher(options?: PasswordHashOptions | null): Argon2HasherInstance {
|
|
209
|
+
const ffi = getBunFFI()
|
|
210
|
+
if (ffi) return ffiArgon2Hasher(options, ffi)
|
|
211
|
+
return new addon.Argon2Hasher(options ?? undefined)
|
|
212
|
+
},
|
|
213
|
+
createMediaTypeMatcher(expected: Uint8Array): MediaTypeMatcherInstance {
|
|
214
|
+
return new addon.MediaTypeMatcher(expected)
|
|
215
|
+
},
|
|
216
|
+
createRateLimiter(
|
|
217
|
+
limit: number,
|
|
218
|
+
windowMs: number,
|
|
219
|
+
maxEntries?: number | null,
|
|
220
|
+
): RateLimiterInstance {
|
|
221
|
+
return new addon.RateLimiter(limit, windowMs, maxEntries ?? undefined)
|
|
222
|
+
},
|
|
223
|
+
initThreadPool(threads?: number): void {
|
|
224
|
+
// Explicit user call also establishes the pool state locally.
|
|
225
|
+
ctx.markPoolInitialized()
|
|
226
|
+
if (threads !== undefined) ctx.setPendingThreads(threads)
|
|
227
|
+
addon.initThreadPool(threads)
|
|
228
|
+
},
|
|
229
|
+
rayonNumThreads(): number {
|
|
230
|
+
return asNumber(addon.rayonNumThreads() as unknown)
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/rust-ffi/scalar/hashing.ts — Hashing scalar methods.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors rust/crypto/hashing.rs + hmac_sha256.rs: crc32 / fnv1a64 / HMAC.
|
|
4
|
+
// (HMAC reuses the context-cached signer to avoid re-constructing the key.)
|
|
5
|
+
|
|
6
|
+
import { asBigInt, asNumber } from '../options'
|
|
7
|
+
import { resolveNative, type RustClientContext } from '../context'
|
|
8
|
+
import { getBunFFI } from '../../native/ffi'
|
|
9
|
+
import { isBun } from '../../shared/runtime'
|
|
10
|
+
|
|
11
|
+
const hexEncoder = new TextEncoder()
|
|
12
|
+
|
|
13
|
+
/** Hashing scalar methods (`Pick<RustScalar, ...>`). */
|
|
14
|
+
export function buildHashing(ctx: RustClientContext) {
|
|
15
|
+
// Bun fast path: `bun:ffi` JIT-calls the C ABI exports with ~1/4 the crossing
|
|
16
|
+
// cost of napi for these sub-µs hashes. `getBunFFI()` is lazy (binds on first
|
|
17
|
+
// use, ~2ns cached check) and null when unavailable / self-test fails.
|
|
18
|
+
return {
|
|
19
|
+
crc32(input: Uint8Array): number {
|
|
20
|
+
// Optimal by default under Bun: `Bun.hash.crc32` (native SIMD) beats the
|
|
21
|
+
// rust+FFI crossing (~2.8-8.4x, decision matrix) and matches the
|
|
22
|
+
// `BUN_WINS`/`opImpl` recommendation. Node keeps the addon.
|
|
23
|
+
// `>>> 0` preserves the unsigned-32 contract.
|
|
24
|
+
if (isBun()) return Bun.hash.crc32(input) >>> 0
|
|
25
|
+
const ffi = getBunFFI()
|
|
26
|
+
if (ffi) return ffi.crc32(input)
|
|
27
|
+
return asNumber(resolveNative(ctx, 'crc32')(input)) >>> 0
|
|
28
|
+
},
|
|
29
|
+
fnv1a64(input: Uint8Array): bigint {
|
|
30
|
+
const ffi = getBunFFI()
|
|
31
|
+
if (ffi) return ffi.fnv1a64(input)
|
|
32
|
+
return asBigInt(resolveNative(ctx, 'fnv1a64')(input))
|
|
33
|
+
},
|
|
34
|
+
xxh3(input: Uint8Array): bigint {
|
|
35
|
+
// Optimal by default under Bun: `Bun.hash.xxHash3` beats the rust+FFI
|
|
36
|
+
// crossing (~4x, decision matrix). Node keeps the addon.
|
|
37
|
+
if (isBun()) return Bun.hash.xxHash3(input)
|
|
38
|
+
const ffi = getBunFFI()
|
|
39
|
+
if (ffi) return ffi.xxh3(input)
|
|
40
|
+
return asBigInt(resolveNative(ctx, 'xxh3')(input))
|
|
41
|
+
},
|
|
42
|
+
hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array {
|
|
43
|
+
// Optimal by default under Bun: `Bun.CryptoHasher` (native) beats the
|
|
44
|
+
// rust+FFI crossing (~1.2x, decision matrix). Re-encoded to hex to match
|
|
45
|
+
// castrum's hex-return contract (64 lowercase-hex bytes — same as the
|
|
46
|
+
// addon). Node keeps the FFI path (`hmacSha256` is classified "proven"
|
|
47
|
+
// vs the node:crypto baseline). Napi fallback keeps the precompiled-key
|
|
48
|
+
// `HmacSigner` (identity semantics).
|
|
49
|
+
if (isBun()) {
|
|
50
|
+
const hasher = new Bun.CryptoHasher('sha256', key)
|
|
51
|
+
hasher.update(data)
|
|
52
|
+
return hexEncoder.encode(Buffer.from(hasher.digest()).toString('hex'))
|
|
53
|
+
}
|
|
54
|
+
const ffi = getBunFFI()
|
|
55
|
+
if (ffi) return ffi.hmacSha256(key, data)
|
|
56
|
+
return ctx.hmacSigner(key).sign(data)
|
|
57
|
+
},
|
|
58
|
+
hmacSha256Into(key: Uint8Array, data: Uint8Array, output: Uint8Array): number {
|
|
59
|
+
const ffi = getBunFFI()
|
|
60
|
+
if (ffi) return ffi.hmacSha256Into(key, data, output)
|
|
61
|
+
const bytes = ctx.hmacSigner(key).sign(data)
|
|
62
|
+
if (output.length < bytes.length) throw new Error('hmac sha256: output buffer too small')
|
|
63
|
+
output.set(bytes)
|
|
64
|
+
return bytes.length
|
|
65
|
+
},
|
|
66
|
+
hmacSha256Verify(key: Uint8Array, data: Uint8Array, sig: Uint8Array): boolean {
|
|
67
|
+
const ffi = getBunFFI()
|
|
68
|
+
if (ffi) return ffi.hmacSha256Verify(key, data, sig)
|
|
69
|
+
return ctx.hmacSigner(key).verify(data, sig)
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// src/rust-ffi/scalar/http.ts — HTTP wire-format + parsing scalar methods.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors rust/http/* (packed parsers, media type, etag, HTTP dates,
|
|
4
|
+
// Accept-Encoding, URL codec/join, MIME lookup) plus the `util/validation`
|
|
5
|
+
// validators and the WebSocket accept-key.
|
|
6
|
+
|
|
7
|
+
import type { EncodingPrefResult, MediaTypeResult } from '../../native'
|
|
8
|
+
import { getBunFFI } from '../../native/ffi'
|
|
9
|
+
import { decoder, encoder } from '../../shared/bytes'
|
|
10
|
+
import { isBun } from '../../shared/runtime'
|
|
11
|
+
import { type RustClientContext, resolveNative } from '../context'
|
|
12
|
+
|
|
13
|
+
// Mirror napi `url_decode`'s UTF-8 validation (rust/http/url_codec.rs): only
|
|
14
|
+
// validate when a high byte is present (the ASCII fast path skips it — same as
|
|
15
|
+
// Rust's saw_high check). `url_decode_bytes` / `url_decode_into` skip the check
|
|
16
|
+
// entirely, so those ffi bindings are byte-faithful with no extra work.
|
|
17
|
+
const FATAL_UTF8 = new TextDecoder('utf-8', { fatal: true })
|
|
18
|
+
function ensureValidUtf8(bytes: Uint8Array): void {
|
|
19
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
20
|
+
if ((bytes[i] ?? 0) >= 0x80) {
|
|
21
|
+
try {
|
|
22
|
+
FATAL_UTF8.decode(bytes)
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error('url decode: invalid UTF-8 in decoded output')
|
|
25
|
+
}
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** HTTP / parsing scalar methods (`Pick<RustScalar, ...>`). */
|
|
32
|
+
export function buildHttp(ctx: RustClientContext) {
|
|
33
|
+
// Resolve the native fns through the shared first-use cache so the hot
|
|
34
|
+
// scalar path skips the lazy-addon Proxy get on every call.
|
|
35
|
+
const n = (name: string) => resolveNative(ctx, name)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
mimeFromExtension(ext: Uint8Array): Uint8Array {
|
|
39
|
+
return ctx.cachedMime(ext)
|
|
40
|
+
},
|
|
41
|
+
urlEncode(input: Uint8Array): Uint8Array {
|
|
42
|
+
// Under Bun, Bun's native `encodeURIComponent` beats the rust+FFI/NAPI
|
|
43
|
+
// crossing (~4x on the string path, ~1.3x on bytes) and skips the C-ABI
|
|
44
|
+
// call entirely — see docs/bun-builtins-decision-matrix.md. Parity is
|
|
45
|
+
// pinned by test/unit/features/url.test.ts (byte-for-byte, incl.
|
|
46
|
+
// unicode/emoji); `urlEncode` is always applied to text, so the
|
|
47
|
+
// bytes→string→encode→bytes round-trip is lossless.
|
|
48
|
+
if (isBun()) return encoder.encode(encodeURIComponent(decoder.decode(input)))
|
|
49
|
+
const ffi = getBunFFI()
|
|
50
|
+
if (ffi) return ffi.urlEncode(input)
|
|
51
|
+
return n('urlEncode')(input) as Uint8Array
|
|
52
|
+
},
|
|
53
|
+
urlDecode(input: Uint8Array): Uint8Array {
|
|
54
|
+
// Same rationale as `urlEncode`; the strict `urlDecode` already throws on
|
|
55
|
+
// invalid UTF-8 (ensureValidUtf8 below), matching decodeURIComponent's
|
|
56
|
+
// URIError (a subclass of Error), so the JS path is behavior-equivalent.
|
|
57
|
+
if (isBun()) return encoder.encode(decodeURIComponent(decoder.decode(input)))
|
|
58
|
+
const ffi = getBunFFI()
|
|
59
|
+
if (ffi) {
|
|
60
|
+
const out = ffi.urlDecode(input)
|
|
61
|
+
// napi `url_decode` validates UTF-8 (only when a high byte is present);
|
|
62
|
+
// `url_decode_bytes`/`url_decode_into` do not. This is the strict one.
|
|
63
|
+
ensureValidUtf8(out)
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
return n('urlDecode')(input) as Uint8Array
|
|
67
|
+
},
|
|
68
|
+
urlDecodeBytes(input: Uint8Array): Uint8Array {
|
|
69
|
+
const ffi = getBunFFI()
|
|
70
|
+
if (ffi) return ffi.urlDecode(input) // byte semantics match url_decode_bytes
|
|
71
|
+
return n('urlDecodeBytes')(input) as Uint8Array
|
|
72
|
+
},
|
|
73
|
+
urlEncodeInto(input: Uint8Array, output: Uint8Array): number {
|
|
74
|
+
const ffi = getBunFFI()
|
|
75
|
+
if (ffi) return ffi.urlEncodeInto(input, output)
|
|
76
|
+
return n('urlEncodeInto')(input, output) as number
|
|
77
|
+
},
|
|
78
|
+
urlDecodeInto(input: Uint8Array, output: Uint8Array): number {
|
|
79
|
+
const ffi = getBunFFI()
|
|
80
|
+
if (ffi) return ffi.urlDecodeInto(input, output) // no UTF-8 check, matches napi
|
|
81
|
+
return n('urlDecodeInto')(input, output) as number
|
|
82
|
+
},
|
|
83
|
+
validateEmail(input: Uint8Array): boolean {
|
|
84
|
+
const ffi = getBunFFI()
|
|
85
|
+
if (ffi) return ffi.validateEmail(input)
|
|
86
|
+
return n('validateEmail')(input) as boolean
|
|
87
|
+
},
|
|
88
|
+
validateUuid(input: Uint8Array): boolean {
|
|
89
|
+
const ffi = getBunFFI()
|
|
90
|
+
if (ffi) return ffi.validateUuid(input)
|
|
91
|
+
return n('validateUuid')(input) as boolean
|
|
92
|
+
},
|
|
93
|
+
validateIpv4(input: Uint8Array): boolean {
|
|
94
|
+
const ffi = getBunFFI()
|
|
95
|
+
if (ffi) return ffi.validateIpv4(input)
|
|
96
|
+
return n('validateIpv4')(input) as boolean
|
|
97
|
+
},
|
|
98
|
+
validateIpv6(input: Uint8Array): boolean {
|
|
99
|
+
const ffi = getBunFFI()
|
|
100
|
+
if (ffi) return ffi.validateIpv6(input)
|
|
101
|
+
return n('validateIpv6')(input) as boolean
|
|
102
|
+
},
|
|
103
|
+
wsAcceptKey(key: Uint8Array): Uint8Array {
|
|
104
|
+
const ffi = getBunFFI()
|
|
105
|
+
if (ffi) return ffi.wsAcceptKey(key)
|
|
106
|
+
return n('wsAcceptKey')(key) as Uint8Array
|
|
107
|
+
},
|
|
108
|
+
httpParseRequestPacked(input: Uint8Array): Uint8Array {
|
|
109
|
+
const ffi = getBunFFI()
|
|
110
|
+
if (ffi) {
|
|
111
|
+
const out = new Uint8Array(input.length * 9 + 16)
|
|
112
|
+
const w = ffi.httpParseRequestPackedInto(input, out)
|
|
113
|
+
return out.subarray(0, w)
|
|
114
|
+
}
|
|
115
|
+
return n('httpParseRequestPacked')(input) as Uint8Array
|
|
116
|
+
},
|
|
117
|
+
httpParseRequestPackedInto(input: Uint8Array, output: Uint8Array): number {
|
|
118
|
+
const ffi = getBunFFI()
|
|
119
|
+
if (ffi) return ffi.httpParseRequestPackedInto(input, output)
|
|
120
|
+
return n('httpParseRequestPackedInto')(input, output) as number
|
|
121
|
+
},
|
|
122
|
+
queryParsePacked(input: Uint8Array): Uint8Array {
|
|
123
|
+
const ffi = getBunFFI()
|
|
124
|
+
if (ffi) {
|
|
125
|
+
const out = new Uint8Array(input.length * 9 + 16)
|
|
126
|
+
const w = ffi.queryParsePackedInto(input, out)
|
|
127
|
+
return out.subarray(0, w)
|
|
128
|
+
}
|
|
129
|
+
return n('queryParsePacked')(input) as Uint8Array
|
|
130
|
+
},
|
|
131
|
+
queryParsePackedInto(input: Uint8Array, output: Uint8Array): number {
|
|
132
|
+
const ffi = getBunFFI()
|
|
133
|
+
if (ffi) return ffi.queryParsePackedInto(input, output)
|
|
134
|
+
return n('queryParsePackedInto')(input, output) as number
|
|
135
|
+
},
|
|
136
|
+
cookieParsePacked(input: Uint8Array): Uint8Array {
|
|
137
|
+
const ffi = getBunFFI()
|
|
138
|
+
if (ffi) {
|
|
139
|
+
const out = new Uint8Array(input.length * 9 + 16)
|
|
140
|
+
const w = ffi.cookieParsePackedInto(input, out)
|
|
141
|
+
return out.subarray(0, w)
|
|
142
|
+
}
|
|
143
|
+
return n('cookieParsePacked')(input) as Uint8Array
|
|
144
|
+
},
|
|
145
|
+
cookieParsePackedInto(input: Uint8Array, output: Uint8Array): number {
|
|
146
|
+
const ffi = getBunFFI()
|
|
147
|
+
if (ffi) return ffi.cookieParsePackedInto(input, output)
|
|
148
|
+
return n('cookieParsePackedInto')(input, output) as number
|
|
149
|
+
},
|
|
150
|
+
formParsePacked(input: Uint8Array): Uint8Array {
|
|
151
|
+
const ffi = getBunFFI()
|
|
152
|
+
if (ffi) {
|
|
153
|
+
// x-www-form-urlencoded uses the SAME packed-pairs core as query parse.
|
|
154
|
+
const out = new Uint8Array(input.length * 9 + 16)
|
|
155
|
+
const w = ffi.formParsePackedInto(input, out)
|
|
156
|
+
return out.subarray(0, w)
|
|
157
|
+
}
|
|
158
|
+
return n('formParsePacked')(input) as Uint8Array
|
|
159
|
+
},
|
|
160
|
+
parseMediaType(input: Uint8Array): MediaTypeResult {
|
|
161
|
+
const r = n('parseMediaType')(input) as MediaTypeResult
|
|
162
|
+
// napi serializes Option fields as undefined; normalize to null to match
|
|
163
|
+
// the declared `string | null` public type.
|
|
164
|
+
return {
|
|
165
|
+
mediaType: r.mediaType,
|
|
166
|
+
charset: r.charset ?? null,
|
|
167
|
+
boundary: r.boundary ?? null,
|
|
168
|
+
params: r.params,
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
etag(data: Uint8Array, weak?: boolean): Uint8Array {
|
|
172
|
+
const ffi = getBunFFI()
|
|
173
|
+
if (ffi) return ffi.etag(data, weak)
|
|
174
|
+
return n('etag')(data, weak ?? undefined) as Uint8Array
|
|
175
|
+
},
|
|
176
|
+
etagInto(data: Uint8Array, output: Uint8Array, weak?: boolean): number {
|
|
177
|
+
const ffi = getBunFFI()
|
|
178
|
+
if (ffi) return ffi.etagInto(data, output, weak)
|
|
179
|
+
return n('etagInto')(data, output, weak ?? undefined) as number
|
|
180
|
+
},
|
|
181
|
+
httpDate(secs?: number): Uint8Array {
|
|
182
|
+
// Bun's Date.toUTCString (RFC 1123 IMF-fixdate) beats the rust+FFI
|
|
183
|
+
// crossing (~3.7x, measured); output is byte-identical for HTTP timestamps.
|
|
184
|
+
if (isBun()) {
|
|
185
|
+
const t = secs ?? Math.floor(Date.now() / 1000)
|
|
186
|
+
return encoder.encode(new Date(t * 1000).toUTCString())
|
|
187
|
+
}
|
|
188
|
+
return n('httpDate')(secs ?? undefined) as Uint8Array
|
|
189
|
+
},
|
|
190
|
+
httpDateInto(secs: number | undefined, output: Uint8Array): number {
|
|
191
|
+
return n('httpDateInto')(secs ?? undefined, output) as number
|
|
192
|
+
},
|
|
193
|
+
parseHttpDate(input: Uint8Array): bigint | null {
|
|
194
|
+
return n('parseHttpDate')(input) as bigint | null
|
|
195
|
+
},
|
|
196
|
+
parseAcceptEncoding(input: Uint8Array): EncodingPrefResult[] {
|
|
197
|
+
return n('parseAcceptEncoding')(input) as EncodingPrefResult[]
|
|
198
|
+
},
|
|
199
|
+
urlResolve(base: Uint8Array, reference: Uint8Array): Uint8Array {
|
|
200
|
+
return n('urlResolve')(base, reference) as Uint8Array
|
|
201
|
+
},
|
|
202
|
+
urlEncodeQuery(params: Record<string, string>): Uint8Array {
|
|
203
|
+
return n('urlEncodeQuery')(params) as Uint8Array
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/rust-ffi/scalar/index.ts — Scalar + feature FFI methods (barrel).
|
|
2
|
+
//
|
|
3
|
+
// The byte-level single-value methods of the Rust client (crc32, hmac, json,
|
|
4
|
+
// jwt, password, aead, compress, ... plus the class factories and runtime
|
|
5
|
+
// controls). Composed into the client by ./client.ts.
|
|
6
|
+
//
|
|
7
|
+
// The `RustScalar` interface (the public surface, with its perf JSDoc) lives
|
|
8
|
+
// in `interface.ts`; the implementation is split into domain builders mirroring
|
|
9
|
+
// `rust/` (hashing, json, http, crypto, payload, factories).
|
|
10
|
+
|
|
11
|
+
import type { RustClientContext } from '../context'
|
|
12
|
+
import { buildHashing } from './hashing'
|
|
13
|
+
import { buildJson } from './json'
|
|
14
|
+
import { buildHttp } from './http'
|
|
15
|
+
import { buildCrypto } from './crypto'
|
|
16
|
+
import { buildPayload } from './payload'
|
|
17
|
+
import { buildFactories } from './factories'
|
|
18
|
+
|
|
19
|
+
export type { RustScalar } from './interface'
|
|
20
|
+
|
|
21
|
+
/** Build the scalar/feature method set for a client context. */
|
|
22
|
+
export function buildScalar(ctx: RustClientContext): import('./interface').RustScalar {
|
|
23
|
+
return {
|
|
24
|
+
...buildHashing(ctx),
|
|
25
|
+
...buildJson(ctx),
|
|
26
|
+
...buildHttp(ctx),
|
|
27
|
+
...buildCrypto(ctx),
|
|
28
|
+
...buildPayload(ctx),
|
|
29
|
+
...buildFactories(ctx),
|
|
30
|
+
}
|
|
31
|
+
}
|