Exclude "&" from the XML entity names

Scanning to the end of the string for every "&" made the entity
resolution quadratic. Stopping at the next "&" also fixes a bare
ampersand swallowing the reference which follows it.
This commit is contained in:
calixteman 2026-08-01 15:54:54 +02:00
parent d0779c411e
commit f7f30dd844
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
2 changed files with 31 additions and 1 deletions

View File

@ -49,7 +49,8 @@ function isWhitespaceString(s) {
class XMLParserBase { class XMLParserBase {
static get _entityRegex() { static get _entityRegex() {
return shadow(this, "_entityRegex", /&(?:#x([^;]+)|#([^;]+)|([^;]+));/g); // Entity references cannot contain "&", keeping the scan linear.
return shadow(this, "_entityRegex", /&(?:#x([^;&]+)|#([^;&]+)|([^;&]+));/g);
} }
_resolveEntities(s) { _resolveEntities(s) {

View File

@ -153,4 +153,33 @@ describe("XML", function () {
["foo", ""], ["foo", ""],
]); ]);
}); });
describe("entities", function () {
const parseText = xml =>
new SimpleXMLParser({}).parseFromString(xml).documentElement.textContent;
it("should resolve the entities", function () {
expect(
parseText("<a>&lt;b&gt; &amp; &quot;c&quot; &apos;d&apos;</a>")
).toEqual(`<b> & "c" 'd'`);
expect(parseText("<a>&#65;&#x42;</a>")).toEqual("AB");
});
it("should keep the unresolved entities as-is", function () {
expect(parseText("<a>&unknown; a&b;c</a>")).toEqual("&unknown; a&b;c");
});
it("should resolve an entity preceded by a bare ampersand", function () {
expect(parseText("<a>AT&T &amp; Co</a>")).toEqual("AT&T & Co");
expect(parseText("<a>&&amp;</a>")).toEqual("&&");
});
it("should handle a long run of ampersands efficiently", function () {
const text = "&".repeat(100000);
const startTime = performance.now();
expect(parseText(`<a>${text}</a>`)).toEqual(text);
expect(performance.now() - startTime).toBeLessThan(1000);
});
});
}); });