@blamejs/core 0.18.50 → 0.18.53
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 +121 -0
- package/NOTICE +1 -1
- package/README.md +1 -1
- package/lib/ai-input.js +25 -3
- package/lib/db-query.js +32 -4
- package/lib/gate-contract.js +11 -2
- package/lib/guard-filename.js +32 -6
- package/lib/guard-jwt.js +2 -2
- package/lib/guard-yaml.js +157 -21
- package/lib/json-schema.js +130 -13
- package/lib/mail-bimi.js +279 -6
- package/lib/markup-tokenizer.js +88 -0
- package/lib/parsers/safe-yaml.js +24 -3
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs-pki.cjs +835 -41
- package/lib/yaml-lex.js +533 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/guard-yaml.js
CHANGED
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
*/
|
|
69
69
|
|
|
70
70
|
var codepointClass = require("./codepoint-class");
|
|
71
|
+
var yamlLex = require("./yaml-lex");
|
|
71
72
|
var lazyRequire = require("./lazy-require");
|
|
72
73
|
var gateContract = require("./gate-contract");
|
|
73
74
|
var C = require("./constants");
|
|
@@ -95,11 +96,11 @@ var SAFE_CORE_TAGS = Object.freeze([
|
|
|
95
96
|
"!!binary", "!!timestamp", "!!merge",
|
|
96
97
|
]);
|
|
97
98
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
// There is no table of characters an anchor or alias is allowed to follow any
|
|
100
|
+
// more. Two of them existed, one per sigil, and each was a guess at YAML's
|
|
101
|
+
// grammar written as a character class: they admitted a sigil in the middle of
|
|
102
|
+
// a plain scalar and excluded the compact flow form `{"a":&anchor v}` that YAML
|
|
103
|
+
// permits. Position is decided once, by the shared lexer's mask.
|
|
103
104
|
|
|
104
105
|
// The YAML 1.1 boolean-shaped tokens that make an unquoted scalar change type.
|
|
105
106
|
// `true` and `false` are valid YAML 1.2 booleans and are not flagged; these
|
|
@@ -119,14 +120,17 @@ function _isSpace(cc) {
|
|
|
119
120
|
|
|
120
121
|
// Every `&name` / `*name` in the document, given the sigil and the characters
|
|
121
122
|
// that may precede it. Returns the names, in order.
|
|
122
|
-
|
|
123
|
+
// `text` is the MASK, in which a sigil survives only where it opens a node, so
|
|
124
|
+
// there is no test on the preceding character here at all. That judgement used
|
|
125
|
+
// to be a hand-written list of characters a sigil was allowed to follow, kept
|
|
126
|
+
// separately for anchors and for aliases, and it was wrong in both directions:
|
|
127
|
+
// it admitted a `&` in the middle of a plain scalar, and it had no way to allow
|
|
128
|
+
// `{"a":&anchor v}`, where YAML's JSON compatibility lets a quoted key take its
|
|
129
|
+
// colon with no space after it. One scanner decides position now.
|
|
130
|
+
function _collectSigilNames(text, sigil) {
|
|
123
131
|
var out = [];
|
|
124
132
|
for (var i = 0; i < text.length; i += 1) {
|
|
125
133
|
if (text.charAt(i) !== sigil) continue;
|
|
126
|
-
if (i > 0) {
|
|
127
|
-
var lead = text.charCodeAt(i - 1);
|
|
128
|
-
if (!_isSpace(lead) && leadChars.indexOf(text.charAt(i - 1)) === -1) continue;
|
|
129
|
-
}
|
|
130
134
|
if (!_isNameStart(text.charCodeAt(i + 1))) continue;
|
|
131
135
|
var end = i + 2;
|
|
132
136
|
while (end < text.length && _isNameChar(text.charCodeAt(end))) end += 1;
|
|
@@ -325,9 +329,11 @@ function _scanTags(text) {
|
|
|
325
329
|
var tags = [];
|
|
326
330
|
for (var i = 0; i < text.length; i += 1) {
|
|
327
331
|
if (text.charAt(i) !== "!") continue;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
332
|
+
// No "preceded by whitespace" test. That WAS the original defect, and once
|
|
333
|
+
// the mask decides position it is not merely redundant but harmful: in
|
|
334
|
+
// `{"a":!!python/object x}` the character before the tag is the colon of a
|
|
335
|
+
// JSON-style key, so the test skipped it and a deserialization tag reached
|
|
336
|
+
// both screens unreported. Position is the mask's question now.
|
|
331
337
|
var nameAt = text.charAt(i + 1) === "!" ? i + 2 : i + 1;
|
|
332
338
|
if (!codepointClass.isAsciiLetter(text.charCodeAt(nameAt))) continue;
|
|
333
339
|
var end = nameAt + 1;
|
|
@@ -354,8 +360,22 @@ function _detectIssues(input, opts) {
|
|
|
354
360
|
if (pre.done) return pre.issues;
|
|
355
361
|
var issues = pre.issues;
|
|
356
362
|
|
|
363
|
+
// Sigil scans run against the MASK, not the source. A `!`, `&` or `*` means
|
|
364
|
+
// what it looks like only at a node start, and the previous rule — "after
|
|
365
|
+
// whitespace" — cannot tell a node start from the middle of a scalar. It
|
|
366
|
+
// reported a tag for the bang in a comment, in a quoted string, in a block
|
|
367
|
+
// scalar's shell script, and in ordinary prose: `x: 1 # note !bang` was
|
|
368
|
+
// refused. The mask is index-aligned with the source and the same length, so
|
|
369
|
+
// every location and line number reported below is still the source's.
|
|
370
|
+
//
|
|
371
|
+
// Only the three sigil scans use it. The value-shaped detectors further down
|
|
372
|
+
// (the Norway problem, leading zeros, merge keys) are asking about scalar
|
|
373
|
+
// CONTENT, which is exactly what the mask removes, so they keep reading the
|
|
374
|
+
// source.
|
|
375
|
+
var masked = yamlLex.maskNonStructural(input);
|
|
376
|
+
|
|
357
377
|
// 1. Tag-injection scan.
|
|
358
|
-
var tagHits = _scanTags(
|
|
378
|
+
var tagHits = _scanTags(masked);
|
|
359
379
|
for (var ti = 0; ti < tagHits.length; ti += 1) {
|
|
360
380
|
var t = tagHits[ti];
|
|
361
381
|
if (t.kind === "dangerous") {
|
|
@@ -392,8 +412,8 @@ function _detectIssues(input, opts) {
|
|
|
392
412
|
}
|
|
393
413
|
|
|
394
414
|
// 2. Anchor / alias recursion scan.
|
|
395
|
-
var anchors = _collectSigilNames(
|
|
396
|
-
var aliases = _collectSigilNames(
|
|
415
|
+
var anchors = _collectSigilNames(masked, "&");
|
|
416
|
+
var aliases = _collectSigilNames(masked, "*");
|
|
397
417
|
if (anchors.length > opts.maxAnchors) {
|
|
398
418
|
issues.push({
|
|
399
419
|
kind: "anchor-cap", severity: "high",
|
|
@@ -531,6 +551,24 @@ function _mappingEntryAt(line) {
|
|
|
531
551
|
return null;
|
|
532
552
|
}
|
|
533
553
|
|
|
554
|
+
// The column the line's content starts in.
|
|
555
|
+
function _indentOfLine(line) {
|
|
556
|
+
var i = 0;
|
|
557
|
+
while (i < line.length && _isSpace(line.charCodeAt(i))) i += 1;
|
|
558
|
+
return i;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// The indent of a sequence dash opening this line, or -1 when the line does not
|
|
562
|
+
// open a sequence item. A dash counts only when it stands alone as a token: `-`
|
|
563
|
+
// at end of line, or followed by a space. `-quux` and `-1` are scalars.
|
|
564
|
+
function _sequenceDashIndent(line) {
|
|
565
|
+
var i = 0;
|
|
566
|
+
while (i < line.length && _isSpace(line.charCodeAt(i))) i += 1;
|
|
567
|
+
if (i >= line.length || line.charAt(i) !== "-") return -1;
|
|
568
|
+
if (i + 1 < line.length && !_isSpace(line.charCodeAt(i + 1))) return -1;
|
|
569
|
+
return i;
|
|
570
|
+
}
|
|
571
|
+
|
|
534
572
|
// Is the line blank, or a comment?
|
|
535
573
|
function _isCommentLine(line) {
|
|
536
574
|
var i = 0;
|
|
@@ -544,19 +582,117 @@ function _detectDuplicateKeysYaml(text) {
|
|
|
544
582
|
var dups = Object.create(null);
|
|
545
583
|
var lines = _splitLines(text);
|
|
546
584
|
var indentScopes = Object.create(null);
|
|
585
|
+
// The sequence items currently open, outermost first, each with the column
|
|
586
|
+
// its first key sat in. Every key of an item at its top level is filed under
|
|
587
|
+
// one scope whatever column it is written in, because it is one mapping.
|
|
588
|
+
//
|
|
589
|
+
// A STACK, because sequences nest. Holding the innermost item in a pair of
|
|
590
|
+
// variables let a nested sequence overwrite its parent's scope and never give
|
|
591
|
+
// it back, so a key repeated in the OUTER item after the nested one closed
|
|
592
|
+
// was filed somewhere else and went unreported:
|
|
593
|
+
//
|
|
594
|
+
// - a: 1
|
|
595
|
+
// inner:
|
|
596
|
+
// - x: 1
|
|
597
|
+
// a: 2 <- a duplicate of the first `a`, and it was missed
|
|
598
|
+
//
|
|
599
|
+
// The same shape as the defect this detector reports, one level up: a single
|
|
600
|
+
// slot standing in for something there can be several of.
|
|
601
|
+
var itemStack = [];
|
|
547
602
|
for (var i = 0; i < lines.length; i += 1) {
|
|
548
603
|
var line = lines[i];
|
|
549
604
|
if (line.length === 0 || _isCommentLine(line)) continue;
|
|
605
|
+
// A sequence item OPENS A NEW MAPPING, so it ends the previous item's and
|
|
606
|
+
// everything nested inside it. That is decided from the DASH, before
|
|
607
|
+
// anything else, because a line may be a sequence item and carry no mapping
|
|
608
|
+
// entry of its own:
|
|
609
|
+
//
|
|
610
|
+
// steps:
|
|
611
|
+
// -
|
|
612
|
+
// p: 1
|
|
613
|
+
// -
|
|
614
|
+
// p: 2
|
|
615
|
+
//
|
|
616
|
+
// A dash alone has no `key: value` on it, so a reset that waited for one
|
|
617
|
+
// never ran and the second item's `p` was still read as a duplicate of the
|
|
618
|
+
// first's. The boundary is the dash; whether the item writes its first key
|
|
619
|
+
// beside it or underneath it is a matter of layout.
|
|
620
|
+
var dashAt = _sequenceDashIndent(line);
|
|
621
|
+
// An item is over once the document comes back out to its dash's column or
|
|
622
|
+
// further left. That is true of the next item's own dash as much as of a
|
|
623
|
+
// key belonging to the enclosing mapping, so it is measured here, before
|
|
624
|
+
// this line is classified at all.
|
|
625
|
+
var lineIndent = dashAt >= 0 ? dashAt : _indentOfLine(line);
|
|
626
|
+
while (itemStack.length &&
|
|
627
|
+
itemStack[itemStack.length - 1].dash >= lineIndent) itemStack.pop();
|
|
628
|
+
if (dashAt >= 0) {
|
|
629
|
+
Object.keys(indentScopes).forEach(function (k) {
|
|
630
|
+
if (Number(k) > dashAt) delete indentScopes[k];
|
|
631
|
+
});
|
|
632
|
+
// keyIndent is set by this item's first key, wherever it is written.
|
|
633
|
+
itemStack.push({ dash: dashAt, keyIndent: -1 });
|
|
634
|
+
}
|
|
550
635
|
var entry = _mappingEntryAt(line);
|
|
551
636
|
if (!entry) continue;
|
|
552
637
|
var indent = entry.indent;
|
|
553
638
|
var key = entry.key.trim();
|
|
554
|
-
if (key.charAt(0) === "
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
639
|
+
if (key.charAt(0) === "[" || key.charAt(0) === "{") continue;
|
|
640
|
+
// The key written INLINE with the dash belongs to the item's mapping, and
|
|
641
|
+
// sits at the indent AFTER the dash and its space — so that is the scope it
|
|
642
|
+
// is registered in, alongside the keys written underneath it. It used to be
|
|
643
|
+
// skipped entirely, which meant repeating it went unreported.
|
|
644
|
+
var dash = key.charAt(0) === "-" &&
|
|
645
|
+
(key.length === 1 || _isSpace(key.charCodeAt(1)));
|
|
646
|
+
var scopeAt = indent;
|
|
647
|
+
if (dash) {
|
|
648
|
+
var after = 1;
|
|
649
|
+
while (after < key.length && _isSpace(key.charCodeAt(after))) after += 1;
|
|
650
|
+
key = key.slice(after).trim();
|
|
651
|
+
// `- ` alone, or `- - x`: no inline key of this item's own to register.
|
|
652
|
+
if (!key || key.charAt(0) === "-" || key.charAt(0) === "[" ||
|
|
653
|
+
key.charAt(0) === "{") continue;
|
|
654
|
+
scopeAt = indent + after;
|
|
655
|
+
}
|
|
656
|
+
// The item's mapping is ONE mapping however its keys are laid out, so the
|
|
657
|
+
// scope it is tracked under must not depend on spacing. `- a: 1` puts its
|
|
658
|
+
// inline key at column 4 while the key written under it sits at column 2,
|
|
659
|
+
// and keying on the raw column filed them separately — so a repeat across
|
|
660
|
+
// those two lines went unreported, which is exactly the smuggling shape
|
|
661
|
+
// this detector exists for (one parser reads two keys, another reads one).
|
|
662
|
+
//
|
|
663
|
+
// The item's top level is every key deeper than the dash and no deeper than
|
|
664
|
+
// the first key it saw. Anything past that is genuinely nested and keeps
|
|
665
|
+
// its own column, so `- a: 1` / ` b:` / ` a: 2` is still not a
|
|
666
|
+
// duplicate.
|
|
667
|
+
// Half a column past the dash: a number, so the pruning comparisons below
|
|
668
|
+
// and at the dash keep working unchanged, and one that sorts between the
|
|
669
|
+
// dash and anything nested inside the item.
|
|
670
|
+
var item = itemStack.length ? itemStack[itemStack.length - 1] : null;
|
|
671
|
+
if (item && scopeAt > item.dash) {
|
|
672
|
+
// The item's top level is every key no deeper than the SHALLOWEST key it
|
|
673
|
+
// has shown, and the bound moves down as shallower ones appear. Two
|
|
674
|
+
// failures pinned this from opposite sides:
|
|
675
|
+
//
|
|
676
|
+
// - a: 1 the inline key sits at column 4 because of the extra
|
|
677
|
+
// b: spacing, but the item's mapping is written at 2. If
|
|
678
|
+
// a: 2 4 is taken as the bound, the NESTED `a` at 4 counts
|
|
679
|
+
// as top level and reads as a duplicate.
|
|
680
|
+
//
|
|
681
|
+
// - a: here the inline key IS the bound, at 2. If it sets
|
|
682
|
+
// x: 1 nothing, the first nested key at 4 becomes the bound
|
|
683
|
+
// a: 2 and the nested `a` reads as a duplicate instead.
|
|
684
|
+
//
|
|
685
|
+
// So the inline key establishes the bound and a later, shallower key
|
|
686
|
+
// lowers it. Extra spacing after the indicator is presentation; the
|
|
687
|
+
// shallowest key is the structure.
|
|
688
|
+
if (item.keyIndent === -1 || scopeAt < item.keyIndent) item.keyIndent = scopeAt;
|
|
689
|
+
if (scopeAt <= item.keyIndent) scopeAt = item.dash + 0.5;
|
|
690
|
+
}
|
|
691
|
+
if (!indentScopes[scopeAt]) indentScopes[scopeAt] = Object.create(null);
|
|
692
|
+
if (indentScopes[scopeAt][key]) dups[key] = true;
|
|
693
|
+
else indentScopes[scopeAt][key] = true;
|
|
558
694
|
Object.keys(indentScopes).forEach(function (k) {
|
|
559
|
-
if (Number(k) >
|
|
695
|
+
if (Number(k) > scopeAt) delete indentScopes[k];
|
|
560
696
|
});
|
|
561
697
|
}
|
|
562
698
|
return Object.keys(dups);
|
package/lib/json-schema.js
CHANGED
|
@@ -141,10 +141,52 @@ function _unescapePointerToken(t) { return t.replace(/~1/g, "/").replace(/~0/g,
|
|
|
141
141
|
|
|
142
142
|
// --- registry: indexes every subschema by canonical URI + anchors ---
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
// A ceiling on how many subschema POSITIONS one registry will index, across
|
|
145
|
+
// every document added to it.
|
|
146
|
+
//
|
|
147
|
+
// This is the total-work half of the bound, and it is a different guarantee
|
|
148
|
+
// from a depth ceiling: depth limits how far one path runs, and says nothing
|
|
149
|
+
// about how many paths there are. The walk below cannot use identity tracking
|
|
150
|
+
// instead, because it indexes each node by `base#pointer` and a shared object
|
|
151
|
+
// legitimately has one entry per pointer that reaches it — skipping the second
|
|
152
|
+
// visit would drop a `$ref` target rather than save work. So what gets bounded
|
|
153
|
+
// is the number of entries, which is the thing that actually grows.
|
|
154
|
+
//
|
|
155
|
+
// A schema graph that reaches one object through a branching position —
|
|
156
|
+
// `anyOf: [shared, shared]`, which is what building a schema out of reused
|
|
157
|
+
// JavaScript objects produces, and which needs no `$ref` — has 2^depth pointers
|
|
158
|
+
// over a handful of objects. A cyclic one has no end at all. Neither is refused
|
|
159
|
+
// by a depth cap.
|
|
160
|
+
//
|
|
161
|
+
// Far above any authored schema: the JSON Schema meta-schema is on the order of
|
|
162
|
+
// a hundred positions, and an OpenAPI document of a thousand endpoints is tens
|
|
163
|
+
// of thousands. A schema that reaches this is not one someone wrote out.
|
|
164
|
+
var MAX_REGISTRY_NODES = 100000; // subschema positions, not bytes
|
|
165
|
+
|
|
166
|
+
// How deep a schema may nest before it is refused. This is the OTHER half of
|
|
167
|
+
// the bound, and it is not the node ceiling in disguise: a chain ten thousand
|
|
168
|
+
// levels deep is ten thousand positions, far under that ceiling, and still runs
|
|
169
|
+
// the JavaScript stack out. What comes back then is `RangeError: Maximum call
|
|
170
|
+
// stack size exceeded` — the engine's error, which says nothing about the
|
|
171
|
+
// schema and is not catchable as a JsonSchemaError.
|
|
172
|
+
//
|
|
173
|
+
// It THROWS rather than stopping the walk. A walk that silently returns at the
|
|
174
|
+
// ceiling leaves a node looking examined when its descendants were skipped,
|
|
175
|
+
// which is how a pattern below one escapes screening entirely. Refusing is the
|
|
176
|
+
// only reading that cannot be mistaken for completion.
|
|
177
|
+
//
|
|
178
|
+
// Well above anything authored — the JSON Schema meta-schema nests about a
|
|
179
|
+
// dozen levels and an OpenAPI document a few dozen — and well below where the
|
|
180
|
+
// stack gives out.
|
|
181
|
+
var MAX_SCHEMA_NESTING = 1000; // nesting levels, not bytes
|
|
182
|
+
|
|
183
|
+
function _Registry() {
|
|
184
|
+
this.schemas = {}; this.dynamicAnchors = {}; this.baseByNode = new Map();
|
|
185
|
+
this.nodesIndexed = 0;
|
|
186
|
+
}
|
|
145
187
|
|
|
146
188
|
_Registry.prototype.add = function (schema, baseUri) {
|
|
147
|
-
this._walk(schema, baseUri || "", "");
|
|
189
|
+
this._walk(schema, baseUri || "", "", null, 0);
|
|
148
190
|
// A document retrieved from URI X is addressable by X even when its own
|
|
149
191
|
// $id is a different (canonical) URI — register the retrieval URI too.
|
|
150
192
|
if (baseUri && (_isObject(schema) || typeof schema === "boolean")) {
|
|
@@ -156,10 +198,43 @@ _Registry.prototype.add = function (schema, baseUri) {
|
|
|
156
198
|
// Walk a schema document, registering $id base changes, $anchor and
|
|
157
199
|
// $dynamicAnchor names, and indexing every subschema by its base URI +
|
|
158
200
|
// JSON-pointer fragment.
|
|
159
|
-
_Registry.prototype._walk = function (node, baseUri, pointer) {
|
|
201
|
+
_Registry.prototype._walk = function (node, baseUri, pointer, path, depth) {
|
|
160
202
|
if (!_isObject(node) && typeof node !== "boolean") return;
|
|
203
|
+
if (depth > MAX_SCHEMA_NESTING) {
|
|
204
|
+
throw new JsonSchemaError("json-schema/schema-too-deep",
|
|
205
|
+
"jsonSchema: schema nests deeper than " + MAX_SCHEMA_NESTING +
|
|
206
|
+
" levels — deeper than the walk can index without exhausting the stack");
|
|
207
|
+
}
|
|
208
|
+
this.nodesIndexed += 1;
|
|
209
|
+
if (this.nodesIndexed > MAX_REGISTRY_NODES) {
|
|
210
|
+
throw new JsonSchemaError("json-schema/schema-too-large",
|
|
211
|
+
"jsonSchema: schema indexes more than " + MAX_REGISTRY_NODES +
|
|
212
|
+
" subschema positions — a shared or cyclic schema graph reaches one " +
|
|
213
|
+
"object through many pointers; give the reused subschema an $id and " +
|
|
214
|
+
"reference it with $ref instead of embedding the same object twice");
|
|
215
|
+
}
|
|
161
216
|
if (typeof node === "boolean") { this.schemas[baseUri + "#" + pointer] = node; return; }
|
|
162
217
|
|
|
218
|
+
// A node already on the ACTIVE PATH is a cycle, and a cycle with one
|
|
219
|
+
// reference per level never reaches the ceiling above: it does not branch, so
|
|
220
|
+
// it recurses straight down and exhausts the JavaScript stack while the count
|
|
221
|
+
// is still in the thousands, surfacing as a RangeError from the engine rather
|
|
222
|
+
// than the refusal this validator documents.
|
|
223
|
+
//
|
|
224
|
+
// The test is the path and not a depth cap, because deep is not the same as
|
|
225
|
+
// cyclic: a schema nesting four hundred levels with no cycle and no `$ref`
|
|
226
|
+
// compiles today, and a depth cap would refuse it — under a `ref-loop` code
|
|
227
|
+
// naming a reference chain it does not have. The path set answers the
|
|
228
|
+
// question actually being asked, and leaves depth alone.
|
|
229
|
+
path = path || new Set();
|
|
230
|
+
var isOwnAncestor = path.has(node);
|
|
231
|
+
if (isOwnAncestor) {
|
|
232
|
+
throw new JsonSchemaError("json-schema/ref-loop",
|
|
233
|
+
"jsonSchema: schema contains a cycle — a subschema is its own ancestor, " +
|
|
234
|
+
"so indexing it has no end; break the cycle with $id + $ref");
|
|
235
|
+
}
|
|
236
|
+
path.add(node);
|
|
237
|
+
|
|
163
238
|
var thisBase = baseUri;
|
|
164
239
|
if (typeof node.$id === "string") {
|
|
165
240
|
thisBase = _resolveUri(node.$id, baseUri);
|
|
@@ -191,7 +266,7 @@ _Registry.prototype._walk = function (node, baseUri, pointer) {
|
|
|
191
266
|
// Recurse. Keywords whose values are schemas vs maps-of-schemas vs
|
|
192
267
|
// arrays-of-schemas are walked with the right shape.
|
|
193
268
|
var self = this;
|
|
194
|
-
function child(key, sub, ptr) { self._walk(sub, thisBase, ptr); }
|
|
269
|
+
function child(key, sub, ptr) { self._walk(sub, thisBase, ptr, path, depth + 1); }
|
|
195
270
|
SCHEMA_KEYWORDS.forEach(function (k) {
|
|
196
271
|
if (node[k] !== undefined) child(k, node[k], pointer + "/" + k);
|
|
197
272
|
});
|
|
@@ -207,6 +282,12 @@ _Registry.prototype._walk = function (node, baseUri, pointer) {
|
|
|
207
282
|
node[k].forEach(function (sub, idx) { child(k, sub, pointer + "/" + k + "/" + idx); });
|
|
208
283
|
}
|
|
209
284
|
});
|
|
285
|
+
|
|
286
|
+
// Off the active path on the way out. A node reached again through a SIBLING
|
|
287
|
+
// branch is shared rather than cyclic, and indexing it under its second
|
|
288
|
+
// pointer is the correct thing to do — leaving it in the set would call that
|
|
289
|
+
// a cycle and refuse a schema that merely reuses a subschema.
|
|
290
|
+
path.delete(node);
|
|
210
291
|
};
|
|
211
292
|
|
|
212
293
|
_Registry.prototype.resolve = function (uri) {
|
|
@@ -255,7 +336,13 @@ var SCHEMA_KEYWORDS = ["additionalProperties", "propertyNames", "items",
|
|
|
255
336
|
"unevaluatedProperties"];
|
|
256
337
|
var SCHEMA_MAP_KEYWORDS = ["$defs", "definitions", "properties",
|
|
257
338
|
"patternProperties", "dependentSchemas"];
|
|
258
|
-
|
|
339
|
+
// `items` appears in BOTH lists on purpose. Draft 2020-12 makes it a single
|
|
340
|
+
// schema, and the legacy tuple form makes it an array of them; each branch
|
|
341
|
+
// checks the type it wants, so exactly one of them walks any given `items`.
|
|
342
|
+
// Listed only as a single schema, an array-valued one was dropped at the
|
|
343
|
+
// `_isObject` test at the top of the walk and never indexed at all — so the
|
|
344
|
+
// subschemas inside a tuple were unreachable by `$ref`, and unbudgeted.
|
|
345
|
+
var SCHEMA_ARRAY_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems", "items"];
|
|
259
346
|
|
|
260
347
|
module.exports = _buildModule();
|
|
261
348
|
|
|
@@ -315,25 +402,55 @@ var _SUBSCHEMA_MAP_KEYS = ["$defs", "definitions", "dependentSchemas",
|
|
|
315
402
|
|
|
316
403
|
// Walk the subschema positions for the two keywords that carry a regular
|
|
317
404
|
// expression, and hand each to the same screen the compile site uses.
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
405
|
+
//
|
|
406
|
+
// Every object is visited ONCE, tracked by identity. The depth ceiling below is
|
|
407
|
+
// not a substitute for that and never was: it limits how far a single path runs
|
|
408
|
+
// and says nothing about how many paths there are. A schema graph reaching one
|
|
409
|
+
// object through a branching position — `anyOf: [shared, shared]`, which is what
|
|
410
|
+
// building a schema in JavaScript out of reused subschema objects produces, and
|
|
411
|
+
// which needs no `$ref` — was walked once per path. Twenty-one objects cost
|
|
412
|
+
// 2.6 seconds, doubling per added level, against a cap that allows 256; a cyclic
|
|
413
|
+
// schema never returned. This screen exists so a pattern cannot make the
|
|
414
|
+
// validator hang, and SECURITY.md promises a screen costs the length of its
|
|
415
|
+
// input and never a function of its shape, so a walk that is itself a function
|
|
416
|
+
// of shape contradicts the thing it was added to guarantee.
|
|
417
|
+
//
|
|
418
|
+
// Identity tracking bounds the total work by the number of distinct objects,
|
|
419
|
+
// which is the length of the schema — so the promise holds without a separate
|
|
420
|
+
// node budget.
|
|
421
|
+
//
|
|
422
|
+
// The nesting ceiling here THROWS, and that distinction is the whole point. A
|
|
423
|
+
// ceiling that silently stopped walking would leave a node looking examined
|
|
424
|
+
// when its descendants were skipped, so a later and shallower path would find
|
|
425
|
+
// it already marked and skip it too — and a catastrophic pattern below it would
|
|
426
|
+
// never be screened at all, which is the opposite of what this walk is for.
|
|
427
|
+
// Refusing cannot be mistaken for completion.
|
|
428
|
+
function _screenPatterns(node, seen, depth) {
|
|
429
|
+
if (!_isObject(node)) return;
|
|
430
|
+
if (depth > MAX_SCHEMA_NESTING) {
|
|
431
|
+
throw new JsonSchemaError("json-schema/schema-too-deep",
|
|
432
|
+
"jsonSchema: schema nests deeper than " + MAX_SCHEMA_NESTING +
|
|
433
|
+
" levels — deeper than the pattern screen can walk without exhausting " +
|
|
434
|
+
"the stack");
|
|
435
|
+
}
|
|
436
|
+
seen = seen || new WeakSet();
|
|
437
|
+
if (seen.has(node)) return;
|
|
438
|
+
seen.add(node);
|
|
323
439
|
|
|
324
440
|
if (typeof node.pattern === "string") _compileRegex(node.pattern);
|
|
325
441
|
if (_isObject(node.patternProperties)) {
|
|
326
442
|
Object.keys(node.patternProperties).forEach(function (p) { _compileRegex(p); });
|
|
327
443
|
}
|
|
328
444
|
|
|
329
|
-
|
|
445
|
+
var next = (depth || 0) + 1;
|
|
446
|
+
_SUBSCHEMA_KEYS.forEach(function (k) { _screenPatterns(node[k], seen, next); });
|
|
330
447
|
_SUBSCHEMA_LIST_KEYS.forEach(function (k) {
|
|
331
448
|
if (!Array.isArray(node[k])) return;
|
|
332
|
-
node[k].forEach(function (sub) { _screenPatterns(sub,
|
|
449
|
+
node[k].forEach(function (sub) { _screenPatterns(sub, seen, next); });
|
|
333
450
|
});
|
|
334
451
|
_SUBSCHEMA_MAP_KEYS.forEach(function (k) {
|
|
335
452
|
if (!_isObject(node[k])) return;
|
|
336
|
-
Object.keys(node[k]).forEach(function (name) { _screenPatterns(node[k][name],
|
|
453
|
+
Object.keys(node[k]).forEach(function (name) { _screenPatterns(node[k][name], seen, next); });
|
|
337
454
|
});
|
|
338
455
|
}
|
|
339
456
|
|