Merge branch 'master' into adam/cleanup

# Conflicts:
#	src/SharpCompress/Common/EntryStream.cs
This commit is contained in:
Adam Hathcock
2026-02-04 12:55:46 +00:00
14 changed files with 130 additions and 40 deletions

View File

@@ -153,12 +153,12 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
}
}
protected override EntryStream GetEntryStream()
protected override EntryStream GetEntryStream(bool useSyncOverAsyncDispose)
{
var entry = _currentEntry.NotNull("currentEntry is not null");
if (entry.IsDirectory)
{
return CreateEntryStream(Stream.Null);
return CreateEntryStream(Stream.Null, false);
}
var folder = entry.FilePart.Folder;
@@ -186,7 +186,8 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
return CreateEntryStream(
new SyncOnlyStream(
new ReadOnlySubStream(_currentFolderStream, entry.Size, leaveOpen: true)
)
),
useSyncOverAsyncDispose
);
}

View File

@@ -14,11 +14,13 @@ public partial class EntryStream : Stream
private readonly Stream _stream;
private bool _completed;
private bool _isDisposed;
private readonly bool _useSyncOverAsyncDispose;
internal EntryStream(IReader reader, Stream stream)
internal EntryStream(IReader reader, Stream stream, bool useSyncOverAsyncDispose)
{
_reader = reader;
_stream = stream;
_useSyncOverAsyncDispose = useSyncOverAsyncDispose;
}
/// <summary>
@@ -39,7 +41,14 @@ public partial class EntryStream : Stream
_isDisposed = true;
if (!(_completed || _reader.Cancelled))
{
SkipEntry();
if (_useSyncOverAsyncDispose)
{
SkipEntryAsync().GetAwaiter().GetResult();
}
else
{
SkipEntry();
}
}
//Need a safe standard approach to this - it's okay for compression to overreads. Handling needs to be standardised

View File

@@ -1,4 +1,6 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Tar.Headers;
namespace SharpCompress.Common.Tar;
@@ -23,10 +25,28 @@ internal sealed class TarFilePart : FilePart
if (_seekableStream != null)
{
_seekableStream.Position = Header.DataStartPosition ?? 0;
return new TarReadOnlySubStream(_seekableStream, Header.Size);
return new TarReadOnlySubStream(_seekableStream, Header.Size, false);
}
return Header.PackedStream.NotNull();
}
internal override ValueTask<Stream?> GetCompressedStreamAsync(
CancellationToken cancellationToken = default
)
{
if (_seekableStream != null)
{
var useSyncOverAsync = false;
#if LEGACY_DOTNET
useSyncOverAsync = true;
#endif
_seekableStream.Position = Header.DataStartPosition ?? 0;
return new ValueTask<Stream?>(
new TarReadOnlySubStream(_seekableStream, Header.Size, useSyncOverAsync)
);
}
return new ValueTask<Stream?>(Header.PackedStream.NotNull());
}
internal override Stream? GetRawStream() => null;
}

View File

@@ -36,7 +36,15 @@ internal static partial class TarHeaderFactory
break;
case StreamingMode.Streaming:
{
header.PackedStream = new TarReadOnlySubStream(stream, header.Size);
var useSyncOverAsync = false;
#if LEGACY_DOTNET
useSyncOverAsync = true;
#endif
header.PackedStream = new TarReadOnlySubStream(
stream,
header.Size,
useSyncOverAsync
);
}
break;
default:

View File

@@ -37,7 +37,11 @@ internal static partial class TarHeaderFactory
break;
case StreamingMode.Streaming:
{
header.PackedStream = new TarReadOnlySubStream(stream, header.Size);
header.PackedStream = new TarReadOnlySubStream(
stream,
header.Size,
false
);
}
break;
default:

View File

@@ -6,13 +6,15 @@ namespace SharpCompress.Common.Tar;
internal class TarReadOnlySubStream : Stream
{
private readonly Stream _stream;
private readonly bool _useSyncOverAsyncDispose;
private bool _isDisposed;
private long _amountRead;
public TarReadOnlySubStream(Stream stream, long bytesToRead)
public TarReadOnlySubStream(Stream stream, long bytesToRead, bool useSyncOverAsyncDispose)
{
_stream = stream;
_useSyncOverAsyncDispose = useSyncOverAsyncDispose;
BytesLeftToRead = bytesToRead;
}
@@ -35,7 +37,14 @@ internal class TarReadOnlySubStream : Stream
if (bytesInLastBlock != 0)
{
_stream.Skip(512 - bytesInLastBlock);
if (_useSyncOverAsyncDispose)
{
_stream.SkipAsync(512 - bytesInLastBlock).GetAwaiter().GetResult();
}
else
{
_stream.Skip(512 - bytesInLastBlock);
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.IO;
using System.Security.Cryptography;
@@ -18,15 +19,18 @@ internal partial class WinzipAesCryptoStream : Stream
private bool _isFinalBlock;
private long _totalBytesLeftToRead;
private bool _isDisposed;
private bool _useSyncOverAsyncDispose;
internal WinzipAesCryptoStream(
Stream stream,
WinzipAesEncryptionData winzipAesEncryptionData,
long length
long length,
bool useSyncOverAsyncDispose
)
{
_stream = stream;
_totalBytesLeftToRead = length;
_useSyncOverAsyncDispose = useSyncOverAsyncDispose;
_cipher = CreateCipher(winzipAesEncryptionData);
@@ -68,20 +72,33 @@ internal partial class WinzipAesCryptoStream : Stream
if (disposing)
{
// Read out last 10 auth bytes - catch exceptions for async-only streams
try
if (_useSyncOverAsyncDispose)
{
var ten = ArrayPool<byte>.Shared.Rent(10);
try
{
_stream.ReadFullyAsync(ten, 0, 10).GetAwaiter().GetResult();
}
finally
{
ArrayPool<byte>.Shared.Return(ten);
}
}
else
{
Span<byte> ten = stackalloc byte[10];
_stream.ReadFully(ten);
}
catch (NotSupportedException)
{
// Stream may be async-only, auth bytes will be skipped
// This is acceptable when the entire stream has been read
}
_stream.Dispose();
}
}
private async Task ReadAuthBytesAsync()
{
byte[] authBytes = new byte[10];
await _stream.ReadFullyAsync(authBytes, 0, 10).ConfigureAwait(false);
}
public override void Flush() { }
public override int Read(byte[] buffer, int offset, int count)

View File

@@ -99,10 +99,15 @@ internal abstract partial class ZipFilePart
{
if (Header.WinzipAesEncryptionData != null)
{
var useSyncOverAsync = false;
#if LEGACY_DOTNET
useSyncOverAsync = true;
#endif
return new WinzipAesCryptoStream(
plainStream,
Header.WinzipAesEncryptionData,
Header.CompressedSize - 10
Header.CompressedSize - 10,
useSyncOverAsync
);
}
return plainStream;

View File

@@ -270,7 +270,8 @@ internal abstract partial class ZipFilePart : FilePart
return new WinzipAesCryptoStream(
plainStream,
Header.WinzipAesEncryptionData,
Header.CompressedSize - 10
Header.CompressedSize - 10,
false
);
}
return plainStream;

View File

@@ -178,7 +178,11 @@ public abstract partial class AbstractReader<TEntry, TVolume>
.Parts.First()
.GetCompressedStreamAsync(cancellationToken)
.ConfigureAwait(false);
return CreateEntryStream(stream);
var useSyncOverAsync = false;
#if LEGACY_DOTNET
useSyncOverAsync = true;
#endif
return CreateEntryStream(stream, useSyncOverAsync);
}
internal virtual ValueTask<bool> NextEntryForCurrentStreamAsync() =>

View File

@@ -35,8 +35,6 @@ public abstract partial class AbstractReader<TEntry, TVolume> : IReader, IAsyncR
public ArchiveType ArchiveType { get; }
protected bool IsAsync => _entriesForCurrentReadStreamAsync is not null;
/// <summary>
/// Current volume that the current entry resides in
/// </summary>
@@ -248,7 +246,7 @@ public abstract partial class AbstractReader<TEntry, TVolume> : IReader, IAsyncR
{
throw new ArgumentException("WriteEntryTo or OpenEntryStream can only be called once.");
}
var stream = GetEntryStream();
var stream = GetEntryStream(false);
_wroteCurrentEntry = true;
return stream;
}
@@ -256,11 +254,11 @@ public abstract partial class AbstractReader<TEntry, TVolume> : IReader, IAsyncR
/// <summary>
/// Retains a reference to the entry stream, so we can check whether it completed later.
/// </summary>
protected EntryStream CreateEntryStream(Stream? decompressed) =>
new(this, decompressed.NotNull());
protected EntryStream CreateEntryStream(Stream? decompressed, bool useSyncOverAsyncDispose) =>
new(this, decompressed.NotNull(), useSyncOverAsyncDispose);
protected virtual EntryStream GetEntryStream() =>
CreateEntryStream(Entry.Parts.First().GetCompressedStream());
protected virtual EntryStream GetEntryStream(bool useSyncOverAsyncDispose) =>
CreateEntryStream(Entry.Parts.First().GetCompressedStream(), useSyncOverAsyncDispose);
#endregion

View File

@@ -27,6 +27,10 @@ public abstract partial class RarReader
CancellationToken cancellationToken = default
)
{
var useSyncOverAsync = false;
#if LEGACY_DOTNET
useSyncOverAsync = true;
#endif
if (Entry.IsRedir)
{
throw new InvalidOperationException("no stream for redirect entry");
@@ -40,7 +44,8 @@ public abstract partial class RarReader
return CreateEntryStream(
await RarCrcStream
.CreateAsync(UnpackV1.Value, Entry.FileHeader, stream, cancellationToken)
.ConfigureAwait(false)
.ConfigureAwait(false),
useSyncOverAsync
);
}
@@ -49,14 +54,16 @@ public abstract partial class RarReader
return CreateEntryStream(
await RarBLAKE2spStream
.CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken)
.ConfigureAwait(false)
.ConfigureAwait(false),
useSyncOverAsync
);
}
return CreateEntryStream(
await RarCrcStream
.CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken)
.ConfigureAwait(false)
.ConfigureAwait(false),
useSyncOverAsync
);
}
}

View File

@@ -110,7 +110,7 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
protected virtual IEnumerable<FilePart> CreateFilePartEnumerableForCurrentEntry() =>
Entry.Parts;
protected override EntryStream GetEntryStream()
protected override EntryStream GetEntryStream(bool useSyncOverAsyncDispose)
{
if (Entry.IsRedir)
{
@@ -122,17 +122,24 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
);
if (Entry.IsRarV3)
{
return CreateEntryStream(RarCrcStream.Create(UnpackV1.Value, Entry.FileHeader, stream));
return CreateEntryStream(
RarCrcStream.Create(UnpackV1.Value, Entry.FileHeader, stream),
useSyncOverAsyncDispose
);
}
if (Entry.FileHeader.FileCrc?.Length > 5)
{
return CreateEntryStream(
RarBLAKE2spStream.Create(UnpackV2017.Value, Entry.FileHeader, stream)
RarBLAKE2spStream.Create(UnpackV2017.Value, Entry.FileHeader, stream),
useSyncOverAsyncDispose
);
}
return CreateEntryStream(RarCrcStream.Create(UnpackV2017.Value, Entry.FileHeader, stream));
return CreateEntryStream(
RarCrcStream.Create(UnpackV2017.Value, Entry.FileHeader, stream),
useSyncOverAsyncDispose
);
}
// GetEntryStreamAsync moved to RarReader.Async.cs

View File

@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
@@ -252,14 +253,14 @@ public class ZipReaderAsyncTests : ReaderTests
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip"))
)
)
using (
IReader baseReader = ZipReader.OpenReader(
await using (
var reader = await ReaderFactory.OpenAsyncReader(
stream,
new ReaderOptions { Password = "test" }
)
)
{
IAsyncReader reader = (IAsyncReader)baseReader;
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
@@ -284,14 +285,13 @@ public class ZipReaderAsyncTests : ReaderTests
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip"))
)
)
using (
IReader baseReader = ZipReader.OpenReader(
await using (
var reader = await ReaderFactory.OpenAsyncReader(
stream,
new ReaderOptions { Password = "test" }
)
)
{
IAsyncReader reader = (IAsyncReader)baseReader;
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)