castrum 0.9.3 → 0.9.6

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 (206) hide show
  1. package/Cargo.lock +2242 -0
  2. package/Cargo.toml +155 -0
  3. package/README.md +14 -6
  4. package/build.rs +5 -0
  5. package/castrum.darwin-arm64.node +0 -0
  6. package/castrum.darwin-x64.node +0 -0
  7. package/castrum.linux-arm64-gnu.node +0 -0
  8. package/castrum.linux-arm64-musl.node +0 -0
  9. package/castrum.linux-x64-gnu.node +0 -0
  10. package/castrum.linux-x64-musl.node +0 -0
  11. package/castrum.linux-x64-v3-gnu.node +0 -0
  12. package/castrum.win32-x64-msvc.node +0 -0
  13. package/dist/index.d.ts +1508 -1291
  14. package/dist/index.js +3391 -2069
  15. package/docs/API.md +2 -1
  16. package/docs/ARCHITECTURE.md +3 -2
  17. package/docs/CASE_STUDY.md +5 -3
  18. package/docs/ENVIRONMENT.md +25 -2
  19. package/docs/FFI_BUN_GUIDE.md +242 -14
  20. package/docs/HOW_TO_ADD_AN_OP.md +10 -0
  21. package/docs/REPO_MAP.md +17 -6
  22. package/docs/RND-CONCURRENCY.md +772 -0
  23. package/docs/bun-builtins-decision-matrix.md +4 -1
  24. package/index.ts +24 -25
  25. package/package.json +173 -156
  26. package/rust/crypto/aead.rs +378 -0
  27. package/rust/crypto/argon2.rs +265 -0
  28. package/rust/crypto/base64.rs +420 -0
  29. package/rust/crypto/bcrypt.rs +89 -0
  30. package/rust/crypto/cookie_sign.rs +169 -0
  31. package/rust/crypto/csrf.rs +106 -0
  32. package/rust/crypto/ed25519.rs +215 -0
  33. package/rust/crypto/hashing.rs +140 -0
  34. package/rust/crypto/hmac_sha256.rs +88 -0
  35. package/rust/crypto/jwt/api.rs +286 -0
  36. package/rust/crypto/jwt/mod.rs +22 -0
  37. package/rust/crypto/jwt/tests.rs +312 -0
  38. package/rust/crypto/jwt/token.rs +360 -0
  39. package/rust/crypto/mod.rs +27 -0
  40. package/rust/crypto/pbkdf2.rs +105 -0
  41. package/rust/crypto/random_token.rs +85 -0
  42. package/rust/crypto/session.rs +222 -0
  43. package/rust/ffi/crypto.rs +708 -0
  44. package/rust/ffi/hashing.rs +285 -0
  45. package/rust/ffi/http.rs +752 -0
  46. package/rust/ffi/ingress.rs +303 -0
  47. package/rust/ffi/json.rs +517 -0
  48. package/rust/ffi/jwt.rs +419 -0
  49. package/rust/ffi/metrics.rs +373 -0
  50. package/rust/ffi/mod.rs +69 -0
  51. package/rust/ffi/payload.rs +275 -0
  52. package/rust/ffi/probe.rs +60 -0
  53. package/rust/ffi/rate_limit.rs +77 -0
  54. package/rust/ffi/route.rs +85 -0
  55. package/rust/ffi/task.rs +172 -0
  56. package/rust/ffi/tests.rs +2333 -0
  57. package/rust/ffi/util.rs +152 -0
  58. package/rust/ffi/validators.rs +123 -0
  59. package/rust/http/accept.rs +606 -0
  60. package/rust/http/cookie_parser.rs +140 -0
  61. package/rust/http/etag.rs +289 -0
  62. package/rust/http/form.rs +136 -0
  63. package/rust/http/headers.rs +267 -0
  64. package/rust/http/http_date.rs +286 -0
  65. package/rust/http/http_parser.rs +252 -0
  66. package/rust/http/media_type.rs +355 -0
  67. package/rust/http/method.rs +200 -0
  68. package/rust/http/mime_lookup.rs +130 -0
  69. package/rust/http/mod.rs +32 -0
  70. package/rust/http/multipart.rs +685 -0
  71. package/rust/http/query_parser.rs +211 -0
  72. package/rust/http/url_codec.rs +296 -0
  73. package/rust/http/url_join.rs +324 -0
  74. package/rust/ingress/api.rs +355 -0
  75. package/rust/ingress/cors.rs +403 -0
  76. package/rust/ingress/ingress_constants.rs +209 -0
  77. package/rust/ingress/ip_trust.rs +312 -0
  78. package/rust/ingress/mod.rs +48 -0
  79. package/rust/ingress/native_route.rs +770 -0
  80. package/rust/ingress/options.rs +176 -0
  81. package/rust/ingress/output.rs +406 -0
  82. package/rust/ingress/packed.rs +191 -0
  83. package/rust/ingress/pipeline.rs +534 -0
  84. package/rust/ingress/proxy.rs +138 -0
  85. package/rust/ingress/rate_limit.rs +538 -0
  86. package/rust/ingress/terminal.rs +114 -0
  87. package/rust/ingress/tests.rs +567 -0
  88. package/rust/ingress/time.rs +70 -0
  89. package/rust/json/fast_schema/capture.rs +336 -0
  90. package/rust/json/fast_schema/compile.rs +556 -0
  91. package/rust/json/fast_schema/cursor.rs +368 -0
  92. package/rust/json/fast_schema/email.rs +282 -0
  93. package/rust/json/fast_schema/errors.rs +247 -0
  94. package/rust/json/fast_schema/mod.rs +57 -0
  95. package/rust/json/fast_schema/tests.rs +1143 -0
  96. package/rust/json/fast_schema/types.rs +140 -0
  97. package/rust/json/fast_schema/validate.rs +931 -0
  98. package/rust/json/json_ops.rs +121 -0
  99. package/rust/json/json_schema.rs +837 -0
  100. package/rust/json/json_ser.rs +883 -0
  101. package/rust/json/mod.rs +19 -0
  102. package/rust/json/napi_marshal.rs +318 -0
  103. package/rust/json/patch/api.rs +60 -0
  104. package/rust/json/patch/engine.rs +153 -0
  105. package/rust/json/patch/mod.rs +47 -0
  106. package/rust/json/patch/ops.rs +270 -0
  107. package/rust/json/patch/pointer.rs +49 -0
  108. package/rust/json/patch/tests.rs +499 -0
  109. package/rust/lib.rs +73 -0
  110. package/rust/metrics/api.rs +148 -0
  111. package/rust/metrics/mod.rs +21 -0
  112. package/rust/metrics/registry.rs +961 -0
  113. package/rust/panic_safety.rs +40 -0
  114. package/rust/payload/compress.rs +512 -0
  115. package/rust/payload/mod.rs +13 -0
  116. package/rust/payload/sse.rs +214 -0
  117. package/rust/payload/template.rs +253 -0
  118. package/rust/payload/websocket.rs +69 -0
  119. package/rust/payload/ws_frames.rs +354 -0
  120. package/rust/proptest_suite.rs +139 -0
  121. package/rust/selection.rs +95 -0
  122. package/rust/src/bin/native-bench.rs +495 -0
  123. package/rust/task/completion.rs +119 -0
  124. package/rust/task/mod.rs +65 -0
  125. package/rust/task/ops.rs +503 -0
  126. package/rust/task/runtime.rs +220 -0
  127. package/rust/task/tests.rs +231 -0
  128. package/rust/test_support.rs +71 -0
  129. package/rust/util/batch/api.rs +266 -0
  130. package/rust/util/batch/core.rs +473 -0
  131. package/rust/util/batch/mod.rs +23 -0
  132. package/rust/util/batch/tests.rs +445 -0
  133. package/rust/util/batch_core.rs +184 -0
  134. package/rust/util/bytes.rs +594 -0
  135. package/rust/util/mod.rs +34 -0
  136. package/rust/util/packed.rs +473 -0
  137. package/rust/util/text.rs +130 -0
  138. package/rust/util/threadpool.rs +128 -0
  139. package/rust/util/validation.rs +419 -0
  140. package/rust-toolchain.toml +17 -0
  141. package/scripts/postinstall.mjs +199 -0
  142. package/src/ingress/constants.ts +1 -1
  143. package/src/ingress/context.ts +2 -2
  144. package/src/ingress/decode/baked-result.ts +13 -13
  145. package/src/ingress/decode/fast-result.ts +17 -17
  146. package/src/ingress/errors.ts +7 -7
  147. package/src/ingress/headers/baked-templates.ts +1 -1
  148. package/src/ingress/headers/fast-templates.ts +3 -3
  149. package/src/ingress/health.ts +1 -2
  150. package/src/ingress/index.ts +1 -1
  151. package/src/ingress/native-route.ts +9 -5
  152. package/src/ingress/options.ts +1 -1
  153. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  154. package/src/ingress/packing/route-wire.ts +1 -5
  155. package/src/ingress/response/error-bodies.ts +5 -5
  156. package/src/ingress/response/terminal.ts +1 -1
  157. package/src/ingress/routes/echo.ts +4 -4
  158. package/src/ingress/routes/fallback.ts +2 -2
  159. package/src/ingress/routes/head.ts +2 -2
  160. package/src/ingress/routes/native.ts +2 -1
  161. package/src/ingress/routes/options.ts +1 -1
  162. package/src/ingress/routes/read.ts +2 -2
  163. package/src/ingress/server.ts +10 -6
  164. package/src/ingress/status.ts +7 -7
  165. package/src/integration/batch.ts +1 -1
  166. package/src/integration/index.ts +9 -9
  167. package/src/integration/pipeline.ts +6 -6
  168. package/src/integration/websocket.ts +2 -2
  169. package/src/loader/index.ts +1 -5
  170. package/src/loader/ops.ts +1 -1
  171. package/src/native/ffi/build/codecs.ts +130 -12
  172. package/src/native/ffi/build/compress.ts +22 -10
  173. package/src/native/ffi/build/instances.ts +46 -4
  174. package/src/native/ffi/build/metrics.ts +247 -0
  175. package/src/native/ffi/build/parse.ts +59 -23
  176. package/src/native/ffi/build/task.ts +79 -0
  177. package/src/native/ffi/build/util.ts +32 -1
  178. package/src/native/ffi/build.ts +4 -0
  179. package/src/native/ffi/selftest.ts +12 -3
  180. package/src/native/ffi/types.ts +151 -9
  181. package/src/native/ffi.ts +160 -15
  182. package/src/native/index.ts +1 -1
  183. package/src/native/loader.ts +55 -5
  184. package/src/native/types/instances.ts +34 -0
  185. package/src/native/types.ts +11 -0
  186. package/src/runtime/codec.ts +53 -9
  187. package/src/runtime/types.ts +7 -0
  188. package/src/rust-ffi/batch/build.ts +16 -9
  189. package/src/rust-ffi/index.ts +7 -6
  190. package/src/rust-ffi/options.ts +1 -3
  191. package/src/rust-ffi/proven.ts +3 -3
  192. package/src/rust-ffi/scalar/crypto.ts +1 -1
  193. package/src/rust-ffi/scalar/factories.ts +73 -0
  194. package/src/rust-ffi/scalar/hashing.ts +1 -1
  195. package/src/rust-ffi/scalar/http.ts +71 -27
  196. package/src/rust-ffi/scalar/index.ts +2 -0
  197. package/src/rust-ffi/scalar/interface.ts +20 -0
  198. package/src/rust-ffi/scalar/payload.ts +11 -3
  199. package/src/rust-ffi/scalar/validation.ts +121 -0
  200. package/src/rust-ffi/text.ts +25 -3
  201. package/src/selection.ts +1 -1
  202. package/src/shared/bytes.ts +24 -4
  203. package/src/shared/codec.ts +18 -3
  204. package/src/task/index.ts +24 -0
  205. package/src/task/op.ts +171 -0
  206. package/src/task/runtime.ts +568 -0
@@ -0,0 +1,752 @@
1
+ // rust/ffi/http.rs — HTTP wire-format C-ABI exports.
2
+ //
3
+ // Cache semantics (etag / conditional / http-date), media-type + accept-encoding
4
+ // negotiation, MIME lookup, URL resolve/encode, and the packed parsers
5
+ // (http request / query / cookie / form / multipart).
6
+
7
+ use std::slice;
8
+
9
+ use super::util::{cstring_return, panic_guard};
10
+
11
+ /// crc32-based ETag (10 strong / 12 weak) into `out`; `weak` is a u8 flag.
12
+ ///
13
+ /// # Safety
14
+ /// ETag (`"<8-hex>"` or `W/"<8-hex>"`) returned as a null-terminated C string
15
+ /// into the per-thread reused buffer (`cstring` return — the engine clones it).
16
+ ///
17
+ /// # Safety
18
+ /// `data` must be valid for reads of `len` bytes.
19
+ #[no_mangle]
20
+ pub unsafe extern "C" fn castrum_etag(
21
+ data: *const u8,
22
+ len: usize,
23
+ weak: u8,
24
+ ) -> *const std::os::raw::c_char {
25
+ if data.is_null() {
26
+ return std::ptr::null();
27
+ }
28
+ let crc = crc32fast::hash(slice::from_raw_parts(data, len));
29
+ let needed = if weak != 0 { 12 } else { 10 };
30
+ cstring_return(needed, |out| {
31
+ crate::http::etag::etag_from_crc32_into(crc, weak != 0, out).ok()
32
+ })
33
+ }
34
+
35
+ /// crc32-based ETag written directly into a caller buffer — the pooled sibling
36
+ /// of `castrum_etag` (no cstring round-trip). Writes 10 strong / 12 weak bytes;
37
+ /// returns bytes written, the exact required size when `out_cap` is too small,
38
+ /// or 0 on invalid input.
39
+ ///
40
+ /// # Safety
41
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
42
+ #[no_mangle]
43
+ pub unsafe extern "C" fn castrum_etag_into(
44
+ data: *const u8,
45
+ len: usize,
46
+ weak: u8,
47
+ out: *mut u8,
48
+ out_cap: usize,
49
+ ) -> usize {
50
+ if data.is_null() || out.is_null() {
51
+ return 0;
52
+ }
53
+ let needed = if weak != 0 { 12 } else { 10 };
54
+ if out_cap < needed {
55
+ return needed;
56
+ }
57
+ let crc = crc32fast::hash(slice::from_raw_parts(data, len));
58
+ crate::http::etag::etag_from_crc32_into(crc, weak != 0, slice::from_raw_parts_mut(out, needed))
59
+ .unwrap_or_default()
60
+ }
61
+
62
+ /// Evaluate `ConditionalRequest::is_not_modified` against the precompiled state
63
+ /// via its opaque inner handle (from the napi `inner_ptr()`). `flags` bit0 =
64
+ /// If-None-Match present, bit1 = If-Modified-Since present (a present-but-empty
65
+ /// header is distinct from absent, matching the napi `Option` semantics).
66
+ /// Returns 1 → 304. A null handle (0) → 0, so a dropped instance can never
67
+ /// dereference freed state.
68
+ ///
69
+ /// `inm`/`ims` cross as `(ptr, len)` byte slices — the same convention as every
70
+ /// other instance op (`castrum_media_type_matcher_matches`, `castrum_jwt_signer_*`,
71
+ /// `castrum_template_render`, `castrum_schema_validator_validate`) — so the JS
72
+ /// side passes arbitrary header bytes with no NUL-termination requirement.
73
+ /// Presence is gated by `flags`, so the pointers are ignored when the
74
+ /// corresponding bit is 0.
75
+ ///
76
+ /// # Safety
77
+ /// `inner` must be a valid `ConditionalRequest` pointer obtained from
78
+ /// `inner_ptr()` and must stay alive for the call (the JS wrapper holds the
79
+ /// napi instance). `inm`/`ims` must be valid for `inm_len`/`ims_len` bytes when
80
+ /// the corresponding `flags` bit is set.
81
+ #[no_mangle]
82
+ pub unsafe extern "C" fn castrum_conditional_is_not_modified(
83
+ inner: usize,
84
+ inm: *const u8,
85
+ inm_len: usize,
86
+ ims: *const u8,
87
+ ims_len: usize,
88
+ flags: u8,
89
+ ) -> u8 {
90
+ if inner == 0 {
91
+ return 0;
92
+ }
93
+ let inm_opt = if flags & 1 != 0 && !inm.is_null() {
94
+ Some(std::slice::from_raw_parts(inm, inm_len))
95
+ } else {
96
+ None
97
+ };
98
+ let ims_opt = if flags & 2 != 0 && !ims.is_null() {
99
+ Some(std::slice::from_raw_parts(ims, ims_len))
100
+ } else {
101
+ None
102
+ };
103
+ panic_guard(
104
+ || {
105
+ u8::from(unsafe {
106
+ crate::http::etag::conditional_is_not_modified(
107
+ inner as *const crate::http::etag::ConditionalRequest,
108
+ inm_opt,
109
+ ims_opt,
110
+ )
111
+ })
112
+ },
113
+ 0,
114
+ )
115
+ }
116
+
117
+ /// MediaTypeMatcher: wildcard match against the PRECOMPILED expected type via
118
+ /// its opaque inner handle. Returns 1 = match. A null handle (0) → 0.
119
+ ///
120
+ /// # Safety
121
+ /// `inner` must be a valid `MediaTypeMatcher` pointer from `inner_ptr()`, alive
122
+ /// for the call (the JS wrapper holds the napi instance).
123
+ #[no_mangle]
124
+ pub unsafe extern "C" fn castrum_media_type_matcher_matches(
125
+ inner: usize,
126
+ actual: *const u8,
127
+ actual_len: usize,
128
+ ) -> u8 {
129
+ if inner == 0 || actual.is_null() {
130
+ return 0;
131
+ }
132
+ let a = slice::from_raw_parts(actual, actual_len);
133
+ panic_guard(
134
+ || {
135
+ u8::from(unsafe {
136
+ crate::http::media_type::media_type_matcher_matches_core(
137
+ inner as *const crate::http::media_type::MediaTypeMatcher,
138
+ a,
139
+ )
140
+ })
141
+ },
142
+ 0,
143
+ )
144
+ }
145
+
146
+ /// AcceptNegotiator: best supported encoding for `header` against the
147
+ /// PRECOMPILED supported list via its opaque inner handle → cstring (`null` =
148
+ /// identity, matching napi `Option<String>`).
149
+ ///
150
+ /// `header` crosses as a `(ptr, len)` byte slice — the same convention as
151
+ /// `castrum_media_type_matcher_matches` — so the JS side passes arbitrary
152
+ /// header bytes with no NUL-termination requirement.
153
+ ///
154
+ /// # Safety
155
+ /// `inner` must be a valid `AcceptNegotiator` pointer from `inner_ptr()`, alive
156
+ /// for the call; `header` must be valid for `header_len` bytes.
157
+ #[no_mangle]
158
+ pub unsafe extern "C" fn castrum_accept_negotiator_negotiate(
159
+ inner: usize,
160
+ header: *const u8,
161
+ header_len: usize,
162
+ ) -> *const std::os::raw::c_char {
163
+ if inner == 0 || header.is_null() {
164
+ return std::ptr::null();
165
+ }
166
+ let h = slice::from_raw_parts(header, header_len);
167
+ let Some(bytes) = panic_guard(
168
+ || unsafe {
169
+ crate::http::accept::accept_negotiator_negotiate_core(
170
+ inner as *const crate::http::accept::AcceptNegotiator,
171
+ h,
172
+ )
173
+ },
174
+ None,
175
+ ) else {
176
+ return std::ptr::null();
177
+ };
178
+ cstring_return(bytes.len(), |out| {
179
+ out[..bytes.len()].copy_from_slice(&bytes);
180
+ Some(bytes.len())
181
+ })
182
+ }
183
+
184
+ /// AcceptNegotiator: best supported encoding for `header` with SERVER-
185
+ /// preference tie-breaking (ignex `negotiateEncoding` semantics — q-only, the
186
+ /// supported list's order breaks ties, empty header → identity). Same opaque
187
+ /// handle + cstring contract as `castrum_accept_negotiator_negotiate`.
188
+ ///
189
+ /// # Safety
190
+ /// `inner` must be a valid `AcceptNegotiator` pointer from `inner_ptr()`, alive
191
+ /// for the call. `header` is a `bun:ffi` `cstring` ARG — a NUL-terminated C
192
+ /// string (the engine transcodes the JS header string in-engine; zero JS
193
+ /// encode), never NUL-containing.
194
+ #[no_mangle]
195
+ pub unsafe extern "C" fn castrum_accept_negotiator_negotiate_server(
196
+ inner: usize,
197
+ header: *const std::os::raw::c_char,
198
+ ) -> *const std::os::raw::c_char {
199
+ if inner == 0 || header.is_null() {
200
+ return std::ptr::null();
201
+ }
202
+ let h = std::ffi::CStr::from_ptr(header).to_bytes();
203
+ let Some(bytes) = panic_guard(
204
+ || unsafe {
205
+ crate::http::accept::accept_negotiator_negotiate_server_core(
206
+ inner as *const crate::http::accept::AcceptNegotiator,
207
+ h,
208
+ )
209
+ },
210
+ None,
211
+ ) else {
212
+ return std::ptr::null();
213
+ };
214
+ cstring_return(bytes.len(), |out| {
215
+ out[..bytes.len()].copy_from_slice(&bytes);
216
+ Some(bytes.len())
217
+ })
218
+ }
219
+
220
+ /// Write an HTTP-date (`Sun, 06 Nov 1994 08:49:37 GMT`) into `out`. Returns
221
+ /// bytes written (29), or 0 on a too-small buffer / out-of-range year (use the
222
+ /// allocating `httpDate` napi path for that fallback). Fixed 32-byte stack
223
+ /// buffer core — the FFI sibling of the napi `httpDateInto` (kills the napi
224
+ /// crossing on the hot `httpDateInto` path).
225
+ ///
226
+ /// # Safety
227
+ /// `out` must be valid for writes of up to `out_cap` bytes.
228
+ #[no_mangle]
229
+ pub unsafe extern "C" fn castrum_http_date_into(secs: f64, out: *mut u8, out_cap: usize) -> usize {
230
+ if out.is_null() {
231
+ return 0;
232
+ }
233
+ let output = slice::from_raw_parts_mut(out, out_cap);
234
+ crate::http::http_date::http_date_into_slice(secs as i64, output).unwrap_or(0)
235
+ }
236
+
237
+ /// Parse an IMF-fixdate back to unix seconds → packed `[u8 ok][i64 secs LE]`
238
+ /// (9 bytes; ok=0 → invalid). Mirrors the `castrum_json_sum_ids` ok-byte
239
+ /// convention so a legit epoch (`0`) is distinct from "invalid". Needed-size
240
+ /// convention: `0` = buffer too small (real error).
241
+ ///
242
+ /// # Safety
243
+ /// `data` must be valid for reads of `len` bytes; `out` for `out_cap` writes.
244
+ #[no_mangle]
245
+ pub unsafe extern "C" fn castrum_parse_http_date(
246
+ data: *const u8,
247
+ len: usize,
248
+ out: *mut u8,
249
+ out_cap: usize,
250
+ ) -> usize {
251
+ if data.is_null() || out.is_null() {
252
+ return 0;
253
+ }
254
+ let input = slice::from_raw_parts(data, len);
255
+ let secs = panic_guard(|| crate::http::http_date::parse_http_date_secs(input), None);
256
+ match secs {
257
+ Some(secs) => {
258
+ if out_cap < 9 {
259
+ return 9;
260
+ }
261
+ let o = slice::from_raw_parts_mut(out, 9);
262
+ o[0] = 1;
263
+ o[1..9].copy_from_slice(&secs.to_le_bytes());
264
+ 9
265
+ }
266
+ None => {
267
+ if out_cap < 1 {
268
+ return 1;
269
+ }
270
+ slice::from_raw_parts_mut(out, 1)[0] = 0;
271
+ 1
272
+ }
273
+ }
274
+ }
275
+
276
+ /// Parse a `Content-Type` header into a packed verdict:
277
+ /// `[u32 mediaTypeLen][mediaType][u32 charsetLen (0xFFFFFFFF = none)][charset]
278
+ /// [u32 boundaryLen (0xFFFFFFFF = none)][boundary][u32 paramCount]{[u32 keyLen]
279
+ /// [key][u32 valLen][val]}`. Needed-size convention: `0` = invalid media type
280
+ /// (real error → throw); `w > out_cap` = exact required size; else written.
281
+ ///
282
+ /// # Safety
283
+ /// `data` must be valid for reads of `len` bytes; `out` for `out_cap` writes.
284
+ #[no_mangle]
285
+ pub unsafe extern "C" fn castrum_parse_media_type(
286
+ data: *const u8,
287
+ len: usize,
288
+ out: *mut u8,
289
+ out_cap: usize,
290
+ ) -> usize {
291
+ if data.is_null() || out.is_null() {
292
+ return 0;
293
+ }
294
+ let input = slice::from_raw_parts(data, len);
295
+ let Some(parsed) = panic_guard(
296
+ || crate::http::media_type::parse_media_type_core(input).ok(),
297
+ None,
298
+ ) else {
299
+ return 0;
300
+ };
301
+ // Borrowed charset/boundary lookups (no String clones) + direct write into
302
+ // the caller's buffer — no `format!`, no intermediate Vec, no final copy.
303
+ let charset = parsed
304
+ .params
305
+ .iter()
306
+ .find(|(k, _)| k == "charset")
307
+ .map(|(_, v)| v.as_bytes());
308
+ let boundary = parsed
309
+ .params
310
+ .iter()
311
+ .find(|(k, _)| k == "boundary")
312
+ .map(|(_, v)| v.as_bytes());
313
+ let mt_len = parsed.ty.len() + 1 + parsed.subtype.len();
314
+ let mut needed = 4 + mt_len;
315
+ needed += charset.map_or(4, |v| 4 + v.len());
316
+ needed += boundary.map_or(4, |v| 4 + v.len());
317
+ needed += 4; // paramCount
318
+ for (k, v) in &parsed.params {
319
+ needed += 4 + k.len() + 4 + v.len();
320
+ }
321
+ if needed > out_cap {
322
+ return needed;
323
+ }
324
+ let out = slice::from_raw_parts_mut(out, needed);
325
+ let mut wp = 0usize;
326
+ out[wp..wp + 4].copy_from_slice(&(mt_len as u32).to_le_bytes());
327
+ wp += 4;
328
+ out[wp..wp + parsed.ty.len()].copy_from_slice(parsed.ty.as_bytes());
329
+ wp += parsed.ty.len();
330
+ out[wp] = b'/';
331
+ wp += 1;
332
+ out[wp..wp + parsed.subtype.len()].copy_from_slice(parsed.subtype.as_bytes());
333
+ wp += parsed.subtype.len();
334
+ // charset option slot ([u32 len][charset] or u32::MAX when absent)
335
+ match charset {
336
+ Some(v) => {
337
+ out[wp..wp + 4].copy_from_slice(&(v.len() as u32).to_le_bytes());
338
+ wp += 4;
339
+ out[wp..wp + v.len()].copy_from_slice(v);
340
+ wp += v.len();
341
+ }
342
+ None => {
343
+ out[wp..wp + 4].copy_from_slice(&u32::MAX.to_le_bytes());
344
+ wp += 4;
345
+ }
346
+ }
347
+ // boundary option slot
348
+ match boundary {
349
+ Some(v) => {
350
+ out[wp..wp + 4].copy_from_slice(&(v.len() as u32).to_le_bytes());
351
+ wp += 4;
352
+ out[wp..wp + v.len()].copy_from_slice(v);
353
+ wp += v.len();
354
+ }
355
+ None => {
356
+ out[wp..wp + 4].copy_from_slice(&u32::MAX.to_le_bytes());
357
+ wp += 4;
358
+ }
359
+ }
360
+ // params
361
+ out[wp..wp + 4].copy_from_slice(&(parsed.params.len() as u32).to_le_bytes());
362
+ wp += 4;
363
+ for (k, v) in &parsed.params {
364
+ out[wp..wp + 4].copy_from_slice(&(k.len() as u32).to_le_bytes());
365
+ wp += 4;
366
+ out[wp..wp + k.len()].copy_from_slice(k.as_bytes());
367
+ wp += k.len();
368
+ out[wp..wp + 4].copy_from_slice(&(v.len() as u32).to_le_bytes());
369
+ wp += 4;
370
+ out[wp..wp + v.len()].copy_from_slice(v.as_bytes());
371
+ wp += v.len();
372
+ }
373
+ debug_assert_eq!(wp, needed);
374
+ needed
375
+ }
376
+
377
+ /// Parse an Accept-Encoding header into a packed verdict:
378
+ /// `[u32 count]{[u32 encLen][enc][f32 q][u32 order]}` (empty header → count 0,
379
+ /// 4 bytes). Needed-size convention: `0` = buffer too small (real error);
380
+ /// `w > out_cap` = exact required size; else bytes written.
381
+ ///
382
+ /// # Safety
383
+ /// `data` must be valid for reads of `len` bytes; `out` for `out_cap` writes.
384
+ #[no_mangle]
385
+ pub unsafe extern "C" fn castrum_parse_accept_encoding(
386
+ data: *const u8,
387
+ len: usize,
388
+ out: *mut u8,
389
+ out_cap: usize,
390
+ ) -> usize {
391
+ if data.is_null() || out.is_null() {
392
+ return 0;
393
+ }
394
+ let input = slice::from_raw_parts(data, len);
395
+ let Some(prefs) = panic_guard(
396
+ || Some(crate::http::accept::parse_accept_encoding_core(input)),
397
+ None,
398
+ ) else {
399
+ return 0;
400
+ };
401
+ // Write the packed [u32 count]{[u32 encLen][enc][f32 q][u32 order]} verdict
402
+ // directly into the caller's buffer — no intermediate Vec + copy.
403
+ let mut needed = 4usize;
404
+ for p in &prefs {
405
+ needed += 4 + p.encoding.len() + 4 + 4;
406
+ }
407
+ if needed > out_cap {
408
+ return needed;
409
+ }
410
+ let out = slice::from_raw_parts_mut(out, needed);
411
+ let mut wp = 4usize;
412
+ out[0..4].copy_from_slice(&(prefs.len() as u32).to_le_bytes());
413
+ for p in &prefs {
414
+ out[wp..wp + 4].copy_from_slice(&(p.encoding.len() as u32).to_le_bytes());
415
+ wp += 4;
416
+ out[wp..wp + p.encoding.len()].copy_from_slice(p.encoding.as_bytes());
417
+ wp += p.encoding.len();
418
+ out[wp..wp + 4].copy_from_slice(&p.q.to_le_bytes());
419
+ wp += 4;
420
+ out[wp..wp + 4].copy_from_slice(&p.order.to_le_bytes());
421
+ wp += 4;
422
+ }
423
+ debug_assert_eq!(wp, needed);
424
+ wp
425
+ }
426
+
427
+ /// Percent-encode a query string from a packed `[u32 count]{[u32 keyLen][key]
428
+ /// [u32 valLen][val]}` input (the JS `packPairs` layout) → cstring, keys
429
+ /// SORTED (matches the napi `BTreeMap` ordering). Returns `null` on malformed
430
+ /// packed input / non-UTF-8 (napi parity: throws).
431
+ ///
432
+ /// # Safety
433
+ /// `data` must be valid for reads of `len` bytes.
434
+ #[no_mangle]
435
+ pub unsafe extern "C" fn castrum_url_encode_query(
436
+ data: *const u8,
437
+ len: usize,
438
+ ) -> *const std::os::raw::c_char {
439
+ if data.is_null() {
440
+ return std::ptr::null();
441
+ }
442
+ let input = slice::from_raw_parts(data, len);
443
+ let Some(s) = panic_guard(
444
+ || {
445
+ if input.len() < 4 {
446
+ return None;
447
+ }
448
+ let count = u32::from_le_bytes([input[0], input[1], input[2], input[3]]) as usize;
449
+ let mut map = std::collections::BTreeMap::new();
450
+ let mut off = 4usize;
451
+ for _ in 0..count {
452
+ if off + 4 > input.len() {
453
+ return None;
454
+ }
455
+ let klen = u32::from_le_bytes([
456
+ input[off],
457
+ input[off + 1],
458
+ input[off + 2],
459
+ input[off + 3],
460
+ ]) as usize;
461
+ off += 4;
462
+ if off + klen > input.len() {
463
+ return None;
464
+ }
465
+ let key = std::str::from_utf8(&input[off..off + klen])
466
+ .ok()?
467
+ .to_string();
468
+ off += klen;
469
+ if off + 4 > input.len() {
470
+ return None;
471
+ }
472
+ let vlen = u32::from_le_bytes([
473
+ input[off],
474
+ input[off + 1],
475
+ input[off + 2],
476
+ input[off + 3],
477
+ ]) as usize;
478
+ off += 4;
479
+ if off + vlen > input.len() {
480
+ return None;
481
+ }
482
+ let val = std::str::from_utf8(&input[off..off + vlen])
483
+ .ok()?
484
+ .to_string();
485
+ off += vlen;
486
+ map.insert(key, val);
487
+ }
488
+ let mut out = Vec::new();
489
+ let mut scratch = Vec::new();
490
+ for (i, (k, v)) in map.iter().enumerate() {
491
+ if i > 0 {
492
+ out.push(b'&');
493
+ }
494
+ crate::http::url_join::encode_query_component(k.as_bytes(), &mut scratch, &mut out)
495
+ .ok()?;
496
+ out.push(b'=');
497
+ crate::http::url_join::encode_query_component(v.as_bytes(), &mut scratch, &mut out)
498
+ .ok()?;
499
+ }
500
+ String::from_utf8(out).ok()
501
+ },
502
+ None,
503
+ ) else {
504
+ return std::ptr::null();
505
+ };
506
+ cstring_return(s.len(), |out| {
507
+ out[..s.len()].copy_from_slice(s.as_bytes());
508
+ Some(s.len())
509
+ })
510
+ }
511
+
512
+ /// RFC 3986 URL resolution → cstring (base + reference). Returns `null` on
513
+ /// non-UTF-8 input (napi parity: throws). Mirrors the napi `url_resolve`.
514
+ ///
515
+ /// # Safety
516
+ /// `base`/`reference` must be valid for reads of their lengths.
517
+ #[no_mangle]
518
+ pub unsafe extern "C" fn castrum_url_resolve(
519
+ base: *const u8,
520
+ blen: usize,
521
+ reference: *const u8,
522
+ rlen: usize,
523
+ ) -> *const std::os::raw::c_char {
524
+ if base.is_null() || reference.is_null() {
525
+ return std::ptr::null();
526
+ }
527
+ let b = slice::from_raw_parts(base, blen);
528
+ let r = slice::from_raw_parts(reference, rlen);
529
+ let Some(s) = panic_guard(
530
+ || {
531
+ let bs = std::str::from_utf8(b).ok()?;
532
+ let rs = std::str::from_utf8(r).ok()?;
533
+ Some(crate::http::url_join::recompose(
534
+ &crate::http::url_join::resolve_target(
535
+ &crate::http::url_join::parse_ref(bs),
536
+ &crate::http::url_join::parse_ref(rs),
537
+ ),
538
+ ))
539
+ },
540
+ None,
541
+ ) else {
542
+ return std::ptr::null();
543
+ };
544
+ cstring_return(s.len(), |out| {
545
+ out[..s.len()].copy_from_slice(s.as_bytes());
546
+ Some(s.len())
547
+ })
548
+ }
549
+
550
+ /// Resolve a reference against a `UrlBuilder`'s PRECOMPILED base via its opaque
551
+ /// inner handle (from the napi `inner_ptr()`). Returns bytes written (0 = null
552
+ /// handle / non-UTF-8 reference / panic → real error); a result larger than
553
+ /// `out_cap` reports the exact needed size so the caller retries once
554
+ /// (growExact).
555
+ ///
556
+ /// # Safety
557
+ /// `inner` must be a valid `UrlBuilder` pointer from `inner_ptr()` and must
558
+ /// stay alive for the call (the JS wrapper holds the napi instance).
559
+ /// `reference` must be valid for reads of `reference_len`; `out` for
560
+ /// `out_cap` writes.
561
+ #[no_mangle]
562
+ pub unsafe extern "C" fn castrum_url_builder_resolve(
563
+ inner: usize,
564
+ reference: *const u8,
565
+ reference_len: usize,
566
+ out: *mut u8,
567
+ out_cap: usize,
568
+ ) -> usize {
569
+ if inner == 0 || reference.is_null() || out.is_null() {
570
+ return 0;
571
+ }
572
+ let r = slice::from_raw_parts(reference, reference_len);
573
+ let Some(bytes) = panic_guard(
574
+ || {
575
+ crate::http::url_join::url_builder_resolve_core(
576
+ inner as *const crate::http::url_join::UrlBuilder,
577
+ r,
578
+ )
579
+ .ok()
580
+ },
581
+ None,
582
+ ) else {
583
+ return 0;
584
+ };
585
+ if bytes.len() > out_cap {
586
+ return bytes.len();
587
+ }
588
+ slice::from_raw_parts_mut(out, bytes.len()).copy_from_slice(&bytes);
589
+ bytes.len()
590
+ }
591
+
592
+ /// Extension → MIME type → cstring (unknown → `application/octet-stream`).
593
+ /// Never fails (the core's fallback is infallible) — `null` only on a null
594
+ /// pointer or a panic (programmer error).
595
+ ///
596
+ /// `ext` is a `bun:ffi` `cstring` ARG — the engine transcodes the JS extension
597
+ /// in-engine (JS does zero encode; the callee borrows via `CStr::from_ptr`).
598
+ ///
599
+ /// # Safety
600
+ /// `ext` must be a valid NUL-terminated C string.
601
+ #[no_mangle]
602
+ pub unsafe extern "C" fn castrum_mime_from_extension(
603
+ ext: *const std::os::raw::c_char,
604
+ ) -> *const std::os::raw::c_char {
605
+ if ext.is_null() {
606
+ return std::ptr::null();
607
+ }
608
+ let input = std::ffi::CStr::from_ptr(ext).to_bytes();
609
+ let mime = panic_guard(
610
+ || crate::http::mime_lookup::mime_from_extension_bytes(input),
611
+ b"application/octet-stream".to_vec(),
612
+ );
613
+ cstring_return(mime.len(), |out| {
614
+ out[..mime.len()].copy_from_slice(&mime);
615
+ Some(mime.len())
616
+ })
617
+ }
618
+
619
+ /// HTTP request parse → packed output into `out`.
620
+ ///
621
+ /// # Safety
622
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
623
+ #[no_mangle]
624
+ pub unsafe extern "C" fn castrum_http_parse_request_packed(
625
+ data: *const u8,
626
+ len: usize,
627
+ out: *mut u8,
628
+ out_cap: usize,
629
+ ) -> usize {
630
+ if data.is_null() || out.is_null() {
631
+ return 0;
632
+ }
633
+ crate::http::http_parser::http_parse_request_packed_into_slice(
634
+ slice::from_raw_parts(data, len),
635
+ slice::from_raw_parts_mut(out, out_cap),
636
+ )
637
+ .unwrap_or(0)
638
+ }
639
+
640
+ /// Query string parse → packed output into `out`.
641
+ ///
642
+ /// # Convention (needed-size)
643
+ /// The single-pass writer runs first; on a too-small buffer the exact-size pass
644
+ /// runs (rare) and returns the EXACT required size, so the JS wrapper allocates
645
+ /// ONCE and retries — no 9× pre-size, no re-run loop. `0` remains a REAL error
646
+ /// (malformed `%XX`).
647
+ ///
648
+ /// # Safety
649
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
650
+ #[no_mangle]
651
+ pub unsafe extern "C" fn castrum_query_parse_packed(
652
+ data: *const u8,
653
+ len: usize,
654
+ out: *mut u8,
655
+ out_cap: usize,
656
+ ) -> usize {
657
+ if data.is_null() || out.is_null() {
658
+ return 0;
659
+ }
660
+ let input = slice::from_raw_parts(data, len);
661
+ let output = slice::from_raw_parts_mut(out, out_cap);
662
+ match crate::http::query_parser::query_parse_packed_into_slice(input, output) {
663
+ Ok(w) => w,
664
+ // Too-small OR malformed — the size pass disambiguates: it parses the
665
+ // same input, so Ok(needed) ⇒ too-small, Err ⇒ malformed (real error).
666
+ Err(_) => crate::http::query_parser::query_parse_packed_size(input).unwrap_or(0),
667
+ }
668
+ }
669
+
670
+ /// Cookie header parse → packed output into `out`.
671
+ ///
672
+ /// Same needed-size convention as `castrum_query_parse_packed`.
673
+ ///
674
+ /// # Safety
675
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
676
+ #[no_mangle]
677
+ pub unsafe extern "C" fn castrum_cookie_parse_packed(
678
+ data: *const u8,
679
+ len: usize,
680
+ out: *mut u8,
681
+ out_cap: usize,
682
+ ) -> usize {
683
+ if data.is_null() || out.is_null() {
684
+ return 0;
685
+ }
686
+ let input = slice::from_raw_parts(data, len);
687
+ let output = slice::from_raw_parts_mut(out, out_cap);
688
+ match crate::http::cookie_parser::cookie_parse_packed_into_slice(input, output) {
689
+ Ok(w) => w,
690
+ Err(_) => crate::http::cookie_parser::cookie_parse_packed_size(input).unwrap_or(0),
691
+ }
692
+ }
693
+
694
+ /// Parse an `application/x-www-form-urlencoded` body into packed pairs — the
695
+ /// x-www-form-urlencoded wire format is identical to the query parser's core
696
+ /// (`query_parse_packed_into_slice`), so this is a thin alias.
697
+ ///
698
+ /// # Safety
699
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
700
+ #[no_mangle]
701
+ pub unsafe extern "C" fn castrum_form_parse_packed(
702
+ data: *const u8,
703
+ len: usize,
704
+ out: *mut u8,
705
+ out_cap: usize,
706
+ ) -> usize {
707
+ castrum_query_parse_packed(data, len, out, out_cap)
708
+ }
709
+
710
+ /// Parse a `multipart/form-data` body into the packed parts layout (the same
711
+ /// `[u32 count]{[u32 name_len][name][...]}` wire format as the batch API).
712
+ /// Returns bytes written (0 on malformed input); a result larger than `out_cap`
713
+ /// reports the exact needed size so the caller can retry once (see
714
+ /// `compress_to_out!` for the convention).
715
+ ///
716
+ /// # Safety
717
+ /// `body`/`boundary` must be valid for reads of their lengths; `out` for
718
+ /// writes up to `out_cap`.
719
+ #[no_mangle]
720
+ pub unsafe extern "C" fn castrum_multipart_parse_packed(
721
+ body: *const u8,
722
+ blen: usize,
723
+ boundary: *const u8,
724
+ boundary_len: usize,
725
+ out: *mut u8,
726
+ out_cap: usize,
727
+ ) -> usize {
728
+ if body.is_null() || boundary.is_null() || out.is_null() {
729
+ return 0;
730
+ }
731
+ // Wrap in panic_guard: the parser allocates internally — a panic must not
732
+ // unwind through the C ABI (process crash); it becomes 0 instead.
733
+ let packed = panic_guard(
734
+ || {
735
+ let parts = crate::http::multipart::parse_multipart_limited(
736
+ slice::from_raw_parts(body, blen),
737
+ slice::from_raw_parts(boundary, boundary_len),
738
+ &Default::default(),
739
+ );
740
+ let mut buf = Vec::new();
741
+ crate::http::multipart::parts_to_packed(&parts, &mut buf);
742
+ buf
743
+ },
744
+ Vec::new(),
745
+ );
746
+ if packed.len() > out_cap {
747
+ // Needed-size convention (see compress_to_out!): exact retry, no re-run.
748
+ return packed.len();
749
+ }
750
+ slice::from_raw_parts_mut(out, packed.len()).copy_from_slice(&packed);
751
+ packed.len()
752
+ }