Browse Source

Ignore delimiter enclosed in double quotes in ScriptUtils

Prior to this commit, the containsSqlScriptDelimiters() method in
ScriptUtils ignored delimiters enclosed in single quotes but not those
enclosed within double quotes, which contradicts the algorithm in
splitSqlScript() and therefore constitutes a bug.

This commit fixes this bug in the ScriptUtils implementations in
spring-jdbc and spring-r2dbc.

Closes gh-26935
pull/26940/head
Sam Brannen 5 years ago
parent
commit
cda72e4a70
  1. 29
      spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java
  2. 34
      spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java
  3. 29
      spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java
  4. 48
      spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ScriptUtilsUnitTests.java

29
spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2020 the original author or authors. * Copyright 2002-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -414,12 +414,18 @@ public abstract class ScriptUtils {
} }
/** /**
* Does the provided SQL script contain the specified delimiter? * Determine if the provided SQL script contains the specified delimiter.
* @param script the SQL script * <p>This method is intended to be used to find the string delimiting each
* @param delim the string delimiting each statement - typically a ';' character * SQL statement &mdash; for example, a ';' character.
* <p>Any occurrence of the delimiter within the script will be ignored if it
* is enclosed within single quotes ({@code '}) or double quotes ({@code "})
* or if it is escaped with a backslash ({@code \}).
* @param script the SQL script to search within
* @param delimiter the delimiter to search for
*/ */
public static boolean containsSqlScriptDelimiters(String script, String delim) { public static boolean containsSqlScriptDelimiters(String script, String delimiter) {
boolean inLiteral = false; boolean inSingleQuote = false;
boolean inDoubleQuote = false;
boolean inEscape = false; boolean inEscape = false;
for (int i = 0; i < script.length(); i++) { for (int i = 0; i < script.length(); i++) {
@ -433,13 +439,18 @@ public abstract class ScriptUtils {
inEscape = true; inEscape = true;
continue; continue;
} }
if (c == '\'') { if (!inDoubleQuote && (c == '\'')) {
inLiteral = !inLiteral; inSingleQuote = !inSingleQuote;
}
else if (!inSingleQuote && (c == '"')) {
inDoubleQuote = !inDoubleQuote;
} }
if (!inLiteral && script.startsWith(delim, i)) { if (!inSingleQuote && !inDoubleQuote) {
if (script.startsWith(delimiter, i)) {
return true; return true;
} }
} }
}
return false; return false;
} }

34
spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,6 +20,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.EncodedResource;
@ -165,17 +167,25 @@ public class ScriptUtilsUnitTests {
assertThat(statements).containsExactly(statement1, statement2); assertThat(statements).containsExactly(statement1, statement2);
} }
@Test @ParameterizedTest
public void containsDelimiters() { @CsvSource(delimiter = '#', value = {
assertThat(containsSqlScriptDelimiters("select 1\n select ';'", ";")).isFalse(); // semicolon
assertThat(containsSqlScriptDelimiters("select 1; select 2", ";")).isTrue(); "'select 1\n select '';''' # ; # false",
assertThat(containsSqlScriptDelimiters("select 1; select '\\n\n';", "\n")).isFalse(); "'select 1\n select \";\"' # ; # false",
assertThat(containsSqlScriptDelimiters("select 1\n select 2", "\n")).isTrue(); "'select 1; select 2' # ; # true",
assertThat(containsSqlScriptDelimiters("select 1\n select 2", "\n\n")).isFalse(); // newline
assertThat(containsSqlScriptDelimiters("select 1\n\n select 2", "\n\n")).isTrue(); "'select 1; select ''\n''' # '\n' # false",
// MySQL style escapes '\\' "'select 1; select \"\n\"' # '\n' # false",
assertThat(containsSqlScriptDelimiters("insert into users(first_name, last_name)\nvalues('a\\\\', 'b;')", ";")).isFalse(); "'select 1\n select 2' # '\n' # true",
assertThat(containsSqlScriptDelimiters("insert into users(first_name, last_name)\nvalues('Charles', 'd\\'Artagnan'); select 1;", ";")).isTrue(); // double newline
"'select 1\n select 2' # '\n\n' # false",
"'select 1\n\n select 2' # '\n\n' # true",
// semicolon with MySQL style escapes '\\'
"'insert into users(first, last)\nvalues(''a\\\\'', ''b;'')' # ; # false",
"'insert into users(first, last)\nvalues(''Charles'', ''d\\''Artagnan''); select 1' # ; # true"
})
public void containsDelimiter(String script, String delimiter, boolean expected) {
assertThat(containsSqlScriptDelimiters(script, delimiter)).isEqualTo(expected);
} }
private String readScript(String path) throws Exception { private String readScript(String path) throws Exception {

29
spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2020 the original author or authors. * Copyright 2002-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -432,12 +432,18 @@ public abstract class ScriptUtils {
} }
/** /**
* Does the provided SQL script contain the specified delimiter? * Determine if the provided SQL script contains the specified delimiter.
* @param script the SQL script * <p>This method is intended to be used to find the string delimiting each
* @param delim the string delimiting each statement - typically a ';' character * SQL statement &mdash; for example, a ';' character.
* <p>Any occurrence of the delimiter within the script will be ignored if it
* is enclosed within single quotes ({@code '}) or double quotes ({@code "})
* or if it is escaped with a backslash ({@code \}).
* @param script the SQL script to search within
* @param delimiter the delimiter to search for
*/ */
public static boolean containsSqlScriptDelimiters(String script, String delim) { public static boolean containsSqlScriptDelimiters(String script, String delimiter) {
boolean inLiteral = false; boolean inSingleQuote = false;
boolean inDoubleQuote = false;
boolean inEscape = false; boolean inEscape = false;
for (int i = 0; i < script.length(); i++) { for (int i = 0; i < script.length(); i++) {
@ -451,13 +457,18 @@ public abstract class ScriptUtils {
inEscape = true; inEscape = true;
continue; continue;
} }
if (c == '\'') { if (!inDoubleQuote && (c == '\'')) {
inLiteral = !inLiteral; inSingleQuote = !inSingleQuote;
}
else if (!inSingleQuote && (c == '"')) {
inDoubleQuote = !inDoubleQuote;
} }
if (!inLiteral && script.startsWith(delim, i)) { if (!inSingleQuote && !inDoubleQuote) {
if (script.startsWith(delimiter, i)) {
return true; return true;
} }
} }
}
return false; return false;
} }

48
spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ScriptUtilsUnitTests.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2020 the original author or authors. * Copyright 2002-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,12 +21,15 @@ import java.util.List;
import org.assertj.core.util.Strings; import org.assertj.core.util.Strings;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.EncodedResource;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.r2dbc.connection.init.ScriptUtils.containsSqlScriptDelimiters;
/** /**
* Unit tests for {@link ScriptUtils}. * Unit tests for {@link ScriptUtils}.
@ -184,30 +187,25 @@ public class ScriptUtilsUnitTests {
assertThat(statements).hasSize(2).containsSequence(statement1, statement2); assertThat(statements).hasSize(2).containsSequence(statement1, statement2);
} }
@Test @ParameterizedTest
public void containsDelimiters() { @CsvSource(delimiter = '#', value = {
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n select ';'", // semicolon
";")).isFalse(); "'select 1\n select '';''' # ; # false",
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1; select 2", "'select 1\n select \";\"' # ; # false",
";")).isTrue(); "'select 1; select 2' # ; # true",
// newline
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1; select '\\n\n';", "'select 1; select ''\n''' # '\n' # false",
"\n")).isFalse(); "'select 1; select \"\n\"' # '\n' # false",
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n select 2", "'select 1\n select 2' # '\n' # true",
"\n")).isTrue(); // double newline
"'select 1\n select 2' # '\n\n' # false",
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n select 2", "'select 1\n\n select 2' # '\n\n' # true",
"\n\n")).isFalse(); // semicolon with MySQL style escapes '\\'
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n\n select 2", "'insert into users(first, last)\nvalues(''a\\\\'', ''b;'')' # ; # false",
"\n\n")).isTrue(); "'insert into users(first, last)\nvalues(''Charles'', ''d\\''Artagnan''); select 1' # ; # true"
})
// MySQL style escapes '\\' public void containsDelimiter(String script, String delimiter, boolean expected) {
assertThat(ScriptUtils.containsSqlScriptDelimiters( assertThat(containsSqlScriptDelimiters(script, delimiter)).isEqualTo(expected);
"insert into users(first_name, last_name)\nvalues('a\\\\', 'b;')",
";")).isFalse();
assertThat(ScriptUtils.containsSqlScriptDelimiters(
"insert into users(first_name, last_name)\nvalues('Charles', 'd\\'Artagnan'); select 1;",
";")).isTrue();
} }
private String readScript(String path) { private String readScript(String path) {

Loading…
Cancel
Save