From c7dc3a96205481d04454f533c7c741c06f0e5b72 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 25 Aug 2021 17:14:44 +0200 Subject: [PATCH 01/16] optimize File.*AllText* APIs --- .../src/System/IO/File.cs | 148 +++++++----------- .../src/System/IO/File.netcoreapp.cs | 95 +++++++++++ 2 files changed, 148 insertions(+), 95 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index ff2a7bd9ef6286..07238afc51643b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -300,18 +300,7 @@ private static string InternalReadAllText(string path, Encoding encoding) return sr.ReadToEnd(); } - public static void WriteAllText(string path, string? contents) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - - using (StreamWriter sw = new StreamWriter(path)) - { - sw.Write(contents); - } - } + public static void WriteAllText(string path, string? contents) => WriteAllText(path, contents, UTF8NoBOM); public static void WriteAllText(string path, string? contents, Encoding encoding) { @@ -322,10 +311,7 @@ public static void WriteAllText(string path, string? contents, Encoding encoding if (path.Length == 0) throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - using (StreamWriter sw = new StreamWriter(path, false, encoding)) - { - sw.Write(contents); - } + WriteToFile(path, append: false, contents, encoding); } public static byte[] ReadAllBytes(string path) @@ -492,18 +478,7 @@ private static void InternalWriteAllLines(TextWriter writer, IEnumerable } } - public static void AppendAllText(string path, string? contents) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - - using (StreamWriter sw = new StreamWriter(path, append: true)) - { - sw.Write(contents); - } - } + public static void AppendAllText(string path, string? contents) => AppendAllText(path, contents, UTF8NoBOM); public static void AppendAllText(string path, string? contents, Encoding encoding) { @@ -514,23 +489,10 @@ public static void AppendAllText(string path, string? contents, Encoding encodin if (path.Length == 0) throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - using (StreamWriter sw = new StreamWriter(path, true, encoding)) - { - sw.Write(contents); - } + WriteToFile(path, append: true, contents, encoding); } - public static void AppendAllLines(string path, IEnumerable contents) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (contents == null) - throw new ArgumentNullException(nameof(contents)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - - InternalWriteAllLines(new StreamWriter(path, append: true), contents); - } + public static void AppendAllLines(string path, IEnumerable contents) => AppendAllLines(path, contents, UTF8NoBOM); public static void AppendAllLines(string path, IEnumerable contents, Encoding encoding) { @@ -706,13 +668,7 @@ private static async Task InternalReadAllTextAsync(string path, Encoding return Task.FromCanceled(cancellationToken); } - if (string.IsNullOrEmpty(contents)) - { - new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read).Dispose(); - return Task.CompletedTask; - } - - return InternalWriteAllTextAsync(AsyncStreamWriter(path, encoding, append: false), contents, cancellationToken); + return WriteToFileAsync(path, append: false, contents, encoding, cancellationToken); } public static Task ReadAllBytesAsync(string path, CancellationToken cancellationToken = default(CancellationToken)) @@ -922,43 +878,6 @@ private static async Task InternalWriteAllLinesAsync(TextWriter writer, IEnumera } } - private static async Task InternalWriteAllTextAsync(StreamWriter sw, string contents, CancellationToken cancellationToken) - { -#if MS_IO_REDIST - char[]? buffer = null; - try - { - buffer = ArrayPool.Shared.Rent(DefaultBufferSize); - int count = contents.Length; - int index = 0; - while (index < count) - { - int batchSize = Math.Min(DefaultBufferSize, count - index); - contents.CopyTo(index, buffer, 0, batchSize); - await sw.WriteAsync(buffer, 0, batchSize).ConfigureAwait(false); - index += batchSize; - } - - cancellationToken.ThrowIfCancellationRequested(); - await sw.FlushAsync().ConfigureAwait(false); - } - finally - { - sw.Dispose(); - if (buffer != null) - { - ArrayPool.Shared.Return(buffer); - } - } -#else - using (sw) - { - await sw.WriteAsync(contents.AsMemory(), cancellationToken).ConfigureAwait(false); - await sw.FlushAsync().ConfigureAwait(false); - } -#endif - } - public static Task AppendAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default(CancellationToken)) => AppendAllTextAsync(path, contents, UTF8NoBOM, cancellationToken); @@ -976,14 +895,7 @@ private static async Task InternalWriteAllTextAsync(StreamWriter sw, string cont return Task.FromCanceled(cancellationToken); } - if (string.IsNullOrEmpty(contents)) - { - // Just to throw exception if there is a problem opening the file. - new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read).Dispose(); - return Task.CompletedTask; - } - - return InternalWriteAllTextAsync(AsyncStreamWriter(path, encoding, append: true), contents, cancellationToken); + return WriteToFileAsync(path, append: true, contents, encoding, cancellationToken); } public static Task AppendAllLinesAsync(string path, IEnumerable contents, CancellationToken cancellationToken = default(CancellationToken)) @@ -1044,5 +956,51 @@ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget FileSystem.VerifyValidPath(linkPath, nameof(linkPath)); return FileSystem.ResolveLinkTarget(linkPath, returnFinalTarget, isDirectory: false); } + +#if MS_IO_REDIST + private static void WriteToFile(string path, bool append, string? contents, Encoding encoding) + { + using (StreamWriter sw = new StreamWriter(path, append, encoding)) + { + sw.Write(contents); + } + } + + private static async Task WriteToFileAsync(string path, bool append, string? contents, Encoding encoding, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(contents)) + { + new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read).Dispose(); + return; + } + + StreamWriter sw = AsyncStreamWriter(path, encoding, append); + char[]? buffer = null; + try + { + buffer = ArrayPool.Shared.Rent(DefaultBufferSize); + int count = contents!.Length; + int index = 0; + while (index < count) + { + int batchSize = Math.Min(DefaultBufferSize, count - index); + contents!.CopyTo(index, buffer, 0, batchSize); + await sw.WriteAsync(buffer, 0, batchSize).ConfigureAwait(false); + index += batchSize; + } + + cancellationToken.ThrowIfCancellationRequested(); + await sw.FlushAsync().ConfigureAwait(false); + } + finally + { + sw.Dispose(); + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } +#endif } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs index b5502ff9926cb8..f001c5180af62a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs @@ -3,12 +3,22 @@ using System.Buffers; using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; namespace System.IO { public static partial class File { + private const int ChunkSize = +#if DEBUG + 100; +#else + 8192; +#endif + /// /// Initializes a new instance of the class with the specified path, creation mode, read/write and sharing permission, the access other FileStreams can have to the same file, the buffer size, additional file options and the allocation size. /// @@ -97,5 +107,90 @@ private static byte[] ReadAllBytesUnknownLength(FileStream fs) } } } + + private static void WriteToFile(string path, bool append, string? contents, Encoding encoding) + { + int preambleSize = encoding.Preamble.Length; + int maxFileSize = string.IsNullOrEmpty(contents) ? 0 : preambleSize + encoding.GetMaxByteCount(contents.Length); + long preallocationSize = maxFileSize < ChunkSize ? 0 : maxFileSize; // for small files setting preallocationSize has no perf benefit, as it requires an additional sys-call + using SafeFileHandle fileHandle = OpenHandle(path, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, FileOptions.None, preallocationSize); + if (string.IsNullOrEmpty(contents)) + { + return; // even if the content is empty, we want to create an empty file + } + + long fileOffset = append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0; + + int bytesNeeded = Math.Min(maxFileSize, preambleSize + encoding.GetMaxByteCount(ChunkSize)); + byte[]? rentedBytes = null; + Span bytes = bytesNeeded <= 1024 ? stackalloc byte[1024] : (rentedBytes = ArrayPool.Shared.Rent(bytesNeeded)); + + try + { + encoding.Preamble.CopyTo(bytes); + + Encoder encoder = encoding.GetEncoder(); + ReadOnlySpan remaining = contents; + while (!remaining.IsEmpty) + { + ReadOnlySpan toEncode = remaining.Slice(0, Math.Min(remaining.Length, ChunkSize)); + int encoded = encoder.GetBytes(toEncode, bytes.Slice(preambleSize), flush: false); + Span toStore = bytes.Slice(0, preambleSize + encoded); + + RandomAccess.WriteAtOffset(fileHandle, toStore, fileOffset); + + fileOffset += toStore.Length; + remaining = remaining.Slice(toEncode.Length); + preambleSize = 0; + } + } + finally + { + if (rentedBytes is not null) + { + ArrayPool.Shared.Return(rentedBytes); + } + } + } + + private static async Task WriteToFileAsync(string path, bool append, string? contents, Encoding encoding, CancellationToken cancellationToken) + { + int preambleSize = encoding.Preamble.Length; + int maxFileSize = string.IsNullOrEmpty(contents) ? 0 : preambleSize + encoding.GetMaxByteCount(contents.Length); + long preallocationSize = maxFileSize < ChunkSize ? 0 : maxFileSize; // for small files setting preallocationSize has no perf benefit, as it requires an additional sys-call + using SafeFileHandle fileHandle = OpenHandle(path, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous, preallocationSize); + if (string.IsNullOrEmpty(contents)) + { + return; // even if the content is empty, we want to create an empty file + } + + long fileOffset = append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0; + + byte[] bytes = ArrayPool.Shared.Rent(Math.Min(maxFileSize, preambleSize + encoding.GetMaxByteCount(ChunkSize))); + + try + { + encoding.Preamble.CopyTo(bytes); + + Encoder encoder = encoding.GetEncoder(); + ReadOnlyMemory remaining = contents.AsMemory(); + while (!remaining.IsEmpty) + { + ReadOnlyMemory toEncode = remaining.Slice(0, Math.Min(remaining.Length, ChunkSize)); + int encoded = encoder.GetBytes(toEncode.Span, bytes.AsSpan(preambleSize), flush: false); + ReadOnlyMemory toStore = new ReadOnlyMemory(bytes, 0, preambleSize + encoded); + + await RandomAccess.WriteAtOffsetAsync(fileHandle, toStore, fileOffset, cancellationToken).ConfigureAwait(false); + + fileOffset += toStore.Length; + remaining = remaining.Slice(toEncode.Length); + preambleSize = 0; + } + } + finally + { + ArrayPool.Shared.Return(bytes); + } + } } } From fe18dbfe17d2487fb3b15e38e9d43b6d0debb2bd Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 26 Aug 2021 11:40:09 +0200 Subject: [PATCH 02/16] remove code duplication --- .../src/System/IO/File.cs | 169 ++++-------------- 1 file changed, 38 insertions(+), 131 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index 07238afc51643b..1078ada9fd6b0b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -268,48 +268,21 @@ public static FileStream OpenWrite(string path) return new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); } - public static string ReadAllText(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - - return InternalReadAllText(path, Encoding.UTF8); - } + public static string ReadAllText(string path) => ReadAllText(path, Encoding.UTF8); public static string ReadAllText(string path, Encoding encoding) { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - - return InternalReadAllText(path, encoding); - } - - private static string InternalReadAllText(string path, Encoding encoding) - { - Debug.Assert(path != null); - Debug.Assert(encoding != null); - Debug.Assert(path.Length > 0); + Validate(path, encoding); - using (StreamReader sr = new StreamReader(path, encoding, detectEncodingFromByteOrderMarks: true)) - return sr.ReadToEnd(); + using StreamReader sr = new StreamReader(path, encoding, detectEncodingFromByteOrderMarks: true); + return sr.ReadToEnd(); } public static void WriteAllText(string path, string? contents) => WriteAllText(path, contents, UTF8NoBOM); public static void WriteAllText(string path, string? contents, Encoding encoding) { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + Validate(path, encoding); WriteToFile(path, append: false, contents, encoding); } @@ -368,62 +341,29 @@ public static void WriteAllBytes(string path, byte[] bytes) RandomAccess.WriteAtOffset(sfh, bytes, 0); #endif } - public static string[] ReadAllLines(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - - return InternalReadAllLines(path, Encoding.UTF8); - } + public static string[] ReadAllLines(string path) => ReadAllLines(path, Encoding.UTF8); public static string[] ReadAllLines(string path, Encoding encoding) { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - - return InternalReadAllLines(path, encoding); - } - - private static string[] InternalReadAllLines(string path, Encoding encoding) - { - Debug.Assert(path != null); - Debug.Assert(encoding != null); - Debug.Assert(path.Length != 0); + Validate(path, encoding); string? line; List lines = new List(); - using (StreamReader sr = new StreamReader(path, encoding)) - while ((line = sr.ReadLine()) != null) - lines.Add(line); + using StreamReader sr = new StreamReader(path, encoding); + while ((line = sr.ReadLine()) != null) + { + lines.Add(line); + } return lines.ToArray(); } - public static IEnumerable ReadLines(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - - return ReadLinesIterator.CreateIterator(path, Encoding.UTF8); - } + public static IEnumerable ReadLines(string path) => ReadLines(path, Encoding.UTF8); public static IEnumerable ReadLines(string path, Encoding encoding) { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + Validate(path, encoding); return ReadLinesIterator.CreateIterator(path, encoding); } @@ -446,20 +386,14 @@ public static void WriteAllLines(string path, IEnumerable contents) } public static void WriteAllLines(string path, string[] contents, Encoding encoding) - { - WriteAllLines(path, (IEnumerable)contents, encoding); - } + => WriteAllLines(path, (IEnumerable)contents, encoding); public static void WriteAllLines(string path, IEnumerable contents, Encoding encoding) { - if (path == null) - throw new ArgumentNullException(nameof(path)); + Validate(path, encoding); + if (contents == null) throw new ArgumentNullException(nameof(contents)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); InternalWriteAllLines(new StreamWriter(path, false, encoding), contents); } @@ -482,12 +416,7 @@ private static void InternalWriteAllLines(TextWriter writer, IEnumerable public static void AppendAllText(string path, string? contents, Encoding encoding) { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + Validate(path, encoding); WriteToFile(path, append: true, contents, encoding); } @@ -496,14 +425,10 @@ public static void AppendAllText(string path, string? contents, Encoding encodin public static void AppendAllLines(string path, IEnumerable contents, Encoding encoding) { - if (path == null) - throw new ArgumentNullException(nameof(path)); + Validate(path, encoding); + if (contents == null) throw new ArgumentNullException(nameof(contents)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); InternalWriteAllLines(new StreamWriter(path, true, encoding), contents); } @@ -602,12 +527,7 @@ private static StreamWriter AsyncStreamWriter(string path, Encoding encoding, bo public static Task ReadAllTextAsync(string path, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + Validate(path, encoding); return cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) @@ -656,12 +576,7 @@ private static async Task InternalReadAllTextAsync(string path, Encoding public static Task WriteAllTextAsync(string path, string? contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + Validate(path, encoding); if (cancellationToken.IsCancellationRequested) { @@ -809,12 +724,7 @@ static async Task Core(string path, byte[] bytes, CancellationToken cancellation public static Task ReadAllLinesAsync(string path, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + Validate(path, encoding); return cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) @@ -846,14 +756,10 @@ private static async Task InternalReadAllLinesAsync(string path, Encod public static Task WriteAllLinesAsync(string path, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) { - if (path == null) - throw new ArgumentNullException(nameof(path)); + Validate(path, encoding); + if (contents == null) throw new ArgumentNullException(nameof(contents)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); return cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) @@ -883,12 +789,7 @@ private static async Task InternalWriteAllLinesAsync(TextWriter writer, IEnumera public static Task AppendAllTextAsync(string path, string? contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + Validate(path, encoding); if (cancellationToken.IsCancellationRequested) { @@ -903,14 +804,10 @@ private static async Task InternalWriteAllLinesAsync(TextWriter writer, IEnumera public static Task AppendAllLinesAsync(string path, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) { - if (path == null) - throw new ArgumentNullException(nameof(path)); + Validate(path, encoding); + if (contents == null) throw new ArgumentNullException(nameof(contents)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); return cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) @@ -957,6 +854,16 @@ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget return FileSystem.ResolveLinkTarget(linkPath, returnFinalTarget, isDirectory: false); } + private static void Validate(string path, Encoding encoding) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + if (path.Length == 0) + throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + } + #if MS_IO_REDIST private static void WriteToFile(string path, bool append, string? contents, Encoding encoding) { From 50a3528dfc27e071c58c4a68596432c494d0e8f6 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 26 Aug 2021 12:02:27 +0200 Subject: [PATCH 03/16] use expression bodies --- .../src/System/IO/File.cs | 192 ++++-------------- 1 file changed, 43 insertions(+), 149 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index 1078ada9fd6b0b..49e4f25445fef2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -29,31 +29,19 @@ public static partial class File private const int MaxByteArrayLength = 0x7FFFFFC7; private static Encoding? s_UTF8NoBOM; + // UTF-8 without BOM and with error detection. Same as the default encoding for StreamWriter. + private static Encoding UTF8NoBOM => s_UTF8NoBOM ?? (s_UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)); + internal const int DefaultBufferSize = 4096; public static StreamReader OpenText(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - - return new StreamReader(path); - } + => new StreamReader(path ?? throw new ArgumentNullException(nameof(path))); public static StreamWriter CreateText(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - - return new StreamWriter(path, append: false); - } + => new StreamWriter(path ?? throw new ArgumentNullException(nameof(path)), append: false); public static StreamWriter AppendText(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - - return new StreamWriter(path, append: true); - } + => new StreamWriter(path ?? throw new ArgumentNullException(nameof(path)), append: true); /// /// Copies an existing file to a new file. @@ -85,10 +73,7 @@ public static void Copy(string sourceFileName, string destFileName, bool overwri // The file is opened with ReadWrite access and cannot be opened by another // application until it has been closed. An IOException is thrown if the // directory specified doesn't exist. - public static FileStream Create(string path) - { - return Create(path, DefaultBufferSize); - } + public static FileStream Create(string path) => Create(path, DefaultBufferSize); // Creates a file in a particular path. If the file exists, it is replaced. // The file is opened with ReadWrite access and cannot be opened by another @@ -107,12 +92,7 @@ public static FileStream Create(string path, int bufferSize, FileOptions options // On Windows, Delete will fail for a file that is open for normal I/O // or a file that is memory mapped. public static void Delete(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - - FileSystem.DeleteFile(Path.GetFullPath(path)); - } + => FileSystem.DeleteFile(Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path)))); // Tests whether a file exists. The result is true if the file // given by the specified path exists; otherwise, the result is @@ -148,125 +128,68 @@ public static bool Exists([NotNullWhen(true)] string? path) } public static FileStream Open(string path, FileMode mode) - { - return Open(path, mode, (mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite), FileShare.None); - } + => Open(path, mode, (mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite), FileShare.None); public static FileStream Open(string path, FileMode mode, FileAccess access) - { - return Open(path, mode, access, FileShare.None); - } + => Open(path, mode, access, FileShare.None); public static FileStream Open(string path, FileMode mode, FileAccess access, FileShare share) - { - return new FileStream(path, mode, access, share); - } + => new FileStream(path, mode, access, share); + // File and Directory UTC APIs treat a DateTimeKind.Unspecified as UTC whereas + // ToUniversalTime treats this as local. internal static DateTimeOffset GetUtcDateTimeOffset(DateTime dateTime) - { - // File and Directory UTC APIs treat a DateTimeKind.Unspecified as UTC whereas - // ToUniversalTime treats this as local. - if (dateTime.Kind == DateTimeKind.Unspecified) - { - return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); - } - - return dateTime.ToUniversalTime(); - } + => dateTime.Kind == DateTimeKind.Unspecified + ? DateTime.SpecifyKind(dateTime, DateTimeKind.Utc) + : dateTime.ToUniversalTime(); public static void SetCreationTime(string path, DateTime creationTime) - { - string fullPath = Path.GetFullPath(path); - FileSystem.SetCreationTime(fullPath, creationTime, asDirectory: false); - } + => FileSystem.SetCreationTime(Path.GetFullPath(path), creationTime, asDirectory: false); public static void SetCreationTimeUtc(string path, DateTime creationTimeUtc) - { - string fullPath = Path.GetFullPath(path); - FileSystem.SetCreationTime(fullPath, GetUtcDateTimeOffset(creationTimeUtc), asDirectory: false); - } + => FileSystem.SetCreationTime(Path.GetFullPath(path), GetUtcDateTimeOffset(creationTimeUtc), asDirectory: false); public static DateTime GetCreationTime(string path) - { - string fullPath = Path.GetFullPath(path); - return FileSystem.GetCreationTime(fullPath).LocalDateTime; - } + => FileSystem.GetCreationTime(Path.GetFullPath(path)).LocalDateTime; public static DateTime GetCreationTimeUtc(string path) - { - string fullPath = Path.GetFullPath(path); - return FileSystem.GetCreationTime(fullPath).UtcDateTime; - } + => FileSystem.GetCreationTime(Path.GetFullPath(path)).UtcDateTime; public static void SetLastAccessTime(string path, DateTime lastAccessTime) - { - string fullPath = Path.GetFullPath(path); - FileSystem.SetLastAccessTime(fullPath, lastAccessTime, asDirectory: false); - } + => FileSystem.SetLastAccessTime(Path.GetFullPath(path), lastAccessTime, asDirectory: false); public static void SetLastAccessTimeUtc(string path, DateTime lastAccessTimeUtc) - { - string fullPath = Path.GetFullPath(path); - FileSystem.SetLastAccessTime(fullPath, GetUtcDateTimeOffset(lastAccessTimeUtc), asDirectory: false); - } + => FileSystem.SetLastAccessTime(Path.GetFullPath(path), GetUtcDateTimeOffset(lastAccessTimeUtc), asDirectory: false); public static DateTime GetLastAccessTime(string path) - { - string fullPath = Path.GetFullPath(path); - return FileSystem.GetLastAccessTime(fullPath).LocalDateTime; - } + => FileSystem.GetLastAccessTime(Path.GetFullPath(path)).LocalDateTime; public static DateTime GetLastAccessTimeUtc(string path) - { - string fullPath = Path.GetFullPath(path); - return FileSystem.GetLastAccessTime(fullPath).UtcDateTime; - } + => FileSystem.GetLastAccessTime(Path.GetFullPath(path)).UtcDateTime; public static void SetLastWriteTime(string path, DateTime lastWriteTime) - { - string fullPath = Path.GetFullPath(path); - FileSystem.SetLastWriteTime(fullPath, lastWriteTime, asDirectory: false); - } + => FileSystem.SetLastWriteTime(Path.GetFullPath(path), lastWriteTime, asDirectory: false); public static void SetLastWriteTimeUtc(string path, DateTime lastWriteTimeUtc) - { - string fullPath = Path.GetFullPath(path); - FileSystem.SetLastWriteTime(fullPath, GetUtcDateTimeOffset(lastWriteTimeUtc), asDirectory: false); - } + => FileSystem.SetLastWriteTime(Path.GetFullPath(path), GetUtcDateTimeOffset(lastWriteTimeUtc), asDirectory: false); public static DateTime GetLastWriteTime(string path) - { - string fullPath = Path.GetFullPath(path); - return FileSystem.GetLastWriteTime(fullPath).LocalDateTime; - } + => FileSystem.GetLastWriteTime(Path.GetFullPath(path)).LocalDateTime; public static DateTime GetLastWriteTimeUtc(string path) - { - string fullPath = Path.GetFullPath(path); - return FileSystem.GetLastWriteTime(fullPath).UtcDateTime; - } + => FileSystem.GetLastWriteTime(Path.GetFullPath(path)).UtcDateTime; public static FileAttributes GetAttributes(string path) - { - string fullPath = Path.GetFullPath(path); - return FileSystem.GetAttributes(fullPath); - } + => FileSystem.GetAttributes(Path.GetFullPath(path)); public static void SetAttributes(string path, FileAttributes fileAttributes) - { - string fullPath = Path.GetFullPath(path); - FileSystem.SetAttributes(fullPath, fileAttributes); - } + => FileSystem.SetAttributes(Path.GetFullPath(path), fileAttributes); public static FileStream OpenRead(string path) - { - return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - } + => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); public static FileStream OpenWrite(string path) - { - return new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); - } + => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); public static string ReadAllText(string path) => ReadAllText(path, Encoding.UTF8); @@ -373,17 +296,7 @@ public static void WriteAllLines(string path, string[] contents) WriteAllLines(path, (IEnumerable)contents); } - public static void WriteAllLines(string path, IEnumerable contents) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - if (contents == null) - throw new ArgumentNullException(nameof(contents)); - if (path.Length == 0) - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - - InternalWriteAllLines(new StreamWriter(path), contents); - } + public static void WriteAllLines(string path, IEnumerable contents) => WriteAllLines(path, contents, UTF8NoBOM); public static void WriteAllLines(string path, string[] contents, Encoding encoding) => WriteAllLines(path, (IEnumerable)contents, encoding); @@ -434,9 +347,7 @@ public static void AppendAllLines(string path, IEnumerable contents, Enc } public static void Replace(string sourceFileName, string destinationFileName, string? destinationBackupFileName) - { - Replace(sourceFileName, destinationFileName, destinationBackupFileName, ignoreMetadataErrors: false); - } + => Replace(sourceFileName, destinationFileName, destinationBackupFileName, ignoreMetadataErrors: false); public static void Replace(string sourceFileName, string destinationFileName, string? destinationBackupFileName, bool ignoreMetadataErrors) { @@ -461,9 +372,7 @@ public static void Replace(string sourceFileName, string destinationFileName, st // permissions to destFileName. // public static void Move(string sourceFileName, string destFileName) - { - Move(sourceFileName, destFileName, false); - } + => Move(sourceFileName, destFileName, false); public static void Move(string sourceFileName, string destFileName, bool overwrite) { @@ -489,38 +398,23 @@ public static void Move(string sourceFileName, string destFileName, bool overwri [SupportedOSPlatform("windows")] public static void Encrypt(string path) - { - FileSystem.Encrypt(path ?? throw new ArgumentNullException(nameof(path))); - } + => FileSystem.Encrypt(path ?? throw new ArgumentNullException(nameof(path))); [SupportedOSPlatform("windows")] public static void Decrypt(string path) - { - FileSystem.Decrypt(path ?? throw new ArgumentNullException(nameof(path))); - } - - // UTF-8 without BOM and with error detection. Same as the default encoding for StreamWriter. - private static Encoding UTF8NoBOM => s_UTF8NoBOM ?? (s_UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)); + => FileSystem.Decrypt(path ?? throw new ArgumentNullException(nameof(path))); // If we use the path-taking constructors we will not have FileOptions.Asynchronous set and // we will have asynchronous file access faked by the thread pool. We want the real thing. private static StreamReader AsyncStreamReader(string path, Encoding encoding) - { - FileStream stream = new FileStream( - path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultBufferSize, - FileOptions.Asynchronous | FileOptions.SequentialScan); - - return new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true); - } + => new StreamReader( + new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan), + encoding, detectEncodingFromByteOrderMarks: true); private static StreamWriter AsyncStreamWriter(string path, Encoding encoding, bool append) - { - FileStream stream = new FileStream( - path, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, DefaultBufferSize, - FileOptions.Asynchronous | FileOptions.SequentialScan); - - return new StreamWriter(stream, encoding); - } + => new StreamWriter( + new FileStream(path, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, DefaultBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan), + encoding); public static Task ReadAllTextAsync(string path, CancellationToken cancellationToken = default(CancellationToken)) => ReadAllTextAsync(path, Encoding.UTF8, cancellationToken); From 9b81560ead34181d868765634dce79ef96e0914b Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 27 Aug 2021 07:43:00 +0200 Subject: [PATCH 04/16] ensure there are no trailing zeroes when EOF < preallocationSize on Unix --- .../src/System/IO/File.netcoreapp.cs | 13 ++++++++++--- .../src/System/IO/Strategies/FileStreamHelpers.cs | 13 +++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs index f001c5180af62a..263ea33dcdf0ca 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.IO.Strategies; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -58,7 +59,7 @@ public static partial class File public static SafeFileHandle OpenHandle(string path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read, FileShare share = FileShare.Read, FileOptions options = FileOptions.None, long preallocationSize = 0) { - Strategies.FileStreamHelpers.ValidateArguments(path, mode, access, share, bufferSize: 0, options, preallocationSize); + FileStreamHelpers.ValidateArguments(path, mode, access, share, bufferSize: 0, options, preallocationSize); return SafeFileHandle.Open(Path.GetFullPath(path), mode, access, share, options, preallocationSize); } @@ -113,7 +114,8 @@ private static void WriteToFile(string path, bool append, string? contents, Enco int preambleSize = encoding.Preamble.Length; int maxFileSize = string.IsNullOrEmpty(contents) ? 0 : preambleSize + encoding.GetMaxByteCount(contents.Length); long preallocationSize = maxFileSize < ChunkSize ? 0 : maxFileSize; // for small files setting preallocationSize has no perf benefit, as it requires an additional sys-call - using SafeFileHandle fileHandle = OpenHandle(path, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, FileOptions.None, preallocationSize); + FileMode mode = append ? FileMode.Append : FileMode.Create; + using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.None, preallocationSize); if (string.IsNullOrEmpty(contents)) { return; // even if the content is empty, we want to create an empty file @@ -150,6 +152,8 @@ private static void WriteToFile(string path, bool append, string? contents, Enco { ArrayPool.Shared.Return(rentedBytes); } + + FileStreamHelpers.EnsureNoTrailingZeros(preallocationSize, mode, fileHandle, fileOffset); } } @@ -158,7 +162,8 @@ private static async Task WriteToFileAsync(string path, bool append, string? con int preambleSize = encoding.Preamble.Length; int maxFileSize = string.IsNullOrEmpty(contents) ? 0 : preambleSize + encoding.GetMaxByteCount(contents.Length); long preallocationSize = maxFileSize < ChunkSize ? 0 : maxFileSize; // for small files setting preallocationSize has no perf benefit, as it requires an additional sys-call - using SafeFileHandle fileHandle = OpenHandle(path, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous, preallocationSize); + FileMode mode = append ? FileMode.Append : FileMode.Create; + using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous, preallocationSize); if (string.IsNullOrEmpty(contents)) { return; // even if the content is empty, we want to create an empty file @@ -190,6 +195,8 @@ private static async Task WriteToFileAsync(string path, bool append, string? con finally { ArrayPool.Shared.Return(bytes); + + FileStreamHelpers.EnsureNoTrailingZeros(preallocationSize, mode, fileHandle, fileOffset); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs index d86c70a621ca1a..2f947eaa67994e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs @@ -63,6 +63,19 @@ internal static bool ShouldPreallocate(long preallocationSize, FileAccess access && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append; + internal static void EnsureNoTrailingZeros(long preallocationSize, FileMode mode, SafeFileHandle fileHandle, long endOfFile) + { + // When the actual EOF is less than initial preallocationSize: + // * Windows does not use more space than needed, + // * Unix zeroes everything between EOF and preallocationSize. + // That is why for non-Windows OSes where preallocationSize was used, we shrink the file + // to ensure there are no trailing zeroes. + if (!OperatingSystem.IsWindows() && ShouldPreallocate(preallocationSize, FileAccess.Write, mode) && fileHandle.CanSeek) + { + SetFileLength(fileHandle, endOfFile); + } + } + internal static void ValidateArguments(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { if (path == null) From 878c4fa505fd886120347b7c692cd540649522c8 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 27 Aug 2021 10:34:05 +0200 Subject: [PATCH 05/16] ensure that all code paths are covered by tests --- .../System.IO.FileSystem/tests/File/Append.cs | 38 +++--------- .../tests/File/AppendAsync.cs | 26 ++------ .../tests/File/ReadWriteAllLines.cs | 25 ++++++-- .../tests/File/ReadWriteAllLinesAsync.cs | 26 ++++++-- .../tests/File/ReadWriteAllText.cs | 60 +++++++++++++++++-- .../tests/File/ReadWriteAllTextAsync.cs | 46 ++++++++++++-- .../tests/FileInfo/AppendText.cs | 13 +--- .../src/System/IO/File.netcoreapp.cs | 7 +-- 8 files changed, 152 insertions(+), 89 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/File/Append.cs b/src/libraries/System.IO.FileSystem/tests/File/Append.cs index 8cb28b7ca38acf..390bdf63ff6bf7 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/Append.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/Append.cs @@ -8,41 +8,28 @@ namespace System.IO.Tests { public class File_AppendText : File_ReadWriteAllText { + protected override bool IsAppend => true; + protected override void Write(string path, string content) { var writer = File.AppendText(path); writer.Write(content); writer.Dispose(); } - - [Fact] - public override void Overwrite() - { - string path = GetTestFilePath(); - string lines = new string('c', 200); - string appendLines = new string('b', 100); - Write(path, lines); - Write(path, appendLines); - Assert.Equal(lines + appendLines, Read(path)); - } } public class File_AppendAllText : File_ReadWriteAllText { + protected override bool IsAppend => true; + protected override void Write(string path, string content) { File.AppendAllText(path, content); } - [Fact] - public override void Overwrite() + protected override void Write(string path, string content, Encoding encoding) { - string path = GetTestFilePath(); - string lines = new string('c', 200); - string appendLines = new string('b', 100); - Write(path, lines); - Write(path, appendLines); - Assert.Equal(lines + appendLines, Read(path)); + File.AppendAllText(path, content, encoding); } } @@ -62,21 +49,12 @@ public void NullEncoding() public class File_AppendAllLines : File_ReadWriteAllLines_Enumerable { + protected override bool IsAppend => true; + protected override void Write(string path, string[] content) { File.AppendAllLines(path, content); } - - [Fact] - public override void Overwrite() - { - string path = GetTestFilePath(); - string[] lines = new string[] { new string('c', 200) }; - string[] appendLines = new string[] { new string('b', 100) }; - Write(path, lines); - Write(path, appendLines); - Assert.Equal(new string[] { lines[0], appendLines[0] }, Read(path)); - } } public class File_AppendAllLines_Encoded : File_AppendAllLines diff --git a/src/libraries/System.IO.FileSystem/tests/File/AppendAsync.cs b/src/libraries/System.IO.FileSystem/tests/File/AppendAsync.cs index 457366266d69cf..dbe1d5197fcdfb 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/AppendAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/AppendAsync.cs @@ -10,18 +10,11 @@ namespace System.IO.Tests { public class File_AppendAllTextAsync : File_ReadWriteAllTextAsync { + protected override bool IsAppend => true; + protected override Task WriteAsync(string path, string content) => File.AppendAllTextAsync(path, content); - [Fact] - public override async Task OverwriteAsync() - { - string path = GetTestFilePath(); - string lines = new string('c', 200); - string appendLines = new string('b', 100); - await WriteAsync(path, lines); - await WriteAsync(path, appendLines); - Assert.Equal(lines + appendLines, await ReadAsync(path)); - } + protected override Task WriteAsync(string path, string content, Encoding encoding) => File.AppendAllTextAsync(path, content, encoding); [Fact] public override Task TaskAlreadyCanceledAsync() @@ -60,18 +53,9 @@ public override Task TaskAlreadyCanceledAsync() public class File_AppendAllLinesAsync : File_ReadWriteAllLines_EnumerableAsync { - protected override Task WriteAsync(string path, string[] content) => File.AppendAllLinesAsync(path, content); + protected override bool IsAppend => true; - [Fact] - public override async Task OverwriteAsync() - { - string path = GetTestFilePath(); - string[] lines = new string[] { new string('c', 200) }; - string[] appendLines = new string[] { new string('b', 100) }; - await WriteAsync(path, lines); - await WriteAsync(path, appendLines); - Assert.Equal(new string[] { lines[0], appendLines[0] }, await ReadAsync(path)); - } + protected override Task WriteAsync(string path, string[] content) => File.AppendAllLinesAsync(path, content); [Fact] public override Task TaskAlreadyCanceledAsync() diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLines.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLines.cs index 99c94b450790fb..99de941c391757 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLines.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLines.cs @@ -12,6 +12,8 @@ public class File_ReadWriteAllLines_Enumerable : FileSystemTest { #region Utilities + protected virtual bool IsAppend { get; } + protected virtual void Write(string path, string[] content) { File.WriteAllLines(path, (IEnumerable)content); @@ -66,15 +68,28 @@ public void ValidWrite(int size) Assert.Equal(lines, Read(path)); } - [Fact] - public virtual void Overwrite() + [Theory] + [InlineData(200, 100)] + [InlineData(50_000, 40_000)] // tests a different code path than the line above + public void AppendOrOverwrite(int linesSizeLength, int overwriteLinesLength) { string path = GetTestFilePath(); - string[] lines = new string[] { new string('c', 200) }; - string[] overwriteLines = new string[] { new string('b', 100) }; + string[] lines = new string[] { new string('c', linesSizeLength) }; + string[] overwriteLines = new string[] { new string('b', overwriteLinesLength) }; + Write(path, lines); Write(path, overwriteLines); - Assert.Equal(overwriteLines, Read(path)); + + if (IsAppend) + { + Assert.Equal(new string[] { lines[0], overwriteLines[0] }, Read(path)); + } + else + { + Assert.DoesNotContain("Append", GetType().Name); // ensure that all "Append" types override this property + + Assert.Equal(overwriteLines, Read(path)); + } } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs index b2a1288639db31..3114e649e6635c 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs @@ -15,6 +15,8 @@ public class File_ReadWriteAllLines_EnumerableAsync : FileSystemTest { #region Utilities + protected virtual bool IsAppend { get; } + protected virtual Task WriteAsync(string path, string[] content) => File.WriteAllLinesAsync(path, content); @@ -64,15 +66,29 @@ public async Task ValidWriteAsync(int size) Assert.Equal(lines, await ReadAsync(path)); } - [Fact] - public virtual async Task OverwriteAsync() + + [Theory] + [InlineData(200, 100)] + [InlineData(50_000, 40_000)] // tests a different code path than the line above + public async Task AppendOrOverwrite(int linesSizeLength, int overwriteLinesLength) { string path = GetTestFilePath(); - string[] lines = { new string('c', 200) }; - string[] overwriteLines = { new string('b', 100) }; + string[] lines = new string[] { new string('c', linesSizeLength) }; + string[] overwriteLines = new string[] { new string('b', overwriteLinesLength) }; + await WriteAsync(path, lines); await WriteAsync(path, overwriteLines); - Assert.Equal(overwriteLines, await ReadAsync(path)); + + if (IsAppend) + { + Assert.Equal(new string[] { lines[0], overwriteLines[0] }, await ReadAsync(path)); + } + else + { + Assert.DoesNotContain("Append", GetType().Name); // ensure that all "Append" types override this property + + Assert.Equal(overwriteLines, await ReadAsync(path)); + } } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs index 1dbe3038496580..411d53c26cdd46 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Text; using Xunit; @@ -10,11 +11,18 @@ public class File_ReadWriteAllText : FileSystemTest { #region Utilities + protected virtual bool IsAppend { get; } + protected virtual void Write(string path, string content) { File.WriteAllText(path, content); } + protected virtual void Write(string path, string content, Encoding encoding) + { + File.WriteAllText(path, content, encoding); + } + protected virtual string Read(string path) { return File.ReadAllText(path); @@ -73,15 +81,28 @@ public void ValidWrite(int size) Assert.Equal(toWrite, Read(path)); } - [Fact] - public virtual void Overwrite() + [Theory] + [InlineData(200, 100)] + [InlineData(50_000, 40_000)] // tests a different code path than the line above + public void AppendOrOverwrite(int linesSizeLength, int overwriteLinesLength) { string path = GetTestFilePath(); - string lines = new string('c', 200); - string overwriteLines = new string('b', 100); + string lines = new string('c', linesSizeLength); + string overwriteLines = new string('b', overwriteLinesLength); + Write(path, lines); Write(path, overwriteLines); - Assert.Equal(overwriteLines, Read(path)); + + if (IsAppend) + { + Assert.Equal(lines + overwriteLines, Read(path)); + } + else + { + Assert.DoesNotContain("Append", GetType().Name); // ensure that all "Append" types override this property + + Assert.Equal(overwriteLines, Read(path)); + } } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] @@ -133,6 +154,35 @@ public void WriteToReadOnlyFile() } } + public static IEnumerable OutputIsTheSameAsForStreamWriter_Args() + { + string longText = new string('z', 50_000); + foreach (Encoding encoding in new[] { Encoding.Unicode , new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false) }) + { + foreach (string text in new[] { null, string.Empty, " ", "shortText", longText }) + { + yield return new object[] { text, encoding }; + } + } + } + + [Theory] + [MemberData(nameof(OutputIsTheSameAsForStreamWriter_Args))] + public void OutputIsTheSameAsForStreamWriter(string content, Encoding encoding) + { + string filePath = GetTestFilePath(); + Write(filePath, content, encoding); // it uses System.File.IO APIs + + string swPath = GetTestFilePath(); + using (StreamWriter sw = new StreamWriter(swPath, IsAppend, encoding)) + { + sw.Write(content); + } + + Assert.Equal(File.ReadAllText(swPath, encoding), File.ReadAllText(filePath, encoding)); + Assert.Equal(File.ReadAllBytes(swPath), File.ReadAllBytes(filePath)); // ensure Preamble was stored + } + #endregion } diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs index 3ad76c272fff6d..a92813c61f6bfe 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs @@ -12,10 +12,14 @@ namespace System.IO.Tests [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class File_ReadWriteAllTextAsync : FileSystemTest { + protected virtual bool IsAppend { get; } + #region Utilities protected virtual Task WriteAsync(string path, string content) => File.WriteAllTextAsync(path, content); + protected virtual Task WriteAsync(string path, string content, Encoding encoding) => File.WriteAllTextAsync(path, content, encoding); + protected virtual Task ReadAsync(string path) => File.ReadAllTextAsync(path); #endregion @@ -72,15 +76,28 @@ public async Task ValidWriteAsync(int size) Assert.Equal(toWrite, await ReadAsync(path)); } - [Fact] - public virtual async Task OverwriteAsync() + [Theory] + [InlineData(200, 100)] + [InlineData(50_000, 40_000)] // tests a different code path than the line above + public async Task AppendOrOverwriteAsync(int linesSizeLength, int overwriteLinesLength) { string path = GetTestFilePath(); - string lines = new string('c', 200); - string overwriteLines = new string('b', 100); + string lines = new string('c', linesSizeLength); + string overwriteLines = new string('b', overwriteLinesLength); + await WriteAsync(path, lines); - await WriteAsync(path, overwriteLines); - Assert.Equal(overwriteLines, await ReadAsync(path)); + await WriteAsync(path, overwriteLines); ; + + if (IsAppend) + { + Assert.Equal(lines + overwriteLines, await ReadAsync(path)); + } + else + { + Assert.DoesNotContain("Append", GetType().Name); // ensure that all "Append" types override this property + + Assert.Equal(overwriteLines, await ReadAsync(path)); + } } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] @@ -141,6 +158,23 @@ public virtual Task TaskAlreadyCanceledAsync() async () => await File.WriteAllTextAsync(path, "", token)); } + [Theory] + [MemberData(nameof(File_ReadWriteAllText.OutputIsTheSameAsForStreamWriter_Args), MemberType = typeof(File_ReadWriteAllText))] + public async Task OutputIsTheSameAsForStreamWriter(string content, Encoding encoding) + { + string filePath = GetTestFilePath(); + await WriteAsync(filePath, content, encoding); // it uses System.File.IO APIs + + string swPath = GetTestFilePath(); + using (StreamWriter sw = new StreamWriter(swPath, IsAppend, encoding)) + { + await sw.WriteAsync(content); + } + + Assert.Equal(await File.ReadAllTextAsync(swPath, encoding), await File.ReadAllTextAsync(filePath, encoding)); + Assert.Equal(await File.ReadAllBytesAsync(swPath), await File.ReadAllBytesAsync(filePath)); // ensure Preamble was stored + } + #endregion } diff --git a/src/libraries/System.IO.FileSystem/tests/FileInfo/AppendText.cs b/src/libraries/System.IO.FileSystem/tests/FileInfo/AppendText.cs index eb86294b119742..3bbf9838a68664 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileInfo/AppendText.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileInfo/AppendText.cs @@ -7,22 +7,13 @@ namespace System.IO.Tests { public class FileInfo_AppendText : File_ReadWriteAllText { + protected override bool IsAppend => true; + protected override void Write(string path, string content) { var writer = new FileInfo(path).AppendText(); writer.Write(content); writer.Dispose(); } - - [Fact] - public override void Overwrite() - { - string path = GetTestFilePath(); - string lines = new string('c', 200); - string appendline = new string('b', 100); - Write(path, lines); - Write(path, appendline); - Assert.Equal(lines + appendline, Read(path)); - } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs index 263ea33dcdf0ca..8c0dd75cd48bce 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs @@ -13,12 +13,7 @@ namespace System.IO { public static partial class File { - private const int ChunkSize = -#if DEBUG - 100; -#else - 8192; -#endif + private const int ChunkSize = 8192; /// /// Initializes a new instance of the class with the specified path, creation mode, read/write and sharing permission, the access other FileStreams can have to the same file, the buffer size, additional file options and the allocation size. From 328cdafed928c65c5119dd1de019475982ac36a3 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 27 Aug 2021 15:23:19 +0200 Subject: [PATCH 06/16] fix bug discovered by new tests: when the content is empty, we still need to store the preamble --- .../src/System/IO/File.netcoreapp.cs | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs index 8c0dd75cd48bce..85cd9f790afcdc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs @@ -106,25 +106,31 @@ private static byte[] ReadAllBytesUnknownLength(FileStream fs) private static void WriteToFile(string path, bool append, string? contents, Encoding encoding) { - int preambleSize = encoding.Preamble.Length; - int maxFileSize = string.IsNullOrEmpty(contents) ? 0 : preambleSize + encoding.GetMaxByteCount(contents.Length); + ReadOnlySpan preamble = encoding.GetPreamble(); + int preambleSize = preamble.Length; + int maxFileSize = preambleSize + (string.IsNullOrEmpty(contents) ? 0 : encoding.GetMaxByteCount(contents.Length)); long preallocationSize = maxFileSize < ChunkSize ? 0 : maxFileSize; // for small files setting preallocationSize has no perf benefit, as it requires an additional sys-call FileMode mode = append ? FileMode.Append : FileMode.Create; + using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.None, preallocationSize); + long fileOffset = append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0; + if (string.IsNullOrEmpty(contents)) { - return; // even if the content is empty, we want to create an empty file + if (preambleSize > 0) // even if the content is empty, we want to store the preamble + { + RandomAccess.WriteAtOffset(fileHandle, preamble, fileOffset); + } + return; } - long fileOffset = append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0; - int bytesNeeded = Math.Min(maxFileSize, preambleSize + encoding.GetMaxByteCount(ChunkSize)); byte[]? rentedBytes = null; Span bytes = bytesNeeded <= 1024 ? stackalloc byte[1024] : (rentedBytes = ArrayPool.Shared.Rent(bytesNeeded)); try { - encoding.Preamble.CopyTo(bytes); + preamble.CopyTo(bytes); Encoder encoder = encoding.GetEncoder(); ReadOnlySpan remaining = contents; @@ -154,23 +160,29 @@ private static void WriteToFile(string path, bool append, string? contents, Enco private static async Task WriteToFileAsync(string path, bool append, string? contents, Encoding encoding, CancellationToken cancellationToken) { - int preambleSize = encoding.Preamble.Length; - int maxFileSize = string.IsNullOrEmpty(contents) ? 0 : preambleSize + encoding.GetMaxByteCount(contents.Length); + ReadOnlyMemory preamble = encoding.GetPreamble(); + int preambleSize = preamble.Length; + int maxFileSize = preambleSize + (string.IsNullOrEmpty(contents) ? 0 : encoding.GetMaxByteCount(contents.Length)); long preallocationSize = maxFileSize < ChunkSize ? 0 : maxFileSize; // for small files setting preallocationSize has no perf benefit, as it requires an additional sys-call FileMode mode = append ? FileMode.Append : FileMode.Create; + using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous, preallocationSize); + long fileOffset = append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0; + if (string.IsNullOrEmpty(contents)) { - return; // even if the content is empty, we want to create an empty file + if (preambleSize > 0) // even if the content is empty, we want to store the preamble + { + await RandomAccess.WriteAtOffsetAsync(fileHandle, preamble, fileOffset, cancellationToken).ConfigureAwait(false); + } + return; } - long fileOffset = append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0; - byte[] bytes = ArrayPool.Shared.Rent(Math.Min(maxFileSize, preambleSize + encoding.GetMaxByteCount(ChunkSize))); try { - encoding.Preamble.CopyTo(bytes); + preamble.CopyTo(bytes); Encoder encoder = encoding.GetEncoder(); ReadOnlyMemory remaining = contents.AsMemory(); From 8da83ee2de60cfd8926ca6241c7a6892209607f7 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 27 Aug 2021 15:43:51 +0200 Subject: [PATCH 07/16] flush the encoder on last loop iteration --- .../src/System/IO/File.netcoreapp.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs index 85cd9f790afcdc..57d0c7f27ab705 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs @@ -137,13 +137,13 @@ private static void WriteToFile(string path, bool append, string? contents, Enco while (!remaining.IsEmpty) { ReadOnlySpan toEncode = remaining.Slice(0, Math.Min(remaining.Length, ChunkSize)); - int encoded = encoder.GetBytes(toEncode, bytes.Slice(preambleSize), flush: false); + remaining = remaining.Slice(toEncode.Length); + int encoded = encoder.GetBytes(toEncode, bytes.Slice(preambleSize), flush: remaining.IsEmpty); Span toStore = bytes.Slice(0, preambleSize + encoded); RandomAccess.WriteAtOffset(fileHandle, toStore, fileOffset); fileOffset += toStore.Length; - remaining = remaining.Slice(toEncode.Length); preambleSize = 0; } } @@ -189,13 +189,13 @@ private static async Task WriteToFileAsync(string path, bool append, string? con while (!remaining.IsEmpty) { ReadOnlyMemory toEncode = remaining.Slice(0, Math.Min(remaining.Length, ChunkSize)); - int encoded = encoder.GetBytes(toEncode.Span, bytes.AsSpan(preambleSize), flush: false); + remaining = remaining.Slice(toEncode.Length); + int encoded = encoder.GetBytes(toEncode.Span, bytes.AsSpan(preambleSize), flush: remaining.IsEmpty); ReadOnlyMemory toStore = new ReadOnlyMemory(bytes, 0, preambleSize + encoded); await RandomAccess.WriteAtOffsetAsync(fileHandle, toStore, fileOffset, cancellationToken).ConfigureAwait(false); fileOffset += toStore.Length; - remaining = remaining.Slice(toEncode.Length); preambleSize = 0; } } From a992375233c2363d05b199721ff079f5f5d68aca Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 27 Aug 2021 15:51:08 +0200 Subject: [PATCH 08/16] for a single write operation setting preallocationSize has no perf benefit, as it requires additional sys-call(s) --- .../System.Private.CoreLib/src/System/IO/File.netcoreapp.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs index 57d0c7f27ab705..bdde8739d1cf8d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs @@ -109,7 +109,7 @@ private static void WriteToFile(string path, bool append, string? contents, Enco ReadOnlySpan preamble = encoding.GetPreamble(); int preambleSize = preamble.Length; int maxFileSize = preambleSize + (string.IsNullOrEmpty(contents) ? 0 : encoding.GetMaxByteCount(contents.Length)); - long preallocationSize = maxFileSize < ChunkSize ? 0 : maxFileSize; // for small files setting preallocationSize has no perf benefit, as it requires an additional sys-call + long preallocationSize = contents is null || contents.Length < ChunkSize ? 0 : maxFileSize; // for a single write operation setting preallocationSize has no perf benefit, as it requires additional sys-call(s) FileMode mode = append ? FileMode.Append : FileMode.Create; using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.None, preallocationSize); @@ -163,7 +163,7 @@ private static async Task WriteToFileAsync(string path, bool append, string? con ReadOnlyMemory preamble = encoding.GetPreamble(); int preambleSize = preamble.Length; int maxFileSize = preambleSize + (string.IsNullOrEmpty(contents) ? 0 : encoding.GetMaxByteCount(contents.Length)); - long preallocationSize = maxFileSize < ChunkSize ? 0 : maxFileSize; // for small files setting preallocationSize has no perf benefit, as it requires an additional sys-call + long preallocationSize = contents is null || contents.Length < ChunkSize ? 0 : maxFileSize; // for a single write operation setting preallocationSize has no perf benefit, as it requires additional sys-call(s) FileMode mode = append ? FileMode.Append : FileMode.Create; using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous, preallocationSize); From eb8f1bfd82cfc56673bdb7273ddbbf08adb68272 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 27 Aug 2021 18:16:30 +0200 Subject: [PATCH 09/16] Apply suggestions from code review Co-authored-by: Stephen Toub --- src/libraries/System.Private.CoreLib/src/System/IO/File.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index 49e4f25445fef2..3d4a63dfa00b2c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -30,7 +30,7 @@ public static partial class File private static Encoding? s_UTF8NoBOM; // UTF-8 without BOM and with error detection. Same as the default encoding for StreamWriter. - private static Encoding UTF8NoBOM => s_UTF8NoBOM ?? (s_UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)); + private static Encoding UTF8NoBOM => s_UTF8NoBOM ??= new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal const int DefaultBufferSize = 4096; @@ -264,6 +264,7 @@ public static void WriteAllBytes(string path, byte[] bytes) RandomAccess.WriteAtOffset(sfh, bytes, 0); #endif } + public static string[] ReadAllLines(string path) => ReadAllLines(path, Encoding.UTF8); public static string[] ReadAllLines(string path, Encoding encoding) From 7eaaa482ddcca8fd708c0ad97f1967c7e37498d9 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 27 Aug 2021 18:21:44 +0200 Subject: [PATCH 10/16] style consistency --- .../src/System/IO/File.cs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index 3d4a63dfa00b2c..faeea65dd87dca 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -73,7 +73,8 @@ public static void Copy(string sourceFileName, string destFileName, bool overwri // The file is opened with ReadWrite access and cannot be opened by another // application until it has been closed. An IOException is thrown if the // directory specified doesn't exist. - public static FileStream Create(string path) => Create(path, DefaultBufferSize); + public static FileStream Create(string path) + => Create(path, DefaultBufferSize); // Creates a file in a particular path. If the file exists, it is replaced. // The file is opened with ReadWrite access and cannot be opened by another @@ -191,7 +192,8 @@ public static FileStream OpenRead(string path) public static FileStream OpenWrite(string path) => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); - public static string ReadAllText(string path) => ReadAllText(path, Encoding.UTF8); + public static string ReadAllText(string path) + => ReadAllText(path, Encoding.UTF8); public static string ReadAllText(string path, Encoding encoding) { @@ -201,7 +203,8 @@ public static string ReadAllText(string path, Encoding encoding) return sr.ReadToEnd(); } - public static void WriteAllText(string path, string? contents) => WriteAllText(path, contents, UTF8NoBOM); + public static void WriteAllText(string path, string? contents) + => WriteAllText(path, contents, UTF8NoBOM); public static void WriteAllText(string path, string? contents, Encoding encoding) { @@ -265,7 +268,8 @@ public static void WriteAllBytes(string path, byte[] bytes) #endif } - public static string[] ReadAllLines(string path) => ReadAllLines(path, Encoding.UTF8); + public static string[] ReadAllLines(string path) + => ReadAllLines(path, Encoding.UTF8); public static string[] ReadAllLines(string path, Encoding encoding) { @@ -283,7 +287,8 @@ public static string[] ReadAllLines(string path, Encoding encoding) return lines.ToArray(); } - public static IEnumerable ReadLines(string path) => ReadLines(path, Encoding.UTF8); + public static IEnumerable ReadLines(string path) + => ReadLines(path, Encoding.UTF8); public static IEnumerable ReadLines(string path, Encoding encoding) { @@ -297,7 +302,8 @@ public static void WriteAllLines(string path, string[] contents) WriteAllLines(path, (IEnumerable)contents); } - public static void WriteAllLines(string path, IEnumerable contents) => WriteAllLines(path, contents, UTF8NoBOM); + public static void WriteAllLines(string path, IEnumerable contents) + => WriteAllLines(path, contents, UTF8NoBOM); public static void WriteAllLines(string path, string[] contents, Encoding encoding) => WriteAllLines(path, (IEnumerable)contents, encoding); @@ -326,7 +332,8 @@ private static void InternalWriteAllLines(TextWriter writer, IEnumerable } } - public static void AppendAllText(string path, string? contents) => AppendAllText(path, contents, UTF8NoBOM); + public static void AppendAllText(string path, string? contents) + => AppendAllText(path, contents, UTF8NoBOM); public static void AppendAllText(string path, string? contents, Encoding encoding) { @@ -335,7 +342,8 @@ public static void AppendAllText(string path, string? contents, Encoding encodin WriteToFile(path, append: true, contents, encoding); } - public static void AppendAllLines(string path, IEnumerable contents) => AppendAllLines(path, contents, UTF8NoBOM); + public static void AppendAllLines(string path, IEnumerable contents) + => AppendAllLines(path, contents, UTF8NoBOM); public static void AppendAllLines(string path, IEnumerable contents, Encoding encoding) { @@ -762,10 +770,8 @@ private static void Validate(string path, Encoding encoding) #if MS_IO_REDIST private static void WriteToFile(string path, bool append, string? contents, Encoding encoding) { - using (StreamWriter sw = new StreamWriter(path, append, encoding)) - { - sw.Write(contents); - } + using StreamWriter sw = new StreamWriter(path, append, encoding); + sw.Write(contents); } private static async Task WriteToFileAsync(string path, bool append, string? contents, Encoding encoding, CancellationToken cancellationToken) From 82190c506fb6a05167c85c3ef8e84ef98fe2be0a Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 30 Aug 2021 10:09:45 +0200 Subject: [PATCH 11/16] address code review feedback --- .../src/System/IO/File.cs | 18 +++--- .../src/System/IO/File.netcoreapp.cs | 55 +++++++++++++------ .../System/IO/Strategies/FileStreamHelpers.cs | 13 ----- 3 files changed, 47 insertions(+), 39 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index faeea65dd87dca..3b419765cdbc81 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -210,7 +210,7 @@ public static void WriteAllText(string path, string? contents, Encoding encoding { Validate(path, encoding); - WriteToFile(path, append: false, contents, encoding); + WriteToFile(path, FileMode.Create, contents, encoding); } public static byte[] ReadAllBytes(string path) @@ -339,7 +339,7 @@ public static void AppendAllText(string path, string? contents, Encoding encodin { Validate(path, encoding); - WriteToFile(path, append: true, contents, encoding); + WriteToFile(path, FileMode.Append, contents, encoding); } public static void AppendAllLines(string path, IEnumerable contents) @@ -486,7 +486,7 @@ private static async Task InternalReadAllTextAsync(string path, Encoding return Task.FromCanceled(cancellationToken); } - return WriteToFileAsync(path, append: false, contents, encoding, cancellationToken); + return WriteToFileAsync(path, FileMode.Create, contents, encoding, cancellationToken); } public static Task ReadAllBytesAsync(string path, CancellationToken cancellationToken = default(CancellationToken)) @@ -699,7 +699,7 @@ private static async Task InternalWriteAllLinesAsync(TextWriter writer, IEnumera return Task.FromCanceled(cancellationToken); } - return WriteToFileAsync(path, append: true, contents, encoding, cancellationToken); + return WriteToFileAsync(path, FileMode.Append, contents, encoding, cancellationToken); } public static Task AppendAllLinesAsync(string path, IEnumerable contents, CancellationToken cancellationToken = default(CancellationToken)) @@ -768,21 +768,21 @@ private static void Validate(string path, Encoding encoding) } #if MS_IO_REDIST - private static void WriteToFile(string path, bool append, string? contents, Encoding encoding) + private static void WriteToFile(string path, FileMode mode, string? contents, Encoding encoding) { - using StreamWriter sw = new StreamWriter(path, append, encoding); + using StreamWriter sw = new StreamWriter(path, mode == FileMode.Append, encoding); sw.Write(contents); } - private static async Task WriteToFileAsync(string path, bool append, string? contents, Encoding encoding, CancellationToken cancellationToken) + private static async Task WriteToFileAsync(string path, FileMode mode, string? contents, Encoding encoding, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(contents)) { - new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read).Dispose(); + new FileStream(path, mode, FileAccess.Write, FileShare.Read).Dispose(); return; } - StreamWriter sw = AsyncStreamWriter(path, encoding, append); + StreamWriter sw = AsyncStreamWriter(path, encoding, mode == FileMode.Append); char[]? buffer = null; try { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs index bdde8739d1cf8d..93c19ee3a47e41 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs @@ -104,16 +104,13 @@ private static byte[] ReadAllBytesUnknownLength(FileStream fs) } } - private static void WriteToFile(string path, bool append, string? contents, Encoding encoding) + private static void WriteToFile(string path, FileMode mode, string? contents, Encoding encoding) { ReadOnlySpan preamble = encoding.GetPreamble(); int preambleSize = preamble.Length; - int maxFileSize = preambleSize + (string.IsNullOrEmpty(contents) ? 0 : encoding.GetMaxByteCount(contents.Length)); - long preallocationSize = contents is null || contents.Length < ChunkSize ? 0 : maxFileSize; // for a single write operation setting preallocationSize has no perf benefit, as it requires additional sys-call(s) - FileMode mode = append ? FileMode.Append : FileMode.Create; - using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.None, preallocationSize); - long fileOffset = append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0; + using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.None, GetPreallocationSize(mode, contents, encoding, preambleSize)); + long fileOffset = mode == FileMode.Append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0; if (string.IsNullOrEmpty(contents)) { @@ -124,7 +121,7 @@ private static void WriteToFile(string path, bool append, string? contents, Enco return; } - int bytesNeeded = Math.Min(maxFileSize, preambleSize + encoding.GetMaxByteCount(ChunkSize)); + int bytesNeeded = preambleSize + encoding.GetMaxByteCount(Math.Min(contents.Length, ChunkSize)); byte[]? rentedBytes = null; Span bytes = bytesNeeded <= 1024 ? stackalloc byte[1024] : (rentedBytes = ArrayPool.Shared.Rent(bytesNeeded)); @@ -146,6 +143,10 @@ private static void WriteToFile(string path, bool append, string? contents, Enco fileOffset += toStore.Length; preambleSize = 0; } + + long fileLength = 0; + Debug.Assert(!fileHandle.CanSeek || fileOffset == (fileLength = RandomAccess.GetFileLength(fileHandle)), + $"File length different than expected. Offset: {fileOffset}, Length: {fileLength}"); } finally { @@ -153,21 +154,16 @@ private static void WriteToFile(string path, bool append, string? contents, Enco { ArrayPool.Shared.Return(rentedBytes); } - - FileStreamHelpers.EnsureNoTrailingZeros(preallocationSize, mode, fileHandle, fileOffset); } } - private static async Task WriteToFileAsync(string path, bool append, string? contents, Encoding encoding, CancellationToken cancellationToken) + private static async Task WriteToFileAsync(string path, FileMode mode, string? contents, Encoding encoding, CancellationToken cancellationToken) { ReadOnlyMemory preamble = encoding.GetPreamble(); int preambleSize = preamble.Length; - int maxFileSize = preambleSize + (string.IsNullOrEmpty(contents) ? 0 : encoding.GetMaxByteCount(contents.Length)); - long preallocationSize = contents is null || contents.Length < ChunkSize ? 0 : maxFileSize; // for a single write operation setting preallocationSize has no perf benefit, as it requires additional sys-call(s) - FileMode mode = append ? FileMode.Append : FileMode.Create; - using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous, preallocationSize); - long fileOffset = append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0; + using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous, GetPreallocationSize(mode, contents, encoding, preambleSize)); + long fileOffset = mode == FileMode.Append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0; if (string.IsNullOrEmpty(contents)) { @@ -178,7 +174,7 @@ private static async Task WriteToFileAsync(string path, bool append, string? con return; } - byte[] bytes = ArrayPool.Shared.Rent(Math.Min(maxFileSize, preambleSize + encoding.GetMaxByteCount(ChunkSize))); + byte[] bytes = ArrayPool.Shared.Rent(preambleSize + encoding.GetMaxByteCount(Math.Min(contents.Length, ChunkSize))); try { @@ -198,13 +194,38 @@ private static async Task WriteToFileAsync(string path, bool append, string? con fileOffset += toStore.Length; preambleSize = 0; } + + long fileLength = 0; + Debug.Assert(!fileHandle.CanSeek || fileOffset == (fileLength = RandomAccess.GetFileLength(fileHandle)), + $"File length different than expected. Offset: {fileOffset}, Length: {fileLength}"); } finally { ArrayPool.Shared.Return(bytes); + } + } - FileStreamHelpers.EnsureNoTrailingZeros(preallocationSize, mode, fileHandle, fileOffset); + private static long GetPreallocationSize(FileMode mode, string? contents, Encoding encoding, int preambleSize) + { + // for a single write operation, setting preallocationSize has no perf benefit, as it requires an additional sys-call + if (contents is null || contents.Length < ChunkSize) + { + return 0; } + + // preallocationSize is ignored for Append mode, there is no need to spend cycles on GetByteCount + if (mode == FileMode.Append) + { + return 0; + } + + // When the actual EOF is less than initial preallocationSize: + // * Windows does not use more space than needed, + // * Unix zeroes everything between EOF and preallocationSize. + // That is why for non-Windows OSes the accurate byte count is calculated + // while for Windows, a cheaper MaxByteCount estimation is fine. + // PreallocationSize saves more time than it takes to calculate the accurate byte count. + return preambleSize + (OperatingSystem.IsWindows() ? encoding.GetMaxByteCount(contents.Length) : encoding.GetByteCount(contents)); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs index 2f947eaa67994e..d86c70a621ca1a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs @@ -63,19 +63,6 @@ internal static bool ShouldPreallocate(long preallocationSize, FileAccess access && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append; - internal static void EnsureNoTrailingZeros(long preallocationSize, FileMode mode, SafeFileHandle fileHandle, long endOfFile) - { - // When the actual EOF is less than initial preallocationSize: - // * Windows does not use more space than needed, - // * Unix zeroes everything between EOF and preallocationSize. - // That is why for non-Windows OSes where preallocationSize was used, we shrink the file - // to ensure there are no trailing zeroes. - if (!OperatingSystem.IsWindows() && ShouldPreallocate(preallocationSize, FileAccess.Write, mode) && fileHandle.CanSeek) - { - SetFileLength(fileHandle, endOfFile); - } - } - internal static void ValidateArguments(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { if (path == null) From e0dafc540a0d6c532fa3f97f269a1b59ecd6e8ca Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 30 Aug 2021 15:59:00 +0200 Subject: [PATCH 12/16] don't write a preamble to non-empty file --- .../System.IO.FileSystem/tests/File/Append.cs | 7 ++++++ .../tests/File/ReadWriteAllText.cs | 21 ++++++++++++++++ .../tests/File/ReadWriteAllTextAsync.cs | 23 +++++++++++++++++- .../tests/FileInfo/AppendText.cs | 8 +++++++ .../src/System/IO/File.netcoreapp.cs | 24 +++++++++++++++---- 5 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/File/Append.cs b/src/libraries/System.IO.FileSystem/tests/File/Append.cs index 390bdf63ff6bf7..68160715a7d635 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/Append.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/Append.cs @@ -16,6 +16,13 @@ protected override void Write(string path, string content) writer.Write(content); writer.Dispose(); } + + protected override void Write(string path, string content, Encoding encoding) + { + var writer = new StreamWriter(path, IsAppend, encoding); + writer.Write(content); + writer.Dispose(); + } } public class File_AppendAllText : File_ReadWriteAllText diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs index 411d53c26cdd46..2036b745e5fecf 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs @@ -183,6 +183,27 @@ public void OutputIsTheSameAsForStreamWriter(string content, Encoding encoding) Assert.Equal(File.ReadAllBytes(swPath), File.ReadAllBytes(filePath)); // ensure Preamble was stored } + [Theory] + [MemberData(nameof(OutputIsTheSameAsForStreamWriter_Args))] + public void OutputIsTheSameAsForStreamWriter_Overwrite(string content, Encoding encoding) + { + string filePath = GetTestFilePath(); + string swPath = GetTestFilePath(); + + for (int i = 0; i < 2; i++) + { + Write(filePath, content, encoding); // it uses System.File.IO APIs + + using (StreamWriter sw = new StreamWriter(swPath, IsAppend, encoding)) + { + sw.Write(content); + } + } + + Assert.Equal(File.ReadAllText(swPath, encoding), File.ReadAllText(filePath, encoding)); + Assert.Equal(File.ReadAllBytes(swPath), File.ReadAllBytes(filePath)); // ensure Preamble was stored once + } + #endregion } diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs index a92813c61f6bfe..4a33f94f7b8f81 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs @@ -160,7 +160,7 @@ public virtual Task TaskAlreadyCanceledAsync() [Theory] [MemberData(nameof(File_ReadWriteAllText.OutputIsTheSameAsForStreamWriter_Args), MemberType = typeof(File_ReadWriteAllText))] - public async Task OutputIsTheSameAsForStreamWriter(string content, Encoding encoding) + public async Task OutputIsTheSameAsForStreamWriterAsync(string content, Encoding encoding) { string filePath = GetTestFilePath(); await WriteAsync(filePath, content, encoding); // it uses System.File.IO APIs @@ -175,6 +175,27 @@ public async Task OutputIsTheSameAsForStreamWriter(string content, Encoding enco Assert.Equal(await File.ReadAllBytesAsync(swPath), await File.ReadAllBytesAsync(filePath)); // ensure Preamble was stored } + [Theory] + [MemberData(nameof(File_ReadWriteAllText.OutputIsTheSameAsForStreamWriter_Args), MemberType = typeof(File_ReadWriteAllText))] + public async Task OutputIsTheSameAsForStreamWriter_OverwriteAsync(string content, Encoding encoding) + { + string filePath = GetTestFilePath(); + string swPath = GetTestFilePath(); + + for (int i = 0; i < 2; i++) + { + await WriteAsync(filePath, content, encoding); // it uses System.File.IO APIs + + using (StreamWriter sw = new StreamWriter(swPath, IsAppend, encoding)) + { + await sw.WriteAsync(content); + } + } + + Assert.Equal(await File.ReadAllTextAsync(swPath, encoding), await File.ReadAllTextAsync(filePath, encoding)); + Assert.Equal(await File.ReadAllBytesAsync(swPath), await File.ReadAllBytesAsync(filePath)); // ensure Preamble was stored once + } + #endregion } diff --git a/src/libraries/System.IO.FileSystem/tests/FileInfo/AppendText.cs b/src/libraries/System.IO.FileSystem/tests/FileInfo/AppendText.cs index 3bbf9838a68664..2a73f63ecf04f2 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileInfo/AppendText.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileInfo/AppendText.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text; using Xunit; namespace System.IO.Tests @@ -15,5 +16,12 @@ protected override void Write(string path, string content) writer.Write(content); writer.Dispose(); } + + protected override void Write(string path, string content, Encoding encoding) + { + var writer = new StreamWriter(path, IsAppend, encoding); + writer.Write(content); + writer.Dispose(); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs index 93c19ee3a47e41..ac65e73116a0db 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs @@ -114,7 +114,8 @@ private static void WriteToFile(string path, FileMode mode, string? contents, En if (string.IsNullOrEmpty(contents)) { - if (preambleSize > 0) // even if the content is empty, we want to store the preamble + if (preambleSize > 0 // even if the content is empty, we want to store the preamble + && fileOffset == 0) // if we're appending to a file that already has data, don't write the preamble. { RandomAccess.WriteAtOffset(fileHandle, preamble, fileOffset); } @@ -127,7 +128,14 @@ private static void WriteToFile(string path, FileMode mode, string? contents, En try { - preamble.CopyTo(bytes); + if (fileOffset == 0) + { + preamble.CopyTo(bytes); + } + else + { + preambleSize = 0; // don't append preamble to a non-empty file + } Encoder encoder = encoding.GetEncoder(); ReadOnlySpan remaining = contents; @@ -167,7 +175,8 @@ private static async Task WriteToFileAsync(string path, FileMode mode, string? c if (string.IsNullOrEmpty(contents)) { - if (preambleSize > 0) // even if the content is empty, we want to store the preamble + if (preambleSize > 0 // even if the content is empty, we want to store the preamble + && fileOffset == 0) // if we're appending to a file that already has data, don't write the preamble. { await RandomAccess.WriteAtOffsetAsync(fileHandle, preamble, fileOffset, cancellationToken).ConfigureAwait(false); } @@ -178,7 +187,14 @@ private static async Task WriteToFileAsync(string path, FileMode mode, string? c try { - preamble.CopyTo(bytes); + if (fileOffset == 0) + { + preamble.CopyTo(bytes); + } + else + { + preambleSize = 0; // don't append preamble to a non-empty file + } Encoder encoder = encoding.GetEncoder(); ReadOnlyMemory remaining = contents.AsMemory(); From 256e5f06c46ab3277ab00ac60cb7327e88ddaa96 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 13 Oct 2021 15:57:36 +0200 Subject: [PATCH 13/16] use exact size --- .../src/System/IO/File.netcoreapp.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs index ac65e73116a0db..c1176def897e9e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs @@ -235,13 +235,7 @@ private static long GetPreallocationSize(FileMode mode, string? contents, Encodi return 0; } - // When the actual EOF is less than initial preallocationSize: - // * Windows does not use more space than needed, - // * Unix zeroes everything between EOF and preallocationSize. - // That is why for non-Windows OSes the accurate byte count is calculated - // while for Windows, a cheaper MaxByteCount estimation is fine. - // PreallocationSize saves more time than it takes to calculate the accurate byte count. - return preambleSize + (OperatingSystem.IsWindows() ? encoding.GetMaxByteCount(contents.Length) : encoding.GetByteCount(contents)); + return preambleSize + encoding.GetByteCount(contents); } } } From dbecac2462a30ddce15309001a7c3d389f53941f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 14 Oct 2021 11:52:27 +0200 Subject: [PATCH 14/16] MS_IO_REDIST has been removed --- .../src/System/IO/File.cs | 48 +------------------ 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index 0297cc9e5bb3cb..e63935c5f0c804 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -284,9 +284,7 @@ public static IEnumerable ReadLines(string path, Encoding encoding) } public static void WriteAllLines(string path, string[] contents) - { - WriteAllLines(path, (IEnumerable)contents); - } + => WriteAllLines(path, (IEnumerable)contents); public static void WriteAllLines(string path, IEnumerable contents) => WriteAllLines(path, contents, UTF8NoBOM); @@ -733,49 +731,5 @@ private static void Validate(string path, Encoding encoding) if (path.Length == 0) throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); } - -#if MS_IO_REDIST - private static void WriteToFile(string path, FileMode mode, string? contents, Encoding encoding) - { - using StreamWriter sw = new StreamWriter(path, mode == FileMode.Append, encoding); - sw.Write(contents); - } - - private static async Task WriteToFileAsync(string path, FileMode mode, string? contents, Encoding encoding, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(contents)) - { - new FileStream(path, mode, FileAccess.Write, FileShare.Read).Dispose(); - return; - } - - StreamWriter sw = AsyncStreamWriter(path, encoding, mode == FileMode.Append); - char[]? buffer = null; - try - { - buffer = ArrayPool.Shared.Rent(DefaultBufferSize); - int count = contents!.Length; - int index = 0; - while (index < count) - { - int batchSize = Math.Min(DefaultBufferSize, count - index); - contents!.CopyTo(index, buffer, 0, batchSize); - await sw.WriteAsync(buffer, 0, batchSize).ConfigureAwait(false); - index += batchSize; - } - - cancellationToken.ThrowIfCancellationRequested(); - await sw.FlushAsync().ConfigureAwait(false); - } - finally - { - sw.Dispose(); - if (buffer != null) - { - ArrayPool.Shared.Return(buffer); - } - } - } -#endif } } From 280009a837691450e06eb4555c4f8833901fbd64 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 14 Oct 2021 12:11:11 +0200 Subject: [PATCH 15/16] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David CantĂș --- .../System.IO.FileSystem/tests/File/ReadWriteAllText.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs index 2036b745e5fecf..8c9c1e66e72a4b 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllText.cs @@ -171,7 +171,7 @@ public static IEnumerable OutputIsTheSameAsForStreamWriter_Args() public void OutputIsTheSameAsForStreamWriter(string content, Encoding encoding) { string filePath = GetTestFilePath(); - Write(filePath, content, encoding); // it uses System.File.IO APIs + Write(filePath, content, encoding); // it uses System.IO.File APIs string swPath = GetTestFilePath(); using (StreamWriter sw = new StreamWriter(swPath, IsAppend, encoding)) @@ -192,7 +192,7 @@ public void OutputIsTheSameAsForStreamWriter_Overwrite(string content, Encoding for (int i = 0; i < 2; i++) { - Write(filePath, content, encoding); // it uses System.File.IO APIs + Write(filePath, content, encoding); // it uses System.IO.File APIs using (StreamWriter sw = new StreamWriter(swPath, IsAppend, encoding)) { From 8a67c96be12ba0ff6ee5cd13cf7a13b64abd04c8 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 14 Oct 2021 13:48:34 +0200 Subject: [PATCH 16/16] remove redundant file size check that was failing for character devices --- .../src/System/IO/File.netcoreapp.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs index c1176def897e9e..1e627dae597329 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs @@ -151,10 +151,6 @@ private static void WriteToFile(string path, FileMode mode, string? contents, En fileOffset += toStore.Length; preambleSize = 0; } - - long fileLength = 0; - Debug.Assert(!fileHandle.CanSeek || fileOffset == (fileLength = RandomAccess.GetFileLength(fileHandle)), - $"File length different than expected. Offset: {fileOffset}, Length: {fileLength}"); } finally { @@ -210,10 +206,6 @@ private static async Task WriteToFileAsync(string path, FileMode mode, string? c fileOffset += toStore.Length; preambleSize = 0; } - - long fileLength = 0; - Debug.Assert(!fileHandle.CanSeek || fileOffset == (fileLength = RandomAccess.GetFileLength(fileHandle)), - $"File length different than expected. Offset: {fileOffset}, Length: {fileLength}"); } finally {