From e0a43e97272d6779029cc5ff692a0132ba7295f2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 18 Jan 2026 15:56:08 +0000
Subject: [PATCH 1/5] Initial plan
From 3e23a6e5a6d0af3904b750df39394759d3fdb3a0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 18 Jan 2026 16:09:54 +0000
Subject: [PATCH 2/5] Add multi-threading support for file-based archives -
sync test passing
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
.../Archives/Rar/SeekableFilePart.cs | 67 +++++++
src/SharpCompress/Common/Tar/TarFilePart.cs | 36 +++-
.../Common/Zip/SeekableZipFilePart.cs | 168 ++++++++++++++++--
src/SharpCompress/IO/SourceStream.cs | 24 +++
.../Tar/TarMultiThreadTests.cs | 101 +++++++++++
.../Zip/ZipMultiThreadTests.cs | 144 +++++++++++++++
6 files changed, 529 insertions(+), 11 deletions(-)
create mode 100644 tests/SharpCompress.Test/Tar/TarMultiThreadTests.cs
create mode 100644 tests/SharpCompress.Test/Zip/ZipMultiThreadTests.cs
diff --git a/src/SharpCompress/Archives/Rar/SeekableFilePart.cs b/src/SharpCompress/Archives/Rar/SeekableFilePart.cs
index 97822d90e..53f95c45e 100644
--- a/src/SharpCompress/Archives/Rar/SeekableFilePart.cs
+++ b/src/SharpCompress/Archives/Rar/SeekableFilePart.cs
@@ -1,6 +1,7 @@
using System.IO;
using SharpCompress.Common.Rar;
using SharpCompress.Common.Rar.Headers;
+using SharpCompress.IO;
namespace SharpCompress.Archives.Rar;
@@ -24,6 +25,72 @@ internal SeekableFilePart(
internal override Stream GetCompressedStream()
{
+ Stream streamToUse;
+
+ // If the stream is a SourceStream in file mode, create an independent stream
+ // to support concurrent multi-threaded extraction
+ if (_stream is SourceStream sourceStream && sourceStream.IsFileMode)
+ {
+ 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)
diff --git a/src/SharpCompress/Common/Tar/TarFilePart.cs b/src/SharpCompress/Common/Tar/TarFilePart.cs
index 065161149..7f5b1f3c5 100644
--- a/src/SharpCompress/Common/Tar/TarFilePart.cs
+++ b/src/SharpCompress/Common/Tar/TarFilePart.cs
@@ -1,5 +1,6 @@
using System.IO;
using SharpCompress.Common.Tar.Headers;
+using SharpCompress.IO;
namespace SharpCompress.Common.Tar;
@@ -20,8 +21,41 @@ 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, create an independent stream
+ // to support concurrent multi-threaded extraction
+ if (_seekableStream is SourceStream sourceStream && sourceStream.IsFileMode)
+ {
+ 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);
}
diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
index 7dbf93baa..ee82442be 100644
--- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
+++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
@@ -2,13 +2,16 @@
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Zip.Headers;
+using SharpCompress.IO;
namespace SharpCompress.Common.Zip;
internal class SeekableZipFilePart : ZipFilePart
{
- private bool _isLocalHeaderLoaded;
+ private volatile bool _isLocalHeaderLoaded;
private readonly SeekableZipHeaderFactory _headerFactory;
+ private readonly object _headerLock = new();
+ private readonly SemaphoreSlim _asyncHeaderSemaphore = new(1, 1);
internal SeekableZipFilePart(
SeekableZipHeaderFactory headerFactory,
@@ -21,8 +24,14 @@ internal override Stream GetCompressedStream()
{
if (!_isLocalHeaderLoaded)
{
- LoadLocalHeader();
- _isLocalHeaderLoaded = true;
+ lock (_headerLock)
+ {
+ if (!_isLocalHeaderLoaded)
+ {
+ LoadLocalHeader();
+ _isLocalHeaderLoaded = true;
+ }
+ }
}
return base.GetCompressedStream();
}
@@ -33,22 +42,161 @@ internal override Stream GetCompressedStream()
{
if (!_isLocalHeaderLoaded)
{
- await LoadLocalHeaderAsync(cancellationToken);
- _isLocalHeaderLoaded = true;
+ await _asyncHeaderSemaphore.WaitAsync(cancellationToken);
+ try
+ {
+ if (!_isLocalHeaderLoaded)
+ {
+ await LoadLocalHeaderAsync(cancellationToken);
+ _isLocalHeaderLoaded = true;
+ }
+ }
+ finally
+ {
+ _asyncHeaderSemaphore.Release();
+ }
}
return await base.GetCompressedStreamAsync(cancellationToken);
}
- private void LoadLocalHeader() =>
- Header = _headerFactory.GetLocalHeader(BaseStream, (DirectoryEntryHeader)Header);
+ private void LoadLocalHeader()
+ {
+ // Use an independent stream for loading the header if possible
+ Stream streamToUse = BaseStream;
+ bool disposeStream = false;
+
+ if (BaseStream is SourceStream sourceStream && sourceStream.IsFileMode)
+ {
+ 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 = _headerFactory.GetLocalHeader(streamToUse, (DirectoryEntryHeader)Header);
+ }
+ finally
+ {
+ if (disposeStream)
+ {
+ streamToUse.Dispose();
+ }
+ }
+ }
+
+ private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default)
+ {
+ // Use an independent stream for loading the header if possible
+ Stream streamToUse = BaseStream;
+ bool disposeStream = false;
+
+ if (BaseStream is SourceStream sourceStream && sourceStream.IsFileMode)
+ {
+ 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();
+ }
- private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default) =>
- Header = await _headerFactory.GetLocalHeaderAsync(BaseStream, (DirectoryEntryHeader)Header);
+ 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
+ );
+ }
+ finally
+ {
+ if (disposeStream)
+ {
+ streamToUse.Dispose();
+ }
+ }
+ }
protected override Stream CreateBaseStream()
{
- BaseStream.Position = Header.DataStartPosition.NotNull();
+ // If BaseStream is a SourceStream in file mode, create an independent stream
+ // to support concurrent multi-threaded extraction
+ if (BaseStream is SourceStream sourceStream && sourceStream.IsFileMode)
+ {
+ // Create a new independent stream for this entry
+ var independentStream = sourceStream.CreateIndependentStream(0);
+ if (independentStream is not null)
+ {
+ independentStream.Position = Header.DataStartPosition.NotNull();
+ return independentStream;
+ }
+ }
+
+ // Check if BaseStream wraps a FileStream (for multi-volume archives)
+ Stream? underlyingStream = BaseStream;
+ if (BaseStream 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.NotNull();
+ return independentStream;
+ }
+
+ // Fall back to existing behavior for stream-based sources
+ BaseStream.Position = Header.DataStartPosition.NotNull();
return BaseStream;
}
}
diff --git a/src/SharpCompress/IO/SourceStream.cs b/src/SharpCompress/IO/SourceStream.cs
index e9cc74a63..b650f55d4 100644
--- a/src/SharpCompress/IO/SourceStream.cs
+++ b/src/SharpCompress/IO/SourceStream.cs
@@ -98,6 +98,30 @@ public void LoadAllParts()
private Stream Current => _streams[_stream];
+ ///
+ /// Creates an independent stream for the specified volume index.
+ /// This allows multiple threads to read from different positions concurrently.
+ /// Only works when IsFileMode is true.
+ ///
+ /// The volume index to create a stream for
+ /// A new independent FileStream, or null if not in file mode or volume doesn't exist
+ public Stream? CreateIndependentStream(int volumeIndex)
+ {
+ if (!IsFileMode)
+ {
+ return null;
+ }
+
+ // Ensure the volume is loaded
+ if (!LoadStream(volumeIndex))
+ {
+ return null;
+ }
+
+ // Create a new independent stream from the FileInfo
+ return _files[volumeIndex].OpenRead();
+ }
+
public bool LoadStream(int index) //ensure all parts to id are loaded
{
while (_streams.Count <= index)
diff --git a/tests/SharpCompress.Test/Tar/TarMultiThreadTests.cs b/tests/SharpCompress.Test/Tar/TarMultiThreadTests.cs
new file mode 100644
index 000000000..f396d7aa7
--- /dev/null
+++ b/tests/SharpCompress.Test/Tar/TarMultiThreadTests.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using SharpCompress.Archives.Tar;
+using SharpCompress.Common;
+using Xunit;
+
+namespace SharpCompress.Test.Tar;
+
+public class TarMultiThreadTests : TestBase
+{
+ [Fact]
+ public void Tar_Archive_Concurrent_Extraction_From_FileInfo()
+ {
+ // Test concurrent extraction of multiple entries from a Tar archive opened from FileInfo
+ var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
+ var fileInfo = new FileInfo(testArchive);
+
+ using var archive = TarArchive.OpenArchive(fileInfo);
+ var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
+
+ // Extract multiple entries concurrently
+ var tasks = new List();
+ var outputFiles = new List();
+
+ foreach (var entry in entries)
+ {
+ var outputFile = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
+ outputFiles.Add(outputFile);
+
+ tasks.Add(
+ Task.Run(() =>
+ {
+ var dir = Path.GetDirectoryName(outputFile);
+ if (dir != null)
+ {
+ Directory.CreateDirectory(dir);
+ }
+ using var entryStream = entry.OpenEntryStream();
+ using var fileStream = File.Create(outputFile);
+ entryStream.CopyTo(fileStream);
+ })
+ );
+ }
+
+ Task.WaitAll(tasks.ToArray());
+
+ // Verify all files were extracted
+ Assert.Equal(entries.Count, outputFiles.Count);
+ foreach (var outputFile in outputFiles)
+ {
+ Assert.True(File.Exists(outputFile), $"File {outputFile} should exist");
+ }
+ }
+
+ [Fact]
+ public async Task Tar_Archive_Concurrent_Extraction_From_FileInfo_Async()
+ {
+ // Test concurrent async extraction of multiple entries from a Tar archive opened from FileInfo
+ var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
+ var fileInfo = new FileInfo(testArchive);
+
+ using var archive = TarArchive.OpenArchive(fileInfo);
+ var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
+
+ // Extract multiple entries concurrently
+ var tasks = new List();
+ var outputFiles = new List();
+
+ foreach (var entry in entries)
+ {
+ var outputFile = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
+ outputFiles.Add(outputFile);
+
+ tasks.Add(
+ Task.Run(async () =>
+ {
+ var dir = Path.GetDirectoryName(outputFile);
+ if (dir != null)
+ {
+ Directory.CreateDirectory(dir);
+ }
+ using var entryStream = await entry.OpenEntryStreamAsync();
+ using var fileStream = File.Create(outputFile);
+ await entryStream.CopyToAsync(fileStream);
+ })
+ );
+ }
+
+ await Task.WhenAll(tasks);
+
+ // Verify all files were extracted
+ Assert.Equal(entries.Count, outputFiles.Count);
+ foreach (var outputFile in outputFiles)
+ {
+ Assert.True(File.Exists(outputFile), $"File {outputFile} should exist");
+ }
+ }
+}
diff --git a/tests/SharpCompress.Test/Zip/ZipMultiThreadTests.cs b/tests/SharpCompress.Test/Zip/ZipMultiThreadTests.cs
new file mode 100644
index 000000000..661740a0e
--- /dev/null
+++ b/tests/SharpCompress.Test/Zip/ZipMultiThreadTests.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using SharpCompress.Archives.Zip;
+using SharpCompress.Common;
+using Xunit;
+
+namespace SharpCompress.Test.Zip;
+
+public class ZipMultiThreadTests : TestBase
+{
+ [Fact]
+ public void Zip_Archive_Concurrent_Extraction_From_FileInfo()
+ {
+ // Test concurrent extraction of multiple entries from a Zip archive opened from FileInfo
+ var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
+ var fileInfo = new FileInfo(testArchive);
+
+ using var archive = ZipArchive.OpenArchive(fileInfo);
+ var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
+
+ // Extract multiple entries concurrently
+ var tasks = new List();
+ var outputFiles = new List();
+
+ foreach (var entry in entries)
+ {
+ var outputFile = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
+ outputFiles.Add(outputFile);
+
+ tasks.Add(
+ Task.Run(() =>
+ {
+ var dir = Path.GetDirectoryName(outputFile);
+ if (dir != null)
+ {
+ Directory.CreateDirectory(dir);
+ }
+ using var entryStream = entry.OpenEntryStream();
+ using var fileStream = File.Create(outputFile);
+ entryStream.CopyTo(fileStream);
+ })
+ );
+ }
+
+ Task.WaitAll(tasks.ToArray());
+
+ // Verify all files were extracted
+ Assert.Equal(entries.Count, outputFiles.Count);
+ foreach (var outputFile in outputFiles)
+ {
+ Assert.True(File.Exists(outputFile), $"File {outputFile} should exist");
+ }
+ }
+
+ [Fact]
+ public async Task Zip_Archive_Concurrent_Extraction_From_FileInfo_Async()
+ {
+ // Test concurrent async extraction of multiple entries from a Zip archive opened from FileInfo
+ var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
+ var fileInfo = new FileInfo(testArchive);
+
+ using var archive = ZipArchive.OpenArchive(fileInfo);
+ var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
+
+ // Extract multiple entries concurrently
+ var tasks = new List();
+ var outputFiles = new List();
+
+ foreach (var entry in entries)
+ {
+ var outputFile = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
+ outputFiles.Add(outputFile);
+
+ tasks.Add(
+ Task.Run(async () =>
+ {
+ var dir = Path.GetDirectoryName(outputFile);
+ if (dir != null)
+ {
+ Directory.CreateDirectory(dir);
+ }
+ using var entryStream = await entry.OpenEntryStreamAsync();
+ using var fileStream = File.Create(outputFile);
+ await entryStream.CopyToAsync(fileStream);
+ })
+ );
+ }
+
+ await Task.WhenAll(tasks);
+
+ // Verify all files were extracted
+ Assert.Equal(entries.Count, outputFiles.Count);
+ foreach (var outputFile in outputFiles)
+ {
+ Assert.True(File.Exists(outputFile), $"File {outputFile} should exist");
+ }
+ }
+
+ [Fact]
+ public void Zip_Archive_Concurrent_Extraction_From_Path()
+ {
+ // Test concurrent extraction when opening from path (should use FileInfo internally)
+ var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
+
+ using var archive = ZipArchive.OpenArchive(testArchive);
+ var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
+
+ // Extract multiple entries concurrently
+ var tasks = new List();
+ var outputFiles = new List();
+
+ foreach (var entry in entries)
+ {
+ var outputFile = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
+ outputFiles.Add(outputFile);
+
+ tasks.Add(
+ Task.Run(() =>
+ {
+ var dir = Path.GetDirectoryName(outputFile);
+ if (dir != null)
+ {
+ Directory.CreateDirectory(dir);
+ }
+ using var entryStream = entry.OpenEntryStream();
+ using var fileStream = File.Create(outputFile);
+ entryStream.CopyTo(fileStream);
+ })
+ );
+ }
+
+ Task.WaitAll(tasks.ToArray());
+
+ // Verify all files were extracted
+ Assert.Equal(entries.Count, outputFiles.Count);
+ foreach (var outputFile in outputFiles)
+ {
+ Assert.True(File.Exists(outputFile), $"File {outputFile} should exist");
+ }
+ }
+}
From 4a6e5232ae664b06f955ae7b775ce0520cc5a884 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 18 Jan 2026 16:27:59 +0000
Subject: [PATCH 3/5] Add opt-in multi-threading support with
SupportsMultiThreadedExtraction flag
- Added IArchive.SupportsMultiThreadedExtraction property to indicate if multi-threading is supported
- Added ReaderOptions.EnableMultiThreadedExtraction option to opt-in to multi-threading
- Updated SeekableZipFilePart, TarFilePart, and SeekableFilePart to check the flag
- Added test to verify multi-threading flag behavior
- Multi-threading is now disabled by default for backward compatibility
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
src/SharpCompress/Archives/AbstractArchive.cs | 13 +++++
src/SharpCompress/Archives/IArchive.cs | 8 +++
.../Archives/Rar/SeekableFilePart.cs | 10 ++--
src/SharpCompress/Common/Tar/TarFilePart.cs | 10 ++--
.../Common/Zip/SeekableZipFilePart.cs | 26 ++++++---
src/SharpCompress/Readers/ReaderOptions.cs | 8 +++
.../Tar/TarMultiThreadTests.cs | 18 ++++++-
.../Zip/ZipMultiThreadTests.cs | 54 +++++++++++++++++--
8 files changed, 129 insertions(+), 18 deletions(-)
diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs
index 63e67e69b..c97e598f5 100644
--- a/src/SharpCompress/Archives/AbstractArchive.cs
+++ b/src/SharpCompress/Archives/AbstractArchive.cs
@@ -145,6 +145,19 @@ public IReader ExtractAllEntries()
///
public virtual bool IsEncrypted => false;
+ ///
+ /// 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)
+ ///
+ public virtual bool SupportsMultiThreadedExtraction =>
+ _sourceStream is not null
+ && _sourceStream.IsFileMode
+ && ReaderOptions.EnableMultiThreadedExtraction
+ && !IsSolid;
+
///
/// The archive can find all the parts of the archive needed to fully extract the archive. This forces the parsing of the entire archive.
///
diff --git a/src/SharpCompress/Archives/IArchive.cs b/src/SharpCompress/Archives/IArchive.cs
index 6016214b7..f36286492 100644
--- a/src/SharpCompress/Archives/IArchive.cs
+++ b/src/SharpCompress/Archives/IArchive.cs
@@ -44,4 +44,12 @@ public interface IArchive : IDisposable
/// Returns whether the archive is encrypted.
///
bool IsEncrypted { get; }
+
+ ///
+ /// 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.
+ ///
+ bool SupportsMultiThreadedExtraction { get; }
}
diff --git a/src/SharpCompress/Archives/Rar/SeekableFilePart.cs b/src/SharpCompress/Archives/Rar/SeekableFilePart.cs
index 53f95c45e..927ba8823 100644
--- a/src/SharpCompress/Archives/Rar/SeekableFilePart.cs
+++ b/src/SharpCompress/Archives/Rar/SeekableFilePart.cs
@@ -27,9 +27,13 @@ internal override Stream GetCompressedStream()
{
Stream streamToUse;
- // If the stream is a SourceStream in file mode, create an independent stream
- // to support concurrent multi-threaded extraction
- if (_stream is SourceStream sourceStream && sourceStream.IsFileMode)
+ // 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)
diff --git a/src/SharpCompress/Common/Tar/TarFilePart.cs b/src/SharpCompress/Common/Tar/TarFilePart.cs
index 7f5b1f3c5..59a8fab30 100644
--- a/src/SharpCompress/Common/Tar/TarFilePart.cs
+++ b/src/SharpCompress/Common/Tar/TarFilePart.cs
@@ -23,9 +23,13 @@ internal override Stream GetCompressedStream()
{
if (_seekableStream is not null)
{
- // If the seekable stream is a SourceStream in file mode, create an independent stream
- // to support concurrent multi-threaded extraction
- if (_seekableStream is SourceStream sourceStream && sourceStream.IsFileMode)
+ // 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)
diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
index ee82442be..c7f530e02 100644
--- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
+++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
@@ -61,11 +61,15 @@ internal override Stream GetCompressedStream()
private void LoadLocalHeader()
{
- // Use an independent stream for loading the header if possible
+ // 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)
+ if (
+ BaseStream is SourceStream sourceStream
+ && sourceStream.IsFileMode
+ && sourceStream.ReaderOptions.EnableMultiThreadedExtraction
+ )
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
@@ -110,11 +114,15 @@ private void LoadLocalHeader()
private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default)
{
- // Use an independent stream for loading the header if possible
+ // 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)
+ if (
+ BaseStream is SourceStream sourceStream
+ && sourceStream.IsFileMode
+ && sourceStream.ReaderOptions.EnableMultiThreadedExtraction
+ )
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
@@ -162,9 +170,13 @@ private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken
protected override Stream CreateBaseStream()
{
- // If BaseStream is a SourceStream in file mode, create an independent stream
- // to support concurrent multi-threaded extraction
- if (BaseStream is SourceStream sourceStream && sourceStream.IsFileMode)
+ // If BaseStream is a SourceStream in file mode with multi-threading enabled,
+ // create an independent stream to support concurrent extraction
+ if (
+ BaseStream is SourceStream sourceStream
+ && sourceStream.IsFileMode
+ && sourceStream.ReaderOptions.EnableMultiThreadedExtraction
+ )
{
// Create a new independent stream for this entry
var independentStream = sourceStream.CreateIndependentStream(0);
diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs
index cedf8bedb..f35a8dcc5 100644
--- a/src/SharpCompress/Readers/ReaderOptions.cs
+++ b/src/SharpCompress/Readers/ReaderOptions.cs
@@ -28,4 +28,12 @@ public class ReaderOptions : OptionsBase
/// When set, progress updates will be reported as entries are extracted.
///
public IProgress? Progress { get; set; }
+
+ ///
+ /// Enable multi-threaded extraction support when the archive is opened from a FileInfo or file path.
+ /// When enabled, multiple threads can extract different entries concurrently by creating
+ /// independent file streams. This is only effective for archives opened from files, not streams.
+ /// Default is false for backward compatibility.
+ ///
+ public bool EnableMultiThreadedExtraction { get; set; }
}
diff --git a/tests/SharpCompress.Test/Tar/TarMultiThreadTests.cs b/tests/SharpCompress.Test/Tar/TarMultiThreadTests.cs
index f396d7aa7..f7cbcc30f 100644
--- a/tests/SharpCompress.Test/Tar/TarMultiThreadTests.cs
+++ b/tests/SharpCompress.Test/Tar/TarMultiThreadTests.cs
@@ -18,7 +18,16 @@ public void Tar_Archive_Concurrent_Extraction_From_FileInfo()
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
var fileInfo = new FileInfo(testArchive);
- using var archive = TarArchive.OpenArchive(fileInfo);
+ var options = new SharpCompress.Readers.ReaderOptions
+ {
+ EnableMultiThreadedExtraction = true,
+ };
+
+ using var archive = TarArchive.OpenArchive(fileInfo, options);
+
+ // Verify multi-threading is supported
+ Assert.True(archive.SupportsMultiThreadedExtraction);
+
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
// Extract multiple entries concurrently
@@ -62,7 +71,12 @@ public async Task Tar_Archive_Concurrent_Extraction_From_FileInfo_Async()
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
var fileInfo = new FileInfo(testArchive);
- using var archive = TarArchive.OpenArchive(fileInfo);
+ var options = new SharpCompress.Readers.ReaderOptions
+ {
+ EnableMultiThreadedExtraction = true,
+ };
+
+ using var archive = TarArchive.OpenArchive(fileInfo, options);
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
// Extract multiple entries concurrently
diff --git a/tests/SharpCompress.Test/Zip/ZipMultiThreadTests.cs b/tests/SharpCompress.Test/Zip/ZipMultiThreadTests.cs
index 661740a0e..4100f5fa7 100644
--- a/tests/SharpCompress.Test/Zip/ZipMultiThreadTests.cs
+++ b/tests/SharpCompress.Test/Zip/ZipMultiThreadTests.cs
@@ -11,6 +11,35 @@ namespace SharpCompress.Test.Zip;
public class ZipMultiThreadTests : TestBase
{
+ [Fact]
+ public void Zip_Archive_Without_MultiThreading_Enabled()
+ {
+ // Test that extraction still works when multi-threading is NOT enabled
+ var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
+ var fileInfo = new FileInfo(testArchive);
+
+ // Default options - multi-threading disabled
+ using var archive = ZipArchive.OpenArchive(fileInfo);
+
+ // Verify multi-threading is NOT supported
+ Assert.False(archive.SupportsMultiThreadedExtraction);
+
+ var entry = archive.Entries.First(e => !e.IsDirectory);
+ var outputFile = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
+
+ var dir = Path.GetDirectoryName(outputFile);
+ if (dir != null)
+ {
+ Directory.CreateDirectory(dir);
+ }
+
+ using var entryStream = entry.OpenEntryStream();
+ using var fileStream = File.Create(outputFile);
+ entryStream.CopyTo(fileStream);
+
+ Assert.True(File.Exists(outputFile));
+ }
+
[Fact]
public void Zip_Archive_Concurrent_Extraction_From_FileInfo()
{
@@ -18,7 +47,16 @@ public void Zip_Archive_Concurrent_Extraction_From_FileInfo()
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
var fileInfo = new FileInfo(testArchive);
- using var archive = ZipArchive.OpenArchive(fileInfo);
+ var options = new SharpCompress.Readers.ReaderOptions
+ {
+ EnableMultiThreadedExtraction = true,
+ };
+
+ using var archive = ZipArchive.OpenArchive(fileInfo, options);
+
+ // Verify multi-threading is supported
+ Assert.True(archive.SupportsMultiThreadedExtraction);
+
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
// Extract multiple entries concurrently
@@ -62,7 +100,12 @@ public async Task Zip_Archive_Concurrent_Extraction_From_FileInfo_Async()
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
var fileInfo = new FileInfo(testArchive);
- using var archive = ZipArchive.OpenArchive(fileInfo);
+ var options = new SharpCompress.Readers.ReaderOptions
+ {
+ EnableMultiThreadedExtraction = true,
+ };
+
+ using var archive = ZipArchive.OpenArchive(fileInfo, options);
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
// Extract multiple entries concurrently
@@ -105,7 +148,12 @@ public void Zip_Archive_Concurrent_Extraction_From_Path()
// Test concurrent extraction when opening from path (should use FileInfo internally)
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.zip");
- using var archive = ZipArchive.OpenArchive(testArchive);
+ var options = new SharpCompress.Readers.ReaderOptions
+ {
+ EnableMultiThreadedExtraction = true,
+ };
+
+ using var archive = ZipArchive.OpenArchive(testArchive, options);
var entries = archive.Entries.Where(e => !e.IsDirectory).Take(5).ToList();
// Extract multiple entries concurrently
From 3530dd9d68299e962ca5e2a0334f809eeb8ba766 Mon Sep 17 00:00:00 2001
From: Adam Hathcock
Date: Thu, 28 May 2026 07:46:25 +0100
Subject: [PATCH 4/5] Fix merge
---
src/SharpCompress/Common/Tar/TarFilePart.cs | 8 +-
.../Common/Tar/TarHeaderFactory.Async.cs | 7 +-
.../Common/Tar/TarHeaderFactory.cs | 3 +-
.../Common/Tar/TarReadOnlySubStream.cs | 2 +-
.../Common/Zip/SeekableZipFilePart.Async.cs | 91 +++++++++++++++++--
.../Common/Zip/SeekableZipFilePart.cs | 58 +++++++++++-
src/SharpCompress/packages.lock.json | 12 +--
7 files changed, 149 insertions(+), 32 deletions(-)
diff --git a/src/SharpCompress/Common/Tar/TarFilePart.cs b/src/SharpCompress/Common/Tar/TarFilePart.cs
index 8a4c39c94..75b0be733 100644
--- a/src/SharpCompress/Common/Tar/TarFilePart.cs
+++ b/src/SharpCompress/Common/Tar/TarFilePart.cs
@@ -63,7 +63,7 @@ _seekableStream is SourceStream sourceStream
// 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();
}
@@ -74,13 +74,9 @@ _seekableStream is SourceStream sourceStream
{
if (_seekableStream != null)
{
- var useSyncOverAsync = false;
-#if LEGACY_DOTNET
- useSyncOverAsync = true;
-#endif
_seekableStream.Position = Header.DataStartPosition ?? 0;
return new ValueTask(
- new TarReadOnlySubStream(_seekableStream, Header.Size, useSyncOverAsync)
+ new TarReadOnlySubStream(_seekableStream, Header.Size)
);
}
return new ValueTask(Header.PackedStream.NotNull());
diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs
index 772a56651..460be1cc0 100644
--- a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs
+++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs
@@ -44,14 +44,9 @@ IArchiveEncoding archiveEncoding
break;
case StreamingMode.Streaming:
{
- var useSyncOverAsync = false;
-#if LEGACY_DOTNET
- useSyncOverAsync = true;
-#endif
header.PackedStream = new TarReadOnlySubStream(
stream,
- header.Size,
- useSyncOverAsync
+ header.Size
);
}
break;
diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs
index df71351e9..6053fa106 100644
--- a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs
+++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs
@@ -40,8 +40,7 @@ IArchiveEncoding archiveEncoding
{
header.PackedStream = new TarReadOnlySubStream(
stream,
- header.Size,
- false
+ header.Size
);
}
break;
diff --git a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs
index 987751a34..79a4c4a4e 100644
--- a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs
+++ b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs
@@ -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;
diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs
index 8fff84365..3b082a712 100644
--- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs
+++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs
@@ -1,26 +1,103 @@
+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 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();
+
}
diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
index 3262020ad..7e797facc 100644
--- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
+++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
@@ -1,6 +1,6 @@
using System.IO;
using SharpCompress.Common.Zip.Headers;
-using SharpCompress.Compressors;
+using SharpCompress.IO;
using SharpCompress.Providers;
namespace SharpCompress.Common.Zip;
@@ -10,7 +10,6 @@ internal partial class SeekableZipFilePart : ZipFilePart
private volatile bool _isLocalHeaderLoaded;
private readonly SeekableZipHeaderFactory _headerFactory;
private readonly object _headerLock = new();
- private readonly SemaphoreSlim _asyncHeaderSemaphore = new(1, 1);
internal SeekableZipFilePart(
SeekableZipHeaderFactory headerFactory,
@@ -36,8 +35,59 @@ internal override Stream GetCompressedStream()
return base.GetCompressedStream();
}
- private void LoadLocalHeader() =>
- Header = _headerFactory.GetLocalHeader(BaseStream, (DirectoryEntryHeader)Header);
+ private void LoadLocalHeader()
+ {
+ // 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 = _headerFactory.GetLocalHeader(streamToUse, (DirectoryEntryHeader)Header);
+ }
+ finally
+ {
+ if (disposeStream)
+ {
+ streamToUse.Dispose();
+ }
+ }
+ }
+
protected override Stream CreateBaseStream()
{
diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json
index e1e7afa51..738df5991 100644
--- a/src/SharpCompress/packages.lock.json
+++ b/src/SharpCompress/packages.lock.json
@@ -286,9 +286,9 @@
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
- "requested": "[10.0.8, )",
- "resolved": "10.0.8",
- "contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw=="
+ "requested": "[10.0.6, )",
+ "resolved": "10.0.6",
+ "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
@@ -388,9 +388,9 @@
"net8.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
- "requested": "[8.0.27, )",
- "resolved": "8.0.27",
- "contentHash": "rQi9TxifHRnXP7lVRZH05DxD2/XGbJp12q0ozcbrlBlBnyyzssFTH/2vLhtKWUp2CT1qVscTrcYTFiwTyKPKRg=="
+ "requested": "[8.0.26, )",
+ "resolved": "8.0.26",
+ "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
From d61b5cde2bfb39ecb0b45602a51b740e56c3697e Mon Sep 17 00:00:00 2001
From: Adam Hathcock
Date: Thu, 28 May 2026 09:22:25 +0100
Subject: [PATCH 5/5] fmt
---
src/SharpCompress/Common/Tar/TarFilePart.cs | 4 +---
src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs | 5 +----
src/SharpCompress/Common/Tar/TarHeaderFactory.cs | 5 +----
.../Common/Zip/SeekableZipFilePart.Async.cs | 9 ++++-----
src/SharpCompress/Common/Zip/SeekableZipFilePart.cs | 1 -
5 files changed, 7 insertions(+), 17 deletions(-)
diff --git a/src/SharpCompress/Common/Tar/TarFilePart.cs b/src/SharpCompress/Common/Tar/TarFilePart.cs
index 75b0be733..8aa166343 100644
--- a/src/SharpCompress/Common/Tar/TarFilePart.cs
+++ b/src/SharpCompress/Common/Tar/TarFilePart.cs
@@ -75,9 +75,7 @@ _seekableStream is SourceStream sourceStream
if (_seekableStream != null)
{
_seekableStream.Position = Header.DataStartPosition ?? 0;
- return new ValueTask(
- new TarReadOnlySubStream(_seekableStream, Header.Size)
- );
+ return new ValueTask(new TarReadOnlySubStream(_seekableStream, Header.Size));
}
return new ValueTask(Header.PackedStream.NotNull());
}
diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs
index 460be1cc0..c011abd25 100644
--- a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs
+++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs
@@ -44,10 +44,7 @@ IArchiveEncoding archiveEncoding
break;
case StreamingMode.Streaming:
{
- header.PackedStream = new TarReadOnlySubStream(
- stream,
- header.Size
- );
+ header.PackedStream = new TarReadOnlySubStream(stream, header.Size);
}
break;
default:
diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs
index 6053fa106..c95efaef5 100644
--- a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs
+++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs
@@ -38,10 +38,7 @@ IArchiveEncoding archiveEncoding
break;
case StreamingMode.Streaming:
{
- header.PackedStream = new TarReadOnlySubStream(
- stream,
- header.Size
- );
+ header.PackedStream = new TarReadOnlySubStream(stream, header.Size);
}
break;
default:
diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs
index 3b082a712..2c17e5134 100644
--- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs
+++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs
@@ -10,6 +10,7 @@ namespace SharpCompress.Common.Zip;
internal partial class SeekableZipFilePart : IDisposable
{
private readonly SemaphoreSlim _asyncHeaderSemaphore = new(1, 1);
+
internal override async ValueTask GetCompressedStreamAsync(
CancellationToken cancellationToken = default
)
@@ -75,10 +76,9 @@ BaseStream is SourceStream sourceStream
try
{
- Header = await _headerFactory.GetLocalHeaderAsync(
- streamToUse,
- (DirectoryEntryHeader)Header
- ).ConfigureAwait(false);
+ Header = await _headerFactory
+ .GetLocalHeaderAsync(streamToUse, (DirectoryEntryHeader)Header)
+ .ConfigureAwait(false);
}
finally
{
@@ -99,5 +99,4 @@ BaseStream is SourceStream sourceStream
}
public void Dispose() => _asyncHeaderSemaphore.Dispose();
-
}
diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
index 7e797facc..d783d2f22 100644
--- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
+++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
@@ -88,7 +88,6 @@ BaseStream is SourceStream sourceStream
}
}
-
protected override Stream CreateBaseStream()
{
// If BaseStream is a SourceStream in file mode with multi-threading enabled,