Merge remote-tracking branch 'origin/release'

This commit is contained in:
Adam Hathcock
2026-08-04 09:30:36 +01:00
44 changed files with 558 additions and 200 deletions

View File

@@ -71,15 +71,12 @@ public static partial class IArchiveEntryExtensions
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
}
#if LEGACY_DOTNET
using var entryStream = await archiveEntry
var entryStream = await archiveEntry
.OpenEntryStreamAsync(cancellationToken)
.ConfigureAwait(false);
#else
await using var entryStream = await archiveEntry
.OpenEntryStreamAsync(cancellationToken)
await using var entryStreamScope = entryStream
.DisposeAsyncScope()
.ConfigureAwait(false);
#endif
var checkedStream = options is null
? entryStream
: IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options);

View File

@@ -18,7 +18,6 @@ public partial class EntryStream
_completed = true;
}
#if !LEGACY_DOTNET
public override async ValueTask DisposeAsync()
{
if (_isDisposed)
@@ -44,9 +43,8 @@ public partial class EntryStream
}
}
await base.DisposeAsync().ConfigureAwait(false);
await _stream.DisposeAsync().ConfigureAwait(false);
await _stream.DisposeAsyncCompat().ConfigureAwait(false);
}
#endif
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public override async Task<int> ReadAsync(

View File

@@ -8,7 +8,7 @@ using SharpCompress.Readers;
namespace SharpCompress.Common;
public partial class EntryStream : Stream
public partial class EntryStream : AsyncDisposableStream
{
private readonly IReader _reader;
private readonly Stream _stream;

View File

@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
@@ -31,16 +30,16 @@ internal partial class RarCrcStream : RarStream
.ConfigureAwait(false);
if (result != 0)
{
currentCrc = RarCRC.CheckCrc(currentCrc, buffer, offset, result);
_currentCrc = RarCRC.CheckCrc(_currentCrc, buffer, offset, result);
}
else if (
!disableCRC
&& GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0)
!_disableCrc
&& GetCrc() != BitConverter.ToUInt32(_readStream.NotNull().CurrentCrc.NotNull(), 0)
&& count != 0
)
{
// NOTE: we use the last FileHeader in a multipart volume to check CRC
throw new InvalidFormatException("file crc mismatch");
throw new InvalidFormatException("file crc mismatch: " + _key);
}
return result;
@@ -56,16 +55,16 @@ internal partial class RarCrcStream : RarStream
var result = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
if (result != 0)
{
currentCrc = RarCRC.CheckCrc(currentCrc, buffer.Span, 0, result);
_currentCrc = RarCRC.CheckCrc(_currentCrc, buffer.Span, 0, result);
}
else if (
!disableCRC
&& GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0)
!_disableCrc
&& GetCrc() != BitConverter.ToUInt32(_readStream.NotNull().CurrentCrc.NotNull(), 0)
&& buffer.Length != 0
)
{
// NOTE: we use the last FileHeader in a multipart volume to check CRC
throw new InvalidFormatException("file crc mismatch");
throw new InvalidFormatException("file crc mismatch: " + _key);
}
return result;

View File

@@ -1,7 +1,4 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Rar.Headers;
@@ -9,9 +6,10 @@ namespace SharpCompress.Compressors.Rar;
internal partial class RarCrcStream : RarStream
{
private readonly MultiVolumeReadOnlyStreamBase readStream;
private uint currentCrc;
private readonly bool disableCRC;
private readonly string? _key;
private readonly MultiVolumeReadOnlyStreamBase _readStream;
private uint _currentCrc;
private readonly bool _disableCrc;
private RarCrcStream(
IRarUnpack unpack,
@@ -20,8 +18,9 @@ internal partial class RarCrcStream : RarStream
)
: base(unpack, fileHeader, readStream)
{
this.readStream = readStream;
disableCRC = fileHeader.IsEncrypted;
this._readStream = readStream;
_key = fileHeader.FileName;
_disableCrc = fileHeader.IsEncrypted;
ResetCrc();
}
@@ -36,31 +35,25 @@ internal partial class RarCrcStream : RarStream
}
// Async methods moved to RarCrcStream.Async.cs
public uint GetCrc() => ~_currentCrc;
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
public uint GetCrc() => ~currentCrc;
public void ResetCrc() => currentCrc = 0xffffffff;
public void ResetCrc() => _currentCrc = 0xffffffff;
public override int Read(byte[] buffer, int offset, int count)
{
var result = base.Read(buffer, offset, count);
if (result != 0)
{
currentCrc = RarCRC.CheckCrc(currentCrc, buffer, offset, result);
_currentCrc = RarCRC.CheckCrc(_currentCrc, buffer, offset, result);
}
else if (
!disableCRC
&& GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0)
!_disableCrc
&& GetCrc() != BitConverter.ToUInt32(_readStream.NotNull().CurrentCrc.NotNull(), 0)
&& count != 0
)
{
// NOTE: we use the last FileHeader in a multipart volume to check CRC
throw new InvalidFormatException("file crc mismatch");
throw new InvalidFormatException("file crc mismatch: " + _key);
}
return result;

View File

@@ -394,28 +394,34 @@ internal partial class Unpack
{
await UnpIO_UnpWriteAsync(OutMem, 0, BlockLength, cancellationToken)
.ConfigureAwait(false);
WrittenFileSize += BlockLength;
}
UnpSomeRead = true;
WrittenFileSize += BlockLength;
WrittenBorder = BlockEnd;
WriteSizeLeft = (UnpPtr - WrittenBorder) & MaxWinMask;
}
}
else
{
NotAllFiltersProcessed = true;
// Current filter intersects the window write border, so we adjust
// the window border to process this filter next time, not now.
WrPtr = WrittenBorder;
// Since Filter start position can only increase, we quit processing
// all following filters for this data block and reset 'NextWindow'
// flag for them.
for (var J = I; J < Filters.Count; J++)
{
var fltj = Filters[J];
if (
fltj.Type != FILTER_NONE
&& fltj.NextWindow == false
&& ((fltj.BlockStart - WrPtr) & MaxWinMask) < FullWriteSize
)
if (fltj.Type != FILTER_NONE)
{
fltj.NextWindow = true;
fltj.NextWindow = false;
}
}
// Do not write data left after current filter now.
NotAllFiltersProcessed = true;
break;
}
}

View File

@@ -151,13 +151,16 @@ public class GZipFactory
CompressionContext.FromStream(sharpCompressStream).WithReaderOptions(options)
)
);
if (TarArchive.IsTarFile(testStream))
var isTarArchive = TarArchive.IsTarFile(testStream);
// The TAR probe can consume arbitrary compressed input before it rejects a stream.
sharpCompressStream.Rewind();
sharpCompressStream.StopRecording();
if (isTarArchive)
{
sharpCompressStream.StopRecording();
reader = new TarReader(sharpCompressStream, options, CompressionType.GZip);
return true;
}
sharpCompressStream.StopRecording();
reader = OpenReader(sharpCompressStream, options);
return true;
}
@@ -182,15 +185,27 @@ public class GZipFactory
}
sharpCompressStream.Rewind();
var tarReader = await new TarFactory()
.TryOpenReaderAsync(sharpCompressStream, options, cancellationToken)
using var testStream = SharpCompressStream.CreateNonDisposing(
await options
.Providers.CreateDecompressStreamAsync(
CompressionType.GZip,
SharpCompressStream.CreateNonDisposing(sharpCompressStream),
CompressionContext.FromStream(sharpCompressStream).WithReaderOptions(options),
cancellationToken
)
.ConfigureAwait(false)
);
var isTarArchive = await TarArchive
.IsTarFileAsync(testStream, cancellationToken)
.ConfigureAwait(false);
if (tarReader is not null)
sharpCompressStream.Rewind();
sharpCompressStream.StopRecording();
if (isTarArchive)
{
return tarReader;
return new TarReader(sharpCompressStream, options, CompressionType.GZip);
}
sharpCompressStream.StopRecording();
return await OpenAsyncReader(sharpCompressStream, options, cancellationToken)
.ConfigureAwait(false);
}

View File

@@ -65,14 +65,17 @@ public class LzwFactory : Factory, IReaderFactory
)
)
{
if (TarArchive.IsTarFile(testStream))
var isTarArchive = TarArchive.IsTarFile(testStream);
// The TAR probe can consume arbitrary compressed input before it rejects a stream.
sharpCompressStream.Rewind();
sharpCompressStream.StopRecording();
if (isTarArchive)
{
sharpCompressStream.StopRecording();
reader = new TarReader(sharpCompressStream, options, CompressionType.Lzw);
return true;
}
}
sharpCompressStream.StopRecording();
reader = OpenReader(sharpCompressStream, options);
return true;
}
@@ -97,15 +100,26 @@ public class LzwFactory : Factory, IReaderFactory
}
sharpCompressStream.Rewind();
var tarReader = await new TarFactory()
.TryOpenReaderAsync(sharpCompressStream, options, cancellationToken)
using var testStream = SharpCompressStream.CreateNonDisposing(
await options
.Providers.CreateDecompressStreamAsync(
CompressionType.Lzw,
SharpCompressStream.CreateNonDisposing(sharpCompressStream),
cancellationToken
)
.ConfigureAwait(false)
);
var isTarArchive = await TarArchive
.IsTarFileAsync(testStream, cancellationToken)
.ConfigureAwait(false);
if (tarReader is not null)
sharpCompressStream.Rewind();
sharpCompressStream.StopRecording();
if (isTarArchive)
{
return tarReader;
return new TarReader(sharpCompressStream, options, CompressionType.Lzw);
}
sharpCompressStream.StopRecording();
return await OpenAsyncReader(sharpCompressStream, options, cancellationToken)
.ConfigureAwait(false);
}

View File

@@ -0,0 +1,36 @@
using System;
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.IO;
/// <summary>
/// A <see cref="Stream"/> that is guaranteed to be asynchronously disposable on every target framework.
/// </summary>
/// <remarks>
/// <para>
/// On .NET Framework 4.8 and .NET Standard 2.0, <see cref="Stream"/> has no <c>DisposeAsync</c>.
/// <c>Microsoft.Bcl.AsyncInterfaces</c> supplies the <see cref="IAsyncDisposable"/> interface on those
/// targets but cannot retrofit it onto the BCL's <see cref="Stream"/>, and C# will not accept an
/// extension method for the pattern - <c>await using</c> requires a reachable <em>instance</em>
/// <c>DisposeAsync</c>. Deriving from this class instead of <see cref="Stream"/> therefore makes a type
/// usable with <c>await using</c> uniformly, with no conditional compilation at the call site.
/// </para>
/// <para>
/// The fallback below is the same behaviour as the BCL's own default <see cref="Stream.DisposeAsync"/>,
/// so a derived type may call <c>await base.DisposeAsync()</c> unconditionally on any target.
/// </para>
/// </remarks>
public abstract class AsyncDisposableStream : Stream
#if NO_STREAM_DISPOSEASYNC
, IAsyncDisposable
#endif
{
#if NO_STREAM_DISPOSEASYNC
public virtual ValueTask DisposeAsync()
{
Dispose();
return default;
}
#endif
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Threading.Tasks;
namespace SharpCompress.IO;
/// <summary>
/// Makes any resource usable with <c>await using</c>, disposing it asynchronously when the runtime type
/// supports it and synchronously otherwise.
/// </summary>
/// <remarks>
/// Needed for locals whose <em>static</em> type is <see cref="System.IO.Stream"/> (or another type that
/// only sometimes has <c>DisposeAsync</c>), where <c>await using</c> cannot bind directly on
/// .NET Framework 4.8 / .NET Standard 2.0. Unlike a compile-time guard, this picks the asynchronous path
/// based on the runtime type, so a stream that really is asynchronously disposable is disposed that way on
/// every target framework. Prefer deriving from <see cref="AsyncDisposableStream"/> where the type is ours.
/// </remarks>
internal readonly struct AsyncDisposeScope(IDisposable? resource) : IAsyncDisposable
{
public ValueTask DisposeAsync()
{
if (resource is IAsyncDisposable asyncDisposable)
{
return asyncDisposable.DisposeAsync();
}
resource?.Dispose();
return default;
}
/// <summary>
/// Mirrors <c>ConfiguredAsyncDisposable</c> so <c>await using</c> can specify context capture without
/// boxing this struct through <see cref="IAsyncDisposable"/>.
/// </summary>
public ConfiguredAsyncDisposeScope ConfigureAwait(bool continueOnCapturedContext) =>
new(resource, continueOnCapturedContext);
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace SharpCompress.IO;
internal readonly struct ConfiguredAsyncDisposeScope(
IDisposable? resource,
bool continueOnCapturedContext
)
{
public ConfiguredValueTaskAwaitable DisposeAsync()
{
if (resource is IAsyncDisposable asyncDisposable)
{
return asyncDisposable.DisposeAsync().ConfigureAwait(continueOnCapturedContext);
}
resource?.Dispose();
return default(ValueTask).ConfigureAwait(continueOnCapturedContext);
}
}

View File

@@ -67,7 +67,16 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream
public override void StartRecording(int? minBufferSize = null) =>
_recordedPosition = _stream.Position;
public override void StopRecording() => _recordedPosition = null;
public override void StopRecording()
{
if (_recordedPosition.HasValue)
{
// Seek back to the recording anchor position, matching the behavior of the
// non-seekable SharpCompressStream.StopRecording() which rewinds _logicalPosition.
_stream.Seek(_recordedPosition.Value, SeekOrigin.Begin);
}
_recordedPosition = null;
}
protected override void Dispose(bool disposing)
{

View File

@@ -25,6 +25,20 @@ public static class StreamExtensions
public void Skip() => stream.CopyTo(Stream.Null);
/// <summary>
/// Returns a scope that disposes this stream when awaited, asynchronously where the runtime type
/// supports it. Lets <c>await using</c> be written against a <see cref="Stream"/>-typed local on
/// every target framework.
/// </summary>
internal AsyncDisposeScope DisposeAsyncScope() => new(stream);
/// <summary>
/// Disposes this stream, asynchronously where the runtime type supports it. Use where the static
/// type is <see cref="Stream"/>, which has no <c>DisposeAsync</c> on .NET Framework 4.8 /
/// .NET Standard 2.0.
/// </summary>
internal ValueTask DisposeAsyncCompat() => new AsyncDisposeScope(stream).DisposeAsync();
public async ValueTask SkipAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

View File

@@ -104,13 +104,8 @@ public abstract partial class AbstractReader<TEntry, TVolume>
}
}
//don't know the size so we have to try to decompress to skip
#if LEGACY_DOTNET
using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
#else
await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
#endif
}
public async ValueTask WriteEntryToAsync(
@@ -139,19 +134,11 @@ public abstract partial class AbstractReader<TEntry, TVolume>
private async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken)
{
#if LEGACY_DOTNET
using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
var sourceStream = WrapWithProgress(s, Entry);
await sourceStream
.CopyToAsync(writeStream, Options.BufferSize, cancellationToken)
.ConfigureAwait(false);
#else
await using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
var sourceStream = WrapWithProgress(s, Entry);
await sourceStream
.CopyToAsync(writeStream, Options.BufferSize, cancellationToken)
.ConfigureAwait(false);
#endif
}
public async ValueTask<EntryStream> OpenEntryStreamAsync(

View File

@@ -108,15 +108,9 @@ public static class IAsyncReaderExtensions
CancellationToken cancellationToken
)
{
#if LEGACY_DOTNET
using var entryStream = await reader
.OpenEntryStreamAsync(cancellationToken)
.ConfigureAwait(false);
#else
await using var entryStream = await reader
.OpenEntryStreamAsync(cancellationToken)
.ConfigureAwait(false);
#endif
var checkedStream = IEntryExtensions.WrapWithChecksumValidation(
reader.Entry,
entryStream,

View File

@@ -31,6 +31,9 @@
<PropertyGroup Condition=" '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'netstandard2.0' Or '$(TargetFramework)' == 'netstandard2.1' ">
<DefineConstants>$(DefineConstants);LEGACY_DOTNET</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'netstandard2.0'">
<DefineConstants>$(DefineConstants);NO_STREAM_DISPOSEASYNC</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net8.0' Or '$(TargetFramework)' == 'net10.0' ">
<IsTrimmable>true</IsTrimmable>
<IsAotCompatible>true</IsAotCompatible>

View File

@@ -339,9 +339,9 @@
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.9, )",
"resolved": "10.0.9",
"contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg=="
"requested": "[10.0.10, )",
"resolved": "10.0.10",
"contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
@@ -471,9 +471,9 @@
"net8.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.28, )",
"resolved": "8.0.28",
"contentHash": "XMqgVjlLxLqWmEh3c49haXLQwsMNtvo6YscUaqfvEGfg1iA8hnYgkUVq3i9Zu9gKeNKMWiiZKVwZExc/qyEAsQ=="
"requested": "[8.0.29, )",
"resolved": "8.0.29",
"contentHash": "HSBTfrkIZijz8z3ybLRKB7E8rHk4QQufFwpHa9fc5CMIgRhRzdn4mBGmlyXZqaueiMPtuJcnjresGvSTfaW8Mg=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",