diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index ab6dc786..b8b388f7 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -185,21 +185,11 @@ public partial class GZipArchive CancellationToken cancellationToken = default ) { - var header = ArrayPool.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.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; } } diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index 614c4408..6f336503 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -173,38 +173,22 @@ public partial class SevenZipArchive private static ReadOnlySpan Signature => [(byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C]; - private static bool SignatureMatch(Stream stream) - { - var buffer = ArrayPool.Shared.Rent(6); - try - { - stream.ReadExact(buffer, 0, 6); - return buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature); - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } + private static bool SignatureMatch(Stream stream) => + stream.WithRentedBufferReadExact( + 6, + buffer => buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature) + ); private static async ValueTask SignatureMatchAsync( Stream stream, CancellationToken cancellationToken ) { - var buffer = ArrayPool.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.Shared.Return(buffer); - } + var result = await stream.TryWithRentedBufferReadFullyAsync( + 6, + buffer => buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature), + cancellationToken + ); + return result.success && result.result; } } diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs index deb4dd10..75c77ce2 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs @@ -16,15 +16,19 @@ internal partial class WinzipAesCryptoStream return; } _isDisposed = true; - // Read out last 10 auth bytes asynchronously - byte[] authBytes = ArrayPool.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.Shared.Return(authBytes); await _stream.DisposeAsync().ConfigureAwait(false); } } diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs index 167f1461..bddfbed5 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs @@ -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.Shared.Rent(10); - try - { - _stream.ReadFullyAsync(ten, 0, 10).GetAwaiter().GetResult(); - } - finally - { - ArrayPool.Shared.Return(ten); - } + _stream + .WithRentedBufferReadFullyAsync( + 10, + _ => 0, // Just consume the bytes, don't need the result + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); } else { diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index 75558ce7..ef6bfe2a 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -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.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.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.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.Shared.Return(buffer); - } + return await stream.WithRentedBufferReadExactAsync( + 2, + buffer => buffer[0] == 0x1A && buffer[1] < 10, //rather thin, but this is all we have + cancellationToken + ); } } diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs index e8e6b0c3..3e95dbf9 100644 --- a/src/SharpCompress/Utility.Async.cs +++ b/src/SharpCompress/Utility.Async.cs @@ -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 { + /// + /// 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. + /// + /// The return type of the callback + /// The stream to read from + /// The size of the buffer to rent and read + /// The callback to execute with the rented buffer + /// Cancellation token + /// The result of the callback + public static async ValueTask WithRentedBufferReadFullyAsync( + this Stream stream, + int size, + Func callback, + CancellationToken cancellationToken = default + ) + { + var buffer = ArrayPool.Shared.Rent(size); + try + { + if ( + !await stream + .ReadFullyAsync(buffer, 0, size, cancellationToken) + .ConfigureAwait(false) + ) + { + throw new EndOfStreamException(); + } + return callback(buffer); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// 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. + /// + /// The return type of the callback + /// The stream to read from + /// The size of the buffer to rent and read + /// The callback to execute with the rented buffer + /// Cancellation token + /// A tuple containing success status and the result + public static async ValueTask<(bool success, T result)> TryWithRentedBufferReadFullyAsync( + this Stream stream, + int size, + Func callback, + CancellationToken cancellationToken = default + ) + { + var buffer = ArrayPool.Shared.Rent(size); + try + { + if ( + !await stream + .ReadFullyAsync(buffer, 0, size, cancellationToken) + .ConfigureAwait(false) + ) + { + return (false, default!); + } + return (true, callback(buffer)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// 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. + /// + /// The return type of the callback + /// The stream to read from + /// The size of the buffer to rent and read + /// The callback to execute with the rented buffer + /// Cancellation token + /// The result of the callback + public static async ValueTask WithRentedBufferReadExactAsync( + this Stream stream, + int size, + Func callback, + CancellationToken cancellationToken = default + ) + { + var buffer = ArrayPool.Shared.Rent(size); + try + { + await stream.ReadExactAsync(buffer, 0, size, cancellationToken).ConfigureAwait(false); + return callback(buffer); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + extension(Stream source) { /// diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index de3a1ddc..7f21a755 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -138,6 +138,97 @@ internal static partial class Utility return sTime.AddSeconds(unixtime); } + /// + /// 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. + /// + /// The return type of the callback + /// The stream to read from + /// The size of the buffer to rent and read + /// The callback to execute with the rented buffer + /// The result of the callback + public static T WithRentedBufferReadFully( + this Stream stream, + int size, + Func callback + ) + { + var buffer = ArrayPool.Shared.Rent(size); + try + { + if (!stream.ReadFully(buffer.AsSpan(0, size))) + { + throw new EndOfStreamException(); + } + return callback(buffer); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// 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. + /// + /// The return type of the callback + /// The stream to read from + /// The size of the buffer to rent and read + /// The callback to execute with the rented buffer + /// The result of the callback, or default if read failed + /// True if the read was successful, false if end of stream was reached + public static bool TryWithRentedBufferReadFully( + this Stream stream, + int size, + Func callback, + out T result + ) + { + var buffer = ArrayPool.Shared.Rent(size); + try + { + if (!stream.ReadFully(buffer.AsSpan(0, size))) + { + result = default!; + return false; + } + result = callback(buffer); + return true; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// 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. + /// + /// The return type of the callback + /// The stream to read from + /// The size of the buffer to rent and read + /// The callback to execute with the rented buffer + /// The result of the callback + public static T WithRentedBufferReadExact( + this Stream stream, + int size, + Func callback + ) + { + var buffer = ArrayPool.Shared.Rent(size); + try + { + stream.ReadExact(buffer, 0, size); + return callback(buffer); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + extension(Stream source) { public long TransferTo(Stream destination, long maxLength) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 29e7a1bd..27e9e496 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -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",