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,2333 @@
|
|
|
1
|
+
// rust/ffi/tests.rs — C-ABI unit tests.
|
|
2
|
+
//
|
|
3
|
+
// Exercises every `castrum_*` export against the pure cores (parity with the
|
|
4
|
+
// napi/scalar paths), the packed-wire layouts (needed-size convention), the
|
|
5
|
+
// cstring-returning writers, and the opaque-handle fast paths. Moved out of
|
|
6
|
+
// the former `ffi.rs` so the export surface is navigable; `use super::*` picks
|
|
7
|
+
// up every symbol via the module-root re-exports in `mod.rs`.
|
|
8
|
+
|
|
9
|
+
use super::*;
|
|
10
|
+
use aws_lc_rs::hmac;
|
|
11
|
+
|
|
12
|
+
#[test]
|
|
13
|
+
fn crc32_c_abi_matches_core() {
|
|
14
|
+
let bytes = b"123456789";
|
|
15
|
+
let out = unsafe { castrum_crc32(bytes.as_ptr(), bytes.len()) };
|
|
16
|
+
assert_eq!(out, crate::crypto::hashing::crc32_bytes(b"123456789"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── Per-route stack C-ABI (castrum_route_*) ────────────────────
|
|
20
|
+
|
|
21
|
+
/// Minimal route descriptor wire (magic + version 3 + parseQuery +
|
|
22
|
+
/// parseCookies stages).
|
|
23
|
+
fn route_desc_parse_both() -> Vec<u8> {
|
|
24
|
+
let mut d = Vec::new();
|
|
25
|
+
d.extend_from_slice(&crate::ingress::native_route::ROUTE_DESC_MAGIC.to_le_bytes());
|
|
26
|
+
d.extend_from_slice(&crate::ingress::native_route::ROUTE_DESC_VERSION.to_le_bytes());
|
|
27
|
+
d.extend_from_slice(&(2 * 1024 * 1024u32).to_le_bytes());
|
|
28
|
+
d.extend_from_slice(&8192u32.to_le_bytes());
|
|
29
|
+
d.extend_from_slice(&8192u32.to_le_bytes());
|
|
30
|
+
d.extend_from_slice(&0u32.to_le_bytes());
|
|
31
|
+
d.extend_from_slice(&2u32.to_le_bytes()); // stageCount
|
|
32
|
+
d.push(crate::ingress::native_route::STAGE_PARSE_QUERY);
|
|
33
|
+
d.push(crate::ingress::native_route::STAGE_PARSE_COOKIES);
|
|
34
|
+
d.extend_from_slice(&0u32.to_le_bytes()); // schemaCount
|
|
35
|
+
d
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Request frame: `[flags u32][qLen][query][cLen][cookie]`.
|
|
39
|
+
fn route_frame(query: &[u8], cookie: &[u8]) -> Vec<u8> {
|
|
40
|
+
let mut f = Vec::new();
|
|
41
|
+
f.extend_from_slice(&0u32.to_le_bytes());
|
|
42
|
+
f.extend_from_slice(&(query.len() as u32).to_le_bytes());
|
|
43
|
+
f.extend_from_slice(query);
|
|
44
|
+
f.extend_from_slice(&(cookie.len() as u32).to_le_bytes());
|
|
45
|
+
f.extend_from_slice(cookie);
|
|
46
|
+
f
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[test]
|
|
50
|
+
fn route_compile_run_destroy_c_abi() {
|
|
51
|
+
let desc = route_desc_parse_both();
|
|
52
|
+
let handle = unsafe { castrum_route_compile(desc.as_ptr(), desc.len()) };
|
|
53
|
+
assert_ne!(handle, 0, "route compile must produce a live handle");
|
|
54
|
+
|
|
55
|
+
let f = route_frame(b"a=1&b=hello%20world", b"s=v");
|
|
56
|
+
let mut out = vec![0u8; 256];
|
|
57
|
+
let w = unsafe { castrum_route_run(handle, f.as_ptr(), f.len(), out.as_mut_ptr(), out.len()) };
|
|
58
|
+
// 8 header + query section [count 4 + (a=1: 4+1+4+1=10) + (b=hello world:
|
|
59
|
+
// 4+1+4+11=20) = 34] + cookie section [count 4 + (s=v: 4+1+4+1=10) = 14].
|
|
60
|
+
assert_eq!(w, 8 + 34 + 14, "exact result size (query+cookie sections)");
|
|
61
|
+
|
|
62
|
+
let flags = u32::from_le_bytes(out[0..4].try_into().unwrap());
|
|
63
|
+
assert_ne!(
|
|
64
|
+
flags & crate::ingress::native_route::ROUTE_RESULT_FLAG_OK,
|
|
65
|
+
0
|
|
66
|
+
);
|
|
67
|
+
assert_ne!(
|
|
68
|
+
flags & crate::ingress::native_route::ROUTE_RESULT_FLAG_QUERY_VALID,
|
|
69
|
+
0
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
unsafe { castrum_route_destroy(handle) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn route_compile_rejects_bad_magic() {
|
|
77
|
+
let mut desc = route_desc_parse_both();
|
|
78
|
+
desc[0] = 0;
|
|
79
|
+
let handle = unsafe { castrum_route_compile(desc.as_ptr(), desc.len()) };
|
|
80
|
+
assert_eq!(handle, 0, "bad magic must fail compilation");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#[test]
|
|
84
|
+
fn route_run_needed_size_convention() {
|
|
85
|
+
let desc = route_desc_parse_both();
|
|
86
|
+
let handle = unsafe { castrum_route_compile(desc.as_ptr(), desc.len()) };
|
|
87
|
+
assert_ne!(handle, 0);
|
|
88
|
+
let f = route_frame(b"a=1&bb=22&ccc=333", b"");
|
|
89
|
+
// A too-small buffer reports the EXACT required size without writing.
|
|
90
|
+
let mut small = [0u8; 8];
|
|
91
|
+
let needed =
|
|
92
|
+
unsafe { castrum_route_run(handle, f.as_ptr(), f.len(), small.as_mut_ptr(), small.len()) };
|
|
93
|
+
assert!(needed > 8);
|
|
94
|
+
let mut big = vec![0u8; needed];
|
|
95
|
+
let w = unsafe { castrum_route_run(handle, f.as_ptr(), f.len(), big.as_mut_ptr(), big.len()) };
|
|
96
|
+
assert_eq!(w, needed);
|
|
97
|
+
assert_eq!(
|
|
98
|
+
&small[..],
|
|
99
|
+
&[0u8; 8],
|
|
100
|
+
"nothing written to the too-small buffer"
|
|
101
|
+
);
|
|
102
|
+
unsafe { castrum_route_destroy(handle) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// The C-ABI ingress layout blob must never drift from the single numeric
|
|
106
|
+
/// source (`output.rs`). If a layout constant changes in `output.rs` (or the
|
|
107
|
+
/// struct field order here) without updating this test, it fails.
|
|
108
|
+
#[test]
|
|
109
|
+
fn ingress_layout_c_abi_matches_output_source() {
|
|
110
|
+
use crate::ingress::output;
|
|
111
|
+
let mut buf = [0u8; core::mem::size_of::<IngressLayout>()];
|
|
112
|
+
let written = unsafe { castrum_ingress_layout(buf.as_mut_ptr(), buf.len()) };
|
|
113
|
+
assert_eq!(written, buf.len(), "layout blob must fill the full struct");
|
|
114
|
+
let ptr = buf.as_ptr() as *const IngressLayout;
|
|
115
|
+
let l = unsafe { &*ptr };
|
|
116
|
+
assert_eq!(l.out_verdict, output::OUT_VERDICT as u32);
|
|
117
|
+
assert_eq!(l.out_error_code, output::OUT_ERROR_CODE as u32);
|
|
118
|
+
assert_eq!(l.out_status, output::OUT_STATUS as u32);
|
|
119
|
+
assert_eq!(l.out_flags, output::OUT_FLAGS as u32);
|
|
120
|
+
assert_eq!(l.out_rate_limit, output::OUT_RATE_LIMIT as u32);
|
|
121
|
+
assert_eq!(l.out_rate_remaining, output::OUT_RATE_REMAINING as u32);
|
|
122
|
+
assert_eq!(l.out_rate_reset, output::OUT_RATE_RESET as u32);
|
|
123
|
+
assert_eq!(l.out_retry_after, output::OUT_RETRY_AFTER as u32);
|
|
124
|
+
assert_eq!(l.out_cookies_json_len, output::OUT_COOKIES_JSON_LEN as u32);
|
|
125
|
+
assert_eq!(l.out_query_json_len, output::OUT_QUERY_JSON_LEN as u32);
|
|
126
|
+
assert_eq!(l.out_header_variant, output::OUT_HEADER_VARIANT as u32);
|
|
127
|
+
assert_eq!(l.out_body_json_len, output::OUT_BODY_JSON_LEN as u32);
|
|
128
|
+
assert_eq!(l.out_data_start, output::OUT_DATA_START as u32);
|
|
129
|
+
assert_eq!(l.flag_has_cookies, output::FLAG_HAS_COOKIES);
|
|
130
|
+
assert_eq!(l.flag_has_query, output::FLAG_HAS_QUERY);
|
|
131
|
+
assert_eq!(l.flag_body_valid_json, output::FLAG_BODY_VALID_JSON);
|
|
132
|
+
assert_eq!(l.flag_schema_valid, output::FLAG_SCHEMA_VALID);
|
|
133
|
+
assert_eq!(l.flag_cors_allowed, output::FLAG_CORS_ALLOWED);
|
|
134
|
+
assert_eq!(l.flag_is_preflight, output::FLAG_IS_PREFLIGHT);
|
|
135
|
+
assert_eq!(l.flag_rate_limited, output::FLAG_RATE_LIMITED);
|
|
136
|
+
assert_eq!(l.flag_https, output::FLAG_HTTPS);
|
|
137
|
+
assert_eq!(l.flag_trusted_proxy, output::FLAG_TRUSTED_PROXY);
|
|
138
|
+
assert_eq!(l.flag_body_truncated, output::FLAG_BODY_TRUNCATED);
|
|
139
|
+
assert_eq!(l.hv_json, output::HV_JSON as u32);
|
|
140
|
+
assert_eq!(l.hv_cors_simple, output::HV_CORS_SIMPLE as u32);
|
|
141
|
+
assert_eq!(l.hv_cors_preflight, output::HV_CORS_PREFLIGHT as u32);
|
|
142
|
+
assert_eq!(l.hv_rate_active, output::HV_RATE_ACTIVE as u32);
|
|
143
|
+
assert_eq!(l.hv_rate_limited, output::HV_RATE_LIMITED as u32);
|
|
144
|
+
assert_eq!(l.hv_count, output::HV_COUNT as u32);
|
|
145
|
+
assert_eq!(l.err_none, output::ERR_CODE_NONE as u32);
|
|
146
|
+
assert_eq!(l.err_cors_preflight, output::ERR_CODE_CORS_PREFLIGHT as u32);
|
|
147
|
+
assert_eq!(l.err_rate_limited, output::ERR_CODE_RATE_LIMITED as u32);
|
|
148
|
+
assert_eq!(l.err_body_too_large, output::ERR_CODE_BODY_TOO_LARGE as u32);
|
|
149
|
+
assert_eq!(l.err_invalid_json, output::ERR_CODE_INVALID_JSON as u32);
|
|
150
|
+
assert_eq!(
|
|
151
|
+
l.err_schema_validation,
|
|
152
|
+
output::ERR_CODE_SCHEMA_VALIDATION as u32
|
|
153
|
+
);
|
|
154
|
+
assert_eq!(l.err_bad_request, output::ERR_CODE_BAD_REQUEST as u32);
|
|
155
|
+
assert_eq!(
|
|
156
|
+
l.err_request_too_large,
|
|
157
|
+
output::ERR_CODE_REQUEST_TOO_LARGE as u32
|
|
158
|
+
);
|
|
159
|
+
assert_eq!(l.err_internal, output::ERR_CODE_INTERNAL as u32);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn fnv_c_abi_matches_core() {
|
|
164
|
+
let bytes = b"foobar";
|
|
165
|
+
let out = unsafe { castrum_fnv1a64(bytes.as_ptr(), bytes.len()) };
|
|
166
|
+
assert_eq!(out, crate::crypto::hashing::fnv1a64_bytes(b"foobar"));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[test]
|
|
170
|
+
fn json_valid_c_abi() {
|
|
171
|
+
let good = b"{\"a\":1}";
|
|
172
|
+
assert_eq!(unsafe { castrum_json_valid(good.as_ptr(), good.len()) }, 1);
|
|
173
|
+
let bad = b"{not json";
|
|
174
|
+
assert_eq!(unsafe { castrum_json_valid(bad.as_ptr(), bad.len()) }, 0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn utf8_valid_c_abi() {
|
|
179
|
+
assert_eq!(unsafe { castrum_utf8_valid(b"hello".as_ptr(), 5) }, 1);
|
|
180
|
+
assert_eq!(
|
|
181
|
+
unsafe { castrum_utf8_valid("héllo 🚀".as_bytes().as_ptr(), "héllo 🚀".len()) },
|
|
182
|
+
1
|
|
183
|
+
);
|
|
184
|
+
assert_eq!(unsafe { castrum_utf8_valid(b"\xff\xfe".as_ptr(), 2) }, 0);
|
|
185
|
+
// A lone 0xC3 (incomplete 2-byte sequence) is invalid.
|
|
186
|
+
assert_eq!(unsafe { castrum_utf8_valid(b"\xc3".as_ptr(), 1) }, 0);
|
|
187
|
+
// Null pointer / zero-length → 0 / empty is valid.
|
|
188
|
+
assert_eq!(unsafe { castrum_utf8_valid(std::ptr::null(), 0) }, 0);
|
|
189
|
+
assert_eq!(unsafe { castrum_utf8_valid(b"".as_ptr(), 0) }, 1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#[test]
|
|
193
|
+
fn hmac_key_cache_reuses_compiled_key() {
|
|
194
|
+
// Same-secret calls must hit the cache (the whole point of the LRU):
|
|
195
|
+
// repeated calls reuse the precomputed key schedule instead of
|
|
196
|
+
// re-deriving it via `hmac::Key::new`.
|
|
197
|
+
HMAC_KEY_CACHE.with(|c| c.borrow_mut().clear());
|
|
198
|
+
HMAC_CACHE_HITS.with(|h| h.set(0));
|
|
199
|
+
let secret = b"same-secret";
|
|
200
|
+
let _ = hmac_key_cached(secret);
|
|
201
|
+
let _ = hmac_key_cached(secret);
|
|
202
|
+
let _ = hmac_key_cached(secret);
|
|
203
|
+
let hits = HMAC_CACHE_HITS.with(|h| h.get());
|
|
204
|
+
assert!(
|
|
205
|
+
hits >= 2,
|
|
206
|
+
"same-secret calls must reuse the cached key (hits={hits})"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[test]
|
|
211
|
+
fn hmac_key_cache_matches_fresh_key() {
|
|
212
|
+
// The cached key must be byte-identical in behavior to a freshly
|
|
213
|
+
// derived key for every secret.
|
|
214
|
+
for secret in [b"a".as_slice(), b"secret-key", b"x".repeat(300).as_slice()] {
|
|
215
|
+
let cached = hmac_key_cached(secret);
|
|
216
|
+
let fresh = hmac_key(secret);
|
|
217
|
+
let data = b"hello world";
|
|
218
|
+
let a = hmac::sign(&cached, data);
|
|
219
|
+
let b = hmac::sign(&fresh, data);
|
|
220
|
+
assert_eq!(a.as_ref(), b.as_ref(), "cached key differs from fresh key");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn hmac_key_cache_survives_eviction_thrash() {
|
|
226
|
+
// Exceed the LRU capacity with distinct secrets: eviction must not
|
|
227
|
+
// corrupt the surviving entries (every secret still verifies).
|
|
228
|
+
for i in 0..(HMAC_KEY_CACHE_CAP * 4) {
|
|
229
|
+
let secret = format!("secret-{i}").into_bytes();
|
|
230
|
+
let key = hmac_key_cached(&secret);
|
|
231
|
+
let data = b"payload";
|
|
232
|
+
let tag = hmac::sign(&key, data);
|
|
233
|
+
assert!(
|
|
234
|
+
hmac::verify(&key, data, tag.as_ref()).is_ok(),
|
|
235
|
+
"secret-{i} failed after cache eviction"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn hex_encode_c_abi_roundtrip() {
|
|
242
|
+
let input = b"hello";
|
|
243
|
+
let mut out = [0u8; 16];
|
|
244
|
+
let written =
|
|
245
|
+
unsafe { castrum_hex_encode(input.as_ptr(), input.len(), out.as_mut_ptr(), out.len()) };
|
|
246
|
+
assert_eq!(written, 10);
|
|
247
|
+
assert_eq!(&out[..10], b"68656c6c6f");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[test]
|
|
251
|
+
fn hex_encode_c_abi_undersized_returns_zero() {
|
|
252
|
+
let input = b"hello";
|
|
253
|
+
let mut out = [0u8; 4];
|
|
254
|
+
let written =
|
|
255
|
+
unsafe { castrum_hex_encode(input.as_ptr(), input.len(), out.as_mut_ptr(), out.len()) };
|
|
256
|
+
assert_eq!(written, 0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#[test]
|
|
260
|
+
fn hex_decode_c_abi_roundtrip() {
|
|
261
|
+
let input = b"68656c6c6f";
|
|
262
|
+
let mut out = [0u8; 8];
|
|
263
|
+
let written =
|
|
264
|
+
unsafe { castrum_hex_decode(input.as_ptr(), input.len(), out.as_mut_ptr(), out.len()) };
|
|
265
|
+
assert_eq!(written, 5);
|
|
266
|
+
assert_eq!(&out[..5], b"hello");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#[test]
|
|
270
|
+
fn hex_decode_c_abi_rejects_invalid() {
|
|
271
|
+
let bad = b"6x";
|
|
272
|
+
let mut out = [0u8; 4];
|
|
273
|
+
let written =
|
|
274
|
+
unsafe { castrum_hex_decode(bad.as_ptr(), bad.len(), out.as_mut_ptr(), out.len()) };
|
|
275
|
+
assert_eq!(written, 0);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#[test]
|
|
279
|
+
fn url_encode_c_abi() {
|
|
280
|
+
let input = b"a b/c";
|
|
281
|
+
let mut out = [0u8; 16];
|
|
282
|
+
let written =
|
|
283
|
+
unsafe { castrum_url_encode(input.as_ptr(), input.len(), out.as_mut_ptr(), out.len()) };
|
|
284
|
+
assert_eq!(&out[..written], b"a%20b%2Fc");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#[test]
|
|
288
|
+
fn url_decode_c_abi() {
|
|
289
|
+
let input = b"a%20b%2Fc";
|
|
290
|
+
let mut out = [0u8; 8];
|
|
291
|
+
let written =
|
|
292
|
+
unsafe { castrum_url_decode(input.as_ptr(), input.len(), out.as_mut_ptr(), out.len()) };
|
|
293
|
+
assert_eq!(&out[..written], b"a b/c");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[test]
|
|
297
|
+
fn validators_c_abi() {
|
|
298
|
+
// Values mirror the core `#[cfg(test)]` vectors in rust/util/validation.rs.
|
|
299
|
+
assert_eq!(unsafe { castrum_validate_email(c"a@b.com".as_ptr()) }, 1);
|
|
300
|
+
assert_eq!(
|
|
301
|
+
unsafe { castrum_validate_email(c"not-an-email".as_ptr()) },
|
|
302
|
+
0
|
|
303
|
+
);
|
|
304
|
+
assert_eq!(
|
|
305
|
+
unsafe { castrum_validate_uuid(c"550e8400-e29b-41d4-a716-446655440000".as_ptr()) },
|
|
306
|
+
1
|
|
307
|
+
);
|
|
308
|
+
assert_eq!(unsafe { castrum_validate_uuid(c"not-a-uuid".as_ptr()) }, 0);
|
|
309
|
+
assert_eq!(unsafe { castrum_validate_ipv4(c"192.168.0.1".as_ptr()) }, 1);
|
|
310
|
+
assert_eq!(unsafe { castrum_validate_ipv4(c"999.1.1.1".as_ptr()) }, 0);
|
|
311
|
+
assert_eq!(unsafe { castrum_validate_ipv6(c"2001:db8::1".as_ptr()) }, 1);
|
|
312
|
+
assert_eq!(unsafe { castrum_validate_ipv6(c"2001:::1".as_ptr()) }, 0);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#[test]
|
|
316
|
+
fn json_sum_ids_c_abi_packed_output() {
|
|
317
|
+
let doc = b"[{\"id\":1},{\"id\":2},{\"id\":3}]";
|
|
318
|
+
let mut out = [0u8; 9];
|
|
319
|
+
let w = unsafe { castrum_json_sum_ids(doc.as_ptr(), doc.len(), out.as_mut_ptr(), out.len()) };
|
|
320
|
+
assert_eq!(w, 9);
|
|
321
|
+
assert_eq!(out[0], 1);
|
|
322
|
+
assert_eq!(i64::from_le_bytes(out[1..9].try_into().unwrap()), 6);
|
|
323
|
+
|
|
324
|
+
// A legit zero-sum array is still "ok" (the old scalar i64 conflated
|
|
325
|
+
// this with invalid input).
|
|
326
|
+
let zero = b"[{\"id\":0},{\"id\":0}]";
|
|
327
|
+
let w = unsafe { castrum_json_sum_ids(zero.as_ptr(), zero.len(), out.as_mut_ptr(), out.len()) };
|
|
328
|
+
assert_eq!(w, 9);
|
|
329
|
+
assert_eq!(out[0], 1);
|
|
330
|
+
assert_eq!(i64::from_le_bytes(out[1..9].try_into().unwrap()), 0);
|
|
331
|
+
|
|
332
|
+
// Invalid (non-array) input → ok=0, 1 byte.
|
|
333
|
+
let w = unsafe { castrum_json_sum_ids(b"nope".as_ptr(), 4, out.as_mut_ptr(), out.len()) };
|
|
334
|
+
assert_eq!(w, 1);
|
|
335
|
+
assert_eq!(out[0], 0);
|
|
336
|
+
|
|
337
|
+
// Too-small output → exact required size (9/1), no write past cap.
|
|
338
|
+
let w = unsafe { castrum_json_sum_ids(doc.as_ptr(), doc.len(), out.as_mut_ptr(), 1) };
|
|
339
|
+
assert_eq!(w, 9);
|
|
340
|
+
let w = unsafe { castrum_json_sum_ids(b"nope".as_ptr(), 4, out.as_mut_ptr(), 0) };
|
|
341
|
+
assert_eq!(w, 1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// Test-side decoder for the packed token stream → `serde_json::Value`,
|
|
345
|
+
/// so the C-ABI layout is verified end-to-end without the JS side.
|
|
346
|
+
fn decode_packed(bytes: &[u8]) -> serde_json::Value {
|
|
347
|
+
fn read_u32(p: &mut usize, b: &[u8]) -> u32 {
|
|
348
|
+
let v = u32::from_le_bytes(b[*p..*p + 4].try_into().unwrap());
|
|
349
|
+
*p += 4;
|
|
350
|
+
v
|
|
351
|
+
}
|
|
352
|
+
fn value(p: &mut usize, b: &[u8], strings: &[&str]) -> serde_json::Value {
|
|
353
|
+
let tag = b[*p];
|
|
354
|
+
*p += 1;
|
|
355
|
+
match tag {
|
|
356
|
+
0 => serde_json::Value::Null,
|
|
357
|
+
1 => serde_json::Value::Bool(false),
|
|
358
|
+
2 => serde_json::Value::Bool(true),
|
|
359
|
+
3 => {
|
|
360
|
+
let f = f64::from_le_bytes(b[*p..*p + 8].try_into().unwrap());
|
|
361
|
+
*p += 8;
|
|
362
|
+
serde_json::Value::from(f)
|
|
363
|
+
}
|
|
364
|
+
4 => serde_json::Value::String(strings[read_u32(p, b) as usize].to_string()),
|
|
365
|
+
5 => {
|
|
366
|
+
// array start … array end (7)
|
|
367
|
+
let mut arr = Vec::new();
|
|
368
|
+
while b[*p] != 7 {
|
|
369
|
+
arr.push(value(p, b, strings));
|
|
370
|
+
}
|
|
371
|
+
*p += 1;
|
|
372
|
+
serde_json::Value::Array(arr)
|
|
373
|
+
}
|
|
374
|
+
6 => {
|
|
375
|
+
// object start: (9, keyIdx, value)* … object end (8)
|
|
376
|
+
let mut obj = serde_json::Map::new();
|
|
377
|
+
while b[*p] != 8 {
|
|
378
|
+
assert_eq!(b[*p], 9, "expected object key tag");
|
|
379
|
+
*p += 1;
|
|
380
|
+
let k = strings[read_u32(p, b) as usize];
|
|
381
|
+
obj.insert(k.to_string(), value(p, b, strings));
|
|
382
|
+
}
|
|
383
|
+
*p += 1;
|
|
384
|
+
serde_json::Value::Object(obj)
|
|
385
|
+
}
|
|
386
|
+
_ => panic!("bad packed tag {tag}"),
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
let mut p = 0usize;
|
|
390
|
+
let str_count = read_u32(&mut p, bytes) as usize;
|
|
391
|
+
let mut strings = Vec::with_capacity(str_count);
|
|
392
|
+
for _ in 0..str_count {
|
|
393
|
+
let n = read_u32(&mut p, bytes) as usize;
|
|
394
|
+
strings.push(std::str::from_utf8(&bytes[p..p + n]).unwrap());
|
|
395
|
+
p += n;
|
|
396
|
+
}
|
|
397
|
+
let tree_len = read_u32(&mut p, bytes) as usize;
|
|
398
|
+
let end = p + tree_len;
|
|
399
|
+
let top = value(&mut p, bytes, &strings);
|
|
400
|
+
assert_eq!(p, end, "tree length must consume exactly the tree");
|
|
401
|
+
assert_eq!(end, bytes.len(), "stream must end at the tree");
|
|
402
|
+
top
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// Normalize every JSON number to f64 so int/float representations compare
|
|
406
|
+
/// equal — JS numbers are all doubles (`1 === 1.0`), and serde_json's
|
|
407
|
+
/// `Number` deliberately distinguishes them.
|
|
408
|
+
fn json_f64_normalize(v: serde_json::Value) -> serde_json::Value {
|
|
409
|
+
match v {
|
|
410
|
+
serde_json::Value::Number(n) => serde_json::Value::from(n.as_f64().unwrap_or(0.0)),
|
|
411
|
+
serde_json::Value::Array(a) => {
|
|
412
|
+
serde_json::Value::Array(a.into_iter().map(json_f64_normalize).collect())
|
|
413
|
+
}
|
|
414
|
+
serde_json::Value::Object(m) => serde_json::Value::Object(
|
|
415
|
+
m.into_iter()
|
|
416
|
+
.map(|(k, v)| (k, json_f64_normalize(v)))
|
|
417
|
+
.collect(),
|
|
418
|
+
),
|
|
419
|
+
other => other,
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#[test]
|
|
424
|
+
fn json_parse_packed_c_abi() {
|
|
425
|
+
// Valid JSON → packed token stream that round-trips exactly.
|
|
426
|
+
let doc = br#"{"a":1,"b":[true,null,"x"],"c":{"d":2.5}}"#;
|
|
427
|
+
let mut out = [0u8; 512];
|
|
428
|
+
let w =
|
|
429
|
+
unsafe { castrum_json_parse_packed(doc.as_ptr(), doc.len(), out.as_mut_ptr(), out.len()) };
|
|
430
|
+
assert!(w > 0 && w <= out.len());
|
|
431
|
+
let parsed = json_f64_normalize(decode_packed(&out[..w]));
|
|
432
|
+
let expected: serde_json::Value =
|
|
433
|
+
serde_json::from_str(r#"{"a":1,"b":[true,null,"x"],"c":{"d":2.5}}"#).unwrap();
|
|
434
|
+
assert_eq!(parsed, json_f64_normalize(expected));
|
|
435
|
+
|
|
436
|
+
// Too-small output → exact required size, no write past cap.
|
|
437
|
+
let mut tiny = [0u8; 4];
|
|
438
|
+
let w2 = unsafe {
|
|
439
|
+
castrum_json_parse_packed(doc.as_ptr(), doc.len(), tiny.as_mut_ptr(), tiny.len())
|
|
440
|
+
};
|
|
441
|
+
assert!(w2 > 4);
|
|
442
|
+
|
|
443
|
+
// Invalid JSON → 0 (real error, JS growExact throws).
|
|
444
|
+
let w3 = unsafe { castrum_json_parse_packed(b"nope".as_ptr(), 4, out.as_mut_ptr(), out.len()) };
|
|
445
|
+
assert_eq!(w3, 0);
|
|
446
|
+
// Null pointers → 0, never UB.
|
|
447
|
+
let w4 = unsafe { castrum_json_parse_packed(std::ptr::null(), 0, out.as_mut_ptr(), out.len()) };
|
|
448
|
+
assert_eq!(w4, 0);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
#[test]
|
|
452
|
+
fn json_parse_packed_dedup_strings() {
|
|
453
|
+
// Repeated keys/values must be interned ONCE into the string table so
|
|
454
|
+
// the JS side decodes each unique string a single time.
|
|
455
|
+
let doc = br#"[{"k":"v","n":"v"},{"k":"w","n":"v"}]"#;
|
|
456
|
+
let mut out = [0u8; 512];
|
|
457
|
+
let w =
|
|
458
|
+
unsafe { castrum_json_parse_packed(doc.as_ptr(), doc.len(), out.as_mut_ptr(), out.len()) };
|
|
459
|
+
assert!(w > 0 && w <= out.len());
|
|
460
|
+
// keys k,n + values v,w = 4 unique strings (not 10 raw occurrences).
|
|
461
|
+
let str_count = u32::from_le_bytes(out[..4].try_into().unwrap());
|
|
462
|
+
assert_eq!(str_count, 4);
|
|
463
|
+
let parsed = decode_packed(&out[..w]);
|
|
464
|
+
let expected: serde_json::Value =
|
|
465
|
+
serde_json::from_str(r#"[{"k":"v","n":"v"},{"k":"w","n":"v"}]"#).unwrap();
|
|
466
|
+
assert_eq!(parsed, expected);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#[test]
|
|
470
|
+
fn parse_media_type_c_abi_packed() {
|
|
471
|
+
let ct = b"application/json; charset=utf-8; foo=bar";
|
|
472
|
+
let mut out = [0u8; 256];
|
|
473
|
+
let w = unsafe { castrum_parse_media_type(ct.as_ptr(), ct.len(), out.as_mut_ptr(), out.len()) };
|
|
474
|
+
assert!(w > 0 && w <= out.len());
|
|
475
|
+
|
|
476
|
+
let mut off = 0usize;
|
|
477
|
+
let mt_len = u32::from_le_bytes(out[0..4].try_into().unwrap()) as usize;
|
|
478
|
+
off += 4;
|
|
479
|
+
assert_eq!(
|
|
480
|
+
String::from_utf8(out[off..off + mt_len].to_vec()).unwrap(),
|
|
481
|
+
"application/json"
|
|
482
|
+
);
|
|
483
|
+
off += mt_len;
|
|
484
|
+
|
|
485
|
+
// charset present.
|
|
486
|
+
let cs_len = u32::from_le_bytes(out[off..off + 4].try_into().unwrap()) as usize;
|
|
487
|
+
assert_ne!(cs_len, u32::MAX as usize);
|
|
488
|
+
off += 4;
|
|
489
|
+
assert_eq!(
|
|
490
|
+
String::from_utf8(out[off..off + cs_len].to_vec()).unwrap(),
|
|
491
|
+
"utf-8"
|
|
492
|
+
);
|
|
493
|
+
off += cs_len;
|
|
494
|
+
|
|
495
|
+
// boundary absent → u32::MAX.
|
|
496
|
+
let b_len = u32::from_le_bytes(out[off..off + 4].try_into().unwrap());
|
|
497
|
+
assert_eq!(b_len, u32::MAX);
|
|
498
|
+
off += 4;
|
|
499
|
+
|
|
500
|
+
// params = charset + foo (2).
|
|
501
|
+
let count = u32::from_le_bytes(out[off..off + 4].try_into().unwrap()) as usize;
|
|
502
|
+
assert_eq!(count, 2);
|
|
503
|
+
off += 4;
|
|
504
|
+
for _ in 0..count {
|
|
505
|
+
let klen = u32::from_le_bytes(out[off..off + 4].try_into().unwrap()) as usize;
|
|
506
|
+
off += 4;
|
|
507
|
+
off += klen;
|
|
508
|
+
let vlen = u32::from_le_bytes(out[off..off + 4].try_into().unwrap()) as usize;
|
|
509
|
+
off += 4;
|
|
510
|
+
off += vlen;
|
|
511
|
+
}
|
|
512
|
+
assert_eq!(off, w);
|
|
513
|
+
|
|
514
|
+
// Invalid media type → 0 (real error → JS throws).
|
|
515
|
+
let w = unsafe {
|
|
516
|
+
castrum_parse_media_type(b"not-a-type".as_ptr(), 10, out.as_mut_ptr(), out.len())
|
|
517
|
+
};
|
|
518
|
+
assert_eq!(w, 0);
|
|
519
|
+
|
|
520
|
+
// Too-small output → exact required size.
|
|
521
|
+
let small_cap = 1usize;
|
|
522
|
+
let w = unsafe { castrum_parse_media_type(ct.as_ptr(), ct.len(), out.as_mut_ptr(), small_cap) };
|
|
523
|
+
assert!(w > small_cap);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
#[test]
|
|
527
|
+
fn parse_http_date_c_abi_packed() {
|
|
528
|
+
let date = b"Sun, 06 Nov 1994 08:49:37 GMT";
|
|
529
|
+
let mut out = [0u8; 9];
|
|
530
|
+
let w =
|
|
531
|
+
unsafe { castrum_parse_http_date(date.as_ptr(), date.len(), out.as_mut_ptr(), out.len()) };
|
|
532
|
+
assert_eq!(w, 9);
|
|
533
|
+
assert_eq!(out[0], 1);
|
|
534
|
+
assert_eq!(
|
|
535
|
+
i64::from_le_bytes(out[1..9].try_into().unwrap()),
|
|
536
|
+
784_111_777
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
// Malformed → ok=0, 1 byte.
|
|
540
|
+
let w = unsafe { castrum_parse_http_date(b"nope".as_ptr(), 4, out.as_mut_ptr(), out.len()) };
|
|
541
|
+
assert_eq!(w, 1);
|
|
542
|
+
assert_eq!(out[0], 0);
|
|
543
|
+
|
|
544
|
+
// Too-small output → exact required size (9 / 1).
|
|
545
|
+
let w = unsafe { castrum_parse_http_date(date.as_ptr(), date.len(), out.as_mut_ptr(), 0) };
|
|
546
|
+
assert_eq!(w, 9);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#[test]
|
|
550
|
+
fn parse_accept_encoding_c_abi_packed() {
|
|
551
|
+
let header = b"gzip, deflate;q=0.5, identity;q=0.2";
|
|
552
|
+
let mut out = [0u8; 256];
|
|
553
|
+
let w = unsafe {
|
|
554
|
+
castrum_parse_accept_encoding(header.as_ptr(), header.len(), out.as_mut_ptr(), out.len())
|
|
555
|
+
};
|
|
556
|
+
assert!(w > 0 && w <= out.len());
|
|
557
|
+
|
|
558
|
+
let count = u32::from_le_bytes(out[0..4].try_into().unwrap()) as usize;
|
|
559
|
+
assert_eq!(count, 3);
|
|
560
|
+
let mut off = 4usize;
|
|
561
|
+
let enc_len = u32::from_le_bytes(out[off..off + 4].try_into().unwrap()) as usize;
|
|
562
|
+
off += 4;
|
|
563
|
+
assert_eq!(
|
|
564
|
+
String::from_utf8(out[off..off + enc_len].to_vec()).unwrap(),
|
|
565
|
+
"gzip"
|
|
566
|
+
);
|
|
567
|
+
off += enc_len;
|
|
568
|
+
let q = f32::from_le_bytes(out[off..off + 4].try_into().unwrap());
|
|
569
|
+
assert_eq!(q, 1.0);
|
|
570
|
+
off += 4;
|
|
571
|
+
let order = u32::from_le_bytes(out[off..off + 4].try_into().unwrap());
|
|
572
|
+
assert_eq!(order, 0);
|
|
573
|
+
|
|
574
|
+
// Empty header → count 0 (4 bytes), never an error.
|
|
575
|
+
let w = unsafe { castrum_parse_accept_encoding(b"".as_ptr(), 0, out.as_mut_ptr(), out.len()) };
|
|
576
|
+
assert_eq!(w, 4);
|
|
577
|
+
assert_eq!(u32::from_le_bytes(out[0..4].try_into().unwrap()), 0);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
#[test]
|
|
581
|
+
fn url_encode_query_c_abi() {
|
|
582
|
+
// Packed [u32 count]{[u32 keyLen][key][u32 valLen][val]} — unsorted.
|
|
583
|
+
let mut packed: Vec<u8> = Vec::new();
|
|
584
|
+
packed.extend_from_slice(&2u32.to_le_bytes());
|
|
585
|
+
for (k, v) in [("b", "2"), ("a", "1")] {
|
|
586
|
+
packed.extend_from_slice(&(k.len() as u32).to_le_bytes());
|
|
587
|
+
packed.extend_from_slice(k.as_bytes());
|
|
588
|
+
packed.extend_from_slice(&(v.len() as u32).to_le_bytes());
|
|
589
|
+
packed.extend_from_slice(v.as_bytes());
|
|
590
|
+
}
|
|
591
|
+
let s = unsafe { castrum_url_encode_query(packed.as_ptr(), packed.len()) };
|
|
592
|
+
assert!(!s.is_null());
|
|
593
|
+
assert_eq!(unsafe { cstr_bytes(s) }.unwrap(), b"a=1&b=2");
|
|
594
|
+
|
|
595
|
+
// Malformed packed input (truncated) → null.
|
|
596
|
+
let malformed = [1u8, 0, 0, 0];
|
|
597
|
+
let s = unsafe { castrum_url_encode_query(malformed.as_ptr(), malformed.len()) };
|
|
598
|
+
assert!(s.is_null());
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#[test]
|
|
602
|
+
fn url_resolve_c_abi() {
|
|
603
|
+
// RFC 3986 §5.4.1: base "http://a/b/c/d;p?q" + "g" → "http://a/b/c/g".
|
|
604
|
+
let base = b"http://a/b/c/d;p?q";
|
|
605
|
+
let s = unsafe { castrum_url_resolve(base.as_ptr(), base.len(), b"g".as_ptr(), 1) };
|
|
606
|
+
assert!(!s.is_null());
|
|
607
|
+
assert_eq!(unsafe { cstr_bytes(s) }.unwrap(), b"http://a/b/c/g");
|
|
608
|
+
|
|
609
|
+
// Non-UTF-8 → null (napi parity: throws).
|
|
610
|
+
let bad = [0xffu8, 0xfe, 0xfd];
|
|
611
|
+
let s = unsafe { castrum_url_resolve(base.as_ptr(), base.len(), bad.as_ptr(), bad.len()) };
|
|
612
|
+
assert!(s.is_null());
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
#[test]
|
|
616
|
+
fn url_builder_resolve_c_abi() {
|
|
617
|
+
use napi::bindgen_prelude::Uint8Array;
|
|
618
|
+
let b = crate::http::url_join::UrlBuilder::new(Uint8Array::new(b"http://a/b/c/d;p?q".to_vec()))
|
|
619
|
+
.unwrap();
|
|
620
|
+
let inner = b.inner_ptr() as usize;
|
|
621
|
+
let mut out = [0u8; 256];
|
|
622
|
+
// Precompiled base + "g" → "http://a/b/c/g" (RFC 3986 §5.4.1).
|
|
623
|
+
let w = unsafe {
|
|
624
|
+
castrum_url_builder_resolve(inner, b"g".as_ptr(), 1, out.as_mut_ptr(), out.len())
|
|
625
|
+
};
|
|
626
|
+
assert!(w > 0 && w <= out.len());
|
|
627
|
+
assert_eq!(&out[..w], b"http://a/b/c/g");
|
|
628
|
+
// Null handle → 0 (never dereferences freed state).
|
|
629
|
+
let w =
|
|
630
|
+
unsafe { castrum_url_builder_resolve(0, b"g".as_ptr(), 1, out.as_mut_ptr(), out.len()) };
|
|
631
|
+
assert_eq!(w, 0);
|
|
632
|
+
// Non-UTF-8 reference → 0 (napi parity: throws).
|
|
633
|
+
let bad = [0xffu8, 0xfe];
|
|
634
|
+
let w = unsafe {
|
|
635
|
+
castrum_url_builder_resolve(inner, bad.as_ptr(), bad.len(), out.as_mut_ptr(), out.len())
|
|
636
|
+
};
|
|
637
|
+
assert_eq!(w, 0);
|
|
638
|
+
// Too-small buffer → exact needed size.
|
|
639
|
+
let w = unsafe { castrum_url_builder_resolve(inner, b"g".as_ptr(), 1, out.as_mut_ptr(), 1) };
|
|
640
|
+
assert!(w > 1);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
#[test]
|
|
644
|
+
fn media_type_matcher_matches_c_abi() {
|
|
645
|
+
use napi::bindgen_prelude::Uint8Array;
|
|
646
|
+
let m =
|
|
647
|
+
crate::http::media_type::MediaTypeMatcher::new(Uint8Array::new(b"application/*".to_vec()))
|
|
648
|
+
.unwrap();
|
|
649
|
+
let inner = m.inner_ptr() as usize;
|
|
650
|
+
let f = |a: &[u8]| unsafe { castrum_media_type_matcher_matches(inner, a.as_ptr(), a.len()) };
|
|
651
|
+
assert_eq!(f(b"application/json"), 1);
|
|
652
|
+
assert_eq!(f(b"Application/JSON"), 1); // case-insensitive
|
|
653
|
+
assert_eq!(f(b"text/html"), 0);
|
|
654
|
+
assert_eq!(
|
|
655
|
+
unsafe { castrum_media_type_matcher_matches(0, b"a".as_ptr(), 1) },
|
|
656
|
+
0
|
|
657
|
+
); // null handle
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
#[test]
|
|
661
|
+
fn accept_negotiator_negotiate_c_abi() {
|
|
662
|
+
let n = crate::http::accept::AcceptNegotiator::new(vec!["gzip".to_string()]);
|
|
663
|
+
let inner = n.inner_ptr() as usize;
|
|
664
|
+
let f = |h: &[u8]| -> Option<Vec<u8>> {
|
|
665
|
+
let s = unsafe { castrum_accept_negotiator_negotiate(inner, h.as_ptr(), h.len()) };
|
|
666
|
+
if s.is_null() {
|
|
667
|
+
None
|
|
668
|
+
} else {
|
|
669
|
+
unsafe { cstr_bytes(s) }
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
assert_eq!(f(b"gzip, deflate;q=0.5"), Some(b"gzip".to_vec()));
|
|
673
|
+
assert_eq!(f(b"identity;q=0.9"), None); // no supported match → identity
|
|
674
|
+
assert!(unsafe { castrum_accept_negotiator_negotiate(0, b"gzip".as_ptr(), 4) }.is_null());
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
#[test]
|
|
678
|
+
fn accept_negotiator_negotiate_server_c_abi() {
|
|
679
|
+
use crate::http::accept::AcceptNegotiator;
|
|
680
|
+
let n = AcceptNegotiator::new(vec!["br".to_string(), "gzip".to_string()]);
|
|
681
|
+
let inner = n.inner_ptr() as usize;
|
|
682
|
+
let f = |h: &std::ffi::CStr| -> Option<Vec<u8>> {
|
|
683
|
+
let s = unsafe { castrum_accept_negotiator_negotiate_server(inner, h.as_ptr()) };
|
|
684
|
+
if s.is_null() {
|
|
685
|
+
None
|
|
686
|
+
} else {
|
|
687
|
+
unsafe { cstr_bytes(s) }
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
// Server-preference: tie → first supported (br), NOT client order.
|
|
691
|
+
assert_eq!(f(c"gzip, br"), Some(b"br".to_vec()));
|
|
692
|
+
assert_eq!(f(c"br;q=0.8, gzip;q=0.9"), Some(b"gzip".to_vec()));
|
|
693
|
+
assert_eq!(f(c"*"), Some(b"br".to_vec()));
|
|
694
|
+
assert_eq!(f(c"gzip;q=0"), None); // q=0 excluded, no wildcard
|
|
695
|
+
assert_eq!(f(c""), None); // empty → identity (server-pref differs from client-order)
|
|
696
|
+
assert!(unsafe { castrum_accept_negotiator_negotiate_server(0, c"gzip".as_ptr()) }.is_null());
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
#[test]
|
|
700
|
+
fn jwt_signer_sign_verify_c_abi() {
|
|
701
|
+
use napi::bindgen_prelude::Uint8Array;
|
|
702
|
+
let s = crate::crypto::jwt::JwtSigner::new(Uint8Array::new(b"my-secret".to_vec()), Some(0));
|
|
703
|
+
let inner = s.inner_ptr() as usize;
|
|
704
|
+
let claims = b"{\"sub\":\"user-1\"}";
|
|
705
|
+
let mut out = [0u8; 512];
|
|
706
|
+
let mut vout = [0u8; 512];
|
|
707
|
+
let w = unsafe {
|
|
708
|
+
castrum_jwt_signer_sign(
|
|
709
|
+
inner,
|
|
710
|
+
claims.as_ptr(),
|
|
711
|
+
claims.len(),
|
|
712
|
+
0,
|
|
713
|
+
out.as_mut_ptr(),
|
|
714
|
+
out.len(),
|
|
715
|
+
)
|
|
716
|
+
};
|
|
717
|
+
assert!(w > 0 && w <= out.len());
|
|
718
|
+
let token = &out[..w];
|
|
719
|
+
assert_eq!(token.iter().filter(|&&b| b == b'.').count(), 2);
|
|
720
|
+
// Verify round-trip → claims JSON.
|
|
721
|
+
let vw = unsafe {
|
|
722
|
+
castrum_jwt_signer_verify(
|
|
723
|
+
inner,
|
|
724
|
+
token.as_ptr(),
|
|
725
|
+
token.len(),
|
|
726
|
+
0,
|
|
727
|
+
vout.as_mut_ptr(),
|
|
728
|
+
vout.len(),
|
|
729
|
+
)
|
|
730
|
+
};
|
|
731
|
+
assert!(vw > 0);
|
|
732
|
+
assert!(String::from_utf8(vout[..vw].to_vec())
|
|
733
|
+
.unwrap()
|
|
734
|
+
.contains("\"sub\":\"user-1\""));
|
|
735
|
+
// Tampered → 0 (invalid).
|
|
736
|
+
let mut bad = token.to_vec();
|
|
737
|
+
let last = bad.len() - 1;
|
|
738
|
+
bad[last] ^= 0x01;
|
|
739
|
+
let bw = unsafe {
|
|
740
|
+
castrum_jwt_signer_verify(
|
|
741
|
+
inner,
|
|
742
|
+
bad.as_ptr(),
|
|
743
|
+
bad.len(),
|
|
744
|
+
0,
|
|
745
|
+
vout.as_mut_ptr(),
|
|
746
|
+
vout.len(),
|
|
747
|
+
)
|
|
748
|
+
};
|
|
749
|
+
assert_eq!(bw, 0);
|
|
750
|
+
// Invalid claims JSON → 0; null handle → 0.
|
|
751
|
+
let w = unsafe {
|
|
752
|
+
castrum_jwt_signer_sign(inner, b"nope".as_ptr(), 4, 0, out.as_mut_ptr(), out.len())
|
|
753
|
+
};
|
|
754
|
+
assert_eq!(w, 0);
|
|
755
|
+
let w = unsafe {
|
|
756
|
+
castrum_jwt_signer_sign(
|
|
757
|
+
0,
|
|
758
|
+
claims.as_ptr(),
|
|
759
|
+
claims.len(),
|
|
760
|
+
0,
|
|
761
|
+
out.as_mut_ptr(),
|
|
762
|
+
out.len(),
|
|
763
|
+
)
|
|
764
|
+
};
|
|
765
|
+
assert_eq!(w, 0);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
#[test]
|
|
769
|
+
fn template_render_c_abi() {
|
|
770
|
+
let t =
|
|
771
|
+
crate::payload::template::TemplateRenderer::new("Hello {{ name }}!".to_string()).unwrap();
|
|
772
|
+
let inner = t.inner_ptr() as usize;
|
|
773
|
+
let ctx = b"{\"name\":\"world\"}";
|
|
774
|
+
let mut out = [0u8; 128];
|
|
775
|
+
let w = unsafe {
|
|
776
|
+
castrum_template_render(inner, ctx.as_ptr(), ctx.len(), out.as_mut_ptr(), out.len())
|
|
777
|
+
};
|
|
778
|
+
assert!(w > 0);
|
|
779
|
+
assert_eq!(&out[..w], b"Hello world!");
|
|
780
|
+
// Invalid context JSON → 0; null handle → 0.
|
|
781
|
+
let w =
|
|
782
|
+
unsafe { castrum_template_render(inner, b"nope".as_ptr(), 4, out.as_mut_ptr(), out.len()) };
|
|
783
|
+
assert_eq!(w, 0);
|
|
784
|
+
let w =
|
|
785
|
+
unsafe { castrum_template_render(0, ctx.as_ptr(), ctx.len(), out.as_mut_ptr(), out.len()) };
|
|
786
|
+
assert_eq!(w, 0);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
#[test]
|
|
790
|
+
fn schema_validator_validate_c_abi() {
|
|
791
|
+
use napi::bindgen_prelude::Uint8Array;
|
|
792
|
+
let schema =
|
|
793
|
+
b"{\"type\":\"object\",\"required\":[\"a\"],\"properties\":{\"a\":{\"type\":\"number\"}}}";
|
|
794
|
+
let v =
|
|
795
|
+
crate::json::json_schema::SchemaValidator::new(Uint8Array::new(schema.to_vec())).unwrap();
|
|
796
|
+
let inner = v.inner_ptr() as usize;
|
|
797
|
+
let f = |d: &[u8]| unsafe { castrum_schema_validator_validate(inner, d.as_ptr(), d.len()) };
|
|
798
|
+
assert_eq!(f(b"{\"a\":1}"), 1);
|
|
799
|
+
assert_eq!(f(b"{}"), 0); // missing required "a"
|
|
800
|
+
assert_eq!(f(b"not-json"), 0);
|
|
801
|
+
assert_eq!(
|
|
802
|
+
unsafe { castrum_schema_validator_validate(0, b"{}".as_ptr(), 2) },
|
|
803
|
+
0
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
#[test]
|
|
808
|
+
fn rate_limiter_check_c_abi() {
|
|
809
|
+
let r = crate::ingress::rate_limit::RateLimiter::new(2, 60_000, Some(1024));
|
|
810
|
+
let inner = r.inner_ptr() as usize;
|
|
811
|
+
let now = 1_700_000_000_000i64;
|
|
812
|
+
let key = c"user-42";
|
|
813
|
+
let mut out = [0u8; 13];
|
|
814
|
+
let w = unsafe {
|
|
815
|
+
castrum_rate_limiter_check(inner, key.as_ptr(), now, out.as_mut_ptr(), out.len())
|
|
816
|
+
};
|
|
817
|
+
assert_eq!(w, 13);
|
|
818
|
+
assert_eq!(out[0], 1); // allowed (1/2)
|
|
819
|
+
unsafe { castrum_rate_limiter_check(inner, key.as_ptr(), now, out.as_mut_ptr(), out.len()) };
|
|
820
|
+
let w = unsafe {
|
|
821
|
+
castrum_rate_limiter_check(inner, key.as_ptr(), now, out.as_mut_ptr(), out.len())
|
|
822
|
+
};
|
|
823
|
+
assert_eq!(w, 13);
|
|
824
|
+
assert_eq!(out[0], 0); // blocked (3/2)
|
|
825
|
+
// Pre-hashed check_key shares the SAME budget.
|
|
826
|
+
let hashed = crate::crypto::hashing::fast_hash_bytes(key.to_bytes());
|
|
827
|
+
let w = unsafe {
|
|
828
|
+
castrum_rate_limiter_check_key(inner, hashed as i64, now, out.as_mut_ptr(), out.len())
|
|
829
|
+
};
|
|
830
|
+
assert_eq!(w, 13);
|
|
831
|
+
assert_eq!(out[0], 0);
|
|
832
|
+
// Null handle → 0; too-small → exact needed size.
|
|
833
|
+
let w =
|
|
834
|
+
unsafe { castrum_rate_limiter_check(0, key.as_ptr(), now, out.as_mut_ptr(), out.len()) };
|
|
835
|
+
assert_eq!(w, 0);
|
|
836
|
+
let w = unsafe { castrum_rate_limiter_check(inner, key.as_ptr(), now, out.as_mut_ptr(), 1) };
|
|
837
|
+
assert_eq!(w, 13);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
#[test]
|
|
841
|
+
fn mime_from_extension_c_abi() {
|
|
842
|
+
let s = unsafe { castrum_mime_from_extension(c".js".as_ptr()) };
|
|
843
|
+
assert!(!s.is_null());
|
|
844
|
+
assert_eq!(unsafe { cstr_bytes(s) }.unwrap(), b"text/javascript");
|
|
845
|
+
|
|
846
|
+
// Unknown extension → application/octet-stream (never null).
|
|
847
|
+
let s = unsafe { castrum_mime_from_extension(c"nope".as_ptr()) };
|
|
848
|
+
assert!(!s.is_null());
|
|
849
|
+
assert_eq!(
|
|
850
|
+
unsafe { cstr_bytes(s) }.unwrap(),
|
|
851
|
+
b"application/octet-stream"
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
#[test]
|
|
856
|
+
fn conditional_is_not_modified_c_abi() {
|
|
857
|
+
use napi::bindgen_prelude::Uint8Array;
|
|
858
|
+
let c = crate::http::etag::ConditionalRequest::new(
|
|
859
|
+
Uint8Array::new(b"\"abc123\"".to_vec()),
|
|
860
|
+
Some(784_111_777f64),
|
|
861
|
+
);
|
|
862
|
+
let inner = c.inner_ptr() as usize;
|
|
863
|
+
let f = |flags: u8, inm: Option<&[u8]>, ims: Option<&[u8]>| -> u8 {
|
|
864
|
+
let (ip, il) = inm.map_or((std::ptr::null(), 0usize), |s| (s.as_ptr(), s.len()));
|
|
865
|
+
let (sp, sl) = ims.map_or((std::ptr::null(), 0usize), |s| (s.as_ptr(), s.len()));
|
|
866
|
+
unsafe { castrum_conditional_is_not_modified(inner, ip, il, sp, sl, flags) }
|
|
867
|
+
};
|
|
868
|
+
// If-None-Match "*" → 304.
|
|
869
|
+
assert_eq!(f(1, Some(b"*"), None), 1);
|
|
870
|
+
// Exact etag → 304; weak compare W/"abc123" → 304.
|
|
871
|
+
assert_eq!(f(1, Some(b"\"abc123\""), None), 1);
|
|
872
|
+
assert_eq!(f(1, Some(b"W/\"abc123\""), None), 1);
|
|
873
|
+
// Non-matching list → not 304.
|
|
874
|
+
assert_eq!(f(1, Some(b"\"xyz\", \"other\""), None), 0);
|
|
875
|
+
// If-Modified-Since == lastModified → 304; 1s before → not 304.
|
|
876
|
+
assert_eq!(f(2, None, Some(b"Sun, 06 Nov 1994 08:49:37 GMT")), 1);
|
|
877
|
+
assert_eq!(f(2, None, Some(b"Sun, 06 Nov 1994 08:49:36 GMT")), 0);
|
|
878
|
+
// Absent flags → not 304 (nothing to match).
|
|
879
|
+
assert_eq!(f(0, None, None), 0);
|
|
880
|
+
// Null handle → 0 (never dereferences freed state).
|
|
881
|
+
assert_eq!(
|
|
882
|
+
unsafe {
|
|
883
|
+
castrum_conditional_is_not_modified(0, std::ptr::null(), 0, std::ptr::null(), 0, 0)
|
|
884
|
+
},
|
|
885
|
+
0
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
#[test]
|
|
890
|
+
fn ws_accept_key_c_abi_rfc6455() {
|
|
891
|
+
// RFC 6455 Sec-WebSocket-Accept test vector.
|
|
892
|
+
let key = c"dGhlIHNhbXBsZSBub25jZQ==";
|
|
893
|
+
let accept = unsafe { castrum_ws_accept_key(key.as_ptr()) };
|
|
894
|
+
assert!(!accept.is_null());
|
|
895
|
+
let bytes = unsafe { cstr_bytes(accept) }.unwrap();
|
|
896
|
+
assert_eq!(bytes, b"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
#[test]
|
|
900
|
+
fn etag_c_abi() {
|
|
901
|
+
let data = b"hello";
|
|
902
|
+
let strong = unsafe { castrum_etag(data.as_ptr(), data.len(), 0) };
|
|
903
|
+
assert!(!strong.is_null());
|
|
904
|
+
let strong_bytes = unsafe { cstr_bytes(strong) }.unwrap();
|
|
905
|
+
let crc = crc32fast::hash(b"hello");
|
|
906
|
+
let expected = format!("\"{crc:08x}\"");
|
|
907
|
+
assert_eq!(strong_bytes, expected.as_bytes());
|
|
908
|
+
|
|
909
|
+
let weak = unsafe { castrum_etag(data.as_ptr(), data.len(), 1) };
|
|
910
|
+
assert!(!weak.is_null());
|
|
911
|
+
let weak_bytes = unsafe { cstr_bytes(weak) }.unwrap();
|
|
912
|
+
assert_eq!(&weak_bytes[..2], b"W/");
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
#[test]
|
|
916
|
+
fn random_token_c_abi() {
|
|
917
|
+
let t = unsafe { castrum_random_token(16) };
|
|
918
|
+
assert!(!t.is_null());
|
|
919
|
+
let bytes = unsafe { cstr_bytes(t) }.unwrap();
|
|
920
|
+
assert_eq!(bytes.len(), 32);
|
|
921
|
+
assert!(bytes.iter().all(u8::is_ascii_hexdigit));
|
|
922
|
+
// byte_len 0 → valid empty string, not null.
|
|
923
|
+
assert!(!unsafe { castrum_random_token(0) }.is_null());
|
|
924
|
+
// Huge len → null (real error).
|
|
925
|
+
assert!(unsafe { castrum_random_token(u32::MAX) }.is_null());
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
#[test]
|
|
929
|
+
fn random_token_into_c_abi() {
|
|
930
|
+
let mut out = [0u8; 64];
|
|
931
|
+
let w = unsafe { castrum_random_token_into(16, out.as_mut_ptr(), out.len()) };
|
|
932
|
+
assert_eq!(w, 32);
|
|
933
|
+
assert!(out[..32].iter().all(u8::is_ascii_hexdigit));
|
|
934
|
+
// Too-small buffer → exact needed size (not a partial write).
|
|
935
|
+
let mut small = [0u8; 16];
|
|
936
|
+
let w2 = unsafe { castrum_random_token_into(16, small.as_mut_ptr(), small.len()) };
|
|
937
|
+
assert_eq!(w2, 32);
|
|
938
|
+
assert_eq!(&small[..], &[0u8; 16]);
|
|
939
|
+
// byte_len 0 → writes 0 bytes.
|
|
940
|
+
let mut zero = [0u8; 4];
|
|
941
|
+
let w3 = unsafe { castrum_random_token_into(0, zero.as_mut_ptr(), zero.len()) };
|
|
942
|
+
assert_eq!(w3, 0);
|
|
943
|
+
// Huge len → 0 (real error); null out → 0.
|
|
944
|
+
assert_eq!(
|
|
945
|
+
unsafe { castrum_random_token_into(u32::MAX, out.as_mut_ptr(), out.len()) },
|
|
946
|
+
0
|
|
947
|
+
);
|
|
948
|
+
assert_eq!(
|
|
949
|
+
unsafe { castrum_random_token_into(16, std::ptr::null_mut(), 0) },
|
|
950
|
+
0
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
#[test]
|
|
955
|
+
fn cstring_into_variants_match_cstring() {
|
|
956
|
+
// ws_accept_key: into == cstring bytes. The `_into` sibling keeps the
|
|
957
|
+
// `(ptr,len)` byte form; the cstring variant takes a NUL-terminated arg.
|
|
958
|
+
let key = b"dGhlIHNhbXBsZSBub25jZQ==";
|
|
959
|
+
let key_c = c"dGhlIHNhbXBsZSBub25jZQ==";
|
|
960
|
+
let mut out = [0u8; 28];
|
|
961
|
+
let w =
|
|
962
|
+
unsafe { castrum_ws_accept_key_into(key.as_ptr(), key.len(), out.as_mut_ptr(), out.len()) };
|
|
963
|
+
assert_eq!(w, 28);
|
|
964
|
+
let cstr_result = unsafe { cstr_bytes(castrum_ws_accept_key(key_c.as_ptr())) }.unwrap();
|
|
965
|
+
assert_eq!(&out[..], &cstr_result[..]);
|
|
966
|
+
// Too-small → 28.
|
|
967
|
+
let mut small = [0u8; 8];
|
|
968
|
+
assert_eq!(
|
|
969
|
+
unsafe {
|
|
970
|
+
castrum_ws_accept_key_into(key.as_ptr(), key.len(), small.as_mut_ptr(), small.len())
|
|
971
|
+
},
|
|
972
|
+
28
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
// etag strong/weak.
|
|
976
|
+
let data = b"hello";
|
|
977
|
+
for weak in [0u8, 1u8] {
|
|
978
|
+
let mut eout = [0u8; 16];
|
|
979
|
+
let ew = unsafe {
|
|
980
|
+
castrum_etag_into(
|
|
981
|
+
data.as_ptr(),
|
|
982
|
+
data.len(),
|
|
983
|
+
weak,
|
|
984
|
+
eout.as_mut_ptr(),
|
|
985
|
+
eout.len(),
|
|
986
|
+
)
|
|
987
|
+
};
|
|
988
|
+
let expect = if weak != 0 { 12 } else { 10 };
|
|
989
|
+
assert_eq!(ew, expect);
|
|
990
|
+
let ecstr_result =
|
|
991
|
+
unsafe { cstr_bytes(castrum_etag(data.as_ptr(), data.len(), weak)) }.unwrap();
|
|
992
|
+
assert_eq!(&eout[..ew], &ecstr_result[..]);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// sign/verify cookie round-trip via into.
|
|
996
|
+
let value = b"session=abc123";
|
|
997
|
+
let secret = b"secret-key";
|
|
998
|
+
let mut sout = [0u8; 256];
|
|
999
|
+
let sw = unsafe {
|
|
1000
|
+
castrum_sign_cookie_into(
|
|
1001
|
+
value.as_ptr(),
|
|
1002
|
+
value.len(),
|
|
1003
|
+
secret.as_ptr(),
|
|
1004
|
+
secret.len(),
|
|
1005
|
+
sout.as_mut_ptr(),
|
|
1006
|
+
sout.len(),
|
|
1007
|
+
)
|
|
1008
|
+
};
|
|
1009
|
+
assert_eq!(sw, value.len() + 65);
|
|
1010
|
+
let signed = &sout[..sw];
|
|
1011
|
+
// Cross-check against the cstring path.
|
|
1012
|
+
let scstr_result = unsafe {
|
|
1013
|
+
cstr_bytes(castrum_sign_cookie(
|
|
1014
|
+
value.as_ptr(),
|
|
1015
|
+
value.len(),
|
|
1016
|
+
secret.as_ptr(),
|
|
1017
|
+
secret.len(),
|
|
1018
|
+
))
|
|
1019
|
+
}
|
|
1020
|
+
.unwrap();
|
|
1021
|
+
assert_eq!(signed, &scstr_result[..]);
|
|
1022
|
+
// Verify via into.
|
|
1023
|
+
let mut vout = [0u8; 256];
|
|
1024
|
+
let vw = unsafe {
|
|
1025
|
+
castrum_verify_cookie_into(
|
|
1026
|
+
signed.as_ptr(),
|
|
1027
|
+
signed.len(),
|
|
1028
|
+
secret.as_ptr(),
|
|
1029
|
+
secret.len(),
|
|
1030
|
+
vout.as_mut_ptr(),
|
|
1031
|
+
vout.len(),
|
|
1032
|
+
)
|
|
1033
|
+
};
|
|
1034
|
+
assert_eq!(vw, value.len());
|
|
1035
|
+
assert_eq!(&vout[..vw], value);
|
|
1036
|
+
// Tampered → 0.
|
|
1037
|
+
let mut tampered = signed.to_vec();
|
|
1038
|
+
tampered[0] ^= 1;
|
|
1039
|
+
let vw0 = unsafe {
|
|
1040
|
+
castrum_verify_cookie_into(
|
|
1041
|
+
tampered.as_ptr(),
|
|
1042
|
+
tampered.len(),
|
|
1043
|
+
secret.as_ptr(),
|
|
1044
|
+
secret.len(),
|
|
1045
|
+
vout.as_mut_ptr(),
|
|
1046
|
+
vout.len(),
|
|
1047
|
+
)
|
|
1048
|
+
};
|
|
1049
|
+
assert_eq!(vw0, 0);
|
|
1050
|
+
|
|
1051
|
+
// csrf_token into: 129 bytes, hex.hex.
|
|
1052
|
+
let mut cout = [0u8; 129];
|
|
1053
|
+
let cw = unsafe {
|
|
1054
|
+
castrum_csrf_token_into(secret.as_ptr(), secret.len(), cout.as_mut_ptr(), cout.len())
|
|
1055
|
+
};
|
|
1056
|
+
assert_eq!(cw, 129);
|
|
1057
|
+
assert_eq!(cout[64], b'.');
|
|
1058
|
+
assert!(cout[..64].iter().all(u8::is_ascii_hexdigit));
|
|
1059
|
+
assert!(cout[65..].iter().all(u8::is_ascii_hexdigit));
|
|
1060
|
+
// Too-small → 129.
|
|
1061
|
+
let mut csmall = [0u8; 8];
|
|
1062
|
+
assert_eq!(
|
|
1063
|
+
unsafe {
|
|
1064
|
+
castrum_csrf_token_into(
|
|
1065
|
+
secret.as_ptr(),
|
|
1066
|
+
secret.len(),
|
|
1067
|
+
csmall.as_mut_ptr(),
|
|
1068
|
+
csmall.len(),
|
|
1069
|
+
)
|
|
1070
|
+
},
|
|
1071
|
+
129
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
// jwt_sign_bytes into == cstring.
|
|
1075
|
+
let claims = b"{\"sub\":\"user-1\"}";
|
|
1076
|
+
let mut jout = [0u8; 512];
|
|
1077
|
+
let jw = unsafe {
|
|
1078
|
+
castrum_jwt_sign_bytes_into(
|
|
1079
|
+
claims.as_ptr(),
|
|
1080
|
+
claims.len(),
|
|
1081
|
+
secret.as_ptr(),
|
|
1082
|
+
secret.len(),
|
|
1083
|
+
0,
|
|
1084
|
+
0,
|
|
1085
|
+
jout.as_mut_ptr(),
|
|
1086
|
+
jout.len(),
|
|
1087
|
+
)
|
|
1088
|
+
};
|
|
1089
|
+
assert!(jw > 0);
|
|
1090
|
+
let jcstr = unsafe {
|
|
1091
|
+
cstr_bytes(castrum_jwt_sign_bytes(
|
|
1092
|
+
claims.as_ptr(),
|
|
1093
|
+
claims.len(),
|
|
1094
|
+
secret.as_ptr(),
|
|
1095
|
+
secret.len(),
|
|
1096
|
+
0,
|
|
1097
|
+
0,
|
|
1098
|
+
))
|
|
1099
|
+
}
|
|
1100
|
+
.unwrap();
|
|
1101
|
+
assert_eq!(&jout[..jw], &jcstr[..]);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
#[test]
|
|
1105
|
+
fn base64_c_abi_roundtrip() {
|
|
1106
|
+
let data = b"hello world";
|
|
1107
|
+
let mut enc = [0u8; 32];
|
|
1108
|
+
let w = unsafe {
|
|
1109
|
+
castrum_base64_encode(data.as_ptr(), data.len(), enc.as_mut_ptr(), enc.len(), 0, 1)
|
|
1110
|
+
};
|
|
1111
|
+
assert_eq!(&enc[..w], b"aGVsbG8gd29ybGQ=");
|
|
1112
|
+
let mut dec = [0u8; 32];
|
|
1113
|
+
let d = unsafe { castrum_base64_decode(enc.as_ptr(), w, dec.as_mut_ptr(), dec.len(), 0, 1) };
|
|
1114
|
+
assert_eq!(&dec[..d], data);
|
|
1115
|
+
// Invalid input → 0.
|
|
1116
|
+
assert_eq!(
|
|
1117
|
+
unsafe { castrum_base64_decode(b"!!!".as_ptr(), 3, dec.as_mut_ptr(), dec.len(), 0, 1) },
|
|
1118
|
+
0
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
#[test]
|
|
1123
|
+
fn hmac_c_abi() {
|
|
1124
|
+
let key = b"key";
|
|
1125
|
+
let data = b"The quick brown fox jumps over the lazy dog";
|
|
1126
|
+
let mut out = [0u8; 64];
|
|
1127
|
+
let w = unsafe {
|
|
1128
|
+
castrum_hmac_sha256(
|
|
1129
|
+
key.as_ptr(),
|
|
1130
|
+
key.len(),
|
|
1131
|
+
data.as_ptr(),
|
|
1132
|
+
data.len(),
|
|
1133
|
+
out.as_mut_ptr(),
|
|
1134
|
+
out.len(),
|
|
1135
|
+
)
|
|
1136
|
+
};
|
|
1137
|
+
assert_eq!(w, 64);
|
|
1138
|
+
assert_eq!(
|
|
1139
|
+
&out[..],
|
|
1140
|
+
b"f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"
|
|
1141
|
+
);
|
|
1142
|
+
let ok = unsafe {
|
|
1143
|
+
castrum_hmac_sha256_verify(
|
|
1144
|
+
key.as_ptr(),
|
|
1145
|
+
key.len(),
|
|
1146
|
+
data.as_ptr(),
|
|
1147
|
+
data.len(),
|
|
1148
|
+
out.as_ptr(),
|
|
1149
|
+
out.len(),
|
|
1150
|
+
)
|
|
1151
|
+
};
|
|
1152
|
+
assert_eq!(ok, 1);
|
|
1153
|
+
out[0] ^= 1;
|
|
1154
|
+
let bad = unsafe {
|
|
1155
|
+
castrum_hmac_sha256_verify(
|
|
1156
|
+
key.as_ptr(),
|
|
1157
|
+
key.len(),
|
|
1158
|
+
data.as_ptr(),
|
|
1159
|
+
data.len(),
|
|
1160
|
+
out.as_ptr(),
|
|
1161
|
+
out.len(),
|
|
1162
|
+
)
|
|
1163
|
+
};
|
|
1164
|
+
assert_eq!(bad, 0);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
#[test]
|
|
1168
|
+
fn sign_verify_cookie_c_abi() {
|
|
1169
|
+
let value = b"session=abc";
|
|
1170
|
+
let secret = b"secret-key";
|
|
1171
|
+
let signed =
|
|
1172
|
+
unsafe { castrum_sign_cookie(value.as_ptr(), value.len(), secret.as_ptr(), secret.len()) };
|
|
1173
|
+
assert!(!signed.is_null());
|
|
1174
|
+
let signed_bytes = unsafe { cstr_bytes(signed) }.unwrap();
|
|
1175
|
+
assert!(signed_bytes.len() > value.len());
|
|
1176
|
+
|
|
1177
|
+
let verified = unsafe {
|
|
1178
|
+
castrum_verify_cookie(
|
|
1179
|
+
signed_bytes.as_ptr(),
|
|
1180
|
+
signed_bytes.len(),
|
|
1181
|
+
secret.as_ptr(),
|
|
1182
|
+
secret.len(),
|
|
1183
|
+
)
|
|
1184
|
+
};
|
|
1185
|
+
assert!(!verified.is_null());
|
|
1186
|
+
let out = unsafe { cstr_bytes(verified) }.unwrap();
|
|
1187
|
+
assert_eq!(out, value);
|
|
1188
|
+
|
|
1189
|
+
// Tampered signature → null.
|
|
1190
|
+
let mut tampered = signed_bytes.clone();
|
|
1191
|
+
let last = tampered.len() - 1;
|
|
1192
|
+
tampered[last] ^= 1;
|
|
1193
|
+
let bad = unsafe {
|
|
1194
|
+
castrum_verify_cookie(
|
|
1195
|
+
tampered.as_ptr(),
|
|
1196
|
+
tampered.len(),
|
|
1197
|
+
secret.as_ptr(),
|
|
1198
|
+
secret.len(),
|
|
1199
|
+
)
|
|
1200
|
+
};
|
|
1201
|
+
assert!(bad.is_null());
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
#[test]
|
|
1205
|
+
fn csrf_c_abi() {
|
|
1206
|
+
let secret = b"csrf-secret";
|
|
1207
|
+
let token = unsafe { castrum_csrf_token(secret.as_ptr(), secret.len()) };
|
|
1208
|
+
assert!(!token.is_null());
|
|
1209
|
+
let token_bytes = unsafe { cstr_bytes(token) }.unwrap();
|
|
1210
|
+
assert_eq!(token_bytes.len(), 129);
|
|
1211
|
+
assert_eq!(token_bytes[64], b'.');
|
|
1212
|
+
let ok = unsafe {
|
|
1213
|
+
castrum_csrf_verify(
|
|
1214
|
+
token_bytes.as_ptr(),
|
|
1215
|
+
token_bytes.len(),
|
|
1216
|
+
secret.as_ptr(),
|
|
1217
|
+
secret.len(),
|
|
1218
|
+
)
|
|
1219
|
+
};
|
|
1220
|
+
assert_eq!(ok, 1);
|
|
1221
|
+
// Wrong secret → 0.
|
|
1222
|
+
let bad = unsafe {
|
|
1223
|
+
castrum_csrf_verify(
|
|
1224
|
+
token_bytes.as_ptr(),
|
|
1225
|
+
token_bytes.len(),
|
|
1226
|
+
b"other".as_ptr(),
|
|
1227
|
+
5,
|
|
1228
|
+
)
|
|
1229
|
+
};
|
|
1230
|
+
assert_eq!(bad, 0);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
#[test]
|
|
1234
|
+
fn password_hash_verify_c_abi() {
|
|
1235
|
+
let pw = b"hunter2";
|
|
1236
|
+
let salt = b"saltsalt";
|
|
1237
|
+
let mut phc = [0u8; 512];
|
|
1238
|
+
let w = unsafe {
|
|
1239
|
+
castrum_password_hash(
|
|
1240
|
+
pw.as_ptr(),
|
|
1241
|
+
pw.len(),
|
|
1242
|
+
salt.as_ptr(),
|
|
1243
|
+
salt.len(),
|
|
1244
|
+
19_456,
|
|
1245
|
+
2,
|
|
1246
|
+
1,
|
|
1247
|
+
32,
|
|
1248
|
+
phc.as_mut_ptr(),
|
|
1249
|
+
phc.len(),
|
|
1250
|
+
)
|
|
1251
|
+
};
|
|
1252
|
+
assert!(w > 0);
|
|
1253
|
+
let ok = unsafe { castrum_password_verify(pw.as_ptr(), pw.len(), phc.as_ptr(), w) };
|
|
1254
|
+
assert_eq!(ok, 1);
|
|
1255
|
+
let bad = unsafe { castrum_password_verify(b"wrong".as_ptr(), 5, phc.as_ptr(), w) };
|
|
1256
|
+
assert_eq!(bad, 0);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
#[test]
|
|
1260
|
+
fn bcrypt_c_abi() {
|
|
1261
|
+
let pw = b"hunter2";
|
|
1262
|
+
let mut phc = [0u8; 128];
|
|
1263
|
+
let w = unsafe {
|
|
1264
|
+
castrum_password_hash_bcrypt(pw.as_ptr(), pw.len(), 4, phc.as_mut_ptr(), phc.len())
|
|
1265
|
+
};
|
|
1266
|
+
assert!(w > 0);
|
|
1267
|
+
assert_eq!(&phc[..4], b"$2b$");
|
|
1268
|
+
// The hash crosses as a NUL-terminated C string (like the engine's
|
|
1269
|
+
// `cstring` arg transcode) — CString::new makes that explicit.
|
|
1270
|
+
let phc_c = std::ffi::CString::new(&phc[..w]).unwrap();
|
|
1271
|
+
let ok = unsafe { castrum_password_verify_bcrypt(pw.as_ptr(), pw.len(), phc_c.as_ptr()) };
|
|
1272
|
+
assert_eq!(ok, 1);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
#[test]
|
|
1276
|
+
fn pbkdf2_c_abi_matches_napi() {
|
|
1277
|
+
let pw = b"password";
|
|
1278
|
+
let salt = b"salt";
|
|
1279
|
+
let mut out = [0u8; 32];
|
|
1280
|
+
let w = unsafe {
|
|
1281
|
+
castrum_pbkdf2_sha256(
|
|
1282
|
+
pw.as_ptr(),
|
|
1283
|
+
pw.len(),
|
|
1284
|
+
salt.as_ptr(),
|
|
1285
|
+
salt.len(),
|
|
1286
|
+
1,
|
|
1287
|
+
32,
|
|
1288
|
+
out.as_mut_ptr(),
|
|
1289
|
+
out.len(),
|
|
1290
|
+
)
|
|
1291
|
+
};
|
|
1292
|
+
assert_eq!(w, 32);
|
|
1293
|
+
// Cross-check against the napi fn (which pins the RFC 7914 vector).
|
|
1294
|
+
let napi = crate::crypto::pbkdf2::pbkdf2_sha256(
|
|
1295
|
+
napi::bindgen_prelude::Uint8Array::new(pw.to_vec()),
|
|
1296
|
+
napi::bindgen_prelude::Uint8Array::new(salt.to_vec()),
|
|
1297
|
+
1,
|
|
1298
|
+
32,
|
|
1299
|
+
)
|
|
1300
|
+
.unwrap();
|
|
1301
|
+
assert_eq!(&out[..], napi.as_ref());
|
|
1302
|
+
// Too-small buffer → 0.
|
|
1303
|
+
let mut tiny = [0u8; 4];
|
|
1304
|
+
assert_eq!(
|
|
1305
|
+
unsafe {
|
|
1306
|
+
castrum_pbkdf2_sha256(
|
|
1307
|
+
pw.as_ptr(),
|
|
1308
|
+
pw.len(),
|
|
1309
|
+
salt.as_ptr(),
|
|
1310
|
+
salt.len(),
|
|
1311
|
+
1,
|
|
1312
|
+
32,
|
|
1313
|
+
tiny.as_mut_ptr(),
|
|
1314
|
+
tiny.len(),
|
|
1315
|
+
)
|
|
1316
|
+
},
|
|
1317
|
+
0
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
#[test]
|
|
1322
|
+
fn aead_c_abi_roundtrip() {
|
|
1323
|
+
let key = b"0123456789abcdef0123456789abcdef"; // 32 bytes
|
|
1324
|
+
let nonce = b"123456789012"; // 12 bytes
|
|
1325
|
+
let plaintext = b"sensitive payload";
|
|
1326
|
+
let mut ct = [0u8; 128];
|
|
1327
|
+
let w = unsafe {
|
|
1328
|
+
castrum_aead_encrypt(
|
|
1329
|
+
key.as_ptr(),
|
|
1330
|
+
key.len(),
|
|
1331
|
+
nonce.as_ptr(),
|
|
1332
|
+
nonce.len(),
|
|
1333
|
+
plaintext.as_ptr(),
|
|
1334
|
+
plaintext.len(),
|
|
1335
|
+
0,
|
|
1336
|
+
ct.as_mut_ptr(),
|
|
1337
|
+
ct.len(),
|
|
1338
|
+
)
|
|
1339
|
+
};
|
|
1340
|
+
assert_eq!(w, plaintext.len() + 16);
|
|
1341
|
+
let mut pt = [0u8; 128];
|
|
1342
|
+
let d = unsafe {
|
|
1343
|
+
castrum_aead_decrypt(
|
|
1344
|
+
key.as_ptr(),
|
|
1345
|
+
key.len(),
|
|
1346
|
+
nonce.as_ptr(),
|
|
1347
|
+
nonce.len(),
|
|
1348
|
+
ct.as_ptr(),
|
|
1349
|
+
w,
|
|
1350
|
+
0,
|
|
1351
|
+
pt.as_mut_ptr(),
|
|
1352
|
+
pt.len(),
|
|
1353
|
+
)
|
|
1354
|
+
};
|
|
1355
|
+
assert_eq!(&pt[..d], plaintext);
|
|
1356
|
+
// Tampered ciphertext → auth failure → 0.
|
|
1357
|
+
ct[0] ^= 1;
|
|
1358
|
+
let bad = unsafe {
|
|
1359
|
+
castrum_aead_decrypt(
|
|
1360
|
+
key.as_ptr(),
|
|
1361
|
+
key.len(),
|
|
1362
|
+
nonce.as_ptr(),
|
|
1363
|
+
nonce.len(),
|
|
1364
|
+
ct.as_ptr(),
|
|
1365
|
+
w,
|
|
1366
|
+
0,
|
|
1367
|
+
pt.as_mut_ptr(),
|
|
1368
|
+
pt.len(),
|
|
1369
|
+
)
|
|
1370
|
+
};
|
|
1371
|
+
assert_eq!(bad, 0);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
#[test]
|
|
1375
|
+
fn ws_frame_encode_c_abi() {
|
|
1376
|
+
let payload = b"hello";
|
|
1377
|
+
let mut out = [0u8; 32];
|
|
1378
|
+
let w = unsafe {
|
|
1379
|
+
castrum_ws_frame_encode(
|
|
1380
|
+
1,
|
|
1381
|
+
payload.as_ptr(),
|
|
1382
|
+
payload.len(),
|
|
1383
|
+
1,
|
|
1384
|
+
1,
|
|
1385
|
+
out.as_mut_ptr(),
|
|
1386
|
+
out.len(),
|
|
1387
|
+
)
|
|
1388
|
+
};
|
|
1389
|
+
// masked text frame, fin: 0x81, len 5 | 0x80, 4-byte mask + payload.
|
|
1390
|
+
assert_eq!(w, 2 + 4 + payload.len());
|
|
1391
|
+
assert_eq!(out[0], 0x81);
|
|
1392
|
+
assert_eq!(out[1], 0x80 | payload.len() as u8);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
#[test]
|
|
1396
|
+
fn json_patch_c_abi() {
|
|
1397
|
+
let doc = br#"{"a":1}"#;
|
|
1398
|
+
let patch = br#"[{"op":"add","path":"/b","value":2}]"#;
|
|
1399
|
+
let mut out = [0u8; 64];
|
|
1400
|
+
let w = unsafe {
|
|
1401
|
+
castrum_json_patch(
|
|
1402
|
+
doc.as_ptr(),
|
|
1403
|
+
doc.len(),
|
|
1404
|
+
patch.as_ptr(),
|
|
1405
|
+
patch.len(),
|
|
1406
|
+
out.as_mut_ptr(),
|
|
1407
|
+
out.len(),
|
|
1408
|
+
)
|
|
1409
|
+
};
|
|
1410
|
+
let patched: serde_json::Value = serde_json::from_slice(&out[..w]).unwrap();
|
|
1411
|
+
assert_eq!(patched["b"], 2);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
#[test]
|
|
1415
|
+
fn gzip_brotli_c_abi_roundtrip() {
|
|
1416
|
+
let data = b"the quick brown fox jumps over the lazy dog ".repeat(10);
|
|
1417
|
+
let mut comp = [0u8; 1024];
|
|
1418
|
+
let cw = unsafe {
|
|
1419
|
+
castrum_gzip_compress(data.as_ptr(), data.len(), 6, comp.as_mut_ptr(), comp.len())
|
|
1420
|
+
};
|
|
1421
|
+
assert!(cw > 0 && cw < data.len());
|
|
1422
|
+
let mut decomp = [0u8; 1024];
|
|
1423
|
+
let dw = unsafe {
|
|
1424
|
+
castrum_gzip_decompress(
|
|
1425
|
+
comp.as_ptr(),
|
|
1426
|
+
cw,
|
|
1427
|
+
1024 * 1024,
|
|
1428
|
+
decomp.as_mut_ptr(),
|
|
1429
|
+
decomp.len(),
|
|
1430
|
+
)
|
|
1431
|
+
};
|
|
1432
|
+
assert_eq!(&decomp[..dw], data);
|
|
1433
|
+
|
|
1434
|
+
let mut bcomp = [0u8; 2048];
|
|
1435
|
+
let bw = unsafe {
|
|
1436
|
+
castrum_brotli_compress(
|
|
1437
|
+
data.as_ptr(),
|
|
1438
|
+
data.len(),
|
|
1439
|
+
5,
|
|
1440
|
+
bcomp.as_mut_ptr(),
|
|
1441
|
+
bcomp.len(),
|
|
1442
|
+
)
|
|
1443
|
+
};
|
|
1444
|
+
assert!(bw > 0);
|
|
1445
|
+
let mut bdecomp = [0u8; 1024];
|
|
1446
|
+
let bdw = unsafe {
|
|
1447
|
+
castrum_brotli_decompress(
|
|
1448
|
+
bcomp.as_ptr(),
|
|
1449
|
+
bw,
|
|
1450
|
+
1024 * 1024,
|
|
1451
|
+
bdecomp.as_mut_ptr(),
|
|
1452
|
+
bdecomp.len(),
|
|
1453
|
+
)
|
|
1454
|
+
};
|
|
1455
|
+
assert_eq!(&bdecomp[..bdw], data);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
#[test]
|
|
1459
|
+
fn gzip_isize_c_abi() {
|
|
1460
|
+
// ISIZE trailer of a standard gzip stream → exact uncompressed size.
|
|
1461
|
+
let data = b"the quick brown fox jumps over the lazy dog ".repeat(4);
|
|
1462
|
+
let mut comp = [0u8; 512];
|
|
1463
|
+
let cw = unsafe {
|
|
1464
|
+
castrum_gzip_compress(data.as_ptr(), data.len(), 6, comp.as_mut_ptr(), comp.len())
|
|
1465
|
+
};
|
|
1466
|
+
assert!(cw > 0);
|
|
1467
|
+
let isize = unsafe { castrum_gzip_isize(comp.as_ptr(), cw) };
|
|
1468
|
+
assert_eq!(isize as usize, data.len());
|
|
1469
|
+
// Not gzip magic → 0.
|
|
1470
|
+
assert_eq!(
|
|
1471
|
+
unsafe { castrum_gzip_isize(b"hello world, this is not gzip at all".as_ptr(), 40) },
|
|
1472
|
+
0
|
|
1473
|
+
);
|
|
1474
|
+
// Too short → 0.
|
|
1475
|
+
assert_eq!(unsafe { castrum_gzip_isize(b"\x1f\x8b".as_ptr(), 2) }, 0);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
#[test]
|
|
1479
|
+
fn needed_size_convention_c_abi() {
|
|
1480
|
+
// gzip compress into a too-small buffer returns the EXACT needed size
|
|
1481
|
+
// (> out_cap), NOT 0 — so the JS wrapper allocates once and retries
|
|
1482
|
+
// instead of re-running the whole compression in a grow loop.
|
|
1483
|
+
let data = b"compress me to prove the needed-size convention works".repeat(20);
|
|
1484
|
+
let mut tiny = [0u8; 4];
|
|
1485
|
+
let needed = unsafe {
|
|
1486
|
+
castrum_gzip_compress(data.as_ptr(), data.len(), 6, tiny.as_mut_ptr(), tiny.len())
|
|
1487
|
+
};
|
|
1488
|
+
assert!(needed > tiny.len());
|
|
1489
|
+
// Allocating exactly `needed` succeeds in one retry.
|
|
1490
|
+
let mut exact = vec![0u8; needed];
|
|
1491
|
+
let w = unsafe {
|
|
1492
|
+
castrum_gzip_compress(
|
|
1493
|
+
data.as_ptr(),
|
|
1494
|
+
data.len(),
|
|
1495
|
+
6,
|
|
1496
|
+
exact.as_mut_ptr(),
|
|
1497
|
+
exact.len(),
|
|
1498
|
+
)
|
|
1499
|
+
};
|
|
1500
|
+
assert_eq!(w, needed);
|
|
1501
|
+
// Invalid input → 0 (a REAL error, not "too small").
|
|
1502
|
+
let mut out = [0u8; 256];
|
|
1503
|
+
let err = unsafe {
|
|
1504
|
+
castrum_gzip_decompress(
|
|
1505
|
+
b"not-a-gzip-stream".as_ptr(),
|
|
1506
|
+
17,
|
|
1507
|
+
1024 * 1024,
|
|
1508
|
+
out.as_mut_ptr(),
|
|
1509
|
+
out.len(),
|
|
1510
|
+
)
|
|
1511
|
+
};
|
|
1512
|
+
assert_eq!(err, 0);
|
|
1513
|
+
|
|
1514
|
+
// jsonPatch: too-small returns the exact needed size.
|
|
1515
|
+
let doc = br#"{"a":1,"b":2,"c":3}"#;
|
|
1516
|
+
let patch = br#"[{"op":"add","path":"/d","value":4}]"#;
|
|
1517
|
+
let mut ptiny = [0u8; 4];
|
|
1518
|
+
let pneed = unsafe {
|
|
1519
|
+
castrum_json_patch(
|
|
1520
|
+
doc.as_ptr(),
|
|
1521
|
+
doc.len(),
|
|
1522
|
+
patch.as_ptr(),
|
|
1523
|
+
patch.len(),
|
|
1524
|
+
ptiny.as_mut_ptr(),
|
|
1525
|
+
ptiny.len(),
|
|
1526
|
+
)
|
|
1527
|
+
};
|
|
1528
|
+
assert!(pneed > ptiny.len());
|
|
1529
|
+
let mut pexact = vec![0u8; pneed];
|
|
1530
|
+
let pw = unsafe {
|
|
1531
|
+
castrum_json_patch(
|
|
1532
|
+
doc.as_ptr(),
|
|
1533
|
+
doc.len(),
|
|
1534
|
+
patch.as_ptr(),
|
|
1535
|
+
patch.len(),
|
|
1536
|
+
pexact.as_mut_ptr(),
|
|
1537
|
+
pexact.len(),
|
|
1538
|
+
)
|
|
1539
|
+
};
|
|
1540
|
+
assert_eq!(pw, pneed);
|
|
1541
|
+
|
|
1542
|
+
// jwtSignBytes: cstring return — the engine clones the token string.
|
|
1543
|
+
let claims = br#"{"sub":"user-1"}"#;
|
|
1544
|
+
let secret = b"my-secret";
|
|
1545
|
+
let jp = unsafe {
|
|
1546
|
+
castrum_jwt_sign_bytes(
|
|
1547
|
+
claims.as_ptr(),
|
|
1548
|
+
claims.len(),
|
|
1549
|
+
secret.as_ptr(),
|
|
1550
|
+
secret.len(),
|
|
1551
|
+
60,
|
|
1552
|
+
1_700_000_000,
|
|
1553
|
+
)
|
|
1554
|
+
};
|
|
1555
|
+
assert!(!jp.is_null());
|
|
1556
|
+
let jtoken = unsafe { cstr_bytes(jp) }.unwrap();
|
|
1557
|
+
assert!(!jtoken.is_empty());
|
|
1558
|
+
assert_eq!(jtoken.iter().filter(|&&b| b == b'.').count(), 2);
|
|
1559
|
+
|
|
1560
|
+
// passwordHash (argon2) too-small returns the exact PHC length.
|
|
1561
|
+
let pw = b"hunter2";
|
|
1562
|
+
let salt = b"saltsalt";
|
|
1563
|
+
let mut tiny = [0u8; 8];
|
|
1564
|
+
let hneed = unsafe {
|
|
1565
|
+
castrum_password_hash(
|
|
1566
|
+
pw.as_ptr(),
|
|
1567
|
+
pw.len(),
|
|
1568
|
+
salt.as_ptr(),
|
|
1569
|
+
salt.len(),
|
|
1570
|
+
19_456,
|
|
1571
|
+
2,
|
|
1572
|
+
1,
|
|
1573
|
+
32,
|
|
1574
|
+
tiny.as_mut_ptr(),
|
|
1575
|
+
tiny.len(),
|
|
1576
|
+
)
|
|
1577
|
+
};
|
|
1578
|
+
assert!(hneed > tiny.len());
|
|
1579
|
+
let mut hexact = vec![0u8; hneed];
|
|
1580
|
+
let hw = unsafe {
|
|
1581
|
+
castrum_password_hash(
|
|
1582
|
+
pw.as_ptr(),
|
|
1583
|
+
pw.len(),
|
|
1584
|
+
salt.as_ptr(),
|
|
1585
|
+
salt.len(),
|
|
1586
|
+
19_456,
|
|
1587
|
+
2,
|
|
1588
|
+
1,
|
|
1589
|
+
32,
|
|
1590
|
+
hexact.as_mut_ptr(),
|
|
1591
|
+
hexact.len(),
|
|
1592
|
+
)
|
|
1593
|
+
};
|
|
1594
|
+
assert_eq!(hw, hneed);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
#[test]
|
|
1598
|
+
fn packed_parsers_c_abi() {
|
|
1599
|
+
// HTTP request line + headers → packed output.
|
|
1600
|
+
let req = b"GET /api/users?page=1 HTTP/1.1\r\nhost: example.com\r\n\r\n";
|
|
1601
|
+
let mut out = [0u8; 256];
|
|
1602
|
+
let w = unsafe {
|
|
1603
|
+
castrum_http_parse_request_packed(req.as_ptr(), req.len(), out.as_mut_ptr(), out.len())
|
|
1604
|
+
};
|
|
1605
|
+
assert!(w > 0);
|
|
1606
|
+
|
|
1607
|
+
let query = b"a=1&b=hello%20world";
|
|
1608
|
+
let mut qout = [0u8; 256];
|
|
1609
|
+
let qw = unsafe {
|
|
1610
|
+
castrum_query_parse_packed(query.as_ptr(), query.len(), qout.as_mut_ptr(), qout.len())
|
|
1611
|
+
};
|
|
1612
|
+
assert!(qw > 0);
|
|
1613
|
+
|
|
1614
|
+
let cookie = b"sid=abc123; theme=dark";
|
|
1615
|
+
let mut cout = [0u8; 256];
|
|
1616
|
+
let cw = unsafe {
|
|
1617
|
+
castrum_cookie_parse_packed(cookie.as_ptr(), cookie.len(), cout.as_mut_ptr(), cout.len())
|
|
1618
|
+
};
|
|
1619
|
+
assert!(cw > 0);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
#[test]
|
|
1623
|
+
fn packed_parsers_needed_size_c_abi() {
|
|
1624
|
+
// The packed pair writers + JSON-text writers report the EXACT needed
|
|
1625
|
+
// size on a too-small buffer (growExact), and 0 stays a real error
|
|
1626
|
+
// (malformed %XX). This removes the JS 9×/8× pre-size AND the re-run
|
|
1627
|
+
// loop — a miss allocates once and retries.
|
|
1628
|
+
let too_small = [0u8; 8];
|
|
1629
|
+
|
|
1630
|
+
// query → packed pairs (strict writer; malformed %XX is a real error).
|
|
1631
|
+
let q = b"a=1&b=hello%20world&c=2";
|
|
1632
|
+
let qneed = unsafe {
|
|
1633
|
+
castrum_query_parse_packed(
|
|
1634
|
+
q.as_ptr(),
|
|
1635
|
+
q.len(),
|
|
1636
|
+
too_small.as_ptr() as *mut u8,
|
|
1637
|
+
too_small.len(),
|
|
1638
|
+
)
|
|
1639
|
+
};
|
|
1640
|
+
assert!(qneed > too_small.len());
|
|
1641
|
+
let mut qexact = vec![0u8; qneed];
|
|
1642
|
+
let qw = unsafe {
|
|
1643
|
+
castrum_query_parse_packed(q.as_ptr(), q.len(), qexact.as_mut_ptr(), qexact.len())
|
|
1644
|
+
};
|
|
1645
|
+
assert_eq!(qw, qneed);
|
|
1646
|
+
|
|
1647
|
+
// cookie → packed pairs.
|
|
1648
|
+
let c = b"sid=abc123; theme=dark";
|
|
1649
|
+
let cneed = unsafe {
|
|
1650
|
+
castrum_cookie_parse_packed(
|
|
1651
|
+
c.as_ptr(),
|
|
1652
|
+
c.len(),
|
|
1653
|
+
too_small.as_ptr() as *mut u8,
|
|
1654
|
+
too_small.len(),
|
|
1655
|
+
)
|
|
1656
|
+
};
|
|
1657
|
+
assert!(cneed > too_small.len());
|
|
1658
|
+
let mut cexact = vec![0u8; cneed];
|
|
1659
|
+
let cw = unsafe {
|
|
1660
|
+
castrum_cookie_parse_packed(c.as_ptr(), c.len(), cexact.as_mut_ptr(), cexact.len())
|
|
1661
|
+
};
|
|
1662
|
+
assert_eq!(cw, cneed);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
#[test]
|
|
1666
|
+
fn form_parse_packed_c_abi_matches_query() {
|
|
1667
|
+
let input = b"a=1&b=hello%20world";
|
|
1668
|
+
let mut out = [0u8; 256];
|
|
1669
|
+
let w = unsafe {
|
|
1670
|
+
castrum_form_parse_packed(input.as_ptr(), input.len(), out.as_mut_ptr(), out.len())
|
|
1671
|
+
};
|
|
1672
|
+
assert!(w > 0);
|
|
1673
|
+
// Two pairs, matching the query core.
|
|
1674
|
+
assert_eq!(u32::from_le_bytes(out[0..4].try_into().unwrap()), 2);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
#[test]
|
|
1678
|
+
fn multipart_parse_packed_c_abi() {
|
|
1679
|
+
let boundary = b"----boundary";
|
|
1680
|
+
let bstr = std::str::from_utf8(boundary).unwrap();
|
|
1681
|
+
let body = format!(
|
|
1682
|
+
"--{b}\r\nContent-Disposition: form-data; name=\"field\"\r\n\r\nvalue\r\n--{b}--",
|
|
1683
|
+
b = bstr
|
|
1684
|
+
);
|
|
1685
|
+
let mut out = [0u8; 256];
|
|
1686
|
+
let w = unsafe {
|
|
1687
|
+
castrum_multipart_parse_packed(
|
|
1688
|
+
body.as_ptr(),
|
|
1689
|
+
body.len(),
|
|
1690
|
+
boundary.as_ptr(),
|
|
1691
|
+
boundary.len(),
|
|
1692
|
+
out.as_mut_ptr(),
|
|
1693
|
+
out.len(),
|
|
1694
|
+
)
|
|
1695
|
+
};
|
|
1696
|
+
assert!(w > 0);
|
|
1697
|
+
assert_eq!(u32::from_le_bytes(out[0..4].try_into().unwrap()), 1);
|
|
1698
|
+
// Malformed (wrong boundary) → empty parts → 4-byte count only.
|
|
1699
|
+
let w2 = unsafe {
|
|
1700
|
+
castrum_multipart_parse_packed(
|
|
1701
|
+
body.as_ptr(),
|
|
1702
|
+
body.len(),
|
|
1703
|
+
b"----nope".as_ptr(),
|
|
1704
|
+
9,
|
|
1705
|
+
out.as_mut_ptr(),
|
|
1706
|
+
out.len(),
|
|
1707
|
+
)
|
|
1708
|
+
};
|
|
1709
|
+
assert!(w2 >= 4);
|
|
1710
|
+
assert_eq!(u32::from_le_bytes(out[0..4].try_into().unwrap()), 0);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
#[test]
|
|
1714
|
+
fn ws_frame_decode_packed_c_abi() {
|
|
1715
|
+
let payload = b"hello";
|
|
1716
|
+
let mut frame = [0u8; 32];
|
|
1717
|
+
let fw = unsafe {
|
|
1718
|
+
castrum_ws_frame_encode(
|
|
1719
|
+
1,
|
|
1720
|
+
payload.as_ptr(),
|
|
1721
|
+
payload.len(),
|
|
1722
|
+
1,
|
|
1723
|
+
1,
|
|
1724
|
+
frame.as_mut_ptr(),
|
|
1725
|
+
frame.len(),
|
|
1726
|
+
)
|
|
1727
|
+
};
|
|
1728
|
+
let mut out = [0u8; 64];
|
|
1729
|
+
let w =
|
|
1730
|
+
unsafe { castrum_ws_frame_decode_packed(frame.as_ptr(), fw, out.as_mut_ptr(), out.len()) };
|
|
1731
|
+
assert_eq!(w, 6 + payload.len());
|
|
1732
|
+
assert_eq!(out[0], 1); // fin
|
|
1733
|
+
assert_eq!(out[1], 1); // opcode text
|
|
1734
|
+
assert_eq!(u32::from_le_bytes(out[2..6].try_into().unwrap()), 5);
|
|
1735
|
+
assert_eq!(&out[6..w], payload);
|
|
1736
|
+
// Truncated frame → 0.
|
|
1737
|
+
let bad =
|
|
1738
|
+
unsafe { castrum_ws_frame_decode_packed(b"\x80".as_ptr(), 1, out.as_mut_ptr(), out.len()) };
|
|
1739
|
+
assert_eq!(bad, 0);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
#[test]
|
|
1743
|
+
fn jwt_sign_bytes_c_abi() {
|
|
1744
|
+
let claims = br#"{"sub":"user-1"}"#;
|
|
1745
|
+
let secret = b"my-secret";
|
|
1746
|
+
let signed = unsafe {
|
|
1747
|
+
castrum_jwt_sign_bytes(
|
|
1748
|
+
claims.as_ptr(),
|
|
1749
|
+
claims.len(),
|
|
1750
|
+
secret.as_ptr(),
|
|
1751
|
+
secret.len(),
|
|
1752
|
+
60,
|
|
1753
|
+
1_700_000_000,
|
|
1754
|
+
)
|
|
1755
|
+
};
|
|
1756
|
+
assert!(!signed.is_null());
|
|
1757
|
+
let token = unsafe { cstr_bytes(signed) }.unwrap();
|
|
1758
|
+
// Compact JWT: header.payload.sig → exactly two dots.
|
|
1759
|
+
assert_eq!(token.iter().filter(|&&b| b == b'.').count(), 2);
|
|
1760
|
+
assert!(crate::crypto::jwt::verify_signature_with_key(
|
|
1761
|
+
&token,
|
|
1762
|
+
&hmac_key(secret)
|
|
1763
|
+
));
|
|
1764
|
+
|
|
1765
|
+
// ttl <= 0 → no iat/exp injection (still signs).
|
|
1766
|
+
let s0 = unsafe {
|
|
1767
|
+
castrum_jwt_sign_bytes(
|
|
1768
|
+
claims.as_ptr(),
|
|
1769
|
+
claims.len(),
|
|
1770
|
+
secret.as_ptr(),
|
|
1771
|
+
secret.len(),
|
|
1772
|
+
0,
|
|
1773
|
+
1_700_000_000,
|
|
1774
|
+
)
|
|
1775
|
+
};
|
|
1776
|
+
assert!(!s0.is_null());
|
|
1777
|
+
// Invalid claims JSON → null.
|
|
1778
|
+
let bad = unsafe {
|
|
1779
|
+
castrum_jwt_sign_bytes(
|
|
1780
|
+
b"not-json".as_ptr(),
|
|
1781
|
+
8,
|
|
1782
|
+
secret.as_ptr(),
|
|
1783
|
+
secret.len(),
|
|
1784
|
+
60,
|
|
1785
|
+
1,
|
|
1786
|
+
)
|
|
1787
|
+
};
|
|
1788
|
+
assert!(bad.is_null());
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
#[test]
|
|
1792
|
+
fn jwt_verify_c_abi() {
|
|
1793
|
+
let claims = br#"{"sub":"user-1"}"#;
|
|
1794
|
+
let secret = b"my-secret";
|
|
1795
|
+
let signed = unsafe {
|
|
1796
|
+
castrum_jwt_sign_bytes(
|
|
1797
|
+
claims.as_ptr(),
|
|
1798
|
+
claims.len(),
|
|
1799
|
+
secret.as_ptr(),
|
|
1800
|
+
secret.len(),
|
|
1801
|
+
60,
|
|
1802
|
+
1_700_000_000,
|
|
1803
|
+
)
|
|
1804
|
+
};
|
|
1805
|
+
assert!(!signed.is_null());
|
|
1806
|
+
let token = unsafe { cstr_bytes(signed) }.unwrap();
|
|
1807
|
+
|
|
1808
|
+
// Valid within TTL → claims JSON.
|
|
1809
|
+
let verified = unsafe {
|
|
1810
|
+
castrum_jwt_verify(
|
|
1811
|
+
token.as_ptr(),
|
|
1812
|
+
token.len(),
|
|
1813
|
+
secret.as_ptr(),
|
|
1814
|
+
secret.len(),
|
|
1815
|
+
1_700_000_030,
|
|
1816
|
+
)
|
|
1817
|
+
};
|
|
1818
|
+
assert!(!verified.is_null());
|
|
1819
|
+
let claims_json = unsafe { cstr_bytes(verified) }.unwrap();
|
|
1820
|
+
let parsed: serde_json::Value = serde_json::from_slice(&claims_json).unwrap();
|
|
1821
|
+
assert_eq!(parsed["sub"], "user-1");
|
|
1822
|
+
|
|
1823
|
+
// Expired (now beyond exp) → null.
|
|
1824
|
+
let expired = unsafe {
|
|
1825
|
+
castrum_jwt_verify(
|
|
1826
|
+
token.as_ptr(),
|
|
1827
|
+
token.len(),
|
|
1828
|
+
secret.as_ptr(),
|
|
1829
|
+
secret.len(),
|
|
1830
|
+
1_700_000_100,
|
|
1831
|
+
)
|
|
1832
|
+
};
|
|
1833
|
+
assert!(expired.is_null());
|
|
1834
|
+
|
|
1835
|
+
// Wrong secret → null.
|
|
1836
|
+
let wrong = unsafe {
|
|
1837
|
+
castrum_jwt_verify(
|
|
1838
|
+
token.as_ptr(),
|
|
1839
|
+
token.len(),
|
|
1840
|
+
b"other".as_ptr(),
|
|
1841
|
+
5,
|
|
1842
|
+
1_700_000_030,
|
|
1843
|
+
)
|
|
1844
|
+
};
|
|
1845
|
+
assert!(wrong.is_null());
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
#[test]
|
|
1849
|
+
fn ingress_handle_packed_c_abi() {
|
|
1850
|
+
use crate::ingress::cors::CorsEngine;
|
|
1851
|
+
use crate::ingress::options::Limits;
|
|
1852
|
+
use crate::ingress::rate_limit::RateLimiterState;
|
|
1853
|
+
|
|
1854
|
+
let inner = crate::ingress::IngressInner {
|
|
1855
|
+
https_fixed: None,
|
|
1856
|
+
max_body_bytes: 1_048_576,
|
|
1857
|
+
proxy_trust: crate::ingress::ip_trust::ProxyTrustMode::None,
|
|
1858
|
+
parse_cookies: false,
|
|
1859
|
+
parse_query: false,
|
|
1860
|
+
require_json_body: false,
|
|
1861
|
+
guard_enabled: true,
|
|
1862
|
+
emit_metadata_json: false,
|
|
1863
|
+
cors_enabled: false,
|
|
1864
|
+
cors: CorsEngine::disabled(),
|
|
1865
|
+
rate: RateLimiterState::Disabled,
|
|
1866
|
+
schema: None,
|
|
1867
|
+
limits: Limits::default(),
|
|
1868
|
+
};
|
|
1869
|
+
// Packed frame: [method 0 = GET][url][ip][rid] sections + empty headers.
|
|
1870
|
+
let mut input = Vec::new();
|
|
1871
|
+
input.push(0);
|
|
1872
|
+
for section in [
|
|
1873
|
+
b"/api/users".as_slice(),
|
|
1874
|
+
b"127.0.0.1".as_slice(),
|
|
1875
|
+
b"rid-1".as_slice(),
|
|
1876
|
+
] {
|
|
1877
|
+
input.extend_from_slice(&(section.len() as u32).to_le_bytes());
|
|
1878
|
+
input.extend_from_slice(section);
|
|
1879
|
+
}
|
|
1880
|
+
input.extend_from_slice(&0u32.to_le_bytes());
|
|
1881
|
+
|
|
1882
|
+
let mut out = [0u8; 512];
|
|
1883
|
+
let ptr = &inner as *const crate::ingress::IngressInner as usize;
|
|
1884
|
+
let w = unsafe {
|
|
1885
|
+
castrum_ingress_handle_packed(
|
|
1886
|
+
ptr,
|
|
1887
|
+
input.as_ptr(),
|
|
1888
|
+
input.len(),
|
|
1889
|
+
std::ptr::null(),
|
|
1890
|
+
0,
|
|
1891
|
+
out.as_mut_ptr(),
|
|
1892
|
+
out.len(),
|
|
1893
|
+
)
|
|
1894
|
+
};
|
|
1895
|
+
assert!(w > 0);
|
|
1896
|
+
// OUT_VERDICT == 0 (ok) at the first output byte.
|
|
1897
|
+
assert_eq!(out[0], 0);
|
|
1898
|
+
// Invalid inner pointer → 0.
|
|
1899
|
+
let bad = unsafe {
|
|
1900
|
+
castrum_ingress_handle_packed(
|
|
1901
|
+
0,
|
|
1902
|
+
input.as_ptr(),
|
|
1903
|
+
input.len(),
|
|
1904
|
+
std::ptr::null(),
|
|
1905
|
+
0,
|
|
1906
|
+
out.as_mut_ptr(),
|
|
1907
|
+
out.len(),
|
|
1908
|
+
)
|
|
1909
|
+
};
|
|
1910
|
+
assert_eq!(bad, 0);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
#[test]
|
|
1914
|
+
fn ingress_handle_components_c_abi() {
|
|
1915
|
+
use crate::ingress::cors::CorsEngine;
|
|
1916
|
+
use crate::ingress::options::Limits;
|
|
1917
|
+
use crate::ingress::rate_limit::RateLimiterState;
|
|
1918
|
+
|
|
1919
|
+
let inner = crate::ingress::IngressInner {
|
|
1920
|
+
https_fixed: None,
|
|
1921
|
+
max_body_bytes: 1_048_576,
|
|
1922
|
+
proxy_trust: crate::ingress::ip_trust::ProxyTrustMode::None,
|
|
1923
|
+
parse_cookies: false,
|
|
1924
|
+
parse_query: false,
|
|
1925
|
+
require_json_body: false,
|
|
1926
|
+
guard_enabled: true,
|
|
1927
|
+
emit_metadata_json: false,
|
|
1928
|
+
cors_enabled: false,
|
|
1929
|
+
cors: CorsEngine::disabled(),
|
|
1930
|
+
rate: RateLimiterState::Disabled,
|
|
1931
|
+
schema: None,
|
|
1932
|
+
limits: Limits::default(),
|
|
1933
|
+
};
|
|
1934
|
+
let ptr = &inner as *const crate::ingress::IngressInner as usize;
|
|
1935
|
+
// Empty packed header block: [u16 count = 0].
|
|
1936
|
+
let empty_headers = [0u8, 0];
|
|
1937
|
+
let rid = b"rid-1";
|
|
1938
|
+
let mut out = [0u8; 512];
|
|
1939
|
+
let w = unsafe {
|
|
1940
|
+
castrum_ingress_handle_components(
|
|
1941
|
+
ptr,
|
|
1942
|
+
0, // GET
|
|
1943
|
+
c"/api/users".as_ptr(),
|
|
1944
|
+
c"127.0.0.1".as_ptr(),
|
|
1945
|
+
rid.as_ptr(),
|
|
1946
|
+
rid.len(),
|
|
1947
|
+
empty_headers.as_ptr(),
|
|
1948
|
+
empty_headers.len(),
|
|
1949
|
+
std::ptr::null(),
|
|
1950
|
+
0,
|
|
1951
|
+
out.as_mut_ptr(),
|
|
1952
|
+
out.len(),
|
|
1953
|
+
)
|
|
1954
|
+
};
|
|
1955
|
+
assert!(w > 0);
|
|
1956
|
+
// OUT_VERDICT == 0 (ok) at the first output byte — same as the packed
|
|
1957
|
+
// path for the same request components.
|
|
1958
|
+
assert_eq!(out[0], 0);
|
|
1959
|
+
|
|
1960
|
+
// Null inner pointer → 0.
|
|
1961
|
+
let bad = unsafe {
|
|
1962
|
+
castrum_ingress_handle_components(
|
|
1963
|
+
0,
|
|
1964
|
+
0,
|
|
1965
|
+
c"/api/users".as_ptr(),
|
|
1966
|
+
c"127.0.0.1".as_ptr(),
|
|
1967
|
+
rid.as_ptr(),
|
|
1968
|
+
rid.len(),
|
|
1969
|
+
empty_headers.as_ptr(),
|
|
1970
|
+
empty_headers.len(),
|
|
1971
|
+
std::ptr::null(),
|
|
1972
|
+
0,
|
|
1973
|
+
out.as_mut_ptr(),
|
|
1974
|
+
out.len(),
|
|
1975
|
+
)
|
|
1976
|
+
};
|
|
1977
|
+
assert_eq!(bad, 0);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
#[test]
|
|
1981
|
+
fn panic_guard_returns_fallback_on_panic() {
|
|
1982
|
+
// Happy path: no panic, the closure's value comes through.
|
|
1983
|
+
assert_eq!(panic_guard(|| 42usize, 0), 42);
|
|
1984
|
+
// A panic is caught and replaced by the fallback (the FFI crash path:
|
|
1985
|
+
// without catch_unwind this would unwind through the C ABI and kill the
|
|
1986
|
+
// host process).
|
|
1987
|
+
assert_eq!(panic_guard(|| panic!("boom"), 0usize), 0);
|
|
1988
|
+
// Panic AFTER partial work is also contained.
|
|
1989
|
+
let mut seen = 0;
|
|
1990
|
+
assert_eq!(
|
|
1991
|
+
panic_guard(
|
|
1992
|
+
|| {
|
|
1993
|
+
seen = 1;
|
|
1994
|
+
panic!("boom2");
|
|
1995
|
+
},
|
|
1996
|
+
7usize,
|
|
1997
|
+
),
|
|
1998
|
+
7
|
|
1999
|
+
);
|
|
2000
|
+
assert_eq!(seen, 1);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
#[test]
|
|
2004
|
+
fn ingress_handle_packed_input_overlap_c_abi() {
|
|
2005
|
+
use crate::ingress::cors::CorsEngine;
|
|
2006
|
+
use crate::ingress::options::Limits;
|
|
2007
|
+
use crate::ingress::rate_limit::RateLimiterState;
|
|
2008
|
+
|
|
2009
|
+
let inner = crate::ingress::IngressInner {
|
|
2010
|
+
https_fixed: None,
|
|
2011
|
+
max_body_bytes: 1_048_576,
|
|
2012
|
+
proxy_trust: crate::ingress::ip_trust::ProxyTrustMode::None,
|
|
2013
|
+
parse_cookies: false,
|
|
2014
|
+
parse_query: false,
|
|
2015
|
+
require_json_body: false,
|
|
2016
|
+
guard_enabled: true,
|
|
2017
|
+
emit_metadata_json: false,
|
|
2018
|
+
cors_enabled: false,
|
|
2019
|
+
cors: CorsEngine::disabled(),
|
|
2020
|
+
rate: RateLimiterState::Disabled,
|
|
2021
|
+
schema: None,
|
|
2022
|
+
limits: Limits::default(),
|
|
2023
|
+
};
|
|
2024
|
+
let mut input = Vec::new();
|
|
2025
|
+
input.push(0);
|
|
2026
|
+
for section in [
|
|
2027
|
+
b"/api/users".as_slice(),
|
|
2028
|
+
b"127.0.0.1".as_slice(),
|
|
2029
|
+
b"rid-1".as_slice(),
|
|
2030
|
+
] {
|
|
2031
|
+
input.extend_from_slice(&(section.len() as u32).to_le_bytes());
|
|
2032
|
+
input.extend_from_slice(section);
|
|
2033
|
+
}
|
|
2034
|
+
input.extend_from_slice(&0u32.to_le_bytes());
|
|
2035
|
+
|
|
2036
|
+
// Shared buffer: the packed input occupies the front and `out` points
|
|
2037
|
+
// INTO THE SAME BUFFER overlapping the input (offset 4) — the aliasing
|
|
2038
|
+
// case the FFI entry must handle by copying the input before writing
|
|
2039
|
+
// (aliased &[u8]/&mut [u8] would otherwise be instant UB).
|
|
2040
|
+
let mut buf = vec![0u8; input.len() + 256];
|
|
2041
|
+
buf[..input.len()].copy_from_slice(&input);
|
|
2042
|
+
let ptr = &inner as *const crate::ingress::IngressInner as usize;
|
|
2043
|
+
let input_ptr = buf.as_ptr();
|
|
2044
|
+
let out_ptr = unsafe { buf.as_mut_ptr().add(4) };
|
|
2045
|
+
let out_cap = buf.len() - 4;
|
|
2046
|
+
assert!(4 < input.len(), "test setup: out region must overlap input");
|
|
2047
|
+
|
|
2048
|
+
let w = unsafe {
|
|
2049
|
+
castrum_ingress_handle_packed(
|
|
2050
|
+
ptr,
|
|
2051
|
+
input_ptr,
|
|
2052
|
+
input.len(),
|
|
2053
|
+
std::ptr::null(),
|
|
2054
|
+
0,
|
|
2055
|
+
out_ptr,
|
|
2056
|
+
out_cap,
|
|
2057
|
+
)
|
|
2058
|
+
};
|
|
2059
|
+
assert!(w > 0, "pipeline should succeed despite input/out overlap");
|
|
2060
|
+
assert_eq!(
|
|
2061
|
+
unsafe { *out_ptr },
|
|
2062
|
+
0,
|
|
2063
|
+
"verdict (ok) must land at out start"
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// ── Metrics registry C-ABI (castrum_metrics_*) ─────────────────
|
|
2068
|
+
|
|
2069
|
+
#[test]
|
|
2070
|
+
fn metrics_registry_c_abi_counter_gauge_histogram() {
|
|
2071
|
+
let h = castrum_metrics_create();
|
|
2072
|
+
assert_ne!(h, 0);
|
|
2073
|
+
let counter = unsafe {
|
|
2074
|
+
castrum_metrics_counter(
|
|
2075
|
+
h,
|
|
2076
|
+
c"ct_requests_total".as_ptr(),
|
|
2077
|
+
c"route\x1fstatus".as_ptr(),
|
|
2078
|
+
)
|
|
2079
|
+
};
|
|
2080
|
+
assert_eq!(counter, 0, "first declared family gets id 0");
|
|
2081
|
+
let gauge = unsafe { castrum_metrics_gauge(h, c"ct_queue_depth".as_ptr(), c"q".as_ptr()) };
|
|
2082
|
+
assert_eq!(gauge, 1);
|
|
2083
|
+
let hist = unsafe {
|
|
2084
|
+
castrum_metrics_histogram(h, c"ct_latency".as_ptr(), c"".as_ptr(), c"0.1,0.5".as_ptr())
|
|
2085
|
+
};
|
|
2086
|
+
assert_eq!(hist, 2);
|
|
2087
|
+
|
|
2088
|
+
// record: two counter hits + one gauge set + one histogram observe
|
|
2089
|
+
let vals = b"/a\x1f200";
|
|
2090
|
+
assert_eq!(
|
|
2091
|
+
unsafe { castrum_metrics_record(h, counter, vals.as_ptr(), vals.len(), 2.0) },
|
|
2092
|
+
1
|
|
2093
|
+
);
|
|
2094
|
+
let q = b"jobs";
|
|
2095
|
+
assert_eq!(
|
|
2096
|
+
unsafe { castrum_metrics_gauge_set(h, gauge, q.as_ptr(), q.len(), 4.5) },
|
|
2097
|
+
1
|
|
2098
|
+
);
|
|
2099
|
+
assert_eq!(
|
|
2100
|
+
unsafe { castrum_metrics_record(h, hist, b"".as_ptr(), 0, 0.25) },
|
|
2101
|
+
1
|
|
2102
|
+
);
|
|
2103
|
+
|
|
2104
|
+
// render (needed-size convention: probe with a 1-byte buffer)
|
|
2105
|
+
let mut probe = [0u8; 1];
|
|
2106
|
+
let cap = unsafe { castrum_metrics_render(h, probe.as_mut_ptr(), 1) };
|
|
2107
|
+
assert!(cap > 1, "render must report its needed size");
|
|
2108
|
+
let mut out = vec![0u8; cap];
|
|
2109
|
+
let written = unsafe { castrum_metrics_render(h, out.as_mut_ptr(), out.len()) };
|
|
2110
|
+
assert_eq!(written, cap);
|
|
2111
|
+
let text = String::from_utf8(out[..written].to_vec()).expect("utf8");
|
|
2112
|
+
assert!(text.contains("ct_requests_total{route=\"/a\",status=\"200\"} 2\n"));
|
|
2113
|
+
assert!(text.contains("ct_queue_depth{q=\"jobs\"} 4.5\n"));
|
|
2114
|
+
assert!(text.contains("ct_latency_bucket{le=\"0.1\"} 0\n"));
|
|
2115
|
+
assert!(text.contains("ct_latency_bucket{le=\"0.5\"} 1\n"));
|
|
2116
|
+
assert!(text.contains("ct_latency_count 1\n"));
|
|
2117
|
+
|
|
2118
|
+
// arity mismatch → record fails safely
|
|
2119
|
+
assert_eq!(
|
|
2120
|
+
unsafe { castrum_metrics_record(h, counter, b"/a".as_ptr(), 3, 1.0) },
|
|
2121
|
+
0
|
|
2122
|
+
);
|
|
2123
|
+
// unknown series → 0
|
|
2124
|
+
assert_eq!(
|
|
2125
|
+
unsafe { castrum_metrics_record(h, 999, b"".as_ptr(), 0, 1.0) },
|
|
2126
|
+
0
|
|
2127
|
+
);
|
|
2128
|
+
// declare error sentinel on an invalid name
|
|
2129
|
+
assert_eq!(
|
|
2130
|
+
unsafe { castrum_metrics_counter(h, c"9 bad".as_ptr(), c"".as_ptr()) },
|
|
2131
|
+
u32::MAX
|
|
2132
|
+
);
|
|
2133
|
+
|
|
2134
|
+
unsafe { castrum_metrics_destroy(h) };
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
#[test]
|
|
2138
|
+
fn metrics_c_abi_null_handle_is_safe() {
|
|
2139
|
+
assert_eq!(
|
|
2140
|
+
unsafe { castrum_metrics_record(0, 0, b"".as_ptr(), 0, 1.0) },
|
|
2141
|
+
0
|
|
2142
|
+
);
|
|
2143
|
+
assert_eq!(
|
|
2144
|
+
unsafe { castrum_metrics_gauge_set(0, 0, b"".as_ptr(), 0, 1.0) },
|
|
2145
|
+
0
|
|
2146
|
+
);
|
|
2147
|
+
assert_eq!(
|
|
2148
|
+
unsafe { castrum_metrics_render(0, std::ptr::null_mut(), 0) },
|
|
2149
|
+
0
|
|
2150
|
+
);
|
|
2151
|
+
assert_eq!(
|
|
2152
|
+
unsafe { castrum_metrics_counter(0, c"x".as_ptr(), c"".as_ptr()) },
|
|
2153
|
+
u32::MAX
|
|
2154
|
+
);
|
|
2155
|
+
unsafe { castrum_metrics_destroy(0) }; // no-op
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// ── Batch hex validation C-ABI ─────────────────────────────────
|
|
2159
|
+
|
|
2160
|
+
#[test]
|
|
2161
|
+
fn hex_validate_batch_c_abi_needed_size_convention() {
|
|
2162
|
+
let input = b"507f1f77bcf86cd799439011\nzz\n";
|
|
2163
|
+
// too-small buffer → exact required size
|
|
2164
|
+
let mut tiny = [0u8; 1];
|
|
2165
|
+
let needed = unsafe {
|
|
2166
|
+
castrum_hex_validate_batch(input.as_ptr(), input.len(), 24, tiny.as_mut_ptr(), 1)
|
|
2167
|
+
};
|
|
2168
|
+
assert_eq!(needed, 2);
|
|
2169
|
+
let mut out = [0u8; 2];
|
|
2170
|
+
let written =
|
|
2171
|
+
unsafe { castrum_hex_validate_batch(input.as_ptr(), input.len(), 24, out.as_mut_ptr(), 2) };
|
|
2172
|
+
assert_eq!(written, 2);
|
|
2173
|
+
assert_eq!(out, [1, 0]);
|
|
2174
|
+
// bad width → real error (0)
|
|
2175
|
+
assert_eq!(
|
|
2176
|
+
unsafe { castrum_hex_validate_batch(input.as_ptr(), input.len(), 0, out.as_mut_ptr(), 2) },
|
|
2177
|
+
0
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
#[test]
|
|
2182
|
+
fn regex_escape_c_abi_needed_size_convention() {
|
|
2183
|
+
let input = b"a.c(x)*";
|
|
2184
|
+
// exact needed size first call writes directly
|
|
2185
|
+
let mut out = [0u8; 32];
|
|
2186
|
+
let written =
|
|
2187
|
+
unsafe { castrum_regex_escape(input.as_ptr(), input.len(), out.as_mut_ptr(), 32) };
|
|
2188
|
+
assert_eq!(written, 11); // 7 chars + 4 backslashes (. ( ) * \\)
|
|
2189
|
+
assert_eq!(&out[..written], br#"a\.c\(x\)\*"#);
|
|
2190
|
+
// too-small buffer → the exact required size
|
|
2191
|
+
let mut small = [0u8; 4];
|
|
2192
|
+
assert_eq!(
|
|
2193
|
+
unsafe { castrum_regex_escape(input.as_ptr(), input.len(), small.as_mut_ptr(), 4) },
|
|
2194
|
+
11
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// ── Zero-copy `_str` siblings (cstring args / cstring return) ──
|
|
2199
|
+
|
|
2200
|
+
#[test]
|
|
2201
|
+
fn metrics_str_variants_match_packed_path() {
|
|
2202
|
+
let h = castrum_metrics_create();
|
|
2203
|
+
assert_ne!(h, 0);
|
|
2204
|
+
let c = unsafe {
|
|
2205
|
+
castrum_metrics_counter(h, c"str_requests".as_ptr(), c"route\x1fstatus".as_ptr())
|
|
2206
|
+
};
|
|
2207
|
+
assert_eq!(c, 0);
|
|
2208
|
+
// cstring-joined values behave identically to the packed (ptr,len) path.
|
|
2209
|
+
assert_eq!(
|
|
2210
|
+
unsafe { castrum_metrics_record_str(h, c, c"/a\x1f200".as_ptr(), 2.0) },
|
|
2211
|
+
1
|
|
2212
|
+
);
|
|
2213
|
+
assert_eq!(
|
|
2214
|
+
unsafe { castrum_metrics_record(h, c, b"/a\x1f200".as_ptr(), 6, 1.0) },
|
|
2215
|
+
1
|
|
2216
|
+
);
|
|
2217
|
+
let mut probe = [0u8; 1];
|
|
2218
|
+
let cap = unsafe { castrum_metrics_render(h, probe.as_mut_ptr(), 1) };
|
|
2219
|
+
let mut out = vec![0u8; cap];
|
|
2220
|
+
let w = unsafe { castrum_metrics_render(h, out.as_mut_ptr(), cap) };
|
|
2221
|
+
let text = String::from_utf8_lossy(&out[..w]).into_owned();
|
|
2222
|
+
assert!(text.contains("str_requests{route=\"/a\",status=\"200\"} 3\n"));
|
|
2223
|
+
// arity mismatch still fails safely through the str path
|
|
2224
|
+
assert_eq!(
|
|
2225
|
+
unsafe { castrum_metrics_record_str(h, c, c"/a".as_ptr(), 1.0) },
|
|
2226
|
+
0
|
|
2227
|
+
);
|
|
2228
|
+
// gauge_set_str assigns
|
|
2229
|
+
let g = unsafe { castrum_metrics_gauge(h, c"str_depth".as_ptr(), c"".as_ptr()) };
|
|
2230
|
+
assert_eq!(
|
|
2231
|
+
unsafe { castrum_metrics_gauge_set_str(h, g, c"".as_ptr(), 7.5) },
|
|
2232
|
+
1
|
|
2233
|
+
);
|
|
2234
|
+
unsafe { castrum_metrics_destroy(h) };
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
#[test]
|
|
2238
|
+
fn regex_escape_str_c_abi_matches_bytes_path() {
|
|
2239
|
+
let s = c"a.c(x)*[y]";
|
|
2240
|
+
let p = unsafe { castrum_regex_escape_str(s.as_ptr()) };
|
|
2241
|
+
assert!(!p.is_null());
|
|
2242
|
+
let via_str = unsafe { cstr_bytes(p).expect("cstring return") };
|
|
2243
|
+
let input = b"a.c(x)*[y]";
|
|
2244
|
+
let mut expected = Vec::new();
|
|
2245
|
+
crate::util::text::regex_escape_into(input, &mut expected);
|
|
2246
|
+
assert_eq!(via_str, expected);
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
#[test]
|
|
2250
|
+
fn hex_validate_batch_str_matches_bytes_path() {
|
|
2251
|
+
let ids = c"507f1f77bcf86cd799439011\nzz";
|
|
2252
|
+
let mut out = [0u8; 4];
|
|
2253
|
+
let w =
|
|
2254
|
+
unsafe { castrum_hex_validate_batch_str(ids.as_ptr(), 24, out.as_mut_ptr(), out.len()) };
|
|
2255
|
+
assert_eq!(w, 2);
|
|
2256
|
+
assert_eq!(out[..2], [1, 0]);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// ── Wire-validate / session / batch C-ABI ──────────────────────
|
|
2260
|
+
|
|
2261
|
+
#[test]
|
|
2262
|
+
fn wire_validate_and_session_c_abi() {
|
|
2263
|
+
// SchemaValidator instance for query/cookie validation
|
|
2264
|
+
// Query/cookie JSON forms carry STRING values (percent-decoded text), so
|
|
2265
|
+
// the fixture schema models them as strings.
|
|
2266
|
+
let schema_json = c"{\"type\":\"object\",\"properties\":{\"route\":{\"type\":\"string\"},\"status\":{\"type\":\"string\"}},\"required\":[\"route\"]}";
|
|
2267
|
+
let inst = crate::json::json_schema::SchemaValidator::new(
|
|
2268
|
+
napi::bindgen_prelude::Uint8Array::new(schema_json.to_bytes().to_vec()),
|
|
2269
|
+
)
|
|
2270
|
+
.expect("compile");
|
|
2271
|
+
let inner = inst.inner_ptr() as usize;
|
|
2272
|
+
|
|
2273
|
+
// query: valid → 1 ; violating → 0
|
|
2274
|
+
assert_eq!(
|
|
2275
|
+
unsafe { castrum_query_validate(inner, c"route=/a&status=200".as_ptr()) },
|
|
2276
|
+
1
|
|
2277
|
+
);
|
|
2278
|
+
assert_eq!(
|
|
2279
|
+
unsafe { castrum_query_validate(inner, c"status=notanumber".as_ptr()) },
|
|
2280
|
+
0
|
|
2281
|
+
);
|
|
2282
|
+
// null inner → 0
|
|
2283
|
+
assert_eq!(unsafe { castrum_query_validate(0, c"a=1".as_ptr()) }, 0);
|
|
2284
|
+
|
|
2285
|
+
// cookie: valid route cookie → 1
|
|
2286
|
+
assert_eq!(
|
|
2287
|
+
unsafe { castrum_cookie_validate(inner, c"route=/a".as_ptr()) },
|
|
2288
|
+
1
|
|
2289
|
+
);
|
|
2290
|
+
|
|
2291
|
+
// session seal/open round trip through the C ABI
|
|
2292
|
+
let token = unsafe {
|
|
2293
|
+
castrum_session_seal(
|
|
2294
|
+
c"sess-9".as_ptr(),
|
|
2295
|
+
c"{\"n\":1}".as_ptr(),
|
|
2296
|
+
1_234_567,
|
|
2297
|
+
c"sekrit".as_ptr(),
|
|
2298
|
+
)
|
|
2299
|
+
};
|
|
2300
|
+
assert!(!token.is_null());
|
|
2301
|
+
// open with grow-once
|
|
2302
|
+
let mut buf = [0u8; 256];
|
|
2303
|
+
let w = unsafe { castrum_session_open(token, c"sekrit".as_ptr(), buf.as_mut_ptr(), buf.len()) };
|
|
2304
|
+
assert!(w > 13);
|
|
2305
|
+
assert_eq!(buf[0], 1);
|
|
2306
|
+
// bad signature → 0
|
|
2307
|
+
assert_eq!(
|
|
2308
|
+
unsafe { castrum_session_open(token, c"wrong".as_ptr(), buf.as_mut_ptr(), buf.len()) },
|
|
2309
|
+
0
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
#[test]
|
|
2314
|
+
fn metrics_record_batch_c_abi() {
|
|
2315
|
+
let h = castrum_metrics_create();
|
|
2316
|
+
let c = unsafe { castrum_metrics_counter(h, c"batch_total".as_ptr(), c"k".as_ptr()) };
|
|
2317
|
+
// packed: 1 entry, vals "v", amount 4
|
|
2318
|
+
let mut b = Vec::new();
|
|
2319
|
+
b.extend_from_slice(&1u32.to_le_bytes());
|
|
2320
|
+
b.extend_from_slice(&c.to_le_bytes());
|
|
2321
|
+
b.extend_from_slice(&1u32.to_le_bytes());
|
|
2322
|
+
b.extend_from_slice(b"v");
|
|
2323
|
+
b.extend_from_slice(&4f64.to_le_bytes());
|
|
2324
|
+
assert_eq!(
|
|
2325
|
+
unsafe { castrum_metrics_record_batch(h, b.as_ptr(), b.len()) },
|
|
2326
|
+
1
|
|
2327
|
+
);
|
|
2328
|
+
let mut out = [0u8; 128];
|
|
2329
|
+
let w = unsafe { castrum_metrics_render(h, out.as_mut_ptr(), out.len()) };
|
|
2330
|
+
let text = String::from_utf8_lossy(&out[..w]).into_owned();
|
|
2331
|
+
assert!(text.contains("batch_total{k=\"v\"} 4"));
|
|
2332
|
+
unsafe { castrum_metrics_destroy(h) };
|
|
2333
|
+
}
|