@blamejs/core 0.7.4 → 0.7.18

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 (180) hide show
  1. package/CHANGELOG.md +423 -395
  2. package/README.md +150 -149
  3. package/bin/blamejs.js +0 -0
  4. package/index.js +308 -284
  5. package/lib/api-key.js +660 -663
  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.js +311 -311
  16. package/lib/auth/lockout.js +436 -436
  17. package/lib/auth/oauth.js +721 -721
  18. package/lib/auth/passkey.js +181 -181
  19. package/lib/auth/password.js +594 -594
  20. package/lib/backup/bundle.js +217 -217
  21. package/lib/backup/crypto.js +176 -176
  22. package/lib/backup/index.js +515 -515
  23. package/lib/backup/manifest.js +282 -282
  24. package/lib/break-glass.js +1338 -1338
  25. package/lib/bundler.js +441 -441
  26. package/lib/cache-redis.js +256 -256
  27. package/lib/cache.js +1206 -1206
  28. package/lib/canonical-json.js +115 -115
  29. package/lib/chain-writer.js +234 -234
  30. package/lib/cli-helpers.js +206 -206
  31. package/lib/cli.js +2334 -2334
  32. package/lib/cluster-provider-db.js +317 -317
  33. package/lib/cluster-storage.js +226 -226
  34. package/lib/cluster.js +703 -703
  35. package/lib/codepoint-class.js +196 -0
  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 +315 -315
  40. package/lib/credential-hash.js +322 -322
  41. package/lib/crypto.js +266 -266
  42. package/lib/csv.js +275 -286
  43. package/lib/db-declare-row-policy.js +267 -267
  44. package/lib/db-declare-view.js +420 -421
  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/file-upload.js +213 -10
  56. package/lib/forms.js +422 -422
  57. package/lib/framework-error.js +293 -215
  58. package/lib/framework-schema.js +717 -717
  59. package/lib/gate-contract.js +971 -0
  60. package/lib/guard-all.js +405 -0
  61. package/lib/guard-archive.js +739 -0
  62. package/lib/guard-csv.js +816 -0
  63. package/lib/guard-email.js +744 -0
  64. package/lib/guard-filename.js +724 -0
  65. package/lib/guard-html.js +976 -0
  66. package/lib/guard-json.js +729 -0
  67. package/lib/guard-markdown.js +586 -0
  68. package/lib/guard-svg.js +976 -0
  69. package/lib/guard-xml.js +405 -0
  70. package/lib/guard-yaml.js +529 -0
  71. package/lib/handlers.js +350 -350
  72. package/lib/http-client-cookie-jar.js +508 -508
  73. package/lib/http-client.js +1195 -1195
  74. package/lib/i18n.js +878 -878
  75. package/lib/jobs.js +185 -185
  76. package/lib/log-stream-cloudwatch.js +369 -369
  77. package/lib/log-stream-local.js +146 -146
  78. package/lib/log-stream-otlp-grpc.js +410 -410
  79. package/lib/log-stream-otlp.js +286 -286
  80. package/lib/log-stream-syslog.js +302 -302
  81. package/lib/log-stream-webhook.js +199 -199
  82. package/lib/log-stream.js +330 -330
  83. package/lib/log.js +500 -500
  84. package/lib/mail-bounce.js +528 -528
  85. package/lib/mail-dkim.js +369 -362
  86. package/lib/mail.js +981 -962
  87. package/lib/metrics.js +683 -683
  88. package/lib/middleware/api-encrypt.js +936 -936
  89. package/lib/middleware/attach-user.js +157 -157
  90. package/lib/middleware/body-parser.js +1170 -1091
  91. package/lib/middleware/bot-guard.js +178 -178
  92. package/lib/middleware/compression.js +452 -452
  93. package/lib/middleware/cors.js +314 -314
  94. package/lib/middleware/csp-nonce.js +348 -348
  95. package/lib/middleware/csrf-protect.js +316 -316
  96. package/lib/middleware/db-role-for.js +264 -264
  97. package/lib/middleware/health.js +392 -392
  98. package/lib/middleware/index.js +79 -79
  99. package/lib/middleware/rate-limit.js +358 -358
  100. package/lib/middleware/request-id.js +61 -61
  101. package/lib/middleware/request-log.js +168 -168
  102. package/lib/middleware/require-auth.js +104 -104
  103. package/lib/middleware/security-headers.js +116 -116
  104. package/lib/middleware/sse.js +166 -166
  105. package/lib/migrations.js +383 -383
  106. package/lib/mtls-ca.js +518 -518
  107. package/lib/mtls-engine-default.js +481 -481
  108. package/lib/network-dns.js +632 -632
  109. package/lib/network-heartbeat.js +290 -290
  110. package/lib/network-nts.js +574 -574
  111. package/lib/network-proxy.js +265 -265
  112. package/lib/network-tls.js +328 -328
  113. package/lib/network.js +233 -233
  114. package/lib/notify.js +612 -612
  115. package/lib/ntp-check.js +229 -229
  116. package/lib/numeric-bounds.js +111 -91
  117. package/lib/object-store/azure-blob-bucket-ops.js +349 -349
  118. package/lib/object-store/azure-blob.js +488 -488
  119. package/lib/object-store/gcs-bucket-ops.js +351 -351
  120. package/lib/object-store/gcs.js +519 -519
  121. package/lib/object-store/http-put.js +153 -153
  122. package/lib/object-store/index.js +197 -197
  123. package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
  124. package/lib/object-store/sigv4.js +903 -903
  125. package/lib/observability.js +151 -151
  126. package/lib/otel-export.js +269 -269
  127. package/lib/pagination.js +464 -464
  128. package/lib/parsers/index.js +80 -80
  129. package/lib/parsers/safe-env.js +642 -642
  130. package/lib/parsers/safe-ini.js +292 -292
  131. package/lib/parsers/safe-toml.js +784 -784
  132. package/lib/parsers/safe-xml.js +390 -390
  133. package/lib/parsers/safe-yaml.js +1015 -1015
  134. package/lib/permissions.js +708 -708
  135. package/lib/pqc-agent.js +87 -87
  136. package/lib/pqc-gate.js +279 -279
  137. package/lib/protobuf-encoder.js +190 -190
  138. package/lib/protocol-dispatcher.js +161 -161
  139. package/lib/pubsub-redis.js +167 -167
  140. package/lib/pubsub.js +429 -429
  141. package/lib/queue-local.js +476 -476
  142. package/lib/queue-redis.js +745 -745
  143. package/lib/queue-sqs.js +319 -319
  144. package/lib/queue.js +695 -695
  145. package/lib/redis-client.js +519 -519
  146. package/lib/request-helpers.js +340 -340
  147. package/lib/restore-bundle.js +237 -237
  148. package/lib/restore-rollback.js +259 -259
  149. package/lib/restore.js +409 -409
  150. package/lib/retry.js +376 -376
  151. package/lib/router.js +748 -748
  152. package/lib/safe-async.js +735 -735
  153. package/lib/safe-buffer.js +237 -237
  154. package/lib/safe-json.js +541 -541
  155. package/lib/safe-schema.js +1266 -1266
  156. package/lib/safe-url.js +159 -159
  157. package/lib/scheduler.js +706 -706
  158. package/lib/security-assert.js +373 -373
  159. package/lib/seeders.js +618 -618
  160. package/lib/session.js +478 -478
  161. package/lib/slug.js +269 -269
  162. package/lib/ssrf-guard.js +401 -401
  163. package/lib/static.js +184 -4
  164. package/lib/storage.js +471 -471
  165. package/lib/subject.js +281 -281
  166. package/lib/template.js +791 -791
  167. package/lib/testing.js +798 -798
  168. package/lib/time.js +310 -310
  169. package/lib/totp.js +302 -302
  170. package/lib/tracing.js +494 -494
  171. package/lib/uuid.js +132 -132
  172. package/lib/validate-opts.js +340 -319
  173. package/lib/vault/index.js +308 -308
  174. package/lib/vault/rotate.js +784 -784
  175. package/lib/vault/wrap.js +296 -296
  176. package/lib/vendor/noble-ciphers.cjs +9 -9
  177. package/lib/webhook.js +595 -595
  178. package/lib/websocket.js +1048 -1048
  179. package/package.json +77 -77
  180. 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
+ };