diff --git a/src/MySqlConnector/MySqlParameter.cs b/src/MySqlConnector/MySqlParameter.cs index dfda400f5..fd85c721a 100644 --- a/src/MySqlConnector/MySqlParameter.cs +++ b/src/MySqlConnector/MySqlParameter.cs @@ -197,15 +197,15 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions } else if (Value is string stringValue) { - WriteString(writer, noBackslashEscapes, writeDelimiters: true, stringValue.AsSpan()); + WriteString(writer, noBackslashEscapes, stringValue.AsSpan()); } else if (Value is ReadOnlyMemory readOnlyMemoryChar) { - WriteString(writer, noBackslashEscapes, writeDelimiters: true, readOnlyMemoryChar.Span); + WriteString(writer, noBackslashEscapes, readOnlyMemoryChar.Span); } else if (Value is Memory memoryChar) { - WriteString(writer, noBackslashEscapes, writeDelimiters: true, memoryChar.Span); + WriteString(writer, noBackslashEscapes, memoryChar.Span); } else if (Value is char charValue) { @@ -447,12 +447,12 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions #if NETCOREAPP3_1_OR_GREATER writer.Write((byte) '\''); foreach (var chunk in stringBuilder.GetChunks()) - WriteString(writer, noBackslashEscapes, writeDelimiters: false, chunk.Span); + WriteStringChunk(writer, noBackslashEscapes, chunk.Span); if (stringBuilder.Length != 0) writer.Write("".AsSpan(), flush: true); writer.Write((byte) '\''); #else - WriteString(writer, noBackslashEscapes, writeDelimiters: true, stringBuilder.ToString().AsSpan()); + WriteString(writer, noBackslashEscapes, stringBuilder.ToString().AsSpan()); #endif } else if (MySqlDbType == MySqlDbType.Int16) @@ -494,10 +494,9 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions throw new NotSupportedException($"Parameter type {Value.GetType().Name} is not supported; see https://fl.vu/mysql-param-type. Value: {Value}"); } - static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, bool writeDelimiters, ReadOnlySpan value) + static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, ReadOnlySpan value) { - if (writeDelimiters) - writer.Write((byte) '\''); + writer.Write((byte) '\''); var charsWritten = 0; while (charsWritten < value.Length) @@ -507,13 +506,13 @@ static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, bool w if (nextDelimiterIndex == -1) { // write the rest of the string - writer.Write(remainingValue, flush: writeDelimiters); + writer.Write(remainingValue); charsWritten += remainingValue.Length; } else { // write up to (and including) the delimiter, then double it - writer.Write(remainingValue[..nextDelimiterIndex], flush: true); + writer.Write(remainingValue[..nextDelimiterIndex]); if (remainingValue[nextDelimiterIndex] == '\\' && !noBackslashEscapes) writer.Write((ushort) 0x5C5C); // \\ else if (remainingValue[nextDelimiterIndex] == '\\' && noBackslashEscapes) @@ -528,9 +527,43 @@ static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, bool w } } - if (writeDelimiters) - writer.Write((byte) '\''); + writer.Write((byte) '\''); } + +#if NETCOREAPP3_1_OR_GREATER + // Writes a partial chunk of a string (that may end with half of a surrogate pair), escaping any delimiter characters. + static void WriteStringChunk(ByteBufferWriter writer, bool noBackslashEscapes, ReadOnlySpan value) + { + var charsWritten = 0; + while (charsWritten < value.Length) + { + var remainingValue = value[charsWritten..]; + var nextDelimiterIndex = remainingValue.IndexOfAny('\0', '\'', '\\'); + if (nextDelimiterIndex == -1) + { + // write the rest of the string + writer.Write(remainingValue, flush: false); + charsWritten += remainingValue.Length; + } + else + { + // write up to (and including) the delimiter, then double it + writer.Write(remainingValue[..nextDelimiterIndex], flush: true); + if (remainingValue[nextDelimiterIndex] == '\\' && !noBackslashEscapes) + writer.Write((ushort) 0x5C5C); // \\ + else if (remainingValue[nextDelimiterIndex] == '\\' && noBackslashEscapes) + writer.Write((byte) 0x5C); // \ + else if (remainingValue[nextDelimiterIndex] == '\'') + writer.Write((ushort) 0x2727); // '' + else if (remainingValue[nextDelimiterIndex] == '\0' && !noBackslashEscapes) + writer.Write((ushort) 0x305C); // \0 + else if (remainingValue[nextDelimiterIndex] == '\0' && noBackslashEscapes) + writer.Write((byte) 0x00); // (nul) + charsWritten += nextDelimiterIndex + 1; + } + } + } +#endif } internal void AppendBinary(ByteBufferWriter writer, StatementPreparerOptions options) diff --git a/src/MySqlConnector/Protocol/Serialization/ByteBufferWriter.cs b/src/MySqlConnector/Protocol/Serialization/ByteBufferWriter.cs index 5819aa2ab..aabcae82a 100644 --- a/src/MySqlConnector/Protocol/Serialization/ByteBufferWriter.cs +++ b/src/MySqlConnector/Protocol/Serialization/ByteBufferWriter.cs @@ -99,11 +99,22 @@ public void Write(ReadOnlySpan span) m_output = m_output[span.Length..]; } - public void Write(string value) => Write(value.AsSpan(), flush: true); + public void Write(string value) => Write(value.AsSpan()); public void WriteAscii(string value) => WriteAscii(value.AsSpan()); - public void Write(string value, int offset, int length) => Write(value.AsSpan(offset, length), flush: true); + public void Write(string value, int offset, int length) => Write(value.AsSpan(offset, length)); + + public void Write(ReadOnlySpan chars) + { + if (m_output.Length < chars.Length * 3) + { + var neededBytes = Encoding.UTF8.GetByteCount(chars); + if (m_output.Length < neededBytes) + Reallocate(neededBytes); + } + m_output = m_output[Encoding.UTF8.GetBytes(chars, m_output.Span)..]; + } public void Write(ReadOnlySpan chars, bool flush) { @@ -273,7 +284,7 @@ public static void WriteLengthEncodedString(this ByteBufferWriter writer, ReadOn { var byteCount = Encoding.UTF8.GetByteCount(value); writer.WriteLengthEncodedInteger((ulong) byteCount); - writer.Write(value, flush: true); + writer.Write(value); } public static void WriteLengthEncodedAsciiString(this ByteBufferWriter writer, string value)