From f4e595d94290bb18f4e17051d15e9025b36e8074 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 13 Jun 2024 16:52:22 -0400 Subject: [PATCH 1/2] Add File.Append/WriteAllText/Bytes{Async} overloads for span/memory --- .../src/System/IO/File.cs | 337 +++++++++++++++--- .../System.Runtime/ref/System.Runtime.cs | 12 + .../System.IO.FileSystem.Tests/File/Append.cs | 29 ++ .../File/AppendAllBytes.cs | 11 +- .../File/AppendAllBytesAsync.cs | 12 +- .../File/AppendAsync.cs | 43 +++ .../File/ReadWriteAllBytes.cs | 28 +- .../File/ReadWriteAllBytesAsync.cs | 35 +- .../File/ReadWriteAllText.cs | 21 ++ .../File/ReadWriteAllTextAsync.cs | 31 ++ 10 files changed, 488 insertions(+), 71 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 9326f6533306e4..2af095f40b439e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -632,6 +632,30 @@ public static string ReadAllText(string path, Encoding encoding) public static void WriteAllText(string path, string? contents) => WriteAllText(path, contents, UTF8NoBOM); + /// + /// Creates a new file, writes the specified string to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The characters to write to the file. + /// is . + /// is empty. + /// The specified path, file name, or both exceed the system-defined maximum length. + /// The specified path is invalid (for example, it is on an unmapped drive). + /// An I/O error occurred while opening the file. + /// specified a file that is read-only. + /// specified a file that is hidden. + /// specified a directory. + /// This operation is not supported on the current platform. + /// The caller does not have the required permission. + /// is in an invalid format. + /// + /// This method uses UTF-8 encoding without a Byte-Order Mark (BOM), so using the GetPreamble method will return an empty byte array. If it is necessary to + /// include a UTF-8 identifier, such as a byte order mark, at the beginning of a file, use the method. + /// + public static void WriteAllText(string path, ReadOnlySpan contents) + => WriteAllText(path, contents, UTF8NoBOM); + public static void WriteAllText(string path, string? contents, Encoding encoding) { Validate(path, encoding); @@ -639,6 +663,32 @@ public static void WriteAllText(string path, string? contents, Encoding encoding WriteToFile(path, FileMode.Create, contents, encoding); } + /// + /// Creates a new file, writes the specified string to the file using the specified encoding, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The characters to write to the file. + /// The encoding to apply to the string. + /// is . + /// is empty. + /// is . + /// The specified path, file name, or both exceed the system-defined maximum length. + /// The specified path is invalid (for example, it is on an unmapped drive). + /// An I/O error occurred while opening the file. + /// specified a file that is read-only. + /// specified a file that is hidden. + /// specified a directory. + /// This operation is not supported on the current platform. + /// The caller does not have the required permission. + /// is in an invalid format. + public static void WriteAllText(string path, ReadOnlySpan contents, Encoding encoding) + { + Validate(path, encoding); + + WriteToFile(path, FileMode.Create, contents, encoding); + } + public static byte[] ReadAllBytes(string path) { // SequentialScan is a perf hint that requires extra sys-call on non-Windows OSes. @@ -682,9 +732,31 @@ public static byte[] ReadAllBytes(string path) public static void WriteAllBytes(string path, byte[] bytes) { - ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(bytes); + WriteAllBytes(path, new ReadOnlySpan(bytes)); + } + + /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. If the target file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The bytes to write to the file. + /// is null. + /// is empty. + /// The specified path, file name, or both exceed the system-defined maximum length. + /// The specified path is invalid (for example, it is on an unmapped drive). + /// An I/O error occurred while opening the file. + /// specified a file that is read-only. + /// specified a file that is hidden. + /// specified a directory. + /// The caller does not have the required permission. + /// This operation is not supported on the current platform. + /// is in an invalid format. + public static void WriteAllBytes(string path, ReadOnlySpan bytes) + { + ArgumentException.ThrowIfNullOrEmpty(path); + using SafeFileHandle sfh = OpenHandle(path, FileMode.Create, FileAccess.Write, FileShare.Read); RandomAccess.WriteAtOffset(sfh, bytes, 0); } @@ -695,17 +767,46 @@ public static void WriteAllBytes(string path, byte[] bytes) /// /// The file to append to. /// The bytes to append to the file. - /// - /// is a zero-length string, contains only white space, or contains one more invalid characters defined by the method. - /// - /// - /// Either or is null. - /// + /// is null. + /// is null. + /// is empty. + /// The specified path, file name, or both exceed the system-defined maximum length. + /// The specified path is invalid (for example, it is on an unmapped drive). + /// An I/O error occurred while opening the file. + /// specified a file that is read-only. + /// specified a file that is hidden. + /// specified a directory. + /// The caller does not have the required permission. + /// This operation is not supported on the current platform. + /// is in an invalid format. public static void AppendAllBytes(string path, byte[] bytes) { - ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(bytes); + AppendAllBytes(path, new ReadOnlySpan(bytes)); + } + + /// + /// Appends the specified byte array to the end of the file at the given path. + /// If the file doesn't exist, this method creates a new file. + /// + /// The file to append to. + /// The bytes to append to the file. + /// is null. + /// is empty. + /// The specified path, file name, or both exceed the system-defined maximum length. + /// The specified path is invalid (for example, it is on an unmapped drive). + /// An I/O error occurred while opening the file. + /// specified a file that is read-only. + /// specified a file that is hidden. + /// specified a directory. + /// The caller does not have the required permission. + /// This operation is not supported on the current platform. + /// is in an invalid format. + public static void AppendAllBytes(string path, ReadOnlySpan bytes) + { + ArgumentException.ThrowIfNullOrEmpty(path); + using SafeFileHandle fileHandle = OpenHandle(path, FileMode.Append, FileAccess.Write, FileShare.Read); long fileOffset = RandomAccess.GetLength(fileHandle); RandomAccess.WriteAtOffset(fileHandle, bytes, fileOffset); @@ -717,27 +818,37 @@ public static void AppendAllBytes(string path, byte[] bytes) /// /// The file to append to. /// The bytes to append to the file. - /// The token to monitor for cancellation requests. The default value is . - /// A task that represents the asynchronous append operation. - /// - /// is a zero-length string, contains only white space, or contains one more invalid characters defined by the method. - /// - /// - /// Either or is null. - /// - /// - /// The cancellation token was canceled. This exception is stored into the returned task. - /// - public static Task AppendAllBytesAsync(string path, byte[] bytes, CancellationToken cancellationToken = default(CancellationToken)) + /// The token to monitor for cancellation requests. The default value is . + /// is null. + /// is null. + /// is empty. + /// The cancellation token was canceled. This exception is stored into the returned task. + public static Task AppendAllBytesAsync(string path, byte[] bytes, CancellationToken cancellationToken = default) { - ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(bytes); + return AppendAllBytesAsync(path, new ReadOnlyMemory(bytes), cancellationToken); + } + + /// + /// Asynchronously appends the specified byte array to the end of the file at the given path. + /// If the file doesn't exist, this method creates a new file. If the operation is canceled, the task will return in a canceled state. + /// + /// The file to append to. + /// The bytes to append to the file. + /// The token to monitor for cancellation requests. The default value is . + /// is null. + /// is empty. + /// The cancellation token was canceled. This exception is stored into the returned task. + public static Task AppendAllBytesAsync(string path, ReadOnlyMemory bytes, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(path); + return cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Core(path, bytes, cancellationToken); - static async Task Core(string path, byte[] bytes, CancellationToken cancellationToken) + static async Task Core(string path, ReadOnlyMemory bytes, CancellationToken cancellationToken) { using SafeFileHandle fileHandle = OpenHandle(path, FileMode.Append, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous); long fileOffset = RandomAccess.GetLength(fileHandle); @@ -831,6 +942,30 @@ private static void InternalWriteAllLines(StreamWriter writer, IEnumerable AppendAllText(path, contents, UTF8NoBOM); + /// + /// Appends the specified string to the file, creating the file if it does not already exist. + /// + /// The file to append to. + /// The characters to write to the file. + /// is . + /// is empty. + /// The specified path, file name, or both exceed the system-defined maximum length. + /// The specified path is invalid (for example, it is on an unmapped drive). + /// An I/O error occurred while opening the file. + /// specified a file that is read-only. + /// specified a file that is hidden. + /// specified a directory. + /// This operation is not supported on the current platform. + /// The caller does not have the required permission. + /// is in an invalid format. + /// + /// Given a string and a file path, this method opens the specified file, appends the string to the end of the file using the specified encoding, + /// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised. The method creates the file + /// if it doesn't exist, but it doesn't create new directories. Therefore, the value of the path parameter must contain existing directories. + /// + public static void AppendAllText(string path, ReadOnlySpan contents) + => AppendAllText(path, contents, UTF8NoBOM); + public static void AppendAllText(string path, string? contents, Encoding encoding) { Validate(path, encoding); @@ -838,6 +973,36 @@ public static void AppendAllText(string path, string? contents, Encoding encodin WriteToFile(path, FileMode.Append, contents, encoding); } + /// + /// Appends the specified string to the file, creating the file if it does not already exist. + /// + /// The file to append to. + /// The characters to write to the file. + /// The encoding to apply to the string. + /// is . + /// is empty. + /// is . + /// The specified path, file name, or both exceed the system-defined maximum length. + /// The specified path is invalid (for example, it is on an unmapped drive). + /// An I/O error occurred while opening the file. + /// specified a file that is read-only. + /// specified a file that is hidden. + /// specified a directory. + /// This operation is not supported on the current platform. + /// The caller does not have the required permission. + /// is in an invalid format. + /// + /// Given a string and a file path, this method opens the specified file, appends the string to the end of the file using the specified encoding, + /// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised. The method creates the file + /// if it doesn't exist, but it doesn't create new directories. Therefore, the value of the path parameter must contain existing directories. + /// + public static void AppendAllText(string path, ReadOnlySpan contents, Encoding encoding) + { + Validate(path, encoding); + + WriteToFile(path, FileMode.Append, contents, encoding); + } + public static void AppendAllLines(string path, IEnumerable contents) => AppendAllLines(path, contents, UTF8NoBOM); @@ -911,10 +1076,10 @@ private static StreamReader AsyncStreamReader(string path, Encoding encoding) new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan), encoding, detectEncodingFromByteOrderMarks: true); - public static Task ReadAllTextAsync(string path, CancellationToken cancellationToken = default(CancellationToken)) + public static Task ReadAllTextAsync(string path, CancellationToken cancellationToken = default) => ReadAllTextAsync(path, Encoding.UTF8, cancellationToken); - public static Task ReadAllTextAsync(string path, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + public static Task ReadAllTextAsync(string path, Encoding encoding, CancellationToken cancellationToken = default) { Validate(path, encoding); @@ -956,10 +1121,35 @@ private static async Task InternalReadAllTextAsync(string path, Encoding } } - public static Task WriteAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default(CancellationToken)) + public static Task WriteAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default) + => WriteAllTextAsync(path, contents, UTF8NoBOM, cancellationToken); + + /// + /// Asynchronously creates a new file, writes the specified string to the file, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The characters to write to the file. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous write operation. + /// The cancellation token was canceled. This exception is stored into the returned task. + public static Task WriteAllTextAsync(string path, ReadOnlyMemory contents, CancellationToken cancellationToken = default) => WriteAllTextAsync(path, contents, UTF8NoBOM, cancellationToken); - public static Task WriteAllTextAsync(string path, string? contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + public static Task WriteAllTextAsync(string path, string? contents, Encoding encoding, CancellationToken cancellationToken = default) + => WriteAllTextAsync(path, contents.AsMemory(), encoding, cancellationToken); + + /// + /// Asynchronously creates a new file, writes the specified string to the file using the specified encoding, and then closes the file. + /// If the target file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The characters to write to the file. + /// The encoding to apply to the string. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous write operation. + /// The cancellation token was canceled. This exception is stored into the returned task. + public static Task WriteAllTextAsync(string path, ReadOnlyMemory contents, Encoding encoding, CancellationToken cancellationToken = default) { Validate(path, encoding); @@ -971,7 +1161,7 @@ private static async Task InternalReadAllTextAsync(string path, Encoding return WriteToFileAsync(path, FileMode.Create, contents, encoding, cancellationToken); } - public static Task ReadAllBytesAsync(string path, CancellationToken cancellationToken = default(CancellationToken)) + public static Task ReadAllBytesAsync(string path, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { @@ -1060,26 +1250,42 @@ private static async Task InternalReadAllBytesUnknownLengthAsync(SafeFil } } - public static Task WriteAllBytesAsync(string path, byte[] bytes, CancellationToken cancellationToken = default(CancellationToken)) + public static Task WriteAllBytesAsync(string path, byte[] bytes, CancellationToken cancellationToken = default) { - ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(bytes); + return WriteAllBytesAsync(path, new ReadOnlyMemory(bytes), cancellationToken); + } + + /// + /// Asynchronously creates a new file, writes the specified byte array to the file, and then closes the file. If the target file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The bytes to write to the file. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous write operation. + /// is null. + /// is empty. + /// The cancellation token was canceled. This exception is stored into the returned task. + public static Task WriteAllBytesAsync(string path, ReadOnlyMemory bytes, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(path); + return cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Core(path, bytes, cancellationToken); - static async Task Core(string path, byte[] bytes, CancellationToken cancellationToken) + static async Task Core(string path, ReadOnlyMemory bytes, CancellationToken cancellationToken) { using SafeFileHandle sfh = OpenHandle(path, FileMode.Create, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous); await RandomAccess.WriteAtOffsetAsync(sfh, bytes, 0, cancellationToken).ConfigureAwait(false); } } - public static Task ReadAllLinesAsync(string path, CancellationToken cancellationToken = default(CancellationToken)) + public static Task ReadAllLinesAsync(string path, CancellationToken cancellationToken = default) => ReadAllLinesAsync(path, Encoding.UTF8, cancellationToken); - public static Task ReadAllLinesAsync(string path, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + public static Task ReadAllLinesAsync(string path, Encoding encoding, CancellationToken cancellationToken = default) { Validate(path, encoding); @@ -1108,10 +1314,10 @@ private static async Task InternalReadAllLinesAsync(string path, Encod } } - public static Task WriteAllLinesAsync(string path, IEnumerable contents, CancellationToken cancellationToken = default(CancellationToken)) + public static Task WriteAllLinesAsync(string path, IEnumerable contents, CancellationToken cancellationToken = default) => WriteAllLinesAsync(path, contents, UTF8NoBOM, cancellationToken); - public static Task WriteAllLinesAsync(string path, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) => + public static Task WriteAllLinesAsync(string path, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default) => WriteAllLinesAsync(path, contents, encoding, append: false, cancellationToken); private static Task WriteAllLinesAsync(string path, IEnumerable contents, Encoding encoding, bool append, CancellationToken cancellationToken) @@ -1154,10 +1360,33 @@ private static async Task InternalWriteAllLinesAsync(StreamWriter writer, IEnume } } - public static Task AppendAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default(CancellationToken)) + public static Task AppendAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default) + => AppendAllTextAsync(path, contents, UTF8NoBOM, cancellationToken); + + /// + /// Asynchronously opens a file or creates a file if it does not already exist, appends the specified string to the file, and then closes the file. + /// + /// The file to append the specified string to. + /// The characters to append to the file. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous append operation. + /// The cancellation token was canceled. This exception is stored into the returned task. + public static Task AppendAllTextAsync(string path, ReadOnlyMemory contents, CancellationToken cancellationToken = default) => AppendAllTextAsync(path, contents, UTF8NoBOM, cancellationToken); - public static Task AppendAllTextAsync(string path, string? contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + public static Task AppendAllTextAsync(string path, string? contents, Encoding encoding, CancellationToken cancellationToken = default) + => AppendAllTextAsync(path, contents.AsMemory(), encoding, cancellationToken); + + /// + /// Asynchronously opens a file or creates the file if it does not already exist, appends the specified string to the file using the specified encoding, and then closes the file. + /// + /// The file to append the specified string to. + /// The characters to append to the file. + /// The character encoding to use. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous append operation. + /// The cancellation token was canceled. This exception is stored into the returned task. + public static Task AppendAllTextAsync(string path, ReadOnlyMemory contents, Encoding encoding, CancellationToken cancellationToken = default) { Validate(path, encoding); @@ -1169,10 +1398,10 @@ private static async Task InternalWriteAllLinesAsync(StreamWriter writer, IEnume return WriteToFileAsync(path, FileMode.Append, contents, encoding, cancellationToken); } - public static Task AppendAllLinesAsync(string path, IEnumerable contents, CancellationToken cancellationToken = default(CancellationToken)) + public static Task AppendAllLinesAsync(string path, IEnumerable contents, CancellationToken cancellationToken = default) => AppendAllLinesAsync(path, contents, UTF8NoBOM, cancellationToken); - public static Task AppendAllLinesAsync(string path, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) => + public static Task AppendAllLinesAsync(string path, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default) => WriteAllLinesAsync(path, contents, encoding, append: true, cancellationToken); /// @@ -1266,7 +1495,7 @@ private static byte[] ReadAllBytesUnknownLength(SafeFileHandle sfh) } } - private static void WriteToFile(string path, FileMode mode, string? contents, Encoding encoding) + private static void WriteToFile(string path, FileMode mode, ReadOnlySpan contents, Encoding encoding) { ReadOnlySpan preamble = encoding.GetPreamble(); int preambleSize = preamble.Length; @@ -1274,7 +1503,7 @@ private static void WriteToFile(string path, FileMode mode, string? contents, En 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 (contents.IsEmpty) { 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. @@ -1300,12 +1529,11 @@ private static void WriteToFile(string path, FileMode mode, string? contents, En } Encoder encoder = encoding.GetEncoder(); - ReadOnlySpan remaining = contents; - while (!remaining.IsEmpty) + while (!contents.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); + ReadOnlySpan toEncode = contents.Slice(0, Math.Min(contents.Length, ChunkSize)); + contents = contents.Slice(toEncode.Length); + int encoded = encoder.GetBytes(toEncode, bytes.Slice(preambleSize), flush: contents.IsEmpty); Span toStore = bytes.Slice(0, preambleSize + encoded); RandomAccess.WriteAtOffset(fileHandle, toStore, fileOffset); @@ -1323,15 +1551,15 @@ private static void WriteToFile(string path, FileMode mode, string? contents, En } } - private static async Task WriteToFileAsync(string path, FileMode mode, string? contents, Encoding encoding, CancellationToken cancellationToken) + private static async Task WriteToFileAsync(string path, FileMode mode, ReadOnlyMemory 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)); + using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous, GetPreallocationSize(mode, contents.Span, encoding, preambleSize)); long fileOffset = mode == FileMode.Append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0; - if (string.IsNullOrEmpty(contents)) + if (contents.IsEmpty) { 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. @@ -1355,12 +1583,11 @@ private static async Task WriteToFileAsync(string path, FileMode mode, string? c } Encoder encoder = encoding.GetEncoder(); - ReadOnlyMemory remaining = contents.AsMemory(); - while (!remaining.IsEmpty) + while (!contents.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 toEncode = contents.Slice(0, Math.Min(contents.Length, ChunkSize)); + contents = contents.Slice(toEncode.Length); + int encoded = encoder.GetBytes(toEncode.Span, bytes.AsSpan(preambleSize), flush: contents.IsEmpty); ReadOnlyMemory toStore = new ReadOnlyMemory(bytes, 0, preambleSize + encoded); await RandomAccess.WriteAtOffsetAsync(fileHandle, toStore, fileOffset, cancellationToken).ConfigureAwait(false); @@ -1375,10 +1602,10 @@ private static async Task WriteToFileAsync(string path, FileMode mode, string? c } } - private static long GetPreallocationSize(FileMode mode, string? contents, Encoding encoding, int preambleSize) + private static long GetPreallocationSize(FileMode mode, ReadOnlySpan 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) + if (contents.Length < ChunkSize) { return 0; } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 9562467301a4ae..15e5c61b5da6f5 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -10061,15 +10061,21 @@ public EnumerationOptions() { } public static partial class File { public static void AppendAllBytes(string path, byte[] bytes) { } + public static void AppendAllBytes(string path, System.ReadOnlySpan bytes) { } public static System.Threading.Tasks.Task AppendAllBytesAsync(string path, byte[] bytes, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task AppendAllBytesAsync(string path, System.ReadOnlyMemory bytes, System.Threading.CancellationToken cancellationToken = default) { throw null; } public static void AppendAllLines(string path, System.Collections.Generic.IEnumerable contents) { } public static void AppendAllLines(string path, System.Collections.Generic.IEnumerable contents, System.Text.Encoding encoding) { } public static System.Threading.Tasks.Task AppendAllLinesAsync(string path, System.Collections.Generic.IEnumerable contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.Task AppendAllLinesAsync(string path, System.Collections.Generic.IEnumerable contents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static void AppendAllText(string path, string? contents) { } public static void AppendAllText(string path, string? contents, System.Text.Encoding encoding) { } + public static void AppendAllText(string path, System.ReadOnlySpan contents) { } + public static void AppendAllText(string path, System.ReadOnlySpan contents, System.Text.Encoding encoding) { } public static System.Threading.Tasks.Task AppendAllTextAsync(string path, string? contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.Task AppendAllTextAsync(string path, string? contents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task AppendAllTextAsync(string path, System.ReadOnlyMemory contents, System.Threading.CancellationToken cancellationToken = default) { throw null; } + public static System.Threading.Tasks.Task AppendAllTextAsync(string path, System.ReadOnlyMemory contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default) { throw null; } public static System.IO.StreamWriter AppendText(string path) { throw null; } public static void Copy(string sourceFileName, string destFileName) { } public static void Copy(string sourceFileName, string destFileName, bool overwrite) { } @@ -10148,7 +10154,9 @@ public static void SetUnixFileMode(Microsoft.Win32.SafeHandles.SafeFileHandle fi [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] public static void SetUnixFileMode(string path, System.IO.UnixFileMode mode) { } public static void WriteAllBytes(string path, byte[] bytes) { } + public static void WriteAllBytes(string path, System.ReadOnlySpan bytes) { } public static System.Threading.Tasks.Task WriteAllBytesAsync(string path, byte[] bytes, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task WriteAllBytesAsync(string path, System.ReadOnlyMemory bytes, System.Threading.CancellationToken cancellationToken = default) { throw null; } public static void WriteAllLines(string path, System.Collections.Generic.IEnumerable contents) { } public static void WriteAllLines(string path, System.Collections.Generic.IEnumerable contents, System.Text.Encoding encoding) { } public static void WriteAllLines(string path, string[] contents) { } @@ -10157,8 +10165,12 @@ public static void WriteAllLines(string path, string[] contents, System.Text.Enc public static System.Threading.Tasks.Task WriteAllLinesAsync(string path, System.Collections.Generic.IEnumerable contents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static void WriteAllText(string path, string? contents) { } public static void WriteAllText(string path, string? contents, System.Text.Encoding encoding) { } + public static void WriteAllText(string path, System.ReadOnlySpan contents) { } + public static void WriteAllText(string path, System.ReadOnlySpan contents, System.Text.Encoding encoding) { } public static System.Threading.Tasks.Task WriteAllTextAsync(string path, string? contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.Task WriteAllTextAsync(string path, string? contents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task WriteAllTextAsync(string path, System.ReadOnlyMemory contents, System.Threading.CancellationToken cancellationToken = default) { throw null; } + public static System.Threading.Tasks.Task WriteAllTextAsync(string path, System.ReadOnlyMemory contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default) { throw null; } } [System.FlagsAttribute] public enum FileAccess diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Append.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Append.cs index 68160715a7d635..aa2d1829a711a7 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Append.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Append.cs @@ -40,6 +40,21 @@ protected override void Write(string path, string content, Encoding encoding) } } + public class File_AppendAllText_Span : File_ReadWriteAllText + { + protected override bool IsAppend => true; + + protected override void Write(string path, string content) + { + File.AppendAllText(path, content.AsSpan()); + } + + protected override void Write(string path, string content, Encoding encoding) + { + File.AppendAllText(path, content.AsSpan(), encoding); + } + } + public class File_AppendAllText_Encoded : File_AppendAllText { protected override void Write(string path, string content) @@ -54,6 +69,20 @@ public void NullEncoding() } } + public class File_AppendAllText_Span_Encoded : File_AppendAllText + { + protected override void Write(string path, string content) + { + File.AppendAllText(path, content.AsSpan(), new UTF8Encoding(false)); + } + + [Fact] + public void NullEncoding() + { + Assert.Throws(() => File.AppendAllText(GetTestFilePath(), "Text".AsSpan(), null)); + } + } + public class File_AppendAllLines : File_ReadWriteAllLines_Enumerable { protected override bool IsAppend => true; diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAllBytes.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAllBytes.cs index 9b29f971e20dfa..da68cd7b406a2a 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAllBytes.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAllBytes.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.IO.Tests; using System.Linq; using System.Text; using Xunit; @@ -10,13 +9,13 @@ namespace System.IO.Tests { public class File_AppendAllBytes : FileSystemTest { - [Fact] public void NullParameters() { string path = GetTestFilePath(); Assert.Throws(() => File.AppendAllBytes(null, new byte[0])); + Assert.Throws(() => File.AppendAllBytes(null, ReadOnlySpan.Empty)); Assert.Throws(() => File.AppendAllBytes(path, null)); } @@ -24,12 +23,14 @@ public void NullParameters() public void NonExistentPath() { Assert.Throws(() => File.AppendAllBytes(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), new byte[0])); + Assert.Throws(() => File.AppendAllBytes(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), ReadOnlySpan.Empty)); } [Fact] public void InvalidParameters() { Assert.Throws(() => File.AppendAllBytes(string.Empty, new byte[0])); + Assert.Throws(() => File.AppendAllBytes(string.Empty, ReadOnlySpan.Empty)); } @@ -43,10 +44,11 @@ public void AppendAllBytes_WithValidInput_AppendsBytes() File.WriteAllBytes(path, initialBytes); File.AppendAllBytes(path, additionalBytes); + File.AppendAllBytes(path, additionalBytes.AsSpan()); byte[] result = File.ReadAllBytes(path); - byte[] expectedBytes = initialBytes.Concat(additionalBytes).ToArray(); + byte[] expectedBytes = initialBytes.Concat(additionalBytes).Concat(additionalBytes).ToArray(); Assert.True(result.SequenceEqual(expectedBytes)); } @@ -58,6 +60,7 @@ public void EmptyContentCreatesFile() string path = GetTestFilePath(); Assert.False(File.Exists(path)); File.AppendAllBytes(path, new byte[0]); + File.AppendAllBytes(path, ReadOnlySpan.Empty); Assert.True(File.Exists(path)); Assert.Empty(File.ReadAllBytes(path)); } @@ -71,6 +74,7 @@ public void OpenFile_ThrowsIOException() using (File.Create(path)) { Assert.Throws(() => File.AppendAllBytes(path, bytes)); + Assert.Throws(() => File.AppendAllBytes(path, bytes.AsSpan())); } } @@ -97,6 +101,7 @@ public void AppendToReadOnlyFileAsync() else { Assert.Throws(() => File.AppendAllBytes(path, dataToAppend)); + Assert.Throws(() => File.AppendAllBytes(path, dataToAppend.AsSpan())); } } finally diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAllBytesAsync.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAllBytesAsync.cs index 84ef880f4aa93f..89ef9b63f665de 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAllBytesAsync.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAllBytesAsync.cs @@ -19,6 +19,7 @@ public async Task NullParametersAsync() string path = GetTestFilePath(); await Assert.ThrowsAsync("path", async () => await File.AppendAllBytesAsync(null, new byte[0])); + await Assert.ThrowsAsync("path", async () => await File.AppendAllBytesAsync(null, ReadOnlyMemory.Empty)); await Assert.ThrowsAsync("bytes", async () => await File.AppendAllBytesAsync(path, null)); } @@ -26,12 +27,14 @@ public async Task NullParametersAsync() public void NonExistentPathAsync() { Assert.ThrowsAsync(() => File.AppendAllBytesAsync(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), new byte[0])); + Assert.ThrowsAsync(() => File.AppendAllBytesAsync(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), ReadOnlyMemory.Empty)); } [Fact] public async Task InvalidParametersAsync() { await Assert.ThrowsAsync("path", async () => await File.AppendAllBytesAsync(string.Empty, new byte[0])); + await Assert.ThrowsAsync("path", async () => await File.AppendAllBytesAsync(string.Empty, ReadOnlyMemory.Empty)); } [Fact] @@ -44,10 +47,11 @@ public async Task AppendAllBytesAsync_WithValidInput_AppendsBytes() await File.WriteAllBytesAsync(path, initialBytes); await File.AppendAllBytesAsync(path, additionalBytes); + await File.AppendAllBytesAsync(path, additionalBytes.AsMemory()); byte[] result = await File.ReadAllBytesAsync(path); - byte[] expectedBytes = initialBytes.Concat(additionalBytes).ToArray(); + byte[] expectedBytes = initialBytes.Concat(additionalBytes).Concat(additionalBytes).ToArray(); Assert.True(result.SequenceEqual(expectedBytes)); } @@ -57,6 +61,7 @@ public async Task EmptyContentCreatesFileAsync() { string path = GetTestFilePath(); await File.AppendAllBytesAsync(path, new byte[0]); + await File.AppendAllBytesAsync(path, ReadOnlyMemory.Empty); Assert.True(File.Exists(path)); Assert.Empty(await File.ReadAllBytesAsync(path)); } @@ -70,6 +75,7 @@ public async Task OpenFile_ThrowsIOExceptionAsync() using (File.Create(path)) { await Assert.ThrowsAsync(async () => await File.AppendAllBytesAsync(path, bytes)); + await Assert.ThrowsAsync(async () => await File.AppendAllBytesAsync(path, bytes.AsMemory())); } } @@ -91,11 +97,13 @@ public async Task AppendToReadOnlyFileAsync() if (PlatformDetection.IsNotWindows && PlatformDetection.IsPrivilegedProcess) { await File.AppendAllBytesAsync(path, dataToAppend); - Assert.Equal(dataToAppend, await File.ReadAllBytesAsync(path)); + await File.AppendAllBytesAsync(path, dataToAppend.AsMemory()); + Assert.Equal(dataToAppend.Concat(dataToAppend), await File.ReadAllBytesAsync(path)); } else { await Assert.ThrowsAsync(async () => await File.AppendAllBytesAsync(path, dataToAppend)); + await Assert.ThrowsAsync(async () => await File.AppendAllBytesAsync(path, dataToAppend.AsMemory())); } } finally diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAsync.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAsync.cs index a4be5ccdd4e950..6a7dca62bc6a98 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAsync.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/AppendAsync.cs @@ -28,6 +28,26 @@ public override Task TaskAlreadyCanceledAsync() } } + public class File_AppendAllTextAsync_Memory : File_ReadWriteAllTextAsync + { + protected override bool IsAppend => true; + + protected override Task WriteAsync(string path, string content) => File.AppendAllTextAsync(path, content.AsMemory()); + + protected override Task WriteAsync(string path, string content, Encoding encoding) => File.AppendAllTextAsync(path, content.AsMemory(), encoding); + + [Fact] + public override Task TaskAlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + source.Cancel(); + Assert.True(File.AppendAllTextAsync(path, "".AsMemory(), token).IsCanceled); + return Assert.ThrowsAsync(async () => await File.AppendAllTextAsync(path, "".AsMemory(), token)); + } + } + public class File_AppendAllTextAsync_Encoded : File_AppendAllTextAsync { protected override Task WriteAsync(string path, string content) => @@ -51,6 +71,29 @@ public override Task TaskAlreadyCanceledAsync() } } + public class File_AppendAllTextAsync_Memory_Encoded : File_AppendAllTextAsync + { + protected override Task WriteAsync(string path, string content) => + File.AppendAllTextAsync(path, content.AsMemory(), new UTF8Encoding(false)); + + [Fact] + public Task NullEncodingAsync() => Assert.ThrowsAsync( + "encoding", + async () => await File.AppendAllTextAsync(GetTestFilePath(), "Text".AsMemory(), null)); + + [Fact] + public override Task TaskAlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + source.Cancel(); + Assert.True(File.AppendAllTextAsync(path, "".AsMemory(), Encoding.UTF8, token).IsCanceled); + return Assert.ThrowsAsync( + async () => await File.AppendAllTextAsync(path, "".AsMemory(), Encoding.UTF8, token)); + } + } + public class File_AppendAllLinesAsync : File_ReadWriteAllLines_EnumerableAsync { protected override bool IsAppend => true; diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllBytes.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllBytes.cs index 0f5ecbec917824..5f56d4476d27d3 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllBytes.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllBytes.cs @@ -7,6 +7,7 @@ using Xunit; using System.IO.Pipes; using Microsoft.DotNet.XUnitExtensions; +using System.Linq; namespace System.IO.Tests { @@ -17,6 +18,7 @@ public void NullParameters() { string path = GetTestFilePath(); Assert.Throws(() => File.WriteAllBytes(null, new byte[0])); + Assert.Throws(() => File.WriteAllBytes(null, ReadOnlySpan.Empty)); Assert.Throws(() => File.WriteAllBytes(path, null)); Assert.Throws(() => File.ReadAllBytes(null)); } @@ -25,6 +27,7 @@ public void NullParameters() public void InvalidParameters() { Assert.Throws(() => File.WriteAllBytes(string.Empty, new byte[0])); + Assert.Throws(() => File.WriteAllBytes(string.Empty, ReadOnlySpan.Empty)); Assert.Throws(() => File.ReadAllBytes(string.Empty)); } @@ -40,6 +43,7 @@ public void EmptyContentCreatesFile() { string path = GetTestFilePath(); File.WriteAllBytes(path, new byte[0]); + File.WriteAllBytes(path, ReadOnlySpan.Empty); Assert.True(File.Exists(path)); Assert.Empty(File.ReadAllText(path)); File.Delete(path); @@ -54,17 +58,28 @@ public void ValidWrite(int size) byte[] buffer = Encoding.UTF8.GetBytes(new string('c', size)); File.WriteAllBytes(path, buffer); Assert.Equal(buffer, File.ReadAllBytes(path)); + File.WriteAllBytes(path, buffer.AsSpan()); + Assert.Equal(buffer, File.ReadAllBytes(path)); File.Delete(path); } - [Fact] - public void Overwrite() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Overwrite(bool useSpan) { string path = GetTestFilePath(); byte[] bytes = Encoding.UTF8.GetBytes(new string('c', 100)); byte[] overwriteBytes = Encoding.UTF8.GetBytes(new string('b', 50)); File.WriteAllBytes(path, bytes); - File.WriteAllBytes(path, overwriteBytes); + if (useSpan) + { + File.WriteAllBytes(path, overwriteBytes.AsSpan()); + } + else + { + File.WriteAllBytes(path, overwriteBytes); + } Assert.Equal(overwriteBytes, File.ReadAllBytes(path)); } @@ -76,6 +91,7 @@ public void OpenFile_ThrowsIOException() using (File.Create(path)) { Assert.Throws(() => File.WriteAllBytes(path, bytes)); + Assert.Throws(() => File.WriteAllBytes(path, bytes.AsSpan())); Assert.Throws(() => File.ReadAllBytes(path)); } } @@ -96,10 +112,14 @@ public void WriteToReadOnlyFile() if (PlatformDetection.IsNotWindows && PlatformDetection.IsPrivilegedProcess) { File.WriteAllBytes(path, "text"u8.ToArray()); - Assert.Equal("text"u8.ToArray(), File.ReadAllBytes(path)); + File.WriteAllBytes(path, "text"u8); + Assert.Equal("texttext"u8.ToArray(), File.ReadAllBytes(path)); } else + { Assert.Throws(() => File.WriteAllBytes(path, "text"u8.ToArray())); + Assert.Throws(() => File.WriteAllBytes(path, "text"u8)); + } } finally { diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllBytesAsync.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllBytesAsync.cs index 62772f2ee1e862..662e4aa109100e 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllBytesAsync.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllBytesAsync.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Xunit; using System.IO.Pipes; +using System.Linq; namespace System.IO.Tests { @@ -16,6 +17,7 @@ public async Task NullParametersAsync() { string path = GetTestFilePath(); await Assert.ThrowsAsync("path", async () => await File.WriteAllBytesAsync(null, new byte[0])); + await Assert.ThrowsAsync("path", async () => await File.WriteAllBytesAsync(null, ReadOnlyMemory.Empty)); await Assert.ThrowsAsync("bytes", async () => await File.WriteAllBytesAsync(path, null)); await Assert.ThrowsAsync("path", async () => await File.ReadAllBytesAsync(null)); } @@ -24,6 +26,7 @@ public async Task NullParametersAsync() public async Task InvalidParametersAsync() { await Assert.ThrowsAsync("path", async () => await File.WriteAllBytesAsync(string.Empty, new byte[0])); + await Assert.ThrowsAsync("path", async () => await File.WriteAllBytesAsync(string.Empty, ReadOnlyMemory.Empty)); await Assert.ThrowsAsync("path", async () => await File.ReadAllBytesAsync(string.Empty)); } @@ -39,6 +42,7 @@ public async Task EmptyContentCreatesFileAsync() { string path = GetTestFilePath(); await File.WriteAllBytesAsync(path, new byte[0]); + await File.WriteAllBytesAsync(path, ReadOnlyMemory.Empty); Assert.True(File.Exists(path)); Assert.Empty(await File.ReadAllTextAsync(path)); File.Delete(path); @@ -53,29 +57,41 @@ public async Task ValidWriteAsync(int size) byte[] buffer = Encoding.UTF8.GetBytes(new string('c', size)); await File.WriteAllBytesAsync(path, buffer); Assert.Equal(buffer, await File.ReadAllBytesAsync(path)); + await File.WriteAllBytesAsync(path, buffer.AsMemory()); + Assert.Equal(buffer, await File.ReadAllBytesAsync(path)); File.Delete(path); } [Fact] - public Task AlreadyCanceledAsync() + public async Task AlreadyCanceledAsync() { string path = GetTestFilePath(); CancellationTokenSource source = new CancellationTokenSource(); CancellationToken token = source.Token; source.Cancel(); Assert.True(File.WriteAllBytesAsync(path, new byte[0], token).IsCanceled); - return Assert.ThrowsAsync( - async () => await File.WriteAllBytesAsync(path, new byte[0], token)); + Assert.True(File.WriteAllBytesAsync(path, ReadOnlyMemory.Empty, token).IsCanceled); + await Assert.ThrowsAsync(async () => await File.WriteAllBytesAsync(path, new byte[0], token)); + await Assert.ThrowsAsync(async () => await File.WriteAllBytesAsync(path, ReadOnlyMemory.Empty, token)); } - [Fact] - public async Task OverwriteAsync() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task OverwriteAsync(bool useMemory) { string path = GetTestFilePath(); byte[] bytes = Encoding.UTF8.GetBytes(new string('c', 100)); byte[] overwriteBytes = Encoding.UTF8.GetBytes(new string('b', 50)); await File.WriteAllBytesAsync(path, bytes); - await File.WriteAllBytesAsync(path, overwriteBytes); + if (useMemory) + { + await File.WriteAllBytesAsync(path, overwriteBytes); + } + else + { + await File.WriteAllBytesAsync(path, overwriteBytes); + } Assert.Equal(overwriteBytes, await File.ReadAllBytesAsync(path)); } @@ -87,6 +103,7 @@ public async Task OpenFile_ThrowsIOExceptionAsync() using (File.Create(path)) { await Assert.ThrowsAsync(async () => await File.WriteAllBytesAsync(path, bytes)); + await Assert.ThrowsAsync(async () => await File.WriteAllBytesAsync(path, bytes.AsMemory())); await Assert.ThrowsAsync(async () => await File.ReadAllBytesAsync(path)); } } @@ -107,10 +124,14 @@ public async Task WriteToReadOnlyFileAsync() if (PlatformDetection.IsNotWindows && PlatformDetection.IsPrivilegedProcess) { await File.WriteAllBytesAsync(path, "text"u8.ToArray()); - Assert.Equal("text"u8.ToArray(), await File.ReadAllBytesAsync(path)); + await File.WriteAllBytesAsync(path, "text"u8.ToArray().AsMemory()); + Assert.Equal("texttext"u8.ToArray(), await File.ReadAllBytesAsync(path)); } else + { await Assert.ThrowsAsync(async () => await File.WriteAllBytesAsync(path, "text"u8.ToArray())); + await Assert.ThrowsAsync(async () => await File.WriteAllBytesAsync(path, "text"u8.ToArray().AsMemory())); + } } finally { diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllText.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllText.cs index 749877010e6c1d..e22512ed059acd 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllText.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllText.cs @@ -225,4 +225,25 @@ public void NullEncoding() Assert.Throws(() => File.ReadAllText(path, null)); } } + + public class File_ReadWriteAllText_Span_Encoded : File_ReadWriteAllText + { + protected override void Write(string path, string content) + { + File.WriteAllText(path, content.AsSpan(), new UTF8Encoding(false)); + } + + protected override string Read(string path) + { + return File.ReadAllText(path, new UTF8Encoding(false)); + } + + [Fact] + public void NullEncoding() + { + string path = GetTestFilePath(); + Assert.Throws(() => File.WriteAllText(path, "Text".AsSpan(), null)); + Assert.Throws(() => File.ReadAllText(path, null)); + } + } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllTextAsync.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllTextAsync.cs index 09825f49f5c260..723ed95aa5da7f 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllTextAsync.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/ReadWriteAllTextAsync.cs @@ -224,4 +224,35 @@ public override Task TaskAlreadyCanceledAsync() async () => await File.WriteAllTextAsync(path, "", Encoding.UTF8, token)); } } + + public class File_ReadWriteAllText_Memory_EncodedAsync : File_ReadWriteAllTextAsync + { + protected override Task WriteAsync(string path, string content) => + File.WriteAllTextAsync(path, content.AsMemory(), new UTF8Encoding(false)); + + protected override Task ReadAsync(string path) => + File.ReadAllTextAsync(path, new UTF8Encoding(false)); + + [Fact] + public async Task NullEncodingAsync() + { + string path = GetTestFilePath(); + await Assert.ThrowsAsync("encoding", async () => await File.WriteAllTextAsync(path, "Text", null)); + await Assert.ThrowsAsync("encoding", async () => await File.WriteAllTextAsync(path, "Text".AsMemory(), null)); + await Assert.ThrowsAsync("encoding", async () => await File.ReadAllTextAsync(path, null)); + } + + [Fact] + public override async Task TaskAlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + source.Cancel(); + Assert.True(File.WriteAllTextAsync(path, "", Encoding.UTF8, token).IsCanceled); + Assert.True(File.WriteAllTextAsync(path, "".AsMemory(), Encoding.UTF8, token).IsCanceled); + await Assert.ThrowsAsync(async () => await File.WriteAllTextAsync(path, "", Encoding.UTF8, token)); + await Assert.ThrowsAsync(async () => await File.WriteAllTextAsync(path, "".AsMemory(), Encoding.UTF8, token)); + } + } } From 2154e92335563c0de68d3df08c6099b8c9c6bebd Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 14 Jun 2024 14:12:35 -0400 Subject: [PATCH 2/2] Fix faulty assert --- .../src/System/IO/RandomAccess.Windows.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs index 151a0abfc84fac..6918da0413fc65 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs @@ -279,7 +279,7 @@ private static unsafe (SafeFileHandle.OverlappedValueTaskSource? vts, int errorC try { NativeOverlapped* nativeOverlapped = vts.PrepareForOperation(buffer, fileOffset, strategy); - Debug.Assert(vts._memoryHandle.Pointer != null); + Debug.Assert(vts._memoryHandle.Pointer != null || buffer.IsEmpty); // Queue an async ReadFile operation. if (Interop.Kernel32.ReadFile(handle, (byte*)vts._memoryHandle.Pointer, buffer.Length, IntPtr.Zero, nativeOverlapped) == 0) @@ -372,7 +372,7 @@ private static unsafe (SafeFileHandle.OverlappedValueTaskSource? vts, int errorC try { NativeOverlapped* nativeOverlapped = vts.PrepareForOperation(buffer, fileOffset, strategy); - Debug.Assert(vts._memoryHandle.Pointer != null); + Debug.Assert(vts._memoryHandle.Pointer != null || buffer.IsEmpty); // Queue an async WriteFile operation. if (Interop.Kernel32.WriteFile(handle, (byte*)vts._memoryHandle.Pointer, buffer.Length, IntPtr.Zero, nativeOverlapped) == 0)