Skip to content

Commit d38857d

Browse files
authored
fix: resolve backslash escapes in StringValue.getNotExcapedValue() (#2791)
getNotExcapedValue() only collapsed doubled single quotes. With MySQL backslash escapes it returned 'it\'s' as it\'s and 'a\\b' as a\\b, PostgreSQL E'it\'s' kept its backslash, and "it""s" kept both quotes. Record at parse time how a literal's backslashes were read: MySQL and MariaDB backslash escapes, including the default configuration with backslash escapes enabled, PostgreSQL escape strings, or standard SQL. getNotExcapedValue() resolves the literal's own doubled quote and these escapes; PostgreSQL octal and hexadecimal escapes are decoded as UTF-8 bytes. Constructed values and other dialects keep the standard reading. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 731a817 commit d38857d

4 files changed

Lines changed: 405 additions & 74 deletions

File tree

‎src/main/java/net/sf/jsqlparser/expression/StringValue.java‎

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

12+
import java.io.ByteArrayOutputStream;
13+
import java.nio.charset.StandardCharsets;
1214
import java.util.Arrays;
1315
import java.util.List;
1416
import java.util.Objects;
@@ -22,9 +24,24 @@ public final class StringValue extends ASTNodeAccessImpl implements Expression {
2224

2325
public static final List<String> ALLOWED_PREFIXES =
2426
Arrays.asList("N", "U", "E", "R", "B", "RB", "_utf8", "Q");
27+
28+
/**
29+
* How a backslash in the literal body was read. The parser records the escaping of the
30+
* configuration that lexed the literal; constructed values default to {@link #STANDARD}.
31+
*/
32+
public enum Escaping {
33+
/** Only a doubled quote character is an escape, as in standard SQL. */
34+
STANDARD,
35+
/** MySQL/MariaDB backslash escapes, for example {@code \n}, {@code \'} and {@code \\}. */
36+
MYSQL_BACKSLASH,
37+
/** PostgreSQL escape string escapes, for example {@code E'\n'} and {@code E'\x41'}. */
38+
POSTGRESQL_ESCAPE_STRING
39+
}
40+
2541
private String value = "";
2642
private String prefix = null;
2743
private String quoteStr = "'";
44+
private Escaping escaping = Escaping.STANDARD;
2845

2946
public StringValue() {
3047
// empty constructor
@@ -119,19 +136,175 @@ public StringValue setQuoteStr(String quoteStr) {
119136
return this;
120137
}
121138

139+
public Escaping getEscaping() {
140+
return escaping;
141+
}
142+
143+
public void setEscaping(Escaping escaping) {
144+
this.escaping = escaping == null ? Escaping.STANDARD : escaping;
145+
}
146+
147+
public StringValue withEscaping(Escaping escaping) {
148+
setEscaping(escaping);
149+
return this;
150+
}
151+
152+
/**
153+
* Returns the literal's characters with its escapes resolved: a doubled quote character and,
154+
* depending on {@link #getEscaping()}, backslash escape sequences. PostgreSQL octal and
155+
* hexadecimal escapes denote bytes, which are decoded as UTF-8. Dollar-quoted bodies are
156+
* returned unchanged.
157+
*/
122158
public String getNotExcapedValue() {
123159
if (quoteStr != null && quoteStr.startsWith("$")) {
124160
return value;
125161
}
126-
StringBuilder buffer = new StringBuilder(value.length());
127-
int from = 0;
128-
int index;
129-
while ((index = value.indexOf("''", from)) != -1) {
130-
buffer.append(value, from, index + 1);
131-
from = index + 2;
162+
char quote = "\"".equals(quoteStr) ? '"' : '\'';
163+
Unescaped result = new Unescaped(value.length());
164+
int i = 0;
165+
while (i < value.length()) {
166+
char c = value.charAt(i);
167+
if (c == '\\' && escaping != Escaping.STANDARD && i + 1 < value.length()) {
168+
i = escaping == Escaping.MYSQL_BACKSLASH
169+
? appendMySqlEscape(result, value.charAt(i + 1), i + 1)
170+
: appendPostgresEscape(result, value, i + 1);
171+
} else if (c == quote && i + 1 < value.length() && value.charAt(i + 1) == quote) {
172+
result.append(quote);
173+
i += 2;
174+
} else {
175+
result.append(c);
176+
i++;
177+
}
178+
}
179+
return result.toString();
180+
}
181+
182+
/** Appends the MySQL escape {@code c} at {@code index} and returns the index after it. */
183+
private static int appendMySqlEscape(Unescaped result, char c, int index) {
184+
switch (c) {
185+
case '0':
186+
result.append('\0');
187+
break;
188+
case 'b':
189+
result.append('\b');
190+
break;
191+
case 'n':
192+
result.append('\n');
193+
break;
194+
case 'r':
195+
result.append('\r');
196+
break;
197+
case 't':
198+
result.append('\t');
199+
break;
200+
case 'Z':
201+
result.append((char) 0x1A);
202+
break;
203+
case '%':
204+
case '_':
205+
// kept with the backslash, so that LIKE patterns still match them literally
206+
result.append('\\');
207+
result.append(c);
208+
break;
209+
default:
210+
result.append(c);
211+
}
212+
return index + 1;
213+
}
214+
215+
/** Appends the PostgreSQL escape string escape at {@code index}; returns the next index. */
216+
private static int appendPostgresEscape(Unescaped result, String text, int index) {
217+
char c = text.charAt(index);
218+
switch (c) {
219+
case 'b':
220+
result.append('\b');
221+
return index + 1;
222+
case 'f':
223+
result.append('\f');
224+
return index + 1;
225+
case 'n':
226+
result.append('\n');
227+
return index + 1;
228+
case 'r':
229+
result.append('\r');
230+
return index + 1;
231+
case 't':
232+
result.append('\t');
233+
return index + 1;
234+
case 'x':
235+
return appendNumber(result, text, index + 1, 16, 1, 2, c, true);
236+
case 'u':
237+
return appendNumber(result, text, index + 1, 16, 4, 4, c, false);
238+
case 'U':
239+
return appendNumber(result, text, index + 1, 16, 8, 8, c, false);
240+
default:
241+
if (c >= '0' && c <= '7') {
242+
return appendNumber(result, text, index, 8, 1, 3, c, true);
243+
}
244+
result.append(c);
245+
return index + 1;
246+
}
247+
}
248+
249+
/**
250+
* Appends the byte or code point spelled by {@code min} to {@code max} digits at {@code start},
251+
* or {@code fallback} when fewer digits follow, and returns the next index.
252+
*/
253+
private static int appendNumber(Unescaped result, String text, int start, int radix,
254+
int min, int max, char fallback, boolean isByte) {
255+
int end = start;
256+
while (end < text.length() && end - start < max
257+
&& Character.digit(text.charAt(end), radix) >= 0) {
258+
end++;
259+
}
260+
if (end - start < min) {
261+
result.append(fallback);
262+
return start;
263+
}
264+
int number = Integer.parseInt(text.substring(start, end), radix);
265+
if (isByte) {
266+
result.appendByte(number);
267+
} else {
268+
result.appendCodePoint(number);
269+
}
270+
return end;
271+
}
272+
273+
/** Collects unescaped characters; consecutive escaped bytes are decoded as UTF-8. */
274+
private static final class Unescaped {
275+
private final StringBuilder chars;
276+
private final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
277+
278+
Unescaped(int capacity) {
279+
chars = new StringBuilder(capacity);
280+
}
281+
282+
void append(char c) {
283+
flushBytes();
284+
chars.append(c);
285+
}
286+
287+
void appendCodePoint(int codePoint) {
288+
flushBytes();
289+
chars.appendCodePoint(codePoint);
290+
}
291+
292+
void appendByte(int value) {
293+
bytes.write(value);
294+
}
295+
296+
private void flushBytes() {
297+
if (bytes.size() > 0) {
298+
chars.append(new String(bytes.toByteArray(), StandardCharsets.UTF_8));
299+
bytes.reset();
300+
}
301+
}
302+
303+
@Override
304+
public String toString() {
305+
flushBytes();
306+
return chars.toString();
132307
}
133-
buffer.append(value, from, value.length());
134-
return buffer.toString();
135308
}
136309

137310
@Override

0 commit comments

Comments
 (0)