change the rented versions to structs

This commit is contained in:
Adam Hathcock
2026-02-10 13:53:59 +00:00
parent 27536d5019
commit 479bfb7025
8 changed files with 241 additions and 120 deletions

View File

@@ -180,16 +180,23 @@ public partial class GZipArchive
return true;
}
private readonly struct GZipSignatureTryProcessor : Utility.ITryBufferProcessor<(bool, bool)>
{
public (bool, bool) OnSuccess(ReadOnlySpan<byte> buffer) => (true, buffer[0] == 0x1F && buffer[1] == 0x8B && buffer[2] == 8);
public (bool, bool) OnFailure() => (false, false);
}
public static async ValueTask<bool> IsGZipFileAsync(
Stream stream,
CancellationToken cancellationToken = default
)
{
var result = await stream.TryWithRentedBufferReadFullyAsync(
var processor = new GZipSignatureTryProcessor();
var result = await stream.TryReadFullyRentedAsync<GZipSignatureTryProcessor, (bool success, bool match)>(
10,
header => header[0] == 0x1F && header[1] == 0x8B && header[2] == 8,
processor,
cancellationToken
);
return result.success && result.result;
return result.success && result.match;
}
}

View File

@@ -173,22 +173,37 @@ public partial class SevenZipArchive
private static ReadOnlySpan<byte> Signature => [(byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C];
private static bool SignatureMatch(Stream stream) =>
stream.WithRentedBufferReadExact(
private readonly struct SevenZipSignatureProcessor : Utility.IBufferProcessor<bool>
{
public bool Process(ReadOnlySpan<byte> buffer) => buffer.Slice(0, 6).SequenceEqual(Signature);
}
private readonly struct SevenZipSignatureTryProcessor : Utility.ITryBufferProcessor<(bool, bool)>
{
public (bool, bool) OnSuccess(ReadOnlySpan<byte> 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<SevenZipSignatureProcessor, bool>(
6,
buffer => buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature)
ref processor
);
}
private static async ValueTask<bool> SignatureMatchAsync(
Stream stream,
CancellationToken cancellationToken
)
{
var result = await stream.TryWithRentedBufferReadFullyAsync(
var processor = new SevenZipSignatureTryProcessor();
var result = await stream.TryReadFullyRentedAsync<SevenZipSignatureTryProcessor, (bool success, bool match)>(
6,
buffer => buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature),
processor,
cancellationToken
);
return result.success && result.result;
return result.success && result.match;
}
}

View File

@@ -8,6 +8,11 @@ namespace SharpCompress.Common.Zip;
internal partial class WinzipAesCryptoStream
{
private readonly struct DiscardProcessor : Utility.IBufferProcessor<int>
{
public int Process(ReadOnlySpan<byte> 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<DiscardProcessor, int>(
10,
_ => 0, // Just consume the bytes, don't need the result
processor,
CancellationToken.None
)
.ConfigureAwait(false);

View File

@@ -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<DiscardProcessor, int>(
10,
_ => 0, // Just consume the bytes, don't need the result
processor,
CancellationToken.None
)
.GetAwaiter()

View File

@@ -25,6 +25,18 @@ public class ArcFactory : Factory, IReaderFactory
yield return "arc";
}
private readonly struct ArcSignatureProcessor : Utility.IBufferProcessor<bool>
{
public bool Process(ReadOnlySpan<byte> buffer) =>
buffer[0] == 0x1A && buffer[1] < 10;
}
private readonly struct ArcSignatureTryProcessor : Utility.ITryBufferProcessor<(bool, bool)>
{
public (bool, bool) OnSuccess(ReadOnlySpan<byte> 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<ArcSignatureTryProcessor, (bool success, bool match)>(
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<ArcSignatureProcessor, bool>(
2,
buffer => buffer[0] == 0x1A && buffer[1] < 10, //rather thin, but this is all we have
processor,
cancellationToken
);
}

View File

@@ -10,21 +10,23 @@ 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.
/// 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.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <typeparam name="TProcessor">The processor type (struct)</typeparam>
/// <typeparam name="TResult">The return type</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="processor">The processor 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>(
/// <returns>The result of the processor</returns>
public static async ValueTask<TResult> ReadFullyRentedAsync<TProcessor, TResult>(
this Stream stream,
int size,
Func<byte[], T> callback,
TProcessor processor,
CancellationToken cancellationToken = default
)
where TProcessor : struct, IBufferProcessor<TResult>
{
var buffer = ArrayPool<byte>.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
}
/// <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.
/// 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.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <typeparam name="TProcessor">The processor type (struct)</typeparam>
/// <typeparam name="TResult">The return type</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="processor">The processor 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>(
/// <returns>The result of the processor (success or failure)</returns>
public static async ValueTask<TResult> TryReadFullyRentedAsync<TProcessor, TResult>(
this Stream stream,
int size,
Func<byte[], T> callback,
TProcessor processor,
CancellationToken cancellationToken = default
)
where TProcessor : struct, ITryBufferProcessor<TResult>
{
var buffer = ArrayPool<byte>.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
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <typeparam name="TProcessor">The processor type (struct)</typeparam>
/// <typeparam name="TResult">The return type</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="processor">The processor 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>(
/// <returns>The result of the processor</returns>
public static async ValueTask<TResult> ReadExactRentedAsync<TProcessor, TResult>(
this Stream stream,
int size,
Func<byte[], T> callback,
TProcessor processor,
CancellationToken cancellationToken = default
)
where TProcessor : struct, IBufferProcessor<TResult>
{
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
await stream.ReadExactAsync(buffer, 0, size, cancellationToken).ConfigureAwait(false);
return callback(buffer);
return processor.Process(buffer.AsSpan(0, size));
}
finally
{

View File

@@ -139,19 +139,40 @@ internal static partial class Utility
}
/// <summary>
/// Rents a buffer from the shared ArrayPool, reads data into it, and executes a callback with the buffer.
/// Interface for struct-based buffer processors.
/// </summary>
/// <typeparam name="T">The return type of the processor</typeparam>
internal interface IBufferProcessor<TResult>
{
TResult Process(ReadOnlySpan<byte> buffer);
}
/// <summary>
/// Interface for struct-based try buffer processors.
/// </summary>
/// <typeparam name="T">The return type of the processor</typeparam>
internal interface ITryBufferProcessor<TResult>
{
TResult OnSuccess(ReadOnlySpan<byte> buffer);
TResult OnFailure();
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <typeparam name="TProcessor">The processor type (struct)</typeparam>
/// <typeparam name="TResult">The return type</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>(
/// <param name="processor">The processor to execute with the rented buffer</param>
/// <returns>The result of the processor</returns>
public static TResult ReadFullyRented<TProcessor, TResult>(
this Stream stream,
int size,
Func<byte[], T> callback
ref TProcessor processor
)
where TProcessor : struct, IBufferProcessor<TResult>
{
var buffer = ArrayPool<byte>.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
}
/// <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.
/// 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.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <typeparam name="TProcessor">The processor type (struct)</typeparam>
/// <typeparam name="TResult">The return type</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>(
/// <param name="processor">The processor to execute with the rented buffer</param>
/// <returns>The result of the processor (success or failure)</returns>
public static TResult TryReadFullyRented<TProcessor, TResult>(
this Stream stream,
int size,
Func<byte[], T> callback,
out T result
ref TProcessor processor
)
where TProcessor : struct, ITryBufferProcessor<TResult>
{
var buffer = ArrayPool<byte>.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
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The return type of the callback</typeparam>
/// <typeparam name="TProcessor">The processor type (struct)</typeparam>
/// <typeparam name="TResult">The return type</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>(
/// <param name="processor">The processor to execute with the rented buffer</param>
/// <returns>The result of the processor</returns>
public static TResult ReadExactRented<TProcessor, TResult>(
this Stream stream,
int size,
Func<byte[], T> callback
ref TProcessor processor
)
where TProcessor : struct, IBufferProcessor<TResult>
{
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
stream.ReadExact(buffer, 0, size);
return callback(buffer);
return processor.Process(buffer.AsSpan(0, size));
}
finally
{

View File

@@ -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<int>
{
public int Process(ReadOnlySpan<byte> buffer)
{
int sum = 0;
for (int i = 0; i < buffer.Length; i++)
{
sum += buffer[i];
}
return sum;
}
}
private readonly struct IdentityProcessor : Utility.IBufferProcessor<bool>
{
public bool Process(ReadOnlySpan<byte> buffer) => buffer.Length > 0 && buffer[0] != 0;
}
private readonly struct ArcLikeTryProcessor : Utility.ITryBufferProcessor<(bool, bool)>
{
public (bool, bool) OnSuccess(ReadOnlySpan<byte> 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<byte> 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<SumProcessor, int>(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<EndOfStreamException>(() =>
stream.WithRentedBufferReadFully(5, buffer => buffer[0])
stream.ReadFullyRented<IdentityProcessor, bool>(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<ArcLikeTryProcessor, (bool success, bool match)>(
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<AlwaysTrueTryProcessor, (bool success, bool match)>(
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<SumProcessor, int>(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<EndOfStreamException>(() =>
stream.WithRentedBufferReadExact(5, buffer => buffer[0])
stream.ReadExactRented<IdentityProcessor, bool>(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<SumProcessor, int>(
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<EndOfStreamException>(async () =>
await stream.WithRentedBufferReadFullyAsync(5, buffer => buffer[0])
await stream.ReadFullyRentedAsync<IdentityProcessor, bool>(
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<SumProcessor, int>(
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<EndOfStreamException>(async () =>
await stream.WithRentedBufferReadExactAsync(5, buffer => buffer[0])
await stream.ReadExactRentedAsync<IdentityProcessor, bool>(
5,
processor,
CancellationToken.None
)
);
}