diff --git a/src/libraries/System.IO.FileSystem/tests/File/AppendAllBytes.cs b/src/libraries/System.IO.FileSystem/tests/File/AppendAllBytes.cs new file mode 100644 index 00000000000000..9b29f971e20dfa --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/File/AppendAllBytes.cs @@ -0,0 +1,108 @@ +// 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; + +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(path, null)); + } + + [Fact] + public void NonExistentPath() + { + Assert.Throws(() => File.AppendAllBytes(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), new byte[0])); + } + + [Fact] + public void InvalidParameters() + { + Assert.Throws(() => File.AppendAllBytes(string.Empty, new byte[0])); + } + + + [Fact] + public void AppendAllBytes_WithValidInput_AppendsBytes() + { + string path = GetTestFilePath(); + + byte[] initialBytes = Encoding.UTF8.GetBytes("bytes"); + byte[] additionalBytes = Encoding.UTF8.GetBytes("additional bytes"); + + File.WriteAllBytes(path, initialBytes); + File.AppendAllBytes(path, additionalBytes); + + byte[] result = File.ReadAllBytes(path); + + byte[] expectedBytes = initialBytes.Concat(additionalBytes).ToArray(); + + Assert.True(result.SequenceEqual(expectedBytes)); + } + + + [Fact] + public void EmptyContentCreatesFile() + { + string path = GetTestFilePath(); + Assert.False(File.Exists(path)); + File.AppendAllBytes(path, new byte[0]); + Assert.True(File.Exists(path)); + Assert.Empty(File.ReadAllBytes(path)); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] + public void OpenFile_ThrowsIOException() + { + string path = GetTestFilePath(); + byte[] bytes = Encoding.UTF8.GetBytes("bytes"); + + using (File.Create(path)) + { + Assert.Throws(() => File.AppendAllBytes(path, bytes)); + } + } + + /// + /// On Unix, modifying a file that is ReadOnly will fail under normal permissions. + /// If the test is being run under the superuser, however, modification of a ReadOnly + /// file is allowed. On Windows, modifying a file that is ReadOnly will always fail. + /// + [Fact] + public void AppendToReadOnlyFileAsync() + { + string path = GetTestFilePath(); + File.Create(path).Dispose(); + File.SetAttributes(path, FileAttributes.ReadOnly); + byte[] dataToAppend = Encoding.UTF8.GetBytes("bytes"); + + try + { + if (PlatformDetection.IsNotWindows && PlatformDetection.IsPrivilegedProcess) + { + File.AppendAllBytes(path, dataToAppend); + Assert.Equal(dataToAppend, File.ReadAllBytes(path)); + } + else + { + Assert.Throws(() => File.AppendAllBytes(path, dataToAppend)); + } + } + finally + { + File.SetAttributes(path, FileAttributes.Normal); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/File/AppendAllBytesAsync.cs b/src/libraries/System.IO.FileSystem/tests/File/AppendAllBytesAsync.cs new file mode 100644 index 00000000000000..84ef880f4aa93f --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/File/AppendAllBytesAsync.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + public class File_AppendAllBytesAsync : FileSystemTest + { + + [Fact] + public async Task NullParametersAsync() + { + string path = GetTestFilePath(); + + await Assert.ThrowsAsync("path", async () => await File.AppendAllBytesAsync(null, new byte[0])); + await Assert.ThrowsAsync("bytes", async () => await File.AppendAllBytesAsync(path, null)); + } + + [Fact] + public void NonExistentPathAsync() + { + Assert.ThrowsAsync(() => File.AppendAllBytesAsync(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), new byte[0])); + } + + [Fact] + public async Task InvalidParametersAsync() + { + await Assert.ThrowsAsync("path", async () => await File.AppendAllBytesAsync(string.Empty, new byte[0])); + } + + [Fact] + public async Task AppendAllBytesAsync_WithValidInput_AppendsBytes() + { + string path = GetTestFilePath(); + + byte[] initialBytes = Encoding.UTF8.GetBytes("bytes"); + byte[] additionalBytes = Encoding.UTF8.GetBytes("additional bytes"); + + await File.WriteAllBytesAsync(path, initialBytes); + await File.AppendAllBytesAsync(path, additionalBytes); + + byte[] result = await File.ReadAllBytesAsync(path); + + byte[] expectedBytes = initialBytes.Concat(additionalBytes).ToArray(); + + Assert.True(result.SequenceEqual(expectedBytes)); + } + + [Fact] + public async Task EmptyContentCreatesFileAsync() + { + string path = GetTestFilePath(); + await File.AppendAllBytesAsync(path, new byte[0]); + Assert.True(File.Exists(path)); + Assert.Empty(await File.ReadAllBytesAsync(path)); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] + public async Task OpenFile_ThrowsIOExceptionAsync() + { + string path = GetTestFilePath(); + byte[] bytes = Encoding.UTF8.GetBytes("bytes"); + + using (File.Create(path)) + { + await Assert.ThrowsAsync(async () => await File.AppendAllBytesAsync(path, bytes)); + } + } + + /// + /// On Unix, modifying a file that is ReadOnly will fail under normal permissions. + /// If the test is being run under the superuser, however, modification of a ReadOnly + /// file is allowed. On Windows, modifying a file that is ReadOnly will always fail. + /// + [Fact] + public async Task AppendToReadOnlyFileAsync() + { + string path = GetTestFilePath(); + File.Create(path).Dispose(); + File.SetAttributes(path, FileAttributes.ReadOnly); + byte[] dataToAppend = Encoding.UTF8.GetBytes("bytes"); + + try + { + if (PlatformDetection.IsNotWindows && PlatformDetection.IsPrivilegedProcess) + { + await File.AppendAllBytesAsync(path, dataToAppend); + Assert.Equal(dataToAppend, await File.ReadAllBytesAsync(path)); + } + else + { + await Assert.ThrowsAsync(async () => await File.AppendAllBytesAsync(path, dataToAppend)); + } + } + finally + { + File.SetAttributes(path, FileAttributes.Normal); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index 069f1cc08949a9..738960903f5045 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -33,7 +33,9 @@ + + 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 84e8aa18df8a04..ec17866f079217 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -689,6 +689,62 @@ public static void WriteAllBytes(string path, byte[] bytes) RandomAccess.WriteAtOffset(sfh, bytes, 0); } + /// + /// 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 a zero-length string, contains only white space, or contains one more invalid characters defined by the method. + /// + /// + /// Either or is null. + /// + public static void AppendAllBytes(string path, byte[] bytes) + { + ArgumentException.ThrowIfNullOrEmpty(path); + ArgumentNullException.ThrowIfNull(bytes); + + using SafeFileHandle fileHandle = OpenHandle(path, FileMode.Append, FileAccess.Write, FileShare.Read); + long fileOffset = RandomAccess.GetLength(fileHandle); + RandomAccess.WriteAtOffset(fileHandle, bytes, fileOffset); + } + + /// + /// 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 . + /// 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)) + { + ArgumentException.ThrowIfNullOrEmpty(path); + ArgumentNullException.ThrowIfNull(bytes); + + return cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : Core(path, bytes, cancellationToken); + + static async Task Core(string path, byte[] bytes, CancellationToken cancellationToken) + { + using SafeFileHandle fileHandle = OpenHandle(path, FileMode.Append, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous); + long fileOffset = RandomAccess.GetLength(fileHandle); + await RandomAccess.WriteAtOffsetAsync(fileHandle, bytes, fileOffset, cancellationToken).ConfigureAwait(false); + } + } + public static string[] ReadAllLines(string path) => ReadAllLines(path, Encoding.UTF8); diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 2c522cb01bdb5d..605def50c04a14 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -9638,6 +9638,8 @@ public EnumerationOptions() { } } public static partial class File { + public static void AppendAllBytes(string path, byte[] bytes) { } + public static System.Threading.Tasks.Task AppendAllBytesAsync(string path, byte[] bytes, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { 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; }