castrum 0.9.2 → 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/packed.rs — Zero-alloc packed-wire iterators + byte writers.
2
+ //
3
+ // The canonical implementation of the packed batch format
4
+ // (`[u32 count] { [u32 len] [bytes] }`) and the low-level byte writers used by
5
+ // the parser/output modules. Everything here is allocation-free apart from the
6
+ // explicitly-allocating compat helper (`unpack`).
7
+
8
+ use napi::bindgen_prelude::Uint8Array;
9
+ use napi::{Error, Result};
10
+
11
+ // ── Zero-Alloc Packed Iterator ─────────────────────────────────────
12
+
13
+ /// A zero-allocation iterator over packed batch buffers.
14
+ /// Format: [u32 count] repeated: [u32 len] [bytes]
15
+ #[derive(Clone, Copy)]
16
+ pub struct PackedIter<'a> {
17
+ data: &'a [u8],
18
+ offset: usize,
19
+ count: usize,
20
+ }
21
+
22
+ impl<'a> PackedIter<'a> {
23
+ /// Create a new `PackedIter` from a packed buffer.
24
+ /// Returns `None` if the buffer is malformed (too short, count impossible).
25
+ #[inline]
26
+ pub fn new(data: &'a [u8]) -> Result<Self> {
27
+ if data.len() < 4 {
28
+ return Err(Error::from_reason("packed buffer: missing count"));
29
+ }
30
+
31
+ let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
32
+
33
+ // Validate count is plausible
34
+ let max_possible = (data.len() - 4) / 4;
35
+ if count > max_possible {
36
+ return Err(Error::from_reason("packed buffer: impossible item count"));
37
+ }
38
+
39
+ Ok(Self {
40
+ data,
41
+ offset: 4,
42
+ count,
43
+ })
44
+ }
45
+
46
+ /// Total number of items (from the header).
47
+ #[inline(always)]
48
+ pub fn len(&self) -> usize {
49
+ self.count
50
+ }
51
+
52
+ #[inline(always)]
53
+ pub fn is_empty(&self) -> bool {
54
+ self.count == 0
55
+ }
56
+
57
+ /// Collect into a `Vec<&[u8]>` (allocating, for compatibility with existing code).
58
+ #[inline]
59
+ pub fn collect_vec(&self) -> Result<Vec<&'a [u8]>> {
60
+ let mut items = Vec::with_capacity(self.count);
61
+ let mut offset = 4usize;
62
+ for _ in 0..self.count {
63
+ if offset + 4 > self.data.len() {
64
+ return Err(Error::from_reason("packed buffer: truncated length"));
65
+ }
66
+ let len = u32::from_le_bytes([
67
+ self.data[offset],
68
+ self.data[offset + 1],
69
+ self.data[offset + 2],
70
+ self.data[offset + 3],
71
+ ]) as usize;
72
+ offset += 4;
73
+ if offset + len > self.data.len() {
74
+ return Err(Error::from_reason("packed buffer: truncated item"));
75
+ }
76
+ items.push(&self.data[offset..offset + len]);
77
+ offset += len;
78
+ }
79
+ Ok(items)
80
+ }
81
+
82
+ /// One-pass, zero-alloc statistics: `(item_count, total_payload_bytes)`.
83
+ ///
84
+ /// Reads the item-count header and scans the per-item length fields
85
+ /// WITHOUT materializing the item slices (unlike [`Self::collect_vec`]).
86
+ /// Used by the batch dispatchers to decide serial-vs-parallel execution.
87
+ #[inline]
88
+ pub fn count_and_total_bytes(&self) -> Result<(usize, usize)> {
89
+ let mut offset = 4usize;
90
+ let mut total = 0usize;
91
+ for _ in 0..self.count {
92
+ if offset + 4 > self.data.len() {
93
+ return Err(Error::from_reason("packed buffer: truncated length"));
94
+ }
95
+ let len = u32::from_le_bytes([
96
+ self.data[offset],
97
+ self.data[offset + 1],
98
+ self.data[offset + 2],
99
+ self.data[offset + 3],
100
+ ]) as usize;
101
+ offset += 4;
102
+ if offset + len > self.data.len() {
103
+ return Err(Error::from_reason("packed buffer: truncated item"));
104
+ }
105
+ offset += len;
106
+ total += len;
107
+ }
108
+ Ok((self.count, total))
109
+ }
110
+ }
111
+
112
+ impl<'a> Iterator for PackedIter<'a> {
113
+ type Item = &'a [u8];
114
+
115
+ #[inline]
116
+ fn next(&mut self) -> Option<Self::Item> {
117
+ if self.offset >= self.data.len() || self.count == 0 {
118
+ return None;
119
+ }
120
+
121
+ if self.offset + 4 > self.data.len() {
122
+ return None;
123
+ }
124
+
125
+ let len = u32::from_le_bytes([
126
+ self.data[self.offset],
127
+ self.data[self.offset + 1],
128
+ self.data[self.offset + 2],
129
+ self.data[self.offset + 3],
130
+ ]) as usize;
131
+
132
+ self.offset += 4;
133
+
134
+ if self.offset + len > self.data.len() {
135
+ return None;
136
+ }
137
+
138
+ let item = &self.data[self.offset..self.offset + len];
139
+ self.offset += len;
140
+ self.count -= 1;
141
+
142
+ Some(item)
143
+ }
144
+
145
+ #[inline]
146
+ fn size_hint(&self) -> (usize, Option<usize>) {
147
+ (self.count, Some(self.count))
148
+ }
149
+ }
150
+
151
+ impl<'a> ExactSizeIterator for PackedIter<'a> {}
152
+
153
+ // ── Legacy unpack (wraps PackedIter for backwards compat) ──────────
154
+
155
+ /// Packed batch input format:
156
+ /// [u32 count]
157
+ /// repeated count times:
158
+ /// [u32 byte_length]
159
+ /// [bytes]
160
+ #[inline]
161
+ pub fn unpack(data: &[u8]) -> Result<Vec<&[u8]>> {
162
+ PackedIter::new(data)?.collect_vec()
163
+ }
164
+
165
+ #[inline(always)]
166
+ pub fn total_bytes(items: &[&[u8]]) -> usize {
167
+ items.iter().map(|x| x.len()).sum()
168
+ }
169
+
170
+ #[inline(always)]
171
+ pub fn ensure_capacity(out: &[u8], pos: usize, additional: usize) -> Result<()> {
172
+ let end = pos
173
+ .checked_add(additional)
174
+ .ok_or_else(|| Error::from_reason("packed output: overflow"))?;
175
+
176
+ if end > out.len() {
177
+ return Err(Error::from_reason("packed output: buffer too small"));
178
+ }
179
+
180
+ Ok(())
181
+ }
182
+
183
+ #[inline(always)]
184
+ pub fn write_u32_le(out: &mut [u8], pos: &mut usize, value: u32) -> Result<()> {
185
+ ensure_capacity(out, *pos, 4)?;
186
+ out[*pos..*pos + 4].copy_from_slice(&value.to_le_bytes());
187
+ *pos += 4;
188
+ Ok(())
189
+ }
190
+
191
+ #[inline(always)]
192
+ pub fn write_bytes(out: &mut [u8], pos: &mut usize, bytes: &[u8]) -> Result<()> {
193
+ ensure_capacity(out, *pos, bytes.len())?;
194
+ out[*pos..*pos + bytes.len()].copy_from_slice(bytes);
195
+ *pos += bytes.len();
196
+ Ok(())
197
+ }
198
+
199
+ #[inline(always)]
200
+ pub fn write_bytes_lowercase(out: &mut [u8], pos: &mut usize, bytes: &[u8]) -> Result<()> {
201
+ ensure_capacity(out, *pos, bytes.len())?;
202
+ // One pass: lowercase each byte as it is written (no copy-then-lowercase).
203
+ for (i, &b) in bytes.iter().enumerate() {
204
+ out[*pos + i] = b.to_ascii_lowercase();
205
+ }
206
+ *pos += bytes.len();
207
+ Ok(())
208
+ }
209
+
210
+ #[inline(always)]
211
+ pub fn slices_overlap(a: &[u8], b: &[u8]) -> bool {
212
+ if a.is_empty() || b.is_empty() {
213
+ return false;
214
+ }
215
+
216
+ let a_start = a.as_ptr() as usize;
217
+ let b_start = b.as_ptr() as usize;
218
+ let a_end = a_start.saturating_add(a.len());
219
+ let b_end = b_start.saturating_add(b.len());
220
+
221
+ a_start < b_end && b_start < a_end
222
+ }
223
+
224
+ #[inline]
225
+ pub fn run_packed_into<F>(input: &Uint8Array, output: &mut Uint8Array, f: F) -> Result<u32>
226
+ where
227
+ F: FnOnce(&[u8], &mut [u8]) -> Result<usize>,
228
+ {
229
+ let input_bytes = input.as_ref();
230
+
231
+ let overlaps = {
232
+ let output_bytes = output.as_ref();
233
+ slices_overlap(input_bytes, output_bytes)
234
+ };
235
+
236
+ let written = if overlaps {
237
+ let owned_input = input_bytes.to_vec();
238
+ // SAFETY: `output.as_mut()` yields a mutable slice over the napi
239
+ // Uint8Array's backing store. This is sound because (1) no other
240
+ // reference to `output` is live while `f` runs, (2) the input was
241
+ // copied to `owned_input` when it could alias `output` (checked by
242
+ // `slices_overlap` above), and (3) `f`'s writers are capacity-checked
243
+ // (`ingress/output.rs` `write_*` panic instead of writing OOB).
244
+ unsafe { f(&owned_input, output.as_mut())? }
245
+ } else {
246
+ // SAFETY: as above — `input_bytes` is a shared borrow that does not
247
+ // alias `output` (verified by `slices_overlap`), so taking a mutable
248
+ // slice of `output` cannot create aliasing UB.
249
+ unsafe { f(input_bytes, output.as_mut())? }
250
+ };
251
+
252
+ Ok(written as u32)
253
+ }
254
+
255
+ // ── Bounds-checked packed OUTPUT writers (reusable-output `_into`) ──
256
+ //
257
+ // These write the same packed formats as the allocating batch functions but
258
+ // into a caller-provided `&mut [u8]`, returning the number of bytes written.
259
+ // Capacity is enforced via `write_u32_le`/`ensure_capacity` (Err "packed
260
+ // output: buffer too small" instead of an OOB write). The `_into` NAPI
261
+ // wrappers route through `run_packed_into` so input/output aliasing is
262
+ // detected and the input is copied when they overlap.
263
+
264
+ /// Write `[u32 count][bits…]` (batch bitset) into `out`. Returns bytes written.
265
+ #[inline]
266
+ pub fn write_bitset_batch_into<F>(data: &[u8], out: &mut [u8], f: F) -> Result<usize>
267
+ where
268
+ F: Fn(&[u8]) -> bool + Sync,
269
+ {
270
+ let iter = PackedIter::new(data)?;
271
+ let (count, total) = iter.count_and_total_bytes()?;
272
+ let bitset_len = count.div_ceil(8);
273
+ let mut pos = 0usize;
274
+ write_u32_le(out, &mut pos, count as u32)?;
275
+ ensure_capacity(out, pos, bitset_len)?;
276
+ if crate::util::should_parallelize(count, total) {
277
+ use rayon::prelude::*;
278
+ let items = crate::util::unpack(data)?;
279
+ // Write the bitset DIRECTLY into the caller's buffer — no intermediate
280
+ // `Vec<u8>` allocation + copy (mirrors `validation_bitset_chunked`'s
281
+ // parallel logic, but writes in place). Zero first (the pooled output
282
+ // buffer may hold stale bytes).
283
+ out[pos..pos + bitset_len].fill(0);
284
+ let bits = &mut out[pos..pos + bitset_len];
285
+ let chunk_bytes = (4096usize).div_ceil(8);
286
+ bits.par_chunks_mut(chunk_bytes)
287
+ .enumerate()
288
+ .for_each(|(chunk_idx, chunk)| {
289
+ let start_item = chunk_idx * chunk_bytes * 8;
290
+ let end_item = (start_item + chunk.len() * 8).min(count);
291
+ let start_byte = start_item / 8;
292
+ for i in start_item..end_item {
293
+ if f(items[i]) {
294
+ chunk[(i / 8) - start_byte] |= 1 << (i & 7);
295
+ }
296
+ }
297
+ });
298
+ } else {
299
+ // The pooled output buffer may hold stale bytes — zero the bits first.
300
+ out[pos..pos + bitset_len].fill(0);
301
+ for (i, item) in iter.enumerate() {
302
+ if f(item) {
303
+ out[pos + (i >> 3)] |= 1 << (i & 7);
304
+ }
305
+ }
306
+ }
307
+ Ok(pos + bitset_len)
308
+ }
309
+
310
+ /// Scalar `_into` writer bound: `T` can be written as a little-endian byte
311
+ /// array of `SIZE` bytes into a caller-provided slice.
312
+ pub(crate) trait ToLeBytes: Copy {
313
+ const SIZE: usize;
314
+ fn write_le(self, out: &mut [u8]);
315
+ }
316
+
317
+ impl ToLeBytes for i64 {
318
+ const SIZE: usize = 8;
319
+ #[inline]
320
+ fn write_le(self, out: &mut [u8]) {
321
+ out.copy_from_slice(&self.to_le_bytes());
322
+ }
323
+ }
324
+
325
+ impl ToLeBytes for u32 {
326
+ const SIZE: usize = 4;
327
+ #[inline]
328
+ fn write_le(self, out: &mut [u8]) {
329
+ out.copy_from_slice(&self.to_le_bytes());
330
+ }
331
+ }
332
+
333
+ /// Write `[u32 count][T…]` (batch scalar array: i64 sums, u32 hashes, …) into
334
+ /// `out`. Returns bytes written. Shared body of the scalar `_into` writers.
335
+ #[inline]
336
+ pub(crate) fn write_scalar_batch_into<T, F>(data: &[u8], out: &mut [u8], f: F) -> Result<usize>
337
+ where
338
+ T: ToLeBytes + Send,
339
+ F: Fn(&[u8]) -> T + Sync,
340
+ {
341
+ let iter = PackedIter::new(data)?;
342
+ let (count, total) = iter.count_and_total_bytes()?;
343
+ let mut pos = 0usize;
344
+ write_u32_le(out, &mut pos, count as u32)?;
345
+ let item_bytes = T::SIZE;
346
+ ensure_capacity(out, pos, count.saturating_mul(item_bytes))?;
347
+ if crate::util::should_parallelize(count, total) {
348
+ use rayon::prelude::*;
349
+ let items = crate::util::unpack(data)?;
350
+ let vals: Vec<T> = items.par_iter().map(|item| f(item)).collect();
351
+ for (i, v) in vals.iter().enumerate() {
352
+ v.write_le(&mut out[pos + i * item_bytes..pos + (i + 1) * item_bytes]);
353
+ }
354
+ } else {
355
+ for (i, item) in iter.enumerate() {
356
+ let v = f(item);
357
+ v.write_le(&mut out[pos + i * item_bytes..pos + (i + 1) * item_bytes]);
358
+ }
359
+ }
360
+ Ok(pos + count.saturating_mul(item_bytes))
361
+ }
362
+
363
+ /// Write `[u32 count][i64…]` (batch i64 array) into `out`. Returns bytes written.
364
+ #[inline]
365
+ pub fn write_sum_batch_into<F>(data: &[u8], out: &mut [u8], f: F) -> Result<usize>
366
+ where
367
+ F: Fn(&[u8]) -> i64 + Sync,
368
+ {
369
+ write_scalar_batch_into::<i64, F>(data, out, f)
370
+ }
371
+
372
+ /// Write `[u32 count][u32…]` (batch u32 array, e.g. crc32) into `out`.
373
+ /// Returns bytes written.
374
+ #[inline]
375
+ pub fn write_u32_batch_into<F>(data: &[u8], out: &mut [u8], f: F) -> Result<usize>
376
+ where
377
+ F: Fn(&[u8]) -> u32 + Sync,
378
+ {
379
+ write_scalar_batch_into::<u32, F>(data, out, f)
380
+ }
381
+
382
+ #[cfg(test)]
383
+ mod tests {
384
+ use super::*;
385
+
386
+ fn packed_from(items: &[&[u8]]) -> Vec<u8> {
387
+ let mut out = Vec::new();
388
+ out.extend_from_slice(&(items.len() as u32).to_le_bytes());
389
+ for item in items {
390
+ out.extend_from_slice(&(item.len() as u32).to_le_bytes());
391
+ out.extend_from_slice(item);
392
+ }
393
+ out
394
+ }
395
+
396
+ #[test]
397
+ fn packed_iter_roundtrip() {
398
+ let buf = packed_from(&[b"a", b"", b"hello", b"w\x00\xff"]);
399
+ let iter = PackedIter::new(&buf).unwrap();
400
+ assert_eq!(iter.len(), 4);
401
+ let items: Vec<&[u8]> = iter.collect();
402
+ assert_eq!(
403
+ items,
404
+ vec![&b"a"[..], &b""[..], &b"hello"[..], &b"w\x00\xff"[..]]
405
+ );
406
+ }
407
+
408
+ #[test]
409
+ fn packed_iter_empty() {
410
+ let buf = packed_from(&[]);
411
+ let iter = PackedIter::new(&buf).unwrap();
412
+ assert!(iter.is_empty());
413
+ assert_eq!(iter.len(), 0);
414
+ assert!(iter.collect_vec().unwrap().is_empty());
415
+ assert!(unpack(&buf).unwrap().is_empty());
416
+ }
417
+
418
+ #[test]
419
+ fn packed_iter_missing_count_is_error() {
420
+ assert!(PackedIter::new(b"").is_err());
421
+ assert!(PackedIter::new(b"\x01\x02").is_err());
422
+ // Claimed count is impossible for a 4-byte-only buffer.
423
+ assert!(PackedIter::new(&[1, 0, 0, 0]).is_err());
424
+ }
425
+
426
+ #[test]
427
+ fn packed_iter_truncated_item() {
428
+ let mut buf = packed_from(&[b"abc", b"defg"]);
429
+ buf.truncate(buf.len() - 1); // cut into the last item
430
+ // The Iterator stops silently on a malformed tail…
431
+ let iter = PackedIter::new(&buf).unwrap();
432
+ let items: Vec<&[u8]> = iter.collect();
433
+ assert_eq!(items, vec![b"abc"]);
434
+ // …while the validating collect_vec/unpack report it.
435
+ assert!(unpack(&buf).is_err());
436
+ }
437
+
438
+ #[test]
439
+ fn write_helpers_capacity_checks() {
440
+ let mut out = [0u8; 8];
441
+ let mut pos = 0usize;
442
+ write_u32_le(&mut out, &mut pos, 7).unwrap();
443
+ write_bytes(&mut out, &mut pos, b"abcd").unwrap();
444
+ assert_eq!(pos, 8);
445
+ // One byte over the end → error, position unchanged.
446
+ assert!(write_bytes(&mut out, &mut pos, b"e").is_err());
447
+ assert_eq!(pos, 8);
448
+ // Overflow path.
449
+ assert!(ensure_capacity(&out, usize::MAX, 4).is_err());
450
+ // Lowercase writer is one-pass and capacity-checked.
451
+ let mut small = [0u8; 2];
452
+ assert!(write_bytes_lowercase(&mut small, &mut 0usize, b"abc").is_err());
453
+ let mut out2 = [0u8; 3];
454
+ let mut p2 = 0usize;
455
+ write_bytes_lowercase(&mut out2, &mut p2, b"AbC").unwrap();
456
+ assert_eq!(&out2, b"abc");
457
+ assert_eq!(p2, 3);
458
+ }
459
+
460
+ #[test]
461
+ fn slices_overlap_cases() {
462
+ let a = [1u8, 2, 3, 4];
463
+ let b = [5u8, 6];
464
+ assert!(!slices_overlap(&a, &b));
465
+ // Overlapping views of one backing buffer.
466
+ assert!(slices_overlap(&a[0..3], &a[2..4]));
467
+ assert!(slices_overlap(&a, &a[1..2]));
468
+ assert!(slices_overlap(&a[2..], &a[0..3]));
469
+ // Empty slices never overlap.
470
+ assert!(!slices_overlap(&a[0..0], &a));
471
+ assert!(!slices_overlap(&a, &b[0..0]));
472
+ }
473
+ }
@@ -0,0 +1,130 @@
1
+ // rust/util/text.rs — text utilities (pure core + napi entry points).
2
+ //
3
+ // Currently: JS-RegExp metacharacter escaping — the "turn untrusted user
4
+ // input into a literal substring pattern" utility every search endpoint
5
+ // re-implements (usually as a per-request `replace` chain + `new RegExp`
6
+ // compile). Escapes exactly the MDN escapeRegExp set:
7
+ // \ . * + ? ^ $ { } | ( ) [ ]
8
+ // `-` is only special inside character classes (which escaped-literal input
9
+ // never contains), so it is left untouched — matching the de-facto standard.
10
+
11
+ use napi_derive::napi;
12
+
13
+ /// The metacharacters escaped by [`regex_escape_into`] /
14
+ /// [`regex_escape_write`].
15
+ const REGEX_META: &[u8; 14] = b"\\.*+?^${}()|[]";
16
+
17
+ /// Append the RegExp-escaped form of `input` to `out`. ASCII-only transform
18
+ /// (backslash before metachar bytes) — multi-byte UTF-8 sequences pass
19
+ /// through byte-exact because no UTF-8 continuation byte is a metachar.
20
+ pub fn regex_escape_into(input: &[u8], out: &mut Vec<u8>) {
21
+ for &c in input {
22
+ if REGEX_META.contains(&c) {
23
+ out.push(b'\\');
24
+ }
25
+ out.push(c);
26
+ }
27
+ }
28
+
29
+ /// Exact escaped length (metachar count + input length) — lets callers size
30
+ /// output buffers without running the escape twice.
31
+ #[inline]
32
+ pub fn regex_escape_len(input: &[u8]) -> usize {
33
+ input.len() + input.iter().filter(|b| REGEX_META.contains(b)).count()
34
+ }
35
+
36
+ /// Write the escaped form of `input` into `out` (which must be at least
37
+ /// [`regex_escape_len`] bytes — the C-ABI needed-size path). Returns bytes
38
+ /// written.
39
+ pub fn regex_escape_write(input: &[u8], out: &mut [u8]) -> usize {
40
+ let mut pos = 0usize;
41
+ for &c in input {
42
+ if REGEX_META.contains(&c) {
43
+ out[pos] = b'\\';
44
+ pos += 1;
45
+ }
46
+ out[pos] = c;
47
+ pos += 1;
48
+ }
49
+ pos
50
+ }
51
+
52
+ /// Escape JS RegExp metacharacters in a string → a pattern that matches the
53
+ /// input literally inside `new RegExp(escaped)` (any flags).
54
+ ///
55
+ /// @example
56
+ /// ```ts
57
+ /// new RegExp(regexEscape(userInput), 'i') // safe literal substring match
58
+ /// ```
59
+ #[napi]
60
+ pub fn regex_escape(input: String) -> String {
61
+ let bytes = input.as_bytes();
62
+ let mut out = Vec::with_capacity(regex_escape_len(bytes));
63
+ regex_escape_into(bytes, &mut out);
64
+ // Escaping only inserts ASCII backslashes before ASCII bytes, so the
65
+ // result is valid UTF-8 whenever the input was.
66
+ String::from_utf8(out).unwrap_or(input)
67
+ }
68
+
69
+ #[cfg(test)]
70
+ mod tests {
71
+ use super::*;
72
+
73
+ #[test]
74
+ fn escapes_all_metacharacters() {
75
+ let mut out = Vec::new();
76
+ regex_escape_into(b".*+?^${}()|[]\\", &mut out);
77
+ assert_eq!(out, b"\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\");
78
+ }
79
+
80
+ #[test]
81
+ fn plain_text_passes_through() {
82
+ let mut out = Vec::new();
83
+ regex_escape_into(b"hello world 123", &mut out);
84
+ assert_eq!(out, b"hello world 123");
85
+ }
86
+
87
+ #[test]
88
+ fn hyphen_and_slash_untouched() {
89
+ let mut out = Vec::new();
90
+ regex_escape_into(b"a-b/c", &mut out);
91
+ assert_eq!(out, b"a-b/c");
92
+ }
93
+
94
+ #[test]
95
+ fn utf8_passes_through_byte_exact() {
96
+ let input = "héllo→世界";
97
+ let mut out = Vec::new();
98
+ regex_escape_into(input.as_bytes(), &mut out);
99
+ assert_eq!(String::from_utf8(out).expect("utf8"), input);
100
+ }
101
+
102
+ #[test]
103
+ fn len_matches_written() {
104
+ for case in ["", "plain", "a.b*(c)[d]{e}|f^g$h?i\\j"] {
105
+ let mut out = Vec::new();
106
+ regex_escape_into(case.as_bytes(), &mut out);
107
+ assert_eq!(regex_escape_len(case.as_bytes()), out.len());
108
+ }
109
+ }
110
+
111
+ #[test]
112
+ fn write_matches_into() {
113
+ let case = "a.b*(c)[d]{e}|f^g$h?i\\j";
114
+ let mut pushed = Vec::new();
115
+ regex_escape_into(case.as_bytes(), &mut pushed);
116
+ let mut buf = vec![0u8; regex_escape_len(case.as_bytes())];
117
+ let written = regex_escape_write(case.as_bytes(), &mut buf);
118
+ assert_eq!(&buf[..written], &pushed[..]);
119
+ }
120
+
121
+ #[test]
122
+ fn escaped_pattern_matches_input_literally() {
123
+ // Round-trip through a tiny backtracking matcher is overkill; instead
124
+ // verify the classic injection is defused: "." would match anything.
125
+ let mut out = Vec::new();
126
+ regex_escape_into(b"a.c", &mut out);
127
+ let escaped = String::from_utf8(out).expect("utf8");
128
+ assert_eq!(escaped, r"a\.c");
129
+ }
130
+ }