ata-validator 1.7.0 → 1.7.1

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.
@@ -205,13 +205,27 @@ function deepEqual(a, b) {
205
205
  function codePointLength(s) {
206
206
  let n = 0;
207
207
  for (let i = 0; i < s.length; i++) {
208
- const c = s.charCodeAt(i);
209
- if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length) i++;
208
+ // Wraparound test: one compare classifies a high surrogate.
209
+ if (((s.charCodeAt(i) - 0xd800) >>> 0) < 0x400 && i + 1 < s.length) i++;
210
210
  n++;
211
211
  }
212
212
  return n;
213
213
  }
214
214
 
215
+ // The code point count of a UTF-16 string sits between ceil(length / 2) and
216
+ // length, so most length bounds are decided without counting anything.
217
+ function cpAtLeast(s, min) {
218
+ if (s.length >= 2 * min) return true;
219
+ if (s.length < min) return false;
220
+ return codePointLength(s) >= min;
221
+ }
222
+
223
+ function cpAtMost(s, max) {
224
+ if (s.length <= max) return true;
225
+ if (s.length > 2 * max + 1) return false;
226
+ return codePointLength(s) <= max;
227
+ }
228
+
215
229
  function typeMatches(type, d) {
216
230
  switch (type) {
217
231
  case 'string': return typeof d === 'string';
@@ -256,6 +270,11 @@ const FORMAT_CHECKS = {
256
270
  // it sees this object, so it is never mutated.
257
271
  const DISCARD = { props: null, items: null };
258
272
 
273
+ // Verdict-only error sink. When eval() sees this identity it neither builds
274
+ // error objects nor formats messages; every push site is guarded on it. The
275
+ // array is frozen so a missed guard throws in tests instead of hiding.
276
+ const NOERRORS = Object.freeze([]);
277
+
259
278
  function mergeAnnotations(target, from) {
260
279
  if (from.props && from.props.size) {
261
280
  if (!target.props) target.props = new Set();
@@ -331,6 +350,8 @@ class Plan {
331
350
  // evaluation time and cached per base URI.
332
351
  const child = (sub) => interp.node(sub);
333
352
  this.refCache = null;
353
+ this.refBase0 = undefined;
354
+ this.refRes0 = null;
334
355
  this.dynCache = null;
335
356
  this.ref = typeof schema.$ref === 'string' ? schema.$ref : null;
336
357
  this.dynamicRef = typeof schema.$dynamicRef === 'string' ? schema.$dynamicRef : null;
@@ -425,7 +446,148 @@ class Plan {
425
446
  const nodeBase = interp.state.nodeBase.get(schema);
426
447
  this.nodeBase = nodeBase !== undefined ? nodeBase : null;
427
448
  this.isResourceRoot = nodeBase !== undefined && interp.state.resources.get(nodeBase) === schema;
449
+
450
+ // A leaf plan checks only value-level keywords: no references, no
451
+ // subschemas, no annotations, no resource boundary. Those skip the whole
452
+ // eval() prologue (cycle guard, scope, annotation sinks) and run through
453
+ // evalLeaf(). Most schemas are mostly leaves.
454
+ this.leaf = !this.tracked && !this.hasApplicators && !this.hasUnevaluated &&
455
+ this.properties === null && this.patternProperties === null &&
456
+ this.additionalProperties === undefined && this.propertyNames === undefined &&
457
+ this.dependentSchemas === null && this.propertyDependencies === null &&
458
+ this.prefixItems === null && this.items === undefined && this.contains === undefined &&
459
+ this.nodeBase === null;
460
+ }
461
+ }
462
+
463
+
464
+ // Verdict-only twin of evalLeaf: no error objects, no path strings, first
465
+ // failure returns. Kept adjacent so the two cannot drift.
466
+ function evalLeafV(P, data) {
467
+ const bits = dataBits(data);
468
+ if (P.hasType && (bits & P.typeMask) === 0) return false;
469
+ if (P.enum !== null) {
470
+ let found = false;
471
+ for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
472
+ if (!found) return false;
473
+ }
474
+ if (P.hasConst && !deepEqual(P.const, data)) return false;
475
+ if (P.hasNumber && typeof data === 'number') {
476
+ if (P.minimum !== undefined && !(data >= P.minimum)) return false;
477
+ if (P.maximum !== undefined && !(data <= P.maximum)) return false;
478
+ if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) return false;
479
+ if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) return false;
480
+ if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) return false;
481
+ }
482
+ if (P.hasString && bits === T_STRING) {
483
+ if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) return false;
484
+ if (P.maxLength !== undefined && !cpAtMost(data, P.maxLength)) return false;
485
+ if (P.pattern !== null && !P.pattern.test(data)) return false;
486
+ if (P.formatFn !== null && !P.formatFn(data)) return false;
487
+ }
488
+ if (P.hasArray && bits === T_ARRAY) {
489
+ if (P.minItems !== undefined && data.length < P.minItems) return false;
490
+ if (P.maxItems !== undefined && data.length > P.maxItems) return false;
491
+ if (P.uniqueItems) {
492
+ for (let i = 0; i < data.length; i++) {
493
+ for (let j = i + 1; j < data.length; j++) {
494
+ if (deepEqual(data[i], data[j])) return false;
495
+ }
496
+ }
497
+ }
498
+ }
499
+ if (P.hasObject && bits === T_OBJECT) {
500
+ if (P.required !== null) {
501
+ const req = P.required;
502
+ for (let i = 0; i < req.length; i++) {
503
+ if (!Object.hasOwn(data, req[i])) return false;
504
+ }
505
+ }
506
+ if (P.minProperties !== undefined && Object.keys(data).length < P.minProperties) return false;
507
+ if (P.maxProperties !== undefined && Object.keys(data).length > P.maxProperties) return false;
508
+ if (P.dependentRequired !== null) {
509
+ for (const [key, deps] of P.dependentRequired) {
510
+ if (Object.hasOwn(data, key)) {
511
+ for (const dep of deps) if (!Object.hasOwn(data, dep)) return false;
512
+ }
513
+ }
514
+ }
515
+ }
516
+ return true;
517
+ }
518
+
519
+ // Value-level checks only; the caller guarantees P.leaf. The bodies mirror
520
+ // the corresponding sections of eval() exactly, minus the prologue.
521
+ function evalLeaf(P, data, errors, instancePath, schemaPath) {
522
+ let valid = true;
523
+ const schema = P.schema;
524
+ const bits = dataBits(data);
525
+ if (P.hasType && (bits & P.typeMask) === 0) {
526
+ if (errors !== NOERRORS) errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${P.typeNames.join(' or ')}`));
527
+ valid = false;
528
+ }
529
+ if (P.enum !== null) {
530
+ let found = false;
531
+ for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
532
+ if (!found) {
533
+ if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
534
+ valid = false;
535
+ }
536
+ }
537
+ if (P.hasConst && !deepEqual(P.const, data)) {
538
+ if (errors !== NOERRORS) errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
539
+ valid = false;
540
+ }
541
+ if (P.hasNumber && typeof data === 'number') {
542
+ if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
543
+ if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
544
+ if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
545
+ if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) { errors === NOERRORS || errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: P.exclusiveMaximum }, `must be < ${P.exclusiveMaximum}`)); valid = false; }
546
+ if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) { errors === NOERRORS || errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: P.multipleOf }, `must be multiple of ${P.multipleOf}`)); valid = false; }
547
+ }
548
+ if (P.hasString && bits === T_STRING) {
549
+ if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) { errors === NOERRORS || errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: P.minLength }, `must NOT have fewer than ${P.minLength} characters`)); valid = false; }
550
+ if (P.maxLength !== undefined && !cpAtMost(data, P.maxLength)) { errors === NOERRORS || errors.push(err('maxLength', 'maxLength', instancePath, schemaPath + '/maxLength', { limit: P.maxLength }, `must NOT have more than ${P.maxLength} characters`)); valid = false; }
551
+ if (P.pattern !== null && !P.pattern.test(data)) { errors === NOERRORS || errors.push(err('pattern', 'pattern', instancePath, schemaPath + '/pattern', { pattern: P.patternSource }, `must match pattern "${P.patternSource}"`)); valid = false; }
552
+ if (P.formatFn !== null && !P.formatFn(data)) { errors === NOERRORS || errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: P.format }, `must match format "${P.format}"`)); valid = false; }
553
+ }
554
+ if (P.hasArray && bits === T_ARRAY) {
555
+ if (P.minItems !== undefined && data.length < P.minItems) { errors === NOERRORS || errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: P.minItems }, `must NOT have fewer than ${P.minItems} items`)); valid = false; }
556
+ if (P.maxItems !== undefined && data.length > P.maxItems) { errors === NOERRORS || errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: P.maxItems }, `must NOT have more than ${P.maxItems} items`)); valid = false; }
557
+ if (P.uniqueItems) {
558
+ outer: for (let i = 0; i < data.length; i++) {
559
+ for (let j = i + 1; j < data.length; j++) {
560
+ if (deepEqual(data[i], data[j])) {
561
+ if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, 'must NOT have duplicate items'));
562
+ valid = false;
563
+ break outer;
564
+ }
565
+ }
566
+ }
567
+ }
568
+ }
569
+ if (P.hasObject && bits === T_OBJECT) {
570
+ const keys = Object.keys(data);
571
+ if (P.required !== null) {
572
+ const req = P.required;
573
+ for (let i = 0; i < req.length; i++) {
574
+ const key = req[i];
575
+ if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
576
+ }
577
+ }
578
+ if (P.minProperties !== undefined && keys.length < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
579
+ if (P.maxProperties !== undefined && keys.length > P.maxProperties) { errors === NOERRORS || errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
580
+ if (P.dependentRequired !== null) {
581
+ for (const [key, deps] of P.dependentRequired) {
582
+ if (Object.hasOwn(data, key)) {
583
+ for (const dep of deps) {
584
+ if (!Object.hasOwn(data, dep)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
585
+ }
586
+ }
587
+ }
588
+ }
428
589
  }
590
+ return valid;
429
591
  }
430
592
 
431
593
  class Interpreter {
@@ -441,6 +603,18 @@ class Interpreter {
441
603
  // Plans are built on first visit; a schema node is never re-read after.
442
604
  this.plans = new Map();
443
605
  this.rootNode = this.node(rootSchema);
606
+ // Compiled closure tree: undefined = not attempted, null = out of scope.
607
+ this._fast = undefined;
608
+ }
609
+
610
+ // The closure-tree compiler covers single-resource schemas without
611
+ // unevaluated keywords; everything else stays on eval(). Compiled lazily so
612
+ // construction stays cheap for schemas that never validate.
613
+ _fastRoot() {
614
+ if (this._fast === undefined) {
615
+ try { this._fast = compileInterpreter(this); } catch { this._fast = null; }
616
+ }
617
+ return this._fast;
444
618
  }
445
619
 
446
620
  pattern(src) {
@@ -473,14 +647,19 @@ class Interpreter {
473
647
  }
474
648
 
475
649
  // $ref resolution depends on the base URI in effect, which can differ
476
- // between visits of the same node; cache per (plan, base).
650
+ // between visits of the same node; cache per (plan, base). Nearly every
651
+ // plan only ever sees one base, so the last resolution lives in two plain
652
+ // fields and the Map exists only for the rare second base. The cached
653
+ // entry carries the already-planned child, so a visit does no Map lookups.
477
654
  resolveRefCached(P, base) {
655
+ if (P.refBase0 === base) return P.refRes0;
478
656
  let cache = P.refCache;
479
- if (cache === null) cache = P.refCache = new Map();
480
- let r = cache.get(base);
657
+ let r = cache !== null ? cache.get(base) : undefined;
481
658
  if (r === undefined) {
482
- r = resolveRef(P.ref, base, this.state);
483
- cache.set(base, r);
659
+ const raw = resolveRef(P.ref, base, this.state);
660
+ r = { child: raw.node === undefined ? undefined : this.node(raw.node), base: raw.base };
661
+ if (P.refBase0 === undefined) { P.refBase0 = base; P.refRes0 = r; }
662
+ else { if (cache === null) cache = P.refCache = new Map(); cache.set(base, r); }
484
663
  }
485
664
  return r;
486
665
  }
@@ -508,7 +687,21 @@ class Interpreter {
508
687
  return this.plan(schema);
509
688
  }
510
689
 
690
+ // Verdict only: same walk as validate(), no error construction at all.
691
+ isValid(data) {
692
+ const fast = this._fastRoot();
693
+ if (fast !== null) return fast.v(data, []);
694
+ const dynScope = [this.state.rootBase];
695
+ return this.eval(this.rootNode, data, this.state.rootBase, dynScope, NOERRORS, '', '#', [], DISCARD);
696
+ }
697
+
511
698
  validate(data) {
699
+ const fast = this._fastRoot();
700
+ if (fast !== null) {
701
+ const errors = [];
702
+ const valid = fast.c(data, errors, '', '#', []);
703
+ return valid ? { valid: true, data, errors: [] } : { valid: false, errors };
704
+ }
512
705
  const errors = [];
513
706
  const dynScope = [this.state.rootBase];
514
707
  // Nothing reads the root's annotations, so they are discarded from the start.
@@ -522,9 +715,10 @@ class Interpreter {
522
715
  eval(P, data, base, dynScope, errors, instancePath, schemaPath, stack, sink) {
523
716
  if (P === true) return true;
524
717
  if (P === false) {
525
- errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
718
+ if (errors !== NOERRORS) errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
526
719
  return false;
527
720
  }
721
+ if (P.leaf) return evalLeaf(P, data, errors, instancePath, schemaPath);
528
722
  const schema = P.schema;
529
723
 
530
724
  // Only a reference can lead evaluation back to a schema it is already
@@ -542,9 +736,13 @@ class Interpreter {
542
736
  if (P.nodeBase !== null && P.nodeBase !== base) base = P.nodeBase;
543
737
  // Entering a schema resource extends the dynamic scope, whether evaluation
544
738
  // lands on the resource's root or on a subschema inside it: a reference
545
- // to first#/$defs/stuff still passes through resource `first`.
739
+ // to first#/$defs/stuff still passes through resource `first`. The scope
740
+ // is pushed in place and popped on the way out; evaluation is depth-first
741
+ // and nothing retains the array.
742
+ let scopePushed = false;
546
743
  if (dynScope[dynScope.length - 1] !== base) {
547
- dynScope = dynScope.concat(base);
744
+ dynScope.push(base);
745
+ scopePushed = true;
548
746
  }
549
747
 
550
748
  let valid = true;
@@ -556,13 +754,13 @@ class Interpreter {
556
754
 
557
755
  // $ref / $dynamicRef: in-place applicators, annotations flow through
558
756
  if (P.ref !== null) {
559
- const { node, base: refBase } = this.resolveRefCached(P, base);
560
- if (node === undefined) {
561
- errors.push(err('$ref', '$ref', instancePath, schemaPath + '/$ref', { ref: P.ref }, `cannot resolve $ref ${P.ref}`));
757
+ const { child, base: refBase } = this.resolveRefCached(P, base);
758
+ if (child === undefined) {
759
+ if (errors !== NOERRORS) errors.push(err('$ref', '$ref', instancePath, schemaPath + '/$ref', { ref: P.ref }, `cannot resolve $ref ${P.ref}`));
562
760
  valid = false;
563
761
  } else {
564
762
  const sub = collect ? { props: null, items: null } : DISCARD;
565
- if (!this.eval(this.node(node), data, refBase, dynScope, errors, instancePath, schemaPath + '/$ref', stack, sub)) valid = false;
763
+ if (!this.eval(child, data, refBase, dynScope, errors, instancePath, schemaPath + '/$ref', stack, sub)) valid = false;
566
764
  else if (collect) mergeAnnotations(local, sub);
567
765
  }
568
766
  }
@@ -597,7 +795,7 @@ class Interpreter {
597
795
  }
598
796
  }
599
797
  if (node === undefined) {
600
- errors.push(err('$dynamicRef', '$dynamicRef', instancePath, schemaPath + '/$dynamicRef', { ref }, `cannot resolve $dynamicRef ${ref}`));
798
+ if (errors !== NOERRORS) errors.push(err('$dynamicRef', '$dynamicRef', instancePath, schemaPath + '/$dynamicRef', { ref }, `cannot resolve $dynamicRef ${ref}`));
601
799
  valid = false;
602
800
  } else {
603
801
  const sub = collect ? { props: null, items: null } : DISCARD;
@@ -610,48 +808,48 @@ class Interpreter {
610
808
 
611
809
  // type / enum / const
612
810
  if (P.hasType && (bits & P.typeMask) === 0) {
613
- errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${P.typeNames.join(' or ')}`));
811
+ if (errors !== NOERRORS) errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${P.typeNames.join(' or ')}`));
614
812
  valid = false;
615
813
  }
616
814
  if (P.enum !== null) {
617
815
  let found = false;
618
816
  for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
619
817
  if (!found) {
620
- errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
818
+ if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
621
819
  valid = false;
622
820
  }
623
821
  }
624
822
  if (P.hasConst && !deepEqual(P.const, data)) {
625
- errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
823
+ if (errors !== NOERRORS) errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
626
824
  valid = false;
627
825
  }
628
826
 
629
827
  // numbers
630
828
  if (P.hasNumber && typeof data === 'number') {
631
- if (P.minimum !== undefined && !(data >= P.minimum)) { errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
632
- if (P.maximum !== undefined && !(data <= P.maximum)) { errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
633
- if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
634
- if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) { errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: P.exclusiveMaximum }, `must be < ${P.exclusiveMaximum}`)); valid = false; }
635
- if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) { errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: P.multipleOf }, `must be multiple of ${P.multipleOf}`)); valid = false; }
829
+ if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
830
+ if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
831
+ if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
832
+ if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) { errors === NOERRORS || errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: P.exclusiveMaximum }, `must be < ${P.exclusiveMaximum}`)); valid = false; }
833
+ if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) { errors === NOERRORS || errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: P.multipleOf }, `must be multiple of ${P.multipleOf}`)); valid = false; }
636
834
  }
637
835
 
638
836
  // strings
639
837
  if (P.hasString && bits === T_STRING) {
640
- if (P.minLength !== undefined && codePointLength(data) < P.minLength) { errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: P.minLength }, `must NOT have fewer than ${P.minLength} characters`)); valid = false; }
641
- if (P.maxLength !== undefined && codePointLength(data) > P.maxLength) { errors.push(err('maxLength', 'maxLength', instancePath, schemaPath + '/maxLength', { limit: P.maxLength }, `must NOT have more than ${P.maxLength} characters`)); valid = false; }
642
- if (P.pattern !== null && !P.pattern.test(data)) { errors.push(err('pattern', 'pattern', instancePath, schemaPath + '/pattern', { pattern: P.patternSource }, `must match pattern "${P.patternSource}"`)); valid = false; }
643
- if (P.formatFn !== null && !P.formatFn(data)) { errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: P.format }, `must match format "${P.format}"`)); valid = false; }
838
+ if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) { errors === NOERRORS || errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: P.minLength }, `must NOT have fewer than ${P.minLength} characters`)); valid = false; }
839
+ if (P.maxLength !== undefined && !cpAtMost(data, P.maxLength)) { errors === NOERRORS || errors.push(err('maxLength', 'maxLength', instancePath, schemaPath + '/maxLength', { limit: P.maxLength }, `must NOT have more than ${P.maxLength} characters`)); valid = false; }
840
+ if (P.pattern !== null && !P.pattern.test(data)) { errors === NOERRORS || errors.push(err('pattern', 'pattern', instancePath, schemaPath + '/pattern', { pattern: P.patternSource }, `must match pattern "${P.patternSource}"`)); valid = false; }
841
+ if (P.formatFn !== null && !P.formatFn(data)) { errors === NOERRORS || errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: P.format }, `must match format "${P.format}"`)); valid = false; }
644
842
  }
645
843
 
646
844
  // arrays
647
845
  if (P.hasArray && bits === T_ARRAY) {
648
- if (P.minItems !== undefined && data.length < P.minItems) { errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: P.minItems }, `must NOT have fewer than ${P.minItems} items`)); valid = false; }
649
- if (P.maxItems !== undefined && data.length > P.maxItems) { errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: P.maxItems }, `must NOT have more than ${P.maxItems} items`)); valid = false; }
846
+ if (P.minItems !== undefined && data.length < P.minItems) { errors === NOERRORS || errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: P.minItems }, `must NOT have fewer than ${P.minItems} items`)); valid = false; }
847
+ if (P.maxItems !== undefined && data.length > P.maxItems) { errors === NOERRORS || errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: P.maxItems }, `must NOT have more than ${P.maxItems} items`)); valid = false; }
650
848
  if (P.uniqueItems) {
651
849
  outer: for (let i = 0; i < data.length; i++) {
652
850
  for (let j = i + 1; j < data.length; j++) {
653
851
  if (deepEqual(data[i], data[j])) {
654
- errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, 'must NOT have duplicate items'));
852
+ if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, 'must NOT have duplicate items'));
655
853
  valid = false;
656
854
  break outer;
657
855
  }
@@ -676,12 +874,12 @@ class Interpreter {
676
874
  if (P.contains !== undefined) {
677
875
  const matched = [];
678
876
  for (let i = 0; i < data.length; i++) {
679
- const scratch = [];
877
+ const scratch = errors === NOERRORS ? NOERRORS : [];
680
878
  if (this.eval(P.contains, data[i], base, dynScope, scratch, instancePath + '/' + i, schemaPath + '/contains', stack, DISCARD)) matched.push(i);
681
879
  }
682
880
  const minC = P.minContains !== undefined ? P.minContains : 1;
683
- if (matched.length < minC) { errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC }, `must contain at least ${minC} valid item(s)`)); valid = false; }
684
- if (P.maxContains !== undefined && matched.length > P.maxContains) { errors.push(err('maxContains', 'maxContains', instancePath, schemaPath + '/maxContains', { limit: P.maxContains }, `must NOT contain more than ${P.maxContains} valid item(s)`)); valid = false; }
881
+ if (matched.length < minC) { errors === NOERRORS || errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC }, `must contain at least ${minC} valid item(s)`)); valid = false; }
882
+ if (P.maxContains !== undefined && matched.length > P.maxContains) { errors === NOERRORS || errors.push(err('maxContains', 'maxContains', instancePath, schemaPath + '/maxContains', { limit: P.maxContains }, `must NOT contain more than ${P.maxContains} valid item(s)`)); valid = false; }
685
883
  if (collect && matched.length) {
686
884
  if (!local.items) local.items = new Set();
687
885
  for (const i of matched) local.items.add(i);
@@ -696,16 +894,16 @@ class Interpreter {
696
894
  const req = P.required;
697
895
  for (let i = 0; i < req.length; i++) {
698
896
  const key = req[i];
699
- if (!Object.hasOwn(data, key)) { errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
897
+ if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
700
898
  }
701
899
  }
702
- if (P.minProperties !== undefined && keys.length < P.minProperties) { errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
703
- if (P.maxProperties !== undefined && keys.length > P.maxProperties) { errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
900
+ if (P.minProperties !== undefined && keys.length < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
901
+ if (P.maxProperties !== undefined && keys.length > P.maxProperties) { errors === NOERRORS || errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
704
902
  if (P.dependentRequired !== null) {
705
903
  for (const [key, deps] of P.dependentRequired) {
706
904
  if (Object.hasOwn(data, key)) {
707
905
  for (const dep of deps) {
708
- if (!Object.hasOwn(data, dep)) { errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
906
+ if (!Object.hasOwn(data, dep)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
709
907
  }
710
908
  }
711
909
  }
@@ -782,7 +980,7 @@ class Interpreter {
782
980
  }
783
981
  if (P.anyOf !== null) {
784
982
  let any = false;
785
- const scratch = [];
983
+ const scratch = errors === NOERRORS ? NOERRORS : [];
786
984
  for (let i = 0; i < P.anyOf.length; i++) {
787
985
  const sub = collect ? { props: null, items: null } : DISCARD;
788
986
  if (this.eval(P.anyOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, sub)) {
@@ -791,15 +989,15 @@ class Interpreter {
791
989
  }
792
990
  }
793
991
  if (!any) {
794
- for (const e of scratch) errors.push(e);
795
- errors.push(err('anyOf', 'anyOf', instancePath, schemaPath + '/anyOf', {}, 'must match a schema in anyOf'));
992
+ if (errors !== NOERRORS) for (const e of scratch) errors.push(e);
993
+ if (errors !== NOERRORS) errors.push(err('anyOf', 'anyOf', instancePath, schemaPath + '/anyOf', {}, 'must match a schema in anyOf'));
796
994
  valid = false;
797
995
  }
798
996
  }
799
997
  if (P.oneOf !== null) {
800
998
  let count = 0;
801
999
  let winner = null;
802
- const scratch = [];
1000
+ const scratch = errors === NOERRORS ? NOERRORS : [];
803
1001
  for (let i = 0; i < P.oneOf.length; i++) {
804
1002
  const sub = collect ? { props: null, items: null } : DISCARD;
805
1003
  if (this.eval(P.oneOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, sub)) {
@@ -810,20 +1008,20 @@ class Interpreter {
810
1008
  if (count === 1) { if (collect) mergeAnnotations(local, winner); }
811
1009
  else {
812
1010
  if (count === 0) for (const e of scratch) errors.push(e);
813
- errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
1011
+ if (errors !== NOERRORS) errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
814
1012
  valid = false;
815
1013
  }
816
1014
  }
817
1015
  if (P.not !== undefined) {
818
- const scratch = [];
1016
+ const scratch = errors === NOERRORS ? NOERRORS : [];
819
1017
  if (this.eval(P.not, data, base, dynScope, scratch, instancePath, schemaPath + '/not', stack, DISCARD)) {
820
- errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
1018
+ if (errors !== NOERRORS) errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
821
1019
  valid = false;
822
1020
  }
823
1021
  }
824
1022
  if (P.if !== undefined) {
825
1023
  const ifSub = collect ? { props: null, items: null } : DISCARD;
826
- const ifScratch = [];
1024
+ const ifScratch = errors === NOERRORS ? NOERRORS : [];
827
1025
  const ifOk = this.eval(P.if, data, base, dynScope, ifScratch, instancePath, schemaPath + '/if', stack, ifSub);
828
1026
  if (ifOk) {
829
1027
  if (collect) mergeAnnotations(local, ifSub);
@@ -860,6 +1058,7 @@ class Interpreter {
860
1058
  }
861
1059
  }
862
1060
 
1061
+ if (scopePushed) dynScope.pop();
863
1062
  if (tracked) stack.length -= 2;
864
1063
  if (valid && collect && sink !== DISCARD) mergeAnnotations(sink, local);
865
1064
  return valid;
@@ -880,6 +1079,11 @@ function err(code, keyword, instancePath, schemaPath, params, message) {
880
1079
  return { keyword, instancePath, schemaPath, params, message };
881
1080
  }
882
1081
 
1082
+ const { compileInterpreter } = require('./plan-compiler').install({
1083
+ Plan, NOERRORS, err, evalLeaf, evalLeafV, deepEqual, dataBits, escapePointer,
1084
+ T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
1085
+ });
1086
+
883
1087
  function createInterpreter(schema, options) {
884
1088
  return new Interpreter(schema, options);
885
1089
  }
@@ -53,7 +53,8 @@ function buildErrorLiteral (opts) {
53
53
  function _cpLen(s) {
54
54
  const len = s.length;
55
55
  for (let i = 0; i < len; i++) {
56
- if (s.charCodeAt(i) >= 0xD800 && s.charCodeAt(i) <= 0xDBFF) {
56
+ // Wraparound test: one compare classifies a high surrogate.
57
+ if (((s.charCodeAt(i) - 0xD800) >>> 0) < 0x400) {
57
58
  // Found a high surrogate — count code points the slow way
58
59
  let n = 0; for (const _ of s) n++; return n;
59
60
  }
@@ -354,7 +355,7 @@ function compileToJS(schema, defs, schemaMap) {
354
355
  }
355
356
  if (schema.maxLength !== undefined) {
356
357
  const max = schema.maxLength
357
- checks.push((d) => typeof d !== 'string' || d.length <= max || _cpLen(d) <= max)
358
+ checks.push((d) => typeof d !== 'string' || d.length <= max || (d.length <= 2 * max + 1 && _cpLen(d) <= max))
358
359
  }
359
360
  if (schema.pattern) {
360
361
  try {