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.
Files changed (184) hide show
  1. package/Cargo.lock +2259 -0
  2. package/Cargo.toml +138 -0
  3. package/README.md +6 -0
  4. package/build.rs +5 -0
  5. package/castrum.linux-x64-gnu.node +0 -0
  6. package/dist/index.d.ts +1348 -1295
  7. package/dist/index.js +4307 -3776
  8. package/docs/API.md +1 -1
  9. package/docs/ARCHITECTURE.md +3 -2
  10. package/docs/CASE_STUDY.md +2 -2
  11. package/docs/ENVIRONMENT.md +16 -0
  12. package/docs/FFI_BUN_GUIDE.md +89 -6
  13. package/docs/REPO_MAP.md +2 -2
  14. package/index.ts +19 -25
  15. package/package.json +9 -1
  16. package/rust/crypto/aead.rs +378 -0
  17. package/rust/crypto/argon2.rs +265 -0
  18. package/rust/crypto/base64.rs +420 -0
  19. package/rust/crypto/bcrypt.rs +89 -0
  20. package/rust/crypto/cookie_sign.rs +169 -0
  21. package/rust/crypto/csrf.rs +106 -0
  22. package/rust/crypto/ed25519.rs +215 -0
  23. package/rust/crypto/hashing.rs +140 -0
  24. package/rust/crypto/hmac_sha256.rs +88 -0
  25. package/rust/crypto/jwt/api.rs +286 -0
  26. package/rust/crypto/jwt/mod.rs +22 -0
  27. package/rust/crypto/jwt/tests.rs +312 -0
  28. package/rust/crypto/jwt/token.rs +360 -0
  29. package/rust/crypto/mod.rs +27 -0
  30. package/rust/crypto/pbkdf2.rs +96 -0
  31. package/rust/crypto/random_token.rs +85 -0
  32. package/rust/crypto/session.rs +222 -0
  33. package/rust/ffi/crypto.rs +708 -0
  34. package/rust/ffi/hashing.rs +285 -0
  35. package/rust/ffi/http.rs +752 -0
  36. package/rust/ffi/ingress.rs +303 -0
  37. package/rust/ffi/json.rs +517 -0
  38. package/rust/ffi/jwt.rs +419 -0
  39. package/rust/ffi/metrics.rs +373 -0
  40. package/rust/ffi/mod.rs +68 -0
  41. package/rust/ffi/payload.rs +275 -0
  42. package/rust/ffi/probe.rs +60 -0
  43. package/rust/ffi/rate_limit.rs +77 -0
  44. package/rust/ffi/route.rs +85 -0
  45. package/rust/ffi/tests.rs +2333 -0
  46. package/rust/ffi/util.rs +152 -0
  47. package/rust/ffi/validators.rs +123 -0
  48. package/rust/http/accept.rs +606 -0
  49. package/rust/http/cookie_parser.rs +140 -0
  50. package/rust/http/etag.rs +289 -0
  51. package/rust/http/form.rs +132 -0
  52. package/rust/http/headers.rs +267 -0
  53. package/rust/http/http_date.rs +286 -0
  54. package/rust/http/http_parser.rs +252 -0
  55. package/rust/http/media_type.rs +355 -0
  56. package/rust/http/method.rs +200 -0
  57. package/rust/http/mime_lookup.rs +130 -0
  58. package/rust/http/mod.rs +32 -0
  59. package/rust/http/multipart.rs +685 -0
  60. package/rust/http/query_parser.rs +180 -0
  61. package/rust/http/url_codec.rs +296 -0
  62. package/rust/http/url_join.rs +324 -0
  63. package/rust/ingress/api.rs +355 -0
  64. package/rust/ingress/cors.rs +403 -0
  65. package/rust/ingress/ingress_constants.rs +209 -0
  66. package/rust/ingress/ip_trust.rs +312 -0
  67. package/rust/ingress/mod.rs +48 -0
  68. package/rust/ingress/native_route.rs +779 -0
  69. package/rust/ingress/options.rs +176 -0
  70. package/rust/ingress/output.rs +406 -0
  71. package/rust/ingress/packed.rs +191 -0
  72. package/rust/ingress/pipeline.rs +534 -0
  73. package/rust/ingress/proxy.rs +138 -0
  74. package/rust/ingress/rate_limit.rs +538 -0
  75. package/rust/ingress/terminal.rs +114 -0
  76. package/rust/ingress/tests.rs +566 -0
  77. package/rust/ingress/time.rs +70 -0
  78. package/rust/json/fast_schema/capture.rs +336 -0
  79. package/rust/json/fast_schema/compile.rs +556 -0
  80. package/rust/json/fast_schema/cursor.rs +368 -0
  81. package/rust/json/fast_schema/email.rs +282 -0
  82. package/rust/json/fast_schema/errors.rs +247 -0
  83. package/rust/json/fast_schema/mod.rs +57 -0
  84. package/rust/json/fast_schema/tests.rs +1143 -0
  85. package/rust/json/fast_schema/types.rs +140 -0
  86. package/rust/json/fast_schema/validate.rs +931 -0
  87. package/rust/json/json_ops.rs +121 -0
  88. package/rust/json/json_schema.rs +837 -0
  89. package/rust/json/json_ser.rs +881 -0
  90. package/rust/json/mod.rs +19 -0
  91. package/rust/json/napi_marshal.rs +318 -0
  92. package/rust/json/patch/api.rs +60 -0
  93. package/rust/json/patch/engine.rs +153 -0
  94. package/rust/json/patch/mod.rs +47 -0
  95. package/rust/json/patch/ops.rs +270 -0
  96. package/rust/json/patch/pointer.rs +49 -0
  97. package/rust/json/patch/tests.rs +499 -0
  98. package/rust/lib.rs +72 -0
  99. package/rust/metrics/api.rs +148 -0
  100. package/rust/metrics/mod.rs +21 -0
  101. package/rust/metrics/registry.rs +961 -0
  102. package/rust/panic_safety.rs +40 -0
  103. package/rust/payload/compress.rs +512 -0
  104. package/rust/payload/mod.rs +13 -0
  105. package/rust/payload/sse.rs +214 -0
  106. package/rust/payload/template.rs +253 -0
  107. package/rust/payload/websocket.rs +69 -0
  108. package/rust/payload/ws_frames.rs +354 -0
  109. package/rust/proptest_suite.rs +139 -0
  110. package/rust/selection.rs +95 -0
  111. package/rust/src/bin/native-bench.rs +495 -0
  112. package/rust/test_support.rs +71 -0
  113. package/rust/util/batch/api.rs +266 -0
  114. package/rust/util/batch/core.rs +473 -0
  115. package/rust/util/batch/mod.rs +23 -0
  116. package/rust/util/batch/tests.rs +445 -0
  117. package/rust/util/batch_core.rs +184 -0
  118. package/rust/util/bytes.rs +407 -0
  119. package/rust/util/mod.rs +34 -0
  120. package/rust/util/packed.rs +473 -0
  121. package/rust/util/text.rs +130 -0
  122. package/rust/util/threadpool.rs +128 -0
  123. package/rust/util/validation.rs +419 -0
  124. package/rust-toolchain.toml +17 -0
  125. package/scripts/postinstall.mjs +199 -0
  126. package/src/ingress/constants.ts +1 -1
  127. package/src/ingress/context.ts +2 -2
  128. package/src/ingress/decode/baked-result.ts +13 -13
  129. package/src/ingress/decode/fast-result.ts +17 -17
  130. package/src/ingress/errors.ts +7 -7
  131. package/src/ingress/headers/baked-templates.ts +1 -1
  132. package/src/ingress/headers/fast-templates.ts +3 -3
  133. package/src/ingress/index.ts +1 -1
  134. package/src/ingress/native-route.ts +9 -5
  135. package/src/ingress/options.ts +1 -1
  136. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  137. package/src/ingress/packing/route-wire.ts +1 -5
  138. package/src/ingress/response/error-bodies.ts +5 -5
  139. package/src/ingress/response/terminal.ts +1 -1
  140. package/src/ingress/routes/echo.ts +4 -4
  141. package/src/ingress/routes/fallback.ts +2 -2
  142. package/src/ingress/routes/head.ts +2 -2
  143. package/src/ingress/routes/native.ts +2 -1
  144. package/src/ingress/routes/options.ts +1 -1
  145. package/src/ingress/routes/read.ts +2 -2
  146. package/src/ingress/server.ts +10 -6
  147. package/src/ingress/status.ts +7 -7
  148. package/src/integration/batch.ts +1 -1
  149. package/src/integration/index.ts +9 -9
  150. package/src/integration/pipeline.ts +6 -6
  151. package/src/integration/websocket.ts +2 -2
  152. package/src/loader/index.ts +1 -5
  153. package/src/loader/ops.ts +1 -1
  154. package/src/native/ffi/build/codecs.ts +104 -8
  155. package/src/native/ffi/build/compress.ts +14 -10
  156. package/src/native/ffi/build/instances.ts +40 -3
  157. package/src/native/ffi/build/metrics.ts +224 -0
  158. package/src/native/ffi/build/parse.ts +59 -23
  159. package/src/native/ffi/build/util.ts +14 -1
  160. package/src/native/ffi/build.ts +2 -0
  161. package/src/native/ffi/selftest.ts +10 -3
  162. package/src/native/ffi/types.ts +111 -9
  163. package/src/native/ffi.ts +87 -1
  164. package/src/native/index.ts +1 -1
  165. package/src/native/loader.ts +2 -2
  166. package/src/native/types/instances.ts +34 -0
  167. package/src/native/types.ts +11 -0
  168. package/src/runtime/codec.ts +53 -9
  169. package/src/runtime/types.ts +7 -0
  170. package/src/rust-ffi/batch/build.ts +16 -9
  171. package/src/rust-ffi/index.ts +7 -6
  172. package/src/rust-ffi/options.ts +1 -3
  173. package/src/rust-ffi/proven.ts +3 -3
  174. package/src/rust-ffi/scalar/crypto.ts +1 -1
  175. package/src/rust-ffi/scalar/factories.ts +73 -0
  176. package/src/rust-ffi/scalar/hashing.ts +1 -1
  177. package/src/rust-ffi/scalar/http.ts +17 -11
  178. package/src/rust-ffi/scalar/index.ts +2 -0
  179. package/src/rust-ffi/scalar/interface.ts +20 -0
  180. package/src/rust-ffi/scalar/payload.ts +11 -3
  181. package/src/rust-ffi/scalar/validation.ts +121 -0
  182. package/src/selection.ts +1 -1
  183. package/src/shared/bytes.ts +5 -4
  184. package/src/shared/codec.ts +18 -3
@@ -0,0 +1,473 @@
1
+ // rust/util/batch/core.rs — packed-buffer batch routing + domain helpers.
2
+ //
3
+ // The reusable plumbing behind the `*_batch_packed` napi APIs (in `api.rs`):
4
+ // every helper takes a packed `[u32 count]{[u32 len][bytes]}` buffer and
5
+ // produces the packed/aggregate result. Serial paths iterate `PackedIter`
6
+ // zero-alloc (no `Vec<&[u8]>` materialization); parallel paths materialize the
7
+ // `unpack` Vec and delegate to the generic `util::batch_core` helpers (which
8
+ // own the rayon chunking). Both paths are byte-identical.
9
+ //
10
+ // Note: signatures use napi `Result` (legacy), so this module depends on napi
11
+ // types even though it performs no JS marshaling.
12
+
13
+ use napi::bindgen_prelude::*;
14
+
15
+ use crate::json::json_ops::{json_sum_ids_bytes, json_valid_bytes};
16
+ use crate::util::packed::PackedIter;
17
+ use crate::util::validation::{
18
+ validate_email_bytes, validate_ipv4_bytes, validate_ipv6_bytes, validate_uuid_bytes,
19
+ };
20
+ use crate::util::{count_batch, should_parallelize, sum_batch_i64, total_bytes, unpack};
21
+
22
+ // ── Internal helpers (serial zero-alloc + direct-write variants) ──
23
+
24
+ /// Direct-write sum batch — eliminates intermediate Vec<i64>.
25
+ #[inline]
26
+ fn sum_batch_direct(items: &[&[u8]], out: &mut Vec<u8>, f: impl Fn(&[u8]) -> i64 + Sync) {
27
+ let n = items.len();
28
+ out.reserve(4 + n * 8);
29
+ out.extend_from_slice(&(n as u32).to_le_bytes());
30
+ let start = out.len();
31
+ out.resize(start + n * 8, 0);
32
+
33
+ // Slice the output buffer to write directly into it.
34
+ let out_slice = &mut out[start..];
35
+
36
+ if should_parallelize(items.len(), total_bytes(items)) {
37
+ use rayon::prelude::*;
38
+ let chunk_size = 256;
39
+
40
+ // Use par_chunks_mut to get mutable disjoint slices of the output buffer.
41
+ out_slice
42
+ .par_chunks_mut(chunk_size * 8)
43
+ .enumerate()
44
+ .for_each(|(ci, out_chunk)| {
45
+ let num_items = out_chunk.len() / 8;
46
+ let item_start = ci * chunk_size;
47
+ let item_end = (item_start + num_items).min(items.len());
48
+
49
+ for (i, item) in items[item_start..item_end].iter().enumerate() {
50
+ let sum = f(item);
51
+ out_chunk[i * 8..(i + 1) * 8].copy_from_slice(&sum.to_le_bytes());
52
+ }
53
+ });
54
+ } else {
55
+ for (i, item) in items.iter().enumerate() {
56
+ let sum = f(item);
57
+ out_slice[i * 8..(i + 1) * 8].copy_from_slice(&sum.to_le_bytes());
58
+ }
59
+ }
60
+ }
61
+
62
+ /// Zero-alloc serial sum over a packed buffer — no `Vec<&[u8]>` materialization.
63
+ #[inline]
64
+ pub(crate) fn sum_batch_serial(data: &[u8], f: impl Fn(&[u8]) -> i64) -> Result<Vec<u8>> {
65
+ let iter = PackedIter::new(data)?;
66
+ let count = iter.len();
67
+ let mut out = Vec::with_capacity(4 + count * 8);
68
+ out.extend_from_slice(&(count as u32).to_le_bytes());
69
+ out.resize(4 + count * 8, 0);
70
+ for (i, item) in iter.enumerate() {
71
+ let sum = f(item);
72
+ out[4 + i * 8..4 + (i + 1) * 8].copy_from_slice(&sum.to_le_bytes());
73
+ }
74
+ Ok(out)
75
+ }
76
+
77
+ /// Shared packed-batch routing prelude.
78
+ ///
79
+ /// Parses the packed buffer once, decides parallel-vs-serial, and materializes
80
+ /// the `unpack` Vec ONLY when parallelizing (the serial path iterates `iter`
81
+ /// zero-alloc). Returns the iterator (for serial passes), the parallel flag and
82
+ /// the unpacked slices (only meaningful when `parallel`). All batch functions
83
+ /// route through this so the prelude can never drift.
84
+ #[inline]
85
+ fn packed_routing<'a>(data: &'a [u8]) -> Result<(PackedIter<'a>, bool, Vec<&'a [u8]>)> {
86
+ let iter = PackedIter::new(data)?;
87
+ let (count, bytes) = iter.count_and_total_bytes()?;
88
+ if should_parallelize(count, bytes) {
89
+ let items = unpack(data)?;
90
+ Ok((iter, true, items))
91
+ } else {
92
+ Ok((iter, false, Vec::new()))
93
+ }
94
+ }
95
+
96
+ /// Route a sum batch: rayon direct-write when the workload justifies it, else a
97
+ /// zero-alloc serial pass. Identical `[u32 count][i64…]` output on both paths.
98
+ #[inline]
99
+ pub(crate) fn run_sum_batch(data: &[u8], f: impl Fn(&[u8]) -> i64 + Sync) -> Result<Vec<u8>> {
100
+ let (iter, parallel, items) = packed_routing(data)?;
101
+ let count = iter.len();
102
+ if parallel {
103
+ let mut out = Vec::with_capacity(4 + count * 8);
104
+ sum_batch_direct(&items, &mut out, f);
105
+ Ok(out)
106
+ } else {
107
+ sum_batch_serial(data, f)
108
+ }
109
+ }
110
+
111
+ /// Zero-alloc serial bitset over a packed buffer — no `Vec<&[u8]>` materialization.
112
+ #[inline]
113
+ pub(crate) fn bitset_serial(data: &[u8], f: impl Fn(&[u8]) -> bool) -> Result<Vec<u8>> {
114
+ let iter = PackedIter::new(data)?;
115
+ let count = iter.len();
116
+ let bitset_len = count.div_ceil(8);
117
+ let mut out = Vec::with_capacity(4 + bitset_len);
118
+ out.extend_from_slice(&(count as u32).to_le_bytes());
119
+ out.resize(4 + bitset_len, 0);
120
+ for (i, item) in iter.enumerate() {
121
+ if f(item) {
122
+ out[4 + (i >> 3)] |= 1 << (i & 7);
123
+ }
124
+ }
125
+ Ok(out)
126
+ }
127
+
128
+ /// Route a bitset batch: rayon `validation_bitset_chunked` when the workload
129
+ /// justifies it, else a zero-alloc serial pass. Identical output on both paths.
130
+ #[inline]
131
+ pub(crate) fn run_bitset_batch(
132
+ data: &[u8],
133
+ f: impl Fn(&[u8]) -> bool + Sync,
134
+ chunk_items: usize,
135
+ ) -> Result<Vec<u8>> {
136
+ let (_iter, parallel, items) = packed_routing(data)?;
137
+ if parallel {
138
+ Ok(crate::util::validation_bitset_chunked(
139
+ &items,
140
+ f,
141
+ chunk_items,
142
+ ))
143
+ } else {
144
+ bitset_serial(data, f)
145
+ }
146
+ }
147
+
148
+ /// Serial byte-map over a packed buffer — writes `[u32 len][bytes]` per item
149
+ /// directly, skipping the `unpack` Vec. Single computation per item.
150
+ #[inline]
151
+ pub(crate) fn bytes_map_serial(data: &[u8], f: impl Fn(&[u8]) -> Vec<u8>) -> Result<Vec<u8>> {
152
+ let iter = PackedIter::new(data)?;
153
+ let count = iter.len();
154
+ let mut out = Vec::with_capacity(4 + count * 32);
155
+ out.extend_from_slice(&(count as u32).to_le_bytes());
156
+ for item in iter {
157
+ let r = f(item);
158
+ out.extend_from_slice(&(r.len() as u32).to_le_bytes());
159
+ out.extend_from_slice(&r);
160
+ }
161
+ Ok(out)
162
+ }
163
+
164
+ /// Generic packed→packed batch: `[u32 count] { [u32 len][bytes] }` in →
165
+ /// `[u32 count] { [u32 len][result-bytes] }` out.
166
+ ///
167
+ /// The serial path iterates the packed input directly (zero-alloc — no `unpack`
168
+ /// Vec); the rayon path materializes `Vec<&[u8]>` (owned `Send` slices are
169
+ /// required for parallel work) and collects per-item results. Both paths write
170
+ /// byte-identical output, so callers can't tell which path ran. A mapper that
171
+ /// cannot produce a result for an item should return an empty `Vec` so the item
172
+ /// count stays aligned — feature modules own that policy (e.g. `unwrap_or_default`).
173
+ #[inline]
174
+ pub(crate) fn run_packed_batch(
175
+ data: &[u8],
176
+ f: impl Fn(&[u8]) -> Vec<u8> + Sync,
177
+ ) -> Result<Vec<u8>> {
178
+ let (iter, parallel, items) = packed_routing(data)?;
179
+ let count = iter.len();
180
+ let mut out = Vec::with_capacity(4 + count * 32);
181
+ out.extend_from_slice(&(count as u32).to_le_bytes());
182
+ if parallel {
183
+ use rayon::prelude::*;
184
+ let results: Vec<Vec<u8>> = items.par_iter().map(|c| f(c)).collect();
185
+ for r in results {
186
+ out.extend_from_slice(&(r.len() as u32).to_le_bytes());
187
+ out.extend_from_slice(&r);
188
+ }
189
+ } else {
190
+ for item in iter {
191
+ let r = f(item);
192
+ out.extend_from_slice(&(r.len() as u32).to_le_bytes());
193
+ out.extend_from_slice(&r);
194
+ }
195
+ }
196
+ Ok(out)
197
+ }
198
+
199
+ /// Indexed variant of [`run_packed_batch`] for mappers that need the item index
200
+ /// (e.g. AEAD batch nonce derivation: nonce = base XOR idx).
201
+ #[inline]
202
+ pub(crate) fn run_packed_batch_idx(
203
+ data: &[u8],
204
+ f: impl Fn(&[u8], usize) -> Vec<u8> + Sync,
205
+ ) -> Result<Vec<u8>> {
206
+ let (iter, parallel, items) = packed_routing(data)?;
207
+ let count = iter.len();
208
+ let mut out = Vec::with_capacity(4 + count * 32);
209
+ out.extend_from_slice(&(count as u32).to_le_bytes());
210
+ if parallel {
211
+ use rayon::prelude::*;
212
+ let results: Vec<Vec<u8>> = items.par_iter().enumerate().map(|(i, c)| f(c, i)).collect();
213
+ for r in results {
214
+ out.extend_from_slice(&(r.len() as u32).to_le_bytes());
215
+ out.extend_from_slice(&r);
216
+ }
217
+ } else {
218
+ for (i, item) in iter.enumerate() {
219
+ let r = f(item, i);
220
+ out.extend_from_slice(&(r.len() as u32).to_le_bytes());
221
+ out.extend_from_slice(&r);
222
+ }
223
+ }
224
+ Ok(out)
225
+ }
226
+
227
+ /// Packed HMAC-SHA256 sign batch (hex signatures out) — key compiled once.
228
+ #[inline]
229
+ pub(crate) fn hmac_sha256_batch_bytes(data: &[u8], key: &aws_lc_rs::hmac::Key) -> Result<Vec<u8>> {
230
+ bytes_map_serial(data, |v| {
231
+ let tag = aws_lc_rs::hmac::sign(key, v);
232
+ let mut sig = [0u8; 64];
233
+ crate::util::bytes::hex_encode_32(tag.as_ref(), &mut sig);
234
+ sig.to_vec()
235
+ })
236
+ }
237
+
238
+ /// Packed HMAC-SHA256 verify batch (two packed lists: data + hex sigs, zipped)
239
+ /// → bitset. Key compiled once.
240
+ #[inline]
241
+ pub(crate) fn hmac_sha256_verify_batch_bytes(
242
+ data: &[u8],
243
+ sigs: &[u8],
244
+ key: &aws_lc_rs::hmac::Key,
245
+ ) -> Result<Vec<u8>> {
246
+ let count = PackedIter::new(data)?
247
+ .len()
248
+ .min(PackedIter::new(sigs)?.len());
249
+ let mut out = Vec::with_capacity(4 + count.div_ceil(8));
250
+ out.extend_from_slice(&(count as u32).to_le_bytes());
251
+ out.resize(4 + count.div_ceil(8), 0);
252
+ for (i, (item, sig)) in PackedIter::new(data)?
253
+ .zip(PackedIter::new(sigs)?)
254
+ .enumerate()
255
+ {
256
+ let sig = crate::util::bytes::trim_ascii_whitespace(sig);
257
+ if let Some(sig_bytes) = crate::util::bytes::hex_decode_32(sig) {
258
+ if aws_lc_rs::hmac::verify(key, item, &sig_bytes).is_ok() {
259
+ out[4 + (i >> 3)] |= 1 << (i & 7);
260
+ }
261
+ }
262
+ }
263
+ Ok(out)
264
+ }
265
+
266
+ #[cfg(test)]
267
+ #[inline]
268
+ pub(crate) fn query_parse_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
269
+ crate::http::query_parser::query_parse_packed_vec(input)
270
+ }
271
+
272
+ #[inline]
273
+ fn http_parse_request_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
274
+ crate::http::http_parser::http_parse_request_packed_vec(input)
275
+ }
276
+
277
+ /// Packed sign-cookie batch (signed cookie bytes out) — key compiled once.
278
+ #[inline]
279
+ pub(crate) fn sign_cookie_batch_bytes(data: &[u8], secret: &[u8]) -> Result<Vec<u8>> {
280
+ let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret);
281
+ bytes_map_serial(data, |v| {
282
+ crate::crypto::cookie_sign::sign_cookie_bytes(v, &key)
283
+ })
284
+ }
285
+
286
+ #[inline]
287
+ pub(crate) fn count_batch_data(
288
+ data: &[u8],
289
+ f: impl Fn(&[u8]) -> bool + Sync,
290
+ chunk_items: usize,
291
+ ) -> Result<u32> {
292
+ let (iter, parallel, items) = packed_routing(data)?;
293
+ if parallel {
294
+ return Ok(count_batch(&items, f, chunk_items) as u32);
295
+ }
296
+ let mut n = 0u32;
297
+ for item in iter {
298
+ if f(item) {
299
+ n += 1;
300
+ }
301
+ }
302
+ Ok(n)
303
+ }
304
+
305
+ #[inline]
306
+ pub(crate) fn sum_ids_batch_data(data: &[u8], chunk_items: usize) -> Result<i64> {
307
+ let (iter, parallel, items) = packed_routing(data)?;
308
+ if parallel {
309
+ return Ok(sum_batch_i64(
310
+ &items,
311
+ |item| json_sum_ids_bytes(item).unwrap_or(0),
312
+ chunk_items,
313
+ ));
314
+ }
315
+ let mut total = 0i64;
316
+ for item in iter {
317
+ total = total.saturating_add(json_sum_ids_bytes(item).unwrap_or(0));
318
+ }
319
+ Ok(total)
320
+ }
321
+
322
+ #[inline]
323
+ pub(crate) fn total_len_batch_data(
324
+ data: &[u8],
325
+ f: impl Fn(&[u8]) -> Result<Vec<u8>> + Sync,
326
+ chunk_items: usize,
327
+ ) -> Result<u32> {
328
+ let (iter, parallel, items) = packed_routing(data)?;
329
+ if parallel {
330
+ let chunk_items = chunk_items.max(64);
331
+ use rayon::prelude::*;
332
+ let total: usize = items
333
+ .par_chunks(chunk_items)
334
+ .map(|chunk| {
335
+ chunk
336
+ .iter()
337
+ .map(|item| f(item).map(|v| v.len()).unwrap_or(0))
338
+ .sum::<usize>()
339
+ })
340
+ .sum();
341
+ return Ok(total as u32);
342
+ }
343
+ let mut total = 0usize;
344
+ for item in iter {
345
+ total += f(item).map(|v| v.len()).unwrap_or(0);
346
+ }
347
+ Ok(total as u32)
348
+ }
349
+
350
+ // ── Packed input → packed output (domain helpers) ──
351
+
352
+ #[inline]
353
+ pub(crate) fn json_valid_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
354
+ run_bitset_batch(data, json_valid_bytes, 512)
355
+ }
356
+
357
+ #[inline]
358
+ pub(crate) fn validate_email_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
359
+ run_bitset_batch(data, validate_email_bytes, 4096)
360
+ }
361
+
362
+ #[inline]
363
+ pub(crate) fn validate_uuid_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
364
+ run_bitset_batch(data, validate_uuid_bytes, 4096)
365
+ }
366
+
367
+ #[inline]
368
+ pub(crate) fn validate_ipv4_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
369
+ run_bitset_batch(data, validate_ipv4_bytes, 4096)
370
+ }
371
+
372
+ #[inline]
373
+ pub(crate) fn validate_ipv6_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
374
+ run_bitset_batch(data, validate_ipv6_bytes, 4096)
375
+ }
376
+
377
+ #[inline]
378
+ pub(crate) fn json_sum_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
379
+ run_sum_batch(data, |item| json_sum_ids_bytes(item).unwrap_or(0))
380
+ }
381
+
382
+ /// Shared scratch writer for the packed-pairs parse batches (query/cookie/form).
383
+ ///
384
+ /// Each item parses via its `*_into_slice` writer into ONE reused scratch buffer
385
+ /// (grown to the worst-case `9·len + 16` bound) instead of allocating a fresh
386
+ /// `vec![0u8; 9·len + 16]` per item (which the allocating `*_packed_vec` helpers
387
+ /// do). A malformed item emits an empty result frame — byte-parity with the
388
+ /// old `*_packed_vec(...).unwrap_or_default()` behavior.
389
+ #[inline]
390
+ pub(crate) fn parse_pairs_batch(
391
+ data: &[u8],
392
+ parse: impl Fn(&[u8], &mut [u8]) -> Result<usize>,
393
+ ) -> Result<Vec<u8>> {
394
+ let iter = PackedIter::new(data)?;
395
+ let count = iter.len();
396
+ let mut out = Vec::with_capacity(4 + count * 32);
397
+ out.extend_from_slice(&(count as u32).to_le_bytes());
398
+ let mut scratch: Vec<u8> = Vec::new();
399
+ for item in iter {
400
+ let cap = item.len().saturating_mul(9).saturating_add(16);
401
+ if scratch.len() < cap {
402
+ scratch.resize(cap, 0);
403
+ }
404
+ match parse(item, &mut scratch) {
405
+ Ok(written) => {
406
+ out.extend_from_slice(&(written as u32).to_le_bytes());
407
+ out.extend_from_slice(&scratch[..written]);
408
+ }
409
+ // Malformed %XX → empty result frame (parity with `unwrap_or_default`).
410
+ Err(_) => out.extend_from_slice(&0u32.to_le_bytes()),
411
+ }
412
+ }
413
+ Ok(out)
414
+ }
415
+
416
+ #[inline]
417
+ pub(crate) fn query_parse_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
418
+ parse_pairs_batch(
419
+ data,
420
+ crate::http::query_parser::query_parse_packed_into_slice,
421
+ )
422
+ }
423
+
424
+ #[inline]
425
+ pub(crate) fn cookie_parse_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
426
+ parse_pairs_batch(
427
+ data,
428
+ crate::http::cookie_parser::cookie_parse_packed_into_slice,
429
+ )
430
+ }
431
+
432
+ #[inline]
433
+ pub(crate) fn form_parse_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
434
+ parse_pairs_batch(
435
+ data,
436
+ crate::http::query_parser::query_parse_packed_into_slice,
437
+ )
438
+ }
439
+
440
+ #[inline]
441
+ pub(crate) fn http_parse_request_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
442
+ bytes_map_serial(data, |v| {
443
+ http_parse_request_packed_vec(v).unwrap_or_default()
444
+ })
445
+ }
446
+
447
+ #[inline]
448
+ pub(crate) fn hex_encode_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
449
+ // Direct-write: hex output size is deterministic (2x input), so write each
450
+ // item straight into the shared output Vec — no per-item String/Vec alloc.
451
+ // The nibble loop lives in `util::bytes::hex_encode` (single source).
452
+ let iter = PackedIter::new(data)?;
453
+ let count = iter.len();
454
+ let mut out = Vec::with_capacity(4 + count * 32);
455
+ out.extend_from_slice(&(count as u32).to_le_bytes());
456
+ for item in iter {
457
+ let out_len = item.len() * 2;
458
+ out.extend_from_slice(&(out_len as u32).to_le_bytes());
459
+ let start = out.len();
460
+ out.resize(start + out_len, 0);
461
+ crate::util::bytes::hex_encode(item, &mut out[start..start + out_len]);
462
+ }
463
+ Ok(out)
464
+ }
465
+
466
+ #[inline]
467
+ pub(crate) fn hex_decode_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
468
+ bytes_map_serial(data, |v| {
469
+ crate::crypto::base64::hex_decode_bytes(v)
470
+ .map_err(|e| Error::from_reason(e.to_string()))
471
+ .unwrap_or_default()
472
+ })
473
+ }
@@ -0,0 +1,23 @@
1
+ //! util/batch/ — packed aggregate batch helpers.
2
+ //!
3
+ //! The packed-wire batch machinery used by the `*_batch_packed` napi APIs and
4
+ //! the C-ABI `castrum_*_batch_packed` exports:
5
+ //! - `core.rs` — generic packed-buffer routing (serial zero-alloc vs rayon
6
+ //! direct-write) for the sum/bitset/bytes-map/count/total-len shapes, plus
7
+ //! the one-off domain `*_bytes` helpers they wire. The rayon branch
8
+ //! delegates to the generic `util::batch_core` helpers.
9
+ //! - `api.rs` — the thin `#[napi]` boundary over `core.rs`.
10
+ //! - `tests.rs` — serial/parallel parity + `_into` byte-parity tests.
11
+ //!
12
+ //! Public names are re-exported here so existing call sites keep working
13
+ //! unchanged (`crate::util::batch::*` and the `crate::util::run_*` shims in
14
+ //! `util/mod.rs`).
15
+
16
+ mod api;
17
+ mod core;
18
+
19
+ #[cfg(test)]
20
+ mod tests;
21
+
22
+ pub use self::api::*;
23
+ pub(crate) use self::core::*;