@blamejs/core 0.7.18 → 0.7.19

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 (167) hide show
  1. package/CHANGELOG.md +425 -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 +315 -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 +316 -316
  84. package/lib/middleware/db-role-for.js +264 -264
  85. package/lib/middleware/health.js +392 -392
  86. package/lib/middleware/index.js +82 -79
  87. package/lib/middleware/rate-limit.js +358 -358
  88. package/lib/middleware/request-id.js +61 -61
  89. package/lib/middleware/request-log.js +168 -168
  90. package/lib/middleware/require-auth.js +104 -104
  91. package/lib/middleware/security-headers.js +116 -116
  92. package/lib/middleware/sse.js +166 -166
  93. package/lib/migrations.js +383 -383
  94. package/lib/mtls-ca.js +518 -518
  95. package/lib/mtls-engine-default.js +481 -481
  96. package/lib/network-dns.js +632 -632
  97. package/lib/network-heartbeat.js +290 -290
  98. package/lib/network-nts.js +574 -574
  99. package/lib/network-proxy.js +265 -265
  100. package/lib/network-tls.js +328 -328
  101. package/lib/network.js +233 -233
  102. package/lib/notify.js +612 -612
  103. package/lib/ntp-check.js +229 -229
  104. package/lib/numeric-bounds.js +111 -111
  105. package/lib/object-store/azure-blob-bucket-ops.js +349 -349
  106. package/lib/object-store/azure-blob.js +488 -488
  107. package/lib/object-store/gcs-bucket-ops.js +351 -351
  108. package/lib/object-store/gcs.js +519 -519
  109. package/lib/object-store/http-put.js +153 -153
  110. package/lib/object-store/index.js +197 -197
  111. package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
  112. package/lib/object-store/sigv4.js +903 -903
  113. package/lib/observability.js +151 -151
  114. package/lib/otel-export.js +269 -269
  115. package/lib/pagination.js +464 -464
  116. package/lib/parsers/index.js +80 -80
  117. package/lib/parsers/safe-env.js +642 -642
  118. package/lib/parsers/safe-ini.js +292 -292
  119. package/lib/parsers/safe-toml.js +784 -784
  120. package/lib/parsers/safe-xml.js +390 -390
  121. package/lib/parsers/safe-yaml.js +1015 -1015
  122. package/lib/permissions.js +708 -708
  123. package/lib/pqc-agent.js +87 -87
  124. package/lib/pqc-gate.js +279 -279
  125. package/lib/protobuf-encoder.js +190 -190
  126. package/lib/protocol-dispatcher.js +161 -161
  127. package/lib/pubsub-redis.js +167 -167
  128. package/lib/pubsub.js +429 -429
  129. package/lib/queue-local.js +476 -476
  130. package/lib/queue-redis.js +745 -745
  131. package/lib/queue-sqs.js +319 -319
  132. package/lib/queue.js +695 -695
  133. package/lib/redis-client.js +519 -519
  134. package/lib/request-helpers.js +340 -340
  135. package/lib/restore-bundle.js +237 -237
  136. package/lib/restore-rollback.js +259 -259
  137. package/lib/restore.js +409 -409
  138. package/lib/retry.js +376 -376
  139. package/lib/router.js +748 -748
  140. package/lib/safe-async.js +735 -735
  141. package/lib/safe-buffer.js +237 -237
  142. package/lib/safe-json.js +541 -541
  143. package/lib/safe-schema.js +1266 -1266
  144. package/lib/safe-url.js +159 -159
  145. package/lib/scheduler.js +706 -706
  146. package/lib/security-assert.js +373 -373
  147. package/lib/seeders.js +618 -618
  148. package/lib/session.js +535 -478
  149. package/lib/slug.js +269 -269
  150. package/lib/ssrf-guard.js +401 -401
  151. package/lib/storage.js +471 -471
  152. package/lib/subject.js +281 -281
  153. package/lib/template.js +791 -791
  154. package/lib/testing.js +798 -798
  155. package/lib/time.js +310 -310
  156. package/lib/totp.js +302 -302
  157. package/lib/tracing.js +494 -494
  158. package/lib/uuid.js +132 -132
  159. package/lib/validate-opts.js +340 -340
  160. package/lib/vault/index.js +308 -308
  161. package/lib/vault/rotate.js +784 -784
  162. package/lib/vault/wrap.js +296 -296
  163. package/lib/vendor/noble-ciphers.cjs +9 -9
  164. package/lib/webhook.js +595 -595
  165. package/lib/websocket.js +1048 -1048
  166. package/package.json +77 -77
  167. 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
+ };