@flighthq/xml 0.3.0-next.906.07cea63 → 0.3.0
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/dist/xmlParse.d.ts.map +1 -1
- package/dist/xmlParse.js +142 -22
- package/dist/xmlParse.js.map +1 -1
- package/package.json +4 -2
- package/src/xmlParse.test.ts +113 -0
package/dist/xmlParse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xmlParse.d.ts","sourceRoot":"","sources":["../src/xmlParse.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"xmlParse.d.ts","sourceRoot":"","sources":["../src/xmlParse.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE3D;8EAC8E;AAC9E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxE;AAED;;qEAEqE;AACrE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAY/D"}
|
package/dist/xmlParse.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Tree-building (DOM-style) XML parser sufficient for atlas and plist file formats.
|
|
2
2
|
// Not a general-purpose XML parser, but handles namespaced/extra attributes and elements,
|
|
3
3
|
// both double-quoted and single-quoted attribute values, XML entity escapes
|
|
4
|
-
// (& < > " ' plus numeric references),
|
|
5
|
-
// CDATA sections (<![CDATA[...]]>), the XML declaration,
|
|
4
|
+
// (& < > " ' plus numeric references), general entities declared in a DOCTYPE's
|
|
5
|
+
// internal subset, XML comments (<!-- -->), CDATA sections (<![CDATA[...]]>), the XML declaration,
|
|
6
|
+
// and DOCTYPE.
|
|
6
7
|
/** Parse all attributes from an element's attribute string.
|
|
7
8
|
* Supports double-quoted and single-quoted values and XML entity escapes. */
|
|
8
9
|
export function parseXmlAttributes(attrs) {
|
|
@@ -21,16 +22,20 @@ export function parseXmlAttributes(attrs) {
|
|
|
21
22
|
* Returns the root element, or null when the input contains no recognizable element.
|
|
22
23
|
* Does not validate DTD, namespaces, or processing instructions. */
|
|
23
24
|
export function parseXmlDocument(xml) {
|
|
24
|
-
// Normalize: strip comments
|
|
25
|
-
let src =
|
|
26
|
-
// Strip XML declaration and DOCTYPE
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return parseElement(src, { pos: 0 });
|
|
25
|
+
// Normalize: strip comments and normalize line endings.
|
|
26
|
+
let src = stripXmlComments(xml).replace(/\r\n?/g, '\n');
|
|
27
|
+
// Strip XML declaration and DOCTYPE before parsing the root element. The DOCTYPE is discarded but
|
|
28
|
+
// its internal subset is read on the way out, because a general entity's replacement text is markup
|
|
29
|
+
// and so has to be substituted into the source before the tree is built, not decoded out of a text
|
|
30
|
+
// node afterwards.
|
|
31
|
+
const entities = {};
|
|
32
|
+
src = stripXmlDoctypes(src.replace(/<\?[\s\S]*?\?>/g, ''), entities).trim();
|
|
33
|
+
return parseElement(expandXmlEntities(src, entities), { pos: 0 });
|
|
33
34
|
}
|
|
35
|
+
// An entity may nest a few levels legitimately; past that a document is recursing, not composing.
|
|
36
|
+
const MAX_XML_ENTITY_PASSES = 8;
|
|
37
|
+
const MAX_XML_ENTITY_GROWTH = 16;
|
|
38
|
+
const MAX_XML_ENTITY_BUDGET = 65536;
|
|
34
39
|
const XML_ENTITIES = {
|
|
35
40
|
amp: '&',
|
|
36
41
|
apos: "'",
|
|
@@ -38,13 +43,46 @@ const XML_ENTITIES = {
|
|
|
38
43
|
lt: '<',
|
|
39
44
|
quot: '"',
|
|
40
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Substitutes declared general entities into the source ahead of parsing.
|
|
48
|
+
*
|
|
49
|
+
* A replacement may itself reference an entity, so expansion repeats until it settles. Both the pass
|
|
50
|
+
* count and the resulting size are capped: entities that reference each other are otherwise an
|
|
51
|
+
* exponential bomb ("billion laughs"), and the budget is what keeps a hostile document costing
|
|
52
|
+
* bounded work. Exhausting the budget stops expansion and keeps what resolved, since a partially
|
|
53
|
+
* expanded document still parses.
|
|
54
|
+
*
|
|
55
|
+
* Only the predefined five and declared general entities substitute. An undeclared reference is left
|
|
56
|
+
* alone for `decodeXmlEntities` to see as text.
|
|
57
|
+
*/
|
|
58
|
+
function expandXmlEntities(src, entities) {
|
|
59
|
+
let output = src;
|
|
60
|
+
const budget = src.length * MAX_XML_ENTITY_GROWTH + MAX_XML_ENTITY_BUDGET;
|
|
61
|
+
for (let pass = 0; pass < MAX_XML_ENTITY_PASSES; pass++) {
|
|
62
|
+
let expanded = false;
|
|
63
|
+
const next = output.replace(/&([\w:.-]+);/g, (reference, name) => {
|
|
64
|
+
const replacement = entities[name];
|
|
65
|
+
if (replacement === undefined)
|
|
66
|
+
return reference;
|
|
67
|
+
expanded = true;
|
|
68
|
+
return replacement;
|
|
69
|
+
});
|
|
70
|
+
if (!expanded || next.length > budget)
|
|
71
|
+
return output;
|
|
72
|
+
output = next;
|
|
73
|
+
}
|
|
74
|
+
return output;
|
|
75
|
+
}
|
|
41
76
|
function decodeXmlEntities(s) {
|
|
42
|
-
return s.replace(/&(?:#(\d+)|#x([\da-fA-F]+)|(\w+));/g, (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
77
|
+
return s.replace(/&(?:#(\d+)|#x([\da-fA-F]+)|(\w+));/g, (reference, dec, hex, name) => {
|
|
78
|
+
const numeric = dec ?? hex;
|
|
79
|
+
if (numeric !== undefined) {
|
|
80
|
+
const codepoint = parseInt(numeric, dec !== undefined ? 10 : 16);
|
|
81
|
+
if (codepoint > 0x10ffff || (codepoint >= 0xd800 && codepoint <= 0xdfff))
|
|
82
|
+
return reference;
|
|
83
|
+
return String.fromCodePoint(codepoint);
|
|
84
|
+
}
|
|
85
|
+
return XML_ENTITIES[name] ?? reference;
|
|
48
86
|
});
|
|
49
87
|
}
|
|
50
88
|
function parseElement(src, state) {
|
|
@@ -109,6 +147,17 @@ function parseElement(src, state) {
|
|
|
109
147
|
content.push(decoded);
|
|
110
148
|
continue;
|
|
111
149
|
}
|
|
150
|
+
if (src.slice(state.pos, state.pos + 9) === '<![CDATA[') {
|
|
151
|
+
const cdataStart = state.pos + 9;
|
|
152
|
+
const cdataEnd = src.indexOf(']]>', cdataStart);
|
|
153
|
+
const contentEnd = cdataEnd >= 0 ? cdataEnd : src.length;
|
|
154
|
+
const cdata = src.slice(cdataStart, contentEnd);
|
|
155
|
+
text += cdata.trim();
|
|
156
|
+
if (cdata !== '')
|
|
157
|
+
content.push(cdata);
|
|
158
|
+
state.pos = cdataEnd >= 0 ? cdataEnd + 3 : src.length;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
112
161
|
// Check for closing tag
|
|
113
162
|
if (src[state.pos + 1] === '/') {
|
|
114
163
|
// Skip to '>'
|
|
@@ -130,11 +179,82 @@ function skipWhitespace(src, state) {
|
|
|
130
179
|
while (state.pos < src.length && /\s/.test(src[state.pos]))
|
|
131
180
|
state.pos++;
|
|
132
181
|
}
|
|
133
|
-
function stripCdata(xml) {
|
|
134
|
-
// Replace CDATA with its raw content
|
|
135
|
-
return xml.replace(/<!\[CDATA\[[\s\S]*?]]>/g, (m) => m.slice(9, m.length - 3));
|
|
136
|
-
}
|
|
137
182
|
function stripXmlComments(xml) {
|
|
138
|
-
|
|
183
|
+
let copyStart = 0;
|
|
184
|
+
let output = '';
|
|
185
|
+
let pos = 0;
|
|
186
|
+
while (pos < xml.length) {
|
|
187
|
+
if (xml.slice(pos, pos + 9) === '<![CDATA[') {
|
|
188
|
+
const cdataEnd = xml.indexOf(']]>', pos + 9);
|
|
189
|
+
pos = cdataEnd >= 0 ? cdataEnd + 3 : xml.length;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (xml.slice(pos, pos + 4) !== '<!--') {
|
|
193
|
+
pos++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
output += xml.slice(copyStart, pos);
|
|
197
|
+
const commentEnd = xml.indexOf('-->', pos + 4);
|
|
198
|
+
pos = commentEnd >= 0 ? commentEnd + 3 : xml.length;
|
|
199
|
+
copyStart = pos;
|
|
200
|
+
}
|
|
201
|
+
return output + xml.slice(copyStart);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Removes every DOCTYPE, collecting the general entities its internal subset declares into `out`.
|
|
205
|
+
*
|
|
206
|
+
* Parameter entities (`<!ENTITY % name …>`) and external ones (`SYSTEM` / `PUBLIC`) are deliberately
|
|
207
|
+
* not collected: an external entity resolves a URL or a file path at parse time, which is a document
|
|
208
|
+
* reading whatever the process can reach, so the parser has no business honoring one.
|
|
209
|
+
*/
|
|
210
|
+
function stripXmlDoctypes(xml, out) {
|
|
211
|
+
let copyStart = 0;
|
|
212
|
+
let output = '';
|
|
213
|
+
let pos = 0;
|
|
214
|
+
while (pos < xml.length) {
|
|
215
|
+
if (xml[pos] !== '<' || xml.slice(pos, pos + 9).toLowerCase() !== '<!doctype') {
|
|
216
|
+
pos++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
output += xml.slice(copyStart, pos);
|
|
220
|
+
const doctypeStart = pos;
|
|
221
|
+
pos += 9;
|
|
222
|
+
let internalSubsetDepth = 0;
|
|
223
|
+
let quote = '';
|
|
224
|
+
while (pos < xml.length) {
|
|
225
|
+
const ch = xml[pos];
|
|
226
|
+
if (quote) {
|
|
227
|
+
if (ch === quote)
|
|
228
|
+
quote = '';
|
|
229
|
+
}
|
|
230
|
+
else if (ch === '"' || ch === "'") {
|
|
231
|
+
quote = ch;
|
|
232
|
+
}
|
|
233
|
+
else if (ch === '[') {
|
|
234
|
+
internalSubsetDepth++;
|
|
235
|
+
}
|
|
236
|
+
else if (ch === ']' && internalSubsetDepth > 0) {
|
|
237
|
+
internalSubsetDepth--;
|
|
238
|
+
}
|
|
239
|
+
else if (ch === '>' && internalSubsetDepth === 0) {
|
|
240
|
+
pos++;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
pos++;
|
|
244
|
+
}
|
|
245
|
+
collectXmlEntityDeclarations(xml.slice(doctypeStart, pos), out);
|
|
246
|
+
copyStart = pos;
|
|
247
|
+
}
|
|
248
|
+
return output + xml.slice(copyStart);
|
|
249
|
+
}
|
|
250
|
+
// The name must be followed directly by a quoted replacement, which is what excludes both the
|
|
251
|
+
// parameter form (a `%` where the name belongs) and the external form (a SYSTEM/PUBLIC keyword where
|
|
252
|
+
// the quote belongs) without testing for either.
|
|
253
|
+
function collectXmlEntityDeclarations(doctype, out) {
|
|
254
|
+
const declaration = /<!ENTITY\s+([\w:.-]+)\s*(?:"([^"]*)"|'([^']*)')\s*>/g;
|
|
255
|
+
let match;
|
|
256
|
+
while ((match = declaration.exec(doctype)) !== null) {
|
|
257
|
+
out[match[1]] = match[2] ?? match[3] ?? '';
|
|
258
|
+
}
|
|
139
259
|
}
|
|
140
260
|
//# sourceMappingURL=xmlParse.js.map
|
package/dist/xmlParse.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xmlParse.js","sourceRoot":"","sources":["../src/xmlParse.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,0FAA0F;AAC1F,4EAA4E;AAC5E,
|
|
1
|
+
{"version":3,"file":"xmlParse.js","sourceRoot":"","sources":["../src/xmlParse.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/xml",
|
|
3
|
-
"version": "0.3.0
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
4
6
|
"repository": {
|
|
5
7
|
"type": "git",
|
|
6
8
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@flighthq/types": "0.3.0
|
|
41
|
+
"@flighthq/types": "0.3.0"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
44
|
"typescript": "^5.3.0"
|
package/src/xmlParse.test.ts
CHANGED
|
@@ -39,6 +39,11 @@ describe('parseXmlAttributes', () => {
|
|
|
39
39
|
expect(attrs['name']).toBe('A');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
it('preserves out-of-range numeric references without throwing', () => {
|
|
43
|
+
const attrs = parseXmlAttributes('decimal="�" hex="�"');
|
|
44
|
+
expect(attrs).toEqual({ decimal: '�', hex: '�' });
|
|
45
|
+
});
|
|
46
|
+
|
|
42
47
|
it('returns empty object for empty string', () => {
|
|
43
48
|
expect(parseXmlAttributes('')).toEqual({});
|
|
44
49
|
});
|
|
@@ -145,6 +150,14 @@ describe('parseXmlDocument', () => {
|
|
|
145
150
|
expect(doc?.text).toBe('Hello!');
|
|
146
151
|
});
|
|
147
152
|
|
|
153
|
+
it('preserves markup and comment syntax inside CDATA as text', () => {
|
|
154
|
+
const doc = parseXmlDocument('<root><![CDATA[<tag>&<!-- literal -->]]><child/>tail</root>');
|
|
155
|
+
|
|
156
|
+
expect(doc?.children.map((child) => child.name)).toEqual(['child']);
|
|
157
|
+
expect(doc?.content[0]).toBe('<tag>&<!-- literal -->');
|
|
158
|
+
expect(doc?.text).toBe('<tag>&<!-- literal -->tail');
|
|
159
|
+
});
|
|
160
|
+
|
|
148
161
|
it('keeps a > inside a quoted attribute value as data, not tag-end', () => {
|
|
149
162
|
const root = parseXmlDocument('<node attr="a>b"/>');
|
|
150
163
|
expect(root?.name).toBe('node');
|
|
@@ -167,4 +180,104 @@ describe('parseXmlDocument', () => {
|
|
|
167
180
|
expect(doc?.name).toBe('plist');
|
|
168
181
|
expect(doc?.attributes.version).toBe('1.0');
|
|
169
182
|
});
|
|
183
|
+
|
|
184
|
+
it('strips an internal subset when an entity value contains ] and >', () => {
|
|
185
|
+
const xml = '<!DOCTYPE root [<!ENTITY tricky "a ] > b"><!ELEMENT root (#PCDATA)>]>' + '<root value="ok"/>';
|
|
186
|
+
const doc = parseXmlDocument(xml);
|
|
187
|
+
expect(doc?.name).toBe('root');
|
|
188
|
+
expect(doc?.attributes.value).toBe('ok');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('strips a DOCTYPE when its external identifier contains >', () => {
|
|
192
|
+
const xml = '<!DOCTYPE root SYSTEM "https://example.invalid/a>b.dtd"><root/>';
|
|
193
|
+
const doc = parseXmlDocument(xml);
|
|
194
|
+
expect(doc?.name).toBe('root');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// A general entity's replacement text is markup, so it has to be substituted into the source before
|
|
199
|
+
// the tree is built. Declarations live in a DOCTYPE's internal subset, which is otherwise discarded.
|
|
200
|
+
describe('parseXmlDocument internal DTD entities', () => {
|
|
201
|
+
const doctype = (subset: string, body: string): string =>
|
|
202
|
+
`<?xml version="1.0"?><!DOCTYPE root [${subset}]><root>${body}</root>`;
|
|
203
|
+
|
|
204
|
+
it('expands an entity whose replacement is an element', () => {
|
|
205
|
+
const root = parseXmlDocument(doctype('<!ENTITY dot "<circle r=\'4\'/>">', '˙'))!;
|
|
206
|
+
|
|
207
|
+
expect(root.children).toHaveLength(1);
|
|
208
|
+
expect(root.children[0].name).toBe('circle');
|
|
209
|
+
expect(root.children[0].attributes.r).toBe('4');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('expands the same entity at every reference', () => {
|
|
213
|
+
const root = parseXmlDocument(doctype('<!ENTITY dot "<circle/>">', '˙˙˙'))!;
|
|
214
|
+
|
|
215
|
+
expect(root.children).toHaveLength(3);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('expands an entity declared with single quotes', () => {
|
|
219
|
+
const root = parseXmlDocument(doctype("<!ENTITY dot '<circle/>'", '˙').replace(']', '>]'))!;
|
|
220
|
+
|
|
221
|
+
expect(root.children[0].name).toBe('circle');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('expands an entity that references another entity', () => {
|
|
225
|
+
const subset = '<!ENTITY inner "<circle/>"><!ENTITY outer "<g>&inner;</g>">';
|
|
226
|
+
const root = parseXmlDocument(doctype(subset, '&outer;'))!;
|
|
227
|
+
|
|
228
|
+
expect(root.children[0].name).toBe('g');
|
|
229
|
+
expect(root.children[0].children[0].name).toBe('circle');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('expands an entity used as an attribute value', () => {
|
|
233
|
+
const root = parseXmlDocument(doctype('<!ENTITY w "40">', '<rect width="&w;"/>'))!;
|
|
234
|
+
|
|
235
|
+
expect(root.children[0].attributes.width).toBe('40');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('leaves an undeclared reference alone rather than dropping it', () => {
|
|
239
|
+
const root = parseXmlDocument(doctype('<!ENTITY dot "<circle/>">', '&missing;'))!;
|
|
240
|
+
|
|
241
|
+
expect(root.text).toBe('&missing;');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('still decodes the predefined entities, which no subset needs to declare', () => {
|
|
245
|
+
const root = parseXmlDocument(doctype('<!ENTITY dot "<circle/>">', 'a & b'))!;
|
|
246
|
+
|
|
247
|
+
expect(root.text).toBe('a & b');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// An external entity resolves a URL or file path at parse time — a document reading whatever the
|
|
251
|
+
// process can reach. Not honoring one is the point, not a gap.
|
|
252
|
+
it.each([
|
|
253
|
+
{ form: 'external SYSTEM', subset: '<!ENTITY x SYSTEM "/etc/passwd">' },
|
|
254
|
+
{ form: 'external PUBLIC', subset: '<!ENTITY x PUBLIC "-//X//EN" "http://example.test/x">' },
|
|
255
|
+
{ form: 'parameter', subset: '<!ENTITY % x "<circle/>">' },
|
|
256
|
+
])('does not honor an $form entity', ({ subset }) => {
|
|
257
|
+
const root = parseXmlDocument(doctype(subset, '&x;'))!;
|
|
258
|
+
|
|
259
|
+
expect(root.children).toHaveLength(0);
|
|
260
|
+
expect(root.text).toBe('&x;');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Entities that reference each other expand exponentially. The budget is what keeps a hostile
|
|
264
|
+
// document costing bounded work; it stops expanding and keeps what resolved.
|
|
265
|
+
it('does not expand a nested entity without bound', () => {
|
|
266
|
+
// Six levels, each ten references deep: fully expanded this is ten million characters, so an
|
|
267
|
+
// unbudgeted parser materializes it and a budgeted one stops well short.
|
|
268
|
+
let subset = '<!ENTITY e0 "aaaaaaaaaa">';
|
|
269
|
+
for (let level = 1; level <= 6; level++) {
|
|
270
|
+
subset += `<!ENTITY e${level} "${`&e${level - 1};`.repeat(10)}">`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const root = parseXmlDocument(doctype(subset, '&e6;'))!;
|
|
274
|
+
|
|
275
|
+
expect(root.text.length).toBeLessThan(200000);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('terminates on an entity that references itself', () => {
|
|
279
|
+
const root = parseXmlDocument(doctype('<!ENTITY loop "&loop;">', '&loop;'))!;
|
|
280
|
+
|
|
281
|
+
expect(root).not.toBeNull();
|
|
282
|
+
});
|
|
170
283
|
});
|