Refactor RAR to create new Unpack instances per stream for multi-threading support

- For non-solid archives: create new IRarUnpack instance per RarStream to enable multi-threading
- For solid archives: use shared Unpack instance (solid archives require sequential processing)
- Add ownsUnpack parameter to RarStream to properly manage disposal of Unpack instances
- RarStream now disposes Unpack only if it owns the instance

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-29 13:21:24 +00:00
parent 31fd8f46d8
commit acffccd889
4 changed files with 66 additions and 25 deletions

View File

@@ -15,6 +15,8 @@ namespace SharpCompress.Archives.Rar;
public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
{
private bool _disposed;
// Shared Unpack instances for solid archives (must be used sequentially)
internal Lazy<IRarUnpack> UnpackV2017 { get; } =
new(() => new Compressors.Rar.UnpackV2017.Unpack());
internal Lazy<IRarUnpack> UnpackV1 { get; } = new(() => new Compressors.Rar.UnpackV1.Unpack());
@@ -89,6 +91,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
}
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;

View File

@@ -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<RarFilePart>(), archive)
);
unpack = IsRarV3 ? archive.UnpackV1.Value : archive.UnpackV2017.Value;
ownsUnpack = false;
}
else
{
stream = new RarStream(
archive.UnpackV2017.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), 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<RarFilePart>(), archive),
ownsUnpack
);
stream.Initialize();
return stream;
}
public async Task<Stream> 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<RarFilePart>(), archive)
);
unpack = IsRarV3 ? archive.UnpackV1.Value : archive.UnpackV2017.Value;
ownsUnpack = false;
}
else
{
stream = new RarStream(
archive.UnpackV2017.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), 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<RarFilePart>(), archive),
ownsUnpack
);
await stream.InitializeAsync(cancellationToken);
return stream;
}
@@ -134,4 +148,6 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
);
}
}
public override bool SupportsMultiThreading => Parts.Single().SupportsMultiThreading;
}

View File

@@ -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<byte>.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);

View File

@@ -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()
{