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/batch.rs
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// rust/batch.rs — v2: DIRECT WRITE, NO INTERMEDIATE Vec<Vec<u8>>
|
|
2
|
+
use napi::bindgen_prelude::*;
|
|
3
|
+
use napi_derive::napi;
|
|
4
|
+
|
|
5
|
+
use crate::json_ops::{json_sum_ids_bytes, json_valid_bytes};
|
|
6
|
+
use crate::util::{
|
|
7
|
+
count_batch, should_parallelize, sum_batch_i64, total_bytes, unpack,
|
|
8
|
+
};
|
|
9
|
+
use crate::validation::{
|
|
10
|
+
validate_email_bytes, validate_ipv4_bytes, validate_ipv6_bytes, validate_uuid_bytes,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// ── Internal helpers (direct-write variants) ──
|
|
14
|
+
|
|
15
|
+
/// Direct-write JSON sum batch — eliminates intermediate Vec<i64>.
|
|
16
|
+
#[inline]
|
|
17
|
+
fn json_sum_batch_direct(items: &[&[u8]], out: &mut Vec<u8>) {
|
|
18
|
+
let n = items.len();
|
|
19
|
+
out.reserve(4 + n * 8);
|
|
20
|
+
out.extend_from_slice(&(n as u32).to_le_bytes());
|
|
21
|
+
let start = out.len();
|
|
22
|
+
out.resize(start + n * 8, 0);
|
|
23
|
+
|
|
24
|
+
// Slice the output buffer to write directly into it.
|
|
25
|
+
let out_slice = &mut out[start..];
|
|
26
|
+
|
|
27
|
+
if should_parallelize(items.len(), total_bytes(items)) {
|
|
28
|
+
use rayon::prelude::*;
|
|
29
|
+
let chunk_size = 256;
|
|
30
|
+
|
|
31
|
+
// Use par_chunks_mut to get mutable disjoint slices of the output buffer.
|
|
32
|
+
out_slice.par_chunks_mut(chunk_size * 8).enumerate().for_each(|(ci, out_chunk)| {
|
|
33
|
+
let num_items = out_chunk.len() / 8;
|
|
34
|
+
let item_start = ci * chunk_size;
|
|
35
|
+
let item_end = (item_start + num_items).min(items.len());
|
|
36
|
+
|
|
37
|
+
for (i, item) in items[item_start..item_end].iter().enumerate() {
|
|
38
|
+
let sum = json_sum_ids_bytes(item).unwrap_or(0);
|
|
39
|
+
out_chunk[i * 8..(i + 1) * 8].copy_from_slice(&sum.to_le_bytes());
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
for (i, item) in items.iter().enumerate() {
|
|
44
|
+
let sum = json_sum_ids_bytes(item).unwrap_or(0);
|
|
45
|
+
out_slice[i * 8..(i + 1) * 8].copy_from_slice(&sum.to_le_bytes());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Direct-write parse batch — eliminates intermediate Vec<Vec<u8>>.
|
|
51
|
+
#[inline]
|
|
52
|
+
fn parse_batch_direct<F>(items: &[&[u8]], out: &mut Vec<u8>, f: F)
|
|
53
|
+
where
|
|
54
|
+
F: Fn(&[u8]) -> Result<Vec<u8>> + Sync,
|
|
55
|
+
{
|
|
56
|
+
let n = items.len();
|
|
57
|
+
out.reserve(4 + n * 32);
|
|
58
|
+
out.extend_from_slice(&(n as u32).to_le_bytes());
|
|
59
|
+
for item in items {
|
|
60
|
+
let r = f(item).unwrap_or_default();
|
|
61
|
+
out.extend_from_slice(&(r.len() as u32).to_le_bytes());
|
|
62
|
+
out.extend_from_slice(&r);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[inline]
|
|
67
|
+
fn query_parse_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
|
|
68
|
+
crate::query_parser::query_parse_packed_vec(input)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[inline]
|
|
72
|
+
fn cookie_parse_packed_vec(input: &[u8]) -> Vec<u8> {
|
|
73
|
+
crate::cookie_parser::cookie_parse_packed_vec(input)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[inline]
|
|
77
|
+
fn http_parse_request_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
|
|
78
|
+
crate::http_parser::http_parse_request_packed_vec(input)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[inline]
|
|
82
|
+
fn count_batch_data(data: &[u8], f: impl Fn(&[u8]) -> bool + Sync, chunk_items: usize) -> Result<u32> {
|
|
83
|
+
let items = unpack(data)?;
|
|
84
|
+
Ok(count_batch(&items, f, chunk_items) as u32)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[inline]
|
|
88
|
+
fn sum_ids_batch_data(data: &[u8], chunk_items: usize) -> Result<i64> {
|
|
89
|
+
let items = unpack(data)?;
|
|
90
|
+
Ok(sum_batch_i64(&items, |item| json_sum_ids_bytes(item).unwrap_or(0), chunk_items))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[inline]
|
|
94
|
+
fn total_len_batch_data(data: &[u8], f: impl Fn(&[u8]) -> Result<Vec<u8>> + Sync, chunk_items: usize) -> Result<u32> {
|
|
95
|
+
let items = unpack(data)?;
|
|
96
|
+
let chunk_items = chunk_items.max(64);
|
|
97
|
+
let total: usize = if should_parallelize(items.len(), total_bytes(&items)) {
|
|
98
|
+
use rayon::prelude::*;
|
|
99
|
+
items.par_chunks(chunk_items)
|
|
100
|
+
.map(|chunk| chunk.iter().map(|item| f(item).map(|v| v.len()).unwrap_or(0)).sum::<usize>())
|
|
101
|
+
.sum()
|
|
102
|
+
} else {
|
|
103
|
+
items.iter().map(|item| f(item).map(|v| v.len()).unwrap_or(0)).sum()
|
|
104
|
+
};
|
|
105
|
+
Ok(total as u32)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Packed input → packed output ──
|
|
109
|
+
|
|
110
|
+
#[inline]
|
|
111
|
+
fn json_valid_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
112
|
+
let items = unpack(data)?;
|
|
113
|
+
Ok(crate::util::validation_bitset_chunked(&items, json_valid_bytes, 512))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[inline]
|
|
117
|
+
fn validate_email_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
118
|
+
let items = unpack(data)?;
|
|
119
|
+
Ok(crate::util::validation_bitset_chunked(&items, validate_email_bytes, 4096))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#[inline]
|
|
123
|
+
fn validate_uuid_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
124
|
+
let items = unpack(data)?;
|
|
125
|
+
Ok(crate::util::validation_bitset_chunked(&items, validate_uuid_bytes, 4096))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[inline]
|
|
129
|
+
fn validate_ipv4_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
130
|
+
let items = unpack(data)?;
|
|
131
|
+
Ok(crate::util::validation_bitset_chunked(&items, validate_ipv4_bytes, 4096))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[inline]
|
|
135
|
+
fn validate_ipv6_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
136
|
+
let items = unpack(data)?;
|
|
137
|
+
Ok(crate::util::validation_bitset_chunked(&items, validate_ipv6_bytes, 4096))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[inline]
|
|
141
|
+
fn json_sum_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
142
|
+
let items = unpack(data)?;
|
|
143
|
+
let mut out = Vec::with_capacity(4 + items.len() * 8);
|
|
144
|
+
json_sum_batch_direct(&items, &mut out);
|
|
145
|
+
Ok(out)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[inline]
|
|
149
|
+
fn query_parse_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
150
|
+
let items = unpack(data)?;
|
|
151
|
+
let mut out = Vec::with_capacity(items.len() * 32);
|
|
152
|
+
parse_batch_direct(&items, &mut out, query_parse_packed_vec);
|
|
153
|
+
Ok(out)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[inline]
|
|
157
|
+
fn cookie_parse_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
158
|
+
let items = unpack(data)?;
|
|
159
|
+
let mut out = Vec::with_capacity(items.len() * 32);
|
|
160
|
+
parse_batch_direct(&items, &mut out, |i| Ok(cookie_parse_packed_vec(i)));
|
|
161
|
+
Ok(out)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#[inline]
|
|
165
|
+
fn http_parse_request_batch_bytes(data: &[u8]) -> Result<Vec<u8>> {
|
|
166
|
+
let items = unpack(data)?;
|
|
167
|
+
let mut out = Vec::with_capacity(items.len() * 64);
|
|
168
|
+
parse_batch_direct(&items, &mut out, http_parse_request_packed_vec);
|
|
169
|
+
Ok(out)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Sync packed batch APIs ──
|
|
173
|
+
|
|
174
|
+
#[napi] pub fn json_valid_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(json_valid_batch_bytes(input.as_ref())?)) }
|
|
175
|
+
#[napi] pub fn validate_email_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_email_batch_bytes(input.as_ref())?)) }
|
|
176
|
+
#[napi] pub fn validate_uuid_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_uuid_batch_bytes(input.as_ref())?)) }
|
|
177
|
+
#[napi] pub fn validate_ipv4_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_ipv4_batch_bytes(input.as_ref())?)) }
|
|
178
|
+
#[napi] pub fn validate_ipv6_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(validate_ipv6_batch_bytes(input.as_ref())?)) }
|
|
179
|
+
#[napi] pub fn json_sum_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(json_sum_batch_bytes(input.as_ref())?)) }
|
|
180
|
+
#[napi] pub fn query_parse_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(query_parse_batch_bytes(input.as_ref())?)) }
|
|
181
|
+
#[napi] pub fn cookie_parse_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(cookie_parse_batch_bytes(input.as_ref())?)) }
|
|
182
|
+
#[napi] pub fn http_parse_request_batch_packed(input: Uint8Array) -> Result<Buffer> { Ok(Buffer::from(http_parse_request_batch_bytes(input.as_ref())?)) }
|
|
183
|
+
|
|
184
|
+
// ── Aggregate sync packed batch APIs ──
|
|
185
|
+
|
|
186
|
+
#[napi] pub fn json_valid_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), json_valid_bytes, 512) }
|
|
187
|
+
#[napi] pub fn validate_email_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_email_bytes, 4096) }
|
|
188
|
+
#[napi] pub fn validate_uuid_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_uuid_bytes, 4096) }
|
|
189
|
+
#[napi] pub fn validate_ipv4_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_ipv4_bytes, 4096) }
|
|
190
|
+
#[napi] pub fn validate_ipv6_batch_count_packed(input: Uint8Array) -> Result<u32> { count_batch_data(input.as_ref(), validate_ipv6_bytes, 4096) }
|
|
191
|
+
#[napi] pub fn json_sum_batch_total_packed(input: Uint8Array) -> Result<i64> { sum_ids_batch_data(input.as_ref(), 512) }
|
|
192
|
+
#[napi] pub fn query_parse_batch_total_len_packed(input: Uint8Array) -> Result<u32> { total_len_batch_data(input.as_ref(), query_parse_packed_vec, 256) }
|
|
193
|
+
#[napi] pub fn cookie_parse_batch_total_len_packed(input: Uint8Array) -> Result<u32> { total_len_batch_data(input.as_ref(), |i| Ok(cookie_parse_packed_vec(i)), 256) }
|
|
194
|
+
#[napi] pub fn http_parse_request_batch_total_len_packed(input: Uint8Array) -> Result<u32> { total_len_batch_data(input.as_ref(), http_parse_request_packed_vec, 256) }
|
|
195
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// rust/cookie_parser.rs — Unified zero-alloc cookie parser
|
|
2
|
+
// Eliminated the _vec variant; all callers use the single _into_slice path.
|
|
3
|
+
// Upper-bound pre-allocation for callers that need a Vec.
|
|
4
|
+
|
|
5
|
+
use crate::util::{trim_ascii_whitespace, write_bytes, write_u32_le};
|
|
6
|
+
use napi::bindgen_prelude::*;
|
|
7
|
+
use napi_derive::napi;
|
|
8
|
+
|
|
9
|
+
/// Parse cookies into a caller-provided slice (zero-alloc).
|
|
10
|
+
/// Output format: [u32 count] repeated { [u32 name_len] [name] [u32 value_len] [value] }
|
|
11
|
+
#[inline]
|
|
12
|
+
pub fn cookie_parse_packed_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
|
|
13
|
+
let mut pos = 0usize;
|
|
14
|
+
write_u32_le(out, &mut pos, 0)?;
|
|
15
|
+
|
|
16
|
+
let mut count = 0u32;
|
|
17
|
+
|
|
18
|
+
for pair in input.split(|&b| b == b';') {
|
|
19
|
+
let pair = trim_ascii_whitespace(pair);
|
|
20
|
+
if pair.is_empty() { continue; }
|
|
21
|
+
|
|
22
|
+
let (name, value) = match pair.iter().position(|&b| b == b'=') {
|
|
23
|
+
Some(eq) => (&pair[..eq], &pair[eq + 1..]),
|
|
24
|
+
None => (pair, &[][..]),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let name = trim_ascii_whitespace(name);
|
|
28
|
+
let value = trim_ascii_whitespace(value);
|
|
29
|
+
|
|
30
|
+
if name.is_empty() { continue; }
|
|
31
|
+
|
|
32
|
+
write_u32_le(out, &mut pos, name.len() as u32)?;
|
|
33
|
+
write_bytes(out, &mut pos, name)?;
|
|
34
|
+
|
|
35
|
+
write_u32_le(out, &mut pos, value.len() as u32)?;
|
|
36
|
+
write_bytes(out, &mut pos, value)?;
|
|
37
|
+
|
|
38
|
+
count += 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
out[0..4].copy_from_slice(&count.to_le_bytes());
|
|
42
|
+
Ok(pos)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Allocate and parse cookies. Uses conservative upper bound to avoid a pre-scan.
|
|
46
|
+
#[inline]
|
|
47
|
+
pub fn cookie_parse_packed_vec(input: &[u8]) -> Vec<u8> {
|
|
48
|
+
let upper_bound = input.len().saturating_mul(9).saturating_add(16);
|
|
49
|
+
let mut out = vec![0u8; upper_bound];
|
|
50
|
+
match cookie_parse_packed_into_slice(input, &mut out) {
|
|
51
|
+
Ok(written) => {
|
|
52
|
+
out.truncate(written);
|
|
53
|
+
out
|
|
54
|
+
}
|
|
55
|
+
Err(_) => {
|
|
56
|
+
// Fall back to VecWriter if slice is too small (rare).
|
|
57
|
+
vec![0u8; 4] // empty result
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[napi]
|
|
63
|
+
pub fn cookie_parse_packed(input: Uint8Array) -> Result<Buffer> {
|
|
64
|
+
Ok(Buffer::from(cookie_parse_packed_vec(input.as_ref())))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[napi]
|
|
68
|
+
pub fn cookie_parse_packed_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
|
|
69
|
+
crate::util::run_packed_into(&input, &mut output, cookie_parse_packed_into_slice)
|
|
70
|
+
}
|
package/rust/cors.rs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// rust/cors.rs — CORS engine extracted from ingress.rs
|
|
2
|
+
// Pre-computes method bitmask for O(1) method matching
|
|
3
|
+
|
|
4
|
+
use crate::headers::HeaderRefs;
|
|
5
|
+
use crate::method::MethodKind;
|
|
6
|
+
use crate::util::trim_ascii_whitespace;
|
|
7
|
+
use napi::{Error, Result, Status};
|
|
8
|
+
use napi_derive::napi;
|
|
9
|
+
|
|
10
|
+
// ── CORS engine ───────────────────────────────────────────────────
|
|
11
|
+
#[derive(Clone)]
|
|
12
|
+
pub enum CorsMode {
|
|
13
|
+
Disabled,
|
|
14
|
+
Wildcard,
|
|
15
|
+
Allowlist(Vec<Box<[u8]>>),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[derive(Clone)]
|
|
19
|
+
pub struct CorsEngine {
|
|
20
|
+
mode: CorsMode,
|
|
21
|
+
credentials: bool,
|
|
22
|
+
methods_wildcard: bool,
|
|
23
|
+
// Bitmask of allowed methods for O(1) lookup
|
|
24
|
+
method_bits: u16,
|
|
25
|
+
headers_wildcard: bool,
|
|
26
|
+
headers: Vec<Box<[u8]>>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub struct CorsEvaluation {
|
|
30
|
+
pub allowed: bool,
|
|
31
|
+
pub preflight: bool,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl CorsEngine {
|
|
35
|
+
pub fn disabled() -> Self {
|
|
36
|
+
Self {
|
|
37
|
+
mode: CorsMode::Disabled,
|
|
38
|
+
credentials: false,
|
|
39
|
+
methods_wildcard: false,
|
|
40
|
+
method_bits: 0,
|
|
41
|
+
headers_wildcard: false,
|
|
42
|
+
headers: Vec::new(),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub fn from_options(opts: Option<CorsOptions>) -> Result<Self> {
|
|
47
|
+
let Some(opts) = opts else {
|
|
48
|
+
return Ok(Self::disabled());
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let credentials = opts.allow_credentials.unwrap_or(false);
|
|
52
|
+
|
|
53
|
+
let mode = match opts.allow_origin {
|
|
54
|
+
Some(list) if !list.is_empty() => {
|
|
55
|
+
if list.iter().any(|o| o == "*") {
|
|
56
|
+
if credentials {
|
|
57
|
+
return Err(Error::new(
|
|
58
|
+
Status::InvalidArg,
|
|
59
|
+
"CORS allowCredentials cannot be used with allowOrigin ['*']",
|
|
60
|
+
));
|
|
61
|
+
}
|
|
62
|
+
CorsMode::Wildcard
|
|
63
|
+
} else {
|
|
64
|
+
CorsMode::Allowlist(
|
|
65
|
+
list.iter()
|
|
66
|
+
.map(|s| s.as_bytes().to_vec().into_boxed_slice())
|
|
67
|
+
.collect(),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
_ => {
|
|
72
|
+
if credentials {
|
|
73
|
+
return Err(Error::new(
|
|
74
|
+
Status::InvalidArg,
|
|
75
|
+
"CORS allowCredentials requires an explicit allowOrigin list",
|
|
76
|
+
));
|
|
77
|
+
}
|
|
78
|
+
CorsMode::Wildcard
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let (methods_wildcard, method_bits) = parse_allowed_methods_bitset(opts.allow_methods);
|
|
83
|
+
let (headers_wildcard, headers) = parse_allowed_headers(opts.allow_headers);
|
|
84
|
+
|
|
85
|
+
Ok(Self {
|
|
86
|
+
mode,
|
|
87
|
+
credentials,
|
|
88
|
+
methods_wildcard,
|
|
89
|
+
method_bits,
|
|
90
|
+
headers_wildcard,
|
|
91
|
+
headers,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[inline]
|
|
96
|
+
pub fn evaluate(&self, method: MethodKind, headers: &HeaderRefs) -> CorsEvaluation {
|
|
97
|
+
let origin = match headers.origin() {
|
|
98
|
+
Some(o) => o,
|
|
99
|
+
None => {
|
|
100
|
+
return CorsEvaluation {
|
|
101
|
+
allowed: false,
|
|
102
|
+
preflight: false,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
let preflight = method == MethodKind::Options && headers.has_acrm();
|
|
108
|
+
|
|
109
|
+
let mut allowed = match &self.mode {
|
|
110
|
+
CorsMode::Disabled => false,
|
|
111
|
+
CorsMode::Wildcard => !self.credentials,
|
|
112
|
+
CorsMode::Allowlist(list) => {
|
|
113
|
+
let origin_trim = trim_ascii_whitespace(origin);
|
|
114
|
+
list.iter().any(|allowed| &**allowed == origin_trim)
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if allowed {
|
|
119
|
+
if preflight {
|
|
120
|
+
allowed = self.preflight_method_allowed(headers.acrm())
|
|
121
|
+
&& self.headers_allowed(headers.acrh());
|
|
122
|
+
} else {
|
|
123
|
+
allowed = self.method_allowed_kind(method);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
CorsEvaluation { allowed, preflight }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[inline(always)]
|
|
131
|
+
fn method_allowed_kind(&self, kind: MethodKind) -> bool {
|
|
132
|
+
if self.methods_wildcard {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if self.method_bits == 0 {
|
|
137
|
+
return matches!(kind, MethodKind::Get | MethodKind::Head | MethodKind::Post);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
(self.method_bits & kind.bit()) != 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[inline]
|
|
144
|
+
fn preflight_method_allowed(&self, acrm: Option<&[u8]>) -> bool {
|
|
145
|
+
let Some(acrm) = acrm else {
|
|
146
|
+
return false;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
let trimmed = trim_ascii_whitespace(acrm);
|
|
150
|
+
if trimmed.is_empty() {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let kind = MethodKind::from_bytes_ignore_case(trimmed);
|
|
155
|
+
self.method_allowed_kind(kind)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[inline]
|
|
159
|
+
fn headers_allowed(&self, acrh: Option<&[u8]>) -> bool {
|
|
160
|
+
let Some(acrh) = acrh else {
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if self.headers_wildcard {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for requested in acrh.split(|&b| b == b',') {
|
|
169
|
+
let name = trim_ascii_whitespace(requested);
|
|
170
|
+
if name.is_empty() {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let lower = name.to_ascii_lowercase();
|
|
175
|
+
|
|
176
|
+
if self.headers.is_empty() {
|
|
177
|
+
if !is_cors_safelisted_request_header(lower.as_slice()) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
} else if !self
|
|
181
|
+
.headers
|
|
182
|
+
.iter()
|
|
183
|
+
.any(|allowed| allowed.as_ref() == lower.as_slice())
|
|
184
|
+
{
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
true
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
fn is_cors_safelisted_request_header(name: &[u8]) -> bool {
|
|
194
|
+
matches!(
|
|
195
|
+
name,
|
|
196
|
+
b"accept" | b"accept-language" | b"content-language" | b"content-type"
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn parse_allowed_methods_bitset(list: Option<Vec<String>>) -> (bool, u16) {
|
|
201
|
+
let Some(list) = list else {
|
|
202
|
+
return (false, 0);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
let mut bits = 0u16;
|
|
206
|
+
|
|
207
|
+
for raw in list {
|
|
208
|
+
let trimmed = raw.trim();
|
|
209
|
+
|
|
210
|
+
if trimmed == "*" {
|
|
211
|
+
return (true, 0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if trimmed.is_empty() {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let upper = trimmed.to_ascii_uppercase();
|
|
219
|
+
let kind = MethodKind::from_str(&upper);
|
|
220
|
+
|
|
221
|
+
if kind != MethodKind::Other {
|
|
222
|
+
bits |= kind.bit();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
(false, bits)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn parse_allowed_headers(list: Option<Vec<String>>) -> (bool, Vec<Box<[u8]>>) {
|
|
230
|
+
let Some(list) = list else {
|
|
231
|
+
return (false, Vec::new());
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
let mut headers = Vec::new();
|
|
235
|
+
|
|
236
|
+
for raw in list {
|
|
237
|
+
let trimmed = raw.trim();
|
|
238
|
+
|
|
239
|
+
if trimmed == "*" {
|
|
240
|
+
return (true, Vec::new());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if trimmed.is_empty() {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
headers.push(trimmed.to_ascii_lowercase().into_bytes().into_boxed_slice());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
(false, headers)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── NAPI option struct ───────────────────────────────────────────
|
|
254
|
+
#[napi(object)]
|
|
255
|
+
#[derive(Clone)]
|
|
256
|
+
pub struct CorsOptions {
|
|
257
|
+
pub allow_origin: Option<Vec<String>>,
|
|
258
|
+
pub allow_methods: Option<Vec<String>>,
|
|
259
|
+
pub allow_headers: Option<Vec<String>>,
|
|
260
|
+
pub expose_headers: Option<Vec<String>>,
|
|
261
|
+
pub allow_credentials: Option<bool>,
|
|
262
|
+
pub max_age: Option<u32>,
|
|
263
|
+
}
|
package/rust/hashing.rs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
use napi::bindgen_prelude::*;
|
|
2
|
+
use napi_derive::napi;
|
|
3
|
+
|
|
4
|
+
use crate::util::should_parallelize;
|
|
5
|
+
use xxhash_rust::xxh3::{xxh3_64, xxh3_64_with_seed, Xxh3};
|
|
6
|
+
|
|
7
|
+
pub const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
|
|
8
|
+
pub const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
|
|
9
|
+
|
|
10
|
+
// ── FNV-1a (byte-by-byte, correct per spec) ──────────────────────
|
|
11
|
+
|
|
12
|
+
/// FNV-1a in the "continue" style. FNV-1a is inherently byte-serial,
|
|
13
|
+
/// so we process each byte individually (this is the correct algorithm).
|
|
14
|
+
#[inline]
|
|
15
|
+
pub fn fnv1a64_continue(mut hash: u64, input: &[u8]) -> u64 {
|
|
16
|
+
for &b in input {
|
|
17
|
+
hash ^= u64::from(b);
|
|
18
|
+
hash = hash.wrapping_mul(FNV_PRIME);
|
|
19
|
+
}
|
|
20
|
+
hash
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// FNV-1a on a full byte slice, with fast path for small inputs.
|
|
24
|
+
#[inline]
|
|
25
|
+
pub fn fnv1a64_bytes(input: &[u8]) -> u64 {
|
|
26
|
+
fnv1a64_continue(FNV_OFFSET_BASIS, input)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── XXHash wrappers ────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
#[inline]
|
|
32
|
+
pub fn fast_hash_bytes(input: &[u8]) -> u64 {
|
|
33
|
+
xxh3_64(input)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[inline]
|
|
37
|
+
pub fn fast_hash_seeded(input: &[u8], seed: u64) -> u64 {
|
|
38
|
+
xxh3_64_with_seed(input, seed)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Compute a composite cache key from method+path+query.
|
|
42
|
+
#[inline]
|
|
43
|
+
pub fn fast_hash_cache_key(method: &[u8], path: &[u8], query: &[u8]) -> u64 {
|
|
44
|
+
let mut h = Xxh3::with_seed(0x9E37_79B9_7F4A_7C15);
|
|
45
|
+
h.update(method);
|
|
46
|
+
h.update(&[0]);
|
|
47
|
+
h.update(path);
|
|
48
|
+
h.update(&[0]);
|
|
49
|
+
h.update(query);
|
|
50
|
+
h.digest()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Cow helpers for lazy / cached hashing ──────────────────────────
|
|
54
|
+
|
|
55
|
+
/// A lazy hash value that can be computed once and cached.
|
|
56
|
+
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
57
|
+
pub struct LazyHash(u64);
|
|
58
|
+
|
|
59
|
+
impl LazyHash {
|
|
60
|
+
#[inline(always)]
|
|
61
|
+
pub fn new(value: u64) -> Self {
|
|
62
|
+
Self(value)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[inline(always)]
|
|
66
|
+
pub fn get(&self) -> u64 {
|
|
67
|
+
self.0
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Napi exports ───────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
#[napi]
|
|
74
|
+
pub fn crc32(input: Uint8Array) -> u32 {
|
|
75
|
+
crc32fast::hash(input.as_ref())
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#[napi(js_name = "fnv1a64")]
|
|
79
|
+
pub fn fnv1a64(input: Uint8Array) -> u64 {
|
|
80
|
+
fnv1a64_bytes(input.as_ref())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// CRC32 batch — zero-alloc inline iteration (no Vec allocation from unpack).
|
|
84
|
+
#[napi]
|
|
85
|
+
pub fn crc32_batch_packed(input: Uint8Array) -> Result<Buffer> {
|
|
86
|
+
let data = input.as_ref();
|
|
87
|
+
if data.len() < 4 {
|
|
88
|
+
return Err(Error::from_reason("packed buffer: missing count"));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
|
92
|
+
|
|
93
|
+
// Validate count early
|
|
94
|
+
let max_possible = (data.len() - 4) / 4;
|
|
95
|
+
if count > max_possible {
|
|
96
|
+
return Err(Error::from_reason("packed buffer: impossible item count"));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let mut out = Vec::with_capacity(4 + count * 4);
|
|
100
|
+
out.extend_from_slice(&(count as u32).to_le_bytes());
|
|
101
|
+
out.resize(4 + count * 4, 0);
|
|
102
|
+
|
|
103
|
+
let out_slice = &mut out[4..];
|
|
104
|
+
|
|
105
|
+
if should_parallelize(count, data.len().saturating_sub(4)) {
|
|
106
|
+
use rayon::prelude::*;
|
|
107
|
+
|
|
108
|
+
// Pre-compute offsets so we can iterate in parallel without allocations
|
|
109
|
+
let offsets: Vec<(usize, usize)> = {
|
|
110
|
+
let mut offsets = Vec::with_capacity(count);
|
|
111
|
+
let mut offset = 4usize;
|
|
112
|
+
for _ in 0..count {
|
|
113
|
+
if offset + 4 > data.len() {
|
|
114
|
+
return Err(Error::from_reason("packed buffer: truncated length"));
|
|
115
|
+
}
|
|
116
|
+
let len = u32::from_le_bytes([
|
|
117
|
+
data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
|
|
118
|
+
]) as usize;
|
|
119
|
+
offset += 4;
|
|
120
|
+
let end = offset + len;
|
|
121
|
+
if end > data.len() {
|
|
122
|
+
return Err(Error::from_reason("packed buffer: truncated item"));
|
|
123
|
+
}
|
|
124
|
+
offsets.push((offset, len));
|
|
125
|
+
offset = end;
|
|
126
|
+
}
|
|
127
|
+
offsets
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const CHUNK_ITEMS: usize = 256;
|
|
131
|
+
|
|
132
|
+
out_slice
|
|
133
|
+
.par_chunks_mut(CHUNK_ITEMS * 4)
|
|
134
|
+
.enumerate()
|
|
135
|
+
.for_each(|(ci, chunk)| {
|
|
136
|
+
let num_items = chunk.len() / 4;
|
|
137
|
+
let start = ci * CHUNK_ITEMS;
|
|
138
|
+
for (i, item) in offsets[start..start + num_items].iter().enumerate() {
|
|
139
|
+
let crc = crc32fast::hash(&data[item.0..item.0 + item.1]);
|
|
140
|
+
chunk[i * 4..(i + 1) * 4].copy_from_slice(&crc.to_le_bytes());
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
let mut offset = 4usize;
|
|
145
|
+
for i in 0..count {
|
|
146
|
+
if offset + 4 > data.len() {
|
|
147
|
+
return Err(Error::from_reason("packed buffer: truncated length"));
|
|
148
|
+
}
|
|
149
|
+
let len = u32::from_le_bytes([
|
|
150
|
+
data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
|
|
151
|
+
]) as usize;
|
|
152
|
+
offset += 4;
|
|
153
|
+
let end = offset + len;
|
|
154
|
+
if end > data.len() {
|
|
155
|
+
return Err(Error::from_reason("packed buffer: truncated item"));
|
|
156
|
+
}
|
|
157
|
+
let crc = crc32fast::hash(&data[offset..end]);
|
|
158
|
+
out_slice[i * 4..(i + 1) * 4].copy_from_slice(&crc.to_le_bytes());
|
|
159
|
+
offset = end;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
Ok(Buffer::from(out))
|
|
164
|
+
}
|