2026-01-28 16:50:35 +00:00
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
namespace SharpCompress.IO;
|
|
|
|
|
|
2026-02-04 14:12:04 +00:00
|
|
|
internal partial class SharpCompressStream
|
2026-01-28 16:50:35 +00:00
|
|
|
{
|
2026-02-04 12:40:39 +00:00
|
|
|
public override Task<int> ReadAsync(
|
2026-01-28 16:50:35 +00:00
|
|
|
byte[] buffer,
|
|
|
|
|
int offset,
|
|
|
|
|
int count,
|
|
|
|
|
CancellationToken cancellationToken
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
if (count == 0)
|
|
|
|
|
{
|
2026-02-04 12:40:39 +00:00
|
|
|
return Task.FromResult(0);
|
2026-01-28 16:50:35 +00:00
|
|
|
}
|
2026-01-29 15:23:53 +00:00
|
|
|
|
2026-02-04 12:40:39 +00:00
|
|
|
// In passthrough mode, delegate directly to underlying stream
|
|
|
|
|
if (_isPassthrough)
|
|
|
|
|
{
|
|
|
|
|
return stream.ReadAsync(buffer, offset, count, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ReadAsyncCore(buffer, offset, count, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<int> ReadAsyncCore(
|
|
|
|
|
byte[] buffer,
|
|
|
|
|
int offset,
|
|
|
|
|
int count,
|
|
|
|
|
CancellationToken cancellationToken
|
|
|
|
|
)
|
|
|
|
|
{
|
2026-02-04 08:20:05 +00:00
|
|
|
// If recording is active or we're reading from the recording buffer, use legacy behavior
|
|
|
|
|
if (IsRecording || (isRewound && bufferStream.Position != bufferStream.Length))
|
|
|
|
|
{
|
|
|
|
|
return await ReadWithRecordingAsync(buffer, offset, count, cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 08:30:02 +00:00
|
|
|
// If ring buffer is enabled (and not recording), use ring buffer logic
|
|
|
|
|
if (_ringBuffer is not null)
|
2026-02-04 08:20:05 +00:00
|
|
|
{
|
2026-02-04 08:30:02 +00:00
|
|
|
return await ReadWithRingBufferAsync(buffer, offset, count, cancellationToken)
|
2026-02-04 08:20:05 +00:00
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No buffering - read directly from stream
|
|
|
|
|
int read = await stream
|
|
|
|
|
.ReadAsync(buffer, offset, count, cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
streamPosition += read;
|
|
|
|
|
_logicalPosition = streamPosition;
|
|
|
|
|
return read;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Async version of ReadWithRecording (legacy behavior for format detection).
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task<int> ReadWithRecordingAsync(
|
|
|
|
|
byte[] buffer,
|
|
|
|
|
int offset,
|
|
|
|
|
int count,
|
|
|
|
|
CancellationToken cancellationToken
|
|
|
|
|
)
|
|
|
|
|
{
|
2026-01-28 16:50:35 +00:00
|
|
|
int read;
|
2026-01-29 15:23:53 +00:00
|
|
|
if (isRewound && bufferStream.Position != bufferStream.Length)
|
2026-01-28 16:50:35 +00:00
|
|
|
{
|
2026-01-29 15:23:53 +00:00
|
|
|
var readCount = Math.Min(count, (int)(bufferStream.Length - bufferStream.Position));
|
|
|
|
|
read = await bufferStream
|
|
|
|
|
.ReadAsync(buffer, offset, readCount, cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
2026-02-03 08:56:35 +00:00
|
|
|
if (read < count)
|
2026-01-28 16:50:35 +00:00
|
|
|
{
|
2026-01-29 15:23:53 +00:00
|
|
|
var tempRead = await stream
|
2026-01-28 16:50:35 +00:00
|
|
|
.ReadAsync(buffer, offset + read, count - read, cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
if (IsRecording)
|
|
|
|
|
{
|
2026-01-29 15:23:53 +00:00
|
|
|
await bufferStream
|
|
|
|
|
.WriteAsync(buffer, offset + read, tempRead, cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
2026-01-28 16:50:35 +00:00
|
|
|
}
|
2026-02-04 08:30:02 +00:00
|
|
|
else if (_ringBuffer is not null && tempRead > 0)
|
2026-02-04 08:20:05 +00:00
|
|
|
{
|
2026-02-04 08:30:02 +00:00
|
|
|
// When transitioning out of recording mode, add to ring buffer
|
2026-02-04 08:20:05 +00:00
|
|
|
// so that future rewinds will work
|
2026-02-04 08:30:02 +00:00
|
|
|
_ringBuffer.Write(buffer, offset + read, tempRead);
|
2026-02-04 08:20:05 +00:00
|
|
|
}
|
2026-01-29 15:23:53 +00:00
|
|
|
streamPosition += tempRead;
|
2026-02-04 08:20:05 +00:00
|
|
|
_logicalPosition = streamPosition;
|
2026-01-28 16:50:35 +00:00
|
|
|
read += tempRead;
|
|
|
|
|
}
|
2026-01-29 15:23:53 +00:00
|
|
|
if (bufferStream.Position == bufferStream.Length)
|
2026-01-28 16:50:35 +00:00
|
|
|
{
|
2026-01-29 15:23:53 +00:00
|
|
|
isRewound = false;
|
2026-01-28 16:50:35 +00:00
|
|
|
}
|
|
|
|
|
return read;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
read = await stream
|
|
|
|
|
.ReadAsync(buffer, offset, count, cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
if (IsRecording)
|
|
|
|
|
{
|
2026-01-29 15:23:53 +00:00
|
|
|
await bufferStream
|
|
|
|
|
.WriteAsync(buffer, offset, read, cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
2026-01-28 16:50:35 +00:00
|
|
|
}
|
2026-01-29 15:23:53 +00:00
|
|
|
streamPosition += read;
|
2026-02-04 08:20:05 +00:00
|
|
|
_logicalPosition = streamPosition;
|
2026-01-28 16:50:35 +00:00
|
|
|
return read;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 08:20:05 +00:00
|
|
|
/// <summary>
|
2026-02-04 08:30:02 +00:00
|
|
|
/// Async version of ReadWithRingBuffer.
|
2026-02-04 08:20:05 +00:00
|
|
|
/// </summary>
|
2026-02-04 08:30:02 +00:00
|
|
|
private async Task<int> ReadWithRingBufferAsync(
|
2026-02-04 08:20:05 +00:00
|
|
|
byte[] buffer,
|
|
|
|
|
int offset,
|
|
|
|
|
int count,
|
|
|
|
|
CancellationToken cancellationToken
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
int totalRead = 0;
|
|
|
|
|
|
2026-02-04 08:30:02 +00:00
|
|
|
// If logical position is behind stream position, read from ring buffer first
|
2026-02-04 08:20:05 +00:00
|
|
|
while (count > 0 && _logicalPosition < streamPosition)
|
|
|
|
|
{
|
|
|
|
|
long bytesFromEnd = streamPosition - _logicalPosition;
|
2026-02-04 08:30:02 +00:00
|
|
|
int available = _ringBuffer!.ReadFromEnd(bytesFromEnd, buffer, offset, count);
|
|
|
|
|
totalRead += available;
|
|
|
|
|
offset += available;
|
|
|
|
|
count -= available;
|
|
|
|
|
_logicalPosition += available;
|
2026-02-04 08:20:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If more data needed, read from underlying stream
|
|
|
|
|
if (count > 0)
|
|
|
|
|
{
|
|
|
|
|
int read = await stream
|
|
|
|
|
.ReadAsync(buffer, offset, count, cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
if (read > 0)
|
|
|
|
|
{
|
2026-02-04 08:30:02 +00:00
|
|
|
_ringBuffer!.Write(buffer, offset, read);
|
2026-02-04 08:20:05 +00:00
|
|
|
streamPosition += read;
|
|
|
|
|
_logicalPosition += read;
|
|
|
|
|
totalRead += read;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return totalRead;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 16:50:35 +00:00
|
|
|
#if !LEGACY_DOTNET
|
2026-02-04 12:40:39 +00:00
|
|
|
public override ValueTask<int> ReadAsync(
|
2026-01-28 16:50:35 +00:00
|
|
|
Memory<byte> buffer,
|
|
|
|
|
CancellationToken cancellationToken = default
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
if (buffer.Length == 0)
|
|
|
|
|
{
|
2026-02-04 12:40:39 +00:00
|
|
|
return ValueTask.FromResult(0);
|
2026-01-28 16:50:35 +00:00
|
|
|
}
|
2026-01-29 15:23:53 +00:00
|
|
|
|
2026-02-04 12:40:39 +00:00
|
|
|
// In passthrough mode, delegate directly to underlying stream
|
|
|
|
|
if (_isPassthrough)
|
|
|
|
|
{
|
|
|
|
|
return stream.ReadAsync(buffer, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ReadAsyncCore(buffer, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async ValueTask<int> ReadAsyncCore(
|
|
|
|
|
Memory<byte> buffer,
|
|
|
|
|
CancellationToken cancellationToken
|
|
|
|
|
)
|
|
|
|
|
{
|
2026-02-04 08:20:05 +00:00
|
|
|
// If recording is active or we're reading from the recording buffer, use legacy behavior
|
|
|
|
|
if (IsRecording || (isRewound && bufferStream.Position != bufferStream.Length))
|
|
|
|
|
{
|
|
|
|
|
return await ReadWithRecordingAsync(buffer, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 08:30:02 +00:00
|
|
|
// If ring buffer is enabled (and not recording), use ring buffer logic
|
|
|
|
|
if (_ringBuffer is not null)
|
2026-02-04 08:20:05 +00:00
|
|
|
{
|
2026-02-04 08:30:02 +00:00
|
|
|
return await ReadWithRingBufferAsync(buffer, cancellationToken).ConfigureAwait(false);
|
2026-02-04 08:20:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No buffering - read directly from stream
|
|
|
|
|
int read = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
streamPosition += read;
|
|
|
|
|
_logicalPosition = streamPosition;
|
|
|
|
|
return read;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Async version of ReadWithRecording for Memory<byte> (legacy behavior for format detection).
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async ValueTask<int> ReadWithRecordingAsync(
|
|
|
|
|
Memory<byte> buffer,
|
|
|
|
|
CancellationToken cancellationToken
|
|
|
|
|
)
|
|
|
|
|
{
|
2026-01-28 16:50:35 +00:00
|
|
|
int read;
|
2026-01-29 15:23:53 +00:00
|
|
|
if (isRewound && bufferStream.Position != bufferStream.Length)
|
2026-01-28 16:50:35 +00:00
|
|
|
{
|
2026-01-29 15:23:53 +00:00
|
|
|
var readCount = (int)
|
|
|
|
|
Math.Min(buffer.Length, bufferStream.Length - bufferStream.Position);
|
|
|
|
|
read = await bufferStream
|
|
|
|
|
.ReadAsync(buffer.Slice(0, readCount), cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
2026-02-03 08:56:35 +00:00
|
|
|
if (read < buffer.Length)
|
2026-01-28 16:50:35 +00:00
|
|
|
{
|
2026-01-29 15:23:53 +00:00
|
|
|
var tempRead = await stream
|
2026-01-28 16:50:35 +00:00
|
|
|
.ReadAsync(buffer.Slice(read), cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
if (IsRecording)
|
|
|
|
|
{
|
2026-01-29 15:23:53 +00:00
|
|
|
await bufferStream
|
|
|
|
|
.WriteAsync(buffer.Slice(read, tempRead), cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
2026-01-28 16:50:35 +00:00
|
|
|
}
|
2026-02-04 08:30:02 +00:00
|
|
|
else if (_ringBuffer is not null && tempRead > 0)
|
2026-02-04 08:20:05 +00:00
|
|
|
{
|
2026-02-04 08:30:02 +00:00
|
|
|
// When transitioning out of recording mode, add to ring buffer
|
2026-02-04 08:20:05 +00:00
|
|
|
// so that future rewinds will work
|
|
|
|
|
var tempBuffer = buffer.Slice(read, tempRead).ToArray();
|
2026-02-04 08:30:02 +00:00
|
|
|
_ringBuffer.Write(tempBuffer, 0, tempRead);
|
2026-02-04 08:20:05 +00:00
|
|
|
}
|
2026-02-03 08:56:35 +00:00
|
|
|
streamPosition += tempRead;
|
2026-02-04 08:20:05 +00:00
|
|
|
_logicalPosition = streamPosition;
|
2026-01-28 16:50:35 +00:00
|
|
|
read += tempRead;
|
|
|
|
|
}
|
2026-01-29 15:23:53 +00:00
|
|
|
if (bufferStream.Position == bufferStream.Length)
|
2026-01-28 16:50:35 +00:00
|
|
|
{
|
2026-01-29 15:23:53 +00:00
|
|
|
isRewound = false;
|
2026-01-28 16:50:35 +00:00
|
|
|
}
|
|
|
|
|
return read;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
read = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
if (IsRecording)
|
|
|
|
|
{
|
2026-01-29 15:23:53 +00:00
|
|
|
await bufferStream
|
|
|
|
|
.WriteAsync(buffer.Slice(0, read), cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
2026-01-28 16:50:35 +00:00
|
|
|
}
|
2026-02-03 08:56:35 +00:00
|
|
|
streamPosition += read;
|
2026-02-04 08:20:05 +00:00
|
|
|
_logicalPosition = streamPosition;
|
2026-01-28 16:50:35 +00:00
|
|
|
return read;
|
|
|
|
|
}
|
2026-02-04 08:20:05 +00:00
|
|
|
|
|
|
|
|
/// <summary>
|
2026-02-04 08:30:02 +00:00
|
|
|
/// Async version of ReadWithRingBuffer for Memory<byte>.
|
2026-02-04 08:20:05 +00:00
|
|
|
/// </summary>
|
2026-02-04 08:30:02 +00:00
|
|
|
private async ValueTask<int> ReadWithRingBufferAsync(
|
2026-02-04 08:20:05 +00:00
|
|
|
Memory<byte> buffer,
|
|
|
|
|
CancellationToken cancellationToken
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
int totalRead = 0;
|
|
|
|
|
int count = buffer.Length;
|
|
|
|
|
int offset = 0;
|
|
|
|
|
|
2026-02-04 08:30:02 +00:00
|
|
|
// If logical position is behind stream position, read from ring buffer first
|
|
|
|
|
// Note: We need to use a temporary byte array because RingBuffer.ReadFromEnd expects byte[]
|
2026-02-04 08:20:05 +00:00
|
|
|
while (count > 0 && _logicalPosition < streamPosition)
|
|
|
|
|
{
|
|
|
|
|
long bytesFromEnd = streamPosition - _logicalPosition;
|
2026-02-04 08:30:02 +00:00
|
|
|
var tempBuffer = new byte[Math.Min(count, (int)bytesFromEnd)];
|
|
|
|
|
int available = _ringBuffer!.ReadFromEnd(
|
|
|
|
|
bytesFromEnd,
|
|
|
|
|
tempBuffer,
|
|
|
|
|
0,
|
|
|
|
|
tempBuffer.Length
|
2026-02-04 08:20:05 +00:00
|
|
|
);
|
2026-02-04 08:30:02 +00:00
|
|
|
tempBuffer.AsSpan(0, available).CopyTo(buffer.Span.Slice(offset));
|
2026-02-04 08:20:05 +00:00
|
|
|
|
2026-02-04 08:30:02 +00:00
|
|
|
totalRead += available;
|
|
|
|
|
offset += available;
|
|
|
|
|
count -= available;
|
|
|
|
|
_logicalPosition += available;
|
2026-02-04 08:20:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If more data needed, read from underlying stream
|
|
|
|
|
if (count > 0)
|
|
|
|
|
{
|
|
|
|
|
int read = await stream
|
|
|
|
|
.ReadAsync(buffer.Slice(offset, count), cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
if (read > 0)
|
|
|
|
|
{
|
2026-02-04 08:30:02 +00:00
|
|
|
// RingBuffer.Write expects byte[], so we need to copy
|
2026-02-04 08:20:05 +00:00
|
|
|
var tempBuffer = buffer.Slice(offset, read).ToArray();
|
2026-02-04 08:30:02 +00:00
|
|
|
_ringBuffer!.Write(tempBuffer, 0, read);
|
2026-02-04 08:20:05 +00:00
|
|
|
streamPosition += read;
|
|
|
|
|
_logicalPosition += read;
|
|
|
|
|
totalRead += read;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return totalRead;
|
|
|
|
|
}
|
2026-01-28 16:50:35 +00:00
|
|
|
#endif
|
2026-01-29 15:23:53 +00:00
|
|
|
|
|
|
|
|
public override Task WriteAsync(
|
|
|
|
|
byte[] buffer,
|
|
|
|
|
int offset,
|
|
|
|
|
int count,
|
|
|
|
|
CancellationToken cancellationToken
|
2026-02-04 12:40:39 +00:00
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
if (_isPassthrough)
|
|
|
|
|
{
|
|
|
|
|
return stream.WriteAsync(buffer, offset, count, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
throw new NotSupportedException();
|
|
|
|
|
}
|
2026-01-29 15:23:53 +00:00
|
|
|
|
|
|
|
|
#if !LEGACY_DOTNET
|
|
|
|
|
public override ValueTask WriteAsync(
|
|
|
|
|
ReadOnlyMemory<byte> buffer,
|
|
|
|
|
CancellationToken cancellationToken = default
|
2026-02-04 12:40:39 +00:00
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
if (_isPassthrough)
|
|
|
|
|
{
|
|
|
|
|
return stream.WriteAsync(buffer, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
throw new NotSupportedException();
|
|
|
|
|
}
|
2026-01-29 15:23:53 +00:00
|
|
|
#endif
|
|
|
|
|
|
2026-02-04 12:40:39 +00:00
|
|
|
public override Task FlushAsync(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
if (_isPassthrough)
|
|
|
|
|
{
|
|
|
|
|
return stream.FlushAsync(cancellationToken);
|
|
|
|
|
}
|
2026-01-29 15:23:53 +00:00
|
|
|
throw new NotSupportedException();
|
2026-02-04 12:40:39 +00:00
|
|
|
}
|
2026-01-29 15:23:53 +00:00
|
|
|
|
|
|
|
|
public override async Task CopyToAsync(
|
|
|
|
|
Stream destination,
|
|
|
|
|
int bufferSize,
|
|
|
|
|
CancellationToken cancellationToken
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
byte[] buffer = new byte[bufferSize];
|
|
|
|
|
int bytesRead;
|
|
|
|
|
while ((bytesRead = await ReadAsync(buffer, 0, buffer.Length, cancellationToken)) != 0)
|
|
|
|
|
{
|
|
|
|
|
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if !LEGACY_DOTNET
|
|
|
|
|
public override async ValueTask DisposeAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!isDisposed)
|
|
|
|
|
{
|
2026-02-04 12:40:39 +00:00
|
|
|
if (ThrowOnDispose)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
2026-02-04 14:12:04 +00:00
|
|
|
$"Attempt to dispose of a {nameof(SharpCompressStream)} when {nameof(ThrowOnDispose)} is true"
|
2026-02-04 12:40:39 +00:00
|
|
|
);
|
|
|
|
|
}
|
2026-01-29 15:23:53 +00:00
|
|
|
isDisposed = true;
|
2026-02-04 12:40:39 +00:00
|
|
|
if (!LeaveStreamOpen)
|
|
|
|
|
{
|
|
|
|
|
await stream.DisposeAsync();
|
|
|
|
|
}
|
2026-02-04 08:30:02 +00:00
|
|
|
_ringBuffer?.Dispose();
|
|
|
|
|
_ringBuffer = null;
|
2026-01-29 15:23:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2026-01-28 16:50:35 +00:00
|
|
|
}
|