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,