Skip to content

Commit 44de2a3

Browse files
avoid quadratic quote stripping in MultiPartName.unquote
1 parent 2927bad commit 44de2a3

2 files changed

Lines changed: 55 additions & 4 deletions

File tree

‎src/main/java/net/sf/jsqlparser/schema/MultiPartName.java‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,34 @@ public interface MultiPartName {
2323
* @return the pure identifier without quotes
2424
*/
2525
static String unquote(String quotedIdentifier) {
26-
return quotedIdentifier != null
27-
? LEADING_TRAILING_QUOTES_PATTERN.matcher(quotedIdentifier).replaceAll("")
28-
: null;
26+
if (quotedIdentifier == null) {
27+
return null;
28+
}
29+
// same result as LEADING_TRAILING_QUOTES_PATTERN.matcher(..).replaceAll(""), but linear:
30+
// the regex retries its trailing alternative at every offset of a run of quotes
31+
int start = 0;
32+
int end = quotedIdentifier.length();
33+
while (start < end && isLeadingQuote(quotedIdentifier.charAt(start))) {
34+
start++;
35+
}
36+
while (end > start && isTrailingQuote(quotedIdentifier.charAt(end - 1))) {
37+
end--;
38+
}
39+
return quotedIdentifier.substring(start, end);
2940
}
3041

3142
static boolean isQuoted(String identifier) {
32-
return identifier != null && LEADING_TRAILING_QUOTES_PATTERN.matcher(identifier).find();
43+
return identifier != null && !identifier.isEmpty()
44+
&& (isLeadingQuote(identifier.charAt(0))
45+
|| isTrailingQuote(identifier.charAt(identifier.length() - 1)));
46+
}
47+
48+
private static boolean isLeadingQuote(char c) {
49+
return c == '"' || c == '[' || c == '`';
50+
}
51+
52+
private static boolean isTrailingQuote(char c) {
53+
return c == '"' || c == ']' || c == '`';
3354
}
3455

3556
String getFullyQualifiedName();

‎src/test/java/net/sf/jsqlparser/schema/MultiPartNameTest.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
*/
1010
package net.sf.jsqlparser.schema;
1111

12+
import java.time.Duration;
13+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
14+
import net.sf.jsqlparser.statement.select.PlainSelect;
1215
import org.assertj.core.api.Assertions;
1316
import org.junit.jupiter.api.Test;
1417

@@ -21,4 +24,31 @@ void replaceBackticksWithDoubleQuotes() {
2124
Assertions.assertThat("\"starbake\".\"customers\"").isEqualToIgnoringCase(
2225
MultiPartName.replaceBackticksWithDoubleQuotes("`starbake`.`customers`"));
2326
}
27+
28+
@Test
29+
void unquoteMatchesQuotePattern() {
30+
String[] names = {"", "a", "\"a\"", "[a]", "`a`", "\"a\".\"b\"", "\"\"\"\"", "\"]",
31+
"[\"a\"]", "a\"", "\"a", "]a[", "``a``", "\"a\"\"b\"", "[[a]]"};
32+
for (String name : names) {
33+
assertEquals(MultiPartName.LEADING_TRAILING_QUOTES_PATTERN.matcher(name).replaceAll(""),
34+
MultiPartName.unquote(name), name);
35+
assertEquals(MultiPartName.LEADING_TRAILING_QUOTES_PATTERN.matcher(name).find(),
36+
MultiPartName.isQuoted(name), name);
37+
}
38+
assertNull(MultiPartName.unquote(null));
39+
assertFalse(MultiPartName.isQuoted(null));
40+
}
41+
42+
@Test
43+
void unquoteIsLinearOnRunsOfEscapedQuotes() {
44+
// a quoted identifier with a dot and many doubled quotes used to backtrack quadratically
45+
String quotes = "\"\"".repeat(40000);
46+
String sql = "SELECT * FROM \"a" + quotes + ".b\"";
47+
PlainSelect select = (PlainSelect) assertTimeoutPreemptively(Duration.ofSeconds(2),
48+
() -> CCJSqlParserUtil.parse(sql));
49+
assertNotNull(select.getFromItem());
50+
assertEquals("a" + quotes + ".b",
51+
assertTimeoutPreemptively(Duration.ofSeconds(2),
52+
() -> MultiPartName.unquote("\"a" + quotes + ".b\"")));
53+
}
2454
}

0 commit comments

Comments
 (0)