99 */
1010package net .sf .jsqlparser .expression ;
1111
12+ import java .io .ByteArrayOutputStream ;
13+ import java .nio .charset .StandardCharsets ;
1214import java .util .Arrays ;
1315import java .util .List ;
1416import 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