schemurai 2.2.3 → 3.0.0
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.
- checksums.yaml +4 -4
- data/README.md +81 -7
- data/ext/schemurai_native/Cargo.lock +99 -0
- data/ext/schemurai_native/Cargo.toml +21 -0
- data/ext/schemurai_native/build.rs +35 -0
- data/ext/schemurai_native/extconf.rb +63 -0
- data/ext/schemurai_native/glue.c +780 -0
- data/ext/schemurai_native/protocol.def +63 -0
- data/ext/schemurai_native/protocol.h +10 -0
- data/ext/schemurai_native/src/budget.rs +101 -0
- data/ext/schemurai_native/src/content.rs +574 -0
- data/ext/schemurai_native/src/diagnostics.rs +1135 -0
- data/ext/schemurai_native/src/enumeration.rs +154 -0
- data/ext/schemurai_native/src/evaluation.rs +219 -0
- data/ext/schemurai_native/src/format.rs +357 -0
- data/ext/schemurai_native/src/graph.rs +50 -0
- data/ext/schemurai_native/src/instance_graph.rs +84 -0
- data/ext/schemurai_native/src/lib.rs +3139 -0
- data/ext/schemurai_native/src/memo.rs +1149 -0
- data/ext/schemurai_native/src/memo_scope.rs +240 -0
- data/ext/schemurai_native/src/number.rs +1054 -0
- data/ext/schemurai_native/src/pattern.rs +431 -0
- data/ext/schemurai_native/src/scope.rs +420 -0
- data/ext/schemurai_native/src/structural.rs +86 -0
- data/ext/schemurai_native/src/tracking.rs +687 -0
- data/lib/schemurai/backend.rb +3 -1
- data/lib/schemurai/evaluator.rb +45 -3
- data/lib/schemurai/limit_budget.rb +78 -0
- data/lib/schemurai/limit_exceeded.rb +18 -0
- data/lib/schemurai/limited_evaluator.rb +100 -0
- data/lib/schemurai/native/diagnostics.rb +52 -0
- data/lib/schemurai/native/evaluator.rb +59 -0
- data/lib/schemurai/schema_domain.rb +12 -7
- data/lib/schemurai/schema_graph.rb +10 -1
- data/lib/schemurai/unique_items.rb +2 -0
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/compiler.rb +7 -3
- data/lib/schemurai/vm/evaluator.rb +50 -3
- data/lib/schemurai/vm/limited_evaluator.rb +97 -0
- data/lib/schemurai.rb +81 -33
- data/sig/schemurai.rbs +21 -13
- metadata +36 -6
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
//! Diagnostics traverse borrowed VALUEs without Ruby allocation. Only after
|
|
2
|
+
//! returning to C are the output records materialized as Ruby objects.
|
|
3
|
+
use super::*;
|
|
4
|
+
use tracking::Tracked;
|
|
5
|
+
|
|
6
|
+
struct Error {
|
|
7
|
+
keyword: &'static str,
|
|
8
|
+
instance_path: String,
|
|
9
|
+
schema_path: String,
|
|
10
|
+
opcode: i32,
|
|
11
|
+
operand: Value,
|
|
12
|
+
instance: Value,
|
|
13
|
+
detail: usize,
|
|
14
|
+
}
|
|
15
|
+
#[derive(Default)]
|
|
16
|
+
pub struct Report {
|
|
17
|
+
errors: Vec<Error>,
|
|
18
|
+
instance_path: String,
|
|
19
|
+
schema_path: String,
|
|
20
|
+
// Explicit failure signal for the current evaluation, independent of the
|
|
21
|
+
// number or rendering of diagnostic records. Child evaluations isolate it.
|
|
22
|
+
failed: bool,
|
|
23
|
+
limits: budget::Limits,
|
|
24
|
+
limit_hit: (u32, usize),
|
|
25
|
+
}
|
|
26
|
+
impl Report {
|
|
27
|
+
fn add(
|
|
28
|
+
&mut self,
|
|
29
|
+
keyword: &'static str,
|
|
30
|
+
source: (i32, Value),
|
|
31
|
+
instance: Value,
|
|
32
|
+
detail: usize,
|
|
33
|
+
) -> Result<(), Verdict> {
|
|
34
|
+
if self.limits.enabled & 1 != 0 && self.errors.len() >= self.limits.max_errors {
|
|
35
|
+
self.limit_hit = (1, self.errors.len().saturating_add(1));
|
|
36
|
+
return Err(Unsupported);
|
|
37
|
+
}
|
|
38
|
+
self.failed = true;
|
|
39
|
+
let schema_path = if keyword == "falseSchema" {
|
|
40
|
+
self.schema_path.clone()
|
|
41
|
+
} else {
|
|
42
|
+
format!("{}/{keyword}", self.schema_path)
|
|
43
|
+
};
|
|
44
|
+
self.errors.push(Error {
|
|
45
|
+
keyword,
|
|
46
|
+
instance_path: self.instance_path.clone(),
|
|
47
|
+
schema_path,
|
|
48
|
+
opcode: source.0,
|
|
49
|
+
operand: source.1,
|
|
50
|
+
instance,
|
|
51
|
+
detail,
|
|
52
|
+
});
|
|
53
|
+
Ok(())
|
|
54
|
+
}
|
|
55
|
+
fn scoped<T>(
|
|
56
|
+
&mut self,
|
|
57
|
+
segment: &str,
|
|
58
|
+
instance: Option<usize>,
|
|
59
|
+
visit: impl FnOnce(&mut Self) -> Result<T, Verdict>,
|
|
60
|
+
) -> Result<T, Verdict> {
|
|
61
|
+
use std::fmt::Write;
|
|
62
|
+
let schema_len = self.schema_path.len();
|
|
63
|
+
let instance_len = self.instance_path.len();
|
|
64
|
+
self.schema_path.push('/');
|
|
65
|
+
self.schema_path.push_str(segment);
|
|
66
|
+
if let Some(index) = instance {
|
|
67
|
+
write!(self.instance_path, "/{index}").unwrap();
|
|
68
|
+
}
|
|
69
|
+
let result = visit(self);
|
|
70
|
+
self.schema_path.truncate(schema_len);
|
|
71
|
+
self.instance_path.truncate(instance_len);
|
|
72
|
+
result
|
|
73
|
+
}
|
|
74
|
+
fn property<T>(
|
|
75
|
+
&mut self,
|
|
76
|
+
segment: &str,
|
|
77
|
+
key: Value,
|
|
78
|
+
schema_key: Option<Value>,
|
|
79
|
+
visit: impl FnOnce(&mut Self) -> Result<T, Verdict>,
|
|
80
|
+
) -> Result<T, Verdict> {
|
|
81
|
+
let name = pointer_name(key)?;
|
|
82
|
+
let segment = if let Some(key) = schema_key {
|
|
83
|
+
format!("{segment}/{}", pointer_name(key)?)
|
|
84
|
+
} else {
|
|
85
|
+
segment.to_owned()
|
|
86
|
+
};
|
|
87
|
+
let instance_len = self.instance_path.len();
|
|
88
|
+
self.instance_path.push('/');
|
|
89
|
+
self.instance_path.push_str(&name);
|
|
90
|
+
let result = self.scoped(&segment, None, visit);
|
|
91
|
+
self.instance_path.truncate(instance_len);
|
|
92
|
+
result
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn pointer_name(value: Value) -> Result<String, Verdict> {
|
|
97
|
+
if unsafe { sn_kind(value) } != TYPE_STRING {
|
|
98
|
+
return Err(Unsupported);
|
|
99
|
+
}
|
|
100
|
+
let mut data = std::ptr::null();
|
|
101
|
+
let mut len = 0;
|
|
102
|
+
let encoding = unsafe { sn_string_view(value, &mut data, &mut len) };
|
|
103
|
+
let bytes = unsafe { std::slice::from_raw_parts(data, len) };
|
|
104
|
+
if encoding == ENCODING_OTHER || (encoding == ENCODING_ASCII && !bytes.is_ascii()) {
|
|
105
|
+
return Err(Unsupported);
|
|
106
|
+
}
|
|
107
|
+
let name = std::str::from_utf8(bytes).map_err(|_| Unsupported)?;
|
|
108
|
+
Ok(name.replace('~', "~0").replace('/', "~1"))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
impl Execution<'_> {
|
|
112
|
+
fn collect<const ANNOTATIONS: bool>(
|
|
113
|
+
&self,
|
|
114
|
+
report: &mut Report,
|
|
115
|
+
node: usize,
|
|
116
|
+
value: Value,
|
|
117
|
+
depth: usize,
|
|
118
|
+
active: scope::Context<'_>,
|
|
119
|
+
) -> Result<Tracked, Verdict> {
|
|
120
|
+
if depth > MAX_EVALUATION_DEPTH {
|
|
121
|
+
return Err(Unsupported);
|
|
122
|
+
}
|
|
123
|
+
if !self.budget.checkpoint() {
|
|
124
|
+
return Err(Unsupported);
|
|
125
|
+
}
|
|
126
|
+
let kind = unsafe { sn_kind(value) };
|
|
127
|
+
if kind == TYPE_UNSUPPORTED {
|
|
128
|
+
return Err(Unsupported);
|
|
129
|
+
}
|
|
130
|
+
self.collect_kind::<ANNOTATIONS>(report, node, value, depth, active, kind)
|
|
131
|
+
}
|
|
132
|
+
fn collect_kind<const ANNOTATIONS: bool>(
|
|
133
|
+
&self,
|
|
134
|
+
report: &mut Report,
|
|
135
|
+
node: usize,
|
|
136
|
+
value: Value,
|
|
137
|
+
depth: usize,
|
|
138
|
+
active: scope::Context<'_>,
|
|
139
|
+
kind: i32,
|
|
140
|
+
) -> Result<Tracked, Verdict> {
|
|
141
|
+
// A diagnostic success is established by complete traversal, never by
|
|
142
|
+
// a short-circuit validity result. Keep exported annotations and the
|
|
143
|
+
// collection policy separate; failures retain their path-specific errors.
|
|
144
|
+
if !self.budget.checkpoint() {
|
|
145
|
+
return Err(Unsupported);
|
|
146
|
+
}
|
|
147
|
+
let key = self
|
|
148
|
+
.memo
|
|
149
|
+
.key(self, node, value, depth, active, &self.budget);
|
|
150
|
+
if self.budget.hit.get().0 != 0 {
|
|
151
|
+
return Err(Unsupported);
|
|
152
|
+
}
|
|
153
|
+
if let Some(key) = key
|
|
154
|
+
&& let Some(result) = self.memo.diagnostic_success(key, ANNOTATIONS)
|
|
155
|
+
{
|
|
156
|
+
return Ok(result);
|
|
157
|
+
}
|
|
158
|
+
let result = self.collect_uncached::<ANNOTATIONS>(report, node, value, depth, active, kind);
|
|
159
|
+
if let Some(key) = key
|
|
160
|
+
&& let Ok(coverage) = &result
|
|
161
|
+
&& coverage.status == Valid
|
|
162
|
+
{
|
|
163
|
+
self.memo
|
|
164
|
+
.record_diagnostic_success(key, ANNOTATIONS, coverage);
|
|
165
|
+
}
|
|
166
|
+
result
|
|
167
|
+
}
|
|
168
|
+
fn collect_uncached<const ANNOTATIONS: bool>(
|
|
169
|
+
&self,
|
|
170
|
+
report: &mut Report,
|
|
171
|
+
node: usize,
|
|
172
|
+
value: Value,
|
|
173
|
+
depth: usize,
|
|
174
|
+
active: scope::Context<'_>,
|
|
175
|
+
kind: i32,
|
|
176
|
+
) -> Result<Tracked, Verdict> {
|
|
177
|
+
if depth > MAX_EVALUATION_DEPTH {
|
|
178
|
+
return Err(Unsupported);
|
|
179
|
+
}
|
|
180
|
+
let frame = scope::ScopeFrame {
|
|
181
|
+
resource: self.metadata[node].resource,
|
|
182
|
+
parent: active.scope,
|
|
183
|
+
primary_binding: active
|
|
184
|
+
.scope
|
|
185
|
+
.and_then(|f| f.primary_binding)
|
|
186
|
+
.or(self.metadata[node].primary_binding),
|
|
187
|
+
};
|
|
188
|
+
let active = self.enter_scope(node, active, &frame);
|
|
189
|
+
let tracked = ANNOTATIONS || self.tracks[node];
|
|
190
|
+
let parent_failed = std::mem::replace(&mut report.failed, false);
|
|
191
|
+
let mut prior = Tracked::new(Valid);
|
|
192
|
+
for (op, &source) in self.nodes[node].iter().zip(&self.error_operands[node]) {
|
|
193
|
+
if !self.budget.checkpoint() {
|
|
194
|
+
return Err(Unsupported);
|
|
195
|
+
}
|
|
196
|
+
match op {
|
|
197
|
+
Op::Unsupported => return Err(Unsupported),
|
|
198
|
+
Op::Boolean(false) => {
|
|
199
|
+
report.add("falseSchema", source, value, 0)?;
|
|
200
|
+
prior = Tracked::new(Invalid);
|
|
201
|
+
}
|
|
202
|
+
Op::Boolean(true) => {}
|
|
203
|
+
Op::Type(mask) => {
|
|
204
|
+
if kind & mask == 0 {
|
|
205
|
+
report.add("type", source, value, 0)?;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
Op::Const(..) | Op::Enum(..) => match self.leaf(op, kind, value, depth) {
|
|
209
|
+
Unsupported => return Err(Unsupported),
|
|
210
|
+
Invalid => report.add(
|
|
211
|
+
if matches!(op, Op::Const(_)) {
|
|
212
|
+
"const"
|
|
213
|
+
} else {
|
|
214
|
+
"enum"
|
|
215
|
+
},
|
|
216
|
+
source,
|
|
217
|
+
value,
|
|
218
|
+
0,
|
|
219
|
+
)?,
|
|
220
|
+
Valid => {}
|
|
221
|
+
},
|
|
222
|
+
Op::Number(required, rules) => {
|
|
223
|
+
if *required != 0 && kind & required == 0 {
|
|
224
|
+
report.add("type", source, value, 0)?;
|
|
225
|
+
}
|
|
226
|
+
if kind & TYPE_NUMBER != 0 {
|
|
227
|
+
let bits = rules.failures(value)?;
|
|
228
|
+
for (bit, keyword) in [
|
|
229
|
+
(MAXIMUM, "maximum"),
|
|
230
|
+
(MINIMUM, "minimum"),
|
|
231
|
+
(EXCLUSIVE_MAXIMUM, "exclusiveMaximum"),
|
|
232
|
+
(EXCLUSIVE_MINIMUM, "exclusiveMinimum"),
|
|
233
|
+
(MULTIPLE_OF, "multipleOf"),
|
|
234
|
+
] {
|
|
235
|
+
if !self.budget.checkpoint() {
|
|
236
|
+
return Err(Unsupported);
|
|
237
|
+
}
|
|
238
|
+
if bits & bit != 0 {
|
|
239
|
+
report.add(keyword, source, value, 0)?;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
Op::String(required, rules) => {
|
|
245
|
+
if *required != 0 && kind != TYPE_STRING {
|
|
246
|
+
report.add("type", source, value, 0)?;
|
|
247
|
+
}
|
|
248
|
+
if kind == TYPE_STRING {
|
|
249
|
+
let len = if rules.min.is_some() || rules.max.is_some() {
|
|
250
|
+
unsafe { sn_len(value) }
|
|
251
|
+
} else {
|
|
252
|
+
0
|
|
253
|
+
};
|
|
254
|
+
if rules.max.is_some_and(|max| len > max) {
|
|
255
|
+
report.add("maxLength", source, value, len)?;
|
|
256
|
+
}
|
|
257
|
+
if rules.min.is_some_and(|min| len < min) {
|
|
258
|
+
report.add("minLength", source, value, len)?;
|
|
259
|
+
}
|
|
260
|
+
if let Some(pattern) = &rules.pattern {
|
|
261
|
+
match pattern.eval(self.caches, value) {
|
|
262
|
+
Unsupported => return Err(Unsupported),
|
|
263
|
+
Invalid => report.add("pattern", source, value, 0)?,
|
|
264
|
+
Valid => {}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if let Some(format) = &rules.format {
|
|
268
|
+
match format.eval(value) {
|
|
269
|
+
Unsupported => return Err(Unsupported),
|
|
270
|
+
Invalid => report.add("format", source, value, 0)?,
|
|
271
|
+
Valid => {}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if let Some(content) = &rules.content {
|
|
275
|
+
match content.eval(value) {
|
|
276
|
+
Unsupported => return Err(Unsupported),
|
|
277
|
+
Invalid => report.add(
|
|
278
|
+
if content.base64 {
|
|
279
|
+
"contentEncoding"
|
|
280
|
+
} else {
|
|
281
|
+
"contentMediaType"
|
|
282
|
+
},
|
|
283
|
+
source,
|
|
284
|
+
value,
|
|
285
|
+
0,
|
|
286
|
+
)?,
|
|
287
|
+
Valid => {}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
Op::Array(required, rules) => {
|
|
293
|
+
if kind == TYPE_ARRAY && !self.budget.checkpoint() {
|
|
294
|
+
return Err(Unsupported);
|
|
295
|
+
}
|
|
296
|
+
if *required != 0 && kind != TYPE_ARRAY {
|
|
297
|
+
report.add("type", source, value, 0)?;
|
|
298
|
+
}
|
|
299
|
+
if kind != TYPE_ARRAY {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
let mut coverage = Tracked::new(Valid);
|
|
303
|
+
let len = unsafe { sn_len(value) };
|
|
304
|
+
if rules.max.is_some_and(|max| len > max) {
|
|
305
|
+
report.add("maxItems", source, value, len)?;
|
|
306
|
+
}
|
|
307
|
+
if rules.min.is_some_and(|min| len < min) {
|
|
308
|
+
report.add("minItems", source, value, len)?;
|
|
309
|
+
}
|
|
310
|
+
if rules.unique {
|
|
311
|
+
match unique(value, depth, self.fixnum_flag, &self.budget) {
|
|
312
|
+
Unsupported => return Err(Unsupported),
|
|
313
|
+
Invalid => report.add("uniqueItems", source, value, 0)?,
|
|
314
|
+
Valid => {}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (index, &child) in rules.prefix.iter().take(len).enumerate() {
|
|
318
|
+
if !self.budget.checkpoint() {
|
|
319
|
+
return Err(Unsupported);
|
|
320
|
+
}
|
|
321
|
+
if tracked {
|
|
322
|
+
coverage.item(index);
|
|
323
|
+
}
|
|
324
|
+
report.scoped(&format!("prefixItems/{index}"), Some(index), |r| {
|
|
325
|
+
self.collect::<false>(
|
|
326
|
+
r,
|
|
327
|
+
child,
|
|
328
|
+
unsafe { sn_entry(value, index) },
|
|
329
|
+
depth + 1,
|
|
330
|
+
active,
|
|
331
|
+
)
|
|
332
|
+
})?;
|
|
333
|
+
}
|
|
334
|
+
if rules.list {
|
|
335
|
+
for (index, &child) in rules.items.iter().take(len).enumerate() {
|
|
336
|
+
if !self.budget.checkpoint() {
|
|
337
|
+
return Err(Unsupported);
|
|
338
|
+
}
|
|
339
|
+
if tracked {
|
|
340
|
+
coverage.item(index);
|
|
341
|
+
}
|
|
342
|
+
report.scoped(&format!("items/{index}"), Some(index), |r| {
|
|
343
|
+
self.collect::<false>(
|
|
344
|
+
r,
|
|
345
|
+
child,
|
|
346
|
+
unsafe { sn_entry(value, index) },
|
|
347
|
+
depth + 1,
|
|
348
|
+
active,
|
|
349
|
+
)
|
|
350
|
+
})?;
|
|
351
|
+
}
|
|
352
|
+
if let Some(child) = rules.additional {
|
|
353
|
+
for index in rules.items.len()..len {
|
|
354
|
+
if !self.budget.checkpoint() {
|
|
355
|
+
return Err(Unsupported);
|
|
356
|
+
}
|
|
357
|
+
if tracked {
|
|
358
|
+
coverage.item(index);
|
|
359
|
+
}
|
|
360
|
+
report.scoped("additionalItems", Some(index), |r| {
|
|
361
|
+
self.collect::<false>(
|
|
362
|
+
r,
|
|
363
|
+
child,
|
|
364
|
+
unsafe { sn_entry(value, index) },
|
|
365
|
+
depth + 1,
|
|
366
|
+
active,
|
|
367
|
+
)
|
|
368
|
+
})?;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} else if let Some(&child) = rules.items.first() {
|
|
372
|
+
let primitive = match self.nodes[child].as_slice() {
|
|
373
|
+
[Op::Type(mask)]
|
|
374
|
+
if mask & TYPE_NUMBER != 0 || mask & TYPE_INTEGER != 0 =>
|
|
375
|
+
{
|
|
376
|
+
Some(&[][..])
|
|
377
|
+
}
|
|
378
|
+
[Op::Number(_, rules)] => rules.fast_limits.as_deref(),
|
|
379
|
+
_ => None,
|
|
380
|
+
};
|
|
381
|
+
let values = if len == 0 {
|
|
382
|
+
&[]
|
|
383
|
+
} else {
|
|
384
|
+
unsafe { std::slice::from_raw_parts(sn_array_data(value), len) }
|
|
385
|
+
};
|
|
386
|
+
let pattern = match self.nodes[child].as_slice() {
|
|
387
|
+
[Op::String(required, string)] if depth < MAX_EVALUATION_DEPTH => {
|
|
388
|
+
string.pattern_only().map(|pattern| (*required, pattern))
|
|
389
|
+
}
|
|
390
|
+
_ => None,
|
|
391
|
+
};
|
|
392
|
+
let pattern = pattern.filter(|_| !self.budget.timed());
|
|
393
|
+
let number = match self.nodes[child].as_slice() {
|
|
394
|
+
[Op::Number(required, number)] if depth < MAX_EVALUATION_DEPTH => {
|
|
395
|
+
Some((*required, number))
|
|
396
|
+
}
|
|
397
|
+
_ => None,
|
|
398
|
+
};
|
|
399
|
+
let format = match self.nodes[child].as_slice() {
|
|
400
|
+
[Op::String(required, string)] if depth < MAX_EVALUATION_DEPTH => {
|
|
401
|
+
string.format_only().map(|format| (*required, format))
|
|
402
|
+
}
|
|
403
|
+
_ => None,
|
|
404
|
+
};
|
|
405
|
+
let format = format.filter(|_| !self.budget.timed());
|
|
406
|
+
let content = match self.nodes[child].as_slice() {
|
|
407
|
+
[Op::String(required, string)] if depth < MAX_EVALUATION_DEPTH => {
|
|
408
|
+
string.content_only().map(|content| (*required, content))
|
|
409
|
+
}
|
|
410
|
+
_ => None,
|
|
411
|
+
};
|
|
412
|
+
let content = content.filter(|_| !self.budget.timed());
|
|
413
|
+
if pattern.is_some() || format.is_some() || content.is_some() {
|
|
414
|
+
let start = rules.prefix.len().min(len);
|
|
415
|
+
if tracked {
|
|
416
|
+
for index in start..len {
|
|
417
|
+
if !self.budget.checkpoint() {
|
|
418
|
+
return Err(Unsupported);
|
|
419
|
+
}
|
|
420
|
+
coverage.item(index);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// The invalid callback is shared, but each scanner retains
|
|
424
|
+
// its statically selected parser and guarded byte views.
|
|
425
|
+
let invalid = |index, item| match report.scoped(
|
|
426
|
+
"items",
|
|
427
|
+
Some(start + index),
|
|
428
|
+
|r| self.collect::<false>(r, child, item, depth + 1, active),
|
|
429
|
+
) {
|
|
430
|
+
Ok(_) => Valid,
|
|
431
|
+
Err(status) => status,
|
|
432
|
+
};
|
|
433
|
+
let status = match (pattern, format, content) {
|
|
434
|
+
(Some((required, pattern)), _, _) => pattern.visit_invalid_items(
|
|
435
|
+
self.caches,
|
|
436
|
+
required,
|
|
437
|
+
&values[start..],
|
|
438
|
+
invalid,
|
|
439
|
+
),
|
|
440
|
+
(_, Some((required, format)), _) => {
|
|
441
|
+
format.visit_invalid_items(required, &values[start..], invalid)
|
|
442
|
+
}
|
|
443
|
+
(_, _, Some((required, content))) => {
|
|
444
|
+
content.visit_invalid_items(required, &values[start..], invalid)
|
|
445
|
+
}
|
|
446
|
+
_ => unreachable!("a scalar item scanner was selected"),
|
|
447
|
+
};
|
|
448
|
+
if status != Valid {
|
|
449
|
+
return Err(status);
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
for (index, &item) in values.iter().enumerate().skip(rules.prefix.len())
|
|
453
|
+
{
|
|
454
|
+
if !self.budget.checkpoint() {
|
|
455
|
+
return Err(Unsupported);
|
|
456
|
+
}
|
|
457
|
+
if tracked {
|
|
458
|
+
coverage.item(index);
|
|
459
|
+
}
|
|
460
|
+
let needs = if let Some(limits) = primitive.filter(|_| {
|
|
461
|
+
item & self.fixnum_flag != 0 && depth < MAX_EVALUATION_DEPTH
|
|
462
|
+
}) {
|
|
463
|
+
let n = if limits.is_empty() {
|
|
464
|
+
0
|
|
465
|
+
} else {
|
|
466
|
+
immediate_integer(item)
|
|
467
|
+
};
|
|
468
|
+
!limits.iter().all(|&(bit, limit)| match bit {
|
|
469
|
+
MAXIMUM => n <= limit,
|
|
470
|
+
MINIMUM => n >= limit,
|
|
471
|
+
EXCLUSIVE_MAXIMUM => n < limit,
|
|
472
|
+
_ => n > limit,
|
|
473
|
+
})
|
|
474
|
+
} else if let Some((required, number)) = number {
|
|
475
|
+
match number.eval_guarded(required, item) {
|
|
476
|
+
Valid => false,
|
|
477
|
+
Invalid => true,
|
|
478
|
+
Unsupported => return Err(Unsupported),
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
self.needs_diagnostics(child, item, depth + 1, active)?
|
|
482
|
+
};
|
|
483
|
+
if !needs {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
report.scoped("items", Some(index), |r| {
|
|
487
|
+
self.collect::<false>(r, child, item, depth + 1, active)
|
|
488
|
+
})?;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if let Some(child) = rules.contains {
|
|
493
|
+
let values = if len == 0 {
|
|
494
|
+
&[]
|
|
495
|
+
} else {
|
|
496
|
+
unsafe { std::slice::from_raw_parts(sn_array_data(value), len) }
|
|
497
|
+
};
|
|
498
|
+
let matches = evaluation::scan_contains(
|
|
499
|
+
len,
|
|
500
|
+
|index| match self.needs_diagnostics(
|
|
501
|
+
child,
|
|
502
|
+
values[index],
|
|
503
|
+
depth + 1,
|
|
504
|
+
active,
|
|
505
|
+
) {
|
|
506
|
+
Ok(needs) => verdict(!needs),
|
|
507
|
+
Err(status) => status,
|
|
508
|
+
},
|
|
509
|
+
|index| {
|
|
510
|
+
if tracked {
|
|
511
|
+
coverage.item(index);
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
)?;
|
|
515
|
+
if bounds(matches, Some(rules.min_contains), rules.max_contains) == Invalid
|
|
516
|
+
{
|
|
517
|
+
report.add("contains", source, value, matches)?;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if let Some(child) = rules
|
|
521
|
+
.unevaluated
|
|
522
|
+
.filter(|_| prior.item_count() < len && coverage.item_count() < len)
|
|
523
|
+
{
|
|
524
|
+
for index in 0..len {
|
|
525
|
+
if !self.budget.checkpoint() {
|
|
526
|
+
return Err(Unsupported);
|
|
527
|
+
}
|
|
528
|
+
if prior.contains_item(index) || coverage.contains_item(index) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
report.scoped("unevaluatedItems", Some(index), |r| {
|
|
532
|
+
self.collect::<false>(
|
|
533
|
+
r,
|
|
534
|
+
child,
|
|
535
|
+
unsafe { sn_entry(value, index) },
|
|
536
|
+
depth + 1,
|
|
537
|
+
active,
|
|
538
|
+
)
|
|
539
|
+
})?;
|
|
540
|
+
coverage.item(index);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if tracked {
|
|
544
|
+
prior.merge_diagnostics(coverage)?;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
Op::Object(required, rules) => {
|
|
548
|
+
if kind == TYPE_OBJECT && !self.budget.checkpoint() {
|
|
549
|
+
return Err(Unsupported);
|
|
550
|
+
}
|
|
551
|
+
if *required != 0 && kind != TYPE_OBJECT {
|
|
552
|
+
report.add("type", source, value, 0)?;
|
|
553
|
+
}
|
|
554
|
+
if kind != TYPE_OBJECT {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
let mut coverage = Tracked::new(Valid);
|
|
558
|
+
let len = unsafe { sn_len(value) };
|
|
559
|
+
if rules.max.is_some_and(|max| len > max) {
|
|
560
|
+
report.add("maxProperties", source, value, len)?;
|
|
561
|
+
}
|
|
562
|
+
if rules.min.is_some_and(|min| len < min) {
|
|
563
|
+
report.add("minProperties", source, value, len)?;
|
|
564
|
+
}
|
|
565
|
+
for &name in &rules.required_order {
|
|
566
|
+
if !self.budget.checkpoint() {
|
|
567
|
+
return Err(Unsupported);
|
|
568
|
+
}
|
|
569
|
+
if missing(value, name, self.undefined) {
|
|
570
|
+
report.add("required", source, name, 0)?;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
let mut context = ObjectDiagnostics {
|
|
574
|
+
engine: self,
|
|
575
|
+
report,
|
|
576
|
+
rules,
|
|
577
|
+
depth: depth + 1,
|
|
578
|
+
active,
|
|
579
|
+
names: false,
|
|
580
|
+
unevaluated: false,
|
|
581
|
+
coverage: &mut coverage,
|
|
582
|
+
prior: &prior,
|
|
583
|
+
tracked,
|
|
584
|
+
};
|
|
585
|
+
if (!rules.properties.is_empty()
|
|
586
|
+
|| !rules.patterns.is_empty()
|
|
587
|
+
|| rules.additional.is_some())
|
|
588
|
+
&& unsafe {
|
|
589
|
+
sn_hash_each(
|
|
590
|
+
value,
|
|
591
|
+
(&mut context as *mut ObjectDiagnostics).cast(),
|
|
592
|
+
object_diagnostic,
|
|
593
|
+
)
|
|
594
|
+
} != VERDICT_VALID
|
|
595
|
+
{
|
|
596
|
+
return Err(Unsupported);
|
|
597
|
+
}
|
|
598
|
+
if rules.names.is_some() {
|
|
599
|
+
context.names = true;
|
|
600
|
+
if unsafe {
|
|
601
|
+
sn_hash_each(
|
|
602
|
+
value,
|
|
603
|
+
(&mut context as *mut ObjectDiagnostics).cast(),
|
|
604
|
+
object_diagnostic,
|
|
605
|
+
)
|
|
606
|
+
} != VERDICT_VALID
|
|
607
|
+
{
|
|
608
|
+
return Err(Unsupported);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
for dependency in &rules.diagnostic_dependencies {
|
|
612
|
+
if !self.budget.checkpoint() {
|
|
613
|
+
return Err(Unsupported);
|
|
614
|
+
}
|
|
615
|
+
match dependency {
|
|
616
|
+
DiagnosticDependency::Required(name, names, keyword) => {
|
|
617
|
+
if missing(value, *name, self.undefined) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
for &required in names {
|
|
621
|
+
if !self.budget.checkpoint() {
|
|
622
|
+
return Err(Unsupported);
|
|
623
|
+
}
|
|
624
|
+
if missing(value, required, self.undefined) {
|
|
625
|
+
report.add(keyword, (source.0, *name), required, 0)?;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
DiagnosticDependency::Schema(name, child, keyword) => {
|
|
630
|
+
if missing(value, *name, self.undefined) {
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if tracked {
|
|
634
|
+
let result = report.scoped(
|
|
635
|
+
&format!("{keyword}/{}", pointer_name(*name)?),
|
|
636
|
+
None,
|
|
637
|
+
|r| {
|
|
638
|
+
self.collect::<true>(
|
|
639
|
+
r,
|
|
640
|
+
*child,
|
|
641
|
+
value,
|
|
642
|
+
depth + 1,
|
|
643
|
+
active,
|
|
644
|
+
)
|
|
645
|
+
},
|
|
646
|
+
)?;
|
|
647
|
+
if result.status == Valid {
|
|
648
|
+
coverage.merge(result);
|
|
649
|
+
}
|
|
650
|
+
} else if self.needs_diagnostics(
|
|
651
|
+
*child,
|
|
652
|
+
value,
|
|
653
|
+
depth + 1,
|
|
654
|
+
active,
|
|
655
|
+
)? {
|
|
656
|
+
report.scoped(
|
|
657
|
+
&format!("{keyword}/{}", pointer_name(*name)?),
|
|
658
|
+
None,
|
|
659
|
+
|r| {
|
|
660
|
+
self.collect::<false>(
|
|
661
|
+
r,
|
|
662
|
+
*child,
|
|
663
|
+
value,
|
|
664
|
+
depth + 1,
|
|
665
|
+
active,
|
|
666
|
+
)
|
|
667
|
+
},
|
|
668
|
+
)?;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if rules.unevaluated.is_some()
|
|
674
|
+
&& prior.property_count() < len
|
|
675
|
+
&& coverage.property_count() < len
|
|
676
|
+
{
|
|
677
|
+
let mut context = ObjectDiagnostics {
|
|
678
|
+
engine: self,
|
|
679
|
+
report,
|
|
680
|
+
rules,
|
|
681
|
+
depth: depth + 1,
|
|
682
|
+
active,
|
|
683
|
+
names: false,
|
|
684
|
+
unevaluated: true,
|
|
685
|
+
coverage: &mut coverage,
|
|
686
|
+
prior: &prior,
|
|
687
|
+
tracked,
|
|
688
|
+
};
|
|
689
|
+
if unsafe {
|
|
690
|
+
sn_hash_each(
|
|
691
|
+
value,
|
|
692
|
+
(&mut context as *mut ObjectDiagnostics).cast(),
|
|
693
|
+
object_diagnostic,
|
|
694
|
+
)
|
|
695
|
+
} != VERDICT_VALID
|
|
696
|
+
{
|
|
697
|
+
return Err(Unsupported);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if tracked {
|
|
701
|
+
prior.merge_diagnostics(coverage)?;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
Op::All(nodes) => {
|
|
705
|
+
let mut coverage = Tracked::new(Valid);
|
|
706
|
+
for (index, &child) in nodes.iter().enumerate() {
|
|
707
|
+
if !self.budget.checkpoint() {
|
|
708
|
+
return Err(Unsupported);
|
|
709
|
+
}
|
|
710
|
+
let result = report.scoped(&format!("allOf/{index}"), None, |r| {
|
|
711
|
+
if tracked {
|
|
712
|
+
self.collect_kind::<true>(r, child, value, depth + 1, active, kind)
|
|
713
|
+
} else {
|
|
714
|
+
self.collect_kind::<false>(r, child, value, depth + 1, active, kind)
|
|
715
|
+
}
|
|
716
|
+
})?;
|
|
717
|
+
if tracked {
|
|
718
|
+
coverage.merge_diagnostics(result)?;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if tracked {
|
|
722
|
+
prior.merge_diagnostics(coverage)?;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
Op::Any(nodes) | Op::One(nodes) => {
|
|
726
|
+
let branch_kind = if matches!(op, Op::Any(_)) {
|
|
727
|
+
evaluation::BranchKind::Any
|
|
728
|
+
} else {
|
|
729
|
+
evaluation::BranchKind::One
|
|
730
|
+
};
|
|
731
|
+
// Complete scanning preserves exact oneOf counts and VM
|
|
732
|
+
// exceptions from branches after an anyOf success.
|
|
733
|
+
let result = if tracked {
|
|
734
|
+
evaluation::scan_branches(
|
|
735
|
+
nodes,
|
|
736
|
+
evaluation::BranchPolicy::Complete,
|
|
737
|
+
|child| self.eval_tracked_kind(child, value, depth + 1, active, kind),
|
|
738
|
+
)?
|
|
739
|
+
} else {
|
|
740
|
+
let result = evaluation::scan_branches(
|
|
741
|
+
nodes,
|
|
742
|
+
evaluation::BranchPolicy::Complete,
|
|
743
|
+
|child| {
|
|
744
|
+
self.eval_with_kind(child, value, depth + 1, active, Some(kind))
|
|
745
|
+
},
|
|
746
|
+
)?;
|
|
747
|
+
evaluation::BranchMatches {
|
|
748
|
+
matches: result.matches,
|
|
749
|
+
coverage: Tracked::new(Valid),
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
if !branch_kind.accepts(result.matches) {
|
|
753
|
+
report.add(branch_kind.keyword(), source, value, result.matches)?;
|
|
754
|
+
} else if tracked {
|
|
755
|
+
prior.merge_diagnostics(result.coverage)?;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
Op::Not(child) => {
|
|
759
|
+
match self.eval_with_kind(*child, value, depth + 1, active, Some(kind)) {
|
|
760
|
+
Unsupported => return Err(Unsupported),
|
|
761
|
+
Valid => report.add("not", source, value, 0)?,
|
|
762
|
+
Invalid => {}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
Op::Conditional(condition, yes, no) => {
|
|
766
|
+
let condition = if tracked {
|
|
767
|
+
self.eval_tracked_kind(*condition, value, depth + 1, active, kind)
|
|
768
|
+
} else {
|
|
769
|
+
Tracked::new(self.eval_with_kind(
|
|
770
|
+
*condition,
|
|
771
|
+
value,
|
|
772
|
+
depth + 1,
|
|
773
|
+
active,
|
|
774
|
+
Some(kind),
|
|
775
|
+
))
|
|
776
|
+
};
|
|
777
|
+
let mut coverage = Tracked::new(Valid);
|
|
778
|
+
let (child, segment) = match condition.status {
|
|
779
|
+
Unsupported => return Err(Unsupported),
|
|
780
|
+
Valid => {
|
|
781
|
+
if tracked {
|
|
782
|
+
coverage.merge(condition);
|
|
783
|
+
}
|
|
784
|
+
(yes, "then")
|
|
785
|
+
}
|
|
786
|
+
Invalid => (no, "else"),
|
|
787
|
+
};
|
|
788
|
+
if let Some(child) = child {
|
|
789
|
+
let result = report.scoped(segment, None, |r| {
|
|
790
|
+
if tracked {
|
|
791
|
+
self.collect_kind::<true>(r, *child, value, depth + 1, active, kind)
|
|
792
|
+
} else {
|
|
793
|
+
self.collect_kind::<false>(
|
|
794
|
+
r,
|
|
795
|
+
*child,
|
|
796
|
+
value,
|
|
797
|
+
depth + 1,
|
|
798
|
+
active,
|
|
799
|
+
kind,
|
|
800
|
+
)
|
|
801
|
+
}
|
|
802
|
+
})?;
|
|
803
|
+
if tracked {
|
|
804
|
+
coverage.merge_diagnostics(result)?;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if tracked {
|
|
808
|
+
prior.merge_diagnostics(coverage)?;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
Op::Ref(target) => {
|
|
812
|
+
if RefFrame::contains(active.active, node, value) {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
let frame = RefFrame {
|
|
816
|
+
source: node,
|
|
817
|
+
value,
|
|
818
|
+
parent: active.active,
|
|
819
|
+
};
|
|
820
|
+
let keyword = match source.0 {
|
|
821
|
+
OP_RECURSIVE_REF => "$recursiveRef",
|
|
822
|
+
OP_DYNAMIC_REF => "$dynamicRef",
|
|
823
|
+
_ => "$ref",
|
|
824
|
+
};
|
|
825
|
+
let target = self.reference_target(*target, active);
|
|
826
|
+
let active = scope::Context {
|
|
827
|
+
active: Some(&frame),
|
|
828
|
+
..active
|
|
829
|
+
};
|
|
830
|
+
let result = report.scoped(keyword, None, |r| {
|
|
831
|
+
if tracked {
|
|
832
|
+
self.collect_kind::<true>(r, target, value, depth + 1, active, kind)
|
|
833
|
+
} else {
|
|
834
|
+
self.collect_kind::<false>(r, target, value, depth + 1, active, kind)
|
|
835
|
+
}
|
|
836
|
+
})?;
|
|
837
|
+
if tracked {
|
|
838
|
+
prior.merge_diagnostics(result)?;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
let failed = report.failed;
|
|
844
|
+
report.failed |= parent_failed;
|
|
845
|
+
// Diagnostic coverage keeps local attempted locations for subsequent
|
|
846
|
+
// unevaluated checks; only a successful evaluation exports annotations.
|
|
847
|
+
Ok(if failed { Tracked::new(Invalid) } else { prior })
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
fn needs_diagnostics(
|
|
851
|
+
&self,
|
|
852
|
+
node: usize,
|
|
853
|
+
value: Value,
|
|
854
|
+
depth: usize,
|
|
855
|
+
active: scope::Context<'_>,
|
|
856
|
+
) -> Result<bool, Verdict> {
|
|
857
|
+
if !self.budget.checkpoint() {
|
|
858
|
+
return Err(Unsupported);
|
|
859
|
+
}
|
|
860
|
+
let status = match self.nodes[node].as_slice() {
|
|
861
|
+
[Op::Number(required, rules)] if depth <= MAX_EVALUATION_DEPTH => {
|
|
862
|
+
rules.eval_guarded(*required, value)
|
|
863
|
+
}
|
|
864
|
+
[op @ (Op::Type(_) | Op::Boolean(_) | Op::String(..) | Op::Const(_) | Op::Enum(_))]
|
|
865
|
+
if depth <= MAX_EVALUATION_DEPTH =>
|
|
866
|
+
{
|
|
867
|
+
let kind = if value & self.fixnum_flag != 0 {
|
|
868
|
+
TYPE_INTEGRAL_NUMBER
|
|
869
|
+
} else {
|
|
870
|
+
unsafe { sn_kind(value) }
|
|
871
|
+
};
|
|
872
|
+
if kind == TYPE_UNSUPPORTED {
|
|
873
|
+
Unsupported
|
|
874
|
+
} else {
|
|
875
|
+
self.leaf(op, kind, value, depth)
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
_ => self.eval(node, value, depth, active),
|
|
879
|
+
};
|
|
880
|
+
match status {
|
|
881
|
+
Valid => Ok(false),
|
|
882
|
+
Invalid => Ok(true),
|
|
883
|
+
Unsupported => Err(Unsupported),
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
struct ObjectDiagnostics<'a, 'b, 'c> {
|
|
889
|
+
engine: &'a Execution<'a>,
|
|
890
|
+
report: &'b mut Report,
|
|
891
|
+
rules: &'a Object,
|
|
892
|
+
depth: usize,
|
|
893
|
+
active: scope::Context<'c>,
|
|
894
|
+
names: bool,
|
|
895
|
+
unevaluated: bool,
|
|
896
|
+
coverage: &'b mut Tracked,
|
|
897
|
+
prior: &'b Tracked,
|
|
898
|
+
tracked: bool,
|
|
899
|
+
}
|
|
900
|
+
unsafe extern "C" fn object_diagnostic(context: *mut c_void, key: Value, value: Value) -> i32 {
|
|
901
|
+
catch_unwind(AssertUnwindSafe(|| {
|
|
902
|
+
let c = unsafe { &mut *context.cast::<ObjectDiagnostics<'_, '_, '_>>() };
|
|
903
|
+
if !c.engine.budget.checkpoint() {
|
|
904
|
+
return Unsupported as i32;
|
|
905
|
+
}
|
|
906
|
+
let visit = |report: &mut Report, child: usize, item: Value, segment: &str, schema_key| {
|
|
907
|
+
if c.engine.needs_diagnostics(child, item, c.depth, c.active)? {
|
|
908
|
+
report.property(segment, key, schema_key, |r| {
|
|
909
|
+
c.engine.collect::<false>(r, child, item, c.depth, c.active)
|
|
910
|
+
})?;
|
|
911
|
+
}
|
|
912
|
+
Ok::<_, Verdict>(())
|
|
913
|
+
};
|
|
914
|
+
let result = (|| {
|
|
915
|
+
if unsafe { sn_kind(key) } != TYPE_STRING {
|
|
916
|
+
return Err(Unsupported);
|
|
917
|
+
}
|
|
918
|
+
if c.names {
|
|
919
|
+
return visit(c.report, c.rules.names.unwrap(), key, "propertyNames", None);
|
|
920
|
+
}
|
|
921
|
+
if c.unevaluated {
|
|
922
|
+
if c.prior.contains_property(c.engine, key)
|
|
923
|
+
|| c.coverage.contains_property(c.engine, key)
|
|
924
|
+
{
|
|
925
|
+
return Ok(());
|
|
926
|
+
}
|
|
927
|
+
visit(
|
|
928
|
+
c.report,
|
|
929
|
+
c.rules.unevaluated.unwrap(),
|
|
930
|
+
value,
|
|
931
|
+
"unevaluatedProperties",
|
|
932
|
+
None,
|
|
933
|
+
)?;
|
|
934
|
+
c.coverage.property(c.engine, key);
|
|
935
|
+
return Ok(());
|
|
936
|
+
}
|
|
937
|
+
let evaluated = c.rules.visit_value_rules::<true>(
|
|
938
|
+
c.engine.caches,
|
|
939
|
+
&c.engine.budget,
|
|
940
|
+
key,
|
|
941
|
+
None,
|
|
942
|
+
|segment, name, child| visit(c.report, child, value, segment, name),
|
|
943
|
+
)?;
|
|
944
|
+
if c.tracked && evaluated {
|
|
945
|
+
c.coverage.property(c.engine, key);
|
|
946
|
+
}
|
|
947
|
+
Ok(())
|
|
948
|
+
})();
|
|
949
|
+
match result {
|
|
950
|
+
Ok(()) => VERDICT_VALID,
|
|
951
|
+
Err(_) => Unsupported as i32,
|
|
952
|
+
}
|
|
953
|
+
}))
|
|
954
|
+
.unwrap_or(Unsupported as i32)
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
#[repr(C)]
|
|
958
|
+
pub struct Record {
|
|
959
|
+
keyword: *const u8,
|
|
960
|
+
keyword_len: usize,
|
|
961
|
+
instance_path: *const u8,
|
|
962
|
+
instance_len: usize,
|
|
963
|
+
schema_path: *const u8,
|
|
964
|
+
schema_len: usize,
|
|
965
|
+
opcode: i32,
|
|
966
|
+
operand: Value,
|
|
967
|
+
instance: Value,
|
|
968
|
+
detail: usize,
|
|
969
|
+
}
|
|
970
|
+
/// # Safety
|
|
971
|
+
/// Engine/status must be live under the GVL; root must be in bounds and
|
|
972
|
+
/// instance live. Caches must be null or live, validator-owned and not in use.
|
|
973
|
+
#[unsafe(no_mangle)]
|
|
974
|
+
pub unsafe extern "C" fn sn_diagnose(
|
|
975
|
+
engine: *const Engine,
|
|
976
|
+
caches: *const pattern::Caches,
|
|
977
|
+
root: usize,
|
|
978
|
+
instance: Value,
|
|
979
|
+
status: *mut i32,
|
|
980
|
+
) -> *mut Report {
|
|
981
|
+
let result = catch_unwind(AssertUnwindSafe(|| {
|
|
982
|
+
let engine = unsafe { &*engine };
|
|
983
|
+
let local = pattern::Caches::default();
|
|
984
|
+
let caches = unsafe { caches.as_ref() }.unwrap_or(&local);
|
|
985
|
+
let execution = Execution {
|
|
986
|
+
engine,
|
|
987
|
+
caches,
|
|
988
|
+
memo: memo::Memo::default(),
|
|
989
|
+
budget: budget::Budget::default(),
|
|
990
|
+
};
|
|
991
|
+
let mut report = Report::default();
|
|
992
|
+
execution.collect::<false>(&mut report, root, instance, 0, scope::Context::default())?;
|
|
993
|
+
Ok::<_, Verdict>(Box::into_raw(Box::new(report)))
|
|
994
|
+
}));
|
|
995
|
+
match result {
|
|
996
|
+
Ok(Ok(report)) => {
|
|
997
|
+
unsafe { *status = VERDICT_VALID };
|
|
998
|
+
report
|
|
999
|
+
}
|
|
1000
|
+
other => {
|
|
1001
|
+
unsafe {
|
|
1002
|
+
*status = if other.is_err() {
|
|
1003
|
+
STATUS_PANIC
|
|
1004
|
+
} else {
|
|
1005
|
+
VERDICT_UNSUPPORTED
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
std::ptr::null_mut()
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
/// # Safety
|
|
1013
|
+
/// Same requirements as sn_diagnose; all limit/output pointers must be live.
|
|
1014
|
+
#[unsafe(no_mangle)]
|
|
1015
|
+
pub unsafe extern "C" fn sn_diagnose_with_limits(
|
|
1016
|
+
engine: *const Engine,
|
|
1017
|
+
caches: *const pattern::Caches,
|
|
1018
|
+
root: usize,
|
|
1019
|
+
instance: Value,
|
|
1020
|
+
status: *mut i32,
|
|
1021
|
+
limits: *const budget::Limits,
|
|
1022
|
+
outcome: *mut budget::Outcome,
|
|
1023
|
+
) -> *mut Report {
|
|
1024
|
+
let result = catch_unwind(AssertUnwindSafe(|| {
|
|
1025
|
+
let local = pattern::Caches::default();
|
|
1026
|
+
let caches = unsafe { caches.as_ref() }.unwrap_or(&local);
|
|
1027
|
+
let limits = unsafe { *limits };
|
|
1028
|
+
let execution = Execution {
|
|
1029
|
+
engine: unsafe { &*engine },
|
|
1030
|
+
caches,
|
|
1031
|
+
memo: memo::Memo::default(),
|
|
1032
|
+
budget: budget::Budget::new(limits),
|
|
1033
|
+
};
|
|
1034
|
+
let mut report = Report {
|
|
1035
|
+
limits,
|
|
1036
|
+
..Report::default()
|
|
1037
|
+
};
|
|
1038
|
+
let result = if execution.budget.check_deadline() {
|
|
1039
|
+
execution.collect::<false>(&mut report, root, instance, 0, scope::Context::default())
|
|
1040
|
+
} else {
|
|
1041
|
+
Err(Unsupported)
|
|
1042
|
+
};
|
|
1043
|
+
let result = if execution.budget.check_deadline() {
|
|
1044
|
+
result
|
|
1045
|
+
} else {
|
|
1046
|
+
Err(Unsupported)
|
|
1047
|
+
};
|
|
1048
|
+
let hit = if report.limit_hit.0 != 0 {
|
|
1049
|
+
report.limit_hit
|
|
1050
|
+
} else {
|
|
1051
|
+
execution.budget.hit.get()
|
|
1052
|
+
};
|
|
1053
|
+
unsafe {
|
|
1054
|
+
((*outcome).kind, (*outcome).attempted) = hit;
|
|
1055
|
+
}
|
|
1056
|
+
result.map(|_| Box::into_raw(Box::new(report)))
|
|
1057
|
+
}));
|
|
1058
|
+
match result {
|
|
1059
|
+
Ok(Ok(report)) => {
|
|
1060
|
+
unsafe { *status = VERDICT_VALID };
|
|
1061
|
+
report
|
|
1062
|
+
}
|
|
1063
|
+
other => {
|
|
1064
|
+
unsafe {
|
|
1065
|
+
*status = if other.is_err() {
|
|
1066
|
+
STATUS_PANIC
|
|
1067
|
+
} else {
|
|
1068
|
+
VERDICT_UNSUPPORTED
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
std::ptr::null_mut()
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
/// # Safety
|
|
1076
|
+
/// Report must be live under the GVL.
|
|
1077
|
+
#[unsafe(no_mangle)]
|
|
1078
|
+
pub unsafe extern "C" fn sn_report_len(report: *const Report) -> usize {
|
|
1079
|
+
unsafe { &*report }.errors.len()
|
|
1080
|
+
}
|
|
1081
|
+
/// # Safety
|
|
1082
|
+
/// Report must be live under the GVL.
|
|
1083
|
+
#[unsafe(no_mangle)]
|
|
1084
|
+
pub unsafe extern "C" fn sn_report_size(report: *const Report) -> usize {
|
|
1085
|
+
let r = unsafe { &*report };
|
|
1086
|
+
std::mem::size_of::<Report>()
|
|
1087
|
+
+ r.errors.capacity() * std::mem::size_of::<Error>()
|
|
1088
|
+
+ r.instance_path.capacity()
|
|
1089
|
+
+ r.schema_path.capacity()
|
|
1090
|
+
+ r.errors
|
|
1091
|
+
.iter()
|
|
1092
|
+
.map(|e| e.instance_path.capacity() + e.schema_path.capacity())
|
|
1093
|
+
.sum::<usize>()
|
|
1094
|
+
}
|
|
1095
|
+
/// # Safety
|
|
1096
|
+
/// Report must be live, index in bounds, and record a writable pointer.
|
|
1097
|
+
#[unsafe(no_mangle)]
|
|
1098
|
+
pub unsafe extern "C" fn sn_report_record(
|
|
1099
|
+
report: *const Report,
|
|
1100
|
+
index: usize,
|
|
1101
|
+
record: *mut Record,
|
|
1102
|
+
) {
|
|
1103
|
+
let e = &unsafe { &*report }.errors[index];
|
|
1104
|
+
unsafe {
|
|
1105
|
+
*record = Record {
|
|
1106
|
+
keyword: e.keyword.as_ptr(),
|
|
1107
|
+
keyword_len: e.keyword.len(),
|
|
1108
|
+
instance_path: e.instance_path.as_ptr(),
|
|
1109
|
+
instance_len: e.instance_path.len(),
|
|
1110
|
+
schema_path: e.schema_path.as_ptr(),
|
|
1111
|
+
schema_len: e.schema_path.len(),
|
|
1112
|
+
opcode: e.opcode,
|
|
1113
|
+
operand: e.operand,
|
|
1114
|
+
instance: e.instance,
|
|
1115
|
+
detail: e.detail,
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
/// # Safety
|
|
1120
|
+
/// Report must be live; call only from its synchronous Ruby GC mark callback.
|
|
1121
|
+
#[unsafe(no_mangle)]
|
|
1122
|
+
pub unsafe extern "C" fn sn_report_mark(report: *const Report) {
|
|
1123
|
+
for error in &unsafe { &*report }.errors {
|
|
1124
|
+
unsafe {
|
|
1125
|
+
sn_pin(error.operand);
|
|
1126
|
+
sn_pin(error.instance);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/// # Safety
|
|
1131
|
+
/// Report must be owned, returned by sn_diagnose, and freed exactly once.
|
|
1132
|
+
#[unsafe(no_mangle)]
|
|
1133
|
+
pub unsafe extern "C" fn sn_report_free(report: *mut Report) {
|
|
1134
|
+
drop(unsafe { Box::from_raw(report) });
|
|
1135
|
+
}
|