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.
- package/Cargo.lock +2259 -0
- package/Cargo.toml +138 -0
- package/README.md +6 -0
- package/build.rs +5 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1348 -1295
- package/dist/index.js +4307 -3776
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +16 -0
- package/docs/FFI_BUN_GUIDE.md +89 -6
- package/docs/REPO_MAP.md +2 -2
- package/index.ts +19 -25
- package/package.json +9 -1
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +96 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +68 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +132 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +180 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +779 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +566 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +881 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +72 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +407 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +104 -8
- package/src/native/ffi/build/compress.ts +14 -10
- package/src/native/ffi/build/instances.ts +40 -3
- package/src/native/ffi/build/metrics.ts +224 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/util.ts +14 -1
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +10 -3
- package/src/native/ffi/types.ts +111 -9
- package/src/native/ffi.ts +87 -1
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +2 -2
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +17 -11
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +5 -4
- package/src/shared/codec.ts +18 -3
|
@@ -0,0 +1,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
|
+
}
|