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,104 @@
|
|
|
1
|
+
// src/shared/response.ts — Response helpers for pooled (reusable) output buffers.
|
|
2
|
+
//
|
|
3
|
+
// When a response body is backed by a pooled buffer, the pool handle must not
|
|
4
|
+
// be released until the body has actually been consumed, otherwise a later
|
|
5
|
+
// request could overwrite bytes the in-flight response still references.
|
|
6
|
+
// `pooledBodyResponse` wraps the slice in a streaming body that returns the
|
|
7
|
+
// handle to its pool once the body is read (or the request is aborted).
|
|
8
|
+
|
|
9
|
+
import type { PooledBuffer } from './buffer-pool'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a `Response` whose body is backed by a pooled byte slice, returning the
|
|
13
|
+
* pool handle once the body has been fully consumed (stream closed) or the
|
|
14
|
+
* request has been cancelled.
|
|
15
|
+
*
|
|
16
|
+
* The pooled buffer stays **in flight** (not reusable) until the response
|
|
17
|
+
* stream is closed/cancelled — this is what makes zero-copy pooling safe: a
|
|
18
|
+
* buffer is never handed to a second request while a previous response may
|
|
19
|
+
* still alias it.
|
|
20
|
+
*
|
|
21
|
+
* @param handle - The pooled-buffer handle to release on consumption.
|
|
22
|
+
* @param bytes - The (possibly shared) byte slice to serve as the body.
|
|
23
|
+
* @param init - Standard `ResponseInit` (status, headers, ...).
|
|
24
|
+
* @param timeoutMs - Optional abandonment guard (ms, default 0 = disabled):
|
|
25
|
+
* if the body is neither pulled nor cancelled within this window the pooled
|
|
26
|
+
* buffer is released and the stream closed. Opt-in — lets zero-copy
|
|
27
|
+
* responses bound memory under abandoned (unread) responses.
|
|
28
|
+
* @returns A `Response` that releases `handle` once its body is consumed.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* **Zero-copy caveat**: the body slice is served without copying — it aliases
|
|
32
|
+
* the pooled buffer. Consumers that hold a response open for a long time keep
|
|
33
|
+
* that buffer out of circulation. This helper is only intended for callers that
|
|
34
|
+
* explicitly opt into zero-copy (e.g. `INGRESS_UNSAFE_ZERO_COPY`); the default
|
|
35
|
+
* safe path copies the slice and releases the handle eagerly.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const handle = pool.acquire(size);
|
|
40
|
+
* const used = handle.buffer.subarray(0, written);
|
|
41
|
+
* return pooledBodyResponse(handle, used, { status: 200, headers });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function pooledBodyResponse(
|
|
45
|
+
handle: PooledBuffer,
|
|
46
|
+
bytes: Uint8Array,
|
|
47
|
+
init: ResponseInit = {},
|
|
48
|
+
timeoutMs = 0,
|
|
49
|
+
): Response {
|
|
50
|
+
if (bytes.byteLength === 0) {
|
|
51
|
+
// Nothing to serve: release immediately.
|
|
52
|
+
handle.release()
|
|
53
|
+
return new Response(null, init)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let released = false
|
|
57
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
58
|
+
const release = (): void => {
|
|
59
|
+
if (released) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
released = true
|
|
63
|
+
if (timer !== undefined) {
|
|
64
|
+
clearTimeout(timer)
|
|
65
|
+
}
|
|
66
|
+
handle.release()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const body = new ReadableStream<Uint8Array>({
|
|
70
|
+
start(controller) {
|
|
71
|
+
// Abandonment guard (opt-in): if the body is neither pulled nor
|
|
72
|
+
// cancelled within `timeoutMs`, release the pooled buffer so it can be
|
|
73
|
+
// reused and close the stream. Without this, an abandoned Response would
|
|
74
|
+
// hold the pooled buffer in flight forever (bounded only by maxInFlight).
|
|
75
|
+
// `pull()` re-checks `released` so a late read never serves the reused
|
|
76
|
+
// (potentially overwritten) bytes.
|
|
77
|
+
if (timeoutMs > 0) {
|
|
78
|
+
timer = setTimeout(() => {
|
|
79
|
+
if (released) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
release()
|
|
83
|
+
controller.close()
|
|
84
|
+
}, timeoutMs)
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
pull(controller) {
|
|
88
|
+
if (released) {
|
|
89
|
+
// Released by the abandonment guard (or a racing cancel): the pooled
|
|
90
|
+
// bytes may already be reused — never serve them. End-of-stream.
|
|
91
|
+
controller.close()
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
controller.enqueue(bytes)
|
|
95
|
+
controller.close()
|
|
96
|
+
release()
|
|
97
|
+
},
|
|
98
|
+
cancel() {
|
|
99
|
+
release()
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return new Response(body, init)
|
|
104
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/shared/runtime.ts — runtime detection helpers.
|
|
2
|
+
//
|
|
3
|
+
// castrum is Bun-first but must also run under Node.js. This module is the
|
|
4
|
+
// single place that detects the host runtime, so the rest of the codebase can
|
|
5
|
+
// branch on `isBun()` / `isNode()` without sprinkling `typeof Bun` checks
|
|
6
|
+
// everywhere. Importing this module has zero side effects.
|
|
7
|
+
|
|
8
|
+
export type RuntimeName = 'bun' | 'node' | 'unknown'
|
|
9
|
+
|
|
10
|
+
/** Whether the current runtime is Bun (the primary target). */
|
|
11
|
+
export function isBun(): boolean {
|
|
12
|
+
// Bun defines the global `Bun` object; Node does not.
|
|
13
|
+
return typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Whether the current runtime is Node.js. */
|
|
17
|
+
export function isNode(): boolean {
|
|
18
|
+
return !isBun() && typeof process !== 'undefined' && process.versions?.node != null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Best-effort name of the current runtime. */
|
|
22
|
+
export function runtimeName(): RuntimeName {
|
|
23
|
+
if (isBun()) return 'bun'
|
|
24
|
+
if (isNode()) return 'node'
|
|
25
|
+
return 'unknown'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The Node.js major version when running under Node, otherwise `null`. */
|
|
29
|
+
export function nodeMajorVersion(): number | null {
|
|
30
|
+
if (!isNode()) return null
|
|
31
|
+
const v = process.versions.node
|
|
32
|
+
const major = Number(v?.split('.')[0])
|
|
33
|
+
return Number.isFinite(major) ? major : null
|
|
34
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/shared/trace.ts — W3C Trace Context helpers (zero-dep).
|
|
2
|
+
//
|
|
3
|
+
// Parses the `traceparent` request header and generates span ids so ingress
|
|
4
|
+
// logs/hooks can correlate requests across services. This is the "tracing"
|
|
5
|
+
// primitive — wire `parseTraceContext(req)` in an `onRequest` hook and pass the
|
|
6
|
+
// traceId/spanId into your structured logger (pino/otel), or use it directly.
|
|
7
|
+
//
|
|
8
|
+
// Format: `version-traceid-spanid-flags` (e.g. `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`).
|
|
9
|
+
|
|
10
|
+
/** A parsed W3C trace context (valid per the spec's length/hex rules). */
|
|
11
|
+
export interface TraceContext {
|
|
12
|
+
/** Version, always `"00"` for accepted headers. */
|
|
13
|
+
version: string
|
|
14
|
+
/** 32 hex chars — the trace id (shared across the whole trace). */
|
|
15
|
+
traceId: string
|
|
16
|
+
/** 16 hex chars — the current span id. */
|
|
17
|
+
spanId: string
|
|
18
|
+
/** The flags byte (bit 0 = sampled). */
|
|
19
|
+
flags: number
|
|
20
|
+
/** True when the `sampled` (bit 0) flag is set. */
|
|
21
|
+
sampled: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const HEX = /^[0-9a-fA-F]+$/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse a `traceparent` header value into a {@link TraceContext}, or `null`
|
|
28
|
+
* when malformed (wrong version / length / non-hex). Invalid contexts are
|
|
29
|
+
* ignored per the spec — callers should generate a fresh trace then.
|
|
30
|
+
*/
|
|
31
|
+
export function parseTraceParent(header: string | null | undefined): TraceContext | null {
|
|
32
|
+
if (!header) return null
|
|
33
|
+
const parts = header.trim().split('-')
|
|
34
|
+
if (parts.length !== 4) return null
|
|
35
|
+
const [version, traceId, spanId, flagsStr] = parts
|
|
36
|
+
// Only version 00 is currently defined.
|
|
37
|
+
if (version !== '00') return null
|
|
38
|
+
if (traceId === undefined || traceId.length !== 32 || !HEX.test(traceId)) return null
|
|
39
|
+
if (spanId === undefined || spanId.length !== 16 || !HEX.test(spanId)) return null
|
|
40
|
+
if (flagsStr === undefined || flagsStr.length !== 2 || !HEX.test(flagsStr)) return null
|
|
41
|
+
const flags = Number.parseInt(flagsStr, 16)
|
|
42
|
+
return {
|
|
43
|
+
version,
|
|
44
|
+
traceId: traceId.toLowerCase(),
|
|
45
|
+
spanId: spanId.toLowerCase(),
|
|
46
|
+
flags,
|
|
47
|
+
sampled: (flags & 0x01) === 0x01,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Random 32-hex trace id (crypto-backed where available). */
|
|
52
|
+
export function createTraceId(): string {
|
|
53
|
+
return randomHex(16)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Random 16-hex span id. */
|
|
57
|
+
export function createSpanId(): string {
|
|
58
|
+
return randomHex(8)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Generate `n` random bytes as lowercase hex. */
|
|
62
|
+
function randomHex(bytes: number): string {
|
|
63
|
+
const buf = new Uint8Array(bytes)
|
|
64
|
+
const c = globalThis.crypto
|
|
65
|
+
if (c?.getRandomValues) {
|
|
66
|
+
c.getRandomValues(buf)
|
|
67
|
+
} else {
|
|
68
|
+
for (let i = 0; i < bytes; i++) buf[i] = Math.floor(Math.random() * 256)
|
|
69
|
+
}
|
|
70
|
+
let out = ''
|
|
71
|
+
for (let i = 0; i < bytes; i++) {
|
|
72
|
+
out += (buf[i] ?? 0).toString(16).padStart(2, '0')
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Serialize a trace context back to a `traceparent` header value. */
|
|
78
|
+
export function serializeTraceParent(ctx: TraceContext): string {
|
|
79
|
+
const flags = ctx.flags.toString(16).padStart(2, '0')
|
|
80
|
+
return `${ctx.version}-${ctx.traceId}-${ctx.spanId}-${flags}`
|
|
81
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/shared/uuid.ts — UUIDv7 generation.
|
|
2
|
+
//
|
|
3
|
+
// Deliberately does NOT implement UUIDv7 in Rust: the decision matrix
|
|
4
|
+
// (docs/bun-builtins-decision-matrix.md) measured Bun.randomUUIDv7 ~2x faster
|
|
5
|
+
// than the FFI-crossing rust random-token path, so this delegates to the Bun
|
|
6
|
+
// built-in and falls back to crypto.randomUUID (RFC 4122 v4) under Node.
|
|
7
|
+
|
|
8
|
+
/** Generate a UUIDv7 (time-ordered) string, preferring Bun's native built-in. */
|
|
9
|
+
export function uuidv7(): string {
|
|
10
|
+
const bun = (globalThis as { Bun?: { randomUUIDv7?: () => string } }).Bun
|
|
11
|
+
if (typeof bun?.randomUUIDv7 === 'function') {
|
|
12
|
+
return bun.randomUUIDv7()
|
|
13
|
+
}
|
|
14
|
+
// Node / non-Bun fallback: crypto.randomUUID (v4 — order not guaranteed).
|
|
15
|
+
return globalThis.crypto?.randomUUID?.() ?? fallbackRandomUuid()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Last-resort synchronous UUIDv4 (only if crypto.randomUUID is unavailable). */
|
|
19
|
+
function fallbackRandomUuid(): string {
|
|
20
|
+
const bytes = new Uint8Array(16)
|
|
21
|
+
const c = globalThis.crypto
|
|
22
|
+
if (c?.getRandomValues) {
|
|
23
|
+
c.getRandomValues(bytes)
|
|
24
|
+
} else {
|
|
25
|
+
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256)
|
|
26
|
+
}
|
|
27
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40 // version 4
|
|
28
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80 // variant 10xx
|
|
29
|
+
const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
30
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
|
31
|
+
}
|
package/Cargo.toml
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "castrum"
|
|
3
|
-
# Keep in sync with package.json "version" (single source of truth: the npm package).
|
|
4
|
-
version = "0.7.0"
|
|
5
|
-
edition = "2021"
|
|
6
|
-
|
|
7
|
-
[lib]
|
|
8
|
-
name = "castrum"
|
|
9
|
-
crate-type = ["cdylib"]
|
|
10
|
-
path = "rust/lib.rs"
|
|
11
|
-
|
|
12
|
-
[dependencies]
|
|
13
|
-
napi = { version = "3", features = ["napi10"] }
|
|
14
|
-
napi-derive = "3"
|
|
15
|
-
|
|
16
|
-
serde = { version = "1", features = ["derive"] }
|
|
17
|
-
serde_json = "1"
|
|
18
|
-
sonic-rs = "0.5"
|
|
19
|
-
|
|
20
|
-
# default-features=false: drop resolve-http + tls-aws-lc-rs which pull in
|
|
21
|
-
# reqwest -> hyper/h2/tokio/rustls (large binary, attack surface). Format
|
|
22
|
-
# validators (email etc.) are built-in and NOT feature-gated in 0.48.
|
|
23
|
-
jsonschema = { version = "0.48", default-features = false, features = ["resolve-file"] }
|
|
24
|
-
json-patch = "4"
|
|
25
|
-
aws-lc-rs = "1"
|
|
26
|
-
base64 = "0.23"
|
|
27
|
-
crc32fast = "1"
|
|
28
|
-
httparse = "1"
|
|
29
|
-
mime_guess = "2"
|
|
30
|
-
phf = { version = "0.14", features = ["macros"] }
|
|
31
|
-
getrandom = "0.4"
|
|
32
|
-
memchr = "2"
|
|
33
|
-
simdutf8 = "0.1"
|
|
34
|
-
email_address = "0.2"
|
|
35
|
-
fast_chemail = { version = "0.9", optional = true }
|
|
36
|
-
rayon = "1"
|
|
37
|
-
# mimalloc is set as the Rust #[global_allocator] (see rust/lib.rs). We DELIBERATELY
|
|
38
|
-
# do NOT enable the "override" feature: it interposes libc malloc/free at the
|
|
39
|
-
# process level, which is an embedding hazard when this cdylib is dlopen'd into
|
|
40
|
-
# Bun (which itself embeds mimalloc -> symbol clashes). The Rust-level global
|
|
41
|
-
# allocator keeps most of the allocation win without replacing the host malloc.
|
|
42
|
-
mimalloc = { version = "0.1", features = ["local_dynamic_tls"] }
|
|
43
|
-
core_affinity = "0.8"
|
|
44
|
-
|
|
45
|
-
parking_lot = "0.12"
|
|
46
|
-
xxhash-rust = { version = "0.8", features = ["xxh3"] }
|
|
47
|
-
ipnet = "2"
|
|
48
|
-
lru = "0.12"
|
|
49
|
-
|
|
50
|
-
[build-dependencies]
|
|
51
|
-
napi-build = "2"
|
|
52
|
-
|
|
53
|
-
[features]
|
|
54
|
-
default = ["fast-email"]
|
|
55
|
-
fast-email = ["dep:fast_chemail"]
|
|
56
|
-
|
|
57
|
-
[profile.release]
|
|
58
|
-
opt-level = 3
|
|
59
|
-
lto = "fat"
|
|
60
|
-
codegen-units = 1
|
|
61
|
-
strip = "symbols"
|
|
62
|
-
debug = 0
|
|
63
|
-
incremental = false
|
|
64
|
-
# Safety: this is an embedded cdylib loaded into the Bun process. panic = "unwind"
|
|
65
|
-
# (the default) lets napi's catch_unwind convert Rust panics into JS exceptions
|
|
66
|
-
# instead of aborting the host process. overflow-checks = true catches integer
|
|
67
|
-
# overflow in release; the cost is negligible relative to the safety gain.
|
|
68
|
-
overflow-checks = true
|
|
69
|
-
debug-assertions = false
|
|
70
|
-
|
|
71
|
-
[profile.release.package."*"]
|
|
72
|
-
opt-level = 3
|
|
73
|
-
codegen-units = 1
|
package/bench.ts
DELETED
package/rust/batch.rs
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
// rust/batch.rs — v2: DIRECT WRITE, NO INTERMEDIATE Vec<Vec<u8>>
|
|
2
|
-
use napi::bindgen_prelude::*;
|
|
3
|
-
use napi_derive::napi;
|
|
4
|
-
|
|
5
|
-
use crate::json_ops::{json_sum_ids_bytes, json_valid_bytes};
|
|
6
|
-
use crate::util::{
|
|
7
|
-
count_batch, should_parallelize, sum_batch_i64, total_bytes, unpack,
|
|
8
|
-
};
|
|
9
|
-
use crate::validation::{
|
|
10
|
-
validate_email_bytes, validate_ipv4_bytes, validate_ipv6_bytes, validate_uuid_bytes,
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
// ── Internal helpers (direct-write variants) ──
|
|
14
|
-
|
|
15
|
-
/// Direct-write JSON sum batch — eliminates intermediate Vec<i64>.
|
|
16
|
-
#[inline]
|
|
17
|
-
fn json_sum_batch_direct(items: &[&[u8]], out: &mut Vec<u8>) {
|
|
18
|
-
let n = items.len();
|
|
19
|
-
out.reserve(4 + n * 8);
|
|
20
|
-
out.extend_from_slice(&(n as u32).to_le_bytes());
|
|
21
|
-
let start = out.len();
|
|
22
|
-
out.resize(start + n * 8, 0);
|
|
23
|
-
|
|
24
|
-
// Slice the output buffer to write directly into it.
|
|
25
|
-
let out_slice = &mut out[start..];
|
|
26
|
-
|
|
27
|
-
if should_parallelize(items.len(), total_bytes(items)) {
|
|
28
|
-
use rayon::prelude::*;
|
|
29
|
-
let chunk_size = 256;
|
|
30
|
-
|
|
31
|
-
// Use par_chunks_mut to get mutable disjoint slices of the output buffer.
|
|
32
|
-
out_slice.par_chunks_mut(chunk_size * 8).enumerate().for_each(|(ci, out_chunk)| {
|
|
33
|
-
let num_items = out_chunk.len() / 8;
|
|
34
|
-
let item_start = ci * chunk_size;
|
|
35
|
-
let item_end = (item_start + num_items).min(items.len());
|
|
36
|
-
|
|
37
|
-
for (i, item) in items[item_start..item_end].iter().enumerate() {
|
|
38
|
-
let sum = json_sum_ids_bytes(item).unwrap_or(0);
|
|
39
|
-
out_chunk[i * 8..(i + 1) * 8].copy_from_slice(&sum.to_le_bytes());
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
} else {
|
|
43
|
-
for (i, item) in items.iter().enumerate() {
|
|
44
|
-
let sum = json_sum_ids_bytes(item).unwrap_or(0);
|
|
45
|
-
out_slice[i * 8..(i + 1) * 8].copy_from_slice(&sum.to_le_bytes());
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/// Direct-write parse batch — eliminates intermediate Vec<Vec<u8>>.
|
|
51
|
-
#[inline]
|
|
52
|
-
fn parse_batch_direct<F>(items: &[&[u8]], out: &mut Vec<u8>, f: F)
|
|
53
|
-
where
|
|
54
|
-
F: Fn(&[u8]) -> Result<Vec<u8>> + Sync,
|
|
55
|
-
{
|
|
56
|
-
let n = items.len();
|
|
57
|
-
out.reserve(4 + n * 32);
|
|
58
|
-
out.extend_from_slice(&(n as u32).to_le_bytes());
|
|
59
|
-
for item in items {
|
|
60
|
-
let r = f(item).unwrap_or_default();
|
|
61
|
-
out.extend_from_slice(&(r.len() as u32).to_le_bytes());
|
|
62
|
-
out.extend_from_slice(&r);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
#[inline]
|
|
67
|
-
fn query_parse_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
|
|
68
|
-
crate::query_parser::query_parse_packed_vec(input)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
#[inline]
|
|
72
|
-
fn cookie_parse_packed_vec(input: &[u8]) -> Vec<u8> {
|
|
73
|
-
crate::cookie_parser::cookie_parse_packed_vec(input)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
#[inline]
|
|
77
|
-
fn http_parse_request_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
|
|
78
|
-
crate::http_parser::http_parse_request_packed_vec(input)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
#[inline]
|
|
82
|
-
fn count_batch_data(data: &[u8], f: impl Fn(&[u8]) -> bool + Sync, chunk_items: usize) -> Result<u32> {
|
|
83
|
-
let items = unpack(data)?;
|
|
84
|
-
Ok(count_batch(&items, f, chunk_items) as u32)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
#[inline]
|
|
88
|
-
fn sum_ids_batch_data(data: &[u8], chunk_items: usize) -> Result<i64> {
|
|
89
|
-
let items = unpack(data)?;
|
|
90
|
-
Ok(sum_batch_i64(&items, |item| json_sum_ids_bytes(item).unwrap_or(0), chunk_items))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
#[inline]
|
|
94
|
-
fn total_len_batch_data(data: &[u8], f: impl Fn(&[u8]) -> Result<Vec<u8>> + Sync, chunk_items: usize) -> Result<u32> {
|
|
95
|
-
let items = unpack(data)?;
|
|
96
|
-
let chunk_items = chunk_items.max(64);
|
|
97
|
-
let total: usize = if should_parallelize(items.len(), total_bytes(&items)) {
|
|
98
|
-
use rayon::prelude::*;
|
|
99
|
-
items.par_chunks(chunk_items)
|
|
100
|
-
.map(|chunk| chunk.iter().map(|item| f(item).map(|v| v.len()).unwrap_or(0)).sum::<usize>())
|
|
101
|
-
.sum()
|
|
102
|
-
} else {
|
|
103
|
-
items.iter().map(|item| f(item).map(|v| v.len()).unwrap_or(0)).sum()
|
|
104
|
-
};
|
|
105
|
-
Ok(total as u32)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ── Packed input → packed output ──
|
|
109
|
-
|
|
110
|
-
#[inline]
|
|
111
|
-
fn json_valid_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
112
|
-
let items = unpack(data)?;
|
|
113
|
-
Ok(crate::util::validation_bitset_chunked(&items, json_valid_bytes, 512))
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
#[inline]
|
|
117
|
-
fn validate_email_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
118
|
-
let items = unpack(data)?;
|
|
119
|
-
Ok(crate::util::validation_bitset_chunked(&items, validate_email_bytes, 4096))
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
#[inline]
|
|
123
|
-
fn validate_uuid_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
124
|
-
let items = unpack(data)?;
|
|
125
|
-
Ok(crate::util::validation_bitset_chunked(&items, validate_uuid_bytes, 4096))
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
#[inline]
|
|
129
|
-
fn validate_ipv4_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
130
|
-
let items = unpack(data)?;
|
|
131
|
-
Ok(crate::util::validation_bitset_chunked(&items, validate_ipv4_bytes, 4096))
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
#[inline]
|
|
135
|
-
fn validate_ipv6_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
136
|
-
let items = unpack(data)?;
|
|
137
|
-
Ok(crate::util::validation_bitset_chunked(&items, validate_ipv6_bytes, 4096))
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
#[inline]
|
|
141
|
-
fn json_sum_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
142
|
-
let items = unpack(data)?;
|
|
143
|
-
let mut out = Vec::with_capacity(4 + items.len() * 8);
|
|
144
|
-
json_sum_batch_direct(&items, &mut out);
|
|
145
|
-
Ok(out)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
#[inline]
|
|
149
|
-
fn query_parse_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
150
|
-
let items = unpack(data)?;
|
|
151
|
-
let mut out = Vec::with_capacity(items.len() * 32);
|
|
152
|
-
parse_batch_direct(&items, &mut out, query_parse_packed_vec);
|
|
153
|
-
Ok(out)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
#[inline]
|
|
157
|
-
fn cookie_parse_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
158
|
-
let items = unpack(data)?;
|
|
159
|
-
let mut out = Vec::with_capacity(items.len() * 32);
|
|
160
|
-
parse_batch_direct(&items, &mut out, |i| Ok(cookie_parse_packed_vec(i)));
|
|
161
|
-
Ok(out)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
#[inline]
|
|
165
|
-
fn http_parse_request_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
166
|
-
let items = unpack(data)?;
|
|
167
|
-
let mut out = Vec::with_capacity(items.len() * 64);
|
|
168
|
-
parse_batch_direct(&items, &mut out, http_parse_request_packed_vec);
|
|
169
|
-
Ok(out)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ── Sync packed batch APIs ──
|
|
173
|
-
|
|
174
|
-
#[napi] pub fn json_valid_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(json_valid_batch_bytes(input.as_ref())?)) }
|
|
175
|
-
#[napi] pub fn validate_email_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_email_batch_bytes(input.as_ref())?)) }
|
|
176
|
-
#[napi] pub fn validate_uuid_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_uuid_batch_bytes(input.as_ref())?)) }
|
|
177
|
-
#[napi] pub fn validate_ipv4_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_ipv4_batch_bytes(input.as_ref())?)) }
|
|
178
|
-
#[napi] pub fn validate_ipv6_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_ipv6_batch_bytes(input.as_ref())?)) }
|
|
179
|
-
#[napi] pub fn json_sum_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(json_sum_batch_bytes(input.as_ref())?)) }
|
|
180
|
-
#[napi] pub fn query_parse_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(query_parse_batch_bytes(input.as_ref())?)) }
|
|
181
|
-
#[napi] pub fn cookie_parse_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(cookie_parse_batch_bytes(input.as_ref())?)) }
|
|
182
|
-
#[napi] pub fn http_parse_request_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(http_parse_request_batch_bytes(input.as_ref())?)) }
|
|
183
|
-
|
|
184
|
-
// ── Aggregate sync packed batch APIs ──
|
|
185
|
-
|
|
186
|
-
#[napi] pub fn json_valid_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), json_valid_bytes, 512) }
|
|
187
|
-
#[napi] pub fn validate_email_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_email_bytes, 4096) }
|
|
188
|
-
#[napi] pub fn validate_uuid_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_uuid_bytes, 4096) }
|
|
189
|
-
#[napi] pub fn validate_ipv4_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_ipv4_bytes, 4096) }
|
|
190
|
-
#[napi] pub fn validate_ipv6_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_ipv6_bytes, 4096) }
|
|
191
|
-
#[napi] pub fn json_sum_batch_total_packed(input: Uint8Array) -> Result<i64> { sum_ids_batch_data(input.as_ref(), 512) }
|
|
192
|
-
#[napi] pub fn query_parse_batch_total_len_packed(input: Uint8Array) -> Result<u32> { total_len_batch_data(input.as_ref(), query_parse_packed_vec, 256) }
|
|
193
|
-
#[napi] pub fn cookie_parse_batch_total_len_packed(input: Uint8Array) -> Result<u32> { total_len_batch_data(input.as_ref(), |i| Ok(cookie_parse_packed_vec(i)), 256) }
|
|
194
|
-
#[napi] pub fn http_parse_request_batch_total_len_packed(input: Uint8Array) -> Result<u32> { total_len_batch_data(input.as_ref(), http_parse_request_packed_vec, 256) }
|
|
195
|
-
|
package/rust/cookie_parser.rs
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
// rust/cookie_parser.rs — Unified zero-alloc cookie parser
|
|
2
|
-
// Eliminated the _vec variant; all callers use the single _into_slice path.
|
|
3
|
-
// Upper-bound pre-allocation for callers that need a Vec.
|
|
4
|
-
|
|
5
|
-
use crate::util::{trim_ascii_whitespace, write_bytes, write_u32_le};
|
|
6
|
-
use napi::bindgen_prelude::*;
|
|
7
|
-
use napi_derive::napi;
|
|
8
|
-
|
|
9
|
-
/// Parse cookies into a caller-provided slice (zero-alloc).
|
|
10
|
-
/// Output format: [u32 count] repeated { [u32 name_len] [name] [u32 value_len] [value] }
|
|
11
|
-
#[inline]
|
|
12
|
-
pub fn cookie_parse_packed_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
|
|
13
|
-
let mut pos = 0usize;
|
|
14
|
-
write_u32_le(out, &mut pos, 0)?;
|
|
15
|
-
|
|
16
|
-
let mut count = 0u32;
|
|
17
|
-
|
|
18
|
-
for pair in input.split(|&b| b == b';') {
|
|
19
|
-
let pair = trim_ascii_whitespace(pair);
|
|
20
|
-
if pair.is_empty() { continue; }
|
|
21
|
-
|
|
22
|
-
let (name, value) = match pair.iter().position(|&b| b == b'=') {
|
|
23
|
-
Some(eq) => (&pair[..eq], &pair[eq + 1..]),
|
|
24
|
-
None => (pair, &[][..]),
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
let name = trim_ascii_whitespace(name);
|
|
28
|
-
let value = trim_ascii_whitespace(value);
|
|
29
|
-
|
|
30
|
-
if name.is_empty() { continue; }
|
|
31
|
-
|
|
32
|
-
write_u32_le(out, &mut pos, name.len() as u32)?;
|
|
33
|
-
write_bytes(out, &mut pos, name)?;
|
|
34
|
-
|
|
35
|
-
write_u32_le(out, &mut pos, value.len() as u32)?;
|
|
36
|
-
write_bytes(out, &mut pos, value)?;
|
|
37
|
-
|
|
38
|
-
count += 1;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
out[0..4].copy_from_slice(&count.to_le_bytes());
|
|
42
|
-
Ok(pos)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/// Allocate and parse cookies. Uses conservative upper bound to avoid a pre-scan.
|
|
46
|
-
#[inline]
|
|
47
|
-
pub fn cookie_parse_packed_vec(input: &[u8]) -> Vec<u8> {
|
|
48
|
-
let upper_bound = input.len().saturating_mul(9).saturating_add(16);
|
|
49
|
-
let mut out = vec![0u8; upper_bound];
|
|
50
|
-
match cookie_parse_packed_into_slice(input, &mut out) {
|
|
51
|
-
Ok(written) => {
|
|
52
|
-
out.truncate(written);
|
|
53
|
-
out
|
|
54
|
-
}
|
|
55
|
-
Err(_) => {
|
|
56
|
-
// Fall back to VecWriter if slice is too small (rare).
|
|
57
|
-
vec![0u8; 4] // empty result
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
#[napi]
|
|
63
|
-
pub fn cookie_parse_packed(input: Uint8Array) -> Result<Buffer> {
|
|
64
|
-
Ok(Buffer::from(cookie_parse_packed_vec(input.as_ref())))
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
#[napi]
|
|
68
|
-
pub fn cookie_parse_packed_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
|
|
69
|
-
crate::util::run_packed_into(&input, &mut output, cookie_parse_packed_into_slice)
|
|
70
|
-
}
|