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,499 @@
1
+ // rust/json/patch/tests.rs — unit tests for the RFC 6902 JSON Patch engine.
2
+ //
3
+ // Covers the op semantics in `ops.rs`, the size guards + batch orchestration
4
+ // in `engine.rs`, and the napi surface in `api.rs`.
5
+
6
+ use super::engine::apply_json_patch_bytes_limited;
7
+ use super::{
8
+ apply_json_patch_bytes, json_patch, json_patch_batch_packed, run_json_patch_batch,
9
+ JsonPatchError,
10
+ };
11
+ use crate::util::unpack;
12
+ use napi::bindgen_prelude::*;
13
+ use serde_json::Value;
14
+
15
+ fn apply(doc: &[u8], patch: &[u8]) -> std::result::Result<Vec<u8>, JsonPatchError> {
16
+ apply_json_patch_bytes(doc, patch)
17
+ }
18
+
19
+ fn as_value(bytes: &[u8]) -> Value {
20
+ sonic_rs::from_slice(bytes).expect("test bytes must be valid JSON")
21
+ }
22
+
23
+ // ── Scalar: RFC 6902 operations ─────────────────────────────
24
+
25
+ #[test]
26
+ fn replace_field() {
27
+ let out = apply(
28
+ br#"{"name":"alice","age":30}"#,
29
+ br#"[{"op":"replace","path":"/age","value":31}]"#,
30
+ )
31
+ .unwrap();
32
+ let result = as_value(&out);
33
+ assert_eq!(result["age"], 31);
34
+ assert_eq!(result["name"], "alice");
35
+ }
36
+
37
+ #[test]
38
+ fn add_and_remove() {
39
+ let out = apply(
40
+ br#"{"items":["a","b"]}"#,
41
+ br#"[{"op":"add","path":"/items/-","value":"c"},{"op":"remove","path":"/items/0"}]"#,
42
+ )
43
+ .unwrap();
44
+ let result = as_value(&out);
45
+ let expected: Value = sonic_rs::from_slice(br#"["b","c"]"#).unwrap();
46
+ assert_eq!(result["items"], expected);
47
+ }
48
+
49
+ #[test]
50
+ fn remove_field_and_array_index() {
51
+ let out = apply(
52
+ br#"{"a":1,"b":2,"arr":[10,20,30]}"#,
53
+ br#"[{"op":"remove","path":"/b"},{"op":"remove","path":"/arr/1"}]"#,
54
+ )
55
+ .unwrap();
56
+ assert_eq!(as_value(&out), as_value(br#"{"a":1,"arr":[10,30]}"#));
57
+ }
58
+
59
+ #[test]
60
+ fn add_to_root_replaces_document() {
61
+ // RFC 6902: the empty-string path targets the document root; `/` would
62
+ // add a member named `""` to the root object instead.
63
+ let out = apply(
64
+ br#"{"a":1}"#,
65
+ br#"[{"op":"add","path":"","value":{"b":2}}]"#,
66
+ )
67
+ .unwrap();
68
+ assert_eq!(as_value(&out), as_value(br#"{"b":2}"#));
69
+ }
70
+
71
+ #[test]
72
+ fn add_to_array_index() {
73
+ let out = apply(
74
+ br#"{"items":["a","c"]}"#,
75
+ br#"[{"op":"add","path":"/items/1","value":"b"}]"#,
76
+ )
77
+ .unwrap();
78
+ assert_eq!(as_value(&out), as_value(br#"{"items":["a","b","c"]}"#));
79
+ }
80
+
81
+ #[test]
82
+ fn move_op_between_containers() {
83
+ let out = apply(
84
+ br#"{"from":{"v":1},"to":{}}"#,
85
+ br#"[{"op":"move","from":"/from/v","path":"/to/v"}]"#,
86
+ )
87
+ .unwrap();
88
+ assert_eq!(as_value(&out), as_value(br#"{"from":{},"to":{"v":1}}"#));
89
+ }
90
+
91
+ #[test]
92
+ fn copy_op_duplicates_subtree() {
93
+ let out = apply(
94
+ br#"{"a":{"x":1,"y":[2,3]},"b":null}"#,
95
+ br#"[{"op":"copy","from":"/a","path":"/b"}]"#,
96
+ )
97
+ .unwrap();
98
+ let result = as_value(&out);
99
+ assert_eq!(result["b"], result["a"]);
100
+ assert_eq!(result["a"], as_value(br#"{"x":1,"y":[2,3]}"#));
101
+ }
102
+
103
+ #[test]
104
+ fn test_op_success_is_identity() {
105
+ let doc = br#"{"v":42,"s":"x"}"#;
106
+ let out = apply(doc, br#"[{"op":"test","path":"/v","value":42}]"#).unwrap();
107
+ assert_eq!(as_value(&out), as_value(doc));
108
+ }
109
+
110
+ #[test]
111
+ fn test_op_mismatch_fails() {
112
+ let err = apply(br#"{"v":42}"#, br#"[{"op":"test","path":"/v","value":43}]"#).unwrap_err();
113
+ assert!(err.to_string().contains("apply failed"), "{err}");
114
+ }
115
+
116
+ #[test]
117
+ fn path_token_escapes_tilde_and_slash() {
118
+ // RFC 6901: `~1` decodes to `/`, `~0` decodes to `~`.
119
+ let out = apply(
120
+ br#"{"a/b":0,"m~n":0}"#,
121
+ br#"[{"op":"replace","path":"/a~1b","value":1},{"op":"replace","path":"/m~0n","value":2}]"#,
122
+ )
123
+ .unwrap();
124
+ assert_eq!(as_value(&out), as_value(br#"{"a/b":1,"m~n":2}"#));
125
+ }
126
+
127
+ #[test]
128
+ fn test_op_on_array_and_nested() {
129
+ let doc = br#"{"arr":[1,2,3],"obj":{"x":10}}"#;
130
+ // Array element + nested object tests succeed.
131
+ apply(doc, br#"[{"op":"test","path":"/arr/1","value":2}]"#).unwrap();
132
+ apply(doc, br#"[{"op":"test","path":"/obj/x","value":10}]"#).unwrap();
133
+ // Mismatch on a nested/array value fails.
134
+ let err = apply(doc, br#"[{"op":"test","path":"/arr/2","value":99}]"#).unwrap_err();
135
+ assert!(err.to_string().contains("apply failed"), "{err}");
136
+ }
137
+
138
+ #[test]
139
+ fn move_from_missing_path_fails() {
140
+ let err = apply(
141
+ br#"{"a":1}"#,
142
+ br#"[{"op":"move","from":"/nope","path":"/b"}]"#,
143
+ )
144
+ .unwrap_err();
145
+ assert!(err.to_string().contains("apply failed"), "{err}");
146
+ }
147
+
148
+ #[test]
149
+ fn copy_from_missing_path_fails() {
150
+ let err = apply(
151
+ br#"{"a":1}"#,
152
+ br#"[{"op":"copy","from":"/nope","path":"/b"}]"#,
153
+ )
154
+ .unwrap_err();
155
+ assert!(err.to_string().contains("apply failed"), "{err}");
156
+ }
157
+
158
+ #[test]
159
+ fn move_array_element_reorders() {
160
+ // RFC 6902: remove `/items/0` then add at `/items/2` (post-removal index).
161
+ let out = apply(
162
+ br#"{"items":["a","b","c"]}"#,
163
+ br#"[{"op":"move","from":"/items/0","path":"/items/2"}]"#,
164
+ )
165
+ .unwrap();
166
+ assert_eq!(as_value(&out), as_value(br#"{"items":["b","c","a"]}"#));
167
+ }
168
+
169
+ #[test]
170
+ fn move_array_element_into_object() {
171
+ let out = apply(
172
+ br#"{"arr":[1,2],"obj":{}}"#,
173
+ br#"[{"op":"move","from":"/arr/0","path":"/obj/first"}]"#,
174
+ )
175
+ .unwrap();
176
+ assert_eq!(
177
+ as_value(&out),
178
+ as_value(br#"{"arr":[2],"obj":{"first":1}}"#)
179
+ );
180
+ }
181
+
182
+ #[test]
183
+ fn move_into_own_descendant_fails() {
184
+ // RFC 6902: a node cannot be moved into one of its own children.
185
+ let err = apply(
186
+ br#"{"a":{"b":1},"c":{}}"#,
187
+ br#"[{"op":"move","from":"/a","path":"/a/b"}]"#,
188
+ )
189
+ .unwrap_err();
190
+ assert!(err.to_string().contains("apply failed"), "{err}");
191
+ }
192
+
193
+ #[test]
194
+ fn add_dash_on_non_array_fails() {
195
+ // RFC 6902: `-` in a path is only valid for arrays.
196
+ let err = apply(br#"{"a":1}"#, br#"[{"op":"add","path":"/a/-","value":2}]"#).unwrap_err();
197
+ assert!(err.to_string().contains("apply failed"), "{err}");
198
+ }
199
+
200
+ #[test]
201
+ fn remove_out_of_range_array_index_fails() {
202
+ let err = apply(br#"{"arr":[1,2]}"#, br#"[{"op":"remove","path":"/arr/5"}]"#).unwrap_err();
203
+ assert!(err.to_string().contains("apply failed"), "{err}");
204
+ }
205
+
206
+ #[test]
207
+ fn invalid_escape_tilde_2_rejected() {
208
+ // RFC 6901: only `~0` and `~1` are valid escapes; `~2` is rejected at
209
+ // patch deserialization (json-patch validates the pointer eagerly).
210
+ let err = apply(
211
+ br#"{"a/b":1}"#,
212
+ br#"[{"op":"replace","path":"/a~2b","value":2}]"#,
213
+ )
214
+ .unwrap_err();
215
+ assert!(err.to_string().contains("invalid patch"), "{err}");
216
+ }
217
+
218
+ #[test]
219
+ fn tilde_at_end_of_token_rejected() {
220
+ // A trailing `~` is an incomplete escape (RFC 6901) and is rejected at
221
+ // patch deserialization.
222
+ let err = apply(
223
+ br#"{"a":1}"#,
224
+ br#"[{"op":"replace","path":"/a~","value":2}]"#,
225
+ )
226
+ .unwrap_err();
227
+ assert!(err.to_string().contains("invalid patch"), "{err}");
228
+ }
229
+
230
+ #[test]
231
+ fn sequential_ops_on_same_path_apply_in_order() {
232
+ let out = apply(
233
+ br#"{"v":0}"#,
234
+ br#"[{"op":"replace","path":"/v","value":1},{"op":"replace","path":"/v","value":2}]"#,
235
+ )
236
+ .unwrap();
237
+ assert_eq!(as_value(&out), as_value(br#"{"v":2}"#));
238
+ }
239
+
240
+ #[test]
241
+ fn unicode_content_roundtrip() {
242
+ // Byte-string literals must stay ASCII; build the UTF-8 bytes from a
243
+ // regular string literal instead.
244
+ let doc = "{\"名前\":\"アリス\",\"emoji\":\"👍\",\"nested\":{\"café\":\"☕\"}}";
245
+ let patch = "[{\"op\":\"replace\",\"path\":\"/emoji\",\"value\":\"🚀\"}]";
246
+ let out = apply(doc.as_bytes(), patch.as_bytes()).unwrap();
247
+ let result = as_value(&out);
248
+ assert_eq!(result["名前"], "アリス");
249
+ assert_eq!(result["emoji"], "🚀");
250
+ assert_eq!(result["nested"]["café"], "☕");
251
+ }
252
+
253
+ #[test]
254
+ fn empty_patch_is_byte_identity() {
255
+ let doc = br#"{"a":[1,2,3],"b":{"c":true}}"#;
256
+ let out = apply(doc, b"[]").unwrap();
257
+ assert_eq!(out.as_slice(), doc);
258
+ }
259
+
260
+ #[test]
261
+ fn empty_doc_and_scalar_roots_roundtrip() {
262
+ assert_eq!(apply(b"{}", b"[]").unwrap().as_slice(), b"{}");
263
+ assert_eq!(apply(b"42", b"[]").unwrap().as_slice(), b"42");
264
+ assert_eq!(apply(b"null", b"[]").unwrap().as_slice(), b"null");
265
+ }
266
+
267
+ #[test]
268
+ fn deeply_nested_paths() {
269
+ let out = apply(
270
+ br#"{"l1":{"l2":{"l3":{"l4":{"l5":[0,1,2]}}}}}"#,
271
+ br#"[{"op":"replace","path":"/l1/l2/l3/l4/l5/2","value":99}]"#,
272
+ )
273
+ .unwrap();
274
+ let result = as_value(&out);
275
+ assert_eq!(result["l1"]["l2"]["l3"]["l4"]["l5"][2], 99);
276
+ }
277
+
278
+ #[test]
279
+ fn real_world_profile_patch() {
280
+ let doc = br#"{
281
+ "id": "usr_01H2X9K7",
282
+ "profile": {
283
+ "displayName": "Alice",
284
+ "email": "alice@example.com",
285
+ "preferences": { "theme": "dark", "notifications": true, "digest": "weekly" }
286
+ },
287
+ "roles": ["admin", "editor"],
288
+ "meta": { "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-08-01T08:30:00Z" }
289
+ }"#;
290
+ let patch = br#"[
291
+ {"op":"replace","path":"/profile/preferences/theme","value":"light"},
292
+ {"op":"add","path":"/roles/-","value":"reviewer"},
293
+ {"op":"replace","path":"/meta/updatedAt","value":"2026-08-10T12:00:00Z"},
294
+ {"op":"add","path":"/profile/preferences/locale","value":"en-US"}
295
+ ]"#;
296
+ let result = as_value(&apply(doc, patch).unwrap());
297
+ assert_eq!(result["profile"]["preferences"]["theme"], "light");
298
+ assert_eq!(result["profile"]["preferences"]["locale"], "en-US");
299
+ assert_eq!(
300
+ result["roles"],
301
+ as_value(br#"["admin","editor","reviewer"]"#)
302
+ );
303
+ assert_eq!(result["meta"]["updatedAt"], "2026-08-10T12:00:00Z");
304
+ assert_eq!(result["profile"]["displayName"], "Alice");
305
+ }
306
+
307
+ #[test]
308
+ fn numeric_precision_preserved() {
309
+ let doc = br#"{"big":9007199254740993,"frac":3.141592653589793,"neg":-42}"#;
310
+ let out = apply(doc, br#"[{"op":"add","path":"/extra","value":-0.5}]"#).unwrap();
311
+ let result = as_value(&out);
312
+ assert_eq!(result["big"], 9_007_199_254_740_993i64);
313
+ assert_eq!(result["frac"], std::f64::consts::PI);
314
+ assert_eq!(result["neg"], -42);
315
+ assert_eq!(result["extra"], -0.5);
316
+ }
317
+
318
+ // ── Error handling ──────────────────────────────────────────
319
+
320
+ #[test]
321
+ fn error_context_is_specific() {
322
+ let err = apply(b"not-json", b"[]").unwrap_err();
323
+ assert!(err.to_string().contains("invalid document"), "{err}");
324
+ let err = apply(br#"{}"#, b"not-json").unwrap_err();
325
+ assert!(err.to_string().contains("invalid patch"), "{err}");
326
+ let err = apply(br#"{}"#, br#"[{"op":"remove","path":"/nope"}]"#).unwrap_err();
327
+ assert!(err.to_string().contains("apply failed"), "{err}");
328
+ }
329
+
330
+ #[test]
331
+ fn non_array_patch_is_invalid() {
332
+ let err = apply(br#"{}"#, br#"{"op":"replace","path":"/a","value":1}"#).unwrap_err();
333
+ assert!(err.to_string().contains("invalid patch"), "{err}");
334
+ }
335
+
336
+ #[test]
337
+ fn invalid_patch_operations_error() {
338
+ assert!(apply(br#"{"a":1}"#, br#"[{"op":"nonsense","path":"/a"}]"#).is_err());
339
+ assert!(apply(
340
+ br#"{"a":1}"#,
341
+ br#"[{"op":"replace","path":"/nope","value":2}]"#
342
+ )
343
+ .is_err());
344
+ }
345
+
346
+ // ── Size guards ─────────────────────────────────────────────
347
+
348
+ #[test]
349
+ fn input_size_guard_rejects_oversized() {
350
+ let big = vec![b' '; 1024];
351
+ let err = apply_json_patch_bytes_limited(&big, b"[]", 512, 4096).unwrap_err();
352
+ assert!(matches!(err, JsonPatchError::TooLarge("document", n) if n == 1024));
353
+ let err = apply_json_patch_bytes_limited(br#"{}"#, &big, 512, 4096).unwrap_err();
354
+ assert!(matches!(err, JsonPatchError::TooLarge("patch", n) if n == 1024));
355
+ }
356
+
357
+ #[test]
358
+ fn output_size_guard_rejects_copy_amplification() {
359
+ // A `copy` that duplicates a subtree doubles the output; with a tight
360
+ // cap the bounded writer must refuse to materialize it.
361
+ let doc = br#"{"a":{"x":[1,2,3,4,5],"y":"padding-for-size"}}"#;
362
+ let patch = br#"[{"op":"copy","from":"/a","path":"/b"}]"#;
363
+ let err = apply_json_patch_bytes_limited(doc, patch, 1024 * 1024, doc.len()).unwrap_err();
364
+ assert!(err.to_string().contains("exceeds maximum"), "{err}");
365
+ }
366
+
367
+ #[test]
368
+ fn output_size_guard_allows_at_limit_and_rejects_above() {
369
+ // `{"a":1}` serializes to exactly 7 bytes.
370
+ let ok = apply_json_patch_bytes_limited(br#"{"a":1}"#, b"[]", 1024, 7).unwrap();
371
+ assert_eq!(ok.as_slice(), b"{\"a\":1}");
372
+ let err = apply_json_patch_bytes_limited(br#"{"a":1}"#, b"[]", 1024, 6).unwrap_err();
373
+ assert!(err.to_string().contains("exceeds maximum"), "{err}");
374
+ }
375
+
376
+ // ── Batch ───────────────────────────────────────────────────
377
+
378
+ #[test]
379
+ fn batch_matches_scalar_parity() {
380
+ let docs: Vec<&[u8]> = vec![
381
+ br#"{"a":1}"#,
382
+ br#"{"items":["x","y"]}"#,
383
+ br#"{"name":"alice"}"#,
384
+ ];
385
+ let patches: Vec<&[u8]> = vec![
386
+ br#"[{"op":"replace","path":"/a","value":2}]"#,
387
+ br#"[{"op":"add","path":"/items/-","value":"z"}]"#,
388
+ br#"[]"#,
389
+ ];
390
+ let results = run_json_patch_batch(&docs, &patches).unwrap();
391
+ assert_eq!(results.len(), 3);
392
+ for ((doc, patch), result) in docs.iter().zip(patches.iter()).zip(results.iter()) {
393
+ assert_eq!(result, &apply(doc, patch).unwrap(), "batch item mismatch");
394
+ }
395
+ }
396
+
397
+ #[test]
398
+ fn batch_fails_fast_with_index() {
399
+ let docs: Vec<&[u8]> = vec![br#"{"a":1}"#, b"not-json", br#"{"c":3}"#];
400
+ let patches: Vec<&[u8]> = vec![br#"[]"#, br#"[]"#, br#"[]"#];
401
+ let err = run_json_patch_batch(&docs, &patches).unwrap_err();
402
+ let msg = err.to_string();
403
+ assert!(msg.contains("item 1"), "{msg}");
404
+ }
405
+
406
+ #[test]
407
+ fn batch_empty_inputs() {
408
+ let results = run_json_patch_batch(&[], &[]).unwrap();
409
+ assert!(results.is_empty());
410
+ }
411
+
412
+ /// Number of items that reliably crosses `should_parallelize`'s item
413
+ /// threshold regardless of the ambient rayon pool size, forcing the
414
+ /// rayon-parallel branch of `run_json_patch_batch`.
415
+ fn parallel_batch_len() -> usize {
416
+ rayon::current_num_threads().max(1).saturating_mul(2048) + 128
417
+ }
418
+
419
+ #[test]
420
+ fn batch_parallel_path_order_preserving() {
421
+ // All-success batch above the parallel threshold: results must be
422
+ // order-preserving and byte-identical to the scalar application.
423
+ let n = parallel_batch_len();
424
+ let mut docs = Vec::with_capacity(n);
425
+ let mut patches = Vec::with_capacity(n);
426
+ for i in 0..n {
427
+ docs.push(format!("{{\"i\":{i}}}").into_bytes());
428
+ patches.push(format!("[{{\"op\":\"add\",\"path\":\"/k\",\"value\":{i}}}]").into_bytes());
429
+ }
430
+ let doc_refs: Vec<&[u8]> = docs.iter().map(|d| d.as_slice()).collect();
431
+ let patch_refs: Vec<&[u8]> = patches.iter().map(|p| p.as_slice()).collect();
432
+
433
+ let results = run_json_patch_batch(&doc_refs, &patch_refs).unwrap();
434
+ assert_eq!(results.len(), n);
435
+ for (i, result) in results.iter().enumerate() {
436
+ let v: Value = sonic_rs::from_slice(result).unwrap();
437
+ assert_eq!(v["i"], i as i64, "item {i} content");
438
+ assert_eq!(v["k"], i as i64, "item {i} patched value");
439
+ }
440
+ }
441
+
442
+ #[test]
443
+ fn batch_parallel_path_fail_index_deterministic() {
444
+ // Exactly ONE item is corrupt, at a known index in the middle of a
445
+ // parallel-sized batch. Because only that item fails, the index-bearing
446
+ // error must deterministically report it (rayon short-circuits, but the
447
+ // sole Err can only carry that item's index).
448
+ let n = parallel_batch_len();
449
+ let mut docs = Vec::with_capacity(n);
450
+ let mut patches = Vec::with_capacity(n);
451
+ for i in 0..n {
452
+ docs.push(format!("{{\"i\":{i}}}").into_bytes());
453
+ patches.push(format!("[{{\"op\":\"add\",\"path\":\"/k\",\"value\":{i}}}]").into_bytes());
454
+ }
455
+ let bad_index = n / 2;
456
+ docs[bad_index] = b"not-json".to_vec();
457
+
458
+ let doc_refs: Vec<&[u8]> = docs.iter().map(|d| d.as_slice()).collect();
459
+ let patch_refs: Vec<&[u8]> = patches.iter().map(|p| p.as_slice()).collect();
460
+
461
+ let err = run_json_patch_batch(&doc_refs, &patch_refs).unwrap_err();
462
+ let msg = err.to_string();
463
+ assert!(msg.contains(&format!("item {bad_index}")), "{msg}");
464
+ assert!(msg.contains("invalid document"), "{msg}");
465
+ }
466
+
467
+ // ── napi surface ────────────────────────────────────────────
468
+
469
+ #[test]
470
+ fn napi_surface_roundtrip() {
471
+ let out = json_patch(
472
+ Uint8Array::new(br#"{"name":"alice","age":30}"#.to_vec()),
473
+ Uint8Array::new(br#"[{"op":"replace","path":"/age","value":31}]"#.to_vec()),
474
+ )
475
+ .unwrap();
476
+ let result: Value = sonic_rs::from_slice(out.as_ref()).unwrap();
477
+ assert_eq!(result["age"], 31);
478
+ }
479
+
480
+ #[test]
481
+ fn napi_batch_surface_packs_results() {
482
+ fn packed(items: &[&[u8]]) -> Vec<u8> {
483
+ let mut out = Vec::new();
484
+ out.extend_from_slice(&(items.len() as u32).to_le_bytes());
485
+ for item in items {
486
+ out.extend_from_slice(&(item.len() as u32).to_le_bytes());
487
+ out.extend_from_slice(item);
488
+ }
489
+ out
490
+ }
491
+
492
+ let docs = packed(&[br#"{"a":1}"#, br#"{"b":2}"#]);
493
+ let patches = packed(&[br#"[{"op":"replace","path":"/a","value":10}]"#, br#"[]"#]);
494
+ let out = json_patch_batch_packed(Uint8Array::new(docs), Uint8Array::new(patches)).unwrap();
495
+ let items = unpack(out.as_ref()).unwrap();
496
+ assert_eq!(items.len(), 2);
497
+ assert_eq!(items[0], &b"{\"a\":10}"[..]);
498
+ assert_eq!(items[1], &b"{\"b\":2}"[..]);
499
+ }
package/rust/lib.rs ADDED
@@ -0,0 +1,73 @@
1
+ //! # castrum — Rust acceleration addon (napi-rs cdylib)
2
+ //!
3
+ //! One cdylib crate decomposed into domain folders (`util`, `http`, `crypto`,
4
+ //! `json`, `payload`, `ingress`). Modules keep a pure-Rust core (no napi types
5
+ //! in signatures) so they stay testable and composable; only the `#[napi]`
6
+ //! entry points use napi types. See the module map below and `docs/REPO_MAP.md`.
7
+
8
+ // The napi `Uint8Array` exposes its backing JS-buffer memory to Rust through
9
+ // `as_mut()`. The pattern is deliberate and audited: the caller-provided
10
+ // buffer is only borrowed for the duration of the call, aliasing with the
11
+ // input/body is checked via `slices_overlap`, and every writer is
12
+ // capacity-checked (`ingress/output.rs` panics on overflow, which napi
13
+ // converts to a JS 500). Do not widen the unsafe surface beyond these sites
14
+ // without re-auditing.
15
+
16
+ use mimalloc::MiMalloc;
17
+
18
+ #[global_allocator]
19
+ static GLOBAL: MiMalloc = MiMalloc;
20
+
21
+ // ── Module map ────────────────────────────────────────────────────
22
+ // rust/ is ONE cdylib crate, decomposed into domain folders. Modules keep a
23
+ // pure-Rust core (no napi types in signatures) so they stay testable and
24
+ // composable; only the entry points use napi types.
25
+ //
26
+ // util/ shared infrastructure: bytes, packed iterators/writers,
27
+ // batch/ (packed routing + napi) + batch_core (rayon helpers),
28
+ // threadpool, validation. The `util::*` re-exports keep legacy
29
+ // call sites.
30
+ // http/ HTTP wire formats & parsing: headers, method, http/cookie/
31
+ // query parsers, form, media_type, url_codec, url_join, etag,
32
+ // http_date, accept, mime_lookup, multipart.
33
+ // crypto/ auth & hashing: hmac_sha256, cookie_sign, csrf, jwt/ (token +
34
+ // api), aead, argon2, base64, hashing (fnv/crc32/xxh3),
35
+ // random_token.
36
+ // json/ JSON & schema: json_ops, json_ser, patch/ (pointer/ops/engine/
37
+ // api), json_schema (napi) + fast_schema (zero-DOM engine).
38
+ // payload/ output & streaming: compress, sse, ws_frames, websocket,
39
+ // template.
40
+ // ingress/ the ingress pipeline: api.rs (Ingress class + IngressInner),
41
+ // pipeline, options/time/packed submodules, cors, proxy,
42
+ // ip_trust, rate_limit, terminal, output (single numeric layout
43
+ // source), ingress_constants (napi projection of output).
44
+ // metrics/ sharded counters/gauges/histograms registry + Prometheus
45
+ // render: registry (pure core) + api (napi MetricsRegistry
46
+ // class); C-ABI exports live in ffi/metrics.rs.
47
+ // ffi/ the C-ABI surface for bun:ffi, split by domain: util (shared
48
+ // helpers), hashing, validators, crypto, jwt, http, payload,
49
+ // json, rate_limit, ingress, route, metrics, probe (bench-only).
50
+ //
51
+ // Hot-path napi APIs (do not remove): ingress::handle_request_packed,
52
+ // ingress::handle_request_full_sync{,_into}, util::init_thread_pool,
53
+ // util::batch, ingress::ingress_constants.
54
+ pub mod crypto;
55
+ pub mod ffi;
56
+ pub mod http;
57
+ pub mod ingress;
58
+ pub mod json;
59
+ pub mod metrics;
60
+ pub mod payload;
61
+ pub mod selection;
62
+ pub mod task;
63
+ pub mod util;
64
+
65
+ // ── Unit tests (cargo test) ───────────────────────────────────────
66
+ #[cfg(test)]
67
+ mod test_support;
68
+
69
+ #[cfg(test)]
70
+ mod panic_safety;
71
+
72
+ #[cfg(test)]
73
+ mod proptest_suite;