Skip to content

Commit 9d297a0

Browse files
fix: validate Unicode escapes of PostgreSQL escape strings
With Dialect.POSTGRESQL, StringValue.getNotExcapedValue resolved the \uXXXX and \UXXXXXXXX escapes of an E'...' string with Integer.parseInt and StringBuilder.appendCodePoint and no range or surrogate check, so E'\uD800' produced an unpaired surrogate, E'\u0000' a NUL, E'\U00110000' threw from StringBuilder and E'\UFFFFFFFF' a NumberFormatException, while the parser accepted them all. PostgreSQL rejects each of them. Decode the E'' Unicode escapes with the rules the U&'' path already applies: exactly 4 or 8 hexadecimal digits, no code point zero or value above U+10FFFF, and a surrogate pair only as two consecutive escapes. The parser rejects such a literal under the PostgreSQL dialect as it does for U&'' strings.
1 parent 5d07ba4 commit 9d297a0

3 files changed

Lines changed: 108 additions & 23 deletions

File tree

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

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,8 @@ public StringValue withUnicodeEscapeCharacter(Character unicodeEscapeCharacter)
234234
* returned unchanged.
235235
*
236236
* @throws IllegalArgumentException if a {@link Escaping#POSTGRESQL_UNICODE_ESCAPE} value has an
237-
* escape that PostgreSQL rejects; the parser rejects such literals
237+
* escape that PostgreSQL rejects, or a {@link Escaping#POSTGRESQL_ESCAPE_STRING} value
238+
* has a Unicode escape that PostgreSQL rejects; the parser rejects such literals
238239
*/
239240
public String getNotExcapedValue() {
240241
if (quoteStr != null && quoteStr.startsWith("$")) {
@@ -317,26 +318,26 @@ private static int appendPostgresEscape(Unescaped result, String text, int index
317318
result.append('\t');
318319
return index + 1;
319320
case 'x':
320-
return appendNumber(result, text, index + 1, 16, 1, 2, c, true);
321+
return appendByte(result, text, index + 1, 16, 1, 2, c);
321322
case 'u':
322-
return appendNumber(result, text, index + 1, 16, 4, 4, c, false);
323+
return appendCodePoint(result, text, index, 4);
323324
case 'U':
324-
return appendNumber(result, text, index + 1, 16, 8, 8, c, false);
325+
return appendCodePoint(result, text, index, 8);
325326
default:
326327
if (c >= '0' && c <= '7') {
327-
return appendNumber(result, text, index, 8, 1, 3, c, true);
328+
return appendByte(result, text, index, 8, 1, 3, c);
328329
}
329330
result.append(c);
330331
return index + 1;
331332
}
332333
}
333334

334335
/**
335-
* Appends the byte or code point spelled by {@code min} to {@code max} digits at {@code start},
336-
* or {@code fallback} when fewer digits follow, and returns the next index.
336+
* Appends the byte spelled by {@code min} to {@code max} digits at {@code start}, or
337+
* {@code fallback} when fewer digits follow, and returns the next index.
337338
*/
338-
private static int appendNumber(Unescaped result, String text, int start, int radix,
339-
int min, int max, char fallback, boolean isByte) {
339+
private static int appendByte(Unescaped result, String text, int start, int radix, int min,
340+
int max, char fallback) {
340341
int end = start;
341342
while (end < text.length() && end - start < max
342343
&& Character.digit(text.charAt(end), radix) >= 0) {
@@ -346,12 +347,28 @@ private static int appendNumber(Unescaped result, String text, int start, int ra
346347
result.append(fallback);
347348
return start;
348349
}
349-
int number = Integer.parseInt(text.substring(start, end), radix);
350-
if (isByte) {
351-
result.appendByte(number);
352-
} else {
353-
result.appendCodePoint(number);
350+
result.appendByte(Integer.parseInt(text.substring(start, end), radix));
351+
return end;
352+
}
353+
354+
/**
355+
* Appends the code point spelled by exactly {@code digits} hexadecimal digits after the escape
356+
* letter at {@code index} and returns the next index. As in a Unicode escape string, the code
357+
* point must be valid and a UTF-16 surrogate pair must be written as two consecutive escapes.
358+
*/
359+
private static int appendCodePoint(Unescaped result, String text, int index, int digits) {
360+
int start = index + 1;
361+
int end = start + digits;
362+
long codePoint = parseHex(text, start, end);
363+
if (codePoint < 0) {
364+
throw new IllegalArgumentException("invalid Unicode escape at position " + (index - 1)
365+
+ ": Unicode escapes must be \\u followed by 4 or \\U by 8 hexadecimal digits");
366+
}
367+
if (codePoint == 0 || codePoint > Character.MAX_CODE_POINT) {
368+
throw new IllegalArgumentException(
369+
"invalid Unicode escape value at position " + (index - 1));
354370
}
371+
result.appendCodePoint((int) codePoint, index - 1);
355372
return end;
356373
}
357374

@@ -393,7 +410,7 @@ private static int appendUnicodeEscape(UnicodeUnescaped result, String text, int
393410
boolean plus = position + 1 < text.length() && text.charAt(position + 1) == '+';
394411
int start = plus ? position + 2 : position + 1;
395412
int end = start + (plus ? 6 : 4);
396-
int codePoint = parseHex(text, start, end);
413+
long codePoint = parseHex(text, start, end);
397414
if (codePoint < 0) {
398415
throw new IllegalArgumentException("invalid Unicode escape at position " + position
399416
+ ": Unicode escapes must be " + escape + "XXXX or " + escape + "+XXXXXX");
@@ -402,7 +419,7 @@ private static int appendUnicodeEscape(UnicodeUnescaped result, String text, int
402419
throw new IllegalArgumentException(
403420
"invalid Unicode escape value at position " + position);
404421
}
405-
result.appendCodePoint(codePoint, position);
422+
result.appendCodePoint((int) codePoint, position);
406423
return end;
407424
}
408425

@@ -416,11 +433,11 @@ private static boolean isHexDigit(char c) {
416433
}
417434

418435
/** Returns the ASCII hexadecimal number spelled by text[start, end), or -1. */
419-
private static int parseHex(String text, int start, int end) {
436+
private static long parseHex(String text, int start, int end) {
420437
if (end > text.length()) {
421438
return -1;
422439
}
423-
int number = 0;
440+
long number = 0;
424441
for (int i = start; i < end; i++) {
425442
char c = text.charAt(i);
426443
if (!isHexDigit(c)) {
@@ -431,26 +448,50 @@ private static int parseHex(String text, int start, int end) {
431448
return number;
432449
}
433450

434-
/** Collects unescaped characters; consecutive escaped bytes are decoded as UTF-8. */
451+
/**
452+
* Collects unescaped characters; consecutive escaped bytes are decoded as UTF-8. A UTF-16
453+
* surrogate pair must be written as two consecutive Unicode escapes, as PostgreSQL requires.
454+
*/
435455
private static final class Unescaped {
436456
private final StringBuilder chars;
437457
private final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
458+
private char highSurrogate;
459+
private int highSurrogatePosition;
438460

439461
Unescaped(int capacity) {
440462
chars = new StringBuilder(capacity);
441463
}
442464

443465
void append(char c) {
466+
requireNoPendingSurrogate();
444467
flushBytes();
445468
chars.append(c);
446469
}
447470

448-
void appendCodePoint(int codePoint) {
471+
/** Appends the code point of the Unicode escape at {@code position}. */
472+
void appendCodePoint(int codePoint, int position) {
449473
flushBytes();
450-
chars.appendCodePoint(codePoint);
474+
if (codePoint >= Character.MIN_LOW_SURROGATE
475+
&& codePoint <= Character.MAX_LOW_SURROGATE) {
476+
if (highSurrogate == 0) {
477+
throw invalidSurrogatePair(position);
478+
}
479+
chars.appendCodePoint(Character.toCodePoint(highSurrogate, (char) codePoint));
480+
highSurrogate = 0;
481+
return;
482+
}
483+
requireNoPendingSurrogate();
484+
if (codePoint >= Character.MIN_HIGH_SURROGATE
485+
&& codePoint <= Character.MAX_HIGH_SURROGATE) {
486+
highSurrogate = (char) codePoint;
487+
highSurrogatePosition = position;
488+
} else {
489+
chars.appendCodePoint(codePoint);
490+
}
451491
}
452492

453493
void appendByte(int value) {
494+
requireNoPendingSurrogate();
454495
bytes.write(value);
455496
}
456497

@@ -461,8 +502,15 @@ private void flushBytes() {
461502
}
462503
}
463504

505+
private void requireNoPendingSurrogate() {
506+
if (highSurrogate != 0) {
507+
throw invalidSurrogatePair(highSurrogatePosition);
508+
}
509+
}
510+
464511
@Override
465512
public String toString() {
513+
requireNoPendingSurrogate();
466514
flushBytes();
467515
return chars.toString();
468516
}

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3427,8 +3427,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
34273427
}
34283428
}
34293429

3430-
/** Creates a string literal that remembers how this configuration lexed its backslashes. */
3431-
private StringValue stringValue(Token token) {
3430+
/**
3431+
* Creates a string literal that remembers how this configuration lexed its backslashes. A
3432+
* PostgreSQL escape string with a Unicode escape that the server rejects is rejected as well.
3433+
*/
3434+
private StringValue stringValue(Token token) throws ParseException {
34323435
StringValue value = new StringValue(token.image);
34333436
// a U&'...' string brings its own escaping
34343437
if (value.getQuoteStr() == null || value.getQuoteStr().startsWith("$")
@@ -3440,6 +3443,12 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
34403443
if (Dialect.POSTGRESQL.name().equals(dialect)) {
34413444
if (backslashes || "E".equalsIgnoreCase(value.getPrefix())) {
34423445
value.setEscaping(StringValue.Escaping.POSTGRESQL_ESCAPE_STRING);
3446+
try {
3447+
value.getNotExcapedValue();
3448+
} catch (IllegalArgumentException e) {
3449+
throw new ParseException("Invalid escape string " + token.image + ": "
3450+
+ e.getMessage());
3451+
}
34433452
}
34443453
} else if (backslashes && (dialect == null || isMySqlDialect())) {
34453454
value.setEscaping(StringValue.Escaping.MYSQL_BACKSLASH);

‎src/test/java/net/sf/jsqlparser/expression/StringValueTest.java‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,34 @@ void testPostgresEscapeStrings() throws JSQLParserException {
147147
.getNotExcapedValue());
148148
}
149149

150+
@Test
151+
void testPostgresEscapeStringUnicodeEscapes() throws JSQLParserException {
152+
String[][] cases = {
153+
{"E'\\U0010FFFF'", new String(Character.toChars(0x10FFFF))},
154+
// a surrogate pair as two escapes, in either form
155+
{"E'\\uD83D\\uDE00'", "\uD83D\uDE00"},
156+
{"E'\\ud83d\\U0000de00'", "\uD83D\uDE00"},
157+
{"E'\\x41\\uD83D\\uDE00\\101'", "A\uD83D\uDE00A"}};
158+
for (String[] c : cases) {
159+
assertEquals(c[1], literal(c[0], Dialect.POSTGRESQL).getNotExcapedValue(), c[0]);
160+
}
161+
162+
// the escapes PostgreSQL rejects: too few digits, invalid code points and a surrogate
163+
// not written as a pair
164+
for (String sql : new String[] {"E'\\u00e'", "E'\\U0001F60'", "E'\\u0000'",
165+
"E'\\U00000000'", "E'\\U00110000'", "E'\\U7FFFFFFF'", "E'\\UFFFFFFFF'",
166+
"E'\\uD800'", "E'\\uD800x'", "E'\\uD800\\u0041'", "E'\\uD800\\x41'",
167+
"E'\\uD800'''", "E'\\uDE00'", "E'\\uD800\\uD800\\uDE00'"}) {
168+
assertThrows(IllegalArgumentException.class, () -> new StringValue(sql)
169+
.withEscaping(StringValue.Escaping.POSTGRESQL_ESCAPE_STRING)
170+
.getNotExcapedValue(), sql);
171+
assertThrows(JSQLParserException.class, () -> literal(sql, Dialect.POSTGRESQL), sql);
172+
}
173+
// only the escapes of PostgreSQL strings are checked
174+
assertEquals("\\uD800", literal("'\\uD800'", Dialect.POSTGRESQL).getNotExcapedValue());
175+
assertEquals("\\uD800", literal("E'\\uD800'", null).getNotExcapedValue());
176+
}
177+
150178
@Test
151179
void testPostgresUnicodeEscapeStrings() throws JSQLParserException {
152180
StringValue value = literal("U&'\\0041BC \\00e9'", Dialect.POSTGRESQL);

0 commit comments

Comments
 (0)