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,517 @@
1
+ // rust/ffi/json.rs — JSON C-ABI exports.
2
+ //
3
+ // RFC 6902 JSON patch, the packed JSON token stream (`castrum_json_parse_packed`),
4
+ // schema validation, and template rendering (opaque-handle paths).
5
+
6
+ use rustc_hash::FxHashMap;
7
+ use std::slice;
8
+
9
+ use super::util::panic_guard;
10
+
11
+ /// RFC 6902 JSON patch → patched doc into `out` (0 on invalid/inapplicable).
12
+ ///
13
+ /// # Safety
14
+ /// `doc`/`patch` must be valid for reads of their lengths; `out` for `out_cap`.
15
+ #[no_mangle]
16
+ pub unsafe extern "C" fn castrum_json_patch(
17
+ doc: *const u8,
18
+ dlen: usize,
19
+ patch: *const u8,
20
+ plen: usize,
21
+ out: *mut u8,
22
+ out_cap: usize,
23
+ ) -> usize {
24
+ if doc.is_null() || patch.is_null() || out.is_null() {
25
+ return 0;
26
+ }
27
+ // Wrap in panic_guard: a panic in the patch engine must not unwind through
28
+ // the C ABI (process crash) — it becomes 0 (invalid input) instead.
29
+ let Some(patched) = panic_guard(
30
+ || {
31
+ crate::json::patch::apply_json_patch_bytes(
32
+ slice::from_raw_parts(doc, dlen),
33
+ slice::from_raw_parts(patch, plen),
34
+ )
35
+ .ok()
36
+ },
37
+ None,
38
+ ) else {
39
+ return 0;
40
+ };
41
+ if patched.len() > out_cap {
42
+ // Needed-size convention (see compress_to_out!): exact retry, no re-run.
43
+ return patched.len();
44
+ }
45
+ slice::from_raw_parts_mut(out, patched.len()).copy_from_slice(&patched);
46
+ patched.len()
47
+ }
48
+
49
+ // ── Packed JSON token stream (structural parse, no re-parse) ────────
50
+ // `castrum_json_parse_packed` parses ONCE with sonic-rs and emits a typed
51
+ // token stream with a DEDUPLICATED string table. The JS side assembles the
52
+ // value directly from the tokens — it never re-parses JSON text, and repeated
53
+ // keys/values are decoded exactly once (the old cstring path re-serialized the
54
+ // whole doc to text and `JSON.parse`d it again, measured 3.92x slower than
55
+ // Bun's JSON.parse on the 5k-row fixture).
56
+ //
57
+ // Layout: `[u32 strCount]{[u32 len][utf8 bytes]}... [u32 treeLen][tree]`
58
+ // The tree is a single value encoded as a start/end-marker token stream
59
+ // (little-endian; no counts — the sonic-rs SeqAccess/MapAccess expose no
60
+ // size_hint, and markers let JS decode iteratively with `push`/`pop`):
61
+ // 0 = null | 1 = false | 2 = true | 3 = number (f64 LE, 8 bytes)
62
+ // 4 = string (u32 index into the string table)
63
+ // 5 = array start | 6 = object start
64
+ // 7 = array end | 8 = object end
65
+ // 9 = object key (u32 index into the string table)
66
+ // Object body: `6, (9, keyIdx, value)*, 8`.
67
+
68
+ const JSON_PACKED_NULL: u8 = 0;
69
+ const JSON_PACKED_FALSE: u8 = 1;
70
+ const JSON_PACKED_TRUE: u8 = 2;
71
+ const JSON_PACKED_NUMBER: u8 = 3;
72
+ const JSON_PACKED_STRING: u8 = 4;
73
+ const JSON_PACKED_ARRAY_START: u8 = 5;
74
+ const JSON_PACKED_OBJECT_START: u8 = 6;
75
+ const JSON_PACKED_ARRAY_END: u8 = 7;
76
+ const JSON_PACKED_OBJECT_END: u8 = 8;
77
+ const JSON_PACKED_KEY: u8 = 9;
78
+
79
+ /// Single-pass emitter. sonic-rs's fast parser drives this via serde, writing
80
+ /// the packed token stream DIRECTLY — there is NO intermediate `sonic_rs::Value`
81
+ /// DOM (building it measured ~1.0ms for the 5k-row fixture). Strings (keys +
82
+ /// values) are interned into a deduplicated table keyed by OWNED bytes
83
+ /// (`Vec<u8>`) so escaped strings work uniformly; lookups borrow (`get(&[u8])`)
84
+ /// without allocating. `FxHashMap` (rustc-hash) is used because default SipHash
85
+ /// is slow on short keys (same choice fast_schema makes); a hash collision only
86
+ /// costs an extra byte compare, never a wrong index. Dedup keeps the JS-side
87
+ /// decode to ~1 decode per unique string (a per-occurrence blob was ~10x worse
88
+ /// on the JS side due to rope-slicing).
89
+ pub(crate) struct JsonPackedEmitter {
90
+ strings: FxHashMap<Vec<u8>, u32>,
91
+ table: Vec<u8>,
92
+ tree: Vec<u8>,
93
+ }
94
+
95
+ impl JsonPackedEmitter {
96
+ #[inline]
97
+ fn intern(&mut self, bytes: &[u8]) -> u32 {
98
+ if let Some(&idx) = self.strings.get(bytes) {
99
+ return idx;
100
+ }
101
+ let idx = self.strings.len() as u32;
102
+ self.strings.insert(bytes.to_vec(), idx);
103
+ self.table
104
+ .extend_from_slice(&(bytes.len() as u32).to_le_bytes());
105
+ self.table.extend_from_slice(bytes);
106
+ idx
107
+ }
108
+
109
+ #[inline]
110
+ fn emit_u32(&mut self, v: u32) {
111
+ self.tree.extend_from_slice(&v.to_le_bytes());
112
+ }
113
+
114
+ #[inline]
115
+ fn emit_string(&mut self, s: &str) {
116
+ self.tree.push(JSON_PACKED_STRING);
117
+ let idx = self.intern(s.as_bytes());
118
+ self.emit_u32(idx);
119
+ }
120
+
121
+ #[inline]
122
+ fn emit_key(&mut self, s: &str) {
123
+ self.tree.push(JSON_PACKED_KEY);
124
+ let idx = self.intern(s.as_bytes());
125
+ self.emit_u32(idx);
126
+ }
127
+
128
+ #[inline]
129
+ fn emit_number(&mut self, f: f64) {
130
+ self.tree.push(JSON_PACKED_NUMBER);
131
+ self.tree.extend_from_slice(&f.to_le_bytes());
132
+ }
133
+ }
134
+
135
+ /// Seed that deserializes any nested value through the shared emitter.
136
+ struct JsonPackedSeed<'a> {
137
+ out: &'a mut JsonPackedEmitter,
138
+ }
139
+
140
+ impl<'de> serde::de::DeserializeSeed<'de> for JsonPackedSeed<'_> {
141
+ type Value = ();
142
+
143
+ #[inline]
144
+ fn deserialize<D>(self, d: D) -> Result<Self::Value, D::Error>
145
+ where
146
+ D: serde::de::Deserializer<'de>,
147
+ {
148
+ d.deserialize_any(JsonPackedVisitor { out: self.out })
149
+ }
150
+ }
151
+
152
+ /// Seed for object KEYS (always JSON strings).
153
+ struct JsonPackedKeySeed<'a> {
154
+ out: &'a mut JsonPackedEmitter,
155
+ }
156
+
157
+ impl<'de> serde::de::DeserializeSeed<'de> for JsonPackedKeySeed<'_> {
158
+ type Value = ();
159
+
160
+ #[inline]
161
+ fn deserialize<D>(self, d: D) -> Result<Self::Value, D::Error>
162
+ where
163
+ D: serde::de::Deserializer<'de>,
164
+ {
165
+ d.deserialize_str(JsonPackedKeyVisitor { out: self.out })
166
+ }
167
+ }
168
+
169
+ struct JsonPackedKeyVisitor<'a> {
170
+ out: &'a mut JsonPackedEmitter,
171
+ }
172
+
173
+ impl<'de> serde::de::Visitor<'de> for JsonPackedKeyVisitor<'_> {
174
+ type Value = ();
175
+
176
+ fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
177
+ f.write_str("an object key")
178
+ }
179
+
180
+ #[inline]
181
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
182
+ where
183
+ E: serde::de::Error,
184
+ {
185
+ self.out.emit_key(v);
186
+ Ok(())
187
+ }
188
+
189
+ #[inline]
190
+ fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
191
+ where
192
+ E: serde::de::Error,
193
+ {
194
+ self.visit_str(v)
195
+ }
196
+
197
+ #[inline]
198
+ fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
199
+ where
200
+ E: serde::de::Error,
201
+ {
202
+ self.visit_str(v.as_str())
203
+ }
204
+ }
205
+
206
+ struct JsonPackedVisitor<'a> {
207
+ out: &'a mut JsonPackedEmitter,
208
+ }
209
+
210
+ impl<'de> serde::de::Visitor<'de> for JsonPackedVisitor<'_> {
211
+ type Value = ();
212
+
213
+ fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
214
+ f.write_str("any JSON value")
215
+ }
216
+
217
+ #[inline]
218
+ fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
219
+ where
220
+ E: serde::de::Error,
221
+ {
222
+ self.out.tree.push(if v {
223
+ JSON_PACKED_TRUE
224
+ } else {
225
+ JSON_PACKED_FALSE
226
+ });
227
+ Ok(())
228
+ }
229
+
230
+ #[inline]
231
+ fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
232
+ where
233
+ E: serde::de::Error,
234
+ {
235
+ // f64 = JS number semantics (the napi serde_json::Value path rounds the
236
+ // same way JSON.parse does).
237
+ self.out.emit_number(v as f64);
238
+ Ok(())
239
+ }
240
+
241
+ #[inline]
242
+ fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
243
+ where
244
+ E: serde::de::Error,
245
+ {
246
+ self.out.emit_number(v as f64);
247
+ Ok(())
248
+ }
249
+
250
+ #[inline]
251
+ fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
252
+ where
253
+ E: serde::de::Error,
254
+ {
255
+ self.out.emit_number(v);
256
+ Ok(())
257
+ }
258
+
259
+ #[inline]
260
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
261
+ where
262
+ E: serde::de::Error,
263
+ {
264
+ self.out.emit_string(v);
265
+ Ok(())
266
+ }
267
+
268
+ #[inline]
269
+ fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
270
+ where
271
+ E: serde::de::Error,
272
+ {
273
+ self.out.emit_string(v);
274
+ Ok(())
275
+ }
276
+
277
+ #[inline]
278
+ fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
279
+ where
280
+ E: serde::de::Error,
281
+ {
282
+ self.out.emit_string(v.as_str());
283
+ Ok(())
284
+ }
285
+
286
+ #[inline]
287
+ fn visit_none<E>(self) -> Result<Self::Value, E>
288
+ where
289
+ E: serde::de::Error,
290
+ {
291
+ self.out.tree.push(JSON_PACKED_NULL);
292
+ Ok(())
293
+ }
294
+
295
+ #[inline]
296
+ fn visit_unit<E>(self) -> Result<Self::Value, E>
297
+ where
298
+ E: serde::de::Error,
299
+ {
300
+ self.out.tree.push(JSON_PACKED_NULL);
301
+ Ok(())
302
+ }
303
+
304
+ #[inline]
305
+ fn visit_some<D>(self, d: D) -> Result<Self::Value, D::Error>
306
+ where
307
+ D: serde::de::Deserializer<'de>,
308
+ {
309
+ d.deserialize_any(self)
310
+ }
311
+
312
+ #[inline]
313
+ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
314
+ where
315
+ A: serde::de::SeqAccess<'de>,
316
+ {
317
+ self.out.tree.push(JSON_PACKED_ARRAY_START);
318
+ while let Some(()) = seq.next_element_seed(JsonPackedSeed {
319
+ out: &mut *self.out,
320
+ })? {}
321
+ self.out.tree.push(JSON_PACKED_ARRAY_END);
322
+ Ok(())
323
+ }
324
+
325
+ #[inline]
326
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
327
+ where
328
+ A: serde::de::MapAccess<'de>,
329
+ {
330
+ self.out.tree.push(JSON_PACKED_OBJECT_START);
331
+ while let Some(()) = map.next_key_seed(JsonPackedKeySeed {
332
+ out: &mut *self.out,
333
+ })? {
334
+ map.next_value_seed(JsonPackedSeed {
335
+ out: &mut *self.out,
336
+ })?;
337
+ }
338
+ self.out.tree.push(JSON_PACKED_OBJECT_END);
339
+ Ok(())
340
+ }
341
+ }
342
+
343
+ impl<'de> serde::Deserialize<'de> for JsonPackedEmitter {
344
+ #[inline]
345
+ fn deserialize<D>(d: D) -> Result<Self, D::Error>
346
+ where
347
+ D: serde::de::Deserializer<'de>,
348
+ {
349
+ let mut out = JsonPackedEmitter {
350
+ strings: FxHashMap::default(),
351
+ table: Vec::new(),
352
+ tree: Vec::new(),
353
+ };
354
+ d.deserialize_any(JsonPackedVisitor { out: &mut out })?;
355
+ Ok(out)
356
+ }
357
+ }
358
+
359
+ /// Parse JSON → packed token stream (see the layout above). Needed-size
360
+ /// convention: `0` = invalid JSON (real error → JS throws); `w > out_cap` =
361
+ /// exact required size (JS allocates once and retries); else bytes written.
362
+ ///
363
+ /// # Safety
364
+ /// `data` must be valid for reads of `len` bytes; `out` for `out_cap` writes.
365
+ #[no_mangle]
366
+ pub unsafe extern "C" fn castrum_json_parse_packed(
367
+ data: *const u8,
368
+ len: usize,
369
+ out: *mut u8,
370
+ out_cap: usize,
371
+ ) -> usize {
372
+ if data.is_null() || out.is_null() {
373
+ return 0;
374
+ }
375
+ let input = slice::from_raw_parts(data, len);
376
+ let Some(emitter) = panic_guard(
377
+ || sonic_rs::from_slice::<JsonPackedEmitter>(input).ok(),
378
+ None,
379
+ ) else {
380
+ return 0;
381
+ };
382
+ // Write the packed [u32 stringsLen][table][u32 treeLen][tree] directly
383
+ // into the caller's buffer — no intermediate Vec + second copy.
384
+ let needed = 4 + emitter.table.len() + 4 + emitter.tree.len();
385
+ if needed > out_cap {
386
+ return needed;
387
+ }
388
+ let out = slice::from_raw_parts_mut(out, needed);
389
+ let mut wp = 0usize;
390
+ out[wp..wp + 4].copy_from_slice(&(emitter.strings.len() as u32).to_le_bytes());
391
+ wp += 4;
392
+ out[wp..wp + emitter.table.len()].copy_from_slice(&emitter.table);
393
+ wp += emitter.table.len();
394
+ out[wp..wp + 4].copy_from_slice(&(emitter.tree.len() as u32).to_le_bytes());
395
+ wp += 4;
396
+ out[wp..wp + emitter.tree.len()].copy_from_slice(&emitter.tree);
397
+ wp += emitter.tree.len();
398
+ wp
399
+ }
400
+
401
+ /// SchemaValidator: validate a document against the COMPILED schema via its
402
+ /// opaque inner handle. Returns 1 = valid. A null handle (0) → 0.
403
+ ///
404
+ /// # Safety
405
+ /// `inner` must be a valid `SchemaValidator` pointer from `inner_ptr()`, alive
406
+ /// for the call.
407
+ #[no_mangle]
408
+ pub unsafe extern "C" fn castrum_schema_validator_validate(
409
+ inner: usize,
410
+ doc: *const u8,
411
+ doc_len: usize,
412
+ ) -> u8 {
413
+ if inner == 0 || doc.is_null() {
414
+ return 0;
415
+ }
416
+ let d = slice::from_raw_parts(doc, doc_len);
417
+ panic_guard(
418
+ || {
419
+ u8::from(unsafe {
420
+ crate::json::json_schema::schema_validator_validate_core(
421
+ inner as *const crate::json::json_schema::SchemaValidator,
422
+ d,
423
+ )
424
+ })
425
+ },
426
+ 0,
427
+ )
428
+ }
429
+
430
+ /// TemplateRenderer: render the compiled template with a pre-serialized JSON
431
+ /// context via its opaque inner handle. Needed-size convention; 0 = invalid
432
+ /// context / render error / null handle (real error).
433
+ ///
434
+ /// # Safety
435
+ /// `inner` must be a valid `TemplateRenderer` pointer from `inner_ptr()`, alive
436
+ /// for the call; `out` for writes up to `out_cap`.
437
+ #[no_mangle]
438
+ pub unsafe extern "C" fn castrum_template_render(
439
+ inner: usize,
440
+ context: *const u8,
441
+ context_len: usize,
442
+ out: *mut u8,
443
+ out_cap: usize,
444
+ ) -> usize {
445
+ if inner == 0 || context.is_null() || out.is_null() {
446
+ return 0;
447
+ }
448
+ let c = slice::from_raw_parts(context, context_len);
449
+ let Some(rendered) = panic_guard(
450
+ || unsafe {
451
+ crate::payload::template::template_render_core(
452
+ inner as *const crate::payload::template::TemplateRenderer,
453
+ c,
454
+ )
455
+ .ok()
456
+ },
457
+ None,
458
+ ) else {
459
+ return 0;
460
+ };
461
+ if rendered.len() > out_cap {
462
+ return rendered.len();
463
+ }
464
+ slice::from_raw_parts_mut(out, rendered.len()).copy_from_slice(&rendered);
465
+ rendered.len()
466
+ }
467
+
468
+ /// Fused wire-level validation: RAW query string → JSON → draft-07 validate
469
+ /// against the compiled schema, ONE call. Returns 1 = valid.
470
+ ///
471
+ /// # Safety
472
+ /// `inner` must be a live `SchemaValidator` pointer from `inner_ptr()`;
473
+ /// `qs` must be a valid NUL-terminated C string.
474
+ #[no_mangle]
475
+ pub unsafe extern "C" fn castrum_query_validate(
476
+ inner: usize,
477
+ qs: *const std::os::raw::c_char,
478
+ ) -> u8 {
479
+ if inner == 0 || qs.is_null() {
480
+ return 0;
481
+ }
482
+ let q = std::ffi::CStr::from_ptr(qs).to_bytes();
483
+ panic_guard(
484
+ || {
485
+ crate::json::json_schema::schema_validate_query_core(
486
+ inner as *const crate::json::json_schema::SchemaValidator,
487
+ q,
488
+ )
489
+ },
490
+ false,
491
+ ) as u8
492
+ }
493
+
494
+ /// Fused cookie-header variant of [`castrum_query_validate`] (trim +
495
+ /// DQUOTE-unwrap values, NO percent-decode). Returns 1 = valid.
496
+ ///
497
+ /// # Safety
498
+ /// See [`castrum_query_validate`].
499
+ #[no_mangle]
500
+ pub unsafe extern "C" fn castrum_cookie_validate(
501
+ inner: usize,
502
+ header: *const std::os::raw::c_char,
503
+ ) -> u8 {
504
+ if inner == 0 || header.is_null() {
505
+ return 0;
506
+ }
507
+ let h = std::ffi::CStr::from_ptr(header).to_bytes();
508
+ panic_guard(
509
+ || {
510
+ crate::json::json_schema::schema_validate_cookie_core(
511
+ inner as *const crate::json::json_schema::SchemaValidator,
512
+ h,
513
+ )
514
+ },
515
+ false,
516
+ ) as u8
517
+ }