Permalink
Browse files

fix: makes escape processing strict according to JDBC spec

before, escape code parsing was very lenient and
invalid escape code formats were accidentally processed.

closes #657
  • Loading branch information...
seut authored and vlsi committed Oct 5, 2016
1 parent 7270435 commit 00a847879f313105de124776b7edaab928bf5b4c
@@ -22,6 +22,7 @@

/**
* Basic query parser infrastructure.
* Note: This class should not be considered as pgjdbc public API.
*
* @author Michael Paesold (mpaesold@gmx.at)
* @author Christopher Deckers (chrriis@gmail.com)
@@ -961,7 +962,7 @@ public static JdbcCallParseInfo modifyJdbcCall(String jdbcSql, boolean stdString
* @param standardConformingStrings whether standard_conforming_strings is on
* @return PostgreSQL-compatible SQL
*/
static String replaceProcessing(String p_sql, boolean replaceProcessingEnabled,
public static String replaceProcessing(String p_sql, boolean replaceProcessingEnabled,
boolean standardConformingStrings) throws SQLException {
if (replaceProcessingEnabled) {
// Since escape codes can only appear in SQL CODE, we keep track
@@ -1012,6 +1013,8 @@ private static int parseSql(char[] p_sql, int i, StringBuilder newsql, boolean s
i--;
while (!endOfNested && ++i < len) {
char c = p_sql[i];

state_switch:
switch (state) {
case IN_SQLCODE:
if (c == '$') {
@@ -1054,37 +1057,20 @@ private static int parseSql(char[] p_sql, int i, StringBuilder newsql, boolean s
break;
} else if (c == '{') { // start of an escape code?
if (i + 1 < len) {
char next = p_sql[i + 1];
char nextnext = (i + 2 < len) ? p_sql[i + 2] : '\0';
if (next == 'd' || next == 'D') {
state = SqlParseState.ESC_TIMEDATE;
i++;
newsql.append("DATE ");
break;
} else if (next == 't' || next == 'T') {
state = SqlParseState.ESC_TIMEDATE;
if (nextnext == 's' || nextnext == 'S') {
// timestamp constant
i += 2;
newsql.append("TIMESTAMP ");
} else {
// time constant
i++;
newsql.append("TIME ");
SqlParseState[] availableStates = SqlParseState.values();
// skip first state, it's not a escape code state
for (int j = 1; j < availableStates.length; j++) {
SqlParseState availableState = availableStates[j];
int matchedPosition = availableState.getMatchedPosition(p_sql, i + 1);
if (matchedPosition == 0) {
continue;
}
i += matchedPosition;
if (availableState.replacementKeyword != null) {
newsql.append(availableState.replacementKeyword);
}
break;
} else if (next == 'f' || next == 'F') {
state = SqlParseState.ESC_FUNCTION;
i += (nextnext == 'n' || nextnext == 'N') ? 2 : 1;
break;
} else if (next == 'o' || next == 'O') {
state = SqlParseState.ESC_OUTERJOIN;
i += (nextnext == 'j' || nextnext == 'J') ? 2 : 1;
break;
} else if (next == 'e' || next == 'E') {
// we assume that escape is the only escape sequence beginning with e
state = SqlParseState.ESC_ESCAPECHAR;
break;
state = availableState;
break state_switch;
}
}
}
@@ -1114,7 +1100,9 @@ private static int parseSql(char[] p_sql, int i, StringBuilder newsql, boolean s
}
state = SqlParseState.IN_SQLCODE; // end of escaped function (or query)
break;
case ESC_TIMEDATE:
case ESC_DATE:
case ESC_TIME:
case ESC_TIMESTAMP:
case ESC_OUTERJOIN:
case ESC_ESCAPECHAR:
if (c == '}') {
@@ -1178,12 +1166,67 @@ private static String escapeFunction(String functionName, String args, boolean s
}
}

private final static char[] QUOTE_OR_ALPHABETIC_MARKER = new char[]{'\"', '0'};
private final static char[] SINGLE_QUOTE = new char[]{'\''};

// Static variables for parsing SQL when replaceProcessing is true.
private enum SqlParseState {
IN_SQLCODE,
ESC_TIMEDATE,
ESC_FUNCTION,
ESC_OUTERJOIN,
ESC_ESCAPECHAR;
ESC_DATE("d", SINGLE_QUOTE, "DATE "),
ESC_TIME("t", SINGLE_QUOTE, "TIME "),

ESC_TIMESTAMP("ts", SINGLE_QUOTE, "TIMESTAMP "),
ESC_FUNCTION("fn", QUOTE_OR_ALPHABETIC_MARKER, null),
ESC_OUTERJOIN("oj", QUOTE_OR_ALPHABETIC_MARKER, null),
ESC_ESCAPECHAR("escape", SINGLE_QUOTE, "ESCAPE ");

private final char[] escapeKeyword;
private final char[] allowedValues;
private final String replacementKeyword;

SqlParseState() {
this("", new char[0], null);
}

SqlParseState(String escapeKeyword, char[] allowedValues, String replacementKeyword) {
this.escapeKeyword = escapeKeyword.toCharArray();
this.allowedValues = allowedValues;
this.replacementKeyword = replacementKeyword;
}

private int getMatchedPosition(char[] p_sql, int pos) {
int newPos = pos;

// check for the keyword
for (char c : escapeKeyword) {
if (newPos >= p_sql.length) {
return 0;
}
char curr = p_sql[newPos++];
if (curr != c && curr != Character.toUpperCase(c)) {
return 0;
}
}
if (newPos >= p_sql.length) {
return 0;
}

// check for the beginning of the value
char curr = p_sql[newPos];
// ignore any in-between whitespace
while (curr == ' ') {
newPos++;
if (newPos >= p_sql.length) {
return 0;
}
curr = p_sql[newPos];
}
for (char c : allowedValues) {
if (curr == c || (c == '0' && Character.isLetter(curr))) {
return newPos - pos;
}
}
return 0;
}
}
}
@@ -103,4 +103,29 @@ public void testSelectCommandParsing() {
"select".getChars(0, 6, command, 0);
Assert.assertTrue("Failed to correctly parse lower case command.", Parser.parseSelectKeyword(command, 0));
}

public void testEscapeProcessing() throws Exception {
Assert.assertEquals("DATE '1999-01-09'", Parser.replaceProcessing("{d '1999-01-09'}", true, false));
Assert.assertEquals("DATE '1999-01-09'", Parser.replaceProcessing("{D '1999-01-09'}", true, false));
Assert.assertEquals("TIME '20:00:03'", Parser.replaceProcessing("{t '20:00:03'}", true, false));
Assert.assertEquals("TIME '20:00:03'", Parser.replaceProcessing("{T '20:00:03'}", true, false));
Assert.assertEquals("TIMESTAMP '1999-01-09 20:11:11.123455'", Parser.replaceProcessing("{ts '1999-01-09 20:11:11.123455'}", true, false));
Assert.assertEquals("TIMESTAMP '1999-01-09 20:11:11.123455'", Parser.replaceProcessing("{Ts '1999-01-09 20:11:11.123455'}", true, false));

Assert.assertEquals("user", Parser.replaceProcessing("{fn user()}", true, false));
Assert.assertEquals("cos(1)", Parser.replaceProcessing("{fn cos(1)}", true, false));
Assert.assertEquals("extract(week from DATE '2005-01-24')", Parser.replaceProcessing("{fn week({d '2005-01-24'})}", true, false));

Assert.assertEquals("\"T1\" LEFT OUTER JOIN t2 ON \"T1\".id = t2.id",
Parser.replaceProcessing("{oj \"T1\" LEFT OUTER JOIN t2 ON \"T1\".id = t2.id}", true, false));

Assert.assertEquals("ESCAPE '_'", Parser.replaceProcessing("{escape '_'}", true, false));

// nothing should be changed in that case, no valid escape code
Assert.assertEquals("{obj : 1}", Parser.replaceProcessing("{obj : 1}", true, false));
}

public void testUnterminatedEscape() throws Exception {
Assert.assertEquals("{oj ", Parser.replaceProcessing("{oj ", true, false));
}
}
@@ -109,7 +109,7 @@ public void testPrepareCall() {
public void testNativeSQL() throws Exception {
// test a simple escape
con = TestUtil.openDB();
assertEquals("DATE '2005-01-24'", con.nativeSQL("{d '2005-01-24'}"));
assertEquals("DATE '2005-01-24'", con.nativeSQL("{d '2005-01-24'}"));
}

/*
@@ -0,0 +1,33 @@
package org.postgresql.benchmark.escaping;

import org.postgresql.core.Parser;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;

import java.util.concurrent.TimeUnit;

@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Thread)
@Threads(1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class EscapeProcessing {

private String fnEscapeSQL = "{fn week({d '2005-01-24'})}";
private boolean replaceProcessingEnabled = true;
private boolean standardConformingStrings = false;

@Benchmark
public String escapeFunctionWithDate() throws Exception {
return Parser.replaceProcessing(fnEscapeSQL, replaceProcessingEnabled, standardConformingStrings);
}
}

0 comments on commit 00a8478

Please sign in to comment.