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.
- package/Cargo.toml +73 -0
- package/README.md +432 -0
- package/bench.ts +3 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/index.ts +24 -0
- package/package.json +94 -0
- package/rust/batch.rs +195 -0
- package/rust/cookie_parser.rs +70 -0
- package/rust/cors.rs +263 -0
- package/rust/hashing.rs +164 -0
- package/rust/headers.rs +234 -0
- package/rust/hmac_sha256.rs +113 -0
- package/rust/http_parser.rs +132 -0
- package/rust/ingress.rs +616 -0
- package/rust/ingress_constants.rs +89 -0
- package/rust/ip_trust.rs +185 -0
- package/rust/json_ops.rs +66 -0
- package/rust/json_patch_ops.rs +25 -0
- package/rust/json_schema.rs +151 -0
- package/rust/json_ser.rs +429 -0
- package/rust/lib.rs +48 -0
- package/rust/method.rs +141 -0
- package/rust/mime_lookup.rs +76 -0
- package/rust/output.rs +238 -0
- package/rust/proxy.rs +90 -0
- package/rust/query_parser.rs +141 -0
- package/rust/random_token.rs +52 -0
- package/rust/rate_limit.rs +203 -0
- package/rust/terminal.rs +95 -0
- package/rust/unit_tests.rs +843 -0
- package/rust/url_codec.rs +133 -0
- package/rust/util.rs +537 -0
- package/rust/validation.rs +107 -0
- package/rust/websocket.rs +23 -0
- package/src/baseline/index.ts +12 -0
- package/src/baseline/tasks/cookie.ts +22 -0
- package/src/baseline/tasks/hashing.ts +18 -0
- package/src/baseline/tasks/hmac.ts +38 -0
- package/src/baseline/tasks/http.ts +62 -0
- package/src/baseline/tasks/json-patch.ts +12 -0
- package/src/baseline/tasks/json.ts +50 -0
- package/src/baseline/tasks/mime.ts +5 -0
- package/src/baseline/tasks/query.ts +14 -0
- package/src/baseline/tasks/token.ts +15 -0
- package/src/baseline/tasks/url.ts +11 -0
- package/src/baseline/tasks/validation.ts +19 -0
- package/src/baseline/tasks/websocket.ts +14 -0
- package/src/bench/assert.ts +70 -0
- package/src/bench/checks.ts +227 -0
- package/src/bench/checksum.ts +27 -0
- package/src/bench/comparisons.ts +70 -0
- package/src/bench/concurrent-worker.ts +135 -0
- package/src/bench/fixtures.ts +233 -0
- package/src/bench/index.ts +2 -0
- package/src/bench/measure.ts +177 -0
- package/src/bench/now.ts +3 -0
- package/src/bench/report.ts +79 -0
- package/src/bench/run.ts +57 -0
- package/src/bench/tasks/complex.ts +232 -0
- package/src/bench/tasks/concurrent.ts +213 -0
- package/src/bench/tasks/cookie.ts +44 -0
- package/src/bench/tasks/hashing.ts +33 -0
- package/src/bench/tasks/hmac.ts +36 -0
- package/src/bench/tasks/http.ts +50 -0
- package/src/bench/tasks/index.ts +52 -0
- package/src/bench/tasks/json-patch.ts +21 -0
- package/src/bench/tasks/json.ts +33 -0
- package/src/bench/tasks/mime.ts +21 -0
- package/src/bench/tasks/query.ts +44 -0
- package/src/bench/tasks/stress.ts +142 -0
- package/src/bench/tasks/token.ts +20 -0
- package/src/bench/tasks/url.ts +34 -0
- package/src/bench/tasks/validation.ts +57 -0
- package/src/bench/tasks/websocket.ts +21 -0
- package/src/bench/types.ts +60 -0
- package/src/data/json-rows.ts +33 -0
- package/src/ingress/constants.ts +56 -0
- package/src/ingress/fast.ts +1082 -0
- package/src/ingress/handlers.ts +1202 -0
- package/src/ingress/index.ts +457 -0
- package/src/ingress/packed-input.ts +59 -0
- package/src/native/index.ts +207 -0
- package/src/rust-ffi/index.ts +555 -0
- package/src/shared/bytes.ts +36 -0
- package/src/shared/json.ts +18 -0
- package/src/shared/packed.ts +316 -0
package/rust/json_ser.rs
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// rust/json_ser.rs — JSON serialization helpers extracted from ingress.rs
|
|
2
|
+
// Zero-alloc JSON escaping, cookie→JSON and query→JSON conversion
|
|
3
|
+
// Uses memchr-based skip for bulk unescaped bytes (~95% path)
|
|
4
|
+
|
|
5
|
+
use crate::util::trim_ascii_whitespace;
|
|
6
|
+
use napi::{Error, Result};
|
|
7
|
+
|
|
8
|
+
const JSON_HEX_LOWER: &[u8; 16] = b"0123456789abcdef";
|
|
9
|
+
|
|
10
|
+
/// Determine whether bytes are valid UTF-8 (cached via a bit check).
|
|
11
|
+
#[inline(always)]
|
|
12
|
+
fn is_valid_utf8(bytes: &[u8]) -> bool {
|
|
13
|
+
// Fast path: small ASCII-only strings are very common
|
|
14
|
+
if bytes.is_empty() {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
// Check first byte — if >= 0x80, we need a real check
|
|
18
|
+
// For pure ASCII we can skip the full simdutf8 check
|
|
19
|
+
if bytes.iter().all(|&b| b < 0x80) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
std::str::from_utf8(bytes).is_ok()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Compute the JSON-escaped length without writing.
|
|
26
|
+
/// Uses memchr to find special characters in bulk.
|
|
27
|
+
#[inline(always)]
|
|
28
|
+
pub fn json_escaped_len(bytes: &[u8]) -> usize {
|
|
29
|
+
if is_valid_utf8(bytes) {
|
|
30
|
+
json_escaped_len_impl(bytes)
|
|
31
|
+
} else {
|
|
32
|
+
bytes.len().saturating_mul(6)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Inner: assumes valid UTF-8, uses memchr for skip.
|
|
37
|
+
#[inline(always)]
|
|
38
|
+
fn json_escaped_len_impl(bytes: &[u8]) -> usize {
|
|
39
|
+
let mut total = bytes.len(); // start with all bytes as 1
|
|
40
|
+
let mut offset = 0;
|
|
41
|
+
|
|
42
|
+
// Use memchr3 to find any of the special chars: ", \, \n
|
|
43
|
+
while let Some(pos) = memchr::memchr3(b'"', b'\\', b'\n', &bytes[offset..]) {
|
|
44
|
+
let abs = offset + pos;
|
|
45
|
+
let b = bytes[abs];
|
|
46
|
+
offset = abs + 1;
|
|
47
|
+
|
|
48
|
+
total += match b {
|
|
49
|
+
b'\\' | b'"' | b'\n' => 1, // 2 bytes instead of 1 → +1
|
|
50
|
+
_ => {
|
|
51
|
+
// \r, \t, \x08, \x0c: also 2 bytes
|
|
52
|
+
if b == b'\r' || b == b'\t' || b == 0x08 || b == 0x0c {
|
|
53
|
+
1
|
|
54
|
+
} else if b < 0x20 {
|
|
55
|
+
// \uXXXX → 6 bytes instead of 1 → +5
|
|
56
|
+
5
|
|
57
|
+
} else {
|
|
58
|
+
0
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Now check for remaining control characters (<0x20) that memchr3 missed
|
|
65
|
+
// (tab and \r are 0x09 and 0x0d which are < 0x20 but already handled above)
|
|
66
|
+
for &b in &bytes[offset..] {
|
|
67
|
+
if b < 0x20 && b != b'\n' && b != b'\r' && b != b'\t' && b != 0x08 && b != 0x0c {
|
|
68
|
+
total += 5; // \uXXXX
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
total
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Write JSON-escaped bytes into the buffer at the current position.
|
|
76
|
+
/// Returns the number of bytes written.
|
|
77
|
+
#[inline(always)]
|
|
78
|
+
pub fn write_json_escaped(out: &mut [u8], pos: &mut usize, bytes: &[u8]) -> usize {
|
|
79
|
+
let start = *pos;
|
|
80
|
+
if is_valid_utf8(bytes) {
|
|
81
|
+
write_json_escaped_utf8(out, pos, bytes);
|
|
82
|
+
} else {
|
|
83
|
+
// Binary: escape every byte as \u00XX
|
|
84
|
+
for &b in bytes {
|
|
85
|
+
out[*pos] = b'\\';
|
|
86
|
+
out[*pos + 1] = b'u';
|
|
87
|
+
out[*pos + 2] = b'0';
|
|
88
|
+
out[*pos + 3] = b'0';
|
|
89
|
+
out[*pos + 4] = JSON_HEX_LOWER[(b >> 4) as usize];
|
|
90
|
+
out[*pos + 5] = JSON_HEX_LOWER[(b & 0x0f) as usize];
|
|
91
|
+
*pos += 6;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
*pos - start
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Write escaped bytes for valid UTF-8 input.
|
|
98
|
+
#[inline(always)]
|
|
99
|
+
fn write_json_escaped_utf8(out: &mut [u8], pos: &mut usize, bytes: &[u8]) {
|
|
100
|
+
let mut offset = 0usize;
|
|
101
|
+
|
|
102
|
+
while let Some(mut idx) = memchr::memchr3(b'"', b'\\', b'\n', &bytes[offset..]) {
|
|
103
|
+
idx += offset;
|
|
104
|
+
|
|
105
|
+
// Copy unescaped bytes before the special character
|
|
106
|
+
let unescaped = &bytes[offset..idx];
|
|
107
|
+
if !unescaped.is_empty() {
|
|
108
|
+
out[*pos..*pos + unescaped.len()].copy_from_slice(unescaped);
|
|
109
|
+
*pos += unescaped.len();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let b = bytes[idx];
|
|
113
|
+
match b {
|
|
114
|
+
b'"' | b'\\' | b'\n' | b'\r' | b'\t' | 0x08 | 0x0c => {
|
|
115
|
+
let esc = match b {
|
|
116
|
+
b'"' => b'"',
|
|
117
|
+
b'\\' => b'\\',
|
|
118
|
+
b'\n' => b'n',
|
|
119
|
+
b'\r' => b'r',
|
|
120
|
+
b'\t' => b't',
|
|
121
|
+
0x08 => b'b',
|
|
122
|
+
0x0c => b'f',
|
|
123
|
+
_ => unreachable!(),
|
|
124
|
+
};
|
|
125
|
+
out[*pos] = b'\\';
|
|
126
|
+
out[*pos + 1] = esc;
|
|
127
|
+
*pos += 2;
|
|
128
|
+
}
|
|
129
|
+
_ => {
|
|
130
|
+
// Control character (< 0x20) → \u00XX
|
|
131
|
+
out[*pos] = b'\\';
|
|
132
|
+
out[*pos + 1] = b'u';
|
|
133
|
+
out[*pos + 2] = b'0';
|
|
134
|
+
out[*pos + 3] = b'0';
|
|
135
|
+
out[*pos + 4] = JSON_HEX_LOWER[(b >> 4) as usize];
|
|
136
|
+
out[*pos + 5] = JSON_HEX_LOWER[(b & 0x0f) as usize];
|
|
137
|
+
*pos += 6;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
offset = idx + 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Copy remaining
|
|
145
|
+
let remaining = &bytes[offset..];
|
|
146
|
+
if !remaining.is_empty() {
|
|
147
|
+
// Check for remaining low control chars that memchr3 missed (\r, \t, etc.)
|
|
148
|
+
let mut i = 0;
|
|
149
|
+
while i < remaining.len() {
|
|
150
|
+
let b = remaining[i];
|
|
151
|
+
match b {
|
|
152
|
+
b'\r' | b'\t' | 0x08 | 0x0c => {
|
|
153
|
+
let esc = match b {
|
|
154
|
+
b'\r' => b'r',
|
|
155
|
+
b'\t' => b't',
|
|
156
|
+
0x08 => b'b',
|
|
157
|
+
0x0c => b'f',
|
|
158
|
+
_ => unreachable!(),
|
|
159
|
+
};
|
|
160
|
+
out[*pos] = b'\\';
|
|
161
|
+
out[*pos + 1] = esc;
|
|
162
|
+
*pos += 2;
|
|
163
|
+
i += 1;
|
|
164
|
+
}
|
|
165
|
+
b if b < 0x20 => {
|
|
166
|
+
out[*pos] = b'\\';
|
|
167
|
+
out[*pos + 1] = b'u';
|
|
168
|
+
out[*pos + 2] = b'0';
|
|
169
|
+
out[*pos + 3] = b'0';
|
|
170
|
+
out[*pos + 4] = JSON_HEX_LOWER[(b >> 4) as usize];
|
|
171
|
+
out[*pos + 5] = JSON_HEX_LOWER[(b & 0x0f) as usize];
|
|
172
|
+
*pos += 6;
|
|
173
|
+
i += 1;
|
|
174
|
+
}
|
|
175
|
+
_ => {
|
|
176
|
+
// Bulk copy of safe bytes up to next special char
|
|
177
|
+
let remaining_slice = &remaining[i..];
|
|
178
|
+
let next_special = remaining_slice.iter().position(|&c| {
|
|
179
|
+
c < 0x20
|
|
180
|
+
});
|
|
181
|
+
match next_special {
|
|
182
|
+
Some(n) => {
|
|
183
|
+
out[*pos..*pos + n].copy_from_slice(&remaining_slice[..n]);
|
|
184
|
+
*pos += n;
|
|
185
|
+
i += n;
|
|
186
|
+
}
|
|
187
|
+
None => {
|
|
188
|
+
out[*pos..*pos + remaining_slice.len()].copy_from_slice(remaining_slice);
|
|
189
|
+
*pos += remaining_slice.len();
|
|
190
|
+
i = remaining.len();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Parse cookies from `input` and write JSON to `out` with max_pairs limit.
|
|
200
|
+
#[inline]
|
|
201
|
+
pub fn cookie_json_into_slice(input: &[u8], out: &mut [u8], max_pairs: usize) -> Result<usize> {
|
|
202
|
+
if out.len() < 2 {
|
|
203
|
+
return Err(Error::from_reason("output buffer too small for cookie JSON"));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
out[0] = b'{';
|
|
207
|
+
let mut pos = 1usize;
|
|
208
|
+
let mut count = 0usize;
|
|
209
|
+
|
|
210
|
+
for pair in input.split(|&b| b == b';') {
|
|
211
|
+
let pair = trim_ascii_whitespace(pair);
|
|
212
|
+
if pair.is_empty() {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let (name, value) = match pair.iter().position(|&b| b == b'=') {
|
|
217
|
+
Some(eq) => (&pair[..eq], &pair[eq + 1..]),
|
|
218
|
+
None => (pair, &[][..]),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
let name = trim_ascii_whitespace(name);
|
|
222
|
+
let value = trim_ascii_whitespace(value);
|
|
223
|
+
|
|
224
|
+
if name.is_empty() {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if count >= max_pairs {
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let needed = (if count == 0 { 0 } else { 1 })
|
|
233
|
+
+ 5
|
|
234
|
+
+ json_escaped_len(name)
|
|
235
|
+
+ json_escaped_len(value);
|
|
236
|
+
|
|
237
|
+
if needed > out.len().saturating_sub(pos) {
|
|
238
|
+
return Err(Error::from_reason("output buffer too small for cookie JSON"));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if count != 0 {
|
|
242
|
+
out[pos] = b',';
|
|
243
|
+
pos += 1;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
out[pos] = b'"';
|
|
247
|
+
pos += 1;
|
|
248
|
+
write_json_escaped(out, &mut pos, name);
|
|
249
|
+
out[pos] = b'"';
|
|
250
|
+
pos += 1;
|
|
251
|
+
out[pos] = b':';
|
|
252
|
+
pos += 1;
|
|
253
|
+
out[pos] = b'"';
|
|
254
|
+
pos += 1;
|
|
255
|
+
write_json_escaped(out, &mut pos, value);
|
|
256
|
+
out[pos] = b'"';
|
|
257
|
+
pos += 1;
|
|
258
|
+
|
|
259
|
+
count += 1;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if pos + 1 > out.len() {
|
|
263
|
+
return Err(Error::from_reason("output buffer too small for cookie JSON"));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
out[pos] = b'}';
|
|
267
|
+
pos += 1;
|
|
268
|
+
Ok(pos)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Parse packed query pairs and write JSON to `out` with max_pairs limit.
|
|
272
|
+
#[inline]
|
|
273
|
+
pub fn packed_pairs_to_json_into_slice(
|
|
274
|
+
packed: &[u8],
|
|
275
|
+
out: &mut [u8],
|
|
276
|
+
max_pairs: usize,
|
|
277
|
+
) -> Result<usize> {
|
|
278
|
+
if out.len() < 2 {
|
|
279
|
+
return Err(Error::from_reason("output buffer too small for packed pairs JSON"));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if packed.len() < 4 {
|
|
283
|
+
out[0..2].copy_from_slice(b"{}");
|
|
284
|
+
return Ok(2);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let count = crate::util::read_u32_le(packed, 0)? as usize;
|
|
288
|
+
|
|
289
|
+
out[0] = b'{';
|
|
290
|
+
let mut pos = 1usize;
|
|
291
|
+
let mut src = 4usize;
|
|
292
|
+
let mut written_pairs = 0usize;
|
|
293
|
+
|
|
294
|
+
for _ in 0..count {
|
|
295
|
+
if written_pairs >= max_pairs {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let key_len = crate::util::read_u32_le(packed, src)? as usize;
|
|
300
|
+
src += 4;
|
|
301
|
+
|
|
302
|
+
if src + key_len > packed.len() {
|
|
303
|
+
return Err(Error::from_reason("packed pairs: truncated key"));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let key = &packed[src..src + key_len];
|
|
307
|
+
src += key_len;
|
|
308
|
+
|
|
309
|
+
let val_len = crate::util::read_u32_le(packed, src)? as usize;
|
|
310
|
+
src += 4;
|
|
311
|
+
|
|
312
|
+
if src + val_len > packed.len() {
|
|
313
|
+
return Err(Error::from_reason("packed pairs: truncated value"));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let val = &packed[src..src + val_len];
|
|
317
|
+
src += val_len;
|
|
318
|
+
|
|
319
|
+
let needed = (if written_pairs == 0 { 0 } else { 1 })
|
|
320
|
+
+ 5
|
|
321
|
+
+ json_escaped_len(key)
|
|
322
|
+
+ json_escaped_len(val);
|
|
323
|
+
|
|
324
|
+
if needed > out.len().saturating_sub(pos) {
|
|
325
|
+
return Err(Error::from_reason("output buffer too small for packed pairs JSON"));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if written_pairs != 0 {
|
|
329
|
+
out[pos] = b',';
|
|
330
|
+
pos += 1;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
out[pos] = b'"';
|
|
334
|
+
pos += 1;
|
|
335
|
+
write_json_escaped(out, &mut pos, key);
|
|
336
|
+
out[pos] = b'"';
|
|
337
|
+
pos += 1;
|
|
338
|
+
out[pos] = b':';
|
|
339
|
+
pos += 1;
|
|
340
|
+
out[pos] = b'"';
|
|
341
|
+
pos += 1;
|
|
342
|
+
write_json_escaped(out, &mut pos, val);
|
|
343
|
+
out[pos] = b'"';
|
|
344
|
+
pos += 1;
|
|
345
|
+
|
|
346
|
+
written_pairs += 1;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if pos + 1 > out.len() {
|
|
350
|
+
return Err(Error::from_reason("output buffer too small for packed pairs JSON"));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
out[pos] = b'}';
|
|
354
|
+
pos += 1;
|
|
355
|
+
Ok(pos)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// Write the full metadata JSON envelope (requestId, path, cookies, query).
|
|
359
|
+
#[inline]
|
|
360
|
+
pub fn write_full_body_json(
|
|
361
|
+
out: &mut [u8],
|
|
362
|
+
pos: usize,
|
|
363
|
+
request_id: &[u8],
|
|
364
|
+
path: &[u8],
|
|
365
|
+
cookies_start: usize,
|
|
366
|
+
cookies_len: usize,
|
|
367
|
+
query_start: usize,
|
|
368
|
+
query_len: usize,
|
|
369
|
+
) -> usize {
|
|
370
|
+
const P1: &[u8] = b"{\"ok\":true,\"requestId\":\"";
|
|
371
|
+
const P2: &[u8] = b"\",\"path\":\"";
|
|
372
|
+
const P3: &[u8] = b"\",\"cookies\":";
|
|
373
|
+
const P4: &[u8] = b",\"query\":";
|
|
374
|
+
|
|
375
|
+
let cookies_eff_len = if cookies_len > 0 { cookies_len } else { 2 };
|
|
376
|
+
let query_eff_len = if query_len > 0 { query_len } else { 2 };
|
|
377
|
+
|
|
378
|
+
let required = P1.len()
|
|
379
|
+
+ json_escaped_len(request_id)
|
|
380
|
+
+ P2.len()
|
|
381
|
+
+ json_escaped_len(path)
|
|
382
|
+
+ P3.len()
|
|
383
|
+
+ cookies_eff_len
|
|
384
|
+
+ P4.len()
|
|
385
|
+
+ query_eff_len
|
|
386
|
+
+ 1;
|
|
387
|
+
|
|
388
|
+
let end = match pos.checked_add(required) {
|
|
389
|
+
Some(v) => v,
|
|
390
|
+
None => return 0,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if end > out.len() {
|
|
394
|
+
return 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let mut wp = pos;
|
|
398
|
+
out[wp..wp + P1.len()].copy_from_slice(P1);
|
|
399
|
+
wp += P1.len();
|
|
400
|
+
write_json_escaped(out, &mut wp, request_id);
|
|
401
|
+
out[wp..wp + P2.len()].copy_from_slice(P2);
|
|
402
|
+
wp += P2.len();
|
|
403
|
+
write_json_escaped(out, &mut wp, path);
|
|
404
|
+
out[wp..wp + P3.len()].copy_from_slice(P3);
|
|
405
|
+
wp += P3.len();
|
|
406
|
+
|
|
407
|
+
if cookies_len > 0 {
|
|
408
|
+
out.copy_within(cookies_start..cookies_start + cookies_len, wp);
|
|
409
|
+
wp += cookies_len;
|
|
410
|
+
} else {
|
|
411
|
+
out[wp..wp + 2].copy_from_slice(b"{}");
|
|
412
|
+
wp += 2;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
out[wp..wp + P4.len()].copy_from_slice(P4);
|
|
416
|
+
wp += P4.len();
|
|
417
|
+
|
|
418
|
+
if query_len > 0 {
|
|
419
|
+
out.copy_within(query_start..query_start + query_len, wp);
|
|
420
|
+
wp += query_len;
|
|
421
|
+
} else {
|
|
422
|
+
out[wp..wp + 2].copy_from_slice(b"{}");
|
|
423
|
+
wp += 2;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
out[wp] = b'}';
|
|
427
|
+
wp += 1;
|
|
428
|
+
wp - pos
|
|
429
|
+
}
|
package/rust/lib.rs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#![allow(clippy::not_unsafe_ptr_arg_deref)]
|
|
2
|
+
|
|
3
|
+
use mimalloc::MiMalloc;
|
|
4
|
+
|
|
5
|
+
#[global_allocator]
|
|
6
|
+
static GLOBAL: MiMalloc = MiMalloc;
|
|
7
|
+
|
|
8
|
+
// ── napi-rs addon modules ─────────────────────────────────────────
|
|
9
|
+
// Each module is exported to JS via #[napi]. Modules keep a pure-Rust
|
|
10
|
+
// core (no napi types in signatures) so they stay testable and
|
|
11
|
+
// composable; only the entry points use napi types.
|
|
12
|
+
pub mod output;
|
|
13
|
+
pub mod method;
|
|
14
|
+
pub mod headers;
|
|
15
|
+
pub mod json_ser;
|
|
16
|
+
pub mod cors;
|
|
17
|
+
pub mod proxy;
|
|
18
|
+
pub mod terminal;
|
|
19
|
+
|
|
20
|
+
pub mod util;
|
|
21
|
+
|
|
22
|
+
pub mod ip_trust;
|
|
23
|
+
pub mod rate_limit;
|
|
24
|
+
|
|
25
|
+
pub mod json_schema;
|
|
26
|
+
|
|
27
|
+
pub mod cookie_parser;
|
|
28
|
+
pub mod hashing;
|
|
29
|
+
pub mod hmac_sha256;
|
|
30
|
+
pub mod http_parser;
|
|
31
|
+
pub mod json_ops;
|
|
32
|
+
pub mod json_patch_ops;
|
|
33
|
+
pub mod mime_lookup;
|
|
34
|
+
pub mod query_parser;
|
|
35
|
+
pub mod random_token;
|
|
36
|
+
pub mod url_codec;
|
|
37
|
+
pub mod validation;
|
|
38
|
+
pub mod websocket;
|
|
39
|
+
|
|
40
|
+
pub mod batch;
|
|
41
|
+
|
|
42
|
+
pub mod ingress_constants;
|
|
43
|
+
|
|
44
|
+
pub mod ingress;
|
|
45
|
+
|
|
46
|
+
// ── Unit tests (cargo test) ───────────────────────────────────────
|
|
47
|
+
#[cfg(test)]
|
|
48
|
+
mod unit_tests;
|
package/rust/method.rs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// rust/method.rs — Extracted HTTP method classification
|
|
2
|
+
// Optimized for Bun 1.4: uses u64 word-at-a-time comparison with zero allocation
|
|
3
|
+
|
|
4
|
+
#[inline(always)]
|
|
5
|
+
fn load_u64_padded(bytes: &[u8]) -> u64 {
|
|
6
|
+
let mut buf = [0u8; 8];
|
|
7
|
+
let len = bytes.len().min(8);
|
|
8
|
+
buf[..len].copy_from_slice(&bytes[..len]);
|
|
9
|
+
u64::from_le_bytes(buf)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
13
|
+
pub enum MethodKind {
|
|
14
|
+
Get,
|
|
15
|
+
Head,
|
|
16
|
+
Post,
|
|
17
|
+
Put,
|
|
18
|
+
Patch,
|
|
19
|
+
Delete,
|
|
20
|
+
Options,
|
|
21
|
+
Other,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const METHOD_GET: u64 = u64::from_le_bytes(*b"GET\0\0\0\0\0");
|
|
25
|
+
const METHOD_HEAD: u64 = u64::from_le_bytes(*b"HEAD\0\0\0\0");
|
|
26
|
+
const METHOD_POST: u64 = u64::from_le_bytes(*b"POST\0\0\0\0");
|
|
27
|
+
const METHOD_PUT: u64 = u64::from_le_bytes(*b"PUT\0\0\0\0\0");
|
|
28
|
+
const METHOD_PATCH: u64 = u64::from_le_bytes(*b"PATCH\0\0\0");
|
|
29
|
+
const METHOD_DELETE: u64 = u64::from_le_bytes(*b"DELETE\0\0");
|
|
30
|
+
const METHOD_OPTIONS: u64 = u64::from_le_bytes(*b"OPTIONS\0");
|
|
31
|
+
|
|
32
|
+
impl MethodKind {
|
|
33
|
+
/// Classify HTTP method from a &str.
|
|
34
|
+
#[inline(always)]
|
|
35
|
+
pub fn from_str(method: &str) -> Self {
|
|
36
|
+
let bytes = method.as_bytes();
|
|
37
|
+
let loaded = load_u64_padded(bytes);
|
|
38
|
+
|
|
39
|
+
match bytes.len() {
|
|
40
|
+
3 if loaded == METHOD_GET => Self::Get,
|
|
41
|
+
3 if loaded == METHOD_PUT => Self::Put,
|
|
42
|
+
4 if loaded == METHOD_POST => Self::Post,
|
|
43
|
+
4 if loaded == METHOD_HEAD => Self::Head,
|
|
44
|
+
5 if loaded == METHOD_PATCH => Self::Patch,
|
|
45
|
+
6 if loaded == METHOD_DELETE => Self::Delete,
|
|
46
|
+
7 if loaded == METHOD_OPTIONS => Self::Options,
|
|
47
|
+
_ => Self::Other,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Classify HTTP method from raw bytes with case-insensitive comparison.
|
|
52
|
+
/// Avoids heap allocation from to_ascii_uppercase().
|
|
53
|
+
#[inline(always)]
|
|
54
|
+
pub fn from_bytes_ignore_case(bytes: &[u8]) -> Self {
|
|
55
|
+
let len = bytes.len();
|
|
56
|
+
if len > 7 {
|
|
57
|
+
return Self::Other;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let loaded = load_u64_padded(bytes);
|
|
61
|
+
|
|
62
|
+
// Mask the 5th bit (0x20) of all alphabetic bytes for case-insensitive comparison.
|
|
63
|
+
let lower = loaded | 0x2020_2020_2020_2020;
|
|
64
|
+
|
|
65
|
+
// Zero the bytes beyond `len` on BOTH operands. The mask must be applied
|
|
66
|
+
// to the reference constants too, otherwise the padding bytes (0x20 after
|
|
67
|
+
// the OR above) never match and every method classifies as Other.
|
|
68
|
+
let mask = match len {
|
|
69
|
+
3 => 0x00FF_FFFF_FFFF_FFFFu64,
|
|
70
|
+
4 => 0x0000_FFFF_FFFF_FFFFu64,
|
|
71
|
+
5 => 0x0000_00FF_FFFF_FFFFu64,
|
|
72
|
+
6 => 0x0000_0000_FFFF_FFFFu64,
|
|
73
|
+
7 => 0x0000_0000_00FF_FFFFu64,
|
|
74
|
+
_ => 0,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let lower_masked = lower & mask;
|
|
78
|
+
let get = (METHOD_GET | 0x2020_2020_2020_2020) & mask;
|
|
79
|
+
let put = (METHOD_PUT | 0x2020_2020_2020_2020) & mask;
|
|
80
|
+
let post = (METHOD_POST | 0x2020_2020_2020_2020) & mask;
|
|
81
|
+
let head = (METHOD_HEAD | 0x2020_2020_2020_2020) & mask;
|
|
82
|
+
let patch = (METHOD_PATCH | 0x2020_2020_2020_2020) & mask;
|
|
83
|
+
let delete = (METHOD_DELETE | 0x2020_2020_2020_2020) & mask;
|
|
84
|
+
let options = (METHOD_OPTIONS | 0x2020_2020_2020_2020) & mask;
|
|
85
|
+
|
|
86
|
+
match len {
|
|
87
|
+
3 if lower_masked == get => Self::Get,
|
|
88
|
+
3 if lower_masked == put => Self::Put,
|
|
89
|
+
4 if lower_masked == post => Self::Post,
|
|
90
|
+
4 if lower_masked == head => Self::Head,
|
|
91
|
+
5 if lower_masked == patch => Self::Patch,
|
|
92
|
+
6 if lower_masked == delete => Self::Delete,
|
|
93
|
+
7 if lower_masked == options => Self::Options,
|
|
94
|
+
_ => Self::Other,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Classify from u8 discriminator (0-6).
|
|
99
|
+
#[inline(always)]
|
|
100
|
+
pub fn from_u8(v: u8) -> Self {
|
|
101
|
+
match v {
|
|
102
|
+
0 => Self::Get,
|
|
103
|
+
1 => Self::Head,
|
|
104
|
+
2 => Self::Post,
|
|
105
|
+
3 => Self::Put,
|
|
106
|
+
4 => Self::Patch,
|
|
107
|
+
5 => Self::Delete,
|
|
108
|
+
6 => Self::Options,
|
|
109
|
+
_ => Self::Other,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Returns true if the method may carry a body.
|
|
114
|
+
#[inline(always)]
|
|
115
|
+
pub fn may_have_body(&self) -> bool {
|
|
116
|
+
matches!(self, Self::Post | Self::Put | Self::Patch | Self::Delete)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Returns u8 discriminant for packed format.
|
|
120
|
+
#[inline(always)]
|
|
121
|
+
pub fn to_u8(&self) -> u8 {
|
|
122
|
+
match self {
|
|
123
|
+
Self::Get => 0,
|
|
124
|
+
Self::Head => 1,
|
|
125
|
+
Self::Post => 2,
|
|
126
|
+
Self::Put => 3,
|
|
127
|
+
Self::Patch => 4,
|
|
128
|
+
Self::Delete => 5,
|
|
129
|
+
Self::Options => 6,
|
|
130
|
+
Self::Other => 7,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
impl MethodKind {
|
|
136
|
+
/// Bitmask representation for fast CORS method matching.
|
|
137
|
+
#[inline(always)]
|
|
138
|
+
pub fn bit(&self) -> u16 {
|
|
139
|
+
1u16 << (self.to_u8() as u16)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
use napi::bindgen_prelude::*;
|
|
2
|
+
use napi_derive::napi;
|
|
3
|
+
use phf::{phf_map, Map};
|
|
4
|
+
|
|
5
|
+
const OCTET_STREAM: &str = "application/octet-stream";
|
|
6
|
+
|
|
7
|
+
static MIME_TABLE: Map<&'static str, &'static str> = phf_map! {
|
|
8
|
+
"json" => "application/json",
|
|
9
|
+
"js" => "text/javascript",
|
|
10
|
+
"mjs" => "text/javascript",
|
|
11
|
+
"cjs" => "text/javascript",
|
|
12
|
+
"html" => "text/html",
|
|
13
|
+
"htm" => "text/html",
|
|
14
|
+
"css" => "text/css",
|
|
15
|
+
"csv" => "text/csv",
|
|
16
|
+
"xml" => "application/xml",
|
|
17
|
+
"txt" => "text/plain",
|
|
18
|
+
"png" => "image/png",
|
|
19
|
+
"jpg" => "image/jpeg",
|
|
20
|
+
"jpeg" => "image/jpeg",
|
|
21
|
+
"gif" => "image/gif",
|
|
22
|
+
"webp" => "image/webp",
|
|
23
|
+
"avif" => "image/avif",
|
|
24
|
+
"svg" => "image/svg+xml",
|
|
25
|
+
"ico" => "image/x-icon",
|
|
26
|
+
"pdf" => "application/pdf",
|
|
27
|
+
"zip" => "application/zip",
|
|
28
|
+
"gz" => "application/gzip",
|
|
29
|
+
"wasm" => "application/wasm",
|
|
30
|
+
"map" => "application/json",
|
|
31
|
+
"woff" => "font/woff",
|
|
32
|
+
"woff2" => "font/woff2",
|
|
33
|
+
"ttf" => "font/ttf",
|
|
34
|
+
"otf" => "font/otf",
|
|
35
|
+
"mp4" => "video/mp4",
|
|
36
|
+
"webm" => "video/webm",
|
|
37
|
+
"mp3" => "audio/mpeg",
|
|
38
|
+
"ogg" => "audio/ogg",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
#[napi]
|
|
42
|
+
pub fn mime_from_extension(ext: Uint8Array) -> Buffer {
|
|
43
|
+
let bytes = ext.as_ref();
|
|
44
|
+
|
|
45
|
+
let text = match std::str::from_utf8(bytes) {
|
|
46
|
+
Ok(text) => text,
|
|
47
|
+
Err(_) => return Buffer::from(OCTET_STREAM.as_bytes().to_vec()),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let text = text.trim_start_matches('.');
|
|
51
|
+
|
|
52
|
+
let mut stack = [0u8; 64];
|
|
53
|
+
|
|
54
|
+
let lower_owned: String;
|
|
55
|
+
|
|
56
|
+
let lower: &str = if text.len() <= stack.len() {
|
|
57
|
+
for (i, b) in text.bytes().enumerate() {
|
|
58
|
+
stack[i] = b.to_ascii_lowercase();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
match std::str::from_utf8(&stack[..text.len()]) {
|
|
62
|
+
Ok(s) => s,
|
|
63
|
+
Err(_) => return Buffer::from(OCTET_STREAM.as_bytes().to_vec()),
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
lower_owned = text.to_ascii_lowercase();
|
|
67
|
+
&lower_owned
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if let Some(mime) = MIME_TABLE.get(lower) {
|
|
71
|
+
return Buffer::from(mime.as_bytes().to_vec());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let guessed = mime_guess::from_ext(lower).first_or_octet_stream();
|
|
75
|
+
Buffer::from(guessed.essence_str().as_bytes().to_vec())
|
|
76
|
+
}
|