Compare commits

...

5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
5a7472146c Add IRarUnpackFactory and refactor RarStream to use factory pattern
- Created IRarUnpackFactory interface with UnpackV1Factory and UnpackV2017Factory implementations
- RarStream now uses factory to create owned IRarUnpack instances for non-solid archives
- RarStream can still accept shared IRarUnpack instance for solid archives (with ownsUnpack flag)
- Updated RarCrcStream and RarBLAKE2spStream to support both factory and shared instance modes
- Solid archives continue to use shared Unpack instance (required for sequential decompression)
- Non-solid archives use factory to create new instances per stream (enables multi-threading)

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 14:02:28 +00:00
copilot-swe-agent[bot]
3ec4035d55 Fix SupportsMultiThreading to consider solid archives
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 13:26:24 +00:00
copilot-swe-agent[bot]
acffccd889 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>
2025-11-29 13:21:24 +00:00
copilot-swe-agent[bot]
31fd8f46d8 Fix Windows-only test failure by removing multi-threading support for RAR archives
RAR decompression uses a shared Unpack instance which is not thread-safe.
Removed SupportsMultiThreading override from RarArchive and RarArchiveEntry
classes. Also removed the RAR multi-threaded tests since RAR doesn't support
parallel extraction.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-27 17:31:13 +00:00
copilot-swe-agent[bot]
ac32f2eb1f Initial plan 2025-11-27 16:49:51 +00:00
7 changed files with 295 additions and 61 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());

View File

@@ -70,50 +70,50 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
public Stream OpenEntryStream()
{
RarStream stream;
if (IsRarV3)
var readStream = new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive);
// For solid archives, use shared Unpack instance (must be processed sequentially)
// For non-solid archives, use factory to create owned instance (supports multi-threading)
if (archive.IsSolid)
{
stream = new RarStream(
archive.UnpackV1.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
);
var unpack = IsRarV3 ? archive.UnpackV1.Value : archive.UnpackV2017.Value;
var stream = new RarStream(unpack, FileHeader, readStream, ownsUnpack: false);
stream.Initialize();
return stream;
}
else
{
stream = new RarStream(
archive.UnpackV2017.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
);
var factory = IsRarV3
? (IRarUnpackFactory)UnpackV1Factory.Instance
: UnpackV2017Factory.Instance;
var stream = new RarStream(factory, FileHeader, readStream);
stream.Initialize();
return stream;
}
stream.Initialize();
return stream;
}
public async Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
{
RarStream stream;
if (IsRarV3)
var readStream = new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive);
// For solid archives, use shared Unpack instance (must be processed sequentially)
// For non-solid archives, use factory to create owned instance (supports multi-threading)
if (archive.IsSolid)
{
stream = new RarStream(
archive.UnpackV1.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
);
var unpack = IsRarV3 ? archive.UnpackV1.Value : archive.UnpackV2017.Value;
var stream = new RarStream(unpack, FileHeader, readStream, ownsUnpack: false);
await stream.InitializeAsync(cancellationToken);
return stream;
}
else
{
stream = new RarStream(
archive.UnpackV2017.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
);
var factory = IsRarV3
? (IRarUnpackFactory)UnpackV1Factory.Instance
: UnpackV2017Factory.Instance;
var stream = new RarStream(factory, FileHeader, readStream);
await stream.InitializeAsync(cancellationToken);
return stream;
}
await stream.InitializeAsync(cancellationToken);
return stream;
}
public bool IsComplete
@@ -135,5 +135,6 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
}
}
public override bool SupportsMultiThreading => Parts.Single().SupportsMultiThreading;
public override bool SupportsMultiThreading =>
!archive.IsSolid && Parts.Single().SupportsMultiThreading;
}

View File

@@ -0,0 +1,34 @@
namespace SharpCompress.Compressors.Rar;
/// <summary>
/// Factory interface for creating IRarUnpack instances.
/// Each created instance is owned by the caller and should be disposed when done.
/// </summary>
internal interface IRarUnpackFactory
{
/// <summary>
/// Creates a new IRarUnpack instance.
/// The caller is responsible for disposing the returned instance.
/// </summary>
IRarUnpack Create();
}
/// <summary>
/// Factory for creating UnpackV1 instances (RAR v3 and earlier).
/// </summary>
internal sealed class UnpackV1Factory : IRarUnpackFactory
{
public static readonly UnpackV1Factory Instance = new();
public IRarUnpack Create() => new UnpackV1.Unpack();
}
/// <summary>
/// Factory for creating UnpackV2017 instances (RAR v5+).
/// </summary>
internal sealed class UnpackV2017Factory : IRarUnpackFactory
{
public static readonly UnpackV2017Factory Instance = new();
public IRarUnpack Create() => new UnpackV2017.Unpack();
}

View File

@@ -106,11 +106,30 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack
byte[] _hash = { };
private RarBLAKE2spStream(
IRarUnpack unpack,
IRarUnpackFactory unpackFactory,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream
)
: base(unpack, fileHeader, readStream)
: base(unpackFactory, fileHeader, readStream)
{
this.readStream = readStream;
#if DEBUG_STREAMS
this.DebugConstruct(typeof(RarBLAKE2spStream));
#endif
disableCRCCheck = fileHeader.IsEncrypted;
_hash = fileHeader.FileCrc.NotNull();
_blake2sp = new BLAKE2SP();
ResetCrc();
}
private RarBLAKE2spStream(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
bool ownsUnpack
)
: base(unpack, fileHeader, readStream, ownsUnpack)
{
this.readStream = readStream;
@@ -124,24 +143,49 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack
}
public static RarBLAKE2spStream Create(
IRarUnpack unpack,
IRarUnpackFactory unpackFactory,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream
)
{
var stream = new RarBLAKE2spStream(unpack, fileHeader, readStream);
var stream = new RarBLAKE2spStream(unpackFactory, fileHeader, readStream);
stream.Initialize();
return stream;
}
public static RarBLAKE2spStream Create(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
bool ownsUnpack
)
{
var stream = new RarBLAKE2spStream(unpack, fileHeader, readStream, ownsUnpack);
stream.Initialize();
return stream;
}
public static async Task<RarBLAKE2spStream> CreateAsync(
IRarUnpackFactory unpackFactory,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
CancellationToken cancellationToken = default
)
{
var stream = new RarBLAKE2spStream(unpackFactory, fileHeader, readStream);
await stream.InitializeAsync(cancellationToken);
return stream;
}
public static async Task<RarBLAKE2spStream> CreateAsync(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
bool ownsUnpack,
CancellationToken cancellationToken = default
)
{
var stream = new RarBLAKE2spStream(unpack, fileHeader, readStream);
var stream = new RarBLAKE2spStream(unpack, fileHeader, readStream, ownsUnpack);
await stream.InitializeAsync(cancellationToken);
return stream;
}

View File

@@ -34,11 +34,27 @@ internal class RarCrcStream : RarStream, IStreamStack
private readonly bool disableCRC;
private RarCrcStream(
IRarUnpack unpack,
IRarUnpackFactory unpackFactory,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream
)
: base(unpack, fileHeader, readStream)
: base(unpackFactory, fileHeader, readStream)
{
this.readStream = readStream;
#if DEBUG_STREAMS
this.DebugConstruct(typeof(RarCrcStream));
#endif
disableCRC = fileHeader.IsEncrypted;
ResetCrc();
}
private RarCrcStream(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
bool ownsUnpack
)
: base(unpack, fileHeader, readStream, ownsUnpack)
{
this.readStream = readStream;
#if DEBUG_STREAMS
@@ -49,24 +65,49 @@ internal class RarCrcStream : RarStream, IStreamStack
}
public static RarCrcStream Create(
IRarUnpack unpack,
IRarUnpackFactory unpackFactory,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream
)
{
var stream = new RarCrcStream(unpack, fileHeader, readStream);
var stream = new RarCrcStream(unpackFactory, fileHeader, readStream);
stream.Initialize();
return stream;
}
public static RarCrcStream Create(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
bool ownsUnpack
)
{
var stream = new RarCrcStream(unpack, fileHeader, readStream, ownsUnpack);
stream.Initialize();
return stream;
}
public static async Task<RarCrcStream> CreateAsync(
IRarUnpackFactory unpackFactory,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
CancellationToken cancellationToken = default
)
{
var stream = new RarCrcStream(unpackFactory, fileHeader, readStream);
await stream.InitializeAsync(cancellationToken);
return stream;
}
public static async Task<RarCrcStream> CreateAsync(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
bool ownsUnpack,
CancellationToken cancellationToken = default
)
{
var stream = new RarCrcStream(unpack, fileHeader, readStream);
var stream = new RarCrcStream(unpack, fileHeader, readStream, ownsUnpack);
await stream.InitializeAsync(cancellationToken);
return stream;
}

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,28 @@ internal class RarStream : Stream, IStreamStack
private bool isDisposed;
private long _position;
public RarStream(IRarUnpack unpack, FileHeader fileHeader, Stream readStream)
/// <summary>
/// Creates a new RarStream that owns and will dispose its IRarUnpack instance.
/// </summary>
/// <param name="unpackFactory">Factory to create the IRarUnpack instance</param>
/// <param name="fileHeader">File header for the entry</param>
/// <param name="readStream">Stream to read compressed data from</param>
public RarStream(IRarUnpackFactory unpackFactory, FileHeader fileHeader, Stream readStream)
: this(unpackFactory.Create(), fileHeader, readStream, ownsUnpack: true) { }
/// <summary>
/// Creates a new RarStream with the specified unpack instance.
/// </summary>
/// <param name="unpack">The IRarUnpack instance to use</param>
/// <param name="fileHeader">File header for the entry</param>
/// <param name="readStream">Stream to read compressed data from</param>
/// <param name="ownsUnpack">Whether this stream should dispose the unpack instance</param>
internal RarStream(IRarUnpack unpack, FileHeader fileHeader, Stream readStream, bool ownsUnpack)
{
this.unpack = unpack;
this.fileHeader = fileHeader;
this.readStream = readStream;
this.ownsUnpack = ownsUnpack;
#if DEBUG_STREAMS
this.DebugConstruct(typeof(RarStream));
@@ -84,6 +102,12 @@ internal class RarStream : Stream, IStreamStack
{
ArrayPool<byte>.Shared.Return(this.tmpBuffer);
this.tmpBuffer = null;
// Dispose the unpack instance if we own it
if (ownsUnpack && unpack is IDisposable disposableUnpack)
{
disposableUnpack.Dispose();
}
}
isDisposed = true;
base.Dispose(disposing);

View File

@@ -15,6 +15,8 @@ public abstract class RarReader : AbstractReader<RarReaderEntry, RarVolume>
{
private bool _disposed;
private RarVolume? volume;
// Shared Unpack instances for solid archives (must be used sequentially)
private Lazy<IRarUnpack> UnpackV2017 { get; } =
new(() => new Compressors.Rar.UnpackV2017.Unpack());
private Lazy<IRarUnpack> UnpackV1 { get; } = new(() => new Compressors.Rar.UnpackV1.Unpack());
@@ -111,19 +113,50 @@ public abstract class RarReader : AbstractReader<RarReaderEntry, RarVolume>
CreateFilePartEnumerableForCurrentEntry().Cast<RarFilePart>(),
this
);
if (Entry.IsRarV3)
{
return CreateEntryStream(RarCrcStream.Create(UnpackV1.Value, Entry.FileHeader, stream));
}
if (Entry.FileHeader.FileCrc?.Length > 5)
// For solid archives, use shared Unpack instance (must be processed sequentially)
// For non-solid archives, use factory to create owned instance
if (Entry.IsSolid || Entry.FileHeader.IsSolid)
{
var unpack = Entry.IsRarV3 ? UnpackV1.Value : UnpackV2017.Value;
if (Entry.IsRarV3)
{
return CreateEntryStream(
RarCrcStream.Create(unpack, Entry.FileHeader, stream, ownsUnpack: false)
);
}
if (Entry.FileHeader.FileCrc?.Length > 5)
{
return CreateEntryStream(
RarBLAKE2spStream.Create(unpack, Entry.FileHeader, stream, ownsUnpack: false)
);
}
return CreateEntryStream(
RarBLAKE2spStream.Create(UnpackV2017.Value, Entry.FileHeader, stream)
RarCrcStream.Create(unpack, Entry.FileHeader, stream, ownsUnpack: false)
);
}
else
{
var factory = Entry.IsRarV3
? (IRarUnpackFactory)UnpackV1Factory.Instance
: UnpackV2017Factory.Instance;
return CreateEntryStream(RarCrcStream.Create(UnpackV2017.Value, Entry.FileHeader, stream));
if (Entry.IsRarV3)
{
return CreateEntryStream(RarCrcStream.Create(factory, Entry.FileHeader, stream));
}
if (Entry.FileHeader.FileCrc?.Length > 5)
{
return CreateEntryStream(
RarBLAKE2spStream.Create(factory, Entry.FileHeader, stream)
);
}
return CreateEntryStream(RarCrcStream.Create(factory, Entry.FileHeader, stream));
}
}
protected override async System.Threading.Tasks.Task<EntryStream> GetEntryStreamAsync(
@@ -139,28 +172,83 @@ public abstract class RarReader : AbstractReader<RarReaderEntry, RarVolume>
CreateFilePartEnumerableForCurrentEntry().Cast<RarFilePart>(),
this
);
if (Entry.IsRarV3)
// For solid archives, use shared Unpack instance (must be processed sequentially)
// For non-solid archives, use factory to create owned instance
if (Entry.IsSolid || Entry.FileHeader.IsSolid)
{
var unpack = Entry.IsRarV3 ? UnpackV1.Value : UnpackV2017.Value;
if (Entry.IsRarV3)
{
return CreateEntryStream(
await RarCrcStream
.CreateAsync(
unpack,
Entry.FileHeader,
stream,
ownsUnpack: false,
cancellationToken
)
.ConfigureAwait(false)
);
}
if (Entry.FileHeader.FileCrc?.Length > 5)
{
return CreateEntryStream(
await RarBLAKE2spStream
.CreateAsync(
unpack,
Entry.FileHeader,
stream,
ownsUnpack: false,
cancellationToken
)
.ConfigureAwait(false)
);
}
return CreateEntryStream(
await RarCrcStream
.CreateAsync(UnpackV1.Value, Entry.FileHeader, stream, cancellationToken)
.CreateAsync(
unpack,
Entry.FileHeader,
stream,
ownsUnpack: false,
cancellationToken
)
.ConfigureAwait(false)
);
}
if (Entry.FileHeader.FileCrc?.Length > 5)
else
{
var factory = Entry.IsRarV3
? (IRarUnpackFactory)UnpackV1Factory.Instance
: UnpackV2017Factory.Instance;
if (Entry.IsRarV3)
{
return CreateEntryStream(
await RarCrcStream
.CreateAsync(factory, Entry.FileHeader, stream, cancellationToken)
.ConfigureAwait(false)
);
}
if (Entry.FileHeader.FileCrc?.Length > 5)
{
return CreateEntryStream(
await RarBLAKE2spStream
.CreateAsync(factory, Entry.FileHeader, stream, cancellationToken)
.ConfigureAwait(false)
);
}
return CreateEntryStream(
await RarBLAKE2spStream
.CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken)
await RarCrcStream
.CreateAsync(factory, Entry.FileHeader, stream, cancellationToken)
.ConfigureAwait(false)
);
}
return CreateEntryStream(
await RarCrcStream
.CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken)
.ConfigureAwait(false)
);
}
}