From d155800cc7f30d25767c70f9c086977a53cfb62b Mon Sep 17 00:00:00 2001 From: Laurents Meyer Date: Sat, 21 Sep 2019 20:53:27 +0200 Subject: [PATCH] Add NO_BACKSLASH_ESCAPES support to StatementPreparer, SqlParser and derived classes and MySqlParameter. Single quotes are now always escaped with an additional single quote instead of a leading backspace. This addresses everything except triggering/detecting the NO_BACKSLASH_ESCAPES from #701 and https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/827 Signed-off-by: Laurents Meyer --- src/MySqlConnector/Core/SqlParser.cs | 9 ++++-- src/MySqlConnector/Core/StatementPreparer.cs | 31 +++++++++---------- .../Core/StatementPreparerOptions.cs | 1 + .../MySql.Data.MySqlClient/MySqlParameter.cs | 17 ++++++++-- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/MySqlConnector/Core/SqlParser.cs b/src/MySqlConnector/Core/SqlParser.cs index 03554cd52..0b5809f3b 100644 --- a/src/MySqlConnector/Core/SqlParser.cs +++ b/src/MySqlConnector/Core/SqlParser.cs @@ -5,11 +5,16 @@ namespace MySqlConnector.Core { internal abstract class SqlParser { + protected StatementPreparer Preparer { get; } + + protected SqlParser(StatementPreparer preparer) => Preparer = preparer; + public void Parse(string sql) { OnBeforeParse(sql ?? throw new ArgumentNullException(nameof(sql))); int parameterStartIndex = -1; + var noBackslashEscapes = (Preparer.Options & StatementPreparerOptions.NoBackslashEscapes) == StatementPreparerOptions.NoBackslashEscapes; var state = State.Beginning; var beforeCommentState = State.Beginning; @@ -35,7 +40,7 @@ public void Parse(string sql) { if (ch == '\'') state = State.SingleQuotedStringSingleQuote; - else if (ch == '\\') + else if (ch == '\\' && !noBackslashEscapes) state = State.SingleQuotedStringBackslash; } else if (state == State.SingleQuotedStringBackslash) @@ -46,7 +51,7 @@ public void Parse(string sql) { if (ch == '"') state = State.DoubleQuotedStringDoubleQuote; - else if (ch == '\\') + else if (ch == '\\' && !noBackslashEscapes) state = State.DoubleQuotedStringBackslash; } else if (state == State.DoubleQuotedStringBackslash) diff --git a/src/MySqlConnector/Core/StatementPreparer.cs b/src/MySqlConnector/Core/StatementPreparer.cs index 7b45ecf46..143a2ad51 100644 --- a/src/MySqlConnector/Core/StatementPreparer.cs +++ b/src/MySqlConnector/Core/StatementPreparer.cs @@ -9,11 +9,13 @@ namespace MySqlConnector.Core { internal sealed class StatementPreparer { + public StatementPreparerOptions Options { get; } + public StatementPreparer(string commandText, MySqlParameterCollection? parameters, StatementPreparerOptions options) { m_commandText = commandText; m_parameters = parameters; - m_options = options; + Options = options; } public ParsedStatements SplitStatements() @@ -42,7 +44,7 @@ public bool ParseAndBindParameters(ByteBufferWriter writer) private int GetParameterIndex(string name) { var index = m_parameters?.NormalizedIndexOf(name) ?? -1; - if (index == -1 && (m_options & StatementPreparerOptions.AllowUserVariables) == 0) + if (index == -1 && (Options & StatementPreparerOptions.AllowUserVariables) == 0) throw new MySqlException("Parameter '{0}' must be defined. To use this as a variable, set 'Allow User Variables=true' in the connection string.".FormatInvariant(name)); return index; } @@ -52,7 +54,7 @@ private MySqlParameter GetInputParameter(int index) if (index >= (m_parameters?.Count ?? 0)) throw new MySqlException("Parameter index {0} is invalid when only {1} parameter{2} defined.".FormatInvariant(index, m_parameters?.Count ?? 0, m_parameters?.Count == 1 ? " is" : "s are")); var parameter = m_parameters![index]; - if (parameter.Direction != ParameterDirection.Input && (m_options & StatementPreparerOptions.AllowOutputParameters) == 0) + if (parameter.Direction != ParameterDirection.Input && (Options & StatementPreparerOptions.AllowOutputParameters) == 0) throw new MySqlException("Only ParameterDirection.Input is supported when CommandType is Text (parameter name: {0})".FormatInvariant(parameter.ParameterName)); return parameter; } @@ -60,8 +62,8 @@ private MySqlParameter GetInputParameter(int index) private sealed class ParameterSqlParser : SqlParser { public ParameterSqlParser(StatementPreparer preparer, ByteBufferWriter writer) + : base(preparer) { - m_preparer = preparer; m_writer = writer; } @@ -69,7 +71,7 @@ public ParameterSqlParser(StatementPreparer preparer, ByteBufferWriter writer) protected override void OnNamedParameter(int index, int length) { - var parameterIndex = m_preparer.GetParameterIndex(m_preparer.m_commandText.Substring(index, length)); + var parameterIndex = Preparer.GetParameterIndex(Preparer.m_commandText.Substring(index, length)); if (parameterIndex != -1) DoAppendParameter(parameterIndex, index, length); } @@ -82,15 +84,15 @@ protected override void OnPositionalParameter(int index) private void DoAppendParameter(int parameterIndex, int textIndex, int textLength) { - m_writer.Write(m_preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex); - var parameter = m_preparer.GetInputParameter(parameterIndex); - parameter.AppendSqlString(m_writer, m_preparer.m_options); + m_writer.Write(Preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex); + var parameter = Preparer.GetInputParameter(parameterIndex); + parameter.AppendSqlString(m_writer, Preparer.Options); m_lastIndex = textIndex + textLength; } protected override void OnParsed(FinalParseStates states) { - m_writer.Write(m_preparer.m_commandText, m_lastIndex, m_preparer.m_commandText.Length - m_lastIndex); + m_writer.Write(Preparer.m_commandText, m_lastIndex, Preparer.m_commandText.Length - m_lastIndex); if ((states & FinalParseStates.NeedsNewline) == FinalParseStates.NeedsNewline) m_writer.Write((byte) '\n'); if ((states & FinalParseStates.NeedsSemicolon) == FinalParseStates.NeedsSemicolon) @@ -98,7 +100,6 @@ protected override void OnParsed(FinalParseStates states) IsComplete = (states & FinalParseStates.Complete) == FinalParseStates.Complete; } - readonly StatementPreparer m_preparer; readonly ByteBufferWriter m_writer; int m_currentParameterIndex; int m_lastIndex; @@ -107,8 +108,8 @@ protected override void OnParsed(FinalParseStates states) private sealed class PreparedCommandSqlParser : SqlParser { public PreparedCommandSqlParser(StatementPreparer preparer, List statements, List statementStartEndIndexes, ByteBufferWriter writer) + : base(preparer) { - m_preparer = preparer; m_statements = statements; m_statementStartEndIndexes = statementStartEndIndexes; m_writer = writer; @@ -124,7 +125,7 @@ protected override void OnStatementBegin(int index) protected override void OnNamedParameter(int index, int length) { - var parameterName = m_preparer.m_commandText.Substring(index, length); + var parameterName = Preparer.m_commandText.Substring(index, length); DoAppendParameter(parameterName, -1, index, length); } @@ -137,7 +138,7 @@ protected override void OnPositionalParameter(int index) private void DoAppendParameter(string? parameterName, int parameterIndex, int textIndex, int textLength) { // write all SQL up to the parameter - m_writer.Write(m_preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex); + m_writer.Write(Preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex); m_lastIndex = textIndex + textLength; // replace the parameter with a ? placeholder @@ -150,12 +151,11 @@ private void DoAppendParameter(string? parameterName, int parameterIndex, int te protected override void OnStatementEnd(int index) { - m_writer.Write(m_preparer.m_commandText, m_lastIndex, index - m_lastIndex); + m_writer.Write(Preparer.m_commandText, m_lastIndex, index - m_lastIndex); m_lastIndex = index; m_statementStartEndIndexes.Add(m_writer.Position); } - readonly StatementPreparer m_preparer; readonly List m_statements; readonly List m_statementStartEndIndexes; readonly ByteBufferWriter m_writer; @@ -166,6 +166,5 @@ protected override void OnStatementEnd(int index) readonly string m_commandText; readonly MySqlParameterCollection? m_parameters; - readonly StatementPreparerOptions m_options; } } diff --git a/src/MySqlConnector/Core/StatementPreparerOptions.cs b/src/MySqlConnector/Core/StatementPreparerOptions.cs index 4cfb2783f..003bb55b2 100644 --- a/src/MySqlConnector/Core/StatementPreparerOptions.cs +++ b/src/MySqlConnector/Core/StatementPreparerOptions.cs @@ -16,5 +16,6 @@ internal enum StatementPreparerOptions GuidFormatTimeSwapBinary16 = 0x80, GuidFormatLittleEndianBinary16 = 0xA0, GuidFormatMask = 0xE0, + NoBackslashEscapes = 0x100, } } diff --git a/src/MySqlConnector/MySql.Data.MySqlClient/MySqlParameter.cs b/src/MySqlConnector/MySql.Data.MySqlClient/MySqlParameter.cs index f64db4305..65b6f7049 100644 --- a/src/MySqlConnector/MySql.Data.MySqlClient/MySqlParameter.cs +++ b/src/MySqlConnector/MySql.Data.MySqlClient/MySqlParameter.cs @@ -207,6 +207,8 @@ private MySqlParameter(MySqlParameter other, string parameterName) internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions options) { + var noBackslashEscapes = (options & StatementPreparerOptions.NoBackslashEscapes) == StatementPreparerOptions.NoBackslashEscapes; + if (Value is null || Value == DBNull.Value) { writer.Write(s_nullBytes); @@ -214,7 +216,12 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions else if (Value is string stringValue) { writer.Write((byte) '\''); - writer.Write(stringValue.Replace("\\", "\\\\").Replace("'", "\\'")); + + if (noBackslashEscapes) + writer.Write(stringValue.Replace("'", "''")); + else + writer.Write(stringValue.Replace("\\", "\\\\").Replace("'", "''")); + writer.Write((byte) '\''); } else if (Value is char charValue) @@ -223,8 +230,14 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions switch (charValue) { case '\'': + writer.Write((byte) '\''); + writer.Write((byte) charValue); + break; + case '\\': - writer.Write((byte) '\\'); + if (!noBackslashEscapes) + writer.Write((byte) '\\'); + writer.Write((byte) charValue); break;