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.
Files changed (184) hide show
  1. package/Cargo.lock +2259 -0
  2. package/Cargo.toml +138 -0
  3. package/README.md +6 -0
  4. package/build.rs +5 -0
  5. package/castrum.linux-x64-gnu.node +0 -0
  6. package/dist/index.d.ts +1348 -1295
  7. package/dist/index.js +4307 -3776
  8. package/docs/API.md +1 -1
  9. package/docs/ARCHITECTURE.md +3 -2
  10. package/docs/CASE_STUDY.md +2 -2
  11. package/docs/ENVIRONMENT.md +16 -0
  12. package/docs/FFI_BUN_GUIDE.md +89 -6
  13. package/docs/REPO_MAP.md +2 -2
  14. package/index.ts +19 -25
  15. package/package.json +9 -1
  16. package/rust/crypto/aead.rs +378 -0
  17. package/rust/crypto/argon2.rs +265 -0
  18. package/rust/crypto/base64.rs +420 -0
  19. package/rust/crypto/bcrypt.rs +89 -0
  20. package/rust/crypto/cookie_sign.rs +169 -0
  21. package/rust/crypto/csrf.rs +106 -0
  22. package/rust/crypto/ed25519.rs +215 -0
  23. package/rust/crypto/hashing.rs +140 -0
  24. package/rust/crypto/hmac_sha256.rs +88 -0
  25. package/rust/crypto/jwt/api.rs +286 -0
  26. package/rust/crypto/jwt/mod.rs +22 -0
  27. package/rust/crypto/jwt/tests.rs +312 -0
  28. package/rust/crypto/jwt/token.rs +360 -0
  29. package/rust/crypto/mod.rs +27 -0
  30. package/rust/crypto/pbkdf2.rs +96 -0
  31. package/rust/crypto/random_token.rs +85 -0
  32. package/rust/crypto/session.rs +222 -0
  33. package/rust/ffi/crypto.rs +708 -0
  34. package/rust/ffi/hashing.rs +285 -0
  35. package/rust/ffi/http.rs +752 -0
  36. package/rust/ffi/ingress.rs +303 -0
  37. package/rust/ffi/json.rs +517 -0
  38. package/rust/ffi/jwt.rs +419 -0
  39. package/rust/ffi/metrics.rs +373 -0
  40. package/rust/ffi/mod.rs +68 -0
  41. package/rust/ffi/payload.rs +275 -0
  42. package/rust/ffi/probe.rs +60 -0
  43. package/rust/ffi/rate_limit.rs +77 -0
  44. package/rust/ffi/route.rs +85 -0
  45. package/rust/ffi/tests.rs +2333 -0
  46. package/rust/ffi/util.rs +152 -0
  47. package/rust/ffi/validators.rs +123 -0
  48. package/rust/http/accept.rs +606 -0
  49. package/rust/http/cookie_parser.rs +140 -0
  50. package/rust/http/etag.rs +289 -0
  51. package/rust/http/form.rs +132 -0
  52. package/rust/http/headers.rs +267 -0
  53. package/rust/http/http_date.rs +286 -0
  54. package/rust/http/http_parser.rs +252 -0
  55. package/rust/http/media_type.rs +355 -0
  56. package/rust/http/method.rs +200 -0
  57. package/rust/http/mime_lookup.rs +130 -0
  58. package/rust/http/mod.rs +32 -0
  59. package/rust/http/multipart.rs +685 -0
  60. package/rust/http/query_parser.rs +180 -0
  61. package/rust/http/url_codec.rs +296 -0
  62. package/rust/http/url_join.rs +324 -0
  63. package/rust/ingress/api.rs +355 -0
  64. package/rust/ingress/cors.rs +403 -0
  65. package/rust/ingress/ingress_constants.rs +209 -0
  66. package/rust/ingress/ip_trust.rs +312 -0
  67. package/rust/ingress/mod.rs +48 -0
  68. package/rust/ingress/native_route.rs +779 -0
  69. package/rust/ingress/options.rs +176 -0
  70. package/rust/ingress/output.rs +406 -0
  71. package/rust/ingress/packed.rs +191 -0
  72. package/rust/ingress/pipeline.rs +534 -0
  73. package/rust/ingress/proxy.rs +138 -0
  74. package/rust/ingress/rate_limit.rs +538 -0
  75. package/rust/ingress/terminal.rs +114 -0
  76. package/rust/ingress/tests.rs +566 -0
  77. package/rust/ingress/time.rs +70 -0
  78. package/rust/json/fast_schema/capture.rs +336 -0
  79. package/rust/json/fast_schema/compile.rs +556 -0
  80. package/rust/json/fast_schema/cursor.rs +368 -0
  81. package/rust/json/fast_schema/email.rs +282 -0
  82. package/rust/json/fast_schema/errors.rs +247 -0
  83. package/rust/json/fast_schema/mod.rs +57 -0
  84. package/rust/json/fast_schema/tests.rs +1143 -0
  85. package/rust/json/fast_schema/types.rs +140 -0
  86. package/rust/json/fast_schema/validate.rs +931 -0
  87. package/rust/json/json_ops.rs +121 -0
  88. package/rust/json/json_schema.rs +837 -0
  89. package/rust/json/json_ser.rs +881 -0
  90. package/rust/json/mod.rs +19 -0
  91. package/rust/json/napi_marshal.rs +318 -0
  92. package/rust/json/patch/api.rs +60 -0
  93. package/rust/json/patch/engine.rs +153 -0
  94. package/rust/json/patch/mod.rs +47 -0
  95. package/rust/json/patch/ops.rs +270 -0
  96. package/rust/json/patch/pointer.rs +49 -0
  97. package/rust/json/patch/tests.rs +499 -0
  98. package/rust/lib.rs +72 -0
  99. package/rust/metrics/api.rs +148 -0
  100. package/rust/metrics/mod.rs +21 -0
  101. package/rust/metrics/registry.rs +961 -0
  102. package/rust/panic_safety.rs +40 -0
  103. package/rust/payload/compress.rs +512 -0
  104. package/rust/payload/mod.rs +13 -0
  105. package/rust/payload/sse.rs +214 -0
  106. package/rust/payload/template.rs +253 -0
  107. package/rust/payload/websocket.rs +69 -0
  108. package/rust/payload/ws_frames.rs +354 -0
  109. package/rust/proptest_suite.rs +139 -0
  110. package/rust/selection.rs +95 -0
  111. package/rust/src/bin/native-bench.rs +495 -0
  112. package/rust/test_support.rs +71 -0
  113. package/rust/util/batch/api.rs +266 -0
  114. package/rust/util/batch/core.rs +473 -0
  115. package/rust/util/batch/mod.rs +23 -0
  116. package/rust/util/batch/tests.rs +445 -0
  117. package/rust/util/batch_core.rs +184 -0
  118. package/rust/util/bytes.rs +407 -0
  119. package/rust/util/mod.rs +34 -0
  120. package/rust/util/packed.rs +473 -0
  121. package/rust/util/text.rs +130 -0
  122. package/rust/util/threadpool.rs +128 -0
  123. package/rust/util/validation.rs +419 -0
  124. package/rust-toolchain.toml +17 -0
  125. package/scripts/postinstall.mjs +199 -0
  126. package/src/ingress/constants.ts +1 -1
  127. package/src/ingress/context.ts +2 -2
  128. package/src/ingress/decode/baked-result.ts +13 -13
  129. package/src/ingress/decode/fast-result.ts +17 -17
  130. package/src/ingress/errors.ts +7 -7
  131. package/src/ingress/headers/baked-templates.ts +1 -1
  132. package/src/ingress/headers/fast-templates.ts +3 -3
  133. package/src/ingress/index.ts +1 -1
  134. package/src/ingress/native-route.ts +9 -5
  135. package/src/ingress/options.ts +1 -1
  136. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  137. package/src/ingress/packing/route-wire.ts +1 -5
  138. package/src/ingress/response/error-bodies.ts +5 -5
  139. package/src/ingress/response/terminal.ts +1 -1
  140. package/src/ingress/routes/echo.ts +4 -4
  141. package/src/ingress/routes/fallback.ts +2 -2
  142. package/src/ingress/routes/head.ts +2 -2
  143. package/src/ingress/routes/native.ts +2 -1
  144. package/src/ingress/routes/options.ts +1 -1
  145. package/src/ingress/routes/read.ts +2 -2
  146. package/src/ingress/server.ts +10 -6
  147. package/src/ingress/status.ts +7 -7
  148. package/src/integration/batch.ts +1 -1
  149. package/src/integration/index.ts +9 -9
  150. package/src/integration/pipeline.ts +6 -6
  151. package/src/integration/websocket.ts +2 -2
  152. package/src/loader/index.ts +1 -5
  153. package/src/loader/ops.ts +1 -1
  154. package/src/native/ffi/build/codecs.ts +104 -8
  155. package/src/native/ffi/build/compress.ts +14 -10
  156. package/src/native/ffi/build/instances.ts +40 -3
  157. package/src/native/ffi/build/metrics.ts +224 -0
  158. package/src/native/ffi/build/parse.ts +59 -23
  159. package/src/native/ffi/build/util.ts +14 -1
  160. package/src/native/ffi/build.ts +2 -0
  161. package/src/native/ffi/selftest.ts +10 -3
  162. package/src/native/ffi/types.ts +111 -9
  163. package/src/native/ffi.ts +87 -1
  164. package/src/native/index.ts +1 -1
  165. package/src/native/loader.ts +2 -2
  166. package/src/native/types/instances.ts +34 -0
  167. package/src/native/types.ts +11 -0
  168. package/src/runtime/codec.ts +53 -9
  169. package/src/runtime/types.ts +7 -0
  170. package/src/rust-ffi/batch/build.ts +16 -9
  171. package/src/rust-ffi/index.ts +7 -6
  172. package/src/rust-ffi/options.ts +1 -3
  173. package/src/rust-ffi/proven.ts +3 -3
  174. package/src/rust-ffi/scalar/crypto.ts +1 -1
  175. package/src/rust-ffi/scalar/factories.ts +73 -0
  176. package/src/rust-ffi/scalar/hashing.ts +1 -1
  177. package/src/rust-ffi/scalar/http.ts +17 -11
  178. package/src/rust-ffi/scalar/index.ts +2 -0
  179. package/src/rust-ffi/scalar/interface.ts +20 -0
  180. package/src/rust-ffi/scalar/payload.ts +11 -3
  181. package/src/rust-ffi/scalar/validation.ts +121 -0
  182. package/src/selection.ts +1 -1
  183. package/src/shared/bytes.ts +5 -4
  184. package/src/shared/codec.ts +18 -3
@@ -0,0 +1,495 @@
1
+ // rust/src/bin/native-bench.rs — native-only timing of the pure-Rust cores.
2
+ //
3
+ // Diagnostic binary (NEVER shipped; not part of the addon). It times the same
4
+ // core functions `bun:ffi` wraps — but with NO JS / FFI boundary — so
5
+ // `bench/ffi-margin.ts` can compare them against the same ops called through
6
+ // `bun:ffi` and pinpoint exactly where the crossing cost is.
7
+ //
8
+ // Inputs: reads base64-NDJSON on stdin (`key\t<base64>`), byte-identical to
9
+ // what the JS runner feeds its FFI side, so both sides measure the same work.
10
+ // Missing keys fall back to the embedded defaults below (so
11
+ // `cargo run --release --bin native-bench` works standalone).
12
+ //
13
+ // Output: one `name\t<ns_per_op>` line per op (min-of-3 batches, after
14
+ // warmup). Build/run from the repo root:
15
+ // cargo run --release --bin native-bench < inputs.ndjson
16
+
17
+ use std::collections::HashMap;
18
+ use std::hint::black_box;
19
+ use std::io::{self, BufRead};
20
+ use std::time::Instant;
21
+
22
+ use aws_lc_rs::aead::AES_256_GCM;
23
+ use aws_lc_rs::hmac;
24
+ use base64::engine::general_purpose::STANDARD as B64;
25
+ use base64::Engine as _;
26
+
27
+ use castrum::crypto::base64::{
28
+ base64_decode_into_slice, base64_encode_into_slice, hex_decode_into_slice,
29
+ hex_encode_into_slice,
30
+ };
31
+ use castrum::crypto::hashing::{crc32_bytes, fast_hash_bytes, fnv1a64_bytes};
32
+ use castrum::http::cookie_parser::cookie_parse_packed_into_slice;
33
+ use castrum::http::etag::etag_from_crc32_into;
34
+ use castrum::http::http_date::http_date_into_slice;
35
+ use castrum::http::http_parser::http_parse_request_packed_into_slice;
36
+ use castrum::http::query_parser::query_parse_packed_into_slice;
37
+ use castrum::http::url_codec::{url_decode_into_slice, url_encode_into_slice};
38
+ use castrum::json::json_ops::{json_sum_ids_bytes, json_valid_bytes};
39
+ use castrum::payload::compress::{
40
+ brotli_compress_into, brotli_decompress_into, gzip_compress_into, gzip_decompress_into,
41
+ };
42
+ use castrum::payload::sse::encode_event_into_slice;
43
+ use castrum::payload::websocket::ws_accept_key_into;
44
+ use castrum::util::validation::{
45
+ validate_email_bytes, validate_ipv4_bytes, validate_ipv6_bytes, validate_uuid_bytes,
46
+ };
47
+
48
+ // ── Embedded default inputs (used when stdin supplies no key) ──────────────
49
+ const DEFAULT_CRC: &[u8] = b"Hello, practical CRC32 checksum test data!";
50
+ const DEFAULT_ENCODE: &[u8] = b"the quick brown fox jumps over the lazy dog 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789";
51
+ const DEFAULT_HEX: &[u8] = b"74686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f6720303132333435363738392030313233343536373839203031323334353637383920303132333435363738392030313233343536373839203031323334353637383920303132333435363738392030313233343536373839";
52
+ const DEFAULT_URL_ENCODE: &[u8] = b"hello world & foo=bar";
53
+ const DEFAULT_URL_DECODE: &[u8] = b"hello%20world%20%26%20foo%3Dbar";
54
+ const DEFAULT_EMAIL: &[u8] = b"user@example.com";
55
+ const DEFAULT_UUID: &[u8] = b"550e8400-e29b-41d4-a716-446655440000";
56
+ const DEFAULT_IPV4: &[u8] = b"192.168.1.100";
57
+ const DEFAULT_IPV6: &[u8] = b"2001:db8::1";
58
+ const DEFAULT_JSON: &[u8] = b"[{\"id\":1,\"name\":\"a\"},{\"id\":2,\"name\":\"b\"}]";
59
+ const DEFAULT_ETAG: &[u8] = b"etag data payload for the margin bench";
60
+ const DEFAULT_WS_KEY: &[u8] = b"dGhlIHNhbXBsZSBub25jZQ==";
61
+ const DEFAULT_HTTP: &[u8] = b"GET /api/users?page=1&limit=20 HTTP/1.1\r\nHost: example.com\r\nAccept: application/json\r\n\r\n";
62
+ const DEFAULT_QUERY: &[u8] = b"name=John+Doe&age=30&tags[]=a&tags[]=b";
63
+ const DEFAULT_COOKIE: &[u8] = b"session=abc123; theme=dark; lang=en-US";
64
+ const DEFAULT_HMAC_KEY: &[u8] = b"super-secret-key-2026";
65
+ const DEFAULT_HMAC_DATA: &[u8] = b"message to sign with HMAC-SHA256";
66
+ const DEFAULT_COMPRESS: &[u8] = b"row 0: the quick brown fox jumps over the lazy dog 0\nrow 1: the quick brown fox jumps over the lazy dog 1\nrow 2: the quick brown fox jumps over the lazy dog 2";
67
+ const DEFAULT_COOKIE_VALUE: &[u8] = b"session-value";
68
+ const DEFAULT_COOKIE_SECRET: &[u8] = b"s3cr3t-secret";
69
+ const DEFAULT_CSRF_SECRET: &[u8] = b"csrf-secret-2026";
70
+ const DEFAULT_PASSWORD: &[u8] = b"correct horse battery staple";
71
+ const DEFAULT_PASSWORD_SALT: &[u8] = b"0123456789abcdef";
72
+ const DEFAULT_PBKDF2_PW: &[u8] = b"password";
73
+ const DEFAULT_PBKDF2_SALT: &[u8] = b"salt";
74
+ const DEFAULT_AEAD_KEY: &[u8] = b"0123456789abcdef0123456789abcdef";
75
+ const DEFAULT_AEAD_NONCE: &[u8] = b"0123456789ab";
76
+ const DEFAULT_AEAD_PT: &[u8] = b"sensitive session payload for the margin bench";
77
+ const DEFAULT_WS_PAYLOAD: &[u8] = b"Hello WebSocket! x10";
78
+ const DEFAULT_JSON_DOC: &[u8] = b"{\"a\":\"b\",\"c\":1}";
79
+ const DEFAULT_JSON_PATCH: &[u8] = b"[{\"op\":\"replace\",\"path\":\"/a\",\"value\":42}]";
80
+ const DEFAULT_JWT_CLAIMS: &[u8] = b"{\"sub\":\"user-1\"}";
81
+ const DEFAULT_JWT_SECRET: &[u8] = b"my-secret";
82
+ const DEFAULT_MULTIPART_BODY: &[u8] = b"--FormBoundary1234\r\nContent-Disposition: form-data; name=\"field1\"\r\n\r\nhello world\r\n--FormBoundary1234--\r\n";
83
+ const DEFAULT_MULTIPART_BOUNDARY: &[u8] = b"FormBoundary1234";
84
+ const DEFAULT_HTTP_DATE_SECS: i64 = 1_750_000_000;
85
+
86
+ /// Min-of-3-batches timing: warm up, then take the fastest batch's ns/op.
87
+ fn bench<F: FnMut() -> u64>(name: &str, iters: u64, mut f: F) {
88
+ for _ in 0..(iters / 20).max(1) {
89
+ black_box(f());
90
+ }
91
+ let mut best = u64::MAX;
92
+ for _ in 0..3 {
93
+ let start = Instant::now();
94
+ for _ in 0..iters {
95
+ black_box(f());
96
+ }
97
+ let ns = start.elapsed().as_nanos() as u64;
98
+ if ns < best {
99
+ best = ns;
100
+ }
101
+ }
102
+ println!("{}\t{:.1}", name, best as f64 / iters as f64);
103
+ }
104
+
105
+ fn packed_out_cap(input_len: usize) -> usize {
106
+ input_len * 9 + 16
107
+ }
108
+
109
+ fn main() {
110
+ let mut inputs: HashMap<String, Vec<u8>> = HashMap::new();
111
+ let stdin = io::stdin();
112
+ for line in stdin.lock().lines().map_while(Result::ok) {
113
+ let line = line.trim_end();
114
+ if line.is_empty() {
115
+ continue;
116
+ }
117
+ if let Some((key, b64)) = line.split_once('\t') {
118
+ if let Ok(bytes) = B64.decode(b64) {
119
+ inputs.insert(key.to_string(), bytes);
120
+ }
121
+ }
122
+ }
123
+ let inp = |key: &str, fallback: &[u8]| -> Vec<u8> {
124
+ inputs
125
+ .get(key)
126
+ .cloned()
127
+ .unwrap_or_else(|| fallback.to_vec())
128
+ };
129
+
130
+ let crc = inp("crc_input", DEFAULT_CRC);
131
+ let encode = inp("encode_data", DEFAULT_ENCODE);
132
+ let hex_in = inp("hex_input", DEFAULT_HEX);
133
+ let url_enc = inp("url_encode_input", DEFAULT_URL_ENCODE);
134
+ let url_dec = inp("url_decode_input", DEFAULT_URL_DECODE);
135
+ let email = inp("email_ok", DEFAULT_EMAIL);
136
+ let uuid = inp("uuid_ok", DEFAULT_UUID);
137
+ let ipv4 = inp("ipv4_ok", DEFAULT_IPV4);
138
+ let ipv6 = inp("ipv6_ok", DEFAULT_IPV6);
139
+ let json = inp("json_payload", DEFAULT_JSON);
140
+ let etag_data = inp("etag_data", DEFAULT_ETAG);
141
+ let ws_key = inp("ws_key", DEFAULT_WS_KEY);
142
+ let http = inp("http_raw", DEFAULT_HTTP);
143
+ let query = inp("query_str", DEFAULT_QUERY);
144
+ let cookie = inp("cookie_str", DEFAULT_COOKIE);
145
+ let hmac_key = inp("hmac_key", DEFAULT_HMAC_KEY);
146
+ let hmac_data = inp("hmac_data", DEFAULT_HMAC_DATA);
147
+ let compress = inp("compress_payload", DEFAULT_COMPRESS);
148
+ let base64_input = inp("base64_input", B64.encode(&encode).as_bytes());
149
+ let hmac_sig = inp("hmac_sig", &[0u8; 64]);
150
+ let cookie_value = inp("cookie_value", DEFAULT_COOKIE_VALUE);
151
+ let cookie_secret = inp("cookie_secret", DEFAULT_COOKIE_SECRET);
152
+ let csrf_secret = inp("csrf_secret", DEFAULT_CSRF_SECRET);
153
+ let password = inp("password_bytes", DEFAULT_PASSWORD);
154
+ let password_salt = inp("password_salt", DEFAULT_PASSWORD_SALT);
155
+ let pbkdf2_pw = inp("pbkdf2_password", DEFAULT_PBKDF2_PW);
156
+ let pbkdf2_salt = inp("pbkdf2_salt", DEFAULT_PBKDF2_SALT);
157
+ let aead_key = inp("aead_key", DEFAULT_AEAD_KEY);
158
+ let aead_nonce = inp("aead_nonce", DEFAULT_AEAD_NONCE);
159
+ let aead_pt = inp("aead_plaintext", DEFAULT_AEAD_PT);
160
+ let ws_payload = inp("ws_payload", DEFAULT_WS_PAYLOAD);
161
+ let json_doc = inp("json_doc", DEFAULT_JSON_DOC);
162
+ let json_patch_data = inp("json_patch_data", DEFAULT_JSON_PATCH);
163
+ let jwt_claims = inp("jwt_claims", DEFAULT_JWT_CLAIMS);
164
+ let jwt_secret = inp("jwt_secret", DEFAULT_JWT_SECRET);
165
+ let multipart_body = inp("multipart_body", DEFAULT_MULTIPART_BODY);
166
+ let multipart_boundary = inp("multipart_boundary", DEFAULT_MULTIPART_BOUNDARY);
167
+ let gzip_compressed = inp("gzip_compressed", &[]);
168
+ let brotli_compressed = inp("brotli_compressed", &[]);
169
+ let content_type = inp(
170
+ "content_type_multipart",
171
+ b"multipart/form-data; boundary=FormBoundary1234; charset=UTF-8".as_slice(),
172
+ );
173
+
174
+ // Reusable output buffers.
175
+ let mut hex_out = vec![0u8; encode.len() * 2];
176
+ let mut hex_dec_out = vec![0u8; hex_in.len() / 2 + 1];
177
+ let mut b64_out = vec![0u8; encode.len() * 4 / 3 + 4];
178
+ let mut b64_dec_out = vec![0u8; encode.len() + 8];
179
+ let mut url_out = vec![0u8; url_enc.len() * 3];
180
+ let mut urld_out = vec![0u8; url_dec.len()];
181
+ let mut etag_out = vec![0u8; 32];
182
+ let mut date_out = vec![0u8; 32];
183
+ let mut ws_out = vec![0u8; 32];
184
+ let mut http_out = vec![0u8; packed_out_cap(http.len())];
185
+ let mut query_out = vec![0u8; packed_out_cap(query.len())];
186
+ let mut cookie_out = vec![0u8; packed_out_cap(cookie.len())];
187
+ let mut gz_out = vec![0u8; compress.len() * 2 + 64];
188
+ let mut br_out = vec![0u8; compress.len() * 2 + 64];
189
+ let mut gz_d_out = vec![0u8; compress.len() * 2 + 64];
190
+ let mut br_d_out = vec![0u8; compress.len() * 2 + 64];
191
+ let mut cookie_sig_out = vec![0u8; cookie_value.len() + 65];
192
+ let mut cookie_val_out = vec![0u8; cookie_value.len() + 65];
193
+ let mut csrf_out = [0u8; 129];
194
+ let mut ws_frame_out = vec![0u8; ws_payload.len() + 14];
195
+ let mut pbkdf2_out = vec![0u8; 32];
196
+ let mut random_out = vec![0u8; 32];
197
+
198
+ // Compiled once (matches the C-ABI per-thread key cache steady state).
199
+ let hmac_key_compiled = hmac::Key::new(hmac::HMAC_SHA256, &hmac_key);
200
+ let cookie_key = hmac::Key::new(hmac::HMAC_SHA256, &cookie_secret);
201
+ let csrf_key = hmac::Key::new(hmac::HMAC_SHA256, &csrf_secret);
202
+ let mut hmac_tag_out = [0u8; 64];
203
+ let mut sig_bytes = [0u8; 32];
204
+
205
+ // Derived inputs computed ONCE (matching the FFI side's setup).
206
+ let ws_frame = castrum::payload::ws_frames::encode_frame_into(
207
+ 1,
208
+ &ws_payload,
209
+ false,
210
+ true,
211
+ &mut ws_frame_out,
212
+ )
213
+ .unwrap();
214
+ let aead_ct =
215
+ castrum::crypto::aead::encrypt(&AES_256_GCM, &aead_key, &aead_nonce, &aead_pt).unwrap();
216
+ let argon_phc = castrum::crypto::argon2::hash_password(&password, &password_salt, 8, 1, 1, 16)
217
+ .unwrap()
218
+ .into_bytes();
219
+ let bcrypt_phc = bcrypt::hash(&password, castrum::crypto::bcrypt::clamp_cost(4))
220
+ .unwrap()
221
+ .into_bytes();
222
+ let signed_cookie_len = castrum::crypto::cookie_sign::sign_cookie_into(
223
+ &cookie_value,
224
+ &cookie_key,
225
+ &mut cookie_sig_out,
226
+ )
227
+ .unwrap();
228
+ let csrf_token_bytes: Vec<u8> = {
229
+ let mut rnd = [0u8; 32];
230
+ getrandom::fill(&mut rnd).unwrap();
231
+ let mut rnd_hex = [0u8; 64];
232
+ castrum::util::bytes::hex_encode(&rnd, &mut rnd_hex);
233
+ let mut sig_hex = [0u8; 64];
234
+ castrum::util::bytes::hex_encode(hmac::sign(&csrf_key, &rnd_hex).as_ref(), &mut sig_hex);
235
+ let mut t = Vec::with_capacity(129);
236
+ t.extend_from_slice(&rnd_hex);
237
+ t.push(b'.');
238
+ t.extend_from_slice(&sig_hex);
239
+ t
240
+ };
241
+
242
+ // ── Hash / checksum ──
243
+ bench("crc32", 1_000_000, || crc32_bytes(&crc) as u64);
244
+ bench("fnv1a64", 1_000_000, || fnv1a64_bytes(&crc) as u64);
245
+ bench("xxh3", 1_000_000, || fast_hash_bytes(&crc) as u64);
246
+
247
+ // ── Encode / decode ──
248
+ bench("hex_encode", 500_000, || {
249
+ hex_encode_into_slice(&encode, &mut hex_out).unwrap() as u64
250
+ });
251
+ bench("hex_decode", 500_000, || {
252
+ hex_decode_into_slice(&hex_in, &mut hex_dec_out).unwrap() as u64
253
+ });
254
+ bench("base64_encode", 500_000, || {
255
+ base64_encode_into_slice(&encode, &mut b64_out, false, true).unwrap() as u64
256
+ });
257
+ bench("base64_decode", 500_000, || {
258
+ base64_decode_into_slice(&base64_input, &mut b64_dec_out, false, true).unwrap() as u64
259
+ });
260
+ bench("url_encode", 500_000, || {
261
+ url_encode_into_slice(&url_enc, &mut url_out).unwrap() as u64
262
+ });
263
+ bench("url_decode", 500_000, || {
264
+ url_decode_into_slice(&url_dec, &mut urld_out).unwrap() as u64
265
+ });
266
+
267
+ // ── Validators ──
268
+ bench("validate_email", 1_000_000, || {
269
+ validate_email_bytes(&email) as u64
270
+ });
271
+ bench("validate_uuid", 1_000_000, || {
272
+ validate_uuid_bytes(&uuid) as u64
273
+ });
274
+ bench("validate_ipv4", 1_000_000, || {
275
+ validate_ipv4_bytes(&ipv4) as u64
276
+ });
277
+ bench("validate_ipv6", 1_000_000, || {
278
+ validate_ipv6_bytes(&ipv6) as u64
279
+ });
280
+
281
+ // ── JSON (zero-DOM) ──
282
+ bench("json_valid", 500_000, || json_valid_bytes(&json) as u64);
283
+ bench("json_sum_ids", 500_000, || {
284
+ json_sum_ids_bytes(&json).unwrap() as u64
285
+ });
286
+ // ── JSON DOM parse ALONE (no napi marshal) — the reference for the
287
+ // structural json_parse split: proves the sonic-rs parse is cheap and the
288
+ // ~5x CPU-bench loss is the napi DOM→JS marshal, not the parser. ──
289
+ bench("json_parse_dom", 200_000, || {
290
+ let v: serde_json::Value = sonic_rs::from_slice(&json).unwrap();
291
+ v.as_array().map_or(0, |a| a.len()) as u64
292
+ });
293
+
294
+ // ── ETag / HTTP-date / WebSocket accept ──
295
+ bench("etag", 500_000, || {
296
+ let crc = crc32_bytes(&etag_data);
297
+ etag_from_crc32_into(crc, false, &mut etag_out).unwrap() as u64
298
+ });
299
+ bench("http_date", 500_000, || {
300
+ http_date_into_slice(DEFAULT_HTTP_DATE_SECS, &mut date_out).unwrap() as u64
301
+ });
302
+ bench("ws_accept_key", 500_000, || {
303
+ ws_accept_key_into(&ws_key, &mut ws_out).unwrap() as u64
304
+ });
305
+
306
+ // ── SSE (write-into-buffer core) ──
307
+ let mut sse_out = [0u8; 512];
308
+ bench("sse_encode", 200_000, || {
309
+ encode_event_into_slice(
310
+ Some("update"),
311
+ &ws_payload,
312
+ Some("42"),
313
+ Some(3000),
314
+ &mut sse_out,
315
+ )
316
+ .unwrap() as u64
317
+ });
318
+
319
+ // ── Media type parse (allocating core — the reference for the napi-object
320
+ // vs JS-baseline loss; shows the parse itself is not the bottleneck). ──
321
+ bench("media_type_parse", 200_000, || {
322
+ let parsed = castrum::http::media_type::parse_media_type_core(&content_type).unwrap();
323
+ parsed.ty.len() as u64 + parsed.subtype.len() as u64
324
+ });
325
+
326
+ // ── HMAC (compiled key) ──
327
+ bench("hmac_sha256", 200_000, || {
328
+ let tag = hmac::sign(&hmac_key_compiled, &hmac_data);
329
+ hex_encode_into_slice(tag.as_ref(), &mut hmac_tag_out).unwrap() as u64
330
+ });
331
+ bench("hmac_sha256_verify", 200_000, || {
332
+ hex_decode_into_slice(&hmac_sig, &mut sig_bytes).unwrap();
333
+ u64::from(hmac::verify(&hmac_key_compiled, &hmac_data, &sig_bytes).is_ok())
334
+ });
335
+
336
+ // ── Cookies / CSRF ──
337
+ bench("sign_cookie", 300_000, || {
338
+ castrum::crypto::cookie_sign::sign_cookie_into(
339
+ &cookie_value,
340
+ &cookie_key,
341
+ &mut cookie_sig_out,
342
+ )
343
+ .unwrap_or(0) as u64
344
+ });
345
+ bench("verify_cookie", 300_000, || {
346
+ castrum::crypto::cookie_sign::verify_cookie_into(
347
+ &cookie_sig_out[..signed_cookie_len],
348
+ &cookie_key,
349
+ &mut cookie_val_out,
350
+ )
351
+ .unwrap_or(0) as u64
352
+ });
353
+ bench("csrf_token", 300_000, || {
354
+ let mut rnd = [0u8; 32];
355
+ getrandom::fill(&mut rnd).unwrap();
356
+ let mut rnd_hex = [0u8; 64];
357
+ castrum::util::bytes::hex_encode(&rnd, &mut rnd_hex);
358
+ let mut sig_hex = [0u8; 64];
359
+ castrum::util::bytes::hex_encode(hmac::sign(&csrf_key, &rnd_hex).as_ref(), &mut sig_hex);
360
+ csrf_out[..64].copy_from_slice(&rnd_hex);
361
+ csrf_out[64] = b'.';
362
+ csrf_out[65..129].copy_from_slice(&sig_hex);
363
+ 129
364
+ });
365
+ bench("csrf_verify", 300_000, || {
366
+ u64::from(castrum::crypto::csrf::csrf_verify_with_key(
367
+ &csrf_token_bytes,
368
+ &csrf_key,
369
+ ))
370
+ });
371
+
372
+ // ── Password KDFs (work-bound; low iterations) ──
373
+ bench("password_hash", 50, || {
374
+ castrum::crypto::argon2::hash_password(&password, &password_salt, 8, 1, 1, 16)
375
+ .unwrap()
376
+ .len() as u64
377
+ });
378
+ bench("password_verify", 50, || {
379
+ u64::from(castrum::crypto::argon2::verify_password(
380
+ &password, &argon_phc,
381
+ ))
382
+ });
383
+ bench("password_hash_bcrypt", 3, || {
384
+ bcrypt::hash(&password, castrum::crypto::bcrypt::clamp_cost(4))
385
+ .unwrap()
386
+ .len() as u64
387
+ });
388
+ bench("password_verify_bcrypt", 3, || {
389
+ u64::from(
390
+ bcrypt::verify(&password, std::str::from_utf8(&bcrypt_phc).unwrap()).unwrap_or(false),
391
+ )
392
+ });
393
+
394
+ // ── PBKDF2 ──
395
+ bench("pbkdf2_sha256", 200, || {
396
+ pbkdf2::pbkdf2_hmac::<sha2::Sha256>(&pbkdf2_pw, &pbkdf2_salt, 1, &mut pbkdf2_out);
397
+ pbkdf2_out.len() as u64
398
+ });
399
+
400
+ // ── AEAD ──
401
+ bench("aead_encrypt", 200_000, || {
402
+ castrum::crypto::aead::encrypt(&AES_256_GCM, &aead_key, &aead_nonce, &aead_pt)
403
+ .unwrap()
404
+ .len() as u64
405
+ });
406
+ bench("aead_decrypt", 200_000, || {
407
+ castrum::crypto::aead::decrypt(&AES_256_GCM, &aead_key, &aead_nonce, &aead_ct)
408
+ .unwrap()
409
+ .len() as u64
410
+ });
411
+
412
+ // ── WebSocket frames ──
413
+ bench("ws_frame_encode", 300_000, || {
414
+ castrum::payload::ws_frames::encode_frame_into(
415
+ 1,
416
+ &ws_payload,
417
+ false,
418
+ true,
419
+ &mut ws_frame_out,
420
+ )
421
+ .unwrap() as u64
422
+ });
423
+ bench("ws_frame_decode", 300_000, || {
424
+ let frame = castrum::payload::ws_frames::decode_frame(&ws_frame_out[..ws_frame]).unwrap();
425
+ frame.payload.len() as u64
426
+ });
427
+
428
+ // ── JSON patch ──
429
+ bench("json_patch", 200_000, || {
430
+ castrum::json::patch::apply_json_patch_bytes(&json_doc, &json_patch_data)
431
+ .unwrap()
432
+ .len() as u64
433
+ });
434
+
435
+ // ── Random token (getrandom + hex) ──
436
+ bench("random_token", 200_000, || {
437
+ let mut rnd = vec![0u8; 16];
438
+ getrandom::fill(&mut rnd).unwrap();
439
+ castrum::util::bytes::hex_encode(&rnd, &mut random_out);
440
+ 32
441
+ });
442
+
443
+ // ── Packed parsers ──
444
+ bench("http_parse_packed", 200_000, || {
445
+ http_parse_request_packed_into_slice(&http, &mut http_out).unwrap() as u64
446
+ });
447
+ bench("query_parse_packed", 200_000, || {
448
+ query_parse_packed_into_slice(&query, &mut query_out).unwrap() as u64
449
+ });
450
+ bench("cookie_parse_packed", 200_000, || {
451
+ cookie_parse_packed_into_slice(&cookie, &mut cookie_out).unwrap() as u64
452
+ });
453
+ bench("form_parse_packed", 200_000, || {
454
+ query_parse_packed_into_slice(&query, &mut query_out).unwrap() as u64
455
+ });
456
+ bench("multipart_parse_packed", 100_000, || {
457
+ let parts = castrum::http::multipart::parse_multipart_limited(
458
+ &multipart_body,
459
+ &multipart_boundary,
460
+ &Default::default(),
461
+ );
462
+ let mut buf = Vec::new();
463
+ castrum::http::multipart::parts_to_packed(&parts, &mut buf);
464
+ buf.len() as u64
465
+ });
466
+
467
+ // ── JWT sign (bytes) ──
468
+ bench("jwt_sign_bytes", 200_000, || {
469
+ let mut value: serde_json::Value = serde_json::from_slice(&jwt_claims).unwrap();
470
+ let payload_b64 =
471
+ castrum::crypto::jwt::inject_and_payload_b64(&mut value, None, 0).unwrap();
472
+ castrum::crypto::jwt::build_token(
473
+ castrum::crypto::jwt::jwt_header_b64(),
474
+ &payload_b64,
475
+ &jwt_secret,
476
+ )
477
+ .len() as u64
478
+ });
479
+
480
+ // ── Compression (write-into-buffer cores) ──
481
+ bench("gzip_compress", 20_000, || {
482
+ gzip_compress_into(&compress, 6, &mut gz_out).unwrap() as u64
483
+ });
484
+ bench("brotli_compress", 20_000, || {
485
+ brotli_compress_into(&compress, 5, &mut br_out).unwrap() as u64
486
+ });
487
+ if !gzip_compressed.is_empty() && !brotli_compressed.is_empty() {
488
+ bench("gzip_decompress", 20_000, || {
489
+ gzip_decompress_into(&gzip_compressed, 1024 * 1024, &mut gz_d_out).unwrap() as u64
490
+ });
491
+ bench("brotli_decompress", 20_000, || {
492
+ brotli_decompress_into(&brotli_compressed, 1024 * 1024, &mut br_d_out).unwrap() as u64
493
+ });
494
+ }
495
+ }
@@ -0,0 +1,71 @@
1
+ // rust/test_support.rs — Shared helpers for Rust unit tests (`cargo test`).
2
+ //
3
+ // Only compiled when running tests. Each module's `#[cfg(test)] mod tests`
4
+ // imports whatever it needs from here instead of redefining it.
5
+
6
+ /// Build a packed header buffer:
7
+ /// `[u16 count] repeated { [u16 name_len][name][u32 val_len][val] }`.
8
+ pub(crate) fn pack_headers<'a, I>(pairs: I) -> Vec<u8>
9
+ where
10
+ I: IntoIterator<Item = (&'a str, &'a str)>,
11
+ {
12
+ let pairs: Vec<(&str, &str)> = pairs.into_iter().collect();
13
+ let mut out = Vec::new();
14
+ out.extend_from_slice(&(pairs.len() as u16).to_le_bytes());
15
+ for (name, value) in pairs {
16
+ out.extend_from_slice(&(name.len() as u16).to_le_bytes());
17
+ out.extend_from_slice(name.as_bytes());
18
+ out.extend_from_slice(&(value.len() as u32).to_le_bytes());
19
+ out.extend_from_slice(value.as_bytes());
20
+ }
21
+ out
22
+ }
23
+
24
+ /// Decode a packed pairs buffer:
25
+ /// `[u32 count] repeated { [u32 key_len][key][u32 val_len][val] }`.
26
+ pub(crate) fn decode_packed_pairs(packed: &[u8]) -> Vec<(Vec<u8>, Vec<u8>)> {
27
+ assert!(packed.len() >= 4, "packed buffer too short");
28
+ let count = u32::from_le_bytes([packed[0], packed[1], packed[2], packed[3]]) as usize;
29
+ let mut out = Vec::with_capacity(count);
30
+ let mut pos = 4usize;
31
+ for _ in 0..count {
32
+ let key_len = u32::from_le_bytes([
33
+ packed[pos],
34
+ packed[pos + 1],
35
+ packed[pos + 2],
36
+ packed[pos + 3],
37
+ ]) as usize;
38
+ pos += 4;
39
+ let key = packed[pos..pos + key_len].to_vec();
40
+ pos += key_len;
41
+ let val_len = u32::from_le_bytes([
42
+ packed[pos],
43
+ packed[pos + 1],
44
+ packed[pos + 2],
45
+ packed[pos + 3],
46
+ ]) as usize;
47
+ pos += 4;
48
+ let val = packed[pos..pos + val_len].to_vec();
49
+ pos += val_len;
50
+ out.push((key, val));
51
+ }
52
+ out
53
+ }
54
+
55
+ /// Deterministic xorshift PRNG so fuzz-style tests are reproducible.
56
+ pub(crate) struct Rng(pub u64);
57
+
58
+ impl Rng {
59
+ pub fn next(&mut self) -> u64 {
60
+ let mut x = self.0;
61
+ x ^= x << 13;
62
+ x ^= x >> 7;
63
+ x ^= x << 17;
64
+ self.0 = x;
65
+ x
66
+ }
67
+
68
+ pub fn bytes(&mut self, len: usize) -> Vec<u8> {
69
+ (0..len).map(|_| self.next() as u8).collect()
70
+ }
71
+ }