castrum 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2259 -0
- package/Cargo.toml +138 -0
- package/README.md +6 -0
- package/build.rs +5 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1348 -1295
- package/dist/index.js +4307 -3776
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +16 -0
- package/docs/FFI_BUN_GUIDE.md +89 -6
- package/docs/REPO_MAP.md +2 -2
- package/index.ts +19 -25
- package/package.json +9 -1
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +96 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +68 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +132 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +180 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +779 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +566 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +881 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +72 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +407 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +104 -8
- package/src/native/ffi/build/compress.ts +14 -10
- package/src/native/ffi/build/instances.ts +40 -3
- package/src/native/ffi/build/metrics.ts +224 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/util.ts +14 -1
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +10 -3
- package/src/native/ffi/types.ts +111 -9
- package/src/native/ffi.ts +87 -1
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +2 -2
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +17 -11
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +5 -4
- package/src/shared/codec.ts +18 -3
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
// rust/util/batch/tests.rs — parity tests for the packed batch core + napi API.
|
|
2
|
+
//
|
|
3
|
+
// Verifies serial/parallel byte parity (both routing paths must produce
|
|
4
|
+
// identical output) and the reusable-output (`_into`) writers against the
|
|
5
|
+
// allocating variants.
|
|
6
|
+
|
|
7
|
+
use super::core::{bitset_serial, query_parse_packed_vec, sum_batch_serial};
|
|
8
|
+
use super::*;
|
|
9
|
+
use crate::util::unpack;
|
|
10
|
+
use napi::bindgen_prelude::*;
|
|
11
|
+
|
|
12
|
+
/// Build packed input: `[u32 count] repeated { [u32 len][bytes] }`.
|
|
13
|
+
fn pack_items(items: &[&[u8]]) -> Vec<u8> {
|
|
14
|
+
let mut out = Vec::new();
|
|
15
|
+
out.extend_from_slice(&(items.len() as u32).to_le_bytes());
|
|
16
|
+
for item in items {
|
|
17
|
+
out.extend_from_slice(&(item.len() as u32).to_le_bytes());
|
|
18
|
+
out.extend_from_slice(item);
|
|
19
|
+
}
|
|
20
|
+
out
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Decode packed byte results: `[u32 count] repeated { [u32 len][bytes] }`.
|
|
24
|
+
fn unpack_results(packed: &[u8]) -> Vec<Vec<u8>> {
|
|
25
|
+
assert!(packed.len() >= 4);
|
|
26
|
+
let n = u32::from_le_bytes(packed[0..4].try_into().unwrap()) as usize;
|
|
27
|
+
let mut pos = 4usize;
|
|
28
|
+
let mut out = Vec::with_capacity(n);
|
|
29
|
+
for _ in 0..n {
|
|
30
|
+
let len = u32::from_le_bytes(packed[pos..pos + 4].try_into().unwrap()) as usize;
|
|
31
|
+
pos += 4;
|
|
32
|
+
out.push(packed[pos..pos + len].to_vec());
|
|
33
|
+
pos += len;
|
|
34
|
+
}
|
|
35
|
+
out
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[test]
|
|
39
|
+
fn hex_encode_batch_matches_scalar() {
|
|
40
|
+
let data = pack_items(&[b"hi\0\xff", b"AB"]);
|
|
41
|
+
let out = hex_encode_batch_bytes(&data).unwrap();
|
|
42
|
+
let results = unpack_results(&out);
|
|
43
|
+
assert_eq!(results.len(), 2);
|
|
44
|
+
assert_eq!(
|
|
45
|
+
results[0],
|
|
46
|
+
crate::crypto::base64::hex_encode_bytes(b"hi\0\xff").into_bytes()
|
|
47
|
+
);
|
|
48
|
+
assert_eq!(results[1], b"4142");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn hex_decode_batch_roundtrips() {
|
|
53
|
+
let data = pack_items(&[b"686900ff", b"4142"]);
|
|
54
|
+
let out = hex_decode_batch_bytes(&data).unwrap();
|
|
55
|
+
let results = unpack_results(&out);
|
|
56
|
+
assert_eq!(results.len(), 2);
|
|
57
|
+
assert_eq!(results[0], b"hi\0\xff");
|
|
58
|
+
assert_eq!(results[1], b"AB");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[test]
|
|
62
|
+
fn base64_encode_batch_matches_scalar() {
|
|
63
|
+
let data = pack_items(&[b"hello world!", b"\xfb"]);
|
|
64
|
+
let out = base64_encode_batch_packed(Uint8Array::new(data), None, None).unwrap();
|
|
65
|
+
let results = unpack_results(out.as_ref());
|
|
66
|
+
assert_eq!(results.len(), 2);
|
|
67
|
+
assert_eq!(
|
|
68
|
+
results[0],
|
|
69
|
+
crate::crypto::base64::base64_encode_bytes(b"hello world!", false, true)
|
|
70
|
+
);
|
|
71
|
+
assert_eq!(
|
|
72
|
+
results[1],
|
|
73
|
+
crate::crypto::base64::base64_encode_bytes(b"\xfb", false, true)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn base64_decode_batch_roundtrips() {
|
|
79
|
+
let a = crate::crypto::base64::base64_encode_bytes(b"hello world!", false, true);
|
|
80
|
+
let b = crate::crypto::base64::base64_encode_bytes(b"\xfb", false, true);
|
|
81
|
+
let data = pack_items(&[&a, &b]);
|
|
82
|
+
let out = base64_decode_batch_packed(Uint8Array::new(data), None, None).unwrap();
|
|
83
|
+
let results = unpack_results(out.as_ref());
|
|
84
|
+
assert_eq!(results.len(), 2);
|
|
85
|
+
assert_eq!(results[0], b"hello world!");
|
|
86
|
+
assert_eq!(results[1], [0xfb]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn base64_encode_batch_urlsafe_no_pad() {
|
|
91
|
+
let data = pack_items(&[b"\xfb"]);
|
|
92
|
+
let out = base64_encode_batch_packed(Uint8Array::new(data), Some(true), Some(false)).unwrap();
|
|
93
|
+
let results = unpack_results(out.as_ref());
|
|
94
|
+
assert_eq!(results[0], b"-w");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Item count that reliably crosses `should_parallelize`'s item threshold,
|
|
98
|
+
/// forcing the rayon branch of the routing helpers.
|
|
99
|
+
fn parallel_item_count() -> usize {
|
|
100
|
+
rayon::current_num_threads().max(1).saturating_mul(2048) + 64
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[test]
|
|
104
|
+
fn bitset_serial_matches_parallel() {
|
|
105
|
+
let n = parallel_item_count();
|
|
106
|
+
let items: Vec<Vec<u8>> = (0..n).map(|i| vec![b'x'; 1 + (i % 3)]).collect();
|
|
107
|
+
let refs: Vec<&[u8]> = items.iter().map(|v| v.as_slice()).collect();
|
|
108
|
+
let packed = pack_items(&refs);
|
|
109
|
+
|
|
110
|
+
let serial = bitset_serial(&packed, |x| x.len() == 1).unwrap();
|
|
111
|
+
let routed = run_bitset_batch(&packed, |x| x.len() == 1, 64).unwrap();
|
|
112
|
+
assert_eq!(serial, routed);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
fn sum_serial_matches_parallel() {
|
|
117
|
+
let n = parallel_item_count();
|
|
118
|
+
let items: Vec<Vec<u8>> = (0..n).map(|i| i.to_string().into_bytes()).collect();
|
|
119
|
+
let refs: Vec<&[u8]> = items.iter().map(|v| v.as_slice()).collect();
|
|
120
|
+
let packed = pack_items(&refs);
|
|
121
|
+
|
|
122
|
+
let serial = sum_batch_serial(&packed, |x| x.len() as i64).unwrap();
|
|
123
|
+
let routed = run_sum_batch(&packed, |x| x.len() as i64).unwrap();
|
|
124
|
+
assert_eq!(serial, routed);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn bytes_map_serial_matches_direct() {
|
|
129
|
+
let items: Vec<Vec<u8>> = (0..32)
|
|
130
|
+
.map(|i| format!("a={}&b={}", i, i).into_bytes())
|
|
131
|
+
.collect();
|
|
132
|
+
let refs: Vec<&[u8]> = items.iter().map(|v| v.as_slice()).collect();
|
|
133
|
+
let packed = pack_items(&refs);
|
|
134
|
+
|
|
135
|
+
let serial =
|
|
136
|
+
bytes_map_serial(&packed, |v| query_parse_packed_vec(v).unwrap_or_default()).unwrap();
|
|
137
|
+
|
|
138
|
+
// Reconstruct the previous unpack + per-item write behavior for parity.
|
|
139
|
+
let unpacked = unpack(&packed).unwrap();
|
|
140
|
+
let mut expected = Vec::new();
|
|
141
|
+
expected.extend_from_slice(&(unpacked.len() as u32).to_le_bytes());
|
|
142
|
+
for item in &unpacked {
|
|
143
|
+
let r = query_parse_packed_vec(item).unwrap_or_default();
|
|
144
|
+
expected.extend_from_slice(&(r.len() as u32).to_le_bytes());
|
|
145
|
+
expected.extend_from_slice(&r);
|
|
146
|
+
}
|
|
147
|
+
assert_eq!(serial, expected);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn hmac_batch_matches_scalar() {
|
|
152
|
+
let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, b"secret");
|
|
153
|
+
let data = pack_items(&[b"hello", b"world"]);
|
|
154
|
+
let out = hmac_sha256_batch_bytes(&data, &key).unwrap();
|
|
155
|
+
let results = unpack_results(&out);
|
|
156
|
+
assert_eq!(results.len(), 2);
|
|
157
|
+
for (input, result) in [b"hello".as_slice(), b"world".as_slice()]
|
|
158
|
+
.iter()
|
|
159
|
+
.zip(&results)
|
|
160
|
+
{
|
|
161
|
+
let tag = aws_lc_rs::hmac::sign(&key, input);
|
|
162
|
+
let mut sig = [0u8; 64];
|
|
163
|
+
crate::util::bytes::hex_encode_32(tag.as_ref(), &mut sig);
|
|
164
|
+
assert_eq!(result.as_slice(), &sig[..]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[test]
|
|
169
|
+
fn hmac_verify_batch_matches_scalar() {
|
|
170
|
+
let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, b"secret");
|
|
171
|
+
let mut sig_ok = [0u8; 64];
|
|
172
|
+
let tag = aws_lc_rs::hmac::sign(&key, b"hello");
|
|
173
|
+
crate::util::bytes::hex_encode_32(tag.as_ref(), &mut sig_ok);
|
|
174
|
+
let sig_bad = [b'0'; 64];
|
|
175
|
+
|
|
176
|
+
let data = pack_items(&[b"hello", b"tampered"]);
|
|
177
|
+
let sigs = pack_items(&[&sig_ok, &sig_bad]);
|
|
178
|
+
let out = hmac_sha256_verify_batch_packed(
|
|
179
|
+
Uint8Array::new(data),
|
|
180
|
+
Uint8Array::new(sigs),
|
|
181
|
+
Uint8Array::new(b"secret".to_vec()),
|
|
182
|
+
)
|
|
183
|
+
.unwrap();
|
|
184
|
+
let bits = out.as_ref();
|
|
185
|
+
assert_eq!(&bits[..4], &2u32.to_le_bytes());
|
|
186
|
+
assert_eq!(bits[4], 0b0000_0001); // item 0 valid, item 1 invalid
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── reusable-output (_into) variants ──
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn bitset_into_matches_allocating() {
|
|
193
|
+
// Force the rayon branch (large count) so serial/parallel parity holds.
|
|
194
|
+
let n = parallel_item_count();
|
|
195
|
+
let items: Vec<Vec<u8>> = (0..n).map(|i| vec![b'x'; 1 + (i % 3)]).collect();
|
|
196
|
+
let refs: Vec<&[u8]> = items.iter().map(|v| v.as_slice()).collect();
|
|
197
|
+
let packed = pack_items(&refs);
|
|
198
|
+
|
|
199
|
+
let allocating = run_bitset_batch(&packed, |x| x.len() == 1, 64).unwrap();
|
|
200
|
+
let mut out = vec![0u8; allocating.len()];
|
|
201
|
+
let written =
|
|
202
|
+
crate::util::write_bitset_batch_into(&packed, &mut out, |x| x.len() == 1).unwrap();
|
|
203
|
+
assert_eq!(written, allocating.len());
|
|
204
|
+
assert_eq!(&out[..written], &allocating[..]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn bitset_into_zeroes_stale_bytes() {
|
|
209
|
+
let packed = pack_items(&[b"a", b"bb", b"ccc"]);
|
|
210
|
+
let mut out = vec![0xFFu8; 4 + 1]; // stale bytes must be zeroed by writer
|
|
211
|
+
let written = crate::util::write_bitset_batch_into(&packed, &mut out, |_| false).unwrap();
|
|
212
|
+
assert_eq!(written, 5);
|
|
213
|
+
assert_eq!(&out[..written], &[3, 0, 0, 0, 0]); // count=3, all bits clear
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[test]
|
|
217
|
+
fn bitset_into_errors_on_small_buffer() {
|
|
218
|
+
let packed = pack_items(&[b"a", b"bb", b"ccc"]);
|
|
219
|
+
let mut out = vec![0u8; 4]; // needs 4 + ceil(3/8) = 5
|
|
220
|
+
assert!(crate::util::write_bitset_batch_into(&packed, &mut out, |_| true).is_err());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[test]
|
|
224
|
+
fn sum_into_matches_allocating() {
|
|
225
|
+
let n = parallel_item_count();
|
|
226
|
+
let items: Vec<Vec<u8>> = (0..n).map(|i| i.to_string().into_bytes()).collect();
|
|
227
|
+
let refs: Vec<&[u8]> = items.iter().map(|v| v.as_slice()).collect();
|
|
228
|
+
let packed = pack_items(&refs);
|
|
229
|
+
|
|
230
|
+
let allocating = run_sum_batch(&packed, |x| x.len() as i64).unwrap();
|
|
231
|
+
let mut out = vec![0u8; allocating.len()];
|
|
232
|
+
let written = crate::util::write_sum_batch_into(&packed, &mut out, |x| x.len() as i64).unwrap();
|
|
233
|
+
assert_eq!(written, allocating.len());
|
|
234
|
+
assert_eq!(&out[..written], &allocating[..]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[test]
|
|
238
|
+
fn u32_into_matches_expected_layout() {
|
|
239
|
+
let packed = pack_items(&[b"a", b"bb", b"ccc"]);
|
|
240
|
+
let mut out = vec![0u8; 4 + 3 * 4];
|
|
241
|
+
let written = crate::util::write_u32_batch_into(&packed, &mut out, |x| x.len() as u32).unwrap();
|
|
242
|
+
assert_eq!(written, 4 + 3 * 4);
|
|
243
|
+
assert_eq!(u32::from_le_bytes(out[0..4].try_into().unwrap()), 3);
|
|
244
|
+
for (i, x) in [&b"a"[..], b"bb", b"ccc"].iter().enumerate() {
|
|
245
|
+
let v = u32::from_le_bytes(out[4 + i * 4..4 + (i + 1) * 4].try_into().unwrap());
|
|
246
|
+
assert_eq!(v as usize, x.len());
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[test]
|
|
251
|
+
fn validate_email_into_reports_length_and_errors() {
|
|
252
|
+
let data = pack_items(&[b"a@b.com", b"not-an-email"]);
|
|
253
|
+
let out = Uint8Array::new(vec![0u8; 16]);
|
|
254
|
+
let n = validate_email_batch_packed_into(Uint8Array::new(data), out).unwrap();
|
|
255
|
+
assert_eq!(n as usize, 4 + 1); // count + 1-byte bitset for 2 items
|
|
256
|
+
|
|
257
|
+
let out2 = Uint8Array::new(vec![0u8; 4]);
|
|
258
|
+
assert!(validate_email_batch_packed_into(
|
|
259
|
+
Uint8Array::new(pack_items(&[b"a@b.com", b"x"])),
|
|
260
|
+
out2,
|
|
261
|
+
)
|
|
262
|
+
.is_err());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn fnv1a64_into_matches_allocating() {
|
|
267
|
+
let data = pack_items(&[b"hello", b"world", b""]);
|
|
268
|
+
let allocating = fnv1a64_batch_packed(Uint8Array::new(data.clone())).unwrap();
|
|
269
|
+
let out = Uint8Array::new(vec![0u8; allocating.len()]);
|
|
270
|
+
let n = fnv1a64_batch_packed_into(Uint8Array::new(data), out).unwrap();
|
|
271
|
+
assert_eq!(n as usize, allocating.len());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Packed-batch parity (moved from unit_tests.rs) ───────────────
|
|
275
|
+
// These exercise the packed `[u32 count]{[u32 len][bytes]}` wire format across
|
|
276
|
+
// the batch entry points (util::batch + the per-domain batch fns).
|
|
277
|
+
|
|
278
|
+
/// Build the `[u32 count]{[u32 len][bytes]}` packed batch input for tests.
|
|
279
|
+
fn pack_slices(items: &[&[u8]]) -> Vec<u8> {
|
|
280
|
+
let mut out = Vec::new();
|
|
281
|
+
out.extend_from_slice(&(items.len() as u32).to_le_bytes());
|
|
282
|
+
for it in items {
|
|
283
|
+
out.extend_from_slice(&(it.len() as u32).to_le_bytes());
|
|
284
|
+
out.extend_from_slice(it);
|
|
285
|
+
}
|
|
286
|
+
out
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fn read_u32(buf: &[u8], off: usize) -> u32 {
|
|
290
|
+
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
fn read_i64(buf: &[u8], off: usize) -> i64 {
|
|
294
|
+
i64::from_le_bytes([
|
|
295
|
+
buf[off],
|
|
296
|
+
buf[off + 1],
|
|
297
|
+
buf[off + 2],
|
|
298
|
+
buf[off + 3],
|
|
299
|
+
buf[off + 4],
|
|
300
|
+
buf[off + 5],
|
|
301
|
+
buf[off + 6],
|
|
302
|
+
buf[off + 7],
|
|
303
|
+
])
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#[test]
|
|
307
|
+
fn fnv1a64_batch_matches_scalar() {
|
|
308
|
+
let out =
|
|
309
|
+
fnv1a64_batch_packed(Uint8Array::new(pack_slices(&[b"foobar", b"", b"castrum"]))).unwrap();
|
|
310
|
+
let out = out.as_ref();
|
|
311
|
+
assert_eq!(read_u32(out, 0), 3);
|
|
312
|
+
assert_eq!(read_i64(out, 4), 0x8594_4171_f739_67e8u64 as i64);
|
|
313
|
+
assert_eq!(read_i64(out, 12), 0xcbf2_9ce4_8422_2325u64 as i64);
|
|
314
|
+
assert_eq!(
|
|
315
|
+
read_i64(out, 20),
|
|
316
|
+
crate::crypto::hashing::fnv1a64_bytes(b"castrum") as i64
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#[test]
|
|
321
|
+
fn etag_batch_matches_scalar() {
|
|
322
|
+
let out = crate::http::etag::etag_batch_packed(
|
|
323
|
+
Uint8Array::new(pack_slices(&[b"123456789", b"hello"])),
|
|
324
|
+
None,
|
|
325
|
+
)
|
|
326
|
+
.unwrap();
|
|
327
|
+
let out = out.as_ref();
|
|
328
|
+
assert_eq!(read_u32(out, 0), 2);
|
|
329
|
+
let len0 = read_u32(out, 4) as usize;
|
|
330
|
+
assert_eq!(&out[8..8 + len0], b"\"cbf43926\"");
|
|
331
|
+
// Weak variant is 12 bytes with the W/ prefix.
|
|
332
|
+
let weak = crate::http::etag::etag_batch_packed(
|
|
333
|
+
Uint8Array::new(pack_slices(&[b"123456789"])),
|
|
334
|
+
Some(true),
|
|
335
|
+
)
|
|
336
|
+
.unwrap();
|
|
337
|
+
let weak = weak.as_ref();
|
|
338
|
+
let wlen = read_u32(weak, 4) as usize;
|
|
339
|
+
assert_eq!(wlen, 12);
|
|
340
|
+
assert_eq!(&weak[8..8 + wlen], b"W/\"cbf43926\"");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#[test]
|
|
344
|
+
fn url_encode_batch_matches_scalar() {
|
|
345
|
+
let out = crate::http::url_codec::url_encode_batch_packed(Uint8Array::new(pack_slices(&[
|
|
346
|
+
b"a b&c", b"plain",
|
|
347
|
+
])))
|
|
348
|
+
.unwrap();
|
|
349
|
+
let out = out.as_ref();
|
|
350
|
+
assert_eq!(read_u32(out, 0), 2);
|
|
351
|
+
let len0 = read_u32(out, 4) as usize;
|
|
352
|
+
assert_eq!(&out[8..8 + len0], b"a%20b%26c");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#[test]
|
|
356
|
+
fn url_decode_batch_matches_scalar() {
|
|
357
|
+
let out = crate::http::url_codec::url_decode_batch_packed(Uint8Array::new(pack_slices(&[
|
|
358
|
+
b"a%20b", b"plain",
|
|
359
|
+
])))
|
|
360
|
+
.unwrap();
|
|
361
|
+
let out = out.as_ref();
|
|
362
|
+
assert_eq!(read_u32(out, 0), 2);
|
|
363
|
+
let len0 = read_u32(out, 4) as usize;
|
|
364
|
+
assert_eq!(&out[8..8 + len0], b"a b");
|
|
365
|
+
|
|
366
|
+
// Strict bytes decode (no UTF-8 validation): %C3%A9 → the two raw bytes.
|
|
367
|
+
let out =
|
|
368
|
+
crate::http::url_codec::url_decode_bytes_batch_packed(Uint8Array::new(pack_slices(&[
|
|
369
|
+
b"%C3%A9",
|
|
370
|
+
])))
|
|
371
|
+
.unwrap();
|
|
372
|
+
let out = out.as_ref();
|
|
373
|
+
let len0 = read_u32(out, 4) as usize;
|
|
374
|
+
assert_eq!(&out[8..8 + len0], &[0xc3, 0xa9]);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#[test]
|
|
378
|
+
fn mime_from_extension_batch_matches_scalar() {
|
|
379
|
+
let out = crate::http::mime_lookup::mime_from_extension_batch_packed(Uint8Array::new(
|
|
380
|
+
pack_slices(&[b".js", b"PNG", b"nope"]),
|
|
381
|
+
))
|
|
382
|
+
.unwrap();
|
|
383
|
+
let out = out.as_ref();
|
|
384
|
+
assert_eq!(read_u32(out, 0), 3);
|
|
385
|
+
let mut off = 4usize;
|
|
386
|
+
let expected = [
|
|
387
|
+
b"text/javascript".as_slice(),
|
|
388
|
+
b"image/png".as_slice(),
|
|
389
|
+
b"application/octet-stream".as_slice(),
|
|
390
|
+
];
|
|
391
|
+
for exp in expected {
|
|
392
|
+
let len = read_u32(out, off) as usize;
|
|
393
|
+
off += 4;
|
|
394
|
+
assert_eq!(&out[off..off + len], exp);
|
|
395
|
+
off += len;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[test]
|
|
400
|
+
fn ws_accept_key_batch_matches_rfc6455() {
|
|
401
|
+
// Expected vector mirrors the repo's existing scalar test in
|
|
402
|
+
// rust/payload/websocket.rs (rfc6455_accept_key_vector).
|
|
403
|
+
let out =
|
|
404
|
+
crate::payload::websocket::ws_accept_key_batch_packed(Uint8Array::new(pack_slices(&[
|
|
405
|
+
b"dGhlIHNhbXBsZSBub25jZQ==",
|
|
406
|
+
])))
|
|
407
|
+
.unwrap();
|
|
408
|
+
let out = out.as_ref();
|
|
409
|
+
let len0 = read_u32(out, 4) as usize;
|
|
410
|
+
assert_eq!(&out[8..8 + len0], b"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#[test]
|
|
414
|
+
fn password_verify_batch_matches_scalar() {
|
|
415
|
+
use crate::crypto::argon2::hash_password;
|
|
416
|
+
// Low-cost argon2id so the test stays fast; salt must be >= 8 bytes.
|
|
417
|
+
let phc = hash_password(b"hunter2", b"0123456789abcdef", 8, 1, 1, 16).unwrap();
|
|
418
|
+
let good = crate::crypto::argon2::password_verify_batch_packed(
|
|
419
|
+
Uint8Array::new(pack_slices(&[b"hunter2", b"wrong"])),
|
|
420
|
+
Uint8Array::new(pack_slices(&[phc.as_bytes(), phc.as_bytes()])),
|
|
421
|
+
)
|
|
422
|
+
.unwrap();
|
|
423
|
+
let good = good.as_ref();
|
|
424
|
+
// bitset: bit0 = true (correct password), bit1 = false.
|
|
425
|
+
assert_eq!(read_u32(good, 0), 2);
|
|
426
|
+
assert_eq!(good[4] & 0b01, 0b01);
|
|
427
|
+
assert_eq!(good[4] & 0b10, 0);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#[test]
|
|
431
|
+
fn url_resolve_batch_matches_rfc3986() {
|
|
432
|
+
let base = b"http://a/b/c/d;p?q";
|
|
433
|
+
let out = crate::http::url_join::url_resolve_batch_packed(
|
|
434
|
+
Uint8Array::new(pack_slices(&[base.as_slice(), base.as_slice()])),
|
|
435
|
+
Uint8Array::new(pack_slices(&[b"g", b"../g"])),
|
|
436
|
+
)
|
|
437
|
+
.unwrap();
|
|
438
|
+
let out = out.as_ref();
|
|
439
|
+
assert_eq!(read_u32(out, 0), 2);
|
|
440
|
+
let len0 = read_u32(out, 4) as usize;
|
|
441
|
+
assert_eq!(&out[8..8 + len0], b"http://a/b/c/g");
|
|
442
|
+
let off = 8 + len0;
|
|
443
|
+
let len1 = read_u32(out, off) as usize;
|
|
444
|
+
assert_eq!(&out[off + 4..off + 4 + len1], b"http://a/b/g");
|
|
445
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// rust/util/batch_core.rs — Generic rayon-parallel batch helpers.
|
|
2
|
+
//
|
|
3
|
+
// The shared patterns for "run a predicate/map over many byte items" with an
|
|
4
|
+
// automatic serial-vs-parallel decision. Feature modules keep their own
|
|
5
|
+
// zero-alloc direct-write loops where they exist deliberately; these helpers
|
|
6
|
+
// cover the bitset / count / sum / map-to-bytes shapes.
|
|
7
|
+
|
|
8
|
+
use crate::util::threadpool::should_parallelize;
|
|
9
|
+
|
|
10
|
+
/// Generic batch validation using `AsRef<[u8]>`.
|
|
11
|
+
#[inline]
|
|
12
|
+
pub fn validation_bitset_chunked<T: AsRef<[u8]> + Sync>(
|
|
13
|
+
items: &[T],
|
|
14
|
+
f: impl Fn(&[u8]) -> bool + Sync,
|
|
15
|
+
chunk_items: usize,
|
|
16
|
+
) -> Vec<u8> {
|
|
17
|
+
let count = items.len();
|
|
18
|
+
let bitset_len = count.div_ceil(8);
|
|
19
|
+
|
|
20
|
+
let mut out = Vec::with_capacity(4 + bitset_len);
|
|
21
|
+
out.extend_from_slice(&(count as u32).to_le_bytes());
|
|
22
|
+
out.resize(4 + bitset_len, 0);
|
|
23
|
+
|
|
24
|
+
if count == 0 {
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if should_parallelize(count, total_bytes_ref(items)) {
|
|
29
|
+
use rayon::prelude::*;
|
|
30
|
+
|
|
31
|
+
let chunk_items = chunk_items.max(64);
|
|
32
|
+
let chunk_bytes = chunk_items.div_ceil(8);
|
|
33
|
+
let bits = &mut out[4..];
|
|
34
|
+
|
|
35
|
+
bits.par_chunks_mut(chunk_bytes)
|
|
36
|
+
.enumerate()
|
|
37
|
+
.for_each(|(chunk_idx, chunk)| {
|
|
38
|
+
let start_item = chunk_idx * chunk_bytes * 8;
|
|
39
|
+
let end_item = (start_item + chunk.len() * 8).min(count);
|
|
40
|
+
let start_byte = start_item / 8;
|
|
41
|
+
|
|
42
|
+
for i in start_item..end_item {
|
|
43
|
+
if f(items[i].as_ref()) {
|
|
44
|
+
chunk[(i / 8) - start_byte] |= 1 << (i & 7);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
for (i, item) in items.iter().enumerate() {
|
|
50
|
+
if f(item.as_ref()) {
|
|
51
|
+
out[4 + (i >> 3)] |= 1 << (i & 7);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
out
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Generic batch count using `AsRef<[u8]>`.
|
|
60
|
+
#[inline]
|
|
61
|
+
pub fn count_batch<T: AsRef<[u8]> + Sync>(
|
|
62
|
+
items: &[T],
|
|
63
|
+
f: impl Fn(&[u8]) -> bool + Sync,
|
|
64
|
+
chunk_items: usize,
|
|
65
|
+
) -> usize {
|
|
66
|
+
let chunk_items = chunk_items.max(64);
|
|
67
|
+
|
|
68
|
+
if should_parallelize(items.len(), total_bytes_ref(items)) {
|
|
69
|
+
use rayon::prelude::*;
|
|
70
|
+
|
|
71
|
+
items
|
|
72
|
+
.par_chunks(chunk_items)
|
|
73
|
+
.map(|chunk| chunk.iter().filter(|item| f(item.as_ref())).count())
|
|
74
|
+
.sum()
|
|
75
|
+
} else {
|
|
76
|
+
items.iter().filter(|item| f(item.as_ref())).count()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Generic batch sum using `AsRef<[u8]>`.
|
|
81
|
+
#[inline]
|
|
82
|
+
pub fn sum_batch_i64<T: AsRef<[u8]> + Sync>(
|
|
83
|
+
items: &[T],
|
|
84
|
+
f: impl Fn(&[u8]) -> i64 + Sync,
|
|
85
|
+
chunk_items: usize,
|
|
86
|
+
) -> i64 {
|
|
87
|
+
let chunk_items = chunk_items.max(64);
|
|
88
|
+
|
|
89
|
+
if should_parallelize(items.len(), total_bytes_ref(items)) {
|
|
90
|
+
use rayon::prelude::*;
|
|
91
|
+
|
|
92
|
+
items
|
|
93
|
+
.par_chunks(chunk_items)
|
|
94
|
+
.fold(
|
|
95
|
+
|| 0i64,
|
|
96
|
+
|acc, chunk| {
|
|
97
|
+
chunk
|
|
98
|
+
.iter()
|
|
99
|
+
.fold(acc, |a, item| a.saturating_add(f(item.as_ref())))
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
.reduce(|| 0i64, |a, b| a.saturating_add(b))
|
|
103
|
+
} else {
|
|
104
|
+
items
|
|
105
|
+
.iter()
|
|
106
|
+
.fold(0i64, |acc, item| acc.saturating_add(f(item.as_ref())))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Helper: total bytes for `AsRef<[u8]>` slice.
|
|
111
|
+
#[inline(always)]
|
|
112
|
+
fn total_bytes_ref<T: AsRef<[u8]>>(items: &[T]) -> usize {
|
|
113
|
+
items.iter().map(|x| x.as_ref().len()).sum()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[cfg(test)]
|
|
117
|
+
mod tests {
|
|
118
|
+
use super::*;
|
|
119
|
+
|
|
120
|
+
/// Item count that reliably crosses `should_parallelize`'s item threshold
|
|
121
|
+
/// regardless of the ambient rayon pool size, forcing the parallel branch.
|
|
122
|
+
fn parallel_len() -> usize {
|
|
123
|
+
rayon::current_num_threads().max(1).saturating_mul(2048) + 64
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#[test]
|
|
127
|
+
fn count_batch_matches_manual() {
|
|
128
|
+
let items: Vec<&[u8]> = vec![b"a", b"bb", b"c", b"dd", b""];
|
|
129
|
+
assert_eq!(count_batch(&items, |x| x.len() == 1, 64), 2);
|
|
130
|
+
assert_eq!(count_batch(&items, |x| x.is_empty(), 64), 1);
|
|
131
|
+
assert_eq!(count_batch(&items, |_| true, 64), 5);
|
|
132
|
+
assert_eq!(count_batch(&items, |_| false, 64), 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[test]
|
|
136
|
+
fn count_batch_parallel_matches_expected() {
|
|
137
|
+
let n = parallel_len();
|
|
138
|
+
let items: Vec<Vec<u8>> = (0..n).map(|_| vec![b'x']).collect();
|
|
139
|
+
let refs: Vec<&[u8]> = items.iter().map(|v| v.as_slice()).collect();
|
|
140
|
+
assert_eq!(count_batch(&refs, |x| x[0] == b'x', 64), n);
|
|
141
|
+
assert_eq!(count_batch(&refs, |x| x[0] == b'y', 64), 0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#[test]
|
|
145
|
+
fn validation_bitset_layout() {
|
|
146
|
+
let items: Vec<&[u8]> = vec![b"a", b"bb", b"c", b"dd", b""];
|
|
147
|
+
// Predicate len==1 → items 0 and 2 → bits 0b0000_0101.
|
|
148
|
+
let bits = validation_bitset_chunked(&items, |x| x.len() == 1, 64);
|
|
149
|
+
assert_eq!(&bits[..4], &5u32.to_le_bytes());
|
|
150
|
+
assert_eq!(bits[4], 0b0000_0101);
|
|
151
|
+
assert_eq!(bits.len(), 4 + 1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#[test]
|
|
155
|
+
fn validation_bitset_parallel_all_set() {
|
|
156
|
+
let n = parallel_len();
|
|
157
|
+
let items: Vec<Vec<u8>> = (0..n).map(|_| vec![b'x']).collect();
|
|
158
|
+
let refs: Vec<&[u8]> = items.iter().map(|v| v.as_slice()).collect();
|
|
159
|
+
let bits = validation_bitset_chunked(&refs, |x| x[0] == b'x', 64);
|
|
160
|
+
assert_eq!(&bits[..4], &(n as u32).to_le_bytes());
|
|
161
|
+
let set: usize = bits[4..].iter().map(|b| b.count_ones() as usize).sum();
|
|
162
|
+
assert_eq!(set, n);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[test]
|
|
166
|
+
fn validation_bitset_empty() {
|
|
167
|
+
let bits = validation_bitset_chunked::<&[u8]>(&[], |_| true, 64);
|
|
168
|
+
assert_eq!(bits, vec![0u8; 4]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn sum_batch_matches_manual() {
|
|
173
|
+
let items: Vec<&[u8]> = vec![b"a", b"bb", b"ccc"];
|
|
174
|
+
assert_eq!(sum_batch_i64(&items, |x| x.len() as i64, 64), 6);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn sum_batch_saturates() {
|
|
179
|
+
let items: Vec<Vec<u8>> = vec![vec![0u8; 8], vec![0u8; 8]];
|
|
180
|
+
let refs: Vec<&[u8]> = items.iter().map(|v| v.as_slice()).collect();
|
|
181
|
+
assert_eq!(sum_batch_i64(&refs, |_| i64::MAX, 64), i64::MAX);
|
|
182
|
+
assert_eq!(sum_batch_i64(&refs, |_| i64::MIN, 64), i64::MIN);
|
|
183
|
+
}
|
|
184
|
+
}
|