castrum 0.9.8 → 0.9.10

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.
Files changed (61) hide show
  1. package/Cargo.lock +11 -1
  2. package/Cargo.toml +17 -3
  3. package/castrum.darwin-arm64.node +0 -0
  4. package/castrum.darwin-x64.node +0 -0
  5. package/castrum.linux-arm64-gnu.node +0 -0
  6. package/castrum.linux-arm64-musl.node +0 -0
  7. package/castrum.linux-x64-gnu.node +0 -0
  8. package/castrum.linux-x64-musl.node +0 -0
  9. package/castrum.linux-x64-v3-gnu.node +0 -0
  10. package/castrum.win32-x64-msvc.node +0 -0
  11. package/dist/index.d.ts +164 -4
  12. package/dist/index.js +434 -70
  13. package/docs/ENVIRONMENT.md +7 -0
  14. package/docs/RND-CONCURRENCY.md +20 -4
  15. package/docs/bun-builtins-decision-matrix.md +17 -7
  16. package/docs/superpowers/plans/2026-09-18-elysia-learnings-adoption.md +634 -0
  17. package/index.ts +5 -0
  18. package/package.json +2 -1
  19. package/rust/crypto/base64.rs +472 -57
  20. package/rust/crypto/hashing.rs +68 -0
  21. package/rust/ffi/task.rs +19 -18
  22. package/rust/ingress/api.rs +21 -5
  23. package/rust/ingress/mod.rs +7 -0
  24. package/rust/ingress/native_route.rs +4 -2
  25. package/rust/ingress/schema_cache.rs +266 -0
  26. package/rust/payload/sse.rs +78 -5
  27. package/rust/task/mod.rs +1 -0
  28. package/rust/task/runtime.rs +55 -7
  29. package/rust/task/tests.rs +98 -1
  30. package/rust/util/text.rs +112 -10
  31. package/rust/util/validation.rs +34 -0
  32. package/src/ingress/abort.ts +48 -0
  33. package/src/ingress/body.ts +104 -8
  34. package/src/ingress/handlers.ts +1 -0
  35. package/src/ingress/health.ts +22 -0
  36. package/src/ingress/index.ts +3 -1
  37. package/src/ingress/router.ts +36 -1
  38. package/src/ingress/routes/common.ts +9 -1
  39. package/src/ingress/routes/echo.ts +10 -0
  40. package/src/ingress/routes/json-write.ts +11 -0
  41. package/src/ingress/routes/native.ts +24 -5
  42. package/src/ingress/routes/options.ts +6 -2
  43. package/src/ingress/routes/responder.ts +25 -5
  44. package/src/ingress/routes/safe-record.ts +57 -0
  45. package/src/ingress/server-error.ts +111 -0
  46. package/src/ingress/server-node.ts +102 -23
  47. package/src/ingress/server.ts +140 -32
  48. package/src/integration/pipeline.ts +6 -0
  49. package/src/native/ffi/build/task.ts +2 -1
  50. package/src/native/ffi/types.ts +6 -1
  51. package/src/native/types.ts +6 -0
  52. package/src/runtime/detect.ts +14 -0
  53. package/src/rust-ffi/context.ts +46 -3
  54. package/src/rust-ffi/scalar/factories.ts +6 -0
  55. package/src/rust-ffi/scalar/interface.ts +14 -0
  56. package/src/rust-ffi/text.ts +5 -4
  57. package/src/shared/buffer-pool.ts +10 -0
  58. package/src/shared/memory.ts +68 -0
  59. package/src/shared/metrics.ts +86 -3
  60. package/src/shared/runtime.ts +7 -0
  61. package/src/task/runtime.ts +52 -9
@@ -1,25 +1,66 @@
1
1
  // rust/crypto/base64.rs — base64 (standard/url-safe) + hex encode/decode.
2
2
  //
3
- // Uses the `base64` crate engines (Cargo dep already present). The
4
- // `Base64Codec` higher-order instance precompiles the alphabet/decoding
5
- // configuration once in its constructor and reuses it across calls.
3
+ // Uses the `base64` crate engines (Cargo dep already present). On the shipped
4
+ // targets (x86_64 / aarch64) the hot cores dispatch to the crate's
5
+ // runtime-detected SIMD engine (`engine::simd::Simd` — AVX2 / NEON kernels):
6
+ // payloads ≥ 64 B (decode) / ≥ 128 B (encode) go through the wide kernels,
7
+ // anything smaller falls back to the scalar engine internally, so both length
8
+ // regimes stay efficient and output stays byte-identical to the scalar path.
9
+ // Other targets keep the scalar `GeneralPurpose` engine (not a shipped
10
+ // castrum target — local dev / test builds only). The `Base64Codec`
11
+ // higher-order instance precompiles the alphabet/decoding configuration once
12
+ // in its constructor and reuses it across calls.
6
13
 
7
14
  use napi::bindgen_prelude::*;
8
15
  use napi_derive::napi;
9
16
 
10
- use crate::util::bytes::{hex_val, HEX_LOWER};
11
17
  use base64::Engine as _;
12
18
 
13
- fn engine(url_safe: bool, padding: bool) -> base64::engine::general_purpose::GeneralPurpose {
19
+ /// Engine selection for a base64 call.
20
+ ///
21
+ /// SIMD branch (x86_64 / aarch64 — every shipped castrum target): the
22
+ /// `engine::simd::Simd` engine detects AVX2 / NEON once at construction and
23
+ /// falls back to the scalar `GeneralPurpose` when the CPU (or a payload below
24
+ /// the 64 B decode / 128 B encode thresholds) doesn't benefit, so small
25
+ /// payloads keep the same fixed costs while large ones get wide kernels. The
26
+ /// `PAD` / `NO_PAD` `GeneralPurposeConfig` consts are the exact configs the
27
+ /// preconfigured scalar engines use, so the wire contract is unchanged.
28
+ #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
29
+ mod engine_sel {
30
+ use base64::engine::general_purpose::{NO_PAD, PAD};
31
+ use base64::engine::simd::Simd;
32
+
33
+ pub(super) type Engine = Simd;
34
+
35
+ pub(super) fn engine(url_safe: bool, padding: bool) -> Simd {
36
+ match (url_safe, padding) {
37
+ (false, true) => Simd::standard(PAD),
38
+ (false, false) => Simd::standard(NO_PAD),
39
+ (true, true) => Simd::url_safe(PAD),
40
+ (true, false) => Simd::url_safe(NO_PAD),
41
+ }
42
+ }
43
+ }
44
+
45
+ /// Scalar fallback (non-x86_64 / non-aarch64 targets only).
46
+ #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
47
+ mod engine_sel {
14
48
  use base64::engine::general_purpose::*;
15
- match (url_safe, padding) {
16
- (false, true) => STANDARD,
17
- (false, false) => STANDARD_NO_PAD,
18
- (true, true) => URL_SAFE,
19
- (true, false) => URL_SAFE_NO_PAD,
49
+
50
+ pub(super) type Engine = GeneralPurpose;
51
+
52
+ pub(super) fn engine(url_safe: bool, padding: bool) -> GeneralPurpose {
53
+ match (url_safe, padding) {
54
+ (false, true) => STANDARD,
55
+ (false, false) => STANDARD_NO_PAD,
56
+ (true, true) => URL_SAFE,
57
+ (true, false) => URL_SAFE_NO_PAD,
58
+ }
20
59
  }
21
60
  }
22
61
 
62
+ use engine_sel::engine;
63
+
23
64
  #[napi]
24
65
  pub fn base64_encode(input: Uint8Array, url_safe: Option<bool>, padding: Option<bool>) -> Buffer {
25
66
  let out = engine(url_safe.unwrap_or(false), padding.unwrap_or(true)).encode(input.as_ref());
@@ -40,16 +81,12 @@ pub fn base64_decode(
40
81
 
41
82
  #[napi]
42
83
  pub fn base64url_encode(input: Uint8Array) -> Buffer {
43
- Buffer::from(
44
- base64::engine::general_purpose::URL_SAFE_NO_PAD
45
- .encode(input.as_ref())
46
- .into_bytes(),
47
- )
84
+ Buffer::from(engine(true, false).encode(input.as_ref()).into_bytes())
48
85
  }
49
86
 
50
87
  #[napi]
51
88
  pub fn base64url_decode(input: Uint8Array) -> Result<Buffer> {
52
- base64::engine::general_purpose::URL_SAFE_NO_PAD
89
+ engine(true, false)
53
90
  .decode(input.as_ref())
54
91
  .map(Buffer::from)
55
92
  .map_err(|e| Error::from_reason(e.to_string()))
@@ -76,9 +113,7 @@ pub fn base64_decode_bytes(input: &[u8], url_safe: bool, padding: bool) -> Resul
76
113
  /// one place.
77
114
  #[inline]
78
115
  pub fn base64url_encode_bytes(data: &[u8]) -> Vec<u8> {
79
- base64::engine::general_purpose::URL_SAFE_NO_PAD
80
- .encode(data)
81
- .into_bytes()
116
+ engine(true, false).encode(data).into_bytes()
82
117
  }
83
118
 
84
119
  /// Base64url (RFC 7515 §2 — URL-safe alphabet, NO padding) decode from raw
@@ -86,15 +121,26 @@ pub fn base64url_encode_bytes(data: &[u8]) -> Vec<u8> {
86
121
  #[inline]
87
122
  pub fn base64url_decode_bytes(data: &[u8]) -> Option<Vec<u8>> {
88
123
  let text = std::str::from_utf8(data).ok()?;
89
- base64::engine::general_purpose::URL_SAFE_NO_PAD
90
- .decode(text)
91
- .ok()
124
+ engine(true, false).decode(text).ok()
92
125
  }
93
126
 
94
127
  // ── hex ──────────────────────────────────────────────────────────
95
-
96
- /// Encode bytes as lowercase hex (uses the shared `HEX_LOWER` table — direct
97
- /// byte writes, no per-nibble `char` push).
128
+ //
129
+ // Lowercase hex encode + case-insensitive decode. Encode goes through the
130
+ // `faster-hex` SIMD engine (runtime-dispatched SSE→AVX2; measured 4.3-18.9x
131
+ // faster than Buffer.toString("hex") at every size 64B→1MB, replacing the
132
+ // per-byte table loop that lost 0.30x; the 1KB outlier ~19x is Bun-side). Decode has TWO tiers:
133
+ // - x86_64 + AVX2: our single-pass `hex_fast` kernel (validity mask + value
134
+ // map in one vector pass, 64 chars/iteration, scalar fallback on any
135
+ // invalid chunk). faster-hex decode does a separate validation scan +
136
+ // decode pass (~5GB/s cap vs Buffer.from(s,"hex") ~10.5GB/s); the
137
+ // single-pass kernel targets that class directly.
138
+ // - everywhere else: faster-hex decode (SSE/scalar, validated).
139
+ // Both tiers are byte-identical to the HEX_VAL_LUT semantics — verified by
140
+ // exhaustive tests (all 256 bytes × encode, all 65,536 byte-pairs × decode,
141
+ // plus a length sweep across the 64-char chunk boundary).
142
+
143
+ /// Lowercase-hex encode (SIMD via `faster-hex`).
98
144
  ///
99
145
  /// # Safety
100
146
  ///
@@ -102,26 +148,31 @@ pub fn base64url_decode_bytes(data: &[u8]) -> Option<Vec<u8>> {
102
148
  /// the built `Vec<u8>` is valid UTF-8 by construction; `from_utf8_unchecked`
103
149
  /// skips a redundant validation pass on the allocating hot path.
104
150
  pub fn hex_encode_bytes(input: &[u8]) -> String {
105
- let mut out = Vec::with_capacity(input.len() * 2);
106
- for &b in input {
107
- out.push(HEX_LOWER[(b >> 4) as usize]);
108
- out.push(HEX_LOWER[(b & 0x0f) as usize]);
109
- }
110
- // SAFETY: every byte pushed is an ASCII hex digit from `HEX_LOWER`.
151
+ let mut out = vec![0u8; input.len() * 2];
152
+ // faster-hex encode cannot fail on an exactly-sized buffer.
153
+ faster_hex::hex_encode(input, &mut out).expect("hex encode: exact-size output buffer");
154
+ // SAFETY: every byte written is an ASCII hex digit from `HEX_LOWER`.
111
155
  unsafe { String::from_utf8_unchecked(out) }
112
156
  }
113
157
 
114
- /// Decode lowercase/uppercase hex to bytes.
158
+ /// Decode lowercase/uppercase hex to bytes. On x86_64 + AVX2 this uses the
159
+ /// single-pass `hex_fast` kernel; elsewhere the validated `faster-hex` engine
160
+ /// — both reject `[^0-9a-fA-F]` exactly like `hex_val`.
115
161
  pub fn hex_decode_bytes(input: &[u8]) -> std::result::Result<Vec<u8>, &'static str> {
116
162
  if !input.len().is_multiple_of(2) {
117
163
  return Err("odd hex length");
118
164
  }
119
- let mut out = Vec::with_capacity(input.len() / 2);
120
- for i in (0..input.len()).step_by(2) {
121
- let hi = hex_val(input[i]).ok_or("invalid hex digit")?;
122
- let lo = hex_val(input[i + 1]).ok_or("invalid hex digit")?;
123
- out.push((hi << 4) | lo);
165
+ let mut out = vec![0u8; input.len() / 2];
166
+ #[cfg(target_arch = "x86_64")]
167
+ {
168
+ if std::arch::is_x86_feature_detected!("avx2") {
169
+ // SAFETY: AVX2 runtime-detected; `input` even, `out` exactly
170
+ // `input.len() / 2` (both established above).
171
+ unsafe { hex_fast::decode(input, &mut out) }?;
172
+ return Ok(out);
173
+ }
124
174
  }
175
+ faster_hex::hex_decode(input, &mut out).map_err(|_| "invalid hex digit")?;
125
176
  Ok(out)
126
177
  }
127
178
 
@@ -144,8 +195,8 @@ pub fn hex_decode(input: Uint8Array) -> Result<Buffer> {
144
195
  // a fresh Vec + napi Buffer per call. They error on a buffer that is too small;
145
196
  // input/output overlap is handled by `crate::util::run_packed_into`.
146
197
 
147
- /// Lowercase-hex encode directly into `out`. Returns bytes written
148
- /// (`input.len() * 2`). Errors if `out` is too small.
198
+ /// Lowercase-hex encode directly into `out` (SIMD via `faster-hex`). Returns
199
+ /// bytes written (`input.len() * 2`). Errors if `out` is too small.
149
200
  #[inline]
150
201
  pub fn hex_encode_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
151
202
  let needed = input
@@ -155,13 +206,12 @@ pub fn hex_encode_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
155
206
  if out.len() < needed {
156
207
  return Err(Error::from_reason("hex encode: output buffer too small"));
157
208
  }
158
- let mut pos = 0usize;
159
- for &b in input {
160
- out[pos] = HEX_LOWER[(b >> 4) as usize];
161
- out[pos + 1] = HEX_LOWER[(b & 0x0f) as usize];
162
- pos += 2;
163
- }
164
- Ok(pos)
209
+ // Slice to the exact size so the pipeline never sees a ragged tail: the
210
+ // caller-sized-buffer contract is preserved (error above), and the engine
211
+ // fills `needed` bytes exactly.
212
+ let exact = &mut out[..needed];
213
+ faster_hex::hex_encode(input, exact).expect("hex encode: exact-size output buffer");
214
+ Ok(needed)
165
215
  }
166
216
 
167
217
  #[napi]
@@ -169,8 +219,12 @@ pub fn hex_encode_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32>
169
219
  crate::util::run_packed_into(&input, &mut output, hex_encode_into_slice)
170
220
  }
171
221
 
172
- /// Hex-decode directly into `out`. Returns bytes written (`input.len() / 2`).
173
- /// Errors on odd length, invalid digits, or an output buffer that is too small.
222
+ /// Hex-decode directly into `out` — case-insensitive, rejects
223
+ /// `[^0-9a-fA-F]` exactly like `hex_val`. Returns bytes written
224
+ /// (`input.len() / 2`). Errors on odd length, invalid digits, or an output
225
+ /// buffer that is too small. On x86_64 with AVX2 this dispatches to the
226
+ /// single-pass `hex_fast` kernel; everywhere else it uses the validated
227
+ /// `faster-hex` engine — both byte-identical to the scalar LUT semantics.
174
228
  #[inline]
175
229
  pub fn hex_decode_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
176
230
  if !input.len().is_multiple_of(2) {
@@ -180,14 +234,27 @@ pub fn hex_decode_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
180
234
  if out.len() < needed {
181
235
  return Err(Error::from_reason("hex decode: output buffer too small"));
182
236
  }
183
- let mut pos = 0usize;
184
- for i in (0..input.len()).step_by(2) {
185
- let hi = hex_val(input[i]).ok_or_else(|| Error::from_reason("invalid hex digit"))?;
186
- let lo = hex_val(input[i + 1]).ok_or_else(|| Error::from_reason("invalid hex digit"))?;
187
- out[pos] = (hi << 4) | lo;
188
- pos += 1;
237
+ hex_decode_into_slice_impl(input, &mut out[..needed])
238
+ }
239
+
240
+ #[cfg(target_arch = "x86_64")]
241
+ #[inline]
242
+ fn hex_decode_into_slice_impl(input: &[u8], out: &mut [u8]) -> Result<usize> {
243
+ if std::arch::is_x86_feature_detected!("avx2") {
244
+ // SAFETY: AVX2 runtime-detected; `input` even and `out` exactly
245
+ // `input.len() / 2` (checked above).
246
+ unsafe { hex_fast::decode(input, out) }.map_err(Error::from_reason)
247
+ } else {
248
+ faster_hex::hex_decode(input, out).map_err(|_| Error::from_reason("invalid hex digit"))?;
249
+ Ok(out.len())
189
250
  }
190
- Ok(pos)
251
+ }
252
+
253
+ #[cfg(not(target_arch = "x86_64"))]
254
+ #[inline]
255
+ fn hex_decode_into_slice_impl(input: &[u8], out: &mut [u8]) -> Result<usize> {
256
+ faster_hex::hex_decode(input, out).map_err(|_| Error::from_reason("invalid hex digit"))?;
257
+ Ok(out.len())
191
258
  }
192
259
 
193
260
  #[napi]
@@ -195,6 +262,113 @@ pub fn hex_decode_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32>
195
262
  crate::util::run_packed_into(&input, &mut output, hex_decode_into_slice)
196
263
  }
197
264
 
265
+ /// Single-pass AVX2 hex decode — the accelerated decode tier on x86_64 (see
266
+ /// the hex section header). Decodes 64 hex chars (32 output bytes) per
267
+ /// iteration, computing the validity mask and the hex-digit values in ONE
268
+ /// vector pass. Any chunk containing a non-hex byte rejects the whole input
269
+ /// (byte-identical to the HEX_VAL_LUT scalar reject-everywhere semantics —
270
+ /// no partial writes). The remainder (< 64 chars) is decoded by the validated
271
+ /// `faster-hex` engine, so error behavior and output are identical everywhere.
272
+ ///
273
+ /// faster-hex decode does a separate validation scan + decode pass (~5GB/s
274
+ /// cap measured vs Buffer.from(hex) ~10.5GB/s); this kernel is single-pass
275
+ /// and targets that gap.
276
+ #[cfg(target_arch = "x86_64")]
277
+ pub mod hex_fast {
278
+ use std::arch::x86_64::*;
279
+
280
+ /// Decode `chars[0..64]` → `out[0..32]`. Returns false when any of the 64
281
+ /// chars is not `[0-9a-fA-F]`; on false, `out` is left untouched so
282
+ /// callers can error without partial writes.
283
+ #[target_feature(enable = "avx2")]
284
+ unsafe fn decode_chunk(chars: *const u8, out: *mut u8) -> bool {
285
+ let lo = _mm256_loadu_si256(chars as *const __m256i); // chars 0..31
286
+ let hi = _mm256_loadu_si256(chars.add(32) as *const __m256i); // chars 32..63
287
+
288
+ // Lowercase for the letter test ('A'-'F' | 0x20 == 'a'-'f').
289
+ let loo = _mm256_or_si256(lo, _mm256_set1_epi8(0x20));
290
+ let hio = _mm256_or_si256(hi, _mm256_set1_epi8(0x20));
291
+
292
+ // Validity per byte (signed compares; all bounds are < 0x80 so signed
293
+ // and unsigned agree for the printable range, and bytes >= 0x80 fail
294
+ // both bounds): (b in '0'..='9') | (b|0x20 in 'a'..='f').
295
+ let lo_digit = _mm256_and_si256(
296
+ _mm256_cmpgt_epi8(loo, _mm256_set1_epi8((b'0' - 1) as i8)),
297
+ _mm256_cmpgt_epi8(_mm256_set1_epi8((b'9' + 1) as i8), loo),
298
+ );
299
+ let lo_letter = _mm256_and_si256(
300
+ _mm256_cmpgt_epi8(loo, _mm256_set1_epi8((b'a' - 1) as i8)),
301
+ _mm256_cmpgt_epi8(_mm256_set1_epi8((b'f' + 1) as i8), loo),
302
+ );
303
+ let hi_digit = _mm256_and_si256(
304
+ _mm256_cmpgt_epi8(hio, _mm256_set1_epi8((b'0' - 1) as i8)),
305
+ _mm256_cmpgt_epi8(_mm256_set1_epi8((b'9' + 1) as i8), hio),
306
+ );
307
+ let hi_letter = _mm256_and_si256(
308
+ _mm256_cmpgt_epi8(hio, _mm256_set1_epi8((b'a' - 1) as i8)),
309
+ _mm256_cmpgt_epi8(_mm256_set1_epi8((b'f' + 1) as i8), hio),
310
+ );
311
+ let lo_valid = _mm256_or_si256(lo_digit, lo_letter);
312
+ let hi_valid = _mm256_or_si256(hi_digit, hi_letter);
313
+ if _mm256_movemask_epi8(lo_valid) != -1 || _mm256_movemask_epi8(hi_valid) != -1 {
314
+ return false;
315
+ }
316
+
317
+ // Value: (b|0x20) - '0' gives 0-9 for digits and 49-54 for a-f;
318
+ // subtract 39 where it is a letter so a-f map to 10-15.
319
+ let lo_letter_sel = _mm256_cmpgt_epi8(loo, _mm256_set1_epi8((b'a' - 1) as i8));
320
+ let hi_letter_sel = _mm256_cmpgt_epi8(hio, _mm256_set1_epi8((b'a' - 1) as i8));
321
+ let v_lo = _mm256_sub_epi8(
322
+ _mm256_sub_epi8(loo, _mm256_set1_epi8(b'0' as i8)),
323
+ _mm256_and_si256(lo_letter_sel, _mm256_set1_epi8(39)),
324
+ );
325
+ let v_hi = _mm256_sub_epi8(
326
+ _mm256_sub_epi8(hio, _mm256_set1_epi8(b'0' as i8)),
327
+ _mm256_and_si256(hi_letter_sel, _mm256_set1_epi8(39)),
328
+ );
329
+
330
+ // Pack adjacent pairs (v0, v1) -> (v0 << 4) | v1 with pmaddubsw
331
+ // (unsigned v * signed pattern [16, 1] per pair, summed into u16).
332
+ let pat = _mm256_set1_epi16(0x0110); // little-endian bytes: 0x10=16, 0x01=1
333
+ let p_lo = _mm256_maddubs_epi16(v_lo, pat); // u16 lanes: out[0..8] (half0), out[8..16] (half1)
334
+ let p_hi = _mm256_maddubs_epi16(v_hi, pat); // u16 lanes: out[16..24] (half0), out[24..32] (half1)
335
+
336
+ // packus_epi16 lays halves side by side per 128-bit lane:
337
+ // lane0 = out[0..8] + out[16..24] lane1 = out[8..16] + out[24..32]
338
+ // so a 64-bit lane permute [0, 2, 1, 3] restores the byte order.
339
+ let packed = _mm256_packus_epi16(p_lo, p_hi);
340
+ let ordered = _mm256_permute4x64_epi64(packed, 0b11_01_10_00);
341
+ _mm256_storeu_si256(out as *mut __m256i, ordered);
342
+ true
343
+ }
344
+
345
+ /// Single-pass decode of `input` into `out`. Preconditions (checked by the
346
+ /// callers): `input.len()` is even and `out.len() == input.len()/2`.
347
+ /// Returns `Ok(limit / 2)` or `Err("invalid hex digit")`.
348
+ ///
349
+ /// # Safety
350
+ /// The caller must have runtime-detected AVX2
351
+ /// (`is_x86_feature_detected!("avx2")`) and must uphold the length
352
+ /// preconditions above.
353
+ #[target_feature(enable = "avx2")]
354
+ pub unsafe fn decode(input: &[u8], out: &mut [u8]) -> std::result::Result<usize, &'static str> {
355
+ let limit = input.len();
356
+ let mut i = 0usize;
357
+ while i + 64 <= limit {
358
+ if !decode_chunk(input.as_ptr().add(i), out.as_mut_ptr().add(i / 2)) {
359
+ return Err("invalid hex digit");
360
+ }
361
+ i += 64;
362
+ }
363
+ if i < limit {
364
+ // Tail (a multiple-of-2 remainder < 64 chars): validated engine.
365
+ faster_hex::hex_decode(&input[i..], &mut out[i / 2..limit / 2])
366
+ .map_err(|_| "invalid hex digit")?;
367
+ }
368
+ Ok(limit / 2)
369
+ }
370
+ }
371
+
198
372
  /// Pure core: base64-encode `input` into `out` (zero-alloc `encode_slice`).
199
373
  /// Returns bytes written; errors if `out` is too small. Shared by the `_into`
200
374
  /// napi path and the pure-core unit tests.
@@ -255,10 +429,11 @@ pub fn base64_decode_into(
255
429
 
256
430
  /// Higher-order instance: the alphabet/padding engine is selected ONCE at
257
431
  /// construction and stored concretely, so `encode`/`decode` never re-run the
258
- /// 4-way engine match on the per-call path.
432
+ /// 4-way engine match on the per-call path. On x86_64/aarch64 this holds the
433
+ /// SIMD runtime-dispatched engine, so large payloads also get AVX2/NEON.
259
434
  #[napi]
260
435
  pub struct Base64Codec {
261
- engine: base64::engine::general_purpose::GeneralPurpose,
436
+ engine: engine_sel::Engine,
262
437
  }
263
438
 
264
439
  #[napi]
@@ -335,6 +510,81 @@ mod tests {
335
510
  assert_eq!(enc.as_ref(), b"-w");
336
511
  }
337
512
 
513
+ // ── SIMD engine (x86_64 / aarch64: AVX2 / NEON kernels) ──
514
+ //
515
+ // The SIMD engine must be a drop-in for the scalar one: byte-identical
516
+ // output, identical validity/padding errors, on payloads both above the
517
+ // SIMD thresholds (decode ≥ 64 B, encode ≥ 128 B) and below them.
518
+
519
+ fn deterministic(n: usize) -> Vec<u8> {
520
+ (0..n)
521
+ .map(|i| (i.wrapping_mul(31) ^ (i >> 3)) as u8)
522
+ .collect()
523
+ }
524
+
525
+ #[test]
526
+ fn simd_large_payload_roundtrip_into_slice() {
527
+ // 16 KiB: well above both SIMD thresholds — exercises the wide kernels
528
+ // end-to-end and must round-trip byte-identically.
529
+ let data = deterministic(16 * 1024);
530
+ let mut enc = vec![0u8; data.len().div_ceil(3) * 4];
531
+ let w = base64_encode_into_slice(&data, &mut enc, false, true).unwrap();
532
+ assert_eq!(w, enc.len());
533
+ let mut dec = vec![0u8; data.len()];
534
+ let m = base64_decode_into_slice(&enc, &mut dec, false, true).unwrap();
535
+ assert_eq!(&dec[..m], &data[..]);
536
+ }
537
+
538
+ #[test]
539
+ fn simd_output_byte_identical_to_scalar() {
540
+ // Large payload: SIMD encode/decode must match the scalar engines
541
+ // exactly (alphabet + padding wire contract unchanged).
542
+ let data = deterministic(2048);
543
+ // standard w/ padding
544
+ let mut enc = vec![0u8; data.len().div_ceil(3) * 4];
545
+ let w = base64_encode_into_slice(&data, &mut enc, false, true).unwrap();
546
+ let scalar_exp = base64::engine::general_purpose::STANDARD.encode(&data);
547
+ assert_eq!(&enc[..w], scalar_exp.as_bytes());
548
+ // url-safe, no padding
549
+ let mut encu = vec![0u8; data.len().div_ceil(3) * 4];
550
+ let wu = base64_encode_into_slice(&data, &mut encu, true, false).unwrap();
551
+ let scalar_expu = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&data);
552
+ assert_eq!(&encu[..wu], scalar_expu.as_bytes());
553
+ // decode path parity
554
+ let mut dec = vec![0u8; data.len()];
555
+ let m = base64_decode_into_slice(scalar_exp.as_bytes(), &mut dec, false, true).unwrap();
556
+ assert_eq!(&dec[..m], &data[..]);
557
+ let mut decu = vec![0u8; data.len()];
558
+ let mu = base64_decode_into_slice(scalar_expu.as_bytes(), &mut decu, true, false).unwrap();
559
+ assert_eq!(&decu[..mu], &data[..]);
560
+ }
561
+
562
+ #[test]
563
+ fn simd_large_decode_rejects_invalid() {
564
+ // Invalid base64 symbol mid-buffer above the SIMD threshold: the wide
565
+ // kernel breaks out and the scalar decoder reports the error — parity
566
+ // with the scalar engine's rejection.
567
+ let mut bad = vec![b'A'; 4096];
568
+ bad[2048] = b'!';
569
+ let mut out = vec![0u8; 4096];
570
+ assert!(base64_decode_into_slice(&bad, &mut out, false, true).is_err());
571
+ let scalar = base64::engine::general_purpose::STANDARD.decode(bad.as_slice());
572
+ assert!(scalar.is_err());
573
+ }
574
+
575
+ #[test]
576
+ fn simd_small_payload_matches_scalar() {
577
+ // Below the SIMD thresholds the engine is scalar internally; output
578
+ // and errors must match the preconfigured engines.
579
+ let data = b"hello world!";
580
+ let mut enc = [0u8; 32];
581
+ let w = base64_encode_into_slice(data, &mut enc, false, true).unwrap();
582
+ assert_eq!(&enc[..w], b"aGVsbG8gd29ybGQh");
583
+ let mut dec = [0u8; 16];
584
+ let m = base64_decode_into_slice(b"aGVsbG8gd29ybGQh", &mut dec, false, true).unwrap();
585
+ assert_eq!(&dec[..m], data);
586
+ }
587
+
338
588
  // ── reusable-output (_into) variants ──
339
589
 
340
590
  #[test]
@@ -384,6 +634,171 @@ mod tests {
384
634
  assert!(hex_decode_into_slice(b"0001020304", &mut out).is_err());
385
635
  }
386
636
 
637
+ // ── hex SIMD engine (faster-hex: runtime-dispatched SSE→AVX2) ──
638
+ //
639
+ // faster-hex must be a drop-in for the previous HEX_VAL_LUT scalar loops:
640
+ // byte-identical encode output, identical accept/reject + values on
641
+ // decode, at lengths both below and above the SIMD chunk boundaries
642
+ // (pshufb processes 16 bytes/iteration, so 15/16/17, 31/32/33, … are the
643
+ // straddle cases that would expose a broken fast path).
644
+
645
+ /// Reference lowercase-hex encode (the previous per-byte LUT loop).
646
+ fn ref_hex_encode(input: &[u8]) -> Vec<u8> {
647
+ let hex = b"0123456789abcdef";
648
+ let mut out = Vec::with_capacity(input.len() * 2);
649
+ for &b in input {
650
+ out.push(hex[(b >> 4) as usize]);
651
+ out.push(hex[(b & 0x0f) as usize]);
652
+ }
653
+ out
654
+ }
655
+
656
+ /// Reference hex-digit value (the previous HEX_VAL_LUT semantics).
657
+ fn ref_hex_val(b: u8) -> Option<u8> {
658
+ match b {
659
+ b'0'..=b'9' => Some(b - b'0'),
660
+ b'a'..=b'f' => Some(b - b'a' + 10),
661
+ b'A'..=b'F' => Some(b - b'A' + 10),
662
+ _ => None,
663
+ }
664
+ }
665
+
666
+ #[test]
667
+ fn hex_simd_encode_parity_all_lengths_and_sizes() {
668
+ // Every length 0..=96 (covers every pshufb chunk boundary twice) plus
669
+ // larger payloads that exercise the wide kernels end-to-end.
670
+ let mut lens: Vec<usize> = (0..=96).collect();
671
+ lens.extend([128, 255, 256, 511, 512, 1024, 16 * 1024, 256 * 1024]);
672
+ for n in lens {
673
+ let data = deterministic(n);
674
+ let expect = ref_hex_encode(&data);
675
+ // allocating path
676
+ assert_eq!(hex_encode_bytes(&data).as_bytes(), &expect[..], "len {n}");
677
+ // into-slice path (exact-size buffer, like the C-ABI caller)
678
+ let mut out = vec![0u8; n * 2];
679
+ let w = hex_encode_into_slice(&data, &mut out).unwrap();
680
+ assert_eq!(w, n * 2);
681
+ assert_eq!(&out[..w], &expect[..], "len {n}");
682
+ }
683
+ }
684
+
685
+ #[test]
686
+ fn hex_simd_decode_exhaustive_pair_matrix() {
687
+ // All 65,536 two-char combos: accept/reject and decoded value must be
688
+ // identical to the reference LUT semantics (case-insensitive digits,
689
+ // everything else rejected). A SIMD engine with a divergent validity
690
+ // mask fails here.
691
+ for hi in 0u16..=255 {
692
+ for lo in 0u16..=255 {
693
+ let pair = [hi as u8, lo as u8];
694
+ let want = match (ref_hex_val(pair[0]), ref_hex_val(pair[1])) {
695
+ (Some(h), Some(l)) => Some((h << 4) | l),
696
+ _ => None,
697
+ };
698
+ let mut out = [0u8; 1];
699
+ let got = hex_decode_into_slice(&pair, &mut out);
700
+ match want {
701
+ Some(v) => {
702
+ assert!(got.is_ok(), "{pair:02x?}: expected OK");
703
+ assert_eq!(out[0], v, "{pair:02x?}: value");
704
+ }
705
+ None => assert!(got.is_err(), "{pair:02x?}: expected reject"),
706
+ }
707
+ }
708
+ }
709
+ }
710
+
711
+ #[test]
712
+ fn hex_simd_large_roundtrip() {
713
+ // 2 MiB deterministic payload: SIMD encode → SIMD decode must be the
714
+ // identity, and the decoded bytes must match the source exactly.
715
+ let data = deterministic(2 * 1024 * 1024);
716
+ let mut enc = vec![0u8; data.len() * 2];
717
+ let w = hex_encode_into_slice(&data, &mut enc).unwrap();
718
+ assert_eq!(w, data.len() * 2);
719
+ let mut dec = vec![0u8; data.len()];
720
+ let m = hex_decode_into_slice(&enc, &mut dec).unwrap();
721
+ assert_eq!(m, data.len());
722
+ assert_eq!(&dec[..], &data[..]);
723
+ // uppercase decodes to the same bytes (case-insensitive)
724
+ for b in enc.iter_mut() {
725
+ if b.is_ascii_lowercase() {
726
+ *b = b.to_ascii_uppercase();
727
+ }
728
+ }
729
+ let mut dec2 = vec![0u8; data.len()];
730
+ let m2 = hex_decode_into_slice(&enc, &mut dec2).unwrap();
731
+ assert_eq!(&dec2[..m2], &data[..]);
732
+ }
733
+
734
+ #[test]
735
+ fn hex_simd_large_decode_rejects_invalid_tail() {
736
+ // An invalid char at the very END of a 256 KiB string: the SIMD tail
737
+ // handler must still reject (parity with the scalar loop's rejection
738
+ // at any position).
739
+ let mut hex = Vec::with_capacity(256 * 1024);
740
+ for _ in 0..(128 * 1024) {
741
+ hex.extend_from_slice(b"ab");
742
+ }
743
+ hex.push(b'g'); // 'g' is not a hex digit → odd+invalid
744
+ let mut out = vec![0u8; 128 * 1024];
745
+ assert!(hex_decode_into_slice(&hex, &mut out).is_err());
746
+
747
+ // Even-length with a bad char one past the SIMD chunk boundary.
748
+ let mut hex2 = hex[..hex.len() - 1].to_vec(); // even, all "ab..."
749
+ hex2[200_001] = b'z';
750
+ assert!(hex_decode_into_slice(&hex2, &mut out).is_err());
751
+ }
752
+
753
+ #[test]
754
+ fn hex_simd_decode_chunk_boundary_sweep() {
755
+ // Every length 0..=160 crosses the AVX2 kernel's 64-char chunk
756
+ // boundary several times: valid inputs must decode to the reference
757
+ // bytes, and an invalid char must be rejected no matter which bucket
758
+ // it lands in (chunk body, chunk boundary, or the < 64-char tail).
759
+ // This is the direct guard on the single-pass kernel's chunk loop +
760
+ // tail handoff (the exhaustive pair matrix above covers 2-char inputs,
761
+ // which only ever exercise the tail path).
762
+ let mut lens: Vec<usize> = (0..=160).collect();
763
+ lens.extend([192, 200, 512, 4096]);
764
+ for n in lens {
765
+ let even = n - n % 2; // even number of HEX CHARS in the input
766
+ let expect: Vec<u8> = (0..even / 2)
767
+ .map(|i| (i.wrapping_mul(13) + 7) as u8)
768
+ .collect();
769
+ let hex: String = expect.iter().map(|b| format!("{b:02x}")).collect();
770
+ assert_eq!(hex.len(), even);
771
+ let mut out = vec![0u8; even / 2];
772
+ let w = hex_decode_into_slice(hex.as_bytes(), &mut out).unwrap();
773
+ assert_eq!(&out[..w], &expect[..], "len {even}");
774
+ // Reject an invalid char at one position per bucket.
775
+ for &pos in &[
776
+ 0usize,
777
+ 1,
778
+ 14,
779
+ 30,
780
+ 31,
781
+ 32,
782
+ 62,
783
+ 63,
784
+ 64,
785
+ 65,
786
+ even.saturating_sub(2),
787
+ even.saturating_sub(1),
788
+ ] {
789
+ if pos < even {
790
+ let mut bad = hex.clone().into_bytes();
791
+ bad[pos] = b'g'; // 'g' is never a hex digit
792
+ let mut o = vec![0u8; even / 2];
793
+ assert!(
794
+ hex_decode_into_slice(&bad, &mut o).is_err(),
795
+ "len {even} pos {pos}"
796
+ );
797
+ }
798
+ }
799
+ }
800
+ }
801
+
387
802
  #[test]
388
803
  fn base64_encode_into_reports_length() {
389
804
  let data = b"hello world!";