fix quadratic time when removing doctype declarations

This commit is contained in:
Athou
2026-11-03 16:37:01 +01:00
parent 69cf674a02
commit ae707a2602
2 changed files with 55 additions and 2 deletions

View File

@@ -8,12 +8,13 @@ import org.apache.commons.lang3.StringUtils;
import org.jdom2.Verifier;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Singleton
public class XMLCleaner {
private static final Pattern DOCTYPE_PATTERN = Pattern.compile("<!DOCTYPE[^>]*>", Pattern.CASE_INSENSITIVE);
private static final Pattern DOCTYPE_START_PATTERN = Pattern.compile("<!DOCTYPE", Pattern.CASE_INSENSITIVE);
private final Trie trie = Trie.builder().ignoreOverlaps().addKeywords(HtmlEntities.HTML_ENTITIES).build();
@@ -76,6 +77,22 @@ public class XMLCleaner {
return null;
}
return DOCTYPE_PATTERN.matcher(xml).replaceAll("");
// Equivalent to removing all matches of "<!DOCTYPE[^>]*>", without the quadratic cost of that regex when the
// input contains many "<!DOCTYPE" and no closing bracket after them.
Matcher matcher = DOCTYPE_START_PATTERN.matcher(xml);
StringBuilder sb = new StringBuilder(xml.length());
int pos = 0;
while (matcher.find(pos)) {
int end = xml.indexOf('>', matcher.end());
if (end < 0) {
break;
}
sb.append(xml, pos, matcher.start());
pos = end + 1;
}
return sb.append(xml, pos, xml.length()).toString();
}
}

View File

@@ -4,6 +4,8 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.time.Duration;
class XMLCleanerTest {
XMLCleaner xmlCleaner = new XMLCleaner();
@@ -279,5 +281,39 @@ class XMLCleanerTest {
String source = "<!DOCTYPE html ><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void preservesUnterminatedDoctype() {
String source = "<html></html><!DOCTYPE html";
Assertions.assertEquals(source, xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesTerminatedDoctypeAndPreservesUnterminatedDoctype() {
String source = "<!DOCTYPE html><html></html><!DOCTYPE html";
Assertions.assertEquals("<html></html><!DOCTYPE html", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesNestedDoctypeUpToFirstClosingBracket() {
String source = "<!DOCTYPE<!DOCTYPE html><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void handlesManyUnterminatedDoctypesInLinearTime() {
String source = "<!DOCTYPE".repeat(64000);
Assertions.assertTimeout(
Duration.ofSeconds(2),
() -> Assertions.assertEquals(source, xmlCleaner.removeDoctypeDeclarations(source)));
}
@Test
void handlesManyUnterminatedDoctypesAfterClosingBracketInLinearTime() {
String source = "<a>" + "<!DOCTYPE".repeat(64000);
Assertions.assertTimeout(
Duration.ofSeconds(2),
() -> Assertions.assertEquals(source, xmlCleaner.removeDoctypeDeclarations(source)));
}
}
}