ata-validator 1.61.3 → 1.62.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.
Files changed (48) hide show
  1. package/README.md +67 -10
  2. package/bin/ata.js +3 -2
  3. package/compat.js +15 -2
  4. package/index.d.ts +7 -0
  5. package/index.js +10 -0
  6. package/lib/aot-build.js +27 -1
  7. package/lib/aot-impl.js +108 -17
  8. package/lib/buffer-gate.js +25 -14
  9. package/lib/bun-target.js +21 -5
  10. package/lib/clone-emit.js +115 -10
  11. package/lib/codegen-engine.js +94 -18
  12. package/lib/compiled-common.js +11 -4
  13. package/lib/compiled-verdict.js +2 -2
  14. package/lib/compiled.js +1 -1
  15. package/lib/data-positions.js +18 -0
  16. package/lib/defaults.js +101 -23
  17. package/lib/describe-schema.js +38 -5
  18. package/lib/diagnose.js +51 -29
  19. package/lib/enrich-error.js +23 -1
  20. package/lib/error-sort-rank.js +12 -0
  21. package/lib/formats-source.js +12 -1
  22. package/lib/formats.js +19 -19
  23. package/lib/interpreter-core.js +23 -4
  24. package/lib/js-compiler-errors.js +70 -72
  25. package/lib/js-compiler.js +212 -72
  26. package/lib/json-rejections.js +1 -1
  27. package/lib/migrate.js +24 -5
  28. package/lib/native-attach.browser.js +7 -0
  29. package/lib/native-attach.js +76 -0
  30. package/lib/native-positions.js +7 -0
  31. package/lib/plan-compiler.js +7 -1
  32. package/lib/regex-linear.js +88 -0
  33. package/lib/rejections.js +13 -3
  34. package/lib/render-compact.js +5 -5
  35. package/lib/render-pretty.js +23 -10
  36. package/lib/safe-regex.js +29 -4
  37. package/lib/scan-compiler.js +16 -4
  38. package/lib/stream.js +293 -0
  39. package/lib/tier0.js +6 -2
  40. package/lib/ts-gen.js +25 -12
  41. package/lib/unique-items.js +11 -3
  42. package/lib/validator-core.js +61 -66
  43. package/lib/verify.js +25 -6
  44. package/lib/version.js +1 -1
  45. package/package.json +17 -8
  46. package/stream.d.ts +51 -0
  47. package/stream.js +3 -0
  48. package/stream.mjs +3 -0
package/lib/stream.js ADDED
@@ -0,0 +1,293 @@
1
+ 'use strict';
2
+
3
+ // Validation as the body arrives. A request that carries many documents, a
4
+ // JSON array or NDJSON, is today read to the end, parsed as a whole and then
5
+ // validated, so a bad third record is reported after the last byte: on a
6
+ // 100 MB upload that is half a second and 500 to 800 MB of memory before the
7
+ // caller hears "record 2, email". Here each record is validated the moment
8
+ // its last byte lands, so the same upload is refused after 100 KB, in
9
+ // milliseconds, with memory that does not grow with the body. Measured in
10
+ // the commit that added this: 517 ms and 783 MB to 6 ms and 114 MB for an
11
+ // early failure; the late failure costs the same time with a third of the
12
+ // memory.
13
+ //
14
+ // The splitting reads the text once and parses nothing it does not have to.
15
+ // NDJSON is split at newlines. A JSON array is split by a scan that tracks
16
+ // strings and nesting depth and hands out each top-level element; inside a
17
+ // string it jumps to the next quote with indexOf and counts the backslashes
18
+ // before it, so string bodies, most of a document's bytes, cost no branch
19
+ // per character. Each element is then parsed and validated on its own, by
20
+ // the validator's own verdict and, when it fails, its own error list.
21
+
22
+ const PARSE_KEYWORD = '__parse__';
23
+
24
+ // A record's failure as the stream reports it: the record's index from zero
25
+ // and the errors validate() would give for that document, or the parse
26
+ // error in the shape validateJSON() uses for a document that is not JSON.
27
+ function failure (record, errors) {
28
+ return { valid: false, record, errors };
29
+ }
30
+ function parseFailure (record, message) {
31
+ return failure(record, [{ keyword: PARSE_KEYWORD, instancePath: '', schemaPath: '', params: {}, message: 'invalid JSON: ' + message }]);
32
+ }
33
+
34
+ // The decoder for byte chunks: a multi-byte character split across two
35
+ // chunks is completed by the next one. Strings pass through untouched.
36
+ function decoderFor () {
37
+ const d = typeof TextDecoder === 'function' ? new TextDecoder('utf-8') : null;
38
+ return (chunk, last) => {
39
+ if (typeof chunk === 'string') return chunk;
40
+ if (d === null) return Buffer.from(chunk).toString('utf8');
41
+ return d.decode(chunk, { stream: !last });
42
+ };
43
+ }
44
+
45
+ // The end of the string that opens at `from` (the index after the quote), or
46
+ // -1 when the chunk ends first. A quote preceded by an odd number of
47
+ // backslashes is escaped.
48
+ function stringEnd (s, from) {
49
+ let i = from;
50
+ for (;;) {
51
+ const q = s.indexOf('"', i);
52
+ if (q < 0) return -1;
53
+ let bs = 0;
54
+ for (let k = q - 1; k >= from && s.charCodeAt(k) === 92; k--) bs++;
55
+ if ((bs & 1) === 0) return q;
56
+ i = q + 1;
57
+ }
58
+ }
59
+
60
+ function isSpace (c) { return c === 32 || c === 10 || c === 13 || c === 9; }
61
+
62
+ /**
63
+ * A record stream over one validator. `push(chunk)` takes a string, Buffer or
64
+ * Uint8Array and returns null while every record so far is valid, or the
65
+ * first failure as soon as a record fails (and the stream is then done).
66
+ * `end()` closes the input and returns `{ valid, records, errors }`, where
67
+ * `errors` lists every failure seen (one, unless `stopOnFirst` is false).
68
+ */
69
+ class RecordStream {
70
+ constructor (validator, opts) {
71
+ this._v = validator;
72
+ this._format = (opts && opts.format) || 'auto';
73
+ this._stop = !(opts && opts.stopOnFirst === false);
74
+ this._onRecord = (opts && typeof opts.onRecord === 'function') ? opts.onRecord : null;
75
+ this._decode = decoderFor();
76
+ this._buf = '';
77
+ this._pos = 0; // scan position in _buf
78
+ this._start = -1; // where the element in progress begins, array form
79
+ this._depth = 0; // nesting inside the current element, array form
80
+ this._opened = false; // the outer '[' has been read
81
+ this._closed = false; // the outer ']' has been read
82
+ this._expectValue = true; // array form: a value may start here (after '[' or ',')
83
+ this.records = 0;
84
+ this.failures = [];
85
+ this.done = false;
86
+ this.result = null;
87
+ }
88
+
89
+ // One record's text: parse, validate, report. Returns the failure or null.
90
+ _record (text) {
91
+ const index = this.records++;
92
+ let doc;
93
+ try { doc = JSON.parse(text); } catch (e) { return this._fail(parseFailure(index, e.message)); }
94
+ if (this._v.isValidObject(doc)) {
95
+ if (this._onRecord !== null) this._onRecord(doc, index);
96
+ return null;
97
+ }
98
+ const r = this._v.validate(doc);
99
+ return this._fail(failure(index, r.valid ? [] : r.errors));
100
+ }
101
+
102
+ _fail (f) {
103
+ this.failures.push(f);
104
+ if (this._stop) { this.done = true; this.result = { valid: false, records: this.records, errors: this.failures }; }
105
+ return f;
106
+ }
107
+
108
+ push (chunk) {
109
+ if (this.done) return this.failures.length ? this.failures[0] : null;
110
+ const text = this._decode(chunk, false);
111
+ if (text.length === 0) return null;
112
+ this._buf += text;
113
+ if (this._format === 'auto') {
114
+ let i = 0;
115
+ while (i < this._buf.length && isSpace(this._buf.charCodeAt(i))) i++;
116
+ if (i === this._buf.length) return null;
117
+ this._format = this._buf.charCodeAt(i) === 91 ? 'array' : 'ndjson';
118
+ }
119
+ return this._format === 'array' ? this._scanArray() : this._scanLines();
120
+ }
121
+
122
+ _scanLines () {
123
+ const s = this._buf;
124
+ let from = 0;
125
+ for (;;) {
126
+ const nl = s.indexOf('\n', from);
127
+ if (nl < 0) break;
128
+ let end = nl;
129
+ if (end > from && s.charCodeAt(end - 1) === 13) end--;
130
+ const line = s.slice(from, end);
131
+ from = nl + 1;
132
+ if (line.trim().length === 0) continue;
133
+ const f = this._record(line);
134
+ if (f !== null && this.done) { this._buf = ''; return f; }
135
+ }
136
+ this._buf = from === 0 ? s : s.slice(from);
137
+ return null;
138
+ }
139
+
140
+ _scanArray () {
141
+ const s = this._buf;
142
+ const n = s.length;
143
+ let pos = this._pos;
144
+ while (pos < n) {
145
+ const c = s.charCodeAt(pos);
146
+ if (!this._opened) {
147
+ if (isSpace(c)) { pos++; continue; }
148
+ if (c !== 91) return this._syntax('a JSON array must start with "["');
149
+ this._opened = true; pos++; continue;
150
+ }
151
+ if (this._closed) {
152
+ if (isSpace(c)) { pos++; continue; }
153
+ return this._syntax('unexpected text after the closing "]"');
154
+ }
155
+ if (this._start < 0) {
156
+ // Between elements: whitespace, a comma, the closing bracket, or the
157
+ // first character of a value.
158
+ if (isSpace(c)) { pos++; continue; }
159
+ if (c === 93) {
160
+ if (this._expectValue && this.records > 0) return this._syntax('a trailing comma before "]"');
161
+ this._closed = true; pos++; continue;
162
+ }
163
+ if (c === 44) {
164
+ if (this._expectValue) return this._syntax('two commas with no value between');
165
+ this._expectValue = true; pos++; continue;
166
+ }
167
+ if (!this._expectValue) return this._syntax('a value with no comma before it');
168
+ this._start = pos;
169
+ if (c === 34) {
170
+ const q = stringEnd(s, pos + 1);
171
+ if (q < 0) break; // the string continues in the next chunk
172
+ pos = q + 1;
173
+ const f = this._emit(s.slice(this._start, pos));
174
+ if (f !== null) return f;
175
+ continue;
176
+ }
177
+ if (c === 123 || c === 91) { this._depth = 1; pos++; continue; }
178
+ pos++; continue; // a scalar: it runs to a delimiter
179
+ }
180
+ if (this._depth === 0) {
181
+ // A scalar in progress ends at whitespace, a comma or the bracket,
182
+ // which the next round reads.
183
+ if (isSpace(c) || c === 44 || c === 93) {
184
+ const f = this._emit(s.slice(this._start, pos));
185
+ if (f !== null) return f;
186
+ continue;
187
+ }
188
+ pos++; continue;
189
+ }
190
+ // Inside an object or array element.
191
+ if (c === 34) {
192
+ const q = stringEnd(s, pos + 1);
193
+ if (q < 0) break;
194
+ pos = q + 1; continue;
195
+ }
196
+ if (c === 123 || c === 91) { this._depth++; pos++; continue; }
197
+ if (c === 125 || c === 93) {
198
+ this._depth--; pos++;
199
+ if (this._depth === 0) {
200
+ const f = this._emit(s.slice(this._start, pos));
201
+ if (f !== null) return f;
202
+ }
203
+ continue;
204
+ }
205
+ pos++;
206
+ }
207
+ // Keep only the element in progress, or nothing.
208
+ if (this._start >= 0) { this._buf = s.slice(this._start); this._pos = pos - this._start; this._start = 0; }
209
+ else { this._buf = ''; this._pos = 0; }
210
+ return null;
211
+ }
212
+
213
+ _syntax (message) {
214
+ this._buf = ''; this._pos = 0; this.done = true;
215
+ return this._fail(parseFailure(this.records, message));
216
+ }
217
+
218
+ _emit (text) {
219
+ this._start = -1; this._depth = 0; this._expectValue = false;
220
+ const f = this._record(text);
221
+ if (f !== null && this.done) { this._buf = ''; this._pos = 0; return f; }
222
+ return null;
223
+ }
224
+
225
+ end () {
226
+ if (!this.done) {
227
+ const tail = this._decode(new Uint8Array(0), true);
228
+ if (tail) this.push(tail);
229
+ if (!this.done) {
230
+ if (this._format === 'ndjson') { if (this._buf.trim().length > 0) this._record(this._buf); }
231
+ else if (this._format === 'array') {
232
+ if (!this._opened) this._fail(parseFailure(this.records, 'no JSON array in the input'));
233
+ else if (this._start >= 0 || !this._closed) this._fail(parseFailure(this.records, 'the array is not closed'));
234
+ }
235
+ }
236
+ this.done = true;
237
+ }
238
+ this._buf = '';
239
+ this.result = { valid: this.failures.length === 0, records: this.records, errors: this.failures };
240
+ return this.result;
241
+ }
242
+ }
243
+
244
+ function streamValidator (validator, opts) { return new RecordStream(validator, opts); }
245
+
246
+ // A Node http request (http.IncomingMessage) rather than any other Readable.
247
+ // Destroying one at the first failure closes the socket under the response
248
+ // the caller writes next: Node's fetch then gets `write EPIPE` and Chrome
249
+ // "Failed to fetch", never the 400. So reading stops and the rest of the
250
+ // body is drained and dropped; memory stays flat and the answer goes out
251
+ // (Node's fetch read it after 24 ms and 1 MB of a 100 MB upload).
252
+ function isHttpRequest (source) {
253
+ return typeof source.httpVersion === 'string' && typeof source.resume === 'function';
254
+ }
255
+
256
+ // The whole of an async source (a Node Readable, a Web ReadableStream, any
257
+ // async iterable of chunks) through a record stream. On the first failure
258
+ // the source is stopped: a Node stream is destroyed (a Node http request is
259
+ // drained instead, see isHttpRequest), a Web stream's reader cancelled, an
260
+ // iterator returned, so a failing upload is not validated to its end.
261
+ async function validateStream (validator, source, opts) {
262
+ const rs = new RecordStream(validator, opts);
263
+ if (source && typeof source.getReader === 'function') {
264
+ const reader = source.getReader();
265
+ try {
266
+ for (;;) {
267
+ const { value, done } = await reader.read();
268
+ if (done) break;
269
+ const f = rs.push(value);
270
+ if (f !== null && rs.done) { await reader.cancel('ata: record ' + f.record + ' is invalid'); return rs.end(); }
271
+ }
272
+ } finally { reader.releaseLock(); }
273
+ return rs.end();
274
+ }
275
+ const request = isHttpRequest(source);
276
+ // A request's iterator must leave the stream alive when returned.
277
+ const it = request && typeof source.iterator === 'function' ? source.iterator({ destroyOnReturn: false })
278
+ : source[Symbol.asyncIterator] ? source[Symbol.asyncIterator]() : source[Symbol.iterator]();
279
+ for (;;) {
280
+ const { value, done } = await it.next();
281
+ if (done) break;
282
+ const f = rs.push(value);
283
+ if (f !== null && rs.done) {
284
+ if (request) { if (typeof it.return === 'function') await it.return(); source.resume(); }
285
+ else if (typeof source.destroy === 'function') source.destroy();
286
+ else if (typeof it.return === 'function') await it.return();
287
+ return rs.end();
288
+ }
289
+ }
290
+ return rs.end();
291
+ }
292
+
293
+ module.exports = { RecordStream, streamValidator, validateStream, PARSE_KEYWORD };
package/lib/tier0.js CHANGED
@@ -55,8 +55,12 @@ const _PN = typeof Bun === 'undefined' && ({}).__proto__ === Object.prototype; /
55
55
  function formatFnOf(propSchema, userFormats) {
56
56
  const f = propSchema.format;
57
57
  if (typeof f !== 'string') return null;
58
- if (userFormats && typeof userFormats[f] === 'function') return userFormats[f];
59
- return require('./formats').formatChecks()[f] || null;
58
+ // Own properties only: `format: 'constructor'` or '__proto__' read an
59
+ // inherited Object member and called it as a check, or threw.
60
+ const own = Object.prototype.hasOwnProperty;
61
+ if (userFormats && own.call(userFormats, f) && typeof userFormats[f] === 'function') return userFormats[f];
62
+ const fc = require('./formats').formatChecks();
63
+ return own.call(fc, f) && typeof fc[f] === 'function' ? fc[f] : null;
60
64
  }
61
65
 
62
66
  function primConstraint(key, propSchema, userFormats) {
package/lib/ts-gen.js CHANGED
@@ -104,25 +104,30 @@ function renderObject(schema, defs, depth) {
104
104
  }
105
105
  return 'Record<string, unknown>';
106
106
  }
107
- const lines = keys.map((k) => {
108
- const t = renderValueType(props[k], defs, depth + 1);
107
+ // Each property's type is rendered once and the index signature below
108
+ // reuses the text; rendering it again there doubled the work per level.
109
+ const types = keys.map((k) => renderValueType(props[k], defs, depth + 1));
110
+ const lines = keys.map((k, i) => {
109
111
  const opt = required.has(k) ? '' : '?';
110
112
  const safeKey = /^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k);
111
113
  const doc = renderJsDoc(props[k], ' ');
112
- return `${doc} ${safeKey}${opt}: ${t};`;
114
+ return `${doc} ${safeKey}${opt}: ${types[i]};`;
113
115
  });
114
116
  // extra keys when additionalProperties is present as a schema or true
115
117
  const extra = schema.additionalProperties;
116
118
  if (extra && typeof extra === 'object') {
117
119
  // TypeScript requires the index signature to be a supertype of every
118
120
  // named property's emitted type. Widen to a union covering each property
119
- // type, plus undefined when any property is optional.
121
+ // type, plus undefined when any property is optional. A property whose
122
+ // type is an object literal is not repeated in the union: with one at
123
+ // every level the output doubled per level (2^levels characters) and a
124
+ // 22-level chain did not finish. `unknown` covers it.
120
125
  const widen = new Set();
121
126
  widen.add(renderValueType(extra, defs, depth + 1));
122
127
  let hasOptional = false;
123
- for (const k of keys) {
124
- widen.add(renderValueType(props[k], defs, depth + 1));
125
- if (!required.has(k)) hasOptional = true;
128
+ for (let i = 0; i < keys.length; i++) {
129
+ widen.add(types[i].includes('{') ? 'unknown' : types[i]);
130
+ if (!required.has(keys[i])) hasOptional = true;
126
131
  }
127
132
  if (hasOptional) widen.add('undefined');
128
133
  const indexType = widen.has('unknown') ? 'unknown' : Array.from(widen).join(' | ');
@@ -154,12 +159,20 @@ function toTypeName(name) {
154
159
  // constraints the TypeScript type cannot express (minLength, format, range,
155
160
  // etc.). Editors and TypeDoc surface these on hover, so authors can see what
156
161
  // the schema requires even though tsc does not enforce it.
162
+ // Text from the schema inside a block comment: a `*/` in a pattern, a format,
163
+ // a default or an example closed the comment and the rest of the value was
164
+ // read as a declaration. The slash is escaped, which a comment reader shows
165
+ // as written.
166
+ function commentSafe(s) {
167
+ return String(s).replace(/\*\//g, '*\\/');
168
+ }
169
+
157
170
  function renderJsDoc(schema, indent) {
158
171
  if (!schema || typeof schema !== 'object') return '';
159
172
 
160
173
  let description = '';
161
174
  if (typeof schema.description === 'string' && schema.description.length > 0) {
162
- description = schema.description.replace(/\*\//g, '* /');
175
+ description = commentSafe(schema.description);
163
176
  }
164
177
 
165
178
  const tags = [];
@@ -168,15 +181,15 @@ function renderJsDoc(schema, indent) {
168
181
  for (const k of numKeys) {
169
182
  if (typeof schema[k] === 'number') tags.push(`@${k} ${schema[k]}`);
170
183
  }
171
- if (typeof schema.pattern === 'string') tags.push(`@pattern ${schema.pattern}`);
172
- if (typeof schema.format === 'string') tags.push(`@format ${schema.format}`);
184
+ if (typeof schema.pattern === 'string') tags.push(`@pattern ${commentSafe(schema.pattern)}`);
185
+ if (typeof schema.format === 'string') tags.push(`@format ${commentSafe(schema.format)}`);
173
186
  if (schema.uniqueItems === true) tags.push('@uniqueItems');
174
187
  if (schema.deprecated === true) tags.push('@deprecated');
175
188
  if (schema.default !== undefined) {
176
- try { tags.push(`@default ${JSON.stringify(schema.default)}`); } catch (_) {}
189
+ try { tags.push(`@default ${commentSafe(JSON.stringify(schema.default))}`); } catch (_) {}
177
190
  }
178
191
  if (Array.isArray(schema.examples) && schema.examples.length > 0) {
179
- try { tags.push(`@example ${JSON.stringify(schema.examples[0])}`); } catch (_) {}
192
+ try { tags.push(`@example ${commentSafe(JSON.stringify(schema.examples[0]))}`); } catch (_) {}
180
193
  }
181
194
 
182
195
  if (description === '' && tags.length === 0) return '';
@@ -10,8 +10,8 @@
10
10
  // primitives key a Set or Map by the value itself. Longer arrays that hold an
11
11
  // object key a Map by a numeric structural hash, and only items whose hashes
12
12
  // collide are compared with `_deq`. Equal values always hash alike: numbers
13
- // by value (so 1 and 1.0, 0 and -0 agree), strings by their characters,
14
- // arrays by position, and objects by the sum of their own key-value hashes,
13
+ // by their bits, with 0 and -0 as one (1 and 1.0 are one double already),
14
+ // strings by their characters, arrays by position, and objects by the sum of their own key-value hashes,
15
15
  // whatever the key order. The hash replaced a sorted-key canonical string
16
16
  // built for every item on every call, which was over a quarter of the time a
17
17
  // generated verdict spent on SchemaStore's sample documents.
@@ -55,7 +55,15 @@ function _uqh(x) {
55
55
  if (t === 'number') {
56
56
  if (x === 0) return 4;
57
57
  h = x | 0;
58
- return Math.imul(h ^ (((x - h) * 1e9) | 0), 0x9e3779b1) ^ 5;
58
+ if (h === x) return Math.imul(h, 0x9e3779b1) ^ 5;
59
+ // Every bit of the number goes into the hash, read through a typed
60
+ // array kept on the function (its source is emitted into modules, so
61
+ // nothing outside it is read). Nine decimal places, which this used to
62
+ // keep, hashed every number below 1e-9 alike, and an array of them was
63
+ // compared item against item.
64
+ var f = _uqh.f || (_uqh.f = new Float64Array(1)), w = _uqh.w || (_uqh.w = new Uint32Array(f.buffer));
65
+ f[0] = x;
66
+ return Math.imul(w[0] ^ Math.imul(w[1], 0x9e3779b1), 0x85ebca6b) ^ 5;
59
67
  }
60
68
  if (t === 'string') {
61
69
  h = 0x811c9dc5 ^ x.length;
@@ -76,6 +76,11 @@ function buildDefaultsApplier(schema) {
76
76
  return _defaults.buildDefaultsApplier(schema);
77
77
  }
78
78
 
79
+ // The preprocess passes act on own properties only; what the document
80
+ // inherits is not part of it, and a value found through the prototype belongs
81
+ // to some other object.
82
+ const _hasOwn = Object.prototype.hasOwnProperty;
83
+
79
84
  // Build a function that coerces property values to match schema types in-place.
80
85
  // Handles string→number, string→integer, string→boolean, number→string, boolean→string.
81
86
  function buildCoercer(schema) {
@@ -109,7 +114,9 @@ function buildNodeCoercer(schema, seen) {
109
114
  if (!c) continue;
110
115
  const { scalar, inner } = c;
111
116
  steps.push((o) => {
112
- if (!(key in o)) return;
117
+ // Own properties only: `key in o` found values the object inherits,
118
+ // and the pass then rewrote the shared object behind them.
119
+ if (!_hasOwn.call(o, key)) return;
113
120
  if (scalar) {
114
121
  const v = scalar(o[key]);
115
122
  if (v !== undefined) o[key] = v;
@@ -211,7 +218,9 @@ function collectRemovals(schema, actions, path) {
211
218
  actions.push((data) => {
212
219
  let target = data;
213
220
  for (let j = 0; j < parentPath.length; j++) {
214
- if (typeof target !== "object" || target === null) return;
221
+ // Own keys only: a nested object the document inherits is not part
222
+ // of it, and deleting from it would strip a shared object.
223
+ if (typeof target !== "object" || target === null || !_hasOwn.call(target, parentPath[j])) return;
215
224
  target = target[parentPath[j]];
216
225
  }
217
226
  if (
@@ -386,6 +395,11 @@ function _jsonSyntaxRejection(e) {
386
395
 
387
396
  function createPaddedBuffer(jsonStr) {
388
397
  if (typeof Buffer === 'undefined') throw new Error('createPaddedBuffer requires Node.js Buffer');
398
+ // JSON text only. Buffer.from takes an array-like too, so { length: 1e8 }
399
+ // allocated a hundred million zero bytes and called that the document.
400
+ if (typeof jsonStr !== 'string' && !(jsonStr instanceof Uint8Array)) {
401
+ throw new TypeError('createPaddedBuffer expects a string or a Uint8Array (Buffer) of JSON text');
402
+ }
389
403
  const jsonBuf = Buffer.from(jsonStr);
390
404
  const padded = Buffer.allocUnsafe(jsonBuf.length + SIMDJSON_PADDING);
391
405
  jsonBuf.copy(padded);
@@ -832,10 +846,11 @@ class Validator {
832
846
  // and previously invalid data validates. `true` throws here, at
833
847
  // construction, with every finding; `'log'` reports through
834
848
  // `options.logger` or the console and continues. The check runs on the
835
- // schema as written, before any normalization touches it.
849
+ // schema as written, before any normalization touches it. A schema given
850
+ // as JSON text is parsed first: the string itself has no keywords.
836
851
  if (options.strictSchema === true || options.strictSchema === 'log') {
837
852
  const { checkSchemaStrict } = require('./strict-check');
838
- const problems = checkSchemaStrict(schema, { userKeywords: options.keywords || null });
853
+ const problems = checkSchemaStrict(typeof schema === 'string' ? JSON.parse(schema) : schema, { userKeywords: options.keywords || null });
839
854
  if (problems.length > 0) {
840
855
  const text = problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('\n');
841
856
  if (options.strictSchema === true) {
@@ -1586,7 +1601,7 @@ class Validator {
1586
1601
  // which rebind themselves on first use through _bindEntry, so the
1587
1602
  // extension is applied to whatever each one is now and again on rebind.
1588
1603
  if (_vx) {
1589
- this._entryExt = _jsonEntryWrappers(_vx);
1604
+ this._entryExt = _jsonEntryWrappers(this, _vx);
1590
1605
  for (const name of ['validateJSON', 'isValidJSON', 'validateAndParse']) {
1591
1606
  if (Object.prototype.hasOwnProperty.call(this, name) && typeof this[name] === 'function') _bindEntry(this, name, this[name]);
1592
1607
  }
@@ -1605,34 +1620,13 @@ class Validator {
1605
1620
  return this._engine || 'interpreter';
1606
1621
  }
1607
1622
 
1623
+ // The native engine's compiled schema and fast slot, set up on the first
1624
+ // buffer call. lib/native-attach.js; the browser build maps it to a stub,
1625
+ // since there is no addon there.
1608
1626
  _ensureNative() {
1609
1627
  if (this._nativeReady) return;
1610
1628
  this._nativeReady = true;
1611
- const native = getNative();
1612
- if (!native) return;
1613
- if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
1614
- let nativeSchemaStr = this._schemaStr;
1615
- // A boolean root references nothing, so the registry has nowhere to go and
1616
- // is left out; merging it used to throw on `true` and `false`.
1617
- if (this._schemaMap.size > 0 && typeof this._schemaObj === 'object' && this._schemaObj !== null) {
1618
- const merged = JSON.parse(this._schemaStr);
1619
- if (!merged.$defs) merged.$defs = {};
1620
- for (const [id, s] of this._schemaMap) {
1621
- merged.$defs['__ext_' + id.replace(/[^a-zA-Z0-9]/g, '_')] = s;
1622
- }
1623
- nativeSchemaStr = JSON.stringify(merged);
1624
- }
1625
- this._compiled = new native.CompiledSchema(nativeSchemaStr);
1626
- // The fast registry is a fixed array of slots in the addon, and registering
1627
- // a distinct schema past the last one throws. It is an accelerator for the
1628
- // buffer path, not a requirement, so a full registry leaves the slot at -1
1629
- // and the buffer methods answer from the JS engine instead. Letting this
1630
- // throw made every validator built after the 4096th unusable on that path.
1631
- try {
1632
- this._fastSlot = native.fastRegister(nativeSchemaStr);
1633
- } catch {
1634
- this._fastSlot = -1;
1635
- }
1629
+ require('./native-attach.js')(this, getNative());
1636
1630
  }
1637
1631
 
1638
1632
  // The buffer APIs, installed on the first call to any of them. The schema
@@ -1653,33 +1647,7 @@ class Validator {
1653
1647
  if (!native && typeof Buffer === 'undefined') throw new Error(`Native addon required for ${name}(). Use validate(), isValidObject() or validateJSON(), which do not need it.`);
1654
1648
  const { bufferNeedsSlowPath, installSlowBufferApis } = require('./buffer-gate.js');
1655
1649
  if (!native) return installSlowBufferApis(this);
1656
- if (bufferNeedsSlowPath(this._schemaObj, this._schemaMap, this._keywords)) return installSlowBufferApis(this);
1657
- this._ensureNative();
1658
- const slot = this._fastSlot;
1659
- if (!(slot >= 0)) return installSlowBufferApis(this);
1660
- const bytes = (b, who) => {
1661
- if (typeof b === 'string') return Buffer.from(b);
1662
- if (b instanceof Uint8Array) return b;
1663
- throw new TypeError(`${who}() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().`);
1664
- };
1665
- this.isValid = (b) => native.rawFastValidate(slot, bytes(b, 'isValid'));
1666
- this.isValidPrepadded = (paddedBuffer, jsonLength) => native.rawFastValidate(slot, paddedBuffer, jsonLength);
1667
- this.isValidParallel = (b) => native.rawParallelValidate(slot, bytes(b, 'isValidParallel'));
1668
- this.isValidNDJSON = (b) => native.rawNDJSONValidate(slot, bytes(b, 'isValidNDJSON'));
1669
- this.countValid = (b) => {
1670
- const r = native.rawNDJSONValidate(slot, bytes(b, 'countValid'));
1671
- let n = 0;
1672
- for (let i = 0; i < r.length; i++) if (r[i]) n++;
1673
- return n;
1674
- };
1675
- this.batchIsValid = (buffers) => {
1676
- let n = 0;
1677
- for (const b of buffers) {
1678
- if (!(b instanceof Uint8Array)) throw new TypeError('batchIsValid() requires Buffer or Uint8Array elements');
1679
- if (native.rawFastValidate(slot, b)) n++;
1680
- }
1681
- return n;
1682
- };
1650
+ require('./native-attach.js').installFast(this, native, bufferNeedsSlowPath, installSlowBufferApis);
1683
1651
  }
1684
1652
 
1685
1653
  addSchema(schema) {
@@ -1696,6 +1664,10 @@ class Validator {
1696
1664
  const normalized = _normalizeCallerSchema(schema, rootIsDraft7)
1697
1665
  this._ownSchemaMap()
1698
1666
  this._schemaMap.set(normalized.$id, normalized)
1667
+ // An interpreted twin built for the first calls (lib/codegen-engine.js
1668
+ // coldTwin) carries the registry as it was; the next call builds one
1669
+ // from the registry as it is now.
1670
+ if (this._twin !== undefined) this._twin = undefined
1699
1671
  }
1700
1672
 
1701
1673
  // buildSchemaMap hands the same map to every validator built from the same
@@ -1754,23 +1726,38 @@ class Validator {
1754
1726
  v._preprocess = preprocess;
1755
1727
 
1756
1728
  // Error function — use pre-compiled from standalone if available, else compile
1757
- let errFn = (d) => ({
1729
+ const stub = (d) => ({
1758
1730
  valid: false,
1759
1731
  errors: [
1760
1732
  { code: "validation_failed", path: "", message: "validation failed" },
1761
1733
  ],
1762
1734
  });
1735
+ let errFn = stub;
1763
1736
  if (mod.errFn) {
1764
1737
  errFn = (d) => mod.errFn(d, true);
1765
1738
  } else if (compileToJSCodegenWithErrors !== undefined) {
1766
1739
  // Without the code generator (the edge entry) a module that carries no
1767
- // error function reports the stub above.
1768
- const jsErrFn = compileToJSCodegenWithErrors(schemaObj);
1740
+ // error function reports the stub above. The function is compiled from
1741
+ // the schema as the runtime reads it, normalized, with the registry and
1742
+ // the caller's formats: compiled from the schema as written, a draft-04
1743
+ // boolean exclusiveMinimum was an annotation and the function said
1744
+ // valid. It only explains a rejection: the module's verdict stands, so
1745
+ // a valid: true from here is reported as the stub, never as acceptance.
1746
+ let jsErrFn = null;
1747
+ try {
1748
+ const draft = schemaObj && typeof schemaObj === 'object' ? draftCode(schemaObj.$schema) : 0;
1749
+ const normalized = _normalizeCallerSchema(schemaObj, 0);
1750
+ const map = buildSchemaMap(options.schemas, draft);
1751
+ jsErrFn = compileToJSCodegenWithErrors(normalized, map && map.size > 0 ? map : null, options.formats || null);
1752
+ if (jsErrFn) jsErrFn({}, true);
1753
+ } catch {
1754
+ jsErrFn = null;
1755
+ }
1769
1756
  if (jsErrFn) {
1770
- try {
1771
- jsErrFn({}, true);
1772
- errFn = (d) => jsErrFn(d, true);
1773
- } catch {}
1757
+ errFn = (d) => {
1758
+ const r = jsErrFn(d, true);
1759
+ return r && r.valid === false ? r : stub(d);
1760
+ };
1774
1761
  }
1775
1762
  }
1776
1763
 
@@ -2083,8 +2070,16 @@ function _bindEntry(self, name, fn) {
2083
2070
 
2084
2071
  // The JSON entry points under an extension: the schema answers first, and only
2085
2072
  // text it accepts is parsed for the check, so a rejection costs nothing extra.
2086
- function _jsonEntryWrappers({ check, errors }) {
2087
- const parse = (text) => JSON.parse(typeof text === 'string' ? text : new TextDecoder().decode(text));
2073
+ // The check sees the document the schema checked: the parsed text after the
2074
+ // preprocess pass (defaults, coercion, removal). Checking the raw text let a
2075
+ // default the check rejects through.
2076
+ function _jsonEntryWrappers(self, { check, errors }) {
2077
+ const parse = (text) => {
2078
+ const data = JSON.parse(typeof text === 'string' ? text : new TextDecoder().decode(text));
2079
+ const pre = self._preprocess;
2080
+ if (pre !== null && pre !== undefined) pre(data);
2081
+ return data;
2082
+ };
2088
2083
  return {
2089
2084
  validateJSON: (inner) => (text) => {
2090
2085
  const res = inner(text);