diff options
Diffstat (limited to 'ThirdParty/CsvHelper-master/src/CsvHelper/CsvWriter.cs')
-rw-r--r-- | ThirdParty/CsvHelper-master/src/CsvHelper/CsvWriter.cs | 842 |
1 files changed, 842 insertions, 0 deletions
diff --git a/ThirdParty/CsvHelper-master/src/CsvHelper/CsvWriter.cs b/ThirdParty/CsvHelper-master/src/CsvHelper/CsvWriter.cs new file mode 100644 index 0000000..4120926 --- /dev/null +++ b/ThirdParty/CsvHelper-master/src/CsvHelper/CsvWriter.cs @@ -0,0 +1,842 @@ +// 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 +{ + /// <summary> + /// Used to write CSV files. + /// </summary> + public class CsvWriter : IWriter + { + private readonly TextWriter writer; + private readonly CsvContext context; + private readonly Lazy<RecordManager> recordManager; + private readonly TypeConverterCache typeConverterCache; + private readonly TrimOptions trimOptions; + private readonly ShouldQuote shouldQuote; + private readonly MemberMapData reusableMemberMapData = new MemberMapData(null); + private readonly Dictionary<Type, TypeConverterOptions> typeConverterOptionsCache = new Dictionary<Type, TypeConverterOptions>(); + 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<string> 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; + + /// <inheritdoc/> + public virtual string[] HeaderRecord { get; private set; } + + /// <inheritdoc/> + public virtual int Row => row; + + /// <inheritdoc/> + public virtual int Index => index; + + /// <inheritdoc/> + public virtual CsvContext Context => context; + + /// <inheritdoc/> + public virtual IWriterConfiguration Configuration { get; private set; } + + /// <summary> + /// Initializes a new instance of the <see cref="CsvWriter"/> class. + /// </summary> + /// <param name="writer">The writer.</param> + /// <param name="culture">The culture.</param> + /// <param name="leaveOpen"><c>true</c> to leave the <see cref="TextWriter"/> open after the <see cref="CsvWriter"/> object is disposed, otherwise <c>false</c>.</param> + public CsvWriter(TextWriter writer, CultureInfo culture, bool leaveOpen = false) : this(writer, new CsvConfiguration(culture), leaveOpen) { } + + /// <summary> + /// Initializes a new instance of the <see cref="CsvWriter"/> class. + /// </summary> + /// <param name="writer">The writer.</param> + /// <param name="configuration">The configuration.</param> + /// <param name="leaveOpen"><c>true</c> to leave the <see cref="TextWriter"/> open after the <see cref="CsvWriter"/> object is disposed, otherwise <c>false</c>.</param> + 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<RecordManager>(() => ObjectResolver.Current.Resolve<RecordManager>(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]; + } + + /// <inheritdoc/> + public virtual void WriteConvertedField(string field, Type fieldType) + { + this.fieldType = fieldType; + + if (field == null) + { + return; + } + + WriteField(field); + } + + /// <inheritdoc/> + 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); + } + + /// <inheritdoc/> + 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; + } + + /// <inheritdoc/> + public virtual void WriteField<T>(T field) + { + var type = field == null ? typeof(string) : field.GetType(); + var converter = typeConverterCache.GetConverter(type); + WriteField(field, converter); + } + + /// <inheritdoc/> + public virtual void WriteField<T>(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); + } + + /// <inheritdoc/> + public virtual void WriteField<T, TConverter>(T field) + { + var converter = typeConverterCache.GetConverter<TConverter>(); + + WriteField(field, converter); + } + + /// <inheritdoc/> + public virtual void WriteComment(string text) + { + WriteField(comment + text, false); + } + + /// <inheritdoc/> + public virtual void WriteHeader<T>() + { + WriteHeader(typeof(T)); + } + + /// <inheritdoc/> + 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<string>(); + + 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; + } + + /// <summary> + /// Writes a dynamic header record. + /// </summary> + /// <param name="record">The header record to write.</param> + /// <exception cref="ArgumentNullException">Thrown when no record is passed.</exception> + 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; + } + + /// <inheritdoc/> + public virtual void WriteRecord<T>(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); + } + } + + /// <inheritdoc/> + public virtual void WriteRecords(IEnumerable records) + { + // Changes in this method require changes in method WriteRecords<T>(IEnumerable<T> 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); + } + } + + /// <inheritdoc/> + public virtual void WriteRecords<T>(IEnumerable<T> 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); + } + } + + /// <inheritdoc/> + public virtual async Task WriteRecordsAsync(IEnumerable records, CancellationToken cancellationToken = default) + { + // These methods should all be the same; + // - WriteRecordsAsync(IEnumerable records) + // - WriteRecordsAsync<T>(IEnumerable<T> records) + // - WriteRecordsAsync<T>(IAsyncEnumerable<T> 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); + } + } + + /// <inheritdoc/> + public virtual async Task WriteRecordsAsync<T>(IEnumerable<T> records, CancellationToken cancellationToken = default) + { + // These methods should all be the same; + // - WriteRecordsAsync(IEnumerable records) + // - WriteRecordsAsync<T>(IEnumerable<T> records) + // - WriteRecordsAsync<T>(IAsyncEnumerable<T> 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 + /// <inheritdoc/> + public virtual async Task WriteRecordsAsync<T>(IAsyncEnumerable<T> records, CancellationToken cancellationToken = default) + { + // These methods should all be the same; + // - WriteRecordsAsync(IEnumerable records) + // - WriteRecordsAsync<T>(IEnumerable<T> records) + // - WriteRecordsAsync<T>(IAsyncEnumerable<T> 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 + + /// <inheritdoc/> + public virtual void NextRecord() + { + WriteToBuffer(newLine); + FlushBuffer(); + + index = 0; + row++; + } + + /// <inheritdoc/> + public virtual async Task NextRecordAsync() + { + WriteToBuffer(newLine); + await FlushBufferAsync().ConfigureAwait(false); + + index = 0; + row++; + } + + /// <inheritdoc/> + public virtual void Flush() + { + FlushBuffer(); + writer.Flush(); + } + + /// <inheritdoc/> + public virtual async Task FlushAsync() + { + await FlushBufferAsync().ConfigureAwait(false); + await writer.FlushAsync().ConfigureAwait(false); + } + + /// <summary> + /// Flushes the buffer. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual void FlushBuffer() + { + writer.Write(buffer, 0, bufferPosition); + bufferPosition = 0; + } + + /// <summary> + /// Asynchronously flushes the buffer. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual async Task FlushBufferAsync() + { + await writer.WriteAsync(buffer, 0, bufferPosition).ConfigureAwait(false); + bufferPosition = 0; + } + + /// <summary> + /// Indicates if values can be written. + /// </summary> + /// <param name="memberMap">The member map.</param> + /// <returns>True if values can be written.</returns> + 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; + } + + /// <summary> + /// Determines the type for the given record. + /// </summary> + /// <typeparam name="T">The type of the record.</typeparam> + /// <param name="record">The record to determine the type of.</param> + /// <returns>The System.Type for the record.</returns> + public virtual Type GetTypeForRecord<T>(T record) + { + var type = typeof(T); + if (type == typeof(object)) + { + type = record.GetType(); + } + + return type; + } + + /// <summary> + /// Sanitizes the given field, before it is injected. + /// </summary> + /// <param name="field">The field to sanitize.</param> + /// <returns>The sanitized field.</returns> + /// <exception cref="WriterException">Thrown when an injection character is found in the field.</exception> + [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; + } + + /// <summary> + /// Writes the given value to the buffer. + /// </summary> + /// <param name="value">The value to write.</param> + [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; + } + + /// <inheritdoc/> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Disposes the object. + /// </summary> + /// <param name="disposing">Indicates if the object is being disposed.</param> + 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 + /// <inheritdoc/> + public async ValueTask DisposeAsync() + { + await DisposeAsync(true).ConfigureAwait(false); + GC.SuppressFinalize(this); + } + + /// <inheritdoc/> + 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<bool> WriteHeaderAsync<T>(IAsyncEnumerable<T> 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<T>(IEnumerable<T> 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; + } + } +} |