castrum 0.9.9 → 0.9.11
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 +140 -89
- package/Cargo.toml +29 -5
- package/README.md +5 -2
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/docs/FFI_BUN_GUIDE.md +8 -4
- package/docs/HOW_TO_ADD_AN_OP.md +12 -9
- package/docs/REPO_MAP.md +10 -6
- package/docs/ai/TREE.md +3 -1
- package/docs/bun-builtins-decision-matrix.md +17 -7
- package/package.json +3 -2
- package/rust/crypto/base64.rs +472 -57
- package/rust/crypto/hashing.rs +68 -0
- package/rust/ffi/crypto.rs +224 -197
- package/rust/ffi/hashing.rs +68 -51
- package/rust/ffi/http.rs +33 -24
- package/rust/ffi/ingress.rs +13 -12
- package/rust/ffi/metrics.rs +8 -7
- package/rust/ffi/payload.rs +26 -23
- package/rust/ffi/probe.rs +18 -11
- package/rust/ffi/route.rs +6 -5
- package/rust/ffi/task.rs +14 -11
- package/rust/ffi/tests.rs +95 -0
- package/rust/ffi/util.rs +13 -0
- package/rust/ffi/validators.rs +36 -29
- package/rust/http/http_date.rs +4 -0
- package/rust/panic_safety.rs +191 -0
- package/rust/payload/sse.rs +78 -5
- package/rust/proptest_suite.rs +131 -1
- package/rust/util/bytes.rs +32 -0
- package/rust/util/text.rs +112 -10
- package/rust/util/validation.rs +34 -0
package/rust/util/text.rs
CHANGED
|
@@ -14,23 +14,50 @@ use napi_derive::napi;
|
|
|
14
14
|
/// [`regex_escape_write`].
|
|
15
15
|
const REGEX_META: &[u8; 14] = b"\\.*+?^${}()|[]";
|
|
16
16
|
|
|
17
|
+
/// 256-entry membership lookup for [`REGEX_META`] — one table load per byte.
|
|
18
|
+
/// The previous per-byte `REGEX_META.contains(&c)` did a linear 14-element
|
|
19
|
+
/// scan PER byte (O(n·14)); a 16KB payload took ~100µs (~160MB/s) and the
|
|
20
|
+
/// win margin vs JS regex-replace collapsed with size. The LUT makes the hot
|
|
21
|
+
/// loop branch-predictable (~1 load/byte), and safe runs are copied in bulk
|
|
22
|
+
/// (memcpy) instead of one push per byte.
|
|
23
|
+
const REGEX_META_LUT: [u8; 256] = {
|
|
24
|
+
let mut t = [0u8; 256];
|
|
25
|
+
let mut i = 0usize;
|
|
26
|
+
while i < REGEX_META.len() {
|
|
27
|
+
t[REGEX_META[i] as usize] = 1;
|
|
28
|
+
i += 1;
|
|
29
|
+
}
|
|
30
|
+
t
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
#[inline(always)]
|
|
34
|
+
fn is_meta(b: u8) -> bool {
|
|
35
|
+
REGEX_META_LUT[b as usize] != 0
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
/// Append the RegExp-escaped form of `input` to `out`. ASCII-only transform
|
|
18
39
|
/// (backslash before metachar bytes) — multi-byte UTF-8 sequences pass
|
|
19
40
|
/// through byte-exact because no UTF-8 continuation byte is a metachar.
|
|
41
|
+
/// Safe runs are bulk-copied (memcpy) between metachars, so throughput scales
|
|
42
|
+
/// with memory bandwidth, not the metachar-set size.
|
|
20
43
|
pub fn regex_escape_into(input: &[u8], out: &mut Vec<u8>) {
|
|
21
|
-
|
|
22
|
-
|
|
44
|
+
let mut run_start = 0usize;
|
|
45
|
+
for (i, &c) in input.iter().enumerate() {
|
|
46
|
+
if is_meta(c) {
|
|
47
|
+
out.extend_from_slice(&input[run_start..i]);
|
|
23
48
|
out.push(b'\\');
|
|
49
|
+
out.push(c);
|
|
50
|
+
run_start = i + 1;
|
|
24
51
|
}
|
|
25
|
-
out.push(c);
|
|
26
52
|
}
|
|
53
|
+
out.extend_from_slice(&input[run_start..]);
|
|
27
54
|
}
|
|
28
55
|
|
|
29
56
|
/// Exact escaped length (metachar count + input length) — lets callers size
|
|
30
57
|
/// output buffers without running the escape twice.
|
|
31
58
|
#[inline]
|
|
32
59
|
pub fn regex_escape_len(input: &[u8]) -> usize {
|
|
33
|
-
input.len() + input.iter().filter(
|
|
60
|
+
input.len() + input.iter().filter(|&&b| is_meta(b)).count()
|
|
34
61
|
}
|
|
35
62
|
|
|
36
63
|
/// Write the escaped form of `input` into `out` (which must be at least
|
|
@@ -38,15 +65,21 @@ pub fn regex_escape_len(input: &[u8]) -> usize {
|
|
|
38
65
|
/// written.
|
|
39
66
|
pub fn regex_escape_write(input: &[u8], out: &mut [u8]) -> usize {
|
|
40
67
|
let mut pos = 0usize;
|
|
41
|
-
|
|
42
|
-
|
|
68
|
+
let mut run_start = 0usize;
|
|
69
|
+
for (i, &c) in input.iter().enumerate() {
|
|
70
|
+
if is_meta(c) {
|
|
71
|
+
let run = &input[run_start..i];
|
|
72
|
+
out[pos..pos + run.len()].copy_from_slice(run);
|
|
73
|
+
pos += run.len();
|
|
43
74
|
out[pos] = b'\\';
|
|
44
|
-
pos
|
|
75
|
+
out[pos + 1] = c;
|
|
76
|
+
pos += 2;
|
|
77
|
+
run_start = i + 1;
|
|
45
78
|
}
|
|
46
|
-
out[pos] = c;
|
|
47
|
-
pos += 1;
|
|
48
79
|
}
|
|
49
|
-
|
|
80
|
+
let tail = &input[run_start..];
|
|
81
|
+
out[pos..pos + tail.len()].copy_from_slice(tail);
|
|
82
|
+
pos + tail.len()
|
|
50
83
|
}
|
|
51
84
|
|
|
52
85
|
/// Escape JS RegExp metacharacters in a string → a pattern that matches the
|
|
@@ -127,4 +160,73 @@ mod tests {
|
|
|
127
160
|
let escaped = String::from_utf8(out).expect("utf8");
|
|
128
161
|
assert_eq!(escaped, r"a\.c");
|
|
129
162
|
}
|
|
163
|
+
|
|
164
|
+
/// Reference escape (the previous per-byte `REGEX_META.contains` loop).
|
|
165
|
+
fn ref_escape(input: &[u8]) -> Vec<u8> {
|
|
166
|
+
const META: &[u8] = b"\\.*+?^${}()|[]";
|
|
167
|
+
let mut out = Vec::new();
|
|
168
|
+
for &c in input {
|
|
169
|
+
if META.contains(&c) {
|
|
170
|
+
out.push(b'\\');
|
|
171
|
+
}
|
|
172
|
+
out.push(c);
|
|
173
|
+
}
|
|
174
|
+
out
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn regex_escape_matches_reference_for_all_lengths() {
|
|
179
|
+
// Every length 0..=96 plus larger payloads: the LUT + run-copy core
|
|
180
|
+
// (bulk memcpy between metachars) must be byte-identical to the naive
|
|
181
|
+
// per-byte reference. Lengths 15/16/17… straddle no SIMD chunk here
|
|
182
|
+
// (the transform is scalar), but the sweep locks positional behavior:
|
|
183
|
+
// a run-copy off-by-one (dropping or duplicating a safe byte) surfaces.
|
|
184
|
+
let mut lens: Vec<usize> = (0..=96).collect();
|
|
185
|
+
lens.extend([128, 512, 16 * 1024, 256 * 1024]);
|
|
186
|
+
for n in lens {
|
|
187
|
+
let input: Vec<u8> = (0..n).map(|i| (i.wrapping_mul(31) + 5) as u8).collect();
|
|
188
|
+
let expect = ref_escape(&input);
|
|
189
|
+
// allocating path
|
|
190
|
+
let mut out = Vec::new();
|
|
191
|
+
regex_escape_into(&input, &mut out);
|
|
192
|
+
assert_eq!(out, expect, "len {n}");
|
|
193
|
+
// len formula
|
|
194
|
+
assert_eq!(regex_escape_len(&input), expect.len(), "len {n}");
|
|
195
|
+
// write path (exact-size buffer, like the C-ABI caller)
|
|
196
|
+
let mut buf = vec![0u8; expect.len()];
|
|
197
|
+
let written = regex_escape_write(&input, &mut buf);
|
|
198
|
+
assert_eq!(written, expect.len(), "len {n}");
|
|
199
|
+
assert_eq!(&buf[..written], &expect[..], "len {n}");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[test]
|
|
204
|
+
fn regex_escape_large_payload_edges() {
|
|
205
|
+
// Three 256 KiB extremes: every byte a metachar (no bulk runs — the
|
|
206
|
+
// worst case for the run-copy core), no metachars at all (pure bulk
|
|
207
|
+
// copy), and a dense alternating pattern. All must match the reference
|
|
208
|
+
// and the exact-length contract.
|
|
209
|
+
let all_meta: Vec<u8> = (0..256 * 1024)
|
|
210
|
+
.map(|i| b"\\.*+?^${}()|[]"[i % 14])
|
|
211
|
+
.collect();
|
|
212
|
+
let none: Vec<u8> = (0..256 * 1024).map(|i| b"abcXYZ0123_-/"[i % 13]).collect();
|
|
213
|
+
let dense: Vec<u8> = (0..256 * 1024)
|
|
214
|
+
.map(|i| if i % 2 == 0 { b'.' } else { b'a' })
|
|
215
|
+
.collect();
|
|
216
|
+
for input in [all_meta, none, dense] {
|
|
217
|
+
let expect = ref_escape(&input);
|
|
218
|
+
let mut out = Vec::new();
|
|
219
|
+
regex_escape_into(&input, &mut out);
|
|
220
|
+
assert_eq!(out, expect);
|
|
221
|
+
assert_eq!(regex_escape_len(&input), expect.len());
|
|
222
|
+
let mut buf = vec![0u8; expect.len()];
|
|
223
|
+
let written = regex_escape_write(&input, &mut buf);
|
|
224
|
+
assert_eq!(written, expect.len());
|
|
225
|
+
assert_eq!(&buf[..written], &expect[..]);
|
|
226
|
+
}
|
|
227
|
+
// All-metachar bound: escaped length is exactly 2x (every byte gets a
|
|
228
|
+
// backslash); all-safe bound: escaped length equals input length.
|
|
229
|
+
assert_eq!(regex_escape_len(&vec![b'.'; 1024]), 2048);
|
|
230
|
+
assert_eq!(regex_escape_len(&vec![b'a'; 1024]), 1024);
|
|
231
|
+
}
|
|
130
232
|
}
|
package/rust/util/validation.rs
CHANGED
|
@@ -317,6 +317,40 @@ mod tests {
|
|
|
317
317
|
assert!(!validate_uuid_bytes(b"550e8400e29b41d4a716446655440000")); // no dashes
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/// Byte-for-byte pins the accept set: replacing ANY single hex position
|
|
321
|
+
/// (0..7, 9..12, 15..17, 20..22, 24..35) with a non-hex digit must reject,
|
|
322
|
+
/// and every one of the 32 hex positions must accept uppercase + lowercase
|
|
323
|
+
/// digits. Guards against a future rewrite (e.g. SIMD/run-split) silently
|
|
324
|
+
/// dropping or mis-scoping a position — measured perf-neutral, kept as a
|
|
325
|
+
/// spec-lock regression net.
|
|
326
|
+
#[test]
|
|
327
|
+
fn validate_uuid_hex_run_coverage() {
|
|
328
|
+
let dash = |i: usize| matches!(i, 8 | 13 | 18 | 23);
|
|
329
|
+
for pos in 0..36 {
|
|
330
|
+
let valid = b"550e8400-e29b-41d4-a716-446655440000";
|
|
331
|
+
if dash(pos) {
|
|
332
|
+
continue; // dash positions are pinned by their own check
|
|
333
|
+
}
|
|
334
|
+
// every non-dash position is a hex position (version/variant cols
|
|
335
|
+
// included — 14 must stay '4', 19 must stay in 8/9/a/b set)
|
|
336
|
+
if valid[pos].is_ascii_hexdigit() {
|
|
337
|
+
let mut upper = *valid;
|
|
338
|
+
upper[pos] = valid[pos].to_ascii_uppercase();
|
|
339
|
+
assert!(validate_uuid_bytes(&upper), "uppercase at {pos}");
|
|
340
|
+
}
|
|
341
|
+
let mut bad = *valid;
|
|
342
|
+
bad[pos] = b'g'; // 'g' is not a hex digit
|
|
343
|
+
assert!(!validate_uuid_bytes(&bad), "g at {pos}");
|
|
344
|
+
}
|
|
345
|
+
// and the version / variant columns still reject wrong values
|
|
346
|
+
let mut v = *b"550e8400-e29b-41d4-a716-446655440000";
|
|
347
|
+
v[14] = b'5';
|
|
348
|
+
assert!(!validate_uuid_bytes(&v));
|
|
349
|
+
v = *b"550e8400-e29b-41d4-a716-446655440000";
|
|
350
|
+
v[19] = b'c';
|
|
351
|
+
assert!(!validate_uuid_bytes(&v));
|
|
352
|
+
}
|
|
353
|
+
|
|
320
354
|
#[test]
|
|
321
355
|
fn validate_ipv4_cases() {
|
|
322
356
|
assert!(validate_ipv4_bytes(b"192.168.0.1"));
|