ata-validator 1.63.1 → 1.63.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ata.js CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  const { env } = require('../lib/env');
4
+ // Text that comes from a schema file (a $ref, a keyword name in a warning) is
5
+ // written with its control characters as escapes, as `ata validate` writes
6
+ // it: a schema could otherwise clear the screen or forge an `ata:` line.
7
+ const term = (t) => require('../lib/diagnose').term(String(t));
4
8
 
5
9
  const fs = require('fs');
6
10
  const path = require('path');
@@ -299,7 +303,7 @@ function cmdCompile(args) {
299
303
  for (const w of compileWarnings) reportCompileError(input, w);
300
304
  process.exit(1);
301
305
  }
302
- for (const w of compileWarnings) process.stderr.write(`ata: warning: ${input}: ${w}\n`);
306
+ for (const w of compileWarnings) process.stderr.write(`ata: warning: ${input}: ${term(w)}\n`);
303
307
  }
304
308
  if (!src) {
305
309
  reportCompileError(input, 'schema is too complex for standalone compilation');
@@ -397,7 +401,7 @@ function cmdBuild(args) {
397
401
  for (const c of report.compiled) {
398
402
  process.stdout.write(`ata: ${c.input} -> ${c.output} (${c.bytes.toLocaleString()} bytes${c.errors === 'compact' ? ', shared errors' : c.errors === 'schema' ? ', schema module' : ''})\n`);
399
403
  for (const t of (report.bunTargets || []).filter((t) => t.input === c.input)) process.stdout.write(`ata: bun target -> ${t.output} (for bun build --compile --aot)\n`);
400
- for (const d of (report.bunDeclined || []).filter((d) => d.input === c.input)) process.stderr.write(`ata: warning: ${d.input}: ${d.reason}\n`);
404
+ for (const d of (report.bunDeclined || []).filter((d) => d.input === c.input)) process.stderr.write(`ata: warning: ${d.input}: ${term(d.reason)}\n`);
401
405
  }
402
406
  if (report.runtime) {
403
407
  process.stdout.write(`ata: error runtime -> ${report.runtime.output} (${report.runtime.bytes.toLocaleString()} bytes, shared by the modules above)\n`);
@@ -412,7 +416,7 @@ function cmdBuild(args) {
412
416
  noDetail++;
413
417
  process.stderr.write(`ata: warning ${w.input}: no error detail, failures report the single ATA9000 error (the verdict is exact)\n`);
414
418
  } else {
415
- process.stderr.write(`ata: warning ${w.input}: ${w.warning}\n`);
419
+ process.stderr.write(`ata: warning ${w.input}: ${term(w.warning)}\n`);
416
420
  }
417
421
  }
418
422
  if (noDetail > 0 && args.opts.mode !== 'schema') {
@@ -422,7 +426,7 @@ function cmdBuild(args) {
422
426
  process.stdout.write(`ata: cached ${c.input}\n`);
423
427
  }
424
428
  for (const s of report.skipped) {
425
- process.stdout.write(`ata: skipped ${s.input}: ${s.reason}\n`);
429
+ process.stdout.write(`ata: skipped ${s.input}: ${term(s.reason)}\n`);
426
430
  }
427
431
  for (const f of report.failed) {
428
432
  reportCompileError(f.input, f.error);
package/lib/ir/emit.mjs CHANGED
@@ -16,8 +16,16 @@ const FAST = {
16
16
  pattern: (n, x) => compilePatternInline(n, x),
17
17
  // A string has between length/2 and length code points, so the length
18
18
  // alone settles most cases and the count runs only near the bound.
19
- minLength: (n, x) => `${x}.length>=${2 * n}||${x}.length>=${n}&&cpLen(${x})>=${n}`,
20
- maxLength: (n, x) => `${x}.length<=${n}||${x}.length<=${2 * n}&&cpLen(${x})<=${n}`,
19
+ // The bound is written into the module as a number, so it has to be one: a
20
+ // schema that gave a string here had it written as source, and what the
21
+ // string said ran in whatever imported the module. Refused, as the closure
22
+ // writer refuses it.
23
+ minLength: (n, x) => { const b = bound(n, 'minLength'); return `${x}.length>=${2 * b}||${x}.length>=${b}&&cpLen(${x})>=${b}` },
24
+ maxLength: (n, x) => { const b = bound(n, 'maxLength'); return `${x}.length<=${b}||${x}.length<=${2 * b}&&cpLen(${x})<=${b}` },
25
+ }
26
+ function bound (n, k) {
27
+ if (typeof n !== 'number' || !Number.isFinite(n)) throw new Error(`ir: ${k} is not a number`)
28
+ return n
21
29
  }
22
30
  // enum and const with only primitive values need no deep equality: the
23
31
  // value is one of them exactly when it is === one of them (JSON numbers have
@@ -1147,11 +1147,16 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1147
1147
  const expected = types.join(', ')
1148
1148
  // As written: a one-element array stays an array, as the interpreted engine
1149
1149
  // reports it.
1150
- const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
1150
+ // Escaped like every other schema string written into this source: a
1151
+ // type name is the schema author's text, and a quote in one that is not
1152
+ // a JSON Schema type closed the literal and ran what followed as code,
1153
+ // in the process that compiled the schema and in the module written
1154
+ // from it.
1155
+ const expectedParam = !Array.isArray(schema.type) ? `'${esc(String(types[0]))}'` : JSON.stringify(types)
1151
1156
  {
1152
1157
  const typeSp = `${schemaPrefix}/type`
1153
1158
  const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
1154
- lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type:${expectedParam}},message:'must be ${expected}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
1159
+ lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type:${expectedParam}},message:'must be ${esc(expected)}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
1155
1160
  }
1156
1161
  }
1157
1162
 
@@ -2038,7 +2043,9 @@ TYPE_KEYWORDS.integer = TYPE_KEYWORDS.number
2038
2043
  const NOT_VALIDATING = new Set(['$schema', '$id', '$anchor', '$comment', '$defs', 'definitions', 'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly', '$vocabulary'])
2039
2044
  function typeMismatchRest(schema, types) {
2040
2045
  const drop = new Set(['type', 'enum', 'const', 'anyOf', 'oneOf'])
2041
- for (const t of types) for (const k of (TYPE_KEYWORDS[t] || [])) drop.add(k)
2046
+ // Own names only: a type called `constructor` or `__proto__` read
2047
+ // Object.prototype's member here and threw while compiling.
2048
+ for (const t of types) if (Object.prototype.hasOwnProperty.call(TYPE_KEYWORDS, t)) for (const k of TYPE_KEYWORDS[t]) drop.add(k)
2042
2049
  // A built-in format is written for strings only (`typeof v==='string'&&`
2043
2050
  // off the string path), so for a value that failed `type: string` it can
2044
2051
  // never fire: carried here it was dead code in every module that checks one.
@@ -2435,11 +2442,16 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2435
2442
  const expected = types.join(', ')
2436
2443
  // As written: a one-element array stays an array, as the interpreted engine
2437
2444
  // reports it.
2438
- const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
2445
+ // Escaped like every other schema string written into this source: a
2446
+ // type name is the schema author's text, and a quote in one that is not
2447
+ // a JSON Schema type closed the literal and ran what followed as code,
2448
+ // in the process that compiled the schema and in the module written
2449
+ // from it.
2450
+ const expectedParam = !Array.isArray(schema.type) ? `'${esc(String(types[0]))}'` : JSON.stringify(types)
2439
2451
  // Type check: push error but continue — wrap remaining in type-success block
2440
2452
  const typeOk = `_tok${ctx.varCounter++}`
2441
2453
  lines.push(`const ${typeOk}=${conds.join('||')}`)
2442
- lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:${expectedParam}}`, `'must be ${expected}'`)}}`)
2454
+ lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:${expectedParam}}`, `'must be ${esc(expected)}'`)}}`)
2443
2455
  // Subsequent optimized code runs inside if(typeOk){...}
2444
2456
  if (types.length === 1) {
2445
2457
  isObj = types[0] === 'object'
@@ -832,9 +832,9 @@ function resolveRef(ref, defs, schemaMap) {
832
832
 
833
833
  function buildTypeCheck(types) {
834
834
  if (types.length === 1) {
835
- return TYPE_CHECKS[types[0]] || (() => true)
835
+ return typeCheckOf(types[0]) || (() => true)
836
836
  }
837
- const fns = types.map(t => TYPE_CHECKS[t]).filter(Boolean)
837
+ const fns = types.map(typeCheckOf).filter(Boolean)
838
838
  return (d) => {
839
839
  for (let i = 0; i < fns.length; i++) {
840
840
  if (fns[i](d)) return true
@@ -902,6 +902,10 @@ function multipleOfBad(v, m) {
902
902
  return `!(Number.isInteger(${v}/${m})||Math.abs(${v}/${m}-Math.round(${v}/${m}))<1e-9)`
903
903
  }
904
904
 
905
+ // Own names only: a type called `__proto__` or `valueOf` read
906
+ // Object.prototype's member and was handed out as the check, and the
907
+ // Validator then threw on the first document (`_fast is not a function`).
908
+ const typeCheckOf = (t) => (Object.prototype.hasOwnProperty.call(TYPE_CHECKS, t) ? TYPE_CHECKS[t] : undefined)
905
909
  const TYPE_CHECKS = {
906
910
  string: (d) => typeof d === 'string',
907
911
  number: (d) => Number.isFinite(d),
@@ -4198,7 +4202,11 @@ function genDynamicRefNode(schema, v, lines, ctx, knownType) {
4198
4202
  function genUniqueItemsNode(schema, v, lines, ctx, knownType, hoisted, isArr, types) {
4199
4203
  const si = ctx.varCounter++
4200
4204
  const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
4201
- const isPrimItems = itemType === 'string' || itemType === 'number' || itemType === 'integer'
4205
+ // `items` speaks for the elements after the tuple only: with `prefixItems`
4206
+ // the first positions may hold objects, and comparing those by identity
4207
+ // took two equal objects for different ones and accepted the array.
4208
+ const noTuple = !Array.isArray(schema.prefixItems) || schema.prefixItems.length === 0
4209
+ const isPrimItems = noTuple && (itemType === 'string' || itemType === 'number' || itemType === 'integer')
4202
4210
  const maxItems = schema.maxItems
4203
4211
  // Small primitive arrays (maxItems <= 16): nested loop is 6x faster than Set
4204
4212
  // No allocation, no hash computation — just direct === comparison
@@ -36,6 +36,15 @@ const { parse } = require('./safe-regex')
36
36
  // holds them: every accepted pattern answers as the linear engine does and
37
37
  // stays fast on inputs built to make a backtracking engine retry.
38
38
  const NATIVE_ESCAPES = new Set(['d', 'D', 'w', 'W', 's', 'S', 'n', 'r', 't', 'f', 'v', '.', '\\', '/', '^', '$', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}', '-', 'x', 'u'])
39
+ // How many ways a backtracking engine can split one string over the bounded
40
+ // choices of a pattern. Choices in a row multiply: `(a?){30}` is thirty
41
+ // choices and two to the thirtieth ways, which the engine walks through on a
42
+ // string that does not match. The count used to be a sum capped at 64, so
43
+ // that pattern and `^([a-z]?){64}$` passed as linear and a string of 17
44
+ // characters ran for more than half a minute. Now the product, capped where
45
+ // the worst case stays under a millisecond for a hundred characters.
46
+ const MAX_WAYS = 4096
47
+ // The sum of bounded choices, which both rules below also hold to.
39
48
  const MAX_BOUNDED_CHOICES = 64
40
49
 
41
50
  const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
@@ -52,7 +61,9 @@ function anchored (ast) {
52
61
  function simpleRule (ast) {
53
62
  // Per subtree: how many unbounded quantifiers it holds (`u`) and how many
54
63
  // bounded choices a backtracking engine could try (`b`), or null where the
55
- // shape is not provably linear.
64
+ // shape is not provably linear. The choices are held to a sum
65
+ // (MAX_BOUNDED_CHOICES: a long counted run is slow to compile and to
66
+ // unwind) and to a product (MAX_WAYS, waysOf below).
56
67
  const info = (n) => {
57
68
  switch (n.t) {
58
69
  case 'star': case 'plus':
@@ -78,7 +89,7 @@ function simpleRule (ast) {
78
89
  }
79
90
  }
80
91
  const r = info(ast)
81
- if (r === null || r.u > 1 || r.b > MAX_BOUNDED_CHOICES) return false
92
+ if (r === null || r.u > 1 || r.b > MAX_BOUNDED_CHOICES || !(waysOf(ast) <= MAX_WAYS)) return false
82
93
  return r.u === 0 || anchored(ast)
83
94
  }
84
95
 
@@ -207,9 +218,31 @@ function deterministicRule (ast) {
207
218
  for (let p = 0; p < sets.length && ok; p++) disjoint(follow[p])
208
219
  if (!ok) return false
209
220
  if (bounded > MAX_BOUNDED_CHOICES) return false
221
+ // A counted repeat of a body that can end in more than one place is not a
222
+ // loop to the walk above (`(a?){30}`, `(a{1,8}){8}`), and it is where the
223
+ // ways multiply: the same product as the simple rule holds here too.
224
+ if (!(waysOf(ast) <= MAX_WAYS)) return false
210
225
  return unbounded === 0 || anchored(ast)
211
226
  }
212
227
 
228
+ // The product the simple rule counts, over any shape: an unbounded
229
+ // quantifier adds no bounded choice of its own.
230
+ function waysOf (n) {
231
+ switch (n.t) {
232
+ case 'star': case 'plus': return waysOf(n.child)
233
+ case 'quest': return atom(n.child) ? 2 : 1 + waysOf(n.child)
234
+ case 'repeat': {
235
+ if (n.max === Infinity) return waysOf(n.child)
236
+ if (atom(n.child)) return n.max - n.min + 1
237
+ return (n.max - n.min + 1) * Math.pow(waysOf(n.child), n.max)
238
+ }
239
+ case 'group': return waysOf(n.child)
240
+ case 'concat': { let w = 1; for (const part of n.parts) w *= waysOf(part); return w }
241
+ case 'alt': { let w = 0; for (const part of n.opts) w += waysOf(part); return w }
242
+ default: return 1
243
+ }
244
+ }
245
+
213
246
  // Two more shapes the rules above decline, both anchored with `^` and `$`
214
247
  // around one top-level sequence.
215
248
  //
@@ -393,6 +426,11 @@ function nativeIsLinear (src) {
393
426
  }
394
427
  let ast
395
428
  try { ast = parse(src) } catch { return false }
429
+ // The product of the bounded choices holds for every rule below, so it is
430
+ // asked here, once: the one-choice and delimited-loop rules did not ask it,
431
+ // and `^(a*b)*` followed by thirty `a?` reached the platform RegExp, where
432
+ // each `a?` more doubles the time on a string that does not match.
433
+ if (!(waysOf(ast) <= MAX_WAYS)) return false
396
434
  return simpleRule(ast) || deterministicRule(ast) || oneChoiceRule(ast) || delimitedLoopRule(ast)
397
435
  }
398
436
 
@@ -981,6 +981,12 @@ function emitObject(schema, ctx, out, depth) {
981
981
  const reqMasks = new Array(words).fill(0);
982
982
  if (required) for (const k of required) { const x = bit.get(k); reqMasks[x.w] |= x.b; }
983
983
 
984
+ // Many names are dispatched on a hash of the key, and two names with one
985
+ // hash cannot be: the code below then took the other branch, which reads
986
+ // variables only the few-names form declares, and the scanner threw a
987
+ // ReferenceError on every document that carried a declared key. Such a
988
+ // schema is left to the parser.
989
+ if (names.length >= HASH_DISPATCH_MIN && new Set(names.map(spanHash)).size !== names.length) decline('properties:hash-collision');
984
990
  out.push('c=s.charCodeAt(++i);');
985
991
  const useHash = names.length >= HASH_DISPATCH_MIN;
986
992
  if (useHash) {
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.63.1';
10
+ module.exports = '1.63.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.63.1",
3
+ "version": "1.63.2",
4
4
  "description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",
@@ -266,13 +266,13 @@
266
266
  "codegen.d.ts"
267
267
  ],
268
268
  "optionalDependencies": {
269
- "@ata-validator/native-darwin-arm64": "1.63.1",
270
- "@ata-validator/native-darwin-x64": "1.63.1",
271
- "@ata-validator/native-linux-arm64-gnu": "1.63.1",
272
- "@ata-validator/native-linux-arm64-musl": "1.63.1",
273
- "@ata-validator/native-linux-x64-gnu": "1.63.1",
274
- "@ata-validator/native-linux-x64-musl": "1.63.1",
275
- "@ata-validator/native-win32-x64": "1.63.1"
269
+ "@ata-validator/native-darwin-arm64": "1.63.2",
270
+ "@ata-validator/native-darwin-x64": "1.63.2",
271
+ "@ata-validator/native-linux-arm64-gnu": "1.63.2",
272
+ "@ata-validator/native-linux-arm64-musl": "1.63.2",
273
+ "@ata-validator/native-linux-x64-gnu": "1.63.2",
274
+ "@ata-validator/native-linux-x64-musl": "1.63.2",
275
+ "@ata-validator/native-win32-x64": "1.63.2"
276
276
  },
277
277
  "peerDependencies": {
278
278
  "yaml": "^2.0.0"