@blamejs/core 0.7.18 → 0.7.20
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 +427 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt-external.js +365 -0
- package/lib/auth/jwt.js +337 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +628 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +350 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -369
- package/lib/mail.js +981 -981
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +399 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/fetch-metadata.js +129 -0
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +85 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +121 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -111
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +7 -5
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -340
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/parsers/safe-yaml.js
CHANGED
|
@@ -1,1015 +1,1015 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Security-focused YAML parser (safe subset of YAML 1.2).
|
|
4
|
-
*
|
|
5
|
-
* YAML's full grammar is large and historically the source of many CVEs:
|
|
6
|
-
* `!!python/object` deserialization in PyYAML, anchor-cycle billion-laughs
|
|
7
|
-
* DoS, tag-based RCE chains. This parser implements only the JSON-shaped
|
|
8
|
-
* subset that real-world config files actually use, and REJECTS the rest
|
|
9
|
-
* with explicit errors.
|
|
10
|
-
*
|
|
11
|
-
* Supported:
|
|
12
|
-
* - Block mappings: `key: value`, indented children
|
|
13
|
-
* - Block sequences: `- item`, indented multi-line items
|
|
14
|
-
* - Flow mappings: `{ key: value, ... }`
|
|
15
|
-
* - Flow sequences: `[a, b, c]`
|
|
16
|
-
* - Plain scalars with YAML 1.2 *core schema* type inference:
|
|
17
|
-
* null / Null / NULL / ~ / empty null
|
|
18
|
-
* true / True / TRUE / false / False boolean
|
|
19
|
-
* (only these forms — NOT yes/no/on/off, fixing the
|
|
20
|
-
* "Norway problem" where country: NO became country: false)
|
|
21
|
-
* integers: 0, -17, 0o755, 0x1F number
|
|
22
|
-
* floats: 3.14, 5e+22, .inf, -.inf, .nan number
|
|
23
|
-
* everything else string
|
|
24
|
-
* - Single-quoted strings (no escapes; `''` for literal apostrophe)
|
|
25
|
-
* - Double-quoted strings (\n, \t, \\, \", \uXXXX, \UXXXXXXXX)
|
|
26
|
-
* - Block scalars: literal `|` and folded `>` with chomp indicators
|
|
27
|
-
* (-, +, or default)
|
|
28
|
-
* - `#` comments (line-rest)
|
|
29
|
-
*
|
|
30
|
-
* REJECTED with explicit errors:
|
|
31
|
-
* - Anchors `&name` / aliases `*name` (billion-laughs / cycles)
|
|
32
|
-
* - Tags `!tag`, `!!tag`, `!<uri>` (tag-driven deserialization)
|
|
33
|
-
* - YAML directives `%YAML`, `%TAG`
|
|
34
|
-
* - Multi-document streams (`---` / `...` separators)
|
|
35
|
-
* - Complex keys (`? mapping_key`)
|
|
36
|
-
* - Merge keys `<<` (anchor-using feature)
|
|
37
|
-
* - Tabs in indentation (per YAML 1.2 spec)
|
|
38
|
-
* - __proto__ / constructor / prototype as map keys (prototype pollution)
|
|
39
|
-
*
|
|
40
|
-
* Output shape:
|
|
41
|
-
* - mappings plain {} object
|
|
42
|
-
* - sequences JS array
|
|
43
|
-
* - scalars string / number / boolean / null per core schema
|
|
44
|
-
*
|
|
45
|
-
* Public API:
|
|
46
|
-
* yaml.parse(input, opts?) value | throws SafeYamlError
|
|
47
|
-
* yaml.SafeYamlError error class
|
|
48
|
-
*
|
|
49
|
-
* Defaults:
|
|
50
|
-
* maxBytes: 1 MiB
|
|
51
|
-
* maxDepth: 100
|
|
52
|
-
* maxKeys: 50000
|
|
53
|
-
*/
|
|
54
|
-
|
|
55
|
-
var C = require("../constants");
|
|
56
|
-
var numericBounds = require("../numeric-bounds");
|
|
57
|
-
var safeBuffer = require("../safe-buffer");
|
|
58
|
-
var { FrameworkError } = require("../framework-error");
|
|
59
|
-
|
|
60
|
-
class SafeYamlError extends FrameworkError {
|
|
61
|
-
constructor(message, code, line, col) {
|
|
62
|
-
super(line != null ? message + " at line " + line + ":" + col : message);
|
|
63
|
-
this.name = "SafeYamlError";
|
|
64
|
-
this.code = code || "yaml/invalid";
|
|
65
|
-
this.line = line == null ? null : line;
|
|
66
|
-
this.col = col == null ? null : col;
|
|
67
|
-
this.isSafeYamlError = true;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// parseInt radices, named so the line itself doesn't carry a bare 8 / 16
|
|
72
|
-
// integer literal that reads as a byte count.
|
|
73
|
-
var RADIX_OCTAL = 0x8;
|
|
74
|
-
var RADIX_HEX = 0x10;
|
|
75
|
-
|
|
76
|
-
// Defensive cap on the size of a single scalar token before regex
|
|
77
|
-
// classification. The parser already bounds the whole input via maxBytes;
|
|
78
|
-
// this cap keeps a pathological single-line scalar from feeding the type-
|
|
79
|
-
// inference regexes an arbitrarily long string.
|
|
80
|
-
var MAX_SCALAR_BYTES = C.BYTES.kib(64);
|
|
81
|
-
|
|
82
|
-
var DEFAULTS = {
|
|
83
|
-
maxBytes: C.BYTES.mib(1),
|
|
84
|
-
maxDepth: 100,
|
|
85
|
-
maxKeys: 50_000,
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
89
|
-
|
|
90
|
-
// YAML 1.2 core-schema scalar resolution. Order matters: null first
|
|
91
|
-
// (covers ~ and empty), then bool, then int (with base prefixes), then
|
|
92
|
-
// float, then string fallback.
|
|
93
|
-
var NULL_RE = /^(null|Null|NULL|~|)$/;
|
|
94
|
-
var BOOL_RE = /^(true|True|TRUE|false|False|FALSE)$/;
|
|
95
|
-
var INT_RE = /^[-+]?(0|[1-9][0-9]*)$/;
|
|
96
|
-
var INT_OCT = /^0o[0-7]+$/;
|
|
97
|
-
var INT_HEX = /^0x[0-9a-fA-F]+$/;
|
|
98
|
-
var FLOAT_RE = /^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$/;
|
|
99
|
-
var FLOAT_INF = /^[-+]?\.(inf|Inf|INF)$/;
|
|
100
|
-
var FLOAT_NAN = /^\.(nan|NaN|NAN)$/;
|
|
101
|
-
|
|
102
|
-
function _resolveScalar(s) {
|
|
103
|
-
// Fall back to string for any token whose length exceeds the scalar cap
|
|
104
|
-
// before running type-inference regexes. The parser caller has already
|
|
105
|
-
// applied the input-wide maxBytes cap; this is a per-token guard so the
|
|
106
|
-
// type-inference regexes never see a pathologically long string.
|
|
107
|
-
if (typeof s !== "string" || s.length > MAX_SCALAR_BYTES) return s;
|
|
108
|
-
// Below: every regex test sees an `s` whose s.length <= MAX_SCALAR_BYTES.
|
|
109
|
-
if (NULL_RE.test(s)) return null;
|
|
110
|
-
if (BOOL_RE.test(s)) return s.toLowerCase() === "true";
|
|
111
|
-
// s.length <= MAX_SCALAR_BYTES asserted at function entry above.
|
|
112
|
-
if (INT_RE.test(s)) {
|
|
113
|
-
var n = parseInt(s, 10);
|
|
114
|
-
if (Number.isSafeInteger(n)) return n;
|
|
115
|
-
return s; // fallback to string for huge ints (don't lose precision silently)
|
|
116
|
-
}
|
|
117
|
-
// s.length <= MAX_SCALAR_BYTES asserted at function entry above.
|
|
118
|
-
if (INT_OCT.test(s)) {
|
|
119
|
-
var oct = parseInt(s.substring(2), RADIX_OCTAL);
|
|
120
|
-
if (Number.isSafeInteger(oct)) return oct;
|
|
121
|
-
return s;
|
|
122
|
-
}
|
|
123
|
-
// s.length <= MAX_SCALAR_BYTES asserted at function entry above.
|
|
124
|
-
if (INT_HEX.test(s)) {
|
|
125
|
-
var hex = parseInt(s.substring(2), RADIX_HEX);
|
|
126
|
-
if (Number.isSafeInteger(hex)) return hex;
|
|
127
|
-
return s;
|
|
128
|
-
}
|
|
129
|
-
// s.length <= MAX_SCALAR_BYTES asserted at function entry above.
|
|
130
|
-
if (FLOAT_INF.test(s)) return s.charAt(0) === "-" ? -Infinity : Infinity;
|
|
131
|
-
if (FLOAT_NAN.test(s)) return NaN;
|
|
132
|
-
// s.length <= MAX_SCALAR_BYTES asserted at function entry above.
|
|
133
|
-
if (FLOAT_RE.test(s)) {
|
|
134
|
-
var f = parseFloat(s);
|
|
135
|
-
if (!isNaN(f)) return f;
|
|
136
|
-
}
|
|
137
|
-
return s;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function parse(input, opts) {
|
|
141
|
-
opts = opts || {};
|
|
142
|
-
// Validate operator-supplied numeric opts via lib/numeric-bounds —
|
|
143
|
-
// Infinity / NaN / negative / non-integer all bypass the
|
|
144
|
-
// `> 0` shape and silently lift the DoS cap they were meant to enforce.
|
|
145
|
-
if (opts.maxBytes !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxBytes)) {
|
|
146
|
-
throw new SafeYamlError("yaml.parse: maxBytes must be a positive finite integer; got " +
|
|
147
|
-
numericBounds.shape(opts.maxBytes), "yaml/bad-opt");
|
|
148
|
-
}
|
|
149
|
-
if (opts.maxDepth !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxDepth)) {
|
|
150
|
-
throw new SafeYamlError("yaml.parse: maxDepth must be a positive finite integer; got " +
|
|
151
|
-
numericBounds.shape(opts.maxDepth), "yaml/bad-opt");
|
|
152
|
-
}
|
|
153
|
-
if (opts.maxKeys !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxKeys)) {
|
|
154
|
-
throw new SafeYamlError("yaml.parse: maxKeys must be a positive finite integer; got " +
|
|
155
|
-
numericBounds.shape(opts.maxKeys), "yaml/bad-opt");
|
|
156
|
-
}
|
|
157
|
-
var maxBytes = opts.maxBytes !== undefined
|
|
158
|
-
? Math.min(opts.maxBytes, C.BYTES.mib(64)) : DEFAULTS.maxBytes;
|
|
159
|
-
var maxDepth = opts.maxDepth !== undefined
|
|
160
|
-
? Math.min(opts.maxDepth, 1_000) : DEFAULTS.maxDepth;
|
|
161
|
-
var maxKeys = opts.maxKeys !== undefined
|
|
162
|
-
? Math.min(opts.maxKeys, 1_000_000) : DEFAULTS.maxKeys;
|
|
163
|
-
|
|
164
|
-
input = safeBuffer.normalizeText(input, {
|
|
165
|
-
maxBytes: maxBytes,
|
|
166
|
-
errorClass: SafeYamlError,
|
|
167
|
-
typeCode: "yaml/wrong-input-type",
|
|
168
|
-
sizeCode: "yaml/too-large",
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// Pre-validate: scan for banned constructs ANYWHERE in the input. Cheap
|
|
172
|
-
// up-front rejection beats partial-parse-then-throw, and it surfaces a
|
|
173
|
-
// clearer diagnostic than "unexpected & at line 47".
|
|
174
|
-
// Skip bans that appear inside quoted strings — those are legitimate
|
|
175
|
-
// string content. Block-scalar content also exempt.
|
|
176
|
-
_preValidate(input);
|
|
177
|
-
|
|
178
|
-
// Normalize line endings: CRLF / CR → LF for consistent line-based work.
|
|
179
|
-
input = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
180
|
-
|
|
181
|
-
// Split into raw lines preserving line numbers.
|
|
182
|
-
var rawLines = input.split("\n");
|
|
183
|
-
var lines = [];
|
|
184
|
-
for (var i = 0; i < rawLines.length; i++) {
|
|
185
|
-
var raw = rawLines[i];
|
|
186
|
-
// Strip trailing whitespace from the line for analysis (block-scalar
|
|
187
|
-
// content paths preserve their own internal spacing separately).
|
|
188
|
-
var trimmed = raw.replace(safeBuffer.TRAILING_HSPACE_RE, "");
|
|
189
|
-
var indent = 0;
|
|
190
|
-
while (indent < raw.length && raw.charAt(indent) === " ") indent += 1;
|
|
191
|
-
if (indent < raw.length && raw.charAt(indent) === "\t") {
|
|
192
|
-
throw new SafeYamlError("tab in indentation (YAML 1.2 forbids)", "yaml/tab-indent", i + 1, indent + 1);
|
|
193
|
-
}
|
|
194
|
-
var content = raw.substring(indent);
|
|
195
|
-
var isBlank = content.length === 0;
|
|
196
|
-
var isComment = content.charAt(0) === "#";
|
|
197
|
-
lines.push({
|
|
198
|
-
lineNumber: i + 1,
|
|
199
|
-
raw: raw,
|
|
200
|
-
indent: indent,
|
|
201
|
-
content: content,
|
|
202
|
-
trimmed: trimmed,
|
|
203
|
-
isBlank: isBlank,
|
|
204
|
-
isComment: isComment,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Skip leading blank/comment lines and an optional `---` document marker.
|
|
209
|
-
// Reject any later `---` or `...` (multi-document streams not supported).
|
|
210
|
-
var idx = 0;
|
|
211
|
-
while (idx < lines.length && (lines[idx].isBlank || lines[idx].isComment)) idx += 1;
|
|
212
|
-
if (idx < lines.length && /^---(\s|$)/.test(lines[idx].content)) idx += 1;
|
|
213
|
-
// Subsequent doc markers anywhere = reject.
|
|
214
|
-
for (var j = idx; j < lines.length; j++) {
|
|
215
|
-
var c = lines[j].content;
|
|
216
|
-
if (/^---(\s|$)/.test(c) || /^\.\.\.(\s|$)/.test(c)) {
|
|
217
|
-
throw new SafeYamlError(
|
|
218
|
-
"multi-document YAML streams are not supported",
|
|
219
|
-
"yaml/multi-document", lines[j].lineNumber, 1
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
var keyCount = 0;
|
|
225
|
-
|
|
226
|
-
function _bumpKeys(line) {
|
|
227
|
-
keyCount += 1;
|
|
228
|
-
if (keyCount > maxKeys) {
|
|
229
|
-
throw new SafeYamlError("input exceeds maxKeys", "yaml/too-many-keys", line, 1);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ---- Recursive parse ----
|
|
234
|
-
// parseValue takes a starting line index and the *minimum* indent the
|
|
235
|
-
// value occupies. Returns { value, nextLine }.
|
|
236
|
-
|
|
237
|
-
function parseValueAtLine(startIdx, parentIndent, depth) {
|
|
238
|
-
if (depth > maxDepth) {
|
|
239
|
-
throw new SafeYamlError("input exceeds maxDepth", "yaml/too-deep",
|
|
240
|
-
startIdx < lines.length ? lines[startIdx].lineNumber : null, 1);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Skip blank + comment lines.
|
|
244
|
-
var k = startIdx;
|
|
245
|
-
while (k < lines.length && (lines[k].isBlank || lines[k].isComment)) k += 1;
|
|
246
|
-
if (k >= lines.length) return { value: null, nextLine: lines.length };
|
|
247
|
-
|
|
248
|
-
var firstLine = lines[k];
|
|
249
|
-
if (firstLine.indent <= parentIndent) {
|
|
250
|
-
// No content for this value — it's an empty value (e.g. `key:` with nothing after)
|
|
251
|
-
return { value: null, nextLine: k };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
var content = firstLine.content;
|
|
255
|
-
var indent = firstLine.indent;
|
|
256
|
-
|
|
257
|
-
// Block sequence: line starts with `- ` or just `-` (then empty / value)
|
|
258
|
-
if (content === "-" || content.startsWith("- ")) {
|
|
259
|
-
return _parseBlockSequence(k, indent, depth);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Block mapping starts with a key (bare or quoted) followed by `:`.
|
|
263
|
-
var keyRange = _scanKeyRange(content, firstLine.lineNumber, indent);
|
|
264
|
-
if (keyRange) {
|
|
265
|
-
return _parseBlockMapping(k, indent, depth);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Block scalar header: `|` or `>` (with optional chomp/indent indicator)
|
|
269
|
-
if (/^[|>][-+]?[0-9]?\s*(#.*)?$/.test(content)) {
|
|
270
|
-
return _parseBlockScalar(k, indent, content);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Otherwise: a single scalar (possibly continued via flow style on
|
|
274
|
-
// following lines, but for the safe subset we restrict scalars to
|
|
275
|
-
// single-line plain or quoted, or wrapped flow on a single line).
|
|
276
|
-
return _parseScalarOrFlow(k, indent);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// _scanKeyRange returns { keyLiteral, valueStart } if the line starts
|
|
280
|
-
// with a bare-or-quoted key followed by ":" + (space|EOL), else null.
|
|
281
|
-
function _scanKeyRange(content, lineNumber, indent) {
|
|
282
|
-
var p = 0;
|
|
283
|
-
var len = content.length;
|
|
284
|
-
if (len === 0) return null;
|
|
285
|
-
if (content.charAt(0) === "?") {
|
|
286
|
-
throw new SafeYamlError("complex keys (`? key`) are not supported",
|
|
287
|
-
"yaml/complex-key-banned", lineNumber, indent + 1);
|
|
288
|
-
}
|
|
289
|
-
if (content.charAt(0) === '"') {
|
|
290
|
-
// Find closing quote
|
|
291
|
-
var i = 1;
|
|
292
|
-
while (i < len) {
|
|
293
|
-
var ch = content.charAt(i);
|
|
294
|
-
if (ch === "\\") { i += 2; continue; }
|
|
295
|
-
if (ch === '"') break;
|
|
296
|
-
i += 1;
|
|
297
|
-
}
|
|
298
|
-
if (i >= len) return null; // unterminated → not a key
|
|
299
|
-
p = i + 1;
|
|
300
|
-
} else if (content.charAt(0) === "'") {
|
|
301
|
-
var j = 1;
|
|
302
|
-
while (j < len) {
|
|
303
|
-
if (content.charAt(j) === "'") {
|
|
304
|
-
if (content.charAt(j + 1) === "'") { j += 2; continue; }
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
j += 1;
|
|
308
|
-
}
|
|
309
|
-
if (j >= len) return null;
|
|
310
|
-
p = j + 1;
|
|
311
|
-
} else {
|
|
312
|
-
// Plain scalar key: read up to ':' followed by space or EOL.
|
|
313
|
-
while (p < len) {
|
|
314
|
-
if (content.charAt(p) === ":" &&
|
|
315
|
-
(p + 1 === len || content.charAt(p + 1) === " ")) {
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
p += 1;
|
|
319
|
-
}
|
|
320
|
-
if (p >= len) return null;
|
|
321
|
-
}
|
|
322
|
-
if (content.charAt(p) !== ":") return null;
|
|
323
|
-
var afterColon = p + 1;
|
|
324
|
-
if (afterColon !== len && content.charAt(afterColon) !== " ") return null;
|
|
325
|
-
return { keyEnd: p, valueStart: afterColon };
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function _decodeKeyLiteral(raw, lineNumber, col) {
|
|
329
|
-
if (raw.charAt(0) === '"') {
|
|
330
|
-
return _decodeDoubleQuoted(raw, lineNumber, col);
|
|
331
|
-
}
|
|
332
|
-
if (raw.charAt(0) === "'") {
|
|
333
|
-
return _decodeSingleQuoted(raw, lineNumber, col);
|
|
334
|
-
}
|
|
335
|
-
var trimmed = raw.replace(safeBuffer.TRAILING_HSPACE_RE, "");
|
|
336
|
-
if (POISONED_KEYS.has(trimmed)) {
|
|
337
|
-
throw new SafeYamlError("forbidden key '" + trimmed + "'",
|
|
338
|
-
"yaml/poisoned-key", lineNumber, col);
|
|
339
|
-
}
|
|
340
|
-
return trimmed;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function _parseBlockMapping(startIdx, indent, depth) {
|
|
344
|
-
var result = Object.create(null);
|
|
345
|
-
var seen = new Set();
|
|
346
|
-
var k = startIdx;
|
|
347
|
-
while (k < lines.length) {
|
|
348
|
-
var ln = lines[k];
|
|
349
|
-
if (ln.isBlank || ln.isComment) { k += 1; continue; }
|
|
350
|
-
if (ln.indent < indent) break;
|
|
351
|
-
if (ln.indent > indent) {
|
|
352
|
-
// Shouldn't happen if our caller dispatched correctly
|
|
353
|
-
throw new SafeYamlError("unexpected indent", "yaml/bad-indent", ln.lineNumber, ln.indent + 1);
|
|
354
|
-
}
|
|
355
|
-
// Reject a block sequence at the same indent — that's not a mapping.
|
|
356
|
-
if (ln.content === "-" || ln.content.startsWith("- ")) {
|
|
357
|
-
throw new SafeYamlError("sequence item where mapping key expected",
|
|
358
|
-
"yaml/expected-key", ln.lineNumber, ln.indent + 1);
|
|
359
|
-
}
|
|
360
|
-
var keyRange = _scanKeyRange(ln.content, ln.lineNumber, ln.indent);
|
|
361
|
-
if (!keyRange) {
|
|
362
|
-
throw new SafeYamlError("expected mapping key 'name:'",
|
|
363
|
-
"yaml/expected-key", ln.lineNumber, ln.indent + 1);
|
|
364
|
-
}
|
|
365
|
-
var keyLiteral = ln.content.substring(0, keyRange.keyEnd);
|
|
366
|
-
var key = _decodeKeyLiteral(keyLiteral, ln.lineNumber, ln.indent + 1);
|
|
367
|
-
if (typeof key !== "string") {
|
|
368
|
-
throw new SafeYamlError("non-string mapping key not supported",
|
|
369
|
-
"yaml/bad-key", ln.lineNumber, ln.indent + 1);
|
|
370
|
-
}
|
|
371
|
-
if (POISONED_KEYS.has(key)) {
|
|
372
|
-
throw new SafeYamlError("forbidden key '" + key + "'",
|
|
373
|
-
"yaml/poisoned-key", ln.lineNumber, ln.indent + 1);
|
|
374
|
-
}
|
|
375
|
-
if (key === "<<") {
|
|
376
|
-
throw new SafeYamlError("merge key '<<' not supported (anchor-using feature)",
|
|
377
|
-
"yaml/merge-key-banned", ln.lineNumber, ln.indent + 1);
|
|
378
|
-
}
|
|
379
|
-
if (seen.has(key)) {
|
|
380
|
-
throw new SafeYamlError("duplicate mapping key '" + key + "'",
|
|
381
|
-
"yaml/duplicate-key", ln.lineNumber, ln.indent + 1);
|
|
382
|
-
}
|
|
383
|
-
seen.add(key);
|
|
384
|
-
_bumpKeys(ln.lineNumber);
|
|
385
|
-
|
|
386
|
-
// Determine the value:
|
|
387
|
-
// 1. if there's content after the colon, that's a flow value or
|
|
388
|
-
// a plain scalar continuation on the same line.
|
|
389
|
-
// 2. otherwise the value lives on subsequent more-indented lines.
|
|
390
|
-
var afterColon = ln.content.substring(keyRange.valueStart).replace(/^[ \t]+/, "");
|
|
391
|
-
// Strip end-of-line comment
|
|
392
|
-
afterColon = _stripEolComment(afterColon);
|
|
393
|
-
var value;
|
|
394
|
-
if (afterColon.length > 0) {
|
|
395
|
-
// Inline value on this line.
|
|
396
|
-
if (afterColon.charAt(0) === "|" || afterColon.charAt(0) === ">") {
|
|
397
|
-
// Block scalar header inline with key
|
|
398
|
-
if (!/^[|>][-+]?[0-9]?\s*$/.test(afterColon)) {
|
|
399
|
-
throw new SafeYamlError("malformed block scalar header",
|
|
400
|
-
"yaml/bad-block-scalar", ln.lineNumber, ln.indent + 1);
|
|
401
|
-
}
|
|
402
|
-
var bs = _parseBlockScalar(k, indent, afterColon);
|
|
403
|
-
// Block scalar header was on this line; content is on the
|
|
404
|
-
// following lines. _parseBlockScalar handles the header
|
|
405
|
-
// tokenisation when given the header content directly.
|
|
406
|
-
value = bs.value;
|
|
407
|
-
k = bs.nextLine;
|
|
408
|
-
result[key] = value;
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
value = _parseInlineValue(afterColon, ln.lineNumber, ln.indent + keyRange.valueStart);
|
|
412
|
-
k += 1;
|
|
413
|
-
} else {
|
|
414
|
-
// Value is on more-indented lines below.
|
|
415
|
-
var nested = parseValueAtLine(k + 1, indent, depth + 1);
|
|
416
|
-
value = nested.value;
|
|
417
|
-
k = nested.nextLine;
|
|
418
|
-
}
|
|
419
|
-
result[key] = value;
|
|
420
|
-
}
|
|
421
|
-
// Convert null-prototype to plain {} below in _normalize.
|
|
422
|
-
return { value: result, nextLine: k };
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function _parseBlockSequence(startIdx, indent, depth) {
|
|
426
|
-
var arr = [];
|
|
427
|
-
var k = startIdx;
|
|
428
|
-
while (k < lines.length) {
|
|
429
|
-
var ln = lines[k];
|
|
430
|
-
if (ln.isBlank || ln.isComment) { k += 1; continue; }
|
|
431
|
-
if (ln.indent < indent) break;
|
|
432
|
-
if (ln.indent !== indent) {
|
|
433
|
-
throw new SafeYamlError("unexpected indent in sequence",
|
|
434
|
-
"yaml/bad-indent", ln.lineNumber, ln.indent + 1);
|
|
435
|
-
}
|
|
436
|
-
if (ln.content !== "-" && !ln.content.startsWith("- ")) break;
|
|
437
|
-
_bumpKeys(ln.lineNumber);
|
|
438
|
-
|
|
439
|
-
// Item value:
|
|
440
|
-
// - "- " followed by content → inline value (could be flow, scalar,
|
|
441
|
-
// or the start of a nested mapping if "key: value" pattern)
|
|
442
|
-
// - "-" alone → value on more-indented lines below
|
|
443
|
-
var afterDash = ln.content === "-" ? "" : ln.content.substring(2);
|
|
444
|
-
afterDash = _stripEolComment(afterDash);
|
|
445
|
-
var item;
|
|
446
|
-
if (afterDash.length === 0) {
|
|
447
|
-
var nested = parseValueAtLine(k + 1, indent, depth + 1);
|
|
448
|
-
item = nested.value;
|
|
449
|
-
k = nested.nextLine;
|
|
450
|
-
} else {
|
|
451
|
-
// Could be a nested mapping: `- key: value` (compact mapping)
|
|
452
|
-
var mapKey = _scanKeyRange(afterDash, ln.lineNumber, ln.indent + 2);
|
|
453
|
-
if (mapKey) {
|
|
454
|
-
// Treat as a single-line block mapping at indent+2 by
|
|
455
|
-
// synthesising a sub-region. The mapping's "indent" is the
|
|
456
|
-
// column where the key starts (ln.indent + 2).
|
|
457
|
-
// We need to feed _parseBlockMapping starting at this line but
|
|
458
|
-
// with the mapping treating its own indent as ln.indent + 2.
|
|
459
|
-
// Simplest: clone the line with the dash stripped, then continue
|
|
460
|
-
// with subsequent more-indented lines as the mapping's body.
|
|
461
|
-
var synthetic = {
|
|
462
|
-
lineNumber: ln.lineNumber,
|
|
463
|
-
raw: ln.raw,
|
|
464
|
-
indent: ln.indent + 2,
|
|
465
|
-
content: afterDash,
|
|
466
|
-
trimmed: afterDash,
|
|
467
|
-
isBlank: false,
|
|
468
|
-
isComment: false,
|
|
469
|
-
};
|
|
470
|
-
var saved = lines[k];
|
|
471
|
-
lines[k] = synthetic;
|
|
472
|
-
try {
|
|
473
|
-
var sub = _parseBlockMapping(k, ln.indent + 2, depth + 1);
|
|
474
|
-
item = sub.value;
|
|
475
|
-
k = sub.nextLine;
|
|
476
|
-
} finally {
|
|
477
|
-
lines[saved.lineNumber - 1] = saved;
|
|
478
|
-
}
|
|
479
|
-
} else {
|
|
480
|
-
item = _parseInlineValue(afterDash, ln.lineNumber, ln.indent + 2);
|
|
481
|
-
k += 1;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
arr.push(item);
|
|
485
|
-
}
|
|
486
|
-
return { value: arr, nextLine: k };
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function _parseInlineValue(text, lineNumber, col) {
|
|
490
|
-
// Strip trailing whitespace/comment already done by caller (mostly).
|
|
491
|
-
// Handle: flow [...] / {...}, quoted strings, plain scalars.
|
|
492
|
-
var t = text;
|
|
493
|
-
if (t.charAt(0) === "[") return _parseFlowSequence(t, lineNumber, col, 0);
|
|
494
|
-
if (t.charAt(0) === "{") return _parseFlowMapping(t, lineNumber, col, 0);
|
|
495
|
-
if (t.charAt(0) === '"') {
|
|
496
|
-
var dq = _decodeDoubleQuoted(t, lineNumber, col);
|
|
497
|
-
var afterDq = _trailingAfterQuoted(t, '"');
|
|
498
|
-
if (afterDq.length > 0 && afterDq.replace(/^\s+/, "") !== "") {
|
|
499
|
-
throw new SafeYamlError("unexpected content after quoted string",
|
|
500
|
-
"yaml/trailing-content", lineNumber, col);
|
|
501
|
-
}
|
|
502
|
-
return dq;
|
|
503
|
-
}
|
|
504
|
-
if (t.charAt(0) === "'") {
|
|
505
|
-
var sq = _decodeSingleQuoted(t, lineNumber, col);
|
|
506
|
-
return sq;
|
|
507
|
-
}
|
|
508
|
-
// Plain scalar — strip trailing space, resolve type.
|
|
509
|
-
return _resolveScalar(t.replace(safeBuffer.TRAILING_HSPACE_RE, ""));
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// For quoted-string termination check on a single-line value.
|
|
513
|
-
function _trailingAfterQuoted(text, quote) {
|
|
514
|
-
if (text.charAt(0) !== quote) return text;
|
|
515
|
-
var i = 1;
|
|
516
|
-
while (i < text.length) {
|
|
517
|
-
var ch = text.charAt(i);
|
|
518
|
-
if (quote === '"' && ch === "\\") { i += 2; continue; }
|
|
519
|
-
if (ch === quote) {
|
|
520
|
-
if (quote === "'" && text.charAt(i + 1) === "'") { i += 2; continue; }
|
|
521
|
-
return text.substring(i + 1);
|
|
522
|
-
}
|
|
523
|
-
i += 1;
|
|
524
|
-
}
|
|
525
|
-
return "";
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// ---- Flow style ----
|
|
529
|
-
|
|
530
|
-
function _parseFlowSequence(text, lineNumber, col, depthIncoming) {
|
|
531
|
-
var p = 1; // skip [
|
|
532
|
-
var arr = [];
|
|
533
|
-
while (p < text.length) {
|
|
534
|
-
_flowSkipWs(text, p);
|
|
535
|
-
p = _flowSkipWsIndex(text, p);
|
|
536
|
-
if (text.charAt(p) === "]") return arr;
|
|
537
|
-
var v = _parseFlowValue(text, p, lineNumber, col, depthIncoming + 1);
|
|
538
|
-
arr.push(v.value);
|
|
539
|
-
p = v.nextPos;
|
|
540
|
-
p = _flowSkipWsIndex(text, p);
|
|
541
|
-
if (text.charAt(p) === ",") { p += 1; continue; }
|
|
542
|
-
if (text.charAt(p) === "]") { return arr; }
|
|
543
|
-
throw new SafeYamlError("expected ',' or ']' in flow sequence",
|
|
544
|
-
"yaml/bad-flow", lineNumber, col + p);
|
|
545
|
-
}
|
|
546
|
-
throw new SafeYamlError("unterminated flow sequence",
|
|
547
|
-
"yaml/unterminated-flow", lineNumber, col);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function _parseFlowMapping(text, lineNumber, col, depthIncoming) {
|
|
551
|
-
if (depthIncoming > maxDepth) {
|
|
552
|
-
throw new SafeYamlError("input exceeds maxDepth", "yaml/too-deep", lineNumber, col);
|
|
553
|
-
}
|
|
554
|
-
var p = 1;
|
|
555
|
-
var result = Object.create(null);
|
|
556
|
-
while (p < text.length) {
|
|
557
|
-
p = _flowSkipWsIndex(text, p);
|
|
558
|
-
if (text.charAt(p) === "}") { return result; }
|
|
559
|
-
// Read key
|
|
560
|
-
var keyVal = _parseFlowKey(text, p, lineNumber, col);
|
|
561
|
-
var key = keyVal.key;
|
|
562
|
-
if (typeof key !== "string") {
|
|
563
|
-
throw new SafeYamlError("non-string flow-mapping key",
|
|
564
|
-
"yaml/bad-key", lineNumber, col + p);
|
|
565
|
-
}
|
|
566
|
-
if (POISONED_KEYS.has(key)) {
|
|
567
|
-
throw new SafeYamlError("forbidden key '" + key + "'",
|
|
568
|
-
"yaml/poisoned-key", lineNumber, col + p);
|
|
569
|
-
}
|
|
570
|
-
_bumpKeys(lineNumber);
|
|
571
|
-
p = keyVal.nextPos;
|
|
572
|
-
p = _flowSkipWsIndex(text, p);
|
|
573
|
-
if (text.charAt(p) !== ":") {
|
|
574
|
-
throw new SafeYamlError("expected ':' in flow mapping",
|
|
575
|
-
"yaml/bad-flow", lineNumber, col + p);
|
|
576
|
-
}
|
|
577
|
-
p += 1;
|
|
578
|
-
p = _flowSkipWsIndex(text, p);
|
|
579
|
-
var valRes = _parseFlowValue(text, p, lineNumber, col, depthIncoming + 1);
|
|
580
|
-
result[key] = valRes.value;
|
|
581
|
-
p = valRes.nextPos;
|
|
582
|
-
p = _flowSkipWsIndex(text, p);
|
|
583
|
-
if (text.charAt(p) === ",") { p += 1; continue; }
|
|
584
|
-
if (text.charAt(p) === "}") { return result; }
|
|
585
|
-
throw new SafeYamlError("expected ',' or '}' in flow mapping",
|
|
586
|
-
"yaml/bad-flow", lineNumber, col + p);
|
|
587
|
-
}
|
|
588
|
-
throw new SafeYamlError("unterminated flow mapping",
|
|
589
|
-
"yaml/unterminated-flow", lineNumber, col);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function _parseFlowValue(text, p, lineNumber, col, depthIncoming) {
|
|
593
|
-
if (depthIncoming > maxDepth) {
|
|
594
|
-
throw new SafeYamlError("input exceeds maxDepth", "yaml/too-deep", lineNumber, col + p);
|
|
595
|
-
}
|
|
596
|
-
var ch = text.charAt(p);
|
|
597
|
-
if (ch === "[") {
|
|
598
|
-
var sub = _parseFlowSequence(text.substring(p), lineNumber, col + p, depthIncoming);
|
|
599
|
-
// We need to find where the matching ] closes — re-scan
|
|
600
|
-
var endP = _findMatchingBracket(text, p, "[", "]", lineNumber, col);
|
|
601
|
-
return { value: sub, nextPos: endP + 1 };
|
|
602
|
-
}
|
|
603
|
-
if (ch === "{") {
|
|
604
|
-
var subM = _parseFlowMapping(text.substring(p), lineNumber, col + p, depthIncoming);
|
|
605
|
-
var endM = _findMatchingBracket(text, p, "{", "}", lineNumber, col);
|
|
606
|
-
return { value: subM, nextPos: endM + 1 };
|
|
607
|
-
}
|
|
608
|
-
if (ch === '"') {
|
|
609
|
-
var dq = _decodeDoubleQuoted(text.substring(p), lineNumber, col + p);
|
|
610
|
-
var endQ = _findClosingQuote(text, p, '"', lineNumber, col);
|
|
611
|
-
return { value: dq, nextPos: endQ + 1 };
|
|
612
|
-
}
|
|
613
|
-
if (ch === "'") {
|
|
614
|
-
var sq = _decodeSingleQuoted(text.substring(p), lineNumber, col + p);
|
|
615
|
-
var endSQ = _findClosingQuote(text, p, "'", lineNumber, col);
|
|
616
|
-
return { value: sq, nextPos: endSQ + 1 };
|
|
617
|
-
}
|
|
618
|
-
// Plain scalar — read until , } ] or EOL
|
|
619
|
-
var start = p;
|
|
620
|
-
while (p < text.length) {
|
|
621
|
-
var c = text.charAt(p);
|
|
622
|
-
if (c === "," || c === "}" || c === "]") break;
|
|
623
|
-
p += 1;
|
|
624
|
-
}
|
|
625
|
-
var raw = text.substring(start, p).replace(safeBuffer.TRAILING_HSPACE_RE, "");
|
|
626
|
-
return { value: _resolveScalar(raw), nextPos: p };
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function _parseFlowKey(text, p, lineNumber, col) {
|
|
630
|
-
var ch = text.charAt(p);
|
|
631
|
-
if (ch === '"') {
|
|
632
|
-
var dq = _decodeDoubleQuoted(text.substring(p), lineNumber, col + p);
|
|
633
|
-
var endQ = _findClosingQuote(text, p, '"', lineNumber, col);
|
|
634
|
-
return { key: dq, nextPos: endQ + 1 };
|
|
635
|
-
}
|
|
636
|
-
if (ch === "'") {
|
|
637
|
-
var sq = _decodeSingleQuoted(text.substring(p), lineNumber, col + p);
|
|
638
|
-
var endSQ = _findClosingQuote(text, p, "'", lineNumber, col);
|
|
639
|
-
return { key: sq, nextPos: endSQ + 1 };
|
|
640
|
-
}
|
|
641
|
-
// Plain key — read until ':' (followed by space/comma/end) or comma/end
|
|
642
|
-
var start = p;
|
|
643
|
-
while (p < text.length) {
|
|
644
|
-
var c = text.charAt(p);
|
|
645
|
-
if (c === ":" || c === "," || c === "}" || c === "]") break;
|
|
646
|
-
p += 1;
|
|
647
|
-
}
|
|
648
|
-
return { key: text.substring(start, p).replace(safeBuffer.TRAILING_HSPACE_RE, ""), nextPos: p };
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function _findMatchingBracket(text, start, open, close, lineNumber, col) {
|
|
652
|
-
var depth = 0;
|
|
653
|
-
var i = start;
|
|
654
|
-
while (i < text.length) {
|
|
655
|
-
var c = text.charAt(i);
|
|
656
|
-
if (c === '"') { i = _findClosingQuote(text, i, '"', lineNumber, col) + 1; continue; }
|
|
657
|
-
if (c === "'") { i = _findClosingQuote(text, i, "'", lineNumber, col) + 1; continue; }
|
|
658
|
-
if (c === open) depth += 1;
|
|
659
|
-
else if (c === close) {
|
|
660
|
-
depth -= 1;
|
|
661
|
-
if (depth === 0) return i;
|
|
662
|
-
}
|
|
663
|
-
i += 1;
|
|
664
|
-
}
|
|
665
|
-
throw new SafeYamlError("unterminated flow brackets",
|
|
666
|
-
"yaml/unterminated-flow", lineNumber, col + start);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function _findClosingQuote(text, start, quote, lineNumber, col) {
|
|
670
|
-
var i = start + 1;
|
|
671
|
-
while (i < text.length) {
|
|
672
|
-
var c = text.charAt(i);
|
|
673
|
-
if (quote === '"' && c === "\\") { i += 2; continue; }
|
|
674
|
-
if (c === quote) {
|
|
675
|
-
if (quote === "'" && text.charAt(i + 1) === "'") { i += 2; continue; }
|
|
676
|
-
return i;
|
|
677
|
-
}
|
|
678
|
-
i += 1;
|
|
679
|
-
}
|
|
680
|
-
throw new SafeYamlError("unterminated quoted string",
|
|
681
|
-
"yaml/unterminated-string", lineNumber, col + start);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function _flowSkipWs(_text, _p) { /* no-op shim — real version below */ }
|
|
685
|
-
function _flowSkipWsIndex(text, p) {
|
|
686
|
-
while (p < text.length) {
|
|
687
|
-
var c = text.charAt(p);
|
|
688
|
-
if (c === " " || c === "\t") p += 1;
|
|
689
|
-
else break;
|
|
690
|
-
}
|
|
691
|
-
return p;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// ---- Scalar decoding ----
|
|
695
|
-
|
|
696
|
-
function _decodeDoubleQuoted(raw, lineNumber, col) {
|
|
697
|
-
if (raw.charAt(0) !== '"') {
|
|
698
|
-
throw new SafeYamlError("expected '\"'", "yaml/bad-string", lineNumber, col);
|
|
699
|
-
}
|
|
700
|
-
var i = 1;
|
|
701
|
-
var out = "";
|
|
702
|
-
while (i < raw.length) {
|
|
703
|
-
var ch = raw.charAt(i);
|
|
704
|
-
if (ch === '"') return out;
|
|
705
|
-
if (ch === "\\") {
|
|
706
|
-
var esc = raw.charAt(i + 1);
|
|
707
|
-
switch (esc) {
|
|
708
|
-
case '"': out += '"'; i += 2; break;
|
|
709
|
-
case "\\": out += "\\"; i += 2; break;
|
|
710
|
-
case "/": out += "/"; i += 2; break;
|
|
711
|
-
case "n": out += "\n"; i += 2; break;
|
|
712
|
-
case "t": out += "\t"; i += 2; break;
|
|
713
|
-
case "r": out += "\r"; i += 2; break;
|
|
714
|
-
case "b": out += "\b"; i += 2; break;
|
|
715
|
-
case "f": out += "\f"; i += 2; break;
|
|
716
|
-
case "0": out += "\0"; i += 2; break;
|
|
717
|
-
case "u": {
|
|
718
|
-
var hex = raw.substring(i + 2, i + 6);
|
|
719
|
-
if (!safeBuffer.isHex(hex, 4)) {
|
|
720
|
-
throw new SafeYamlError("bad \\u escape", "yaml/bad-escape", lineNumber, col + i);
|
|
721
|
-
}
|
|
722
|
-
out += String.fromCharCode(parseInt(hex, RADIX_HEX));
|
|
723
|
-
i += 6;
|
|
724
|
-
break;
|
|
725
|
-
}
|
|
726
|
-
case "U": {
|
|
727
|
-
var hex8 = raw.substring(i + 2, i + 10);
|
|
728
|
-
if (!/^[0-9a-fA-F]{8}$/.test(hex8)) {
|
|
729
|
-
throw new SafeYamlError("bad \\U escape", "yaml/bad-escape", lineNumber, col + i);
|
|
730
|
-
}
|
|
731
|
-
var code = parseInt(hex8, RADIX_HEX);
|
|
732
|
-
if (code > 0x10FFFF) {
|
|
733
|
-
throw new SafeYamlError("\\U code point > U+10FFFF",
|
|
734
|
-
"yaml/bad-escape", lineNumber, col + i);
|
|
735
|
-
}
|
|
736
|
-
out += String.fromCodePoint(code);
|
|
737
|
-
i += 10;
|
|
738
|
-
break;
|
|
739
|
-
}
|
|
740
|
-
default:
|
|
741
|
-
throw new SafeYamlError("unknown escape '\\" + esc + "'",
|
|
742
|
-
"yaml/bad-escape", lineNumber, col + i);
|
|
743
|
-
}
|
|
744
|
-
continue;
|
|
745
|
-
}
|
|
746
|
-
out += ch;
|
|
747
|
-
i += 1;
|
|
748
|
-
}
|
|
749
|
-
throw new SafeYamlError("unterminated double-quoted string",
|
|
750
|
-
"yaml/unterminated-string", lineNumber, col);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function _decodeSingleQuoted(raw, lineNumber, col) {
|
|
754
|
-
if (raw.charAt(0) !== "'") {
|
|
755
|
-
throw new SafeYamlError("expected \"'\"", "yaml/bad-string", lineNumber, col);
|
|
756
|
-
}
|
|
757
|
-
var i = 1;
|
|
758
|
-
var out = "";
|
|
759
|
-
while (i < raw.length) {
|
|
760
|
-
var ch = raw.charAt(i);
|
|
761
|
-
if (ch === "'") {
|
|
762
|
-
if (raw.charAt(i + 1) === "'") { out += "'"; i += 2; continue; }
|
|
763
|
-
return out;
|
|
764
|
-
}
|
|
765
|
-
out += ch;
|
|
766
|
-
i += 1;
|
|
767
|
-
}
|
|
768
|
-
throw new SafeYamlError("unterminated single-quoted string",
|
|
769
|
-
"yaml/unterminated-string", lineNumber, col);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function _stripEolComment(text) {
|
|
773
|
-
// Strip ` #...` comments (must be preceded by whitespace) at end of line.
|
|
774
|
-
var match = text.match(/^(.*?)(\s+#.*)?$/);
|
|
775
|
-
return (match && match[1] != null ? match[1] : text).replace(safeBuffer.TRAILING_HSPACE_RE, "");
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// ---- Block scalars (| literal, > folded) ----
|
|
779
|
-
|
|
780
|
-
function _parseBlockScalar(startIdx, parentIndent, headerContent) {
|
|
781
|
-
var headerLine = lines[startIdx];
|
|
782
|
-
var header = headerContent.trim();
|
|
783
|
-
var style = header.charAt(0); // '|' or '>'
|
|
784
|
-
var rest = header.substring(1);
|
|
785
|
-
var chomp = ""; // "-" strip, "+" keep, "" clip (default)
|
|
786
|
-
var explicitIndent = null;
|
|
787
|
-
for (var i = 0; i < rest.length; i++) {
|
|
788
|
-
var ch = rest.charAt(i);
|
|
789
|
-
if (ch === "-" || ch === "+") {
|
|
790
|
-
if (chomp) throw new SafeYamlError("multiple chomping indicators",
|
|
791
|
-
"yaml/bad-block-scalar", headerLine.lineNumber, parentIndent + 1);
|
|
792
|
-
chomp = ch;
|
|
793
|
-
} else if (ch >= "1" && ch <= "9") {
|
|
794
|
-
if (explicitIndent != null) throw new SafeYamlError("multiple indentation indicators",
|
|
795
|
-
"yaml/bad-block-scalar", headerLine.lineNumber, parentIndent + 1);
|
|
796
|
-
explicitIndent = parseInt(ch, 10);
|
|
797
|
-
} else if (ch === " " || ch === "\t" || ch === "#") {
|
|
798
|
-
// comment / trailing whitespace — stop reading indicators
|
|
799
|
-
break;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// Collect content lines: anything more-indented than parentIndent.
|
|
804
|
-
// The first non-blank content line establishes the block's indent.
|
|
805
|
-
var k = startIdx + 1;
|
|
806
|
-
var blockIndent = null;
|
|
807
|
-
var contentLines = [];
|
|
808
|
-
while (k < lines.length) {
|
|
809
|
-
var ln = lines[k];
|
|
810
|
-
if (ln.isBlank) { contentLines.push(""); k += 1; continue; }
|
|
811
|
-
if (ln.indent <= parentIndent) break;
|
|
812
|
-
if (blockIndent === null) {
|
|
813
|
-
blockIndent = explicitIndent != null ? parentIndent + explicitIndent : ln.indent;
|
|
814
|
-
}
|
|
815
|
-
if (ln.indent < blockIndent) break;
|
|
816
|
-
var trimmedToIndent = ln.raw.substring(blockIndent);
|
|
817
|
-
contentLines.push(trimmedToIndent);
|
|
818
|
-
k += 1;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Trim trailing blank lines based on chomp.
|
|
822
|
-
var trailingBlanks = 0;
|
|
823
|
-
while (contentLines.length > 0 && contentLines[contentLines.length - 1] === "") {
|
|
824
|
-
contentLines.pop();
|
|
825
|
-
trailingBlanks += 1;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
var body;
|
|
829
|
-
if (style === "|") {
|
|
830
|
-
// Literal: each content line + a newline; preserve internal blanks
|
|
831
|
-
// (we just popped trailing ones for chomp handling).
|
|
832
|
-
body = contentLines.join("\n");
|
|
833
|
-
if (contentLines.length > 0) body += "\n";
|
|
834
|
-
} else {
|
|
835
|
-
// Folded: blank line separates paragraphs (becomes a newline);
|
|
836
|
-
// adjacent non-blank lines fold to single space.
|
|
837
|
-
var folded = "";
|
|
838
|
-
for (var n = 0; n < contentLines.length; n++) {
|
|
839
|
-
var cl = contentLines[n];
|
|
840
|
-
if (cl === "") {
|
|
841
|
-
folded += "\n";
|
|
842
|
-
continue;
|
|
843
|
-
}
|
|
844
|
-
if (folded.length > 0 && !folded.endsWith("\n")) {
|
|
845
|
-
folded += " ";
|
|
846
|
-
}
|
|
847
|
-
folded += cl;
|
|
848
|
-
}
|
|
849
|
-
body = folded;
|
|
850
|
-
if (contentLines.length > 0) body += "\n";
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
if (chomp === "-") {
|
|
854
|
-
// strip — remove trailing newline(s)
|
|
855
|
-
body = body.replace(/\n+$/, "");
|
|
856
|
-
} else if (chomp === "+") {
|
|
857
|
-
// keep — restore trailing blanks we popped
|
|
858
|
-
body += "\n".repeat(trailingBlanks);
|
|
859
|
-
} /* else "clip" — keep one trailing newline (already there) */
|
|
860
|
-
|
|
861
|
-
return { value: body, nextLine: k };
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
function _parseScalarOrFlow(startIdx, indent) {
|
|
865
|
-
var ln = lines[startIdx];
|
|
866
|
-
var content = _stripEolComment(ln.content);
|
|
867
|
-
var v = _parseInlineValue(content, ln.lineNumber, ln.indent + 1);
|
|
868
|
-
return { value: v, nextLine: startIdx + 1 };
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// ---- Top-level dispatch ----
|
|
872
|
-
var top = parseValueAtLine(idx, -1, 1);
|
|
873
|
-
|
|
874
|
-
// Normalize null-prototype objects to plain {} so JSON.stringify and
|
|
875
|
-
// for-in behave naturally for the operator.
|
|
876
|
-
function _normalize(value) {
|
|
877
|
-
if (Array.isArray(value)) return value.map(_normalize);
|
|
878
|
-
if (value && typeof value === "object" && !(value instanceof Date)) {
|
|
879
|
-
var out = {};
|
|
880
|
-
for (var k in value) {
|
|
881
|
-
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
|
882
|
-
out[k] = _normalize(value[k]);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
return out;
|
|
886
|
-
}
|
|
887
|
-
return value;
|
|
888
|
-
}
|
|
889
|
-
return _normalize(top.value);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// ---- Pre-validation: scan input for banned constructs ----
|
|
893
|
-
//
|
|
894
|
-
// We mask out string content (single-quoted, double-quoted) and block-
|
|
895
|
-
// scalar bodies before scanning, so legitimate string content with `&`
|
|
896
|
-
// or `!` characters doesn't false-positive.
|
|
897
|
-
function _preValidate(input) {
|
|
898
|
-
// Build a "scan-safe" copy where quoted-string bodies and block-scalar
|
|
899
|
-
// bodies are replaced with same-length whitespace. Simple state machine.
|
|
900
|
-
var len = input.length;
|
|
901
|
-
var line = 1;
|
|
902
|
-
var col = 1;
|
|
903
|
-
var i = 0;
|
|
904
|
-
var safe = "";
|
|
905
|
-
|
|
906
|
-
function advance(n) {
|
|
907
|
-
n = n == null ? 1 : n;
|
|
908
|
-
for (var z = 0; z < n; z++) {
|
|
909
|
-
if (i < len && input.charCodeAt(i) === 0x0A) { line += 1; col = 1; }
|
|
910
|
-
else col += 1;
|
|
911
|
-
i += 1;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
while (i < len) {
|
|
916
|
-
var c = input.charAt(i);
|
|
917
|
-
// Comments — pass through
|
|
918
|
-
if (c === "#") {
|
|
919
|
-
while (i < len && input.charAt(i) !== "\n") {
|
|
920
|
-
safe += input.charAt(i);
|
|
921
|
-
i += 1;
|
|
922
|
-
col += 1;
|
|
923
|
-
}
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
// Quoted strings — mask body
|
|
927
|
-
if (c === '"' || c === "'") {
|
|
928
|
-
var quote = c;
|
|
929
|
-
var startLine = line;
|
|
930
|
-
var startCol = col;
|
|
931
|
-
safe += c;
|
|
932
|
-
advance();
|
|
933
|
-
while (i < len) {
|
|
934
|
-
var ch = input.charAt(i);
|
|
935
|
-
if (quote === '"' && ch === "\\" && i + 1 < len) {
|
|
936
|
-
// mask both
|
|
937
|
-
safe += " ";
|
|
938
|
-
advance(2);
|
|
939
|
-
continue;
|
|
940
|
-
}
|
|
941
|
-
if (ch === quote) {
|
|
942
|
-
if (quote === "'" && input.charAt(i + 1) === "'") {
|
|
943
|
-
safe += " ";
|
|
944
|
-
advance(2);
|
|
945
|
-
continue;
|
|
946
|
-
}
|
|
947
|
-
safe += ch;
|
|
948
|
-
advance();
|
|
949
|
-
break;
|
|
950
|
-
}
|
|
951
|
-
// Mask content (preserve newlines for line counting)
|
|
952
|
-
safe += (ch === "\n") ? "\n" : " ";
|
|
953
|
-
advance();
|
|
954
|
-
}
|
|
955
|
-
if (i > len) {
|
|
956
|
-
throw new SafeYamlError("unterminated quoted string",
|
|
957
|
-
"yaml/unterminated-string", startLine, startCol);
|
|
958
|
-
}
|
|
959
|
-
continue;
|
|
960
|
-
}
|
|
961
|
-
// Pass through everything else
|
|
962
|
-
safe += c;
|
|
963
|
-
advance();
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// Now scan `safe` for banned constructs.
|
|
967
|
-
// Banned tokens (must be at line-start or after whitespace, not in keys):
|
|
968
|
-
// &name anchor
|
|
969
|
-
// *name alias
|
|
970
|
-
// !tag, !!tag
|
|
971
|
-
// %YAML / %TAG directive (only at column 0)
|
|
972
|
-
|
|
973
|
-
// Anchors: `&` followed by a name char, with whitespace or `-`/`?` before
|
|
974
|
-
// (or start of value position). A simple heuristic: any unescaped `&`
|
|
975
|
-
// that's followed by an identifier char and is preceded by space or
|
|
976
|
-
// line start is an anchor. Same for `*`.
|
|
977
|
-
var anchorOrAliasRe = /(^|\s)([&*])([A-Za-z0-9_][A-Za-z0-9_-]*)/;
|
|
978
|
-
var m = safe.match(anchorOrAliasRe);
|
|
979
|
-
if (m) {
|
|
980
|
-
var posIdx = safe.indexOf(m[0]);
|
|
981
|
-
var lineCount = safe.substring(0, posIdx).split("\n").length;
|
|
982
|
-
throw new SafeYamlError(
|
|
983
|
-
m[2] === "&" ? "anchors are not supported" : "aliases are not supported",
|
|
984
|
-
m[2] === "&" ? "yaml/anchors-banned" : "yaml/aliases-banned",
|
|
985
|
-
lineCount, 1
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// Tags: `!` at start of value or after `: ` / `- `. False-positive risk:
|
|
990
|
-
// "key: !something" vs "key: ![bracket". We match `!` followed by
|
|
991
|
-
// alphanumeric or `<`.
|
|
992
|
-
var tagRe = /(^|[\s-])(!{1,2}[A-Za-z<])/;
|
|
993
|
-
var mt = safe.match(tagRe);
|
|
994
|
-
if (mt) {
|
|
995
|
-
var tagIdx = safe.indexOf(mt[0]);
|
|
996
|
-
var tagLine = safe.substring(0, tagIdx).split("\n").length;
|
|
997
|
-
throw new SafeYamlError("tags are not supported",
|
|
998
|
-
"yaml/tags-banned", tagLine, 1);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// Directives: `%YAML` or `%TAG` at column 0
|
|
1002
|
-
var dirRe = /(^|\n)%(YAML|TAG)\b/;
|
|
1003
|
-
var md = safe.match(dirRe);
|
|
1004
|
-
if (md) {
|
|
1005
|
-
var dirIdx = safe.indexOf(md[0]);
|
|
1006
|
-
var dirLine = safe.substring(0, dirIdx).split("\n").length + (md[1] === "\n" ? 1 : 0);
|
|
1007
|
-
throw new SafeYamlError("directives are not supported",
|
|
1008
|
-
"yaml/directives-banned", dirLine, 1);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
module.exports = {
|
|
1013
|
-
parse: parse,
|
|
1014
|
-
SafeYamlError: SafeYamlError,
|
|
1015
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Security-focused YAML parser (safe subset of YAML 1.2).
|
|
4
|
+
*
|
|
5
|
+
* YAML's full grammar is large and historically the source of many CVEs:
|
|
6
|
+
* `!!python/object` deserialization in PyYAML, anchor-cycle billion-laughs
|
|
7
|
+
* DoS, tag-based RCE chains. This parser implements only the JSON-shaped
|
|
8
|
+
* subset that real-world config files actually use, and REJECTS the rest
|
|
9
|
+
* with explicit errors.
|
|
10
|
+
*
|
|
11
|
+
* Supported:
|
|
12
|
+
* - Block mappings: `key: value`, indented children
|
|
13
|
+
* - Block sequences: `- item`, indented multi-line items
|
|
14
|
+
* - Flow mappings: `{ key: value, ... }`
|
|
15
|
+
* - Flow sequences: `[a, b, c]`
|
|
16
|
+
* - Plain scalars with YAML 1.2 *core schema* type inference:
|
|
17
|
+
* null / Null / NULL / ~ / empty null
|
|
18
|
+
* true / True / TRUE / false / False boolean
|
|
19
|
+
* (only these forms — NOT yes/no/on/off, fixing the
|
|
20
|
+
* "Norway problem" where country: NO became country: false)
|
|
21
|
+
* integers: 0, -17, 0o755, 0x1F number
|
|
22
|
+
* floats: 3.14, 5e+22, .inf, -.inf, .nan number
|
|
23
|
+
* everything else string
|
|
24
|
+
* - Single-quoted strings (no escapes; `''` for literal apostrophe)
|
|
25
|
+
* - Double-quoted strings (\n, \t, \\, \", \uXXXX, \UXXXXXXXX)
|
|
26
|
+
* - Block scalars: literal `|` and folded `>` with chomp indicators
|
|
27
|
+
* (-, +, or default)
|
|
28
|
+
* - `#` comments (line-rest)
|
|
29
|
+
*
|
|
30
|
+
* REJECTED with explicit errors:
|
|
31
|
+
* - Anchors `&name` / aliases `*name` (billion-laughs / cycles)
|
|
32
|
+
* - Tags `!tag`, `!!tag`, `!<uri>` (tag-driven deserialization)
|
|
33
|
+
* - YAML directives `%YAML`, `%TAG`
|
|
34
|
+
* - Multi-document streams (`---` / `...` separators)
|
|
35
|
+
* - Complex keys (`? mapping_key`)
|
|
36
|
+
* - Merge keys `<<` (anchor-using feature)
|
|
37
|
+
* - Tabs in indentation (per YAML 1.2 spec)
|
|
38
|
+
* - __proto__ / constructor / prototype as map keys (prototype pollution)
|
|
39
|
+
*
|
|
40
|
+
* Output shape:
|
|
41
|
+
* - mappings plain {} object
|
|
42
|
+
* - sequences JS array
|
|
43
|
+
* - scalars string / number / boolean / null per core schema
|
|
44
|
+
*
|
|
45
|
+
* Public API:
|
|
46
|
+
* yaml.parse(input, opts?) value | throws SafeYamlError
|
|
47
|
+
* yaml.SafeYamlError error class
|
|
48
|
+
*
|
|
49
|
+
* Defaults:
|
|
50
|
+
* maxBytes: 1 MiB
|
|
51
|
+
* maxDepth: 100
|
|
52
|
+
* maxKeys: 50000
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
var C = require("../constants");
|
|
56
|
+
var numericBounds = require("../numeric-bounds");
|
|
57
|
+
var safeBuffer = require("../safe-buffer");
|
|
58
|
+
var { FrameworkError } = require("../framework-error");
|
|
59
|
+
|
|
60
|
+
class SafeYamlError extends FrameworkError {
|
|
61
|
+
constructor(message, code, line, col) {
|
|
62
|
+
super(line != null ? message + " at line " + line + ":" + col : message);
|
|
63
|
+
this.name = "SafeYamlError";
|
|
64
|
+
this.code = code || "yaml/invalid";
|
|
65
|
+
this.line = line == null ? null : line;
|
|
66
|
+
this.col = col == null ? null : col;
|
|
67
|
+
this.isSafeYamlError = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// parseInt radices, named so the line itself doesn't carry a bare 8 / 16
|
|
72
|
+
// integer literal that reads as a byte count.
|
|
73
|
+
var RADIX_OCTAL = 0x8;
|
|
74
|
+
var RADIX_HEX = 0x10;
|
|
75
|
+
|
|
76
|
+
// Defensive cap on the size of a single scalar token before regex
|
|
77
|
+
// classification. The parser already bounds the whole input via maxBytes;
|
|
78
|
+
// this cap keeps a pathological single-line scalar from feeding the type-
|
|
79
|
+
// inference regexes an arbitrarily long string.
|
|
80
|
+
var MAX_SCALAR_BYTES = C.BYTES.kib(64);
|
|
81
|
+
|
|
82
|
+
var DEFAULTS = {
|
|
83
|
+
maxBytes: C.BYTES.mib(1),
|
|
84
|
+
maxDepth: 100,
|
|
85
|
+
maxKeys: 50_000,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
89
|
+
|
|
90
|
+
// YAML 1.2 core-schema scalar resolution. Order matters: null first
|
|
91
|
+
// (covers ~ and empty), then bool, then int (with base prefixes), then
|
|
92
|
+
// float, then string fallback.
|
|
93
|
+
var NULL_RE = /^(null|Null|NULL|~|)$/;
|
|
94
|
+
var BOOL_RE = /^(true|True|TRUE|false|False|FALSE)$/;
|
|
95
|
+
var INT_RE = /^[-+]?(0|[1-9][0-9]*)$/;
|
|
96
|
+
var INT_OCT = /^0o[0-7]+$/;
|
|
97
|
+
var INT_HEX = /^0x[0-9a-fA-F]+$/;
|
|
98
|
+
var FLOAT_RE = /^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$/;
|
|
99
|
+
var FLOAT_INF = /^[-+]?\.(inf|Inf|INF)$/;
|
|
100
|
+
var FLOAT_NAN = /^\.(nan|NaN|NAN)$/;
|
|
101
|
+
|
|
102
|
+
function _resolveScalar(s) {
|
|
103
|
+
// Fall back to string for any token whose length exceeds the scalar cap
|
|
104
|
+
// before running type-inference regexes. The parser caller has already
|
|
105
|
+
// applied the input-wide maxBytes cap; this is a per-token guard so the
|
|
106
|
+
// type-inference regexes never see a pathologically long string.
|
|
107
|
+
if (typeof s !== "string" || s.length > MAX_SCALAR_BYTES) return s;
|
|
108
|
+
// Below: every regex test sees an `s` whose s.length <= MAX_SCALAR_BYTES.
|
|
109
|
+
if (NULL_RE.test(s)) return null;
|
|
110
|
+
if (BOOL_RE.test(s)) return s.toLowerCase() === "true";
|
|
111
|
+
// s.length <= MAX_SCALAR_BYTES asserted at function entry above.
|
|
112
|
+
if (INT_RE.test(s)) {
|
|
113
|
+
var n = parseInt(s, 10);
|
|
114
|
+
if (Number.isSafeInteger(n)) return n;
|
|
115
|
+
return s; // fallback to string for huge ints (don't lose precision silently)
|
|
116
|
+
}
|
|
117
|
+
// s.length <= MAX_SCALAR_BYTES asserted at function entry above.
|
|
118
|
+
if (INT_OCT.test(s)) {
|
|
119
|
+
var oct = parseInt(s.substring(2), RADIX_OCTAL);
|
|
120
|
+
if (Number.isSafeInteger(oct)) return oct;
|
|
121
|
+
return s;
|
|
122
|
+
}
|
|
123
|
+
// s.length <= MAX_SCALAR_BYTES asserted at function entry above.
|
|
124
|
+
if (INT_HEX.test(s)) {
|
|
125
|
+
var hex = parseInt(s.substring(2), RADIX_HEX);
|
|
126
|
+
if (Number.isSafeInteger(hex)) return hex;
|
|
127
|
+
return s;
|
|
128
|
+
}
|
|
129
|
+
// s.length <= MAX_SCALAR_BYTES asserted at function entry above.
|
|
130
|
+
if (FLOAT_INF.test(s)) return s.charAt(0) === "-" ? -Infinity : Infinity;
|
|
131
|
+
if (FLOAT_NAN.test(s)) return NaN;
|
|
132
|
+
// s.length <= MAX_SCALAR_BYTES asserted at function entry above.
|
|
133
|
+
if (FLOAT_RE.test(s)) {
|
|
134
|
+
var f = parseFloat(s);
|
|
135
|
+
if (!isNaN(f)) return f;
|
|
136
|
+
}
|
|
137
|
+
return s;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parse(input, opts) {
|
|
141
|
+
opts = opts || {};
|
|
142
|
+
// Validate operator-supplied numeric opts via lib/numeric-bounds —
|
|
143
|
+
// Infinity / NaN / negative / non-integer all bypass the
|
|
144
|
+
// `> 0` shape and silently lift the DoS cap they were meant to enforce.
|
|
145
|
+
if (opts.maxBytes !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxBytes)) {
|
|
146
|
+
throw new SafeYamlError("yaml.parse: maxBytes must be a positive finite integer; got " +
|
|
147
|
+
numericBounds.shape(opts.maxBytes), "yaml/bad-opt");
|
|
148
|
+
}
|
|
149
|
+
if (opts.maxDepth !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxDepth)) {
|
|
150
|
+
throw new SafeYamlError("yaml.parse: maxDepth must be a positive finite integer; got " +
|
|
151
|
+
numericBounds.shape(opts.maxDepth), "yaml/bad-opt");
|
|
152
|
+
}
|
|
153
|
+
if (opts.maxKeys !== undefined && !numericBounds.isPositiveFiniteInt(opts.maxKeys)) {
|
|
154
|
+
throw new SafeYamlError("yaml.parse: maxKeys must be a positive finite integer; got " +
|
|
155
|
+
numericBounds.shape(opts.maxKeys), "yaml/bad-opt");
|
|
156
|
+
}
|
|
157
|
+
var maxBytes = opts.maxBytes !== undefined
|
|
158
|
+
? Math.min(opts.maxBytes, C.BYTES.mib(64)) : DEFAULTS.maxBytes;
|
|
159
|
+
var maxDepth = opts.maxDepth !== undefined
|
|
160
|
+
? Math.min(opts.maxDepth, 1_000) : DEFAULTS.maxDepth;
|
|
161
|
+
var maxKeys = opts.maxKeys !== undefined
|
|
162
|
+
? Math.min(opts.maxKeys, 1_000_000) : DEFAULTS.maxKeys;
|
|
163
|
+
|
|
164
|
+
input = safeBuffer.normalizeText(input, {
|
|
165
|
+
maxBytes: maxBytes,
|
|
166
|
+
errorClass: SafeYamlError,
|
|
167
|
+
typeCode: "yaml/wrong-input-type",
|
|
168
|
+
sizeCode: "yaml/too-large",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Pre-validate: scan for banned constructs ANYWHERE in the input. Cheap
|
|
172
|
+
// up-front rejection beats partial-parse-then-throw, and it surfaces a
|
|
173
|
+
// clearer diagnostic than "unexpected & at line 47".
|
|
174
|
+
// Skip bans that appear inside quoted strings — those are legitimate
|
|
175
|
+
// string content. Block-scalar content also exempt.
|
|
176
|
+
_preValidate(input);
|
|
177
|
+
|
|
178
|
+
// Normalize line endings: CRLF / CR → LF for consistent line-based work.
|
|
179
|
+
input = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
180
|
+
|
|
181
|
+
// Split into raw lines preserving line numbers.
|
|
182
|
+
var rawLines = input.split("\n");
|
|
183
|
+
var lines = [];
|
|
184
|
+
for (var i = 0; i < rawLines.length; i++) {
|
|
185
|
+
var raw = rawLines[i];
|
|
186
|
+
// Strip trailing whitespace from the line for analysis (block-scalar
|
|
187
|
+
// content paths preserve their own internal spacing separately).
|
|
188
|
+
var trimmed = raw.replace(safeBuffer.TRAILING_HSPACE_RE, "");
|
|
189
|
+
var indent = 0;
|
|
190
|
+
while (indent < raw.length && raw.charAt(indent) === " ") indent += 1;
|
|
191
|
+
if (indent < raw.length && raw.charAt(indent) === "\t") {
|
|
192
|
+
throw new SafeYamlError("tab in indentation (YAML 1.2 forbids)", "yaml/tab-indent", i + 1, indent + 1);
|
|
193
|
+
}
|
|
194
|
+
var content = raw.substring(indent);
|
|
195
|
+
var isBlank = content.length === 0;
|
|
196
|
+
var isComment = content.charAt(0) === "#";
|
|
197
|
+
lines.push({
|
|
198
|
+
lineNumber: i + 1,
|
|
199
|
+
raw: raw,
|
|
200
|
+
indent: indent,
|
|
201
|
+
content: content,
|
|
202
|
+
trimmed: trimmed,
|
|
203
|
+
isBlank: isBlank,
|
|
204
|
+
isComment: isComment,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Skip leading blank/comment lines and an optional `---` document marker.
|
|
209
|
+
// Reject any later `---` or `...` (multi-document streams not supported).
|
|
210
|
+
var idx = 0;
|
|
211
|
+
while (idx < lines.length && (lines[idx].isBlank || lines[idx].isComment)) idx += 1;
|
|
212
|
+
if (idx < lines.length && /^---(\s|$)/.test(lines[idx].content)) idx += 1;
|
|
213
|
+
// Subsequent doc markers anywhere = reject.
|
|
214
|
+
for (var j = idx; j < lines.length; j++) {
|
|
215
|
+
var c = lines[j].content;
|
|
216
|
+
if (/^---(\s|$)/.test(c) || /^\.\.\.(\s|$)/.test(c)) {
|
|
217
|
+
throw new SafeYamlError(
|
|
218
|
+
"multi-document YAML streams are not supported",
|
|
219
|
+
"yaml/multi-document", lines[j].lineNumber, 1
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
var keyCount = 0;
|
|
225
|
+
|
|
226
|
+
function _bumpKeys(line) {
|
|
227
|
+
keyCount += 1;
|
|
228
|
+
if (keyCount > maxKeys) {
|
|
229
|
+
throw new SafeYamlError("input exceeds maxKeys", "yaml/too-many-keys", line, 1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---- Recursive parse ----
|
|
234
|
+
// parseValue takes a starting line index and the *minimum* indent the
|
|
235
|
+
// value occupies. Returns { value, nextLine }.
|
|
236
|
+
|
|
237
|
+
function parseValueAtLine(startIdx, parentIndent, depth) {
|
|
238
|
+
if (depth > maxDepth) {
|
|
239
|
+
throw new SafeYamlError("input exceeds maxDepth", "yaml/too-deep",
|
|
240
|
+
startIdx < lines.length ? lines[startIdx].lineNumber : null, 1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Skip blank + comment lines.
|
|
244
|
+
var k = startIdx;
|
|
245
|
+
while (k < lines.length && (lines[k].isBlank || lines[k].isComment)) k += 1;
|
|
246
|
+
if (k >= lines.length) return { value: null, nextLine: lines.length };
|
|
247
|
+
|
|
248
|
+
var firstLine = lines[k];
|
|
249
|
+
if (firstLine.indent <= parentIndent) {
|
|
250
|
+
// No content for this value — it's an empty value (e.g. `key:` with nothing after)
|
|
251
|
+
return { value: null, nextLine: k };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
var content = firstLine.content;
|
|
255
|
+
var indent = firstLine.indent;
|
|
256
|
+
|
|
257
|
+
// Block sequence: line starts with `- ` or just `-` (then empty / value)
|
|
258
|
+
if (content === "-" || content.startsWith("- ")) {
|
|
259
|
+
return _parseBlockSequence(k, indent, depth);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Block mapping starts with a key (bare or quoted) followed by `:`.
|
|
263
|
+
var keyRange = _scanKeyRange(content, firstLine.lineNumber, indent);
|
|
264
|
+
if (keyRange) {
|
|
265
|
+
return _parseBlockMapping(k, indent, depth);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Block scalar header: `|` or `>` (with optional chomp/indent indicator)
|
|
269
|
+
if (/^[|>][-+]?[0-9]?\s*(#.*)?$/.test(content)) {
|
|
270
|
+
return _parseBlockScalar(k, indent, content);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Otherwise: a single scalar (possibly continued via flow style on
|
|
274
|
+
// following lines, but for the safe subset we restrict scalars to
|
|
275
|
+
// single-line plain or quoted, or wrapped flow on a single line).
|
|
276
|
+
return _parseScalarOrFlow(k, indent);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// _scanKeyRange returns { keyLiteral, valueStart } if the line starts
|
|
280
|
+
// with a bare-or-quoted key followed by ":" + (space|EOL), else null.
|
|
281
|
+
function _scanKeyRange(content, lineNumber, indent) {
|
|
282
|
+
var p = 0;
|
|
283
|
+
var len = content.length;
|
|
284
|
+
if (len === 0) return null;
|
|
285
|
+
if (content.charAt(0) === "?") {
|
|
286
|
+
throw new SafeYamlError("complex keys (`? key`) are not supported",
|
|
287
|
+
"yaml/complex-key-banned", lineNumber, indent + 1);
|
|
288
|
+
}
|
|
289
|
+
if (content.charAt(0) === '"') {
|
|
290
|
+
// Find closing quote
|
|
291
|
+
var i = 1;
|
|
292
|
+
while (i < len) {
|
|
293
|
+
var ch = content.charAt(i);
|
|
294
|
+
if (ch === "\\") { i += 2; continue; }
|
|
295
|
+
if (ch === '"') break;
|
|
296
|
+
i += 1;
|
|
297
|
+
}
|
|
298
|
+
if (i >= len) return null; // unterminated → not a key
|
|
299
|
+
p = i + 1;
|
|
300
|
+
} else if (content.charAt(0) === "'") {
|
|
301
|
+
var j = 1;
|
|
302
|
+
while (j < len) {
|
|
303
|
+
if (content.charAt(j) === "'") {
|
|
304
|
+
if (content.charAt(j + 1) === "'") { j += 2; continue; }
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
j += 1;
|
|
308
|
+
}
|
|
309
|
+
if (j >= len) return null;
|
|
310
|
+
p = j + 1;
|
|
311
|
+
} else {
|
|
312
|
+
// Plain scalar key: read up to ':' followed by space or EOL.
|
|
313
|
+
while (p < len) {
|
|
314
|
+
if (content.charAt(p) === ":" &&
|
|
315
|
+
(p + 1 === len || content.charAt(p + 1) === " ")) {
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
p += 1;
|
|
319
|
+
}
|
|
320
|
+
if (p >= len) return null;
|
|
321
|
+
}
|
|
322
|
+
if (content.charAt(p) !== ":") return null;
|
|
323
|
+
var afterColon = p + 1;
|
|
324
|
+
if (afterColon !== len && content.charAt(afterColon) !== " ") return null;
|
|
325
|
+
return { keyEnd: p, valueStart: afterColon };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function _decodeKeyLiteral(raw, lineNumber, col) {
|
|
329
|
+
if (raw.charAt(0) === '"') {
|
|
330
|
+
return _decodeDoubleQuoted(raw, lineNumber, col);
|
|
331
|
+
}
|
|
332
|
+
if (raw.charAt(0) === "'") {
|
|
333
|
+
return _decodeSingleQuoted(raw, lineNumber, col);
|
|
334
|
+
}
|
|
335
|
+
var trimmed = raw.replace(safeBuffer.TRAILING_HSPACE_RE, "");
|
|
336
|
+
if (POISONED_KEYS.has(trimmed)) {
|
|
337
|
+
throw new SafeYamlError("forbidden key '" + trimmed + "'",
|
|
338
|
+
"yaml/poisoned-key", lineNumber, col);
|
|
339
|
+
}
|
|
340
|
+
return trimmed;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function _parseBlockMapping(startIdx, indent, depth) {
|
|
344
|
+
var result = Object.create(null);
|
|
345
|
+
var seen = new Set();
|
|
346
|
+
var k = startIdx;
|
|
347
|
+
while (k < lines.length) {
|
|
348
|
+
var ln = lines[k];
|
|
349
|
+
if (ln.isBlank || ln.isComment) { k += 1; continue; }
|
|
350
|
+
if (ln.indent < indent) break;
|
|
351
|
+
if (ln.indent > indent) {
|
|
352
|
+
// Shouldn't happen if our caller dispatched correctly
|
|
353
|
+
throw new SafeYamlError("unexpected indent", "yaml/bad-indent", ln.lineNumber, ln.indent + 1);
|
|
354
|
+
}
|
|
355
|
+
// Reject a block sequence at the same indent — that's not a mapping.
|
|
356
|
+
if (ln.content === "-" || ln.content.startsWith("- ")) {
|
|
357
|
+
throw new SafeYamlError("sequence item where mapping key expected",
|
|
358
|
+
"yaml/expected-key", ln.lineNumber, ln.indent + 1);
|
|
359
|
+
}
|
|
360
|
+
var keyRange = _scanKeyRange(ln.content, ln.lineNumber, ln.indent);
|
|
361
|
+
if (!keyRange) {
|
|
362
|
+
throw new SafeYamlError("expected mapping key 'name:'",
|
|
363
|
+
"yaml/expected-key", ln.lineNumber, ln.indent + 1);
|
|
364
|
+
}
|
|
365
|
+
var keyLiteral = ln.content.substring(0, keyRange.keyEnd);
|
|
366
|
+
var key = _decodeKeyLiteral(keyLiteral, ln.lineNumber, ln.indent + 1);
|
|
367
|
+
if (typeof key !== "string") {
|
|
368
|
+
throw new SafeYamlError("non-string mapping key not supported",
|
|
369
|
+
"yaml/bad-key", ln.lineNumber, ln.indent + 1);
|
|
370
|
+
}
|
|
371
|
+
if (POISONED_KEYS.has(key)) {
|
|
372
|
+
throw new SafeYamlError("forbidden key '" + key + "'",
|
|
373
|
+
"yaml/poisoned-key", ln.lineNumber, ln.indent + 1);
|
|
374
|
+
}
|
|
375
|
+
if (key === "<<") {
|
|
376
|
+
throw new SafeYamlError("merge key '<<' not supported (anchor-using feature)",
|
|
377
|
+
"yaml/merge-key-banned", ln.lineNumber, ln.indent + 1);
|
|
378
|
+
}
|
|
379
|
+
if (seen.has(key)) {
|
|
380
|
+
throw new SafeYamlError("duplicate mapping key '" + key + "'",
|
|
381
|
+
"yaml/duplicate-key", ln.lineNumber, ln.indent + 1);
|
|
382
|
+
}
|
|
383
|
+
seen.add(key);
|
|
384
|
+
_bumpKeys(ln.lineNumber);
|
|
385
|
+
|
|
386
|
+
// Determine the value:
|
|
387
|
+
// 1. if there's content after the colon, that's a flow value or
|
|
388
|
+
// a plain scalar continuation on the same line.
|
|
389
|
+
// 2. otherwise the value lives on subsequent more-indented lines.
|
|
390
|
+
var afterColon = ln.content.substring(keyRange.valueStart).replace(/^[ \t]+/, "");
|
|
391
|
+
// Strip end-of-line comment
|
|
392
|
+
afterColon = _stripEolComment(afterColon);
|
|
393
|
+
var value;
|
|
394
|
+
if (afterColon.length > 0) {
|
|
395
|
+
// Inline value on this line.
|
|
396
|
+
if (afterColon.charAt(0) === "|" || afterColon.charAt(0) === ">") {
|
|
397
|
+
// Block scalar header inline with key
|
|
398
|
+
if (!/^[|>][-+]?[0-9]?\s*$/.test(afterColon)) {
|
|
399
|
+
throw new SafeYamlError("malformed block scalar header",
|
|
400
|
+
"yaml/bad-block-scalar", ln.lineNumber, ln.indent + 1);
|
|
401
|
+
}
|
|
402
|
+
var bs = _parseBlockScalar(k, indent, afterColon);
|
|
403
|
+
// Block scalar header was on this line; content is on the
|
|
404
|
+
// following lines. _parseBlockScalar handles the header
|
|
405
|
+
// tokenisation when given the header content directly.
|
|
406
|
+
value = bs.value;
|
|
407
|
+
k = bs.nextLine;
|
|
408
|
+
result[key] = value;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
value = _parseInlineValue(afterColon, ln.lineNumber, ln.indent + keyRange.valueStart);
|
|
412
|
+
k += 1;
|
|
413
|
+
} else {
|
|
414
|
+
// Value is on more-indented lines below.
|
|
415
|
+
var nested = parseValueAtLine(k + 1, indent, depth + 1);
|
|
416
|
+
value = nested.value;
|
|
417
|
+
k = nested.nextLine;
|
|
418
|
+
}
|
|
419
|
+
result[key] = value;
|
|
420
|
+
}
|
|
421
|
+
// Convert null-prototype to plain {} below in _normalize.
|
|
422
|
+
return { value: result, nextLine: k };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function _parseBlockSequence(startIdx, indent, depth) {
|
|
426
|
+
var arr = [];
|
|
427
|
+
var k = startIdx;
|
|
428
|
+
while (k < lines.length) {
|
|
429
|
+
var ln = lines[k];
|
|
430
|
+
if (ln.isBlank || ln.isComment) { k += 1; continue; }
|
|
431
|
+
if (ln.indent < indent) break;
|
|
432
|
+
if (ln.indent !== indent) {
|
|
433
|
+
throw new SafeYamlError("unexpected indent in sequence",
|
|
434
|
+
"yaml/bad-indent", ln.lineNumber, ln.indent + 1);
|
|
435
|
+
}
|
|
436
|
+
if (ln.content !== "-" && !ln.content.startsWith("- ")) break;
|
|
437
|
+
_bumpKeys(ln.lineNumber);
|
|
438
|
+
|
|
439
|
+
// Item value:
|
|
440
|
+
// - "- " followed by content → inline value (could be flow, scalar,
|
|
441
|
+
// or the start of a nested mapping if "key: value" pattern)
|
|
442
|
+
// - "-" alone → value on more-indented lines below
|
|
443
|
+
var afterDash = ln.content === "-" ? "" : ln.content.substring(2);
|
|
444
|
+
afterDash = _stripEolComment(afterDash);
|
|
445
|
+
var item;
|
|
446
|
+
if (afterDash.length === 0) {
|
|
447
|
+
var nested = parseValueAtLine(k + 1, indent, depth + 1);
|
|
448
|
+
item = nested.value;
|
|
449
|
+
k = nested.nextLine;
|
|
450
|
+
} else {
|
|
451
|
+
// Could be a nested mapping: `- key: value` (compact mapping)
|
|
452
|
+
var mapKey = _scanKeyRange(afterDash, ln.lineNumber, ln.indent + 2);
|
|
453
|
+
if (mapKey) {
|
|
454
|
+
// Treat as a single-line block mapping at indent+2 by
|
|
455
|
+
// synthesising a sub-region. The mapping's "indent" is the
|
|
456
|
+
// column where the key starts (ln.indent + 2).
|
|
457
|
+
// We need to feed _parseBlockMapping starting at this line but
|
|
458
|
+
// with the mapping treating its own indent as ln.indent + 2.
|
|
459
|
+
// Simplest: clone the line with the dash stripped, then continue
|
|
460
|
+
// with subsequent more-indented lines as the mapping's body.
|
|
461
|
+
var synthetic = {
|
|
462
|
+
lineNumber: ln.lineNumber,
|
|
463
|
+
raw: ln.raw,
|
|
464
|
+
indent: ln.indent + 2,
|
|
465
|
+
content: afterDash,
|
|
466
|
+
trimmed: afterDash,
|
|
467
|
+
isBlank: false,
|
|
468
|
+
isComment: false,
|
|
469
|
+
};
|
|
470
|
+
var saved = lines[k];
|
|
471
|
+
lines[k] = synthetic;
|
|
472
|
+
try {
|
|
473
|
+
var sub = _parseBlockMapping(k, ln.indent + 2, depth + 1);
|
|
474
|
+
item = sub.value;
|
|
475
|
+
k = sub.nextLine;
|
|
476
|
+
} finally {
|
|
477
|
+
lines[saved.lineNumber - 1] = saved;
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
item = _parseInlineValue(afterDash, ln.lineNumber, ln.indent + 2);
|
|
481
|
+
k += 1;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
arr.push(item);
|
|
485
|
+
}
|
|
486
|
+
return { value: arr, nextLine: k };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function _parseInlineValue(text, lineNumber, col) {
|
|
490
|
+
// Strip trailing whitespace/comment already done by caller (mostly).
|
|
491
|
+
// Handle: flow [...] / {...}, quoted strings, plain scalars.
|
|
492
|
+
var t = text;
|
|
493
|
+
if (t.charAt(0) === "[") return _parseFlowSequence(t, lineNumber, col, 0);
|
|
494
|
+
if (t.charAt(0) === "{") return _parseFlowMapping(t, lineNumber, col, 0);
|
|
495
|
+
if (t.charAt(0) === '"') {
|
|
496
|
+
var dq = _decodeDoubleQuoted(t, lineNumber, col);
|
|
497
|
+
var afterDq = _trailingAfterQuoted(t, '"');
|
|
498
|
+
if (afterDq.length > 0 && afterDq.replace(/^\s+/, "") !== "") {
|
|
499
|
+
throw new SafeYamlError("unexpected content after quoted string",
|
|
500
|
+
"yaml/trailing-content", lineNumber, col);
|
|
501
|
+
}
|
|
502
|
+
return dq;
|
|
503
|
+
}
|
|
504
|
+
if (t.charAt(0) === "'") {
|
|
505
|
+
var sq = _decodeSingleQuoted(t, lineNumber, col);
|
|
506
|
+
return sq;
|
|
507
|
+
}
|
|
508
|
+
// Plain scalar — strip trailing space, resolve type.
|
|
509
|
+
return _resolveScalar(t.replace(safeBuffer.TRAILING_HSPACE_RE, ""));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// For quoted-string termination check on a single-line value.
|
|
513
|
+
function _trailingAfterQuoted(text, quote) {
|
|
514
|
+
if (text.charAt(0) !== quote) return text;
|
|
515
|
+
var i = 1;
|
|
516
|
+
while (i < text.length) {
|
|
517
|
+
var ch = text.charAt(i);
|
|
518
|
+
if (quote === '"' && ch === "\\") { i += 2; continue; }
|
|
519
|
+
if (ch === quote) {
|
|
520
|
+
if (quote === "'" && text.charAt(i + 1) === "'") { i += 2; continue; }
|
|
521
|
+
return text.substring(i + 1);
|
|
522
|
+
}
|
|
523
|
+
i += 1;
|
|
524
|
+
}
|
|
525
|
+
return "";
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ---- Flow style ----
|
|
529
|
+
|
|
530
|
+
function _parseFlowSequence(text, lineNumber, col, depthIncoming) {
|
|
531
|
+
var p = 1; // skip [
|
|
532
|
+
var arr = [];
|
|
533
|
+
while (p < text.length) {
|
|
534
|
+
_flowSkipWs(text, p);
|
|
535
|
+
p = _flowSkipWsIndex(text, p);
|
|
536
|
+
if (text.charAt(p) === "]") return arr;
|
|
537
|
+
var v = _parseFlowValue(text, p, lineNumber, col, depthIncoming + 1);
|
|
538
|
+
arr.push(v.value);
|
|
539
|
+
p = v.nextPos;
|
|
540
|
+
p = _flowSkipWsIndex(text, p);
|
|
541
|
+
if (text.charAt(p) === ",") { p += 1; continue; }
|
|
542
|
+
if (text.charAt(p) === "]") { return arr; }
|
|
543
|
+
throw new SafeYamlError("expected ',' or ']' in flow sequence",
|
|
544
|
+
"yaml/bad-flow", lineNumber, col + p);
|
|
545
|
+
}
|
|
546
|
+
throw new SafeYamlError("unterminated flow sequence",
|
|
547
|
+
"yaml/unterminated-flow", lineNumber, col);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function _parseFlowMapping(text, lineNumber, col, depthIncoming) {
|
|
551
|
+
if (depthIncoming > maxDepth) {
|
|
552
|
+
throw new SafeYamlError("input exceeds maxDepth", "yaml/too-deep", lineNumber, col);
|
|
553
|
+
}
|
|
554
|
+
var p = 1;
|
|
555
|
+
var result = Object.create(null);
|
|
556
|
+
while (p < text.length) {
|
|
557
|
+
p = _flowSkipWsIndex(text, p);
|
|
558
|
+
if (text.charAt(p) === "}") { return result; }
|
|
559
|
+
// Read key
|
|
560
|
+
var keyVal = _parseFlowKey(text, p, lineNumber, col);
|
|
561
|
+
var key = keyVal.key;
|
|
562
|
+
if (typeof key !== "string") {
|
|
563
|
+
throw new SafeYamlError("non-string flow-mapping key",
|
|
564
|
+
"yaml/bad-key", lineNumber, col + p);
|
|
565
|
+
}
|
|
566
|
+
if (POISONED_KEYS.has(key)) {
|
|
567
|
+
throw new SafeYamlError("forbidden key '" + key + "'",
|
|
568
|
+
"yaml/poisoned-key", lineNumber, col + p);
|
|
569
|
+
}
|
|
570
|
+
_bumpKeys(lineNumber);
|
|
571
|
+
p = keyVal.nextPos;
|
|
572
|
+
p = _flowSkipWsIndex(text, p);
|
|
573
|
+
if (text.charAt(p) !== ":") {
|
|
574
|
+
throw new SafeYamlError("expected ':' in flow mapping",
|
|
575
|
+
"yaml/bad-flow", lineNumber, col + p);
|
|
576
|
+
}
|
|
577
|
+
p += 1;
|
|
578
|
+
p = _flowSkipWsIndex(text, p);
|
|
579
|
+
var valRes = _parseFlowValue(text, p, lineNumber, col, depthIncoming + 1);
|
|
580
|
+
result[key] = valRes.value;
|
|
581
|
+
p = valRes.nextPos;
|
|
582
|
+
p = _flowSkipWsIndex(text, p);
|
|
583
|
+
if (text.charAt(p) === ",") { p += 1; continue; }
|
|
584
|
+
if (text.charAt(p) === "}") { return result; }
|
|
585
|
+
throw new SafeYamlError("expected ',' or '}' in flow mapping",
|
|
586
|
+
"yaml/bad-flow", lineNumber, col + p);
|
|
587
|
+
}
|
|
588
|
+
throw new SafeYamlError("unterminated flow mapping",
|
|
589
|
+
"yaml/unterminated-flow", lineNumber, col);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function _parseFlowValue(text, p, lineNumber, col, depthIncoming) {
|
|
593
|
+
if (depthIncoming > maxDepth) {
|
|
594
|
+
throw new SafeYamlError("input exceeds maxDepth", "yaml/too-deep", lineNumber, col + p);
|
|
595
|
+
}
|
|
596
|
+
var ch = text.charAt(p);
|
|
597
|
+
if (ch === "[") {
|
|
598
|
+
var sub = _parseFlowSequence(text.substring(p), lineNumber, col + p, depthIncoming);
|
|
599
|
+
// We need to find where the matching ] closes — re-scan
|
|
600
|
+
var endP = _findMatchingBracket(text, p, "[", "]", lineNumber, col);
|
|
601
|
+
return { value: sub, nextPos: endP + 1 };
|
|
602
|
+
}
|
|
603
|
+
if (ch === "{") {
|
|
604
|
+
var subM = _parseFlowMapping(text.substring(p), lineNumber, col + p, depthIncoming);
|
|
605
|
+
var endM = _findMatchingBracket(text, p, "{", "}", lineNumber, col);
|
|
606
|
+
return { value: subM, nextPos: endM + 1 };
|
|
607
|
+
}
|
|
608
|
+
if (ch === '"') {
|
|
609
|
+
var dq = _decodeDoubleQuoted(text.substring(p), lineNumber, col + p);
|
|
610
|
+
var endQ = _findClosingQuote(text, p, '"', lineNumber, col);
|
|
611
|
+
return { value: dq, nextPos: endQ + 1 };
|
|
612
|
+
}
|
|
613
|
+
if (ch === "'") {
|
|
614
|
+
var sq = _decodeSingleQuoted(text.substring(p), lineNumber, col + p);
|
|
615
|
+
var endSQ = _findClosingQuote(text, p, "'", lineNumber, col);
|
|
616
|
+
return { value: sq, nextPos: endSQ + 1 };
|
|
617
|
+
}
|
|
618
|
+
// Plain scalar — read until , } ] or EOL
|
|
619
|
+
var start = p;
|
|
620
|
+
while (p < text.length) {
|
|
621
|
+
var c = text.charAt(p);
|
|
622
|
+
if (c === "," || c === "}" || c === "]") break;
|
|
623
|
+
p += 1;
|
|
624
|
+
}
|
|
625
|
+
var raw = text.substring(start, p).replace(safeBuffer.TRAILING_HSPACE_RE, "");
|
|
626
|
+
return { value: _resolveScalar(raw), nextPos: p };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function _parseFlowKey(text, p, lineNumber, col) {
|
|
630
|
+
var ch = text.charAt(p);
|
|
631
|
+
if (ch === '"') {
|
|
632
|
+
var dq = _decodeDoubleQuoted(text.substring(p), lineNumber, col + p);
|
|
633
|
+
var endQ = _findClosingQuote(text, p, '"', lineNumber, col);
|
|
634
|
+
return { key: dq, nextPos: endQ + 1 };
|
|
635
|
+
}
|
|
636
|
+
if (ch === "'") {
|
|
637
|
+
var sq = _decodeSingleQuoted(text.substring(p), lineNumber, col + p);
|
|
638
|
+
var endSQ = _findClosingQuote(text, p, "'", lineNumber, col);
|
|
639
|
+
return { key: sq, nextPos: endSQ + 1 };
|
|
640
|
+
}
|
|
641
|
+
// Plain key — read until ':' (followed by space/comma/end) or comma/end
|
|
642
|
+
var start = p;
|
|
643
|
+
while (p < text.length) {
|
|
644
|
+
var c = text.charAt(p);
|
|
645
|
+
if (c === ":" || c === "," || c === "}" || c === "]") break;
|
|
646
|
+
p += 1;
|
|
647
|
+
}
|
|
648
|
+
return { key: text.substring(start, p).replace(safeBuffer.TRAILING_HSPACE_RE, ""), nextPos: p };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function _findMatchingBracket(text, start, open, close, lineNumber, col) {
|
|
652
|
+
var depth = 0;
|
|
653
|
+
var i = start;
|
|
654
|
+
while (i < text.length) {
|
|
655
|
+
var c = text.charAt(i);
|
|
656
|
+
if (c === '"') { i = _findClosingQuote(text, i, '"', lineNumber, col) + 1; continue; }
|
|
657
|
+
if (c === "'") { i = _findClosingQuote(text, i, "'", lineNumber, col) + 1; continue; }
|
|
658
|
+
if (c === open) depth += 1;
|
|
659
|
+
else if (c === close) {
|
|
660
|
+
depth -= 1;
|
|
661
|
+
if (depth === 0) return i;
|
|
662
|
+
}
|
|
663
|
+
i += 1;
|
|
664
|
+
}
|
|
665
|
+
throw new SafeYamlError("unterminated flow brackets",
|
|
666
|
+
"yaml/unterminated-flow", lineNumber, col + start);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function _findClosingQuote(text, start, quote, lineNumber, col) {
|
|
670
|
+
var i = start + 1;
|
|
671
|
+
while (i < text.length) {
|
|
672
|
+
var c = text.charAt(i);
|
|
673
|
+
if (quote === '"' && c === "\\") { i += 2; continue; }
|
|
674
|
+
if (c === quote) {
|
|
675
|
+
if (quote === "'" && text.charAt(i + 1) === "'") { i += 2; continue; }
|
|
676
|
+
return i;
|
|
677
|
+
}
|
|
678
|
+
i += 1;
|
|
679
|
+
}
|
|
680
|
+
throw new SafeYamlError("unterminated quoted string",
|
|
681
|
+
"yaml/unterminated-string", lineNumber, col + start);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function _flowSkipWs(_text, _p) { /* no-op shim — real version below */ }
|
|
685
|
+
function _flowSkipWsIndex(text, p) {
|
|
686
|
+
while (p < text.length) {
|
|
687
|
+
var c = text.charAt(p);
|
|
688
|
+
if (c === " " || c === "\t") p += 1;
|
|
689
|
+
else break;
|
|
690
|
+
}
|
|
691
|
+
return p;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ---- Scalar decoding ----
|
|
695
|
+
|
|
696
|
+
function _decodeDoubleQuoted(raw, lineNumber, col) {
|
|
697
|
+
if (raw.charAt(0) !== '"') {
|
|
698
|
+
throw new SafeYamlError("expected '\"'", "yaml/bad-string", lineNumber, col);
|
|
699
|
+
}
|
|
700
|
+
var i = 1;
|
|
701
|
+
var out = "";
|
|
702
|
+
while (i < raw.length) {
|
|
703
|
+
var ch = raw.charAt(i);
|
|
704
|
+
if (ch === '"') return out;
|
|
705
|
+
if (ch === "\\") {
|
|
706
|
+
var esc = raw.charAt(i + 1);
|
|
707
|
+
switch (esc) {
|
|
708
|
+
case '"': out += '"'; i += 2; break;
|
|
709
|
+
case "\\": out += "\\"; i += 2; break;
|
|
710
|
+
case "/": out += "/"; i += 2; break;
|
|
711
|
+
case "n": out += "\n"; i += 2; break;
|
|
712
|
+
case "t": out += "\t"; i += 2; break;
|
|
713
|
+
case "r": out += "\r"; i += 2; break;
|
|
714
|
+
case "b": out += "\b"; i += 2; break;
|
|
715
|
+
case "f": out += "\f"; i += 2; break;
|
|
716
|
+
case "0": out += "\0"; i += 2; break;
|
|
717
|
+
case "u": {
|
|
718
|
+
var hex = raw.substring(i + 2, i + 6);
|
|
719
|
+
if (!safeBuffer.isHex(hex, 4)) {
|
|
720
|
+
throw new SafeYamlError("bad \\u escape", "yaml/bad-escape", lineNumber, col + i);
|
|
721
|
+
}
|
|
722
|
+
out += String.fromCharCode(parseInt(hex, RADIX_HEX));
|
|
723
|
+
i += 6;
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
case "U": {
|
|
727
|
+
var hex8 = raw.substring(i + 2, i + 10);
|
|
728
|
+
if (!/^[0-9a-fA-F]{8}$/.test(hex8)) {
|
|
729
|
+
throw new SafeYamlError("bad \\U escape", "yaml/bad-escape", lineNumber, col + i);
|
|
730
|
+
}
|
|
731
|
+
var code = parseInt(hex8, RADIX_HEX);
|
|
732
|
+
if (code > 0x10FFFF) {
|
|
733
|
+
throw new SafeYamlError("\\U code point > U+10FFFF",
|
|
734
|
+
"yaml/bad-escape", lineNumber, col + i);
|
|
735
|
+
}
|
|
736
|
+
out += String.fromCodePoint(code);
|
|
737
|
+
i += 10;
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
default:
|
|
741
|
+
throw new SafeYamlError("unknown escape '\\" + esc + "'",
|
|
742
|
+
"yaml/bad-escape", lineNumber, col + i);
|
|
743
|
+
}
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
out += ch;
|
|
747
|
+
i += 1;
|
|
748
|
+
}
|
|
749
|
+
throw new SafeYamlError("unterminated double-quoted string",
|
|
750
|
+
"yaml/unterminated-string", lineNumber, col);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function _decodeSingleQuoted(raw, lineNumber, col) {
|
|
754
|
+
if (raw.charAt(0) !== "'") {
|
|
755
|
+
throw new SafeYamlError("expected \"'\"", "yaml/bad-string", lineNumber, col);
|
|
756
|
+
}
|
|
757
|
+
var i = 1;
|
|
758
|
+
var out = "";
|
|
759
|
+
while (i < raw.length) {
|
|
760
|
+
var ch = raw.charAt(i);
|
|
761
|
+
if (ch === "'") {
|
|
762
|
+
if (raw.charAt(i + 1) === "'") { out += "'"; i += 2; continue; }
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
out += ch;
|
|
766
|
+
i += 1;
|
|
767
|
+
}
|
|
768
|
+
throw new SafeYamlError("unterminated single-quoted string",
|
|
769
|
+
"yaml/unterminated-string", lineNumber, col);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function _stripEolComment(text) {
|
|
773
|
+
// Strip ` #...` comments (must be preceded by whitespace) at end of line.
|
|
774
|
+
var match = text.match(/^(.*?)(\s+#.*)?$/);
|
|
775
|
+
return (match && match[1] != null ? match[1] : text).replace(safeBuffer.TRAILING_HSPACE_RE, "");
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ---- Block scalars (| literal, > folded) ----
|
|
779
|
+
|
|
780
|
+
function _parseBlockScalar(startIdx, parentIndent, headerContent) {
|
|
781
|
+
var headerLine = lines[startIdx];
|
|
782
|
+
var header = headerContent.trim();
|
|
783
|
+
var style = header.charAt(0); // '|' or '>'
|
|
784
|
+
var rest = header.substring(1);
|
|
785
|
+
var chomp = ""; // "-" strip, "+" keep, "" clip (default)
|
|
786
|
+
var explicitIndent = null;
|
|
787
|
+
for (var i = 0; i < rest.length; i++) {
|
|
788
|
+
var ch = rest.charAt(i);
|
|
789
|
+
if (ch === "-" || ch === "+") {
|
|
790
|
+
if (chomp) throw new SafeYamlError("multiple chomping indicators",
|
|
791
|
+
"yaml/bad-block-scalar", headerLine.lineNumber, parentIndent + 1);
|
|
792
|
+
chomp = ch;
|
|
793
|
+
} else if (ch >= "1" && ch <= "9") {
|
|
794
|
+
if (explicitIndent != null) throw new SafeYamlError("multiple indentation indicators",
|
|
795
|
+
"yaml/bad-block-scalar", headerLine.lineNumber, parentIndent + 1);
|
|
796
|
+
explicitIndent = parseInt(ch, 10);
|
|
797
|
+
} else if (ch === " " || ch === "\t" || ch === "#") {
|
|
798
|
+
// comment / trailing whitespace — stop reading indicators
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Collect content lines: anything more-indented than parentIndent.
|
|
804
|
+
// The first non-blank content line establishes the block's indent.
|
|
805
|
+
var k = startIdx + 1;
|
|
806
|
+
var blockIndent = null;
|
|
807
|
+
var contentLines = [];
|
|
808
|
+
while (k < lines.length) {
|
|
809
|
+
var ln = lines[k];
|
|
810
|
+
if (ln.isBlank) { contentLines.push(""); k += 1; continue; }
|
|
811
|
+
if (ln.indent <= parentIndent) break;
|
|
812
|
+
if (blockIndent === null) {
|
|
813
|
+
blockIndent = explicitIndent != null ? parentIndent + explicitIndent : ln.indent;
|
|
814
|
+
}
|
|
815
|
+
if (ln.indent < blockIndent) break;
|
|
816
|
+
var trimmedToIndent = ln.raw.substring(blockIndent);
|
|
817
|
+
contentLines.push(trimmedToIndent);
|
|
818
|
+
k += 1;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Trim trailing blank lines based on chomp.
|
|
822
|
+
var trailingBlanks = 0;
|
|
823
|
+
while (contentLines.length > 0 && contentLines[contentLines.length - 1] === "") {
|
|
824
|
+
contentLines.pop();
|
|
825
|
+
trailingBlanks += 1;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
var body;
|
|
829
|
+
if (style === "|") {
|
|
830
|
+
// Literal: each content line + a newline; preserve internal blanks
|
|
831
|
+
// (we just popped trailing ones for chomp handling).
|
|
832
|
+
body = contentLines.join("\n");
|
|
833
|
+
if (contentLines.length > 0) body += "\n";
|
|
834
|
+
} else {
|
|
835
|
+
// Folded: blank line separates paragraphs (becomes a newline);
|
|
836
|
+
// adjacent non-blank lines fold to single space.
|
|
837
|
+
var folded = "";
|
|
838
|
+
for (var n = 0; n < contentLines.length; n++) {
|
|
839
|
+
var cl = contentLines[n];
|
|
840
|
+
if (cl === "") {
|
|
841
|
+
folded += "\n";
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
if (folded.length > 0 && !folded.endsWith("\n")) {
|
|
845
|
+
folded += " ";
|
|
846
|
+
}
|
|
847
|
+
folded += cl;
|
|
848
|
+
}
|
|
849
|
+
body = folded;
|
|
850
|
+
if (contentLines.length > 0) body += "\n";
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (chomp === "-") {
|
|
854
|
+
// strip — remove trailing newline(s)
|
|
855
|
+
body = body.replace(/\n+$/, "");
|
|
856
|
+
} else if (chomp === "+") {
|
|
857
|
+
// keep — restore trailing blanks we popped
|
|
858
|
+
body += "\n".repeat(trailingBlanks);
|
|
859
|
+
} /* else "clip" — keep one trailing newline (already there) */
|
|
860
|
+
|
|
861
|
+
return { value: body, nextLine: k };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function _parseScalarOrFlow(startIdx, indent) {
|
|
865
|
+
var ln = lines[startIdx];
|
|
866
|
+
var content = _stripEolComment(ln.content);
|
|
867
|
+
var v = _parseInlineValue(content, ln.lineNumber, ln.indent + 1);
|
|
868
|
+
return { value: v, nextLine: startIdx + 1 };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ---- Top-level dispatch ----
|
|
872
|
+
var top = parseValueAtLine(idx, -1, 1);
|
|
873
|
+
|
|
874
|
+
// Normalize null-prototype objects to plain {} so JSON.stringify and
|
|
875
|
+
// for-in behave naturally for the operator.
|
|
876
|
+
function _normalize(value) {
|
|
877
|
+
if (Array.isArray(value)) return value.map(_normalize);
|
|
878
|
+
if (value && typeof value === "object" && !(value instanceof Date)) {
|
|
879
|
+
var out = {};
|
|
880
|
+
for (var k in value) {
|
|
881
|
+
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
|
882
|
+
out[k] = _normalize(value[k]);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return out;
|
|
886
|
+
}
|
|
887
|
+
return value;
|
|
888
|
+
}
|
|
889
|
+
return _normalize(top.value);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ---- Pre-validation: scan input for banned constructs ----
|
|
893
|
+
//
|
|
894
|
+
// We mask out string content (single-quoted, double-quoted) and block-
|
|
895
|
+
// scalar bodies before scanning, so legitimate string content with `&`
|
|
896
|
+
// or `!` characters doesn't false-positive.
|
|
897
|
+
function _preValidate(input) {
|
|
898
|
+
// Build a "scan-safe" copy where quoted-string bodies and block-scalar
|
|
899
|
+
// bodies are replaced with same-length whitespace. Simple state machine.
|
|
900
|
+
var len = input.length;
|
|
901
|
+
var line = 1;
|
|
902
|
+
var col = 1;
|
|
903
|
+
var i = 0;
|
|
904
|
+
var safe = "";
|
|
905
|
+
|
|
906
|
+
function advance(n) {
|
|
907
|
+
n = n == null ? 1 : n;
|
|
908
|
+
for (var z = 0; z < n; z++) {
|
|
909
|
+
if (i < len && input.charCodeAt(i) === 0x0A) { line += 1; col = 1; }
|
|
910
|
+
else col += 1;
|
|
911
|
+
i += 1;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
while (i < len) {
|
|
916
|
+
var c = input.charAt(i);
|
|
917
|
+
// Comments — pass through
|
|
918
|
+
if (c === "#") {
|
|
919
|
+
while (i < len && input.charAt(i) !== "\n") {
|
|
920
|
+
safe += input.charAt(i);
|
|
921
|
+
i += 1;
|
|
922
|
+
col += 1;
|
|
923
|
+
}
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
// Quoted strings — mask body
|
|
927
|
+
if (c === '"' || c === "'") {
|
|
928
|
+
var quote = c;
|
|
929
|
+
var startLine = line;
|
|
930
|
+
var startCol = col;
|
|
931
|
+
safe += c;
|
|
932
|
+
advance();
|
|
933
|
+
while (i < len) {
|
|
934
|
+
var ch = input.charAt(i);
|
|
935
|
+
if (quote === '"' && ch === "\\" && i + 1 < len) {
|
|
936
|
+
// mask both
|
|
937
|
+
safe += " ";
|
|
938
|
+
advance(2);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (ch === quote) {
|
|
942
|
+
if (quote === "'" && input.charAt(i + 1) === "'") {
|
|
943
|
+
safe += " ";
|
|
944
|
+
advance(2);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
safe += ch;
|
|
948
|
+
advance();
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
// Mask content (preserve newlines for line counting)
|
|
952
|
+
safe += (ch === "\n") ? "\n" : " ";
|
|
953
|
+
advance();
|
|
954
|
+
}
|
|
955
|
+
if (i > len) {
|
|
956
|
+
throw new SafeYamlError("unterminated quoted string",
|
|
957
|
+
"yaml/unterminated-string", startLine, startCol);
|
|
958
|
+
}
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
// Pass through everything else
|
|
962
|
+
safe += c;
|
|
963
|
+
advance();
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Now scan `safe` for banned constructs.
|
|
967
|
+
// Banned tokens (must be at line-start or after whitespace, not in keys):
|
|
968
|
+
// &name anchor
|
|
969
|
+
// *name alias
|
|
970
|
+
// !tag, !!tag
|
|
971
|
+
// %YAML / %TAG directive (only at column 0)
|
|
972
|
+
|
|
973
|
+
// Anchors: `&` followed by a name char, with whitespace or `-`/`?` before
|
|
974
|
+
// (or start of value position). A simple heuristic: any unescaped `&`
|
|
975
|
+
// that's followed by an identifier char and is preceded by space or
|
|
976
|
+
// line start is an anchor. Same for `*`.
|
|
977
|
+
var anchorOrAliasRe = /(^|\s)([&*])([A-Za-z0-9_][A-Za-z0-9_-]*)/;
|
|
978
|
+
var m = safe.match(anchorOrAliasRe);
|
|
979
|
+
if (m) {
|
|
980
|
+
var posIdx = safe.indexOf(m[0]);
|
|
981
|
+
var lineCount = safe.substring(0, posIdx).split("\n").length;
|
|
982
|
+
throw new SafeYamlError(
|
|
983
|
+
m[2] === "&" ? "anchors are not supported" : "aliases are not supported",
|
|
984
|
+
m[2] === "&" ? "yaml/anchors-banned" : "yaml/aliases-banned",
|
|
985
|
+
lineCount, 1
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Tags: `!` at start of value or after `: ` / `- `. False-positive risk:
|
|
990
|
+
// "key: !something" vs "key: ![bracket". We match `!` followed by
|
|
991
|
+
// alphanumeric or `<`.
|
|
992
|
+
var tagRe = /(^|[\s-])(!{1,2}[A-Za-z<])/;
|
|
993
|
+
var mt = safe.match(tagRe);
|
|
994
|
+
if (mt) {
|
|
995
|
+
var tagIdx = safe.indexOf(mt[0]);
|
|
996
|
+
var tagLine = safe.substring(0, tagIdx).split("\n").length;
|
|
997
|
+
throw new SafeYamlError("tags are not supported",
|
|
998
|
+
"yaml/tags-banned", tagLine, 1);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Directives: `%YAML` or `%TAG` at column 0
|
|
1002
|
+
var dirRe = /(^|\n)%(YAML|TAG)\b/;
|
|
1003
|
+
var md = safe.match(dirRe);
|
|
1004
|
+
if (md) {
|
|
1005
|
+
var dirIdx = safe.indexOf(md[0]);
|
|
1006
|
+
var dirLine = safe.substring(0, dirIdx).split("\n").length + (md[1] === "\n" ? 1 : 0);
|
|
1007
|
+
throw new SafeYamlError("directives are not supported",
|
|
1008
|
+
"yaml/directives-banned", dirLine, 1);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
module.exports = {
|
|
1013
|
+
parse: parse,
|
|
1014
|
+
SafeYamlError: SafeYamlError,
|
|
1015
|
+
};
|