Dispose 7Zip reader decoders without closing borrowed volumes

This commit is contained in:
Adam Hathcock
2026-10-15 14:07:46 +01:00
parent 7505ba7e5f
commit 454ec7e98b
3 changed files with 150 additions and 3 deletions

View File

@@ -207,9 +207,39 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
public override void Dispose()
{
_currentFolderStream?.Dispose();
var stream = _currentFolderStream;
_currentFolderStream = null;
base.Dispose();
_currentFolder = null;
try
{
stream?.Dispose();
}
finally
{
base.Dispose();
}
}
public override async ValueTask DisposeAsync()
{
var stream = _currentFolderStream;
_currentFolderStream = null;
_currentFolder = null;
try
{
if (stream is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
}
else
{
stream?.Dispose();
}
}
finally
{
await base.DisposeAsync().ConfigureAwait(false);
}
}
}

View File

@@ -19,7 +19,11 @@ public abstract partial class AbstractReader<TEntry, TVolume>
await _entriesForCurrentReadStreamAsync.DisposeAsync().ConfigureAwait(false);
}
// If Volume implements IAsyncDisposable, use async disposal
// Readers created by an archive borrow its volume, just as on the sync path.
if (!_disposeVolume)
{
return;
}
if (Volume is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);

View File

@@ -0,0 +1,113 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Compressors.Filters;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Readers;
using Xunit;
namespace SharpCompress.Test.Streams;
public class SevenZipLifetimeTests : TestBase
{
[Theory]
[InlineData("7Zip.LZMA.7z", false, false)]
[InlineData("7Zip.LZMA.7z", false, true)]
[InlineData("7Zip.LZMA.7z", true, false)]
[InlineData("7Zip.LZMA.7z", true, true)]
[InlineData("7Zip.LZMA2.7z", false, false)]
[InlineData("7Zip.LZMA2.7z", false, true)]
[InlineData("7Zip.LZMA2.7z", true, false)]
[InlineData("7Zip.LZMA2.7z", true, true)]
[InlineData("7Zip.solid.7z", false, false)]
[InlineData("7Zip.solid.7z", false, true)]
[InlineData("7Zip.solid.7z", true, false)]
[InlineData("7Zip.solid.7z", true, true)]
public async Task Reader_Dispose_Releases_Decoder_And_Preserves_Archive(
string filename,
bool asyncDispose,
bool readAll
)
{
using var input = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename));
await using var archive = await SevenZipArchive.OpenAsyncArchive(
input,
new ReaderOptions { LeaveStreamOpen = false },
CancellationToken.None
);
await using var reader = await archive.ExtractAllEntriesAsync();
var sevenZipReader = Assert.IsType<SevenZipArchive.SevenZipReader>(reader);
sevenZipReader.DiagnosticsEnabled = true;
while (await reader.MoveToNextEntryAsync(CancellationToken.None))
{
if (!reader.Entry.IsDirectory)
{
#if LEGACY_DOTNET
using var stream = await reader.OpenEntryStreamAsync(CancellationToken.None);
#else
await using var stream = await reader.OpenEntryStreamAsync(CancellationToken.None);
#endif
await stream.CopyToAsync(Stream.Null, Constants.BufferSize, CancellationToken.None);
if (!readAll)
{
break;
}
}
}
var folderStream = sevenZipReader.DiagnosticsCurrentFolderStream;
Assert.NotNull(folderStream);
while (folderStream is Filter)
{
folderStream = (Stream)GetField(folderStream, "_baseStream")!;
}
var decoder = Assert.IsType<LzmaStream>(folderStream);
var window = GetField(decoder, "_outWindow")!;
Assert.NotNull(GetField(window, "_buffer"));
try
{
if (asyncDispose)
{
await reader.DisposeAsync();
await reader.DisposeAsync();
}
else
{
sevenZipReader.Dispose();
sevenZipReader.Dispose();
}
Assert.Null(sevenZipReader.DiagnosticsCurrentFolderStream);
Assert.Null(GetField(window, "_buffer"));
Assert.True(input.CanRead);
// Disposing a borrowed reader must not prevent another extraction.
var entry = await archive.EntriesAsync.Where(entry => !entry.IsDirectory).FirstAsync();
#if LEGACY_DOTNET
using var entryStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#else
await using var entryStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#endif
await entryStream.CopyToAsync(
Stream.Null,
Constants.BufferSize,
CancellationToken.None
);
}
finally
{
await archive.DisposeAsync();
}
Assert.False(input.CanRead);
}
private static object? GetField(object instance, string name) =>
instance
.GetType()
.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(instance);
}