diff --git a/src/libraries/System.IO.FileSystem/tests/File/Append.cs b/src/libraries/System.IO.FileSystem/tests/File/Append.cs index 8cb28b7ca38acf..68160715a7d635 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/Append.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/Append.cs @@ -8,6 +8,8 @@ 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); @@ -15,34 +17,26 @@ protected override void Write(string path, string content) writer.Dispose(); } - [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)); + var writer = new StreamWriter(path, IsAppend, encoding); + writer.Write(content); + writer.Dispose(); } } 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 +56,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..8c9c1e66e72a4b 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,56 @@ 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.IO.File 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 + } + + [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.IO.File 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 3ad76c272fff6d..4a33f94f7b8f81 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,44 @@ public virtual Task TaskAlreadyCanceledAsync() async () => await File.WriteAllTextAsync(path, "", token)); } + [Theory] + [MemberData(nameof(File_ReadWriteAllText.OutputIsTheSameAsForStreamWriter_Args), MemberType = typeof(File_ReadWriteAllText))] + public async Task OutputIsTheSameAsForStreamWriterAsync(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 + } + + [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 eb86294b119742..2a73f63ecf04f2 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileInfo/AppendText.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileInfo/AppendText.cs @@ -1,12 +1,15 @@ // 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 { 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(); @@ -14,15 +17,11 @@ protected override void Write(string path, string content) writer.Dispose(); } - [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 appendline = new string('b', 100); - Write(path, lines); - Write(path, appendline); - Assert.Equal(lines + appendline, Read(path)); + var writer = new StreamWriter(path, IsAppend, encoding); + writer.Write(content); + writer.Dispose(); } } } 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 4efdb1366d7a0f..e63935c5f0c804 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -22,31 +22,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 ??= 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. @@ -79,9 +67,7 @@ public static void Copy(string sourceFileName, string destFileName, bool overwri // 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); - } + => 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 @@ -100,12 +86,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 @@ -141,184 +122,88 @@ 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) - { - 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); - } + => 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)); + Validate(path, encoding); - 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); - - 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) - { - 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); - } - } + => 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); - using (StreamWriter sw = new StreamWriter(path, false, encoding)) - { - sw.Write(contents); - } + WriteToFile(path, FileMode.Create, contents, encoding); } public static byte[] ReadAllBytes(string path) @@ -368,98 +253,51 @@ public static void WriteAllBytes(string path, byte[] bytes) using SafeFileHandle sfh = OpenHandle(path, FileMode.Create, FileAccess.Write, FileShare.Read); RandomAccess.WriteAtOffset(sfh, bytes, 0); } - 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); - } + => 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); } public static void WriteAllLines(string path, string[] contents) - { - WriteAllLines(path, (IEnumerable)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); - } + => WriteAllLines(path, contents, UTF8NoBOM); 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); } @@ -479,63 +317,30 @@ 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); - } - } + => AppendAllText(path, contents, UTF8NoBOM); 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); - using (StreamWriter sw = new StreamWriter(path, true, encoding)) - { - sw.Write(contents); - } + WriteToFile(path, FileMode.Append, 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); - } + => AppendAllLines(path, contents, UTF8NoBOM); 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); } 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) { @@ -560,9 +365,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) { @@ -588,50 +391,30 @@ 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); - - return new StreamWriter(stream, encoding); - } + => new StreamWriter( + new FileStream(path, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, DefaultBufferSize, FileOptions.Asynchronous), + encoding); public static Task ReadAllTextAsync(string path, CancellationToken cancellationToken = default(CancellationToken)) => ReadAllTextAsync(path, Encoding.UTF8, cancellationToken); 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) @@ -676,25 +459,14 @@ 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) { 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, FileMode.Create, contents, encoding, cancellationToken); } public static Task ReadAllBytesAsync(string path, CancellationToken cancellationToken = default(CancellationToken)) @@ -820,12 +592,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) @@ -857,14 +624,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) @@ -889,40 +652,19 @@ private static async Task InternalWriteAllLinesAsync(TextWriter writer, IEnumera } } - private static async Task InternalWriteAllTextAsync(StreamWriter sw, string contents, CancellationToken cancellationToken) - { - using (sw) - { - await sw.WriteAsync(contents.AsMemory(), cancellationToken).ConfigureAwait(false); - await sw.FlushAsync().ConfigureAwait(false); - } - } - public static Task AppendAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default(CancellationToken)) => AppendAllTextAsync(path, contents, UTF8NoBOM, cancellationToken); 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) { 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, FileMode.Append, contents, encoding, cancellationToken); } public static Task AppendAllLinesAsync(string path, IEnumerable contents, CancellationToken cancellationToken = default(CancellationToken)) @@ -930,14 +672,10 @@ private static async Task InternalWriteAllTextAsync(StreamWriter sw, string cont 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) @@ -983,5 +721,15 @@ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget FileSystem.VerifyValidPath(linkPath, nameof(linkPath)); 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)); + } } } 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..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 @@ -3,12 +3,18 @@ using System.Buffers; using System.Diagnostics; +using System.IO.Strategies; +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 = 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. /// @@ -48,7 +54,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); } @@ -97,5 +103,131 @@ private static byte[] ReadAllBytesUnknownLength(FileStream fs) } } } + + private static void WriteToFile(string path, FileMode mode, string? contents, Encoding encoding) + { + ReadOnlySpan preamble = encoding.GetPreamble(); + int preambleSize = preamble.Length; + + 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)) + { + 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); + } + return; + } + + 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)); + + try + { + 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; + while (!remaining.IsEmpty) + { + ReadOnlySpan toEncode = remaining.Slice(0, Math.Min(remaining.Length, ChunkSize)); + 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; + preambleSize = 0; + } + } + finally + { + if (rentedBytes is not null) + { + ArrayPool.Shared.Return(rentedBytes); + } + } + } + + private static async Task WriteToFileAsync(string path, FileMode mode, string? contents, Encoding encoding, CancellationToken cancellationToken) + { + ReadOnlyMemory preamble = encoding.GetPreamble(); + int preambleSize = preamble.Length; + + 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)) + { + 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); + } + return; + } + + byte[] bytes = ArrayPool.Shared.Rent(preambleSize + encoding.GetMaxByteCount(Math.Min(contents.Length, ChunkSize))); + + try + { + 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(); + while (!remaining.IsEmpty) + { + ReadOnlyMemory toEncode = remaining.Slice(0, Math.Min(remaining.Length, ChunkSize)); + 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; + preambleSize = 0; + } + } + finally + { + ArrayPool.Shared.Return(bytes); + } + } + + 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; + } + + return preambleSize + encoding.GetByteCount(contents); + } } }