Skip to content

Commit

Permalink
Implement API AppendAllBytes 84532 (dotnet#93519)
Browse files Browse the repository at this point in the history
* Implement AppendAllBytes API

* Add AppendAllBytes declaration

* Add unit tests for AppendAllBytes

* Add AppendAllBytesAsync tests

* Add AppendAllBytesAsync implemntation

* Add AppendAllBytesAsync definition

* Removed unnecessary seek capability

* Changed FileMode to Append to make it more readable and match the behavior

* Move AppendAllBytes to separate class

* Add extra tests to cover more scenarios

* Add tests to cover AppendAllBytesAsync

* Remove unnecessary delete test file, becuase its removed by cleanup

* Add API documentation

* Remove extra space

Co-authored-by: David Cantú <dacantu@microsoft.com>

* Remove extra space

Co-authored-by: David Cantú <dacantu@microsoft.com>

* update description of API

Co-authored-by: David Cantú <dacantu@microsoft.com>

* Update param description

Co-authored-by: David Cantú <dacantu@microsoft.com>

* Remove extra space

Co-authored-by: David Cantú <dacantu@microsoft.com>

* Remove extra space

Co-authored-by: David Cantú <dacantu@microsoft.com>

* Remove unnecessary test scenario

Co-authored-by: David Cantú <dacantu@microsoft.com>

* Remove unnecessary test scenario

Co-authored-by: David Cantú <dacantu@microsoft.com>

* Remove extra space

Co-authored-by: David Cantú <dacantu@microsoft.com>

* Fix docusmentation of API

* Apply suggestions from code review

- Remove unwanted spaces in files
- Fix API documentation
- Add assertation for file exists

Co-authored-by: David Cantú <dacantu@microsoft.com>

* Add test for scenario when directory not exists

* Fix Async documentation and match it with none-async version

* Apply suggestions from code review

* Change tabs for spaces

---------

Co-authored-by: Moji <mojtabatajik@hotmail.com>
Co-authored-by: David Cantú <dacantu@microsoft.com>
  • Loading branch information
3 people authored Oct 24, 2023
1 parent b07b990 commit f28a8a7
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 0 deletions.
108 changes: 108 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/File/AppendAllBytes.cs
Original file line number Diff line number Diff line change
@@ -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<ArgumentNullException>(() => File.AppendAllBytes(null, new byte[0]));
Assert.Throws<ArgumentNullException>(() => File.AppendAllBytes(path, null));
}

[Fact]
public void NonExistentPath()
{
Assert.Throws<DirectoryNotFoundException>(() => File.AppendAllBytes(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), new byte[0]));
}

[Fact]
public void InvalidParameters()
{
Assert.Throws<ArgumentException>(() => 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<IOException>(() => File.AppendAllBytes(path, bytes));
}
}

/// <summary>
/// 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.
/// </summary>
[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<UnauthorizedAccessException>(() => File.AppendAllBytes(path, dataToAppend));
}
}
finally
{
File.SetAttributes(path, FileAttributes.Normal);
}
}
}
}
107 changes: 107 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/File/AppendAllBytesAsync.cs
Original file line number Diff line number Diff line change
@@ -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<ArgumentNullException>("path", async () => await File.AppendAllBytesAsync(null, new byte[0]));
await Assert.ThrowsAsync<ArgumentNullException>("bytes", async () => await File.AppendAllBytesAsync(path, null));
}

[Fact]
public void NonExistentPathAsync()
{
Assert.ThrowsAsync<DirectoryNotFoundException>(() => File.AppendAllBytesAsync(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), new byte[0]));
}

[Fact]
public async Task InvalidParametersAsync()
{
await Assert.ThrowsAsync<ArgumentException>("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<IOException>(async () => await File.AppendAllBytesAsync(path, bytes));
}
}

/// <summary>
/// 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.
/// </summary>
[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<UnauthorizedAccessException>(async () => await File.AppendAllBytesAsync(path, dataToAppend));
}
}
finally
{
File.SetAttributes(path, FileAttributes.Normal);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
<Compile Include="Directory\GetLogicalDrives.cs" />
<Compile Include="FileStream\LockUnlock.cs" />
<Compile Include="FileSystemTest.cs" />
<Compile Include="File\AppendAllBytesAsync.cs" />
<Compile Include="File\EncryptDecrypt.cs" />
<Compile Include="File\AppendAllBytes.cs" />
<Compile Include="File\GetSetAttributes_String.cs" />
<Compile Include="File\GetSetAttributes_SafeFileHandle.cs" />
<Compile Include="File\GetSetTimes.cs" />
Expand Down
56 changes: 56 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/IO/File.cs
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,62 @@ public static void WriteAllBytes(string path, byte[] bytes)
RandomAccess.WriteAtOffset(sfh, bytes, 0);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="path">The file to append to.</param>
/// <param name="bytes">The bytes to append to the file.</param>
/// <exception cref="System.ArgumentException">
/// <paramref name="path"/> is a zero-length string, contains only white space, or contains one more invalid characters defined by the <see cref="System.IO.Path.GetInvalidPathChars"/> method.
/// </exception>
/// <exception cref="System.ArgumentNullException">
/// Either <paramref name="path"/> or <paramref name="bytes"/> is null.
/// </exception>
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);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="path">The file to append to.</param>
/// <param name="bytes">The bytes to append to the file.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="System.Threading.CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous append operation.</returns>
/// <exception cref="System.ArgumentException">
/// <paramref name="path"/> is a zero-length string, contains only white space, or contains one more invalid characters defined by the <see cref="System.IO.Path.GetInvalidPathChars"/> method.
/// </exception>
/// <exception cref="System.ArgumentNullException">
/// Either <paramref name="path"/> or <paramref name="bytes"/> is null.
/// </exception>
/// <exception cref="T:System.OperationCanceledException">
/// The cancellation token was canceled. This exception is stored into the returned task.
/// </exception>
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);

Expand Down
2 changes: 2 additions & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9650,6 +9650,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<string> contents) { }
public static void AppendAllLines(string path, System.Collections.Generic.IEnumerable<string> contents, System.Text.Encoding encoding) { }
public static System.Threading.Tasks.Task AppendAllLinesAsync(string path, System.Collections.Generic.IEnumerable<string> contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
Expand Down

0 comments on commit f28a8a7

Please sign in to comment.