Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/SharpCompress/Archives/AbstractArchive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ public IReader ExtractAllEntries()
/// </summary>
public virtual bool IsEncrypted => false;

/// <summary>
/// Returns whether multi-threaded extraction is supported for this archive.
/// Multi-threading is supported when:
/// 1. The archive is opened from a FileInfo or file path (not a stream)
/// 2. Multi-threading is explicitly enabled in ReaderOptions
/// 3. The archive is not SOLID (SOLID archives should use sequential extraction)
/// </summary>
public virtual bool SupportsMultiThreadedExtraction =>
_sourceStream is not null
&& _sourceStream.IsFileMode
&& ReaderOptions.EnableMultiThreadedExtraction
&& !IsSolid;

/// <summary>
/// The archive can find all the parts of the archive needed to fully extract the archive. This forces the parsing of the entire archive.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/SharpCompress/Archives/IArchive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,12 @@ public interface IArchive : IDisposable
/// Returns whether the archive is encrypted.
/// </summary>
bool IsEncrypted { get; }

/// <summary>
/// Returns whether multi-threaded extraction is supported for this archive.
/// Multi-threading is supported when the archive is opened from a FileInfo or file path
/// (not a stream) and the format supports random access (e.g., Zip, Tar, Rar).
/// SOLID archives (some Rar, all 7Zip) should use sequential extraction for best performance.
/// </summary>
bool SupportsMultiThreadedExtraction { get; }
}
71 changes: 71 additions & 0 deletions src/SharpCompress/Archives/Rar/SeekableFilePart.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.IO;
using SharpCompress.Common.Rar;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.IO;

namespace SharpCompress.Archives.Rar;

Expand All @@ -24,6 +25,76 @@ internal SeekableFilePart(

internal override Stream GetCompressedStream()
{
Stream streamToUse;

// If the stream is a SourceStream in file mode with multi-threading enabled,
// create an independent stream to support concurrent extraction
if (
_stream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
{
streamToUse = independentStream;
streamToUse.Position = FileHeader.DataStartPosition;

if (FileHeader.R4Salt != null)
{
var cryptKey = new CryptKey3(_password!);
return new RarCryptoWrapper(streamToUse, FileHeader.R4Salt, cryptKey);
}

if (FileHeader.Rar5CryptoInfo != null)
{
var cryptKey = new CryptKey5(_password!, FileHeader.Rar5CryptoInfo);
return new RarCryptoWrapper(
streamToUse,
FileHeader.Rar5CryptoInfo.Salt,
cryptKey
);
}

return streamToUse;
}
}

// Check if the stream wraps a FileStream
Stream? underlyingStream = _stream;
if (_stream is IStreamStack streamStack)
{
underlyingStream = streamStack.BaseStream();
}

if (underlyingStream is FileStream fileStream)
{
// Create a new independent stream from the file
streamToUse = new FileStream(
fileStream.Name,
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
streamToUse.Position = FileHeader.DataStartPosition;

if (FileHeader.R4Salt != null)
{
var cryptKey = new CryptKey3(_password!);
return new RarCryptoWrapper(streamToUse, FileHeader.R4Salt, cryptKey);
}

if (FileHeader.Rar5CryptoInfo != null)
{
var cryptKey = new CryptKey5(_password!, FileHeader.Rar5CryptoInfo);
return new RarCryptoWrapper(streamToUse, FileHeader.Rar5CryptoInfo.Salt, cryptKey);
}

return streamToUse;
}

// Fall back to existing behavior for stream-based sources
_stream.Position = FileHeader.DataStartPosition;

if (FileHeader.R4Salt != null)
Expand Down
50 changes: 41 additions & 9 deletions src/SharpCompress/Common/Tar/TarFilePart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Tar.Headers;
using SharpCompress.IO;

namespace SharpCompress.Common.Tar;

Expand All @@ -22,10 +23,47 @@ internal TarFilePart(TarHeader header, Stream? seekableStream)

internal override Stream GetCompressedStream()
{
if (_seekableStream != null)
if (_seekableStream is not null)
{
// If the seekable stream is a SourceStream in file mode with multi-threading enabled,
// create an independent stream to support concurrent extraction
if (
_seekableStream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
{
independentStream.Position = Header.DataStartPosition ?? 0;
return new TarReadOnlySubStream(independentStream, Header.Size);
}
}

// Check if the seekable stream wraps a FileStream
Stream? underlyingStream = _seekableStream;
if (_seekableStream is IStreamStack streamStack)
{
underlyingStream = streamStack.BaseStream();
}

if (underlyingStream is FileStream fileStream)
{
// Create a new independent stream from the file
var independentStream = new FileStream(
fileStream.Name,
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
independentStream.Position = Header.DataStartPosition ?? 0;
return new TarReadOnlySubStream(independentStream, Header.Size);
}

// Fall back to existing behavior for stream-based sources
_seekableStream.Position = Header.DataStartPosition ?? 0;
return new TarReadOnlySubStream(_seekableStream, Header.Size, false);
return new TarReadOnlySubStream(_seekableStream, Header.Size);
}
return Header.PackedStream.NotNull();
}
Expand All @@ -36,14 +74,8 @@ internal override Stream GetCompressedStream()
{
if (_seekableStream != null)
{
var useSyncOverAsync = false;
#if LEGACY_DOTNET
useSyncOverAsync = true;
#endif
_seekableStream.Position = Header.DataStartPosition ?? 0;
return new ValueTask<Stream?>(
new TarReadOnlySubStream(_seekableStream, Header.Size, useSyncOverAsync)
);
return new ValueTask<Stream?>(new TarReadOnlySubStream(_seekableStream, Header.Size));
}
return new ValueTask<Stream?>(Header.PackedStream.NotNull());
}
Expand Down
10 changes: 1 addition & 9 deletions src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,7 @@ IArchiveEncoding archiveEncoding
break;
case StreamingMode.Streaming:
{
var useSyncOverAsync = false;
#if LEGACY_DOTNET
useSyncOverAsync = true;
#endif
header.PackedStream = new TarReadOnlySubStream(
stream,
header.Size,
useSyncOverAsync
);
header.PackedStream = new TarReadOnlySubStream(stream, header.Size);
}
break;
default:
Expand Down
6 changes: 1 addition & 5 deletions src/SharpCompress/Common/Tar/TarHeaderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@ IArchiveEncoding archiveEncoding
break;
case StreamingMode.Streaming:
{
header.PackedStream = new TarReadOnlySubStream(
stream,
header.Size,
false
);
header.PackedStream = new TarReadOnlySubStream(stream, header.Size);
}
break;
default:
Expand Down
2 changes: 1 addition & 1 deletion src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal class TarReadOnlySubStream : Stream
private bool _isDisposed;
private long _amountRead;

public TarReadOnlySubStream(Stream stream, long bytesToRead, bool useSyncOverAsyncDispose)
public TarReadOnlySubStream(Stream stream, long bytesToRead)
{
_stream = stream;
BytesLeftToRead = bytesToRead;
Expand Down
90 changes: 83 additions & 7 deletions src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,102 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;

namespace SharpCompress.Common.Zip;

internal partial class SeekableZipFilePart
internal partial class SeekableZipFilePart : IDisposable
{
private readonly SemaphoreSlim _asyncHeaderSemaphore = new(1, 1);

internal override async ValueTask<Stream?> GetCompressedStreamAsync(
CancellationToken cancellationToken = default
)
{
if (!_isLocalHeaderLoaded)
{
await LoadLocalHeaderAsync(cancellationToken).ConfigureAwait(false);
_isLocalHeaderLoaded = true;
await _asyncHeaderSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (!_isLocalHeaderLoaded)
{
await LoadLocalHeaderAsync(cancellationToken).ConfigureAwait(false);
_isLocalHeaderLoaded = true;
}
}
finally
{
_asyncHeaderSemaphore.Release();
}
}
return await base.GetCompressedStreamAsync(cancellationToken).ConfigureAwait(false);
}

private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default) =>
Header = await _headerFactory
.GetLocalHeaderAsync(BaseStream, (DirectoryEntryHeader)Header)
.ConfigureAwait(false);
private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default)
{
// Use an independent stream for loading the header if multi-threading is enabled
Stream streamToUse = BaseStream;
bool disposeStream = false;

if (
BaseStream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
{
streamToUse = independentStream;
disposeStream = true;
}
}
else
{
// Check if BaseStream wraps a FileStream
Stream? underlyingStream = BaseStream;
if (BaseStream is IStreamStack streamStack)
{
underlyingStream = streamStack.BaseStream();
}

if (underlyingStream is FileStream fileStream)
{
streamToUse = new FileStream(
fileStream.Name,
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
disposeStream = true;
}
}

try
{
Header = await _headerFactory
.GetLocalHeaderAsync(streamToUse, (DirectoryEntryHeader)Header)
.ConfigureAwait(false);
}
finally
{
if (disposeStream)
{
if (streamToUse is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
}
else
{
#pragma warning disable VSTHRD103
streamToUse.Dispose();
#pragma warning restore VSTHRD103
}
}
}
}

public void Dispose() => _asyncHeaderSemaphore.Dispose();
}
Loading
Loading