Skip to content

Commit

Permalink
Make newline escaping logic more robust
Browse files Browse the repository at this point in the history
Fixes #22553
  • Loading branch information
bricelam committed Sep 18, 2020
1 parent c17b1ba commit 85aa742
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 34 deletions.
141 changes: 124 additions & 17 deletions src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Data;
using System.Data.Common;
using System.Text;
using JetBrains.Annotations;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore.Storage;
Expand Down Expand Up @@ -161,32 +162,138 @@ protected override void ConfigureParameter(DbParameter parameter)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateNonNullSqlLiteral(object value)
=> EscapeLineBreaks(EscapeSqlLiteral((string)value));
{
var stringValue = (string)value;
var builder = new StringBuilder();

private static readonly char[] LineBreakChars = { '\r', '\n' };
var start = 0;
int i;
int length;
var concatenated = false;
var openApostrophe = false;
for (i = 0; i < stringValue.Length; i++)
{
var lineFeed = stringValue[i] == '\n';
var carriageReturn = stringValue[i] == '\r';
var apostrophe = stringValue[i] == '\'';
if (lineFeed || carriageReturn || apostrophe)
{
length = i - start;
if (length != 0)
{
if (!openApostrophe)
{
if (builder.Length != 0)
{
builder.Append(", ");
concatenated = true;
}

private string EscapeLineBreaks(string value)
{
var unicodePrefix = IsUnicode ? "N" : string.Empty;
if (IsUnicode)
{
builder.Append('N');
}

builder.Append('\'');
openApostrophe = true;
}

builder.Append(stringValue.AsSpan().Slice(start, length));
}

if (lineFeed || carriageReturn)
{
if (openApostrophe)
{
builder.Append('\'');
openApostrophe = false;
}

if (value == null
|| value.IndexOfAny(LineBreakChars) == -1)
if (builder.Length != 0)
{
builder.Append(", ");
concatenated = true;
}

if (IsUnicode)
{
builder.Append('N');
}

builder
.Append("CHAR(")
.Append(lineFeed ? "10" : "13")
.Append(')');
}
else if (apostrophe)
{
if (!openApostrophe)
{
if (builder.Length != 0)
{
builder.Append(", ");
concatenated = true;
}

if (IsUnicode)
{
builder.Append('N');
}

builder.Append("'");
openApostrophe = true;
}
builder.Append("''");
}
start = i + 1;
}
}
length = i - start;
if (length != 0)
{
return $"{unicodePrefix}'{value}'";
if (!openApostrophe)
{
if (builder.Length != 0)
{
builder.Append(", ");
concatenated = true;
}

if (IsUnicode)
{
builder.Append('N');
}

builder.Append('\'');
openApostrophe = true;
}

builder.Append(stringValue.AsSpan().Slice(start, length));
}

if (openApostrophe)
{
builder.Append('\'');
}

if (value.Length == 1)
if (concatenated)
{
return value[0] == '\n' ? "CHAR(10)" : "CHAR(13)";
builder
.Insert(0, "CONCAT(")
.Append(')');
}

if (builder.Length == 0)
{
if (IsUnicode)
{
builder.Append('N');
}

builder.Append("''");
}

return ($"CONCAT({unicodePrefix}'"
+ value
.Replace("\r", $"', CHAR(13), {unicodePrefix}'")
.Replace("\n", $"', CHAR(10), {unicodePrefix}'")
+ "')")
.Replace($"{unicodePrefix}'', ", string.Empty)
.Replace($", {unicodePrefix}''", string.Empty);
return builder.ToString();
}
}
}
122 changes: 108 additions & 14 deletions src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Data;
using System.Text;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Storage;

Expand Down Expand Up @@ -55,25 +57,117 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override string GenerateNonNullSqlLiteral(object value)
=> EscapeLineBreaks(EscapeSqlLiteral((string)value));
{
var stringValue = (string)value;
var builder = new StringBuilder();

private static readonly char[] LineBreakChars = { '\r', '\n' };
var start = 0;
int i;
int length;
var concatenated = false;
var openApostrophe = false;
for (i = 0; i < stringValue.Length; i++)
{
var lineFeed = stringValue[i] == '\n';
var carriageReturn = stringValue[i] == '\r';
var apostrophe = stringValue[i] == '\'';
if (lineFeed || carriageReturn || apostrophe)
{
length = i - start;
if (length != 0)
{
if (!openApostrophe)
{
if (builder.Length != 0)
{
builder.Append(" || ");
concatenated = true;
}

private static string EscapeLineBreaks(string value)
{
if (value == null
|| value.IndexOfAny(LineBreakChars) == -1)
builder.Append('\'');
openApostrophe = true;
}

builder.Append(stringValue.AsSpan().Slice(start, length));
}

if (lineFeed || carriageReturn)
{
if (openApostrophe)
{
builder.Append('\'');
openApostrophe = false;
}

if (builder.Length != 0)
{
builder.Append(" || ");
concatenated = true;
}

builder
.Append("CHAR(")
.Append(lineFeed ? "10" : "13")
.Append(')');

}
else if (apostrophe)
{
if (!openApostrophe)
{
if (builder.Length != 0)
{
builder.Append(" || ");
concatenated = true;
}

builder.Append("'");
openApostrophe = true;
}

builder.Append("''");
}

start = i + 1;
}
}

length = i - start;
if (length != 0)
{
if (!openApostrophe)
{
if (builder.Length != 0)
{
builder.Append(" || ");
concatenated = true;
}

builder.Append('\'');
openApostrophe = true;
}

builder.Append(stringValue.AsSpan().Slice(start, length));
}

if (openApostrophe)
{
builder.Append('\'');
}

if (concatenated)
{
builder
.Insert(0, '(')
.Append(')');
}

if (builder.Length == 0)
{
return $"'{value}'";
builder.Append("''");
}

return ("('"
+ value
.Replace("\r", "' || CHAR(13) || '")
.Replace("\n", "' || CHAR(10) || '")
+ "')")
.Replace("'' || ", string.Empty)
.Replace(" || ''", string.Empty);
return builder.ToString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ [Name] nvarchar(max) NULL
DECLARE @defaultSchema AS sysname;
SET @defaultSchema = SCHEMA_NAME();
DECLARE @description AS sql_variant;
SET @description = CONCAT(N'This is a multi-line', CHAR(13), CHAR(10), N'table comment.', CHAR(13), CHAR(10), N'More information can', CHAR(13), CHAR(10), N'be found in the docs.');
SET @description = CONCAT(N'This is a multi-line', NCHAR(13), NCHAR(10), N'table comment.', NCHAR(13), NCHAR(10), N'More information can', NCHAR(13), NCHAR(10), N'be found in the docs.');
EXEC sp_addextendedproperty 'MS_Description', @description, 'SCHEMA', @defaultSchema, 'TABLE', N'People';
SET @description = CONCAT(N'This is a multi-line', CHAR(10), N'column comment.', CHAR(10), N'More information can', CHAR(10), N'be found in the docs.');
SET @description = CONCAT(N'This is a multi-line', NCHAR(10), N'column comment.', NCHAR(10), N'More information can', NCHAR(10), N'be found in the docs.');
EXEC sp_addextendedproperty 'MS_Description', @description, 'SCHEMA', @defaultSchema, 'TABLE', N'People', 'COLUMN', N'Name';");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -926,7 +926,7 @@ public override void DefaultValue_with_line_breaks(bool isUnicode)
var storeType = isUnicode ? "nvarchar(max)" : "varchar(max)";
var unicodePrefix = isUnicode ? "N" : string.Empty;
var expectedSql = @$"CREATE TABLE [dbo].[TestLineBreaks] (
[TestDefaultValue] {storeType} NOT NULL DEFAULT CONCAT(CHAR(13), CHAR(10), {unicodePrefix}'Various Line', CHAR(13), {unicodePrefix}'Breaks', CHAR(10))
[TestDefaultValue] {storeType} NOT NULL DEFAULT CONCAT({unicodePrefix}CHAR(13), {unicodePrefix}CHAR(10), {unicodePrefix}'Various Line', {unicodePrefix}CHAR(13), {unicodePrefix}'Breaks', {unicodePrefix}CHAR(10))
);
";
AssertSql(expectedSql);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Xunit;

namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal
{
public class SqlServerStringTypeMappingTest
{
[ConditionalTheory]
[InlineData("", "''")]
[InlineData("'Sup", "'''Sup'")]
[InlineData("I'm", "'I''m'")]
[InlineData("lovin'", "'lovin'''")]
[InlineData("it", "'it'")]
[InlineData("'", "''''")]
[InlineData("''", "''''''")]
[InlineData("I'm lovin'", "'I''m lovin'''")]
[InlineData("I'm lovin' it", "'I''m lovin'' it'")]
[InlineData("\r", "CHAR(13)")]
[InlineData("\n", "CHAR(10)")]
[InlineData("\r\n", "CONCAT(CHAR(13), CHAR(10))")]
[InlineData("\n'sup", "CONCAT(CHAR(10), '''sup')")]
[InlineData("I'm\n", "CONCAT('I''m', CHAR(10))")]
[InlineData("lovin'\n", "CONCAT('lovin''', CHAR(10))")]
[InlineData("it\n", "CONCAT('it', CHAR(10))")]
[InlineData("\nit", "CONCAT(CHAR(10), 'it')")]
[InlineData("'\n", "CONCAT('''', CHAR(10))")]
public void GenerateProviderValueSqlLiteral_works(string value, string expected)
{
var mapping = new SqlServerStringTypeMapping("varchar(max)");
Assert.Equal(expected, mapping.GenerateProviderValueSqlLiteral(value));
}

[ConditionalTheory]
[InlineData("", "N''")]
[InlineData("'Sup", "N'''Sup'")]
[InlineData("I'm", "N'I''m'")]
[InlineData("lovin'", "N'lovin'''")]
[InlineData("it", "N'it'")]
[InlineData("'", "N''''")]
[InlineData("''", "N''''''")]
[InlineData("I'm lovin'", "N'I''m lovin'''")]
[InlineData("I'm lovin' it", "N'I''m lovin'' it'")]
[InlineData("\r", "NCHAR(13)")]
[InlineData("\n", "NCHAR(10)")]
[InlineData("\r\n", "CONCAT(NCHAR(13), NCHAR(10))")]
[InlineData("\n'sup", "CONCAT(NCHAR(10), N'''sup')")]
[InlineData("I'm\n", "CONCAT(N'I''m', NCHAR(10))")]
[InlineData("lovin'\n", "CONCAT(N'lovin''', NCHAR(10))")]
[InlineData("it\n", "CONCAT(N'it', NCHAR(10))")]
[InlineData("\nit", "CONCAT(NCHAR(10), N'it')")]
[InlineData("'\n", "CONCAT(N'''', NCHAR(10))")]
public void GenerateProviderValueSqlLiteral_works_unicode(string value, string expected)
{
var mapping = new SqlServerStringTypeMapping("nvarchar(max)", unicode: true);
Assert.Equal(expected, mapping.GenerateProviderValueSqlLiteral(value));
}
}
}
Loading

0 comments on commit 85aa742

Please sign in to comment.