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,140 @@
|
|
|
1
|
+
// rust/http/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::bytes::cookie_pairs;
|
|
6
|
+
use crate::util::{write_bytes, write_u32_le};
|
|
7
|
+
use napi::bindgen_prelude::*;
|
|
8
|
+
use napi_derive::napi;
|
|
9
|
+
|
|
10
|
+
/// Parse cookies into a caller-provided slice (zero-alloc).
|
|
11
|
+
/// Output format: [u32 count] repeated { [u32 name_len] [name] [u32 value_len] [value] }
|
|
12
|
+
#[inline]
|
|
13
|
+
pub fn cookie_parse_packed_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
|
|
14
|
+
let mut pos = 0usize;
|
|
15
|
+
write_u32_le(out, &mut pos, 0)?;
|
|
16
|
+
|
|
17
|
+
let mut count = 0u32;
|
|
18
|
+
|
|
19
|
+
for (name, value) in cookie_pairs(input) {
|
|
20
|
+
write_u32_le(out, &mut pos, name.len() as u32)?;
|
|
21
|
+
write_bytes(out, &mut pos, name)?;
|
|
22
|
+
|
|
23
|
+
write_u32_le(out, &mut pos, value.len() as u32)?;
|
|
24
|
+
write_bytes(out, &mut pos, value)?;
|
|
25
|
+
|
|
26
|
+
count += 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
out[0..4].copy_from_slice(&count.to_le_bytes());
|
|
30
|
+
Ok(pos)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Compute the EXACT packed output size for `input` WITHOUT writing — the
|
|
34
|
+
/// "needed-size" pass for the C-ABI convention. Mirrors
|
|
35
|
+
/// [`cookie_parse_packed_into_slice`] (trim + DQUOTE-unwrap, no decode), so the
|
|
36
|
+
/// reported size is byte-exact.
|
|
37
|
+
#[inline]
|
|
38
|
+
pub fn cookie_parse_packed_size(input: &[u8]) -> Result<usize> {
|
|
39
|
+
let mut size = 4usize; // count prefix
|
|
40
|
+
for (name, value) in crate::util::bytes::cookie_pairs(input) {
|
|
41
|
+
size += 4 + name.len();
|
|
42
|
+
size += 4 + value.len();
|
|
43
|
+
}
|
|
44
|
+
Ok(size)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Allocate and parse cookies. Uses conservative upper bound to avoid a pre-scan.
|
|
48
|
+
#[inline]
|
|
49
|
+
pub fn cookie_parse_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
|
|
50
|
+
let upper_bound = input.len().saturating_mul(9).saturating_add(16);
|
|
51
|
+
let mut out = vec![0u8; upper_bound];
|
|
52
|
+
let written = cookie_parse_packed_into_slice(input, &mut out)?;
|
|
53
|
+
out.truncate(written);
|
|
54
|
+
Ok(out)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#[napi]
|
|
58
|
+
pub fn cookie_parse_packed(input: Uint8Array) -> Result<Buffer> {
|
|
59
|
+
cookie_parse_packed_vec(input.as_ref()).map(Buffer::from)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[napi]
|
|
63
|
+
pub fn cookie_parse_packed_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
|
|
64
|
+
crate::util::run_packed_into(&input, &mut output, cookie_parse_packed_into_slice)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[cfg(test)]
|
|
68
|
+
mod tests {
|
|
69
|
+
use super::cookie_parse_packed_vec;
|
|
70
|
+
use crate::test_support::decode_packed_pairs;
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn cookie_parse_basic() {
|
|
74
|
+
let packed = cookie_parse_packed_vec(b"a=1; b=2").unwrap();
|
|
75
|
+
let pairs = decode_packed_pairs(&packed);
|
|
76
|
+
assert_eq!(pairs.len(), 2);
|
|
77
|
+
assert_eq!(pairs[0], (b"a".to_vec(), b"1".to_vec()));
|
|
78
|
+
assert_eq!(pairs[1], (b"b".to_vec(), b"2".to_vec()));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[test]
|
|
82
|
+
fn cookie_parse_trims_whitespace() {
|
|
83
|
+
let packed = cookie_parse_packed_vec(b" a = 1 ; b = hello world ").unwrap();
|
|
84
|
+
let pairs = decode_packed_pairs(&packed);
|
|
85
|
+
assert_eq!(pairs.len(), 2);
|
|
86
|
+
assert_eq!(pairs[0], (b"a".to_vec(), b"1".to_vec()));
|
|
87
|
+
assert_eq!(pairs[1], (b"b".to_vec(), b"hello world".to_vec()));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[test]
|
|
91
|
+
fn cookie_parse_skips_empty_name() {
|
|
92
|
+
let packed = cookie_parse_packed_vec(b"=1; =2; ok=3").unwrap();
|
|
93
|
+
let pairs = decode_packed_pairs(&packed);
|
|
94
|
+
assert_eq!(pairs.len(), 1);
|
|
95
|
+
assert_eq!(pairs[0], (b"ok".to_vec(), b"3".to_vec()));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[test]
|
|
99
|
+
fn cookie_parse_empty_value() {
|
|
100
|
+
let packed = cookie_parse_packed_vec(b"session=").unwrap();
|
|
101
|
+
let pairs = decode_packed_pairs(&packed);
|
|
102
|
+
assert_eq!(pairs.len(), 1);
|
|
103
|
+
assert_eq!(pairs[0], (b"session".to_vec(), b"".to_vec()));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn cookie_parse_equals_in_value() {
|
|
108
|
+
// Only the first `=` separates name from value.
|
|
109
|
+
let packed = cookie_parse_packed_vec(b"a=b=c").unwrap();
|
|
110
|
+
let pairs = decode_packed_pairs(&packed);
|
|
111
|
+
assert_eq!(pairs.len(), 1);
|
|
112
|
+
assert_eq!(pairs[0], (b"a".to_vec(), b"b=c".to_vec()));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
fn cookie_parse_expires_value_with_commas() {
|
|
117
|
+
let packed =
|
|
118
|
+
cookie_parse_packed_vec(b"session=abc; expires=Wed, 21 Oct 2015 07:28:00 GMT").unwrap();
|
|
119
|
+
let pairs = decode_packed_pairs(&packed);
|
|
120
|
+
assert_eq!(pairs.len(), 2);
|
|
121
|
+
assert_eq!(
|
|
122
|
+
pairs[1],
|
|
123
|
+
(
|
|
124
|
+
b"expires".to_vec(),
|
|
125
|
+
b"Wed, 21 Oct 2015 07:28:00 GMT".to_vec()
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn cookie_parse_quotes_are_not_special() {
|
|
132
|
+
// Cookies split on `;` unconditionally — quotes are ordinary data, and a
|
|
133
|
+
// token without `=` becomes a pair with an empty value.
|
|
134
|
+
let packed = cookie_parse_packed_vec(b"a=\"x;y\"").unwrap();
|
|
135
|
+
let pairs = decode_packed_pairs(&packed);
|
|
136
|
+
assert_eq!(pairs.len(), 2);
|
|
137
|
+
assert_eq!(pairs[0], (b"a".to_vec(), b"\"x".to_vec()));
|
|
138
|
+
assert_eq!(pairs[1], (b"y\"".to_vec(), b"".to_vec()));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// rust/http/etag.rs — HTTP cache semantics (ETag + conditional requests).
|
|
2
|
+
//
|
|
3
|
+
// ETag generation (strong/weak, crc32-based) and the `ConditionalRequest`
|
|
4
|
+
// higher-order instance that turns If-None-Match / If-Modified-Since into a
|
|
5
|
+
// 304 decision. HTTP-date (IMF-fixdate) formatting/parsing lives in the
|
|
6
|
+
// `http_date` submodule; the conditional request reuses
|
|
7
|
+
// `http_date::parse_http_date_secs` for If-Modified-Since.
|
|
8
|
+
|
|
9
|
+
use napi::bindgen_prelude::*;
|
|
10
|
+
use napi_derive::napi;
|
|
11
|
+
|
|
12
|
+
use crate::http::http_date::parse_http_date_secs;
|
|
13
|
+
use crate::util::bytes::trim_ascii_whitespace;
|
|
14
|
+
|
|
15
|
+
// ── ETag ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/// Format a crc32 digest as an 8-char lowercase hex ETag value. Written into a
|
|
18
|
+
/// fixed stack buffer (10 or 12 bytes) — no `format!`/heap allocation.
|
|
19
|
+
pub fn etag_from_crc32(crc: u32, weak: bool) -> String {
|
|
20
|
+
let mut buf = [0u8; 12];
|
|
21
|
+
let start = if weak { 2 } else { 0 };
|
|
22
|
+
if weak {
|
|
23
|
+
buf[0] = b'W';
|
|
24
|
+
buf[1] = b'/';
|
|
25
|
+
}
|
|
26
|
+
let hex = crate::util::bytes::HEX_LOWER;
|
|
27
|
+
buf[start] = b'"';
|
|
28
|
+
buf[start + 1] = hex[((crc >> 28) & 0x0f) as usize];
|
|
29
|
+
buf[start + 2] = hex[((crc >> 24) & 0x0f) as usize];
|
|
30
|
+
buf[start + 3] = hex[((crc >> 20) & 0x0f) as usize];
|
|
31
|
+
buf[start + 4] = hex[((crc >> 16) & 0x0f) as usize];
|
|
32
|
+
buf[start + 5] = hex[((crc >> 12) & 0x0f) as usize];
|
|
33
|
+
buf[start + 6] = hex[((crc >> 8) & 0x0f) as usize];
|
|
34
|
+
buf[start + 7] = hex[((crc >> 4) & 0x0f) as usize];
|
|
35
|
+
buf[start + 8] = hex[(crc & 0x0f) as usize];
|
|
36
|
+
buf[start + 9] = b'"';
|
|
37
|
+
String::from_utf8(buf[..start + 10].to_vec()).expect("etag is ASCII")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Format a crc32 digest as an 8-char lowercase hex ETag value directly into
|
|
41
|
+
/// `out` (zero-alloc — no `String`/heap allocation). Returns the number of
|
|
42
|
+
/// bytes written (10 strong, 12 weak). Errors if `out` is too small.
|
|
43
|
+
pub fn etag_from_crc32_into(crc: u32, weak: bool, out: &mut [u8]) -> Result<usize> {
|
|
44
|
+
let len = if weak { 12 } else { 10 };
|
|
45
|
+
if out.len() < len {
|
|
46
|
+
return Err(Error::from_reason("etag: output buffer too small"));
|
|
47
|
+
}
|
|
48
|
+
let hex = crate::util::bytes::HEX_LOWER;
|
|
49
|
+
let mut pos = 0usize;
|
|
50
|
+
if weak {
|
|
51
|
+
out[pos] = b'W';
|
|
52
|
+
out[pos + 1] = b'/';
|
|
53
|
+
pos += 2;
|
|
54
|
+
}
|
|
55
|
+
out[pos] = b'"';
|
|
56
|
+
pos += 1;
|
|
57
|
+
out[pos] = hex[((crc >> 28) & 0x0f) as usize];
|
|
58
|
+
out[pos + 1] = hex[((crc >> 24) & 0x0f) as usize];
|
|
59
|
+
out[pos + 2] = hex[((crc >> 20) & 0x0f) as usize];
|
|
60
|
+
out[pos + 3] = hex[((crc >> 16) & 0x0f) as usize];
|
|
61
|
+
out[pos + 4] = hex[((crc >> 12) & 0x0f) as usize];
|
|
62
|
+
out[pos + 5] = hex[((crc >> 8) & 0x0f) as usize];
|
|
63
|
+
out[pos + 6] = hex[((crc >> 4) & 0x0f) as usize];
|
|
64
|
+
out[pos + 7] = hex[(crc & 0x0f) as usize];
|
|
65
|
+
out[pos + 8] = b'"';
|
|
66
|
+
Ok(len)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[napi]
|
|
70
|
+
pub fn etag(input: Uint8Array, weak: Option<bool>) -> Buffer {
|
|
71
|
+
let crc = crc32fast::hash(input.as_ref());
|
|
72
|
+
Buffer::from(etag_from_crc32(crc, weak.unwrap_or(false)).into_bytes())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Reusable-output etag: writes the ETag into `output` and returns bytes
|
|
76
|
+
/// written (10 strong / 12 weak). Errors if `output` is too small.
|
|
77
|
+
#[napi]
|
|
78
|
+
pub fn etag_into(input: Uint8Array, mut output: Uint8Array, weak: Option<bool>) -> Result<u32> {
|
|
79
|
+
let weak = weak.unwrap_or(false);
|
|
80
|
+
crate::util::run_packed_into(&input, &mut output, move |inp, out| {
|
|
81
|
+
let crc = crc32fast::hash(inp);
|
|
82
|
+
etag_from_crc32_into(crc, weak, out)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Packed ETag batch: `[u32 count]{[u32 len][data]}` in →
|
|
87
|
+
/// `[u32 count]{[u32 len][etag]}` out (10 strong / 12 weak bytes each).
|
|
88
|
+
#[napi]
|
|
89
|
+
pub fn etag_batch_packed(input: Uint8Array, weak: Option<bool>) -> Result<Buffer> {
|
|
90
|
+
let weak = weak.unwrap_or(false);
|
|
91
|
+
let iter = crate::util::packed::PackedIter::new(input.as_ref())?;
|
|
92
|
+
let count = iter.len();
|
|
93
|
+
let per = if weak { 12 } else { 10 };
|
|
94
|
+
// Direct-write: ETags are fixed-size (10 strong / 12 weak), so no per-item
|
|
95
|
+
// `String`/`Vec` allocation.
|
|
96
|
+
let mut out = Vec::with_capacity(4 + count.saturating_mul(4 + per));
|
|
97
|
+
out.extend_from_slice(&(count as u32).to_le_bytes());
|
|
98
|
+
for item in iter {
|
|
99
|
+
let crc = crc32fast::hash(item);
|
|
100
|
+
out.extend_from_slice(&(per as u32).to_le_bytes());
|
|
101
|
+
let start = out.len();
|
|
102
|
+
out.resize(start + per, 0);
|
|
103
|
+
etag_from_crc32_into(crc, weak, &mut out[start..start + per])?;
|
|
104
|
+
}
|
|
105
|
+
Ok(Buffer::from(out))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Conditional requests (If-None-Match / If-Modified-Since) ─────
|
|
109
|
+
|
|
110
|
+
/// Strip the `W/` weak prefix from an opaque-tag.
|
|
111
|
+
fn strip_weak(tag: &[u8]) -> &[u8] {
|
|
112
|
+
if tag.starts_with(b"W/") {
|
|
113
|
+
&tag[2..]
|
|
114
|
+
} else {
|
|
115
|
+
tag
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Weak equality of two opaque-tags (RFC 7232 §2.3.2).
|
|
120
|
+
fn etag_weak_eq(a: &[u8], b: &[u8]) -> bool {
|
|
121
|
+
strip_weak(trim_ascii_whitespace(a)) == strip_weak(trim_ascii_whitespace(b))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Higher-order instance: per-resource state (etag + last-modified) computed
|
|
125
|
+
/// once, then reused across requests to answer "is this 304?".
|
|
126
|
+
#[napi]
|
|
127
|
+
pub struct ConditionalRequest {
|
|
128
|
+
etag_value: Vec<u8>,
|
|
129
|
+
last_modified_secs: i64,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Pure core: evaluate is-not-modified against a precompiled etag + last
|
|
133
|
+
/// modified. Shared by the napi method and the C-ABI opaque-handle path, so
|
|
134
|
+
/// both transports answer identically (304 semantics: If-None-Match wins over
|
|
135
|
+
/// If-Modified-Since).
|
|
136
|
+
pub(crate) fn is_not_modified_core(
|
|
137
|
+
etag_value: &[u8],
|
|
138
|
+
last_modified_secs: i64,
|
|
139
|
+
if_none_match: Option<&[u8]>,
|
|
140
|
+
if_modified_since: Option<&[u8]>,
|
|
141
|
+
) -> bool {
|
|
142
|
+
if let Some(inm) = if_none_match {
|
|
143
|
+
let header = trim_ascii_whitespace(inm);
|
|
144
|
+
if header == b"*" {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return header
|
|
148
|
+
.split(|&b| b == b',')
|
|
149
|
+
.any(|candidate| etag_weak_eq(candidate, etag_value));
|
|
150
|
+
}
|
|
151
|
+
if let Some(ims) = if_modified_since {
|
|
152
|
+
if last_modified_secs > 0 {
|
|
153
|
+
if let Some(secs) = parse_http_date_secs(ims) {
|
|
154
|
+
return last_modified_secs <= secs;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[napi]
|
|
162
|
+
impl ConditionalRequest {
|
|
163
|
+
#[napi(constructor)]
|
|
164
|
+
pub fn new(etag_value: Uint8Array, last_modified_secs: Option<f64>) -> Self {
|
|
165
|
+
Self {
|
|
166
|
+
etag_value: etag_value.as_ref().to_vec(),
|
|
167
|
+
last_modified_secs: last_modified_secs.map(|v| v as i64).unwrap_or(0).max(0),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Opaque handle to the precompiled state, for the `bun:ffi` C-ABI fast path
|
|
172
|
+
/// (`castrum_conditional_is_not_modified` in rust/ffi.rs). Only valid while
|
|
173
|
+
/// THIS instance is alive; the JS wrapper holds the instance for the handle
|
|
174
|
+
/// lifetime (same contract as `Ingress::ingress_inner_ptr`).
|
|
175
|
+
#[napi]
|
|
176
|
+
pub fn inner_ptr(&self) -> u64 {
|
|
177
|
+
self as *const ConditionalRequest as u64
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// true → "304 Not Modified" (If-None-Match wins over If-Modified-Since).
|
|
181
|
+
#[napi]
|
|
182
|
+
pub fn is_not_modified(
|
|
183
|
+
&self,
|
|
184
|
+
if_none_match: Option<Uint8Array>,
|
|
185
|
+
if_modified_since: Option<Uint8Array>,
|
|
186
|
+
) -> bool {
|
|
187
|
+
is_not_modified_core(
|
|
188
|
+
&self.etag_value,
|
|
189
|
+
self.last_modified_secs,
|
|
190
|
+
if_none_match.as_deref(),
|
|
191
|
+
if_modified_since.as_deref(),
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// C-ABI support: evaluate is-not-modified against the precompiled state.
|
|
197
|
+
///
|
|
198
|
+
/// # Safety
|
|
199
|
+
/// `p` must be a valid `*const ConditionalRequest` obtained from `inner_ptr`
|
|
200
|
+
/// and must stay alive for the call (the JS wrapper holds the napi instance).
|
|
201
|
+
pub(crate) unsafe fn conditional_is_not_modified(
|
|
202
|
+
p: *const ConditionalRequest,
|
|
203
|
+
inm: Option<&[u8]>,
|
|
204
|
+
ims: Option<&[u8]>,
|
|
205
|
+
) -> bool {
|
|
206
|
+
let c = &*p;
|
|
207
|
+
is_not_modified_core(&c.etag_value, c.last_modified_secs, inm, ims)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[cfg(test)]
|
|
211
|
+
mod tests {
|
|
212
|
+
use super::*;
|
|
213
|
+
|
|
214
|
+
#[test]
|
|
215
|
+
fn etag_formatting() {
|
|
216
|
+
assert_eq!(etag_from_crc32(0xdead_beef, false), "\"deadbeef\"");
|
|
217
|
+
assert_eq!(etag_from_crc32(0x0000_0001, true), "W/\"00000001\"");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#[test]
|
|
221
|
+
fn etag_from_crc32_into_matches_allocating() {
|
|
222
|
+
let crc = crc32fast::hash(b"hello world, this is etag data");
|
|
223
|
+
let strong = etag_from_crc32(crc, false);
|
|
224
|
+
let weak = etag_from_crc32(crc, true);
|
|
225
|
+
let mut out = [0u8; 12];
|
|
226
|
+
let n = etag_from_crc32_into(crc, false, &mut out).unwrap();
|
|
227
|
+
assert_eq!(n, strong.len());
|
|
228
|
+
assert_eq!(&out[..n], strong.as_bytes());
|
|
229
|
+
let n2 = etag_from_crc32_into(crc, true, &mut out).unwrap();
|
|
230
|
+
assert_eq!(n2, weak.len());
|
|
231
|
+
assert_eq!(&out[..n2], weak.as_bytes());
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#[test]
|
|
235
|
+
fn etag_from_crc32_into_small_buffer_errors() {
|
|
236
|
+
let mut out = [0u8; 4];
|
|
237
|
+
assert!(etag_from_crc32_into(0xdead_beef, false, &mut out).is_err());
|
|
238
|
+
let mut out2 = [0u8; 10];
|
|
239
|
+
assert!(etag_from_crc32_into(0xdead_beef, true, &mut out2).is_err());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#[test]
|
|
243
|
+
fn etag_into_reports_length() {
|
|
244
|
+
let data = b"hello world, this is etag data";
|
|
245
|
+
let out = Uint8Array::new(vec![0u8; 16]);
|
|
246
|
+
let n = etag_into(Uint8Array::new(data.to_vec()), out, None).unwrap();
|
|
247
|
+
assert_eq!(
|
|
248
|
+
n as usize,
|
|
249
|
+
etag_from_crc32(crc32fast::hash(data), false).len()
|
|
250
|
+
);
|
|
251
|
+
let out2 = Uint8Array::new(vec![0u8; 16]);
|
|
252
|
+
let n2 = etag_into(Uint8Array::new(data.to_vec()), out2, Some(true)).unwrap();
|
|
253
|
+
assert_eq!(
|
|
254
|
+
n2 as usize,
|
|
255
|
+
etag_from_crc32(crc32fast::hash(data), true).len()
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#[test]
|
|
260
|
+
fn conditional_if_none_match_star() {
|
|
261
|
+
let c = ConditionalRequest::new(Uint8Array::new(b"\"abc123\"".to_vec()), Some(1_000.0));
|
|
262
|
+
assert!(c.is_not_modified(Some(Uint8Array::new(b"*".to_vec())), None));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn conditional_weak_etag_match() {
|
|
267
|
+
let c = ConditionalRequest::new(Uint8Array::new(b"\"abc123\"".to_vec()), Some(1_000.0));
|
|
268
|
+
assert!(c.is_not_modified(
|
|
269
|
+
Some(Uint8Array::new(b"\"xyz\", W/\"abc123\"".to_vec())),
|
|
270
|
+
None
|
|
271
|
+
));
|
|
272
|
+
assert!(!c.is_not_modified(Some(Uint8Array::new(b"\"xyz\", \"abc124\"".to_vec())), None));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn conditional_if_modified_since() {
|
|
277
|
+
// last-modified = 1970-01-01 00:16:40 (secs 1000).
|
|
278
|
+
let c = ConditionalRequest::new(Uint8Array::new(b"\"abc123\"".to_vec()), Some(1_000.0));
|
|
279
|
+
// IMS equal to last-modified → not modified → 304.
|
|
280
|
+
let equal = Uint8Array::new(b"Thu, 01 Jan 1970 00:16:40 GMT".to_vec());
|
|
281
|
+
assert!(c.is_not_modified(None, Some(equal)));
|
|
282
|
+
// IMS after last-modified → not modified → 304.
|
|
283
|
+
let after = Uint8Array::new(b"Thu, 01 Jan 1970 00:16:41 GMT".to_vec());
|
|
284
|
+
assert!(c.is_not_modified(None, Some(after)));
|
|
285
|
+
// IMS before last-modified → modified → 200.
|
|
286
|
+
let before = Uint8Array::new(b"Thu, 01 Jan 1970 00:00:01 GMT".to_vec());
|
|
287
|
+
assert!(!c.is_not_modified(None, Some(before)));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// rust/http/form.rs — application/x-www-form-urlencoded body parser.
|
|
2
|
+
//
|
|
3
|
+
// Reuses the zero-alloc query-parser core (`query_parse_packed_into_slice`):
|
|
4
|
+
// it already splits on '&', decodes '+'→space and %XX, which is exactly the
|
|
5
|
+
// x-www-form-urlencoded wire format. This module adds the public napi surface:
|
|
6
|
+
// a scalar `form_parse_packed` and the higher-order `FormParser` instance,
|
|
7
|
+
// which owns a reusable output buffer so repeated parses don't reallocate.
|
|
8
|
+
|
|
9
|
+
use napi::bindgen_prelude::*;
|
|
10
|
+
use napi_derive::napi;
|
|
11
|
+
|
|
12
|
+
use crate::http::query_parser::query_parse_packed_into_slice;
|
|
13
|
+
|
|
14
|
+
/// Parse an application/x-www-form-urlencoded body into packed pairs.
|
|
15
|
+
/// Output: `[u32 count] repeated { [u32 name_len] [name] [u32 value_len] [value] }`
|
|
16
|
+
#[inline]
|
|
17
|
+
pub fn form_parse_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
|
|
18
|
+
// Same conservative upper bound as the query parser (9x + 16).
|
|
19
|
+
let upper_bound = input.len().saturating_mul(9).saturating_add(16);
|
|
20
|
+
let mut out = vec![0u8; upper_bound];
|
|
21
|
+
let written = query_parse_packed_into_slice(input, &mut out)?;
|
|
22
|
+
out.truncate(written);
|
|
23
|
+
Ok(out)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[napi]
|
|
27
|
+
pub fn form_parse_packed(input: Uint8Array) -> Result<Buffer> {
|
|
28
|
+
form_parse_packed_vec(input.as_ref()).map(Buffer::from)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Higher-order instance: owns a reusable output buffer so repeated parses do
|
|
32
|
+
/// not reallocate. Buffer allocation happens once in the constructor (or is
|
|
33
|
+
/// amortized via grow-on-demand in `parse`), and `parse` writes into the
|
|
34
|
+
/// reused buffer — the compiled-once pattern shared by `SchemaValidator` /
|
|
35
|
+
/// `HmacSigner` / `TemplateRenderer`.
|
|
36
|
+
#[napi]
|
|
37
|
+
pub struct FormParser {
|
|
38
|
+
buf: Vec<u8>,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[napi]
|
|
42
|
+
impl FormParser {
|
|
43
|
+
#[napi(constructor)]
|
|
44
|
+
pub fn new(capacity: Option<u32>) -> Self {
|
|
45
|
+
// Clamp the initial buffer to a sane cap so a huge capacity can't
|
|
46
|
+
// force a multi-GiB single allocation; parse() still grows on demand.
|
|
47
|
+
const MAX_CAPACITY: usize = 16 * 1024 * 1024;
|
|
48
|
+
let cap = (capacity.unwrap_or(4096).max(256) as usize).min(MAX_CAPACITY);
|
|
49
|
+
Self {
|
|
50
|
+
buf: vec![0u8; cap],
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Parse into the instance's reusable buffer and return the packed pairs.
|
|
55
|
+
#[napi]
|
|
56
|
+
pub fn parse(&mut self, input: Uint8Array) -> Result<Buffer> {
|
|
57
|
+
let need = input.len().saturating_mul(9).saturating_add(16);
|
|
58
|
+
if self.buf.len() < need {
|
|
59
|
+
self.buf.resize(need, 0);
|
|
60
|
+
}
|
|
61
|
+
let written = query_parse_packed_into_slice(input.as_ref(), &mut self.buf)?;
|
|
62
|
+
Ok(Buffer::from(self.buf[..written].to_vec()))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Zero-alloc parse into a caller-provided output buffer.
|
|
66
|
+
#[napi]
|
|
67
|
+
pub fn parse_into(&self, input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
|
|
68
|
+
crate::util::run_packed_into(&input, &mut output, query_parse_packed_into_slice)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[cfg(test)]
|
|
73
|
+
mod tests {
|
|
74
|
+
use super::*;
|
|
75
|
+
use crate::test_support::decode_packed_pairs;
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn form_parse_basic_pairs() {
|
|
79
|
+
let packed = form_parse_packed_vec(b"name=John%20Doe&age=30&tags=a&tags=b").unwrap();
|
|
80
|
+
let pairs = decode_packed_pairs(&packed);
|
|
81
|
+
assert_eq!(pairs.len(), 4);
|
|
82
|
+
assert_eq!(pairs[0], (b"name".to_vec(), b"John Doe".to_vec()));
|
|
83
|
+
assert_eq!(pairs[1], (b"age".to_vec(), b"30".to_vec()));
|
|
84
|
+
assert_eq!(pairs[2], (b"tags".to_vec(), b"a".to_vec()));
|
|
85
|
+
assert_eq!(pairs[3], (b"tags".to_vec(), b"b".to_vec()));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#[test]
|
|
89
|
+
fn form_parse_plus_and_percent() {
|
|
90
|
+
let packed = form_parse_packed_vec(b"a+b=c%2Bd&empty=").unwrap();
|
|
91
|
+
let pairs = decode_packed_pairs(&packed);
|
|
92
|
+
assert_eq!(pairs[0], (b"a b".to_vec(), b"c+d".to_vec()));
|
|
93
|
+
assert_eq!(pairs[1], (b"empty".to_vec(), b"".to_vec()));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn form_parse_key_without_equals() {
|
|
98
|
+
let packed = form_parse_packed_vec(b"flag&x=1").unwrap();
|
|
99
|
+
let pairs = decode_packed_pairs(&packed);
|
|
100
|
+
assert_eq!(pairs[0], (b"flag".to_vec(), b"".to_vec()));
|
|
101
|
+
assert_eq!(pairs[1], (b"x".to_vec(), b"1".to_vec()));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn form_parse_empty_input() {
|
|
106
|
+
let packed = form_parse_packed_vec(b"").unwrap();
|
|
107
|
+
assert!(decode_packed_pairs(&packed).is_empty());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn form_parse_rejects_malformed_percent() {
|
|
112
|
+
assert!(form_parse_packed_vec(b"a=%ZZ").is_err());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
fn form_parser_reuses_buffer_across_parses() {
|
|
117
|
+
let mut parser = FormParser::new(Some(64));
|
|
118
|
+
let a = parser.parse(Uint8Array::new(b"x=1&y=2".to_vec())).unwrap();
|
|
119
|
+
assert_eq!(
|
|
120
|
+
decode_packed_pairs(a.as_ref()),
|
|
121
|
+
vec![
|
|
122
|
+
(b"x".to_vec(), b"1".to_vec()),
|
|
123
|
+
(b"y".to_vec(), b"2".to_vec())
|
|
124
|
+
]
|
|
125
|
+
);
|
|
126
|
+
// A larger input grows the reusable buffer and still parses.
|
|
127
|
+
let big = format!("k={}", "a".repeat(200));
|
|
128
|
+
let b = parser.parse(Uint8Array::new(big.into_bytes())).unwrap();
|
|
129
|
+
let pairs = decode_packed_pairs(b.as_ref());
|
|
130
|
+
assert_eq!(pairs[0].1, b"a".repeat(200));
|
|
131
|
+
}
|
|
132
|
+
}
|