diff --git a/src/SharpCompress/Archives/Rar/RarArchive.cs b/src/SharpCompress/Archives/Rar/RarArchive.cs index e0246687..47a586ee 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.cs @@ -15,6 +15,8 @@ namespace SharpCompress.Archives.Rar; public class RarArchive : AbstractArchive { private bool _disposed; + + // Shared Unpack instances for solid archives (must be used sequentially) internal Lazy UnpackV2017 { get; } = new(() => new Compressors.Rar.UnpackV2017.Unpack()); internal Lazy UnpackV1 { get; } = new(() => new Compressors.Rar.UnpackV1.Unpack()); @@ -89,6 +91,7 @@ public class RarArchive : AbstractArchive } public override bool IsSolid => Volumes.First().IsSolidArchive; + public override bool SupportsMultiThreading => !IsMultiVolume && !IsSolid; public virtual int MinVersion => Volumes.First().MinVersion; public virtual int MaxVersion => Volumes.First().MaxVersion; diff --git a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs index aaba6d1e..e9a38be1 100644 --- a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs @@ -70,48 +70,62 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry public Stream OpenEntryStream() { - RarStream stream; - if (IsRarV3) + IRarUnpack unpack; + bool ownsUnpack; + + // For solid archives, use shared Unpack instance (must be processed sequentially) + // For non-solid archives, create new instance per stream to support multi-threading + if (archive.IsSolid) { - stream = new RarStream( - archive.UnpackV1.Value, - FileHeader, - new MultiVolumeReadOnlyStream(Parts.Cast(), archive) - ); + unpack = IsRarV3 ? archive.UnpackV1.Value : archive.UnpackV2017.Value; + ownsUnpack = false; } else { - stream = new RarStream( - archive.UnpackV2017.Value, - FileHeader, - new MultiVolumeReadOnlyStream(Parts.Cast(), archive) - ); + unpack = IsRarV3 + ? new Compressors.Rar.UnpackV1.Unpack() + : new Compressors.Rar.UnpackV2017.Unpack(); + ownsUnpack = true; } + var stream = new RarStream( + unpack, + FileHeader, + new MultiVolumeReadOnlyStream(Parts.Cast(), archive), + ownsUnpack + ); + stream.Initialize(); return stream; } public async Task OpenEntryStreamAsync(CancellationToken cancellationToken = default) { - RarStream stream; - if (IsRarV3) + IRarUnpack unpack; + bool ownsUnpack; + + // For solid archives, use shared Unpack instance (must be processed sequentially) + // For non-solid archives, create new instance per stream to support multi-threading + if (archive.IsSolid) { - stream = new RarStream( - archive.UnpackV1.Value, - FileHeader, - new MultiVolumeReadOnlyStream(Parts.Cast(), archive) - ); + unpack = IsRarV3 ? archive.UnpackV1.Value : archive.UnpackV2017.Value; + ownsUnpack = false; } else { - stream = new RarStream( - archive.UnpackV2017.Value, - FileHeader, - new MultiVolumeReadOnlyStream(Parts.Cast(), archive) - ); + unpack = IsRarV3 + ? new Compressors.Rar.UnpackV1.Unpack() + : new Compressors.Rar.UnpackV2017.Unpack(); + ownsUnpack = true; } + var stream = new RarStream( + unpack, + FileHeader, + new MultiVolumeReadOnlyStream(Parts.Cast(), archive), + ownsUnpack + ); + await stream.InitializeAsync(cancellationToken); return stream; } @@ -134,4 +148,6 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry ); } } + + public override bool SupportsMultiThreading => Parts.Single().SupportsMultiThreading; } diff --git a/src/SharpCompress/Compressors/Rar/RarStream.cs b/src/SharpCompress/Compressors/Rar/RarStream.cs index 0611c0c4..e3bb056e 100644 --- a/src/SharpCompress/Compressors/Rar/RarStream.cs +++ b/src/SharpCompress/Compressors/Rar/RarStream.cs @@ -35,6 +35,7 @@ internal class RarStream : Stream, IStreamStack private readonly IRarUnpack unpack; private readonly FileHeader fileHeader; private readonly Stream readStream; + private readonly bool ownsUnpack; private bool fetch; @@ -49,11 +50,17 @@ internal class RarStream : Stream, IStreamStack private bool isDisposed; private long _position; - public RarStream(IRarUnpack unpack, FileHeader fileHeader, Stream readStream) + public RarStream( + IRarUnpack unpack, + FileHeader fileHeader, + Stream readStream, + bool ownsUnpack = false + ) { this.unpack = unpack; this.fileHeader = fileHeader; this.readStream = readStream; + this.ownsUnpack = ownsUnpack; #if DEBUG_STREAMS this.DebugConstruct(typeof(RarStream)); @@ -84,6 +91,12 @@ internal class RarStream : Stream, IStreamStack { ArrayPool.Shared.Return(this.tmpBuffer); this.tmpBuffer = null; + + // Only dispose the unpack instance if we own it + if (ownsUnpack && unpack is IDisposable disposableUnpack) + { + disposableUnpack.Dispose(); + } } isDisposed = true; base.Dispose(disposing); diff --git a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs index 9970b15c..025205db 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -293,9 +293,15 @@ public class RarArchiveTests : ArchiveTests [Fact] public void Rar_ArchiveFileRead() => ArchiveFileRead("Rar.rar"); + [Fact] + public Task Rar_ArchiveFileRead_Multithreaded() => ArchiveFileRead_Multithreaded("Rar.rar"); + [Fact] public void Rar5_ArchiveFileRead() => ArchiveFileRead("Rar5.rar"); + [Fact] + public Task Rar5_ArchiveFileRead_Multithreaded() => ArchiveFileRead_Multithreaded("Rar5.rar"); + [Fact] public void Rar_ArchiveFileRead_HasDirectories() => DoRar_ArchiveFileRead_HasDirectories("Rar.rar"); @@ -360,6 +366,9 @@ public class RarArchiveTests : ArchiveTests [Fact] public void Rar2_ArchiveFileRead() => ArchiveFileRead("Rar2.rar"); + [Fact] + public Task Rar2_ArchiveFileRead_Multithreaded() => ArchiveFileRead_Multithreaded("Rar2.rar"); + [Fact] public void Rar15_ArchiveFileRead() {