castrum 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2259 -0
- package/Cargo.toml +138 -0
- package/README.md +6 -0
- package/build.rs +5 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1348 -1295
- package/dist/index.js +4307 -3776
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +16 -0
- package/docs/FFI_BUN_GUIDE.md +89 -6
- package/docs/REPO_MAP.md +2 -2
- package/index.ts +19 -25
- package/package.json +9 -1
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +96 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +68 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +132 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +180 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +779 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +566 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +881 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +72 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +407 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +104 -8
- package/src/native/ffi/build/compress.ts +14 -10
- package/src/native/ffi/build/instances.ts +40 -3
- package/src/native/ffi/build/metrics.ts +224 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/util.ts +14 -1
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +10 -3
- package/src/native/ffi/types.ts +111 -9
- package/src/native/ffi.ts +87 -1
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +2 -2
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +17 -11
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +5 -4
- package/src/shared/codec.ts +18 -3
package/src/integration/index.ts
CHANGED
|
@@ -8,22 +8,22 @@
|
|
|
8
8
|
// - sseResponse → server-sent-events response framed on the fast path
|
|
9
9
|
|
|
10
10
|
export {
|
|
11
|
+
runMany,
|
|
12
|
+
runOne,
|
|
13
|
+
validateCount,
|
|
14
|
+
validateMany,
|
|
15
|
+
} from './batch'
|
|
16
|
+
export {
|
|
17
|
+
type CreatePipelineOptions,
|
|
11
18
|
createPipeline,
|
|
12
19
|
type IngressPipeline,
|
|
13
|
-
type PipelineResult,
|
|
14
20
|
type PipelineContext,
|
|
21
|
+
type PipelineResult,
|
|
15
22
|
type PreprocessOutcome,
|
|
16
|
-
type CreatePipelineOptions,
|
|
17
23
|
} from './pipeline'
|
|
24
|
+
export { type SseEvent, sseResponse } from './streaming'
|
|
18
25
|
export {
|
|
19
26
|
createWebSocketUpgrade,
|
|
20
27
|
type WebSocketUpgradeOptions,
|
|
21
28
|
type WebSocketUpgradeResult,
|
|
22
29
|
} from './websocket'
|
|
23
|
-
export { sseResponse, type SseEvent } from './streaming'
|
|
24
|
-
export {
|
|
25
|
-
validateMany,
|
|
26
|
-
validateCount,
|
|
27
|
-
runMany,
|
|
28
|
-
runOne,
|
|
29
|
-
} from './batch'
|
|
@@ -7,18 +7,18 @@
|
|
|
7
7
|
// context to the app's own handler. `handleRequest` is a fetch-compatible
|
|
8
8
|
// convenience that serves the terminal response or a rendered OK response.
|
|
9
9
|
|
|
10
|
+
import { readBodyWithLimit } from '../ingress/body'
|
|
11
|
+
import type { BakedIngressResult } from '../ingress/decode/baked-result'
|
|
10
12
|
import {
|
|
11
|
-
createIngressHandler,
|
|
12
13
|
type BakedIngressRuntime,
|
|
14
|
+
createIngressHandler,
|
|
13
15
|
type OptimizedIngressHandler,
|
|
14
16
|
} from '../ingress/handlers'
|
|
15
|
-
import type { BakedContext } from '../ingress/types'
|
|
16
17
|
import type { IngressHandlerOptions } from '../ingress/options'
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { generateRequestId } from '../shared/request-id'
|
|
18
|
+
import { DEFAULT_BODY_TIMEOUT_MS, DEFAULT_MAX_BODY_BYTES } from '../ingress/shared'
|
|
19
|
+
import type { BakedContext } from '../ingress/types'
|
|
20
20
|
import { decoder } from '../shared/bytes'
|
|
21
|
-
import
|
|
21
|
+
import { generateRequestId } from '../shared/request-id'
|
|
22
22
|
|
|
23
23
|
/** Readonly snapshot of the ingress decision, safe after `run()` returns. */
|
|
24
24
|
export interface PipelineResult {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/integration/websocket.ts — WebSocket upgrade handshake helper.
|
|
2
2
|
|
|
3
|
-
import { isBun } from '../shared/runtime'
|
|
4
3
|
import { rust } from '../rust-ffi'
|
|
5
|
-
|
|
4
|
+
import { isBun } from '../shared/runtime'
|
|
5
|
+
/** Options for `createWebSocketUpgrade` (supported subprotocols). */ export interface WebSocketUpgradeOptions {
|
|
6
6
|
/** Subprotocols this server supports; the first client-requested match wins. */
|
|
7
7
|
protocols?: ReadonlyArray<string>
|
|
8
8
|
}
|
package/src/loader/index.ts
CHANGED
|
@@ -27,11 +27,7 @@
|
|
|
27
27
|
import { rust } from '../rust-ffi'
|
|
28
28
|
import { encoder, viewForArrayBuffer } from '../shared/bytes'
|
|
29
29
|
import type { SchemaValidator } from '../shared/packed'
|
|
30
|
-
import {
|
|
31
|
-
createLruCache,
|
|
32
|
-
createTickCoalescer,
|
|
33
|
-
type LoadRequest,
|
|
34
|
-
} from './batch'
|
|
30
|
+
import { createLruCache, createTickCoalescer, type LoadRequest } from './batch'
|
|
35
31
|
import { createCostModel, nowNs } from './cost'
|
|
36
32
|
import {
|
|
37
33
|
fnvBatchKeysInto,
|
package/src/loader/ops.ts
CHANGED
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
// the loader never pays a conversion cost on the hot path. This module is the
|
|
20
20
|
// single source of truth for what the loader can dispatch.
|
|
21
21
|
|
|
22
|
+
import type { PasswordHashOptions } from '../native'
|
|
22
23
|
import { rust } from '../rust-ffi'
|
|
23
24
|
import { addon } from '../rust-ffi/addon'
|
|
24
25
|
import { decoder } from '../shared/bytes'
|
|
25
|
-
import type { PasswordHashOptions } from '../native'
|
|
26
26
|
import type { SchemaValidator } from '../shared/packed'
|
|
27
27
|
|
|
28
28
|
/** Empty bytes sentinel for skip-on-error scalar parity (e.g. AEAD decrypt). */
|
|
@@ -9,7 +9,7 @@ import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
|
|
|
9
9
|
import { SELFTEST_HEX, SELFTEST_JSON } from '../constants'
|
|
10
10
|
import type { BunFFI, Raw3, Raw4, Raw5, Raw6, Raw8, Raw9, Raw10, RawCStr } from '../types'
|
|
11
11
|
import type { BuildCtx } from './util'
|
|
12
|
-
import { argon2PhcLength, cstr, flag, growExact, writeOrThrow } from './util'
|
|
12
|
+
import { allocOut, argon2PhcLength, cstr, flag, growExact, writeOrThrow } from './util'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Build the codec/crypto/auth methods of the BunFFI surface. `ctx` is
|
|
@@ -34,7 +34,17 @@ export function buildCodecs(
|
|
|
34
34
|
const validateUuid = sym.castrum_validate_uuid as (...a: unknown[]) => number | bigint
|
|
35
35
|
const validateIpv4 = sym.castrum_validate_ipv4 as (...a: unknown[]) => number | bigint
|
|
36
36
|
const validateIpv6 = sym.castrum_validate_ipv6 as (...a: unknown[]) => number | bigint
|
|
37
|
+
const validateEmailBytes = sym.castrum_validate_email_bytes as (
|
|
38
|
+
...a: unknown[]
|
|
39
|
+
) => number | bigint
|
|
40
|
+
const validateUuidBytes = sym.castrum_validate_uuid_bytes as (...a: unknown[]) => number | bigint
|
|
41
|
+
const validateIpv4Bytes = sym.castrum_validate_ipv4_bytes as (...a: unknown[]) => number | bigint
|
|
42
|
+
const validateIpv6Bytes = sym.castrum_validate_ipv6_bytes as (...a: unknown[]) => number | bigint
|
|
37
43
|
const jsonSumRaw = sym.castrum_json_sum_ids as Raw4
|
|
44
|
+
const hexBatchRaw = sym.castrum_hex_validate_batch as Raw5
|
|
45
|
+
const hexBatchStrRaw = sym.castrum_hex_validate_batch_str as Raw4
|
|
46
|
+
const regexEscapeRaw = sym.castrum_regex_escape as Raw4
|
|
47
|
+
const regexEscapeStrRaw = sym.castrum_regex_escape_str as RawCStr
|
|
38
48
|
const hmacVerify = sym.castrum_hmac_sha256_verify as Raw6
|
|
39
49
|
const csrfVerify = sym.castrum_csrf_verify as Raw4
|
|
40
50
|
const passwordVerify = sym.castrum_password_verify as Raw4
|
|
@@ -176,6 +186,41 @@ export function buildCodecs(
|
|
|
176
186
|
validateUuid: (input) => Number(validateUuid(input)) === 1,
|
|
177
187
|
validateIpv4: (input) => Number(validateIpv4(input)) === 1,
|
|
178
188
|
validateIpv6: (input) => Number(validateIpv6(input)) === 1,
|
|
189
|
+
// Byte-input validators: `(ptr,len)` — zero transcode (no CString decode
|
|
190
|
+
// + engine re-encode like the cstring-ARG string forms above).
|
|
191
|
+
validateEmailBytes: (input) => Number(oneArg(validateEmailBytes, input)) === 1,
|
|
192
|
+
validateUuidBytes: (input) => Number(oneArg(validateUuidBytes, input)) === 1,
|
|
193
|
+
validateIpv4Bytes: (input) => Number(oneArg(validateIpv4Bytes, input)) === 1,
|
|
194
|
+
validateIpv6Bytes: (input) => Number(oneArg(validateIpv6Bytes, input)) === 1,
|
|
195
|
+
hexValidateBatchInto(input, width, output) {
|
|
196
|
+
// Batch fixed-width hex validation: NEWLINE-separated lines in; one
|
|
197
|
+
// verdict byte (1/0) per line out. Needed-size convention: `0` = bad
|
|
198
|
+
// width / null pointers (real error → throw); `> output.length` = the
|
|
199
|
+
// exact required size (caller grows once and retries). Width is
|
|
200
|
+
// validated here so an empty input can't mask it.
|
|
201
|
+
if (width === 0 || width > 4096) {
|
|
202
|
+
throw new Error('hex validate batch: width must be 1..=4096')
|
|
203
|
+
}
|
|
204
|
+
const w = Number(hexBatchRaw(input, lenOrView(input), width >>> 0, output, lenOrView(output)))
|
|
205
|
+
return w
|
|
206
|
+
},
|
|
207
|
+
regexEscapeInto(input, output) {
|
|
208
|
+
const w = Number(regexEscapeRaw(input, lenOrView(input), output, lenOrView(output)))
|
|
209
|
+
if (w === 0) {
|
|
210
|
+
throw new Error('regex escape: output buffer too small')
|
|
211
|
+
}
|
|
212
|
+
return w
|
|
213
|
+
},
|
|
214
|
+
// Zero-copy text path: cstring ARG in (engine-transcoded), cstring return
|
|
215
|
+
// out (engine-cloned) — the JS side does zero encode AND zero decode.
|
|
216
|
+
regexEscapeStr: (input) => regexEscapeStrRaw(input),
|
|
217
|
+
hexValidateBatchStr(ids, width, output) {
|
|
218
|
+
if (width === 0 || width > 4096) {
|
|
219
|
+
throw new Error('hex validate batch: width must be 1..=4096')
|
|
220
|
+
}
|
|
221
|
+
const w = Number(hexBatchStrRaw(ids, width >>> 0, output, lenOrView(output)))
|
|
222
|
+
return w
|
|
223
|
+
},
|
|
179
224
|
jsonSumIds: (input) => {
|
|
180
225
|
// Packed [u8 ok][i64 sum LE] output (9 B): ok=1 → valid array (the sum
|
|
181
226
|
// may be 0); ok=0 → invalid input. Bytes written: 9/1/0 (0 = real error).
|
|
@@ -202,19 +247,19 @@ export function buildCodecs(
|
|
|
202
247
|
Number(passwordVerifyBcrypt(password, lenOrView(password), phc)) === 1,
|
|
203
248
|
|
|
204
249
|
hexDecode(input) {
|
|
205
|
-
const out =
|
|
250
|
+
const out = allocOut(Math.floor(input.length / 2))
|
|
206
251
|
const w = hexDecodeInto(input, out)
|
|
207
252
|
return out.subarray(0, w)
|
|
208
253
|
},
|
|
209
254
|
hexDecodeInto,
|
|
210
255
|
urlDecode(input) {
|
|
211
|
-
const out =
|
|
256
|
+
const out = allocOut(input.length)
|
|
212
257
|
const w = urlDecodeInto(input, out)
|
|
213
258
|
return out.subarray(0, w)
|
|
214
259
|
},
|
|
215
260
|
urlDecodeInto,
|
|
216
261
|
base64Decode(input, urlSafe, padding) {
|
|
217
|
-
const out =
|
|
262
|
+
const out = allocOut(Math.ceil((input.length * 3) / 4))
|
|
218
263
|
const w = base64DecodeInto(input, out, urlSafe, padding)
|
|
219
264
|
return out.subarray(0, w)
|
|
220
265
|
},
|
|
@@ -398,7 +443,7 @@ export function buildCodecs(
|
|
|
398
443
|
},
|
|
399
444
|
passwordHashBcrypt(password, cost) {
|
|
400
445
|
// `$2b$CC$` + 22 salt chars + 31 hash chars = 60 chars.
|
|
401
|
-
const out =
|
|
446
|
+
const out = allocOut(64)
|
|
402
447
|
const w = Number(passwordHashBcrypt(password, lenOrView(password), cost, out, lenOrView(out)))
|
|
403
448
|
if (w === 0) {
|
|
404
449
|
throw new Error('password hash bcrypt: output buffer too small')
|
|
@@ -409,7 +454,7 @@ export function buildCodecs(
|
|
|
409
454
|
// Rust clamps dkLen to [1, 1MiB] (PBKDF2_MIN_LEN/MAX_LEN) AFTER sizing its
|
|
410
455
|
// own buffer — so pre-clamp here so dkLen 0 still yields a 1-byte result.
|
|
411
456
|
const dk = Math.min(Math.max(dkLen, 1), 1024 * 1024)
|
|
412
|
-
const out =
|
|
457
|
+
const out = allocOut(dk)
|
|
413
458
|
const w = Number(
|
|
414
459
|
pbkdf2(
|
|
415
460
|
password,
|
|
@@ -429,7 +474,7 @@ export function buildCodecs(
|
|
|
429
474
|
},
|
|
430
475
|
aeadEncrypt(key, nonce, plaintext, algorithm = 0) {
|
|
431
476
|
// ciphertext + 16-byte auth tag.
|
|
432
|
-
const out =
|
|
477
|
+
const out = allocOut(plaintext.length + 16)
|
|
433
478
|
const w = Number(
|
|
434
479
|
aeadEncrypt(
|
|
435
480
|
key,
|
|
@@ -472,7 +517,7 @@ export function buildCodecs(
|
|
|
472
517
|
return w
|
|
473
518
|
},
|
|
474
519
|
aeadDecrypt(key, nonce, ciphertext, algorithm = 0) {
|
|
475
|
-
const out =
|
|
520
|
+
const out = allocOut(ciphertext.length)
|
|
476
521
|
const w = Number(
|
|
477
522
|
aeadDecrypt(
|
|
478
523
|
key,
|
|
@@ -534,6 +579,17 @@ export function selfTestCodecs(b: BunFFI): boolean {
|
|
|
534
579
|
) {
|
|
535
580
|
return false
|
|
536
581
|
}
|
|
582
|
+
// Byte-input validator siblings: `(ptr,len)` parity with the string forms.
|
|
583
|
+
if (
|
|
584
|
+
!b.validateEmailBytes(enc.encode('a@b.com')) ||
|
|
585
|
+
!b.validateUuidBytes(enc.encode('550e8400-e29b-41d4-a716-446655440000')) ||
|
|
586
|
+
!b.validateIpv4Bytes(enc.encode('192.168.0.1')) ||
|
|
587
|
+
!b.validateIpv6Bytes(enc.encode('2001:db8::1')) ||
|
|
588
|
+
b.validateEmailBytes(enc.encode('not-an-email')) ||
|
|
589
|
+
b.validateIpv6Bytes(enc.encode('999'))
|
|
590
|
+
) {
|
|
591
|
+
return false
|
|
592
|
+
}
|
|
537
593
|
// Packed `[u8 ok][i64 sum LE]` ABI: legit zero-sum is ok, invalid input throws.
|
|
538
594
|
if (b.jsonSumIds(enc.encode(`[{"id":1},{"id":2}]`)) !== 3n) return false
|
|
539
595
|
if (b.jsonSumIds(enc.encode(`[{"id":0},{"id":0}]`)) !== 0n) return false
|
|
@@ -545,6 +601,46 @@ export function selfTestCodecs(b: BunFFI): boolean {
|
|
|
545
601
|
}
|
|
546
602
|
if (!sumInvalidThrew) return false
|
|
547
603
|
|
|
604
|
+
// Batch fixed-width hex validation (needed-size convention + verdicts).
|
|
605
|
+
{
|
|
606
|
+
const ids = enc.encode('507f1f77bcf86cd799439011\nzz\n507F1F77BCF86CD799439012')
|
|
607
|
+
const tiny = new Uint8Array(2)
|
|
608
|
+
const needed = b.hexValidateBatchInto(ids, 24, tiny)
|
|
609
|
+
if (needed !== 3) return false // exact required size on a too-small buffer
|
|
610
|
+
const out = new Uint8Array(needed)
|
|
611
|
+
if (b.hexValidateBatchInto(ids, 24, out) !== 3) return false
|
|
612
|
+
if (out[0] !== 1 || out[1] !== 0 || out[2] !== 1) return false
|
|
613
|
+
let widthThrew = false
|
|
614
|
+
try {
|
|
615
|
+
b.hexValidateBatchInto(ids, 0, new Uint8Array(3))
|
|
616
|
+
} catch {
|
|
617
|
+
widthThrew = true
|
|
618
|
+
}
|
|
619
|
+
if (!widthThrew) return false
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// RegExp escaping: metachars get backslashes; plain text is untouched.
|
|
623
|
+
{
|
|
624
|
+
const out = new Uint8Array(64)
|
|
625
|
+
const w = b.regexEscapeInto(enc.encode('a.c(x)'), out)
|
|
626
|
+
if (w !== 9 || dec.decode(out.subarray(0, w)) !== 'a\\.c\\(x\\)') return false
|
|
627
|
+
const plain = new Uint8Array(16)
|
|
628
|
+
const wp = b.regexEscapeInto(enc.encode('hello'), plain)
|
|
629
|
+
if (wp !== 5 || dec.decode(plain.subarray(0, wp)) !== 'hello') return false
|
|
630
|
+
// too-small buffer reports the exact required size
|
|
631
|
+
if (b.regexEscapeInto(enc.encode('a.b'), new Uint8Array(2)) !== 4) return false
|
|
632
|
+
// zero-copy str sibling must produce the identical escaped string
|
|
633
|
+
if (b.regexEscapeStr('a.c(x)') !== 'a\\.c\\(x\\)') return false
|
|
634
|
+
if (b.regexEscapeStr('plain text') !== 'plain text') return false
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// String-input batch hex validation (cstring ARG sibling).
|
|
638
|
+
{
|
|
639
|
+
const out = new Uint8Array(8)
|
|
640
|
+
const w = b.hexValidateBatchStr('507f1f77bcf86cd799439011\nzz', 24, out)
|
|
641
|
+
if (w !== 2 || out[0] !== 1 || out[1] !== 0) return false
|
|
642
|
+
}
|
|
643
|
+
|
|
548
644
|
// HMAC RFC 4231 test case 1 (0x0b × 20 key, "Hi There" data).
|
|
549
645
|
const hmacKey = new Uint8Array(20).fill(0x0b)
|
|
550
646
|
const hmacData = enc.encode('Hi There')
|
|
@@ -29,7 +29,7 @@ export function buildCompress(
|
|
|
29
29
|
sym: Record<string, (...a: unknown[]) => unknown>,
|
|
30
30
|
ctx: BuildCtx,
|
|
31
31
|
): Partial<BunFFI> {
|
|
32
|
-
const { lenOrView } = ctx
|
|
32
|
+
const { lenOrView, scratchFor } = ctx
|
|
33
33
|
|
|
34
34
|
const gzipCompress = sym.castrum_gzip_compress as Raw5
|
|
35
35
|
const gzipDecompress = sym.castrum_gzip_decompress as Raw5
|
|
@@ -40,15 +40,19 @@ export function buildCompress(
|
|
|
40
40
|
|
|
41
41
|
return {
|
|
42
42
|
jsonPatch(doc, patch) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
),
|
|
51
|
-
|
|
43
|
+
// Pooled scratch — the patched doc is decoded to an immutable string
|
|
44
|
+
// synchronously (same call-scope contract as the cstring returns), so
|
|
45
|
+
// the bytes never need a fresh buffer. One exact retry on miss.
|
|
46
|
+
let out = scratchFor(Math.min(Math.max(doc.length, patch.length) + 16, 64 * 1024))
|
|
47
|
+
let w = Number(jsonPatch(doc, lenOrView(doc), patch, lenOrView(patch), out, lenOrView(out)))
|
|
48
|
+
if (w > out.length && w <= MAX_JSON_PATCH_OUTPUT) {
|
|
49
|
+
out = new Uint8Array(w)
|
|
50
|
+
w = Number(jsonPatch(doc, lenOrView(doc), patch, lenOrView(patch), out, lenOrView(out)))
|
|
51
|
+
}
|
|
52
|
+
if (w === 0 || w > out.length) {
|
|
53
|
+
throw new Error('json patch: output buffer too small or patch inapplicable')
|
|
54
|
+
}
|
|
55
|
+
return decodeUtf8(out.subarray(0, w))
|
|
52
56
|
},
|
|
53
57
|
gzipCompress(data, level = 6) {
|
|
54
58
|
// The C ABI streams the compressed output directly into `out` (no internal
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
|
|
10
10
|
import { EMPTY_VIEW } from '../constants'
|
|
11
|
-
import type { BunFFI, Raw2, Raw3, Raw5, Raw6, Raw7, Raw8, Raw12, RawCStr } from '../types'
|
|
11
|
+
import type { BunFFI, Raw2, Raw3, Raw4, Raw5, Raw6, Raw7, Raw8, Raw12, RawCStr } from '../types'
|
|
12
12
|
import type { BuildCtx } from './util'
|
|
13
13
|
import { cstr, growExact, unpackRateCheck } from './util'
|
|
14
14
|
|
|
@@ -25,8 +25,9 @@ export function buildInstances(
|
|
|
25
25
|
const conditionalIsNotModifiedRaw = sym.castrum_conditional_is_not_modified as Raw6
|
|
26
26
|
const mediaTypeMatcherMatchesRaw = sym.castrum_media_type_matcher_matches as Raw3
|
|
27
27
|
const acceptNegotiatorNegotiateRaw = sym.castrum_accept_negotiator_negotiate as RawCStr
|
|
28
|
-
const acceptNegotiatorNegotiateServerRaw = sym
|
|
29
|
-
|
|
28
|
+
const acceptNegotiatorNegotiateServerRaw = sym.castrum_accept_negotiator_negotiate_server as (
|
|
29
|
+
...a: unknown[]
|
|
30
|
+
) => string | null
|
|
30
31
|
const ed25519GenerateKeypairRaw = sym.castrum_ed25519_generate_keypair as Raw2
|
|
31
32
|
const ed25519SignRaw = sym.castrum_ed25519_sign as Raw6
|
|
32
33
|
const ed25519VerifyRaw = sym.castrum_ed25519_verify as Raw6
|
|
@@ -36,6 +37,10 @@ export function buildInstances(
|
|
|
36
37
|
const jwtSignerVerifyRaw = sym.castrum_jwt_signer_verify as Raw6
|
|
37
38
|
const templateRenderRaw = sym.castrum_template_render as Raw5
|
|
38
39
|
const schemaValidatorValidateRaw = sym.castrum_schema_validator_validate as Raw3
|
|
40
|
+
const queryValidateSym = sym.castrum_query_validate as (...a: unknown[]) => number | bigint
|
|
41
|
+
const cookieValidateSym = sym.castrum_cookie_validate as (...a: unknown[]) => number | bigint
|
|
42
|
+
const sessionSealSym = sym.castrum_session_seal as (...a: unknown[]) => string | null
|
|
43
|
+
const sessionOpenSym = sym.castrum_session_open as Raw4
|
|
39
44
|
const rateLimiterCheckRaw = sym.castrum_rate_limiter_check as Raw5
|
|
40
45
|
const rateLimiterCheckKeyRaw = sym.castrum_rate_limiter_check_key as Raw5
|
|
41
46
|
const jwtSignBytes = sym.castrum_jwt_sign_bytes as RawCStr
|
|
@@ -138,6 +143,22 @@ export function buildInstances(
|
|
|
138
143
|
schemaValidatorValidate(inner, doc) {
|
|
139
144
|
return Number(schemaValidatorValidateRaw(inner, doc, lenOrView(doc))) === 1
|
|
140
145
|
},
|
|
146
|
+
queryValidate(inner, qs) {
|
|
147
|
+
// `qs` is a cstring ARG — engine-transcoded (zero JS encode).
|
|
148
|
+
return Number(queryValidateSym(inner, qs)) === 1
|
|
149
|
+
},
|
|
150
|
+
cookieValidate(inner, header) {
|
|
151
|
+
return Number(cookieValidateSym(inner, header)) === 1
|
|
152
|
+
},
|
|
153
|
+
sessionSeal(id, dataJson, expSecs, secret) {
|
|
154
|
+
// exp is an i64 C arg → BigInt.
|
|
155
|
+
return sessionSealSym(id, dataJson, BigInt(expSecs), secret)
|
|
156
|
+
},
|
|
157
|
+
sessionOpen(token, secret, output) {
|
|
158
|
+
// Needed-size convention; 0 = bad signature / malformed.
|
|
159
|
+
const w = Number(sessionOpenSym(token, secret, output, lenOrView(output)))
|
|
160
|
+
return w
|
|
161
|
+
},
|
|
141
162
|
rateLimiterCheck(inner, key, nowMs) {
|
|
142
163
|
// Packed [u8 allowed][u32 remaining LE][i64 reset_ms LE] (13 bytes).
|
|
143
164
|
// Reused scratch + cached DataView (no per-call allocs). `key` is a
|
|
@@ -583,6 +604,22 @@ export function selfTestInstances(b: BunFFI): boolean {
|
|
|
583
604
|
if (!urlBuilderThrew) {
|
|
584
605
|
return false
|
|
585
606
|
}
|
|
607
|
+
// Wire-validate: null inner (0) → false (ABI exercise); real parity lives
|
|
608
|
+
// in ffi.test.ts with a live compiled instance.
|
|
609
|
+
if (b.queryValidate(0, 'route=/a') !== false) return false
|
|
610
|
+
if (b.cookieValidate(0, 'route=/a') !== false) return false
|
|
611
|
+
|
|
612
|
+
// Session seal/open round trip through the C ABI.
|
|
613
|
+
{
|
|
614
|
+
const tok = b.sessionSeal('sess-9', '{"n":1}', 1234567, 'sekrit')
|
|
615
|
+
if (tok === null || !tok.includes('.') || !tok.startsWith('{"id":"sess-9"')) return false
|
|
616
|
+
const out = new Uint8Array(256)
|
|
617
|
+
const w = b.sessionOpen(tok, 'sekrit', out)
|
|
618
|
+
if (w <= 13 || out[0] !== 1) return false
|
|
619
|
+
// bad signature → 0
|
|
620
|
+
if (b.sessionOpen(tok, 'wrong', out) !== 0) return false
|
|
621
|
+
}
|
|
622
|
+
|
|
586
623
|
// rateLimiterCheckKey: null (0) handle → throw (ABI exercise).
|
|
587
624
|
let rlKeyThrew = false
|
|
588
625
|
try {
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// src/native/ffi/build/metrics.ts — metrics-registry BunFFI methods.
|
|
2
|
+
//
|
|
3
|
+
// Wraps the `castrum_metrics_*` caller-owned-handle surface (route-stack
|
|
4
|
+
// ownership model): create returns a native `Box<MetricsRegistry>` handle,
|
|
5
|
+
// declare fns cross name/label-keys as cstring ARGS and return the u32 series
|
|
6
|
+
// id, and record/set/render take packed byte slices. Receives the raw dlopen'd
|
|
7
|
+
// symbols from `build()`.
|
|
8
|
+
|
|
9
|
+
import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
|
|
10
|
+
import type { BunFFI, Raw0, Raw3, Raw4, Raw5 } from '../types'
|
|
11
|
+
import type { BuildCtx } from './util'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sentinel returned by the declare fns (`u32::MAX`) on invalid input — fits
|
|
15
|
+
* f64 exactly and can never collide with a real dense family id.
|
|
16
|
+
*/
|
|
17
|
+
export const METRICS_DECLARE_ERR = 0xffffffff
|
|
18
|
+
|
|
19
|
+
/** Empty label-values view (zero-label families). */
|
|
20
|
+
const EMPTY = new Uint8Array(0)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the metrics-registry methods of the BunFFI surface.
|
|
24
|
+
*/
|
|
25
|
+
export function buildMetrics(
|
|
26
|
+
sym: Record<string, (...a: unknown[]) => unknown>,
|
|
27
|
+
ctx: BuildCtx,
|
|
28
|
+
): Partial<BunFFI> {
|
|
29
|
+
const { lenOrView } = ctx
|
|
30
|
+
const metricsCreateRaw = sym.castrum_metrics_create as Raw0
|
|
31
|
+
const metricsCounterRaw = sym.castrum_metrics_counter as Raw3
|
|
32
|
+
const metricsGaugeRaw = sym.castrum_metrics_gauge as Raw3
|
|
33
|
+
const metricsHistogramRaw = sym.castrum_metrics_histogram as Raw4
|
|
34
|
+
const metricsRecordRaw = sym.castrum_metrics_record as Raw5
|
|
35
|
+
const metricsGaugeSetRaw = sym.castrum_metrics_gauge_set as Raw5
|
|
36
|
+
// Zero-encode siblings: joined values cross as a cstring ARG.
|
|
37
|
+
const metricsRecordStrRaw = sym.castrum_metrics_record_str as (...a: unknown[]) => number | bigint
|
|
38
|
+
const metricsGaugeSetStrRaw = sym.castrum_metrics_gauge_set_str as (
|
|
39
|
+
...a: unknown[]
|
|
40
|
+
) => number | bigint
|
|
41
|
+
const metricsRenderRaw = sym.castrum_metrics_render as Raw3
|
|
42
|
+
const metricsSnapshotRaw = sym.castrum_metrics_snapshot as Raw3
|
|
43
|
+
const metricsRecordBatchRaw = sym.castrum_metrics_record_batch as Raw3
|
|
44
|
+
const metricsDestroyRaw = sym.castrum_metrics_destroy as (h: unknown) => void
|
|
45
|
+
|
|
46
|
+
/** Declare helper: maps the C sentinel to a throw (fail fast at declare). */
|
|
47
|
+
const declareOrThrow = (id: number | bigint, what: string): number => {
|
|
48
|
+
const n = Number(id)
|
|
49
|
+
if (n === METRICS_DECLARE_ERR) {
|
|
50
|
+
throw new Error(`metrics: failed to declare ${what} (invalid name/labels/buckets)`)
|
|
51
|
+
}
|
|
52
|
+
return n
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
metricsCreate() {
|
|
57
|
+
const h = Number(metricsCreateRaw())
|
|
58
|
+
if (h === 0) throw new Error('metrics: registry allocation failed')
|
|
59
|
+
return h
|
|
60
|
+
},
|
|
61
|
+
metricsCounter(handle, name, labelKeys) {
|
|
62
|
+
return declareOrThrow(metricsCounterRaw(handle, name, labelKeys), `counter "${name}"`)
|
|
63
|
+
},
|
|
64
|
+
metricsGauge(handle, name, labelKeys) {
|
|
65
|
+
return declareOrThrow(metricsGaugeRaw(handle, name, labelKeys), `gauge "${name}"`)
|
|
66
|
+
},
|
|
67
|
+
metricsHistogram(handle, name, labelKeys, bucketsCsv) {
|
|
68
|
+
return declareOrThrow(
|
|
69
|
+
metricsHistogramRaw(handle, name, labelKeys, bucketsCsv),
|
|
70
|
+
`histogram "${name}"`,
|
|
71
|
+
)
|
|
72
|
+
},
|
|
73
|
+
metricsRecord(handle, series, values, amount) {
|
|
74
|
+
// Packed `\x1f`-separated values cross as `(ptr,len)`; u8 verdict.
|
|
75
|
+
return Number(metricsRecordRaw(handle, series, values, lenOrView(values), amount)) === 1
|
|
76
|
+
},
|
|
77
|
+
metricsGaugeSet(handle, series, values, value) {
|
|
78
|
+
return Number(metricsGaugeSetRaw(handle, series, values, lenOrView(values), value)) === 1
|
|
79
|
+
},
|
|
80
|
+
metricsRecordStr(handle, series, values, amount) {
|
|
81
|
+
// `values` is the JOINED `\x1f` string — engine-transcoded (zero encode).
|
|
82
|
+
return Number(metricsRecordStrRaw(handle, series, values, amount)) === 1
|
|
83
|
+
},
|
|
84
|
+
metricsGaugeSetStr(handle, series, values, value) {
|
|
85
|
+
return Number(metricsGaugeSetStrRaw(handle, series, values, value)) === 1
|
|
86
|
+
},
|
|
87
|
+
metricsRender(handle, output) {
|
|
88
|
+
// Needed-size convention (routeRun parity): `0` = real error → throw;
|
|
89
|
+
// `> output.length` = the EXACT required size (caller grows once and
|
|
90
|
+
// retries — never a doubling re-run loop).
|
|
91
|
+
const w = Number(metricsRenderRaw(handle, output, lenOrView(output)))
|
|
92
|
+
if (w === 0) throw new Error('metrics render: registry handle invalid or render failed')
|
|
93
|
+
return w
|
|
94
|
+
},
|
|
95
|
+
metricsSnapshot(handle, output) {
|
|
96
|
+
// Needed-size convention (routeRun parity): 0 throws; > length = exact.
|
|
97
|
+
const w = Number(metricsSnapshotRaw(handle, output, lenOrView(output)))
|
|
98
|
+
if (w === 0) throw new Error('metrics snapshot: registry handle invalid')
|
|
99
|
+
return w
|
|
100
|
+
},
|
|
101
|
+
metricsRecordBatch(handle, packed) {
|
|
102
|
+
return Number(metricsRecordBatchRaw(handle, packed, lenOrView(packed))) === 1
|
|
103
|
+
},
|
|
104
|
+
metricsDestroy(handle) {
|
|
105
|
+
metricsDestroyRaw(handle)
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Bind-time self-test for the metrics surface (unique `ct.selftest.*` names).
|
|
112
|
+
* Exercises every symbol's ABI at bind time; `false` disables the ffi layer
|
|
113
|
+
* and forces the napi fallback.
|
|
114
|
+
*/
|
|
115
|
+
export function selfTestMetrics(b: BunFFI): boolean {
|
|
116
|
+
let handle = 0
|
|
117
|
+
try {
|
|
118
|
+
handle = b.metricsCreate()
|
|
119
|
+
if (handle === 0) return false
|
|
120
|
+
|
|
121
|
+
const counter = b.metricsCounter(handle, 'ct_selftest_hits', 'route\u001fstatus')
|
|
122
|
+
if (!Number.isInteger(counter) || counter < 0 || counter === METRICS_DECLARE_ERR) {
|
|
123
|
+
console.error('FAIL: declare')
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
if (!b.metricsRecord(handle, counter, encodeUtf8('/a\u001f200'), 2)) {
|
|
127
|
+
console.error('FAIL: record1')
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
if (!b.metricsRecord(handle, counter, encodeUtf8('/a\u001f200'), 1)) {
|
|
131
|
+
console.error('FAIL: record2')
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
if (!b.metricsRecordStr(handle, counter, '/b\u001f200', 2)) {
|
|
135
|
+
console.error('FAIL: recordStr')
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.error('CK: counter')
|
|
140
|
+
const gauge = b.metricsGauge(handle, 'ct_selftest_depth', 'q')
|
|
141
|
+
if (gauge === METRICS_DECLARE_ERR) return false
|
|
142
|
+
if (!b.metricsGaugeSet(handle, gauge, encodeUtf8('jobs'), 4)) return false
|
|
143
|
+
if (!b.metricsGaugeSetStr(handle, gauge, 'io', 6)) return false
|
|
144
|
+
|
|
145
|
+
console.error('CK: gauge')
|
|
146
|
+
const hist = b.metricsHistogram(handle, 'ct_selftest_latency', '', '0.1,0.5')
|
|
147
|
+
if (hist === METRICS_DECLARE_ERR) return false
|
|
148
|
+
if (!b.metricsRecord(handle, hist, EMPTY, 0.25)) return false
|
|
149
|
+
if (b.metricsRecord(handle, hist, EMPTY, -1)) return false // negatives rejected
|
|
150
|
+
|
|
151
|
+
console.error('CK: hist')
|
|
152
|
+
// Arity mismatch must fail safely.
|
|
153
|
+
if (b.metricsRecord(handle, counter, encodeUtf8('/a'), 1)) return false
|
|
154
|
+
|
|
155
|
+
console.error('CK: arity')
|
|
156
|
+
// Render: probe with a small buffer → exact required size; then render.
|
|
157
|
+
const probe = new Uint8Array(16)
|
|
158
|
+
const needed = b.metricsRender(handle, probe)
|
|
159
|
+
if (needed <= probe.length) return false
|
|
160
|
+
const out = new Uint8Array(needed)
|
|
161
|
+
const written = b.metricsRender(handle, out)
|
|
162
|
+
if (written !== needed) return false
|
|
163
|
+
const text = decodeUtf8(out.subarray(0, written))
|
|
164
|
+
if (
|
|
165
|
+
!text.includes('ct_selftest_hits{route="/a",status="200"} 3\n') ||
|
|
166
|
+
!text.includes('ct_selftest_hits{route="/b",status="200"} 2\n') ||
|
|
167
|
+
!text.includes('ct_selftest_depth{q="jobs"} 4\n') ||
|
|
168
|
+
!text.includes('ct_selftest_depth{q="io"} 6\n') ||
|
|
169
|
+
!text.includes('ct_selftest_latency_bucket{le="0.5"} 1\n') ||
|
|
170
|
+
!text.includes('ct_selftest_latency_count 1\n')
|
|
171
|
+
) {
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.error('CK: render')
|
|
176
|
+
// Snapshot: packed dump starts with version=1 + familyCount>=3.
|
|
177
|
+
{
|
|
178
|
+
const probe2 = new Uint8Array(16)
|
|
179
|
+
const need = b.metricsSnapshot(handle, probe2)
|
|
180
|
+
if (need <= probe2.length) return false
|
|
181
|
+
const snap = new Uint8Array(need)
|
|
182
|
+
if (b.metricsSnapshot(handle, snap) !== need) return false
|
|
183
|
+
const sv = new DataView(snap.buffer)
|
|
184
|
+
if (sv.getUint32(0, true) !== 1) return false
|
|
185
|
+
if (sv.getUint32(4, true) < 3) return false
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.error('CK: snapshot')
|
|
189
|
+
// Batch: two entries in ONE crossing.
|
|
190
|
+
{
|
|
191
|
+
const c2 = b.metricsCounter(handle, 'ct_selftest_batch', 'k')
|
|
192
|
+
const buf = new Uint8Array(64)
|
|
193
|
+
const dv = new DataView(buf.buffer)
|
|
194
|
+
dv.setUint32(0, 1, true)
|
|
195
|
+
dv.setUint32(4, c2, true)
|
|
196
|
+
dv.setUint32(8, 1, true)
|
|
197
|
+
buf[12] = 'v'.charCodeAt(0)
|
|
198
|
+
dv.setFloat64(13, 4, true)
|
|
199
|
+
if (!b.metricsRecordBatch(handle, buf.subarray(0, 21))) return false
|
|
200
|
+
const probe3 = new Uint8Array(16)
|
|
201
|
+
const need3 = b.metricsRender(handle, probe3)
|
|
202
|
+
const out3 = new Uint8Array(need3)
|
|
203
|
+
b.metricsRender(handle, out3)
|
|
204
|
+
if (!decodeUtf8(out3.subarray(0, need3)).includes('ct_selftest_batch{k="v"} 4')) {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.error('CK: batch')
|
|
210
|
+
// A null handle must throw on declare (never dereference freed state).
|
|
211
|
+
try {
|
|
212
|
+
b.metricsCounter(0, 'ct_selftest_x', '')
|
|
213
|
+
return false
|
|
214
|
+
} catch {
|
|
215
|
+
// expected
|
|
216
|
+
}
|
|
217
|
+
return true
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error('METRICS SELFTEST THROW:', err)
|
|
220
|
+
return false
|
|
221
|
+
} finally {
|
|
222
|
+
b.metricsDestroy(handle)
|
|
223
|
+
}
|
|
224
|
+
}
|