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 +17 -0
- package/README.md +3 -0
- package/index.d.ts +12 -1
- package/index.js +67 -18
- package/lib/aot-impl.js +21 -4
- package/lib/data-positions.js +84 -48
- package/lib/version.js +1 -1
- package/package.json +9 -9
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
|
|
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
|
|
43
|
-
data
|
|
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
|
|
83
|
+
!Object.hasOwn(target, key)
|
|
61
84
|
) {
|
|
62
|
-
target
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}; })()`;
|
package/lib/data-positions.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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++;
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
80
|
-
const
|
|
81
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
126
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
127
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
128
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
129
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
130
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
131
|
-
"@ata-validator/native-win32-x64": "1.
|
|
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"
|