@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.
@@ -1 +1 @@
1
- {"version":3,"file":"xmlParse.d.ts","sourceRoot":"","sources":["../src/xmlParse.ts"],"names":[],"mappings":"AAMA,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"}
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
- // (&amp; &lt; &gt; &quot; &apos; plus numeric references), XML comments (<!-- -->),
5
- // CDATA sections (<![CDATA[...]]>), the XML declaration, and DOCTYPE.
4
+ // (&amp; &lt; &gt; &quot; &apos; 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, process CDATA, normalize line endings
25
- let src = stripCdata(stripXmlComments(xml)).replace(/\r\n?/g, '\n');
26
- // Strip XML declaration and DOCTYPE (including an internal subset `[ ... ]`,
27
- // whose contents may hold `>` characters the flat `[^>]*` form would stop at).
28
- src = src
29
- .replace(/<\?[\s\S]*?\?>/g, '')
30
- .replace(/<!DOCTYPE[^>[]*(?:\[[\s\S]*?\][^>]*)?>/gi, '')
31
- .trim();
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, (_, dec, hex, name) => {
43
- if (dec)
44
- return String.fromCodePoint(parseInt(dec, 10));
45
- if (hex)
46
- return String.fromCodePoint(parseInt(hex, 16));
47
- return XML_ENTITIES[name] ?? _;
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
- return xml.replace(/<!--[\s\S]*?-->/g, '');
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
@@ -1 +1 @@
1
- {"version":3,"file":"xmlParse.js","sourceRoot":"","sources":["../src/xmlParse.ts"],"names":[],"mappings":"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"}
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-next.906.07cea63",
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-next.906.07cea63"
41
+ "@flighthq/types": "0.3.0"
40
42
  },
41
43
  "devDependencies": {
42
44
  "typescript": "^5.3.0"
@@ -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="&#1114112;" hex="&#x110000;"');
44
+ expect(attrs).toEqual({ decimal: '&#1114112;', hex: '&#x110000;' });
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>&amp;<!-- literal -->]]><child/>tail</root>');
155
+
156
+ expect(doc?.children.map((child) => child.name)).toEqual(['child']);
157
+ expect(doc?.content[0]).toBe('<tag>&amp;<!-- literal -->');
158
+ expect(doc?.text).toBe('<tag>&amp;<!-- 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\'/>">', '&dot;'))!;
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/>">', '&dot;&dot;&dot;'))!;
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/>'", '&dot;').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 &amp; 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
  });