ata-validator 1.27.0 → 1.28.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
4
4
 
5
+ ## 1.28.0 - 2026-09-23
6
+
7
+ ### Added
8
+
9
+ - `engine: 'interpreter'`, a per-validator setting that keeps a schema off code generation: no `new Function`, no shared compile cache; the eval-free interpreted engine answers `validate()`, `isValidObject()` and `validateJSON()`. The default is the right trade for a schema the application wrote. A schema that arrives at runtime from a plugin or a tenant is input, and until now the only way to validate against one without executing source derived from it was the process-wide `ATA_FORCE_NAPI` switch. `engine()` reports `'interpreter'` for such a validator; any value other than `'auto'` or `'interpreter'` throws, so a misspelling cannot fall through to the generator. `tests/test_engine_option.js` holds the verdicts equal across engines and proves, with `Function` guarded, that the interpreter validator never builds a function from source while the default one does. Contributed by @d4tocchini in #43.
10
+
11
+ ### Changed
12
+
13
+ - The position map behind `dataFrame` is built without per-node work, which makes an invalid document cheaper to report on. It used to record an entry for every node in the document: a JSON pointer rebuilt from a path array with `concat`, `map` and `join`, a binary search and an object allocation for the value and another pair for its key, and a `JSON.parse` of every string value whose result was then discarded. String values are now spanned and not decoded, a key decodes only when its token holds an escape or a raw control character, the pointer is carried down the walk instead of being rebuilt, and line and column are resolved at the end for the nodes that were recorded. On a 132 KB config with three errors the map went from 2800 to about 1040 microseconds, which is 19.8x down to 7.3x the cost of `JSON.parse` on the same text, and the whole `validateJSON` call went from 3673 to 1825 microseconds. Output is unchanged: 112661 fuzzed documents, random and truncated and CRLF and BOM-prefixed and byte-punched, map identically against the previous walker, field order and throws included, so a document that declined to map still declines. Standalone modules built with `{ positions: true }` embed this walk, so they carry the same improvement once rebuilt. `tests/test_position_map_cost.js` gates it as a ratio to `JSON.parse` rather than as a time, because the cost is what regressed.
14
+
15
+ ## 1.27.1 - 2026-09-17
16
+
17
+ ### Fixed
18
+
19
+ - A default fills a property the instance does not carry, and both engines asked `key in data` to find out, which also answers for what the instance inherits: a property named `constructor` never received its default, and one named `__proto__` sent its nested defaults to `Object.prototype`, since the interpreter's applier walked `data.__proto__` as the parent. Both now ask `Object.hasOwn`. `tests/test_defaults_own_keys.js` holds it for either engine. Reported and fixed by @d4tocchini in #43.
20
+ - Completing that report, every remaining prototype-unsafe write and walk in the defaults machinery is closed. The nested-defaults applier now refuses to walk a parent the instance does not carry as an own key, which was the reachable pollution: a schema declaring a property named `__proto__` with nested defaults, no matter what the input held, resolved `data.__proto__` to `Object.prototype` and wrote the child defaults onto it, for every object in the realm, on both engines. Fills of a property named `__proto__` go through `Object.defineProperty`, because plain assignment of that name rewrites the object's prototype instead of creating a property; type coercion skips that name for the same reason, and the raw value still goes through validation. The AOT `parse()` emitter gets the same treatment defensively (computed keys in literals, `Object.hasOwn` presence checks, `defineProperty` for the one dangerous name), and its record loop, whose keys come from the input, now keeps a JSON-parsed own `__proto__` key as an own key in the output instead of silently rewriting the output's prototype.
21
+
5
22
  ## 1.27.0 - 2026-09-17
6
23
 
7
24
  ### Added
package/README.md CHANGED
@@ -445,11 +445,14 @@ const v = new Validator(schema, {
445
445
  removeAdditional: true, // strip properties not in schema
446
446
  schemas: [otherSchema], // cross-schema $ref registry
447
447
  abortEarly: true, // skip detailed error collection on failure (~4x faster on invalid data)
448
+ engine: 'interpreter', // eval-free interpreted engine only: for a schema from outside the trust boundary
448
449
  });
449
450
  ```
450
451
 
451
452
  `abortEarly` returns a shared `{ valid: false, errors: [{ message: 'validation failed' }] }` on failure instead of running the detailed error collector. Useful when the caller only needs a pass/fail decision (Fastify route guards, high-throughput gatekeepers, request rejection at the edge).
452
453
 
454
+ `engine: 'interpreter'` keeps one validator off code generation: no `new Function`, no shared compile cache, the interpreted engine answers every call. The default turns a schema into JavaScript source, which is the right trade for a schema you wrote; a schema that arrives at runtime from a plugin or a tenant is input, and this option validates against it without ever executing anything derived from it. Same verdict, higher per-call cost; `ATA_FORCE_NAPI=1` does the same for a whole process.
455
+
453
456
  ### Build-time compile (`ata compile`)
454
457
 
455
458
  The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. Typical output is about 4.5 KB gzipped for a ten-field schema, full error detail included, against 87 KB for the runtime bundled for the browser.
package/index.d.ts CHANGED
@@ -425,6 +425,17 @@ export interface ValidatorOptions {
425
425
  * v0.14 error shape.
426
426
  */
427
427
  richErrors?: boolean;
428
+ /**
429
+ * Which engine may answer this validator. 'auto' (default) picks the fastest
430
+ * engine that handles the schema, generated JavaScript for most schemas.
431
+ * 'interpreter' keeps the schema off code generation entirely: no
432
+ * `new Function`, no shared compile cache; the eval-free interpreted engine
433
+ * answers `validate()`, `isValidObject()` and `validateJSON()`. For a schema
434
+ * that arrives from outside the trust boundary. The verdict is the same on
435
+ * every engine; the cost is not. The buffer APIs (`isValid`, `countValid`,
436
+ * `batchIsValid`) are native-only and unaffected.
437
+ */
438
+ engine?: 'auto' | 'interpreter';
428
439
  }
429
440
 
430
441
  export interface BundleStandaloneOptions extends ValidatorOptions {
@@ -473,7 +484,7 @@ export interface Validator<T = unknown> {
473
484
  * Which engine answers `validate()` for this schema: 'codegen' (generated
474
485
  * JS), 'closure' (the closure compiler) or 'interpreter'; 'native' is
475
486
  * reserved. The verdict is the same on every engine; the cost is not.
476
- * A diagnostic, not a configuration.
487
+ * A diagnostic; the `engine` option is the setting.
477
488
  */
478
489
  engine(): 'codegen' | 'closure' | 'native' | 'interpreter';
479
490
 
package/index.js CHANGED
@@ -30,6 +30,23 @@ function buildDefaultsApplier(schema) {
30
30
  };
31
31
  }
32
32
 
33
+ // Write an own property. Plain assignment of a key named `__proto__` does
34
+ // not create a property at all: it hits the Object.prototype setter and
35
+ // rewrites the object's prototype, which is how a schema could reach
36
+ // Object.prototype itself. defineProperty has no such special case.
37
+ function setOwn(obj, key, val) {
38
+ if (key === "__proto__") {
39
+ Object.defineProperty(obj, key, {
40
+ value: val,
41
+ writable: true,
42
+ enumerable: true,
43
+ configurable: true,
44
+ });
45
+ } else {
46
+ obj[key] = val;
47
+ }
48
+ }
49
+
33
50
  function collectDefaults(schema, actions, path) {
34
51
  if (typeof schema !== "object" || schema === null) return;
35
52
  const props = schema.properties;
@@ -39,11 +56,12 @@ function collectDefaults(schema, actions, path) {
39
56
  const defaultVal = prop.default;
40
57
  if (!path) {
41
58
  actions.push((data) => {
42
- if (typeof data === "object" && data !== null && !(key in data)) {
43
- data[key] =
59
+ if (typeof data === "object" && data !== null && !Object.hasOwn(data, key)) {
60
+ setOwn(data,
61
+ key,
44
62
  typeof defaultVal === "object" && defaultVal !== null
45
63
  ? JSON.parse(JSON.stringify(defaultVal))
46
- : defaultVal;
64
+ : defaultVal);
47
65
  }
48
66
  });
49
67
  } else {
@@ -52,17 +70,23 @@ function collectDefaults(schema, actions, path) {
52
70
  let target = data;
53
71
  for (let j = 0; j < parentPath.length; j++) {
54
72
  if (typeof target !== "object" || target === null) return;
73
+ // Own keys only. `target[key]` for an inherited name walks the
74
+ // prototype chain: a parent named `__proto__` that the instance
75
+ // does not carry resolved to Object.prototype, and the child
76
+ // defaults were written onto it, for every object in the realm.
77
+ if (!Object.hasOwn(target, parentPath[j])) return;
55
78
  target = target[parentPath[j]];
56
79
  }
57
80
  if (
58
81
  typeof target === "object" &&
59
82
  target !== null &&
60
- !(key in target)
83
+ !Object.hasOwn(target, key)
61
84
  ) {
62
- target[key] =
85
+ setOwn(target,
86
+ key,
63
87
  typeof defaultVal === "object" && defaultVal !== null
64
88
  ? JSON.parse(JSON.stringify(defaultVal))
65
- : defaultVal;
89
+ : defaultVal);
66
90
  }
67
91
  });
68
92
  }
@@ -263,6 +287,10 @@ function buildPreprocessCodegen(schema, options) {
263
287
  if (options.coerceTypes) {
264
288
  for (const [key, prop] of Object.entries(props)) {
265
289
  if (!prop || typeof prop !== 'object' || !prop.type) continue;
290
+ // Coercion writes with plain assignment, which for a key named
291
+ // __proto__ rewrites the prototype instead. The raw value still goes
292
+ // through validation, so skipping is a refusal to coerce, not a hole.
293
+ if (key === '__proto__') continue;
266
294
  const t = Array.isArray(prop.type) ? null : prop.type;
267
295
  if (!t) continue;
268
296
  const k = JSON.stringify(key);
@@ -289,7 +317,11 @@ function buildPreprocessCodegen(schema, options) {
289
317
  if (prop && typeof prop === 'object' && prop.default !== undefined) {
290
318
  const k = JSON.stringify(key);
291
319
  const def = JSON.stringify(prop.default);
292
- lines.push(`if(!(${k} in d))d[${k}]=${def}`);
320
+ // Assignment to a key named __proto__ hits the prototype setter
321
+ // instead of creating a property; defineProperty writes an own key.
322
+ lines.push(key === '__proto__'
323
+ ? `if(!Object.hasOwn(d,${k}))Object.defineProperty(d,${k},{value:${def},writable:true,enumerable:true,configurable:true})`
324
+ : `if(!Object.hasOwn(d,${k}))d[${k}]=${def}`);
293
325
  }
294
326
  }
295
327
  }
@@ -864,6 +896,18 @@ class Validator {
864
896
  this._rawIsCallers = typeof schema !== "string";
865
897
  this._options = options;
866
898
  this._noOpts = !opts;
899
+ // engine: 'interpreter' keeps this validator off code generation: no
900
+ // `new Function`, no shared compile cache, the eval-free interpreted
901
+ // engine answers validate(), isValidObject() and validateJSON(). For a
902
+ // schema that arrives from outside the trust boundary (a plugin's
903
+ // declared config shape, a tenant's upload), where turning it into source
904
+ // is not an acceptable execution model. ATA_FORCE_NAPI does this for the
905
+ // whole process; the option does it for one validator. A misspelling
906
+ // must not fall through to codegen, so anything else is refused.
907
+ if (options.engine !== undefined && options.engine !== 'auto' && options.engine !== 'interpreter') {
908
+ throw new TypeError("engine must be 'auto' or 'interpreter', got " + JSON.stringify(options.engine));
909
+ }
910
+ this._interpretOnly = options.engine === 'interpreter';
867
911
  this._initialized = false;
868
912
  this._nativeReady = false;
869
913
  this._compiled = null;
@@ -1012,11 +1056,14 @@ class Validator {
1012
1056
  // Check cache first -- reuse compiled functions for same schema
1013
1057
  const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
1014
1058
  const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
1059
+ var _forceNapi = this._interpretOnly || (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI);
1015
1060
  // Custom formats are JS functions: bypass the compile cache since they can
1016
- // differ between validators that share the same schema string.
1017
- const cached = (this._userFormats || this._usesKeywords) ? null : _compileCache.get(mapKey);
1061
+ // differ between validators that share the same schema string. An
1062
+ // interpreter-only validator never touches it either way: a function a
1063
+ // trusted validator compiled for the same schema string must not answer
1064
+ // for it.
1065
+ const cached = (this._userFormats || this._usesKeywords || _forceNapi) ? null : _compileCache.get(mapKey);
1018
1066
  let jsFn, jsCombinedFn, jsErrFn, _isCodegen = false;
1019
- var _forceNapi = typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI;
1020
1067
  // v1 removes the bookending requirement for $dynamicRef. Only the
1021
1068
  // interpreted engine implements that; the JS compiler and the native
1022
1069
  // engine both resolve the 2020-12 way, so a v1 schema using the keyword
@@ -1031,15 +1078,16 @@ class Validator {
1031
1078
  // either. The closure path does not call `new Function` itself, so it
1032
1079
  // survives the block and would quietly handle schemas it gets wrong; the
1033
1080
  // interpreted engine is both eval-free and more correct, so go straight
1034
- // there.
1035
- if (this._v1Dynamic || this._usesKeywords || !codegenAvailable()) {
1081
+ // there. The forced case is decided before the probe: the probe is a
1082
+ // `new Function` too, and a validator that promised none must not run it.
1083
+ if (_forceNapi || this._v1Dynamic || this._usesKeywords || !codegenAvailable()) {
1036
1084
  jsFn = null; jsCombinedFn = null; jsErrFn = null;
1037
1085
  // `full` separates an entry that holds every compiled function from one
1038
1086
  // the verdict-only fast path seeded, where `combined` and `errFn` are null
1039
1087
  // because nothing has tried to build them yet. Both halves of that
1040
1088
  // distinction are null, and reading the second as the first costs this
1041
1089
  // schema its generated error function for the life of the process.
1042
- } else if (cached && cached.jsFn !== undefined && !_forceNapi) {
1090
+ } else if (cached && cached.jsFn !== undefined) {
1043
1091
  // `full` says the error and combined functions exist too. An entry
1044
1092
  // without it still carries a verdict function worth reusing; the pair is
1045
1093
  // built by _buildDeferred below if something asks for an error, and the
@@ -1052,7 +1100,7 @@ class Validator {
1052
1100
  jsErrFn = cached.errFn;
1053
1101
  _isCodegen = !!cached.isCodegen;
1054
1102
  this._engine = _isCodegen ? 'codegen' : jsFn ? 'closure' : null;
1055
- } else if (!_forceNapi) {
1103
+ } else {
1056
1104
  const uf = this._userFormats;
1057
1105
  const _cgFn = compileToJSCodegen(schemaObj, sm, uf);
1058
1106
  jsFn = _cgFn || compileToJS(schemaObj, null, sm);
@@ -1068,8 +1116,6 @@ class Validator {
1068
1116
  if (!uf) {
1069
1117
  _compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: _isCodegen, full: false });
1070
1118
  }
1071
- } else {
1072
- jsFn = null; jsCombinedFn = null; jsErrFn = null;
1073
1119
  }
1074
1120
  this._jsFn = jsFn;
1075
1121
  if (this._engine === undefined) this._engine = null;
@@ -1079,7 +1125,10 @@ class Validator {
1079
1125
  // shape (e.g. Fastify `params: { $ref: 'shared#' }` or property refs like
1080
1126
  // `{ id: { $ref: 'shared#/properties/id' } }`).
1081
1127
  const preprocessSchema = resolveSchemaForPreprocess(schemaObj, this._schemaMap);
1082
- let preprocess = buildPreprocessCodegen(preprocessSchema, options);
1128
+ // The mutator pass (defaults, coercion, removal) is generated source too,
1129
+ // with the schema's `default` values embedded; an interpreter-only
1130
+ // validator takes the closure mutators instead.
1131
+ let preprocess = this._interpretOnly ? null : buildPreprocessCodegen(preprocessSchema, options);
1083
1132
  if (!preprocess) {
1084
1133
  const applyDefaults = options.useDefaults === false ? null : buildDefaultsApplier(preprocessSchema);
1085
1134
  const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
@@ -1967,7 +2016,7 @@ class Validator {
1967
2016
  return;
1968
2017
  }
1969
2018
  this._ensureVocabularies();
1970
- if (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI) return;
2019
+ if (this._interpretOnly || (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI)) return;
1971
2020
  if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
1972
2021
  const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
1973
2022
  const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
package/lib/aot-impl.js CHANGED
@@ -351,7 +351,9 @@ function emitClone(node, access, depth) {
351
351
  const read = `${access}[${JSON.stringify(key)}]`;
352
352
  const value = emitValueExpr(prop, read, depth);
353
353
  if (value === null) return null;
354
- if (required.has(key)) fixed.push(`${JSON.stringify(key)}: ${value}`);
354
+ // Computed key syntax: a plain `"__proto__":` in an object literal is
355
+ // the prototype slot, not a property; `["__proto__"]:` is a property.
356
+ if (required.has(key)) fixed.push(`[${JSON.stringify(key)}]: ${value}`);
355
357
  else conditional.push({ key, value, dflt: prop.default });
356
358
  }
357
359
  if (fixed.length === 0 && conditional.length === 0 && !apSchema) return null;
@@ -377,7 +379,10 @@ function emitClone(node, access, depth) {
377
379
  } else if (keys.length > 0) {
378
380
  skip = `if (${keys.map((k) => `${kVar} === ${JSON.stringify(k)}`).join(' || ')}) continue; `;
379
381
  }
380
- recordLoop = ` ${declSet}for (const ${kVar} of Object.keys(${access})) { ${skip}const ${vVar} = ${access}[${kVar}]; ${tmp}[${kVar}] = ${valueExpr}; }`;
382
+ // The key comes from the input, so it can be the string "__proto__" (an
383
+ // own key only JSON.parse writes); assignment would hit the prototype
384
+ // setter, so that one key goes through defineProperty.
385
+ recordLoop = ` ${declSet}for (const ${kVar} of Object.keys(${access})) { ${skip}const ${vVar} = ${access}[${kVar}]; if (${kVar} === "__proto__") Object.defineProperty(${tmp}, ${kVar}, { value: ${valueExpr}, writable: true, enumerable: true, configurable: true }); else ${tmp}[${kVar}] = ${valueExpr}; }`;
381
386
  }
382
387
 
383
388
  const literal = `{ ${fixed.join(', ')} }`;
@@ -386,10 +391,22 @@ function emitClone(node, access, depth) {
386
391
  // gains a key the input did not have.
387
392
  const adds = conditional
388
393
  .map(({ key, value, dflt }) => {
389
- const set = `if (${JSON.stringify(key)} in ${access}) ${tmp}[${JSON.stringify(key)}] = ${value};`;
394
+ // Object.hasOwn, not `in`: `in` also answers for what the instance
395
+ // inherits, so a property named `constructor` would copy the inherited
396
+ // function into the output that validate().data does not carry. And a
397
+ // key named `__proto__` cannot be written by assignment, which hits
398
+ // the prototype setter; defineProperty writes an own key.
399
+ const assign = key === '__proto__'
400
+ ? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${value}, writable: true, enumerable: true, configurable: true });`
401
+ : `${tmp}[${JSON.stringify(key)}] = ${value};`;
402
+ const set = `if (Object.hasOwn(${access}, ${JSON.stringify(key)})) ${assign}`;
390
403
  // The default is emitted as a literal inside the call, so an object or
391
404
  // array default is a fresh value on every parse, never shared state.
392
- return dflt === undefined ? set : `${set} else ${tmp}[${JSON.stringify(key)}] = ${JSON.stringify(dflt)};`;
405
+ if (dflt === undefined) return set;
406
+ const setDflt = key === '__proto__'
407
+ ? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${JSON.stringify(dflt)}, writable: true, enumerable: true, configurable: true });`
408
+ : `${tmp}[${JSON.stringify(key)}] = ${JSON.stringify(dflt)};`;
409
+ return `${set} else ${setDflt}`;
393
410
  })
394
411
  .join(' ');
395
412
  return `(function(){ const ${tmp} = ${literal}; ${adds}${recordLoop} return ${tmp}; })()`;
@@ -9,27 +9,22 @@
9
9
  // Self-contained on purpose: `toStandaloneModule({ positions: true })` embeds
10
10
  // this function verbatim via Function.prototype.toString, so it may not close
11
11
  // over anything in this module and may not assume Node globals. The inlined
12
- // escapePtr below duplicates lib/source-positions.js for that reason.
12
+ // pointer escaping below duplicates lib/source-positions.js for that reason.
13
13
  function buildDataPositionMap (input) {
14
- const escapePtr = (s) => s.replace(/~/g, '~0').replace(/\//g, '~1');
15
14
  const text = (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) ? input.toString('utf8') : String(input);
16
15
  const map = Object.create(null);
17
- const lines = text.split('\n');
18
- const lineStart = new Array(lines.length + 1);
19
- lineStart[0] = 0;
20
- for (let i = 0; i < lines.length; i++) lineStart[i + 1] = lineStart[i] + lines[i].length + 1;
16
+ const n = text.length;
21
17
 
22
- function offsetToLineCol (off) {
23
- let lo = 0, hi = lineStart.length - 1;
24
- while (lo < hi) {
25
- const mid = (lo + hi + 1) >> 1;
26
- if (lineStart[mid] <= off) lo = mid; else hi = mid - 1;
27
- }
28
- return { line: lo + 1, col: off - lineStart[lo] + 1, text: lines[lo] || '' };
29
- }
18
+ // Offsets during the walk, line and column afterwards. Resolving them here
19
+ // cost a binary search and an object allocation for every node in the
20
+ // document, when only the nodes an error names are ever read.
21
+ const ptrs = [];
22
+ const starts = [];
23
+ const lens = [];
24
+ const keyStarts = [];
25
+ const keyLens = [];
30
26
 
31
27
  let i = 0;
32
- const n = text.length;
33
28
 
34
29
  function skipWs () {
35
30
  while (i < n) {
@@ -38,28 +33,40 @@ function buildDataPositionMap (input) {
38
33
  }
39
34
  }
40
35
 
41
- function readString () {
36
+ // Span the string token, leaving `i` just past the closing quote. A plain
37
+ // token is sliced; one holding an escape or a raw control character goes
38
+ // through JSON.parse, which is what decodes `é` and what rejects a
39
+ // literal newline or a bad escape. Values are spanned and not decoded, but a
40
+ // malformed one still has to be rejected, so it is parsed for the throw.
41
+ //
42
+ // A token that does not open with a quote is not simple either. Object keys
43
+ // are read here unconditionally, so a document whose opening quote is missing
44
+ // (`{ mo": 1 }`) arrives with `m` under the cursor, and slicing it would map a
45
+ // key nobody wrote. Parsing the span rejects it instead.
46
+ function readString (decode) {
42
47
  const start = i;
48
+ let simple = text.charCodeAt(i) === 0x22;
43
49
  i++;
50
+ let closed = false;
44
51
  while (i < n) {
45
52
  const ch = text.charCodeAt(i);
46
- if (ch === 0x5c) { i += 2; continue; }
47
- if (ch === 0x22) { i++; return JSON.parse(text.slice(start, i)); }
53
+ if (ch === 0x5c) { simple = false; i += 2; continue; }
54
+ if (ch === 0x22) { i++; closed = true; break; }
55
+ if (ch < 0x20) simple = false;
48
56
  i++;
49
57
  }
50
- throw new Error('unterminated string at offset ' + start);
58
+ if (!closed) throw new Error('unterminated string at offset ' + start);
59
+ if (simple) return decode ? text.slice(start + 1, i - 1) : null;
60
+ const decoded = JSON.parse(text.slice(start, i));
61
+ return decode ? decoded : null;
51
62
  }
52
63
 
53
- function pointerOf (path) {
54
- if (path.length === 0) return '';
55
- return '/' + path.map(escapePtr).join('/');
56
- }
57
-
58
- function walk (path, keySpan) {
64
+ // The pointer is carried down the walk. Rebuilding it per node from a path
65
+ // array cost an array copy, a map and a join for every node.
66
+ function walk (ptr, keyStart, keyLen) {
59
67
  skipWs();
60
68
  if (i >= n) return;
61
69
  const start = i;
62
- const pos = offsetToLineCol(start);
63
70
 
64
71
  const ch = text.charCodeAt(i);
65
72
  if (ch === 0x7b) {
@@ -76,19 +83,16 @@ function buildDataPositionMap (input) {
76
83
  // Record the key token before consuming it. The caret for an error
77
84
  // that names a property belongs on the property, not on the value and
78
85
  // not on the enclosing object.
79
- const keyStart = i;
80
- const keyPos = offsetToLineCol(keyStart);
81
- const key = readString();
82
- const span = {
83
- keyOffset: keyStart,
84
- keyLength: i - keyStart,
85
- keyLine: keyPos.line,
86
- keyCol: keyPos.col,
87
- };
86
+ const memberKeyStart = i;
87
+ const key = readString(true);
88
+ const memberKeyLen = i - memberKeyStart;
88
89
  skipWs();
89
90
  if (text.charCodeAt(i) !== 0x3a) throw new Error('expected ":" at offset ' + i);
90
91
  i++;
91
- walk(path.concat([key]), span);
92
+ const seg = (key.indexOf('~') === -1 && key.indexOf('/') === -1)
93
+ ? key
94
+ : key.replace(/~/g, '~0').replace(/\//g, '~1');
95
+ walk(ptr + '/' + seg, memberKeyStart, memberKeyLen);
92
96
  }
93
97
  } else if (ch === 0x5b) {
94
98
  i++;
@@ -99,7 +103,7 @@ function buildDataPositionMap (input) {
99
103
  if (text.charCodeAt(i) === 0x5d) { i++; break; }
100
104
  if (text.charCodeAt(i) === 0x2c) { i++; continue; }
101
105
  const before = i;
102
- walk(path.concat([String(idx)]));
106
+ walk(ptr + '/' + idx, -1, 0);
103
107
  // A character that begins no value at all, a stray `}` for instance,
104
108
  // leaves the position where it was, and a loop that does not advance
105
109
  // does not end. `validateJSON('[')` hung here on one byte.
@@ -107,7 +111,7 @@ function buildDataPositionMap (input) {
107
111
  idx++;
108
112
  }
109
113
  } else if (ch === 0x22) {
110
- readString();
114
+ readString(false);
111
115
  } else {
112
116
  while (i < n) {
113
117
  const c = text.charCodeAt(i);
@@ -116,19 +120,51 @@ function buildDataPositionMap (input) {
116
120
  }
117
121
  }
118
122
 
119
- const length = i - start;
120
- const entry = { byteOffset: start, length, line: pos.line, col: pos.col, text: pos.text };
121
- if (keySpan) {
122
- entry.keyOffset = keySpan.keyOffset;
123
- entry.keyLength = keySpan.keyLength;
124
- entry.keyLine = keySpan.keyLine;
125
- entry.keyCol = keySpan.keyCol;
126
- }
127
- map[pointerOf(path)] = entry;
123
+ ptrs.push(ptr);
124
+ starts.push(start);
125
+ lens.push(i - start);
126
+ keyStarts.push(keyStart);
127
+ keyLens.push(keyLen);
128
128
  }
129
129
 
130
130
  if (text.charCodeAt(0) === 0xfeff) i = 1;
131
- walk([]);
131
+ walk('', -1, 0);
132
+
133
+ if (ptrs.length === 0) return map;
134
+
135
+ const lines = text.split('\n');
136
+ const lineStart = new Array(lines.length + 1);
137
+ lineStart[0] = 0;
138
+ for (let k = 0; k < lines.length; k++) lineStart[k + 1] = lineStart[k] + lines[k].length + 1;
139
+
140
+ function lineOf (off) {
141
+ let lo = 0, hi = lineStart.length - 1;
142
+ while (lo < hi) {
143
+ const mid = (lo + hi + 1) >> 1;
144
+ if (lineStart[mid] <= off) lo = mid; else hi = mid - 1;
145
+ }
146
+ return lo;
147
+ }
148
+
149
+ for (let k = 0; k < ptrs.length; k++) {
150
+ const off = starts[k];
151
+ const line = lineOf(off);
152
+ const entry = {
153
+ byteOffset: off,
154
+ length: lens[k],
155
+ line: line + 1,
156
+ col: off - lineStart[line] + 1,
157
+ text: lines[line] || '',
158
+ };
159
+ if (keyStarts[k] >= 0) {
160
+ const keyLine = lineOf(keyStarts[k]);
161
+ entry.keyOffset = keyStarts[k];
162
+ entry.keyLength = keyLens[k];
163
+ entry.keyLine = keyLine + 1;
164
+ entry.keyCol = keyStarts[k] - lineStart[keyLine] + 1;
165
+ }
166
+ map[ptrs[k]] = entry;
167
+ }
132
168
  return map;
133
169
  }
134
170
 
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.27.0';
10
+ module.exports = '1.28.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "description": "JSON Schema validation that compiles for speed and still runs where code generation is blocked. Compiled and interpreted engines answer identically at 100% of the official suite. TypeScript inference, Standard Schema V1, and a build step that emits dependency-free modules.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -51,7 +51,7 @@
51
51
  "release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
52
52
  "build": "cmake-js build --target ata",
53
53
  "rebuild": "cmake-js rebuild --target ata",
54
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_uri_helper_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_file_positions.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
54
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_uri_helper_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_position_map_cost.js && node tests/test_file_positions.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
55
55
  "bench:size": "node benchmark/bench_aot_size.mjs",
56
56
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
57
57
  "test:compat": "node tests/test_compat.js",
@@ -122,13 +122,13 @@
122
122
  "LICENSE"
123
123
  ],
124
124
  "optionalDependencies": {
125
- "@ata-validator/native-darwin-arm64": "1.27.0",
126
- "@ata-validator/native-darwin-x64": "1.27.0",
127
- "@ata-validator/native-linux-arm64-gnu": "1.27.0",
128
- "@ata-validator/native-linux-arm64-musl": "1.27.0",
129
- "@ata-validator/native-linux-x64-gnu": "1.27.0",
130
- "@ata-validator/native-linux-x64-musl": "1.27.0",
131
- "@ata-validator/native-win32-x64": "1.27.0"
125
+ "@ata-validator/native-darwin-arm64": "1.28.0",
126
+ "@ata-validator/native-darwin-x64": "1.28.0",
127
+ "@ata-validator/native-linux-arm64-gnu": "1.28.0",
128
+ "@ata-validator/native-linux-arm64-musl": "1.28.0",
129
+ "@ata-validator/native-linux-x64-gnu": "1.28.0",
130
+ "@ata-validator/native-linux-x64-musl": "1.28.0",
131
+ "@ata-validator/native-win32-x64": "1.28.0"
132
132
  },
133
133
  "peerDependencies": {
134
134
  "yaml": "^2.0.0"