ata-validator 1.48.0 → 1.50.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.
@@ -9,7 +9,7 @@ const { compileFunction } = require('./compile-fn')
9
9
  // the first call that compiles errors. Everything here was written as part
10
10
  // of js-compiler.js and reads its helpers from that module's _internal.
11
11
  const core = require('./js-compiler')
12
- const { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun } = core._internal
12
+ const { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun } = core._internal
13
13
 
14
14
 
15
15
  function emitPtrEsc(ctx) {
@@ -289,7 +289,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
289
289
  if (hasUnresolvableRef(schema, eRootDefs, eAnchors, schemaMap, new Set())) return null
290
290
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
291
291
 
292
- const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootE: schema, rootSchema: inputSchema, userFormats: userFormats || null,
292
+ const ctx = { noOrdinal: !!(sourceOpts && sourceOpts.noOrdinal), varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootE: schema, rootSchema: inputSchema, userFormats: userFormats || null,
293
293
  // Custom format checkers referenced by the body, bound as
294
294
  // closure parameters below the way the other entry points do.
295
295
  closureVars: [], closureVals: [],
@@ -1143,7 +1143,8 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1143
1143
 
1144
1144
  // enum
1145
1145
  if (schema.enum) {
1146
- lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
1146
+ const ev = enumOperand(ctx, schema.enum, v, lines)
1147
+ lines.push(`if(!(${enumCondition(ctx, schema.enum, ev)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
1147
1148
  }
1148
1149
 
1149
1150
  // const — use canonical (sorted-key) comparison for objects
@@ -2330,7 +2331,8 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2330
2331
  const emitEnumConst = () => {
2331
2332
  // enum
2332
2333
  if (schema.enum) {
2333
- lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
2334
+ const ev = enumOperand(ctx, schema.enum, v, lines)
2335
+ lines.push(`if(!(${enumCondition(ctx, schema.enum, ev)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
2334
2336
  }
2335
2337
 
2336
2338
  // const
@@ -135,6 +135,21 @@ function emitConstant(ctx, value) {
135
135
  return name
136
136
  }
137
137
 
138
+ // The operand an `enum` test compares: `v` itself when it is a plain
139
+ // identifier, otherwise a local bound once. The test is a chain of `===`,
140
+ // one per member, and each used to repeat the access expression, so a
141
+ // 32-member enum on a required property read `_e3["tool"]` 32 times; on an
142
+ // array of items in several shapes those reads are polymorphic and cost
143
+ // 59 ns against 20 with the value in a local. The binding goes to `lines`
144
+ // ahead of the test.
145
+ const IDENT_RE = /^[A-Za-z_$][\w$]*$/
146
+ function enumOperand(ctx, vals, v, lines) {
147
+ if (IDENT_RE.test(v) || vals.length < 2) return v
148
+ const name = `_ev${ctx.varCounter++}`
149
+ lines.push(`const ${name}=${v}`)
150
+ return name
151
+ }
152
+
138
153
  // One `enum` test: primitives compare with `===`, which needs no call, and
139
154
  // object members go through structural equality.
140
155
  function enumCondition(ctx, vals, v) {
@@ -1316,6 +1331,14 @@ function sharedDefNames (root, defs) {
1316
1331
  }
1317
1332
  return out
1318
1333
  }
1334
+ // Raising this to inline small definitions at every site was measured and
1335
+ // rejected: SchemaStore's detekt applies one 200-character rule definition
1336
+ // at 293 sites, and inlining it took the verdict from 5287 ns to 452 but the
1337
+ // standalone module from 168 KB to 1692 KB and its compile to 33 ms. The
1338
+ // slowness of the shared function is megamorphic property reads (474 objects
1339
+ // of 72 shapes through one function); a for-in body for the shared function
1340
+ // was tried too and gave 1.5x, not enough to carry its complexity. Left as a
1341
+ // known trade: on that shape the shared function costs 27 ns a call.
1319
1342
  const SHARED_DEF_MIN_CHARS = 120
1320
1343
 
1321
1344
  function cyclicDefNames (defs) {
@@ -1986,8 +2009,32 @@ function sharedCodegenGate(schema, schemaMap) {
1986
2009
  _gateSchema = schema; _gateMap = schemaMap; _gateOk = ok
1987
2010
  return ok
1988
2011
  }
2012
+ // `unevaluatedProperties` or `unevaluatedItems` beside a `$ref` back to the
2013
+ // document root: the generated evaluated-set tracking does not credit what
2014
+ // the root evaluates through that reference, so a tree whose branches say
2015
+ // `{ unevaluatedProperties: false, $ref: '#' }` rejected valid branches (a
2016
+ // wrong reject, found through the 2019-09 `$recursiveRef` tests, and the same
2017
+ // in 2020-12). The interpreted engine answers it. A text check first: almost
2018
+ // no schema has both.
2019
+ function unevaluatedBesideRootRef(schema) {
2020
+ const text = JSON.stringify(schema)
2021
+ if (!text.includes('"$ref":"#"') || !/"unevaluated(?:Properties|Items)"/.test(text)) return false
2022
+ let found = false
2023
+ const seen = new Set()
2024
+ const walk = (n) => {
2025
+ if (found || n === null || typeof n !== 'object' || seen.has(n)) return
2026
+ seen.add(n)
2027
+ if (Array.isArray(n)) { for (const x of n) walk(x); return }
2028
+ if (n.$ref === '#' && (n.unevaluatedProperties !== undefined || n.unevaluatedItems !== undefined)) { found = true; return }
2029
+ for (const [k, v] of Object.entries(n)) if (k !== 'enum' && k !== 'const' && k !== 'default' && k !== 'examples') walk(v)
2030
+ }
2031
+ walk(schema)
2032
+ return found
2033
+ }
2034
+
1989
2035
  function sharedCodegenGateUncached(schema, schemaMap) {
1990
2036
  if (bothDefsContainers(schema)) return false
2037
+ if (unevaluatedBesideRootRef(schema)) return false
1991
2038
  if (hasSameInstanceRefCycle(schema)) return false
1992
2039
  if (!codegenSafe(schema, schemaMap)) return false
1993
2040
  if (needsBaseTracking(schema, schemaMap, new Set())) return false
@@ -3297,7 +3344,8 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3297
3344
 
3298
3345
  // enum
3299
3346
  if (schema.enum) {
3300
- lines.push(`if(!(${enumCondition(ctx, schema.enum, v)}))return false`)
3347
+ const ev = enumOperand(ctx, schema.enum, v, lines)
3348
+ lines.push(`if(!(${enumCondition(ctx, schema.enum, ev)}))return false`)
3301
3349
  }
3302
3350
 
3303
3351
  // const
@@ -5371,4 +5419,4 @@ Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDyn
5371
5419
  // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
5372
5420
  Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
5373
5421
 
5374
- module.exports._internal = { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun }
5422
+ module.exports._internal = { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun }
@@ -2,8 +2,14 @@
2
2
 
3
3
  // The official meta-schemas, vendored so a `$ref` to one of them resolves
4
4
  // without a network. Draft 2020-12 is eight documents joined by $dynamicRef;
5
- // draft-07 is one. Sources: https://json-schema.org/draft/2020-12/schema and
6
- // http://json-schema.org/draft-07/schema, fetched 2026-08-23, unmodified.
5
+ // draft-07 and draft-06 are one each; 2019-09 is seven. Sources:
6
+ // https://json-schema.org/draft/2020-12/schema and
7
+ // http://json-schema.org/draft-07/schema, fetched 2026-08-23, and
8
+ // http://json-schema.org/draft-06/schema and
9
+ // https://json-schema.org/draft/2019-09/schema, fetched 2026-10-10, unmodified.
10
+ // The 2019-09 documents are read through the same 2019-09 rewrite as a
11
+ // caller's schema (lib/draft7.js) once, at load, since they use
12
+ // `$recursiveRef`, which the engines read as `$dynamicRef`.
7
13
  // Keyed by $id; index.js also registers the http/https and trailing-# spellings.
8
14
 
9
15
  const METASCHEMAS = new Map([
@@ -16,6 +22,19 @@ const METASCHEMAS = new Map([
16
22
  ["https://json-schema.org/draft/2020-12/meta/content", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/content","$dynamicAnchor":"meta","title":"Content vocabulary meta-schema","type":["object","boolean"],"properties":{"contentEncoding":{"type":"string"},"contentMediaType":{"type":"string"},"contentSchema":{"$dynamicRef":"#meta"}}}],
17
23
  ["https://json-schema.org/draft/2020-12/meta/unevaluated", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/unevaluated","$dynamicAnchor":"meta","title":"Unevaluated applicator vocabulary meta-schema","type":["object","boolean"],"properties":{"unevaluatedItems":{"$dynamicRef":"#meta"},"unevaluatedProperties":{"$dynamicRef":"#meta"}}}],
18
24
  ["http://json-schema.org/draft-07/schema#", {"$schema":"http://json-schema.org/draft-07/schema#","$id":"http://json-schema.org/draft-07/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"$comment":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"default":true,"readOnly":{"type":"boolean","default":false},"writeOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":true},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":true,"enum":{"type":"array","items":true,"minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"if":{"$ref":"#"},"then":{"$ref":"#"},"else":{"$ref":"#"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":true}],
25
+ ["http://json-schema.org/draft-06/schema#", {"$schema":"http://json-schema.org/draft-06/schema#","$id":"http://json-schema.org/draft-06/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"title":{"type":"string"},"description":{"type":"string"},"default":{},"examples":{"type":"array","items":{}},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":{}},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":{},"enum":{"type":"array","minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":{}}],
26
+ ["https://json-schema.org/draft/2019-09/schema", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/schema","$vocabulary":{"https://json-schema.org/draft/2019-09/vocab/core":true,"https://json-schema.org/draft/2019-09/vocab/applicator":true,"https://json-schema.org/draft/2019-09/vocab/validation":true,"https://json-schema.org/draft/2019-09/vocab/meta-data":true,"https://json-schema.org/draft/2019-09/vocab/format":false,"https://json-schema.org/draft/2019-09/vocab/content":true},"$recursiveAnchor":true,"title":"Core and Validation specifications meta-schema","allOf":[{"$ref":"meta/core"},{"$ref":"meta/applicator"},{"$ref":"meta/validation"},{"$ref":"meta/meta-data"},{"$ref":"meta/format"},{"$ref":"meta/content"}],"type":["object","boolean"],"properties":{"definitions":{"$comment":"While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.","type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}},"dependencies":{"$comment":"\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"","type":"object","additionalProperties":{"anyOf":[{"$recursiveRef":"#"},{"$ref":"meta/validation#/$defs/stringArray"}]}}}}],
27
+ ["https://json-schema.org/draft/2019-09/meta/core", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/core","$recursiveAnchor":true,"title":"Core vocabulary meta-schema","type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference","$comment":"Non-empty fragments not allowed.","pattern":"^[^#]*#?$"},"$schema":{"type":"string","format":"uri"},"$anchor":{"type":"string","pattern":"^[A-Za-z][-A-Za-z0-9.:_]*$"},"$ref":{"type":"string","format":"uri-reference"},"$recursiveRef":{"type":"string","format":"uri-reference"},"$recursiveAnchor":{"type":"boolean","default":false},"$vocabulary":{"type":"object","propertyNames":{"type":"string","format":"uri"},"additionalProperties":{"type":"boolean"}},"$comment":{"type":"string"},"$defs":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}}}}],
28
+ ["https://json-schema.org/draft/2019-09/meta/applicator", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/applicator","$recursiveAnchor":true,"title":"Applicator vocabulary meta-schema","type":["object","boolean"],"properties":{"additionalItems":{"$recursiveRef":"#"},"unevaluatedItems":{"$recursiveRef":"#"},"items":{"anyOf":[{"$recursiveRef":"#"},{"$ref":"#/$defs/schemaArray"}]},"contains":{"$recursiveRef":"#"},"additionalProperties":{"$recursiveRef":"#"},"unevaluatedProperties":{"$recursiveRef":"#"},"properties":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependentSchemas":{"type":"object","additionalProperties":{"$recursiveRef":"#"}},"propertyNames":{"$recursiveRef":"#"},"if":{"$recursiveRef":"#"},"then":{"$recursiveRef":"#"},"else":{"$recursiveRef":"#"},"allOf":{"$ref":"#/$defs/schemaArray"},"anyOf":{"$ref":"#/$defs/schemaArray"},"oneOf":{"$ref":"#/$defs/schemaArray"},"not":{"$recursiveRef":"#"}},"$defs":{"schemaArray":{"type":"array","minItems":1,"items":{"$recursiveRef":"#"}}}}],
29
+ ["https://json-schema.org/draft/2019-09/meta/validation", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/validation","$recursiveAnchor":true,"title":"Validation vocabulary meta-schema","type":["object","boolean"],"properties":{"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/$defs/nonNegativeInteger"},"minLength":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"maxItems":{"$ref":"#/$defs/nonNegativeInteger"},"minItems":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"maxContains":{"$ref":"#/$defs/nonNegativeInteger"},"minContains":{"$ref":"#/$defs/nonNegativeInteger","default":1},"maxProperties":{"$ref":"#/$defs/nonNegativeInteger"},"minProperties":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"required":{"$ref":"#/$defs/stringArray"},"dependentRequired":{"type":"object","additionalProperties":{"$ref":"#/$defs/stringArray"}},"const":true,"enum":{"type":"array","items":true},"type":{"anyOf":[{"$ref":"#/$defs/simpleTypes"},{"type":"array","items":{"$ref":"#/$defs/simpleTypes"},"minItems":1,"uniqueItems":true}]}},"$defs":{"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"$ref":"#/$defs/nonNegativeInteger","default":0},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}}}],
30
+ ["https://json-schema.org/draft/2019-09/meta/meta-data", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/meta-data","$recursiveAnchor":true,"title":"Meta-data vocabulary meta-schema","type":["object","boolean"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"default":true,"deprecated":{"type":"boolean","default":false},"readOnly":{"type":"boolean","default":false},"writeOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true}}}],
31
+ ["https://json-schema.org/draft/2019-09/meta/format", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/format","$recursiveAnchor":true,"title":"Format vocabulary meta-schema","type":["object","boolean"],"properties":{"format":{"type":"string"}}}],
32
+ ["https://json-schema.org/draft/2019-09/meta/content", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/content","$recursiveAnchor":true,"title":"Content vocabulary meta-schema","type":["object","boolean"],"properties":{"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"contentSchema":{"$recursiveRef":"#"}}}],
19
33
  ]);
20
34
 
35
+ {
36
+ const { isDraft2019, normalizeDraft2019 } = require('./draft7')
37
+ for (const [id, doc] of METASCHEMAS) if (isDraft2019(doc)) METASCHEMAS.set(id, normalizeDraft2019(JSON.parse(JSON.stringify(doc)), true))
38
+ }
39
+
21
40
  module.exports = { METASCHEMAS };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ // The public names of the package, shared by index.js (with the code
4
+ // generator) and index.edge.js (without it), so the two entries export the
5
+ // same thing.
6
+ const core = require('./validator-core.js');
7
+
8
+
9
+
10
+ // The parts no Validator calls: TypeScript generation, the error renderers,
11
+ // the spec output format, the retry message for language models, the schema
12
+ // description and the suggestion helper. ata-validator/lite leaves them out.
13
+ // Each loads its module on first call. None of them is on the path to a first
14
+ // validated request, and together they were about a sixth of what requiring
15
+ // the package cost.
16
+ function toTypeScript (...a) { return require('./ts-gen').toTypeScript(...a); }
17
+ function renderPretty (...a) { return require('./render-pretty').renderPretty(...a); }
18
+ function renderCompact (...a) { return require('./render-compact').renderCompact(...a); }
19
+ function toOutput (...a) { return require('./output-format').toOutput(...a); }
20
+ function toRetryMessage (...a) { return require('./retry-message').toRetryMessage(...a); }
21
+ function describeSchema (...a) { return require('./describe-schema').describeSchema(...a); }
22
+ function renderJSON (...a) { return require('./render-json').renderJSON(...a); }
23
+
24
+ // Walk a JSON pointer (RFC 6901 escapes) into a data tree. Mirrors the helper
25
+ // inside lib/suggestions.js, kept local to avoid exporting an internal.
26
+ function _walkPointer (root, pointer) {
27
+ if (!pointer) return root;
28
+ const parts = pointer.replace(/^\//, '').split('/').map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
29
+ let cur = root;
30
+ for (const p of parts) { if (cur == null) return undefined; cur = cur[p]; }
31
+ return cur;
32
+ }
33
+
34
+ // Post-hoc suggestion enrichment for AOT-compiled validators. The standalone
35
+ // modules do not embed the suggestion engine, so consumers who want
36
+ // suggestions pass the error array through this after validation.
37
+ function attachSuggestions (errors, data) {
38
+ if (!errors) return errors;
39
+ const { suggestFor } = require('./suggestions');
40
+ const { reprValue } = require('./enrich-error');
41
+ for (const e of errors) {
42
+ if (!e || e.suggestion) continue;
43
+ let received = e.received;
44
+ if (received === undefined && data !== undefined) {
45
+ const ptr = e.instancePath != null ? e.instancePath : (e.path || '');
46
+ const raw = _walkPointer(data, ptr);
47
+ if (raw !== undefined || ptr === '') received = reprValue(raw);
48
+ }
49
+ const probe = received !== undefined && e.received === undefined
50
+ ? Object.assign({}, e, { received })
51
+ : e;
52
+ const s = suggestFor(probe, data);
53
+ if (s) e.suggestion = s;
54
+ }
55
+ // AOT modules import nothing, so this is their only route to a frame. The
56
+ // caller holds the original object and ran no preprocessing through here.
57
+ require('./diagnostic-source').setDiagnosticSource(errors, { data, mutatesInput: false });
58
+ return errors;
59
+ }
60
+
61
+ module.exports = {
62
+ Validator: core.Validator,
63
+ compile: core.compile,
64
+ validate: core.validate,
65
+ validateAsync: core.validateAsync,
66
+ parseAsync: core.parseAsync,
67
+ version: core.version,
68
+ createPaddedBuffer: core.createPaddedBuffer,
69
+ SIMDJSON_PADDING: core.SIMDJSON_PADDING,
70
+ parseJSON: core.parseJSON,
71
+ toTypeScript,
72
+ defineSchema: core.defineSchema,
73
+ renderPretty,
74
+ renderCompact,
75
+ toOutput,
76
+ toRetryMessage,
77
+ describeSchema,
78
+ renderJSON,
79
+ attachSuggestions, // internal: used by the renderers; not public API
80
+ };
@@ -3,34 +3,56 @@
3
3
  const { parse } = require('./safe-regex')
4
4
 
5
5
  // True when the platform RegExp, a backtracking engine, also runs `src` in
6
- // linear time, so it can take the pattern: every unbounded quantifier on one
7
- // character, class or `.`; at most one of them, and then anchored with `^`;
8
- // a finite repeat may cover a group that holds no unbounded quantifier; few
9
- // bounded choices in all; only escapes both engines read alike; nothing
10
- // outside the BMP, so both see the same UTF-16 units.
11
- // tests/test_regex_native_linear.js holds it.
6
+ // linear time, so it can take the pattern instead of the linear-time engine
7
+ // in safe-regex.js, which costs three to five times as much per character.
8
+ // Two rules, and a pattern passes on either:
9
+ //
10
+ // The simple rule, the original one: every unbounded quantifier is on one
11
+ // character, class or `.`; at most one of them, and then the pattern is
12
+ // anchored with `^` on every branch; a finite repeat may cover a group that
13
+ // holds no unbounded quantifier; few bounded choices in all. One loop whose
14
+ // body cannot be split two ways, exited at most once per position, with
15
+ // bounded work after it.
16
+ //
17
+ // The deterministic rule, for the patterns the simple one declines: dotted
18
+ // identifiers, `[^/]+/[^/]+`, version numbers. On the Glushkov construction
19
+ // the characters, classes and dots are positions. Around every unbounded
20
+ // loop the next character must decide the single way to continue: the
21
+ // positions that can follow a position inside a loop accept pairwise
22
+ // disjoint characters, and so do the positions a match can start at when a
23
+ // loop is among them; a position reaches a follower by one route only, since
24
+ // `(a+)+` reaches `a` from `a` through the inner loop and again through the
25
+ // outer one and that double route is what a backtracking engine explores
26
+ // exponentially; a loop body must not match the empty string, since `(a*)*`
27
+ // iterates emptily without bound. Ambiguity away from any loop, `(pwa|PWA)`,
28
+ // costs a bounded factor and is allowed, as the simple rule allows it. A
29
+ // bounded repeat is a loop only when its body holds an unbounded one, since
30
+ // `(\w+\s?){5}` splits five ways where `(?:[A-F0-9]{2}:){31}` counts. With
31
+ // an unbounded quantifier the pattern must be anchored with `^` on every
32
+ // branch, since an unanchored search retries from each position.
33
+ //
34
+ // Both rules allow only escapes both engines read alike and nothing outside
35
+ // the BMP, so both see the same UTF-16 units. tests/test_regex_native_linear.js
36
+ // holds them: every accepted pattern answers as the linear engine does and
37
+ // stays fast on inputs built to make a backtracking engine retry.
12
38
  const NATIVE_ESCAPES = new Set(['d', 'D', 'w', 'W', 's', 'S', 'n', 'r', 't', 'f', 'v', '.', '\\', '/', '^', '$', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}', '-', 'x', 'u'])
13
- function nativeIsLinear (src) {
14
- if (typeof src !== 'string' || src.length > 512) return false
15
- for (let i = 0; i < src.length; i++) {
16
- const c = src.charCodeAt(i)
17
- if (c >= 0xd800 && c <= 0xdfff) return false
18
- if (src[i] === '\\') {
19
- const e = src[i + 1]
20
- if (!NATIVE_ESCAPES.has(e)) return false
21
- if (e === 'x' && !/^[0-9a-fA-F]{2}$/.test(src.slice(i + 2, i + 4))) return false
22
- if (e === 'u' && !/^[0-9a-fA-F]{4}$/.test(src.slice(i + 2, i + 6))) return false
23
- i++
24
- }
25
- }
26
- let ast
27
- try { ast = parse(src) } catch { return false }
39
+ const MAX_BOUNDED_CHOICES = 64
40
+
41
+ const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
42
+
43
+ // Anchored at the start on every top-level branch.
44
+ function anchored (ast) {
45
+ let top = ast
46
+ while (top.t === 'group') top = top.child
47
+ const branches = top.t === 'alt' ? top.opts : [top]
48
+ const first = (n) => { while (n && n.t === 'group') n = n.child; return n && n.t === 'concat' ? first(n.parts[0]) : n }
49
+ return branches.every((b) => { const f = first(b); return f && f.t === 'bol' })
50
+ }
51
+
52
+ function simpleRule (ast) {
28
53
  // Per subtree: how many unbounded quantifiers it holds (`u`) and how many
29
54
  // bounded choices a backtracking engine could try (`b`), or null where the
30
- // shape is not provably linear. A finite repeat of a group is linear when
31
- // the group holds no unbounded quantifier: `(?:[A-F0-9]{2}:){31}` has
32
- // nothing to retry, and its choices multiply through the nesting.
33
- const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
55
+ // shape is not provably linear.
34
56
  const info = (n) => {
35
57
  switch (n.t) {
36
58
  case 'star': case 'plus':
@@ -56,19 +78,154 @@ function nativeIsLinear (src) {
56
78
  }
57
79
  }
58
80
  const r = info(ast)
59
- const ok = r !== null
60
- const unbounded = ok ? r.u : 0
61
- const bounded = ok ? r.b : 0
62
- if (!ok || unbounded > 1 || bounded > 64) return false
63
- if (unbounded === 1) {
64
- // Anchored at the start on every top-level branch.
65
- let top = ast
66
- while (top.t === 'group') top = top.child
67
- const branches = top.t === 'alt' ? top.opts : [top]
68
- const first = (n) => { while (n && n.t === 'group') n = n.child; return n && n.t === 'concat' ? first(n.parts[0]) : n }
69
- if (!branches.every((b) => { const f = first(b); return f && f.t === 'bol' })) return false
81
+ if (r === null || r.u > 1 || r.b > MAX_BOUNDED_CHOICES) return false
82
+ return r.u === 0 || anchored(ast)
83
+ }
84
+
85
+ // The characters a position accepts: 128 ASCII bits and one bit for "some
86
+ // code point at 128 or above", which keeps two classes with non-ASCII
87
+ // members apart only when one of them has none. Conservative in the right
88
+ // direction: an overlap that is not there only sends a pattern to the
89
+ // linear engine.
90
+ function charSet (node) {
91
+ const bits = new Uint8Array(128)
92
+ let high = 0
93
+ if (node.t === 'any') {
94
+ bits.fill(1); bits[10] = 0; bits[13] = 0; high = 1
95
+ } else if (node.t === 'char') {
96
+ if (node.c < 128) bits[node.c] = 1; else high = 1
97
+ } else {
98
+ for (const [lo, hi] of node.ranges) {
99
+ for (let c = Math.max(0, lo), top = Math.min(127, hi); c <= top; c++) bits[c] = 1
100
+ if (hi >= 128) high = 1
101
+ }
102
+ if (node.neg) { for (let c = 0; c < 128; c++) bits[c] ^= 1; high = 1 }
70
103
  }
71
- return true
104
+ return { bits, high }
105
+ }
106
+
107
+ function overlap (a, b) {
108
+ if (a.high && b.high) return true
109
+ for (let c = 0; c < 128; c++) if (a.bits[c] & b.bits[c]) return true
110
+ return false
111
+ }
112
+
113
+ // Whether a subtree holds an unbounded quantifier.
114
+ function hasUnbounded (n) {
115
+ switch (n.t) {
116
+ case 'star': case 'plus': return true
117
+ case 'repeat': return n.max === Infinity || hasUnbounded(n.child)
118
+ case 'quest': case 'group': return hasUnbounded(n.child)
119
+ case 'concat': return n.parts.some(hasUnbounded)
120
+ case 'alt': return n.opts.some(hasUnbounded)
121
+ default: return false
122
+ }
123
+ }
124
+
125
+ function deterministicRule (ast) {
126
+ const sets = []
127
+ const follow = []
128
+ const inLoop = []
129
+ let ok = true
130
+ let unbounded = 0
131
+ let bounded = 0
132
+ let depth = 0
133
+ // Pairwise disjoint, required when a loop position is among them.
134
+ const disjoint = (ids) => {
135
+ const arr = Array.from(ids)
136
+ if (!arr.some((p) => inLoop[p])) return
137
+ for (let i = 0; i < arr.length && ok; i++) {
138
+ for (let j = i + 1; j < arr.length; j++) if (overlap(sets[arr[i]], sets[arr[j]])) { ok = false; return }
139
+ }
140
+ }
141
+ const link = (from, to) => {
142
+ for (const p of to) {
143
+ if (follow[from].has(p)) { ok = false; return }
144
+ follow[from].add(p)
145
+ }
146
+ }
147
+ const empty = () => ({ nullable: true, first: new Set(), last: new Set() })
148
+ const walk = (n) => {
149
+ if (!ok) return empty()
150
+ switch (n.t) {
151
+ case 'char': case 'class': case 'any': {
152
+ const id = sets.length
153
+ sets.push(charSet(n))
154
+ follow.push(new Set())
155
+ inLoop.push(depth > 0)
156
+ return { nullable: false, first: new Set([id]), last: new Set([id]) }
157
+ }
158
+ case 'bol': case 'eol': return empty()
159
+ case 'group': return walk(n.child)
160
+ case 'star': case 'plus': case 'quest': case 'repeat': {
161
+ const infinite = n.t === 'star' || n.t === 'plus' || (n.t === 'repeat' && n.max === Infinity)
162
+ const loops = infinite || (n.t === 'repeat' && n.max > 1 && hasUnbounded(n.child))
163
+ const optional = n.t === 'star' || n.t === 'quest' || (n.t === 'repeat' && n.min === 0)
164
+ if (loops) depth++
165
+ const c = walk(n.child)
166
+ if (loops) depth--
167
+ if (infinite) {
168
+ unbounded++
169
+ if (c.nullable) ok = false
170
+ } else if (n.t === 'quest') {
171
+ bounded += 1
172
+ } else {
173
+ bounded += (n.max - n.min)
174
+ if (!atom(n.child)) bounded += n.max
175
+ }
176
+ if (loops) for (const l of c.last) link(l, c.first)
177
+ return { nullable: c.nullable || optional, first: c.first, last: c.last }
178
+ }
179
+ case 'concat': {
180
+ let acc = empty()
181
+ for (const part of n.parts) {
182
+ const r = walk(part)
183
+ for (const l of acc.last) link(l, r.first)
184
+ const first = new Set(acc.first)
185
+ if (acc.nullable) for (const f of r.first) first.add(f)
186
+ const last = new Set(r.last)
187
+ if (r.nullable) for (const l of acc.last) last.add(l)
188
+ acc = { nullable: acc.nullable && r.nullable, first, last }
189
+ }
190
+ return acc
191
+ }
192
+ case 'alt': {
193
+ const rs = n.opts.map(walk)
194
+ const first = new Set()
195
+ const last = new Set()
196
+ for (const r of rs) { for (const f of r.first) first.add(f); for (const l of r.last) last.add(l) }
197
+ return { nullable: rs.some((r) => r.nullable), first, last }
198
+ }
199
+ default:
200
+ ok = false
201
+ return empty()
202
+ }
203
+ }
204
+ const root = walk(ast)
205
+ if (!ok) return false
206
+ disjoint(root.first)
207
+ for (let p = 0; p < sets.length && ok; p++) disjoint(follow[p])
208
+ if (!ok) return false
209
+ if (bounded > MAX_BOUNDED_CHOICES) return false
210
+ return unbounded === 0 || anchored(ast)
211
+ }
212
+
213
+ function nativeIsLinear (src) {
214
+ if (typeof src !== 'string' || src.length > 512) return false
215
+ for (let i = 0; i < src.length; i++) {
216
+ const c = src.charCodeAt(i)
217
+ if (c >= 0xd800 && c <= 0xdfff) return false
218
+ if (src[i] === '\\') {
219
+ const e = src[i + 1]
220
+ if (!NATIVE_ESCAPES.has(e)) return false
221
+ if (e === 'x' && !/^[0-9a-fA-F]{2}$/.test(src.slice(i + 2, i + 4))) return false
222
+ if (e === 'u' && !/^[0-9a-fA-F]{4}$/.test(src.slice(i + 2, i + 6))) return false
223
+ i++
224
+ }
225
+ }
226
+ let ast
227
+ try { ast = parse(src) } catch { return false }
228
+ return simpleRule(ast) || deterministicRule(ast)
72
229
  }
73
230
 
74
231
  module.exports = { nativeIsLinear }
package/lib/safe-regex.js CHANGED
@@ -410,7 +410,8 @@ function __ataSafeRegex () {
410
410
  id = states.length
411
411
  let isMatch = false
412
412
  for (let k = 0; k < pcs.length; k++) if (ops[pcs[k]] === OP_MATCH) { isMatch = true; break }
413
- states.push({ pcs: Int32Array.from(pcs), isMatch, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })
413
+ // `dead`: an anchored match with no thread left cannot recover.
414
+ states.push({ pcs: Int32Array.from(pcs), isMatch, dead: anchored && pcs.length === 0, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })
414
415
  stateIds.set(key, id)
415
416
  return id
416
417
  }
@@ -452,23 +453,36 @@ function __ataSafeRegex () {
452
453
  }
453
454
  if (id < 0) return testNFA(s)
454
455
  let state = states[id]
455
- for (let pos = 0; pos < len; pos++) {
456
+ if (len === 0) return state.isMatch
457
+ // The last character is stepped with the end-of-input flag set and is
458
+ // handled after the loop, so the loop body selects no table and tests
459
+ // no position.
460
+ const last = len - 1
461
+ for (let pos = 0; pos < last; pos++) {
456
462
  if (state.isMatch) return true
457
463
  const c = s.charCodeAt(pos)
458
- const atEnd = pos + 1 === len
459
464
  let nid
460
465
  if (c < 128) {
461
- const table = atEnd ? state.nextEnd : state.next
462
- nid = table[c]
463
- if (nid === -2) { nid = step(state, c, atEnd); table[c] = nid }
466
+ nid = state.next[c]
467
+ if (nid === -2) { nid = step(state, c, false); state.next[c] = nid }
464
468
  } else {
465
- nid = step(state, c, atEnd)
469
+ nid = step(state, c, false)
466
470
  }
467
471
  if (nid < 0) return testNFA(s)
468
472
  state = states[nid]
469
- if (anchored && state.pcs.length === 0) return false
473
+ if (state.dead) return false
470
474
  }
471
- return state.isMatch
475
+ if (state.isMatch) return true
476
+ const c = s.charCodeAt(last)
477
+ let nid
478
+ if (c < 128) {
479
+ nid = state.nextEnd[c]
480
+ if (nid === -2) { nid = step(state, c, true); state.nextEnd[c] = nid }
481
+ } else {
482
+ nid = step(state, c, true)
483
+ }
484
+ if (nid < 0) return testNFA(s)
485
+ return states[nid].isMatch
472
486
  }
473
487
 
474
488
  return function test (s) {
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ // The runtime a schema module (`ata build --mode schema`) imports: the verdict
4
+ // and the errors both come from the interpreted engine walking the embedded
5
+ // schema, so a module is its schema and a few lines. Smaller and quicker to
6
+ // load than generated code, slower per check once warm; see docs/API.md.
7
+ // One interpreter per schema answers both, so the schema is prepared once.
8
+ // It loads lib/interpreter.js, which brings the plan compiler for schemas
9
+ // used often (TIER in lib/interpreter-core.js).
10
+ const { createInterpreter } = require('./interpreter');
11
+ const { errorsOf, interpreterOptions } = require('./error-runtime-entry');
12
+
13
+ function createValidator(schema, codeOf, options) {
14
+ const interp = createInterpreter(schema, interpreterOptions(options));
15
+ return {
16
+ isValid: (data) => interp.isValid(data),
17
+ errors: (data) => errorsOf(interp, schema, codeOf, data),
18
+ };
19
+ }
20
+
21
+ module.exports = { createValidator };