// Copyright 2009-2022 Josh Close // This file is a part of CsvHelper and is dual licensed under MS-PL and Apache 2.0. // See LICENSE.txt for details or visit http://www.opensource.org/licenses/ms-pl.html for MS-PL and http://opensource.org/licenses/Apache-2.0 for Apache 2.0. // https://github.com/JoshClose/CsvHelper using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Reflection; using CsvHelper.Configuration; using CsvHelper.TypeConversion; using System.Linq; using System.Linq.Expressions; using System.Dynamic; using System.Threading.Tasks; using CsvHelper.Expressions; using System.Globalization; using System.Runtime.CompilerServices; using System.Text; using System.Buffers; using System.Threading; #pragma warning disable 649 #pragma warning disable 169 namespace CsvHelper { /// /// Used to write CSV files. /// public class CsvWriter : IWriter { private readonly TextWriter writer; private readonly CsvContext context; private readonly Lazy recordManager; private readonly TypeConverterCache typeConverterCache; private readonly TrimOptions trimOptions; private readonly ShouldQuote shouldQuote; private readonly MemberMapData reusableMemberMapData = new MemberMapData(null); private readonly Dictionary typeConverterOptionsCache = new Dictionary(); private readonly string quoteString; private readonly char quote; private readonly CultureInfo cultureInfo; private readonly char comment; private readonly bool hasHeaderRecord; private readonly bool includePrivateMembers; private readonly IComparer dynamicPropertySort; private readonly string delimiter; private readonly bool leaveOpen; private readonly string newLine; private readonly char[] injectionCharacters; 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; private int row = 1; private int index; private char[] buffer; private int bufferSize; private int bufferPosition; private Type fieldType; /// public virtual string[] HeaderRecord { get; private set; } /// public virtual int Row => row; /// public virtual int Index => index; /// public virtual CsvContext Context => context; /// public virtual IWriterConfiguration Configuration { get; private set; } /// /// Initializes a new instance of the class. /// /// The writer. /// The culture. /// true to leave the open after the object is disposed, otherwise false. public CsvWriter(TextWriter writer, CultureInfo culture, bool leaveOpen = false) : this(writer, new CsvConfiguration(culture), leaveOpen) { } /// /// Initializes a new instance of the class. /// /// The writer. /// The configuration. /// true to leave the open after the object is disposed, otherwise false. public CsvWriter(TextWriter writer, IWriterConfiguration configuration, bool leaveOpen = false) { configuration.Validate(); this.writer = writer; Configuration = configuration; context = new CsvContext(this); typeConverterCache = context.TypeConverterCache; recordManager = new Lazy(() => ObjectResolver.Current.Resolve(this)); comment = configuration.Comment; bufferSize = configuration.BufferSize; delimiter = configuration.Delimiter; cultureInfo = configuration.CultureInfo; dynamicPropertySort = configuration.DynamicPropertySort; 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; injectionEscapeCharacter = configuration.InjectionEscapeCharacter; this.leaveOpen = leaveOpen; mode = configuration.Mode; newLine = configuration.NewLine; quote = configuration.Quote; quoteString = configuration.Quote.ToString(); escapeString = configuration.Escape.ToString(); injectionOptions = configuration.InjectionOptions; shouldQuote = configuration.ShouldQuote; trimOptions = configuration.TrimOptions; buffer = new char[bufferSize]; } /// public virtual void WriteConvertedField(string field, Type fieldType) { this.fieldType = fieldType; if (field == null) { return; } WriteField(field); } /// public virtual void WriteField(string field) { if (field != null && (trimOptions & TrimOptions.Trim) == TrimOptions.Trim) { field = field.Trim(); } fieldType ??= typeof(string); var args = new ShouldQuoteArgs(field, fieldType, this); var shouldQuoteResult = shouldQuote(args); WriteField(field, shouldQuoteResult); } /// public virtual void WriteField(string field, bool shouldQuote) { if (mode == CsvMode.RFC4180) { // All quotes must be escaped. if (shouldQuote) { if (escapeString != quoteString) { field = field?.Replace(escapeString, escapeEscapeString); } field = field?.Replace(quoteString, escapeQuoteString); field = quote + field + quote; } } else if (mode == CsvMode.Escape) { field = field? .Replace(escapeString, escapeEscapeString) .Replace(quoteString, escapeQuoteString) .Replace(delimiter, escapeDelimiterString) .Replace(newLine, escapeNewlineString); } if (injectionOptions != InjectionOptions.None) { field = SanitizeForInjection(field); } if (index > 0) { WriteToBuffer(delimiter); } WriteToBuffer(field); index++; fieldType = null; } /// public virtual void WriteField(T field) { var type = field == null ? typeof(string) : field.GetType(); var converter = typeConverterCache.GetConverter(type); WriteField(field, converter); } /// public virtual void WriteField(T field, ITypeConverter converter) { var type = field == null ? typeof(string) : field.GetType(); reusableMemberMapData.TypeConverter = converter; if (!typeConverterOptionsCache.TryGetValue(type, out TypeConverterOptions typeConverterOptions)) { typeConverterOptions = TypeConverterOptions.Merge(new TypeConverterOptions { CultureInfo = cultureInfo }, context.TypeConverterOptionsCache.GetOptions(type)); typeConverterOptionsCache.Add(type, typeConverterOptions); } reusableMemberMapData.TypeConverterOptions = typeConverterOptions; var fieldString = converter.ConvertToString(field, this, reusableMemberMapData); WriteConvertedField(fieldString, type); } /// public virtual void WriteField(T field) { var converter = typeConverterCache.GetConverter(); WriteField(field, converter); } /// public virtual void WriteComment(string text) { WriteField(comment + text, false); } /// public virtual void WriteHeader() { WriteHeader(typeof(T)); } /// public virtual void WriteHeader(Type type) { if (type == null) { throw new ArgumentNullException(nameof(type)); } if (type == typeof(object)) { return; } if (context.Maps[type] == null) { context.Maps.Add(context.AutoMap(type)); } var members = new MemberMapCollection(); members.AddMembers(context.Maps[type]); var headerRecord = new List(); foreach (var member in members) { if (CanWrite(member)) { if (member.Data.IndexEnd >= member.Data.Index) { var count = member.Data.IndexEnd - member.Data.Index + 1; for (var i = 1; i <= count; i++) { var header = member.Data.Names.FirstOrDefault() + i; WriteField(header); headerRecord.Add(header); } } else { var header = member.Data.Names.FirstOrDefault(); WriteField(header); headerRecord.Add(header); } } } HeaderRecord = headerRecord.ToArray(); hasHeaderBeenWritten = true; } /// /// Writes a dynamic header record. /// /// The header record to write. /// Thrown when no record is passed. public virtual void WriteDynamicHeader(IDynamicMetaObjectProvider record) { if (record == null) { throw new ArgumentNullException(nameof(record)); } var metaObject = record.GetMetaObject(Expression.Constant(record)); var names = metaObject.GetDynamicMemberNames().ToList(); if (dynamicPropertySort != null) { names = names.OrderBy(name => name, dynamicPropertySort).ToList(); } HeaderRecord = names.ToArray(); foreach (var name in names) { WriteField(name); } hasHeaderBeenWritten = true; } /// public virtual void WriteRecord(T? record) { try { recordManager.Value.Write(record); } catch (TargetInvocationException ex) { if (ex.InnerException != null) { throw ex.InnerException; } else { throw; } } catch (Exception ex) when (ex is not CsvHelperException) { throw new WriterException(context, "An unexpected error occurred. See inner exception for details.", ex); } } /// public virtual void WriteRecords(IEnumerable records) { // Changes in this method require changes in method WriteRecords(IEnumerable records) also. try { if (WriteHeader(records)) { NextRecord(); } foreach (var record in records) { if (record == null) { // Since every record could be a different type, just write a blank line. NextRecord(); continue; } WriteRecord(record); NextRecord(); } } catch (Exception ex) when (ex is not CsvHelperException) { throw new WriterException(context, "An unexpected error occurred. See inner exception for details.", ex); } } /// public virtual void WriteRecords(IEnumerable records) { // Changes in this method require changes in method WriteRecords(IEnumerable records) also. try { if (WriteHeader(records)) { NextRecord(); } foreach (var record in records) { WriteRecord(record); NextRecord(); } } catch (Exception ex) when (ex is not CsvHelperException) { throw new WriterException(context, "An unexpected error occurred. See inner exception for details.", ex); } } /// public virtual async Task WriteRecordsAsync(IEnumerable records, CancellationToken cancellationToken = default) { // These methods should all be the same; // - WriteRecordsAsync(IEnumerable records) // - WriteRecordsAsync(IEnumerable records) // - WriteRecordsAsync(IAsyncEnumerable records) try { if (WriteHeader(records)) { await NextRecordAsync().ConfigureAwait(false); } foreach (var record in records) { cancellationToken.ThrowIfCancellationRequested(); WriteRecord(record); await NextRecordAsync().ConfigureAwait(false); } } catch (Exception ex) when (ex is not CsvHelperException) { throw new WriterException(context, "An unexpected error occurred. See inner exception for details.", ex); } } /// public virtual async Task WriteRecordsAsync(IEnumerable records, CancellationToken cancellationToken = default) { // These methods should all be the same; // - WriteRecordsAsync(IEnumerable records) // - WriteRecordsAsync(IEnumerable records) // - WriteRecordsAsync(IAsyncEnumerable records) try { if (WriteHeader(records)) { await NextRecordAsync().ConfigureAwait(false); } foreach (var record in records) { cancellationToken.ThrowIfCancellationRequested(); WriteRecord(record); await NextRecordAsync().ConfigureAwait(false); } } catch (Exception ex) when (ex is not CsvHelperException) { throw new WriterException(context, "An unexpected error occurred. See inner exception for details.", ex); } } #if !NET45 /// public virtual async Task WriteRecordsAsync(IAsyncEnumerable records, CancellationToken cancellationToken = default) { // These methods should all be the same; // - WriteRecordsAsync(IEnumerable records) // - WriteRecordsAsync(IEnumerable records) // - WriteRecordsAsync(IAsyncEnumerable records) try { if (await WriteHeaderAsync(records)) { await NextRecordAsync().ConfigureAwait(false); } await foreach (var record in records.ConfigureAwait(false)) { cancellationToken.ThrowIfCancellationRequested(); WriteRecord(record); await NextRecordAsync().ConfigureAwait(false); } } catch (Exception ex) when (ex is not CsvHelperException) { throw new WriterException(context, "An unexpected error occurred. See inner exception for details.", ex); } } #endif /// public virtual void NextRecord() { WriteToBuffer(newLine); FlushBuffer(); index = 0; row++; } /// public virtual async Task NextRecordAsync() { WriteToBuffer(newLine); await FlushBufferAsync().ConfigureAwait(false); index = 0; row++; } /// public virtual void Flush() { FlushBuffer(); writer.Flush(); } /// public virtual async Task FlushAsync() { await FlushBufferAsync().ConfigureAwait(false); await writer.FlushAsync().ConfigureAwait(false); } /// /// Flushes the buffer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual void FlushBuffer() { writer.Write(buffer, 0, bufferPosition); bufferPosition = 0; } /// /// Asynchronously flushes the buffer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual async Task FlushBufferAsync() { await writer.WriteAsync(buffer, 0, bufferPosition).ConfigureAwait(false); bufferPosition = 0; } /// /// Indicates if values can be written. /// /// The member map. /// True if values can be written. public virtual bool CanWrite(MemberMap memberMap) { var cantWrite = // Ignored members. memberMap.Data.Ignore; if (memberMap.Data.Member is PropertyInfo property) { cantWrite = cantWrite || // Properties that don't have a public getter // and we are honoring the accessor modifier. property.GetGetMethod() == null && !includePrivateMembers || // Properties that don't have a getter at all. property.GetGetMethod(true) == null; } return !cantWrite; } /// /// Determines the type for the given record. /// /// The type of the record. /// The record to determine the type of. /// The System.Type for the record. public virtual Type GetTypeForRecord(T record) { var type = typeof(T); if (type == typeof(object)) { type = record.GetType(); } return type; } /// /// Sanitizes the given field, before it is injected. /// /// The field to sanitize. /// The sanitized field. /// Thrown when an injection character is found in the field. [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual string SanitizeForInjection(string field) { if (string.IsNullOrEmpty(field)) { return field; } int injectionCharIndex; if (ArrayHelper.Contains(injectionCharacters, field[0])) { injectionCharIndex = 0; } else if (field[0] == quote && field[field.Length - 1] == quote && ArrayHelper.Contains(injectionCharacters, field[1])) { injectionCharIndex = 1; } else { return field; } if (injectionOptions == InjectionOptions.Exception) { throw new WriterException(context, $"Injection character '{field[injectionCharIndex]}' detected"); } if (injectionOptions == InjectionOptions.Escape) { if (injectionCharIndex == 0) { // =1+"2 -> "'=1+""2" field = quoteString + injectionEscapeCharacter + field.Replace(quoteString, escapeQuoteString) + quoteString; } else { // "=1+2" -> "'=1+2" field = quoteString + injectionEscapeCharacter + field.Substring(injectionCharIndex); } } else if (injectionOptions == InjectionOptions.Strip) { while (true) { field = field.Substring(1); if (field.Length == 0 || !ArrayHelper.Contains(injectionCharacters, field[0])) { break; } } if (injectionCharIndex == 1) { field = quoteString + field; } } return field; } /// /// Writes the given value to the buffer. /// /// The value to write. [MethodImpl(MethodImplOptions.AggressiveInlining)] protected void WriteToBuffer(string value) { var length = value?.Length ?? 0; if (value == null || length == 0) { return; } var lengthNeeded = bufferPosition + length; if (lengthNeeded >= bufferSize) { while (lengthNeeded >= bufferSize) { bufferSize *= 2; } Array.Resize(ref buffer, bufferSize); } value.CopyTo(0, buffer, bufferPosition, length); bufferPosition += length; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Disposes the object. /// /// Indicates if the object is being disposed. protected virtual void Dispose(bool disposing) { if (disposed) { return; } Flush(); if (disposing) { // Dispose managed state (managed objects) if (!leaveOpen) { writer.Dispose(); } } // Free unmanaged resources (unmanaged objects) and override finalizer // Set large fields to null buffer = null; disposed = true; } #if !NET45 && !NET47 && !NETSTANDARD2_0 /// public async ValueTask DisposeAsync() { await DisposeAsync(true).ConfigureAwait(false); GC.SuppressFinalize(this); } /// protected virtual async ValueTask DisposeAsync(bool disposing) { if (disposed) { return; } await FlushAsync().ConfigureAwait(false); if (disposing) { // Dispose managed state (managed objects) if (!leaveOpen) { await writer.DisposeAsync().ConfigureAwait(false); } } // Free unmanaged resources (unmanaged objects) and override finalizer // Set large fields to null buffer = null; disposed = true; } #endif #if !NET45 private async Task WriteHeaderAsync(IAsyncEnumerable records) { if (!hasHeaderRecord || hasHeaderBeenWritten) { return false; } var recordType = typeof(T); var isPrimitive = recordType.GetTypeInfo().IsPrimitive; if (!isPrimitive && recordType != typeof(object)) { WriteHeader(recordType); return hasHeaderBeenWritten; } return WriteHeader(await records.FirstOrDefaultAsync()); } #endif private bool WriteHeader(IEnumerable records) { if (!hasHeaderRecord || hasHeaderBeenWritten) { return false; } var recordType = typeof(T); var isPrimitive = recordType.GetTypeInfo().IsPrimitive; if (!isPrimitive && recordType != typeof(object)) { WriteHeader(recordType); return hasHeaderBeenWritten; } return WriteHeader(records.FirstOrDefault()); } private bool WriteHeader(IEnumerable records) { object? record = null; foreach (var r in records) { if (r != null) { record = r; } } return WriteHeader(record); } private bool WriteHeader(object? record) { if (record == null) { return false; } if (record is IDynamicMetaObjectProvider dynamicObject) { WriteDynamicHeader(dynamicObject); return true; } var recordType = record.GetType(); var isPrimitive = recordType.GetTypeInfo().IsPrimitive; if (!isPrimitive) { WriteHeader(recordType); return true; } return false; } } }