castrum 0.9.3 → 0.9.6

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 (206) hide show
  1. package/Cargo.lock +2242 -0
  2. package/Cargo.toml +155 -0
  3. package/README.md +14 -6
  4. package/build.rs +5 -0
  5. package/castrum.darwin-arm64.node +0 -0
  6. package/castrum.darwin-x64.node +0 -0
  7. package/castrum.linux-arm64-gnu.node +0 -0
  8. package/castrum.linux-arm64-musl.node +0 -0
  9. package/castrum.linux-x64-gnu.node +0 -0
  10. package/castrum.linux-x64-musl.node +0 -0
  11. package/castrum.linux-x64-v3-gnu.node +0 -0
  12. package/castrum.win32-x64-msvc.node +0 -0
  13. package/dist/index.d.ts +1508 -1291
  14. package/dist/index.js +3391 -2069
  15. package/docs/API.md +2 -1
  16. package/docs/ARCHITECTURE.md +3 -2
  17. package/docs/CASE_STUDY.md +5 -3
  18. package/docs/ENVIRONMENT.md +25 -2
  19. package/docs/FFI_BUN_GUIDE.md +242 -14
  20. package/docs/HOW_TO_ADD_AN_OP.md +10 -0
  21. package/docs/REPO_MAP.md +17 -6
  22. package/docs/RND-CONCURRENCY.md +772 -0
  23. package/docs/bun-builtins-decision-matrix.md +4 -1
  24. package/index.ts +24 -25
  25. package/package.json +173 -156
  26. package/rust/crypto/aead.rs +378 -0
  27. package/rust/crypto/argon2.rs +265 -0
  28. package/rust/crypto/base64.rs +420 -0
  29. package/rust/crypto/bcrypt.rs +89 -0
  30. package/rust/crypto/cookie_sign.rs +169 -0
  31. package/rust/crypto/csrf.rs +106 -0
  32. package/rust/crypto/ed25519.rs +215 -0
  33. package/rust/crypto/hashing.rs +140 -0
  34. package/rust/crypto/hmac_sha256.rs +88 -0
  35. package/rust/crypto/jwt/api.rs +286 -0
  36. package/rust/crypto/jwt/mod.rs +22 -0
  37. package/rust/crypto/jwt/tests.rs +312 -0
  38. package/rust/crypto/jwt/token.rs +360 -0
  39. package/rust/crypto/mod.rs +27 -0
  40. package/rust/crypto/pbkdf2.rs +105 -0
  41. package/rust/crypto/random_token.rs +85 -0
  42. package/rust/crypto/session.rs +222 -0
  43. package/rust/ffi/crypto.rs +708 -0
  44. package/rust/ffi/hashing.rs +285 -0
  45. package/rust/ffi/http.rs +752 -0
  46. package/rust/ffi/ingress.rs +303 -0
  47. package/rust/ffi/json.rs +517 -0
  48. package/rust/ffi/jwt.rs +419 -0
  49. package/rust/ffi/metrics.rs +373 -0
  50. package/rust/ffi/mod.rs +69 -0
  51. package/rust/ffi/payload.rs +275 -0
  52. package/rust/ffi/probe.rs +60 -0
  53. package/rust/ffi/rate_limit.rs +77 -0
  54. package/rust/ffi/route.rs +85 -0
  55. package/rust/ffi/task.rs +172 -0
  56. package/rust/ffi/tests.rs +2333 -0
  57. package/rust/ffi/util.rs +152 -0
  58. package/rust/ffi/validators.rs +123 -0
  59. package/rust/http/accept.rs +606 -0
  60. package/rust/http/cookie_parser.rs +140 -0
  61. package/rust/http/etag.rs +289 -0
  62. package/rust/http/form.rs +136 -0
  63. package/rust/http/headers.rs +267 -0
  64. package/rust/http/http_date.rs +286 -0
  65. package/rust/http/http_parser.rs +252 -0
  66. package/rust/http/media_type.rs +355 -0
  67. package/rust/http/method.rs +200 -0
  68. package/rust/http/mime_lookup.rs +130 -0
  69. package/rust/http/mod.rs +32 -0
  70. package/rust/http/multipart.rs +685 -0
  71. package/rust/http/query_parser.rs +211 -0
  72. package/rust/http/url_codec.rs +296 -0
  73. package/rust/http/url_join.rs +324 -0
  74. package/rust/ingress/api.rs +355 -0
  75. package/rust/ingress/cors.rs +403 -0
  76. package/rust/ingress/ingress_constants.rs +209 -0
  77. package/rust/ingress/ip_trust.rs +312 -0
  78. package/rust/ingress/mod.rs +48 -0
  79. package/rust/ingress/native_route.rs +770 -0
  80. package/rust/ingress/options.rs +176 -0
  81. package/rust/ingress/output.rs +406 -0
  82. package/rust/ingress/packed.rs +191 -0
  83. package/rust/ingress/pipeline.rs +534 -0
  84. package/rust/ingress/proxy.rs +138 -0
  85. package/rust/ingress/rate_limit.rs +538 -0
  86. package/rust/ingress/terminal.rs +114 -0
  87. package/rust/ingress/tests.rs +567 -0
  88. package/rust/ingress/time.rs +70 -0
  89. package/rust/json/fast_schema/capture.rs +336 -0
  90. package/rust/json/fast_schema/compile.rs +556 -0
  91. package/rust/json/fast_schema/cursor.rs +368 -0
  92. package/rust/json/fast_schema/email.rs +282 -0
  93. package/rust/json/fast_schema/errors.rs +247 -0
  94. package/rust/json/fast_schema/mod.rs +57 -0
  95. package/rust/json/fast_schema/tests.rs +1143 -0
  96. package/rust/json/fast_schema/types.rs +140 -0
  97. package/rust/json/fast_schema/validate.rs +931 -0
  98. package/rust/json/json_ops.rs +121 -0
  99. package/rust/json/json_schema.rs +837 -0
  100. package/rust/json/json_ser.rs +883 -0
  101. package/rust/json/mod.rs +19 -0
  102. package/rust/json/napi_marshal.rs +318 -0
  103. package/rust/json/patch/api.rs +60 -0
  104. package/rust/json/patch/engine.rs +153 -0
  105. package/rust/json/patch/mod.rs +47 -0
  106. package/rust/json/patch/ops.rs +270 -0
  107. package/rust/json/patch/pointer.rs +49 -0
  108. package/rust/json/patch/tests.rs +499 -0
  109. package/rust/lib.rs +73 -0
  110. package/rust/metrics/api.rs +148 -0
  111. package/rust/metrics/mod.rs +21 -0
  112. package/rust/metrics/registry.rs +961 -0
  113. package/rust/panic_safety.rs +40 -0
  114. package/rust/payload/compress.rs +512 -0
  115. package/rust/payload/mod.rs +13 -0
  116. package/rust/payload/sse.rs +214 -0
  117. package/rust/payload/template.rs +253 -0
  118. package/rust/payload/websocket.rs +69 -0
  119. package/rust/payload/ws_frames.rs +354 -0
  120. package/rust/proptest_suite.rs +139 -0
  121. package/rust/selection.rs +95 -0
  122. package/rust/src/bin/native-bench.rs +495 -0
  123. package/rust/task/completion.rs +119 -0
  124. package/rust/task/mod.rs +65 -0
  125. package/rust/task/ops.rs +503 -0
  126. package/rust/task/runtime.rs +220 -0
  127. package/rust/task/tests.rs +231 -0
  128. package/rust/test_support.rs +71 -0
  129. package/rust/util/batch/api.rs +266 -0
  130. package/rust/util/batch/core.rs +473 -0
  131. package/rust/util/batch/mod.rs +23 -0
  132. package/rust/util/batch/tests.rs +445 -0
  133. package/rust/util/batch_core.rs +184 -0
  134. package/rust/util/bytes.rs +594 -0
  135. package/rust/util/mod.rs +34 -0
  136. package/rust/util/packed.rs +473 -0
  137. package/rust/util/text.rs +130 -0
  138. package/rust/util/threadpool.rs +128 -0
  139. package/rust/util/validation.rs +419 -0
  140. package/rust-toolchain.toml +17 -0
  141. package/scripts/postinstall.mjs +199 -0
  142. package/src/ingress/constants.ts +1 -1
  143. package/src/ingress/context.ts +2 -2
  144. package/src/ingress/decode/baked-result.ts +13 -13
  145. package/src/ingress/decode/fast-result.ts +17 -17
  146. package/src/ingress/errors.ts +7 -7
  147. package/src/ingress/headers/baked-templates.ts +1 -1
  148. package/src/ingress/headers/fast-templates.ts +3 -3
  149. package/src/ingress/health.ts +1 -2
  150. package/src/ingress/index.ts +1 -1
  151. package/src/ingress/native-route.ts +9 -5
  152. package/src/ingress/options.ts +1 -1
  153. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  154. package/src/ingress/packing/route-wire.ts +1 -5
  155. package/src/ingress/response/error-bodies.ts +5 -5
  156. package/src/ingress/response/terminal.ts +1 -1
  157. package/src/ingress/routes/echo.ts +4 -4
  158. package/src/ingress/routes/fallback.ts +2 -2
  159. package/src/ingress/routes/head.ts +2 -2
  160. package/src/ingress/routes/native.ts +2 -1
  161. package/src/ingress/routes/options.ts +1 -1
  162. package/src/ingress/routes/read.ts +2 -2
  163. package/src/ingress/server.ts +10 -6
  164. package/src/ingress/status.ts +7 -7
  165. package/src/integration/batch.ts +1 -1
  166. package/src/integration/index.ts +9 -9
  167. package/src/integration/pipeline.ts +6 -6
  168. package/src/integration/websocket.ts +2 -2
  169. package/src/loader/index.ts +1 -5
  170. package/src/loader/ops.ts +1 -1
  171. package/src/native/ffi/build/codecs.ts +130 -12
  172. package/src/native/ffi/build/compress.ts +22 -10
  173. package/src/native/ffi/build/instances.ts +46 -4
  174. package/src/native/ffi/build/metrics.ts +247 -0
  175. package/src/native/ffi/build/parse.ts +59 -23
  176. package/src/native/ffi/build/task.ts +79 -0
  177. package/src/native/ffi/build/util.ts +32 -1
  178. package/src/native/ffi/build.ts +4 -0
  179. package/src/native/ffi/selftest.ts +12 -3
  180. package/src/native/ffi/types.ts +151 -9
  181. package/src/native/ffi.ts +160 -15
  182. package/src/native/index.ts +1 -1
  183. package/src/native/loader.ts +55 -5
  184. package/src/native/types/instances.ts +34 -0
  185. package/src/native/types.ts +11 -0
  186. package/src/runtime/codec.ts +53 -9
  187. package/src/runtime/types.ts +7 -0
  188. package/src/rust-ffi/batch/build.ts +16 -9
  189. package/src/rust-ffi/index.ts +7 -6
  190. package/src/rust-ffi/options.ts +1 -3
  191. package/src/rust-ffi/proven.ts +3 -3
  192. package/src/rust-ffi/scalar/crypto.ts +1 -1
  193. package/src/rust-ffi/scalar/factories.ts +73 -0
  194. package/src/rust-ffi/scalar/hashing.ts +1 -1
  195. package/src/rust-ffi/scalar/http.ts +71 -27
  196. package/src/rust-ffi/scalar/index.ts +2 -0
  197. package/src/rust-ffi/scalar/interface.ts +20 -0
  198. package/src/rust-ffi/scalar/payload.ts +11 -3
  199. package/src/rust-ffi/scalar/validation.ts +121 -0
  200. package/src/rust-ffi/text.ts +25 -3
  201. package/src/selection.ts +1 -1
  202. package/src/shared/bytes.ts +24 -4
  203. package/src/shared/codec.ts +18 -3
  204. package/src/task/index.ts +24 -0
  205. package/src/task/op.ts +171 -0
  206. package/src/task/runtime.ts +568 -0
@@ -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
+ }