castrum 0.9.7 → 0.9.9
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 +1 -1
- package/Cargo.toml +1 -1
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +664 -8
- package/dist/index.js +2014 -1327
- package/docs/BENCHMARKS.md +13 -0
- package/docs/CASE_STUDY.md +1 -1
- package/docs/ENVIRONMENT.md +8 -1
- package/docs/FFI_BUN_GUIDE.md +1 -1
- package/docs/INGRESS.md +4 -4
- package/docs/REPO_MAP.md +7 -5
- package/docs/RND-CONCURRENCY.md +20 -4
- package/docs/superpowers/plans/2026-09-18-castrum-hardening-cleanup.md +617 -0
- package/docs/superpowers/plans/2026-09-18-elysia-learnings-adoption.md +634 -0
- package/index.ts +6 -0
- package/package.json +4 -2
- package/rust/crypto/aead.rs +44 -0
- package/rust/crypto/argon2.rs +32 -2
- package/rust/crypto/jwt/tests.rs +19 -0
- package/rust/crypto/jwt/token.rs +43 -28
- package/rust/crypto/session.rs +6 -0
- package/rust/ffi/crypto.rs +63 -32
- package/rust/ffi/hashing.rs +48 -33
- package/rust/ffi/http.rs +62 -36
- package/rust/ffi/metrics.rs +6 -2
- package/rust/ffi/payload.rs +47 -27
- package/rust/ffi/task.rs +55 -35
- package/rust/ffi/tests.rs +24 -0
- package/rust/ffi/util.rs +40 -0
- package/rust/http/headers.rs +13 -0
- package/rust/http/multipart.rs +139 -0
- package/rust/ingress/api.rs +21 -5
- package/rust/ingress/mod.rs +7 -0
- package/rust/ingress/native_route.rs +25 -5
- package/rust/ingress/schema_cache.rs +266 -0
- package/rust/task/completion.rs +7 -0
- package/rust/task/mod.rs +2 -1
- package/rust/task/runtime.rs +55 -7
- package/rust/task/tests.rs +128 -1
- package/src/ingress/abort.ts +48 -0
- package/src/ingress/body.ts +104 -8
- package/src/ingress/decode/baked-result.ts +13 -5
- package/src/ingress/decode/fast-result.ts +1 -1
- package/src/ingress/decode/result-base.ts +16 -2
- package/src/ingress/handlers.ts +104 -31
- package/src/ingress/headers/memoized-headers.ts +40 -0
- package/src/ingress/health.ts +22 -0
- package/src/ingress/index.ts +3 -1
- package/src/ingress/native-route.ts +25 -6
- package/src/ingress/options.ts +26 -2
- package/src/ingress/packing/gather-raw-headers.ts +24 -8
- package/src/ingress/response/baked-response.ts +118 -28
- package/src/ingress/router.ts +50 -3
- package/src/ingress/routes/common.ts +35 -11
- package/src/ingress/routes/echo.ts +60 -7
- package/src/ingress/routes/json-write.ts +32 -8
- package/src/ingress/routes/native.ts +24 -5
- package/src/ingress/routes/options.ts +6 -2
- package/src/ingress/routes/responder.ts +29 -9
- package/src/ingress/routes/safe-record.ts +57 -0
- package/src/ingress/server-error.ts +111 -0
- package/src/ingress/server-node.ts +135 -37
- package/src/ingress/server.ts +143 -36
- package/src/ingress/shared.ts +1 -1
- package/src/ingress/types.ts +15 -0
- package/src/integration/pipeline.ts +6 -0
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs-selftest.ts +305 -0
- package/src/native/ffi/build/codecs.ts +0 -296
- package/src/native/ffi/build/instances-selftest.ts +264 -0
- package/src/native/ffi/build/instances.ts +10 -259
- package/src/native/ffi/build/parse-selftest.ts +238 -0
- package/src/native/ffi/build/parse.ts +11 -234
- package/src/native/ffi/build/task.ts +2 -1
- package/src/native/ffi/build.ts +5 -1
- package/src/native/ffi/selftest.ts +3 -3
- package/src/native/ffi/symbols.ts +460 -0
- package/src/native/ffi/types.ts +6 -1
- package/src/native/ffi.ts +2 -434
- package/src/native/ingress-binding.ts +39 -0
- package/src/native/types.ts +6 -0
- package/src/runtime/codec.ts +24 -0
- package/src/runtime/detect.ts +14 -0
- package/src/runtime/types.ts +11 -0
- package/src/rust-ffi/batch/build.ts +18 -8
- package/src/rust-ffi/batch/types.ts +15 -0
- package/src/rust-ffi/context.ts +46 -3
- package/src/rust-ffi/options.ts +19 -0
- package/src/rust-ffi/scalar/crypto.ts +8 -0
- package/src/rust-ffi/scalar/factories.ts +53 -20
- package/src/rust-ffi/scalar/http.ts +60 -10
- package/src/rust-ffi/scalar/interface.ts +18 -1
- package/src/rust-ffi/scalar/payload.ts +27 -1
- package/src/rust-ffi/text.ts +5 -4
- package/src/shared/buffer-pool.ts +54 -9
- package/src/shared/codec.ts +26 -0
- package/src/shared/memory.ts +68 -0
- package/src/shared/metrics.ts +86 -3
- package/src/shared/packed/parsers.ts +64 -7
- package/src/shared/runtime.ts +7 -0
- package/src/task/runtime.ts +81 -37
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// src/native/ffi/build/codecs-selftest.ts — bind-time self-test for the
|
|
2
|
+
// codec/crypto/auth BunFFI surface.
|
|
3
|
+
//
|
|
4
|
+
// Extracted from codecs.ts so the method builders and the known-answer probes
|
|
5
|
+
// stay separate modules; `selftest.ts` ANDs this with the other per-domain
|
|
6
|
+
// probes.
|
|
7
|
+
|
|
8
|
+
import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
|
|
9
|
+
import { SELFTEST_HEX, SELFTEST_JSON } from '../constants'
|
|
10
|
+
import type { BunFFI } from '../types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Bind-time self-test for the codec/crypto/auth surface (the methods built in
|
|
14
|
+
* `buildCodecs`). Exercises every bound function against known-good vectors;
|
|
15
|
+
* `false` disables the ffi layer and forces the napi fallback.
|
|
16
|
+
*/
|
|
17
|
+
export function selfTestCodecs(b: BunFFI): boolean {
|
|
18
|
+
const enc = { encode: encodeUtf8 }
|
|
19
|
+
const dec = { decode: decodeUtf8 }
|
|
20
|
+
|
|
21
|
+
// Checksums + validity probes.
|
|
22
|
+
if (b.crc32(enc.encode('123456789')) !== 0xcbf4_3926) return false
|
|
23
|
+
if (b.fnv1a64(enc.encode('foobar')) !== 0x8594_4171_f739_67e8n) return false
|
|
24
|
+
// XXH3-64 of empty input = 0x2d06800538d394c2 (standard reference vector).
|
|
25
|
+
if (b.xxh3(new Uint8Array(0)) !== 0x2d06800538d394c2n) return false
|
|
26
|
+
if (b.jsonValid(SELFTEST_JSON) !== true || b.jsonValid(enc.encode('{not json')) !== false) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
// UTF-8 validity probe (native replacement for a fatal TextDecoder on the
|
|
30
|
+
// Bun path — used by the urlDecode wrapper).
|
|
31
|
+
if (!b.utf8Valid(enc.encode('héllo')) || b.utf8Valid(new Uint8Array([0xff, 0xfe]))) {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
const hexOut = new Uint8Array(SELFTEST_HEX.length * 2)
|
|
35
|
+
if (b.hexEncodeInto(SELFTEST_HEX, hexOut) !== 10 || dec.decode(hexOut) !== '68656c6c6f') {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
const urlInput = enc.encode('a b/c')
|
|
39
|
+
const urlOut = new Uint8Array(9)
|
|
40
|
+
if (b.urlEncodeInto(urlInput, urlOut) !== 9 || dec.decode(urlOut) !== 'a%20b%2Fc') {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validators take `cstring` ARGs (the engine transcodes the JS string).
|
|
45
|
+
if (
|
|
46
|
+
!b.validateEmail('a@b.com') ||
|
|
47
|
+
!b.validateUuid('550e8400-e29b-41d4-a716-446655440000') ||
|
|
48
|
+
!b.validateIpv4('192.168.0.1') ||
|
|
49
|
+
!b.validateIpv6('2001:db8::1') ||
|
|
50
|
+
b.validateEmail('not-an-email') ||
|
|
51
|
+
b.validateUuid('not-a-uuid')
|
|
52
|
+
) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
// Byte-input validator siblings: `(ptr,len)` parity with the string forms.
|
|
56
|
+
if (
|
|
57
|
+
!b.validateEmailBytes(enc.encode('a@b.com')) ||
|
|
58
|
+
!b.validateUuidBytes(enc.encode('550e8400-e29b-41d4-a716-446655440000')) ||
|
|
59
|
+
!b.validateIpv4Bytes(enc.encode('192.168.0.1')) ||
|
|
60
|
+
!b.validateIpv6Bytes(enc.encode('2001:db8::1')) ||
|
|
61
|
+
b.validateEmailBytes(enc.encode('not-an-email')) ||
|
|
62
|
+
b.validateIpv6Bytes(enc.encode('999'))
|
|
63
|
+
) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
// Packed `[u8 ok][i64 sum LE]` ABI: legit zero-sum is ok, invalid input throws.
|
|
67
|
+
if (b.jsonSumIds(enc.encode(`[{"id":1},{"id":2}]`)) !== 3n) return false
|
|
68
|
+
if (b.jsonSumIds(enc.encode(`[{"id":0},{"id":0}]`)) !== 0n) return false
|
|
69
|
+
let sumInvalidThrew = false
|
|
70
|
+
try {
|
|
71
|
+
b.jsonSumIds(enc.encode('nope'))
|
|
72
|
+
} catch {
|
|
73
|
+
sumInvalidThrew = true
|
|
74
|
+
}
|
|
75
|
+
if (!sumInvalidThrew) return false
|
|
76
|
+
|
|
77
|
+
// Batch fixed-width hex validation (needed-size convention + verdicts).
|
|
78
|
+
{
|
|
79
|
+
const ids = enc.encode('507f1f77bcf86cd799439011\nzz\n507F1F77BCF86CD799439012')
|
|
80
|
+
const tiny = new Uint8Array(2)
|
|
81
|
+
const needed = b.hexValidateBatchInto(ids, 24, tiny)
|
|
82
|
+
if (needed !== 3) return false // exact required size on a too-small buffer
|
|
83
|
+
const out = new Uint8Array(needed)
|
|
84
|
+
if (b.hexValidateBatchInto(ids, 24, out) !== 3) return false
|
|
85
|
+
if (out[0] !== 1 || out[1] !== 0 || out[2] !== 1) return false
|
|
86
|
+
let widthThrew = false
|
|
87
|
+
try {
|
|
88
|
+
b.hexValidateBatchInto(ids, 0, new Uint8Array(3))
|
|
89
|
+
} catch {
|
|
90
|
+
widthThrew = true
|
|
91
|
+
}
|
|
92
|
+
if (!widthThrew) return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// RegExp escaping: metachars get backslashes; plain text is untouched.
|
|
96
|
+
{
|
|
97
|
+
const out = new Uint8Array(64)
|
|
98
|
+
const w = b.regexEscapeInto(enc.encode('a.c(x)'), out)
|
|
99
|
+
if (w !== 9 || dec.decode(out.subarray(0, w)) !== 'a\\.c\\(x\\)') return false
|
|
100
|
+
const plain = new Uint8Array(16)
|
|
101
|
+
const wp = b.regexEscapeInto(enc.encode('hello'), plain)
|
|
102
|
+
if (wp !== 5 || dec.decode(plain.subarray(0, wp)) !== 'hello') return false
|
|
103
|
+
// too-small buffer reports the exact required size
|
|
104
|
+
if (b.regexEscapeInto(enc.encode('a.b'), new Uint8Array(2)) !== 4) return false
|
|
105
|
+
// zero-copy str sibling must produce the identical escaped string
|
|
106
|
+
if (b.regexEscapeStr('a.c(x)') !== 'a\\.c\\(x\\)') return false
|
|
107
|
+
if (b.regexEscapeStr('plain text') !== 'plain text') return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// String-input batch hex validation (cstring ARG sibling).
|
|
111
|
+
{
|
|
112
|
+
const out = new Uint8Array(8)
|
|
113
|
+
const w = b.hexValidateBatchStr('507f1f77bcf86cd799439011\nzz', 24, out)
|
|
114
|
+
if (w !== 2 || out[0] !== 1 || out[1] !== 0) return false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// HMAC RFC 4231 test case 1 (0x0b × 20 key, "Hi There" data).
|
|
118
|
+
const hmacKey = new Uint8Array(20).fill(0x0b)
|
|
119
|
+
const hmacData = enc.encode('Hi There')
|
|
120
|
+
const hmacSig = enc.encode('b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7')
|
|
121
|
+
if (
|
|
122
|
+
b.hmacSha256(hmacKey, hmacData) !==
|
|
123
|
+
'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'
|
|
124
|
+
) {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
if (!b.hmacSha256Verify(hmacKey, hmacData, hmacSig)) {
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Decoders round-trip.
|
|
132
|
+
const decoded = b.hexDecode(enc.encode('68656c6c6f'))
|
|
133
|
+
if (
|
|
134
|
+
dec.decode(decoded) !== 'hello' ||
|
|
135
|
+
dec.decode(b.urlDecode(enc.encode('a%20b%2Fc'))) !== 'a b/c'
|
|
136
|
+
) {
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// WebSocket accept key (RFC 6455 sample) — `key` is a `cstring` ARG.
|
|
141
|
+
if (b.wsAcceptKey('dGhlIHNhbXBsZSBub25jZQ==') !== 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=') {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
// ETag: strong = 10 bytes, weak = 12 bytes.
|
|
145
|
+
if (b.etag(SELFTEST_HEX).length !== 10 || b.etag(SELFTEST_HEX, true).length !== 12) {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// base64.
|
|
150
|
+
if (b.base64Encode(SELFTEST_HEX) !== 'aGVsbG8=') {
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
if (dec.decode(b.base64Decode(enc.encode('aGVsbG8='))) !== 'hello') {
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Signed cookie round-trip.
|
|
158
|
+
const secret = enc.encode('s3cr3t-secret')
|
|
159
|
+
const signed = b.signCookie(SELFTEST_HEX, secret)
|
|
160
|
+
const verified = b.verifyCookie(enc.encode(signed), secret)
|
|
161
|
+
if (verified === null || verified !== 'hello') {
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
if (b.verifyCookie(enc.encode('tampered.0000'), secret) !== null) {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// CSRF token round-trip (issued token verifies against the same secret).
|
|
169
|
+
const csrfTokenStr = b.csrfToken(secret)
|
|
170
|
+
if (csrfTokenStr.length !== 129 || !b.csrfVerify(enc.encode(csrfTokenStr), secret)) {
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Argon2id round-trip at minimum cost (fast) — full defaults would take ~50ms.
|
|
175
|
+
const pw = enc.encode('correct horse battery staple')
|
|
176
|
+
const salt = enc.encode('salty-salt-16b')
|
|
177
|
+
const phc = b.passwordHash(pw, salt, 8, 1, 1, 16)
|
|
178
|
+
if (phc.length === 0 || !b.passwordVerify(pw, enc.encode(phc))) {
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
// bcrypt round-trip at minimum cost (fast) — `phc` is a `cstring` ARG.
|
|
182
|
+
const bcryptPhc = b.passwordHashBcrypt(pw, 4)
|
|
183
|
+
if (bcryptPhc.length === 0 || !b.passwordVerifyBcrypt(pw, bcryptPhc)) {
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
// PBKDF2-HMAC-SHA256: password="password", salt="salt", c=1, dkLen=32.
|
|
187
|
+
const dk = b.pbkdf2Sha256(enc.encode('password'), enc.encode('salt'), 1, 32)
|
|
188
|
+
if (b.hexEncode(dk) !== '120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b') {
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// AEAD AES-256-GCM round-trip (key 32B, nonce 12B).
|
|
193
|
+
const aeadKey = new Uint8Array(32).fill(0x42)
|
|
194
|
+
const nonce = new Uint8Array(12).fill(0x07)
|
|
195
|
+
const ct = b.aeadEncrypt(aeadKey, nonce, SELFTEST_HEX, 0)
|
|
196
|
+
const pt = b.aeadDecrypt(aeadKey, nonce, ct, 0)
|
|
197
|
+
if (pt === null || dec.decode(pt) !== 'hello') {
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Random token pooled Into: writes byteLen*2 hex chars into the buffer.
|
|
202
|
+
const rtOut = new Uint8Array(32)
|
|
203
|
+
const rtW = b.randomTokenInto(16, rtOut)
|
|
204
|
+
if (rtW !== 32 || !/^[0-9a-f]{32}$/.test(dec.decode(rtOut))) {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
// Too-small buffer → throw (needed-size convention).
|
|
208
|
+
try {
|
|
209
|
+
b.randomTokenInto(16, new Uint8Array(8))
|
|
210
|
+
return false
|
|
211
|
+
} catch {
|
|
212
|
+
// expected
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Fixed-size writers pooled Into: ws_accept_key + etag + csrf_token match the
|
|
216
|
+
// cstring path byte-for-byte.
|
|
217
|
+
const waOut = new Uint8Array(28)
|
|
218
|
+
const waW = b.wsAcceptKeyInto(enc.encode('dGhlIHNhbXBsZSBub25jZQ=='), waOut)
|
|
219
|
+
const waExpected = b.wsAcceptKey('dGhlIHNhbXBsZSBub25jZQ==')
|
|
220
|
+
if (waW !== 28 || dec.decode(waOut) !== waExpected) {
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
const etagData = enc.encode('hello')
|
|
224
|
+
const etagOut = new Uint8Array(16)
|
|
225
|
+
const etagW = b.etagInto(etagData, etagOut)
|
|
226
|
+
if (etagW !== 10 || dec.decode(etagOut.subarray(0, etagW)) !== b.etag(etagData)) {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
const csrfOut = new Uint8Array(129)
|
|
230
|
+
const csrfW = b.csrfTokenInto(enc.encode('csrf-secret'), csrfOut)
|
|
231
|
+
if (csrfW !== 129 || csrfOut[64] !== 46 /* '.' */) {
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// sign/verify cookie pooled Into: round-trip value bytes.
|
|
236
|
+
const ckVal = enc.encode('session-value')
|
|
237
|
+
const ckSec = enc.encode('s3cr3t-secret')
|
|
238
|
+
const ckOut = new Uint8Array(256)
|
|
239
|
+
const ckW = b.signCookieInto(ckVal, ckSec, ckOut)
|
|
240
|
+
if (ckW !== ckVal.length + 65) {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
const ckVerifyOut = new Uint8Array(256)
|
|
244
|
+
const ckV = b.verifyCookieInto(ckOut.subarray(0, ckW), ckSec, ckVerifyOut)
|
|
245
|
+
if (ckV !== ckVal.length || dec.decode(ckVerifyOut.subarray(0, ckV)) !== 'session-value') {
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// urlEncode string form.
|
|
250
|
+
if (b.urlEncode(enc.encode('a b/c')) !== 'a%20b%2Fc') {
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
// hexDecodeInto.
|
|
254
|
+
const hdInto = new Uint8Array(8)
|
|
255
|
+
if (
|
|
256
|
+
b.hexDecodeInto(enc.encode('68656c6c6f'), hdInto) !== 5 ||
|
|
257
|
+
dec.decode(hdInto.subarray(0, 5)) !== 'hello'
|
|
258
|
+
) {
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
// urlDecodeInto.
|
|
262
|
+
const udInto = new Uint8Array(16)
|
|
263
|
+
if (
|
|
264
|
+
b.urlDecodeInto(enc.encode('a%20b%2Fc'), udInto) !== 5 ||
|
|
265
|
+
dec.decode(udInto.subarray(0, 5)) !== 'a b/c'
|
|
266
|
+
) {
|
|
267
|
+
return false
|
|
268
|
+
}
|
|
269
|
+
// base64EncodeInto.
|
|
270
|
+
const b64Into = new Uint8Array(16)
|
|
271
|
+
if (
|
|
272
|
+
b.base64EncodeInto(SELFTEST_HEX, b64Into) !== 8 ||
|
|
273
|
+
dec.decode(b64Into.subarray(0, 8)) !== 'aGVsbG8='
|
|
274
|
+
) {
|
|
275
|
+
return false
|
|
276
|
+
}
|
|
277
|
+
// base64DecodeInto.
|
|
278
|
+
const bdInto = new Uint8Array(8)
|
|
279
|
+
if (
|
|
280
|
+
b.base64DecodeInto(enc.encode('aGVsbG8='), bdInto) !== 5 ||
|
|
281
|
+
dec.decode(bdInto.subarray(0, 5)) !== 'hello'
|
|
282
|
+
) {
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
// randomToken string form.
|
|
286
|
+
if (!/^[0-9a-f]{32}$/.test(b.randomToken(16))) {
|
|
287
|
+
return false
|
|
288
|
+
}
|
|
289
|
+
// hmacSha256Into (RFC 4231 test case 1 hex vector).
|
|
290
|
+
const hmacInto = new Uint8Array(64)
|
|
291
|
+
if (
|
|
292
|
+
b.hmacSha256Into(hmacKey, hmacData, hmacInto) !== 64 ||
|
|
293
|
+
dec.decode(hmacInto) !== 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'
|
|
294
|
+
) {
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
// aeadEncryptInto → decrypt round-trip.
|
|
298
|
+
const ctInto = new Uint8Array(64)
|
|
299
|
+
const ctW = b.aeadEncryptInto(aeadKey, nonce, SELFTEST_HEX, ctInto, 0)
|
|
300
|
+
if (ctW === 0 || b.aeadDecrypt(aeadKey, nonce, ctInto.subarray(0, ctW), 0) === null) {
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return true
|
|
305
|
+
}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
// the per-bind context (argument adapters + pooled scratch) from `build()`.
|
|
7
7
|
|
|
8
8
|
import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
|
|
9
|
-
import { SELFTEST_HEX, SELFTEST_JSON } from '../constants'
|
|
10
9
|
import type { BunFFI, Raw3, Raw4, Raw5, Raw6, Raw8, Raw9, Raw10, RawCStr } from '../types'
|
|
11
10
|
import type { BuildCtx } from './util'
|
|
12
11
|
import { allocOut, argon2PhcLength, cstr, flag, growExact, hasNul, writeOrThrow } from './util'
|
|
@@ -557,298 +556,3 @@ export function buildCodecs(
|
|
|
557
556
|
},
|
|
558
557
|
}
|
|
559
558
|
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Bind-time self-test for the codec/crypto/auth surface (the methods built in
|
|
563
|
-
* `buildCodecs`). Exercises every bound function against known-good vectors;
|
|
564
|
-
* `false` disables the ffi layer and forces the napi fallback.
|
|
565
|
-
*/
|
|
566
|
-
export function selfTestCodecs(b: BunFFI): boolean {
|
|
567
|
-
const enc = { encode: encodeUtf8 }
|
|
568
|
-
const dec = { decode: decodeUtf8 }
|
|
569
|
-
|
|
570
|
-
// Checksums + validity probes.
|
|
571
|
-
if (b.crc32(enc.encode('123456789')) !== 0xcbf4_3926) return false
|
|
572
|
-
if (b.fnv1a64(enc.encode('foobar')) !== 0x8594_4171_f739_67e8n) return false
|
|
573
|
-
// XXH3-64 of empty input = 0x2d06800538d394c2 (standard reference vector).
|
|
574
|
-
if (b.xxh3(new Uint8Array(0)) !== 0x2d06800538d394c2n) return false
|
|
575
|
-
if (b.jsonValid(SELFTEST_JSON) !== true || b.jsonValid(enc.encode('{not json')) !== false) {
|
|
576
|
-
return false
|
|
577
|
-
}
|
|
578
|
-
// UTF-8 validity probe (native replacement for a fatal TextDecoder on the
|
|
579
|
-
// Bun path — used by the urlDecode wrapper).
|
|
580
|
-
if (!b.utf8Valid(enc.encode('héllo')) || b.utf8Valid(new Uint8Array([0xff, 0xfe]))) {
|
|
581
|
-
return false
|
|
582
|
-
}
|
|
583
|
-
const hexOut = new Uint8Array(SELFTEST_HEX.length * 2)
|
|
584
|
-
if (b.hexEncodeInto(SELFTEST_HEX, hexOut) !== 10 || dec.decode(hexOut) !== '68656c6c6f') {
|
|
585
|
-
return false
|
|
586
|
-
}
|
|
587
|
-
const urlInput = enc.encode('a b/c')
|
|
588
|
-
const urlOut = new Uint8Array(9)
|
|
589
|
-
if (b.urlEncodeInto(urlInput, urlOut) !== 9 || dec.decode(urlOut) !== 'a%20b%2Fc') {
|
|
590
|
-
return false
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Validators take `cstring` ARGs (the engine transcodes the JS string).
|
|
594
|
-
if (
|
|
595
|
-
!b.validateEmail('a@b.com') ||
|
|
596
|
-
!b.validateUuid('550e8400-e29b-41d4-a716-446655440000') ||
|
|
597
|
-
!b.validateIpv4('192.168.0.1') ||
|
|
598
|
-
!b.validateIpv6('2001:db8::1') ||
|
|
599
|
-
b.validateEmail('not-an-email') ||
|
|
600
|
-
b.validateUuid('not-a-uuid')
|
|
601
|
-
) {
|
|
602
|
-
return false
|
|
603
|
-
}
|
|
604
|
-
// Byte-input validator siblings: `(ptr,len)` parity with the string forms.
|
|
605
|
-
if (
|
|
606
|
-
!b.validateEmailBytes(enc.encode('a@b.com')) ||
|
|
607
|
-
!b.validateUuidBytes(enc.encode('550e8400-e29b-41d4-a716-446655440000')) ||
|
|
608
|
-
!b.validateIpv4Bytes(enc.encode('192.168.0.1')) ||
|
|
609
|
-
!b.validateIpv6Bytes(enc.encode('2001:db8::1')) ||
|
|
610
|
-
b.validateEmailBytes(enc.encode('not-an-email')) ||
|
|
611
|
-
b.validateIpv6Bytes(enc.encode('999'))
|
|
612
|
-
) {
|
|
613
|
-
return false
|
|
614
|
-
}
|
|
615
|
-
// Packed `[u8 ok][i64 sum LE]` ABI: legit zero-sum is ok, invalid input throws.
|
|
616
|
-
if (b.jsonSumIds(enc.encode(`[{"id":1},{"id":2}]`)) !== 3n) return false
|
|
617
|
-
if (b.jsonSumIds(enc.encode(`[{"id":0},{"id":0}]`)) !== 0n) return false
|
|
618
|
-
let sumInvalidThrew = false
|
|
619
|
-
try {
|
|
620
|
-
b.jsonSumIds(enc.encode('nope'))
|
|
621
|
-
} catch {
|
|
622
|
-
sumInvalidThrew = true
|
|
623
|
-
}
|
|
624
|
-
if (!sumInvalidThrew) return false
|
|
625
|
-
|
|
626
|
-
// Batch fixed-width hex validation (needed-size convention + verdicts).
|
|
627
|
-
{
|
|
628
|
-
const ids = enc.encode('507f1f77bcf86cd799439011\nzz\n507F1F77BCF86CD799439012')
|
|
629
|
-
const tiny = new Uint8Array(2)
|
|
630
|
-
const needed = b.hexValidateBatchInto(ids, 24, tiny)
|
|
631
|
-
if (needed !== 3) return false // exact required size on a too-small buffer
|
|
632
|
-
const out = new Uint8Array(needed)
|
|
633
|
-
if (b.hexValidateBatchInto(ids, 24, out) !== 3) return false
|
|
634
|
-
if (out[0] !== 1 || out[1] !== 0 || out[2] !== 1) return false
|
|
635
|
-
let widthThrew = false
|
|
636
|
-
try {
|
|
637
|
-
b.hexValidateBatchInto(ids, 0, new Uint8Array(3))
|
|
638
|
-
} catch {
|
|
639
|
-
widthThrew = true
|
|
640
|
-
}
|
|
641
|
-
if (!widthThrew) return false
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// RegExp escaping: metachars get backslashes; plain text is untouched.
|
|
645
|
-
{
|
|
646
|
-
const out = new Uint8Array(64)
|
|
647
|
-
const w = b.regexEscapeInto(enc.encode('a.c(x)'), out)
|
|
648
|
-
if (w !== 9 || dec.decode(out.subarray(0, w)) !== 'a\\.c\\(x\\)') return false
|
|
649
|
-
const plain = new Uint8Array(16)
|
|
650
|
-
const wp = b.regexEscapeInto(enc.encode('hello'), plain)
|
|
651
|
-
if (wp !== 5 || dec.decode(plain.subarray(0, wp)) !== 'hello') return false
|
|
652
|
-
// too-small buffer reports the exact required size
|
|
653
|
-
if (b.regexEscapeInto(enc.encode('a.b'), new Uint8Array(2)) !== 4) return false
|
|
654
|
-
// zero-copy str sibling must produce the identical escaped string
|
|
655
|
-
if (b.regexEscapeStr('a.c(x)') !== 'a\\.c\\(x\\)') return false
|
|
656
|
-
if (b.regexEscapeStr('plain text') !== 'plain text') return false
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// String-input batch hex validation (cstring ARG sibling).
|
|
660
|
-
{
|
|
661
|
-
const out = new Uint8Array(8)
|
|
662
|
-
const w = b.hexValidateBatchStr('507f1f77bcf86cd799439011\nzz', 24, out)
|
|
663
|
-
if (w !== 2 || out[0] !== 1 || out[1] !== 0) return false
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// HMAC RFC 4231 test case 1 (0x0b × 20 key, "Hi There" data).
|
|
667
|
-
const hmacKey = new Uint8Array(20).fill(0x0b)
|
|
668
|
-
const hmacData = enc.encode('Hi There')
|
|
669
|
-
const hmacSig = enc.encode('b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7')
|
|
670
|
-
if (
|
|
671
|
-
b.hmacSha256(hmacKey, hmacData) !==
|
|
672
|
-
'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'
|
|
673
|
-
) {
|
|
674
|
-
return false
|
|
675
|
-
}
|
|
676
|
-
if (!b.hmacSha256Verify(hmacKey, hmacData, hmacSig)) {
|
|
677
|
-
return false
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Decoders round-trip.
|
|
681
|
-
const decoded = b.hexDecode(enc.encode('68656c6c6f'))
|
|
682
|
-
if (
|
|
683
|
-
dec.decode(decoded) !== 'hello' ||
|
|
684
|
-
dec.decode(b.urlDecode(enc.encode('a%20b%2Fc'))) !== 'a b/c'
|
|
685
|
-
) {
|
|
686
|
-
return false
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// WebSocket accept key (RFC 6455 sample) — `key` is a `cstring` ARG.
|
|
690
|
-
if (b.wsAcceptKey('dGhlIHNhbXBsZSBub25jZQ==') !== 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=') {
|
|
691
|
-
return false
|
|
692
|
-
}
|
|
693
|
-
// ETag: strong = 10 bytes, weak = 12 bytes.
|
|
694
|
-
if (b.etag(SELFTEST_HEX).length !== 10 || b.etag(SELFTEST_HEX, true).length !== 12) {
|
|
695
|
-
return false
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// base64.
|
|
699
|
-
if (b.base64Encode(SELFTEST_HEX) !== 'aGVsbG8=') {
|
|
700
|
-
return false
|
|
701
|
-
}
|
|
702
|
-
if (dec.decode(b.base64Decode(enc.encode('aGVsbG8='))) !== 'hello') {
|
|
703
|
-
return false
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// Signed cookie round-trip.
|
|
707
|
-
const secret = enc.encode('s3cr3t-secret')
|
|
708
|
-
const signed = b.signCookie(SELFTEST_HEX, secret)
|
|
709
|
-
const verified = b.verifyCookie(enc.encode(signed), secret)
|
|
710
|
-
if (verified === null || verified !== 'hello') {
|
|
711
|
-
return false
|
|
712
|
-
}
|
|
713
|
-
if (b.verifyCookie(enc.encode('tampered.0000'), secret) !== null) {
|
|
714
|
-
return false
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// CSRF token round-trip (issued token verifies against the same secret).
|
|
718
|
-
const csrfTokenStr = b.csrfToken(secret)
|
|
719
|
-
if (csrfTokenStr.length !== 129 || !b.csrfVerify(enc.encode(csrfTokenStr), secret)) {
|
|
720
|
-
return false
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Argon2id round-trip at minimum cost (fast) — full defaults would take ~50ms.
|
|
724
|
-
const pw = enc.encode('correct horse battery staple')
|
|
725
|
-
const salt = enc.encode('salty-salt-16b')
|
|
726
|
-
const phc = b.passwordHash(pw, salt, 8, 1, 1, 16)
|
|
727
|
-
if (phc.length === 0 || !b.passwordVerify(pw, enc.encode(phc))) {
|
|
728
|
-
return false
|
|
729
|
-
}
|
|
730
|
-
// bcrypt round-trip at minimum cost (fast) — `phc` is a `cstring` ARG.
|
|
731
|
-
const bcryptPhc = b.passwordHashBcrypt(pw, 4)
|
|
732
|
-
if (bcryptPhc.length === 0 || !b.passwordVerifyBcrypt(pw, bcryptPhc)) {
|
|
733
|
-
return false
|
|
734
|
-
}
|
|
735
|
-
// PBKDF2-HMAC-SHA256: password="password", salt="salt", c=1, dkLen=32.
|
|
736
|
-
const dk = b.pbkdf2Sha256(enc.encode('password'), enc.encode('salt'), 1, 32)
|
|
737
|
-
if (b.hexEncode(dk) !== '120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b') {
|
|
738
|
-
return false
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// AEAD AES-256-GCM round-trip (key 32B, nonce 12B).
|
|
742
|
-
const aeadKey = new Uint8Array(32).fill(0x42)
|
|
743
|
-
const nonce = new Uint8Array(12).fill(0x07)
|
|
744
|
-
const ct = b.aeadEncrypt(aeadKey, nonce, SELFTEST_HEX, 0)
|
|
745
|
-
const pt = b.aeadDecrypt(aeadKey, nonce, ct, 0)
|
|
746
|
-
if (pt === null || dec.decode(pt) !== 'hello') {
|
|
747
|
-
return false
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Random token pooled Into: writes byteLen*2 hex chars into the buffer.
|
|
751
|
-
const rtOut = new Uint8Array(32)
|
|
752
|
-
const rtW = b.randomTokenInto(16, rtOut)
|
|
753
|
-
if (rtW !== 32 || !/^[0-9a-f]{32}$/.test(dec.decode(rtOut))) {
|
|
754
|
-
return false
|
|
755
|
-
}
|
|
756
|
-
// Too-small buffer → throw (needed-size convention).
|
|
757
|
-
try {
|
|
758
|
-
b.randomTokenInto(16, new Uint8Array(8))
|
|
759
|
-
return false
|
|
760
|
-
} catch {
|
|
761
|
-
// expected
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// Fixed-size writers pooled Into: ws_accept_key + etag + csrf_token match the
|
|
765
|
-
// cstring path byte-for-byte.
|
|
766
|
-
const waOut = new Uint8Array(28)
|
|
767
|
-
const waW = b.wsAcceptKeyInto(enc.encode('dGhlIHNhbXBsZSBub25jZQ=='), waOut)
|
|
768
|
-
const waExpected = b.wsAcceptKey('dGhlIHNhbXBsZSBub25jZQ==')
|
|
769
|
-
if (waW !== 28 || dec.decode(waOut) !== waExpected) {
|
|
770
|
-
return false
|
|
771
|
-
}
|
|
772
|
-
const etagData = enc.encode('hello')
|
|
773
|
-
const etagOut = new Uint8Array(16)
|
|
774
|
-
const etagW = b.etagInto(etagData, etagOut)
|
|
775
|
-
if (etagW !== 10 || dec.decode(etagOut.subarray(0, etagW)) !== b.etag(etagData)) {
|
|
776
|
-
return false
|
|
777
|
-
}
|
|
778
|
-
const csrfOut = new Uint8Array(129)
|
|
779
|
-
const csrfW = b.csrfTokenInto(enc.encode('csrf-secret'), csrfOut)
|
|
780
|
-
if (csrfW !== 129 || csrfOut[64] !== 46 /* '.' */) {
|
|
781
|
-
return false
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// sign/verify cookie pooled Into: round-trip value bytes.
|
|
785
|
-
const ckVal = enc.encode('session-value')
|
|
786
|
-
const ckSec = enc.encode('s3cr3t-secret')
|
|
787
|
-
const ckOut = new Uint8Array(256)
|
|
788
|
-
const ckW = b.signCookieInto(ckVal, ckSec, ckOut)
|
|
789
|
-
if (ckW !== ckVal.length + 65) {
|
|
790
|
-
return false
|
|
791
|
-
}
|
|
792
|
-
const ckVerifyOut = new Uint8Array(256)
|
|
793
|
-
const ckV = b.verifyCookieInto(ckOut.subarray(0, ckW), ckSec, ckVerifyOut)
|
|
794
|
-
if (ckV !== ckVal.length || dec.decode(ckVerifyOut.subarray(0, ckV)) !== 'session-value') {
|
|
795
|
-
return false
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// urlEncode string form.
|
|
799
|
-
if (b.urlEncode(enc.encode('a b/c')) !== 'a%20b%2Fc') {
|
|
800
|
-
return false
|
|
801
|
-
}
|
|
802
|
-
// hexDecodeInto.
|
|
803
|
-
const hdInto = new Uint8Array(8)
|
|
804
|
-
if (
|
|
805
|
-
b.hexDecodeInto(enc.encode('68656c6c6f'), hdInto) !== 5 ||
|
|
806
|
-
dec.decode(hdInto.subarray(0, 5)) !== 'hello'
|
|
807
|
-
) {
|
|
808
|
-
return false
|
|
809
|
-
}
|
|
810
|
-
// urlDecodeInto.
|
|
811
|
-
const udInto = new Uint8Array(16)
|
|
812
|
-
if (
|
|
813
|
-
b.urlDecodeInto(enc.encode('a%20b%2Fc'), udInto) !== 5 ||
|
|
814
|
-
dec.decode(udInto.subarray(0, 5)) !== 'a b/c'
|
|
815
|
-
) {
|
|
816
|
-
return false
|
|
817
|
-
}
|
|
818
|
-
// base64EncodeInto.
|
|
819
|
-
const b64Into = new Uint8Array(16)
|
|
820
|
-
if (
|
|
821
|
-
b.base64EncodeInto(SELFTEST_HEX, b64Into) !== 8 ||
|
|
822
|
-
dec.decode(b64Into.subarray(0, 8)) !== 'aGVsbG8='
|
|
823
|
-
) {
|
|
824
|
-
return false
|
|
825
|
-
}
|
|
826
|
-
// base64DecodeInto.
|
|
827
|
-
const bdInto = new Uint8Array(8)
|
|
828
|
-
if (
|
|
829
|
-
b.base64DecodeInto(enc.encode('aGVsbG8='), bdInto) !== 5 ||
|
|
830
|
-
dec.decode(bdInto.subarray(0, 5)) !== 'hello'
|
|
831
|
-
) {
|
|
832
|
-
return false
|
|
833
|
-
}
|
|
834
|
-
// randomToken string form.
|
|
835
|
-
if (!/^[0-9a-f]{32}$/.test(b.randomToken(16))) {
|
|
836
|
-
return false
|
|
837
|
-
}
|
|
838
|
-
// hmacSha256Into (RFC 4231 test case 1 hex vector).
|
|
839
|
-
const hmacInto = new Uint8Array(64)
|
|
840
|
-
if (
|
|
841
|
-
b.hmacSha256Into(hmacKey, hmacData, hmacInto) !== 64 ||
|
|
842
|
-
dec.decode(hmacInto) !== 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'
|
|
843
|
-
) {
|
|
844
|
-
return false
|
|
845
|
-
}
|
|
846
|
-
// aeadEncryptInto → decrypt round-trip.
|
|
847
|
-
const ctInto = new Uint8Array(64)
|
|
848
|
-
const ctW = b.aeadEncryptInto(aeadKey, nonce, SELFTEST_HEX, ctInto, 0)
|
|
849
|
-
if (ctW === 0 || b.aeadDecrypt(aeadKey, nonce, ctInto.subarray(0, ctW), 0) === null) {
|
|
850
|
-
return false
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
return true
|
|
854
|
-
}
|