diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Async.cs b/src/SharpCompress/Archives/Rar/RarArchive.Async.cs index ce74a3c3..92538cd9 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Async.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Async.cs @@ -19,11 +19,7 @@ public partial class RarArchive { if (!_disposed) { - if (UnpackV1.IsValueCreated && UnpackV1.Value is IDisposable unpackV1) - { - unpackV1.Dispose(); - } - + DisposeUnpackers(); _disposed = true; await base.DisposeAsync().ConfigureAwait(false); } diff --git a/src/SharpCompress/Archives/Rar/RarArchive.cs b/src/SharpCompress/Archives/Rar/RarArchive.cs index 14d8aaa3..0f8e7de8 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.cs @@ -41,20 +41,24 @@ public partial class RarArchive { if (!_disposed) { - if (UnpackV1.IsValueCreated && UnpackV1.Value is IDisposable unpackV1) - { - unpackV1.Dispose(); - } - if (UnpackV2017.IsValueCreated && UnpackV2017.Value is IDisposable unpackV2017) - { - unpackV2017.Dispose(); - } - + DisposeUnpackers(); _disposed = true; base.Dispose(); } } + private void DisposeUnpackers() + { + if (UnpackV1.IsValueCreated && UnpackV1.Value is IDisposable unpackV1) + { + unpackV1.Dispose(); + } + if (UnpackV2017.IsValueCreated && UnpackV2017.Value is IDisposable unpackV2017) + { + unpackV2017.Dispose(); + } + } + protected override IEnumerable LoadEntries(IEnumerable volumes) => RarArchiveEntryFactory.GetEntries(this, volumes, ReaderOptions); diff --git a/src/SharpCompress/Readers/Rar/RarReader.Async.cs b/src/SharpCompress/Readers/Rar/RarReader.Async.cs index d8cd74e4..f25c442b 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.Async.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.Async.cs @@ -12,6 +12,17 @@ namespace SharpCompress.Readers.Rar; public abstract partial class RarReader { + public override async ValueTask DisposeAsync() + { + if (!_disposed) + { + // The unpackers own pooled buffers even when extraction uses async I/O. + DisposeUnpackers(); + _disposed = true; + await base.DisposeAsync().ConfigureAwait(false); + } + } + /// /// Returns file parts asynchronously for the current entry. /// Used for async stream operations in solid RAR archives. diff --git a/src/SharpCompress/Readers/Rar/RarReader.cs b/src/SharpCompress/Readers/Rar/RarReader.cs index 829cf97a..967a6148 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.cs @@ -27,20 +27,24 @@ public abstract partial class RarReader : AbstractReader volume; diff --git a/tests/SharpCompress.Test/Streams/RarLifetimeTests.cs b/tests/SharpCompress.Test/Streams/RarLifetimeTests.cs new file mode 100644 index 00000000..eb9743d2 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/RarLifetimeTests.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Archives.Rar; +using SharpCompress.Common; +using SharpCompress.Compressors.Rar; +using SharpCompress.Readers; +using SharpCompress.Readers.Rar; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class RarLifetimeTests : TestBase +{ + [Theory] + [InlineData("Rar.rar", true)] + [InlineData("Rar.rar", false)] + [InlineData("Rar5.rar", true)] + [InlineData("Rar5.rar", false)] + public async Task Reader_Async_Dispose_Releases_Unpacker_Window(string filename, bool leaveOpen) + { + using var input = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); + await using var reader = await ReaderFactory.OpenAsyncReader( + input, + new ReaderOptions { LeaveStreamOpen = leaveOpen }, + CancellationToken.None + ); + while (await reader.MoveToNextEntryAsync(CancellationToken.None)) + { + if (!reader.Entry.IsDirectory) + { + await reader.WriteEntryToAsync(Stream.Null, CancellationToken.None); + } + } + + var property = typeof(RarReader).GetProperty( + filename == "Rar.rar" ? "UnpackV1" : "UnpackV2017", + BindingFlags.Instance | BindingFlags.NonPublic + )!; + var unpack = ((Lazy)property.GetValue(reader)!).Value; + var window = GetWindow(unpack, filename); + Assert.NotNull(window.GetValue(unpack)); + + await reader.DisposeAsync(); + await reader.DisposeAsync(); + ((IDisposable)reader).Dispose(); + + Assert.Null(window.GetValue(unpack)); + Assert.Equal(leaveOpen, input.CanRead); + } + + [Theory] + [InlineData("Rar.rar", true)] + [InlineData("Rar.rar", false)] + [InlineData("Rar5.rar", true)] + [InlineData("Rar5.rar", false)] + public async Task Archive_Async_Dispose_Releases_Unpacker_Window( + string filename, + bool leaveOpen + ) + { + using var input = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); + await using var archive = await RarArchive.OpenAsyncArchive( + input, + new ReaderOptions { LeaveStreamOpen = leaveOpen }, + CancellationToken.None + ); + await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)) + { +#if LEGACY_DOTNET + using var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); +#else + await using var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); +#endif + await stream.CopyToAsync(Stream.Null, Constants.BufferSize, CancellationToken.None); + } + + var rarArchive = (RarArchive)archive; + var unpack = (filename == "Rar.rar" ? rarArchive.UnpackV1 : rarArchive.UnpackV2017).Value; + var window = GetWindow(unpack, filename); + Assert.NotNull(window.GetValue(unpack)); + + await archive.DisposeAsync(); + await archive.DisposeAsync(); + rarArchive.Dispose(); + + Assert.Null(window.GetValue(unpack)); + Assert.Equal(leaveOpen, input.CanRead); + } + + private static FieldInfo GetWindow(IRarUnpack unpack, string filename) => + unpack + .GetType() + .GetField( + filename == "Rar.rar" ? "window" : "Window", + BindingFlags.Instance | BindingFlags.NonPublic + )!; +}