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.
- package/Cargo.lock +11 -1
- package/Cargo.toml +17 -3
- 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 +164 -4
- package/dist/index.js +434 -70
- package/docs/ENVIRONMENT.md +7 -0
- package/docs/RND-CONCURRENCY.md +20 -4
- package/docs/bun-builtins-decision-matrix.md +17 -7
- package/docs/superpowers/plans/2026-09-18-elysia-learnings-adoption.md +634 -0
- package/index.ts +5 -0
- package/package.json +2 -1
- package/rust/crypto/base64.rs +472 -57
- package/rust/crypto/hashing.rs +68 -0
- package/rust/ffi/task.rs +19 -18
- package/rust/ingress/api.rs +21 -5
- package/rust/ingress/mod.rs +7 -0
- package/rust/ingress/native_route.rs +4 -2
- package/rust/ingress/schema_cache.rs +266 -0
- package/rust/payload/sse.rs +78 -5
- package/rust/task/mod.rs +1 -0
- package/rust/task/runtime.rs +55 -7
- package/rust/task/tests.rs +98 -1
- package/rust/util/text.rs +112 -10
- package/rust/util/validation.rs +34 -0
- package/src/ingress/abort.ts +48 -0
- package/src/ingress/body.ts +104 -8
- package/src/ingress/handlers.ts +1 -0
- package/src/ingress/health.ts +22 -0
- package/src/ingress/index.ts +3 -1
- package/src/ingress/router.ts +36 -1
- package/src/ingress/routes/common.ts +9 -1
- package/src/ingress/routes/echo.ts +10 -0
- package/src/ingress/routes/json-write.ts +11 -0
- package/src/ingress/routes/native.ts +24 -5
- package/src/ingress/routes/options.ts +6 -2
- package/src/ingress/routes/responder.ts +25 -5
- package/src/ingress/routes/safe-record.ts +57 -0
- package/src/ingress/server-error.ts +111 -0
- package/src/ingress/server-node.ts +102 -23
- package/src/ingress/server.ts +140 -32
- package/src/integration/pipeline.ts +6 -0
- package/src/native/ffi/build/task.ts +2 -1
- package/src/native/ffi/types.ts +6 -1
- package/src/native/types.ts +6 -0
- package/src/runtime/detect.ts +14 -0
- package/src/rust-ffi/context.ts +46 -3
- package/src/rust-ffi/scalar/factories.ts +6 -0
- package/src/rust-ffi/scalar/interface.ts +14 -0
- package/src/rust-ffi/text.ts +5 -4
- package/src/shared/buffer-pool.ts +10 -0
- package/src/shared/memory.ts +68 -0
- package/src/shared/metrics.ts +86 -3
- package/src/shared/runtime.ts +7 -0
- package/src/task/runtime.ts +52 -9
package/rust/crypto/base64.rs
CHANGED
|
@@ -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).
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
.decode(text)
|
|
91
|
-
.ok()
|
|
124
|
+
engine(true, false).decode(text).ok()
|
|
92
125
|
}
|
|
93
126
|
|
|
94
127
|
// ── hex ──────────────────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
173
|
-
///
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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:
|
|
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!";
|