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,303 @@
1
+ // rust/ffi/ingress.rs — ingress pipeline C-ABI exports.
2
+ //
3
+ // The opaque-handle fast path (`castrum_ingress_handle_packed` /
4
+ // `castrum_ingress_handle_components`) runs the ENTIRE ingress pipeline in one
5
+ // call, plus the binary-layout constants projection (`castrum_ingress_layout`).
6
+ // All three follow the aliasing-insurance + `panic_guard` conventions of the
7
+ // napi entry points.
8
+
9
+ use std::slice;
10
+
11
+ use super::util::panic_guard;
12
+
13
+ /// Run the ingress pipeline on a packed request frame. `inner` is the opaque
14
+ /// pointer (as an integer) from `Ingress.ingressInnerPtr()` — valid ONLY while
15
+ /// the `Ingress` instance is alive (see the method's safety note). This is the
16
+ /// hot path: it runs the ENTIRE pipeline (parsing, trust/proxy, schema, CORS,
17
+ /// rate limit, response) in one C-ABI call, cutting the N-API crossing +
18
+ /// marshaling that a per-request `handleRequestPacked` call would pay.
19
+ ///
20
+ /// Returns bytes written into `out` (0 = error / too-small buffer).
21
+ ///
22
+ /// # Safety
23
+ /// `inner` must be the live `IngressInner` pointer from a live `Ingress`
24
+ /// instance; `input` valid for `input_len` reads; `body` valid for `body_len`
25
+ /// reads (null/0 = no body); `out` valid for `out_cap` writes.
26
+ #[no_mangle]
27
+ pub unsafe extern "C" fn castrum_ingress_handle_packed(
28
+ inner: usize,
29
+ input: *const u8,
30
+ input_len: usize,
31
+ body: *const u8,
32
+ body_len: usize,
33
+ out: *mut u8,
34
+ out_cap: usize,
35
+ ) -> usize {
36
+ let inner = inner as *const crate::ingress::IngressInner;
37
+ if inner.is_null() || input.is_null() || out.is_null() {
38
+ return 0;
39
+ }
40
+ let inp = slice::from_raw_parts(input, input_len);
41
+ let out_slice = slice::from_raw_parts_mut(out, out_cap);
42
+ let body_slice: &[u8] = if body.is_null() || body_len == 0 {
43
+ &[]
44
+ } else {
45
+ slice::from_raw_parts(body, body_len)
46
+ };
47
+ // Mirror the napi entry (`util::run_packed_into`): when the input OR the
48
+ // body aliases `out`, copy it first so the shared `&[u8]` and the `&mut
49
+ // [u8]` output borrow never alias (aliased &/&mut is instant UB).
50
+ let owned_input;
51
+ let input_ref: &[u8] = if crate::util::slices_overlap(inp, out_slice) {
52
+ owned_input = inp.to_vec();
53
+ &owned_input
54
+ } else {
55
+ inp
56
+ };
57
+ let owned_body;
58
+ let body_ref: &[u8] = if crate::util::slices_overlap(body_slice, out_slice) {
59
+ owned_body = body_slice.to_vec();
60
+ &owned_body
61
+ } else {
62
+ body_slice
63
+ };
64
+ // A panic must not unwind through the C ABI (that would kill the whole Bun
65
+ // process — the `11-concurrent-burst` crash). Catch it and report 0 (error)
66
+ // so the JS wrapper turns it into a 500, matching the napi path's
67
+ // catch_unwind. `handle_packed` borrows `&self` with no interior mutability
68
+ // on the hot path, so the instance stays consistent after an unwind.
69
+ panic_guard(
70
+ || {
71
+ (&*inner)
72
+ .handle_packed(input_ref, body_ref, out_slice)
73
+ .unwrap_or(0)
74
+ },
75
+ 0,
76
+ )
77
+ }
78
+
79
+ /// Run the ingress pipeline from raw request components — the `bun:ffi`
80
+ /// cstring sibling of `castrum_ingress_handle_packed`.
81
+ ///
82
+ /// `url` / `ip` are NUL-terminated UTF-8 cstrings (`bun:ffi` `cstring` ARGs —
83
+ /// the engine transcodes the JS strings to call-scoped buffers in-engine), so
84
+ /// the JS side skips the frame assembly + `Buffer.write` UTF-8 encode for the
85
+ /// URL/IP. `rid` / `headers` / `body` are `(ptr,len)` byte slices (headers =
86
+ /// the packed `[u16 count] {pairs}` block); `out` receives the packed decision.
87
+ /// Same wire format + semantics as the packed entry — the shared core is
88
+ /// `IngressInner::handle_components`.
89
+ ///
90
+ /// Returns bytes written into `out` (0 = error / too-small buffer).
91
+ ///
92
+ /// # Safety
93
+ /// `inner` must be the live `IngressInner` pointer from a live `Ingress`
94
+ /// instance; `url`/`ip` must be valid NUL-terminated buffers (the engine
95
+ /// guarantees this for `cstring` args); `rid`/`headers`/`body` valid for their
96
+ /// declared lengths (null/0 = no body); `out` valid for `out_cap` writes.
97
+ #[no_mangle]
98
+ pub unsafe extern "C" fn castrum_ingress_handle_components(
99
+ inner: usize,
100
+ method_kind: u8,
101
+ url: *const std::os::raw::c_char,
102
+ ip: *const std::os::raw::c_char,
103
+ rid: *const u8,
104
+ rid_len: usize,
105
+ headers: *const u8,
106
+ headers_len: usize,
107
+ body: *const u8,
108
+ body_len: usize,
109
+ out: *mut u8,
110
+ out_cap: usize,
111
+ ) -> usize {
112
+ let inner = inner as *const crate::ingress::IngressInner;
113
+ if inner.is_null()
114
+ || url.is_null()
115
+ || ip.is_null()
116
+ || rid.is_null()
117
+ || headers.is_null()
118
+ || out.is_null()
119
+ {
120
+ return 0;
121
+ }
122
+ if out_cap < crate::ingress::output::OUT_DATA_START {
123
+ return 0;
124
+ }
125
+ let url_bytes = std::ffi::CStr::from_ptr(url).to_bytes();
126
+ let ip_bytes = std::ffi::CStr::from_ptr(ip).to_bytes();
127
+ let rid_slice = slice::from_raw_parts(rid, rid_len);
128
+ let headers_slice = slice::from_raw_parts(headers, headers_len);
129
+ let body_slice: &[u8] = if body.is_null() || body_len == 0 {
130
+ &[]
131
+ } else {
132
+ slice::from_raw_parts(body, body_len)
133
+ };
134
+ let out_slice = slice::from_raw_parts_mut(out, out_cap);
135
+ // Aliasing insurance (mirrors the packed entry): if any input aliases the
136
+ // output, copy it first so the shared `&[u8]` and the `&mut [u8]` output
137
+ // borrow never alias (aliased &/&mut is instant UB). The `url`/`ip`
138
+ // cstrings are engine-scoped buffers and can never alias `out`.
139
+ let owned_rid;
140
+ let rid_ref: &[u8] = if crate::util::slices_overlap(rid_slice, out_slice) {
141
+ owned_rid = rid_slice.to_vec();
142
+ &owned_rid
143
+ } else {
144
+ rid_slice
145
+ };
146
+ let owned_headers;
147
+ let headers_ref: &[u8] = if crate::util::slices_overlap(headers_slice, out_slice) {
148
+ owned_headers = headers_slice.to_vec();
149
+ &owned_headers
150
+ } else {
151
+ headers_slice
152
+ };
153
+ let owned_body;
154
+ let body_ref: &[u8] = if crate::util::slices_overlap(body_slice, out_slice) {
155
+ owned_body = body_slice.to_vec();
156
+ &owned_body
157
+ } else {
158
+ body_slice
159
+ };
160
+ let mk = crate::http::method::MethodKind::from_u8(method_kind);
161
+ // A panic must not unwind through the C ABI — catch it and report 0 (the
162
+ // JS wrapper turns it into a 500), matching the packed entry.
163
+ panic_guard(
164
+ || {
165
+ (&*inner)
166
+ .handle_components(
167
+ mk,
168
+ url_bytes,
169
+ ip_bytes,
170
+ rid_ref,
171
+ headers_ref,
172
+ body_ref,
173
+ out_slice,
174
+ )
175
+ .unwrap_or(0)
176
+ },
177
+ 0,
178
+ )
179
+ }
180
+
181
+ // ── Ingress binary-layout constants (C ABI) ──────────────────────
182
+ // The napi projection (`ingress_constants.rs`) exposes these to JS by name;
183
+ // this C-ABI variant lets Bun read the SAME values via `bun:ffi` WITHOUT
184
+ // loading the napi addon, so `import castrum` is FFI-only under Bun. Values
185
+ // are read directly from `output.rs` (the single numeric source). Field ORDER
186
+ // is the wire contract — pinned by `ingress_layout_c_abi_matches_output_source`
187
+ // below and by the bun:ffi bind-time self-test (which falls back to napi if the
188
+ // pinned values don't line up).
189
+
190
+ /// `#[repr(C)]` projection of the ingress layout constants for the FFI path.
191
+ /// `castrum_ingress_layout` memcpys this struct into the caller's buffer; the
192
+ /// JS side reads it as 38 × u32 little-endian at the same slot order.
193
+ #[repr(C)]
194
+ pub struct IngressLayout {
195
+ // Output buffer layout (u32)
196
+ pub out_verdict: u32,
197
+ pub out_error_code: u32,
198
+ pub out_status: u32,
199
+ pub out_flags: u32,
200
+ pub out_rate_limit: u32,
201
+ pub out_rate_remaining: u32,
202
+ pub out_rate_reset: u32,
203
+ pub out_retry_after: u32,
204
+ pub out_cookies_json_len: u32,
205
+ pub out_query_json_len: u32,
206
+ pub out_header_variant: u32,
207
+ pub out_body_json_len: u32,
208
+ pub out_data_start: u32,
209
+ // Flags
210
+ pub flag_has_cookies: u32,
211
+ pub flag_has_query: u32,
212
+ pub flag_body_valid_json: u32,
213
+ pub flag_schema_valid: u32,
214
+ pub flag_cors_allowed: u32,
215
+ pub flag_is_preflight: u32,
216
+ pub flag_rate_limited: u32,
217
+ pub flag_https: u32,
218
+ pub flag_trusted_proxy: u32,
219
+ pub flag_body_truncated: u32,
220
+ // Header variant bits
221
+ pub hv_json: u32,
222
+ pub hv_cors_simple: u32,
223
+ pub hv_cors_preflight: u32,
224
+ pub hv_rate_active: u32,
225
+ pub hv_rate_limited: u32,
226
+ pub hv_count: u32,
227
+ // Error codes
228
+ pub err_none: u32,
229
+ pub err_cors_preflight: u32,
230
+ pub err_rate_limited: u32,
231
+ pub err_body_too_large: u32,
232
+ pub err_invalid_json: u32,
233
+ pub err_schema_validation: u32,
234
+ pub err_bad_request: u32,
235
+ pub err_request_too_large: u32,
236
+ pub err_internal: u32,
237
+ }
238
+
239
+ impl IngressLayout {
240
+ /// Build the layout from `output.rs` (the single numeric source).
241
+ const fn from_output() -> Self {
242
+ use crate::ingress::output as o;
243
+ Self {
244
+ out_verdict: o::OUT_VERDICT as u32,
245
+ out_error_code: o::OUT_ERROR_CODE as u32,
246
+ out_status: o::OUT_STATUS as u32,
247
+ out_flags: o::OUT_FLAGS as u32,
248
+ out_rate_limit: o::OUT_RATE_LIMIT as u32,
249
+ out_rate_remaining: o::OUT_RATE_REMAINING as u32,
250
+ out_rate_reset: o::OUT_RATE_RESET as u32,
251
+ out_retry_after: o::OUT_RETRY_AFTER as u32,
252
+ out_cookies_json_len: o::OUT_COOKIES_JSON_LEN as u32,
253
+ out_query_json_len: o::OUT_QUERY_JSON_LEN as u32,
254
+ out_header_variant: o::OUT_HEADER_VARIANT as u32,
255
+ out_body_json_len: o::OUT_BODY_JSON_LEN as u32,
256
+ out_data_start: o::OUT_DATA_START as u32,
257
+ flag_has_cookies: o::FLAG_HAS_COOKIES,
258
+ flag_has_query: o::FLAG_HAS_QUERY,
259
+ flag_body_valid_json: o::FLAG_BODY_VALID_JSON,
260
+ flag_schema_valid: o::FLAG_SCHEMA_VALID,
261
+ flag_cors_allowed: o::FLAG_CORS_ALLOWED,
262
+ flag_is_preflight: o::FLAG_IS_PREFLIGHT,
263
+ flag_rate_limited: o::FLAG_RATE_LIMITED,
264
+ flag_https: o::FLAG_HTTPS,
265
+ flag_trusted_proxy: o::FLAG_TRUSTED_PROXY,
266
+ flag_body_truncated: o::FLAG_BODY_TRUNCATED,
267
+ hv_json: o::HV_JSON as u32,
268
+ hv_cors_simple: o::HV_CORS_SIMPLE as u32,
269
+ hv_cors_preflight: o::HV_CORS_PREFLIGHT as u32,
270
+ hv_rate_active: o::HV_RATE_ACTIVE as u32,
271
+ hv_rate_limited: o::HV_RATE_LIMITED as u32,
272
+ hv_count: o::HV_COUNT as u32,
273
+ err_none: o::ERR_CODE_NONE as u32,
274
+ err_cors_preflight: o::ERR_CODE_CORS_PREFLIGHT as u32,
275
+ err_rate_limited: o::ERR_CODE_RATE_LIMITED as u32,
276
+ err_body_too_large: o::ERR_CODE_BODY_TOO_LARGE as u32,
277
+ err_invalid_json: o::ERR_CODE_INVALID_JSON as u32,
278
+ err_schema_validation: o::ERR_CODE_SCHEMA_VALIDATION as u32,
279
+ err_bad_request: o::ERR_CODE_BAD_REQUEST as u32,
280
+ err_request_too_large: o::ERR_CODE_REQUEST_TOO_LARGE as u32,
281
+ err_internal: o::ERR_CODE_INTERNAL as u32,
282
+ }
283
+ }
284
+ }
285
+
286
+ /// Write the `IngressLayout` blob (38 × u32 LE) into `out`; returns bytes
287
+ /// written (0 when `out` is too small or null).
288
+ ///
289
+ /// # Safety
290
+ /// `out` must be valid for writes up to `out_cap`.
291
+ #[no_mangle]
292
+ pub unsafe extern "C" fn castrum_ingress_layout(out: *mut u8, out_cap: usize) -> usize {
293
+ if out.is_null() {
294
+ return 0;
295
+ }
296
+ const LAYOUT: IngressLayout = IngressLayout::from_output();
297
+ let n = core::mem::size_of::<IngressLayout>();
298
+ if out_cap < n {
299
+ return 0;
300
+ }
301
+ core::ptr::copy_nonoverlapping(&LAYOUT as *const IngressLayout as *const u8, out, n);
302
+ n
303
+ }