@blamejs/core 0.18.51 → 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 +67 -0
- package/lib/guard-filename.js +26 -4
- package/lib/guard-yaml.js +157 -21
- package/lib/parsers/safe-yaml.js +24 -3
- package/lib/yaml-lex.js +533 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,73 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.18.x
|
|
10
10
|
|
|
11
|
+
- v0.18.53 (2026-08-24) — **The YAML screens refused ordinary documents: a list of records, and an exclamation mark in a sentence.** `b.guardYaml.parse` refused any sequence whose items carry more than one key, which is the shape of nearly every list of records anyone writes. Separately, both `b.guardYaml` and `b.parsers.yaml` decided whether a `!`, `&` or `*` opened a tag, anchor or alias by looking at the character in front of it, so they reported one inside a quoted string, inside a block scalar's shell script, in the middle of a value, and inside a comment. `x: 1 # note !bang` was refused. Both screens now read the document's structure, through one scanner shared by the two modules. **Fixed:** *`b.guardYaml` no longer reports a duplicate key for a list of records* — Key uniqueness was tracked per indentation level, and a sequence-item line never reset that tracking. The second item's keys were therefore checked against the first item's, so every key except the one written inline with the dash was reported as a duplicate:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
steps:
|
|
15
|
+
- name: build
|
|
16
|
+
run: make
|
|
17
|
+
- name: test
|
|
18
|
+
run: make check
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That document was refused with `duplicate key "run"`, and it has no duplicate key in it. Under `duplicateKeyPolicy: "reject"` — the strict profile's default — the parse threw.
|
|
22
|
+
|
|
23
|
+
Each item of a sequence is now its own mapping, so a key may appear once per item. Two things that were already true stay true, and both are pinned by tests: a key repeated *within* one item is still a duplicate, and so is a key repeated in an ordinary mapping.
|
|
24
|
+
|
|
25
|
+
The key written inline with the dash was being skipped entirely rather than tracked, so `- name: a` followed by `name: b` inside the same item went unreported. It is now tracked with the rest of its mapping. · *A tag, anchor or alias is recognised where it can actually appear* — `!`, `&` and `*` introduce a tag, an anchor and an alias only at a node start. Both YAML screens decided the question by looking at the preceding character — whitespace meant a sigil, anything else meant not — and that rule cannot tell a node start from the middle of a scalar. Every one of these was refused:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
x: "hello !world" # a bang in a quoted string
|
|
29
|
+
x: hello !world # a bang in the middle of a value
|
|
30
|
+
x: 1 # note !bang # a bang in a comment
|
|
31
|
+
x: |
|
|
32
|
+
echo !boom # a bang in a shell script
|
|
33
|
+
text: fish &chips # an ampersand in prose
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The two modules had separate implementations of the check and separate gaps. `b.parsers.yaml` masked quoted strings but copied comment text through verbatim and had no block-scalar handling, despite a note in it saying both were covered; `b.guardYaml` had none of the three. So a document refused by one was sometimes accepted by the other.
|
|
37
|
+
|
|
38
|
+
Both now use one scanner that tracks where each character sits: inside a quoted scalar, a comment, a block-scalar body, a plain scalar, or in the structure. A sigil is reported only where it opens a node. Scalars that run across several lines are followed rather than read afresh, so neither the continuation of a quoted string nor of an unquoted value is mistaken for structure, and a `!` after the closing quote or the ending comma still is one.
|
|
39
|
+
|
|
40
|
+
**What this changes for a document you already have:** a sigil inside a plain scalar is no longer reported. `text: this ¬anchor` and `x: -&a` used to raise an anchor finding and no longer do. Neither declares an anchor — an anchor is a separate token — so nothing can reference it, and the amplification these findings exist to stop has no path through them. A merge key written without spaces, `<<:*d`, is still refused, by the merge-key screen rather than the alias one.
|
|
41
|
+
|
|
42
|
+
`b.parsers.yaml` continues to refuse every real tag, anchor, alias and directive, and the strict profile of `b.guardYaml` continues to report them. · *The residency gate's raw-SQL timing check no longer depends on how busy the machine is* — The check that a padded raw `UPDATE` costs no extra parse time compared the padded call against an unpadded one and allowed 500ms between them. A difference of two timings carries the noise of both, and on a loaded runner the padded sample alone drifted past that allowance while the statement itself parses in a fraction of a millisecond.
|
|
43
|
+
|
|
44
|
+
It now bounds the padded call on its own, and takes the fastest of fifteen samples rather than three. The defect it guards cost roughly seven seconds against 0.04ms once fixed, so the ceiling sits between the two behaviours with several orders of magnitude to spare rather than tracking the machine. The unpadded baseline is still measured and still printed, because a reader looking at a failure needs to know whether the box was slow or the parse was.
|
|
45
|
+
|
|
46
|
+
- v0.18.52 (2026-08-23) — **A refusal that would not say why, and a currency gate that skipped the one pin able to move underneath it.** `b.guardFilename` refuses a filename carrying NTFS alternate-data-stream syntax whatever `adsPolicy` says, which is correct and was documented, but the refusal never mentioned the option — so a caller who set `"allow"` was left comparing their code against a message that said nothing about the setting they had changed. Separately, the pinned-actions currency gate could only see a `uses:` pinned to a commit SHA, so the one workflow reference pinned to a tag was absent from every run rather than reported, and its `--json` output had a human summary line after the document, so nothing could parse it. **Fixed:** *The NTFS-ADS refusal now says where `adsPolicy: "allow"` applies* — A filename carrying alternate-data-stream syntax (`report.txt:stream`) is refused by `b.guardFilename.sanitize`, `validate` and `gate` regardless of `adsPolicy`. That is deliberate: on Windows the write lands on a hidden stream of the base file rather than on a file anyone can see, so it is one of the shapes a filename guard always refuses. `adsPolicy` still takes `"allow"` because `verifyExtractionPath` honours it, for an operator deliberately extracting stream-suffixed entries into a root they chose.
|
|
47
|
+
|
|
48
|
+
What was missing is that none of this was visible from the call site. The refusal said only that the name contained stream syntax, so setting `"allow"` and watching nothing change read as the option being broken rather than as it being scoped. The message now names the boundary, and the split is pinned by tests at all four entry points.
|
|
49
|
+
|
|
50
|
+
One thing worth knowing if you go looking: a colon is also a Windows reserved character, so `reservedCharPolicy` refuses such a name before the ADS check is reached. Setting `adsPolicy` alone never changes the outcome for a name like `12:30 notes.txt` — `reservedCharPolicy` is the one governing it, and `"strip"` there yields `12_30 notes.txt`.
|
|
51
|
+
|
|
52
|
+
This also records a change that shipped in 0.18.48 without being written down: before it, `sanitize` honoured `adsPolicy: "reject"` and so could be opted out of. It no longer can be, and there is no replacement value, because a stream-suffixed name has no safe repair. · *The pinned-actions currency gate could not see a tag-pinned workflow* — `check-actions-currency.js` matched a `uses:` line only when it was pinned to a 40-character commit SHA. A reference pinned to anything else matched nothing, was collected by nothing, and appeared in the run as neither current nor stale — absent, while the summary counted what it had looked at and read as a clean result.
|
|
53
|
+
|
|
54
|
+
The pin that fell through is the one least able to look after itself. A commit SHA is immutable, so a stale SHA pin becomes visible as soon as upstream cuts a release; a tag can be repointed at new code with no local diff at all. The single reference here pinned to a tag is pinned that way by necessity, because the SLSA provenance generator refuses to run from a commit SHA — so the one exception to the pinning discipline was also the one thing never checked.
|
|
55
|
+
|
|
56
|
+
Tag pins are now collected, version-checked like any other, and reported as tag pins. `--fix` deliberately leaves them alone and says so: there is no old SHA to compare against and none to write, and the review material `--fix` prints is a diff between two SHAs, which is exactly what a tag cannot give.
|
|
57
|
+
|
|
58
|
+
An action can also be pinned by SHA in one workflow and by tag in another, and the two need different answers in different places. The pin type is therefore tracked per reference: the ready-to-paste replacement line is printed only where a SHA reference can take it, each tag reference is marked as one, and `--fix` skips such an action entirely rather than bumping the references that were already current and leaving the stale one behind.
|
|
59
|
+
|
|
60
|
+
The cause of the gap is worth naming because it repeated one level down: the first pattern written to catch these anchored the version at end-of-line, and the line in question carries a trailing comment explaining why it is not SHA-pinned. It matched nothing either.
|
|
61
|
+
|
|
62
|
+
So the gate no longer relies on recognising every shape. It reads the `uses:` scalar first, quoting and all, then classifies what it found; a reference it cannot classify is listed and **fails the run** rather than dropping out of the report. That covers quoted values (`uses: "owner/repo@v1"`), version tags carrying a prerelease or build suffix (`@v2.1.0-rc.1`), a SHA pin whose `# vX.Y.Z` comment is missing, and a reference pinned to a branch. Local actions (`./…`) and `docker://` images are skipped deliberately, because neither has an upstream release to compare against, and the body of a `run: |` block is skipped because those lines are script rather than YAML.
|
|
63
|
+
|
|
64
|
+
Two consequences of reading more shapes are worth calling out. Version comparison now follows semver precedence for prerelease identifiers, so `rc.1` is older than `rc.2` and older than `rc.10`, and a release candidate left pinned after the final release ships reports stale rather than current. And `--fix` now verifies that each rewrite actually landed: it reaches through a closing quote to replace a quoted pin, and if a collected reference does not match the replacement it says so and exits non-zero instead of reporting the action fixed over an unchanged file.
|
|
65
|
+
|
|
66
|
+
The pattern matching is gone. Whether a `uses` token is a key is a question about YAML structure, and a pattern cannot answer it: every attempt to widen one admitted a shape it read wrongly, and every attempt to narrow one dropped a shape it should have read. The collector now scans, tracking the three things that actually decide it — quoting, comments, and flow-collection depth.
|
|
67
|
+
|
|
68
|
+
Every form is read as a result: block style, flow mappings whether `uses` is the first key or the fifth, mappings spanning lines, quoted keys (`"uses":`), quoted values, and values inside nested flow collections. And in the other direction, `uses` inside a quoted string, inside a comment, or inside a `run: |` body is text rather than a key, so `- { run: "echo a, uses: owner/repo@main" }` names no action at all. Both block-scalar indicator orders are handled, since `|2-` and `|-2` are equally valid and misreading one scans a shell script as YAML.
|
|
69
|
+
|
|
70
|
+
Position decides what counts, not the value. A reference is a `uses` under `steps`, or a job's own `uses` for a reusable workflow; anything else spelled `uses` is data. That distinction cannot be made from the value, because `owner/repo@main` is a perfectly ordinary string to put in an `env` block or pass through `with`, and matrix `include` entries carry properties an operator names themselves. Naming the two positions the schema defines is finite; listing every container that is not one is not.
|
|
71
|
+
|
|
72
|
+
What the scan covers is printed on every run, as a `scope` line in the report and a `scope` field in `--json`. The original defect was a form the gate did not read and did not say so; that is now two separate things it cannot do.
|
|
73
|
+
|
|
74
|
+
If you run this gate against your own workflows, expect it to fail on references it could previously not see. That is the point of the change; each one it names is a pin whose currency was never being checked. · *`check-actions-currency.js --json` emits only the JSON document* — The JSON branch wrote the document and then fell through into the summary blocks, which wrote to the same stream regardless of the flag. Every `--json` run therefore ended with a `[actions-currency] …` line after the closing brace, and `JSON.parse` on the stream failed with "Unexpected non-whitespace character after JSON". `--fix --json` was the same shape with more trailing text.
|
|
75
|
+
|
|
76
|
+
Every human-readable line now goes through a writer that is silent under `--json`. Exit codes are unchanged: a machine reader still gets a non-zero exit when something is stale.
|
|
77
|
+
|
|
11
78
|
- v0.18.51 (2026-08-22) — **Five parsers could be made to spend their time on the thing they were parsing.** Three prompt-injection detectors, the BIMI logo parser and the raw-write data-residency gate each ran a pattern whose cost grew with the square or the cube of its input while an ordinary input of the same length cost a millisecond. A 64 KiB prompt took up to 4.5 seconds to classify; a 32 KiB logo took 409 milliseconds to parse; a 4 KB raw UPDATE took 7 seconds to get a verdict from a gate that accepts statements twenty-four times longer. All five now cost what ordinary input of that length costs, and each was checked against a corpus to confirm it still decides what it decided before. Separately, a verified VMC logo was reported as absent when its SVG began with an XML declaration and a DOCTYPE. **Changed:** *`b.guardFilename` no longer repairs a null byte, and five options that accept one value now say so* — 0.18.47 accepted `nullBytePolicy: "strip"` on `b.guardFilename` and removed the null byte from the name. 0.18.48 stopped accepting it. That removal was correct and was not written down, which is the part being fixed here.
|
|
12
79
|
|
|
13
80
|
It was correct because a null byte in a filename is a truncation attack, not a typo: the name the check reads and the name the operating system acts on differ at the byte, so repairing it produces a name nobody validated. The guard refuses with `filename.null-byte` and the message has said `null-byte truncation is never sanitizable` throughout. There is no replacement policy value, because there is no safe repair. A caller that was asking for the strip should refuse the input instead, or rename before validating.
|
package/lib/guard-filename.js
CHANGED
|
@@ -280,6 +280,19 @@ function _hasUncPrefix(name) {
|
|
|
280
280
|
|
|
281
281
|
// An NTFS alternate-data-stream suffix: a colon followed by a run with no
|
|
282
282
|
// further colon and no separator in it, at the very end of the name.
|
|
283
|
+
// One wording for every path that refuses a stream-suffixed name. A caller who
|
|
284
|
+
// set `adsPolicy: "allow"` can arrive at any of them — sanitize in either mode,
|
|
285
|
+
// validate, or gate — and each has to answer the same question, or the option
|
|
286
|
+
// reads as broken from whichever door they happened to use. Saying it in one
|
|
287
|
+
// place and only one is how three of the four ended up silent the first time.
|
|
288
|
+
//
|
|
289
|
+
// `verifyExtractionPath` is deliberately absent: it is the one entry point that
|
|
290
|
+
// HONOURS the option, so it never reaches this text.
|
|
291
|
+
var ADS_SCOPE_NOTE = "always refused here; adsPolicy \"allow\" applies only " +
|
|
292
|
+
"to verifyExtractionPath";
|
|
293
|
+
var ADS_SNIPPET = "NTFS alternate data stream syntax (name:stream) — " + ADS_SCOPE_NOTE;
|
|
294
|
+
var ADS_MESSAGE = "filename contains " + ADS_SNIPPET;
|
|
295
|
+
|
|
283
296
|
function _hasAdsSuffix(name) {
|
|
284
297
|
var colon = name.lastIndexOf(":");
|
|
285
298
|
if (colon === -1 || colon === name.length - 1) return false;
|
|
@@ -469,7 +482,7 @@ function _detectIssues(input, opts) {
|
|
|
469
482
|
issues.push({
|
|
470
483
|
kind: "ntfs-ads", severity: "critical",
|
|
471
484
|
ruleId: "filename.ntfs-ads",
|
|
472
|
-
snippet:
|
|
485
|
+
snippet: ADS_SNIPPET,
|
|
473
486
|
});
|
|
474
487
|
}
|
|
475
488
|
}
|
|
@@ -660,9 +673,18 @@ function _sanitize(input, opts) {
|
|
|
660
673
|
name = "_" + name;
|
|
661
674
|
}
|
|
662
675
|
|
|
663
|
-
// ADS detection.
|
|
676
|
+
// ADS detection. Not gated on `adsPolicy`, and the message says so: a
|
|
677
|
+
// stream-suffixed name is one of the shapes a filename guard always refuses,
|
|
678
|
+
// because on Windows the write lands on a hidden stream of the base file
|
|
679
|
+
// rather than on a file anyone can see. `adsPolicy` still takes "allow", for
|
|
680
|
+
// `verifyExtractionPath` alone, where an operator may be deliberately
|
|
681
|
+
// extracting stream-suffixed entries to a root they chose.
|
|
682
|
+
//
|
|
683
|
+
// The scope is in the message because it is invisible from the call site: a
|
|
684
|
+
// caller who sets "allow" here gets a refusal that, without it, never
|
|
685
|
+
// mentions the setting they changed and so reads as the option being broken.
|
|
664
686
|
if (_hasAdsSuffix(name)) {
|
|
665
|
-
throw _err("filename.ntfs-ads",
|
|
687
|
+
throw _err("filename.ntfs-ads", ADS_MESSAGE);
|
|
666
688
|
}
|
|
667
689
|
|
|
668
690
|
// Length cap.
|
|
@@ -796,7 +818,7 @@ function _sanitizeStripMode(input, opts) {
|
|
|
796
818
|
throw _err("filename.unc", "UNC path syntax");
|
|
797
819
|
}
|
|
798
820
|
if (_hasAdsSuffix(name) && name.charAt(0) !== "/") {
|
|
799
|
-
throw _err("filename.ntfs-ads",
|
|
821
|
+
throw _err("filename.ntfs-ads", ADS_MESSAGE);
|
|
800
822
|
}
|
|
801
823
|
if (Buffer.byteLength(name, "utf8") > opts.maxBytes) {
|
|
802
824
|
throw _err("filename.length", "filename exceeds maxBytes " + opts.maxBytes);
|
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/parsers/safe-yaml.js
CHANGED
|
@@ -60,6 +60,7 @@ var boundedMap = require("../bounded-map");
|
|
|
60
60
|
var numericBounds = require("../numeric-bounds");
|
|
61
61
|
var safeBuffer = require("../safe-buffer");
|
|
62
62
|
var codepointClass = require("../codepoint-class");
|
|
63
|
+
var yamlLex = require("../yaml-lex");
|
|
63
64
|
var { FrameworkError } = require("../framework-error");
|
|
64
65
|
|
|
65
66
|
class SafeYamlError extends FrameworkError {
|
|
@@ -227,7 +228,10 @@ function _findAnchorOrAlias(text) {
|
|
|
227
228
|
var ch = text.charAt(i);
|
|
228
229
|
if (ch !== "&" && ch !== "*") continue;
|
|
229
230
|
var atStart = i === 0;
|
|
230
|
-
|
|
231
|
+
// No test on the preceding character: `text` is the shared mask, in which a
|
|
232
|
+
// sigil survives only where it opens a node. The whitespace test was the
|
|
233
|
+
// original defect and it also had no way to admit `{"a":&anchor v}`, where
|
|
234
|
+
// a quoted key takes its colon with no space after it.
|
|
231
235
|
if (_NAME_HEAD.indexOf(text.charAt(i + 1)) === -1 || text.charAt(i + 1) === "") continue;
|
|
232
236
|
return { index: atStart ? i : i - 1, sigil: ch };
|
|
233
237
|
}
|
|
@@ -239,8 +243,10 @@ function _findTag(text) {
|
|
|
239
243
|
for (var i = 0; i < text.length; i += 1) {
|
|
240
244
|
if (text.charAt(i) !== "!") continue;
|
|
241
245
|
var atStart = i === 0;
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
// Same as above: position is the mask's judgement. Requiring whitespace or
|
|
247
|
+
// a dash before the tag let `{"a":!!python/object x}` through, because the
|
|
248
|
+
// character in front of it is the colon of a JSON-style key — and a
|
|
249
|
+
// deserialization tag is the one thing this ban exists for.
|
|
244
250
|
// The pattern is greedy, so it takes two `!` when both are present.
|
|
245
251
|
var after = text.charAt(i + 1) === "!" ? text.charAt(i + 2) : text.charAt(i + 1);
|
|
246
252
|
if (after.length !== 1) continue;
|
|
@@ -1194,6 +1200,21 @@ function _preValidate(input) {
|
|
|
1194
1200
|
advance();
|
|
1195
1201
|
}
|
|
1196
1202
|
|
|
1203
|
+
// The masking above is kept for ONE thing: it is the scan that notices an
|
|
1204
|
+
// unterminated quote, and that diagnosis is this parser's to give. What it
|
|
1205
|
+
// produced was not a usable screen. Its own header promised that block-scalar
|
|
1206
|
+
// bodies were masked and there was no block-scalar handling in it at all, and
|
|
1207
|
+
// comment text was copied through verbatim under a comment saying so — which
|
|
1208
|
+
// is why `x: 1 # note !bang` and `x: |` / ` echo !boom` were both refused as
|
|
1209
|
+
// tags. A plain scalar was not understood either, so `x: hello !world` was
|
|
1210
|
+
// refused for a bang sitting in the middle of a value.
|
|
1211
|
+
//
|
|
1212
|
+
// The banned-construct scan therefore runs against the shared lexer's mask.
|
|
1213
|
+
// `guard-yaml` had the same three gaps in a separate implementation, and one
|
|
1214
|
+
// scan answering "which region is this character in" is what stops the two
|
|
1215
|
+
// drifting apart again.
|
|
1216
|
+
safe = yamlLex.maskNonStructural(input);
|
|
1217
|
+
|
|
1197
1218
|
// Now scan `safe` for banned constructs.
|
|
1198
1219
|
// Banned tokens (must be at line-start or after whitespace, not in keys):
|
|
1199
1220
|
// &name anchor
|
package/lib/yaml-lex.js
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// Where in a YAML document a character actually SITS.
|
|
6
|
+
//
|
|
7
|
+
// Two modules were answering that question separately and getting different
|
|
8
|
+
// wrong answers. `guard-yaml` decided a `!` opened a tag if it followed
|
|
9
|
+
// whitespace, so it reported one inside a quoted scalar, inside a block-scalar
|
|
10
|
+
// body, and inside a comment. `parsers/safe-yaml` masked quoted scalars but
|
|
11
|
+
// passed comment text through verbatim and had no block-scalar handling at all,
|
|
12
|
+
// despite its own note promising both. Between them every ordinary document
|
|
13
|
+
// carrying an exclamation mark in prose was refused, by one module or the other.
|
|
14
|
+
//
|
|
15
|
+
// The question is not "what precedes this character" — it is "what region is
|
|
16
|
+
// this character in", and no amount of looking at the previous byte answers it.
|
|
17
|
+
// Region is a property of everything before, so it takes a scan. This module is
|
|
18
|
+
// that scan, written once, so the two callers cannot drift apart again.
|
|
19
|
+
//
|
|
20
|
+
// `maskNonStructural` returns a string the SAME LENGTH as its input, with every
|
|
21
|
+
// non-structural region replaced by spaces and newlines preserved. Callers keep
|
|
22
|
+
// their existing sigil searches and run them against the mask instead of the
|
|
23
|
+
// source: an index into one is an index into the other, so reported locations
|
|
24
|
+
// and line numbers stay true.
|
|
25
|
+
//
|
|
26
|
+
// What survives the mask, because it is what the callers are looking for:
|
|
27
|
+
// structural punctuation, node properties (`!tag`, `&anchor`, `*alias`),
|
|
28
|
+
// directive lines, and document markers. What is masked is scalar content in
|
|
29
|
+
// every form it takes: quoted, plain, and block.
|
|
30
|
+
//
|
|
31
|
+
// The mask is deliberately readier to KEEP than to hide. Masking something
|
|
32
|
+
// structural is the dangerous direction: it does not refuse a good document, it
|
|
33
|
+
// hides a real tag inside a bad one and hands the callers something they then
|
|
34
|
+
// call clean. Every case below that looked like a tidy simplification and was
|
|
35
|
+
// not — blanking the rest of a line, measuring a block body from the wrong
|
|
36
|
+
// column — failed in exactly that direction.
|
|
37
|
+
|
|
38
|
+
// No line-splitting helper is used here on purpose: `codepointClass.splitLines`
|
|
39
|
+
// strips the carriage return of a CRLF pair, and this function's whole contract
|
|
40
|
+
// is that its output is the same length as its input. See the split below.
|
|
41
|
+
|
|
42
|
+
// Deliberately not a regex, in either sense. The guard and safe families forbid
|
|
43
|
+
// them, and a scanner is what this file exists to be.
|
|
44
|
+
function _isSpace(code) { return code === 0x20 || code === 0x09; }
|
|
45
|
+
|
|
46
|
+
function _isPlainSpace(ch) { return ch === " " || ch === "\t"; }
|
|
47
|
+
|
|
48
|
+
// A node property is a tag, an anchor, or an alias. They chain, so `!tag &a`
|
|
49
|
+
// and `&a !tag` are both a single node's properties and the scalar begins after
|
|
50
|
+
// the last of them.
|
|
51
|
+
function _isPropertySigil(ch) { return ch === "!" || ch === "&" || ch === "*"; }
|
|
52
|
+
|
|
53
|
+
// A block-scalar header is `|` or `>`, optionally followed by the chomping and
|
|
54
|
+
// indentation indicators in EITHER order (`|2-` and `|-2` are equally valid),
|
|
55
|
+
// and then nothing but whitespace or a comment. Misreading one scans a shell
|
|
56
|
+
// script as if it were YAML.
|
|
57
|
+
// Returns `{ end, declared }` or null. `declared` is the indentation indicator's
|
|
58
|
+
// digit when the header carries one, or 0. That digit is not decoration: it
|
|
59
|
+
// DECLARES the body's indentation relative to the parent node, so a body whose
|
|
60
|
+
// first line happens to be indented further does not move it. Detecting from
|
|
61
|
+
// the first line in that case ends the block early and hands the rest of a
|
|
62
|
+
// scalar to the structural scan.
|
|
63
|
+
function _blockHeaderEnd(line, at) {
|
|
64
|
+
var ch = line.charAt(at);
|
|
65
|
+
if (ch !== "|" && ch !== ">") return null;
|
|
66
|
+
var i = at + 1;
|
|
67
|
+
var declared = 0, sawChomp = false;
|
|
68
|
+
while (i < line.length) {
|
|
69
|
+
var c = line.charAt(i);
|
|
70
|
+
if (c >= "1" && c <= "9" && !declared) { declared = Number(c); i += 1; continue; }
|
|
71
|
+
if ((c === "-" || c === "+") && !sawChomp) { sawChomp = true; i += 1; continue; }
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
// Only a header if the rest of the line is blank or a comment; `x: |foo` is
|
|
75
|
+
// an ordinary plain scalar that happens to start with a bar.
|
|
76
|
+
var j = i;
|
|
77
|
+
while (j < line.length && _isPlainSpace(line.charAt(j))) j += 1;
|
|
78
|
+
if (j < line.length && line.charAt(j) !== "#") return null;
|
|
79
|
+
return { end: i, declared: declared };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The document markers and directive lines a caller still needs to see. A
|
|
83
|
+
// directive is only a directive at column zero, and `---` / `...` only at the
|
|
84
|
+
// start of a line.
|
|
85
|
+
function _isVerbatimLine(line, indent) {
|
|
86
|
+
if (line.charAt(0) === "%") return true;
|
|
87
|
+
// Column ZERO only. YAML puts document markers there and nowhere else, so an
|
|
88
|
+
// indented `---` is scalar content — `- hello` / ` ---` / ` !world` is one
|
|
89
|
+
// plain scalar. Matching it wherever it appeared ended the document, threw
|
|
90
|
+
// away the scalar state, and left the continuation to be read as structure.
|
|
91
|
+
if (indent !== 0) return false;
|
|
92
|
+
if (line.indexOf("---") === 0 &&
|
|
93
|
+
(line.length === 3 || _isPlainSpace(line.charAt(3)))) return true;
|
|
94
|
+
if (line.indexOf("...") === 0 &&
|
|
95
|
+
(line.length === 3 || _isPlainSpace(line.charAt(3)))) return true;
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function maskNonStructural(text) {
|
|
100
|
+
var src = String(text == null ? "" : text);
|
|
101
|
+
// Split on the newline ALONE, keeping any `\r` with the line it terminates.
|
|
102
|
+
// `codepointClass.splitLines` strips it, and rejoining those with "\n" drops
|
|
103
|
+
// one character per CRLF line — which would shift every index after the first
|
|
104
|
+
// such line and silently break the alignment this whole function exists to
|
|
105
|
+
// provide. A mask that is not the same length as its source is worse than no
|
|
106
|
+
// mask, because the locations it reports are confidently wrong.
|
|
107
|
+
var lines = src.split("\n");
|
|
108
|
+
var out = [];
|
|
109
|
+
// A block scalar's body indentation is DETECTED from its first content line,
|
|
110
|
+
// which is what YAML specifies and the only thing that works. Two fixed
|
|
111
|
+
// columns were tried and each failed the other's case: measuring from the
|
|
112
|
+
// key's column masks nothing when extra spacing after a dash puts the body in
|
|
113
|
+
// that same column, and measuring from a canonical column swallows a sibling
|
|
114
|
+
// key written between the two. With extra spacing the body can sit anywhere
|
|
115
|
+
// past the dash, so only the body itself says where it starts.
|
|
116
|
+
//
|
|
117
|
+
// `blockOwner` is the column the body must beat for the block to have one at
|
|
118
|
+
// all; `blockBody` is the detected indentation, -1 until the first content
|
|
119
|
+
// line sets it. Both -1 when no block is open.
|
|
120
|
+
var blockOwner = -1;
|
|
121
|
+
var blockBody = -1;
|
|
122
|
+
// A quoted scalar may span lines, so the quote that opened one carries across
|
|
123
|
+
// until it closes. Scanning each line independently would read the
|
|
124
|
+
// CONTINUATION as structure, which puts back the exact false positive this
|
|
125
|
+
// module exists to remove: `x: "hello` / ` !world"` would name a tag on the
|
|
126
|
+
// second line.
|
|
127
|
+
var openQuote = null;
|
|
128
|
+
// A flow collection may also span lines, so its depth carries across too. It
|
|
129
|
+
// decides where a plain scalar ends, and resetting it per line would end one
|
|
130
|
+
// at the wrong place inside a multi-line `[ ... ]`.
|
|
131
|
+
var flowDepth = 0;
|
|
132
|
+
// A PLAIN scalar spans lines as well — that is how a long description gets
|
|
133
|
+
// written without quotes — and its continuation lines are indented further
|
|
134
|
+
// than the node that opened it. Reading one as a fresh line puts the false
|
|
135
|
+
// positive back a fourth way: in
|
|
136
|
+
//
|
|
137
|
+
// x: hello
|
|
138
|
+
// !world
|
|
139
|
+
//
|
|
140
|
+
// the value is `hello !world` and the bang introduces nothing.
|
|
141
|
+
//
|
|
142
|
+
// -1 when no plain scalar is open. Otherwise the indent of the node that
|
|
143
|
+
// opened it, which a continuation must beat. This is set ONLY when a line
|
|
144
|
+
// actually read plain-scalar content, so `x:` with its value underneath does
|
|
145
|
+
// not arm it and a nested mapping is still read as structure.
|
|
146
|
+
var plainOpen = -1;
|
|
147
|
+
// ...and whether that scalar is a sequence item's own, which decides whether
|
|
148
|
+
// a continuation at the SAME column counts.
|
|
149
|
+
var plainFromDash = false;
|
|
150
|
+
|
|
151
|
+
for (var li = 0; li < lines.length; li += 1) {
|
|
152
|
+
var raw = lines[li];
|
|
153
|
+
// The carriage return of a CRLF pair is a line TERMINATOR, not content, so
|
|
154
|
+
// it is held aside and put back verbatim. Letting it into the scan would
|
|
155
|
+
// have it read as the first character of a plain scalar and masked to a
|
|
156
|
+
// space, which changes the bytes of a document the mask is meant to mirror.
|
|
157
|
+
var cr = raw.length && raw.charAt(raw.length - 1) === "\r";
|
|
158
|
+
var line = cr ? raw.slice(0, raw.length - 1) : raw;
|
|
159
|
+
var indent = 0;
|
|
160
|
+
while (indent < line.length && _isSpace(line.charCodeAt(indent))) indent += 1;
|
|
161
|
+
var blank = indent === line.length;
|
|
162
|
+
|
|
163
|
+
// A quoted scalar carried over from an earlier line owns this one until it
|
|
164
|
+
// closes, and nothing before that point is structure — not a `---` that
|
|
165
|
+
// looks like a document marker, and not a `%` in column zero that looks
|
|
166
|
+
// like a directive. So this is answered before either of those.
|
|
167
|
+
//
|
|
168
|
+
// What follows the closing quote on that line IS structure again, and the
|
|
169
|
+
// scan resumes there rather than blanking it. Blanking hid a real tag: in
|
|
170
|
+
// `x: ["first` / ` second", !tag value]` the comma and the tag after the
|
|
171
|
+
// scalar belong to the collection, and masking them takes a tagged document
|
|
172
|
+
// and hands both screens something they call clean.
|
|
173
|
+
var resumeAt = -1;
|
|
174
|
+
var resumePrefix = "";
|
|
175
|
+
if (openQuote !== null) {
|
|
176
|
+
var cont = _maskQuotedBody(line, 0, openQuote);
|
|
177
|
+
if (!cont.closed) {
|
|
178
|
+
out.push(cont.masked + (cr ? "\r" : ""));
|
|
179
|
+
continue; // the whole line is body
|
|
180
|
+
}
|
|
181
|
+
openQuote = null;
|
|
182
|
+
resumeAt = cont.end;
|
|
183
|
+
resumePrefix = cont.masked;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (resumeAt === -1) {
|
|
187
|
+
if (blockOwner >= 0) {
|
|
188
|
+
if (blank) { out.push(_blanked(line) + (cr ? "\r" : "")); continue; }
|
|
189
|
+
if (blockBody === -1) {
|
|
190
|
+
// The first content line sets the body's indentation — provided it is
|
|
191
|
+
// past the owner at all. If it is not, the block has an empty body and
|
|
192
|
+
// this line is structure.
|
|
193
|
+
if (indent > blockOwner) {
|
|
194
|
+
blockBody = indent;
|
|
195
|
+
out.push(_blanked(line) + (cr ? "\r" : ""));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
blockOwner = -1;
|
|
199
|
+
} else if (indent >= blockBody) {
|
|
200
|
+
out.push(_blanked(line) + (cr ? "\r" : ""));
|
|
201
|
+
continue;
|
|
202
|
+
} else {
|
|
203
|
+
blockOwner = -1;
|
|
204
|
+
blockBody = -1; // the body ended here
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (blank) { out.push(raw); continue; }
|
|
208
|
+
// A directive or a document marker is structure the callers still need to
|
|
209
|
+
// see, but only the marker itself is. A comment may follow one on the same
|
|
210
|
+
// line, and its text is no more YAML there than anywhere else — passing
|
|
211
|
+
// the whole line through left `--- # note !bang` naming a tag, which is
|
|
212
|
+
// the very class this module removes, surviving in the one branch that
|
|
213
|
+
// skipped the scan.
|
|
214
|
+
if (_isVerbatimLine(line, indent)) {
|
|
215
|
+
out.push(_maskTrailingComment(line) + (cr ? "\r" : ""));
|
|
216
|
+
// A document marker ends the document, so EVERY piece of carried state
|
|
217
|
+
// ends with it. Resetting only the flow depth left a scalar open across
|
|
218
|
+
// the boundary, and with the sequence-item rule that meant
|
|
219
|
+
// `- hello` / `---` / ` !tag v` read the new document's first line as
|
|
220
|
+
// a continuation of the old document's last item — masking a real tag.
|
|
221
|
+
// A marker is the one place where "carry this across lines" is always
|
|
222
|
+
// wrong, so the reset is total rather than itemised.
|
|
223
|
+
flowDepth = 0;
|
|
224
|
+
plainOpen = -1;
|
|
225
|
+
plainFromDash = false;
|
|
226
|
+
blockOwner = -1;
|
|
227
|
+
blockBody = -1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Resuming after a closed continuation starts where the quote ended, with
|
|
233
|
+
// that scalar already behind us; otherwise the line's own content begins.
|
|
234
|
+
var masked = resumeAt === -1 ? line.slice(0, indent) : resumePrefix;
|
|
235
|
+
var i = resumeAt === -1 ? indent : resumeAt;
|
|
236
|
+
// A node may begin here: at the start of the line's content, and again
|
|
237
|
+
// after every structural token that introduces one.
|
|
238
|
+
//
|
|
239
|
+
// Two things mean it does NOT begin here. A resumed line has just finished
|
|
240
|
+
// reading a quoted scalar. And a line continuing a PLAIN scalar opened
|
|
241
|
+
// earlier is more of that scalar, so its first character is content:
|
|
242
|
+
//
|
|
243
|
+
// x: hello
|
|
244
|
+
// !world
|
|
245
|
+
//
|
|
246
|
+
// The continuation is scanned rather than blanked, because inside a flow
|
|
247
|
+
// collection it may still end at a `,` or a `]` that belongs to the
|
|
248
|
+
// collection — `x: [hello` / ` !world]` closes a sequence on its second
|
|
249
|
+
// line. Closing the node position is enough to make the leading sigil fall
|
|
250
|
+
// into the plain-scalar branch, and it costs no special case.
|
|
251
|
+
// A scalar that IS a sequence item has its continuation aligned with the
|
|
252
|
+
// item's content column rather than deeper than it — `- hello` then
|
|
253
|
+
// ` !world` is the scalar `hello !world`, and both sit at column 2. So
|
|
254
|
+
// equality continues that one, while a mapping entry's value still needs a
|
|
255
|
+
// strictly deeper line, where equality would be a sibling key.
|
|
256
|
+
var continuesPlain = resumeAt === -1 && plainOpen >= 0 && !blank &&
|
|
257
|
+
(plainFromDash ? indent >= plainOpen : indent > plainOpen);
|
|
258
|
+
var atNodeStart = resumeAt === -1 && !continuesPlain;
|
|
259
|
+
// Where the NODE on this line begins, which is the line's indent until a
|
|
260
|
+
// sequence dash moves it along. A block scalar's body is measured from
|
|
261
|
+
// here, not from the leading whitespace.
|
|
262
|
+
var nodeIndent = indent;
|
|
263
|
+
// The column of the innermost sequence dash read on this line, or -1. A
|
|
264
|
+
// block scalar standing where that dash's own node goes belongs to the
|
|
265
|
+
// ITEM, so this is what bounds its body.
|
|
266
|
+
var dashIndent = -1;
|
|
267
|
+
// Was the token just read a JSON-LIKE key? YAML's JSON compatibility lets
|
|
268
|
+
// one take its value colon with no space after it, and there are two kinds:
|
|
269
|
+
// a quoted scalar, and a flow collection. Both end this flag set.
|
|
270
|
+
var prevWasJsonKey = false;
|
|
271
|
+
// Did the line finish inside a plain scalar? Set by the plain-scalar branch
|
|
272
|
+
// when it runs to the end of the line, and false the moment anything else
|
|
273
|
+
// is read after it.
|
|
274
|
+
var sawPlainToEol = false;
|
|
275
|
+
// The column the last plain scalar on this line started in.
|
|
276
|
+
var plainStartCol = -1;
|
|
277
|
+
|
|
278
|
+
while (i < line.length) {
|
|
279
|
+
var ch = line.charAt(i);
|
|
280
|
+
// Cleared here and re-set only by the quoted-scalar branch, so no branch
|
|
281
|
+
// can leave it true by forgetting to. The colon rule reads the captured
|
|
282
|
+
// copy rather than the live flag.
|
|
283
|
+
var jsonKeyBefore = prevWasJsonKey;
|
|
284
|
+
prevWasJsonKey = false;
|
|
285
|
+
|
|
286
|
+
// A comment opens on `#` at the start of the content or after whitespace,
|
|
287
|
+
// and runs to the end of the line. Its text is not YAML, whatever it
|
|
288
|
+
// says: `x: 1 # note !bang` names no tag.
|
|
289
|
+
if (ch === "#" && (i === indent || _isPlainSpace(line.charAt(i - 1)))) {
|
|
290
|
+
masked += _blanked(line.slice(i));
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Whitespace carries the quoted-key flag rather than clearing it. YAML
|
|
295
|
+
// allows separation between a JSON-style key and its adjacent value, so
|
|
296
|
+
// `{"a" :!!python/object x}` is as valid as `{"a":!!python/object x}` —
|
|
297
|
+
// and clearing here left the second form closed and the first one open,
|
|
298
|
+
// which is a deserialization tag hidden behind one space.
|
|
299
|
+
if (_isPlainSpace(ch)) {
|
|
300
|
+
masked += ch; i += 1; prevWasJsonKey = jsonKeyBefore; continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Structural punctuation stays visible, and each of these opens a node
|
|
304
|
+
// position after it.
|
|
305
|
+
if (ch === "{" || ch === "[") {
|
|
306
|
+
masked += ch; i += 1; flowDepth += 1; atNodeStart = true; continue;
|
|
307
|
+
}
|
|
308
|
+
if (ch === "}" || ch === "]") {
|
|
309
|
+
masked += ch; i += 1; if (flowDepth > 0) flowDepth -= 1;
|
|
310
|
+
atNodeStart = false;
|
|
311
|
+
// A closing delimiter ends a JSON-LIKE key just as a closing quote
|
|
312
|
+
// does, and YAML lets that kind of key take its colon with no space
|
|
313
|
+
// after it. `{{a: b}:!!python/object x}` is valid, and reading the `:`
|
|
314
|
+
// as ordinary text left the tag masked — the same bypass as the quoted
|
|
315
|
+
// key, through the other production for the same rule.
|
|
316
|
+
prevWasJsonKey = true;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (ch === ",") { masked += ch; i += 1; atNodeStart = true; continue; }
|
|
320
|
+
if (ch === "-" && (i + 1 >= line.length || _isPlainSpace(line.charAt(i + 1)))) {
|
|
321
|
+
masked += ch; i += 1; atNodeStart = true;
|
|
322
|
+
// A sequence entry written inline starts a node PAST the dash, and any
|
|
323
|
+
// block scalar it opens is measured from there rather than from the
|
|
324
|
+
// line's leading whitespace. Getting this wrong is not a false refusal
|
|
325
|
+
// but a false ACCEPT: in
|
|
326
|
+
//
|
|
327
|
+
// - key: |
|
|
328
|
+
// body
|
|
329
|
+
// evil: !tag x
|
|
330
|
+
//
|
|
331
|
+
// `evil` is a sibling of `key`, and measuring the body against the
|
|
332
|
+
// dash's indent of zero swallows it — masking a real tag and handing
|
|
333
|
+
// both screens a document they then call clean.
|
|
334
|
+
var afterDash = i;
|
|
335
|
+
while (afterDash < line.length && _isPlainSpace(line.charAt(afterDash))) afterDash += 1;
|
|
336
|
+
if (afterDash < line.length) nodeIndent = afterDash;
|
|
337
|
+
// ...unless the item's node IS a block scalar, with no mapping in
|
|
338
|
+
// between: `- |` then ` !hello`. There the body starts at the same
|
|
339
|
+
// column the `|` sits in, so measuring from that column masks nothing
|
|
340
|
+
// and the scalar's first line reads as a tag. The owner of the block is
|
|
341
|
+
// the sequence ITEM, so the dash's column is what bounds it.
|
|
342
|
+
dashIndent = i - 1;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (ch === "?" && (i + 1 >= line.length || _isPlainSpace(line.charAt(i + 1)))) {
|
|
346
|
+
masked += ch; i += 1; atNodeStart = true; continue;
|
|
347
|
+
}
|
|
348
|
+
// A colon separates a key from its value when whitespace follows, when a
|
|
349
|
+
// flow delimiter does — and, inside a flow collection, when the key was
|
|
350
|
+
// QUOTED. That last form is YAML's JSON compatibility: `{"a":value}` is
|
|
351
|
+
// valid and needs no space, so requiring one left the colon unread, the
|
|
352
|
+
// node position closed, and the value masked as though it were more of
|
|
353
|
+
// the key's scalar. `{"a":!!python/object x}` therefore reached both
|
|
354
|
+
// screens with its tag hidden — a deserialization tag, which is the
|
|
355
|
+
// single most dangerous thing this scan exists to surface.
|
|
356
|
+
if (ch === ":" &&
|
|
357
|
+
(i + 1 >= line.length || _isPlainSpace(line.charAt(i + 1)) ||
|
|
358
|
+
(flowDepth > 0 && ",}]".indexOf(line.charAt(i + 1)) !== -1) ||
|
|
359
|
+
(flowDepth > 0 && jsonKeyBefore))) {
|
|
360
|
+
masked += ch; i += 1; atNodeStart = true; continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// A node's properties survive: they are exactly what the callers scan for,
|
|
364
|
+
// and they are the ONLY place a `!`, `&` or `*` means what it looks like.
|
|
365
|
+
if (atNodeStart && _isPropertySigil(ch)) {
|
|
366
|
+
var pEnd = i + 1;
|
|
367
|
+
if (ch === "!" && line.charAt(pEnd) === "!") pEnd += 1;
|
|
368
|
+
while (pEnd < line.length && !_isPlainSpace(line.charAt(pEnd)) &&
|
|
369
|
+
(flowDepth === 0 || ",}]".indexOf(line.charAt(pEnd)) === -1)) pEnd += 1;
|
|
370
|
+
masked += line.slice(i, pEnd);
|
|
371
|
+
i = pEnd;
|
|
372
|
+
continue; // properties chain
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// A quoted scalar: the quotes stay, the body goes. The body is content by
|
|
376
|
+
// construction, so nothing in it is ever structure.
|
|
377
|
+
if (ch === '"' || ch === "'") {
|
|
378
|
+
var qr = _maskQuotedBody(line, i + 1, ch);
|
|
379
|
+
masked += ch + qr.masked;
|
|
380
|
+
i = qr.end;
|
|
381
|
+
// Not closed on this line means the scalar CONTINUES, which YAML allows
|
|
382
|
+
// and which the line-at-a-time reading would otherwise lose. The quote
|
|
383
|
+
// is remembered so the next line is read as its body rather than as
|
|
384
|
+
// structure.
|
|
385
|
+
if (!qr.closed) { openQuote = ch; break; }
|
|
386
|
+
atNodeStart = false;
|
|
387
|
+
// Remembered for the colon rule above: a quoted scalar is one of the
|
|
388
|
+
// two JSON-like key forms that may take its colon with no space between.
|
|
389
|
+
prevWasJsonKey = true;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// A block-scalar header ends the line's structure; the body is masked by
|
|
394
|
+
// the outer loop.
|
|
395
|
+
var bHead = _blockHeaderEnd(line, i);
|
|
396
|
+
if (bHead) {
|
|
397
|
+
masked += line.slice(i, bHead.end);
|
|
398
|
+
// The body must beat the DASH when there is one, and the line's node
|
|
399
|
+
// otherwise. That is only a floor: where the body actually starts is
|
|
400
|
+
// detected from its first content line, because with extra spacing
|
|
401
|
+
// after a dash it can sit anywhere past that floor.
|
|
402
|
+
// The owner is the node the block hangs off: the ITEM when the header
|
|
403
|
+
// stands where the item's own node goes (`- |`), and the MAPPING ENTRY
|
|
404
|
+
// when it follows a key (`- key: |`). Using the dash for the second
|
|
405
|
+
// case made a sibling written straight after an empty block look like
|
|
406
|
+
// its first body line — `- key: |` / ` evil: !tag x` masked the tag.
|
|
407
|
+
//
|
|
408
|
+
// It also settles the padded case the honest way. `- key: |` puts the
|
|
409
|
+
// mapping at column 4, so a body at column 4 is not content by YAML's
|
|
410
|
+
// own indentation rule; that document is malformed, and reading its
|
|
411
|
+
// next line as structure surfaces whatever it says rather than hiding
|
|
412
|
+
// it. Where a shape is ambiguous, the reading that keeps a sibling
|
|
413
|
+
// VISIBLE is the one to take — the other hides real tags.
|
|
414
|
+
blockOwner = (dashIndent >= 0 && i === nodeIndent) ? dashIndent : nodeIndent;
|
|
415
|
+
// ...unless the header DECLARED it. `|2` fixes the body's indentation
|
|
416
|
+
// relative to the parent, so a first line indented further does not
|
|
417
|
+
// move it, and a later line back at the declared column is still body.
|
|
418
|
+
// Detecting in that case ends the block early and hands the rest of the
|
|
419
|
+
// scalar to the structural scan.
|
|
420
|
+
//
|
|
421
|
+
// Counted from that same owner: for `- key: |2` the indicator is
|
|
422
|
+
// relative to the mapping entry, not the dash, and counting from the
|
|
423
|
+
// dash puts the body two columns too far left and swallows the entry's
|
|
424
|
+
// siblings.
|
|
425
|
+
blockBody = bHead.declared ? blockOwner + bHead.declared : -1;
|
|
426
|
+
i = bHead.end;
|
|
427
|
+
atNodeStart = false;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Anything else begins a PLAIN scalar, and everything to the end of it is
|
|
432
|
+
// content. This is the case the previous implementations had no notion of:
|
|
433
|
+
// in `x: hello !world` the `!world` sits inside a scalar that started at
|
|
434
|
+
// `hello`, so it names no tag, and only knowing a scalar had already begun
|
|
435
|
+
// can tell you that.
|
|
436
|
+
var s = i;
|
|
437
|
+
while (s < line.length) {
|
|
438
|
+
var c3 = line.charAt(s);
|
|
439
|
+
if (c3 === "#" && _isPlainSpace(line.charAt(s - 1))) break;
|
|
440
|
+
if (c3 === ":" &&
|
|
441
|
+
(s + 1 >= line.length || _isPlainSpace(line.charAt(s + 1)) ||
|
|
442
|
+
(flowDepth > 0 && ",}]".indexOf(line.charAt(s + 1)) !== -1))) break;
|
|
443
|
+
if (flowDepth > 0 && ",}][{".indexOf(c3) !== -1) break;
|
|
444
|
+
s += 1;
|
|
445
|
+
}
|
|
446
|
+
masked += _blanked(line.slice(i, s));
|
|
447
|
+
// Where this scalar BEGAN. A dash earlier on the line is not enough to
|
|
448
|
+
// make the scalar the item's own: in `- key: hello` the scalar belongs to
|
|
449
|
+
// `key`, and treating the next line at the mapping's column as its
|
|
450
|
+
// continuation masked the tag on a sibling key.
|
|
451
|
+
plainStartCol = i;
|
|
452
|
+
i = s;
|
|
453
|
+
atNodeStart = false;
|
|
454
|
+
// A plain scalar reaching the end of the line may continue on the next
|
|
455
|
+
// one. Only the LAST thing read on a line can, so this is recorded here
|
|
456
|
+
// and cleared by anything that follows it.
|
|
457
|
+
sawPlainToEol = s >= line.length;
|
|
458
|
+
}
|
|
459
|
+
// Armed only when the line ended inside a plain scalar, and measured
|
|
460
|
+
// against the node that opened it rather than the line's own indent. `x:`
|
|
461
|
+
// with nothing after it never arms this, so the mapping written underneath
|
|
462
|
+
// it is still read as structure rather than swallowed as text.
|
|
463
|
+
// Armed inside a flow collection too: a plain scalar spans lines there just
|
|
464
|
+
// as it does outside one, and requiring depth zero left `x: [hello` /
|
|
465
|
+
// ` !world]` reading its second line at a fresh node start.
|
|
466
|
+
if (sawPlainToEol && openQuote === null && blockOwner < 0) {
|
|
467
|
+
if (!continuesPlain) {
|
|
468
|
+
plainOpen = nodeIndent;
|
|
469
|
+
// Whether the scalar hangs directly off a sequence dash decides how its
|
|
470
|
+
// continuation is measured, so it is remembered with the column.
|
|
471
|
+
// Only when the scalar IS the item's node: a dash on the line, and the
|
|
472
|
+
// scalar starting exactly where that item's node begins.
|
|
473
|
+
plainFromDash = dashIndent >= 0 && plainStartCol === nodeIndent;
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
plainOpen = -1;
|
|
477
|
+
plainFromDash = false;
|
|
478
|
+
}
|
|
479
|
+
out.push(masked + (cr ? "\r" : ""));
|
|
480
|
+
}
|
|
481
|
+
return out.join("\n");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// A line kept for its structure, with any comment on it masked. Used for the
|
|
485
|
+
// directive and document-marker lines, which are passed through whole because
|
|
486
|
+
// what makes them structural is their shape rather than a scan of their parts.
|
|
487
|
+
// The comment is still content and still has to go.
|
|
488
|
+
function _maskTrailingComment(line) {
|
|
489
|
+
for (var i = 0; i < line.length; i += 1) {
|
|
490
|
+
if (line.charAt(i) !== "#") continue;
|
|
491
|
+
if (i !== 0 && !_isPlainSpace(line.charAt(i - 1))) continue;
|
|
492
|
+
return line.slice(0, i) + _blanked(line.slice(i));
|
|
493
|
+
}
|
|
494
|
+
return line;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// The body of a quoted scalar from `at`, masked, stopping at the closing quote.
|
|
498
|
+
// Returns where the scan ended (past the quote when it closed) and whether it
|
|
499
|
+
// did close, which is what tells the caller the scalar runs onto the next line.
|
|
500
|
+
//
|
|
501
|
+
// Both the escape forms are honoured because both hide a quote that would
|
|
502
|
+
// otherwise look like the end: `\"` inside a double-quoted scalar and `''`
|
|
503
|
+
// inside a single-quoted one. Reading either as a terminator ends the mask
|
|
504
|
+
// early and hands the rest of the scalar back to the structural scan as though
|
|
505
|
+
// it were YAML.
|
|
506
|
+
function _maskQuotedBody(line, at, quote) {
|
|
507
|
+
var body = "";
|
|
508
|
+
var k = at;
|
|
509
|
+
while (k < line.length) {
|
|
510
|
+
var c = line.charAt(k);
|
|
511
|
+
if (quote === '"' && c === "\\" && k + 1 < line.length) { body += " "; k += 2; continue; }
|
|
512
|
+
if (c === quote) {
|
|
513
|
+
if (quote === "'" && line.charAt(k + 1) === "'") { body += " "; k += 2; continue; }
|
|
514
|
+
return { masked: body + quote, end: k + 1, closed: true };
|
|
515
|
+
}
|
|
516
|
+
body += c === "\t" ? "\t" : " ";
|
|
517
|
+
k += 1;
|
|
518
|
+
}
|
|
519
|
+
return { masked: body, end: k, closed: false };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Same length, spaces throughout. Newlines cannot appear here — the caller
|
|
523
|
+
// splits on them first — but a tab is preserved so column arithmetic that
|
|
524
|
+
// counts it as one character still agrees with the source.
|
|
525
|
+
function _blanked(s) {
|
|
526
|
+
var o = "";
|
|
527
|
+
for (var i = 0; i < s.length; i += 1) o += s.charAt(i) === "\t" ? "\t" : " ";
|
|
528
|
+
return o;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
module.exports = {
|
|
532
|
+
maskNonStructural: maskNonStructural,
|
|
533
|
+
};
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:f52f2ee6-253b-4bf4-9915-22d453a4407a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-24T12:08:00.505Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.18.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.18.53",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.18.
|
|
25
|
+
"version": "0.18.53",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.18.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.18.53",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.18.
|
|
57
|
+
"ref": "@blamejs/core@0.18.53",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|