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,373 @@
1
+ // rust/ffi/metrics.rs — metrics-registry C-ABI exports (`castrum_metrics_*`).
2
+ //
3
+ // Stateful handles owned by the CALLER (the route-stack ownership model):
4
+ // `castrum_metrics_create` returns a `Box<MetricsRegistry>` handle, every call
5
+ // takes it as a `usize`, and `castrum_metrics_destroy` frees it. Series ids
6
+ // are `u32` family indices (idempotent declaration), label VALUES cross packed
7
+ // `\x1f`-separated in one `(ptr,len)` buffer, and render follows the
8
+ // needed-size convention. A null handle (0) → safe failure (0 / sentinel),
9
+ // never a dereference.
10
+
11
+ use std::slice;
12
+
13
+ use super::util::panic_guard;
14
+
15
+ /// `u32::MAX` — the declare-failure sentinel (fits f64 exactly; real series
16
+ /// ids are dense from 0 and capped well below this).
17
+ pub(crate) const METRICS_DECLARE_ERR: u32 = u32::MAX;
18
+
19
+ /// Create an empty metrics registry → opaque handle (`0` = allocation panic).
20
+ #[no_mangle]
21
+ pub extern "C" fn castrum_metrics_create() -> usize {
22
+ let handle = Box::into_raw(Box::new(crate::metrics::MetricsRegistry::new()));
23
+ handle as usize
24
+ }
25
+
26
+ /// Declare a counter family → series id (`METRICS_DECLARE_ERR` on error).
27
+ /// `label_keys` is a NUL-terminated `\x1f`-separated key list ("" = none).
28
+ ///
29
+ /// # Safety
30
+ /// `handle` must be a live registry handle; `name`/`label_keys` valid C strings.
31
+ #[no_mangle]
32
+ pub unsafe extern "C" fn castrum_metrics_counter(
33
+ handle: usize,
34
+ name: *const std::os::raw::c_char,
35
+ label_keys: *const std::os::raw::c_char,
36
+ ) -> u32 {
37
+ if handle == 0 || name.is_null() || label_keys.is_null() {
38
+ return METRICS_DECLARE_ERR;
39
+ }
40
+ let name = std::ffi::CStr::from_ptr(name).to_bytes();
41
+ let keys = split_keys(std::ffi::CStr::from_ptr(label_keys).to_bytes());
42
+ panic_guard(
43
+ || {
44
+ crate::metrics::MetricsRegistry::declare(
45
+ unsafe { &*(handle as *const crate::metrics::MetricsRegistry) },
46
+ crate::metrics::registry::MetricsKind::Counter,
47
+ std::str::from_utf8(name).unwrap_or_default(),
48
+ &keys,
49
+ &[],
50
+ )
51
+ },
52
+ Err(String::new()),
53
+ )
54
+ .unwrap_or(METRICS_DECLARE_ERR)
55
+ }
56
+
57
+ /// Declare a gauge family → series id. Same ABI as [`castrum_metrics_counter`].
58
+ ///
59
+ /// # Safety
60
+ /// See [`castrum_metrics_counter`].
61
+ #[no_mangle]
62
+ pub unsafe extern "C" fn castrum_metrics_gauge(
63
+ handle: usize,
64
+ name: *const std::os::raw::c_char,
65
+ label_keys: *const std::os::raw::c_char,
66
+ ) -> u32 {
67
+ if handle == 0 || name.is_null() || label_keys.is_null() {
68
+ return METRICS_DECLARE_ERR;
69
+ }
70
+ let name = std::ffi::CStr::from_ptr(name).to_bytes();
71
+ let keys = split_keys(std::ffi::CStr::from_ptr(label_keys).to_bytes());
72
+ panic_guard(
73
+ || {
74
+ crate::metrics::MetricsRegistry::declare(
75
+ unsafe { &*(handle as *const crate::metrics::MetricsRegistry) },
76
+ crate::metrics::registry::MetricsKind::Gauge,
77
+ std::str::from_utf8(name).unwrap_or_default(),
78
+ &keys,
79
+ &[],
80
+ )
81
+ },
82
+ Err(String::new()),
83
+ )
84
+ .unwrap_or(METRICS_DECLARE_ERR)
85
+ }
86
+
87
+ /// Declare a histogram family → series id. `buckets_csv` is a NUL-terminated
88
+ /// comma-separated finite positive list ("" = the default latency buckets).
89
+ ///
90
+ /// # Safety
91
+ /// See [`castrum_metrics_counter`].
92
+ #[no_mangle]
93
+ pub unsafe extern "C" fn castrum_metrics_histogram(
94
+ handle: usize,
95
+ name: *const std::os::raw::c_char,
96
+ label_keys: *const std::os::raw::c_char,
97
+ buckets_csv: *const std::os::raw::c_char,
98
+ ) -> u32 {
99
+ if handle == 0 || name.is_null() || label_keys.is_null() || buckets_csv.is_null() {
100
+ return METRICS_DECLARE_ERR;
101
+ }
102
+ let name = std::ffi::CStr::from_ptr(name).to_bytes();
103
+ let keys = split_keys(std::ffi::CStr::from_ptr(label_keys).to_bytes());
104
+ let csv = std::ffi::CStr::from_ptr(buckets_csv).to_bytes();
105
+ let buckets: Vec<f64> = std::str::from_utf8(csv)
106
+ .unwrap_or_default()
107
+ .split(',')
108
+ .filter_map(|s| s.trim().parse::<f64>().ok())
109
+ .collect();
110
+ panic_guard(
111
+ || {
112
+ crate::metrics::MetricsRegistry::declare(
113
+ unsafe { &*(handle as *const crate::metrics::MetricsRegistry) },
114
+ crate::metrics::registry::MetricsKind::Histogram,
115
+ std::str::from_utf8(name).unwrap_or_default(),
116
+ &keys,
117
+ &buckets,
118
+ )
119
+ },
120
+ Err(String::new()),
121
+ )
122
+ .unwrap_or(METRICS_DECLARE_ERR)
123
+ }
124
+
125
+ /// Record one event: counter/gauge `+= amount`, histogram observe `amount`.
126
+ /// `values` is the packed `\x1f`-separated label values. Returns 1 = ok.
127
+ ///
128
+ /// # Safety
129
+ /// `handle` must be live; `values` valid for reads of `values_len` bytes.
130
+ #[no_mangle]
131
+ pub unsafe extern "C" fn castrum_metrics_record(
132
+ handle: usize,
133
+ series: u32,
134
+ values: *const u8,
135
+ values_len: usize,
136
+ amount: f64,
137
+ ) -> u8 {
138
+ if handle == 0 || (values.is_null() && values_len != 0) {
139
+ return 0;
140
+ }
141
+ let vals = if values_len == 0 {
142
+ &[]
143
+ } else {
144
+ slice::from_raw_parts(values, values_len)
145
+ };
146
+ panic_guard(
147
+ || {
148
+ crate::metrics::MetricsRegistry::add(
149
+ unsafe { &*(handle as *const crate::metrics::MetricsRegistry) },
150
+ series,
151
+ vals,
152
+ amount,
153
+ )
154
+ },
155
+ Err(String::new()),
156
+ )
157
+ .is_ok() as u8
158
+ }
159
+
160
+ /// Gauge assignment (`= value`). Returns 1 = ok.
161
+ ///
162
+ /// # Safety
163
+ /// See [`castrum_metrics_record`].
164
+ #[no_mangle]
165
+ pub unsafe extern "C" fn castrum_metrics_gauge_set(
166
+ handle: usize,
167
+ series: u32,
168
+ values: *const u8,
169
+ values_len: usize,
170
+ value: f64,
171
+ ) -> u8 {
172
+ if handle == 0 || (values.is_null() && values_len != 0) {
173
+ return 0;
174
+ }
175
+ let vals = if values_len == 0 {
176
+ &[]
177
+ } else {
178
+ slice::from_raw_parts(values, values_len)
179
+ };
180
+ panic_guard(
181
+ || {
182
+ crate::metrics::MetricsRegistry::set(
183
+ unsafe { &*(handle as *const crate::metrics::MetricsRegistry) },
184
+ series,
185
+ vals,
186
+ value,
187
+ )
188
+ },
189
+ Err(String::new()),
190
+ )
191
+ .is_ok() as u8
192
+ }
193
+
194
+ /// Record one event with the label VALUES crossing as a SINGLE `cstring` ARG
195
+ /// (`\x1f`-joined) — the engine transcodes the JS string in-engine, so the JS
196
+ /// hot path does ZERO `TextEncoder` work (`values.join('\x1f')` is all it
197
+ /// pays). Values must not contain NUL (a NUL would truncate the list — label
198
+ /// values are validated against NUL anyway). Returns 1 = ok.
199
+ ///
200
+ /// # Safety
201
+ /// `handle` must be live; `values` a valid NUL-terminated C string.
202
+ #[no_mangle]
203
+ pub unsafe extern "C" fn castrum_metrics_record_str(
204
+ handle: usize,
205
+ series: u32,
206
+ values: *const std::os::raw::c_char,
207
+ amount: f64,
208
+ ) -> u8 {
209
+ if handle == 0 || values.is_null() {
210
+ return 0;
211
+ }
212
+ let vals = std::ffi::CStr::from_ptr(values).to_bytes();
213
+ panic_guard(
214
+ || {
215
+ crate::metrics::MetricsRegistry::add(
216
+ unsafe { &*(handle as *const crate::metrics::MetricsRegistry) },
217
+ series,
218
+ vals,
219
+ amount,
220
+ )
221
+ },
222
+ Err(String::new()),
223
+ )
224
+ .is_ok() as u8
225
+ }
226
+
227
+ /// Gauge assignment (`= value`) with `\x1f`-joined `cstring` values — the
228
+ /// zero-encode sibling of [`castrum_metrics_gauge_set`]. Returns 1 = ok.
229
+ ///
230
+ /// # Safety
231
+ /// See [`castrum_metrics_record_str`].
232
+ #[no_mangle]
233
+ pub unsafe extern "C" fn castrum_metrics_gauge_set_str(
234
+ handle: usize,
235
+ series: u32,
236
+ values: *const std::os::raw::c_char,
237
+ value: f64,
238
+ ) -> u8 {
239
+ if handle == 0 || values.is_null() {
240
+ return 0;
241
+ }
242
+ let vals = std::ffi::CStr::from_ptr(values).to_bytes();
243
+ panic_guard(
244
+ || {
245
+ crate::metrics::MetricsRegistry::set(
246
+ unsafe { &*(handle as *const crate::metrics::MetricsRegistry) },
247
+ series,
248
+ vals,
249
+ value,
250
+ )
251
+ },
252
+ Err(String::new()),
253
+ )
254
+ .is_ok() as u8
255
+ }
256
+
257
+ /// Render the Prometheus text format into `out` (needed-size convention:
258
+ /// `0` = null handle / render panic; `w > out_cap` = exact required size).
259
+ ///
260
+ /// # Safety
261
+ /// `handle` must be live; `out` for writes up to `out_cap`.
262
+ #[no_mangle]
263
+ pub unsafe extern "C" fn castrum_metrics_render(
264
+ handle: usize,
265
+ out: *mut u8,
266
+ out_cap: usize,
267
+ ) -> usize {
268
+ if handle == 0 || out.is_null() {
269
+ return 0;
270
+ }
271
+ let rendered = panic_guard(
272
+ || {
273
+ crate::metrics::MetricsRegistry::render(unsafe {
274
+ &*(handle as *const crate::metrics::MetricsRegistry)
275
+ })
276
+ },
277
+ Vec::new(),
278
+ );
279
+ if rendered.len() > out_cap {
280
+ return rendered.len();
281
+ }
282
+ slice::from_raw_parts_mut(out, rendered.len()).copy_from_slice(&rendered);
283
+ rendered.len()
284
+ }
285
+
286
+ /// Destroy a registry handle (idempotent-safe against null only; double-free
287
+ /// is caller error, same contract as `castrum_route_destroy`).
288
+ ///
289
+ /// # Safety
290
+ /// `handle` must be either 0 or a live handle not yet destroyed.
291
+ #[no_mangle]
292
+ pub unsafe extern "C" fn castrum_metrics_destroy(handle: usize) {
293
+ if handle != 0 {
294
+ drop(Box::from_raw(
295
+ handle as *mut crate::metrics::MetricsRegistry,
296
+ ));
297
+ }
298
+ }
299
+
300
+ /// Split a `\x1f`-separated key list into owned strings (empty input → []).
301
+ fn split_keys(bytes: &[u8]) -> Vec<&str> {
302
+ if bytes.is_empty() {
303
+ return Vec::new();
304
+ }
305
+ std::str::from_utf8(bytes)
306
+ .unwrap_or_default()
307
+ .split('\x1f')
308
+ .collect()
309
+ }
310
+
311
+ /// Dump the packed series snapshot (v1 wire format — see
312
+ /// `crate::metrics::registry::MetricsRegistry::snapshot_into`) into `out`.
313
+ /// Needed-size convention: `0` = null handle; `w > out_cap` = exact required
314
+ /// size; else bytes written.
315
+ ///
316
+ /// # Safety
317
+ /// `handle` must be live; `out` for writes up to `out_cap`.
318
+ #[no_mangle]
319
+ pub unsafe extern "C" fn castrum_metrics_snapshot(
320
+ handle: usize,
321
+ out: *mut u8,
322
+ out_cap: usize,
323
+ ) -> usize {
324
+ if handle == 0 || out.is_null() {
325
+ return 0;
326
+ }
327
+ let dumped = panic_guard(
328
+ || {
329
+ crate::metrics::MetricsRegistry::snapshot(unsafe {
330
+ &*(handle as *const crate::metrics::MetricsRegistry)
331
+ })
332
+ },
333
+ Vec::new(),
334
+ );
335
+ if dumped.len() > out_cap {
336
+ return dumped.len();
337
+ }
338
+ slice::from_raw_parts_mut(out, dumped.len()).copy_from_slice(&dumped);
339
+ dumped.len()
340
+ }
341
+
342
+ /// Record a BATCH of events in one crossing. Packed layout:
343
+ /// `[u32 n]{[u32 series][u32 valsLen][vals][f64 amount]}`. Returns 1 = all
344
+ /// applied; 0 = null handle / truncated / any entry failed (entries before a
345
+ /// failure stay applied).
346
+ ///
347
+ /// # Safety
348
+ /// `handle` must be live; `packed` valid for reads of `packed_len` bytes.
349
+ #[no_mangle]
350
+ pub unsafe extern "C" fn castrum_metrics_record_batch(
351
+ handle: usize,
352
+ packed: *const u8,
353
+ packed_len: usize,
354
+ ) -> u8 {
355
+ if handle == 0 || (packed.is_null() && packed_len != 0) {
356
+ return 0;
357
+ }
358
+ let buf = if packed_len == 0 {
359
+ &[]
360
+ } else {
361
+ slice::from_raw_parts(packed, packed_len)
362
+ };
363
+ panic_guard(
364
+ || {
365
+ crate::metrics::MetricsRegistry::record_batch(
366
+ unsafe { &*(handle as *const crate::metrics::MetricsRegistry) },
367
+ buf,
368
+ )
369
+ },
370
+ Err(String::new()),
371
+ )
372
+ .is_ok() as u8
373
+ }
@@ -0,0 +1,68 @@
1
+ //! C-ABI (`extern "C"`) exports for `bun:ffi` (Bun's JIT-compiled FFI,
2
+ //! ~10-20ns crossing vs ~100-300ns for N-API). The same cdylib serves BOTH
3
+ //! runtimes: Node loads it through napi-rs (napi_register_module_v1), Bun can
4
+ //! additionally `dlopen` it via `bun:ffi` and call these functions directly.
5
+ //!
6
+ //! The stateless `&[u8] -> scalar` / `&[u8] -> write-into-buffer` hot functions
7
+ //! (where the N-API crossing dominates the sub-µs cost) are the bulk of the
8
+ //! surface. The stateful ingress pipeline is ALSO exposed via
9
+ //! `castrum_ingress_handle_packed`, which takes the opaque inner handle from
10
+ //! `Ingress.ingressInnerPtr()` (valid only while the instance is alive — see
11
+ //! the function's safety note). Buffer-taking fns use raw `(ptr, len)` pairs;
12
+ //! output-buffer fns use `(out, out_cap)` and return bytes written (0 = error /
13
+ //! too small).
14
+ //!
15
+ //! # Safety
16
+ //! Every function is `unsafe` in the Rust sense (raw pointers) but keeps the
17
+ //! read/write within the caller-declared lengths. Callers (bun:ffi bindings in
18
+ //! `src/native/ffi.ts`) must pass pointers into live buffers of at least the
19
+ //! declared length, and for `castrum_ingress_handle_packed` a live handle from a
20
+ //! live `Ingress` instance.
21
+ //!
22
+ //! # Layout
23
+ //! Split by domain so the ~80 `castrum_*` symbols stay navigable. The symbol
24
+ //! registry contract (the dlopen map in `src/native/ffi.ts` + the source-level
25
+ //! parity guard in `test/unit/features/ffi-symbol-parity.test.ts`) is agnostic
26
+ //! to module layout — symbol NAMES never move, only the files they live in:
27
+ //! - `util.rs` — shared helpers (`panic_guard`, `hmac_key_cached`, `aead_alg`,
28
+ //! `cstring_return`/`CSTR_BUF`, `write_rate_check`)
29
+ //! - `hashing.rs` — crc32 / fnv1a / xxh3, json/utf8 validity, hex/url codecs
30
+ //! - `validators.rs` — email / UUID / IPv4 / IPv6 (`validator_c_abi!`)
31
+ //! - `crypto.rs` — hmac, cookie sign/verify, csrf, password hash/verify, pbkdf2,
32
+ //! aead, base64, random token
33
+ //! - `jwt.rs` — jwt signer/verify + ed25519 / EdDSA JWT
34
+ //! - `http.rs` — etag, conditional, http-date, media-type, accept, mime, url
35
+ //! resolve/encode, http/query/cookie/form/multipart parse
36
+ //! - `payload.rs` — ws accept key + frames, gzip/brotli, sse
37
+ //! - `json.rs` — json patch, packed json token stream, schema validator
38
+ //! - `rate_limit.rs` — rate limiter checks
39
+ //! - `metrics.rs` — metrics registry (`castrum_metrics_*`, caller-owned handles)
40
+ //! - `ingress.rs` — ingress pipeline + layout constants
41
+ //! - `route.rs` — per-route native stack (`castrum_route_*`)
42
+ //! - `tests.rs` — C-ABI unit tests
43
+
44
+ mod crypto;
45
+ mod hashing;
46
+ mod http;
47
+ mod ingress;
48
+ mod json;
49
+ mod jwt;
50
+ mod metrics;
51
+ mod payload;
52
+ mod probe;
53
+ mod rate_limit;
54
+ mod route;
55
+ mod util;
56
+ mod validators;
57
+
58
+ #[cfg(test)]
59
+ mod tests;
60
+
61
+ // Re-export every symbol at the `ffi` module root so the shared `tests.rs`
62
+ // (`use super::*`) keeps working unchanged. The `#[no_mangle]` link symbols are
63
+ // independent of Rust visibility, so nothing in non-test code needs them.
64
+ #[cfg(test)]
65
+ pub(crate) use self::{
66
+ crypto::*, hashing::*, http::*, ingress::*, json::*, jwt::*, metrics::*, payload::*,
67
+ rate_limit::*, route::*, util::*, validators::*,
68
+ };