Add helper methods to consolidate ArrayPool+ReadFully/ReadExact patterns

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-10 11:49:51 +00:00
parent 6b9dd4e7dd
commit 64a6d752a1
8 changed files with 243 additions and 85 deletions

View File

@@ -185,21 +185,11 @@ public partial class GZipArchive
CancellationToken cancellationToken = default
)
{
var header = ArrayPool<byte>.Shared.Rent(10);
try
{
await stream.ReadFullyAsync(header, 0, 10, cancellationToken).ConfigureAwait(false);
if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
{
return false;
}
return true;
}
finally
{
ArrayPool<byte>.Shared.Return(header);
}
var result = await stream.TryWithRentedBufferReadFullyAsync(
10,
header => header[0] == 0x1F && header[1] == 0x8B && header[2] == 8,
cancellationToken
);
return result.success && result.result;
}
}

View File

@@ -173,38 +173,22 @@ public partial class SevenZipArchive
private static ReadOnlySpan<byte> Signature => [(byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C];
private static bool SignatureMatch(Stream stream)
{
var buffer = ArrayPool<byte>.Shared.Rent(6);
try
{
stream.ReadExact(buffer, 0, 6);
return buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
private static bool SignatureMatch(Stream stream) =>
stream.WithRentedBufferReadExact(
6,
buffer => buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature)
);
private static async ValueTask<bool> SignatureMatchAsync(
Stream stream,
CancellationToken cancellationToken
)
{
var buffer = ArrayPool<byte>.Shared.Rent(6);
try
{
if (!await stream.ReadFullyAsync(buffer, 0, 6, cancellationToken).ConfigureAwait(false))
{
return false;
}
return buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
var result = await stream.TryWithRentedBufferReadFullyAsync(
6,
buffer => buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature),
cancellationToken
);
return result.success && result.result;
}
}

View File

@@ -16,15 +16,19 @@ internal partial class WinzipAesCryptoStream
return;
}
_isDisposed = true;
// Read out last 10 auth bytes asynchronously
byte[] authBytes = ArrayPool<byte>.Shared.Rent(10);
try
{
await _stream.ReadFullyAsync(authBytes, 0, 10).ConfigureAwait(false);
// Read out last 10 auth bytes asynchronously
await _stream
.WithRentedBufferReadFullyAsync(
10,
_ => 0, // Just consume the bytes, don't need the result
CancellationToken.None
)
.ConfigureAwait(false);
}
finally
{
ArrayPool<byte>.Shared.Return(authBytes);
await _stream.DisposeAsync().ConfigureAwait(false);
}
}

View File

@@ -3,6 +3,7 @@ using System.Buffers;
using System.Buffers.Binary;
using System.IO;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip;
@@ -71,15 +72,14 @@ internal partial class WinzipAesCryptoStream : Stream
// Read out last 10 auth bytes - catch exceptions for async-only streams
if (Utility.UseSyncOverAsyncDispose())
{
var ten = ArrayPool<byte>.Shared.Rent(10);
try
{
_stream.ReadFullyAsync(ten, 0, 10).GetAwaiter().GetResult();
}
finally
{
ArrayPool<byte>.Shared.Return(ten);
}
_stream
.WithRentedBufferReadFullyAsync(
10,
_ => 0, // Just consume the bytes, don't need the result
CancellationToken.None
)
.GetAwaiter()
.GetResult();
}
else
{

View File

@@ -33,19 +33,11 @@ public class ArcFactory : Factory, IReaderFactory
//Hyper - archive, check the next two bytes for "HP" or "ST"(or look below for
//"HYP").Also the ZOO archiver also does put a 01Ah at the start of the file,
//see the ZOO entry below.
var buffer = ArrayPool<byte>.Shared.Rent(2);
try
{
if (stream.ReadFully(buffer.AsSpan(0, 2)))
{
return buffer[0] == 0x1A && buffer[1] < 10; //rather thin, but this is all we have
}
return false;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
return stream.TryWithRentedBufferReadFully(
2,
buffer => buffer[0] == 0x1A && buffer[1] < 10, //rather thin, but this is all we have
out var result
) && result;
}
public IReader OpenReader(Stream stream, ReaderOptions? options) =>
@@ -73,15 +65,10 @@ public class ArcFactory : Factory, IReaderFactory
//Hyper - archive, check the next two bytes for "HP" or "ST"(or look below for
//"HYP").Also the ZOO archiver also does put a 01Ah at the start of the file,
//see the ZOO entry below.
var buffer = ArrayPool<byte>.Shared.Rent(2);
try
{
await stream.ReadExactAsync(buffer, 0, 2, cancellationToken);
return buffer[0] == 0x1A && buffer[1] < 10; //rather thin, but this is all we have
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
return await stream.WithRentedBufferReadExactAsync(
2,
buffer => buffer[0] == 0x1A && buffer[1] < 10, //rather thin, but this is all we have
cancellationToken
);
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -8,6 +9,107 @@ namespace SharpCompress;
internal static partial class Utility
{
/// <summary>
/// Rents a buffer from the shared ArrayPool, reads data into it asynchronously, and executes a callback with the buffer.
/// The buffer is automatically returned to the pool after use.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <param name="stream">The stream to read from</param>
/// <param name="size">The size of the buffer to rent and read</param>
/// <param name="callback">The callback to execute with the rented buffer</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The result of the callback</returns>
public static async ValueTask<T> WithRentedBufferReadFullyAsync<T>(
this Stream stream,
int size,
Func<byte[], T> callback,
CancellationToken cancellationToken = default
)
{
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
if (
!await stream
.ReadFullyAsync(buffer, 0, size, cancellationToken)
.ConfigureAwait(false)
)
{
throw new EndOfStreamException();
}
return callback(buffer);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
/// <summary>
/// Rents a buffer from the shared ArrayPool, reads data into it asynchronously, and executes a callback with the buffer.
/// The buffer is automatically returned to the pool after use. Returns false if end of stream is reached.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <param name="stream">The stream to read from</param>
/// <param name="size">The size of the buffer to rent and read</param>
/// <param name="callback">The callback to execute with the rented buffer</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>A tuple containing success status and the result</returns>
public static async ValueTask<(bool success, T result)> TryWithRentedBufferReadFullyAsync<T>(
this Stream stream,
int size,
Func<byte[], T> callback,
CancellationToken cancellationToken = default
)
{
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
if (
!await stream
.ReadFullyAsync(buffer, 0, size, cancellationToken)
.ConfigureAwait(false)
)
{
return (false, default!);
}
return (true, callback(buffer));
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
/// <summary>
/// Rents a buffer from the shared ArrayPool, reads exactly the specified amount of data asynchronously, and executes a callback with the buffer.
/// The buffer is automatically returned to the pool after use. Throws EndOfStreamException if not enough data is available.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <param name="stream">The stream to read from</param>
/// <param name="size">The size of the buffer to rent and read</param>
/// <param name="callback">The callback to execute with the rented buffer</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The result of the callback</returns>
public static async ValueTask<T> WithRentedBufferReadExactAsync<T>(
this Stream stream,
int size,
Func<byte[], T> callback,
CancellationToken cancellationToken = default
)
{
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
await stream.ReadExactAsync(buffer, 0, size, cancellationToken).ConfigureAwait(false);
return callback(buffer);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
extension(Stream source)
{
/// <summary>

View File

@@ -138,6 +138,97 @@ internal static partial class Utility
return sTime.AddSeconds(unixtime);
}
/// <summary>
/// Rents a buffer from the shared ArrayPool, reads data into it, and executes a callback with the buffer.
/// The buffer is automatically returned to the pool after use.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <param name="stream">The stream to read from</param>
/// <param name="size">The size of the buffer to rent and read</param>
/// <param name="callback">The callback to execute with the rented buffer</param>
/// <returns>The result of the callback</returns>
public static T WithRentedBufferReadFully<T>(
this Stream stream,
int size,
Func<byte[], T> callback
)
{
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
if (!stream.ReadFully(buffer.AsSpan(0, size)))
{
throw new EndOfStreamException();
}
return callback(buffer);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
/// <summary>
/// Rents a buffer from the shared ArrayPool, reads data into it, and executes a callback with the buffer.
/// The buffer is automatically returned to the pool after use. Returns false if end of stream is reached.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <param name="stream">The stream to read from</param>
/// <param name="size">The size of the buffer to rent and read</param>
/// <param name="callback">The callback to execute with the rented buffer</param>
/// <param name="result">The result of the callback, or default if read failed</param>
/// <returns>True if the read was successful, false if end of stream was reached</returns>
public static bool TryWithRentedBufferReadFully<T>(
this Stream stream,
int size,
Func<byte[], T> callback,
out T result
)
{
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
if (!stream.ReadFully(buffer.AsSpan(0, size)))
{
result = default!;
return false;
}
result = callback(buffer);
return true;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
/// <summary>
/// Rents a buffer from the shared ArrayPool, reads exactly the specified amount of data, and executes a callback with the buffer.
/// The buffer is automatically returned to the pool after use. Throws EndOfStreamException if not enough data is available.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <param name="stream">The stream to read from</param>
/// <param name="size">The size of the buffer to rent and read</param>
/// <param name="callback">The callback to execute with the rented buffer</param>
/// <returns>The result of the callback</returns>
public static T WithRentedBufferReadExact<T>(
this Stream stream,
int size,
Func<byte[], T> callback
)
{
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
stream.ReadExact(buffer, 0, size);
return callback(buffer);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
extension(Stream source)
{
public long TransferTo(Stream destination, long maxLength)

View File

@@ -216,9 +216,9 @@
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.0, )",
"resolved": "10.0.0",
"contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA=="
"requested": "[10.0.2, )",
"resolved": "10.0.2",
"contentHash": "sXdDtMf2qcnbygw9OdE535c2lxSxrZP8gO4UhDJ0xiJbl1wIqXS1OTcTDFTIJPOFd6Mhcm8gPEthqWGUxBsTqw=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
@@ -264,9 +264,9 @@
"net8.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.22, )",
"resolved": "8.0.22",
"contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ=="
"requested": "[8.0.23, )",
"resolved": "8.0.23",
"contentHash": "GqHiB1HbbODWPbY/lc5xLQH8siEEhNA0ptpJCC6X6adtAYNEzu5ZlqV3YHA3Gh7fuEwgA8XqVwMtH2KNtuQM1Q=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",