@@ -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 }
0 commit comments