castrum 0.7.0

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 (86) hide show
  1. package/Cargo.toml +73 -0
  2. package/README.md +432 -0
  3. package/bench.ts +3 -0
  4. package/castrum.linux-x64-gnu.node +0 -0
  5. package/index.ts +24 -0
  6. package/package.json +94 -0
  7. package/rust/batch.rs +195 -0
  8. package/rust/cookie_parser.rs +70 -0
  9. package/rust/cors.rs +263 -0
  10. package/rust/hashing.rs +164 -0
  11. package/rust/headers.rs +234 -0
  12. package/rust/hmac_sha256.rs +113 -0
  13. package/rust/http_parser.rs +132 -0
  14. package/rust/ingress.rs +616 -0
  15. package/rust/ingress_constants.rs +89 -0
  16. package/rust/ip_trust.rs +185 -0
  17. package/rust/json_ops.rs +66 -0
  18. package/rust/json_patch_ops.rs +25 -0
  19. package/rust/json_schema.rs +151 -0
  20. package/rust/json_ser.rs +429 -0
  21. package/rust/lib.rs +48 -0
  22. package/rust/method.rs +141 -0
  23. package/rust/mime_lookup.rs +76 -0
  24. package/rust/output.rs +238 -0
  25. package/rust/proxy.rs +90 -0
  26. package/rust/query_parser.rs +141 -0
  27. package/rust/random_token.rs +52 -0
  28. package/rust/rate_limit.rs +203 -0
  29. package/rust/terminal.rs +95 -0
  30. package/rust/unit_tests.rs +843 -0
  31. package/rust/url_codec.rs +133 -0
  32. package/rust/util.rs +537 -0
  33. package/rust/validation.rs +107 -0
  34. package/rust/websocket.rs +23 -0
  35. package/src/baseline/index.ts +12 -0
  36. package/src/baseline/tasks/cookie.ts +22 -0
  37. package/src/baseline/tasks/hashing.ts +18 -0
  38. package/src/baseline/tasks/hmac.ts +38 -0
  39. package/src/baseline/tasks/http.ts +62 -0
  40. package/src/baseline/tasks/json-patch.ts +12 -0
  41. package/src/baseline/tasks/json.ts +50 -0
  42. package/src/baseline/tasks/mime.ts +5 -0
  43. package/src/baseline/tasks/query.ts +14 -0
  44. package/src/baseline/tasks/token.ts +15 -0
  45. package/src/baseline/tasks/url.ts +11 -0
  46. package/src/baseline/tasks/validation.ts +19 -0
  47. package/src/baseline/tasks/websocket.ts +14 -0
  48. package/src/bench/assert.ts +70 -0
  49. package/src/bench/checks.ts +227 -0
  50. package/src/bench/checksum.ts +27 -0
  51. package/src/bench/comparisons.ts +70 -0
  52. package/src/bench/concurrent-worker.ts +135 -0
  53. package/src/bench/fixtures.ts +233 -0
  54. package/src/bench/index.ts +2 -0
  55. package/src/bench/measure.ts +177 -0
  56. package/src/bench/now.ts +3 -0
  57. package/src/bench/report.ts +79 -0
  58. package/src/bench/run.ts +57 -0
  59. package/src/bench/tasks/complex.ts +232 -0
  60. package/src/bench/tasks/concurrent.ts +213 -0
  61. package/src/bench/tasks/cookie.ts +44 -0
  62. package/src/bench/tasks/hashing.ts +33 -0
  63. package/src/bench/tasks/hmac.ts +36 -0
  64. package/src/bench/tasks/http.ts +50 -0
  65. package/src/bench/tasks/index.ts +52 -0
  66. package/src/bench/tasks/json-patch.ts +21 -0
  67. package/src/bench/tasks/json.ts +33 -0
  68. package/src/bench/tasks/mime.ts +21 -0
  69. package/src/bench/tasks/query.ts +44 -0
  70. package/src/bench/tasks/stress.ts +142 -0
  71. package/src/bench/tasks/token.ts +20 -0
  72. package/src/bench/tasks/url.ts +34 -0
  73. package/src/bench/tasks/validation.ts +57 -0
  74. package/src/bench/tasks/websocket.ts +21 -0
  75. package/src/bench/types.ts +60 -0
  76. package/src/data/json-rows.ts +33 -0
  77. package/src/ingress/constants.ts +56 -0
  78. package/src/ingress/fast.ts +1082 -0
  79. package/src/ingress/handlers.ts +1202 -0
  80. package/src/ingress/index.ts +457 -0
  81. package/src/ingress/packed-input.ts +59 -0
  82. package/src/native/index.ts +207 -0
  83. package/src/rust-ffi/index.ts +555 -0
  84. package/src/shared/bytes.ts +36 -0
  85. package/src/shared/json.ts +18 -0
  86. package/src/shared/packed.ts +316 -0
@@ -0,0 +1,133 @@
1
+ // rust/url_codec.rs — v2: batch 3-byte %XX writes
2
+ use napi::bindgen_prelude::*;
3
+ use napi_derive::napi;
4
+ use memchr::memchr;
5
+ use crate::util::{ensure_capacity, hex_val, write_bytes};
6
+
7
+ const HEX_UPPER: &[u8; 16] = b"0123456789ABCDEF";
8
+
9
+ #[inline(always)]
10
+ fn is_unreserved(b: u8) -> bool {
11
+ matches!(b,
12
+ b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
13
+ | b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | 0x27 | b'(' | b')'
14
+ )
15
+ }
16
+
17
+ #[napi]
18
+ pub fn url_encode(input: Uint8Array) -> Buffer {
19
+ let input = input.as_ref();
20
+
21
+ // Worst case: every byte is encoded → 3x. Use len + len/4 + 8 for typical.
22
+ let mut out = Vec::with_capacity(input.len() + input.len() / 4 + 8);
23
+ let mut start = 0usize;
24
+
25
+ for (i, &b) in input.iter().enumerate() {
26
+ if !is_unreserved(b) {
27
+ out.extend_from_slice(&input[start..i]);
28
+ // ⭐ Batch the 3-byte %XX write — one extend instead of 3 pushes.
29
+ let mut encoded = [b'%', 0, 0];
30
+ encoded[1] = HEX_UPPER[(b >> 4) as usize];
31
+ encoded[2] = HEX_UPPER[(b & 0x0f) as usize];
32
+ out.extend_from_slice(&encoded);
33
+ start = i + 1;
34
+ }
35
+ }
36
+
37
+ out.extend_from_slice(&input[start..]);
38
+ Buffer::from(out)
39
+ }
40
+
41
+ #[napi]
42
+ pub fn url_decode(input: Uint8Array) -> Result<Buffer> {
43
+ let out = url_decode_bytes_vec(input.as_ref())?;
44
+ simdutf8::basic::from_utf8(&out)
45
+ .map_err(|e| Error::from_reason(e.to_string()))?;
46
+ Ok(Buffer::from(out))
47
+ }
48
+
49
+ pub fn url_encode_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
50
+ let mut pos = 0usize;
51
+ let mut start = 0usize;
52
+
53
+ for (i, &b) in input.iter().enumerate() {
54
+ if !is_unreserved(b) {
55
+ write_bytes(out, &mut pos, &input[start..i])?;
56
+ ensure_capacity(out, pos, 3)?;
57
+ out[pos] = b'%';
58
+ out[pos + 1] = HEX_UPPER[(b >> 4) as usize];
59
+ out[pos + 2] = HEX_UPPER[(b & 0x0f) as usize];
60
+ pos += 3;
61
+ start = i + 1;
62
+ }
63
+ }
64
+
65
+ write_bytes(out, &mut pos, &input[start..])?;
66
+ Ok(pos)
67
+ }
68
+
69
+ #[napi]
70
+ pub fn url_encode_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
71
+ crate::util::run_packed_into(&input, &mut output, url_encode_into_slice)
72
+ }
73
+
74
+ #[inline]
75
+ fn url_decode_bytes_vec(input: &[u8]) -> Result<Vec<u8>> {
76
+ let mut out = Vec::with_capacity(input.len());
77
+ let mut pos = 0usize;
78
+
79
+ while let Some(rel) = memchr(b'%', &input[pos..]) {
80
+ let i = pos + rel;
81
+ out.extend_from_slice(&input[pos..i]);
82
+
83
+ if i + 2 >= input.len() {
84
+ return Err(Error::from_reason("invalid %-encoding: missing bytes"));
85
+ }
86
+ let hi = hex_val(input[i + 1])
87
+ .ok_or_else(|| Error::from_reason("invalid %-encoding: bad hi nibble"))?;
88
+ let lo = hex_val(input[i + 2])
89
+ .ok_or_else(|| Error::from_reason("invalid %-encoding: bad lo nibble"))?;
90
+
91
+ out.push((hi << 4) | lo);
92
+ pos = i + 3;
93
+ }
94
+
95
+ out.extend_from_slice(&input[pos..]);
96
+ Ok(out)
97
+ }
98
+
99
+ #[napi]
100
+ pub fn url_decode_bytes(input: Uint8Array) -> Result<Buffer> {
101
+ Ok(Buffer::from(url_decode_bytes_vec(input.as_ref())?))
102
+ }
103
+
104
+ pub fn url_decode_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
105
+ let mut pos = 0usize;
106
+ let mut src_pos = 0usize;
107
+
108
+ while let Some(rel) = memchr(b'%', &input[src_pos..]) {
109
+ let i = src_pos + rel;
110
+ write_bytes(out, &mut pos, &input[src_pos..i])?;
111
+
112
+ if i + 2 >= input.len() {
113
+ return Err(Error::from_reason("invalid %-encoding: missing bytes"));
114
+ }
115
+ let hi = hex_val(input[i + 1])
116
+ .ok_or_else(|| Error::from_reason("invalid %-encoding: bad hi nibble"))?;
117
+ let lo = hex_val(input[i + 2])
118
+ .ok_or_else(|| Error::from_reason("invalid %-encoding: bad lo nibble"))?;
119
+
120
+ ensure_capacity(out, pos, 1)?;
121
+ out[pos] = (hi << 4) | lo;
122
+ pos += 1;
123
+ src_pos = i + 3;
124
+ }
125
+
126
+ write_bytes(out, &mut pos, &input[src_pos..])?;
127
+ Ok(pos)
128
+ }
129
+
130
+ #[napi]
131
+ pub fn url_decode_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
132
+ crate::util::run_packed_into(&input, &mut output, url_decode_into_slice)
133
+ }
package/rust/util.rs ADDED
@@ -0,0 +1,537 @@
1
+ use napi::bindgen_prelude::Uint8Array;
2
+ use napi::{Error, Result};
3
+ use napi_derive::napi;
4
+ use std::borrow::Cow;
5
+ use std::sync::OnceLock;
6
+ use memchr;
7
+
8
+ static RAYON_INIT: OnceLock<std::result::Result<(), String>> = OnceLock::new();
9
+
10
+ #[cfg(target_os = "linux")]
11
+ static CORE_IDS: OnceLock<Option<Vec<core_affinity::CoreId>>> = OnceLock::new();
12
+
13
+ #[napi]
14
+ pub fn init_thread_pool(rayon_threads: Option<u32>) -> Result<()> {
15
+ let stored = RAYON_INIT.get_or_init(|| {
16
+ let default_threads = std::thread::available_parallelism()
17
+ .map(|n| n.get() as u32)
18
+ .unwrap_or(2);
19
+
20
+ // Leave a little headroom for Bun/internal runtime threads.
21
+ let preferred = default_threads.saturating_sub(1).max(1);
22
+
23
+ // Do not hard-cap to 8 by default.
24
+ // If you need a cap, set CASTRUM_MAX_RAYON_THREADS
25
+ // (legacy alias: RUST_BENCH_MAX_RAYON_THREADS).
26
+ let max_threads = std::env::var("CASTRUM_MAX_RAYON_THREADS")
27
+ .ok()
28
+ .or_else(|| std::env::var("RUST_BENCH_MAX_RAYON_THREADS").ok())
29
+ .and_then(|v| v.parse().ok())
30
+ .unwrap_or(default_threads.max(1));
31
+
32
+ let threads = rayon_threads
33
+ .unwrap_or(preferred)
34
+ .clamp(1, max_threads.max(1));
35
+
36
+ rayon::ThreadPoolBuilder::new()
37
+ .num_threads(threads as usize)
38
+ .stack_size(512 * 1024)
39
+ .thread_name(|i| format!("castrum-rayon-{}", i))
40
+ .start_handler(move |id| pin_rayon_thread(id))
41
+ .build_global()
42
+ .map_err(|e| e.to_string())
43
+ });
44
+
45
+ match stored {
46
+ Ok(()) => Ok(()),
47
+ Err(msg) => Err(Error::from_reason(msg.clone())),
48
+ }
49
+ }
50
+ #[cfg(target_os = "linux")]
51
+ fn pin_rayon_thread(id: usize) {
52
+ if std::env::var_os("CASTRUM_PIN_CORES").is_none()
53
+ && std::env::var_os("RUST_BENCH_PIN_CORES").is_none()
54
+ {
55
+ return;
56
+ }
57
+
58
+ let ids = CORE_IDS.get_or_init(core_affinity::get_core_ids);
59
+ if let Some(ids) = ids {
60
+ if ids.len() > 1 {
61
+ let idx = 1 + (id % (ids.len() - 1));
62
+ let _ = core_affinity::set_for_current(ids[idx]);
63
+ }
64
+ }
65
+ }
66
+
67
+ #[cfg(not(target_os = "linux"))]
68
+ fn pin_rayon_thread(_id: usize) {}
69
+
70
+ #[napi]
71
+ pub fn rayon_num_threads() -> u32 {
72
+ rayon::current_num_threads() as u32
73
+ }
74
+
75
+ #[derive(Default)]
76
+ pub struct VecWriter {
77
+ buf: Vec<u8>,
78
+ }
79
+
80
+ impl VecWriter {
81
+ #[inline(always)]
82
+ pub fn with_capacity(cap: usize) -> Self {
83
+ Self {
84
+ buf: Vec::with_capacity(cap),
85
+ }
86
+ }
87
+
88
+ #[inline(always)]
89
+ pub fn len(&self) -> usize {
90
+ self.buf.len()
91
+ }
92
+
93
+ #[inline(always)]
94
+ pub fn into_bytes(self) -> Vec<u8> {
95
+ self.buf
96
+ }
97
+
98
+ #[inline(always)]
99
+ pub fn push(&mut self, byte: u8) {
100
+ self.buf.push(byte);
101
+ }
102
+
103
+ #[inline(always)]
104
+ pub fn write_u32(&mut self, value: u32) {
105
+ self.buf.extend_from_slice(&value.to_le_bytes());
106
+ }
107
+
108
+ #[inline(always)]
109
+ pub fn write_bytes(&mut self, bytes: &[u8]) {
110
+ self.buf.extend_from_slice(bytes);
111
+ }
112
+
113
+ #[inline(always)]
114
+ pub fn write_bytes_ascii_lowercase(&mut self, bytes: &[u8]) {
115
+ let start = self.buf.len();
116
+ self.buf.extend_from_slice(bytes);
117
+ self.buf[start..].make_ascii_lowercase();
118
+ }
119
+
120
+ #[inline(always)]
121
+ pub fn patch_u32(&mut self, pos: usize, value: u32) {
122
+ debug_assert!(pos + 4 <= self.buf.len());
123
+ self.buf[pos..pos + 4].copy_from_slice(&value.to_le_bytes());
124
+ }
125
+
126
+ }
127
+
128
+ #[inline(always)]
129
+ pub fn read_u32_le(data: &[u8], offset: usize) -> Result<u32> {
130
+ let slice = data
131
+ .get(offset..offset + 4)
132
+ .ok_or_else(|| Error::from_reason("packed buffer: truncated u32"))?;
133
+
134
+ let bytes: [u8; 4] = slice
135
+ .try_into()
136
+ .map_err(|_| Error::from_reason("packed buffer: invalid u32"))?;
137
+
138
+ Ok(u32::from_le_bytes(bytes))
139
+ }
140
+
141
+ // ── Zero-Alloc Packed Iterator ─────────────────────────────────────
142
+
143
+ /// A zero-allocation iterator over packed batch buffers.
144
+ /// Format: [u32 count] repeated: [u32 len] [bytes]
145
+ #[derive(Clone, Copy)]
146
+ pub struct PackedIter<'a> {
147
+ data: &'a [u8],
148
+ offset: usize,
149
+ count: usize,
150
+ }
151
+
152
+ impl<'a> PackedIter<'a> {
153
+ /// Create a new `PackedIter` from a packed buffer.
154
+ /// Returns `None` if the buffer is malformed (too short, count impossible).
155
+ #[inline]
156
+ pub fn new(data: &'a [u8]) -> Result<Self> {
157
+ if data.len() < 4 {
158
+ return Err(Error::from_reason("packed buffer: missing count"));
159
+ }
160
+
161
+ let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
162
+
163
+ // Validate count is plausible
164
+ let max_possible = (data.len() - 4) / 4;
165
+ if count > max_possible {
166
+ return Err(Error::from_reason("packed buffer: impossible item count"));
167
+ }
168
+
169
+ Ok(Self {
170
+ data,
171
+ offset: 4,
172
+ count,
173
+ })
174
+ }
175
+
176
+ /// Total number of items (from the header).
177
+ #[inline(always)]
178
+ pub fn len(&self) -> usize {
179
+ self.count
180
+ }
181
+
182
+ #[inline(always)]
183
+ pub fn is_empty(&self) -> bool {
184
+ self.count == 0
185
+ }
186
+
187
+ /// Get the raw item at a specific index without advancing the iterator.
188
+ /// Returns `None` if index out of bounds or data is incomplete.
189
+ #[inline]
190
+ pub fn get(&self, index: usize) -> Option<&'a [u8]> {
191
+ if index >= self.count {
192
+ return None;
193
+ }
194
+
195
+ let mut offset = 4usize;
196
+ for _ in 0..index {
197
+ if offset + 4 > self.data.len() {
198
+ return None;
199
+ }
200
+ let len = u32::from_le_bytes([
201
+ self.data[offset],
202
+ self.data[offset + 1],
203
+ self.data[offset + 2],
204
+ self.data[offset + 3],
205
+ ]) as usize;
206
+ offset += 4 + len;
207
+ }
208
+
209
+ // Read the target item
210
+ if offset + 4 > self.data.len() {
211
+ return None;
212
+ }
213
+ let len = u32::from_le_bytes([
214
+ self.data[offset],
215
+ self.data[offset + 1],
216
+ self.data[offset + 2],
217
+ self.data[offset + 3],
218
+ ]) as usize;
219
+ offset += 4;
220
+ if offset + len > self.data.len() {
221
+ return None;
222
+ }
223
+ Some(&self.data[offset..offset + len])
224
+ }
225
+
226
+ /// Collect into a `Vec<&[u8]>` (allocating, for compatibility with existing code).
227
+ #[inline]
228
+ pub fn collect_vec(&self) -> Result<Vec<&'a [u8]>> {
229
+ let mut items = Vec::with_capacity(self.count);
230
+ let mut offset = 4usize;
231
+ for _ in 0..self.count {
232
+ if offset + 4 > self.data.len() {
233
+ return Err(Error::from_reason("packed buffer: truncated length"));
234
+ }
235
+ let len = u32::from_le_bytes([
236
+ self.data[offset],
237
+ self.data[offset + 1],
238
+ self.data[offset + 2],
239
+ self.data[offset + 3],
240
+ ]) as usize;
241
+ offset += 4;
242
+ if offset + len > self.data.len() {
243
+ return Err(Error::from_reason("packed buffer: truncated item"));
244
+ }
245
+ items.push(&self.data[offset..offset + len]);
246
+ offset += len;
247
+ }
248
+ Ok(items)
249
+ }
250
+ }
251
+
252
+ impl<'a> Iterator for PackedIter<'a> {
253
+ type Item = &'a [u8];
254
+
255
+ #[inline]
256
+ fn next(&mut self) -> Option<Self::Item> {
257
+ if self.offset >= self.data.len() || self.count == 0 {
258
+ return None;
259
+ }
260
+
261
+ if self.offset + 4 > self.data.len() {
262
+ return None;
263
+ }
264
+
265
+ let len = u32::from_le_bytes([
266
+ self.data[self.offset],
267
+ self.data[self.offset + 1],
268
+ self.data[self.offset + 2],
269
+ self.data[self.offset + 3],
270
+ ]) as usize;
271
+
272
+ self.offset += 4;
273
+
274
+ if self.offset + len > self.data.len() {
275
+ return None;
276
+ }
277
+
278
+ let item = &self.data[self.offset..self.offset + len];
279
+ self.offset += len;
280
+ self.count -= 1;
281
+
282
+ Some(item)
283
+ }
284
+
285
+ #[inline]
286
+ fn size_hint(&self) -> (usize, Option<usize>) {
287
+ (self.count, Some(self.count))
288
+ }
289
+ }
290
+
291
+ impl<'a> ExactSizeIterator for PackedIter<'a> {}
292
+
293
+ // ── Generic batch processing helpers ───────────────────────────────
294
+
295
+ /// Generic batch validation using `AsRef<[u8]>`.
296
+ #[inline]
297
+ pub fn validation_bitset_chunked<T: AsRef<[u8]> + Sync>(
298
+ items: &[T],
299
+ f: impl Fn(&[u8]) -> bool + Sync,
300
+ chunk_items: usize,
301
+ ) -> Vec<u8> {
302
+ let count = items.len();
303
+ let bitset_len = count.div_ceil(8);
304
+
305
+ let mut out = Vec::with_capacity(4 + bitset_len);
306
+ out.extend_from_slice(&(count as u32).to_le_bytes());
307
+ out.resize(4 + bitset_len, 0);
308
+
309
+ if count == 0 {
310
+ return out;
311
+ }
312
+
313
+ if should_parallelize(count, total_bytes_ref(items)) {
314
+ use rayon::prelude::*;
315
+
316
+ let chunk_items = chunk_items.max(64);
317
+ let chunk_bytes = chunk_items.div_ceil(8);
318
+ let bits = &mut out[4..];
319
+
320
+ bits.par_chunks_mut(chunk_bytes)
321
+ .enumerate()
322
+ .for_each(|(chunk_idx, chunk)| {
323
+ let start_item = chunk_idx * chunk_bytes * 8;
324
+ let end_item = (start_item + chunk.len() * 8).min(count);
325
+ let start_byte = start_item / 8;
326
+
327
+ for i in start_item..end_item {
328
+ if f(items[i].as_ref()) {
329
+ chunk[(i / 8) - start_byte] |= 1 << (i & 7);
330
+ }
331
+ }
332
+ });
333
+ } else {
334
+ for (i, item) in items.iter().enumerate() {
335
+ if f(item.as_ref()) {
336
+ out[4 + (i >> 3)] |= 1 << (i & 7);
337
+ }
338
+ }
339
+ }
340
+
341
+ out
342
+ }
343
+
344
+ /// Generic batch count using `AsRef<[u8]>`.
345
+ #[inline]
346
+ pub fn count_batch<T: AsRef<[u8]> + Sync>(
347
+ items: &[T],
348
+ f: impl Fn(&[u8]) -> bool + Sync,
349
+ chunk_items: usize,
350
+ ) -> usize {
351
+ let chunk_items = chunk_items.max(64);
352
+
353
+ if should_parallelize(items.len(), total_bytes_ref(items)) {
354
+ use rayon::prelude::*;
355
+
356
+ items
357
+ .par_chunks(chunk_items)
358
+ .map(|chunk| chunk.iter().filter(|item| f(item.as_ref())).count())
359
+ .sum()
360
+ } else {
361
+ items.iter().filter(|item| f(item.as_ref())).count()
362
+ }
363
+ }
364
+
365
+ /// Generic batch sum using `AsRef<[u8]>`.
366
+ #[inline]
367
+ pub fn sum_batch_i64<T: AsRef<[u8]> + Sync>(
368
+ items: &[T],
369
+ f: impl Fn(&[u8]) -> i64 + Sync,
370
+ chunk_items: usize,
371
+ ) -> i64 {
372
+ let chunk_items = chunk_items.max(64);
373
+
374
+ if should_parallelize(items.len(), total_bytes_ref(items)) {
375
+ use rayon::prelude::*;
376
+
377
+ items
378
+ .par_chunks(chunk_items)
379
+ .fold(|| 0i64, |acc, chunk| {
380
+ chunk.iter().fold(acc, |a, item| a.saturating_add(f(item.as_ref())))
381
+ })
382
+ .reduce(|| 0i64, |a, b| a.saturating_add(b))
383
+ } else {
384
+ items
385
+ .iter()
386
+ .fold(0i64, |acc, item| acc.saturating_add(f(item.as_ref())))
387
+ }
388
+ }
389
+
390
+ /// Helper: total bytes for `AsRef<[u8]>` slice.
391
+ #[inline(always)]
392
+ fn total_bytes_ref<T: AsRef<[u8]>>(items: &[T]) -> usize {
393
+ items.iter().map(|x| x.as_ref().len()).sum()
394
+ }
395
+
396
+ // ── Cow helpers ─────────────────────────────────────────────────────
397
+
398
+ /// Returns `Cow::Borrowed` when no escaping/decoding is needed,
399
+ /// `Cow::Owned` when modifications were necessary.
400
+ pub fn cow_decode_url(input: &[u8]) -> Cow<'_, [u8]> {
401
+ if memchr::memchr2(b'+', b'%', input).is_none() {
402
+ return Cow::Borrowed(input);
403
+ }
404
+ // At least some decoding needed; caller should use the _into_slice functions
405
+ Cow::Owned(input.to_vec())
406
+ }
407
+
408
+ /// Returns the underlying data for a Cow, whether owned or borrowed.
409
+ #[inline(always)]
410
+ pub fn cow_as_slice<'a>(c: &'a Cow<'_, [u8]>) -> &'a [u8] {
411
+ c.as_ref()
412
+ }
413
+
414
+ // ── Legacy unpack (wraps PackedIter for backwards compat) ──────────
415
+
416
+ /// Packed batch input format:
417
+ /// [u32 count]
418
+ /// repeated count times:
419
+ /// [u32 byte_length]
420
+ /// [bytes]
421
+ #[inline]
422
+ pub fn unpack<'a>(data: &'a [u8]) -> Result<Vec<&'a [u8]>> {
423
+ PackedIter::new(data)?.collect_vec()
424
+ }
425
+
426
+ #[inline(always)]
427
+ pub fn trim_ascii_whitespace(bytes: &[u8]) -> &[u8] {
428
+ let mut start = 0usize;
429
+ let mut end = bytes.len();
430
+
431
+ while start < end && bytes[start].is_ascii_whitespace() {
432
+ start += 1;
433
+ }
434
+
435
+ while end > start && bytes[end - 1].is_ascii_whitespace() {
436
+ end -= 1;
437
+ }
438
+
439
+ &bytes[start..end]
440
+ }
441
+
442
+ #[inline(always)]
443
+ pub fn total_bytes(items: &[&[u8]]) -> usize {
444
+ items.iter().map(|x| x.len()).sum()
445
+ }
446
+
447
+ #[inline(always)]
448
+ pub fn hex_val(b: u8) -> Option<u8> {
449
+ match b {
450
+ b'0'..=b'9' => Some(b - b'0'),
451
+ b'a'..=b'f' => Some(b - b'a' + 10),
452
+ b'A'..=b'F' => Some(b - b'A' + 10),
453
+ _ => None,
454
+ }
455
+ }
456
+
457
+ #[inline(always)]
458
+ pub fn ensure_capacity(out: &[u8], pos: usize, additional: usize) -> Result<()> {
459
+ let end = pos
460
+ .checked_add(additional)
461
+ .ok_or_else(|| Error::from_reason("packed output: overflow"))?;
462
+
463
+ if end > out.len() {
464
+ return Err(Error::from_reason("packed output: buffer too small"));
465
+ }
466
+
467
+ Ok(())
468
+ }
469
+
470
+ #[inline(always)]
471
+ pub fn write_u32_le(out: &mut [u8], pos: &mut usize, value: u32) -> Result<()> {
472
+ ensure_capacity(out, *pos, 4)?;
473
+ out[*pos..*pos + 4].copy_from_slice(&value.to_le_bytes());
474
+ *pos += 4;
475
+ Ok(())
476
+ }
477
+
478
+ #[inline(always)]
479
+ pub fn write_bytes(out: &mut [u8], pos: &mut usize, bytes: &[u8]) -> Result<()> {
480
+ ensure_capacity(out, *pos, bytes.len())?;
481
+ out[*pos..*pos + bytes.len()].copy_from_slice(bytes);
482
+ *pos += bytes.len();
483
+ Ok(())
484
+ }
485
+
486
+ #[inline(always)]
487
+ pub fn write_bytes_lowercase(out: &mut [u8], pos: &mut usize, bytes: &[u8]) -> Result<()> {
488
+ ensure_capacity(out, *pos, bytes.len())?;
489
+ let start = *pos;
490
+ let end = start + bytes.len();
491
+ out[start..end].copy_from_slice(bytes);
492
+ out[start..end].make_ascii_lowercase();
493
+ *pos = end;
494
+ Ok(())
495
+ }
496
+
497
+ #[inline(always)]
498
+ pub fn should_parallelize(items: usize, bytes: usize) -> bool {
499
+ let threads = rayon::current_num_threads().max(1);
500
+ items >= threads.saturating_mul(2048) || bytes >= threads.saturating_mul(1024 * 1024)
501
+ }
502
+
503
+ #[inline(always)]
504
+ pub fn slices_overlap(a: &[u8], b: &[u8]) -> bool {
505
+ if a.is_empty() || b.is_empty() {
506
+ return false;
507
+ }
508
+
509
+ let a_start = a.as_ptr() as usize;
510
+ let b_start = b.as_ptr() as usize;
511
+ let a_end = a_start.saturating_add(a.len());
512
+ let b_end = b_start.saturating_add(b.len());
513
+
514
+ a_start < b_end && b_start < a_end
515
+ }
516
+
517
+ #[inline]
518
+ pub fn run_packed_into<F>(input: &Uint8Array, output: &mut Uint8Array, f: F) -> Result<u32>
519
+ where
520
+ F: FnOnce(&[u8], &mut [u8]) -> Result<usize>,
521
+ {
522
+ let input_bytes = input.as_ref();
523
+
524
+ let overlaps = {
525
+ let output_bytes = output.as_ref();
526
+ slices_overlap(input_bytes, output_bytes)
527
+ };
528
+
529
+ let written = if overlaps {
530
+ let owned_input = input_bytes.to_vec();
531
+ unsafe { f(&owned_input, output.as_mut())? }
532
+ } else {
533
+ unsafe { f(input_bytes, output.as_mut())? }
534
+ };
535
+
536
+ Ok(written as u32)
537
+ }