Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 181 additions & 8 deletions src/main/java/net/sf/jsqlparser/expression/StringValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
*/
package net.sf.jsqlparser.expression;

import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
Expand All @@ -22,9 +24,24 @@ public final class StringValue extends ASTNodeAccessImpl implements Expression {

public static final List<String> ALLOWED_PREFIXES =
Arrays.asList("N", "U", "E", "R", "B", "RB", "_utf8", "Q");

/**
* How a backslash in the literal body was read. The parser records the escaping of the
* configuration that lexed the literal; constructed values default to {@link #STANDARD}.
*/
public enum Escaping {
/** Only a doubled quote character is an escape, as in standard SQL. */
STANDARD,
/** MySQL/MariaDB backslash escapes, for example {@code \n}, {@code \'} and {@code \\}. */
MYSQL_BACKSLASH,
/** PostgreSQL escape string escapes, for example {@code E'\n'} and {@code E'\x41'}. */
POSTGRESQL_ESCAPE_STRING
}

private String value = "";
private String prefix = null;
private String quoteStr = "'";
private Escaping escaping = Escaping.STANDARD;

public StringValue() {
// empty constructor
Expand Down Expand Up @@ -119,19 +136,175 @@ public StringValue setQuoteStr(String quoteStr) {
return this;
}

public Escaping getEscaping() {
return escaping;
}

public void setEscaping(Escaping escaping) {
this.escaping = escaping == null ? Escaping.STANDARD : escaping;
}

public StringValue withEscaping(Escaping escaping) {
setEscaping(escaping);
return this;
}

/**
* Returns the literal's characters with its escapes resolved: a doubled quote character and,
* depending on {@link #getEscaping()}, backslash escape sequences. PostgreSQL octal and
* hexadecimal escapes denote bytes, which are decoded as UTF-8. Dollar-quoted bodies are
* returned unchanged.
*/
public String getNotExcapedValue() {
if (quoteStr != null && quoteStr.startsWith("$")) {
return value;
}
StringBuilder buffer = new StringBuilder(value.length());
int from = 0;
int index;
while ((index = value.indexOf("''", from)) != -1) {
buffer.append(value, from, index + 1);
from = index + 2;
char quote = "\"".equals(quoteStr) ? '"' : '\'';
Unescaped result = new Unescaped(value.length());
int i = 0;
while (i < value.length()) {
char c = value.charAt(i);
if (c == '\\' && escaping != Escaping.STANDARD && i + 1 < value.length()) {
i = escaping == Escaping.MYSQL_BACKSLASH
? appendMySqlEscape(result, value.charAt(i + 1), i + 1)
: appendPostgresEscape(result, value, i + 1);
} else if (c == quote && i + 1 < value.length() && value.charAt(i + 1) == quote) {
result.append(quote);
i += 2;
} else {
result.append(c);
i++;
}
}
return result.toString();
}

/** Appends the MySQL escape {@code c} at {@code index} and returns the index after it. */
private static int appendMySqlEscape(Unescaped result, char c, int index) {
switch (c) {
case '0':
result.append('\0');
break;
case 'b':
result.append('\b');
break;
case 'n':
result.append('\n');
break;
case 'r':
result.append('\r');
break;
case 't':
result.append('\t');
break;
case 'Z':
result.append((char) 0x1A);
break;
case '%':
case '_':
// kept with the backslash, so that LIKE patterns still match them literally
result.append('\\');
result.append(c);
break;
default:
result.append(c);
}
return index + 1;
}

/** Appends the PostgreSQL escape string escape at {@code index}; returns the next index. */
private static int appendPostgresEscape(Unescaped result, String text, int index) {
char c = text.charAt(index);
switch (c) {
case 'b':
result.append('\b');
return index + 1;
case 'f':
result.append('\f');
return index + 1;
case 'n':
result.append('\n');
return index + 1;
case 'r':
result.append('\r');
return index + 1;
case 't':
result.append('\t');
return index + 1;
case 'x':
return appendNumber(result, text, index + 1, 16, 1, 2, c, true);
case 'u':
return appendNumber(result, text, index + 1, 16, 4, 4, c, false);
case 'U':
return appendNumber(result, text, index + 1, 16, 8, 8, c, false);
default:
if (c >= '0' && c <= '7') {
return appendNumber(result, text, index, 8, 1, 3, c, true);
}
result.append(c);
return index + 1;
}
}

/**
* Appends the byte or code point spelled by {@code min} to {@code max} digits at {@code start},
* or {@code fallback} when fewer digits follow, and returns the next index.
*/
private static int appendNumber(Unescaped result, String text, int start, int radix,
int min, int max, char fallback, boolean isByte) {
int end = start;
while (end < text.length() && end - start < max
&& Character.digit(text.charAt(end), radix) >= 0) {
end++;
}
if (end - start < min) {
result.append(fallback);
return start;
}
int number = Integer.parseInt(text.substring(start, end), radix);
if (isByte) {
result.appendByte(number);
} else {
result.appendCodePoint(number);
}
return end;
}

/** Collects unescaped characters; consecutive escaped bytes are decoded as UTF-8. */
private static final class Unescaped {
private final StringBuilder chars;
private final ByteArrayOutputStream bytes = new ByteArrayOutputStream();

Unescaped(int capacity) {
chars = new StringBuilder(capacity);
}

void append(char c) {
flushBytes();
chars.append(c);
}

void appendCodePoint(int codePoint) {
flushBytes();
chars.appendCodePoint(codePoint);
}

void appendByte(int value) {
bytes.write(value);
}

private void flushBytes() {
if (bytes.size() > 0) {
chars.append(new String(bytes.toByteArray(), StandardCharsets.UTF_8));
bytes.reset();
}
}

@Override
public String toString() {
flushBytes();
return chars.toString();
}
buffer.append(value, from, value.length());
return buffer.toString();
}

@Override
Expand Down
Loading
Loading