castrum 0.9.0 → 0.9.2
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/README.md +153 -630
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1653 -1817
- package/dist/index.js +6172 -4294
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +111 -17
- package/docs/BENCHMARKS.md +117 -49
- package/docs/CASE_STUDY.md +243 -0
- package/docs/ENVIRONMENT.md +24 -7
- package/docs/FFI_BUN_GUIDE.md +335 -0
- package/docs/GETTING_STARTED.md +18 -34
- package/docs/HOW_TO_ADD_AN_OP.md +180 -0
- package/docs/INGRESS-ROUTER.md +82 -0
- package/docs/INGRESS.md +11 -7
- package/docs/NATIVE-ROUTE.md +77 -0
- package/docs/REPO_MAP.md +103 -53
- package/docs/adr/0001-two-wire-formats.md +1 -1
- package/docs/adr/0003-bun-builtins-delegation.md +6 -5
- package/docs/ai/LOCAL_DEV.md +81 -0
- package/docs/ai/TREE.md +1070 -0
- package/docs/bun-builtins-decision-matrix.md +25 -18
- package/index.ts +9 -16
- package/package.json +41 -28
- package/src/ingress/body.ts +167 -27
- package/src/ingress/decode/result-base.ts +10 -2
- package/src/ingress/handlers.ts +116 -285
- package/src/ingress/headers/security.ts +41 -0
- package/src/ingress/health.ts +1 -0
- package/src/ingress/index.ts +45 -169
- package/src/ingress/native-route.ts +182 -0
- package/src/ingress/packing/gather-raw-headers.ts +110 -22
- package/src/ingress/packing/header-packing.ts +5 -5
- package/src/ingress/packing/input-packer.ts +17 -3
- package/src/ingress/packing/route-wire.ts +197 -0
- package/src/ingress/packing/select-headers.ts +20 -7
- package/src/ingress/path-matcher.ts +115 -0
- package/src/ingress/response/baked-response.ts +302 -0
- package/src/ingress/router.ts +277 -0
- package/src/ingress/routes/common.ts +50 -1
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +3 -13
- package/src/ingress/routes/index.ts +5 -4
- package/src/ingress/routes/json-write.ts +26 -47
- package/src/ingress/routes/native.ts +130 -0
- package/src/ingress/routes/options.ts +2 -12
- package/src/ingress/routes/read.ts +3 -13
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +7 -7
- package/src/ingress/server.ts +83 -112
- package/src/ingress/sync.ts +151 -0
- package/src/ingress/types.ts +44 -1
- package/src/integration/pipeline.ts +3 -0
- package/src/integration/streaming.ts +2 -3
- package/src/integration/websocket.ts +4 -3
- package/src/loader/batch.ts +4 -0
- package/src/loader/cost.ts +1 -0
- package/src/loader/index.ts +43 -228
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/build/codecs.ts +736 -0
- package/src/native/ffi/build/compress.ts +248 -0
- package/src/native/ffi/build/instances.ts +598 -0
- package/src/native/ffi/build/parse.ts +539 -0
- package/src/native/ffi/build/util.ts +140 -0
- package/src/native/ffi/build.ts +94 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +22 -0
- package/src/native/ffi/types.ts +499 -0
- package/src/native/ffi.ts +307 -1357
- package/src/native/loader.ts +52 -6
- package/src/native/types/instances.ts +279 -0
- package/src/native/types.ts +41 -238
- package/src/runtime/builtins.ts +131 -0
- package/src/runtime/codec.ts +147 -0
- package/src/runtime/detect.ts +36 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/native.ts +35 -0
- package/src/runtime/server.ts +31 -0
- package/src/runtime/transport.ts +64 -0
- package/src/runtime/types.ts +173 -0
- package/src/runtime/uuid.ts +45 -0
- package/src/runtime/websocket.ts +30 -0
- package/src/rust-ffi/addon.ts +1 -1
- package/src/rust-ffi/batch/build.ts +155 -0
- package/src/rust-ffi/batch/index.ts +8 -0
- package/src/rust-ffi/batch/types.ts +92 -0
- package/src/rust-ffi/client.ts +5 -6
- package/src/rust-ffi/context.ts +39 -10
- package/src/rust-ffi/index.ts +28 -17
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +1 -1
- package/src/rust-ffi/packed.ts +40 -2
- package/src/rust-ffi/proven.ts +16 -30
- package/src/rust-ffi/scalar/crypto.ts +188 -107
- package/src/rust-ffi/scalar/factories.ts +257 -27
- package/src/rust-ffi/scalar/hashing.ts +43 -41
- package/src/rust-ffi/scalar/http.ts +208 -77
- package/src/rust-ffi/scalar/index.ts +3 -3
- package/src/rust-ffi/scalar/interface.ts +88 -213
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +28 -12
- package/src/rust-ffi/scalar/payload.ts +159 -42
- package/src/rust-ffi/text.ts +31 -5
- package/src/selection.ts +27 -31
- package/src/shared/adaptive.ts +1 -2
- package/src/shared/buffer-pool.ts +72 -17
- package/src/shared/bytes.ts +75 -6
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +4 -4
- package/src/shared/log.ts +1 -0
- package/src/shared/packed/index.ts +10 -0
- package/src/shared/packed/parsers.ts +31 -0
- package/src/shared/packed/schema.ts +23 -0
- package/src/shared/{packed.ts → packed/wire.ts} +31 -39
- package/src/shared/proven.ts +189 -496
- package/src/shared/runtime.ts +20 -25
- package/src/shared/uuid.ts +12 -28
- package/src/data/json-rows.ts +0 -33
- package/src/rust-ffi/batch.ts +0 -431
- package/src/shared/bench-classify.ts +0 -209
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
// src/native/ffi/build/codecs.ts — hashing / codec / crypto / auth BunFFI methods.
|
|
2
|
+
//
|
|
3
|
+
// Checksums, JSON/UTF-8 validity, hex + percent codecs, base64, HMAC, cookie
|
|
4
|
+
// signing, CSRF, random tokens, password hashing, PBKDF2, AEAD, and the
|
|
5
|
+
// cstring-based ws accept key + ETag. Receives the raw dlopen'd symbols and
|
|
6
|
+
// the per-bind context (argument adapters + pooled scratch) from `build()`.
|
|
7
|
+
|
|
8
|
+
import { decodeUtf8, encodeUtf8 } from '../../../shared/codec'
|
|
9
|
+
import { SELFTEST_HEX, SELFTEST_JSON } from '../constants'
|
|
10
|
+
import type { BunFFI, Raw3, Raw4, Raw5, Raw6, Raw8, Raw9, Raw10, RawCStr } from '../types'
|
|
11
|
+
import type { BuildCtx } from './util'
|
|
12
|
+
import { argon2PhcLength, cstr, flag, growExact, writeOrThrow } from './util'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build the codec/crypto/auth methods of the BunFFI surface. `ctx` is
|
|
16
|
+
* destructured so the method bodies read exactly as the original `build()`.
|
|
17
|
+
*/
|
|
18
|
+
export function buildCodecs(
|
|
19
|
+
sym: Record<string, (...a: unknown[]) => unknown>,
|
|
20
|
+
ctx: BuildCtx,
|
|
21
|
+
): Partial<BunFFI> {
|
|
22
|
+
const { lenOrView, oneArg, scratchFor, jsonSumOut, jsonSumView } = ctx
|
|
23
|
+
|
|
24
|
+
const crc32 = sym.castrum_crc32 as (...a: unknown[]) => number | bigint
|
|
25
|
+
const fnv = sym.castrum_fnv1a64 as (...a: unknown[]) => number | bigint
|
|
26
|
+
const xxh = sym.castrum_xxh3 as (...a: unknown[]) => number | bigint
|
|
27
|
+
const jsonValid = sym.castrum_json_valid as (...a: unknown[]) => number | bigint
|
|
28
|
+
const utf8Valid = sym.castrum_utf8_valid as (...a: unknown[]) => number | bigint
|
|
29
|
+
const hexEncode = sym.castrum_hex_encode as Raw4
|
|
30
|
+
const hexDecode = sym.castrum_hex_decode as Raw4
|
|
31
|
+
const urlEncode = sym.castrum_url_encode as Raw4
|
|
32
|
+
const urlDecode = sym.castrum_url_decode as Raw4
|
|
33
|
+
const validateEmail = sym.castrum_validate_email as (...a: unknown[]) => number | bigint
|
|
34
|
+
const validateUuid = sym.castrum_validate_uuid as (...a: unknown[]) => number | bigint
|
|
35
|
+
const validateIpv4 = sym.castrum_validate_ipv4 as (...a: unknown[]) => number | bigint
|
|
36
|
+
const validateIpv6 = sym.castrum_validate_ipv6 as (...a: unknown[]) => number | bigint
|
|
37
|
+
const jsonSumRaw = sym.castrum_json_sum_ids as Raw4
|
|
38
|
+
const hmacVerify = sym.castrum_hmac_sha256_verify as Raw6
|
|
39
|
+
const csrfVerify = sym.castrum_csrf_verify as Raw4
|
|
40
|
+
const passwordVerify = sym.castrum_password_verify as Raw4
|
|
41
|
+
const passwordVerifyBcrypt = sym.castrum_password_verify_bcrypt as Raw3
|
|
42
|
+
const wsAcceptKey = sym.castrum_ws_accept_key as RawCStr
|
|
43
|
+
const wsAcceptKeyInto = sym.castrum_ws_accept_key_into as Raw4
|
|
44
|
+
const etagCStr = sym.castrum_etag as RawCStr
|
|
45
|
+
const etagIntoRaw = sym.castrum_etag_into as Raw5
|
|
46
|
+
const randomToken = sym.castrum_random_token as RawCStr
|
|
47
|
+
const randomTokenInto = sym.castrum_random_token_into as Raw3
|
|
48
|
+
const base64Encode = sym.castrum_base64_encode as Raw6
|
|
49
|
+
const base64Decode = sym.castrum_base64_decode as Raw6
|
|
50
|
+
const hmacSha256 = sym.castrum_hmac_sha256 as Raw6
|
|
51
|
+
const signCookie = sym.castrum_sign_cookie as RawCStr
|
|
52
|
+
const signCookieInto = sym.castrum_sign_cookie_into as Raw6
|
|
53
|
+
const verifyCookie = sym.castrum_verify_cookie as RawCStr
|
|
54
|
+
const verifyCookieInto = sym.castrum_verify_cookie_into as Raw6
|
|
55
|
+
const csrfToken = sym.castrum_csrf_token as RawCStr
|
|
56
|
+
const csrfTokenInto = sym.castrum_csrf_token_into as Raw4
|
|
57
|
+
const passwordHash = sym.castrum_password_hash as Raw10
|
|
58
|
+
const passwordHashBcrypt = sym.castrum_password_hash_bcrypt as Raw5
|
|
59
|
+
const pbkdf2 = sym.castrum_pbkdf2_sha256 as Raw8
|
|
60
|
+
const aeadEncrypt = sym.castrum_aead_encrypt as Raw9
|
|
61
|
+
const aeadDecrypt = sym.castrum_aead_decrypt as Raw9
|
|
62
|
+
|
|
63
|
+
// ── Encoder / decoder `Into` helpers ──────────────────────────────
|
|
64
|
+
const hexEncodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
65
|
+
// Mirror napi error semantics: a too-small buffer throws, not returns 0.
|
|
66
|
+
// Hex encode always writes exactly `input.length * 2` bytes on success.
|
|
67
|
+
if (output.length < input.length * 2) {
|
|
68
|
+
throw new Error('hex encode: output buffer too small')
|
|
69
|
+
}
|
|
70
|
+
return Number(hexEncode(input, lenOrView(input), output, lenOrView(output)))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const urlEncodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
74
|
+
const w = Number(urlEncode(input, lenOrView(input), output, lenOrView(output)))
|
|
75
|
+
// Every input byte encodes to >= 1 output byte, so a 0 write on non-empty
|
|
76
|
+
// input means the buffer was too small (napi throws there too). Empty
|
|
77
|
+
// input legitimately writes 0.
|
|
78
|
+
if (w === 0 && input.length !== 0) {
|
|
79
|
+
throw new Error('url encode: output buffer too small')
|
|
80
|
+
}
|
|
81
|
+
return w
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const hexDecodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
85
|
+
const w = Number(hexDecode(input, lenOrView(input), output, lenOrView(output)))
|
|
86
|
+
return writeOrThrow(w, input.length, 'hex decode')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const urlDecodeInto = (input: Uint8Array, output: Uint8Array): number => {
|
|
90
|
+
const w = Number(urlDecode(input, lenOrView(input), output, lenOrView(output)))
|
|
91
|
+
return writeOrThrow(w, input.length, 'url decode')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const base64DecodeInto = (
|
|
95
|
+
input: Uint8Array,
|
|
96
|
+
output: Uint8Array,
|
|
97
|
+
urlSafe?: boolean,
|
|
98
|
+
padding?: boolean,
|
|
99
|
+
): number => {
|
|
100
|
+
// napi defaults: urlSafe=false, padding=true (rust/crypto/base64.rs).
|
|
101
|
+
const w = Number(
|
|
102
|
+
base64Decode(
|
|
103
|
+
input,
|
|
104
|
+
lenOrView(input),
|
|
105
|
+
output,
|
|
106
|
+
lenOrView(output),
|
|
107
|
+
flag(urlSafe),
|
|
108
|
+
flag(padding ?? true),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
return writeOrThrow(w, input.length, 'base64 decode')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const base64EncodeInto = (
|
|
115
|
+
input: Uint8Array,
|
|
116
|
+
output: Uint8Array,
|
|
117
|
+
urlSafe?: boolean,
|
|
118
|
+
padding?: boolean,
|
|
119
|
+
): number => {
|
|
120
|
+
// napi defaults: urlSafe=false, padding=true.
|
|
121
|
+
const w = Number(
|
|
122
|
+
base64Encode(
|
|
123
|
+
input,
|
|
124
|
+
lenOrView(input),
|
|
125
|
+
output,
|
|
126
|
+
lenOrView(output),
|
|
127
|
+
flag(urlSafe),
|
|
128
|
+
flag(padding ?? true),
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
// Empty input legitimately writes 0 bytes — only a 0 write on NON-empty
|
|
132
|
+
// input is a real error (same convention as the decode/etag paths).
|
|
133
|
+
return writeOrThrow(w, input.length, 'base64 encode')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const etagInto = (data: Uint8Array, output: Uint8Array, weak?: boolean): number => {
|
|
137
|
+
// Native pooled `_into`: writes 10/12 bytes directly into the caller
|
|
138
|
+
// buffer (no cstring round-trip). Needed-size convention: a write larger
|
|
139
|
+
// than `output.length` reports the exact required size → throw.
|
|
140
|
+
const w = Number(etagIntoRaw(data, lenOrView(data), flag(weak), output, lenOrView(output)))
|
|
141
|
+
if (w === 0) {
|
|
142
|
+
throw new Error('etag: invalid input')
|
|
143
|
+
}
|
|
144
|
+
if (w > output.length) {
|
|
145
|
+
throw new Error('etag: output buffer too small')
|
|
146
|
+
}
|
|
147
|
+
return w
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
crc32: (input) => Number(oneArg(crc32, input)) >>> 0,
|
|
152
|
+
fnv1a64: (input) => BigInt(oneArg(fnv, input)),
|
|
153
|
+
xxh3: (input) => BigInt(oneArg(xxh, input)),
|
|
154
|
+
jsonValid: (input) => Number(oneArg(jsonValid, input)) === 1,
|
|
155
|
+
utf8Valid: (input) => Number(oneArg(utf8Valid, input)) === 1,
|
|
156
|
+
hexEncode(input) {
|
|
157
|
+
// Pooled scratch — decoded synchronously to an immutable string (safe;
|
|
158
|
+
// removes the per-call `new Uint8Array(len*2)`). ALWAYS decode the
|
|
159
|
+
// written subarray: the shared scratch may be larger than `w` (grown by
|
|
160
|
+
// an earlier op), so decoding the whole buffer would read stale bytes.
|
|
161
|
+
const out = scratchFor(input.length * 2)
|
|
162
|
+
const w = hexEncodeInto(input, out)
|
|
163
|
+
return decodeUtf8(out.subarray(0, w))
|
|
164
|
+
},
|
|
165
|
+
hexEncodeInto,
|
|
166
|
+
urlEncode(input) {
|
|
167
|
+
// RFC 3986 worst case is 3 bytes per input byte (`%XX`). Pooled scratch
|
|
168
|
+
// (decoded synchronously — safe).
|
|
169
|
+
const out = scratchFor(input.length * 3)
|
|
170
|
+
const w = urlEncodeInto(input, out)
|
|
171
|
+
return decodeUtf8(out.subarray(0, w))
|
|
172
|
+
},
|
|
173
|
+
urlEncodeInto,
|
|
174
|
+
|
|
175
|
+
validateEmail: (input) => Number(validateEmail(input)) === 1,
|
|
176
|
+
validateUuid: (input) => Number(validateUuid(input)) === 1,
|
|
177
|
+
validateIpv4: (input) => Number(validateIpv4(input)) === 1,
|
|
178
|
+
validateIpv6: (input) => Number(validateIpv6(input)) === 1,
|
|
179
|
+
jsonSumIds: (input) => {
|
|
180
|
+
// Packed [u8 ok][i64 sum LE] output (9 B): ok=1 → valid array (the sum
|
|
181
|
+
// may be 0); ok=0 → invalid input. Bytes written: 9/1/0 (0 = real error).
|
|
182
|
+
const w = Number(jsonSumRaw(input, lenOrView(input), jsonSumOut, lenOrView(jsonSumOut)))
|
|
183
|
+
if (w === 0) {
|
|
184
|
+
throw new Error('json sum ids: output buffer too small')
|
|
185
|
+
}
|
|
186
|
+
if (jsonSumOut[0] === 0) {
|
|
187
|
+
// Mirrors the napi error phrasing (serde: "expected an array of objects
|
|
188
|
+
// with numeric ids") so both transports throw the same message.
|
|
189
|
+
throw new Error('json sum ids: expected an array of objects with numeric ids')
|
|
190
|
+
}
|
|
191
|
+
return jsonSumView.getBigInt64(1, true)
|
|
192
|
+
},
|
|
193
|
+
hmacSha256Verify: (key, data, signature) =>
|
|
194
|
+
Number(
|
|
195
|
+
hmacVerify(key, lenOrView(key), data, lenOrView(data), signature, lenOrView(signature)),
|
|
196
|
+
) === 1,
|
|
197
|
+
csrfVerify: (token, secret) =>
|
|
198
|
+
Number(csrfVerify(token, lenOrView(token), secret, lenOrView(secret))) === 1,
|
|
199
|
+
passwordVerify: (password, phc) =>
|
|
200
|
+
Number(passwordVerify(password, lenOrView(password), phc, lenOrView(phc))) === 1,
|
|
201
|
+
passwordVerifyBcrypt: (password, phc) =>
|
|
202
|
+
Number(passwordVerifyBcrypt(password, lenOrView(password), phc)) === 1,
|
|
203
|
+
|
|
204
|
+
hexDecode(input) {
|
|
205
|
+
const out = new Uint8Array(Math.floor(input.length / 2))
|
|
206
|
+
const w = hexDecodeInto(input, out)
|
|
207
|
+
return out.subarray(0, w)
|
|
208
|
+
},
|
|
209
|
+
hexDecodeInto,
|
|
210
|
+
urlDecode(input) {
|
|
211
|
+
const out = new Uint8Array(input.length)
|
|
212
|
+
const w = urlDecodeInto(input, out)
|
|
213
|
+
return out.subarray(0, w)
|
|
214
|
+
},
|
|
215
|
+
urlDecodeInto,
|
|
216
|
+
base64Decode(input, urlSafe, padding) {
|
|
217
|
+
const out = new Uint8Array(Math.ceil((input.length * 3) / 4))
|
|
218
|
+
const w = base64DecodeInto(input, out, urlSafe, padding)
|
|
219
|
+
return out.subarray(0, w)
|
|
220
|
+
},
|
|
221
|
+
base64DecodeInto,
|
|
222
|
+
|
|
223
|
+
wsAcceptKey(key) {
|
|
224
|
+
return cstr(wsAcceptKey(key), 'ws accept key: bad key')
|
|
225
|
+
},
|
|
226
|
+
wsAcceptKeyInto(key, output) {
|
|
227
|
+
// Native pooled `_into`: writes the 28-byte accept key directly into the
|
|
228
|
+
// caller buffer (no cstring round-trip). Needed-size convention.
|
|
229
|
+
const w = Number(wsAcceptKeyInto(key, lenOrView(key), output, lenOrView(output)))
|
|
230
|
+
if (w === 0) {
|
|
231
|
+
throw new Error('ws accept key: bad key')
|
|
232
|
+
}
|
|
233
|
+
if (w > output.length) {
|
|
234
|
+
throw new Error('ws accept key: output buffer too small')
|
|
235
|
+
}
|
|
236
|
+
return w
|
|
237
|
+
},
|
|
238
|
+
etag(data, weak) {
|
|
239
|
+
// cstring return (10 strong / 12 weak chars) — zero encode.
|
|
240
|
+
return cstr(etagCStr(data, lenOrView(data), flag(weak)), 'etag: invalid input')
|
|
241
|
+
},
|
|
242
|
+
etagInto,
|
|
243
|
+
randomToken(byteLen) {
|
|
244
|
+
// cstring return of `byteLen*2` hex chars; byteLen 0 → empty string → empty
|
|
245
|
+
// Uint8Array (napi returns empty too). null = random source failed / >16MiB.
|
|
246
|
+
return cstr(
|
|
247
|
+
randomToken(byteLen),
|
|
248
|
+
'random token: output buffer too small or random source failed',
|
|
249
|
+
)
|
|
250
|
+
},
|
|
251
|
+
randomTokenInto(byteLen, output) {
|
|
252
|
+
// Pooled sibling: native writes `byteLen*2` hex chars directly into the
|
|
253
|
+
// caller buffer (no cstring round-trip). Needed-size convention: a write
|
|
254
|
+
// larger than `output.length` reports the exact required size → throw
|
|
255
|
+
// (the caller owns the buffer); 0 = real error (cap / RNG).
|
|
256
|
+
const w = Number(randomTokenInto(byteLen, output, lenOrView(output)))
|
|
257
|
+
if (w === 0) {
|
|
258
|
+
throw new Error('random token: random source failed or byteLen exceeds 16 MiB')
|
|
259
|
+
}
|
|
260
|
+
if (w > output.length) {
|
|
261
|
+
throw new Error('random token: output buffer too small')
|
|
262
|
+
}
|
|
263
|
+
return w
|
|
264
|
+
},
|
|
265
|
+
base64Encode(input, urlSafe, padding) {
|
|
266
|
+
// Pooled scratch — decoded synchronously to an immutable string (safe).
|
|
267
|
+
const out = scratchFor(Math.ceil(input.length / 3) * 4)
|
|
268
|
+
const w = base64EncodeInto(input, out, urlSafe, padding)
|
|
269
|
+
return decodeUtf8(out.subarray(0, w))
|
|
270
|
+
},
|
|
271
|
+
base64EncodeInto,
|
|
272
|
+
hmacSha256(key, data) {
|
|
273
|
+
// Pooled 64-byte scratch — decoded synchronously (safe).
|
|
274
|
+
const out = scratchFor(64)
|
|
275
|
+
const w = Number(hmacSha256(key, lenOrView(key), data, lenOrView(data), out, lenOrView(out)))
|
|
276
|
+
if (w === 0) {
|
|
277
|
+
throw new Error('hmac sha256: output buffer too small')
|
|
278
|
+
}
|
|
279
|
+
return decodeUtf8(out.subarray(0, w))
|
|
280
|
+
},
|
|
281
|
+
hmacSha256Into(key, data, output) {
|
|
282
|
+
if (output.length < 64) {
|
|
283
|
+
throw new Error('hmac sha256: output buffer too small')
|
|
284
|
+
}
|
|
285
|
+
const w = Number(
|
|
286
|
+
hmacSha256(key, lenOrView(key), data, lenOrView(data), output, lenOrView(output)),
|
|
287
|
+
)
|
|
288
|
+
if (w === 0) {
|
|
289
|
+
throw new Error('hmac sha256: output buffer too small')
|
|
290
|
+
}
|
|
291
|
+
return w
|
|
292
|
+
},
|
|
293
|
+
signCookie(value, secret) {
|
|
294
|
+
// `value.<64-hex>` returned as a cstring (value.length + 1 + 64 chars).
|
|
295
|
+
return cstr(
|
|
296
|
+
signCookie(value, lenOrView(value), secret, lenOrView(secret)),
|
|
297
|
+
'sign cookie: invalid input',
|
|
298
|
+
)
|
|
299
|
+
},
|
|
300
|
+
signCookieInto(value, secret, output) {
|
|
301
|
+
// Native pooled `_into`: writes `value.<64-hex>` directly into the caller
|
|
302
|
+
// buffer (no cstring round-trip — this is why pooled sign_cookie was
|
|
303
|
+
// previously a REGRESSION vs allocating). Needed-size convention.
|
|
304
|
+
const w = Number(
|
|
305
|
+
signCookieInto(
|
|
306
|
+
value,
|
|
307
|
+
lenOrView(value),
|
|
308
|
+
secret,
|
|
309
|
+
lenOrView(secret),
|
|
310
|
+
output,
|
|
311
|
+
lenOrView(output),
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
if (w === 0) {
|
|
315
|
+
throw new Error('sign cookie: invalid input')
|
|
316
|
+
}
|
|
317
|
+
if (w > output.length) {
|
|
318
|
+
throw new Error('sign cookie: output buffer too small')
|
|
319
|
+
}
|
|
320
|
+
return w
|
|
321
|
+
},
|
|
322
|
+
verifyCookie(signed, secret) {
|
|
323
|
+
// cstring return; `null` = invalid signature / malformed → null (napi parity).
|
|
324
|
+
// CAVEAT (refactor): the engine clones a UTF-8 string, so a cookie VALUE
|
|
325
|
+
// containing non-UTF-8 bytes or NUL cannot round-trip byte-faithfully on
|
|
326
|
+
// the FFI path (napi still does). Signed values are ASCII in practice.
|
|
327
|
+
const s = verifyCookie(signed, lenOrView(signed), secret, lenOrView(secret))
|
|
328
|
+
return s === null ? null : s
|
|
329
|
+
},
|
|
330
|
+
verifyCookieInto(signed, secret, output) {
|
|
331
|
+
// Native pooled `_into`: writes the verified value directly into the
|
|
332
|
+
// caller buffer. 0 = invalid signature / malformed → null (napi parity,
|
|
333
|
+
// like the allocating `verifyCookie`); a write larger than `output.length`
|
|
334
|
+
// reports the exact required size → throw.
|
|
335
|
+
const w = Number(
|
|
336
|
+
verifyCookieInto(
|
|
337
|
+
signed,
|
|
338
|
+
lenOrView(signed),
|
|
339
|
+
secret,
|
|
340
|
+
lenOrView(secret),
|
|
341
|
+
output,
|
|
342
|
+
lenOrView(output),
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
if (w === 0) {
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
348
|
+
if (w > output.length) {
|
|
349
|
+
throw new Error('verify cookie: output buffer too small')
|
|
350
|
+
}
|
|
351
|
+
return w
|
|
352
|
+
},
|
|
353
|
+
csrfToken(secret) {
|
|
354
|
+
// 129-char `hex.hex` cstring return.
|
|
355
|
+
return cstr(
|
|
356
|
+
csrfToken(secret, lenOrView(secret)),
|
|
357
|
+
'csrf token: output buffer too small or random source failed',
|
|
358
|
+
)
|
|
359
|
+
},
|
|
360
|
+
csrfTokenInto(secret, output) {
|
|
361
|
+
// Native pooled `_into`: writes the 129-char `hex.hex` token directly
|
|
362
|
+
// into the caller buffer (no cstring round-trip). Needed-size convention.
|
|
363
|
+
const w = Number(csrfTokenInto(secret, lenOrView(secret), output, lenOrView(output)))
|
|
364
|
+
if (w === 0) {
|
|
365
|
+
throw new Error('csrf token: random source failed')
|
|
366
|
+
}
|
|
367
|
+
if (w > output.length) {
|
|
368
|
+
throw new Error('csrf token: output buffer too small')
|
|
369
|
+
}
|
|
370
|
+
return w
|
|
371
|
+
},
|
|
372
|
+
passwordHash(password, salt, mCost, tCost, pCost, outLen) {
|
|
373
|
+
// Pre-size with the EXACT PHC string length (computable from the params),
|
|
374
|
+
// so the hash runs once — a grow-retry would re-run the whole argon2
|
|
375
|
+
// hash on a miss. growExact remains the safety net.
|
|
376
|
+
return decodeUtf8(
|
|
377
|
+
growExact(
|
|
378
|
+
(out) =>
|
|
379
|
+
Number(
|
|
380
|
+
passwordHash(
|
|
381
|
+
password,
|
|
382
|
+
lenOrView(password),
|
|
383
|
+
salt,
|
|
384
|
+
lenOrView(salt),
|
|
385
|
+
mCost,
|
|
386
|
+
tCost,
|
|
387
|
+
pCost,
|
|
388
|
+
outLen,
|
|
389
|
+
out,
|
|
390
|
+
lenOrView(out),
|
|
391
|
+
),
|
|
392
|
+
),
|
|
393
|
+
argon2PhcLength(mCost, tCost, pCost, salt.length, outLen),
|
|
394
|
+
2 * 1024 * 1024,
|
|
395
|
+
'password hash: output buffer too small',
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
},
|
|
399
|
+
passwordHashBcrypt(password, cost) {
|
|
400
|
+
// `$2b$CC$` + 22 salt chars + 31 hash chars = 60 chars.
|
|
401
|
+
const out = new Uint8Array(64)
|
|
402
|
+
const w = Number(passwordHashBcrypt(password, lenOrView(password), cost, out, lenOrView(out)))
|
|
403
|
+
if (w === 0) {
|
|
404
|
+
throw new Error('password hash bcrypt: output buffer too small')
|
|
405
|
+
}
|
|
406
|
+
return decodeUtf8(out.subarray(0, w))
|
|
407
|
+
},
|
|
408
|
+
pbkdf2Sha256(password, salt, rounds, dkLen) {
|
|
409
|
+
// Rust clamps dkLen to [1, 1MiB] (PBKDF2_MIN_LEN/MAX_LEN) AFTER sizing its
|
|
410
|
+
// own buffer — so pre-clamp here so dkLen 0 still yields a 1-byte result.
|
|
411
|
+
const dk = Math.min(Math.max(dkLen, 1), 1024 * 1024)
|
|
412
|
+
const out = new Uint8Array(dk)
|
|
413
|
+
const w = Number(
|
|
414
|
+
pbkdf2(
|
|
415
|
+
password,
|
|
416
|
+
lenOrView(password),
|
|
417
|
+
salt,
|
|
418
|
+
lenOrView(salt),
|
|
419
|
+
rounds,
|
|
420
|
+
dkLen,
|
|
421
|
+
out,
|
|
422
|
+
lenOrView(out),
|
|
423
|
+
),
|
|
424
|
+
)
|
|
425
|
+
if (w === 0) {
|
|
426
|
+
throw new Error('pbkdf2: output buffer too small')
|
|
427
|
+
}
|
|
428
|
+
return out.subarray(0, w)
|
|
429
|
+
},
|
|
430
|
+
aeadEncrypt(key, nonce, plaintext, algorithm = 0) {
|
|
431
|
+
// ciphertext + 16-byte auth tag.
|
|
432
|
+
const out = new Uint8Array(plaintext.length + 16)
|
|
433
|
+
const w = Number(
|
|
434
|
+
aeadEncrypt(
|
|
435
|
+
key,
|
|
436
|
+
lenOrView(key),
|
|
437
|
+
nonce,
|
|
438
|
+
lenOrView(nonce),
|
|
439
|
+
plaintext,
|
|
440
|
+
lenOrView(plaintext),
|
|
441
|
+
algorithm,
|
|
442
|
+
out,
|
|
443
|
+
lenOrView(out),
|
|
444
|
+
),
|
|
445
|
+
)
|
|
446
|
+
if (w === 0) {
|
|
447
|
+
throw new Error('aead encrypt: output buffer too small or bad parameters')
|
|
448
|
+
}
|
|
449
|
+
return out.subarray(0, w)
|
|
450
|
+
},
|
|
451
|
+
aeadEncryptInto(key, nonce, plaintext, output, algorithm = 0) {
|
|
452
|
+
const need = plaintext.length + 16
|
|
453
|
+
if (output.length < need) {
|
|
454
|
+
throw new Error('aead encrypt: output buffer too small')
|
|
455
|
+
}
|
|
456
|
+
const w = Number(
|
|
457
|
+
aeadEncrypt(
|
|
458
|
+
key,
|
|
459
|
+
lenOrView(key),
|
|
460
|
+
nonce,
|
|
461
|
+
lenOrView(nonce),
|
|
462
|
+
plaintext,
|
|
463
|
+
lenOrView(plaintext),
|
|
464
|
+
algorithm,
|
|
465
|
+
output,
|
|
466
|
+
lenOrView(output),
|
|
467
|
+
),
|
|
468
|
+
)
|
|
469
|
+
if (w === 0) {
|
|
470
|
+
throw new Error('aead encrypt: output buffer too small or bad parameters')
|
|
471
|
+
}
|
|
472
|
+
return w
|
|
473
|
+
},
|
|
474
|
+
aeadDecrypt(key, nonce, ciphertext, algorithm = 0) {
|
|
475
|
+
const out = new Uint8Array(ciphertext.length)
|
|
476
|
+
const w = Number(
|
|
477
|
+
aeadDecrypt(
|
|
478
|
+
key,
|
|
479
|
+
lenOrView(key),
|
|
480
|
+
nonce,
|
|
481
|
+
lenOrView(nonce),
|
|
482
|
+
ciphertext,
|
|
483
|
+
lenOrView(ciphertext),
|
|
484
|
+
algorithm,
|
|
485
|
+
out,
|
|
486
|
+
lenOrView(out),
|
|
487
|
+
),
|
|
488
|
+
)
|
|
489
|
+
return w === 0 ? null : out.subarray(0, w)
|
|
490
|
+
},
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Bind-time self-test for the codec/crypto/auth surface (the methods built in
|
|
496
|
+
* `buildCodecs`). Exercises every bound function against known-good vectors;
|
|
497
|
+
* `false` disables the ffi layer and forces the napi fallback.
|
|
498
|
+
*/
|
|
499
|
+
export function selfTestCodecs(b: BunFFI): boolean {
|
|
500
|
+
const enc = { encode: encodeUtf8 }
|
|
501
|
+
const dec = { decode: decodeUtf8 }
|
|
502
|
+
|
|
503
|
+
// Checksums + validity probes.
|
|
504
|
+
if (b.crc32(enc.encode('123456789')) !== 0xcbf4_3926) return false
|
|
505
|
+
if (b.fnv1a64(enc.encode('foobar')) !== 0x8594_4171_f739_67e8n) return false
|
|
506
|
+
// XXH3-64 of empty input = 0x2d06800538d394c2 (standard reference vector).
|
|
507
|
+
if (b.xxh3(new Uint8Array(0)) !== 0x2d06800538d394c2n) return false
|
|
508
|
+
if (b.jsonValid(SELFTEST_JSON) !== true || b.jsonValid(enc.encode('{not json')) !== false) {
|
|
509
|
+
return false
|
|
510
|
+
}
|
|
511
|
+
// UTF-8 validity probe (native replacement for a fatal TextDecoder on the
|
|
512
|
+
// Bun path — used by the urlDecode wrapper).
|
|
513
|
+
if (!b.utf8Valid(enc.encode('héllo')) || b.utf8Valid(new Uint8Array([0xff, 0xfe]))) {
|
|
514
|
+
return false
|
|
515
|
+
}
|
|
516
|
+
const hexOut = new Uint8Array(SELFTEST_HEX.length * 2)
|
|
517
|
+
if (b.hexEncodeInto(SELFTEST_HEX, hexOut) !== 10 || dec.decode(hexOut) !== '68656c6c6f') {
|
|
518
|
+
return false
|
|
519
|
+
}
|
|
520
|
+
const urlInput = enc.encode('a b/c')
|
|
521
|
+
const urlOut = new Uint8Array(9)
|
|
522
|
+
if (b.urlEncodeInto(urlInput, urlOut) !== 9 || dec.decode(urlOut) !== 'a%20b%2Fc') {
|
|
523
|
+
return false
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Validators take `cstring` ARGs (the engine transcodes the JS string).
|
|
527
|
+
if (
|
|
528
|
+
!b.validateEmail('a@b.com') ||
|
|
529
|
+
!b.validateUuid('550e8400-e29b-41d4-a716-446655440000') ||
|
|
530
|
+
!b.validateIpv4('192.168.0.1') ||
|
|
531
|
+
!b.validateIpv6('2001:db8::1') ||
|
|
532
|
+
b.validateEmail('not-an-email') ||
|
|
533
|
+
b.validateUuid('not-a-uuid')
|
|
534
|
+
) {
|
|
535
|
+
return false
|
|
536
|
+
}
|
|
537
|
+
// Packed `[u8 ok][i64 sum LE]` ABI: legit zero-sum is ok, invalid input throws.
|
|
538
|
+
if (b.jsonSumIds(enc.encode(`[{"id":1},{"id":2}]`)) !== 3n) return false
|
|
539
|
+
if (b.jsonSumIds(enc.encode(`[{"id":0},{"id":0}]`)) !== 0n) return false
|
|
540
|
+
let sumInvalidThrew = false
|
|
541
|
+
try {
|
|
542
|
+
b.jsonSumIds(enc.encode('nope'))
|
|
543
|
+
} catch {
|
|
544
|
+
sumInvalidThrew = true
|
|
545
|
+
}
|
|
546
|
+
if (!sumInvalidThrew) return false
|
|
547
|
+
|
|
548
|
+
// HMAC RFC 4231 test case 1 (0x0b × 20 key, "Hi There" data).
|
|
549
|
+
const hmacKey = new Uint8Array(20).fill(0x0b)
|
|
550
|
+
const hmacData = enc.encode('Hi There')
|
|
551
|
+
const hmacSig = enc.encode('b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7')
|
|
552
|
+
if (
|
|
553
|
+
b.hmacSha256(hmacKey, hmacData) !==
|
|
554
|
+
'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'
|
|
555
|
+
) {
|
|
556
|
+
return false
|
|
557
|
+
}
|
|
558
|
+
if (!b.hmacSha256Verify(hmacKey, hmacData, hmacSig)) {
|
|
559
|
+
return false
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Decoders round-trip.
|
|
563
|
+
const decoded = b.hexDecode(enc.encode('68656c6c6f'))
|
|
564
|
+
if (
|
|
565
|
+
dec.decode(decoded) !== 'hello' ||
|
|
566
|
+
dec.decode(b.urlDecode(enc.encode('a%20b%2Fc'))) !== 'a b/c'
|
|
567
|
+
) {
|
|
568
|
+
return false
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// WebSocket accept key (RFC 6455 sample) — `key` is a `cstring` ARG.
|
|
572
|
+
if (b.wsAcceptKey('dGhlIHNhbXBsZSBub25jZQ==') !== 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=') {
|
|
573
|
+
return false
|
|
574
|
+
}
|
|
575
|
+
// ETag: strong = 10 bytes, weak = 12 bytes.
|
|
576
|
+
if (b.etag(SELFTEST_HEX).length !== 10 || b.etag(SELFTEST_HEX, true).length !== 12) {
|
|
577
|
+
return false
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// base64.
|
|
581
|
+
if (b.base64Encode(SELFTEST_HEX) !== 'aGVsbG8=') {
|
|
582
|
+
return false
|
|
583
|
+
}
|
|
584
|
+
if (dec.decode(b.base64Decode(enc.encode('aGVsbG8='))) !== 'hello') {
|
|
585
|
+
return false
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Signed cookie round-trip.
|
|
589
|
+
const secret = enc.encode('s3cr3t-secret')
|
|
590
|
+
const signed = b.signCookie(SELFTEST_HEX, secret)
|
|
591
|
+
const verified = b.verifyCookie(enc.encode(signed), secret)
|
|
592
|
+
if (verified === null || verified !== 'hello') {
|
|
593
|
+
return false
|
|
594
|
+
}
|
|
595
|
+
if (b.verifyCookie(enc.encode('tampered.0000'), secret) !== null) {
|
|
596
|
+
return false
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// CSRF token round-trip (issued token verifies against the same secret).
|
|
600
|
+
const csrfTokenStr = b.csrfToken(secret)
|
|
601
|
+
if (csrfTokenStr.length !== 129 || !b.csrfVerify(enc.encode(csrfTokenStr), secret)) {
|
|
602
|
+
return false
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Argon2id round-trip at minimum cost (fast) — full defaults would take ~50ms.
|
|
606
|
+
const pw = enc.encode('correct horse battery staple')
|
|
607
|
+
const salt = enc.encode('salty-salt-16b')
|
|
608
|
+
const phc = b.passwordHash(pw, salt, 8, 1, 1, 16)
|
|
609
|
+
if (phc.length === 0 || !b.passwordVerify(pw, enc.encode(phc))) {
|
|
610
|
+
return false
|
|
611
|
+
}
|
|
612
|
+
// bcrypt round-trip at minimum cost (fast) — `phc` is a `cstring` ARG.
|
|
613
|
+
const bcryptPhc = b.passwordHashBcrypt(pw, 4)
|
|
614
|
+
if (bcryptPhc.length === 0 || !b.passwordVerifyBcrypt(pw, bcryptPhc)) {
|
|
615
|
+
return false
|
|
616
|
+
}
|
|
617
|
+
// PBKDF2-HMAC-SHA256: password="password", salt="salt", c=1, dkLen=32.
|
|
618
|
+
const dk = b.pbkdf2Sha256(enc.encode('password'), enc.encode('salt'), 1, 32)
|
|
619
|
+
if (b.hexEncode(dk) !== '120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b') {
|
|
620
|
+
return false
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// AEAD AES-256-GCM round-trip (key 32B, nonce 12B).
|
|
624
|
+
const aeadKey = new Uint8Array(32).fill(0x42)
|
|
625
|
+
const nonce = new Uint8Array(12).fill(0x07)
|
|
626
|
+
const ct = b.aeadEncrypt(aeadKey, nonce, SELFTEST_HEX, 0)
|
|
627
|
+
const pt = b.aeadDecrypt(aeadKey, nonce, ct, 0)
|
|
628
|
+
if (pt === null || dec.decode(pt) !== 'hello') {
|
|
629
|
+
return false
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Random token pooled Into: writes byteLen*2 hex chars into the buffer.
|
|
633
|
+
const rtOut = new Uint8Array(32)
|
|
634
|
+
const rtW = b.randomTokenInto(16, rtOut)
|
|
635
|
+
if (rtW !== 32 || !/^[0-9a-f]{32}$/.test(dec.decode(rtOut))) {
|
|
636
|
+
return false
|
|
637
|
+
}
|
|
638
|
+
// Too-small buffer → throw (needed-size convention).
|
|
639
|
+
try {
|
|
640
|
+
b.randomTokenInto(16, new Uint8Array(8))
|
|
641
|
+
return false
|
|
642
|
+
} catch {
|
|
643
|
+
// expected
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Fixed-size writers pooled Into: ws_accept_key + etag + csrf_token match the
|
|
647
|
+
// cstring path byte-for-byte.
|
|
648
|
+
const waOut = new Uint8Array(28)
|
|
649
|
+
const waW = b.wsAcceptKeyInto(enc.encode('dGhlIHNhbXBsZSBub25jZQ=='), waOut)
|
|
650
|
+
const waExpected = b.wsAcceptKey('dGhlIHNhbXBsZSBub25jZQ==')
|
|
651
|
+
if (waW !== 28 || dec.decode(waOut) !== waExpected) {
|
|
652
|
+
return false
|
|
653
|
+
}
|
|
654
|
+
const etagData = enc.encode('hello')
|
|
655
|
+
const etagOut = new Uint8Array(16)
|
|
656
|
+
const etagW = b.etagInto(etagData, etagOut)
|
|
657
|
+
if (etagW !== 10 || dec.decode(etagOut.subarray(0, etagW)) !== b.etag(etagData)) {
|
|
658
|
+
return false
|
|
659
|
+
}
|
|
660
|
+
const csrfOut = new Uint8Array(129)
|
|
661
|
+
const csrfW = b.csrfTokenInto(enc.encode('csrf-secret'), csrfOut)
|
|
662
|
+
if (csrfW !== 129 || csrfOut[64] !== 46 /* '.' */) {
|
|
663
|
+
return false
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// sign/verify cookie pooled Into: round-trip value bytes.
|
|
667
|
+
const ckVal = enc.encode('session-value')
|
|
668
|
+
const ckSec = enc.encode('s3cr3t-secret')
|
|
669
|
+
const ckOut = new Uint8Array(256)
|
|
670
|
+
const ckW = b.signCookieInto(ckVal, ckSec, ckOut)
|
|
671
|
+
if (ckW !== ckVal.length + 65) {
|
|
672
|
+
return false
|
|
673
|
+
}
|
|
674
|
+
const ckVerifyOut = new Uint8Array(256)
|
|
675
|
+
const ckV = b.verifyCookieInto(ckOut.subarray(0, ckW), ckSec, ckVerifyOut)
|
|
676
|
+
if (ckV !== ckVal.length || dec.decode(ckVerifyOut.subarray(0, ckV)) !== 'session-value') {
|
|
677
|
+
return false
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// urlEncode string form.
|
|
681
|
+
if (b.urlEncode(enc.encode('a b/c')) !== 'a%20b%2Fc') {
|
|
682
|
+
return false
|
|
683
|
+
}
|
|
684
|
+
// hexDecodeInto.
|
|
685
|
+
const hdInto = new Uint8Array(8)
|
|
686
|
+
if (
|
|
687
|
+
b.hexDecodeInto(enc.encode('68656c6c6f'), hdInto) !== 5 ||
|
|
688
|
+
dec.decode(hdInto.subarray(0, 5)) !== 'hello'
|
|
689
|
+
) {
|
|
690
|
+
return false
|
|
691
|
+
}
|
|
692
|
+
// urlDecodeInto.
|
|
693
|
+
const udInto = new Uint8Array(16)
|
|
694
|
+
if (
|
|
695
|
+
b.urlDecodeInto(enc.encode('a%20b%2Fc'), udInto) !== 5 ||
|
|
696
|
+
dec.decode(udInto.subarray(0, 5)) !== 'a b/c'
|
|
697
|
+
) {
|
|
698
|
+
return false
|
|
699
|
+
}
|
|
700
|
+
// base64EncodeInto.
|
|
701
|
+
const b64Into = new Uint8Array(16)
|
|
702
|
+
if (
|
|
703
|
+
b.base64EncodeInto(SELFTEST_HEX, b64Into) !== 8 ||
|
|
704
|
+
dec.decode(b64Into.subarray(0, 8)) !== 'aGVsbG8='
|
|
705
|
+
) {
|
|
706
|
+
return false
|
|
707
|
+
}
|
|
708
|
+
// base64DecodeInto.
|
|
709
|
+
const bdInto = new Uint8Array(8)
|
|
710
|
+
if (
|
|
711
|
+
b.base64DecodeInto(enc.encode('aGVsbG8='), bdInto) !== 5 ||
|
|
712
|
+
dec.decode(bdInto.subarray(0, 5)) !== 'hello'
|
|
713
|
+
) {
|
|
714
|
+
return false
|
|
715
|
+
}
|
|
716
|
+
// randomToken string form.
|
|
717
|
+
if (!/^[0-9a-f]{32}$/.test(b.randomToken(16))) {
|
|
718
|
+
return false
|
|
719
|
+
}
|
|
720
|
+
// hmacSha256Into (RFC 4231 test case 1 hex vector).
|
|
721
|
+
const hmacInto = new Uint8Array(64)
|
|
722
|
+
if (
|
|
723
|
+
b.hmacSha256Into(hmacKey, hmacData, hmacInto) !== 64 ||
|
|
724
|
+
dec.decode(hmacInto) !== 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'
|
|
725
|
+
) {
|
|
726
|
+
return false
|
|
727
|
+
}
|
|
728
|
+
// aeadEncryptInto → decrypt round-trip.
|
|
729
|
+
const ctInto = new Uint8Array(64)
|
|
730
|
+
const ctW = b.aeadEncryptInto(aeadKey, nonce, SELFTEST_HEX, ctInto, 0)
|
|
731
|
+
if (ctW === 0 || b.aeadDecrypt(aeadKey, nonce, ctInto.subarray(0, ctW), 0) === null) {
|
|
732
|
+
return false
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return true
|
|
736
|
+
}
|