From 44de2a326e36452a40de21ac95401b4121ecc63b Mon Sep 17 00:00:00 2001 From: tanjiro Kamado Date: Sun, 25 Oct 2026 23:14:03 +0530 Subject: [PATCH] avoid quadratic quote stripping in MultiPartName.unquote --- .../sf/jsqlparser/schema/MultiPartName.java | 29 +++++++++++++++--- .../jsqlparser/schema/MultiPartNameTest.java | 30 +++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java b/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java index ce954780d7..b49e58384e 100644 --- a/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java +++ b/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java @@ -23,13 +23,34 @@ public interface MultiPartName { * @return the pure identifier without quotes */ static String unquote(String quotedIdentifier) { - return quotedIdentifier != null - ? LEADING_TRAILING_QUOTES_PATTERN.matcher(quotedIdentifier).replaceAll("") - : null; + if (quotedIdentifier == null) { + return null; + } + // same result as LEADING_TRAILING_QUOTES_PATTERN.matcher(..).replaceAll(""), but linear: + // the regex retries its trailing alternative at every offset of a run of quotes + int start = 0; + int end = quotedIdentifier.length(); + while (start < end && isLeadingQuote(quotedIdentifier.charAt(start))) { + start++; + } + while (end > start && isTrailingQuote(quotedIdentifier.charAt(end - 1))) { + end--; + } + return quotedIdentifier.substring(start, end); } static boolean isQuoted(String identifier) { - return identifier != null && LEADING_TRAILING_QUOTES_PATTERN.matcher(identifier).find(); + return identifier != null && !identifier.isEmpty() + && (isLeadingQuote(identifier.charAt(0)) + || isTrailingQuote(identifier.charAt(identifier.length() - 1))); + } + + private static boolean isLeadingQuote(char c) { + return c == '"' || c == '[' || c == '`'; + } + + private static boolean isTrailingQuote(char c) { + return c == '"' || c == ']' || c == '`'; } String getFullyQualifiedName(); diff --git a/src/test/java/net/sf/jsqlparser/schema/MultiPartNameTest.java b/src/test/java/net/sf/jsqlparser/schema/MultiPartNameTest.java index dfb5034f61..113b019ac5 100644 --- a/src/test/java/net/sf/jsqlparser/schema/MultiPartNameTest.java +++ b/src/test/java/net/sf/jsqlparser/schema/MultiPartNameTest.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.schema; +import java.time.Duration; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.PlainSelect; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -21,4 +24,31 @@ void replaceBackticksWithDoubleQuotes() { Assertions.assertThat("\"starbake\".\"customers\"").isEqualToIgnoringCase( MultiPartName.replaceBackticksWithDoubleQuotes("`starbake`.`customers`")); } + + @Test + void unquoteMatchesQuotePattern() { + String[] names = {"", "a", "\"a\"", "[a]", "`a`", "\"a\".\"b\"", "\"\"\"\"", "\"]", + "[\"a\"]", "a\"", "\"a", "]a[", "``a``", "\"a\"\"b\"", "[[a]]"}; + for (String name : names) { + assertEquals(MultiPartName.LEADING_TRAILING_QUOTES_PATTERN.matcher(name).replaceAll(""), + MultiPartName.unquote(name), name); + assertEquals(MultiPartName.LEADING_TRAILING_QUOTES_PATTERN.matcher(name).find(), + MultiPartName.isQuoted(name), name); + } + assertNull(MultiPartName.unquote(null)); + assertFalse(MultiPartName.isQuoted(null)); + } + + @Test + void unquoteIsLinearOnRunsOfEscapedQuotes() { + // a quoted identifier with a dot and many doubled quotes used to backtrack quadratically + String quotes = "\"\"".repeat(40000); + String sql = "SELECT * FROM \"a" + quotes + ".b\""; + PlainSelect select = (PlainSelect) assertTimeoutPreemptively(Duration.ofSeconds(2), + () -> CCJSqlParserUtil.parse(sql)); + assertNotNull(select.getFromItem()); + assertEquals("a" + quotes + ".b", + assertTimeoutPreemptively(Duration.ofSeconds(2), + () -> MultiPartName.unquote("\"a" + quotes + ".b\""))); + } }