From 90dcb88c2951cccc95e33b280dd9e755fa697803 Mon Sep 17 00:00:00 2001 From: Josh Close Date: Thu, 10 Nov 2022 12:53:10 -0600 Subject: [PATCH] Escape the escape char when writing. --- src/CsvHelper/CsvWriter.cs | 10 +++ .../Writing/WriteCustomEscapeTests.cs | 63 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/CsvHelper.Tests/Writing/WriteCustomEscapeTests.cs diff --git a/src/CsvHelper/CsvWriter.cs b/src/CsvHelper/CsvWriter.cs index cb0070d4f..0ed0828d7 100644 --- a/src/CsvHelper/CsvWriter.cs +++ b/src/CsvHelper/CsvWriter.cs @@ -52,9 +52,11 @@ public class CsvWriter : IWriter private readonly char injectionEscapeCharacter; private readonly InjectionOptions injectionOptions; private readonly CsvMode mode; + private readonly string escapeString; private readonly string escapeQuoteString; private readonly string escapeDelimiterString; private readonly string escapeNewlineString; + private readonly string escapeEscapeString; private bool disposed; private bool hasHeaderBeenWritten; @@ -112,6 +114,7 @@ public CsvWriter(TextWriter writer, IWriterConfiguration configuration, bool lea escapeDelimiterString = new string(configuration.Delimiter.SelectMany(c => new[] { configuration.Escape, c }).ToArray()); escapeNewlineString = new string(configuration.NewLine.SelectMany(c => new[] { configuration.Escape, c }).ToArray()); escapeQuoteString = new string(new[] { configuration.Escape, configuration.Quote }); + escapeEscapeString = new string(new[] { configuration.Escape, configuration.Escape }); hasHeaderRecord = configuration.HasHeaderRecord; includePrivateMembers = configuration.IncludePrivateMembers; injectionCharacters = configuration.InjectionCharacters; @@ -121,6 +124,7 @@ public CsvWriter(TextWriter writer, IWriterConfiguration configuration, bool lea newLine = configuration.NewLine; quote = configuration.Quote; quoteString = configuration.Quote.ToString(); + escapeString = configuration.Escape.ToString(); injectionOptions = configuration.InjectionOptions; shouldQuote = configuration.ShouldQuote; trimOptions = configuration.TrimOptions; @@ -165,6 +169,11 @@ public virtual void WriteField(string field, bool shouldQuote) // All quotes must be escaped. if (shouldQuote) { + if (escapeString != quoteString) + { + field = field?.Replace(escapeString, escapeEscapeString); + } + field = field?.Replace(quoteString, escapeQuoteString); field = quote + field + quote; } @@ -172,6 +181,7 @@ public virtual void WriteField(string field, bool shouldQuote) else if (mode == CsvMode.Escape) { field = field? + .Replace(escapeString, escapeEscapeString) .Replace(quoteString, escapeQuoteString) .Replace(delimiter, escapeDelimiterString) .Replace(newLine, escapeNewlineString); diff --git a/tests/CsvHelper.Tests/Writing/WriteCustomEscapeTests.cs b/tests/CsvHelper.Tests/Writing/WriteCustomEscapeTests.cs new file mode 100644 index 000000000..f45da086e --- /dev/null +++ b/tests/CsvHelper.Tests/Writing/WriteCustomEscapeTests.cs @@ -0,0 +1,63 @@ +using CsvHelper.Configuration; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; + +namespace CsvHelper.Tests.Writing +{ + public class WriteCustomEscapeTests + { + [Fact] + public void WriteField_CustomEscapeChar_ModeRFC4180_EscapesQuotesAndEscapeCharacter() + { + var config = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false, + Escape = '\\', + }; + using (var writer = new StringWriter()) + using (var csv = new CsvWriter(writer, config)) + { + // {"json":"{\"name\":\"foo\"}"} + // json string -> csv field + // "{\"json\":\"{\\\"name\\\":\\\"foo\\\"}\"}" + csv.WriteField(@"{""json"":""{\""name\"":\""foo\""}""}"); + csv.Flush(); + + var expected = @"""{\""json\"":\""{\\\""name\\\"":\\\""foo\\\""}\""}"""; + Assert.Equal(expected, writer.ToString()); + } + } + + [Fact] + public void WriteField_CustomEscapeChar_ModeEscape_EscapesQuotesAndEscapeCharacter() + { + var config = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false, + Escape = '\\', + Mode = CsvMode.Escape, + }; + using (var writer = new StringWriter()) + using (var csv = new CsvWriter(writer, config)) + { + // {"json":"{\"name\":\"foo\"}"} + // json string -> csv field + // {\"json\":\"{\\\"name\\\":\\\"foo\\\"}\"} + csv.WriteField(@"{""json"":""{\""name\"":\""foo\""}""}"); + csv.Flush(); + + var expected = @"{\""json\"":\""{\\\""name\\\"":\\\""foo\\\""}\""}"; + Assert.Equal(expected, writer.ToString()); + } + } + } +}