mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-10 13:39:12 +00:00
Compare commits
8 Commits
adam/more-
...
adam/multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e301becb4 | ||
|
|
2e7d4eb74b | ||
|
|
5b2030bb98 | ||
|
|
c1169539ea | ||
|
|
8d2463f575 | ||
|
|
af7e270b2d | ||
|
|
1984da6997 | ||
|
|
4536fddec2 |
@@ -1,222 +0,0 @@
|
||||
# Async Method Analysis for SharpCompress Stream Implementations
|
||||
|
||||
This document analyzes all Stream implementations in SharpCompress to identify which ones need async method implementations and which tests are missing async versions.
|
||||
|
||||
## Stream Implementations Analysis
|
||||
|
||||
### Criteria
|
||||
- **Needs Async Read**: Has `Read()` implementation but no proper `ReadAsync()` override (or `ReadAsync` just wraps sync)
|
||||
- **Needs Async Write**: Has `Write()` implementation but no proper `WriteAsync()` override (or `WriteAsync` just wraps sync)
|
||||
|
||||
### ✅ Streams with Proper Async Implementations
|
||||
|
||||
| Stream | Location | Read | Write | Notes |
|
||||
|--------|----------|------|-------|-------|
|
||||
| TarReadOnlySubStream | Common/Tar/TarReadOnlySubStream.cs | ✅ | N/A | Has proper ReadAsync with async stream delegation |
|
||||
| EntryStream | Common/EntryStream.cs | ✅ | N/A | Has proper ReadAsync |
|
||||
| BZip2Stream | Compressors/BZip2/BZip2Stream.cs | ✅ | ✅ | Delegates to inner stream async methods |
|
||||
| XZStream | Compressors/Xz/XZStream.cs | ✅ | N/A | Has proper async implementation |
|
||||
| LZipStream | Compressors/LZMA/LZipStream.cs | ✅ | ✅ | Delegates to inner stream async methods |
|
||||
| LzmaStream | Compressors/LZMA/LzmaStream.cs | ✅ | ⚠️ | Read has proper async; Write wraps sync |
|
||||
| AesDecoderStream | Compressors/LZMA/AesDecoderStream.cs | ✅ | N/A | Has proper async implementation |
|
||||
| CrcBuilderStream | Compressors/LZMA/Utilites/CrcBuilderStream.cs | N/A | ✅ | Has proper async write |
|
||||
| CrcCheckStream | Compressors/LZMA/Utilites/CrcCheckStream.cs | N/A | ⚠️ | WriteAsync wraps sync |
|
||||
| DeflateStream | Compressors/Deflate/DeflateStream.cs | ✅ | ✅ | Delegates to base stream async |
|
||||
| ZlibBaseStream | Compressors/Deflate/ZlibBaseStream.cs | ✅ | ✅ | Has proper async implementations |
|
||||
| RarStream | Compressors/Rar/RarStream.cs | ✅ | ⚠️ | Read has async; Write wraps sync |
|
||||
| RarCrcStream | Compressors/Rar/RarCrcStream.cs | ✅ | N/A | Extends RarStream with async |
|
||||
| MultiVolumeReadOnlyStream | Compressors/Rar/MultiVolumeReadOnlyStream.cs | ✅ | N/A | Has proper async read |
|
||||
| RarBLAKE2spStream | Compressors/Rar/RarBLAKE2spStream.cs | ✅ | N/A | Has proper async read |
|
||||
| Deflate64Stream | Compressors/Deflate64/Deflate64Stream.cs | ✅ | N/A | Has proper async read |
|
||||
| ADCStream | Compressors/ADC/ADCStream.cs | ✅ | N/A | Has proper async read |
|
||||
| ReadOnlySubStream | IO/ReadOnlySubStream.cs | ✅ | N/A | Has proper async read |
|
||||
| SharpCompressStream | IO/SharpCompressStream.cs | ✅ | ✅ | Has proper async implementations |
|
||||
|
||||
---
|
||||
|
||||
## ❌ Streams Needing Async Implementation
|
||||
|
||||
### High Priority - Has Read but no proper ReadAsync
|
||||
|
||||
| Stream | Location | Issue | Complexity |
|
||||
|--------|----------|-------|------------|
|
||||
| **Crc32Stream** | Crypto/Crc32Stream.cs | Has `Write()` but no `WriteAsync` | Low - Just add CRC calculation around async write |
|
||||
| **PkwareTraditionalCryptoStream** | Common/Zip/PkwareTraditionalCryptoStream.cs | Has `Read()` and `Write()` but no async versions | Medium - Crypto operations are synchronous, need to buffer async reads |
|
||||
| **WinzipAesCryptoStream** | Common/Zip/WinzipAesCryptoStream.cs | Has `Read()` but no `ReadAsync` | Medium - Crypto operations are synchronous, need async stream reads |
|
||||
| **CBZip2InputStream** | Compressors/BZip2/CBZip2InputStream.cs | `ReadAsync` wraps sync `ReadByte` loop | High - Core decompression is CPU-bound, but stream reads could be async |
|
||||
| **CBZip2OutputStream** | Compressors/BZip2/CBZip2OutputStream.cs | `WriteAsync` wraps sync `WriteByte` loop | High - Core compression is CPU-bound |
|
||||
| **Bcj2DecoderStream** | Compressors/LZMA/Bcj2DecoderStream.cs | `ReadAsync` wraps sync - uses iterator/state machine | High - Complex state machine with multiple streams |
|
||||
| **PpmdStream** | Compressors/PPMd/PpmdStream.cs | No async at all | Medium - Depends on model decoding |
|
||||
| **ShrinkStream** | Compressors/Shrink/ShrinkStream.cs | No async at all | Low - Single-shot decompression |
|
||||
| **LzwStream** | Compressors/Lzw/LzwStream.cs | No async at all | Medium - Bit-level operations, but input reads could be async |
|
||||
| **ExplodeStream** | Compressors/Explode/ExplodeStream.cs | No async at all | Medium - Similar to LzwStream |
|
||||
| **ArcLzwStream** | Compressors/ArcLzw/ArcLzwStream.cs | No async at all | Low - Single-shot decompression |
|
||||
| **BufferedSubStream** | IO/BufferedSubStream.cs | No async - uses sync cache refill | Medium - Cache operations could be async |
|
||||
|
||||
### Streams Where Async May Not Be Beneficial
|
||||
|
||||
| Stream | Location | Reason |
|
||||
|--------|----------|--------|
|
||||
| ZStandardStream | Compressors/ZStandard/ZStandardStream.cs | Extends ZstdSharp.DecompressionStream - async behavior depends on base class |
|
||||
| ReadOnlyStream | Compressors/Xz/ReadOnlyStream.cs | Abstract base - no Read implementation |
|
||||
| XZReadOnlyStream | Compressors/Xz/XZReadOnlyStream.cs | Abstract base - no Read implementation |
|
||||
| DecoderStream2 | Compressors/LZMA/DecoderStream.cs | Abstract base - no Read implementation |
|
||||
|
||||
---
|
||||
|
||||
## Detailed Analysis of Streams Needing Work
|
||||
|
||||
### 1. Crc32Stream (Low Priority)
|
||||
- **Location**: `src/SharpCompress/Crypto/Crc32Stream.cs`
|
||||
- **Issue**: Has `Write()` (lines 83-87) but no `WriteAsync`
|
||||
- **Solution**: Add `WriteAsync` that calls base stream's `WriteAsync` and then calculates CRC synchronously (CRC is fast)
|
||||
|
||||
### 2. PkwareTraditionalCryptoStream (Medium Priority)
|
||||
- **Location**: `src/SharpCompress/Common/Zip/PkwareTraditionalCryptoStream.cs`
|
||||
- **Issue**: Has `Read()` (lines 69-86) and `Write()` (lines 88-113) but no async versions
|
||||
- **Solution**:
|
||||
- ReadAsync: Read from underlying stream asynchronously, then decrypt synchronously
|
||||
- WriteAsync: Encrypt synchronously, then write asynchronously
|
||||
|
||||
### 3. WinzipAesCryptoStream (Medium Priority)
|
||||
- **Location**: `src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs`
|
||||
- **Issue**: Has `Read()` (lines 106-123) but no `ReadAsync`
|
||||
- **Solution**: Read from stream asynchronously, transform blocks synchronously (crypto is CPU-bound)
|
||||
|
||||
### 4. CBZip2InputStream (High Complexity)
|
||||
- **Location**: `src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs`
|
||||
- **Issue**: `ReadAsync` (lines 1132-1152) just wraps sync `ReadByte` in a loop
|
||||
- **Solution**: Would need significant refactoring to make the bit-reading operations async
|
||||
|
||||
### 5. CBZip2OutputStream (High Complexity)
|
||||
- **Location**: `src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs`
|
||||
- **Issue**: `WriteAsync` (lines 2033-2046) wraps sync `WriteByte` loop
|
||||
- **Solution**: Would need significant refactoring of compression pipeline
|
||||
|
||||
### 6. Bcj2DecoderStream (High Complexity)
|
||||
- **Location**: `src/SharpCompress/Compressors/LZMA/Bcj2DecoderStream.cs`
|
||||
- **Issue**: Uses iterator pattern (`Run()`) - `ReadAsync` (lines 196-206) just wraps sync
|
||||
- **Solution**: Would require rewriting the iterator pattern to support async
|
||||
|
||||
### 7. PpmdStream (Medium Priority)
|
||||
- **Location**: `src/SharpCompress/Compressors/PPMd/PpmdStream.cs`
|
||||
- **Issue**: No async methods at all
|
||||
- **Solution**: Add ReadAsync that reads from model decoder asynchronously
|
||||
|
||||
### 8. ShrinkStream (Low Priority)
|
||||
- **Location**: `src/SharpCompress/Compressors/Shrink/ShrinkStream.cs`
|
||||
- **Issue**: No async - does single-shot decompression in Read
|
||||
- **Solution**: Could add async wrapper, but single-shot nature limits benefit
|
||||
|
||||
### 9. LzwStream (Medium Priority)
|
||||
- **Location**: `src/SharpCompress/Compressors/Lzw/LzwStream.cs`
|
||||
- **Issue**: No async methods
|
||||
- **Solution**: Bit-level operations are sync, but underlying stream reads could be buffered async
|
||||
|
||||
### 10. ExplodeStream (Medium Priority)
|
||||
- **Location**: `src/SharpCompress/Compressors/Explode/ExplodeStream.cs`
|
||||
- **Issue**: No async methods
|
||||
- **Solution**: Similar to LzwStream - could async buffer reads from underlying stream
|
||||
|
||||
### 11. ArcLzwStream (Low Priority)
|
||||
- **Location**: `src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.cs`
|
||||
- **Issue**: No async - single-shot decompression
|
||||
- **Solution**: Limited benefit due to single-shot nature
|
||||
|
||||
### 12. BufferedSubStream (Medium Priority)
|
||||
- **Location**: `src/SharpCompress/IO/BufferedSubStream.cs`
|
||||
- **Issue**: `RefillCache()` uses sync read
|
||||
- **Solution**: Add async cache refill and ReadAsync
|
||||
|
||||
---
|
||||
|
||||
## Test Analysis - Missing Async Versions
|
||||
|
||||
### Tests WITH Async Versions (Good ✅)
|
||||
| Sync Test | Async Test |
|
||||
|-----------|------------|
|
||||
| BZip2ReaderTests | BZip2StreamAsyncTests |
|
||||
| TarArchiveTests | TarArchiveAsyncTests |
|
||||
| TarReaderTests | TarReaderAsyncTests |
|
||||
| TarWriterTests | TarWriterAsyncTests |
|
||||
| ZipReaderTests | ZipReaderAsyncTests |
|
||||
| ZipWriterTests | ZipWriterAsyncTests |
|
||||
| ZipArchiveTests | ZipArchiveAsyncTests |
|
||||
| Zip64Tests | Zip64AsyncTests |
|
||||
| ZipMemoryArchiveWithCrcTests | ZipMemoryArchiveWithCrcAsyncTests |
|
||||
| RarArchiveTests | RarArchiveAsyncTests |
|
||||
| RarReaderTests | RarReaderAsyncTests |
|
||||
| GZipArchiveTests | GZipArchiveAsyncTests |
|
||||
| GZipReaderTests | GZipReaderAsyncTests |
|
||||
| GZipWriterTests | GZipWriterAsyncTests |
|
||||
| XZStreamTests | XZStreamAsyncTests |
|
||||
| XZHeaderTests | XZHeaderAsyncTests |
|
||||
| XZIndexTests | XZIndexAsyncTests |
|
||||
| XZBlockTests | XZBlockAsyncTests |
|
||||
| SharpCompressStreamTest | SharpCompressStreamAsyncTests |
|
||||
| RewindableStreamTest | RewindableStreamAsyncTest |
|
||||
| LzmaStreamTests | LzmaStreamAsyncTests |
|
||||
| ZlibBaseStreamTests | ZLibBaseStreamAsyncTests |
|
||||
| ADCTest | AdcAsyncTest |
|
||||
|
||||
### Tests WITHOUT Async Versions (Need Work ❌)
|
||||
|
||||
| Test File | Location | Priority |
|
||||
|-----------|----------|----------|
|
||||
| **SevenZipArchiveTests** | SevenZip/SevenZipArchiveTests.cs | High - 7z is commonly used |
|
||||
| **ArcReaderTests** | Arc/ArcReaderTests.cs | Low - Less common format |
|
||||
| **ArjReaderTests** | Arj/ArjReaderTests.cs | Low - Legacy format |
|
||||
| **Crc32Tests** | Xz/Crc32Tests.cs | Low - Utility tests |
|
||||
| **Crc64Tests** | Xz/Crc64Tests.cs | Low - Utility tests |
|
||||
| **RarCRCTest** | Rar/RarCRCTest.cs | Low - Utility tests |
|
||||
| **RarHeaderFactoryTest** | Rar/RarHeaderFactoryTest.cs | Low - Unit tests |
|
||||
| **Lzma2Tests** | Xz/Filters/Lzma2Tests.cs | Medium - Filter tests |
|
||||
| **BCJTests** | Xz/Filters/BCJTests.cs | Medium - Filter tests |
|
||||
| **BranchExecTests** | Filters/BranchExecTests.cs | Low - Filter tests |
|
||||
| **ExceptionHierarchyTests** | ExceptionHierarchyTests.cs | N/A - No I/O |
|
||||
| **UtilityTests** | UtilityTests.cs | N/A - No I/O |
|
||||
| **Zip64VersionConsistencyTests** | Zip/Zip64VersionConsistencyTests.cs | Low - Unit tests |
|
||||
|
||||
### Directory-level Tests Missing Async
|
||||
|
||||
| Test File | Has Sync | Needs Async |
|
||||
|-----------|----------|-------------|
|
||||
| TarArchiveDirectoryTests | ✅ | ❌ |
|
||||
| TarWriterDirectoryTests | ✅ | ❌ |
|
||||
| ZipArchiveDirectoryTests | ✅ | ❌ |
|
||||
| ZipWriterDirectoryTests | ✅ | ❌ |
|
||||
| GZipArchiveDirectoryTests | ✅ | ❌ |
|
||||
| GZipWriterDirectoryTests | ✅ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
### Stream Implementation Work Required
|
||||
|
||||
| Priority | Count | Examples |
|
||||
|----------|-------|----------|
|
||||
| **High** | 3 | CBZip2InputStream, CBZip2OutputStream, Bcj2DecoderStream |
|
||||
| **Medium** | 6 | PkwareTraditionalCryptoStream, WinzipAesCryptoStream, PpmdStream, LzwStream, ExplodeStream, BufferedSubStream |
|
||||
| **Low** | 4 | Crc32Stream, ShrinkStream, ArcLzwStream |
|
||||
|
||||
### Test Work Required
|
||||
|
||||
| Priority | Count | Examples |
|
||||
|----------|-------|----------|
|
||||
| **High** | 1 | SevenZipArchiveTests |
|
||||
| **Medium** | 2 | Lzma2Tests, BCJTests |
|
||||
| **Low** | 5 | ArcReaderTests, ArjReaderTests, Crc32Tests, etc. |
|
||||
| **Directory Tests** | 6 | TarArchiveDirectoryTests, ZipArchiveDirectoryTests, etc. |
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Start with Medium-Priority Streams**: PkwareTraditionalCryptoStream and WinzipAesCryptoStream are commonly used in ZIP files and have straightforward async patterns (async read → sync crypto).
|
||||
|
||||
2. **Consider the ROI**: The BZip2 and Bcj2 streams are complex to make truly async because their core algorithms are CPU-bound. The benefit would primarily be in avoiding blocking on the underlying stream reads.
|
||||
|
||||
3. **Test Coverage**: Prioritize SevenZipArchiveAsyncTests as 7z is a commonly used format.
|
||||
|
||||
4. **Directory Tests**: Create async versions of directory tests to ensure async extraction to directories works correctly.
|
||||
@@ -25,7 +25,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{CDB425
|
||||
README.md = README.md
|
||||
FORMATS.md = FORMATS.md
|
||||
AGENTS.md = AGENTS.md
|
||||
ASYNC_ANALYSIS.md = ASYNC_ANALYSIS.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCompress.Performance", "tests\SharpCompress.Performance\SharpCompress.Performance.csproj", "{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}"
|
||||
|
||||
@@ -172,4 +172,9 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
return Entries.All(x => x.IsComplete);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool IsMultiVolume =>
|
||||
_sourceStream?.Files.Count > 1 || _sourceStream?.Streams.Count > 1;
|
||||
|
||||
public virtual bool SupportsMultiThreading => false;
|
||||
}
|
||||
|
||||
@@ -45,4 +45,14 @@ public interface IArchive : IDisposable
|
||||
/// The total size of the files as uncompressed in the archive.
|
||||
/// </summary>
|
||||
long TotalUncompressSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Is the archive part of a multi-volume set.
|
||||
/// </summary>
|
||||
bool IsMultiVolume { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Does the archive support multi-threaded extraction.
|
||||
/// </summary>
|
||||
bool SupportsMultiThreading { get; }
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public static class IArchiveEntryExtensions
|
||||
entry,
|
||||
destinationDirectory,
|
||||
options,
|
||||
(x, opt) => entry.WriteToFileAsync(x, opt, cancellationToken),
|
||||
entry.WriteToFileAsync,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
@@ -124,10 +124,10 @@ public static class IArchiveEntryExtensions
|
||||
entry,
|
||||
destinationFileName,
|
||||
options,
|
||||
async (x, fm) =>
|
||||
async (x, fm, ct) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
await entry.WriteToAsync(fs, cancellationToken).ConfigureAwait(false);
|
||||
await entry.WriteToAsync(fs, ct).ConfigureAwait(false);
|
||||
},
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Common.Rar;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives.Rar;
|
||||
|
||||
/// <summary>
|
||||
/// A rar part based on a FileInfo object
|
||||
/// </summary>
|
||||
internal class FileInfoRarArchiveVolume : RarVolume
|
||||
{
|
||||
internal FileInfoRarArchiveVolume(FileInfo fileInfo, ReaderOptions options, int index)
|
||||
: base(StreamingMode.Seekable, fileInfo.OpenRead(), FixOptions(options), index)
|
||||
{
|
||||
FileInfo = fileInfo;
|
||||
FileParts = GetVolumeFileParts().ToArray().ToReadOnly();
|
||||
}
|
||||
|
||||
private static ReaderOptions FixOptions(ReaderOptions options)
|
||||
{
|
||||
//make sure we're closing streams with fileinfo
|
||||
options.LeaveStreamOpen = false;
|
||||
return options;
|
||||
}
|
||||
|
||||
internal ReadOnlyCollection<RarFilePart> FileParts { get; }
|
||||
|
||||
internal FileInfo FileInfo { get; }
|
||||
|
||||
internal override RarFilePart CreateFilePart(MarkHeader markHeader, FileHeader fileHeader) =>
|
||||
new FileInfoRarFilePart(this, ReaderOptions.Password, markHeader, fileHeader, FileInfo);
|
||||
|
||||
internal override IEnumerable<RarFilePart> ReadFileParts() => FileParts;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.IO;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
|
||||
namespace SharpCompress.Archives.Rar;
|
||||
|
||||
internal sealed class FileInfoRarFilePart : SeekableFilePart
|
||||
{
|
||||
internal FileInfoRarFilePart(
|
||||
FileInfoRarArchiveVolume volume,
|
||||
string? password,
|
||||
MarkHeader mh,
|
||||
FileHeader fh,
|
||||
FileInfo fi
|
||||
)
|
||||
: base(mh, fh, volume.Index, volume.Stream, password) => FileInfo = fi;
|
||||
|
||||
internal FileInfo FileInfo { get; }
|
||||
|
||||
internal override string FilePartName =>
|
||||
"Rar File: " + FileInfo.FullName + " File Entry: " + FileHeader.FileName;
|
||||
}
|
||||
@@ -47,9 +47,9 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
{
|
||||
sourceStream.LoadAllParts(); //request all streams
|
||||
var streams = sourceStream.Streams.ToArray();
|
||||
var i = 0;
|
||||
if (streams.Length > 1 && IsRarFile(streams[1], ReaderOptions)) //test part 2 - true = multipart not split
|
||||
{
|
||||
var i = 0;
|
||||
sourceStream.IsVolumes = true;
|
||||
streams[1].Position = 0;
|
||||
sourceStream.Position = 0;
|
||||
@@ -57,12 +57,18 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
return sourceStream.Streams.Select(a => new StreamRarArchiveVolume(
|
||||
a,
|
||||
ReaderOptions,
|
||||
i++
|
||||
i++,
|
||||
IsMultiVolume
|
||||
));
|
||||
}
|
||||
|
||||
//split mode or single file
|
||||
return new StreamRarArchiveVolume(sourceStream, ReaderOptions, i++).AsEnumerable();
|
||||
return new StreamRarArchiveVolume(
|
||||
sourceStream,
|
||||
ReaderOptions,
|
||||
0,
|
||||
IsMultiVolume
|
||||
).AsEnumerable();
|
||||
}
|
||||
|
||||
protected override IReader CreateReaderForSolidExtraction()
|
||||
@@ -83,6 +89,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
}
|
||||
|
||||
public override bool IsSolid => Volumes.First().IsSolidArchive;
|
||||
public override bool SupportsMultiThreading => !IsMultiVolume && !IsSolid;
|
||||
|
||||
public virtual int MinVersion => Volumes.First().MinVersion;
|
||||
public virtual int MaxVersion => Volumes.First().MaxVersion;
|
||||
|
||||
@@ -134,4 +134,6 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool SupportsMultiThreading => Parts.Single().SupportsMultiThreading;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
using System.IO;
|
||||
using SharpCompress.Common.Rar;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Archives.Rar;
|
||||
|
||||
internal class SeekableFilePart : RarFilePart
|
||||
internal class SeekableRarFilePart : RarFilePart
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
private readonly string? _password;
|
||||
private readonly bool _isMultiVolume;
|
||||
|
||||
internal SeekableFilePart(
|
||||
internal SeekableRarFilePart(
|
||||
MarkHeader mh,
|
||||
FileHeader fh,
|
||||
int index,
|
||||
Stream stream,
|
||||
string? password
|
||||
string? password,
|
||||
bool isMultiVolume
|
||||
)
|
||||
: base(mh, fh, index)
|
||||
{
|
||||
_stream = stream;
|
||||
_password = password;
|
||||
_isMultiVolume = isMultiVolume;
|
||||
}
|
||||
|
||||
internal override Stream GetCompressedStream()
|
||||
@@ -42,4 +46,7 @@ internal class SeekableFilePart : RarFilePart
|
||||
}
|
||||
|
||||
internal override string FilePartName => "Unknown Stream - File Entry: " + FileHeader.FileName;
|
||||
|
||||
public override bool SupportsMultiThreading =>
|
||||
!_isMultiVolume && _stream is SourceStream ss && ss.IsFileMode && ss.Files.Count == 1;
|
||||
}
|
||||
@@ -9,11 +9,28 @@ namespace SharpCompress.Archives.Rar;
|
||||
|
||||
internal class StreamRarArchiveVolume : RarVolume
|
||||
{
|
||||
internal StreamRarArchiveVolume(Stream stream, ReaderOptions options, int index)
|
||||
: base(StreamingMode.Seekable, stream, options, index) { }
|
||||
private readonly bool _isMultiVolume;
|
||||
|
||||
internal StreamRarArchiveVolume(
|
||||
Stream stream,
|
||||
ReaderOptions options,
|
||||
int index,
|
||||
bool isMultiVolume
|
||||
)
|
||||
: base(StreamingMode.Seekable, stream, options, index)
|
||||
{
|
||||
_isMultiVolume = isMultiVolume;
|
||||
}
|
||||
|
||||
internal override IEnumerable<RarFilePart> ReadFileParts() => GetVolumeFileParts();
|
||||
|
||||
internal override RarFilePart CreateFilePart(MarkHeader markHeader, FileHeader fileHeader) =>
|
||||
new SeekableFilePart(markHeader, fileHeader, Index, Stream, ReaderOptions.Password);
|
||||
new SeekableRarFilePart(
|
||||
markHeader,
|
||||
fileHeader,
|
||||
Index,
|
||||
Stream,
|
||||
ReaderOptions.Password,
|
||||
_isMultiVolume
|
||||
);
|
||||
}
|
||||
|
||||
@@ -283,7 +283,12 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
|
||||
yield return new ZipArchiveEntry(
|
||||
this,
|
||||
new SeekableZipFilePart(headerFactory.NotNull(), deh, s)
|
||||
new SeekableZipFilePart(
|
||||
headerFactory.NotNull(),
|
||||
deh,
|
||||
s,
|
||||
IsMultiVolume
|
||||
)
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -385,4 +390,6 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
((IStreamStack)stream).StackSeek(0);
|
||||
return ZipReader.Open(stream, ReaderOptions, Entries);
|
||||
}
|
||||
|
||||
public override bool SupportsMultiThreading => !IsMultiVolume;
|
||||
}
|
||||
|
||||
@@ -23,5 +23,7 @@ public class ZipArchiveEntry : ZipEntry, IArchiveEntry
|
||||
|
||||
public bool IsComplete => true;
|
||||
|
||||
public override bool SupportsMultiThreading => Parts.Single().SupportsMultiThreading;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -87,4 +87,5 @@ public abstract class Entry : IEntry
|
||||
/// Entry file attribute.
|
||||
/// </summary>
|
||||
public virtual int? Attrib => throw new NotImplementedException();
|
||||
public virtual bool SupportsMultiThreading => false;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ internal static class ExtractionMethods
|
||||
IEntry entry,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
Func<string, ExtractionOptions?, Task> writeAsync,
|
||||
Func<string, ExtractionOptions?, CancellationToken, Task> writeAsync,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
@@ -189,7 +189,7 @@ internal static class ExtractionMethods
|
||||
"Entry is trying to write a file outside of the destination directory."
|
||||
);
|
||||
}
|
||||
await writeAsync(destinationFileName, options).ConfigureAwait(false);
|
||||
await writeAsync(destinationFileName, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (options.ExtractFullPath && !Directory.Exists(destinationFileName))
|
||||
{
|
||||
@@ -201,7 +201,7 @@ internal static class ExtractionMethods
|
||||
IEntry entry,
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options,
|
||||
Func<string, FileMode, Task> openAndWriteAsync,
|
||||
Func<string, FileMode, CancellationToken, Task> openAndWriteAsync,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
@@ -225,7 +225,8 @@ internal static class ExtractionMethods
|
||||
fm = FileMode.CreateNew;
|
||||
}
|
||||
|
||||
await openAndWriteAsync(destinationFileName, fm).ConfigureAwait(false);
|
||||
await openAndWriteAsync(destinationFileName, fm, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
entry.PreserveExtractionOptions(destinationFileName, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,6 @@ public abstract class FilePart
|
||||
internal abstract Stream? GetCompressedStream();
|
||||
internal abstract Stream? GetRawStream();
|
||||
internal bool Skipped { get; set; }
|
||||
|
||||
public virtual bool SupportsMultiThreading => false;
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ public interface IEntry
|
||||
DateTime? LastModifiedTime { get; }
|
||||
long Size { get; }
|
||||
int? Attrib { get; }
|
||||
bool SupportsMultiThreading { get; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.IO;
|
||||
using SharpCompress.Common.Zip.Headers;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Common.Zip;
|
||||
|
||||
@@ -7,13 +8,19 @@ internal class SeekableZipFilePart : ZipFilePart
|
||||
{
|
||||
private bool _isLocalHeaderLoaded;
|
||||
private readonly SeekableZipHeaderFactory _headerFactory;
|
||||
private readonly bool _isMultiVolume;
|
||||
|
||||
internal SeekableZipFilePart(
|
||||
SeekableZipHeaderFactory headerFactory,
|
||||
DirectoryEntryHeader header,
|
||||
Stream stream
|
||||
Stream stream,
|
||||
bool isMultiVolume
|
||||
)
|
||||
: base(header, stream) => _headerFactory = headerFactory;
|
||||
: base(header, stream)
|
||||
{
|
||||
_headerFactory = headerFactory;
|
||||
_isMultiVolume = isMultiVolume;
|
||||
}
|
||||
|
||||
internal override Stream GetCompressedStream()
|
||||
{
|
||||
@@ -30,8 +37,20 @@ internal class SeekableZipFilePart : ZipFilePart
|
||||
|
||||
protected override Stream CreateBaseStream()
|
||||
{
|
||||
if (!_isMultiVolume && BaseStream is SourceStream ss)
|
||||
{
|
||||
if (ss.IsFileMode && ss.Files.Count == 1)
|
||||
{
|
||||
var fileStream = ss.CurrentFile.OpenRead();
|
||||
fileStream.Position = Header.DataStartPosition.NotNull();
|
||||
return fileStream;
|
||||
}
|
||||
}
|
||||
BaseStream.Position = Header.DataStartPosition.NotNull();
|
||||
|
||||
return BaseStream;
|
||||
}
|
||||
|
||||
public override bool SupportsMultiThreading =>
|
||||
!_isMultiVolume && BaseStream is SourceStream ss && ss.IsFileMode && ss.Files.Count == 1;
|
||||
}
|
||||
|
||||
@@ -544,12 +544,6 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
|
||||
|
||||
private void EndBlock()
|
||||
{
|
||||
// Skip block processing for empty input (no data written)
|
||||
if (last < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
blockCRC = mCrc.GetFinalCRC();
|
||||
combinedCRC = (combinedCRC << 1) | (int)(((uint)combinedCRC) >> 31);
|
||||
combinedCRC ^= blockCRC;
|
||||
|
||||
@@ -62,10 +62,6 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
|
||||
base.Dispose(disposing);
|
||||
if (disposing)
|
||||
{
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(MultiVolumeReadOnlyStream));
|
||||
#endif
|
||||
|
||||
if (filePartEnumerator != null)
|
||||
{
|
||||
filePartEnumerator.Dispose();
|
||||
|
||||
@@ -82,9 +82,6 @@ internal class RarStream : Stream, IStreamStack
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(RarStream));
|
||||
#endif
|
||||
ArrayPool<byte>.Shared.Return(this.tmpBuffer);
|
||||
this.tmpBuffer = null;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public static class BinaryUtils
|
||||
)
|
||||
{
|
||||
var bytes = new byte[4];
|
||||
var read = await stream.ReadFullyAsync(bytes, 0, 4, cancellationToken).ConfigureAwait(false);
|
||||
var read = await stream.ReadFullyAsync(bytes, cancellationToken).ConfigureAwait(false);
|
||||
if (!read)
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
|
||||
@@ -15,7 +15,7 @@ public class SourceStream : Stream, IStreamStack
|
||||
#endif
|
||||
int IStreamStack.DefaultBufferSize { get; set; }
|
||||
|
||||
Stream IStreamStack.BaseStream() => _streams[_stream];
|
||||
Stream IStreamStack.BaseStream() => _streams[_streamIndex];
|
||||
|
||||
int IStreamStack.BufferSize
|
||||
{
|
||||
@@ -35,7 +35,7 @@ public class SourceStream : Stream, IStreamStack
|
||||
private readonly List<Stream> _streams;
|
||||
private readonly Func<int, FileInfo?>? _getFilePart;
|
||||
private readonly Func<int, Stream?>? _getStreamPart;
|
||||
private int _stream;
|
||||
private int _streamIndex;
|
||||
|
||||
public SourceStream(FileInfo file, Func<int, FileInfo?> getPart, ReaderOptions options)
|
||||
: this(null, null, file, getPart, options) { }
|
||||
@@ -59,7 +59,7 @@ public class SourceStream : Stream, IStreamStack
|
||||
|
||||
if (!IsFileMode)
|
||||
{
|
||||
_streams.Add(stream!);
|
||||
_streams.Add(stream.NotNull("stream is null"));
|
||||
_getStreamPart = getStreamPart;
|
||||
_getFilePart = _ => null;
|
||||
if (stream is FileStream fileStream)
|
||||
@@ -69,12 +69,12 @@ public class SourceStream : Stream, IStreamStack
|
||||
}
|
||||
else
|
||||
{
|
||||
_files.Add(file!);
|
||||
_files.Add(file.NotNull("file is null"));
|
||||
_streams.Add(_files[0].OpenRead());
|
||||
_getFilePart = getFilePart;
|
||||
_getStreamPart = _ => null;
|
||||
}
|
||||
_stream = 0;
|
||||
_streamIndex = 0;
|
||||
_prevSize = 0;
|
||||
|
||||
#if DEBUG_STREAMS
|
||||
@@ -93,10 +93,12 @@ public class SourceStream : Stream, IStreamStack
|
||||
public ReaderOptions ReaderOptions { get; }
|
||||
public bool IsFileMode { get; }
|
||||
|
||||
public IEnumerable<FileInfo> Files => _files;
|
||||
public IEnumerable<Stream> Streams => _streams;
|
||||
public IReadOnlyList<FileInfo> Files => _files;
|
||||
public IReadOnlyList<Stream> Streams => _streams;
|
||||
|
||||
private Stream Current => _streams[_stream];
|
||||
private Stream Current => _streams[_streamIndex];
|
||||
|
||||
public FileInfo CurrentFile => _files[_streamIndex];
|
||||
|
||||
public bool LoadStream(int index) //ensure all parts to id are loaded
|
||||
{
|
||||
@@ -107,7 +109,7 @@ public class SourceStream : Stream, IStreamStack
|
||||
var f = _getFilePart.NotNull("GetFilePart is null")(_streams.Count);
|
||||
if (f == null)
|
||||
{
|
||||
_stream = _streams.Count - 1;
|
||||
_streamIndex = _streams.Count - 1;
|
||||
return false;
|
||||
}
|
||||
//throw new Exception($"File part {idx} not available.");
|
||||
@@ -119,7 +121,7 @@ public class SourceStream : Stream, IStreamStack
|
||||
var s = _getStreamPart.NotNull("GetStreamPart is null")(_streams.Count);
|
||||
if (s == null)
|
||||
{
|
||||
_stream = _streams.Count - 1;
|
||||
_streamIndex = _streams.Count - 1;
|
||||
return false;
|
||||
}
|
||||
//throw new Exception($"Stream part {idx} not available.");
|
||||
@@ -137,10 +139,10 @@ public class SourceStream : Stream, IStreamStack
|
||||
{
|
||||
if (LoadStream(idx))
|
||||
{
|
||||
_stream = idx;
|
||||
_streamIndex = idx;
|
||||
}
|
||||
|
||||
return _stream == idx;
|
||||
return _streamIndex == idx;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
@@ -184,7 +186,7 @@ public class SourceStream : Stream, IStreamStack
|
||||
var length = Current.Length;
|
||||
|
||||
// Load next file if present
|
||||
if (!SetStream(_stream + 1))
|
||||
if (!SetStream(_streamIndex + 1))
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -223,7 +225,7 @@ public class SourceStream : Stream, IStreamStack
|
||||
while (_prevSize + Current.Length < pos)
|
||||
{
|
||||
_prevSize += Current.Length;
|
||||
SetStream(_stream + 1);
|
||||
SetStream(_streamIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +275,7 @@ public class SourceStream : Stream, IStreamStack
|
||||
var length = Current.Length;
|
||||
|
||||
// Load next file if present
|
||||
if (!SetStream(_stream + 1))
|
||||
if (!SetStream(_streamIndex + 1))
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -322,7 +324,7 @@ public class SourceStream : Stream, IStreamStack
|
||||
var length = Current.Length;
|
||||
|
||||
// Load next file if present
|
||||
if (!SetStream(_stream + 1))
|
||||
if (!SetStream(_streamIndex + 1))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public static class IReaderExtensions
|
||||
reader.Entry,
|
||||
destinationDirectory,
|
||||
options,
|
||||
(fileName, opts) => reader.WriteEntryToFileAsync(fileName, opts, cancellationToken),
|
||||
reader.WriteEntryToFileAsync,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
@@ -101,10 +101,10 @@ public static class IReaderExtensions
|
||||
reader.Entry,
|
||||
destinationFileName,
|
||||
options,
|
||||
async (x, fm) =>
|
||||
async (x, fm, ct) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
await reader.WriteEntryToAsync(fs, cancellationToken).ConfigureAwait(false);
|
||||
await reader.WriteEntryToAsync(fs, ct).ConfigureAwait(false);
|
||||
},
|
||||
cancellationToken
|
||||
)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>SharpCompress - Pure C# Decompression/Compression</AssemblyTitle>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<VersionPrefix>0.42.0</VersionPrefix>
|
||||
<AssemblyVersion>0.42.0</AssemblyVersion>
|
||||
<FileVersion>0.42.0</FileVersion>
|
||||
<VersionPrefix>0.41.0</VersionPrefix>
|
||||
<AssemblyVersion>0.41.0</AssemblyVersion>
|
||||
<FileVersion>0.41.0</FileVersion>
|
||||
<Authors>Adam Hathcock</Authors>
|
||||
<TargetFrameworks>net48;net481;netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||
<AssemblyName>SharpCompress</AssemblyName>
|
||||
|
||||
@@ -450,30 +450,26 @@ internal static class Utility
|
||||
public static async Task<bool> ReadFullyAsync(
|
||||
this Stream stream,
|
||||
byte[] buffer,
|
||||
int? offset = null,
|
||||
int? total = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
offset ??= 0;
|
||||
total ??= buffer.Length;
|
||||
var count = 0;
|
||||
var total = 0;
|
||||
int read;
|
||||
while (
|
||||
(
|
||||
read = await stream
|
||||
.ReadAsync(buffer, offset.Value + count, total.Value - count, cancellationToken)
|
||||
.ReadAsync(buffer, total, buffer.Length - total, cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
) > 0
|
||||
)
|
||||
{
|
||||
count += read;
|
||||
if (count >= total)
|
||||
total += read;
|
||||
if (total >= buffer.Length)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return (count >= total);
|
||||
return (total >= buffer.Length);
|
||||
}
|
||||
|
||||
public static string TrimNulls(this string source) => source.Replace('\0', ' ').Trim();
|
||||
|
||||
@@ -134,6 +134,7 @@ public class ArchiveTests : ReaderTests
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
Assert.False(entry.SupportsMultiThreading);
|
||||
entry.WriteToDirectory(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
@@ -266,6 +267,31 @@ public class ArchiveTests : ReaderTests
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
protected async Task ArchiveFileRead_Multithreaded(
|
||||
IArchiveFactory archiveFactory,
|
||||
string testArchive,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
|
||||
var tasks = new List<Task>();
|
||||
using (var archive = archiveFactory.Open(new FileInfo(testArchive), readerOptions))
|
||||
{
|
||||
Assert.True(archive.SupportsMultiThreading);
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
Assert.True(entry.SupportsMultiThreading);
|
||||
var t = entry.WriteToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
tasks.Add(t);
|
||||
}
|
||||
}
|
||||
await Task.WhenAll(tasks);
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
protected void ArchiveFileRead(
|
||||
IArchiveFactory archiveFactory,
|
||||
string testArchive,
|
||||
@@ -289,6 +315,11 @@ public class ArchiveTests : ReaderTests
|
||||
protected void ArchiveFileRead(string testArchive, ReaderOptions? readerOptions = null) =>
|
||||
ArchiveFileRead(ArchiveFactory.AutoFactory, testArchive, readerOptions);
|
||||
|
||||
protected Task ArchiveFileRead_Multithreaded(
|
||||
string testArchive,
|
||||
ReaderOptions? readerOptions = null
|
||||
) => ArchiveFileRead_Multithreaded(ArchiveFactory.AutoFactory, testArchive, readerOptions);
|
||||
|
||||
protected void ArchiveFileSkip(
|
||||
string testArchive,
|
||||
string fileOrder,
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// A stream wrapper that only allows async read operations.
|
||||
/// Throws InvalidOperationException on synchronous Read calls to ensure
|
||||
/// async code paths are being used.
|
||||
/// </summary>
|
||||
public class AsyncOnlyStream : Stream
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
private bool _isDisposed;
|
||||
|
||||
public AsyncOnlyStream(Stream stream, bool throwOnSyncMethods = true)
|
||||
{
|
||||
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
ThrowOnSyncMethods = throwOnSyncMethods;
|
||||
}
|
||||
|
||||
public bool ThrowOnSyncMethods { get; set; }
|
||||
|
||||
public override bool CanRead => _stream.CanRead;
|
||||
|
||||
public override bool CanSeek => _stream.CanSeek;
|
||||
|
||||
public override bool CanWrite => _stream.CanWrite;
|
||||
|
||||
public override long Length => _stream.Length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _stream.Position;
|
||||
set => _stream.Position = value;
|
||||
}
|
||||
|
||||
public override void Flush() => _stream.Flush();
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken) =>
|
||||
_stream.FlushAsync(cancellationToken);
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (ThrowOnSyncMethods)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Synchronous Read is not allowed on AsyncOnlyStream. Use ReadAsync instead."
|
||||
);
|
||||
}
|
||||
return _stream.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
if (ThrowOnSyncMethods)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Synchronous ReadByte is not allowed on AsyncOnlyStream. Use ReadAsync instead."
|
||||
);
|
||||
}
|
||||
return _stream.ReadByte();
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
) => _stream.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
) => _stream.ReadAsync(buffer, cancellationToken);
|
||||
|
||||
public override int Read(Span<byte> buffer)
|
||||
{
|
||||
if (ThrowOnSyncMethods)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Synchronous Read is not allowed on AsyncOnlyStream. Use ReadAsync instead."
|
||||
);
|
||||
}
|
||||
return _stream.Read(buffer);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin);
|
||||
|
||||
public override void SetLength(long value) => _stream.SetLength(value);
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (ThrowOnSyncMethods)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Synchronous Write is not allowed on AsyncOnlyStream. Use WriteAsync instead."
|
||||
);
|
||||
}
|
||||
_stream.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override void WriteByte(byte value)
|
||||
{
|
||||
if (ThrowOnSyncMethods)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Synchronous WriteByte is not allowed on AsyncOnlyStream. Use WriteAsync instead."
|
||||
);
|
||||
}
|
||||
_stream.WriteByte(value);
|
||||
}
|
||||
|
||||
public override Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
) => _stream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
) => _stream.WriteAsync(buffer, cancellationToken);
|
||||
|
||||
public override void Write(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (ThrowOnSyncMethods)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Synchronous Write is not allowed on AsyncOnlyStream. Use WriteAsync instead."
|
||||
);
|
||||
}
|
||||
_stream.Write(buffer);
|
||||
}
|
||||
#endif
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_isDisposed = true;
|
||||
if (disposing)
|
||||
{
|
||||
_stream.Dispose();
|
||||
}
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_isDisposed = true;
|
||||
await _stream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Rar;
|
||||
using SharpCompress.Common;
|
||||
@@ -292,9 +293,15 @@ public class RarArchiveTests : ArchiveTests
|
||||
[Fact]
|
||||
public void Rar_ArchiveFileRead() => ArchiveFileRead("Rar.rar");
|
||||
|
||||
[Fact]
|
||||
public Task Rar_ArchiveFileRead_Multithreaded() => ArchiveFileRead_Multithreaded("Rar.rar");
|
||||
|
||||
[Fact]
|
||||
public void Rar5_ArchiveFileRead() => ArchiveFileRead("Rar5.rar");
|
||||
|
||||
[Fact]
|
||||
public Task Rar5_ArchiveFileRead_Multithreaded() => ArchiveFileRead_Multithreaded("Rar5.rar");
|
||||
|
||||
[Fact]
|
||||
public void Rar_ArchiveFileRead_HasDirectories() =>
|
||||
DoRar_ArchiveFileRead_HasDirectories("Rar.rar");
|
||||
@@ -359,6 +366,9 @@ public class RarArchiveTests : ArchiveTests
|
||||
[Fact]
|
||||
public void Rar2_ArchiveFileRead() => ArchiveFileRead("Rar2.rar");
|
||||
|
||||
[Fact]
|
||||
public Task Rar2_ArchiveFileRead_Multithreaded() => ArchiveFileRead_Multithreaded("Rar2.rar");
|
||||
|
||||
[Fact]
|
||||
public void Rar15_ArchiveFileRead()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
@@ -88,6 +89,10 @@ public class ZipArchiveTests : ArchiveTests
|
||||
[Fact]
|
||||
public void Zip_Deflate_ArchiveFileRead() => ArchiveFileRead("Zip.deflate.zip");
|
||||
|
||||
[Fact]
|
||||
public Task Zip_Deflate_ArchiveFileRead_Multithreaded() =>
|
||||
ArchiveFileRead_Multithreaded("Zip.deflate.zip");
|
||||
|
||||
[Fact]
|
||||
public void Zip_Deflate_ArchiveExtractToDirectory() =>
|
||||
ArchiveExtractToDirectory("Zip.deflate.zip");
|
||||
|
||||
@@ -13,21 +13,12 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
{
|
||||
public ZipReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
|
||||
|
||||
/// <summary>
|
||||
/// Opens a file with an AsyncOnlyStream wrapper.
|
||||
/// ThrowOnSyncMethods starts as false to allow synchronous reads during Open/format detection,
|
||||
/// then should be set to true after opening to ensure async code paths are used.
|
||||
/// </summary>
|
||||
private static AsyncOnlyStream OpenAsyncOnlyFile(string path) =>
|
||||
new(new ForwardOnlyStream(File.OpenRead(path)), throwOnSyncMethods: false);
|
||||
|
||||
[Fact]
|
||||
public async Task Issue_269_Double_Skip_Async()
|
||||
{
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "PrePostHeaders.zip");
|
||||
using var stream = OpenAsyncOnlyFile(path);
|
||||
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
stream.ThrowOnSyncMethods = true;
|
||||
var count = 0;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
@@ -69,11 +60,10 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_Streamed_Skip_Async()
|
||||
{
|
||||
using var stream = OpenAsyncOnlyFile(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")
|
||||
using Stream stream = new ForwardOnlyStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"))
|
||||
);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
stream.ThrowOnSyncMethods = true;
|
||||
var x = 0;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
@@ -126,20 +116,21 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
[Fact]
|
||||
public async Task Zip_BZip2_PkwareEncryption_Read_Async()
|
||||
{
|
||||
using var stream = OpenAsyncOnlyFile(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.pkware.zip")
|
||||
);
|
||||
using var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" });
|
||||
stream.ThrowOnSyncMethods = true;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
using (
|
||||
Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.pkware.zip"))
|
||||
)
|
||||
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
VerifyFiles();
|
||||
@@ -148,13 +139,11 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
[Fact]
|
||||
public async Task Zip_Reader_Disposal_Test_Async()
|
||||
{
|
||||
using var innerStream = OpenAsyncOnlyFile(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")
|
||||
using var stream = new TestStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"))
|
||||
);
|
||||
using var stream = new TestStream(innerStream);
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
innerStream.ThrowOnSyncMethods = true;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
@@ -172,12 +161,10 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
[Fact]
|
||||
public async Task Zip_Reader_Disposal_Test2_Async()
|
||||
{
|
||||
using var innerStream = OpenAsyncOnlyFile(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")
|
||||
using var stream = new TestStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"))
|
||||
);
|
||||
using var stream = new TestStream(innerStream);
|
||||
var reader = ReaderFactory.Open(stream);
|
||||
innerStream.ThrowOnSyncMethods = true;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
@@ -195,11 +182,38 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
public async Task Zip_LZMA_WinzipAES_Read_Async() =>
|
||||
await Assert.ThrowsAsync<NotSupportedException>(async () =>
|
||||
{
|
||||
using var stream = OpenAsyncOnlyFile(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.WinzipAES.zip")
|
||||
);
|
||||
using var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" });
|
||||
stream.ThrowOnSyncMethods = true;
|
||||
using (
|
||||
Stream stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.WinzipAES.zip")
|
||||
)
|
||||
)
|
||||
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
|
||||
{
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
VerifyFiles();
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_WinzipAES_Read_Async()
|
||||
{
|
||||
using (
|
||||
Stream stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip")
|
||||
)
|
||||
)
|
||||
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
|
||||
{
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
@@ -211,27 +225,6 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
);
|
||||
}
|
||||
}
|
||||
VerifyFiles();
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_WinzipAES_Read_Async()
|
||||
{
|
||||
using var stream = OpenAsyncOnlyFile(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip")
|
||||
);
|
||||
using var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" });
|
||||
stream.ThrowOnSyncMethods = true;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
VerifyFiles();
|
||||
}
|
||||
@@ -240,21 +233,20 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
public async Task Zip_Deflate_ZipCrypto_Read_Async()
|
||||
{
|
||||
var count = 0;
|
||||
using var stream = OpenAsyncOnlyFile(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip")
|
||||
);
|
||||
using var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" });
|
||||
stream.ThrowOnSyncMethods = true;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip")))
|
||||
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
Assert.Equal(CompressionType.None, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
count++;
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(CompressionType.None, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Assert.Equal(8, count);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Writers;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
@@ -11,42 +9,6 @@ public class ZipWriterTests : WriterTests
|
||||
public ZipWriterTests()
|
||||
: base(ArchiveType.Zip) { }
|
||||
|
||||
[Fact]
|
||||
public void Zip_BZip2_Write_EmptyFile()
|
||||
{
|
||||
// Test that writing an empty file with BZip2 compression doesn't throw DivideByZeroException
|
||||
using var memoryStream = new MemoryStream();
|
||||
var options = new WriterOptions(CompressionType.BZip2)
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding { Default = new UTF8Encoding(false) },
|
||||
};
|
||||
|
||||
using (var writer = WriterFactory.Open(memoryStream, ArchiveType.Zip, options))
|
||||
{
|
||||
writer.Write("test-folder/zero-byte-file.txt", Stream.Null);
|
||||
}
|
||||
|
||||
Assert.True(memoryStream.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_BZip2_Write_EmptyFolder()
|
||||
{
|
||||
// Test that writing an empty folder entry with BZip2 compression doesn't throw DivideByZeroException
|
||||
using var memoryStream = new MemoryStream();
|
||||
var options = new WriterOptions(CompressionType.BZip2)
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding { Default = new UTF8Encoding(false) },
|
||||
};
|
||||
|
||||
using (var writer = WriterFactory.Open(memoryStream, ArchiveType.Zip, options))
|
||||
{
|
||||
writer.Write("test-empty-folder/", Stream.Null);
|
||||
}
|
||||
|
||||
Assert.True(memoryStream.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_Deflate_Write() =>
|
||||
Write(
|
||||
|
||||
Reference in New Issue
Block a user