@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.
- package/CHANGELOG.md +425 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt-external.js +365 -0
- package/lib/auth/jwt.js +337 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +628 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -369
- package/lib/mail.js +981 -981
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +82 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -111
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -340
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/parsers/safe-xml.js
CHANGED
|
@@ -1,390 +1,390 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Security-focused XML parser. Same security defaults blamejs's json
|
|
4
|
-
* parser applies, adapted to XML's threat model.
|
|
5
|
-
*
|
|
6
|
-
* Native XML.parse doesn't exist in Node, and most npm libs (xml2js,
|
|
7
|
-
* fast-xml-parser, sax, libxml) leave at least one of the following to
|
|
8
|
-
* the caller — usually all of them:
|
|
9
|
-
*
|
|
10
|
-
* - XXE (XML External Entity): <!ENTITY foo SYSTEM "file:///etc/passwd">
|
|
11
|
-
* - Billion-laughs entity expansion (DoS)
|
|
12
|
-
* - DTDs (DOCTYPE) opening the door to either of the above
|
|
13
|
-
* - Processing instructions referencing external resources
|
|
14
|
-
* - Unbounded recursion / element count / attribute count
|
|
15
|
-
* - CDATA sections of arbitrary length
|
|
16
|
-
*
|
|
17
|
-
* This parser closes all of them by default. DOCTYPE, external entities,
|
|
18
|
-
* and processing instructions other than '<?xml ?>' are REJECTED — apps
|
|
19
|
-
* that need them are using the wrong parser.
|
|
20
|
-
*
|
|
21
|
-
* Output: a plain JS object. Element with attributes + children:
|
|
22
|
-
* <root id="x"><child>text</child></root>
|
|
23
|
-
* becomes:
|
|
24
|
-
* { root: { '@attrs': { id: 'x' }, child: 'text' } }
|
|
25
|
-
*
|
|
26
|
-
* Multiple children with the same tag become arrays. Mixed content
|
|
27
|
-
* (text + elements interleaved) is preserved as { '#text': '...', ... }.
|
|
28
|
-
*
|
|
29
|
-
* Public API:
|
|
30
|
-
* xml.parse(input, opts?) → JS object | throws SafeXmlError
|
|
31
|
-
* xml.SafeXmlError → error class
|
|
32
|
-
*
|
|
33
|
-
* Defaults:
|
|
34
|
-
* maxBytes: 1 MiB
|
|
35
|
-
* maxDepth: 100
|
|
36
|
-
* maxElements: 10000
|
|
37
|
-
* maxAttributes: 100 per element
|
|
38
|
-
* allowDoctype: false
|
|
39
|
-
* allowProcessing: false (only <?xml version="..."?> is permitted)
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
var C = require("../constants");
|
|
43
|
-
var nb = require("../numeric-bounds");
|
|
44
|
-
var safeBuffer = require("../safe-buffer");
|
|
45
|
-
var { FrameworkError } = require("../framework-error");
|
|
46
|
-
|
|
47
|
-
class SafeXmlError extends FrameworkError {
|
|
48
|
-
constructor(message, code, position) {
|
|
49
|
-
super(message);
|
|
50
|
-
this.name = "SafeXmlError";
|
|
51
|
-
this.code = code || "xml/invalid";
|
|
52
|
-
this.position = position || null;
|
|
53
|
-
this.isSafeXmlError = true;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// parseInt radix — named so the call site doesn't carry a bare 16
|
|
58
|
-
// integer literal that reads as a byte count.
|
|
59
|
-
var RADIX_HEX = 0x10;
|
|
60
|
-
|
|
61
|
-
var DEFAULTS = {
|
|
62
|
-
maxBytes: C.BYTES.mib(1),
|
|
63
|
-
maxDepth: 100,
|
|
64
|
-
maxElements: 10_000,
|
|
65
|
-
maxAttributes: 100,
|
|
66
|
-
allowDoctype: false,
|
|
67
|
-
allowProcessing: false, // <?...?> other than the xml decl
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
var ABSOLUTE_MAX_BYTES = C.BYTES.mib(64);
|
|
71
|
-
var ABSOLUTE_MAX_DEPTH = 1_000;
|
|
72
|
-
var ABSOLUTE_MAX_ELEMENTS = 1_000_000;
|
|
73
|
-
var ABSOLUTE_MAX_ATTRIBUTES = 1_000;
|
|
74
|
-
|
|
75
|
-
// XML built-in entities (the ONLY entities allowed)
|
|
76
|
-
var BUILT_IN_ENTITIES = { lt: "<", gt: ">", amp: "&", quot: "\"", apos: "'" };
|
|
77
|
-
|
|
78
|
-
function _validateAndCap(name, value, defaultValue, ceiling) {
|
|
79
|
-
if (value === undefined) return defaultValue;
|
|
80
|
-
if (!nb.isPositiveFiniteInt(value)) {
|
|
81
|
-
throw new SafeXmlError("xml/bad-opt",
|
|
82
|
-
"xml.parse: " + name + " must be a positive finite integer; got " +
|
|
83
|
-
nb.shape(value));
|
|
84
|
-
}
|
|
85
|
-
return Math.min(value, ceiling);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function parse(input, opts) {
|
|
89
|
-
opts = opts || {};
|
|
90
|
-
|
|
91
|
-
var maxBytes = _validateAndCap("maxBytes", opts.maxBytes, DEFAULTS.maxBytes, ABSOLUTE_MAX_BYTES);
|
|
92
|
-
var maxDepth = _validateAndCap("maxDepth", opts.maxDepth, DEFAULTS.maxDepth, ABSOLUTE_MAX_DEPTH);
|
|
93
|
-
var maxElements = _validateAndCap("maxElements", opts.maxElements, DEFAULTS.maxElements, ABSOLUTE_MAX_ELEMENTS);
|
|
94
|
-
var maxAttrs = _validateAndCap("maxAttributes", opts.maxAttributes, DEFAULTS.maxAttributes, ABSOLUTE_MAX_ATTRIBUTES);
|
|
95
|
-
var allowDoctype = !!opts.allowDoctype;
|
|
96
|
-
var allowProcessing = !!opts.allowProcessing;
|
|
97
|
-
|
|
98
|
-
input = safeBuffer.normalizeText(input, {
|
|
99
|
-
maxBytes: maxBytes,
|
|
100
|
-
errorClass: SafeXmlError,
|
|
101
|
-
typeCode: "xml/wrong-input-type",
|
|
102
|
-
sizeCode: "xml/too-large",
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
var pos = 0;
|
|
106
|
-
var len = input.length;
|
|
107
|
-
var elementCount = 0;
|
|
108
|
-
|
|
109
|
-
function _err(msg, code) {
|
|
110
|
-
return new SafeXmlError(msg + " at position " + pos, code || "xml/invalid", pos);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Skip whitespace
|
|
114
|
-
function skipWs() {
|
|
115
|
-
while (pos < len) {
|
|
116
|
-
var c = input.charCodeAt(pos);
|
|
117
|
-
if (c === 0x20 || c === 0x09 || c === 0x0A || c === 0x0D) pos += 1;
|
|
118
|
-
else break;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function expectChar(ch) {
|
|
123
|
-
if (input.charAt(pos) !== ch) {
|
|
124
|
-
throw _err("expected '" + ch + "'");
|
|
125
|
-
}
|
|
126
|
-
pos += 1;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Decode & / < / etc. — ONLY the 5 built-ins + numeric character refs.
|
|
130
|
-
// Custom entities are forbidden by design (XXE protection).
|
|
131
|
-
function decodeEntities(s) {
|
|
132
|
-
var out = "";
|
|
133
|
-
var i = 0;
|
|
134
|
-
while (i < s.length) {
|
|
135
|
-
var ch = s.charAt(i);
|
|
136
|
-
if (ch !== "&") { out += ch; i += 1; continue; }
|
|
137
|
-
var end = s.indexOf(";", i);
|
|
138
|
-
if (end < 0) throw _err("unterminated entity reference", "xml/bad-entity");
|
|
139
|
-
var name = s.substring(i + 1, end);
|
|
140
|
-
if (name.charAt(0) === "#") {
|
|
141
|
-
// Numeric character reference
|
|
142
|
-
var code;
|
|
143
|
-
if (name.charAt(1) === "x" || name.charAt(1) === "X") {
|
|
144
|
-
code = parseInt(name.substring(2), RADIX_HEX);
|
|
145
|
-
} else {
|
|
146
|
-
code = parseInt(name.substring(1), 10);
|
|
147
|
-
}
|
|
148
|
-
if (!Number.isFinite(code) || code < 0 || code > 0x10FFFF) {
|
|
149
|
-
throw _err("invalid numeric character reference", "xml/bad-entity");
|
|
150
|
-
}
|
|
151
|
-
// XML 1.0 forbids most C0 control chars
|
|
152
|
-
if ((code < 0x20 && code !== 0x09 && code !== 0x0A && code !== 0x0D) || (code >= 0xD800 && code <= 0xDFFF)) {
|
|
153
|
-
throw _err("character reference points to forbidden codepoint", "xml/bad-entity");
|
|
154
|
-
}
|
|
155
|
-
out += String.fromCodePoint(code);
|
|
156
|
-
i = end + 1;
|
|
157
|
-
} else if (BUILT_IN_ENTITIES[name] !== undefined) {
|
|
158
|
-
out += BUILT_IN_ENTITIES[name];
|
|
159
|
-
i = end + 1;
|
|
160
|
-
} else {
|
|
161
|
-
throw _err("unknown entity '" + name + "' (custom entities forbidden — XXE protection)", "xml/external-entity");
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return out;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Parse a name (element / attribute name)
|
|
168
|
-
function parseName() {
|
|
169
|
-
var start = pos;
|
|
170
|
-
while (pos < len) {
|
|
171
|
-
var c = input.charCodeAt(pos);
|
|
172
|
-
// XML name characters — simplified subset (ASCII letters, digits, '-', '_', '.', ':')
|
|
173
|
-
if ((c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A) ||
|
|
174
|
-
(c >= 0x30 && c <= 0x39) ||
|
|
175
|
-
c === 0x2D || c === 0x5F || c === 0x2E || c === 0x3A ||
|
|
176
|
-
(c >= 0x80 && c <= 0xFFFF)) {
|
|
177
|
-
pos += 1;
|
|
178
|
-
} else break;
|
|
179
|
-
}
|
|
180
|
-
if (pos === start) throw _err("expected name", "xml/bad-name");
|
|
181
|
-
return input.substring(start, pos);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Parse an attribute value (single- or double-quoted)
|
|
185
|
-
function parseAttrValue() {
|
|
186
|
-
var quote = input.charAt(pos);
|
|
187
|
-
if (quote !== "\"" && quote !== "'") throw _err("expected quoted attribute value", "xml/bad-attr");
|
|
188
|
-
pos += 1;
|
|
189
|
-
var start = pos;
|
|
190
|
-
while (pos < len && input.charAt(pos) !== quote) {
|
|
191
|
-
if (input.charAt(pos) === "<") throw _err("'<' not allowed in attribute value", "xml/bad-attr");
|
|
192
|
-
pos += 1;
|
|
193
|
-
}
|
|
194
|
-
if (pos >= len) throw _err("unterminated attribute value", "xml/bad-attr");
|
|
195
|
-
var raw = input.substring(start, pos);
|
|
196
|
-
pos += 1;
|
|
197
|
-
return decodeEntities(raw);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Top-level: optional XML declaration, optional DOCTYPE (rejected), root element
|
|
201
|
-
function parseDocument() {
|
|
202
|
-
skipWs();
|
|
203
|
-
// XML declaration <?xml version="1.0" ... ?>
|
|
204
|
-
if (input.startsWith("<?xml", pos)) {
|
|
205
|
-
var declEnd = input.indexOf("?>", pos);
|
|
206
|
-
if (declEnd < 0) throw _err("unterminated XML declaration", "xml/bad-decl");
|
|
207
|
-
pos = declEnd + 2;
|
|
208
|
-
}
|
|
209
|
-
skipWs();
|
|
210
|
-
// DOCTYPE — rejected by default (XXE / billion-laughs vector)
|
|
211
|
-
if (input.startsWith("<!DOCTYPE", pos)) {
|
|
212
|
-
if (!allowDoctype) {
|
|
213
|
-
throw _err("DOCTYPE declarations are forbidden (XXE protection)", "xml/doctype");
|
|
214
|
-
}
|
|
215
|
-
// If allowed (operator opt-in), skip past the DOCTYPE without parsing internal subset
|
|
216
|
-
var doctypeEnd = pos;
|
|
217
|
-
var depth = 0;
|
|
218
|
-
while (doctypeEnd < len) {
|
|
219
|
-
var c = input.charAt(doctypeEnd);
|
|
220
|
-
if (c === "[") depth += 1;
|
|
221
|
-
else if (c === "]") depth -= 1;
|
|
222
|
-
else if (c === ">" && depth === 0) break;
|
|
223
|
-
doctypeEnd += 1;
|
|
224
|
-
}
|
|
225
|
-
pos = doctypeEnd + 1;
|
|
226
|
-
}
|
|
227
|
-
skipWs();
|
|
228
|
-
|
|
229
|
-
// Comments / processing instructions before root
|
|
230
|
-
while (input.startsWith("<!--", pos) || input.startsWith("<?", pos)) {
|
|
231
|
-
if (input.startsWith("<!--", pos)) {
|
|
232
|
-
var commentEnd = input.indexOf("-->", pos);
|
|
233
|
-
if (commentEnd < 0) throw _err("unterminated comment", "xml/bad-comment");
|
|
234
|
-
pos = commentEnd + 3;
|
|
235
|
-
} else {
|
|
236
|
-
// Processing instruction
|
|
237
|
-
if (!allowProcessing) {
|
|
238
|
-
throw _err("processing instructions are forbidden (allowProcessing: true to permit)", "xml/processing");
|
|
239
|
-
}
|
|
240
|
-
var piEnd = input.indexOf("?>", pos);
|
|
241
|
-
if (piEnd < 0) throw _err("unterminated processing instruction", "xml/bad-pi");
|
|
242
|
-
pos = piEnd + 2;
|
|
243
|
-
}
|
|
244
|
-
skipWs();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (input.charAt(pos) !== "<") throw _err("expected root element", "xml/no-root");
|
|
248
|
-
var root = parseElement(0);
|
|
249
|
-
skipWs();
|
|
250
|
-
if (pos !== len) {
|
|
251
|
-
// Trailing comments / whitespace OK; otherwise reject extra content
|
|
252
|
-
while (pos < len && input.startsWith("<!--", pos)) {
|
|
253
|
-
var ce = input.indexOf("-->", pos);
|
|
254
|
-
if (ce < 0) throw _err("unterminated trailing comment", "xml/bad-comment");
|
|
255
|
-
pos = ce + 3;
|
|
256
|
-
skipWs();
|
|
257
|
-
}
|
|
258
|
-
if (pos !== len) throw _err("unexpected content after root element", "xml/extra-content");
|
|
259
|
-
}
|
|
260
|
-
return root;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function parseElement(depth) {
|
|
264
|
-
if (depth > maxDepth) throw _err("nesting exceeds maxDepth", "xml/too-deep");
|
|
265
|
-
elementCount += 1;
|
|
266
|
-
if (elementCount > maxElements) throw _err("element count exceeds maxElements", "xml/too-many-elements");
|
|
267
|
-
|
|
268
|
-
expectChar("<");
|
|
269
|
-
var name = parseName();
|
|
270
|
-
var attrs = {};
|
|
271
|
-
var attrCount = 0;
|
|
272
|
-
|
|
273
|
-
while (pos < len) {
|
|
274
|
-
// Look for attribute, '/>', or '>'
|
|
275
|
-
var c = input.charAt(pos);
|
|
276
|
-
if (c === "/") { pos += 1; expectChar(">"); return _wrap(name, attrs, []); }
|
|
277
|
-
if (c === ">") { pos += 1; break; }
|
|
278
|
-
if (c === " " || c === "\t" || c === "\n" || c === "\r") { pos += 1; continue; }
|
|
279
|
-
|
|
280
|
-
// Attribute
|
|
281
|
-
attrCount += 1;
|
|
282
|
-
if (attrCount > maxAttrs) throw _err("attribute count exceeds maxAttributes", "xml/too-many-attrs");
|
|
283
|
-
var attrName = parseName();
|
|
284
|
-
skipWs();
|
|
285
|
-
expectChar("=");
|
|
286
|
-
skipWs();
|
|
287
|
-
var attrValue = parseAttrValue();
|
|
288
|
-
if (attrs[attrName] !== undefined) {
|
|
289
|
-
throw _err("duplicate attribute '" + attrName + "'", "xml/duplicate-attr");
|
|
290
|
-
}
|
|
291
|
-
attrs[attrName] = attrValue;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Children + text
|
|
295
|
-
var children = [];
|
|
296
|
-
while (pos < len) {
|
|
297
|
-
if (input.startsWith("</", pos)) {
|
|
298
|
-
pos += 2;
|
|
299
|
-
var endName = parseName();
|
|
300
|
-
if (endName !== name) throw _err("mismatched end tag </" + endName + "> for <" + name + ">", "xml/mismatched-tag");
|
|
301
|
-
skipWs();
|
|
302
|
-
expectChar(">");
|
|
303
|
-
return _wrap(name, attrs, children);
|
|
304
|
-
}
|
|
305
|
-
if (input.startsWith("<!--", pos)) {
|
|
306
|
-
var commentEnd = input.indexOf("-->", pos);
|
|
307
|
-
if (commentEnd < 0) throw _err("unterminated comment", "xml/bad-comment");
|
|
308
|
-
pos = commentEnd + 3;
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
if (input.startsWith("<![CDATA[", pos)) {
|
|
312
|
-
var cdataEnd = input.indexOf("]]>", pos + 9);
|
|
313
|
-
if (cdataEnd < 0) throw _err("unterminated CDATA", "xml/bad-cdata");
|
|
314
|
-
children.push({ kind: "text", value: input.substring(pos + 9, cdataEnd) });
|
|
315
|
-
pos = cdataEnd + 3;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (input.startsWith("<?", pos)) {
|
|
319
|
-
if (!allowProcessing) throw _err("processing instructions forbidden", "xml/processing");
|
|
320
|
-
var piEnd = input.indexOf("?>", pos);
|
|
321
|
-
if (piEnd < 0) throw _err("unterminated PI", "xml/bad-pi");
|
|
322
|
-
pos = piEnd + 2;
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
if (input.charAt(pos) === "<") {
|
|
326
|
-
children.push({ kind: "element", value: parseElement(depth + 1) });
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
// Text
|
|
330
|
-
var textStart = pos;
|
|
331
|
-
while (pos < len && input.charAt(pos) !== "<") pos += 1;
|
|
332
|
-
var rawText = input.substring(textStart, pos);
|
|
333
|
-
if (rawText.length > 0) {
|
|
334
|
-
children.push({ kind: "text", value: decodeEntities(rawText) });
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
throw _err("unexpected end of input inside <" + name + ">", "xml/truncated");
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Wrap parsed element into the JS-object shape.
|
|
341
|
-
function _wrap(name, attrs, children) {
|
|
342
|
-
var elementChildren = children.filter(function (c) { return c.kind === "element"; });
|
|
343
|
-
var textParts = children.filter(function (c) { return c.kind === "text"; }).map(function (c) { return c.value; });
|
|
344
|
-
var hasAttrs = Object.keys(attrs).length > 0;
|
|
345
|
-
|
|
346
|
-
// Self-closing or empty element with no attrs → empty string
|
|
347
|
-
if (children.length === 0 && !hasAttrs) {
|
|
348
|
-
return _make(name, "");
|
|
349
|
-
}
|
|
350
|
-
if (elementChildren.length === 0 && !hasAttrs) {
|
|
351
|
-
// Pure-text element → string
|
|
352
|
-
return _make(name, textParts.join("").trim() === "" ? textParts.join("") : textParts.join(""));
|
|
353
|
-
}
|
|
354
|
-
// Mixed / attributed element → object
|
|
355
|
-
var obj = {};
|
|
356
|
-
if (hasAttrs) obj["@attrs"] = attrs;
|
|
357
|
-
var grouped = {};
|
|
358
|
-
for (var i = 0; i < elementChildren.length; i++) {
|
|
359
|
-
var childWrap = elementChildren[i].value;
|
|
360
|
-
var childName = Object.keys(childWrap)[0];
|
|
361
|
-
var childVal = childWrap[childName];
|
|
362
|
-
if (grouped[childName] === undefined) {
|
|
363
|
-
grouped[childName] = childVal;
|
|
364
|
-
} else if (Array.isArray(grouped[childName])) {
|
|
365
|
-
grouped[childName].push(childVal);
|
|
366
|
-
} else {
|
|
367
|
-
grouped[childName] = [grouped[childName], childVal];
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
Object.assign(obj, grouped);
|
|
371
|
-
var combinedText = textParts.join("").replace(/\s+/g, " ").trim();
|
|
372
|
-
if (combinedText.length > 0) obj["#text"] = combinedText;
|
|
373
|
-
return _make(name, obj);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function _make(name, value) {
|
|
377
|
-
var out = {};
|
|
378
|
-
out[name] = value;
|
|
379
|
-
return out;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return parseDocument();
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
module.exports = {
|
|
386
|
-
parse: parse,
|
|
387
|
-
SafeXmlError: SafeXmlError,
|
|
388
|
-
DEFAULTS: DEFAULTS,
|
|
389
|
-
BUILT_IN_ENTITIES: BUILT_IN_ENTITIES,
|
|
390
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Security-focused XML parser. Same security defaults blamejs's json
|
|
4
|
+
* parser applies, adapted to XML's threat model.
|
|
5
|
+
*
|
|
6
|
+
* Native XML.parse doesn't exist in Node, and most npm libs (xml2js,
|
|
7
|
+
* fast-xml-parser, sax, libxml) leave at least one of the following to
|
|
8
|
+
* the caller — usually all of them:
|
|
9
|
+
*
|
|
10
|
+
* - XXE (XML External Entity): <!ENTITY foo SYSTEM "file:///etc/passwd">
|
|
11
|
+
* - Billion-laughs entity expansion (DoS)
|
|
12
|
+
* - DTDs (DOCTYPE) opening the door to either of the above
|
|
13
|
+
* - Processing instructions referencing external resources
|
|
14
|
+
* - Unbounded recursion / element count / attribute count
|
|
15
|
+
* - CDATA sections of arbitrary length
|
|
16
|
+
*
|
|
17
|
+
* This parser closes all of them by default. DOCTYPE, external entities,
|
|
18
|
+
* and processing instructions other than '<?xml ?>' are REJECTED — apps
|
|
19
|
+
* that need them are using the wrong parser.
|
|
20
|
+
*
|
|
21
|
+
* Output: a plain JS object. Element with attributes + children:
|
|
22
|
+
* <root id="x"><child>text</child></root>
|
|
23
|
+
* becomes:
|
|
24
|
+
* { root: { '@attrs': { id: 'x' }, child: 'text' } }
|
|
25
|
+
*
|
|
26
|
+
* Multiple children with the same tag become arrays. Mixed content
|
|
27
|
+
* (text + elements interleaved) is preserved as { '#text': '...', ... }.
|
|
28
|
+
*
|
|
29
|
+
* Public API:
|
|
30
|
+
* xml.parse(input, opts?) → JS object | throws SafeXmlError
|
|
31
|
+
* xml.SafeXmlError → error class
|
|
32
|
+
*
|
|
33
|
+
* Defaults:
|
|
34
|
+
* maxBytes: 1 MiB
|
|
35
|
+
* maxDepth: 100
|
|
36
|
+
* maxElements: 10000
|
|
37
|
+
* maxAttributes: 100 per element
|
|
38
|
+
* allowDoctype: false
|
|
39
|
+
* allowProcessing: false (only <?xml version="..."?> is permitted)
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
var C = require("../constants");
|
|
43
|
+
var nb = require("../numeric-bounds");
|
|
44
|
+
var safeBuffer = require("../safe-buffer");
|
|
45
|
+
var { FrameworkError } = require("../framework-error");
|
|
46
|
+
|
|
47
|
+
class SafeXmlError extends FrameworkError {
|
|
48
|
+
constructor(message, code, position) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "SafeXmlError";
|
|
51
|
+
this.code = code || "xml/invalid";
|
|
52
|
+
this.position = position || null;
|
|
53
|
+
this.isSafeXmlError = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// parseInt radix — named so the call site doesn't carry a bare 16
|
|
58
|
+
// integer literal that reads as a byte count.
|
|
59
|
+
var RADIX_HEX = 0x10;
|
|
60
|
+
|
|
61
|
+
var DEFAULTS = {
|
|
62
|
+
maxBytes: C.BYTES.mib(1),
|
|
63
|
+
maxDepth: 100,
|
|
64
|
+
maxElements: 10_000,
|
|
65
|
+
maxAttributes: 100,
|
|
66
|
+
allowDoctype: false,
|
|
67
|
+
allowProcessing: false, // <?...?> other than the xml decl
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
var ABSOLUTE_MAX_BYTES = C.BYTES.mib(64);
|
|
71
|
+
var ABSOLUTE_MAX_DEPTH = 1_000;
|
|
72
|
+
var ABSOLUTE_MAX_ELEMENTS = 1_000_000;
|
|
73
|
+
var ABSOLUTE_MAX_ATTRIBUTES = 1_000;
|
|
74
|
+
|
|
75
|
+
// XML built-in entities (the ONLY entities allowed)
|
|
76
|
+
var BUILT_IN_ENTITIES = { lt: "<", gt: ">", amp: "&", quot: "\"", apos: "'" };
|
|
77
|
+
|
|
78
|
+
function _validateAndCap(name, value, defaultValue, ceiling) {
|
|
79
|
+
if (value === undefined) return defaultValue;
|
|
80
|
+
if (!nb.isPositiveFiniteInt(value)) {
|
|
81
|
+
throw new SafeXmlError("xml/bad-opt",
|
|
82
|
+
"xml.parse: " + name + " must be a positive finite integer; got " +
|
|
83
|
+
nb.shape(value));
|
|
84
|
+
}
|
|
85
|
+
return Math.min(value, ceiling);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parse(input, opts) {
|
|
89
|
+
opts = opts || {};
|
|
90
|
+
|
|
91
|
+
var maxBytes = _validateAndCap("maxBytes", opts.maxBytes, DEFAULTS.maxBytes, ABSOLUTE_MAX_BYTES);
|
|
92
|
+
var maxDepth = _validateAndCap("maxDepth", opts.maxDepth, DEFAULTS.maxDepth, ABSOLUTE_MAX_DEPTH);
|
|
93
|
+
var maxElements = _validateAndCap("maxElements", opts.maxElements, DEFAULTS.maxElements, ABSOLUTE_MAX_ELEMENTS);
|
|
94
|
+
var maxAttrs = _validateAndCap("maxAttributes", opts.maxAttributes, DEFAULTS.maxAttributes, ABSOLUTE_MAX_ATTRIBUTES);
|
|
95
|
+
var allowDoctype = !!opts.allowDoctype;
|
|
96
|
+
var allowProcessing = !!opts.allowProcessing;
|
|
97
|
+
|
|
98
|
+
input = safeBuffer.normalizeText(input, {
|
|
99
|
+
maxBytes: maxBytes,
|
|
100
|
+
errorClass: SafeXmlError,
|
|
101
|
+
typeCode: "xml/wrong-input-type",
|
|
102
|
+
sizeCode: "xml/too-large",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
var pos = 0;
|
|
106
|
+
var len = input.length;
|
|
107
|
+
var elementCount = 0;
|
|
108
|
+
|
|
109
|
+
function _err(msg, code) {
|
|
110
|
+
return new SafeXmlError(msg + " at position " + pos, code || "xml/invalid", pos);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Skip whitespace
|
|
114
|
+
function skipWs() {
|
|
115
|
+
while (pos < len) {
|
|
116
|
+
var c = input.charCodeAt(pos);
|
|
117
|
+
if (c === 0x20 || c === 0x09 || c === 0x0A || c === 0x0D) pos += 1;
|
|
118
|
+
else break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function expectChar(ch) {
|
|
123
|
+
if (input.charAt(pos) !== ch) {
|
|
124
|
+
throw _err("expected '" + ch + "'");
|
|
125
|
+
}
|
|
126
|
+
pos += 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Decode & / < / etc. — ONLY the 5 built-ins + numeric character refs.
|
|
130
|
+
// Custom entities are forbidden by design (XXE protection).
|
|
131
|
+
function decodeEntities(s) {
|
|
132
|
+
var out = "";
|
|
133
|
+
var i = 0;
|
|
134
|
+
while (i < s.length) {
|
|
135
|
+
var ch = s.charAt(i);
|
|
136
|
+
if (ch !== "&") { out += ch; i += 1; continue; }
|
|
137
|
+
var end = s.indexOf(";", i);
|
|
138
|
+
if (end < 0) throw _err("unterminated entity reference", "xml/bad-entity");
|
|
139
|
+
var name = s.substring(i + 1, end);
|
|
140
|
+
if (name.charAt(0) === "#") {
|
|
141
|
+
// Numeric character reference
|
|
142
|
+
var code;
|
|
143
|
+
if (name.charAt(1) === "x" || name.charAt(1) === "X") {
|
|
144
|
+
code = parseInt(name.substring(2), RADIX_HEX);
|
|
145
|
+
} else {
|
|
146
|
+
code = parseInt(name.substring(1), 10);
|
|
147
|
+
}
|
|
148
|
+
if (!Number.isFinite(code) || code < 0 || code > 0x10FFFF) {
|
|
149
|
+
throw _err("invalid numeric character reference", "xml/bad-entity");
|
|
150
|
+
}
|
|
151
|
+
// XML 1.0 forbids most C0 control chars
|
|
152
|
+
if ((code < 0x20 && code !== 0x09 && code !== 0x0A && code !== 0x0D) || (code >= 0xD800 && code <= 0xDFFF)) {
|
|
153
|
+
throw _err("character reference points to forbidden codepoint", "xml/bad-entity");
|
|
154
|
+
}
|
|
155
|
+
out += String.fromCodePoint(code);
|
|
156
|
+
i = end + 1;
|
|
157
|
+
} else if (BUILT_IN_ENTITIES[name] !== undefined) {
|
|
158
|
+
out += BUILT_IN_ENTITIES[name];
|
|
159
|
+
i = end + 1;
|
|
160
|
+
} else {
|
|
161
|
+
throw _err("unknown entity '" + name + "' (custom entities forbidden — XXE protection)", "xml/external-entity");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Parse a name (element / attribute name)
|
|
168
|
+
function parseName() {
|
|
169
|
+
var start = pos;
|
|
170
|
+
while (pos < len) {
|
|
171
|
+
var c = input.charCodeAt(pos);
|
|
172
|
+
// XML name characters — simplified subset (ASCII letters, digits, '-', '_', '.', ':')
|
|
173
|
+
if ((c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A) ||
|
|
174
|
+
(c >= 0x30 && c <= 0x39) ||
|
|
175
|
+
c === 0x2D || c === 0x5F || c === 0x2E || c === 0x3A ||
|
|
176
|
+
(c >= 0x80 && c <= 0xFFFF)) {
|
|
177
|
+
pos += 1;
|
|
178
|
+
} else break;
|
|
179
|
+
}
|
|
180
|
+
if (pos === start) throw _err("expected name", "xml/bad-name");
|
|
181
|
+
return input.substring(start, pos);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Parse an attribute value (single- or double-quoted)
|
|
185
|
+
function parseAttrValue() {
|
|
186
|
+
var quote = input.charAt(pos);
|
|
187
|
+
if (quote !== "\"" && quote !== "'") throw _err("expected quoted attribute value", "xml/bad-attr");
|
|
188
|
+
pos += 1;
|
|
189
|
+
var start = pos;
|
|
190
|
+
while (pos < len && input.charAt(pos) !== quote) {
|
|
191
|
+
if (input.charAt(pos) === "<") throw _err("'<' not allowed in attribute value", "xml/bad-attr");
|
|
192
|
+
pos += 1;
|
|
193
|
+
}
|
|
194
|
+
if (pos >= len) throw _err("unterminated attribute value", "xml/bad-attr");
|
|
195
|
+
var raw = input.substring(start, pos);
|
|
196
|
+
pos += 1;
|
|
197
|
+
return decodeEntities(raw);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Top-level: optional XML declaration, optional DOCTYPE (rejected), root element
|
|
201
|
+
function parseDocument() {
|
|
202
|
+
skipWs();
|
|
203
|
+
// XML declaration <?xml version="1.0" ... ?>
|
|
204
|
+
if (input.startsWith("<?xml", pos)) {
|
|
205
|
+
var declEnd = input.indexOf("?>", pos);
|
|
206
|
+
if (declEnd < 0) throw _err("unterminated XML declaration", "xml/bad-decl");
|
|
207
|
+
pos = declEnd + 2;
|
|
208
|
+
}
|
|
209
|
+
skipWs();
|
|
210
|
+
// DOCTYPE — rejected by default (XXE / billion-laughs vector)
|
|
211
|
+
if (input.startsWith("<!DOCTYPE", pos)) {
|
|
212
|
+
if (!allowDoctype) {
|
|
213
|
+
throw _err("DOCTYPE declarations are forbidden (XXE protection)", "xml/doctype");
|
|
214
|
+
}
|
|
215
|
+
// If allowed (operator opt-in), skip past the DOCTYPE without parsing internal subset
|
|
216
|
+
var doctypeEnd = pos;
|
|
217
|
+
var depth = 0;
|
|
218
|
+
while (doctypeEnd < len) {
|
|
219
|
+
var c = input.charAt(doctypeEnd);
|
|
220
|
+
if (c === "[") depth += 1;
|
|
221
|
+
else if (c === "]") depth -= 1;
|
|
222
|
+
else if (c === ">" && depth === 0) break;
|
|
223
|
+
doctypeEnd += 1;
|
|
224
|
+
}
|
|
225
|
+
pos = doctypeEnd + 1;
|
|
226
|
+
}
|
|
227
|
+
skipWs();
|
|
228
|
+
|
|
229
|
+
// Comments / processing instructions before root
|
|
230
|
+
while (input.startsWith("<!--", pos) || input.startsWith("<?", pos)) {
|
|
231
|
+
if (input.startsWith("<!--", pos)) {
|
|
232
|
+
var commentEnd = input.indexOf("-->", pos);
|
|
233
|
+
if (commentEnd < 0) throw _err("unterminated comment", "xml/bad-comment");
|
|
234
|
+
pos = commentEnd + 3;
|
|
235
|
+
} else {
|
|
236
|
+
// Processing instruction
|
|
237
|
+
if (!allowProcessing) {
|
|
238
|
+
throw _err("processing instructions are forbidden (allowProcessing: true to permit)", "xml/processing");
|
|
239
|
+
}
|
|
240
|
+
var piEnd = input.indexOf("?>", pos);
|
|
241
|
+
if (piEnd < 0) throw _err("unterminated processing instruction", "xml/bad-pi");
|
|
242
|
+
pos = piEnd + 2;
|
|
243
|
+
}
|
|
244
|
+
skipWs();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (input.charAt(pos) !== "<") throw _err("expected root element", "xml/no-root");
|
|
248
|
+
var root = parseElement(0);
|
|
249
|
+
skipWs();
|
|
250
|
+
if (pos !== len) {
|
|
251
|
+
// Trailing comments / whitespace OK; otherwise reject extra content
|
|
252
|
+
while (pos < len && input.startsWith("<!--", pos)) {
|
|
253
|
+
var ce = input.indexOf("-->", pos);
|
|
254
|
+
if (ce < 0) throw _err("unterminated trailing comment", "xml/bad-comment");
|
|
255
|
+
pos = ce + 3;
|
|
256
|
+
skipWs();
|
|
257
|
+
}
|
|
258
|
+
if (pos !== len) throw _err("unexpected content after root element", "xml/extra-content");
|
|
259
|
+
}
|
|
260
|
+
return root;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function parseElement(depth) {
|
|
264
|
+
if (depth > maxDepth) throw _err("nesting exceeds maxDepth", "xml/too-deep");
|
|
265
|
+
elementCount += 1;
|
|
266
|
+
if (elementCount > maxElements) throw _err("element count exceeds maxElements", "xml/too-many-elements");
|
|
267
|
+
|
|
268
|
+
expectChar("<");
|
|
269
|
+
var name = parseName();
|
|
270
|
+
var attrs = {};
|
|
271
|
+
var attrCount = 0;
|
|
272
|
+
|
|
273
|
+
while (pos < len) {
|
|
274
|
+
// Look for attribute, '/>', or '>'
|
|
275
|
+
var c = input.charAt(pos);
|
|
276
|
+
if (c === "/") { pos += 1; expectChar(">"); return _wrap(name, attrs, []); }
|
|
277
|
+
if (c === ">") { pos += 1; break; }
|
|
278
|
+
if (c === " " || c === "\t" || c === "\n" || c === "\r") { pos += 1; continue; }
|
|
279
|
+
|
|
280
|
+
// Attribute
|
|
281
|
+
attrCount += 1;
|
|
282
|
+
if (attrCount > maxAttrs) throw _err("attribute count exceeds maxAttributes", "xml/too-many-attrs");
|
|
283
|
+
var attrName = parseName();
|
|
284
|
+
skipWs();
|
|
285
|
+
expectChar("=");
|
|
286
|
+
skipWs();
|
|
287
|
+
var attrValue = parseAttrValue();
|
|
288
|
+
if (attrs[attrName] !== undefined) {
|
|
289
|
+
throw _err("duplicate attribute '" + attrName + "'", "xml/duplicate-attr");
|
|
290
|
+
}
|
|
291
|
+
attrs[attrName] = attrValue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Children + text
|
|
295
|
+
var children = [];
|
|
296
|
+
while (pos < len) {
|
|
297
|
+
if (input.startsWith("</", pos)) {
|
|
298
|
+
pos += 2;
|
|
299
|
+
var endName = parseName();
|
|
300
|
+
if (endName !== name) throw _err("mismatched end tag </" + endName + "> for <" + name + ">", "xml/mismatched-tag");
|
|
301
|
+
skipWs();
|
|
302
|
+
expectChar(">");
|
|
303
|
+
return _wrap(name, attrs, children);
|
|
304
|
+
}
|
|
305
|
+
if (input.startsWith("<!--", pos)) {
|
|
306
|
+
var commentEnd = input.indexOf("-->", pos);
|
|
307
|
+
if (commentEnd < 0) throw _err("unterminated comment", "xml/bad-comment");
|
|
308
|
+
pos = commentEnd + 3;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (input.startsWith("<![CDATA[", pos)) {
|
|
312
|
+
var cdataEnd = input.indexOf("]]>", pos + 9);
|
|
313
|
+
if (cdataEnd < 0) throw _err("unterminated CDATA", "xml/bad-cdata");
|
|
314
|
+
children.push({ kind: "text", value: input.substring(pos + 9, cdataEnd) });
|
|
315
|
+
pos = cdataEnd + 3;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (input.startsWith("<?", pos)) {
|
|
319
|
+
if (!allowProcessing) throw _err("processing instructions forbidden", "xml/processing");
|
|
320
|
+
var piEnd = input.indexOf("?>", pos);
|
|
321
|
+
if (piEnd < 0) throw _err("unterminated PI", "xml/bad-pi");
|
|
322
|
+
pos = piEnd + 2;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (input.charAt(pos) === "<") {
|
|
326
|
+
children.push({ kind: "element", value: parseElement(depth + 1) });
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
// Text
|
|
330
|
+
var textStart = pos;
|
|
331
|
+
while (pos < len && input.charAt(pos) !== "<") pos += 1;
|
|
332
|
+
var rawText = input.substring(textStart, pos);
|
|
333
|
+
if (rawText.length > 0) {
|
|
334
|
+
children.push({ kind: "text", value: decodeEntities(rawText) });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
throw _err("unexpected end of input inside <" + name + ">", "xml/truncated");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Wrap parsed element into the JS-object shape.
|
|
341
|
+
function _wrap(name, attrs, children) {
|
|
342
|
+
var elementChildren = children.filter(function (c) { return c.kind === "element"; });
|
|
343
|
+
var textParts = children.filter(function (c) { return c.kind === "text"; }).map(function (c) { return c.value; });
|
|
344
|
+
var hasAttrs = Object.keys(attrs).length > 0;
|
|
345
|
+
|
|
346
|
+
// Self-closing or empty element with no attrs → empty string
|
|
347
|
+
if (children.length === 0 && !hasAttrs) {
|
|
348
|
+
return _make(name, "");
|
|
349
|
+
}
|
|
350
|
+
if (elementChildren.length === 0 && !hasAttrs) {
|
|
351
|
+
// Pure-text element → string
|
|
352
|
+
return _make(name, textParts.join("").trim() === "" ? textParts.join("") : textParts.join(""));
|
|
353
|
+
}
|
|
354
|
+
// Mixed / attributed element → object
|
|
355
|
+
var obj = {};
|
|
356
|
+
if (hasAttrs) obj["@attrs"] = attrs;
|
|
357
|
+
var grouped = {};
|
|
358
|
+
for (var i = 0; i < elementChildren.length; i++) {
|
|
359
|
+
var childWrap = elementChildren[i].value;
|
|
360
|
+
var childName = Object.keys(childWrap)[0];
|
|
361
|
+
var childVal = childWrap[childName];
|
|
362
|
+
if (grouped[childName] === undefined) {
|
|
363
|
+
grouped[childName] = childVal;
|
|
364
|
+
} else if (Array.isArray(grouped[childName])) {
|
|
365
|
+
grouped[childName].push(childVal);
|
|
366
|
+
} else {
|
|
367
|
+
grouped[childName] = [grouped[childName], childVal];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
Object.assign(obj, grouped);
|
|
371
|
+
var combinedText = textParts.join("").replace(/\s+/g, " ").trim();
|
|
372
|
+
if (combinedText.length > 0) obj["#text"] = combinedText;
|
|
373
|
+
return _make(name, obj);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function _make(name, value) {
|
|
377
|
+
var out = {};
|
|
378
|
+
out[name] = value;
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return parseDocument();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
module.exports = {
|
|
386
|
+
parse: parse,
|
|
387
|
+
SafeXmlError: SafeXmlError,
|
|
388
|
+
DEFAULTS: DEFAULTS,
|
|
389
|
+
BUILT_IN_ENTITIES: BUILT_IN_ENTITIES,
|
|
390
|
+
};
|