schemurai 2.2.3 → 2.3.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.
@@ -0,0 +1,965 @@
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
+ }
21
+ impl Report {
22
+ fn add(&mut self, keyword: &'static str, source: (i32, Value), instance: Value, detail: usize) {
23
+ let schema_path = if keyword == "falseSchema" {
24
+ self.schema_path.clone()
25
+ } else {
26
+ format!("{}/{keyword}", self.schema_path)
27
+ };
28
+ self.errors.push(Error {
29
+ keyword,
30
+ instance_path: self.instance_path.clone(),
31
+ schema_path,
32
+ opcode: source.0,
33
+ operand: source.1,
34
+ instance,
35
+ detail,
36
+ });
37
+ }
38
+ fn scoped(
39
+ &mut self,
40
+ segment: &str,
41
+ instance: Option<usize>,
42
+ visit: impl FnOnce(&mut Self) -> Result<(), Verdict>,
43
+ ) -> Result<(), Verdict> {
44
+ use std::fmt::Write;
45
+ let schema_len = self.schema_path.len();
46
+ let instance_len = self.instance_path.len();
47
+ self.schema_path.push('/');
48
+ self.schema_path.push_str(segment);
49
+ if let Some(index) = instance {
50
+ write!(self.instance_path, "/{index}").unwrap();
51
+ }
52
+ let result = visit(self);
53
+ self.schema_path.truncate(schema_len);
54
+ self.instance_path.truncate(instance_len);
55
+ result
56
+ }
57
+ fn property(
58
+ &mut self,
59
+ segment: &str,
60
+ key: Value,
61
+ schema_key: Option<Value>,
62
+ visit: impl FnOnce(&mut Self) -> Result<(), Verdict>,
63
+ ) -> Result<(), Verdict> {
64
+ let name = pointer_name(key)?;
65
+ let segment = if let Some(key) = schema_key {
66
+ format!("{segment}/{}", pointer_name(key)?)
67
+ } else {
68
+ segment.to_owned()
69
+ };
70
+ let instance_len = self.instance_path.len();
71
+ self.instance_path.push('/');
72
+ self.instance_path.push_str(&name);
73
+ let result = self.scoped(&segment, None, visit);
74
+ self.instance_path.truncate(instance_len);
75
+ result
76
+ }
77
+ }
78
+
79
+ fn pointer_name(value: Value) -> Result<String, Verdict> {
80
+ if unsafe { sn_kind(value) } != TYPE_STRING {
81
+ return Err(Unsupported);
82
+ }
83
+ let mut data = std::ptr::null();
84
+ let mut len = 0;
85
+ let encoding = unsafe { sn_string_view(value, &mut data, &mut len) };
86
+ let bytes = unsafe { std::slice::from_raw_parts(data, len) };
87
+ if encoding == ENCODING_OTHER || (encoding == ENCODING_ASCII && !bytes.is_ascii()) {
88
+ return Err(Unsupported);
89
+ }
90
+ let name = std::str::from_utf8(bytes).map_err(|_| Unsupported)?;
91
+ Ok(name.replace('~', "~0").replace('/', "~1"))
92
+ }
93
+
94
+ impl Engine {
95
+ fn collect(
96
+ &self,
97
+ report: &mut Report,
98
+ node: usize,
99
+ value: Value,
100
+ depth: usize,
101
+ active: scope::Context<'_>,
102
+ ) -> Result<(), Verdict> {
103
+ if depth > MAX_EVALUATION_DEPTH {
104
+ return Err(Unsupported);
105
+ }
106
+ let kind = unsafe { sn_kind(value) };
107
+ if kind == TYPE_UNSUPPORTED {
108
+ return Err(Unsupported);
109
+ }
110
+ let frame = scope::ScopeFrame {
111
+ resource: self.metadata[node].resource,
112
+ parent: active.scope,
113
+ primary_binding: active
114
+ .scope
115
+ .and_then(|f| f.primary_binding)
116
+ .or(self.metadata[node].primary_binding),
117
+ };
118
+ let active = self.enter_scope(node, active, &frame);
119
+ let tracked = self.tracks[node];
120
+ let mut prior = Tracked::new(Valid);
121
+ for (op, &source) in self.nodes[node].iter().zip(&self.error_operands[node]) {
122
+ match op {
123
+ Op::Unsupported => return Err(Unsupported),
124
+ Op::Boolean(false) => {
125
+ report.add("falseSchema", source, value, 0);
126
+ prior = Tracked::new(Invalid);
127
+ }
128
+ Op::Boolean(true) => {}
129
+ Op::Type(mask) => {
130
+ if kind & mask == 0 {
131
+ report.add("type", source, value, 0);
132
+ }
133
+ }
134
+ Op::Const(..) | Op::Enum(..) => match self.leaf(op, kind, value, depth) {
135
+ Unsupported => return Err(Unsupported),
136
+ Invalid => report.add(
137
+ if matches!(op, Op::Const(_)) {
138
+ "const"
139
+ } else {
140
+ "enum"
141
+ },
142
+ source,
143
+ value,
144
+ 0,
145
+ ),
146
+ Valid => {}
147
+ },
148
+ Op::Number(required, rules) => {
149
+ if *required != 0 && kind & required == 0 {
150
+ report.add("type", source, value, 0);
151
+ }
152
+ if kind & TYPE_NUMBER != 0 {
153
+ let bits = rules.failures(value)?;
154
+ for (bit, keyword) in [
155
+ (MAXIMUM, "maximum"),
156
+ (MINIMUM, "minimum"),
157
+ (EXCLUSIVE_MAXIMUM, "exclusiveMaximum"),
158
+ (EXCLUSIVE_MINIMUM, "exclusiveMinimum"),
159
+ (MULTIPLE_OF, "multipleOf"),
160
+ ] {
161
+ if bits & bit != 0 {
162
+ report.add(keyword, source, value, 0);
163
+ }
164
+ }
165
+ }
166
+ }
167
+ Op::String(required, rules) => {
168
+ if *required != 0 && kind != TYPE_STRING {
169
+ report.add("type", source, value, 0);
170
+ }
171
+ if kind == TYPE_STRING {
172
+ let len = if rules.min.is_some() || rules.max.is_some() {
173
+ unsafe { sn_len(value) }
174
+ } else {
175
+ 0
176
+ };
177
+ if rules.max.is_some_and(|max| len > max) {
178
+ report.add("maxLength", source, value, len);
179
+ }
180
+ if rules.min.is_some_and(|min| len < min) {
181
+ report.add("minLength", source, value, len);
182
+ }
183
+ if let Some(pattern) = &rules.pattern {
184
+ match pattern.eval(value) {
185
+ Unsupported => return Err(Unsupported),
186
+ Invalid => report.add("pattern", source, value, 0),
187
+ Valid => {}
188
+ }
189
+ }
190
+ if let Some(format) = &rules.format {
191
+ match format.eval(value) {
192
+ Unsupported => return Err(Unsupported),
193
+ Invalid => report.add("format", source, value, 0),
194
+ Valid => {}
195
+ }
196
+ }
197
+ if let Some(content) = &rules.content {
198
+ match content.eval(value) {
199
+ Unsupported => return Err(Unsupported),
200
+ Invalid => report.add(
201
+ if content.base64 {
202
+ "contentEncoding"
203
+ } else {
204
+ "contentMediaType"
205
+ },
206
+ source,
207
+ value,
208
+ 0,
209
+ ),
210
+ Valid => {}
211
+ }
212
+ }
213
+ }
214
+ }
215
+ Op::Array(required, rules) => {
216
+ if *required != 0 && kind != TYPE_ARRAY {
217
+ report.add("type", source, value, 0);
218
+ }
219
+ if kind != TYPE_ARRAY {
220
+ continue;
221
+ }
222
+ let mut coverage = Tracked::new(Valid);
223
+ let len = unsafe { sn_len(value) };
224
+ if rules.max.is_some_and(|max| len > max) {
225
+ report.add("maxItems", source, value, len);
226
+ }
227
+ if rules.min.is_some_and(|min| len < min) {
228
+ report.add("minItems", source, value, len);
229
+ }
230
+ if rules.unique {
231
+ match unique(value, depth, self.fixnum_flag) {
232
+ Unsupported => return Err(Unsupported),
233
+ Invalid => report.add("uniqueItems", source, value, 0),
234
+ Valid => {}
235
+ }
236
+ }
237
+ for (index, &child) in rules.prefix.iter().take(len).enumerate() {
238
+ if tracked {
239
+ coverage.item(index);
240
+ }
241
+ report.scoped(&format!("prefixItems/{index}"), Some(index), |r| {
242
+ self.collect(
243
+ r,
244
+ child,
245
+ unsafe { sn_entry(value, index) },
246
+ depth + 1,
247
+ active,
248
+ )
249
+ })?;
250
+ }
251
+ if rules.list {
252
+ for (index, &child) in rules.items.iter().take(len).enumerate() {
253
+ if tracked {
254
+ coverage.item(index);
255
+ }
256
+ report.scoped(&format!("items/{index}"), Some(index), |r| {
257
+ self.collect(
258
+ r,
259
+ child,
260
+ unsafe { sn_entry(value, index) },
261
+ depth + 1,
262
+ active,
263
+ )
264
+ })?;
265
+ }
266
+ if let Some(child) = rules.additional {
267
+ for index in rules.items.len()..len {
268
+ if tracked {
269
+ coverage.item(index);
270
+ }
271
+ report.scoped("additionalItems", Some(index), |r| {
272
+ self.collect(
273
+ r,
274
+ child,
275
+ unsafe { sn_entry(value, index) },
276
+ depth + 1,
277
+ active,
278
+ )
279
+ })?;
280
+ }
281
+ }
282
+ } else if let Some(&child) = rules.items.first() {
283
+ let primitive = match self.nodes[child].as_slice() {
284
+ [Op::Type(mask)]
285
+ if mask & TYPE_NUMBER != 0 || mask & TYPE_INTEGER != 0 =>
286
+ {
287
+ Some(&[][..])
288
+ }
289
+ [Op::Number(_, rules)] => rules.fast_limits.as_deref(),
290
+ _ => None,
291
+ };
292
+ let values = if len == 0 {
293
+ &[]
294
+ } else {
295
+ unsafe { std::slice::from_raw_parts(sn_array_data(value), len) }
296
+ };
297
+ let pattern = match self.nodes[child].as_slice() {
298
+ [Op::String(required, string)] if depth < MAX_EVALUATION_DEPTH => {
299
+ string.pattern_only().map(|pattern| (*required, pattern))
300
+ }
301
+ _ => None,
302
+ };
303
+ let number = match self.nodes[child].as_slice() {
304
+ [Op::Number(required, number)] if depth < MAX_EVALUATION_DEPTH => {
305
+ Some((*required, number))
306
+ }
307
+ _ => None,
308
+ };
309
+ let format = match self.nodes[child].as_slice() {
310
+ [Op::String(required, string)] if depth < MAX_EVALUATION_DEPTH => {
311
+ string.format_only().map(|format| (*required, format))
312
+ }
313
+ _ => None,
314
+ };
315
+ let content = match self.nodes[child].as_slice() {
316
+ [Op::String(required, string)] if depth < MAX_EVALUATION_DEPTH => {
317
+ string.content_only().map(|content| (*required, content))
318
+ }
319
+ _ => None,
320
+ };
321
+ if let Some((required, pattern)) = pattern {
322
+ let start = rules.prefix.len().min(len);
323
+ if tracked {
324
+ for index in start..len {
325
+ coverage.item(index);
326
+ }
327
+ }
328
+ let status = pattern.visit_invalid_items(
329
+ required,
330
+ &values[start..],
331
+ |index, item| match report.scoped(
332
+ "items",
333
+ Some(start + index),
334
+ |r| self.collect(r, child, item, depth + 1, active),
335
+ ) {
336
+ Ok(()) => Valid,
337
+ Err(status) => status,
338
+ },
339
+ );
340
+ if status != Valid {
341
+ return Err(status);
342
+ }
343
+ } else if let Some((required, format)) = format {
344
+ let start = rules.prefix.len().min(len);
345
+ if tracked {
346
+ for index in start..len {
347
+ coverage.item(index);
348
+ }
349
+ }
350
+ let status = format.visit_invalid_items(
351
+ required,
352
+ &values[start..],
353
+ |index, item| match report.scoped(
354
+ "items",
355
+ Some(start + index),
356
+ |r| self.collect(r, child, item, depth + 1, active),
357
+ ) {
358
+ Ok(()) => Valid,
359
+ Err(status) => status,
360
+ },
361
+ );
362
+ if status != Valid {
363
+ return Err(status);
364
+ }
365
+ } else if let Some((required, content)) = content {
366
+ let start = rules.prefix.len().min(len);
367
+ if tracked {
368
+ for index in start..len {
369
+ coverage.item(index);
370
+ }
371
+ }
372
+ let status = content.visit_invalid_items(
373
+ required,
374
+ &values[start..],
375
+ |index, item| match report.scoped(
376
+ "items",
377
+ Some(start + index),
378
+ |r| self.collect(r, child, item, depth + 1, active),
379
+ ) {
380
+ Ok(()) => Valid,
381
+ Err(status) => status,
382
+ },
383
+ );
384
+ if status != Valid {
385
+ return Err(status);
386
+ }
387
+ } else {
388
+ for (index, &item) in values.iter().enumerate().skip(rules.prefix.len())
389
+ {
390
+ if tracked {
391
+ coverage.item(index);
392
+ }
393
+ let needs = if let Some(limits) = primitive.filter(|_| {
394
+ item & self.fixnum_flag != 0 && depth < MAX_EVALUATION_DEPTH
395
+ }) {
396
+ let n = if limits.is_empty() {
397
+ 0
398
+ } else {
399
+ immediate_integer(item)
400
+ };
401
+ !limits.iter().all(|&(bit, limit)| match bit {
402
+ MAXIMUM => n <= limit,
403
+ MINIMUM => n >= limit,
404
+ EXCLUSIVE_MAXIMUM => n < limit,
405
+ _ => n > limit,
406
+ })
407
+ } else if let Some((required, number)) = number {
408
+ match number.eval_guarded(required, item) {
409
+ Valid => false,
410
+ Invalid => true,
411
+ Unsupported => return Err(Unsupported),
412
+ }
413
+ } else {
414
+ self.needs_diagnostics(child, item, depth + 1, active)?
415
+ };
416
+ if !needs {
417
+ continue;
418
+ }
419
+ report.scoped("items", Some(index), |r| {
420
+ self.collect(r, child, item, depth + 1, active)
421
+ })?;
422
+ }
423
+ }
424
+ }
425
+ if let Some(child) = rules.contains {
426
+ let mut matches = 0;
427
+ let values = if len == 0 {
428
+ &[]
429
+ } else {
430
+ unsafe { std::slice::from_raw_parts(sn_array_data(value), len) }
431
+ };
432
+ for (index, &item) in values.iter().enumerate() {
433
+ if !self.needs_diagnostics(child, item, depth + 1, active)? {
434
+ matches += 1;
435
+ if tracked {
436
+ coverage.item(index);
437
+ }
438
+ }
439
+ }
440
+ if bounds(matches, Some(rules.min_contains), rules.max_contains) == Invalid
441
+ {
442
+ report.add("contains", source, value, matches);
443
+ }
444
+ }
445
+ if let Some(child) = rules
446
+ .unevaluated
447
+ .filter(|_| prior.item_count() < len && coverage.item_count() < len)
448
+ {
449
+ for index in 0..len {
450
+ if prior.contains_item(index) || coverage.contains_item(index) {
451
+ continue;
452
+ }
453
+ report.scoped("unevaluatedItems", Some(index), |r| {
454
+ self.collect(
455
+ r,
456
+ child,
457
+ unsafe { sn_entry(value, index) },
458
+ depth + 1,
459
+ active,
460
+ )
461
+ })?;
462
+ coverage.item(index);
463
+ }
464
+ }
465
+ if tracked {
466
+ prior.merge_diagnostics(coverage)?;
467
+ }
468
+ }
469
+ Op::Object(required, rules) => {
470
+ if *required != 0 && kind != TYPE_OBJECT {
471
+ report.add("type", source, value, 0);
472
+ }
473
+ if kind != TYPE_OBJECT {
474
+ continue;
475
+ }
476
+ let mut coverage = Tracked::new(Valid);
477
+ let len = unsafe { sn_len(value) };
478
+ if rules.max.is_some_and(|max| len > max) {
479
+ report.add("maxProperties", source, value, len);
480
+ }
481
+ if rules.min.is_some_and(|min| len < min) {
482
+ report.add("minProperties", source, value, len);
483
+ }
484
+ for &name in &rules.required_order {
485
+ if missing(value, name, self.undefined) {
486
+ report.add("required", source, name, 0);
487
+ }
488
+ }
489
+ let mut context = ObjectDiagnostics {
490
+ engine: self,
491
+ report,
492
+ rules,
493
+ depth: depth + 1,
494
+ active,
495
+ names: false,
496
+ unevaluated: false,
497
+ coverage: &mut coverage,
498
+ prior: &prior,
499
+ tracked,
500
+ };
501
+ if (!rules.properties.is_empty()
502
+ || !rules.patterns.is_empty()
503
+ || rules.additional.is_some())
504
+ && unsafe {
505
+ sn_hash_each(
506
+ value,
507
+ (&mut context as *mut ObjectDiagnostics).cast(),
508
+ object_diagnostic,
509
+ )
510
+ } != VERDICT_VALID
511
+ {
512
+ return Err(Unsupported);
513
+ }
514
+ if rules.names.is_some() {
515
+ context.names = true;
516
+ if unsafe {
517
+ sn_hash_each(
518
+ value,
519
+ (&mut context as *mut ObjectDiagnostics).cast(),
520
+ object_diagnostic,
521
+ )
522
+ } != VERDICT_VALID
523
+ {
524
+ return Err(Unsupported);
525
+ }
526
+ }
527
+ for dependency in &rules.diagnostic_dependencies {
528
+ match dependency {
529
+ DiagnosticDependency::Required(name, names, keyword) => {
530
+ if missing(value, *name, self.undefined) {
531
+ continue;
532
+ }
533
+ for &required in names {
534
+ if missing(value, required, self.undefined) {
535
+ report.add(keyword, (source.0, *name), required, 0);
536
+ }
537
+ }
538
+ }
539
+ DiagnosticDependency::Schema(name, child, keyword) => {
540
+ if missing(value, *name, self.undefined) {
541
+ continue;
542
+ }
543
+ if self.needs_diagnostics(*child, value, depth + 1, active)? {
544
+ report.scoped(
545
+ &format!("{keyword}/{}", pointer_name(*name)?),
546
+ None,
547
+ |r| self.collect(r, *child, value, depth + 1, active),
548
+ )?;
549
+ }
550
+ if tracked {
551
+ let result = self.eval_tracked_kind(
552
+ *child,
553
+ value,
554
+ depth + 1,
555
+ active,
556
+ kind,
557
+ );
558
+ if result.status == Unsupported {
559
+ return Err(Unsupported);
560
+ }
561
+ if result.status == Valid {
562
+ coverage.merge(result);
563
+ }
564
+ }
565
+ }
566
+ }
567
+ }
568
+ if rules.unevaluated.is_some()
569
+ && prior.property_count() < len
570
+ && coverage.property_count() < len
571
+ {
572
+ let mut context = ObjectDiagnostics {
573
+ engine: self,
574
+ report,
575
+ rules,
576
+ depth: depth + 1,
577
+ active,
578
+ names: false,
579
+ unevaluated: true,
580
+ coverage: &mut coverage,
581
+ prior: &prior,
582
+ tracked,
583
+ };
584
+ if unsafe {
585
+ sn_hash_each(
586
+ value,
587
+ (&mut context as *mut ObjectDiagnostics).cast(),
588
+ object_diagnostic,
589
+ )
590
+ } != VERDICT_VALID
591
+ {
592
+ return Err(Unsupported);
593
+ }
594
+ }
595
+ if tracked {
596
+ prior.merge_diagnostics(coverage)?;
597
+ }
598
+ }
599
+ Op::All(nodes) => {
600
+ let mut coverage = Tracked::new(Valid);
601
+ for (index, &child) in nodes.iter().enumerate() {
602
+ report.scoped(&format!("allOf/{index}"), None, |r| {
603
+ self.collect(r, child, value, depth + 1, active)
604
+ })?;
605
+ if tracked {
606
+ coverage.merge_diagnostics(self.eval_tracked_kind(
607
+ child,
608
+ value,
609
+ depth + 1,
610
+ active,
611
+ kind,
612
+ ))?;
613
+ }
614
+ }
615
+ if tracked {
616
+ prior.merge_diagnostics(coverage)?;
617
+ }
618
+ }
619
+ Op::Any(nodes) | Op::One(nodes) => {
620
+ let mut matches = 0;
621
+ let mut coverage = Tracked::new(Valid);
622
+ for &(child, multiplicity) in nodes {
623
+ let branch = if tracked {
624
+ self.eval_tracked_kind(child, value, depth + 1, active, kind)
625
+ } else {
626
+ Tracked::new(self.eval(child, value, depth + 1, active))
627
+ };
628
+ match branch.status {
629
+ Unsupported => return Err(Unsupported),
630
+ Valid => {
631
+ matches += multiplicity;
632
+ if tracked {
633
+ coverage.merge(branch);
634
+ }
635
+ }
636
+ Invalid => {}
637
+ }
638
+ }
639
+ if (matches!(op, Op::Any(_)) && matches == 0)
640
+ || (matches!(op, Op::One(_)) && matches != 1)
641
+ {
642
+ report.add(
643
+ if matches!(op, Op::Any(_)) {
644
+ "anyOf"
645
+ } else {
646
+ "oneOf"
647
+ },
648
+ source,
649
+ value,
650
+ matches,
651
+ );
652
+ } else if tracked {
653
+ prior.merge_diagnostics(coverage)?;
654
+ }
655
+ }
656
+ Op::Not(child) => match self.eval(*child, value, depth + 1, active) {
657
+ Unsupported => return Err(Unsupported),
658
+ Valid => report.add("not", source, value, 0),
659
+ Invalid => {}
660
+ },
661
+ Op::Conditional(condition, yes, no) => {
662
+ let condition = if tracked {
663
+ self.eval_tracked_kind(*condition, value, depth + 1, active, kind)
664
+ } else {
665
+ Tracked::new(self.eval(*condition, value, depth + 1, active))
666
+ };
667
+ let mut coverage = Tracked::new(Valid);
668
+ let (child, segment) = match condition.status {
669
+ Unsupported => return Err(Unsupported),
670
+ Valid => {
671
+ if tracked {
672
+ coverage.merge(condition);
673
+ }
674
+ (yes, "then")
675
+ }
676
+ Invalid => (no, "else"),
677
+ };
678
+ if let Some(child) = child {
679
+ report.scoped(segment, None, |r| {
680
+ self.collect(r, *child, value, depth + 1, active)
681
+ })?;
682
+ if tracked {
683
+ coverage.merge_diagnostics(self.eval_tracked_kind(
684
+ *child,
685
+ value,
686
+ depth + 1,
687
+ active,
688
+ kind,
689
+ ))?;
690
+ }
691
+ }
692
+ if tracked {
693
+ prior.merge_diagnostics(coverage)?;
694
+ }
695
+ }
696
+ Op::Ref(target) => {
697
+ if RefFrame::contains(active.active, node, value) {
698
+ continue;
699
+ }
700
+ let frame = RefFrame {
701
+ source: node,
702
+ value,
703
+ parent: active.active,
704
+ };
705
+ let keyword = match source.0 {
706
+ OP_RECURSIVE_REF => "$recursiveRef",
707
+ OP_DYNAMIC_REF => "$dynamicRef",
708
+ _ => "$ref",
709
+ };
710
+ report.scoped(keyword, None, |r| {
711
+ self.collect(
712
+ r,
713
+ self.reference_target(*target, active),
714
+ value,
715
+ depth + 1,
716
+ scope::Context {
717
+ active: Some(&frame),
718
+ ..active
719
+ },
720
+ )
721
+ })?;
722
+ if tracked {
723
+ prior.merge_diagnostics(self.eval_tracked_kind(
724
+ self.reference_target(*target, active),
725
+ value,
726
+ depth + 1,
727
+ scope::Context {
728
+ active: Some(&frame),
729
+ ..active
730
+ },
731
+ kind,
732
+ ))?;
733
+ }
734
+ }
735
+ }
736
+ }
737
+ Ok(())
738
+ }
739
+
740
+ fn needs_diagnostics(
741
+ &self,
742
+ node: usize,
743
+ value: Value,
744
+ depth: usize,
745
+ active: scope::Context<'_>,
746
+ ) -> Result<bool, Verdict> {
747
+ let status = match self.nodes[node].as_slice() {
748
+ [Op::Number(required, rules)] if depth <= MAX_EVALUATION_DEPTH => {
749
+ rules.eval_guarded(*required, value)
750
+ }
751
+ [op @ (Op::Type(_) | Op::Boolean(_) | Op::String(..) | Op::Const(_) | Op::Enum(_))]
752
+ if depth <= MAX_EVALUATION_DEPTH =>
753
+ {
754
+ let kind = if value & self.fixnum_flag != 0 {
755
+ TYPE_INTEGRAL_NUMBER
756
+ } else {
757
+ unsafe { sn_kind(value) }
758
+ };
759
+ if kind == TYPE_UNSUPPORTED {
760
+ Unsupported
761
+ } else {
762
+ self.leaf(op, kind, value, depth)
763
+ }
764
+ }
765
+ _ => self.eval(node, value, depth, active),
766
+ };
767
+ match status {
768
+ Valid => Ok(false),
769
+ Invalid => Ok(true),
770
+ Unsupported => Err(Unsupported),
771
+ }
772
+ }
773
+ }
774
+
775
+ struct ObjectDiagnostics<'a, 'b, 'c> {
776
+ engine: &'a Engine,
777
+ report: &'b mut Report,
778
+ rules: &'a Object,
779
+ depth: usize,
780
+ active: scope::Context<'c>,
781
+ names: bool,
782
+ unevaluated: bool,
783
+ coverage: &'b mut Tracked,
784
+ prior: &'b Tracked,
785
+ tracked: bool,
786
+ }
787
+ unsafe extern "C" fn object_diagnostic(context: *mut c_void, key: Value, value: Value) -> i32 {
788
+ catch_unwind(AssertUnwindSafe(|| {
789
+ let c = unsafe { &mut *context.cast::<ObjectDiagnostics<'_, '_, '_>>() };
790
+ let visit = |report: &mut Report, child: usize, item: Value, segment: &str, schema_key| {
791
+ if c.engine.needs_diagnostics(child, item, c.depth, c.active)? {
792
+ report.property(segment, key, schema_key, |r| {
793
+ c.engine.collect(r, child, item, c.depth, c.active)
794
+ })?;
795
+ }
796
+ Ok::<_, Verdict>(())
797
+ };
798
+ let result = (|| {
799
+ if unsafe { sn_kind(key) } != TYPE_STRING {
800
+ return Err(Unsupported);
801
+ }
802
+ if c.names {
803
+ return visit(c.report, c.rules.names.unwrap(), key, "propertyNames", None);
804
+ }
805
+ if c.unevaluated {
806
+ if c.prior.contains_property(c.engine, key)
807
+ || c.coverage.contains_property(c.engine, key)
808
+ {
809
+ return Ok(());
810
+ }
811
+ visit(
812
+ c.report,
813
+ c.rules.unevaluated.unwrap(),
814
+ value,
815
+ "unevaluatedProperties",
816
+ None,
817
+ )?;
818
+ c.coverage.property(c.engine, key);
819
+ return Ok(());
820
+ }
821
+ let mut matched = false;
822
+ if !c.rules.properties.is_empty()
823
+ && let Some(&(name, child)) =
824
+ c.rules.property_targets.get(&tracking::JsonKey::new(key))
825
+ {
826
+ matched = true;
827
+ visit(c.report, child, value, "properties", Some(name))?;
828
+ }
829
+ for ((pattern, child), &name) in c.rules.patterns.iter().zip(&c.rules.pattern_names) {
830
+ match pattern.eval(key) {
831
+ Unsupported => return Err(Unsupported),
832
+ Valid => {
833
+ matched = true;
834
+ visit(c.report, *child, value, "patternProperties", Some(name))?;
835
+ }
836
+ Invalid => {}
837
+ }
838
+ }
839
+ if !matched && let Some(child) = c.rules.additional {
840
+ visit(c.report, child, value, "additionalProperties", None)?;
841
+ }
842
+ if c.tracked && (matched || c.rules.additional.is_some()) {
843
+ c.coverage.property(c.engine, key);
844
+ }
845
+ Ok(())
846
+ })();
847
+ match result {
848
+ Ok(()) => VERDICT_VALID,
849
+ Err(_) => Unsupported as i32,
850
+ }
851
+ }))
852
+ .unwrap_or(Unsupported as i32)
853
+ }
854
+
855
+ #[repr(C)]
856
+ pub struct Record {
857
+ keyword: *const u8,
858
+ keyword_len: usize,
859
+ instance_path: *const u8,
860
+ instance_len: usize,
861
+ schema_path: *const u8,
862
+ schema_len: usize,
863
+ opcode: i32,
864
+ operand: Value,
865
+ instance: Value,
866
+ detail: usize,
867
+ }
868
+ /// # Safety
869
+ /// Engine and status must be live under the GVL; instance must be a Ruby VALUE.
870
+ #[unsafe(no_mangle)]
871
+ pub unsafe extern "C" fn sn_diagnose(
872
+ engine: *const Engine,
873
+ instance: Value,
874
+ status: *mut i32,
875
+ ) -> *mut Report {
876
+ let result = catch_unwind(AssertUnwindSafe(|| {
877
+ let engine = unsafe { &*engine };
878
+ let mut report = Report::default();
879
+ engine.collect(
880
+ &mut report,
881
+ engine.root,
882
+ instance,
883
+ 0,
884
+ scope::Context::default(),
885
+ )?;
886
+ Ok::<_, Verdict>(Box::into_raw(Box::new(report)))
887
+ }));
888
+ match result {
889
+ Ok(Ok(report)) => {
890
+ unsafe { *status = VERDICT_VALID };
891
+ report
892
+ }
893
+ other => {
894
+ unsafe {
895
+ *status = if other.is_err() {
896
+ STATUS_PANIC
897
+ } else {
898
+ VERDICT_UNSUPPORTED
899
+ }
900
+ };
901
+ std::ptr::null_mut()
902
+ }
903
+ }
904
+ }
905
+ /// # Safety
906
+ /// Report must be live under the GVL.
907
+ #[unsafe(no_mangle)]
908
+ pub unsafe extern "C" fn sn_report_len(report: *const Report) -> usize {
909
+ unsafe { &*report }.errors.len()
910
+ }
911
+ /// # Safety
912
+ /// Report must be live under the GVL.
913
+ #[unsafe(no_mangle)]
914
+ pub unsafe extern "C" fn sn_report_size(report: *const Report) -> usize {
915
+ let r = unsafe { &*report };
916
+ std::mem::size_of::<Report>()
917
+ + r.errors.capacity() * std::mem::size_of::<Error>()
918
+ + r.instance_path.capacity()
919
+ + r.schema_path.capacity()
920
+ + r.errors
921
+ .iter()
922
+ .map(|e| e.instance_path.capacity() + e.schema_path.capacity())
923
+ .sum::<usize>()
924
+ }
925
+ /// # Safety
926
+ /// Report must be live, index in bounds, and record a writable pointer.
927
+ #[unsafe(no_mangle)]
928
+ pub unsafe extern "C" fn sn_report_record(
929
+ report: *const Report,
930
+ index: usize,
931
+ record: *mut Record,
932
+ ) {
933
+ let e = &unsafe { &*report }.errors[index];
934
+ unsafe {
935
+ *record = Record {
936
+ keyword: e.keyword.as_ptr(),
937
+ keyword_len: e.keyword.len(),
938
+ instance_path: e.instance_path.as_ptr(),
939
+ instance_len: e.instance_path.len(),
940
+ schema_path: e.schema_path.as_ptr(),
941
+ schema_len: e.schema_path.len(),
942
+ opcode: e.opcode,
943
+ operand: e.operand,
944
+ instance: e.instance,
945
+ detail: e.detail,
946
+ }
947
+ };
948
+ }
949
+ /// # Safety
950
+ /// Report must be live; call only from its synchronous Ruby GC mark callback.
951
+ #[unsafe(no_mangle)]
952
+ pub unsafe extern "C" fn sn_report_mark(report: *const Report) {
953
+ for error in &unsafe { &*report }.errors {
954
+ unsafe {
955
+ sn_pin(error.operand);
956
+ sn_pin(error.instance);
957
+ }
958
+ }
959
+ }
960
+ /// # Safety
961
+ /// Report must be owned, returned by sn_diagnose, and freed exactly once.
962
+ #[unsafe(no_mangle)]
963
+ pub unsafe extern "C" fn sn_report_free(report: *mut Report) {
964
+ drop(unsafe { Box::from_raw(report) });
965
+ }