diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index b8b388f7..8e5e5401 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -180,16 +180,23 @@ public partial class GZipArchive return true; } + private readonly struct GZipSignatureTryProcessor : Utility.ITryBufferProcessor<(bool, bool)> + { + public (bool, bool) OnSuccess(ReadOnlySpan buffer) => (true, buffer[0] == 0x1F && buffer[1] == 0x8B && buffer[2] == 8); + public (bool, bool) OnFailure() => (false, false); + } + public static async ValueTask IsGZipFileAsync( Stream stream, CancellationToken cancellationToken = default ) { - var result = await stream.TryWithRentedBufferReadFullyAsync( + var processor = new GZipSignatureTryProcessor(); + var result = await stream.TryReadFullyRentedAsync( 10, - header => header[0] == 0x1F && header[1] == 0x8B && header[2] == 8, + processor, cancellationToken ); - return result.success && result.result; + return result.success && result.match; } } diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index 6f336503..9ca6dcc3 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -173,22 +173,37 @@ public partial class SevenZipArchive private static ReadOnlySpan Signature => [(byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C]; - private static bool SignatureMatch(Stream stream) => - stream.WithRentedBufferReadExact( + private readonly struct SevenZipSignatureProcessor : Utility.IBufferProcessor + { + public bool Process(ReadOnlySpan buffer) => buffer.Slice(0, 6).SequenceEqual(Signature); + } + + private readonly struct SevenZipSignatureTryProcessor : Utility.ITryBufferProcessor<(bool, bool)> + { + public (bool, bool) OnSuccess(ReadOnlySpan buffer) => (true, buffer.Slice(0, 6).SequenceEqual(Signature)); + public (bool, bool) OnFailure() => (false, false); + } + + private static bool SignatureMatch(Stream stream) + { + var processor = new SevenZipSignatureProcessor(); + return stream.ReadExactRented( 6, - buffer => buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature) + ref processor ); + } private static async ValueTask SignatureMatchAsync( Stream stream, CancellationToken cancellationToken ) { - var result = await stream.TryWithRentedBufferReadFullyAsync( + var processor = new SevenZipSignatureTryProcessor(); + var result = await stream.TryReadFullyRentedAsync( 6, - buffer => buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature), + processor, cancellationToken ); - return result.success && result.result; + return result.success && result.match; } } diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs index 75c77ce2..350acbe0 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs @@ -8,6 +8,11 @@ namespace SharpCompress.Common.Zip; internal partial class WinzipAesCryptoStream { + private readonly struct DiscardProcessor : Utility.IBufferProcessor + { + public int Process(ReadOnlySpan buffer) => 0; // Just consume the bytes, don't need the result + } + #if !LEGACY_DOTNET public override async ValueTask DisposeAsync() { @@ -19,10 +24,11 @@ internal partial class WinzipAesCryptoStream try { // Read out last 10 auth bytes asynchronously + var processor = new DiscardProcessor(); await _stream - .WithRentedBufferReadFullyAsync( + .ReadFullyRentedAsync( 10, - _ => 0, // Just consume the bytes, don't need the result + processor, CancellationToken.None ) .ConfigureAwait(false); diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs index bddfbed5..ac1a3470 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs @@ -72,10 +72,11 @@ internal partial class WinzipAesCryptoStream : Stream // Read out last 10 auth bytes - catch exceptions for async-only streams if (Utility.UseSyncOverAsyncDispose()) { + var processor = new DiscardProcessor(); _stream - .WithRentedBufferReadFullyAsync( + .ReadFullyRentedAsync( 10, - _ => 0, // Just consume the bytes, don't need the result + processor, CancellationToken.None ) .GetAwaiter() diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index ef6bfe2a..b0cf03a1 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -25,6 +25,18 @@ public class ArcFactory : Factory, IReaderFactory yield return "arc"; } + private readonly struct ArcSignatureProcessor : Utility.IBufferProcessor + { + public bool Process(ReadOnlySpan buffer) => + buffer[0] == 0x1A && buffer[1] < 10; + } + + private readonly struct ArcSignatureTryProcessor : Utility.ITryBufferProcessor<(bool, bool)> + { + public (bool, bool) OnSuccess(ReadOnlySpan buffer) => (true, buffer[0] == 0x1A && buffer[1] < 10); + public (bool, bool) OnFailure() => (false, false); + } + public override bool IsArchive(Stream stream, string? password = null) { //You may have to use some(paranoid) checks to ensure that you actually are @@ -33,11 +45,12 @@ 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. - return stream.TryWithRentedBufferReadFully( - 2, - buffer => buffer[0] == 0x1A && buffer[1] < 10, //rather thin, but this is all we have - out var result - ) && result; + var processor = new ArcSignatureTryProcessor(); + var result = stream.TryReadFullyRented( + 2, + ref processor + ); + return result.success && result.match; } public IReader OpenReader(Stream stream, ReaderOptions? options) => @@ -65,9 +78,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. - return await stream.WithRentedBufferReadExactAsync( + var processor = new ArcSignatureProcessor(); + return await stream.ReadExactRentedAsync( 2, - buffer => buffer[0] == 0x1A && buffer[1] < 10, //rather thin, but this is all we have + processor, cancellationToken ); } diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs index 3e95dbf9..2b780db2 100644 --- a/src/SharpCompress/Utility.Async.cs +++ b/src/SharpCompress/Utility.Async.cs @@ -10,21 +10,23 @@ 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. + /// Rents a buffer from the shared ArrayPool, reads data into it asynchronously, and executes a processor with the buffer. /// The buffer is automatically returned to the pool after use. /// - /// The return type of the callback + /// The processor type (struct) + /// The return type /// The stream to read from /// The size of the buffer to rent and read - /// The callback to execute with the rented buffer + /// The processor to execute with the rented buffer /// Cancellation token - /// The result of the callback - public static async ValueTask WithRentedBufferReadFullyAsync( + /// The result of the processor + public static async ValueTask ReadFullyRentedAsync( this Stream stream, int size, - Func callback, + TProcessor processor, CancellationToken cancellationToken = default ) + where TProcessor : struct, IBufferProcessor { var buffer = ArrayPool.Shared.Rent(size); try @@ -37,7 +39,7 @@ internal static partial class Utility { throw new EndOfStreamException(); } - return callback(buffer); + return processor.Process(buffer.AsSpan(0, size)); } finally { @@ -46,21 +48,23 @@ 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. Returns false if end of stream is reached. + /// Rents a buffer from the shared ArrayPool, reads data into it asynchronously, and executes a processor with the buffer. + /// The buffer is automatically returned to the pool after use. Returns failure result if end of stream is reached. /// - /// The return type of the callback + /// The processor type (struct) + /// The return type /// The stream to read from /// The size of the buffer to rent and read - /// The callback to execute with the rented buffer + /// The processor 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( + /// The result of the processor (success or failure) + public static async ValueTask TryReadFullyRentedAsync( this Stream stream, int size, - Func callback, + TProcessor processor, CancellationToken cancellationToken = default ) + where TProcessor : struct, ITryBufferProcessor { var buffer = ArrayPool.Shared.Rent(size); try @@ -71,9 +75,9 @@ internal static partial class Utility .ConfigureAwait(false) ) { - return (false, default!); + return processor.OnFailure(); } - return (true, callback(buffer)); + return processor.OnSuccess(buffer.AsSpan(0, size)); } finally { @@ -82,27 +86,29 @@ internal static partial class Utility } /// - /// Rents a buffer from the shared ArrayPool, reads exactly the specified amount of data asynchronously, and executes a callback with the buffer. + /// Rents a buffer from the shared ArrayPool, reads exactly the specified amount of data asynchronously, and executes a processor 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 processor type (struct) + /// The return type /// The stream to read from /// The size of the buffer to rent and read - /// The callback to execute with the rented buffer + /// The processor to execute with the rented buffer /// Cancellation token - /// The result of the callback - public static async ValueTask WithRentedBufferReadExactAsync( + /// The result of the processor + public static async ValueTask ReadExactRentedAsync( this Stream stream, int size, - Func callback, + TProcessor processor, CancellationToken cancellationToken = default ) + where TProcessor : struct, IBufferProcessor { var buffer = ArrayPool.Shared.Rent(size); try { await stream.ReadExactAsync(buffer, 0, size, cancellationToken).ConfigureAwait(false); - return callback(buffer); + return processor.Process(buffer.AsSpan(0, size)); } finally { diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index 7f21a755..91dbf850 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -139,19 +139,40 @@ internal static partial class Utility } /// - /// Rents a buffer from the shared ArrayPool, reads data into it, and executes a callback with the buffer. + /// Interface for struct-based buffer processors. + /// + /// The return type of the processor + internal interface IBufferProcessor + { + TResult Process(ReadOnlySpan buffer); + } + + /// + /// Interface for struct-based try buffer processors. + /// + /// The return type of the processor + internal interface ITryBufferProcessor + { + TResult OnSuccess(ReadOnlySpan buffer); + TResult OnFailure(); + } + + /// + /// Rents a buffer from the shared ArrayPool, reads data into it, and executes a processor with the buffer. /// The buffer is automatically returned to the pool after use. /// - /// The return type of the callback + /// The processor type (struct) + /// The return type /// 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( + /// The processor to execute with the rented buffer + /// The result of the processor + public static TResult ReadFullyRented( this Stream stream, int size, - Func callback + ref TProcessor processor ) + where TProcessor : struct, IBufferProcessor { var buffer = ArrayPool.Shared.Rent(size); try @@ -160,7 +181,7 @@ internal static partial class Utility { throw new EndOfStreamException(); } - return callback(buffer); + return processor.Process(buffer.AsSpan(0, size)); } finally { @@ -169,32 +190,30 @@ internal static partial class Utility } /// - /// 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. + /// Rents a buffer from the shared ArrayPool, reads data into it, and executes a processor with the buffer. + /// The buffer is automatically returned to the pool after use. Returns failure result if end of stream is reached. /// - /// The return type of the callback + /// The processor type (struct) + /// The return type /// 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( + /// The processor to execute with the rented buffer + /// The result of the processor (success or failure) + public static TResult TryReadFullyRented( this Stream stream, int size, - Func callback, - out T result + ref TProcessor processor ) + where TProcessor : struct, ITryBufferProcessor { var buffer = ArrayPool.Shared.Rent(size); try { if (!stream.ReadFully(buffer.AsSpan(0, size))) { - result = default!; - return false; + return processor.OnFailure(); } - result = callback(buffer); - return true; + return processor.OnSuccess(buffer.AsSpan(0, size)); } finally { @@ -203,25 +222,27 @@ internal static partial class Utility } /// - /// Rents a buffer from the shared ArrayPool, reads exactly the specified amount of data, and executes a callback with the buffer. + /// Rents a buffer from the shared ArrayPool, reads exactly the specified amount of data, and executes a processor 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 processor type (struct) + /// The return type /// 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( + /// The processor to execute with the rented buffer + /// The result of the processor + public static TResult ReadExactRented( this Stream stream, int size, - Func callback + ref TProcessor processor ) + where TProcessor : struct, IBufferProcessor { var buffer = ArrayPool.Shared.Rent(size); try { stream.ReadExact(buffer, 0, size); - return callback(buffer); + return processor.Process(buffer.AsSpan(0, size)); } finally { diff --git a/tests/SharpCompress.Test/UtilityTests.cs b/tests/SharpCompress.Test/UtilityTests.cs index 1a2e29db..20957b06 100644 --- a/tests/SharpCompress.Test/UtilityTests.cs +++ b/tests/SharpCompress.Test/UtilityTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -798,164 +799,214 @@ public class UtilityTests #endregion - #region WithRentedBufferReadFully Tests + #region Rented Buffer Tests + + private readonly struct SumProcessor : Utility.IBufferProcessor + { + public int Process(ReadOnlySpan buffer) + { + int sum = 0; + for (int i = 0; i < buffer.Length; i++) + { + sum += buffer[i]; + } + return sum; + } + } + + private readonly struct IdentityProcessor : Utility.IBufferProcessor + { + public bool Process(ReadOnlySpan buffer) => buffer.Length > 0 && buffer[0] != 0; + } + + private readonly struct ArcLikeTryProcessor : Utility.ITryBufferProcessor<(bool, bool)> + { + public (bool, bool) OnSuccess(ReadOnlySpan buffer) => + (true, buffer[0] == 0x1A && buffer[1] < 10); + + public (bool, bool) OnFailure() => (false, false); + } + + private readonly struct AlwaysTrueTryProcessor : Utility.ITryBufferProcessor<(bool, bool)> + { + public (bool, bool) OnSuccess(ReadOnlySpan buffer) => (true, true); + + public (bool, bool) OnFailure() => (false, false); + } [Fact] - public void WithRentedBufferReadFully_ReadsAndProcessesData() + public void ReadFullyRented_ReadsAndProcessesData() { var data = new byte[] { 1, 2, 3, 4, 5 }; using var stream = new MemoryStream(data); - var sum = stream.WithRentedBufferReadFully( - 5, - buffer => buffer[0] + buffer[1] + buffer[2] + buffer[3] + buffer[4] - ); + var processor = new SumProcessor(); + var sum = stream.ReadFullyRented(5, ref processor); Assert.Equal(15, sum); } [Fact] - public void WithRentedBufferReadFully_NotEnoughData_ThrowsEndOfStreamException() + public void ReadFullyRented_NotEnoughData_ThrowsEndOfStreamException() { var data = new byte[] { 1, 2, 3 }; using var stream = new MemoryStream(data); + var processor = new IdentityProcessor(); Assert.Throws(() => - stream.WithRentedBufferReadFully(5, buffer => buffer[0]) + stream.ReadFullyRented(5, ref processor) ); } [Fact] - public void TryWithRentedBufferReadFully_SuccessfulRead_ReturnsTrue() + public void TryReadFullyRented_SuccessfulRead_ReturnsTrue() { var data = new byte[] { 0x1A, 0x05 }; using var stream = new MemoryStream(data); - var success = stream.TryWithRentedBufferReadFully( + var processor = new ArcLikeTryProcessor(); + var result = stream.TryReadFullyRented( 2, - buffer => buffer[0] == 0x1A && buffer[1] < 10, - out var result + ref processor ); - Assert.True(success); - Assert.True(result); + Assert.True(result.success); + Assert.True(result.match); } [Fact] - public void TryWithRentedBufferReadFully_NotEnoughData_ReturnsFalse() + public void TryReadFullyRented_NotEnoughData_ReturnsFalse() { var data = new byte[] { 1, 2, 3 }; using var stream = new MemoryStream(data); - var success = stream.TryWithRentedBufferReadFully(5, buffer => true, out var result); + var processor = new AlwaysTrueTryProcessor(); + var result = stream.TryReadFullyRented( + 5, + ref processor + ); - Assert.False(success); - Assert.False(result); + Assert.False(result.success); + Assert.False(result.match); } [Fact] - public void WithRentedBufferReadExact_ReadsAndProcessesData() + public void ReadExactRented_ReadsAndProcessesData() { var data = new byte[] { 1, 2, 3, 4, 5 }; using var stream = new MemoryStream(data); - var sum = stream.WithRentedBufferReadExact( - 5, - buffer => buffer[0] + buffer[1] + buffer[2] + buffer[3] + buffer[4] - ); + var processor = new SumProcessor(); + var sum = stream.ReadExactRented(5, ref processor); Assert.Equal(15, sum); } [Fact] - public void WithRentedBufferReadExact_NotEnoughData_ThrowsEndOfStreamException() + public void ReadExactRented_NotEnoughData_ThrowsEndOfStreamException() { var data = new byte[] { 1, 2, 3 }; using var stream = new MemoryStream(data); + var processor = new IdentityProcessor(); Assert.Throws(() => - stream.WithRentedBufferReadExact(5, buffer => buffer[0]) + stream.ReadExactRented(5, ref processor) ); } - #endregion - - #region WithRentedBufferReadFullyAsync Tests - [Fact] - public async ValueTask WithRentedBufferReadFullyAsync_ReadsAndProcessesData() + public async ValueTask ReadFullyRentedAsync_ReadsAndProcessesData() { var data = new byte[] { 1, 2, 3, 4, 5 }; using var stream = new MemoryStream(data); - var sum = await stream.WithRentedBufferReadFullyAsync( + var processor = new SumProcessor(); + var sum = await stream.ReadFullyRentedAsync( 5, - buffer => buffer[0] + buffer[1] + buffer[2] + buffer[3] + buffer[4] + processor, + CancellationToken.None ); Assert.Equal(15, sum); } [Fact] - public async ValueTask WithRentedBufferReadFullyAsync_NotEnoughData_ThrowsEndOfStreamException() + public async ValueTask ReadFullyRentedAsync_NotEnoughData_ThrowsEndOfStreamException() { var data = new byte[] { 1, 2, 3 }; using var stream = new MemoryStream(data); + var processor = new IdentityProcessor(); await Assert.ThrowsAsync(async () => - await stream.WithRentedBufferReadFullyAsync(5, buffer => buffer[0]) + await stream.ReadFullyRentedAsync( + 5, + processor, + CancellationToken.None + ) ); } [Fact] - public async ValueTask TryWithRentedBufferReadFullyAsync_SuccessfulRead_ReturnsTrue() + public async ValueTask TryReadFullyRentedAsync_SuccessfulRead_ReturnsTrue() { var data = new byte[] { 0x1A, 0x05 }; using var stream = new MemoryStream(data); - var (success, result) = await stream.TryWithRentedBufferReadFullyAsync( - 2, - buffer => buffer[0] == 0x1A && buffer[1] < 10 - ); + var processor = new ArcLikeTryProcessor(); + var result = await stream.TryReadFullyRentedAsync< + ArcLikeTryProcessor, + (bool success, bool match) + >(2, processor, CancellationToken.None); - Assert.True(success); - Assert.True(result); + Assert.True(result.success); + Assert.True(result.match); } [Fact] - public async ValueTask TryWithRentedBufferReadFullyAsync_NotEnoughData_ReturnsFalse() + public async ValueTask TryReadFullyRentedAsync_NotEnoughData_ReturnsFalse() { var data = new byte[] { 1, 2, 3 }; using var stream = new MemoryStream(data); - var (success, result) = await stream.TryWithRentedBufferReadFullyAsync(5, buffer => true); + var processor = new AlwaysTrueTryProcessor(); + var result = await stream.TryReadFullyRentedAsync< + AlwaysTrueTryProcessor, + (bool success, bool match) + >(5, processor, CancellationToken.None); - Assert.False(success); - Assert.False(result); + Assert.False(result.success); + Assert.False(result.match); } [Fact] - public async ValueTask WithRentedBufferReadExactAsync_ReadsAndProcessesData() + public async ValueTask ReadExactRentedAsync_ReadsAndProcessesData() { var data = new byte[] { 1, 2, 3, 4, 5 }; using var stream = new MemoryStream(data); - var sum = await stream.WithRentedBufferReadExactAsync( + var processor = new SumProcessor(); + var sum = await stream.ReadExactRentedAsync( 5, - buffer => buffer[0] + buffer[1] + buffer[2] + buffer[3] + buffer[4] + processor, + CancellationToken.None ); Assert.Equal(15, sum); } [Fact] - public async ValueTask WithRentedBufferReadExactAsync_NotEnoughData_ThrowsEndOfStreamException() + public async ValueTask ReadExactRentedAsync_NotEnoughData_ThrowsEndOfStreamException() { var data = new byte[] { 1, 2, 3 }; using var stream = new MemoryStream(data); + var processor = new IdentityProcessor(); await Assert.ThrowsAsync(async () => - await stream.WithRentedBufferReadExactAsync(5, buffer => buffer[0]) + await stream.ReadExactRentedAsync( + 5, + processor, + CancellationToken.None + ) ); }