From 569ce840cf720b6534e5ff670fc34eaa151b73cb Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 13 May 2021 16:17:51 +0200 Subject: [PATCH] Ignore comments when searching for statement delimiter in ScriptUtils Prior to this commit, the implementations of ScriptUtils.containsSqlScriptDelimiters() in spring-jdbc and spring-r2dbc did not ignore comments when searching for the statement delimiter within an SQL script. This resulted in subtle bugs if a comment contained a single single-quote or single double-quote, since the absence of the closing single-quote or double-quote led the algorithm to believe that it was still "within a text literal". Similar issues could arise if a comment contained the sought statement delimiter but the rest of the script did not contain the sought statement delimiter. In such cases, the algorithms in ScriptUtils could erroneously choose an incorrect statement delimiter -- for example, using the fallback statement delimiter instead of the delimiter specified by the user. This commit avoids such bugs by ignoring single-line comments and block comments when searching for the statement delimiter within an SQL script. Closes gh-26911 --- .../jdbc/datasource/init/ScriptUtils.java | 68 +++++++++++++++++-- .../datasource/init/ScriptUtilsUnitTests.java | 20 +++++- ...t-data-with-multi-line-nested-comments.sql | 5 +- .../r2dbc/connection/init/ScriptUtils.java | 64 +++++++++++++++-- .../connection/init/ScriptUtilsUnitTests.java | 20 +++++- ...t-data-with-multi-line-nested-comments.sql | 5 +- 6 files changed, 167 insertions(+), 15 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java index 271eff02109..6397c45d486 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java @@ -418,12 +418,44 @@ public abstract class ScriptUtils { *

This method is intended to be used to find the string delimiting each * SQL statement — for example, a ';' character. *

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 \}). + * is within a literal block of text enclosed in single quotes + * ({@code '}) or double quotes ({@code "}), if it is escaped with a backslash + * ({@code \}), or if it is within a single-line comment or block comment. * @param script the SQL script to search within - * @param delimiter the delimiter to search for + * @param delimiter the statement delimiter to search for + * @see #DEFAULT_COMMENT_PREFIXES + * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER + * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER */ public static boolean containsSqlScriptDelimiters(String script, String delimiter) { + return containsStatementSeparator(null, script, delimiter, DEFAULT_COMMENT_PREFIXES, + DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER); + } + + /** + * Determine if the provided SQL script contains the specified statement separator. + *

This method is intended to be used to find the string separating each + * SQL statement — for example, a ';' character. + *

Any occurrence of the separator within the script will be ignored if it + * is within a literal block of text enclosed in single quotes + * ({@code '}) or double quotes ({@code "}), if it is escaped with a backslash + * ({@code \}), or if it is within a single-line comment or block comment. + * @param resource the resource from which the script was read, or {@code null} + * if unknown + * @param script the SQL script to search within + * @param separator the statement separator to search for + * @param commentPrefixes the prefixes that identify single-line comments + * (typically {@code "--"}) + * @param blockCommentStartDelimiter the start block comment delimiter + * (typically {@code "/*"}) + * @param blockCommentEndDelimiter the end block comment delimiter + * (typically "*/") + * @since 5.2.16 + */ + private static boolean containsStatementSeparator(@Nullable EncodedResource resource, String script, + String separator, String[] commentPrefixes, String blockCommentStartDelimiter, + String blockCommentEndDelimiter) throws ScriptException { + boolean inSingleQuote = false; boolean inDoubleQuote = false; boolean inEscape = false; @@ -446,9 +478,33 @@ public abstract class ScriptUtils { inDoubleQuote = !inDoubleQuote; } if (!inSingleQuote && !inDoubleQuote) { - if (script.startsWith(delimiter, i)) { + if (script.startsWith(separator, i)) { return true; } + else if (startsWithAny(script, commentPrefixes, i)) { + // Skip over any content from the start of the comment to the EOL + int indexOfNextNewline = script.indexOf('\n', i); + if (indexOfNextNewline > i) { + i = indexOfNextNewline; + continue; + } + else { + // If there's no EOL, we must be at the end of the script, so stop here. + break; + } + } + else if (script.startsWith(blockCommentStartDelimiter, i)) { + // Skip over any block comments + int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i); + if (indexOfCommentEnd > i) { + i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1; + continue; + } + else { + throw new ScriptParseException( + "Missing block comment end delimiter: " + blockCommentEndDelimiter, resource); + } + } } } @@ -595,7 +651,9 @@ public abstract class ScriptUtils { if (separator == null) { separator = DEFAULT_STATEMENT_SEPARATOR; } - if (!EOF_STATEMENT_SEPARATOR.equals(separator) && !containsSqlScriptDelimiters(script, separator)) { + if (!EOF_STATEMENT_SEPARATOR.equals(separator) && + !containsStatementSeparator(resource, script, separator, commentPrefixes, + blockCommentStartDelimiter, blockCommentEndDelimiter)) { separator = FALLBACK_STATEMENT_SEPARATOR; } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java index 25c2e7f13a7..8b07ce617ca 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java @@ -205,9 +205,25 @@ public class ScriptUtilsUnitTests { "'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" + "'insert into users(first, last)\nvalues(''Charles'', ''d\\''Artagnan''); select 1' # ; # true", + // semicolon inside comments + "'-- a;b;c\ninsert into colors(color_num) values(42);' # ; # true", + "'/* a;b;c */\ninsert into colors(color_num) values(42);' # ; # true", + "'-- a;b;c\ninsert into colors(color_num) values(42)' # ; # false", + "'/* a;b;c */\ninsert into colors(color_num) values(42)' # ; # false", + // single quotes inside comments + "'-- What\\''s your favorite color?\ninsert into colors(color_num) values(42);' # ; # true", + "'-- What''s your favorite color?\ninsert into colors(color_num) values(42);' # ; # true", + "'/* What\\''s your favorite color? */\ninsert into colors(color_num) values(42);' # ; # true", + "'/* What''s your favorite color? */\ninsert into colors(color_num) values(42);' # ; # true", + // double quotes inside comments + "'-- double \" quotes\ninsert into colors(color_num) values(42);' # ; # true", + "'-- double \\\" quotes\ninsert into colors(color_num) values(42);' # ; # true", + "'/* double \" quotes */\ninsert into colors(color_num) values(42);' # ; # true", + "'/* double \\\" quotes */\ninsert into colors(color_num) values(42);' # ; # true" }) - public void containsDelimiter(String script, String delimiter, boolean expected) { + public void containsStatementSeparator(String script, String delimiter, boolean expected) { + // Indirectly tests ScriptUtils.containsStatementSeparator(EncodedResource, String, String, String[], String, String). assertThat(containsSqlScriptDelimiters(script, delimiter)).isEqualTo(expected); } diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql index 7faa91c250a..5df377ccade 100644 --- a/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql @@ -5,16 +5,19 @@ * x, y, z... */ +-- This is a single line comment containing single (') and double quotes ("). INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller'); -- This is also a comment. /*------------------------------------------- --- A fancy multi-line comments that puts +-- A fancy multi-line comment that puts -- single line comments inside of a multi-line -- comment block. Moreover, the block comment end delimiter appears on a line that can potentially also be a single-line comment if we weren't already inside a multi-line comment run. + +And here's a line containing single and double quotes ("). -------------------------------------------*/ INSERT INTO users(first_name, last_name) -- This is a single line comment containing the block-end-comment sequence here */ but it's still a single-line comment diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java index 6cd2b6a4900..6dddcc16322 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java @@ -436,12 +436,44 @@ public abstract class ScriptUtils { *

This method is intended to be used to find the string delimiting each * SQL statement — for example, a ';' character. *

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 \}). + * is within a literal block of text enclosed in single quotes + * ({@code '}) or double quotes ({@code "}), if it is escaped with a backslash + * ({@code \}), or if it is within a single-line comment or block comment. * @param script the SQL script to search within - * @param delimiter the delimiter to search for + * @param delimiter the statement delimiter to search for + * @see #DEFAULT_COMMENT_PREFIXES + * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER + * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER */ public static boolean containsSqlScriptDelimiters(String script, String delimiter) { + return containsStatementSeparator(null, script, delimiter, DEFAULT_COMMENT_PREFIXES, + DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER); + } + + /** + * Determine if the provided SQL script contains the specified statement separator. + *

This method is intended to be used to find the string separating each + * SQL statement — for example, a ';' character. + *

Any occurrence of the separator within the script will be ignored if it + * is within a literal block of text enclosed in single quotes + * ({@code '}) or double quotes ({@code "}), if it is escaped with a backslash + * ({@code \}), or if it is within a single-line comment or block comment. + * @param resource the resource from which the script was read, or {@code null} + * if unknown + * @param script the SQL script to search within + * @param separator the statement separator to search for + * @param commentPrefixes the prefixes that identify single-line comments + * (typically {@code "--"}) + * @param blockCommentStartDelimiter the start block comment delimiter + * (typically {@code "/*"}) + * @param blockCommentEndDelimiter the end block comment delimiter + * (typically "*/") + * @since 5.2.16 + */ + private static boolean containsStatementSeparator(@Nullable EncodedResource resource, String script, + String separator, String[] commentPrefixes, String blockCommentStartDelimiter, + String blockCommentEndDelimiter) throws ScriptException { + boolean inSingleQuote = false; boolean inDoubleQuote = false; boolean inEscape = false; @@ -464,9 +496,33 @@ public abstract class ScriptUtils { inDoubleQuote = !inDoubleQuote; } if (!inSingleQuote && !inDoubleQuote) { - if (script.startsWith(delimiter, i)) { + if (script.startsWith(separator, i)) { return true; } + else if (startsWithAny(script, commentPrefixes, i)) { + // Skip over any content from the start of the comment to the EOL + int indexOfNextNewline = script.indexOf('\n', i); + if (indexOfNextNewline > i) { + i = indexOfNextNewline; + continue; + } + else { + // If there's no EOL, we must be at the end of the script, so stop here. + break; + } + } + else if (script.startsWith(blockCommentStartDelimiter, i)) { + // Skip over any block comments + int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i); + if (indexOfCommentEnd > i) { + i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1; + continue; + } + else { + throw new ScriptParseException( + "Missing block comment end delimiter: " + blockCommentEndDelimiter, resource); + } + } } } diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ScriptUtilsUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ScriptUtilsUnitTests.java index e98c5dafc00..334afe6e3ae 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ScriptUtilsUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ScriptUtilsUnitTests.java @@ -207,9 +207,25 @@ public class ScriptUtilsUnitTests { "'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" + "'insert into users(first, last)\nvalues(''Charles'', ''d\\''Artagnan''); select 1' # ; # true", + // semicolon inside comments + "'-- a;b;c\ninsert into colors(color_num) values(42);' # ; # true", + "'/* a;b;c */\ninsert into colors(color_num) values(42);' # ; # true", + "'-- a;b;c\ninsert into colors(color_num) values(42)' # ; # false", + "'/* a;b;c */\ninsert into colors(color_num) values(42)' # ; # false", + // single quotes inside comments + "'-- What\\''s your favorite color?\ninsert into colors(color_num) values(42);' # ; # true", + "'-- What''s your favorite color?\ninsert into colors(color_num) values(42);' # ; # true", + "'/* What\\''s your favorite color? */\ninsert into colors(color_num) values(42);' # ; # true", + "'/* What''s your favorite color? */\ninsert into colors(color_num) values(42);' # ; # true", + // double quotes inside comments + "'-- double \" quotes\ninsert into colors(color_num) values(42);' # ; # true", + "'-- double \\\" quotes\ninsert into colors(color_num) values(42);' # ; # true", + "'/* double \" quotes */\ninsert into colors(color_num) values(42);' # ; # true", + "'/* double \\\" quotes */\ninsert into colors(color_num) values(42);' # ; # true" }) - public void containsDelimiter(String script, String delimiter, boolean expected) { + public void containsStatementSeparator(String script, String delimiter, boolean expected) { + // Indirectly tests ScriptUtils.containsStatementSeparator(EncodedResource, String, String, String[], String, String). assertThat(containsSqlScriptDelimiters(script, delimiter)).isEqualTo(expected); } diff --git a/spring-r2dbc/src/test/resources/org/springframework/r2dbc/connection/init/test-data-with-multi-line-nested-comments.sql b/spring-r2dbc/src/test/resources/org/springframework/r2dbc/connection/init/test-data-with-multi-line-nested-comments.sql index 7faa91c250a..5df377ccade 100644 --- a/spring-r2dbc/src/test/resources/org/springframework/r2dbc/connection/init/test-data-with-multi-line-nested-comments.sql +++ b/spring-r2dbc/src/test/resources/org/springframework/r2dbc/connection/init/test-data-with-multi-line-nested-comments.sql @@ -5,16 +5,19 @@ * x, y, z... */ +-- This is a single line comment containing single (') and double quotes ("). INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller'); -- This is also a comment. /*------------------------------------------- --- A fancy multi-line comments that puts +-- A fancy multi-line comment that puts -- single line comments inside of a multi-line -- comment block. Moreover, the block comment end delimiter appears on a line that can potentially also be a single-line comment if we weren't already inside a multi-line comment run. + +And here's a line containing single and double quotes ("). -------------------------------------------*/ INSERT INTO users(first_name, last_name) -- This is a single line comment containing the block-end-comment sequence here */ but it's still a single-line comment