ata-validator 1.23.0 → 1.25.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,158 @@
1
+ 'use strict';
2
+
3
+ // Helpers shared by every generated scanner. The scanner reads a JSON document
4
+ // straight out of the text and answers the schema from it, so anything here
5
+ // that is laxer than JSON.parse turns into a document accepted that parsing
6
+ // would have rejected. Each function below is written to the JSON grammar,
7
+ // not to what looks close enough.
8
+
9
+ // Four hex digits after a \u escape. JSON.parse throws without them.
10
+ function hex4(s, i) {
11
+ for (let k = i; k < i + 4; k++) {
12
+ const c = s.charCodeAt(k);
13
+ if (c >= 48 && c <= 57) continue;
14
+ if (c >= 97 && c <= 102) continue;
15
+ if (c >= 65 && c <= 70) continue;
16
+ return false;
17
+ }
18
+ return true;
19
+ }
20
+
21
+ // The depth at which a skipped subtree stops being skipped and the caller
22
+ // falls back to JSON.parse. V8's own parser gives up somewhere past this, and
23
+ // the point is not to guess where: past this depth the scanner declines rather
24
+ // than risk answering a document the parser would have refused.
25
+ const MAX_SKIP_DEPTH = 512;
26
+
27
+ const MALFORMED = -1;
28
+ const TOO_DEEP = -2;
29
+
30
+ // Validate one JSON value starting at `i` and return the index just past it,
31
+ // MALFORMED if the text is not JSON, or TOO_DEEP. Iterative: a nesting depth
32
+ // the schema knows nothing about must not be able to overflow the stack.
33
+ function skipValue(s, i) {
34
+ const len = s.length;
35
+ // `stack` holds, per open container, 0 for an object and 1 for an array.
36
+ const stack = [];
37
+ let depth = 0;
38
+ for (;;) {
39
+ // --- one value
40
+ let c = s.charCodeAt(i);
41
+ while (c === 32 || c === 10 || c === 9 || c === 13) c = s.charCodeAt(++i);
42
+ if (c === 34) {
43
+ i = skipString(s, i);
44
+ if (i < 0) return MALFORMED;
45
+ } else if (c === 123 || c === 91) {
46
+ if (depth >= MAX_SKIP_DEPTH) return TOO_DEEP;
47
+ stack[depth++] = c === 123 ? 0 : 1;
48
+ i++;
49
+ c = s.charCodeAt(i);
50
+ while (c === 32 || c === 10 || c === 9 || c === 13) c = s.charCodeAt(++i);
51
+ if (c === (stack[depth - 1] === 0 ? 125 : 93)) { depth--; i++; }
52
+ else if (stack[depth - 1] === 0) {
53
+ // an object member starts with a key
54
+ if (c !== 34) return MALFORMED;
55
+ i = skipString(s, i);
56
+ if (i < 0) return MALFORMED;
57
+ c = s.charCodeAt(i);
58
+ while (c === 32 || c === 10 || c === 9 || c === 13) c = s.charCodeAt(++i);
59
+ if (c !== 58) return MALFORMED;
60
+ i++;
61
+ continue;
62
+ } else {
63
+ continue;
64
+ }
65
+ } else if (c === 116) {
66
+ if (s.charCodeAt(i + 1) !== 114 || s.charCodeAt(i + 2) !== 117 || s.charCodeAt(i + 3) !== 101) return MALFORMED;
67
+ i += 4;
68
+ } else if (c === 102) {
69
+ if (s.charCodeAt(i + 1) !== 97 || s.charCodeAt(i + 2) !== 108 || s.charCodeAt(i + 3) !== 115 || s.charCodeAt(i + 4) !== 101) return MALFORMED;
70
+ i += 5;
71
+ } else if (c === 110) {
72
+ if (s.charCodeAt(i + 1) !== 117 || s.charCodeAt(i + 2) !== 108 || s.charCodeAt(i + 3) !== 108) return MALFORMED;
73
+ i += 4;
74
+ } else {
75
+ i = skipNumber(s, i);
76
+ if (i < 0) return MALFORMED;
77
+ }
78
+
79
+ // --- close as many containers as the next punctuation closes
80
+ for (;;) {
81
+ if (depth === 0) return i;
82
+ let c2 = s.charCodeAt(i);
83
+ while (c2 === 32 || c2 === 10 || c2 === 9 || c2 === 13) c2 = s.charCodeAt(++i);
84
+ const inObject = stack[depth - 1] === 0;
85
+ if (c2 === 44) {
86
+ i++;
87
+ if (inObject) {
88
+ let c3 = s.charCodeAt(i);
89
+ while (c3 === 32 || c3 === 10 || c3 === 9 || c3 === 13) c3 = s.charCodeAt(++i);
90
+ if (c3 !== 34) return MALFORMED;
91
+ i = skipString(s, i);
92
+ if (i < 0) return MALFORMED;
93
+ c3 = s.charCodeAt(i);
94
+ while (c3 === 32 || c3 === 10 || c3 === 9 || c3 === 13) c3 = s.charCodeAt(++i);
95
+ if (c3 !== 58) return MALFORMED;
96
+ i++;
97
+ }
98
+ break; // read the next value
99
+ }
100
+ if (c2 === (inObject ? 125 : 93)) { depth--; i++; continue; }
101
+ return MALFORMED;
102
+ }
103
+ if (i > len) return MALFORMED;
104
+ }
105
+ }
106
+
107
+ // `i` is at the opening quote; returns the index just past the closing one.
108
+ function skipString(s, i) {
109
+ i++;
110
+ for (;;) {
111
+ const c = s.charCodeAt(i);
112
+ if (c === 34) return i + 1;
113
+ if (c === 92) {
114
+ const e = s.charCodeAt(i + 1);
115
+ if (e === 117) {
116
+ if (!hex4(s, i + 2)) return -1;
117
+ i += 6;
118
+ continue;
119
+ }
120
+ if (e === 34 || e === 92 || e === 47 || e === 98 || e === 102 || e === 110 || e === 114 || e === 116) { i += 2; continue; }
121
+ return -1;
122
+ }
123
+ // NaN at end of input fails this too, which is the unterminated case.
124
+ if (!(c >= 32)) return -1;
125
+ i++;
126
+ }
127
+ }
128
+
129
+ function skipNumber(s, i) {
130
+ let c = s.charCodeAt(i);
131
+ if (c === 45) c = s.charCodeAt(++i);
132
+ if (c === 48) c = s.charCodeAt(++i);
133
+ else if (c >= 49 && c <= 57) { do { c = s.charCodeAt(++i); } while (c >= 48 && c <= 57); }
134
+ else return -1;
135
+ if (c === 46) {
136
+ c = s.charCodeAt(++i);
137
+ if (!(c >= 48 && c <= 57)) return -1;
138
+ do { c = s.charCodeAt(++i); } while (c >= 48 && c <= 57);
139
+ }
140
+ if (c === 101 || c === 69) {
141
+ c = s.charCodeAt(++i);
142
+ if (c === 43 || c === 45) c = s.charCodeAt(++i);
143
+ if (!(c >= 48 && c <= 57)) return -1;
144
+ do { c = s.charCodeAt(++i); } while (c >= 48 && c <= 57);
145
+ }
146
+ return i;
147
+ }
148
+
149
+ // The decoded value of a string span, for the checks that need the string
150
+ // itself. `p` is just past the opening quote, `e` is at the closing one.
151
+ // An escaped span is decoded by the parser, so the result is what JSON.parse
152
+ // would have produced for that member and nothing subtler.
153
+ function span(s, p, e, escaped) {
154
+ if (!escaped) return s.slice(p, e);
155
+ return JSON.parse(s.slice(p - 1, e + 1));
156
+ }
157
+
158
+ module.exports = { hex4, skipValue, skipString, skipNumber, span, MALFORMED, TOO_DEEP, MAX_SKIP_DEPTH };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ // A stable content hash for a schema, so a compiled artifact can say which
4
+ // schema it came from and a build can detect staleness by comparing hashes
5
+ // instead of embedding its own. The hash is over a canonical JSON form
6
+ // (object keys sorted at every level), so the same schema hashes the same
7
+ // however its keys were ordered, and it is FNV-1a with two independent
8
+ // bases, 16 hex characters. This is an integrity aid, not a security
9
+ // boundary: it detects drift, it does not authenticate anything.
10
+
11
+ function canonical (value) {
12
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
13
+ if (Array.isArray(value)) return '[' + value.map(canonical).join(',') + ']';
14
+ const keys = Object.keys(value).sort();
15
+ const parts = [];
16
+ for (const k of keys) {
17
+ if (value[k] === undefined) continue;
18
+ parts.push(JSON.stringify(k) + ':' + canonical(value[k]));
19
+ }
20
+ return '{' + parts.join(',') + '}';
21
+ }
22
+
23
+ function fnv1a (str, seed) {
24
+ let h = seed >>> 0;
25
+ for (let i = 0; i < str.length; i++) {
26
+ h ^= str.charCodeAt(i);
27
+ h = Math.imul(h, 0x01000193) >>> 0;
28
+ }
29
+ return h >>> 0;
30
+ }
31
+
32
+ function schemaHash (schema) {
33
+ const text = canonical(schema);
34
+ const a = fnv1a(text, 0x811c9dc5);
35
+ const b = fnv1a(text, 0xcbf29ce4);
36
+ return a.toString(16).padStart(8, '0') + b.toString(16).padStart(8, '0');
37
+ }
38
+
39
+ module.exports = { schemaHash };
@@ -32,6 +32,33 @@ function specKeywords() {
32
32
  return SPEC_KEYWORDS;
33
33
  }
34
34
 
35
+ // Which instance type each constraining keyword acts on. A keyword sitting on
36
+ // a node whose `type` excludes it is inert: `minimum` on a string can never
37
+ // fire, so the rule the author meant is absent, which is the same fail-open
38
+ // class as a typo. Only nodes with an explicit `type` are judged, since
39
+ // without one the keyword legitimately waits for matching data.
40
+ const KEYWORD_TYPES = new Map(Object.entries({
41
+ minLength: ['string'], maxLength: ['string'], pattern: ['string'],
42
+ minimum: ['number'], maximum: ['number'],
43
+ exclusiveMinimum: ['number'], exclusiveMaximum: ['number'], multipleOf: ['number'],
44
+ items: ['array'], prefixItems: ['array'], additionalItems: ['array'],
45
+ minItems: ['array'], maxItems: ['array'], uniqueItems: ['array'],
46
+ contains: ['array'], minContains: ['array'], maxContains: ['array'], unevaluatedItems: ['array'],
47
+ properties: ['object'], patternProperties: ['object'], additionalProperties: ['object'],
48
+ required: ['object'], minProperties: ['object'], maxProperties: ['object'],
49
+ propertyNames: ['object'], dependentRequired: ['object'], dependentSchemas: ['object'],
50
+ unevaluatedProperties: ['object'],
51
+ }));
52
+
53
+ function typeAllows(declared, wanted) {
54
+ const list = Array.isArray(declared) ? declared : [declared];
55
+ for (const t of list) {
56
+ if (t === wanted) return true;
57
+ if (wanted === 'number' && t === 'integer') return true;
58
+ }
59
+ return false;
60
+ }
61
+
35
62
  // Where subschemas live, so the walk knows a key under `properties` is a
36
63
  // property name and a key directly on a schema object is a keyword.
37
64
  const SCHEMA_MAPS = new Set(['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']);
@@ -113,6 +140,32 @@ function checkSchemaStrict(root, options) {
113
140
  problems.push({ path: at, message: `$ref "${value}" does not resolve in this document` });
114
141
  continue;
115
142
  }
143
+ // An inert keyword: the node's own type excludes the one this keyword
144
+ // acts on, so it can never fire.
145
+ if (typeof node.type === 'string' || Array.isArray(node.type)) {
146
+ const acts = KEYWORD_TYPES.get(key);
147
+ if (acts && !acts.some((t) => typeAllows(node.type, t))) {
148
+ problems.push({
149
+ path: at,
150
+ message: `"${key}" has no effect here: it applies to ${acts.join('/')} and this node's type is ${JSON.stringify(node.type)}`,
151
+ });
152
+ continue;
153
+ }
154
+ }
155
+ // `required` naming a property that `properties` does not define, on a
156
+ // node that admits no other properties: no document can satisfy it, so
157
+ // the validator built from it rejects everything, silently.
158
+ if (key === 'required' && Array.isArray(value) && node.properties && typeof node.properties === 'object'
159
+ && node.additionalProperties === false && !node.patternProperties) {
160
+ for (const name of value) {
161
+ if (typeof name === 'string' && !Object.prototype.hasOwnProperty.call(node.properties, name)) {
162
+ problems.push({
163
+ path: at,
164
+ message: `required property "${name}" is not defined in properties and additionalProperties is false, so nothing can satisfy this schema`,
165
+ });
166
+ }
167
+ }
168
+ }
116
169
  if (DATA_VALUED.has(key)) continue;
117
170
  if (SCHEMA_MAPS.has(key)) {
118
171
  if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.23.0';
10
+ module.exports = '1.25.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.23.0",
3
+ "version": "1.25.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_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_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_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",
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.23.0",
126
- "@ata-validator/native-darwin-x64": "1.23.0",
127
- "@ata-validator/native-linux-arm64-gnu": "1.23.0",
128
- "@ata-validator/native-linux-arm64-musl": "1.23.0",
129
- "@ata-validator/native-linux-x64-gnu": "1.23.0",
130
- "@ata-validator/native-linux-x64-musl": "1.23.0",
131
- "@ata-validator/native-win32-x64": "1.23.0"
125
+ "@ata-validator/native-darwin-arm64": "1.25.0",
126
+ "@ata-validator/native-darwin-x64": "1.25.0",
127
+ "@ata-validator/native-linux-arm64-gnu": "1.25.0",
128
+ "@ata-validator/native-linux-arm64-musl": "1.25.0",
129
+ "@ata-validator/native-linux-x64-gnu": "1.25.0",
130
+ "@ata-validator/native-linux-x64-musl": "1.25.0",
131
+ "@ata-validator/native-win32-x64": "1.25.0"
132
132
  },
133
133
  "peerDependencies": {
134
134
  "yaml": "^2.0.0"