ata-validator 1.6.2 → 1.7.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 +26 -0
- package/README.md +6 -5
- package/build.d.ts +12 -0
- package/index.d.ts +7 -0
- package/index.js +58 -13
- package/lib/aot-build.js +1 -0
- package/lib/aot.js +69 -19
- package/lib/buffer-gate.js +132 -0
- package/lib/draft7.js +24 -2
- package/lib/interpreter.js +432 -200
- package/lib/js-compiler.js +184 -53
- package/lib/metaschemas.js +21 -0
- package/lib/safe-regex-source.js +1 -1
- package/lib/safe-regex.js +209 -29
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/lib/safe-regex.js
CHANGED
|
@@ -226,60 +226,240 @@ function compileProg (ast) {
|
|
|
226
226
|
return prog
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
// Numeric opcodes for the runner. The program is compiled once into flat
|
|
230
|
+
// typed arrays so the inner loop does no property lookups or string compares.
|
|
231
|
+
const OP_CHAR = 0
|
|
232
|
+
const OP_ANY = 1
|
|
233
|
+
const OP_CLASS = 2
|
|
234
|
+
const OP_SPLIT = 3
|
|
235
|
+
const OP_JMP = 4
|
|
236
|
+
const OP_BOL = 5
|
|
237
|
+
const OP_EOL = 6
|
|
238
|
+
const OP_MATCH = 7
|
|
239
|
+
|
|
240
|
+
function classMatcher (instr) {
|
|
241
|
+
// ASCII is answered from a bitmap; anything above 0x7f walks the ranges.
|
|
242
|
+
const bits = new Uint8Array(128)
|
|
231
243
|
const r = instr.ranges
|
|
232
|
-
for (let k = 0; k < r.length; k++) {
|
|
233
|
-
|
|
244
|
+
for (let k = 0; k < r.length; k++) {
|
|
245
|
+
const hi = Math.min(r[k][1], 127)
|
|
246
|
+
for (let c = r[k][0]; c <= hi; c++) bits[c] = 1
|
|
247
|
+
}
|
|
248
|
+
return { bits, ranges: r, neg: instr.neg }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function matchClass (cls, c) {
|
|
252
|
+
let inside
|
|
253
|
+
if (c < 128) {
|
|
254
|
+
inside = cls.bits[c] === 1
|
|
255
|
+
} else {
|
|
256
|
+
inside = false
|
|
257
|
+
const r = cls.ranges
|
|
258
|
+
for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }
|
|
259
|
+
}
|
|
260
|
+
return cls.neg ? !inside : inside
|
|
234
261
|
}
|
|
235
262
|
|
|
236
263
|
function makeRunner (prog) {
|
|
237
264
|
const n = prog.length
|
|
265
|
+
const ops = new Uint8Array(n)
|
|
266
|
+
const xs = new Int32Array(n)
|
|
267
|
+
const ys = new Int32Array(n)
|
|
268
|
+
const cs = new Int32Array(n)
|
|
269
|
+
const classes = new Array(n)
|
|
270
|
+
for (let i = 0; i < n; i++) {
|
|
271
|
+
const I = prog[i]
|
|
272
|
+
switch (I.op) {
|
|
273
|
+
case 'char': ops[i] = OP_CHAR; cs[i] = I.c; break
|
|
274
|
+
case 'any': ops[i] = OP_ANY; break
|
|
275
|
+
case 'class': ops[i] = OP_CLASS; classes[i] = classMatcher(I); break
|
|
276
|
+
case 'split': ops[i] = OP_SPLIT; xs[i] = I.x; ys[i] = I.y; break
|
|
277
|
+
case 'jmp': ops[i] = OP_JMP; xs[i] = I.x; break
|
|
278
|
+
case 'bol': ops[i] = OP_BOL; break
|
|
279
|
+
case 'eol': ops[i] = OP_EOL; break
|
|
280
|
+
case 'match': ops[i] = OP_MATCH; break
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
238
284
|
const lastGen = new Int32Array(n).fill(-1)
|
|
239
285
|
let gen = 0
|
|
240
|
-
|
|
286
|
+
// Each unvisited instruction is popped once and pushes at most two, so the
|
|
287
|
+
// stack never holds more than 2n + 1 entries.
|
|
288
|
+
const stack = new Int32Array(2 * n + 2)
|
|
289
|
+
// Thread lists hold at most one entry per instruction per step.
|
|
290
|
+
let clist = new Int32Array(n)
|
|
291
|
+
let nlist = new Int32Array(n)
|
|
292
|
+
let clen = 0
|
|
293
|
+
let nlen = 0
|
|
241
294
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
295
|
+
// Follows epsilon edges from `pc` and records every consuming instruction
|
|
296
|
+
// (or match) reached in `list`. `lastGen` dedupes per step.
|
|
297
|
+
function addThread (list, len0, pc, pos, len) {
|
|
298
|
+
// Most transitions land directly on a consuming instruction; skip the
|
|
299
|
+
// stack walk for those.
|
|
300
|
+
if (ops[pc] <= OP_CLASS || ops[pc] === OP_MATCH) {
|
|
301
|
+
if (lastGen[pc] === gen) return len0
|
|
302
|
+
lastGen[pc] = gen
|
|
303
|
+
list[len0] = pc
|
|
304
|
+
return len0 + 1
|
|
305
|
+
}
|
|
306
|
+
let sp = 0
|
|
307
|
+
stack[sp++] = pc
|
|
308
|
+
let count = len0
|
|
309
|
+
while (sp > 0) {
|
|
310
|
+
const p = stack[--sp]
|
|
247
311
|
if (lastGen[p] === gen) continue
|
|
248
312
|
lastGen[p] = gen
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
case
|
|
252
|
-
case
|
|
253
|
-
case
|
|
254
|
-
|
|
255
|
-
default: list.push(p)
|
|
313
|
+
switch (ops[p]) {
|
|
314
|
+
case OP_JMP: stack[sp++] = xs[p]; break
|
|
315
|
+
case OP_SPLIT: stack[sp++] = ys[p]; stack[sp++] = xs[p]; break
|
|
316
|
+
case OP_BOL: if (pos === 0) stack[sp++] = p + 1; break
|
|
317
|
+
case OP_EOL: if (pos === len) stack[sp++] = p + 1; break
|
|
318
|
+
default: list[count++] = p
|
|
256
319
|
}
|
|
257
320
|
}
|
|
321
|
+
return count
|
|
258
322
|
}
|
|
259
323
|
|
|
260
|
-
|
|
324
|
+
// A pattern is anchored when starting it anywhere but position 0 yields no
|
|
325
|
+
// thread, which is the case for `^...` and its alternations. The probe sits
|
|
326
|
+
// at position 1 of a length-1 string so that only `^` can fail. For anchored
|
|
327
|
+
// patterns the per-position restart below is skipped, and an empty thread
|
|
328
|
+
// list means the match has already failed.
|
|
329
|
+
gen++
|
|
330
|
+
const anchored = addThread(nlist, 0, 0, 1, 1) === 0
|
|
331
|
+
|
|
332
|
+
function testNFA (s) {
|
|
261
333
|
const len = s.length
|
|
262
|
-
let clist = []
|
|
263
|
-
let nlist = []
|
|
264
334
|
gen++
|
|
265
|
-
addThread(clist, 0, 0, len)
|
|
335
|
+
clen = addThread(clist, 0, 0, 0, len)
|
|
266
336
|
for (let pos = 0; pos <= len; pos++) {
|
|
267
337
|
const c = pos < len ? s.charCodeAt(pos) : -1
|
|
268
338
|
gen++
|
|
269
|
-
|
|
270
|
-
for (let k = 0; k <
|
|
339
|
+
nlen = 0
|
|
340
|
+
for (let k = 0; k < clen; k++) {
|
|
271
341
|
const pc = clist[k]
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
342
|
+
switch (ops[pc]) {
|
|
343
|
+
case OP_MATCH: return true
|
|
344
|
+
case OP_CHAR: if (c === cs[pc]) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break
|
|
345
|
+
case OP_ANY: if (c !== -1 && c !== 10) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break
|
|
346
|
+
case OP_CLASS: if (c !== -1 && matchClass(classes[pc], c)) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (pos < len) {
|
|
350
|
+
if (!anchored) nlen = addThread(nlist, nlen, 0, pos + 1, len)
|
|
351
|
+
else if (nlen === 0) return false
|
|
277
352
|
}
|
|
278
|
-
if (pos < len) addThread(nlist, 0, pos + 1, len)
|
|
279
353
|
const tmp = clist; clist = nlist; nlist = tmp
|
|
354
|
+
clen = nlen
|
|
280
355
|
}
|
|
281
356
|
return false
|
|
282
357
|
}
|
|
358
|
+
|
|
359
|
+
// Lazy DFA on top of the NFA. A DFA state is the set of consuming
|
|
360
|
+
// instructions live at a position; transitions are computed on first use
|
|
361
|
+
// and cached per ASCII character. `^` and `$` depend on position, so the
|
|
362
|
+
// closure is taken with flags for "at start" and "at end", which gives two
|
|
363
|
+
// start states and two transition tables per state. The state count is
|
|
364
|
+
// capped; past the cap the matcher falls back to the NFA walk above, so the
|
|
365
|
+
// time bound stays linear either way.
|
|
366
|
+
const MAX_STATES = 256
|
|
367
|
+
const states = []
|
|
368
|
+
const stateIds = new Map()
|
|
369
|
+
let overflow = false
|
|
370
|
+
|
|
371
|
+
function closure (list, count, pc, atStart, atEnd) {
|
|
372
|
+
// Same walk as addThread, with the position replaced by the two flags.
|
|
373
|
+
let sp = 0
|
|
374
|
+
stack[sp++] = pc
|
|
375
|
+
while (sp > 0) {
|
|
376
|
+
const p = stack[--sp]
|
|
377
|
+
if (lastGen[p] === gen) continue
|
|
378
|
+
lastGen[p] = gen
|
|
379
|
+
switch (ops[p]) {
|
|
380
|
+
case OP_JMP: stack[sp++] = xs[p]; break
|
|
381
|
+
case OP_SPLIT: stack[sp++] = ys[p]; stack[sp++] = xs[p]; break
|
|
382
|
+
case OP_BOL: if (atStart) stack[sp++] = p + 1; break
|
|
383
|
+
case OP_EOL: if (atEnd) stack[sp++] = p + 1; break
|
|
384
|
+
default: list[count++] = p
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return count
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function internState (list, count) {
|
|
391
|
+
const pcs = Array.from(list.subarray(0, count)).sort((a, b) => a - b)
|
|
392
|
+
const key = pcs.join(',')
|
|
393
|
+
let id = stateIds.get(key)
|
|
394
|
+
if (id !== undefined) return id
|
|
395
|
+
if (states.length >= MAX_STATES) { overflow = true; return -1 }
|
|
396
|
+
id = states.length
|
|
397
|
+
let isMatch = false
|
|
398
|
+
for (let k = 0; k < pcs.length; k++) if (ops[pcs[k]] === OP_MATCH) { isMatch = true; break }
|
|
399
|
+
states.push({ pcs: Int32Array.from(pcs), isMatch, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })
|
|
400
|
+
stateIds.set(key, id)
|
|
401
|
+
return id
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function step (state, c, atEnd) {
|
|
405
|
+
gen++
|
|
406
|
+
let count = 0
|
|
407
|
+
const pcs = state.pcs
|
|
408
|
+
for (let k = 0; k < pcs.length; k++) {
|
|
409
|
+
const pc = pcs[k]
|
|
410
|
+
switch (ops[pc]) {
|
|
411
|
+
case OP_CHAR: if (c === cs[pc]) count = closure(nlist, count, pc + 1, false, atEnd); break
|
|
412
|
+
case OP_ANY: if (c !== 10) count = closure(nlist, count, pc + 1, false, atEnd); break
|
|
413
|
+
case OP_CLASS: if (matchClass(classes[pc], c)) count = closure(nlist, count, pc + 1, false, atEnd); break
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!anchored) count = closure(nlist, count, 0, false, atEnd)
|
|
417
|
+
return internState(nlist, count)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let startEmpty = -2
|
|
421
|
+
let startNonEmpty = -2
|
|
422
|
+
|
|
423
|
+
function startState (atEnd) {
|
|
424
|
+
gen++
|
|
425
|
+
const count = closure(nlist, 0, 0, true, atEnd)
|
|
426
|
+
return internState(nlist, count)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function testDFA (s) {
|
|
430
|
+
const len = s.length
|
|
431
|
+
let id
|
|
432
|
+
if (len === 0) {
|
|
433
|
+
if (startEmpty === -2) startEmpty = startState(true)
|
|
434
|
+
id = startEmpty
|
|
435
|
+
} else {
|
|
436
|
+
if (startNonEmpty === -2) startNonEmpty = startState(false)
|
|
437
|
+
id = startNonEmpty
|
|
438
|
+
}
|
|
439
|
+
if (id < 0) return testNFA(s)
|
|
440
|
+
let state = states[id]
|
|
441
|
+
for (let pos = 0; pos < len; pos++) {
|
|
442
|
+
if (state.isMatch) return true
|
|
443
|
+
const c = s.charCodeAt(pos)
|
|
444
|
+
const atEnd = pos + 1 === len
|
|
445
|
+
let nid
|
|
446
|
+
if (c < 128) {
|
|
447
|
+
const table = atEnd ? state.nextEnd : state.next
|
|
448
|
+
nid = table[c]
|
|
449
|
+
if (nid === -2) { nid = step(state, c, atEnd); table[c] = nid }
|
|
450
|
+
} else {
|
|
451
|
+
nid = step(state, c, atEnd)
|
|
452
|
+
}
|
|
453
|
+
if (nid < 0) return testNFA(s)
|
|
454
|
+
state = states[nid]
|
|
455
|
+
if (anchored && state.pcs.length === 0) return false
|
|
456
|
+
}
|
|
457
|
+
return state.isMatch
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return function test (s) {
|
|
461
|
+
return overflow ? testNFA(s) : testDFA(s)
|
|
462
|
+
}
|
|
283
463
|
}
|
|
284
464
|
|
|
285
465
|
function compileSafe (pattern) {
|
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.7.0",
|
|
4
4
|
"description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"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",
|
|
45
45
|
"build": "cmake-js build --target ata",
|
|
46
46
|
"rebuild": "cmake-js rebuild --target ata",
|
|
47
|
-
"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_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_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_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.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_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_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.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",
|
|
47
|
+
"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_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_diagnostic.js && node tests/test_format_engine_parity.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_pattern_properties_errors.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_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_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.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",
|
|
48
48
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
49
49
|
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
|
|
50
50
|
"test:compat": "node tests/test_compat.js",
|
|
@@ -112,12 +112,12 @@
|
|
|
112
112
|
"LICENSE"
|
|
113
113
|
],
|
|
114
114
|
"optionalDependencies": {
|
|
115
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
116
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
117
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
118
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
119
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
120
|
-
"@ata-validator/native-win32-x64": "1.
|
|
115
|
+
"@ata-validator/native-darwin-arm64": "1.7.0",
|
|
116
|
+
"@ata-validator/native-linux-x64-gnu": "1.7.0",
|
|
117
|
+
"@ata-validator/native-linux-arm64-gnu": "1.7.0",
|
|
118
|
+
"@ata-validator/native-linux-x64-musl": "1.7.0",
|
|
119
|
+
"@ata-validator/native-linux-arm64-musl": "1.7.0",
|
|
120
|
+
"@ata-validator/native-win32-x64": "1.7.0"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
|
123
123
|
"yaml": "^2.0.0"
|