Skip to content

Commit d550a69

Browse files
lauxjpnbgrainger
authored andcommitted
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 PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#827 Signed-off-by: Laurents Meyer <[email protected]>
1 parent 3852548 commit d550a69

File tree

5 files changed

+48
-24
lines changed

5 files changed

+48
-24
lines changed

src/MySqlConnector/Core/SqlParser.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ namespace MySqlConnector.Core
55
{
66
internal abstract class SqlParser
77
{
8+
protected StatementPreparer Preparer { get; }
9+
10+
protected SqlParser(StatementPreparer preparer) => Preparer = preparer;
11+
812
public void Parse(string sql)
913
{
1014
OnBeforeParse(sql ?? throw new ArgumentNullException(nameof(sql)));
1115

1216
int parameterStartIndex = -1;
17+
var noBackslashEscapes = (Preparer.Options & StatementPreparerOptions.NoBackslashEscapes) == StatementPreparerOptions.NoBackslashEscapes;
1318

1419
var state = State.Beginning;
1520
var beforeCommentState = State.Beginning;
@@ -35,7 +40,7 @@ public void Parse(string sql)
3540
{
3641
if (ch == '\'')
3742
state = State.SingleQuotedStringSingleQuote;
38-
else if (ch == '\\')
43+
else if (ch == '\\' && !noBackslashEscapes)
3944
state = State.SingleQuotedStringBackslash;
4045
}
4146
else if (state == State.SingleQuotedStringBackslash)
@@ -46,7 +51,7 @@ public void Parse(string sql)
4651
{
4752
if (ch == '"')
4853
state = State.DoubleQuotedStringDoubleQuote;
49-
else if (ch == '\\')
54+
else if (ch == '\\' && !noBackslashEscapes)
5055
state = State.DoubleQuotedStringBackslash;
5156
}
5257
else if (state == State.DoubleQuotedStringBackslash)

src/MySqlConnector/Core/StatementPreparer.cs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ namespace MySqlConnector.Core
99
{
1010
internal sealed class StatementPreparer
1111
{
12+
public StatementPreparerOptions Options { get; }
13+
1214
public StatementPreparer(string commandText, MySqlParameterCollection? parameters, StatementPreparerOptions options)
1315
{
1416
m_commandText = commandText;
1517
m_parameters = parameters;
16-
m_options = options;
18+
Options = options;
1719
}
1820

1921
public ParsedStatements SplitStatements()
@@ -42,7 +44,7 @@ public bool ParseAndBindParameters(ByteBufferWriter writer)
4244
private int GetParameterIndex(string name)
4345
{
4446
var index = m_parameters?.NormalizedIndexOf(name) ?? -1;
45-
if (index == -1 && (m_options & StatementPreparerOptions.AllowUserVariables) == 0)
47+
if (index == -1 && (Options & StatementPreparerOptions.AllowUserVariables) == 0)
4648
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));
4749
return index;
4850
}
@@ -52,24 +54,24 @@ private MySqlParameter GetInputParameter(int index)
5254
if (index >= (m_parameters?.Count ?? 0))
5355
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"));
5456
var parameter = m_parameters![index];
55-
if (parameter.Direction != ParameterDirection.Input && (m_options & StatementPreparerOptions.AllowOutputParameters) == 0)
57+
if (parameter.Direction != ParameterDirection.Input && (Options & StatementPreparerOptions.AllowOutputParameters) == 0)
5658
throw new MySqlException("Only ParameterDirection.Input is supported when CommandType is Text (parameter name: {0})".FormatInvariant(parameter.ParameterName));
5759
return parameter;
5860
}
5961

6062
private sealed class ParameterSqlParser : SqlParser
6163
{
6264
public ParameterSqlParser(StatementPreparer preparer, ByteBufferWriter writer)
65+
: base(preparer)
6366
{
64-
m_preparer = preparer;
6567
m_writer = writer;
6668
}
6769

6870
public bool IsComplete { get; private set; }
6971

7072
protected override void OnNamedParameter(int index, int length)
7173
{
72-
var parameterIndex = m_preparer.GetParameterIndex(m_preparer.m_commandText.Substring(index, length));
74+
var parameterIndex = Preparer.GetParameterIndex(Preparer.m_commandText.Substring(index, length));
7375
if (parameterIndex != -1)
7476
DoAppendParameter(parameterIndex, index, length);
7577
}
@@ -82,23 +84,22 @@ protected override void OnPositionalParameter(int index)
8284

8385
private void DoAppendParameter(int parameterIndex, int textIndex, int textLength)
8486
{
85-
m_writer.Write(m_preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex);
86-
var parameter = m_preparer.GetInputParameter(parameterIndex);
87-
parameter.AppendSqlString(m_writer, m_preparer.m_options);
87+
m_writer.Write(Preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex);
88+
var parameter = Preparer.GetInputParameter(parameterIndex);
89+
parameter.AppendSqlString(m_writer, Preparer.Options);
8890
m_lastIndex = textIndex + textLength;
8991
}
9092

9193
protected override void OnParsed(FinalParseStates states)
9294
{
93-
m_writer.Write(m_preparer.m_commandText, m_lastIndex, m_preparer.m_commandText.Length - m_lastIndex);
95+
m_writer.Write(Preparer.m_commandText, m_lastIndex, Preparer.m_commandText.Length - m_lastIndex);
9496
if ((states & FinalParseStates.NeedsNewline) == FinalParseStates.NeedsNewline)
9597
m_writer.Write((byte) '\n');
9698
if ((states & FinalParseStates.NeedsSemicolon) == FinalParseStates.NeedsSemicolon)
9799
m_writer.Write((byte) ';');
98100
IsComplete = (states & FinalParseStates.Complete) == FinalParseStates.Complete;
99101
}
100102

101-
readonly StatementPreparer m_preparer;
102103
readonly ByteBufferWriter m_writer;
103104
int m_currentParameterIndex;
104105
int m_lastIndex;
@@ -107,8 +108,8 @@ protected override void OnParsed(FinalParseStates states)
107108
private sealed class PreparedCommandSqlParser : SqlParser
108109
{
109110
public PreparedCommandSqlParser(StatementPreparer preparer, List<ParsedStatement> statements, List<int> statementStartEndIndexes, ByteBufferWriter writer)
111+
: base(preparer)
110112
{
111-
m_preparer = preparer;
112113
m_statements = statements;
113114
m_statementStartEndIndexes = statementStartEndIndexes;
114115
m_writer = writer;
@@ -124,7 +125,7 @@ protected override void OnStatementBegin(int index)
124125

125126
protected override void OnNamedParameter(int index, int length)
126127
{
127-
var parameterName = m_preparer.m_commandText.Substring(index, length);
128+
var parameterName = Preparer.m_commandText.Substring(index, length);
128129
DoAppendParameter(parameterName, -1, index, length);
129130
}
130131

@@ -137,7 +138,7 @@ protected override void OnPositionalParameter(int index)
137138
private void DoAppendParameter(string? parameterName, int parameterIndex, int textIndex, int textLength)
138139
{
139140
// write all SQL up to the parameter
140-
m_writer.Write(m_preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex);
141+
m_writer.Write(Preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex);
141142
m_lastIndex = textIndex + textLength;
142143

143144
// replace the parameter with a ? placeholder
@@ -150,12 +151,11 @@ private void DoAppendParameter(string? parameterName, int parameterIndex, int te
150151

151152
protected override void OnStatementEnd(int index)
152153
{
153-
m_writer.Write(m_preparer.m_commandText, m_lastIndex, index - m_lastIndex);
154+
m_writer.Write(Preparer.m_commandText, m_lastIndex, index - m_lastIndex);
154155
m_lastIndex = index;
155156
m_statementStartEndIndexes.Add(m_writer.Position);
156157
}
157158

158-
readonly StatementPreparer m_preparer;
159159
readonly List<ParsedStatement> m_statements;
160160
readonly List<int> m_statementStartEndIndexes;
161161
readonly ByteBufferWriter m_writer;
@@ -166,6 +166,5 @@ protected override void OnStatementEnd(int index)
166166

167167
readonly string m_commandText;
168168
readonly MySqlParameterCollection? m_parameters;
169-
readonly StatementPreparerOptions m_options;
170169
}
171170
}

src/MySqlConnector/Core/StatementPreparerOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ internal enum StatementPreparerOptions
1616
GuidFormatTimeSwapBinary16 = 0x80,
1717
GuidFormatLittleEndianBinary16 = 0xA0,
1818
GuidFormatMask = 0xE0,
19+
NoBackslashEscapes = 0x100,
1920
}
2021
}

src/MySqlConnector/MySql.Data.MySqlClient/MySqlParameter.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,21 @@ private MySqlParameter(MySqlParameter other, string parameterName)
207207

208208
internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions options)
209209
{
210+
var noBackslashEscapes = (options & StatementPreparerOptions.NoBackslashEscapes) == StatementPreparerOptions.NoBackslashEscapes;
211+
210212
if (Value is null || Value == DBNull.Value)
211213
{
212214
writer.Write(s_nullBytes);
213215
}
214216
else if (Value is string stringValue)
215217
{
216218
writer.Write((byte) '\'');
217-
writer.Write(stringValue.Replace("\\", "\\\\").Replace("'", "\\'"));
219+
220+
if (noBackslashEscapes)
221+
writer.Write(stringValue.Replace("'", "''"));
222+
else
223+
writer.Write(stringValue.Replace("\\", "\\\\").Replace("'", "''"));
224+
218225
writer.Write((byte) '\'');
219226
}
220227
else if (Value is char charValue)
@@ -223,8 +230,14 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
223230
switch (charValue)
224231
{
225232
case '\'':
233+
writer.Write((byte) '\'');
234+
writer.Write((byte) charValue);
235+
break;
236+
226237
case '\\':
227-
writer.Write((byte) '\\');
238+
if (!noBackslashEscapes)
239+
writer.Write((byte) '\\');
240+
228241
writer.Write((byte) charValue);
229242
break;
230243

tests/MySqlConnector.Tests/StatementPreparerTests.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,12 @@ public void Bug589(string sql)
134134

135135
[Theory]
136136
[MemberData(nameof(FormatParameterData))]
137-
public void FormatParameter(object parameterValue, string replacedValue)
137+
public void FormatParameter(object parameterValue, string replacedValue, bool noBackslashEscapes = false)
138138
{
139139
var parameters = new MySqlParameterCollection { new MySqlParameter("@param", parameterValue) };
140140
const string sql = "SELECT @param;";
141-
var parsedSql = GetParsedSql(sql, parameters);
141+
var options = noBackslashEscapes ? StatementPreparerOptions.NoBackslashEscapes : StatementPreparerOptions.None;
142+
var parsedSql = GetParsedSql(sql, parameters, options);
142143
Assert.Equal(sql.Replace("@param", replacedValue), parsedSql);
143144
}
144145

@@ -157,10 +158,15 @@ public void FormatParameter(object parameterValue, string replacedValue)
157158
new object[] { 1.0123456789012346, "1.0123456789012346" },
158159
new object[] { 123456789.123456789m, "123456789.123456789" },
159160
new object[] { "1234", "'1234'" },
160-
new object[] { "it's", "'it\\'s'" },
161+
new object[] { "it's", "'it''s'" },
162+
new object[] { "it's", "'it''s'", true },
161163
new object[] { 'a', "'a'" },
162-
new object[] { '\'', "'\\''" },
164+
new object[] { '\'', "''''" },
165+
new object[] { '\'', "''''", true },
163166
new object[] { '\\', "'\\\\'" },
167+
new object[] { '\\', "'\\'", true },
168+
new object[] { "\\'", "'\\\\'''" },
169+
new object[] { "\\'", "'\\'''", true },
164170
new object[] { 'ffi', "'ffi'" },
165171
new object[] { new DateTime(1234, 12, 23, 12, 34, 56, 789), "timestamp('1234-12-23 12:34:56.789000')" },
166172
new object[] { new DateTimeOffset(1234, 12, 23, 12, 34, 56, 789, TimeSpan.FromHours(2)), "timestamp('1234-12-23 10:34:56.789000')" },

0 commit comments

Comments
 (0)