@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.
Files changed (169) hide show
  1. package/CHANGELOG.md +427 -423
  2. package/README.md +150 -150
  3. package/bin/blamejs.js +0 -0
  4. package/index.js +310 -308
  5. package/lib/api-key.js +660 -660
  6. package/lib/api-snapshot.js +338 -338
  7. package/lib/app-shutdown.js +385 -385
  8. package/lib/app.js +365 -365
  9. package/lib/archive.js +250 -250
  10. package/lib/atomic-file.js +544 -544
  11. package/lib/audit-chain.js +177 -177
  12. package/lib/audit-sign.js +344 -344
  13. package/lib/audit-tools.js +677 -677
  14. package/lib/audit.js +766 -766
  15. package/lib/auth/jwt-external.js +365 -0
  16. package/lib/auth/jwt.js +337 -311
  17. package/lib/auth/lockout.js +436 -436
  18. package/lib/auth/oauth.js +721 -721
  19. package/lib/auth/passkey.js +181 -181
  20. package/lib/auth/password.js +628 -594
  21. package/lib/backup/bundle.js +217 -217
  22. package/lib/backup/crypto.js +176 -176
  23. package/lib/backup/index.js +515 -515
  24. package/lib/backup/manifest.js +282 -282
  25. package/lib/break-glass.js +1338 -1338
  26. package/lib/bundler.js +441 -441
  27. package/lib/cache-redis.js +256 -256
  28. package/lib/cache.js +1206 -1206
  29. package/lib/canonical-json.js +115 -115
  30. package/lib/chain-writer.js +234 -234
  31. package/lib/cli-helpers.js +206 -206
  32. package/lib/cli.js +2334 -2334
  33. package/lib/cluster-provider-db.js +317 -317
  34. package/lib/cluster-storage.js +226 -226
  35. package/lib/cluster.js +703 -703
  36. package/lib/config-drift.js +301 -301
  37. package/lib/consent.js +222 -222
  38. package/lib/constants.js +191 -191
  39. package/lib/cookies.js +350 -315
  40. package/lib/credential-hash.js +322 -322
  41. package/lib/crypto.js +266 -266
  42. package/lib/csv.js +275 -275
  43. package/lib/db-declare-row-policy.js +267 -267
  44. package/lib/db-declare-view.js +420 -420
  45. package/lib/db-query.js +406 -406
  46. package/lib/db-schema.js +319 -319
  47. package/lib/db.js +1288 -1288
  48. package/lib/deprecate.js +222 -222
  49. package/lib/dev.js +335 -335
  50. package/lib/dual-control.js +473 -473
  51. package/lib/error-page.js +420 -420
  52. package/lib/external-db-migrate.js +441 -441
  53. package/lib/external-db.js +1061 -1061
  54. package/lib/file-type.js +273 -273
  55. package/lib/forms.js +422 -422
  56. package/lib/framework-error.js +293 -293
  57. package/lib/framework-schema.js +717 -717
  58. package/lib/handlers.js +350 -350
  59. package/lib/http-client-cookie-jar.js +508 -508
  60. package/lib/http-client.js +1195 -1195
  61. package/lib/i18n.js +878 -878
  62. package/lib/jobs.js +185 -185
  63. package/lib/log-stream-cloudwatch.js +369 -369
  64. package/lib/log-stream-local.js +146 -146
  65. package/lib/log-stream-otlp-grpc.js +410 -410
  66. package/lib/log-stream-otlp.js +286 -286
  67. package/lib/log-stream-syslog.js +302 -302
  68. package/lib/log-stream-webhook.js +199 -199
  69. package/lib/log-stream.js +330 -330
  70. package/lib/log.js +500 -500
  71. package/lib/mail-bounce.js +528 -528
  72. package/lib/mail-dkim.js +369 -369
  73. package/lib/mail.js +981 -981
  74. package/lib/metrics.js +683 -683
  75. package/lib/middleware/api-encrypt.js +936 -936
  76. package/lib/middleware/attach-user.js +157 -157
  77. package/lib/middleware/bearer-auth.js +152 -0
  78. package/lib/middleware/body-parser.js +1170 -1170
  79. package/lib/middleware/bot-guard.js +178 -178
  80. package/lib/middleware/compression.js +452 -452
  81. package/lib/middleware/cors.js +314 -314
  82. package/lib/middleware/csp-nonce.js +348 -348
  83. package/lib/middleware/csrf-protect.js +399 -316
  84. package/lib/middleware/db-role-for.js +264 -264
  85. package/lib/middleware/fetch-metadata.js +129 -0
  86. package/lib/middleware/health.js +392 -392
  87. package/lib/middleware/index.js +85 -79
  88. package/lib/middleware/rate-limit.js +358 -358
  89. package/lib/middleware/request-id.js +61 -61
  90. package/lib/middleware/request-log.js +168 -168
  91. package/lib/middleware/require-auth.js +104 -104
  92. package/lib/middleware/security-headers.js +121 -116
  93. package/lib/middleware/sse.js +166 -166
  94. package/lib/migrations.js +383 -383
  95. package/lib/mtls-ca.js +518 -518
  96. package/lib/mtls-engine-default.js +481 -481
  97. package/lib/network-dns.js +632 -632
  98. package/lib/network-heartbeat.js +290 -290
  99. package/lib/network-nts.js +574 -574
  100. package/lib/network-proxy.js +265 -265
  101. package/lib/network-tls.js +328 -328
  102. package/lib/network.js +233 -233
  103. package/lib/notify.js +612 -612
  104. package/lib/ntp-check.js +229 -229
  105. package/lib/numeric-bounds.js +111 -111
  106. package/lib/object-store/azure-blob-bucket-ops.js +349 -349
  107. package/lib/object-store/azure-blob.js +488 -488
  108. package/lib/object-store/gcs-bucket-ops.js +351 -351
  109. package/lib/object-store/gcs.js +519 -519
  110. package/lib/object-store/http-put.js +153 -153
  111. package/lib/object-store/index.js +197 -197
  112. package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
  113. package/lib/object-store/sigv4.js +903 -903
  114. package/lib/observability.js +151 -151
  115. package/lib/otel-export.js +269 -269
  116. package/lib/pagination.js +464 -464
  117. package/lib/parsers/index.js +80 -80
  118. package/lib/parsers/safe-env.js +642 -642
  119. package/lib/parsers/safe-ini.js +292 -292
  120. package/lib/parsers/safe-toml.js +784 -784
  121. package/lib/parsers/safe-xml.js +390 -390
  122. package/lib/parsers/safe-yaml.js +1015 -1015
  123. package/lib/permissions.js +708 -708
  124. package/lib/pqc-agent.js +87 -87
  125. package/lib/pqc-gate.js +279 -279
  126. package/lib/protobuf-encoder.js +190 -190
  127. package/lib/protocol-dispatcher.js +161 -161
  128. package/lib/pubsub-redis.js +167 -167
  129. package/lib/pubsub.js +429 -429
  130. package/lib/queue-local.js +476 -476
  131. package/lib/queue-redis.js +745 -745
  132. package/lib/queue-sqs.js +319 -319
  133. package/lib/queue.js +695 -695
  134. package/lib/redis-client.js +519 -519
  135. package/lib/request-helpers.js +340 -340
  136. package/lib/restore-bundle.js +237 -237
  137. package/lib/restore-rollback.js +259 -259
  138. package/lib/restore.js +409 -409
  139. package/lib/retry.js +376 -376
  140. package/lib/router.js +748 -748
  141. package/lib/safe-async.js +735 -735
  142. package/lib/safe-buffer.js +237 -237
  143. package/lib/safe-json.js +541 -541
  144. package/lib/safe-schema.js +1266 -1266
  145. package/lib/safe-url.js +159 -159
  146. package/lib/scheduler.js +706 -706
  147. package/lib/security-assert.js +373 -373
  148. package/lib/seeders.js +618 -618
  149. package/lib/session.js +535 -478
  150. package/lib/slug.js +269 -269
  151. package/lib/ssrf-guard.js +401 -401
  152. package/lib/static.js +7 -5
  153. package/lib/storage.js +471 -471
  154. package/lib/subject.js +281 -281
  155. package/lib/template.js +791 -791
  156. package/lib/testing.js +798 -798
  157. package/lib/time.js +310 -310
  158. package/lib/totp.js +302 -302
  159. package/lib/tracing.js +494 -494
  160. package/lib/uuid.js +132 -132
  161. package/lib/validate-opts.js +340 -340
  162. package/lib/vault/index.js +308 -308
  163. package/lib/vault/rotate.js +784 -784
  164. package/lib/vault/wrap.js +296 -296
  165. package/lib/vendor/noble-ciphers.cjs +9 -9
  166. package/lib/webhook.js +595 -595
  167. package/lib/websocket.js +1048 -1048
  168. package/package.json +77 -77
  169. package/sbom.cyclonedx.json +7 -7
@@ -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
+ };