ata-validator 1.31.2 → 1.32.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.
@@ -0,0 +1,302 @@
1
+ 'use strict';
2
+
3
+ // The copy behind parse(): shared by the parse() export of an ahead-of-time
4
+ // module (lib/aot-impl.js) and the runtime's Validator#parse (index.js). It
5
+ // lives apart from the module emitter so the runtime can use it without
6
+ // pulling the emitter, and the safe-regex source it embeds, into a browser
7
+ // bundle.
8
+
9
+ const { compileToJSCodegen, unevalContributions } = require('./js-compiler');
10
+
11
+ // Emit an expression that rebuilds `access` from the properties the schema
12
+ // declares, dropping everything else. This is the sanitising counterpart to
13
+ // removeAdditional: instead of enumerating the input and deleting the keys
14
+ // that do not belong, it reads only the keys that do. That is O(schema)
15
+ // rather than O(input), it leaves the caller's object untouched, and it keeps
16
+ // one hidden class instead of walking the object through a transition per
17
+ // delete.
18
+ //
19
+ // Returns null when the clone cannot be proven exact, and the module then
20
+ // simply has no parse(). Declining is recoverable; a sanitiser that quietly
21
+ // drops a property the schema allows is not.
22
+ // Resolve a local JSON pointer ('#', '#/$defs/x', draft-7 '#/definitions/x',
23
+ // any '#/...' path) against the schema root. Returns null for anything else:
24
+ // external documents, anchors, URNs. Those stay with the runtime engines.
25
+ function resolveLocalPointer(root, ref) {
26
+ if (ref === '#') return root;
27
+ if (typeof ref !== 'string' || !ref.startsWith('#/')) return null;
28
+ let cur = root;
29
+ for (const raw of ref.slice(2).split('/')) {
30
+ let seg = raw.replace(/~1/g, '/').replace(/~0/g, '~');
31
+ if (cur === null || typeof cur !== 'object') return null;
32
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) {
33
+ try { seg = decodeURIComponent(seg); } catch (_) { return null; }
34
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) return null;
35
+ }
36
+ cur = cur[seg];
37
+ }
38
+ return cur;
39
+ }
40
+
41
+ // Inline local, acyclic $refs before the clone proof runs, so a generated
42
+ // schema ($defs + $ref, the shape every schema generator emits) can still get
43
+ // a parse(). Only a node that is exactly a $ref plus annotations is replaced;
44
+ // a $ref with a constraining sibling, a cycle, an unresolvable target or an
45
+ // external reference is left in place, and emitClone declines it as before.
46
+ // The walk never mutates its input and never descends into data positions
47
+ // (default, const, enum, examples), where an object holding a "$ref" key is
48
+ // a value, not a schema.
49
+ const _INLINE_ANNOTATIONS = new Set(['$ref', 'title', 'description', '$comment', 'examples', 'deprecated', 'readOnly', 'writeOnly', 'default']);
50
+ const _INLINE_DATA_KEYS = new Set(['default', 'const', 'enum', 'examples']);
51
+ function inlineRefsForClone(root) {
52
+ const state = { budget: 512 };
53
+ // `inRef` marks content brought in from behind a reference. The runtime's
54
+ // useDefaults fills a default written next to the $ref, and does not fill
55
+ // defaults written inside the referenced definition, so the inlined copy
56
+ // keeps the first and drops the second. parse() must stay exactly as
57
+ // generous as validate(); a parse that fills more is a disagreement, not a
58
+ // feature.
59
+ const walk = (node, active, inRef) => {
60
+ if (!node || typeof node !== 'object') return node;
61
+ if (Array.isArray(node)) return node.map((n) => walk(n, active, inRef));
62
+ let cur = node;
63
+ let entered = false;
64
+ let siblingDefault;
65
+ while (cur && typeof cur === 'object' && !Array.isArray(cur) && typeof cur.$ref === 'string') {
66
+ for (const k of Object.keys(cur)) if (!_INLINE_ANNOTATIONS.has(k)) return node;
67
+ if (active.has(cur.$ref) || state.budget-- <= 0) return node;
68
+ const target = resolveLocalPointer(root, cur.$ref);
69
+ if (!target || typeof target !== 'object' || Array.isArray(target)) return node;
70
+ if (!entered && !inRef && cur.default !== undefined) siblingDefault = cur.default;
71
+ active = new Set(active);
72
+ active.add(cur.$ref);
73
+ cur = target;
74
+ entered = true;
75
+ }
76
+ const strip = inRef || entered;
77
+ const out = {};
78
+ for (const k of Object.keys(cur)) {
79
+ if (strip && k === 'default') continue;
80
+ const v = cur[k];
81
+ if (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') { out[k] = v; continue; }
82
+ out[k] = walk(v, active, strip);
83
+ }
84
+ if (siblingDefault !== undefined) out.default = siblingDefault;
85
+ return out;
86
+ };
87
+ return walk(root, new Set(), false);
88
+ }
89
+
90
+ const PROTO_KEY_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__']);
91
+
92
+ function emitClone(node, access, depth) {
93
+ if (!node || typeof node !== 'object') return null;
94
+ if (depth > 12) return null;
95
+ // Only plain object nodes are cloneable, and only when the schema names
96
+ // every property it keeps. A $ref means the set of allowed keys is not
97
+ // this node's to decide. In-place applicators (allOf, anyOf, oneOf,
98
+ // if/then/else) are admitted under the same proof the unevaluated* error
99
+ // generators use: every property name they contribute must already be
100
+ // declared in this node's own `properties`, so they constrain values but
101
+ // never widen the key set. `not` never widens it. `unevaluatedProperties`
102
+ // is admitted only as `false` under that proof, where it is exactly
103
+ // `additionalProperties: false`.
104
+ if (node.$ref || node.patternProperties || node.additionalProperties === true) {
105
+ return null;
106
+ }
107
+ // additionalProperties as a schema is the record shape (z.record and every
108
+ // generator's map type): the key set is open by declaration, so keeping
109
+ // every key is exact, and every undeclared value is rebuilt against that
110
+ // one schema. That is a proof, not a guess, so it is admitted. What stays
111
+ // out: applicators next to a record (a branch could constrain some keys'
112
+ // values beyond the record schema, and the rebuild would ignore it), and
113
+ // `additionalProperties: true`, where the value is unconstrained and this
114
+ // pass only copies what it can name. An open object with no
115
+ // `additionalProperties` at all also stays out, for the original reason:
116
+ // validate() accepts its unknown keys, so a parse() that strips them would
117
+ // silently drop allowed data, and one that keeps them raw would not be a
118
+ // sanitiser.
119
+ const apSchema = (node.additionalProperties && typeof node.additionalProperties === 'object')
120
+ ? node.additionalProperties
121
+ : null;
122
+ if (apSchema) {
123
+ if (node.unevaluatedProperties !== undefined) return null;
124
+ if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
125
+ node.dependentSchemas !== undefined || node.dependencies !== undefined ||
126
+ node.$dynamicRef !== undefined || node.$recursiveRef !== undefined) return null;
127
+ }
128
+ if (node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== false) return null;
129
+ if (node.type !== 'object') return null;
130
+ if (!node.properties && !apSchema) return null;
131
+ const keys = node.properties ? Object.keys(node.properties) : [];
132
+ if (keys.length === 0 && !apSchema) return null;
133
+ if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
134
+ node.unevaluatedProperties === false) {
135
+ if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined ||
136
+ node.dependentSchemas !== undefined || node.dependencies !== undefined) return null;
137
+ const names = new Set();
138
+ const state = { prefix: 0 };
139
+ for (const k of ['allOf', 'anyOf', 'oneOf']) {
140
+ if (node[k] !== undefined) {
141
+ if (!Array.isArray(node[k])) return null;
142
+ for (const b of node[k]) if (!unevalContributions(b, 'props', names, state, 0)) return null;
143
+ }
144
+ }
145
+ for (const k of ['if', 'then', 'else']) {
146
+ if (node[k] !== undefined && !unevalContributions(node[k], 'props', names, state, 0)) return null;
147
+ }
148
+ for (const n of names) if (!Object.prototype.hasOwnProperty.call(node.properties, n)) return null;
149
+ }
150
+ const required = new Set(Array.isArray(node.required) ? node.required : []);
151
+
152
+ const fixed = [];
153
+ const conditional = [];
154
+ for (const key of keys) {
155
+ const prop = node.properties[key];
156
+ if (!prop || typeof prop !== 'object') return null;
157
+ if (prop.$ref) return null;
158
+ // A property that is required and carries a default is the one place the
159
+ // runtime and a raw-input validation disagree: the runtime fills the
160
+ // default before checking `required`, so `{}` validates there and would
161
+ // throw here. Declining keeps parse() exactly as strict as validate().
162
+ if (required.has(key) && prop.default !== undefined) return null;
163
+ // A default the property's own schema rejects would make parse() hand
164
+ // back a document validate() refuses: the runtime fills defaults before
165
+ // validating, so it catches the bad default, and a raw-input check here
166
+ // never sees it. Probe the default at emit time and decline when it does
167
+ // not hold, or when the subschema cannot be compiled to check it.
168
+ if (prop.default !== undefined) {
169
+ let probe = null;
170
+ try { probe = compileToJSCodegen(prop, null, null); } catch (_) { probe = null; }
171
+ if (!probe || !probe(prop.default)) return null;
172
+ }
173
+ const read = `${access}[${JSON.stringify(key)}]`;
174
+ const value = emitValueExpr(prop, read, depth);
175
+ if (value === null) return null;
176
+ // A plain `"__proto__":` in an object literal is the prototype slot, not
177
+ // a property; `["__proto__"]:` is a property. Only that key is written as
178
+ // a computed one: a literal with computed keys cannot use the engine's
179
+ // literal boilerplate and is built key by key, which made the copy cost
180
+ // about as much as the validation in front of it.
181
+ if (required.has(key)) fixed.push(key === '__proto__' ? `[${JSON.stringify(key)}]: ${value}` : `${JSON.stringify(key)}: ${value}`);
182
+ else conditional.push({ key, value, dflt: prop.default });
183
+ }
184
+ if (fixed.length === 0 && conditional.length === 0 && !apSchema) return null;
185
+
186
+ const tmp = `_c${depth}`;
187
+ // The record loop: every key of the input that is not a declared property
188
+ // is kept, its value rebuilt against the additionalProperties schema. The
189
+ // input is already validated, so every such value satisfies that schema;
190
+ // the rebuild only strips inside it. Defaults under a record value are
191
+ // dropped before emission because the runtime's useDefaults does not fill
192
+ // them there, and parse() must equal validate().data, not improve on it.
193
+ let recordLoop = '';
194
+ if (apSchema) {
195
+ const kVar = `_k${depth}`;
196
+ const vVar = `_v${depth}`;
197
+ const valueExpr = emitValueExpr(stripDefaultsDeep(apSchema), vVar, depth);
198
+ if (valueExpr === null) return null;
199
+ let skip = '';
200
+ let declSet = '';
201
+ if (keys.length > 8) {
202
+ declSet = `const _d${depth} = new Set(${JSON.stringify(keys)}); `;
203
+ skip = `if (_d${depth}.has(${kVar})) continue; `;
204
+ } else if (keys.length > 0) {
205
+ skip = `if (${keys.map((k) => `${kVar} === ${JSON.stringify(k)}`).join(' || ')}) continue; `;
206
+ }
207
+ // The key comes from the input, so it can be the string "__proto__" (an
208
+ // own key only JSON.parse writes); assignment would hit the prototype
209
+ // setter, so that one key goes through defineProperty.
210
+ 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}; }`;
211
+ }
212
+
213
+ const literal = `{ ${fixed.join(', ')} }`;
214
+ if (conditional.length === 0 && !recordLoop) return literal;
215
+ // Optional properties are added only when present, so the result never
216
+ // gains a key the input did not have.
217
+ const adds = conditional
218
+ .map(({ key, value, dflt }) => {
219
+ // Object.hasOwn, not `in`: `in` also answers for what the instance
220
+ // inherits, so a property named `constructor` would copy the inherited
221
+ // function into the output that validate().data does not carry. And a
222
+ // key named `__proto__` cannot be written by assignment, which hits
223
+ // the prototype setter; defineProperty writes an own key.
224
+ const assign = key === '__proto__'
225
+ ? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${value}, writable: true, enumerable: true, configurable: true });`
226
+ : `${tmp}[${JSON.stringify(key)}] = ${value};`;
227
+ // The same own-property test the generators emit, since Object.hasOwn
228
+ // is not inlined: `in`, then the prototype, then hasOwnProperty only
229
+ // for an object that is not an ordinary one.
230
+ const k = JSON.stringify(key);
231
+ const own = PROTO_KEY_NAMES.has(key)
232
+ ? `Object.prototype.hasOwnProperty.call(${access}, ${k})`
233
+ : `(${k} in ${access} && (${access}.__proto__ === Object.prototype || Object.prototype.hasOwnProperty.call(${access}, ${k})))`;
234
+ const set = `if (${own}) ${assign}`;
235
+ // The default is emitted as a literal inside the call, so an object or
236
+ // array default is a fresh value on every parse, never shared state.
237
+ if (dflt === undefined) return set;
238
+ const setDflt = key === '__proto__'
239
+ ? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${JSON.stringify(dflt)}, writable: true, enumerable: true, configurable: true });`
240
+ : `${tmp}[${JSON.stringify(key)}] = ${JSON.stringify(dflt)};`;
241
+ return `${set} else ${setDflt}`;
242
+ })
243
+ .join(' ');
244
+ return `(function(){ const ${tmp} = ${literal}; ${adds}${recordLoop} return ${tmp}; })()`;
245
+ }
246
+
247
+ // The value dispatch shared by declared properties and the record loop. A
248
+ // value is copyable only when the emitter can name everything that may live
249
+ // under it: a primitive holds nothing, an object with declared properties or
250
+ // a record schema is rebuilt the same way, an array of primitives has
251
+ // nowhere to hide a key. A $ref, a composition or an untyped node could all
252
+ // carry keys this code cannot see, and copying the reference would smuggle
253
+ // them into a value that claims to be sanitised, so the caller declines the
254
+ // whole clone instead.
255
+ // Drop every schema-position `default` in a subtree, leaving data positions
256
+ // (const, enum, examples) untouched. Used on a record's value schema, where
257
+ // the runtime's useDefaults fills nothing.
258
+ function stripDefaultsDeep(node) {
259
+ if (!node || typeof node !== 'object') return node;
260
+ if (Array.isArray(node)) return node.map(stripDefaultsDeep);
261
+ const out = {};
262
+ for (const k of Object.keys(node)) {
263
+ if (k === 'default') continue;
264
+ out[k] = _INLINE_DATA_KEYS.has(k) ? node[k] : stripDefaultsDeep(node[k]);
265
+ }
266
+ return out;
267
+ }
268
+
269
+ const _CLONE_PRIMITIVE = new Set(['string', 'number', 'integer', 'boolean', 'null']);
270
+ function emitValueExpr(prop, read, depth) {
271
+ if (!prop || typeof prop !== 'object') return null;
272
+ if (prop.$ref) return null;
273
+ const types = Array.isArray(prop.type) ? prop.type : [prop.type];
274
+ if (types.every((t) => _CLONE_PRIMITIVE.has(t))) return read;
275
+ const isRecordable = (s) => s.properties ||
276
+ (s.additionalProperties && typeof s.additionalProperties === 'object');
277
+ if (prop.type === 'object' && isRecordable(prop)) {
278
+ return emitClone(prop, read, depth + 1);
279
+ }
280
+ if (prop.type === 'array' && prop.items && typeof prop.items === 'object' &&
281
+ !Array.isArray(prop.items)) {
282
+ const it = prop.items;
283
+ const itemTypes = Array.isArray(it.type) ? it.type : [it.type];
284
+ if (itemTypes.every((t) => _CLONE_PRIMITIVE.has(t))) return read;
285
+ if (it.type === 'object' && isRecordable(it)) {
286
+ const el = '_e' + depth;
287
+ const inner = emitClone(it, el, depth + 1);
288
+ if (!inner) return null;
289
+ return `${read}.map((${el}) => (${inner}))`;
290
+ }
291
+ }
292
+ return null;
293
+ }
294
+
295
+ // The copy as an expression over `data`, or null where the kept key set is
296
+ // not provable.
297
+ function cloneExprFor(schema) {
298
+ if (!schema || typeof schema !== 'object') return null;
299
+ return emitClone(inlineRefsForClone(schema), 'data', 0);
300
+ }
301
+
302
+ module.exports = { cloneExprFor, emitClone, inlineRefsForClone, stripDefaultsDeep, resolveLocalPointer };
@@ -230,7 +230,7 @@ function cpAtMost(s, max) {
230
230
  function typeMatches(type, d) {
231
231
  switch (type) {
232
232
  case 'string': return typeof d === 'string';
233
- case 'number': return typeof d === 'number' && isFinite(d);
233
+ case 'number': return Number.isFinite(d);
234
234
  case 'integer': return typeof d === 'number' && Number.isInteger(d);
235
235
  case 'boolean': return typeof d === 'boolean';
236
236
  case 'null': return d === null;
@@ -529,7 +529,7 @@ function evalLeafV(P, data) {
529
529
  if (!found) return false;
530
530
  }
531
531
  if (P.hasConst && !deepEqual(P.const, data)) return false;
532
- if (P.hasNumber && typeof data === 'number' && isFinite(data)) {
532
+ if (P.hasNumber && Number.isFinite(data)) {
533
533
  if (P.minimum !== undefined && !(data >= P.minimum)) return false;
534
534
  if (P.maximum !== undefined && !(data <= P.maximum)) return false;
535
535
  if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) return false;
@@ -646,7 +646,7 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
646
646
  if (errors !== NOERRORS) errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
647
647
  valid = false;
648
648
  }
649
- if (P.hasNumber && typeof data === 'number' && isFinite(data)) {
649
+ if (P.hasNumber && Number.isFinite(data)) {
650
650
  if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
651
651
  if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
652
652
  if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
@@ -937,7 +937,7 @@ class Interpreter {
937
937
  }
938
938
 
939
939
  // numbers
940
- if (P.hasNumber && typeof data === 'number' && isFinite(data)) {
940
+ if (P.hasNumber && Number.isFinite(data)) {
941
941
  if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
942
942
  if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
943
943
  if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
@@ -1255,3 +1255,9 @@ function createInterpreter(schema, options) {
1255
1255
  }
1256
1256
 
1257
1257
  module.exports = { createInterpreter };
1258
+ // For lib/plan-source.js, which emits source from the same plans and must call
1259
+ // the same helpers so the two cannot answer differently.
1260
+ Object.defineProperty(module.exports, '_planInternals', {
1261
+ value: { Plan, deepEqual, cpAtLeast, cpAtMost, multipleOfOk, dataBits, T_STRING, T_NUMBER, T_INTEGER, T_BOOLEAN, T_NULL, T_OBJECT, T_ARRAY, T_ANY },
1262
+ enumerable: false,
1263
+ });