From 54640548ed191980a09e0eb34834ec52a74e460e Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 3 Jan 2026 15:47:10 +0000 Subject: [PATCH 1/6] Consolidate reads --- .../Compressors/Xz/MultiByteIntegers.cs | 37 +- .../Polyfills/BinaryReaderExtensions.cs | 58 +++ .../Polyfills/StreamExtensions.cs | 129 ++++-- src/SharpCompress/Utility.cs | 378 ++++++++---------- tests/SharpCompress.Test/UtilityTests.cs | 92 +++++ 5 files changed, 405 insertions(+), 289 deletions(-) create mode 100644 src/SharpCompress/Polyfills/BinaryReaderExtensions.cs 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..a030d4bc3 --- /dev/null +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -0,0 +1,58 @@ +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 = ArrayPool.Shared.Rent(1); + try + { + var bytesRead = await reader + .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + if (bytesRead != 1) + { + throw new EndOfStreamException(); + } + + return buffer[0]; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public async Task ReadBytesAsync( + int count, + CancellationToken cancellationToken = default + ) + { + var buffer = ArrayPool.Shared.Rent(count); + try + { + var bytesRead = await reader + .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + if (bytesRead != count) + { + throw new EndOfStreamException(); + } + var bytes = new byte[count]; + System.Buffer.BlockCopy(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..bd41ac75d 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,112 @@ 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); + if (stream.CanSeek) + { + stream.Position += advanceAmount; + return; + } - temp.AsSpan(0, read).CopyTo(buffer); + 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; + } + } - return read; + public void Skip() + { + using var buffer = MemoryPool.Shared.Rent(Utility.TEMP_BUFFER_SIZE); + while (stream.Read(buffer.Memory.Span) > 0) { } } - finally + + public async Task SkipAsync(CancellationToken cancellationToken = default) { - ArrayPool.Shared.Return(temp); + 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); + } } - } - internal static void Write(this Stream stream, ReadOnlySpan buffer) - { - var temp = ArrayPool.Shared.Rent(buffer.Length); + internal int Read(Span buffer) + { + var temp = ArrayPool.Shared.Rent(buffer.Length); - buffer.CopyTo(temp); + try + { + var read = stream.Read(temp, 0, buffer.Length); - try - { - stream.Write(temp, 0, buffer.Length); + temp.AsSpan(0, read).CopyTo(buffer); + + return read; + } + finally + { + ArrayPool.Shared.Return(temp); + } } - finally + + internal void Write(ReadOnlySpan buffer) { - ArrayPool.Shared.Return(temp); + var temp = ArrayPool.Shared.Rent(buffer.Length); + + buffer.CopyTo(temp); + + try + { + stream.Write(temp, 0, buffer.Length); + } + 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 async Task ReadExactlyAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) { - var read = await stream - .ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken) - .ConfigureAwait(false); - if (read == 0) + var totalRead = 0; + while (totalRead < count) { - throw new EndOfStreamException(); + var read = await stream + .ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken) + .ConfigureAwait(false); + if (read == 0) + { + throw new EndOfStreamException(); + } + totalRead += read; } - totalRead += read; } } } - -#endif diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index 0c648f12d..ea0faa2a0 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,83 +129,85 @@ 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) + var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); + try { - 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; + 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; + } + finally + { + ArrayPool.Shared.Return(array); } - return total; - } - finally - { - ArrayPool.Shared.Return(array); } - } - public static async Task TransferToAsync( - this Stream source, - Stream destination, - long maxLength, - CancellationToken cancellationToken = default - ) - { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try + public async Task TransferToAsync( + Stream destination, + long maxLength, + CancellationToken cancellationToken = default + ) { - var maxReadSize = array.Length; - long total = 0; - var remaining = maxLength; - if (remaining < maxReadSize) - { - maxReadSize = (int)remaining; - } - 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) - { - break; - } - remaining -= count; + var maxReadSize = array.Length; + long total = 0; + var remaining = maxLength; if (remaining < maxReadSize) { maxReadSize = (int)remaining; } + while ( + await ReadTransferBlockAsync(source, array, maxReadSize, cancellationToken) + .ConfigureAwait(false) + is var (success, count) + && success + ) + { + await destination + .WriteAsync(array, 0, count, cancellationToken) + .ConfigureAwait(false); + total += count; + if (remaining - count < 0) + { + break; + } + remaining -= count; + if (remaining < maxReadSize) + { + maxReadSize = (int)remaining; + } + } + return total; + } + finally + { + ArrayPool.Shared.Return(array); } - return total; - } - finally - { - ArrayPool.Shared.Return(array); } } @@ -288,37 +238,119 @@ CancellationToken cancellationToken return (count != 0, count); } - public static async Task SkipAsync( - this Stream source, - long advanceAmount, - CancellationToken cancellationToken = default - ) + extension(Stream source) { - if (source.CanSeek) + public async Task SkipAsync( + long advanceAmount, + CancellationToken cancellationToken = default + ) { - source.Position += advanceAmount; - return; + if (source.CanSeek) + { + source.Position += advanceAmount; + return; + } + + var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); + try + { + while (advanceAmount > 0) + { + 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; + } + } + finally + { + ArrayPool.Shared.Return(array); + } } - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try + public bool ReadFully(byte[] buffer) { - while (advanceAmount > 0) + var total = 0; + int read; + while ((read = source.Read(buffer, total, buffer.Length - total)) > 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 bool ReadFully(Span buffer) { - ArrayPool.Shared.Return(array); + 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); + } + + public async Task ReadFullyAsync( + byte[] buffer, + CancellationToken cancellationToken = default + ) + { + var total = 0; + int read; + while ( + ( + read = await source + .ReadAsync(buffer, total, buffer.Length - total, cancellationToken) + .ConfigureAwait(false) + ) > 0 + ) + { + total += read; + if (total >= buffer.Length) + { + return true; + } + } + return (total >= buffer.Length); + } + + public async Task ReadFullyAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + 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); } } @@ -350,89 +382,9 @@ public static bool ReadFully(this Stream stream, Span buffer) } } #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) - { - total += read; - if (total >= buffer.Length) - { - return true; - } - } - 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) - { - total += read; - if (total >= buffer.Length) - { - return true; - } - } - return (total >= buffer.Length); - } #endif - public static async Task ReadFullyAsync( - this Stream stream, - byte[] buffer, - CancellationToken cancellationToken = default - ) - { - var total = 0; - int read; - while ( - ( - read = await stream - .ReadAsync(buffer, total, buffer.Length - total, cancellationToken) - .ConfigureAwait(false) - ) > 0 - ) - { - total += read; - if (total >= buffer.Length) - { - return true; - } - } - 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 - ) - { - total += read; - if (total >= count) - { - return true; - } - } - 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] From 1a71c01fd4734741997cc6ae18ec6bd9fe45914b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:58:27 +0000 Subject: [PATCH 2/6] Consolidate ReadExact and ReadFully methods into Utility.cs Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Common/AsyncBinaryReader.cs | 32 +---- .../Compressors/LZMA/Utilites/Utils.cs | 35 ------ .../Polyfills/BinaryReaderExtensions.cs | 45 ++----- .../Polyfills/StreamExtensions.cs | 19 +-- src/SharpCompress/Utility.cs | 115 +++++++++++++++--- 5 files changed, 117 insertions(+), 129 deletions(-) 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/Polyfills/BinaryReaderExtensions.cs b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs index a030d4bc3..d34771cff 100644 --- a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -11,23 +11,11 @@ public static class BinaryReaderExtensions { public async Task ReadByteAsync(CancellationToken cancellationToken = default) { - var buffer = ArrayPool.Shared.Rent(1); - try - { - var bytesRead = await reader - .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - if (bytesRead != 1) - { - throw new EndOfStreamException(); - } - - return buffer[0]; - } - finally - { - ArrayPool.Shared.Return(buffer); - } + var buffer = new byte[1]; + await reader + .BaseStream.ReadExactAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + return buffer[0]; } public async Task ReadBytesAsync( @@ -35,24 +23,11 @@ public async Task ReadBytesAsync( CancellationToken cancellationToken = default ) { - var buffer = ArrayPool.Shared.Rent(count); - try - { - var bytesRead = await reader - .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - if (bytesRead != count) - { - throw new EndOfStreamException(); - } - var bytes = new byte[count]; - System.Buffer.BlockCopy(buffer, 0, bytes, 0, count); - return bytes; - } - finally - { - ArrayPool.Shared.Return(buffer); - } + var bytes = new byte[count]; + await reader + .BaseStream.ReadExactAsync(bytes, 0, count, cancellationToken) + .ConfigureAwait(false); + return bytes; } } } diff --git a/src/SharpCompress/Polyfills/StreamExtensions.cs b/src/SharpCompress/Polyfills/StreamExtensions.cs index bd41ac75d..ab617e954 100644 --- a/src/SharpCompress/Polyfills/StreamExtensions.cs +++ b/src/SharpCompress/Polyfills/StreamExtensions.cs @@ -98,20 +98,9 @@ internal async Task ReadExactlyAsync( int offset, int count, CancellationToken cancellationToken - ) - { - var totalRead = 0; - while (totalRead < count) - { - var read = await stream - .ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken) - .ConfigureAwait(false); - if (read == 0) - { - throw new EndOfStreamException(); - } - totalRead += read; - } - } + ) => + await stream + .ReadExactAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); } } diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index ea0faa2a0..4db9d3408 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -273,6 +273,33 @@ public async Task SkipAsync( } } +#if NET60_OR_GREATER + public bool ReadFully(byte[] buffer) + { + try + { + source.ReadExactly(buffer); + return true; + } + catch (EndOfStreamException) + { + return false; + } + } + + public bool ReadFully(Span buffer) + { + try + { + source.ReadExactly(buffer); + return true; + } + catch (EndOfStreamException) + { + return false; + } + } +#else public bool ReadFully(byte[] buffer) { var total = 0; @@ -302,6 +329,7 @@ public bool ReadFully(Span buffer) } return (total >= buffer.Length); } +#endif public async Task ReadFullyAsync( byte[] buffer, @@ -354,36 +382,89 @@ public async Task ReadFullyAsync( } } -#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) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0 || offset > buffer.Length) { - stream.ReadExactly(buffer); - return true; + throw new ArgumentOutOfRangeException(nameof(offset)); } - catch (EndOfStreamException) + + if (length < 0 || length > buffer.Length - offset) { - return false; + 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; } } - public static bool ReadFully(this Stream stream, Span buffer) + /// + /// 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 + ) { - try + if (stream is null) { - stream.ReadExactly(buffer); - return true; + throw new ArgumentNullException(nameof(stream)); } - catch (EndOfStreamException) + + if (buffer is null) { - return false; + throw new ArgumentNullException(nameof(buffer)); } - } -#else -#endif + 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 = await stream + .ReadAsync(buffer, offset, length, cancellationToken) + .ConfigureAwait(false); + if (fetched <= 0) + { + throw new EndOfStreamException(); + } + + offset += fetched; + length -= fetched; + } + } public static string TrimNulls(this string source) => source.Replace('\0', ' ').Trim(); From 05642cbdc6dbce6983fdf32b3329f3db1e296024 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 17:12:43 +0000 Subject: [PATCH 3/6] Use ArrayPool for temporary buffers in BinaryReaderExtensions Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Polyfills/BinaryReaderExtensions.cs | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs index d34771cff..dbf17c25b 100644 --- a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -11,11 +11,18 @@ public static class BinaryReaderExtensions { 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]; + var buffer = ArrayPool.Shared.Rent(1); + try + { + await reader + .BaseStream.ReadExactAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + return buffer[0]; + } + finally + { + ArrayPool.Shared.Return(buffer); + } } public async Task ReadBytesAsync( @@ -23,11 +30,20 @@ public async Task ReadBytesAsync( CancellationToken cancellationToken = default ) { - var bytes = new byte[count]; - await reader - .BaseStream.ReadExactAsync(bytes, 0, count, cancellationToken) - .ConfigureAwait(false); - return bytes; + var buffer = ArrayPool.Shared.Rent(count); + try + { + await reader + .BaseStream.ReadExactAsync(buffer, 0, count, cancellationToken) + .ConfigureAwait(false); + var bytes = new byte[count]; + System.Array.Copy(buffer, 0, bytes, 0, count); + return bytes; + } + finally + { + ArrayPool.Shared.Return(buffer); + } } } } From 372ecb77d0a92e599a8735eee5fa6d3ec1d4f876 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 17:17:59 +0000 Subject: [PATCH 4/6] Use threshold-based ArrayPool strategy for BinaryReaderExtensions Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Polyfills/BinaryReaderExtensions.cs | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs index dbf17c25b..d3c517a7b 100644 --- a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Buffers; using System.IO; using System.Threading; @@ -11,18 +12,11 @@ public static class BinaryReaderExtensions { public async Task ReadByteAsync(CancellationToken cancellationToken = default) { - var buffer = ArrayPool.Shared.Rent(1); - try - { - await reader - .BaseStream.ReadExactAsync(buffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - return buffer[0]; - } - finally - { - ArrayPool.Shared.Return(buffer); - } + var buffer = new byte[1]; + await reader + .BaseStream.ReadExactAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + return buffer[0]; } public async Task ReadBytesAsync( @@ -30,6 +24,18 @@ public async Task ReadBytesAsync( CancellationToken cancellationToken = default ) { + // 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 { @@ -37,7 +43,7 @@ await reader .BaseStream.ReadExactAsync(buffer, 0, count, cancellationToken) .ConfigureAwait(false); var bytes = new byte[count]; - System.Array.Copy(buffer, 0, bytes, 0, count); + Array.Copy(buffer, 0, bytes, 0, count); return bytes; } finally From 77015224f6be8a6524c604bb8810953fa14780ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 17:19:55 +0000 Subject: [PATCH 5/6] Add input validation for ReadBytesAsync count parameter Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Polyfills/BinaryReaderExtensions.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs index d3c517a7b..da4b3eea5 100644 --- a/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs +++ b/src/SharpCompress/Polyfills/BinaryReaderExtensions.cs @@ -24,6 +24,16 @@ public async Task ReadBytesAsync( 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) From 9bd86f64c927e474c29b70185c8b80af667f62a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 18:41:02 +0000 Subject: [PATCH 6/6] Replace manual TransferTo implementation with Stream.CopyTo framework methods Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Utility.cs | 104 ++++------------------------------- 1 file changed, 10 insertions(+), 94 deletions(-) diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index 4db9d3408..c8c7a73ce 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -133,36 +133,10 @@ public static DateTime UnixTimeToDateTime(long unixtime) { public long TransferTo(Stream destination, long maxLength) { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try - { - 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; - } - finally - { - ArrayPool.Shared.Return(array); - } + // 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; } public async Task TransferToAsync( @@ -171,71 +145,13 @@ public async Task TransferToAsync( CancellationToken cancellationToken = default ) { - var array = ArrayPool.Shared.Rent(TEMP_BUFFER_SIZE); - try - { - var maxReadSize = array.Length; - long total = 0; - var remaining = maxLength; - if (remaining < maxReadSize) - { - maxReadSize = (int)remaining; - } - while ( - await ReadTransferBlockAsync(source, array, maxReadSize, cancellationToken) - .ConfigureAwait(false) - is var (success, count) - && success - ) - { - await destination - .WriteAsync(array, 0, count, cancellationToken) - .ConfigureAwait(false); - total += count; - if (remaining - count < 0) - { - break; - } - remaining -= count; - if (remaining < maxReadSize) - { - maxReadSize = (int)remaining; - } - } - return total; - } - finally - { - ArrayPool.Shared.Return(array); - } - } - } - - private static bool ReadTransferBlock(Stream source, byte[] array, int maxSize, out int count) - { - var size = maxSize; - if (maxSize > array.Length) - { - size = array.Length; - } - 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) - { - size = array.Length; + // 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; } - var count = await source.ReadAsync(array, 0, size, cancellationToken).ConfigureAwait(false); - return (count != 0, count); } extension(Stream source)