diff --git a/src/SharpCompress/Common/AsyncBinaryReader.cs b/src/SharpCompress/Common/AsyncBinaryReader.cs index 2a6eb92cb..51da5d5cd 100644 --- a/src/SharpCompress/Common/AsyncBinaryReader.cs +++ b/src/SharpCompress/Common/AsyncBinaryReader.cs @@ -29,57 +29,35 @@ public AsyncBinaryReader(Stream stream, bool leaveOpen = false, int bufferSize = public async ValueTask ReadByteAsync(CancellationToken ct = default) { - await ReadExactAsync(_buffer, 0, 1, ct).ConfigureAwait(false); + await _stream.ReadExactAsync(_buffer, 0, 1, ct).ConfigureAwait(false); return _buffer[0]; } public async ValueTask ReadUInt16Async(CancellationToken ct = default) { - await ReadExactAsync(_buffer, 0, 2, ct).ConfigureAwait(false); + await _stream.ReadExactAsync(_buffer, 0, 2, ct).ConfigureAwait(false); return BinaryPrimitives.ReadUInt16LittleEndian(_buffer); } public async ValueTask ReadUInt32Async(CancellationToken ct = default) { - await ReadExactAsync(_buffer, 0, 4, ct).ConfigureAwait(false); + await _stream.ReadExactAsync(_buffer, 0, 4, ct).ConfigureAwait(false); return BinaryPrimitives.ReadUInt32LittleEndian(_buffer); } public async ValueTask ReadUInt64Async(CancellationToken ct = default) { - await ReadExactAsync(_buffer, 0, 8, ct).ConfigureAwait(false); + await _stream.ReadExactAsync(_buffer, 0, 8, ct).ConfigureAwait(false); return BinaryPrimitives.ReadUInt64LittleEndian(_buffer); } public async ValueTask ReadBytesAsync(int count, CancellationToken ct = default) { var result = new byte[count]; - await ReadExactAsync(result, 0, count, ct).ConfigureAwait(false); + await _stream.ReadExactAsync(result, 0, count, ct).ConfigureAwait(false); return result; } - private async ValueTask ReadExactAsync( - byte[] destination, - int offset, - int length, - CancellationToken ct - ) - { - var read = 0; - while (read < length) - { - var n = await _stream - .ReadAsync(destination, offset + read, length - read, ct) - .ConfigureAwait(false); - if (n == 0) - { - throw new EndOfStreamException(); - } - - read += n; - } - } - public void Dispose() { if (_disposed) diff --git a/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs b/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs index 19b0f3748..b57cd53f5 100644 --- a/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs +++ b/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs @@ -53,39 +53,4 @@ public static void Assert(bool expression) throw new InvalidOperationException("Assertion failed."); } } - - public static void ReadExact(this Stream stream, byte[] buffer, int offset, int length) - { - if (stream is null) - { - throw new ArgumentNullException(nameof(stream)); - } - - if (buffer is null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (length < 0 || length > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - while (length > 0) - { - var fetched = stream.Read(buffer, offset, length); - if (fetched <= 0) - { - throw new EndOfStreamException(); - } - - offset += fetched; - length -= fetched; - } - } } diff --git a/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs index f5613d661..6f7a863ba 100644 --- a/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs +++ b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs @@ -58,7 +58,7 @@ public static async Task ReadXZIntegerAsync( MaxBytes = 9; } - var LastByte = await ReadByteAsync(reader, cancellationToken).ConfigureAwait(false); + var LastByte = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); var Output = (ulong)LastByte & 0x7F; var i = 0; @@ -69,7 +69,7 @@ public static async Task ReadXZIntegerAsync( throw new InvalidFormatException(); } - LastByte = await ReadByteAsync(reader, cancellationToken).ConfigureAwait(false); + LastByte = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); if (LastByte == 0) { throw new InvalidFormatException(); @@ -79,37 +79,4 @@ public static async Task ReadXZIntegerAsync( } return Output; } - - public static async Task ReadByteAsync( - this BinaryReader reader, - CancellationToken cancellationToken = default - ) - { - var buffer = new byte[1]; - var bytesRead = await reader - .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - if (bytesRead != 1) - { - throw new EndOfStreamException(); - } - return buffer[0]; - } - - public static async Task ReadBytesAsync( - this BinaryReader reader, - int count, - CancellationToken cancellationToken = default - ) - { - var buffer = new byte[count]; - var bytesRead = await reader - .BaseStream.ReadAsync(buffer, 0, count, cancellationToken) - .ConfigureAwait(false); - if (bytesRead != count) - { - throw new EndOfStreamException(); - } - return buffer; - } } diff --git a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs new file mode 100644 index 000000000..da4b3eea5 --- /dev/null +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress; + +public static class BinaryReaderExtensions +{ + extension(BinaryReader reader) + { + public async Task ReadByteAsync(CancellationToken cancellationToken = default) + { + var buffer = new byte[1]; + await reader + .BaseStream.ReadExactAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + return buffer[0]; + } + + public async Task ReadBytesAsync( + int count, + CancellationToken cancellationToken = default + ) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Count must be non-negative."); + } + + if (count == 0) + { + return Array.Empty(); + } + + // For small allocations, direct allocation is more efficient than pooling + // due to ArrayPool overhead and the need to copy data to return array + if (count <= 256) + { + var bytes = new byte[count]; + await reader + .BaseStream.ReadExactAsync(bytes, 0, count, cancellationToken) + .ConfigureAwait(false); + return bytes; + } + + // For larger allocations, use ArrayPool to reduce GC pressure + var buffer = ArrayPool.Shared.Rent(count); + try + { + await reader + .BaseStream.ReadExactAsync(buffer, 0, count, cancellationToken) + .ConfigureAwait(false); + var bytes = new byte[count]; + Array.Copy(buffer, 0, bytes, 0, count); + return bytes; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } +} diff --git a/src/SharpCompress/Polyfills/StreamExtensions.cs b/src/SharpCompress/Polyfills/StreamExtensions.cs index f00b274a1..ab617e954 100644 --- a/src/SharpCompress/Polyfills/StreamExtensions.cs +++ b/src/SharpCompress/Polyfills/StreamExtensions.cs @@ -1,5 +1,3 @@ -#if NETFRAMEWORK || NETSTANDARD2_0 - using System; using System.Buffers; using System.IO; @@ -8,63 +6,101 @@ namespace SharpCompress; -internal static class StreamExtensions +public static class StreamExtensions { - internal static int Read(this Stream stream, Span buffer) + extension(Stream stream) { - var temp = ArrayPool.Shared.Rent(buffer.Length); - - try + public void Skip(long advanceAmount) { - var read = stream.Read(temp, 0, buffer.Length); - - temp.AsSpan(0, read).CopyTo(buffer); + if (stream.CanSeek) + { + stream.Position += advanceAmount; + return; + } - return read; + using var buffer = MemoryPool.Shared.Rent(Utility.TEMP_BUFFER_SIZE); + while (advanceAmount > 0) + { + var toRead = (int)Math.Min(buffer.Memory.Length, advanceAmount); + var read = stream.Read(buffer.Memory.Slice(0, toRead).Span); + if (read <= 0) + { + break; + } + advanceAmount -= read; + } } - finally + + public void Skip() { - ArrayPool.Shared.Return(temp); + using var buffer = MemoryPool.Shared.Rent(Utility.TEMP_BUFFER_SIZE); + while (stream.Read(buffer.Memory.Span) > 0) { } } - } - internal static void Write(this Stream stream, ReadOnlySpan buffer) - { - var temp = ArrayPool.Shared.Rent(buffer.Length); - - buffer.CopyTo(temp); - - try + public async Task SkipAsync(CancellationToken cancellationToken = default) { - stream.Write(temp, 0, buffer.Length); + var array = ArrayPool.Shared.Rent(Utility.TEMP_BUFFER_SIZE); + try + { + while (true) + { + var read = await stream + .ReadAsync(array, 0, array.Length, cancellationToken) + .ConfigureAwait(false); + if (read <= 0) + { + break; + } + } + } + finally + { + ArrayPool.Shared.Return(array); + } } - finally + + internal int Read(Span buffer) { - ArrayPool.Shared.Return(temp); + var temp = ArrayPool.Shared.Rent(buffer.Length); + + try + { + var read = stream.Read(temp, 0, buffer.Length); + + temp.AsSpan(0, read).CopyTo(buffer); + + return read; + } + finally + { + ArrayPool.Shared.Return(temp); + } } - } - internal static async Task ReadExactlyAsync( - this Stream stream, - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - var totalRead = 0; - while (totalRead < count) + internal void Write(ReadOnlySpan buffer) { - var read = await stream - .ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken) - .ConfigureAwait(false); - if (read == 0) + var temp = ArrayPool.Shared.Rent(buffer.Length); + + buffer.CopyTo(temp); + + try + { + stream.Write(temp, 0, buffer.Length); + } + finally { - throw new EndOfStreamException(); + ArrayPool.Shared.Return(temp); } - totalRead += read; } + + internal async Task ReadExactlyAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) => + await stream + .ReadExactAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); } } - -#endif diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index 0c648f12d..c8c7a73ce 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -12,7 +12,7 @@ namespace SharpCompress; internal static class Utility { //80kb is a good industry standard temporary buffer size - private const int TEMP_BUFFER_SIZE = 81920; + internal const int TEMP_BUFFER_SIZE = 81920; private static readonly HashSet invalidChars = new(Path.GetInvalidFileNameChars()); public static ReadOnlyCollection ToReadOnly(this IList items) => new(items); @@ -63,58 +63,6 @@ public static IEnumerable AsEnumerable(this T item) yield return item; } - public static void Skip(this Stream source, long advanceAmount) - { - if (source.CanSeek) - { - source.Position += advanceAmount; - return; - } - - using var buffer = MemoryPool.Shared.Rent(TEMP_BUFFER_SIZE); - while (advanceAmount > 0) - { - var toRead = (int)Math.Min(buffer.Memory.Length, advanceAmount); - var read = source.Read(buffer.Memory.Slice(0, toRead).Span); - if (read <= 0) - { - break; - } - advanceAmount -= read; - } - } - - public static void Skip(this Stream source) - { - using var buffer = MemoryPool.Shared.Rent(TEMP_BUFFER_SIZE); - while (source.Read(buffer.Memory.Span) > 0) { } - } - - public static async Task SkipAsync( - this Stream source, - CancellationToken cancellationToken = default - ) - { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try - { - while (true) - { - var read = await source - .ReadAsync(array, 0, array.Length, cancellationToken) - .ConfigureAwait(false); - if (read <= 0) - { - break; - } - } - } - finally - { - ArrayPool.Shared.Return(array); - } - } - public static DateTime DosDateToDateTime(ushort iDate, ushort iTime) { var year = (iDate / 512) + 1980; @@ -181,256 +129,257 @@ public static DateTime UnixTimeToDateTime(long unixtime) return sTime.AddSeconds(unixtime); } - public static long TransferTo(this Stream source, Stream destination, long maxLength) + extension(Stream source) { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try + public long TransferTo(Stream destination, long maxLength) { - var maxReadSize = array.Length; - long total = 0; - var remaining = maxLength; - if (remaining < maxReadSize) - { - maxReadSize = (int)remaining; - } - while (ReadTransferBlock(source, array, maxReadSize, out var count)) - { - destination.Write(array, 0, count); - total += count; - if (remaining - count < 0) - { - break; - } - remaining -= count; - if (remaining < maxReadSize) - { - maxReadSize = (int)remaining; - } - } - return total; + // Use ReadOnlySubStream to limit reading and leverage framework's CopyTo + using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); + limitedStream.CopyTo(destination, TEMP_BUFFER_SIZE); + return limitedStream.Position; } - finally + + public async Task TransferToAsync( + Stream destination, + long maxLength, + CancellationToken cancellationToken = default + ) { - ArrayPool.Shared.Return(array); + // Use ReadOnlySubStream to limit reading and leverage framework's CopyToAsync + using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); + await limitedStream + .CopyToAsync(destination, TEMP_BUFFER_SIZE, cancellationToken) + .ConfigureAwait(false); + return limitedStream.Position; } } - public static async Task TransferToAsync( - this Stream source, - Stream destination, - long maxLength, - CancellationToken cancellationToken = default - ) + extension(Stream source) { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try + public async Task SkipAsync( + long advanceAmount, + CancellationToken cancellationToken = default + ) { - var maxReadSize = array.Length; - long total = 0; - var remaining = maxLength; - if (remaining < maxReadSize) + if (source.CanSeek) { - maxReadSize = (int)remaining; + source.Position += advanceAmount; + return; } - while ( - await ReadTransferBlockAsync(source, array, maxReadSize, cancellationToken) - .ConfigureAwait(false) - is var (success, count) - && success - ) + + var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); + try { - await destination - .WriteAsync(array, 0, count, cancellationToken) - .ConfigureAwait(false); - total += count; - if (remaining - count < 0) + while (advanceAmount > 0) { - break; - } - remaining -= count; - if (remaining < maxReadSize) - { - maxReadSize = (int)remaining; + var toRead = (int)Math.Min(array.Length, advanceAmount); + var read = await source + .ReadAsync(array, 0, toRead, cancellationToken) + .ConfigureAwait(false); + if (read <= 0) + { + break; + } + advanceAmount -= read; } } - return total; + finally + { + ArrayPool.Shared.Return(array); + } } - finally + +#if NET60_OR_GREATER + public bool ReadFully(byte[] buffer) { - ArrayPool.Shared.Return(array); + try + { + source.ReadExactly(buffer); + return true; + } + catch (EndOfStreamException) + { + return false; + } } - } - private static bool ReadTransferBlock(Stream source, byte[] array, int maxSize, out int count) - { - var size = maxSize; - if (maxSize > array.Length) + public bool ReadFully(Span buffer) { - size = array.Length; + try + { + source.ReadExactly(buffer); + return true; + } + catch (EndOfStreamException) + { + return false; + } } - count = source.Read(array, 0, size); - return count != 0; - } - - private static async Task<(bool success, int count)> ReadTransferBlockAsync( - Stream source, - byte[] array, - int maxSize, - CancellationToken cancellationToken - ) - { - var size = maxSize; - if (maxSize > array.Length) +#else + public bool ReadFully(byte[] buffer) { - size = array.Length; + var total = 0; + int read; + while ((read = source.Read(buffer, total, buffer.Length - total)) > 0) + { + total += read; + if (total >= buffer.Length) + { + return true; + } + } + return (total >= buffer.Length); } - var count = await source.ReadAsync(array, 0, size, cancellationToken).ConfigureAwait(false); - return (count != 0, count); - } - public static async Task SkipAsync( - this Stream source, - long advanceAmount, - CancellationToken cancellationToken = default - ) - { - if (source.CanSeek) + public bool ReadFully(Span buffer) { - source.Position += advanceAmount; - return; + var total = 0; + int read; + while ((read = source.Read(buffer.Slice(total, buffer.Length - total))) > 0) + { + total += read; + if (total >= buffer.Length) + { + return true; + } + } + return (total >= buffer.Length); } +#endif - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try + public async Task ReadFullyAsync( + byte[] buffer, + CancellationToken cancellationToken = default + ) { - while (advanceAmount > 0) + var total = 0; + int read; + while ( + ( + read = await source + .ReadAsync(buffer, total, buffer.Length - total, cancellationToken) + .ConfigureAwait(false) + ) > 0 + ) { - var toRead = (int)Math.Min(array.Length, advanceAmount); - var read = await source - .ReadAsync(array, 0, toRead, cancellationToken) - .ConfigureAwait(false); - if (read <= 0) + total += read; + if (total >= buffer.Length) { - break; + return true; } - advanceAmount -= read; } + return (total >= buffer.Length); } - finally + + public async Task ReadFullyAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) { - ArrayPool.Shared.Return(array); + var total = 0; + int read; + while ( + ( + read = await source + .ReadAsync(buffer, offset + total, count - total, cancellationToken) + .ConfigureAwait(false) + ) > 0 + ) + { + total += read; + if (total >= count) + { + return true; + } + } + return (total >= count); } } -#if NET60_OR_GREATER - - public static bool ReadFully(this Stream stream, byte[] buffer) + /// + /// Read exactly the requested number of bytes from a stream. Throws EndOfStreamException if not enough data is available. + /// + public static void ReadExact(this Stream stream, byte[] buffer, int offset, int length) { - try + if (stream is null) { - stream.ReadExactly(buffer); - return true; + throw new ArgumentNullException(nameof(stream)); } - catch (EndOfStreamException) - { - return false; - } - } - public static bool ReadFully(this Stream stream, Span buffer) - { - try + if (buffer is null) { - stream.ReadExactly(buffer); - return true; + throw new ArgumentNullException(nameof(buffer)); } - catch (EndOfStreamException) + + if (offset < 0 || offset > buffer.Length) { - return false; + throw new ArgumentOutOfRangeException(nameof(offset)); } - } -#else - public static bool ReadFully(this Stream stream, byte[] buffer) - { - var total = 0; - int read; - while ((read = stream.Read(buffer, total, buffer.Length - total)) > 0) + + if (length < 0 || length > buffer.Length - offset) { - total += read; - if (total >= buffer.Length) - { - return true; - } + throw new ArgumentOutOfRangeException(nameof(length)); } - return (total >= buffer.Length); - } - public static bool ReadFully(this Stream stream, Span buffer) - { - var total = 0; - int read; - while ((read = stream.Read(buffer.Slice(total, buffer.Length - total))) > 0) + while (length > 0) { - total += read; - if (total >= buffer.Length) + var fetched = stream.Read(buffer, offset, length); + if (fetched <= 0) { - return true; + throw new EndOfStreamException(); } + + offset += fetched; + length -= fetched; } - return (total >= buffer.Length); } -#endif - public static async Task ReadFullyAsync( + /// + /// Read exactly the requested number of bytes from a stream asynchronously. Throws EndOfStreamException if not enough data is available. + /// + public static async Task ReadExactAsync( this Stream stream, byte[] buffer, + int offset, + int length, CancellationToken cancellationToken = default ) { - var total = 0; - int read; - while ( - ( - read = await stream - .ReadAsync(buffer, total, buffer.Length - total, cancellationToken) - .ConfigureAwait(false) - ) > 0 - ) + if (stream is null) { - total += read; - if (total >= buffer.Length) - { - return true; - } + throw new ArgumentNullException(nameof(stream)); } - return (total >= buffer.Length); - } - public static async Task ReadFullyAsync( - this Stream stream, - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) - { - var total = 0; - int read; - while ( - ( - read = await stream - .ReadAsync(buffer, offset + total, count - total, cancellationToken) - .ConfigureAwait(false) - ) > 0 - ) + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0 || offset > buffer.Length) { - total += read; - if (total >= count) + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (length < 0 || length > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + while (length > 0) + { + var fetched = await stream + .ReadAsync(buffer, offset, length, cancellationToken) + .ConfigureAwait(false); + if (fetched <= 0) { - return true; + throw new EndOfStreamException(); } + + offset += fetched; + length -= fetched; } - return (total >= count); } public static string TrimNulls(this string source) => source.Replace('\0', ' ').Trim(); diff --git a/tests/SharpCompress.Test/UtilityTests.cs b/tests/SharpCompress.Test/UtilityTests.cs index cbd573042..d533e85c0 100644 --- a/tests/SharpCompress.Test/UtilityTests.cs +++ b/tests/SharpCompress.Test/UtilityTests.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using Xunit; namespace SharpCompress.Test; @@ -157,6 +158,97 @@ public void ReadFully_Span_EmptyBuffer_ReturnsTrue() #endregion + #region ReadByteAsync Tests + + [Fact] + public async Task ReadByteAsync_ReadsOneByte() + { + var data = new byte[] { 42, 1, 2, 3 }; + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + var result = await reader.ReadByteAsync(); + + Assert.Equal(42, result); + Assert.Equal(1, stream.Position); + } + + [Fact] + public async Task ReadByteAsync_EmptyStream_ThrowsEndOfStreamException() + { + using var stream = new MemoryStream(); + using var reader = new BinaryReader(stream); + + await Assert.ThrowsAsync(async () => await reader.ReadByteAsync()); + } + + [Fact] + public async Task ReadByteAsync_MultipleReads_ReadsSequentially() + { + var data = new byte[] { 1, 2, 3 }; + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + var first = await reader.ReadByteAsync(); + var second = await reader.ReadByteAsync(); + var third = await reader.ReadByteAsync(); + + Assert.Equal(1, first); + Assert.Equal(2, second); + Assert.Equal(3, third); + } + + #endregion + + #region ReadBytesAsync Tests + + [Fact] + public async Task ReadBytesAsync_ReadsExactlyRequiredBytes() + { + var data = new byte[] { 1, 2, 3, 4, 5 }; + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + var result = await reader.ReadBytesAsync(3); + + Assert.Equal(new byte[] { 1, 2, 3 }, result); + Assert.Equal(3, stream.Position); + } + + [Fact] + public async Task ReadBytesAsync_NotEnoughData_ThrowsEndOfStreamException() + { + var data = new byte[] { 1, 2, 3 }; + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + await Assert.ThrowsAsync(async () => await reader.ReadBytesAsync(5)); + } + + [Fact] + public async Task ReadBytesAsync_EmptyStream_ThrowsEndOfStreamException() + { + using var stream = new MemoryStream(); + using var reader = new BinaryReader(stream); + + await Assert.ThrowsAsync(async () => await reader.ReadBytesAsync(1)); + } + + [Fact] + public async Task ReadBytesAsync_ZeroBytes_ReturnsEmptyArray() + { + var data = new byte[] { 1, 2, 3 }; + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + + var result = await reader.ReadBytesAsync(0); + + Assert.Empty(result); + Assert.Equal(0, stream.Position); + } + + #endregion + #region Skip Tests [Fact]