castrum 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
// rust/json/fast_schema/validate.rs — AST + raw bytes → validation (bool or
|
|
2
|
+
// detailed errors via an optional `Ctx` error sink).
|
|
3
|
+
//
|
|
4
|
+
// Walks a compiled `FastNode` over raw JSON bytes with a `Cursor`. In bool mode
|
|
5
|
+
// (`Ctx::bool_mode`) the walk is allocation-free for the core subset (matching
|
|
6
|
+
// the original `is_valid_bytes` hot path used by the ingress); in detailed mode
|
|
7
|
+
// it additionally collects `SchemaError`s (instance pointer + keyword + message).
|
|
8
|
+
|
|
9
|
+
use super::cursor::{count_chars, decode_string, Cursor};
|
|
10
|
+
use super::errors::Ctx;
|
|
11
|
+
use super::types::{
|
|
12
|
+
kind_of, Additional, FastArray, FastNode, FastObject, Kind, T_ARR, T_BOOL, T_INT, T_NULL,
|
|
13
|
+
T_NUM, T_OBJ, T_STR,
|
|
14
|
+
};
|
|
15
|
+
use fraction::One;
|
|
16
|
+
|
|
17
|
+
use crate::json::napi_marshal::sonic_values_equal;
|
|
18
|
+
|
|
19
|
+
impl FastNode {
|
|
20
|
+
pub(crate) fn any() -> Self {
|
|
21
|
+
Self::default()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub(crate) fn never() -> Self {
|
|
25
|
+
Self {
|
|
26
|
+
never: true,
|
|
27
|
+
..Self::default()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Validate a complete JSON document against this schema (bool mode).
|
|
32
|
+
#[inline]
|
|
33
|
+
pub fn is_valid_bytes(&self, bytes: &[u8]) -> bool {
|
|
34
|
+
let mut c = Cursor::new(bytes);
|
|
35
|
+
let mut ctx = Ctx::bool_mode();
|
|
36
|
+
if !self.validate(&mut c, &mut ctx) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
c.skip_ws();
|
|
40
|
+
c.pos == bytes.len()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Validate a complete JSON document against this schema while capturing
|
|
44
|
+
/// scalar values at `targets` during the SAME walk (one pass, no DOM).
|
|
45
|
+
///
|
|
46
|
+
/// Returns `(schema_valid, capture)`. When invalid, captured values are
|
|
47
|
+
/// meaningless (callers must reject); when valid, each target either
|
|
48
|
+
/// matched (range/length) or the path was absent from the document.
|
|
49
|
+
pub fn validate_with_capture(
|
|
50
|
+
&self,
|
|
51
|
+
bytes: &[u8],
|
|
52
|
+
targets: Vec<super::capture::TargetPath>,
|
|
53
|
+
) -> (bool, super::capture::Capture) {
|
|
54
|
+
let capture = super::capture::Capture::new(targets);
|
|
55
|
+
let mut c = Cursor::new(bytes);
|
|
56
|
+
let mut ctx = Ctx::bool_mode();
|
|
57
|
+
ctx.capture = Some(capture);
|
|
58
|
+
ctx.input_ptr = bytes.as_ptr();
|
|
59
|
+
let ok = self.validate(&mut c, &mut ctx);
|
|
60
|
+
c.skip_ws();
|
|
61
|
+
let ok = ok && c.pos == bytes.len();
|
|
62
|
+
// `capture` was set just above, so `take` is guaranteed Some.
|
|
63
|
+
let capture = ctx.capture.take().expect("capture set before validate");
|
|
64
|
+
(ok, capture)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Validate and return detailed errors (empty = valid). `max_errors` caps
|
|
68
|
+
/// the number of errors collected (1 = first error only).
|
|
69
|
+
pub fn validate_errors(
|
|
70
|
+
&self,
|
|
71
|
+
bytes: &[u8],
|
|
72
|
+
max_errors: usize,
|
|
73
|
+
) -> Vec<super::errors::SchemaError> {
|
|
74
|
+
let mut c = Cursor::new(bytes);
|
|
75
|
+
let mut ctx = Ctx::detailed(max_errors);
|
|
76
|
+
if self.validate(&mut c, &mut ctx) {
|
|
77
|
+
c.skip_ws();
|
|
78
|
+
if c.pos == bytes.len() {
|
|
79
|
+
return Vec::new();
|
|
80
|
+
}
|
|
81
|
+
ctx.record("", "parse", || {
|
|
82
|
+
"trailing data after the JSON value".to_string()
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
ctx.into_errors()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Validate `c` against this schema (bool or detailed mode). Every
|
|
89
|
+
/// recursion of the schema walk (members, tuple items, combinator
|
|
90
|
+
/// sub-schemas) routes through this method, so it is the single choke point
|
|
91
|
+
/// for the nesting-depth cap: `MAX_DEPTH` bounds how deep the walk can
|
|
92
|
+
/// recurse, preventing hostile deeply-nested JSON from exhausting the
|
|
93
|
+
/// native stack (an uncatchable abort — see `Ctx::enter_depth`).
|
|
94
|
+
fn validate(&self, c: &mut Cursor, ctx: &mut Ctx) -> bool {
|
|
95
|
+
if !ctx.enter_depth() {
|
|
96
|
+
ctx.record("", "maxDepth", || {
|
|
97
|
+
"maximum JSON nesting depth exceeded".to_string()
|
|
98
|
+
});
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
let ok = self.validate_inner(c, ctx);
|
|
102
|
+
ctx.leave_depth();
|
|
103
|
+
ok
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn validate_inner(&self, c: &mut Cursor, ctx: &mut Ctx) -> bool {
|
|
107
|
+
let base = self.schema_path.as_deref().unwrap_or("");
|
|
108
|
+
if self.never {
|
|
109
|
+
ctx.record(base, "false schema", || {
|
|
110
|
+
"the schema is false and rejects every value".to_string()
|
|
111
|
+
});
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
c.skip_ws();
|
|
115
|
+
let vstart = c.pos;
|
|
116
|
+
let Some(b) = c.peek() else {
|
|
117
|
+
ctx.record(base, "type", || {
|
|
118
|
+
"expected a JSON value but found end of input".to_string()
|
|
119
|
+
});
|
|
120
|
+
return false;
|
|
121
|
+
};
|
|
122
|
+
// 1. `type`
|
|
123
|
+
if self.types != 0 {
|
|
124
|
+
let Some(k) = kind_of(b) else {
|
|
125
|
+
ctx.record(base, "type", || "unexpected JSON token".to_string());
|
|
126
|
+
return false;
|
|
127
|
+
};
|
|
128
|
+
let ok = match k {
|
|
129
|
+
Kind::Number => self.types & (T_NUM | T_INT) != 0,
|
|
130
|
+
Kind::Null => self.types & T_NULL != 0,
|
|
131
|
+
Kind::Boolean => self.types & T_BOOL != 0,
|
|
132
|
+
Kind::String => self.types & T_STR != 0,
|
|
133
|
+
Kind::Array => self.types & T_ARR != 0,
|
|
134
|
+
Kind::Object => self.types & T_OBJ != 0,
|
|
135
|
+
};
|
|
136
|
+
if !ok {
|
|
137
|
+
ctx.record(base, "type", || {
|
|
138
|
+
format!(
|
|
139
|
+
"expected {} but found {}",
|
|
140
|
+
type_names(self.types),
|
|
141
|
+
kind_name(k)
|
|
142
|
+
)
|
|
143
|
+
});
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 2. `enum` / `const` — compare parsed values, matching the reference
|
|
148
|
+
// (exact number equality; object keys compared in insertion order,
|
|
149
|
+
// replicating jsonschema's cmp::equal).
|
|
150
|
+
if let Some(value) = &self.value {
|
|
151
|
+
let data = c.data;
|
|
152
|
+
if !c.skip_value(ctx.depth) {
|
|
153
|
+
ctx.record(base, "parse", || "malformed JSON value".to_string());
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
let raw = &data[vstart..c.pos];
|
|
157
|
+
let val: sonic_rs::Value = match sonic_rs::from_slice(raw) {
|
|
158
|
+
Ok(v) => v,
|
|
159
|
+
Err(_) => {
|
|
160
|
+
ctx.record(base, "parse", || "malformed JSON value".to_string());
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
let (keyword, ok, message) = match value {
|
|
165
|
+
super::types::ValueConstraint::Enum(members) => (
|
|
166
|
+
"enum",
|
|
167
|
+
members.iter().any(|m| sonic_values_equal(m, &val)),
|
|
168
|
+
"value is not one of the allowed enum values",
|
|
169
|
+
),
|
|
170
|
+
super::types::ValueConstraint::Const(x) => (
|
|
171
|
+
"const",
|
|
172
|
+
sonic_values_equal(x, &val),
|
|
173
|
+
"value is not equal to the required constant",
|
|
174
|
+
),
|
|
175
|
+
};
|
|
176
|
+
if !ok {
|
|
177
|
+
ctx.record(base, keyword, || message.to_string());
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
// Run the structural + combinator constraints over the same value.
|
|
181
|
+
let mut c2 = Cursor::new(raw);
|
|
182
|
+
return self.validate_kind(&mut c2, ctx) && self.validate_comb_raw(raw, ctx);
|
|
183
|
+
}
|
|
184
|
+
// 3. kind-specific structural constraints
|
|
185
|
+
if !self.validate_kind(c, ctx) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
// 4. combinators over the same value (re-scanned from vstart)
|
|
189
|
+
let data = c.data;
|
|
190
|
+
let raw = &data[vstart..c.pos];
|
|
191
|
+
self.validate_comb_raw(raw, ctx)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn validate_kind(&self, c: &mut Cursor, ctx: &mut Ctx) -> bool {
|
|
195
|
+
let base = self.schema_path.as_deref().unwrap_or("");
|
|
196
|
+
let Some(b) = c.peek() else {
|
|
197
|
+
ctx.record(base, "type", || "expected a JSON value".to_string());
|
|
198
|
+
return false;
|
|
199
|
+
};
|
|
200
|
+
match b {
|
|
201
|
+
b'{' => match &self.obj {
|
|
202
|
+
Some(o) => validate_object(o, c, ctx, base),
|
|
203
|
+
None => c.skip_value(ctx.depth),
|
|
204
|
+
},
|
|
205
|
+
b'[' => match &self.arr {
|
|
206
|
+
Some(a) => validate_array(a, c, ctx, base),
|
|
207
|
+
None => c.skip_value(ctx.depth),
|
|
208
|
+
},
|
|
209
|
+
b'"' => {
|
|
210
|
+
let Some(inner) = c.raw_string() else {
|
|
211
|
+
ctx.record(base, "parse", || "malformed string".to_string());
|
|
212
|
+
return false;
|
|
213
|
+
};
|
|
214
|
+
match &self.str {
|
|
215
|
+
Some(_) => self.validate_string(inner, base, ctx),
|
|
216
|
+
None => true,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
b't' => {
|
|
220
|
+
if !c.eat_token(b"true") {
|
|
221
|
+
ctx.record(base, "parse", || "malformed literal".to_string());
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
true
|
|
225
|
+
}
|
|
226
|
+
b'f' => {
|
|
227
|
+
if !c.eat_token(b"false") {
|
|
228
|
+
ctx.record(base, "parse", || "malformed literal".to_string());
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
true
|
|
232
|
+
}
|
|
233
|
+
b'n' => {
|
|
234
|
+
if !c.eat_token(b"null") {
|
|
235
|
+
ctx.record(base, "parse", || "malformed literal".to_string());
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
true
|
|
239
|
+
}
|
|
240
|
+
_ => {
|
|
241
|
+
let Some((n, _is_int)) = c.number() else {
|
|
242
|
+
ctx.record(base, "parse", || "malformed number".to_string());
|
|
243
|
+
return false;
|
|
244
|
+
};
|
|
245
|
+
// `type: "integer"` (without `number`) rejects non-integral values.
|
|
246
|
+
if self.types & T_INT != 0 && self.types & T_NUM == 0 && n.fract() != 0.0 {
|
|
247
|
+
ctx.record(base, "type", || "expected an integer".to_string());
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
match &self.num {
|
|
251
|
+
Some(num) => {
|
|
252
|
+
if let Some(min) = num.minimum {
|
|
253
|
+
if n < min {
|
|
254
|
+
ctx.record(base, "minimum", || format!("number must be >= {min}"));
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if let Some(max) = num.maximum {
|
|
259
|
+
if n > max {
|
|
260
|
+
ctx.record(base, "maximum", || format!("number must be <= {max}"));
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if let Some(e) = num.exclusive_min {
|
|
265
|
+
if n <= e {
|
|
266
|
+
ctx.record(base, "exclusiveMinimum", || {
|
|
267
|
+
format!("number must be > {e}")
|
|
268
|
+
});
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if let Some(e) = num.exclusive_max {
|
|
273
|
+
if n >= e {
|
|
274
|
+
ctx.record(base, "exclusiveMaximum", || {
|
|
275
|
+
format!("number must be < {e}")
|
|
276
|
+
});
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if let Some(m) = num.multiple_of {
|
|
281
|
+
if !is_multiple_of(n, m) {
|
|
282
|
+
ctx.record(base, "multipleOf", || {
|
|
283
|
+
format!("number must be a multiple of {m}")
|
|
284
|
+
});
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
true
|
|
289
|
+
}
|
|
290
|
+
None => true,
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn validate_string(&self, inner: &[u8], base: &str, ctx: &mut Ctx) -> bool {
|
|
297
|
+
let Some(s) = &self.str else {
|
|
298
|
+
return true;
|
|
299
|
+
};
|
|
300
|
+
let len = count_chars(inner);
|
|
301
|
+
if let Some(min) = s.min_len {
|
|
302
|
+
if len < min {
|
|
303
|
+
ctx.record(base, "minLength", || {
|
|
304
|
+
format!("string is shorter than {min} characters")
|
|
305
|
+
});
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if let Some(max) = s.max_len {
|
|
310
|
+
if len > max {
|
|
311
|
+
ctx.record(base, "maxLength", || {
|
|
312
|
+
format!("string is longer than {max} characters")
|
|
313
|
+
});
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if let Some(re) = &s.pattern {
|
|
318
|
+
// Pattern matches the DECODED string value (like the reference).
|
|
319
|
+
let owned;
|
|
320
|
+
let content: &[u8] = if inner.contains(&b'\\') {
|
|
321
|
+
owned = decode_string(inner);
|
|
322
|
+
&owned
|
|
323
|
+
} else {
|
|
324
|
+
inner
|
|
325
|
+
};
|
|
326
|
+
let text = match std::str::from_utf8(content) {
|
|
327
|
+
Ok(t) => t,
|
|
328
|
+
Err(_) => {
|
|
329
|
+
ctx.record(base, "pattern", || "string is not valid UTF-8".to_string());
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
if !re.is_match(text).unwrap_or(false) {
|
|
334
|
+
ctx.record(base, "pattern", || {
|
|
335
|
+
"string does not match the required pattern".to_string()
|
|
336
|
+
});
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if s.format_email {
|
|
341
|
+
let owned;
|
|
342
|
+
let content: &[u8] = if inner.contains(&b'\\') {
|
|
343
|
+
owned = decode_string(inner);
|
|
344
|
+
&owned
|
|
345
|
+
} else {
|
|
346
|
+
inner
|
|
347
|
+
};
|
|
348
|
+
if !super::email::is_valid_email_format(content) {
|
|
349
|
+
ctx.record(base, "format", || {
|
|
350
|
+
"string is not a valid email address".to_string()
|
|
351
|
+
});
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
true
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// Run allOf / anyOf / oneOf / not / if-then-else over the value's raw bytes.
|
|
359
|
+
fn validate_comb_raw(&self, raw: &[u8], ctx: &mut Ctx) -> bool {
|
|
360
|
+
let Some(comb) = &self.comb else {
|
|
361
|
+
return true;
|
|
362
|
+
};
|
|
363
|
+
let base = self.schema_path.as_deref().unwrap_or("");
|
|
364
|
+
// allOf: a failing branch IS the cause — run with the real ctx.
|
|
365
|
+
for sub in &comb.all_of {
|
|
366
|
+
let mut c = Cursor::new(raw);
|
|
367
|
+
if !sub.validate(&mut c, ctx) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// anyOf / oneOf / not / if-guard: a branch's internal outcome is not
|
|
372
|
+
// itself the cause, so suppress its error recording.
|
|
373
|
+
if !comb.any_of.is_empty() {
|
|
374
|
+
let mut any_ok = false;
|
|
375
|
+
for sub in &comb.any_of {
|
|
376
|
+
let mut c = Cursor::new(raw);
|
|
377
|
+
if ctx.with_suppressed(|ctx| sub.validate(&mut c, ctx)) {
|
|
378
|
+
any_ok = true;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if !any_ok {
|
|
383
|
+
ctx.record(base, "anyOf", || {
|
|
384
|
+
"value does not match any of the allowed schemas".to_string()
|
|
385
|
+
});
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if !comb.one_of.is_empty() {
|
|
390
|
+
let mut count = 0usize;
|
|
391
|
+
for sub in &comb.one_of {
|
|
392
|
+
let mut c = Cursor::new(raw);
|
|
393
|
+
if ctx.with_suppressed(|ctx| sub.validate(&mut c, ctx)) {
|
|
394
|
+
count += 1;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if count != 1 {
|
|
398
|
+
ctx.record(base, "oneOf", || {
|
|
399
|
+
if count == 0 {
|
|
400
|
+
"value matches none of the required schemas".to_string()
|
|
401
|
+
} else {
|
|
402
|
+
"value matches more than one of the required schemas".to_string()
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if let Some(sub) = &comb.not {
|
|
409
|
+
let mut c = Cursor::new(raw);
|
|
410
|
+
if ctx.with_suppressed(|ctx| sub.validate(&mut c, ctx)) {
|
|
411
|
+
ctx.record(base, "not", || {
|
|
412
|
+
"value matches the forbidden schema".to_string()
|
|
413
|
+
});
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if let Some((if_node, then_node, else_node)) = &comb.if_then_else {
|
|
418
|
+
let mut c = Cursor::new(raw);
|
|
419
|
+
let if_ok = ctx.with_suppressed(|ctx| if_node.validate(&mut c, ctx));
|
|
420
|
+
if if_ok {
|
|
421
|
+
if let Some(t) = then_node {
|
|
422
|
+
let mut c = Cursor::new(raw);
|
|
423
|
+
if !t.validate(&mut c, ctx) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} else if let Some(e) = else_node {
|
|
428
|
+
let mut c = Cursor::new(raw);
|
|
429
|
+
if !e.validate(&mut c, ctx) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
true
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
fn kind_name(k: Kind) -> &'static str {
|
|
439
|
+
match k {
|
|
440
|
+
Kind::Object => "object",
|
|
441
|
+
Kind::Array => "array",
|
|
442
|
+
Kind::String => "string",
|
|
443
|
+
Kind::Number => "number",
|
|
444
|
+
Kind::Boolean => "boolean",
|
|
445
|
+
Kind::Null => "null",
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
fn type_names(mask: u8) -> String {
|
|
450
|
+
let mut names = Vec::new();
|
|
451
|
+
for (bit, name) in [
|
|
452
|
+
(T_NULL, "null"),
|
|
453
|
+
(T_BOOL, "boolean"),
|
|
454
|
+
(T_NUM, "number"),
|
|
455
|
+
(T_INT, "integer"),
|
|
456
|
+
(T_STR, "string"),
|
|
457
|
+
(T_ARR, "array"),
|
|
458
|
+
(T_OBJ, "object"),
|
|
459
|
+
] {
|
|
460
|
+
if mask & bit != 0 {
|
|
461
|
+
names.push(name);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if names.is_empty() {
|
|
465
|
+
"any value".to_string()
|
|
466
|
+
} else {
|
|
467
|
+
names.join(" or ")
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/// `multipleOf` exactness, replicating jsonschema's `is_multiple_of_float`:
|
|
472
|
+
/// exact rational division via fraction::BigFraction, denominator must be 1.
|
|
473
|
+
fn is_multiple_of(value: f64, multiple: f64) -> bool {
|
|
474
|
+
// `fraction::BigFraction::from(f64)` cannot represent non-finite values
|
|
475
|
+
// (`1e999` parses to `f64::INFINITY` in `Cursor::number`) and PANICS on
|
|
476
|
+
// them. Via the raw C-ABI (bun:ffi) path a panic is a whole-process crash
|
|
477
|
+
// (no catch_unwind equivalent), so guard up front and report "not a
|
|
478
|
+
// multiple" — consistent with the DOM validator, which also yields
|
|
479
|
+
// schema-invalid for such a body (its serde parse of `1e999` fails).
|
|
480
|
+
if !value.is_finite() || !multiple.is_finite() {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
if value == 0.0 {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
if value.abs() < multiple {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
let r = fraction::BigFraction::from(value) / fraction::BigFraction::from(multiple);
|
|
490
|
+
r.denom().is_none_or(One::is_one)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/// Exact distinct-key tracking for `minProperties`/`maxProperties`, WITHOUT a
|
|
494
|
+
/// per-document heap allocation in the common case.
|
|
495
|
+
///
|
|
496
|
+
/// The common case (objects with a modest number of short keys) is tracked in
|
|
497
|
+
/// a fixed stack array; only pathological objects (many distinct keys or long
|
|
498
|
+
/// keys) migrate to a heap `Vec`. Keeps the otherwise zero-alloc schema fast
|
|
499
|
+
/// path heap-light even when the schema uses these keywords.
|
|
500
|
+
const INLINE_DISTINCT: usize = 16;
|
|
501
|
+
const INLINE_KEY_BYTES: usize = 48;
|
|
502
|
+
|
|
503
|
+
// The Inline variant is intentionally much larger than Heap (a stack-resident
|
|
504
|
+
// array for the common case); the enum lives on the stack, so this is fine.
|
|
505
|
+
#[allow(clippy::large_enum_variant)]
|
|
506
|
+
pub(crate) enum DistinctKeys {
|
|
507
|
+
Inline {
|
|
508
|
+
keys: [([u8; INLINE_KEY_BYTES], usize); INLINE_DISTINCT],
|
|
509
|
+
len: usize,
|
|
510
|
+
},
|
|
511
|
+
Heap(Vec<Box<[u8]>>),
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
impl DistinctKeys {
|
|
515
|
+
fn new() -> Self {
|
|
516
|
+
DistinctKeys::Inline {
|
|
517
|
+
keys: [([0u8; INLINE_KEY_BYTES], 0); INLINE_DISTINCT],
|
|
518
|
+
len: 0,
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/// Record `key` as seen (exact byte comparison); returns whether it was new.
|
|
523
|
+
fn insert(&mut self, key: &[u8]) -> bool {
|
|
524
|
+
match self {
|
|
525
|
+
DistinctKeys::Inline { keys, len } => {
|
|
526
|
+
for (k, klen) in keys[..*len].iter() {
|
|
527
|
+
if *klen == key.len() && &k[..*klen] == key {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if *len < INLINE_DISTINCT && key.len() <= INLINE_KEY_BYTES {
|
|
532
|
+
let (k, klen) = &mut keys[*len];
|
|
533
|
+
k[..key.len()].copy_from_slice(key);
|
|
534
|
+
*klen = key.len();
|
|
535
|
+
*len += 1;
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
// Inline capacity exhausted: migrate to the heap tracker.
|
|
539
|
+
let mut v = Vec::with_capacity(*len + 1);
|
|
540
|
+
for (k, klen) in keys[..*len].iter() {
|
|
541
|
+
v.push(k[..*klen].to_vec().into_boxed_slice());
|
|
542
|
+
}
|
|
543
|
+
v.push(key.to_vec().into_boxed_slice());
|
|
544
|
+
*self = DistinctKeys::Heap(v);
|
|
545
|
+
true
|
|
546
|
+
}
|
|
547
|
+
DistinctKeys::Heap(v) => {
|
|
548
|
+
if v.iter().any(|k| k.as_ref() == key) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
v.push(key.to_vec().into_boxed_slice());
|
|
552
|
+
true
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
fn len(&self) -> usize {
|
|
558
|
+
match self {
|
|
559
|
+
DistinctKeys::Inline { len, .. } => *len,
|
|
560
|
+
DistinctKeys::Heap(v) => v.len(),
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
pub(crate) fn validate_object(o: &FastObject, c: &mut Cursor, ctx: &mut Ctx, base: &str) -> bool {
|
|
566
|
+
if !c.eat(b'{') {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
c.skip_ws();
|
|
570
|
+
let need_distinct = o.min_props.is_some() || o.max_props.is_some();
|
|
571
|
+
let need_present = !o.dependent_required.is_empty();
|
|
572
|
+
let mut count: usize = 0;
|
|
573
|
+
let mut seen_required: u64 = 0;
|
|
574
|
+
let mut obj_ok = true;
|
|
575
|
+
let mut distinct: Option<DistinctKeys> = if need_distinct {
|
|
576
|
+
Some(DistinctKeys::new())
|
|
577
|
+
} else {
|
|
578
|
+
None
|
|
579
|
+
};
|
|
580
|
+
let mut present: Option<Vec<Box<[u8]>>> = if need_present { Some(Vec::new()) } else { None };
|
|
581
|
+
|
|
582
|
+
if c.eat(b'}') {
|
|
583
|
+
return finish_object(o, count, seen_required, &distinct, &present, ctx, base) && obj_ok;
|
|
584
|
+
}
|
|
585
|
+
loop {
|
|
586
|
+
c.skip_ws();
|
|
587
|
+
let Some(raw) = c.raw_string() else {
|
|
588
|
+
return false;
|
|
589
|
+
};
|
|
590
|
+
let owned;
|
|
591
|
+
let key: &[u8] = if raw.contains(&b'\\') {
|
|
592
|
+
owned = decode_string(raw);
|
|
593
|
+
&owned
|
|
594
|
+
} else {
|
|
595
|
+
raw
|
|
596
|
+
};
|
|
597
|
+
count += 1;
|
|
598
|
+
if let Some(d) = &mut distinct {
|
|
599
|
+
d.insert(key);
|
|
600
|
+
}
|
|
601
|
+
if let Some(p) = &mut present {
|
|
602
|
+
p.push(key.to_vec().into_boxed_slice());
|
|
603
|
+
}
|
|
604
|
+
// `required` lookup guarded on non-empty so a schema with no `required`
|
|
605
|
+
// keyword skips hashing every member key (exact: an empty map's `get`
|
|
606
|
+
// is always None). `seen_required` stays 0 either way.
|
|
607
|
+
if !o.required.is_empty() {
|
|
608
|
+
if let Some(&bit) = o.required.get(key) {
|
|
609
|
+
seen_required |= 1u64 << bit;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
c.skip_ws();
|
|
613
|
+
if !c.eat(b':') {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
// Align to the value's first byte (whitespace after the colon is not
|
|
617
|
+
// part of the value). `validate_inner` also skips ws at its start, so
|
|
618
|
+
// this is a no-op for validation; it makes `vstart` (used for one-pass
|
|
619
|
+
// derive capture + error realignment) point at the actual value.
|
|
620
|
+
c.skip_ws();
|
|
621
|
+
// Schemas applying to this key: the `properties` schema (if any) plus
|
|
622
|
+
// every matching `patternProperties` schema — ALL apply conjunctively
|
|
623
|
+
// (a key covered by either is not an "additional" property). `props`
|
|
624
|
+
// lookup borrows (no Arc clone per member); pattern matching runs the
|
|
625
|
+
// regexes once per applicable key, inlined (no per-member Vec). The
|
|
626
|
+
// lookup is guarded on non-empty so a bare `{ "type": "object" }`
|
|
627
|
+
// schema (no `properties`) skips hashing every member key (exact: an
|
|
628
|
+
// empty map's `get` is always None → falls to the `additional` branch).
|
|
629
|
+
let prop_node = if o.props.is_empty() {
|
|
630
|
+
None
|
|
631
|
+
} else {
|
|
632
|
+
o.props.get(key)
|
|
633
|
+
};
|
|
634
|
+
let vstart = c.pos;
|
|
635
|
+
ctx.capture_enter_key(c.data.as_ptr(), key);
|
|
636
|
+
ctx.push_key(key);
|
|
637
|
+
let ok = if o.patterns.is_empty() {
|
|
638
|
+
match prop_node {
|
|
639
|
+
Some(node) => node.validate(c, ctx),
|
|
640
|
+
None => match &o.additional {
|
|
641
|
+
Additional::Allow => c.skip_value(ctx.depth),
|
|
642
|
+
Additional::Deny => {
|
|
643
|
+
ctx.record(base, "additionalProperties", || {
|
|
644
|
+
"additional properties are not allowed".to_string()
|
|
645
|
+
});
|
|
646
|
+
false
|
|
647
|
+
}
|
|
648
|
+
Additional::Schema(s) => s.validate(c, ctx),
|
|
649
|
+
},
|
|
650
|
+
}
|
|
651
|
+
} else {
|
|
652
|
+
// property schema + every matching pattern schema (conjunctively).
|
|
653
|
+
// No per-member heap Vec / Arc clones: capture the value span once,
|
|
654
|
+
// then re-scan each applicable schema on a fresh cursor.
|
|
655
|
+
let data = c.data;
|
|
656
|
+
if !c.skip_value(ctx.depth) {
|
|
657
|
+
false
|
|
658
|
+
} else {
|
|
659
|
+
let raw = &data[vstart..c.pos];
|
|
660
|
+
let mut all_ok = true;
|
|
661
|
+
if let Some(p) = prop_node {
|
|
662
|
+
let mut cc = Cursor::new(raw);
|
|
663
|
+
all_ok = p.validate(&mut cc, ctx);
|
|
664
|
+
}
|
|
665
|
+
let mut matched_pattern = false;
|
|
666
|
+
for (re, n) in &o.patterns {
|
|
667
|
+
if !all_ok {
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
if std::str::from_utf8(key).is_ok_and(|k| re.is_match(k).unwrap_or(false)) {
|
|
671
|
+
matched_pattern = true;
|
|
672
|
+
let mut cc = Cursor::new(raw);
|
|
673
|
+
all_ok = n.validate(&mut cc, ctx);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if all_ok && !matched_pattern && prop_node.is_none() {
|
|
677
|
+
// No schema covers this key → apply additionalProperties.
|
|
678
|
+
match &o.additional {
|
|
679
|
+
Additional::Allow => {}
|
|
680
|
+
Additional::Deny => {
|
|
681
|
+
ctx.record(base, "additionalProperties", || {
|
|
682
|
+
"additional properties are not allowed".to_string()
|
|
683
|
+
});
|
|
684
|
+
all_ok = false;
|
|
685
|
+
}
|
|
686
|
+
Additional::Schema(s) => {
|
|
687
|
+
let mut cc = Cursor::new(raw);
|
|
688
|
+
all_ok = s.validate(&mut cc, ctx);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
all_ok
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
// One-pass derive capture: this member's value spans [vstart, c.pos).
|
|
696
|
+
// Entered above (trie descend), recorded here, exited here — all on
|
|
697
|
+
// the ROOT walk only (sub-scan cursors are skipped via the pointer).
|
|
698
|
+
ctx.capture_record_value(c.data.as_ptr(), vstart, c.pos);
|
|
699
|
+
ctx.capture_exit_key(c.data.as_ptr());
|
|
700
|
+
ctx.pop();
|
|
701
|
+
if !ok {
|
|
702
|
+
if ctx.is_detailed() {
|
|
703
|
+
// Collect every failing property: realign past this value and
|
|
704
|
+
// continue validating the remaining members.
|
|
705
|
+
obj_ok = false;
|
|
706
|
+
realign(c, vstart, ctx.depth);
|
|
707
|
+
} else {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
c.skip_ws();
|
|
712
|
+
if c.eat(b'}') {
|
|
713
|
+
let fin = finish_object(o, count, seen_required, &distinct, &present, ctx, base);
|
|
714
|
+
return obj_ok && fin;
|
|
715
|
+
}
|
|
716
|
+
if !c.eat(b',') {
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
pub(crate) fn finish_object(
|
|
723
|
+
o: &FastObject,
|
|
724
|
+
count: usize,
|
|
725
|
+
seen_required: u64,
|
|
726
|
+
distinct: &Option<DistinctKeys>,
|
|
727
|
+
present: &Option<Vec<Box<[u8]>>>,
|
|
728
|
+
ctx: &mut Ctx,
|
|
729
|
+
base: &str,
|
|
730
|
+
) -> bool {
|
|
731
|
+
let total = distinct.as_ref().map(DistinctKeys::len).unwrap_or(count);
|
|
732
|
+
if let Some(min) = o.min_props {
|
|
733
|
+
if total < min {
|
|
734
|
+
ctx.record(base, "minProperties", || {
|
|
735
|
+
format!("object must have at least {min} properties")
|
|
736
|
+
});
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if let Some(max) = o.max_props {
|
|
741
|
+
if total > max {
|
|
742
|
+
ctx.record(base, "maxProperties", || {
|
|
743
|
+
format!("object must have at most {max} properties")
|
|
744
|
+
});
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if o.required_count > 0 {
|
|
749
|
+
let full = if o.required_count == 64 {
|
|
750
|
+
u64::MAX
|
|
751
|
+
} else {
|
|
752
|
+
(1u64 << o.required_count) - 1
|
|
753
|
+
};
|
|
754
|
+
if seen_required != full {
|
|
755
|
+
ctx.record(base, "required", || {
|
|
756
|
+
let missing: Vec<String> = o
|
|
757
|
+
.required
|
|
758
|
+
.iter()
|
|
759
|
+
.filter(|(_, &bit)| seen_required & (1u64 << bit) == 0)
|
|
760
|
+
.map(|(k, _)| String::from_utf8_lossy(k).into_owned())
|
|
761
|
+
.collect();
|
|
762
|
+
format!("missing required properties: {}", missing.join(", "))
|
|
763
|
+
});
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if let Some(present) = present {
|
|
768
|
+
for (prop, reqs) in &o.dependent_required {
|
|
769
|
+
if present.iter().any(|k| k.as_ref() == prop.as_ref()) {
|
|
770
|
+
for r in reqs {
|
|
771
|
+
if !present.iter().any(|k| k.as_ref() == r.as_ref()) {
|
|
772
|
+
ctx.record(base, "dependencies", || {
|
|
773
|
+
format!(
|
|
774
|
+
"property \"{}\" requires property \"{}\"",
|
|
775
|
+
String::from_utf8_lossy(prop),
|
|
776
|
+
String::from_utf8_lossy(r)
|
|
777
|
+
)
|
|
778
|
+
});
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
true
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
pub(crate) fn validate_array(a: &FastArray, c: &mut Cursor, ctx: &mut Ctx, base: &str) -> bool {
|
|
789
|
+
if !c.eat(b'[') {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
c.skip_ws();
|
|
793
|
+
let mut count = 0usize;
|
|
794
|
+
let mut arr_ok = true;
|
|
795
|
+
let mut unique_seen: Option<Vec<sonic_rs::Value>> =
|
|
796
|
+
if a.unique { Some(Vec::new()) } else { None };
|
|
797
|
+
let mut contains_found = a.contains.is_none();
|
|
798
|
+
if c.eat(b']') {
|
|
799
|
+
return check_array_counts(a, count, ctx, base)
|
|
800
|
+
&& contains_ok(a, contains_found, base, ctx)
|
|
801
|
+
&& arr_ok;
|
|
802
|
+
}
|
|
803
|
+
loop {
|
|
804
|
+
count += 1;
|
|
805
|
+
if let Some(max) = a.max_items {
|
|
806
|
+
if count > max {
|
|
807
|
+
ctx.record(base, "maxItems", || {
|
|
808
|
+
format!("array has more than {max} items")
|
|
809
|
+
});
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
let idx = count - 1;
|
|
814
|
+
let elem_start = c.pos;
|
|
815
|
+
let ok = match &a.tuple_items {
|
|
816
|
+
Some(tuple) if idx < tuple.len() => validate_element(&tuple[idx], c, ctx, idx),
|
|
817
|
+
Some(_) => match &a.additional_items {
|
|
818
|
+
Some(Additional::Allow) | None => c.skip_value(ctx.depth),
|
|
819
|
+
Some(Additional::Deny) => {
|
|
820
|
+
ctx.push_idx(idx);
|
|
821
|
+
ctx.record(base, "additionalItems", || {
|
|
822
|
+
"additional array items are not allowed".to_string()
|
|
823
|
+
});
|
|
824
|
+
ctx.pop();
|
|
825
|
+
false
|
|
826
|
+
}
|
|
827
|
+
Some(Additional::Schema(s)) => validate_element(s, c, ctx, idx),
|
|
828
|
+
},
|
|
829
|
+
None => match &a.items {
|
|
830
|
+
Some(node) => validate_element(node, c, ctx, idx),
|
|
831
|
+
None => c.skip_value(ctx.depth),
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
if !ok {
|
|
835
|
+
if ctx.is_detailed() {
|
|
836
|
+
arr_ok = false;
|
|
837
|
+
realign(c, elem_start, ctx.depth);
|
|
838
|
+
} else {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// uniqueItems: compare parsed element values (matches the reference's
|
|
843
|
+
// `is_unique`, incl. `1 == 1.0` and insertion-order object comparison).
|
|
844
|
+
if let Some(seen) = &mut unique_seen {
|
|
845
|
+
let data = c.data;
|
|
846
|
+
let raw = &data[elem_start..c.pos];
|
|
847
|
+
if let Ok(val) = sonic_rs::from_slice::<sonic_rs::Value>(raw) {
|
|
848
|
+
if seen.iter().any(|v| sonic_values_equal(v, &val)) {
|
|
849
|
+
ctx.record(base, "uniqueItems", || {
|
|
850
|
+
"array items must be unique".to_string()
|
|
851
|
+
});
|
|
852
|
+
arr_ok = false;
|
|
853
|
+
} else {
|
|
854
|
+
seen.push(val);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// contains: does this element validate the contains schema?
|
|
859
|
+
if !contains_found {
|
|
860
|
+
if let Some(con) = &a.contains {
|
|
861
|
+
let data = c.data;
|
|
862
|
+
let raw = &data[elem_start..c.pos];
|
|
863
|
+
let mut cc = Cursor::new(raw);
|
|
864
|
+
if ctx.with_suppressed(|ctx| con.validate(&mut cc, ctx)) {
|
|
865
|
+
contains_found = true;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
c.skip_ws();
|
|
870
|
+
if c.eat(b']') {
|
|
871
|
+
let fin = check_array_counts(a, count, ctx, base)
|
|
872
|
+
&& contains_ok(a, contains_found, base, ctx);
|
|
873
|
+
// One-pass derive capture: this array's element count (only on the
|
|
874
|
+
// ROOT walk; sub-scan cursors are skipped via the data-pointer).
|
|
875
|
+
ctx.capture_record_length(c.data.as_ptr(), count);
|
|
876
|
+
return arr_ok && fin;
|
|
877
|
+
}
|
|
878
|
+
if !c.eat(b',') {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
c.skip_ws();
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/// In detailed mode, skip the remainder of a failed value so validation can
|
|
886
|
+
/// continue to the next property/element (collecting every error). No-op if the
|
|
887
|
+
/// value is malformed (can't safely resync).
|
|
888
|
+
fn realign(c: &mut Cursor, vstart: usize, depth: u32) {
|
|
889
|
+
let data = c.data;
|
|
890
|
+
let mut sc = Cursor::new(&data[vstart..]);
|
|
891
|
+
if sc.skip_value(depth) {
|
|
892
|
+
c.pos = vstart + sc.pos;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
fn validate_element(node: &FastNode, c: &mut Cursor, ctx: &mut Ctx, idx: usize) -> bool {
|
|
897
|
+
ctx.push_idx(idx);
|
|
898
|
+
let ok = node.validate(c, ctx);
|
|
899
|
+
ctx.pop();
|
|
900
|
+
ok
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
fn contains_ok(a: &FastArray, contains_found: bool, base: &str, ctx: &mut Ctx) -> bool {
|
|
904
|
+
if a.contains.is_some() && !contains_found {
|
|
905
|
+
ctx.record(base, "contains", || {
|
|
906
|
+
"array does not contain a matching item".to_string()
|
|
907
|
+
});
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
true
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
pub(crate) fn check_array_counts(a: &FastArray, count: usize, ctx: &mut Ctx, base: &str) -> bool {
|
|
914
|
+
if let Some(min) = a.min_items {
|
|
915
|
+
if count < min {
|
|
916
|
+
ctx.record(base, "minItems", || {
|
|
917
|
+
format!("array must have at least {min} items")
|
|
918
|
+
});
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if let Some(max) = a.max_items {
|
|
923
|
+
if count > max {
|
|
924
|
+
ctx.record(base, "maxItems", || {
|
|
925
|
+
format!("array must have at most {max} items")
|
|
926
|
+
});
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
true
|
|
931
|
+
}
|