castrum 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2259 -0
- package/Cargo.toml +138 -0
- package/README.md +6 -0
- package/build.rs +5 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1348 -1295
- package/dist/index.js +4307 -3776
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +16 -0
- package/docs/FFI_BUN_GUIDE.md +89 -6
- package/docs/REPO_MAP.md +2 -2
- package/index.ts +19 -25
- package/package.json +9 -1
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +96 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +68 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +132 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +180 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +779 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +566 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +881 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +72 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +407 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +104 -8
- package/src/native/ffi/build/compress.ts +14 -10
- package/src/native/ffi/build/instances.ts +40 -3
- package/src/native/ffi/build/metrics.ts +224 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/util.ts +14 -1
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +10 -3
- package/src/native/ffi/types.ts +111 -9
- package/src/native/ffi.ts +87 -1
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +2 -2
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +17 -11
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +5 -4
- package/src/shared/codec.ts +18 -3
package/rust/json/mod.rs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//! JSON & schema (zero-DOM validate/sum + DOM parse + fast schema engine).
|
|
2
|
+
//
|
|
3
|
+
// - json_ops.rs zero-DOM validate/sum + DOM parse
|
|
4
|
+
// - json_ser.rs zero-alloc JSON escaping + cookie/query → JSON writers
|
|
5
|
+
// - patch/ RFC 6902 JSON patch (pointer/ops/engine/api)
|
|
6
|
+
// - json_schema.rs SchemaValidator napi class (fast + fallback)
|
|
7
|
+
// - napi_marshal.rs sonic_rs::Value → JS marshaling + sonic values_equal
|
|
8
|
+
// (serde_json::Value DOM swap; jsonschema-compatible
|
|
9
|
+
// equality for fast_schema enum/const/uniqueItems)
|
|
10
|
+
// - fast_schema/ zero-DOM JSON Schema fast path (compiles the common
|
|
11
|
+
// keyword subset into a FastNode AST; unsupported
|
|
12
|
+
// keywords fall back to the jsonschema crate DOM path)
|
|
13
|
+
|
|
14
|
+
pub mod fast_schema;
|
|
15
|
+
pub mod json_ops;
|
|
16
|
+
pub mod json_schema;
|
|
17
|
+
pub mod json_ser;
|
|
18
|
+
pub mod napi_marshal;
|
|
19
|
+
pub mod patch;
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
//! sonic-rs → JS value marshaling + jsonschema-compatible equality.
|
|
2
|
+
//!
|
|
3
|
+
//! `serde_json::Value` DOMs are being replaced with `sonic_rs::Value` (compact
|
|
4
|
+
//! 16-byte tagged nodes, borrowed/inline strings, zero per-key heap `String`)
|
|
5
|
+
//! across the crate. The napi `serde-json` feature only marshals
|
|
6
|
+
//! `serde_json::Value` to JS, so this module supplies the hand-written
|
|
7
|
+
//! equivalent for `sonic_rs::Value`:
|
|
8
|
+
//!
|
|
9
|
+
//! - [`sonic_value_to_js`] walks a `sonic_rs::Value` directly into a JS value
|
|
10
|
+
//! (no `serde_json::Value` intermediate, no generic serde trait dispatch).
|
|
11
|
+
//! Number handling is byte-parity with the napi `serde-json` `ToNapiValue
|
|
12
|
+
//! for serde_json::Number`: `PosInt` u64 ≤ u32::MAX → **Number**, larger →
|
|
13
|
+
//! **BigInt**; `NegInt` i64 within ±2^53-1 → **Number**, outside → **BigInt**;
|
|
14
|
+
//! `Float` → **Number** (see [`sonic_number_to_js`]).
|
|
15
|
+
//! - [`sonic_values_equal`] replicates jsonschema's `cmp::equal` (exact
|
|
16
|
+
//! cross-type numeric equality via `num_cmp`, insertion-order object keys)
|
|
17
|
+
//! for the fast_schema enum/const/uniqueItems paths.
|
|
18
|
+
//!
|
|
19
|
+
//! This module is napi-type-laden by design (it bridges to JS); pure numeric /
|
|
20
|
+
//! equality logic stays in the small private fns below and is unit-tested.
|
|
21
|
+
|
|
22
|
+
use napi::bindgen_prelude::{
|
|
23
|
+
BigInt, Env, FromNapiValue, Null, Object, Result, ToNapiValue, Unknown,
|
|
24
|
+
};
|
|
25
|
+
use sonic_rs::JsonNumberTrait;
|
|
26
|
+
|
|
27
|
+
/// Wrap a `ToNapiValue` as an unconstrained-lifetime `Unknown`.
|
|
28
|
+
///
|
|
29
|
+
/// `Unknown<'env>` cannot borrow from the `Env` function parameter (that local
|
|
30
|
+
/// borrow can't escape the call), so we build the raw `napi_value` and re-wrap
|
|
31
|
+
/// via `Unknown::from_napi_value(env.raw(), …)` — the raw `napi_env`/`napi_value`
|
|
32
|
+
/// are Copy pointers rooted by the caller's handle scope, so the `'static`
|
|
33
|
+
/// phantom is nominal (the same pattern napi-rs uses for
|
|
34
|
+
/// `#[napi] fn(env: Env) -> Result<Unknown>`).
|
|
35
|
+
#[inline]
|
|
36
|
+
fn to_unknown(env: &Env, v: impl ToNapiValue) -> Result<Unknown<'static>> {
|
|
37
|
+
// Safety: `env.raw()` is a valid napi_env for the call, and `v`'s JS value
|
|
38
|
+
// is rooted in the caller's handle scope.
|
|
39
|
+
let raw = unsafe { ToNapiValue::to_napi_value(env.raw(), v) }?;
|
|
40
|
+
// Safety: same env + the just-created value are valid.
|
|
41
|
+
unsafe { Unknown::from_napi_value(env.raw(), raw) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Marshal a `sonic_rs::Value` into a JS value.
|
|
45
|
+
///
|
|
46
|
+
/// The returned JS value is fully independent of `value` (napi builds new
|
|
47
|
+
/// primitives/containers), so `value` may borrow from a caller buffer that is
|
|
48
|
+
/// released after the call.
|
|
49
|
+
pub(crate) fn sonic_value_to_js(env: &Env, value: &sonic_rs::Value) -> Result<Unknown<'static>> {
|
|
50
|
+
match value.as_ref() {
|
|
51
|
+
sonic_rs::value::ValueRef::Null => to_unknown(env, Null),
|
|
52
|
+
sonic_rs::value::ValueRef::Bool(b) => to_unknown(env, b),
|
|
53
|
+
sonic_rs::value::ValueRef::Number(n) => sonic_number_to_js(env, &n),
|
|
54
|
+
sonic_rs::value::ValueRef::String(s) => to_unknown(env, env.create_string(s)?),
|
|
55
|
+
sonic_rs::value::ValueRef::Array(arr) => {
|
|
56
|
+
let mut out = env.create_array(arr.len() as u32)?;
|
|
57
|
+
for (i, item) in arr.into_iter().enumerate() {
|
|
58
|
+
out.set(i as u32, sonic_value_to_js(env, item)?)?;
|
|
59
|
+
}
|
|
60
|
+
to_unknown(env, out)
|
|
61
|
+
}
|
|
62
|
+
sonic_rs::value::ValueRef::Object(obj) => {
|
|
63
|
+
let mut out = Object::new(env)?;
|
|
64
|
+
for (k, v) in obj.iter() {
|
|
65
|
+
out.set(k, sonic_value_to_js(env, v)?)?;
|
|
66
|
+
}
|
|
67
|
+
to_unknown(env, out)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Marshal a sonic `Number` to JS with **exact** parity to the napi
|
|
73
|
+
/// `serde-json` `ToNapiValue for serde_json::Number` (the behavior the old
|
|
74
|
+
/// `serde_json::Value` returns produced):
|
|
75
|
+
/// - PosInt (u64): `> u32::MAX` → **BigInt**, else → **Number** (the napi
|
|
76
|
+
/// serde-json impl routes u64 ≤ u32::MAX through `u32::to_napi_value`).
|
|
77
|
+
/// - NegInt (i64): outside ±2^53-1 (`MAX_SAFE_INT`) → **BigInt**, else →
|
|
78
|
+
/// **Number**.
|
|
79
|
+
/// - Float: **Number**.
|
|
80
|
+
///
|
|
81
|
+
/// `as_u64` is checked first so `PosInt` mirrors serde_json's u64-backed
|
|
82
|
+
/// `Number` (a positive integer that also fits i64 must still take the u64
|
|
83
|
+
/// branch — e.g. `3_000_000_000` is BigInt in serde_json, not Number).
|
|
84
|
+
fn sonic_number_to_js(env: &Env, n: &sonic_rs::Number) -> Result<Unknown<'static>> {
|
|
85
|
+
const MAX_SAFE_INT: i64 = 9007199254740991; // 2^53 - 1 (napi `napi6` guard)
|
|
86
|
+
if let Some(u) = n.as_u64() {
|
|
87
|
+
if u > u32::MAX as u64 {
|
|
88
|
+
to_unknown(env, BigInt::from(u))
|
|
89
|
+
} else {
|
|
90
|
+
to_unknown(env, u as u32)
|
|
91
|
+
}
|
|
92
|
+
} else if let Some(i) = n.as_i64() {
|
|
93
|
+
if !(-MAX_SAFE_INT..=MAX_SAFE_INT).contains(&i) {
|
|
94
|
+
to_unknown(env, BigInt::from(i))
|
|
95
|
+
} else {
|
|
96
|
+
to_unknown(env, i)
|
|
97
|
+
}
|
|
98
|
+
} else if let Some(f) = n.as_f64() {
|
|
99
|
+
to_unknown(env, f)
|
|
100
|
+
} else {
|
|
101
|
+
// Unreachable for a parsed JSON number (all three are mutually
|
|
102
|
+
// exhaustive over PosInt/NegInt/Float); defensive fallback.
|
|
103
|
+
to_unknown(env, Null)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// JSON value equality replicating jsonschema's `cmp::equal` (exact across
|
|
108
|
+
/// u64/i64/f64 via `num_cmp`, insertion-order object keys). This is the sonic
|
|
109
|
+
/// `Value` twin of the serde_json `values_equal` in fast_schema/validate.rs —
|
|
110
|
+
/// keep the two semantically identical (the fast path must stay byte-parity
|
|
111
|
+
/// with the jsonschema crate).
|
|
112
|
+
pub(crate) fn sonic_values_equal(a: &sonic_rs::Value, b: &sonic_rs::Value) -> bool {
|
|
113
|
+
use sonic_rs::value::ValueRef;
|
|
114
|
+
match (a.as_ref(), b.as_ref()) {
|
|
115
|
+
(ValueRef::Null, ValueRef::Null) => true,
|
|
116
|
+
(ValueRef::Bool(x), ValueRef::Bool(y)) => x == y,
|
|
117
|
+
(ValueRef::Number(x), ValueRef::Number(y)) => sonic_numbers_equal(&x, &y),
|
|
118
|
+
(ValueRef::String(x), ValueRef::String(y)) => x == y,
|
|
119
|
+
(ValueRef::Array(x), ValueRef::Array(y)) => {
|
|
120
|
+
x.len() == y.len()
|
|
121
|
+
&& x.iter()
|
|
122
|
+
.zip(y.iter())
|
|
123
|
+
.all(|(a, b)| sonic_values_equal(a, b))
|
|
124
|
+
}
|
|
125
|
+
(ValueRef::Object(x), ValueRef::Object(y)) => {
|
|
126
|
+
// Insertion-order comparison, replicating jsonschema's cmp::equal
|
|
127
|
+
// (key order is SIGNIFICANT in the crate's object equality).
|
|
128
|
+
x.len() == y.len()
|
|
129
|
+
&& x.iter()
|
|
130
|
+
.zip(y.iter())
|
|
131
|
+
.all(|((ka, va), (kb, vb))| ka == kb && sonic_values_equal(va, vb))
|
|
132
|
+
}
|
|
133
|
+
_ => false,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Exact cross-representation number equality (num_cmp semantics, matching
|
|
138
|
+
/// jsonschema's `equal_numbers` without arbitrary-precision).
|
|
139
|
+
fn sonic_numbers_equal(a: &sonic_rs::Number, b: &sonic_rs::Number) -> bool {
|
|
140
|
+
use num_cmp::NumCmp;
|
|
141
|
+
if let Some(x) = a.as_u64() {
|
|
142
|
+
if let Some(y) = b.as_u64() {
|
|
143
|
+
return x == y;
|
|
144
|
+
}
|
|
145
|
+
if let Some(y) = b.as_i64() {
|
|
146
|
+
return y >= 0 && x == y as u64;
|
|
147
|
+
}
|
|
148
|
+
return x.num_eq(b.as_f64().unwrap_or(f64::NAN));
|
|
149
|
+
}
|
|
150
|
+
if let Some(x) = a.as_i64() {
|
|
151
|
+
if let Some(y) = b.as_i64() {
|
|
152
|
+
return x == y;
|
|
153
|
+
}
|
|
154
|
+
if let Some(y) = b.as_u64() {
|
|
155
|
+
return x >= 0 && y == x as u64;
|
|
156
|
+
}
|
|
157
|
+
return x.num_eq(b.as_f64().unwrap_or(f64::NAN));
|
|
158
|
+
}
|
|
159
|
+
if let Some(x) = a.as_f64() {
|
|
160
|
+
if let Some(y) = b.as_f64() {
|
|
161
|
+
return x == y;
|
|
162
|
+
}
|
|
163
|
+
if let Some(y) = b.as_u64() {
|
|
164
|
+
return x.num_eq(y);
|
|
165
|
+
}
|
|
166
|
+
if let Some(y) = b.as_i64() {
|
|
167
|
+
return x.num_eq(y);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
false
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[cfg(test)]
|
|
174
|
+
mod tests {
|
|
175
|
+
use super::*;
|
|
176
|
+
use serde_json::json;
|
|
177
|
+
|
|
178
|
+
/// Round-trip a serde_json Value through JSON bytes into a sonic Value.
|
|
179
|
+
fn to_sonic(sv: &serde_json::Value) -> sonic_rs::Value {
|
|
180
|
+
sonic_rs::from_slice::<sonic_rs::Value>(&serde_json::to_vec(sv).unwrap()).unwrap()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// The serde_json reference implementation this port must match.
|
|
184
|
+
fn serde_values_equal(a: &serde_json::Value, b: &serde_json::Value) -> bool {
|
|
185
|
+
match (a, b) {
|
|
186
|
+
(serde_json::Value::Null, serde_json::Value::Null) => true,
|
|
187
|
+
(serde_json::Value::Bool(x), serde_json::Value::Bool(y)) => x == y,
|
|
188
|
+
(serde_json::Value::Number(x), serde_json::Value::Number(y)) => {
|
|
189
|
+
serde_numbers_equal(x, y)
|
|
190
|
+
}
|
|
191
|
+
(serde_json::Value::String(x), serde_json::Value::String(y)) => x == y,
|
|
192
|
+
(serde_json::Value::Array(x), serde_json::Value::Array(y)) => {
|
|
193
|
+
x.len() == y.len()
|
|
194
|
+
&& x.iter()
|
|
195
|
+
.zip(y.iter())
|
|
196
|
+
.all(|(a, b)| serde_values_equal(a, b))
|
|
197
|
+
}
|
|
198
|
+
(serde_json::Value::Object(x), serde_json::Value::Object(y)) => {
|
|
199
|
+
x.len() == y.len()
|
|
200
|
+
&& x.iter()
|
|
201
|
+
.zip(y.iter())
|
|
202
|
+
.all(|((ka, va), (kb, vb))| ka == kb && serde_values_equal(va, vb))
|
|
203
|
+
}
|
|
204
|
+
_ => false,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn serde_numbers_equal(a: &serde_json::Number, b: &serde_json::Number) -> bool {
|
|
209
|
+
use num_cmp::NumCmp;
|
|
210
|
+
if let Some(x) = a.as_u64() {
|
|
211
|
+
if let Some(y) = b.as_u64() {
|
|
212
|
+
return x == y;
|
|
213
|
+
}
|
|
214
|
+
if let Some(y) = b.as_i64() {
|
|
215
|
+
return y >= 0 && x == y as u64;
|
|
216
|
+
}
|
|
217
|
+
return x.num_eq(b.as_f64().unwrap_or(f64::NAN));
|
|
218
|
+
}
|
|
219
|
+
if let Some(x) = a.as_i64() {
|
|
220
|
+
if let Some(y) = b.as_i64() {
|
|
221
|
+
return x == y;
|
|
222
|
+
}
|
|
223
|
+
if let Some(y) = b.as_u64() {
|
|
224
|
+
return x >= 0 && y == x as u64;
|
|
225
|
+
}
|
|
226
|
+
return x.num_eq(b.as_f64().unwrap_or(f64::NAN));
|
|
227
|
+
}
|
|
228
|
+
if let Some(x) = a.as_f64() {
|
|
229
|
+
if let Some(y) = b.as_f64() {
|
|
230
|
+
return x == y;
|
|
231
|
+
}
|
|
232
|
+
if let Some(y) = b.as_u64() {
|
|
233
|
+
return x.num_eq(y);
|
|
234
|
+
}
|
|
235
|
+
if let Some(y) = b.as_i64() {
|
|
236
|
+
return x.num_eq(y);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
false
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fn corpus() -> Vec<serde_json::Value> {
|
|
243
|
+
vec![
|
|
244
|
+
json!(null),
|
|
245
|
+
json!(true),
|
|
246
|
+
json!(false),
|
|
247
|
+
json!(0),
|
|
248
|
+
json!(1),
|
|
249
|
+
json!(-1),
|
|
250
|
+
json!(i64::MAX),
|
|
251
|
+
json!(i64::MIN),
|
|
252
|
+
json!(u64::MAX),
|
|
253
|
+
json!(1.5),
|
|
254
|
+
json!(-2.25),
|
|
255
|
+
json!(0.0),
|
|
256
|
+
json!(1e300),
|
|
257
|
+
json!(""),
|
|
258
|
+
json!("hello"),
|
|
259
|
+
json!("héllo wörld 😀"),
|
|
260
|
+
json!(r#"escaped "quotes" \n \u0041"#),
|
|
261
|
+
json!([]),
|
|
262
|
+
json!([1, "two", true, null, [3.5], {"k": "v"}]),
|
|
263
|
+
json!({"a": 1, "b": [1, 2, 3], "c": {"d": null, "e": "x"}}),
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[test]
|
|
268
|
+
fn sonic_values_equal_matches_serde_reference() {
|
|
269
|
+
let c = corpus();
|
|
270
|
+
for a in &c {
|
|
271
|
+
for b in &c {
|
|
272
|
+
let sa = to_sonic(a);
|
|
273
|
+
let sb = to_sonic(b);
|
|
274
|
+
let sonic = sonic_values_equal(&sa, &sb);
|
|
275
|
+
let serde = serde_values_equal(a, b);
|
|
276
|
+
assert_eq!(
|
|
277
|
+
sonic, serde,
|
|
278
|
+
"mismatch: {a} vs {b} (sonic={sonic}, serde={serde})"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[test]
|
|
285
|
+
fn numeric_cross_type_equality() {
|
|
286
|
+
// 1 == 1.0 across representations (jsonschema cmp::equal).
|
|
287
|
+
let one_int: serde_json::Value = json!(1);
|
|
288
|
+
let one_float: serde_json::Value = json!(1.0);
|
|
289
|
+
assert!(sonic_values_equal(
|
|
290
|
+
&to_sonic(&one_int),
|
|
291
|
+
&to_sonic(&one_float)
|
|
292
|
+
));
|
|
293
|
+
|
|
294
|
+
// u64 that exceeds i64::MAX is NOT equal to its wrapped i64.
|
|
295
|
+
let big: serde_json::Value = json!(u64::MAX);
|
|
296
|
+
let neg: serde_json::Value = json!(-1);
|
|
297
|
+
assert!(!sonic_values_equal(&to_sonic(&big), &to_sonic(&neg)));
|
|
298
|
+
|
|
299
|
+
// Exact fractional equality (1.5 != 1.5000000000000001 differs).
|
|
300
|
+
let a: serde_json::Value = json!(1.5);
|
|
301
|
+
let b: serde_json::Value = json!(1.5000000000000002);
|
|
302
|
+
assert!(!sonic_values_equal(&to_sonic(&a), &to_sonic(&b)));
|
|
303
|
+
|
|
304
|
+
// String vs number never equal.
|
|
305
|
+
assert!(!sonic_values_equal(
|
|
306
|
+
&to_sonic(&json!("1")),
|
|
307
|
+
&to_sonic(&json!(1))
|
|
308
|
+
));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#[test]
|
|
312
|
+
fn object_key_order_is_significant() {
|
|
313
|
+
let a = json!({"a": 1, "b": 2});
|
|
314
|
+
let b = json!({"b": 2, "a": 1});
|
|
315
|
+
assert!(sonic_values_equal(&to_sonic(&a), &to_sonic(&a)));
|
|
316
|
+
assert!(!sonic_values_equal(&to_sonic(&a), &to_sonic(&b)));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// rust/json/patch/api.rs — napi boundary for RFC 6902 JSON Patch.
|
|
2
|
+
//
|
|
3
|
+
// Thin `#[napi]` wrappers over the pure core in `engine.rs` that map
|
|
4
|
+
// `JsonPatchError` → napi `Error` with stable context.
|
|
5
|
+
|
|
6
|
+
use napi::bindgen_prelude::*;
|
|
7
|
+
use napi_derive::napi;
|
|
8
|
+
|
|
9
|
+
use crate::util::unpack;
|
|
10
|
+
|
|
11
|
+
use super::engine::{apply_json_patch_bytes, run_json_patch_batch};
|
|
12
|
+
|
|
13
|
+
/// Apply an RFC 6902 JSON Patch to a JSON document.
|
|
14
|
+
///
|
|
15
|
+
/// `doc` and `patch` are raw JSON byte buffers; the patched document is
|
|
16
|
+
/// returned as JSON bytes. Throws on invalid JSON or an inapplicable patch.
|
|
17
|
+
#[napi]
|
|
18
|
+
pub fn json_patch(doc: Uint8Array, patch: Uint8Array) -> Result<Buffer> {
|
|
19
|
+
apply_json_patch_bytes(doc.as_ref(), patch.as_ref())
|
|
20
|
+
.map(Buffer::from)
|
|
21
|
+
.map_err(|e| Error::from_reason(e.to_string()))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Apply a batch of RFC 6902 patches (packed in → packed out).
|
|
25
|
+
///
|
|
26
|
+
/// `docs` and `patches` are packed buffers (`[u32 count] { [u32 len] [bytes] }`)
|
|
27
|
+
/// of equal length, zipped per index. Returns packed results
|
|
28
|
+
/// (`[u32 count] { [u32 len] [bytes] }`). **Fail-fast**: an invalid item aborts
|
|
29
|
+
/// the whole batch with an index-bearing error — unlike the bitset-style batch
|
|
30
|
+
/// helpers, silently dropping a patch would risk silent data loss.
|
|
31
|
+
#[napi]
|
|
32
|
+
pub fn json_patch_batch_packed(docs: Uint8Array, patches: Uint8Array) -> Result<Buffer> {
|
|
33
|
+
let doc_items = unpack(docs.as_ref()).map_err(|e| Error::from_reason(e.to_string()))?;
|
|
34
|
+
let patch_items = unpack(patches.as_ref()).map_err(|e| Error::from_reason(e.to_string()))?;
|
|
35
|
+
|
|
36
|
+
if doc_items.len() != patch_items.len() {
|
|
37
|
+
return Err(Error::from_reason(format!(
|
|
38
|
+
"json patch batch: docs count {} != patches count {}",
|
|
39
|
+
doc_items.len(),
|
|
40
|
+
patch_items.len()
|
|
41
|
+
)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if doc_items.is_empty() {
|
|
45
|
+
// Packed empty result: `[u32 count = 0]`.
|
|
46
|
+
return Ok(Buffer::from(vec![0u8; 4]));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let results = run_json_patch_batch(&doc_items, &patch_items)
|
|
50
|
+
.map_err(|e| Error::from_reason(e.to_string()))?;
|
|
51
|
+
|
|
52
|
+
let body: usize = results.iter().map(|result| result.len() + 4).sum();
|
|
53
|
+
let mut out = Vec::with_capacity(4 + body);
|
|
54
|
+
out.extend_from_slice(&(results.len() as u32).to_le_bytes());
|
|
55
|
+
for result in &results {
|
|
56
|
+
out.extend_from_slice(&(result.len() as u32).to_le_bytes());
|
|
57
|
+
out.extend_from_slice(result);
|
|
58
|
+
}
|
|
59
|
+
Ok(Buffer::from(out))
|
|
60
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// rust/json/patch/engine.rs — bounded RFC 6902 apply + parallel packed batch.
|
|
2
|
+
//
|
|
3
|
+
// The napi-free, unit-testable core: `apply_json_patch_bytes` (bounded scalar
|
|
4
|
+
// apply) and `run_json_patch_batch` (serial/rayon batch). The op semantics
|
|
5
|
+
// live in `ops.rs`; the napi boundary is in `api.rs`.
|
|
6
|
+
|
|
7
|
+
use crate::util::{should_parallelize, total_bytes};
|
|
8
|
+
|
|
9
|
+
use super::ops::{apply_patch, parse_patch};
|
|
10
|
+
|
|
11
|
+
/// Maximum accepted size of a single JSON document or patch (defense in depth
|
|
12
|
+
/// before parsing). Generous; only adversarial inputs trip it.
|
|
13
|
+
const MAX_JSON_PATCH_INPUT: usize = 64 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
/// Maximum serialized output size. A result guard: an oversized (e.g.
|
|
16
|
+
/// `copy`-amplified) result is rejected rather than returned to the caller.
|
|
17
|
+
const MAX_JSON_PATCH_OUTPUT: usize = 128 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
/// JSON Patch errors, rendered with stable, human-readable context.
|
|
20
|
+
#[derive(Debug)]
|
|
21
|
+
pub enum JsonPatchError {
|
|
22
|
+
/// The document bytes are not valid JSON.
|
|
23
|
+
InvalidDoc(String),
|
|
24
|
+
/// The patch bytes are not a valid RFC 6902 patch document.
|
|
25
|
+
InvalidPatch(String),
|
|
26
|
+
/// The patch could not be applied (test mismatch, missing path, type clash).
|
|
27
|
+
ApplyFailed(String),
|
|
28
|
+
/// An input or the serialized output exceeded the bounded-size guard.
|
|
29
|
+
TooLarge(&'static str, usize),
|
|
30
|
+
/// Output serialization failed (cannot happen for a valid `Value`).
|
|
31
|
+
Serialize(String),
|
|
32
|
+
/// Batch item `index` failed to apply.
|
|
33
|
+
BatchItem {
|
|
34
|
+
index: usize,
|
|
35
|
+
source: Box<JsonPatchError>,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl std::fmt::Display for JsonPatchError {
|
|
40
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
41
|
+
match self {
|
|
42
|
+
Self::InvalidDoc(e) => write!(f, "json patch: invalid document: {e}"),
|
|
43
|
+
Self::InvalidPatch(e) => write!(f, "json patch: invalid patch: {e}"),
|
|
44
|
+
Self::ApplyFailed(e) => write!(f, "json patch: apply failed: {e}"),
|
|
45
|
+
Self::TooLarge(kind, n) => {
|
|
46
|
+
write!(f, "json patch: {kind} size {n} exceeds maximum")
|
|
47
|
+
}
|
|
48
|
+
Self::Serialize(e) => write!(f, "json patch: serialize failed: {e}"),
|
|
49
|
+
Self::BatchItem { index, source } => {
|
|
50
|
+
write!(f, "json patch batch: item {index} failed: {source}")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Apply `patch` to `doc` (RFC 6902) and return the serialized result.
|
|
57
|
+
///
|
|
58
|
+
/// Pure-Rust core — no napi types, fully unit-testable. Tests exercise the size
|
|
59
|
+
/// guards via [`apply_json_patch_bytes_limited`] with small limits.
|
|
60
|
+
pub fn apply_json_patch_bytes(
|
|
61
|
+
doc: &[u8],
|
|
62
|
+
patch: &[u8],
|
|
63
|
+
) -> std::result::Result<Vec<u8>, JsonPatchError> {
|
|
64
|
+
apply_json_patch_bytes_limited(doc, patch, MAX_JSON_PATCH_INPUT, MAX_JSON_PATCH_OUTPUT)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub(crate) fn apply_json_patch_bytes_limited(
|
|
68
|
+
doc: &[u8],
|
|
69
|
+
patch: &[u8],
|
|
70
|
+
max_input: usize,
|
|
71
|
+
max_output: usize,
|
|
72
|
+
) -> std::result::Result<Vec<u8>, JsonPatchError> {
|
|
73
|
+
if doc.len() > max_input {
|
|
74
|
+
return Err(JsonPatchError::TooLarge("document", doc.len()));
|
|
75
|
+
}
|
|
76
|
+
if patch.len() > max_input {
|
|
77
|
+
return Err(JsonPatchError::TooLarge("patch", patch.len()));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let mut doc_val: sonic_rs::Value =
|
|
81
|
+
sonic_rs::from_slice(doc).map_err(|e| JsonPatchError::InvalidDoc(e.to_string()))?;
|
|
82
|
+
|
|
83
|
+
let ops = parse_patch(patch)?;
|
|
84
|
+
apply_patch(&mut doc_val, &ops).map_err(JsonPatchError::ApplyFailed)?;
|
|
85
|
+
|
|
86
|
+
// The overwhelmingly common case is an in-place field/array edit, which
|
|
87
|
+
// keeps the result ≈ the document size; grow only if the patch expands it.
|
|
88
|
+
// The output is then bounded as a *result guard*: the sonic DOM has
|
|
89
|
+
// already been (potentially `copy`-amplified), so refusing oversized
|
|
90
|
+
// output prevents handing a giant buffer back to the caller.
|
|
91
|
+
let mut out = Vec::with_capacity(doc.len().min(max_output));
|
|
92
|
+
sonic_rs::to_writer(&mut out, &doc_val)
|
|
93
|
+
.map_err(|e| JsonPatchError::Serialize(e.to_string()))?;
|
|
94
|
+
|
|
95
|
+
if out.len() > max_output {
|
|
96
|
+
return Err(JsonPatchError::TooLarge("output", out.len()));
|
|
97
|
+
}
|
|
98
|
+
Ok(out)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// The first failure encountered while processing a batch.
|
|
102
|
+
struct BatchFailure {
|
|
103
|
+
index: usize,
|
|
104
|
+
source: JsonPatchError,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Pure-Rust batch core: zip `docs` with `patches` and apply each pair.
|
|
108
|
+
///
|
|
109
|
+
/// Parallel via rayon when the workload justifies it; otherwise serial and
|
|
110
|
+
/// deterministic (first error wins). Errors are index-bearing
|
|
111
|
+
/// ([`JsonPatchError::BatchItem`]). Successful results are order-preserving.
|
|
112
|
+
pub fn run_json_patch_batch(
|
|
113
|
+
docs: &[&[u8]],
|
|
114
|
+
patches: &[&[u8]],
|
|
115
|
+
) -> std::result::Result<Vec<Vec<u8>>, JsonPatchError> {
|
|
116
|
+
debug_assert_eq!(docs.len(), patches.len());
|
|
117
|
+
|
|
118
|
+
if should_parallelize(docs.len(), total_bytes(docs)) {
|
|
119
|
+
use rayon::prelude::*;
|
|
120
|
+
let results: std::result::Result<Vec<Vec<u8>>, BatchFailure> = docs
|
|
121
|
+
.par_iter()
|
|
122
|
+
.zip(patches.par_iter())
|
|
123
|
+
.enumerate()
|
|
124
|
+
.map(
|
|
125
|
+
|(index, (doc, patch))| match apply_json_patch_bytes(doc, patch) {
|
|
126
|
+
Ok(result) => Ok(result),
|
|
127
|
+
Err(source) => Err(BatchFailure { index, source }),
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
.collect();
|
|
131
|
+
match results {
|
|
132
|
+
Ok(results) => Ok(results),
|
|
133
|
+
Err(BatchFailure { index, source }) => Err(JsonPatchError::BatchItem {
|
|
134
|
+
index,
|
|
135
|
+
source: Box::new(source),
|
|
136
|
+
}),
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
let mut results = Vec::with_capacity(docs.len());
|
|
140
|
+
for (index, (doc, patch)) in docs.iter().zip(patches.iter()).enumerate() {
|
|
141
|
+
match apply_json_patch_bytes(doc, patch) {
|
|
142
|
+
Ok(result) => results.push(result),
|
|
143
|
+
Err(source) => {
|
|
144
|
+
return Err(JsonPatchError::BatchItem {
|
|
145
|
+
index,
|
|
146
|
+
source: Box::new(source),
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
Ok(results)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//! RFC 6902 JSON Patch — bounded scalar apply + parallel packed batch.
|
|
2
|
+
//!
|
|
3
|
+
//! Pure-Rust core (`apply_json_patch_bytes`, `run_json_patch_batch`) is
|
|
4
|
+
//! napi-free and unit-testable; the `#[napi]` entry points (`json_patch`,
|
|
5
|
+
//! `json_patch_batch_packed`) are thin boundaries that map `JsonPatchError` →
|
|
6
|
+
//! napi `Error` with stable context.
|
|
7
|
+
//!
|
|
8
|
+
//! Folder layout:
|
|
9
|
+
//! - `pointer.rs` — RFC 6901 JSON Pointer parsing (escape rules).
|
|
10
|
+
//! - `ops.rs` — patch document parsing + RFC 6902 apply over sonic Value.
|
|
11
|
+
//! - `engine.rs` — bounded scalar apply + serial/rayon packed batch.
|
|
12
|
+
//! - `api.rs` — napi boundary.
|
|
13
|
+
//! - `tests.rs` — unit tests.
|
|
14
|
+
//!
|
|
15
|
+
//! **Sonic engine**: the `json-patch` crate is hardwired to `serde_json::Value`
|
|
16
|
+
//! (a per-key heap `String` DOM). This module implements RFC 6902 directly over
|
|
17
|
+
//! `sonic_rs::Value` (compact tagged nodes, no per-key heap `String`), so the
|
|
18
|
+
//! patch path no longer builds a `serde_json` DOM. RFC 6901 pointer escapes
|
|
19
|
+
//! (`~0`→`~`, `~1`→`/`) are validated eagerly at patch parse (bad escapes →
|
|
20
|
+
//! `InvalidPatch`, matching the crate's eager deserialization). The `test` op
|
|
21
|
+
//! uses sonic's `Value ==`, which has serde_json-identical number semantics
|
|
22
|
+
//! (`1 != 1.0`), preserving the crate's behavior.
|
|
23
|
+
//!
|
|
24
|
+
//! Hardening / efficiency notes:
|
|
25
|
+
//! - **Bounded sizes** — inputs are capped (`MAX_JSON_PATCH_INPUT`) and the
|
|
26
|
+
//! result is capped (`MAX_JSON_PATCH_OUTPUT`). A malicious chain of
|
|
27
|
+
//! RFC 6902 `copy` ops duplicates subtrees and can grow the output
|
|
28
|
+
//! exponentially relative to the input; the cap refuses to hand such output
|
|
29
|
+
//! back to the caller.
|
|
30
|
+
//! - **Capacity** — the common in-place field/array edit keeps the result ≈ the
|
|
31
|
+
//! document size, so serialization starts at `doc.len()` and grows only when
|
|
32
|
+
//! a patch genuinely expands the document (no 2× over-allocation for
|
|
33
|
+
//! `test`/`remove`-heavy patches).
|
|
34
|
+
//! - **Scalability** — `json_patch_batch_packed` zips two packed buffers and
|
|
35
|
+
//! applies patches in parallel via rayon when the workload justifies it
|
|
36
|
+
//! (`should_parallelize`), staying serial and deterministic otherwise.
|
|
37
|
+
|
|
38
|
+
mod api;
|
|
39
|
+
mod engine;
|
|
40
|
+
mod ops;
|
|
41
|
+
mod pointer;
|
|
42
|
+
|
|
43
|
+
#[cfg(test)]
|
|
44
|
+
mod tests;
|
|
45
|
+
|
|
46
|
+
pub use self::api::{json_patch, json_patch_batch_packed};
|
|
47
|
+
pub use self::engine::{apply_json_patch_bytes, run_json_patch_batch, JsonPatchError};
|