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,275 @@
1
+ // rust/ffi/payload.rs — payload C-ABI exports.
2
+ //
3
+ // WebSocket accept-key + frame encode/decode, gzip/brotli compress-decompress
4
+ // (via the shared `compress_to_out!` macro — see the needed-size convention),
5
+ // and SSE event encoding.
6
+
7
+ use std::slice;
8
+
9
+ use super::util::{cstring_return, panic_guard};
10
+
11
+ /// RFC 6455 Sec-WebSocket-Accept (28 bytes) returned as a null-terminated C
12
+ /// string into the per-thread reused buffer (`cstring` return).
13
+ ///
14
+ /// `key` is a `bun:ffi` `cstring` ARG — the engine transcodes the JS base64 key
15
+ /// in-engine (JS does zero encode; the callee borrows via `CStr::from_ptr`).
16
+ /// The pooled `castrum_ws_accept_key_into` keeps the `(ptr,len)` byte form.
17
+ ///
18
+ /// # Safety
19
+ /// `key` must be a valid NUL-terminated C string.
20
+ #[no_mangle]
21
+ pub unsafe extern "C" fn castrum_ws_accept_key(
22
+ key: *const std::os::raw::c_char,
23
+ ) -> *const std::os::raw::c_char {
24
+ if key.is_null() {
25
+ return std::ptr::null();
26
+ }
27
+ cstring_return(28, |out| {
28
+ crate::payload::websocket::ws_accept_key_into(std::ffi::CStr::from_ptr(key).to_bytes(), out)
29
+ .ok()
30
+ })
31
+ }
32
+
33
+ /// RFC 6455 Sec-WebSocket-Accept written directly into a caller buffer — the
34
+ /// pooled sibling of `castrum_ws_accept_key` (no cstring round-trip). Writes 28
35
+ /// bytes; returns bytes written, the exact required size when `out_cap` is too
36
+ /// small, or 0 on a malformed key.
37
+ ///
38
+ /// # Safety
39
+ /// `key` must be valid for reads of `klen` bytes; `out` for writes up to `out_cap`.
40
+ #[no_mangle]
41
+ pub unsafe extern "C" fn castrum_ws_accept_key_into(
42
+ key: *const u8,
43
+ klen: usize,
44
+ out: *mut u8,
45
+ out_cap: usize,
46
+ ) -> usize {
47
+ if key.is_null() || out.is_null() {
48
+ return 0;
49
+ }
50
+ if out_cap < 28 {
51
+ return 28;
52
+ }
53
+ match crate::payload::websocket::ws_accept_key_into(
54
+ slice::from_raw_parts(key, klen),
55
+ slice::from_raw_parts_mut(out, 28),
56
+ ) {
57
+ Ok(28) => 28,
58
+ _ => 0,
59
+ }
60
+ }
61
+
62
+ /// WebSocket frame encode into `out` (opcode 1/2/8/9/10, `mask`/`fin` flags).
63
+ ///
64
+ /// # Safety
65
+ /// `payload` must be valid for reads of `plen` bytes; `out` for `out_cap`.
66
+ #[no_mangle]
67
+ pub unsafe extern "C" fn castrum_ws_frame_encode(
68
+ opcode: u8,
69
+ payload: *const u8,
70
+ plen: usize,
71
+ mask: u8,
72
+ fin: u8,
73
+ out: *mut u8,
74
+ out_cap: usize,
75
+ ) -> usize {
76
+ if payload.is_null() || out.is_null() {
77
+ return 0;
78
+ }
79
+ crate::payload::ws_frames::encode_frame_into(
80
+ opcode,
81
+ slice::from_raw_parts(payload, plen),
82
+ mask != 0,
83
+ fin != 0,
84
+ slice::from_raw_parts_mut(out, out_cap),
85
+ )
86
+ .unwrap_or(0)
87
+ }
88
+
89
+ /// Decode a WebSocket frame into a packed `[u8 flags][u8 opcode][u32 payload_len]
90
+ /// [payload]` layout (flags bit0 = FIN). Returns bytes written, 0 on malformed
91
+ /// input. JS decodes the small header into the `WsFrame` shape.
92
+ ///
93
+ /// # Safety
94
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
95
+ #[no_mangle]
96
+ pub unsafe extern "C" fn castrum_ws_frame_decode_packed(
97
+ data: *const u8,
98
+ len: usize,
99
+ out: *mut u8,
100
+ out_cap: usize,
101
+ ) -> usize {
102
+ if data.is_null() || out.is_null() {
103
+ return 0;
104
+ }
105
+ let Some(frame) = crate::payload::ws_frames::decode_frame(slice::from_raw_parts(data, len))
106
+ else {
107
+ return 0;
108
+ };
109
+ let Some(need) = 6usize.checked_add(frame.payload.len()) else {
110
+ return 0;
111
+ };
112
+ if need > out_cap {
113
+ return 0;
114
+ }
115
+ let o = slice::from_raw_parts_mut(out, need);
116
+ o[0] = u8::from(frame.fin);
117
+ o[1] = frame.opcode;
118
+ o[2..6].copy_from_slice(&(frame.payload.len() as u32).to_le_bytes());
119
+ o[6..].copy_from_slice(&frame.payload);
120
+ need
121
+ }
122
+
123
+ // ── Compression (gzip / brotli) ──────────────────────────────────
124
+
125
+ macro_rules! compress_to_out {
126
+ ($name:ident, $core:path, $extra:ty) => {
127
+ #[doc = concat!("Run `", stringify!($core), "` and write the result into `out`.")]
128
+ ///
129
+ /// # Convention
130
+ /// Returns the exact total bytes the result requires (the written count
131
+ /// on success, or the needed size when `out_cap` is too small). The JS
132
+ /// wrapper compares against `out_cap` to distinguish success from
133
+ /// too-small, so a miss allocates EXACTLY once and retries — it never
134
+ /// re-runs the whole (de)compression in a grow loop. `0` is reserved
135
+ /// for a REAL error (invalid stream / decompression-cap exceeded).
136
+ ///
137
+ /// # Safety
138
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
139
+ #[no_mangle]
140
+ pub unsafe extern "C" fn $name(
141
+ data: *const u8,
142
+ len: usize,
143
+ extra: $extra,
144
+ out: *mut u8,
145
+ out_cap: usize,
146
+ ) -> usize {
147
+ if data.is_null() || out.is_null() {
148
+ return 0;
149
+ }
150
+ let input = slice::from_raw_parts(data, len);
151
+ let output = slice::from_raw_parts_mut(out, out_cap);
152
+ panic_guard(
153
+ || match $core(input, extra, output) {
154
+ Ok(w) => w,
155
+ // Needed-size convention: report the EXACT required size so the
156
+ // JS wrapper allocates once and retries (never a re-run loop).
157
+ Err(crate::payload::compress::StreamError::TooSmall { needed }) => needed,
158
+ // Real error (invalid stream / decompression cap exceeded) → 0.
159
+ Err(_) => 0,
160
+ },
161
+ 0,
162
+ )
163
+ }
164
+ };
165
+ }
166
+
167
+ compress_to_out!(
168
+ castrum_gzip_compress,
169
+ crate::payload::compress::gzip_compress_into,
170
+ u32
171
+ );
172
+ compress_to_out!(
173
+ castrum_gzip_decompress,
174
+ crate::payload::compress::gzip_decompress_into,
175
+ usize
176
+ );
177
+ compress_to_out!(
178
+ castrum_brotli_compress,
179
+ crate::payload::compress::brotli_compress_into,
180
+ u32
181
+ );
182
+ compress_to_out!(
183
+ castrum_brotli_decompress,
184
+ crate::payload::compress::brotli_decompress_into,
185
+ usize
186
+ );
187
+
188
+ /// Read the original (uncompressed) size from a gzip stream's ISIZE trailer
189
+ /// (last 4 bytes, little-endian, original size mod 2^32). Returns the EXACT
190
+ /// output size for a single-member standard gzip stream, or 0 when the input
191
+ /// isn't a usable standard gzip stream (too short / not gzip magic). Lets the
192
+ /// JS wrapper pre-size the decompress buffer exactly so the happy path is a
193
+ /// single pass (no grow-retry re-run).
194
+ ///
195
+ /// # Safety
196
+ /// `data` must be valid for reads of `len` bytes.
197
+ #[no_mangle]
198
+ pub unsafe extern "C" fn castrum_gzip_isize(data: *const u8, len: usize) -> u32 {
199
+ if data.is_null() || len < 18 {
200
+ return 0;
201
+ }
202
+ let s = slice::from_raw_parts(data, len);
203
+ if s[0] != 0x1f || s[1] != 0x8b {
204
+ return 0;
205
+ }
206
+ u32::from_le_bytes([s[len - 4], s[len - 3], s[len - 2], s[len - 1]])
207
+ }
208
+
209
+ /// Encode one SSE event into `out`. Optional fields use `flags` bits
210
+ /// (1 = event present, 2 = id present, 4 = retry present) so a present-but-empty
211
+ /// string is distinct from an absent one, matching the napi `Option<String>`.
212
+ /// Returns bytes written, or 0 on a too-small buffer / invalid UTF-8 in
213
+ /// event/id (JS sizes `data_len + 64` to guarantee success). FFI sibling of the
214
+ /// napi `sse_encode_event` — the wrapper decodes nothing (output is raw SSE
215
+ /// bytes), so this just removes the napi crossing.
216
+ ///
217
+ /// # Safety
218
+ /// `event`/`id` must be valid for reads of `event_len`/`id_len` bytes when their
219
+ /// flag is set; `data` valid for `data_len` reads; `out` for `out_cap` writes.
220
+ #[no_mangle]
221
+ pub unsafe extern "C" fn castrum_sse_encode_into(
222
+ event: *const u8,
223
+ event_len: usize,
224
+ data: *const u8,
225
+ data_len: usize,
226
+ id: *const u8,
227
+ id_len: usize,
228
+ flags: u8,
229
+ retry: u32,
230
+ out: *mut u8,
231
+ out_cap: usize,
232
+ ) -> usize {
233
+ if data.is_null() || out.is_null() {
234
+ return 0;
235
+ }
236
+ // A PRESENT but empty event/id is valid (emits the line) — only a null
237
+ // pointer with its flag set is malformed (would be UB to slice).
238
+ if (flags & 1 != 0 && event.is_null()) || (flags & 2 != 0 && id.is_null()) {
239
+ return 0;
240
+ }
241
+ panic_guard(
242
+ || -> Option<usize> {
243
+ let data_bytes = slice::from_raw_parts(data, data_len);
244
+ let output = slice::from_raw_parts_mut(out, out_cap);
245
+ let event_opt = if flags & 1 != 0 {
246
+ Some(std::str::from_utf8(slice::from_raw_parts(event, event_len)).ok()?)
247
+ } else {
248
+ None
249
+ };
250
+ let id_opt = if flags & 2 != 0 {
251
+ Some(std::str::from_utf8(slice::from_raw_parts(id, id_len)).ok()?)
252
+ } else {
253
+ None
254
+ };
255
+ let retry_opt = if flags & 4 != 0 {
256
+ Some(u64::from(retry))
257
+ } else {
258
+ None
259
+ };
260
+ match crate::payload::sse::encode_event_into_slice(
261
+ event_opt, data_bytes, id_opt, retry_opt, output,
262
+ ) {
263
+ Ok(w) => Some(w),
264
+ // Too-small buffer: return the exact required size so the JS
265
+ // wrapper allocates ONCE and retries (needed-size convention);
266
+ // 0 stays a real error (invalid UTF-8 in event/id above).
267
+ Err(_) => Some(crate::payload::sse::encode_event_size(
268
+ event_opt, data_bytes, id_opt, retry_opt,
269
+ )),
270
+ }
271
+ },
272
+ None,
273
+ )
274
+ .unwrap_or(0)
275
+ }
@@ -0,0 +1,60 @@
1
+ // rust/ffi/probe.rs — diagnostic C-ABI probes (bench-only, NOT in the ffi.ts surface).
2
+ //
3
+ // Used ONLY by bench/ffi-margin.ts to isolate the fixed per-call FFI cost into
4
+ // its components: the bare trampoline (noop), scalar-arg/return conversion
5
+ // (echo_usize, which can be bound with either `usize` or `u64_fast` to measure
6
+ // BigInt boxing in isolation), TypedArray-view→pointer resolution
7
+ // (echo_view), and `cstring`-ARG transcoding (echo_cstr — the engine encodes
8
+ // the JS string to a call-scoped NUL-terminated buffer; the callee borrows it
9
+ // via `CStr::from_ptr`, never dereferencing beyond NUL). They are trivial,
10
+ // non-fallible, allocate nothing, and are NOT part of the shipped
11
+ // `src/native/ffi.ts` dlopen map, so the bind-time self-test does not cover
12
+ // them. The `ffi_probe_*` prefix (NOT `castrum_*`) keeps them out of the
13
+ // shipped-symbol namespace so the source-level symbol-parity guard
14
+ // (test/unit/features/ffi-symbol-parity.test.ts) stays meaningful.
15
+
16
+ use std::slice;
17
+
18
+ /// Bare C-ABI trampoline floor: does nothing, returns nothing.
19
+ #[no_mangle]
20
+ pub extern "C" fn ffi_probe_noop() {}
21
+
22
+ /// Scalar pass-through: returns `v` unchanged. Measures scalar-arg + return
23
+ /// conversion only (bind with `usize` vs `u64_fast` to isolate BigInt boxing).
24
+ #[no_mangle]
25
+ pub extern "C" fn ffi_probe_echo_usize(v: usize) -> usize {
26
+ v
27
+ }
28
+
29
+ /// View pass-through: returns the byte length of the `(ptr, len)` pair.
30
+ /// Measures TypedArray-view→pointer resolution + (ptr,len) arg conversion.
31
+ ///
32
+ /// # Safety
33
+ /// `data` must be valid for reads of `len` bytes (the pointer is only used to
34
+ /// form a slice whose length we return — never dereferenced).
35
+ #[no_mangle]
36
+ pub unsafe extern "C" fn ffi_probe_echo_view(data: *const u8, len: usize) -> usize {
37
+ if data.is_null() && len != 0 {
38
+ return 0;
39
+ }
40
+ let _ = slice::from_raw_parts(data, len);
41
+ len
42
+ }
43
+
44
+ /// `cstring`-ARG pass-through: returns the byte length of the NUL-terminated
45
+ /// C string the engine produced by transcoding a JS string arg. Measures the
46
+ /// engine-side JS-string→UTF-8 transcode + call-scoped buffer + callee
47
+ /// `CStr::from_ptr` borrow — the cost a `'cstring'` input arg adds compared
48
+ /// with a JS-side `encoder.encode` + `(ptr,len)` pair. The pointer is only
49
+ /// used to form a slice whose length we return — never dereferenced beyond
50
+ /// NUL.
51
+ ///
52
+ /// # Safety
53
+ /// `data` must be a valid NUL-terminated C string for reads up to its terminator.
54
+ #[no_mangle]
55
+ pub unsafe extern "C" fn ffi_probe_echo_cstr(data: *const std::os::raw::c_char) -> usize {
56
+ if data.is_null() {
57
+ return 0;
58
+ }
59
+ std::ffi::CStr::from_ptr(data).to_bytes().len()
60
+ }
@@ -0,0 +1,77 @@
1
+ // rust/ffi/rate_limit.rs — rate-limiter C-ABI exports.
2
+ //
3
+ // Both entries check a limit via the opaque `RateLimiter` handle; one hashes a
4
+ // `cstring` key in-engine, the other takes a pre-hashed i64 key. The packed
5
+ // verdict is written by the shared `write_rate_check` helper in `util.rs`.
6
+
7
+ use super::util::{panic_guard, write_rate_check};
8
+
9
+ /// RateLimiter: check a rate limit for a STRING key (hashed internally) at
10
+ /// `now_ms` via its opaque inner handle. Writes packed `[u8 allowed][u32
11
+ /// remaining LE][i64 reset_ms LE]` (13 bytes); needed-size convention; 0 =
12
+ /// null handle (real error).
13
+ ///
14
+ /// `key` is a `bun:ffi` `cstring` ARG — the engine transcodes the JS rate-limit
15
+ /// key in-engine (JS does zero encode; the callee borrows via `CStr::from_ptr`).
16
+ /// Intended keys are IPs / route identifiers (text, no NUL); a key containing a
17
+ /// NUL would be truncated before hashing.
18
+ ///
19
+ /// # Safety
20
+ /// `inner` must be a valid `RateLimiter` pointer from `inner_ptr()`, alive for
21
+ /// the call; `key` must be a valid NUL-terminated C string; `out` for writes up
22
+ /// to `out_cap`.
23
+ #[no_mangle]
24
+ pub unsafe extern "C" fn castrum_rate_limiter_check(
25
+ inner: usize,
26
+ key: *const std::os::raw::c_char,
27
+ now_ms: i64,
28
+ out: *mut u8,
29
+ out_cap: usize,
30
+ ) -> usize {
31
+ if inner == 0 || key.is_null() || out.is_null() {
32
+ return 0;
33
+ }
34
+ let k = std::ffi::CStr::from_ptr(key).to_bytes();
35
+ let hashed = crate::crypto::hashing::fast_hash_bytes(k);
36
+ let (allowed, remaining, reset_ms) = panic_guard(
37
+ || unsafe {
38
+ crate::ingress::rate_limit::rate_limiter_check_core(
39
+ inner as *const crate::ingress::rate_limit::RateLimiter,
40
+ hashed,
41
+ now_ms as u64,
42
+ )
43
+ },
44
+ (false, 0, 0),
45
+ );
46
+ write_rate_check(allowed, remaining, reset_ms, out, out_cap)
47
+ }
48
+
49
+ /// RateLimiter: check a rate limit for a PRE-HASHED i64 key at `now_ms` via its
50
+ /// opaque inner handle (packed `[u8 allowed][u32 remaining LE][i64 reset_ms LE]`).
51
+ ///
52
+ /// # Safety
53
+ /// `inner` must be a valid `RateLimiter` pointer from `inner_ptr()`, alive for
54
+ /// the call; `out` for writes up to `out_cap`.
55
+ #[no_mangle]
56
+ pub unsafe extern "C" fn castrum_rate_limiter_check_key(
57
+ inner: usize,
58
+ key: i64,
59
+ now_ms: i64,
60
+ out: *mut u8,
61
+ out_cap: usize,
62
+ ) -> usize {
63
+ if inner == 0 || out.is_null() {
64
+ return 0;
65
+ }
66
+ let (allowed, remaining, reset_ms) = panic_guard(
67
+ || unsafe {
68
+ crate::ingress::rate_limit::rate_limiter_check_core(
69
+ inner as *const crate::ingress::rate_limit::RateLimiter,
70
+ key as u64,
71
+ now_ms as u64,
72
+ )
73
+ },
74
+ (false, 0, 0),
75
+ );
76
+ write_rate_check(allowed, remaining, reset_ms, out, out_cap)
77
+ }
@@ -0,0 +1,85 @@
1
+ // rust/ffi/route.rs — per-route native stack C-ABI exports (`castrum_route_*`).
2
+ //
3
+ // The live external wire consumed by `@ignex/native` (`createNativeRoute` →
4
+ // `route-wire.ts` v3): a route descriptor compiles ONCE into a pre-baked
5
+ // `NativeRoute` (rust/ingress/native_route.rs), then each request packs a tiny
6
+ // frame and gets a packed verdict in one call. The handle is an owned
7
+ // `Box<NativeRoute>` (opaque u64); `route_run` follows the needed-size
8
+ // convention (`0` = real error, `> out_cap` = exact required size) so the JS
9
+ // wrapper allocates once and retries at most once. All three are `panic_guard`ed
10
+ // — a panic must never unwind through the C ABI.
11
+
12
+ use std::slice;
13
+
14
+ use super::util::panic_guard;
15
+
16
+ /// Compile a route descriptor into an opaque handle (`0` = failure / null
17
+ /// input / panic). The handle is an owned `Box<NativeRoute>`; release it with
18
+ /// {@link castrum_route_destroy}.
19
+ ///
20
+ /// # Safety
21
+ /// `desc` must be valid for reads of `desc_len` bytes.
22
+ #[no_mangle]
23
+ pub unsafe extern "C" fn castrum_route_compile(desc: *const u8, desc_len: usize) -> u64 {
24
+ if desc.is_null() {
25
+ return 0;
26
+ }
27
+ let bytes = slice::from_raw_parts(desc, desc_len);
28
+ panic_guard(
29
+ || match crate::ingress::NativeRoute::compile(bytes) {
30
+ Ok(route) => Box::into_raw(Box::new(route)) as u64,
31
+ Err(_) => 0,
32
+ },
33
+ 0,
34
+ )
35
+ }
36
+
37
+ /// Run a compiled route on one request frame, writing the packed verdict into
38
+ /// `out`. Returns bytes written (`0` = real error / malformed frame / panic;
39
+ /// `> out_cap` = the EXACT required size — allocate once and retry).
40
+ ///
41
+ /// # Safety
42
+ /// `handle` must be a live handle from {@link castrum_route_compile} (not yet
43
+ /// destroyed); `frame` valid for `frame_len` reads; `out` valid for `out_cap`
44
+ /// writes. The route is immutable (`&self`, no interior mutability), so
45
+ /// concurrent calls from multiple worker threads are safe.
46
+ #[no_mangle]
47
+ pub unsafe extern "C" fn castrum_route_run(
48
+ handle: u64,
49
+ frame: *const u8,
50
+ frame_len: usize,
51
+ out: *mut u8,
52
+ out_cap: usize,
53
+ ) -> usize {
54
+ if handle == 0 || frame.is_null() || out.is_null() {
55
+ return 0;
56
+ }
57
+ let route: &crate::ingress::NativeRoute = &*(handle as *const crate::ingress::NativeRoute);
58
+ let frame_slice = slice::from_raw_parts(frame, frame_len);
59
+ let out_slice = slice::from_raw_parts_mut(out, out_cap);
60
+ // Aliasing insurance (mirrors castrum_ingress_handle_packed): a pooled
61
+ // frame that overlaps `out` must be copied before the shared &/&mut borrow
62
+ // pair (aliased &/&mut is instant UB). The JS wrapper uses distinct pooled
63
+ // buffers, so this is a rare defensive copy.
64
+ let owned_frame;
65
+ let frame_ref: &[u8] = if crate::util::slices_overlap(frame_slice, out_slice) {
66
+ owned_frame = frame_slice.to_vec();
67
+ &owned_frame
68
+ } else {
69
+ frame_slice
70
+ };
71
+ panic_guard(|| route.run(frame_ref, out_slice).unwrap_or(0), 0)
72
+ }
73
+
74
+ /// Destroy a compiled route handle (frees the `Box<NativeRoute>`). A null
75
+ /// handle is a no-op; double-destroy is UB and must not be called.
76
+ ///
77
+ /// # Safety
78
+ /// `handle` must come from {@link castrum_route_compile} and be destroyed at
79
+ /// most once.
80
+ #[no_mangle]
81
+ pub unsafe extern "C" fn castrum_route_destroy(handle: u64) {
82
+ if handle != 0 {
83
+ drop(Box::from_raw(handle as *mut crate::ingress::NativeRoute));
84
+ }
85
+ }