mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-05 21:23:57 +00:00
Compare commits
105 Commits
0.43.0
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4377f4f006 | ||
|
|
f230f24109 | ||
|
|
258f9b9bd8 | ||
|
|
cc503a27fb | ||
|
|
22d15f73f0 | ||
|
|
4e0d78d6c8 | ||
|
|
63a1927838 | ||
|
|
3d745bfa05 | ||
|
|
ce26d50792 | ||
|
|
01e162fcc4 | ||
|
|
443f7b8b0c | ||
|
|
df63e152c1 | ||
|
|
ad7e64ba43 | ||
|
|
8737b7a38e | ||
|
|
13199fcfd1 | ||
|
|
9a7bdd39e8 | ||
|
|
484bc740d7 | ||
|
|
8a67d501a8 | ||
|
|
3c87242bd0 | ||
|
|
999124e68e | ||
|
|
db2f5c9cb9 | ||
|
|
af08a7cd54 | ||
|
|
72eaf66f05 | ||
|
|
8a3be35d67 | ||
|
|
d59e4c2a0d | ||
|
|
71655e04c4 | ||
|
|
a706a9d725 | ||
|
|
970934a40b | ||
|
|
a9c28a7b62 | ||
|
|
4d31436740 | ||
|
|
c82744c51c | ||
|
|
f0eaddc6a6 | ||
|
|
d6156f0f1e | ||
|
|
3c88c7fdd5 | ||
|
|
d11f6aefb0 | ||
|
|
010a38bb73 | ||
|
|
53f12d75db | ||
|
|
6c866324b2 | ||
|
|
a114155189 | ||
|
|
014bbc3ea4 | ||
|
|
d52facd4ab | ||
|
|
0a50386ada | ||
|
|
b9fc680548 | ||
|
|
7dcc13c1f0 | ||
|
|
56d3091688 | ||
|
|
a0af0604d1 | ||
|
|
875c2d7694 | ||
|
|
8c95f863cb | ||
|
|
ddf37e82c2 | ||
|
|
a82fda98d7 | ||
|
|
44e4b1804e | ||
|
|
4ca1a7713e | ||
|
|
9caf7be928 | ||
|
|
bf4217fde6 | ||
|
|
d5a8c37113 | ||
|
|
21ce9a38e6 | ||
|
|
7732fbb698 | ||
|
|
97879f18b6 | ||
|
|
d74454f7e9 | ||
|
|
5c947bccc7 | ||
|
|
fbdefc17c1 | ||
|
|
1425c6ff0d | ||
|
|
e038aea694 | ||
|
|
87ccbf329d | ||
|
|
9dcf384263 | ||
|
|
be045c4f15 | ||
|
|
fd968b3f78 | ||
|
|
833dd7b3a2 | ||
|
|
b9258ad496 | ||
|
|
0678318dde | ||
|
|
7116c0d098 | ||
|
|
2fde8436fb | ||
|
|
61ecd6475f | ||
|
|
64b209a772 | ||
|
|
48dbdbfed5 | ||
|
|
cf50311b9c | ||
|
|
e4d8582a2a | ||
|
|
b8e5ee45eb | ||
|
|
9f20a9e7d2 | ||
|
|
201521d814 | ||
|
|
18bb3cba11 | ||
|
|
af951d6f6a | ||
|
|
e5fe92bf90 | ||
|
|
b1aca7c305 | ||
|
|
c0a0cc4a44 | ||
|
|
7a49eb9e93 | ||
|
|
5aa0610882 | ||
|
|
41ed4c8186 | ||
|
|
90a33ce6b0 | ||
|
|
12574798e1 | ||
|
|
83b11254db | ||
|
|
b25493fd29 | ||
|
|
bb66100486 | ||
|
|
3ebf97dd49 | ||
|
|
bfcdeb3784 | ||
|
|
feece3d788 | ||
|
|
94adb77e9e | ||
|
|
909d36c237 | ||
|
|
e1c8aa226d | ||
|
|
2327679f23 | ||
|
|
574d9f970c | ||
|
|
235096a2eb | ||
|
|
a739fdc544 | ||
|
|
6196e26044 | ||
|
|
46a4064989 |
@@ -3,11 +3,11 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"commands": [
|
||||
"csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
.github/workflows/dotnetcore.yml
vendored
25
.github/workflows/dotnetcore.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: SharpCompress
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened, ready_for_review ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
- run: dotnet run --project build/build.csproj
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-sharpcompress.nupkg
|
||||
path: artifacts/*
|
||||
8
.github/workflows/nuget-release.yml
vendored
8
.github/workflows/nuget-release.yml
vendored
@@ -7,6 +7,10 @@ on:
|
||||
- 'release'
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -49,9 +53,9 @@ jobs:
|
||||
name: ${{ matrix.os }}-nuget-package
|
||||
path: artifacts/*.nupkg
|
||||
|
||||
# Push to NuGet.org using C# build target (Windows only)
|
||||
# Push to NuGet.org using C# build target (Windows only, not on PRs)
|
||||
- name: Push to NuGet
|
||||
if: success() && matrix.os == 'windows-latest'
|
||||
if: success() && matrix.os == 'windows-latest' && github.event_name != 'pull_request'
|
||||
run: dotnet run --project build/build.csproj -- push-to-nuget
|
||||
env:
|
||||
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
|
||||
|
||||
@@ -7,14 +7,13 @@
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageVersion Include="SimpleExec" Version="12.1.0" />
|
||||
<PackageVersion Include="SimpleExec" Version="13.0.0" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Buffers" Version="4.6.1" />
|
||||
<PackageVersion Include="System.Memory" Version="4.6.3" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
|
||||
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.102" />
|
||||
<GlobalPackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
|
||||
| Archive Format | Compression Format(s) | Compress/Decompress | Archive API | Reader API | Writer API |
|
||||
| ---------------------- | ------------------------------------------------- | ------------------- | --------------- | ---------- | ------------- |
|
||||
| Rar | Rar | Decompress (1) | RarArchive | RarReader | N/A |
|
||||
| Ace | None | Decompress | N/A | AceReader | N/A |
|
||||
| Arc | None, Packed, Squeezed, Crunched | Decompress | N/A | ArcReader | N/A |
|
||||
| Arj | None | Decompress | N/A | ArjReader | N/A |
|
||||
| Rar | Rar | Decompress | RarArchive | RarReader | N/A |
|
||||
| Zip (2) | None, Shrink, Reduce, Implode, DEFLATE, Deflate64, BZip2, LZMA/LZMA2, PPMd | Both | ZipArchive | ZipReader | ZipWriter |
|
||||
| Tar | None | Both | TarArchive | TarReader | TarWriter (3) |
|
||||
| Tar.GZip | DEFLATE | Both | TarArchive | TarReader | TarWriter (3) |
|
||||
|
||||
142
OLD_CHANGELOG.md
Normal file
142
OLD_CHANGELOG.md
Normal file
@@ -0,0 +1,142 @@
|
||||
|
||||
# Version Log
|
||||
|
||||
* [Releases](https://github.com/adamhathcock/sharpcompress/releases)
|
||||
|
||||
## Version 0.18
|
||||
|
||||
* [Now on Github releases](https://github.com/adamhathcock/sharpcompress/releases/tag/0.18)
|
||||
|
||||
## Version 0.17.1
|
||||
|
||||
* Fix - [Bug Fix for .NET Core on Windows](https://github.com/adamhathcock/sharpcompress/pull/257)
|
||||
|
||||
## Version 0.17.0
|
||||
|
||||
* New - Full LZip support! Can read and write LZip files and Tars inside LZip files. [Make LZip a first class citizen. #241](https://github.com/adamhathcock/sharpcompress/issues/241)
|
||||
* New - XZ read support! Can read XZ files and Tars inside XZ files. [XZ in SharpCompress #91](https://github.com/adamhathcock/sharpcompress/issues/94)
|
||||
* Fix - [Regression - zip file writing on seekable streams always assumed stream start was 0. Introduced with Zip64 writing.](https://github.com/adamhathcock/sharpcompress/issues/244)
|
||||
* Fix - [Zip files with post-data descriptors can be properly skipped via decompression](https://github.com/adamhathcock/sharpcompress/issues/162)
|
||||
|
||||
## Version 0.16.2
|
||||
|
||||
* Fix [.NET 3.5 should support files and cryptography (was a regression from 0.16.0)](https://github.com/adamhathcock/sharpcompress/pull/251)
|
||||
* Fix [Zip per entry compression customization wrote the wrong method into the zip archive](https://github.com/adamhathcock/sharpcompress/pull/249)
|
||||
|
||||
## Version 0.16.1
|
||||
|
||||
* Fix [Preserve compression method when getting a compressed stream](https://github.com/adamhathcock/sharpcompress/pull/235)
|
||||
* Fix [RAR entry key normalization fix](https://github.com/adamhathcock/sharpcompress/issues/201)
|
||||
|
||||
## Version 0.16.0
|
||||
|
||||
* Breaking - [Progress Event Tracking rethink](https://github.com/adamhathcock/sharpcompress/pull/226)
|
||||
* Update to VS2017 - [VS2017](https://github.com/adamhathcock/sharpcompress/pull/231) - Framework targets have been changed.
|
||||
* New - [Add Zip64 writing](https://github.com/adamhathcock/sharpcompress/pull/211)
|
||||
* [Fix invalid/mismatching Zip version flags.](https://github.com/adamhathcock/sharpcompress/issues/164) - This allows nuget/System.IO.Packaging to read zip files generated by SharpCompress
|
||||
* [Fix 7Zip directory hiding](https://github.com/adamhathcock/sharpcompress/pull/215/files)
|
||||
* [Verify RAR CRC headers](https://github.com/adamhathcock/sharpcompress/pull/220)
|
||||
|
||||
## Version 0.15.2
|
||||
|
||||
* [Fix invalid headers](https://github.com/adamhathcock/sharpcompress/pull/210) - fixes an issue creating large-ish zip archives that was introduced with zip64 reading.
|
||||
|
||||
## Version 0.15.1
|
||||
|
||||
* [Zip64 extending information and ZipReader](https://github.com/adamhathcock/sharpcompress/pull/206)
|
||||
|
||||
## Version 0.15.0
|
||||
|
||||
* [Add zip64 support for ZipArchive extraction](https://github.com/adamhathcock/sharpcompress/pull/205)
|
||||
|
||||
## Version 0.14.1
|
||||
|
||||
* [.NET Assemblies aren't strong named](https://github.com/adamhathcock/sharpcompress/issues/158)
|
||||
* [Pkware encryption for Zip files didn't allow for multiple reads of an entry](https://github.com/adamhathcock/sharpcompress/issues/197)
|
||||
* [GZip Entry couldn't be read multiple times](https://github.com/adamhathcock/sharpcompress/issues/198)
|
||||
|
||||
## Version 0.14.0
|
||||
|
||||
* [Support for LZip reading in for Tars](https://github.com/adamhathcock/sharpcompress/pull/191)
|
||||
|
||||
## Version 0.13.1
|
||||
|
||||
* [Fix null password on ReaderFactory. Fix null options on SevenZipArchive](https://github.com/adamhathcock/sharpcompress/pull/188)
|
||||
* [Make PpmdProperties lazy to avoid unnecessary allocations.](https://github.com/adamhathcock/sharpcompress/pull/185)
|
||||
|
||||
## Version 0.13.0
|
||||
|
||||
* Breaking change: Big refactor of Options on API.
|
||||
* 7Zip supports Deflate
|
||||
|
||||
## Version 0.12.4
|
||||
|
||||
* Forward only zip issue fix https://github.com/adamhathcock/sharpcompress/issues/160
|
||||
* Try to fix frameworks again by copying targets from JSON.NET
|
||||
|
||||
## Version 0.12.3
|
||||
|
||||
* 7Zip fixes https://github.com/adamhathcock/sharpcompress/issues/73
|
||||
* Maybe all profiles will work with project.json now
|
||||
|
||||
## Version 0.12.2
|
||||
|
||||
* Support Profile 259 again
|
||||
|
||||
## Version 0.12.1
|
||||
|
||||
* Support Silverlight 5
|
||||
|
||||
## Version 0.12.0
|
||||
|
||||
* .NET Core RTM!
|
||||
* Bug fix for Tar long paths
|
||||
|
||||
## Version 0.11.6
|
||||
|
||||
* Bug fix for global header in Tar
|
||||
* Writers now have a leaveOpen `bool` overload. They won't close streams if not-requested to.
|
||||
|
||||
## Version 0.11.5
|
||||
|
||||
* Bug fix in Skip method
|
||||
|
||||
## Version 0.11.4
|
||||
|
||||
* SharpCompress is now endian neutral (matters for Mono platforms)
|
||||
* Fix for Inflate (need to change implementation)
|
||||
* Fixes for RAR detection
|
||||
|
||||
## Version 0.11.1
|
||||
|
||||
* Added Cancel on IReader
|
||||
* Removed .NET 2.0 support and LinqBridge dependency
|
||||
|
||||
## Version 0.11
|
||||
|
||||
* Been over a year, contains mainly fixes from contributors!
|
||||
* Possible breaking change: ArchiveEncoding is UTF8 by default now.
|
||||
* TAR supports writing long names using longlink
|
||||
* RAR Protect Header added
|
||||
|
||||
## Version 0.10.3
|
||||
|
||||
* Finally fixed Disposal issue when creating a new archive with the Archive API
|
||||
|
||||
## Version 0.10.2
|
||||
|
||||
* Fixed Rar Header reading for invalid extended time headers.
|
||||
* Windows Store assembly is now strong named
|
||||
* Known issues with Long Tar names being worked on
|
||||
* Updated to VS2013
|
||||
* Portable targets SL5 and Windows Phone 8 (up from SL4 and WP7)
|
||||
|
||||
## Version 0.10.1
|
||||
|
||||
* Fixed 7Zip extraction performance problem
|
||||
|
||||
## Version 0.10:
|
||||
|
||||
* Added support for RAR Decryption (thanks to https://github.com/hrasyid)
|
||||
* Embedded some BouncyCastle crypto classes to allow RAR Decryption and Winzip AES Decryption in Portable and Windows Store DLLs
|
||||
* Built in Release (I think)
|
||||
228
README.md
228
README.md
@@ -4,7 +4,7 @@ SharpCompress is a compression library in pure C# for .NET Framework 4.8, .NET 8
|
||||
|
||||
The major feature is support for non-seekable streams so large files can be processed on the fly (i.e. download stream).
|
||||
|
||||
**NEW:** All I/O operations now support async/await for improved performance and scalability. See the [Async Usage](#async-usage) section below.
|
||||
**NEW:** All I/O operations now support async/await for improved performance and scalability. See the [USAGE.md](USAGE.md#async-examples) for examples.
|
||||
|
||||
GitHub Actions Build -
|
||||
[](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml)
|
||||
@@ -34,235 +34,11 @@ Hi everyone. I hope you're using SharpCompress and finding it useful. Please giv
|
||||
|
||||
Please do not email me directly to ask for help. If you think there is a real issue, please report it here.
|
||||
|
||||
## Async Usage
|
||||
|
||||
SharpCompress now provides full async/await support for all I/O operations, allowing for better performance and scalability in modern applications.
|
||||
|
||||
### Async Reading Examples
|
||||
|
||||
Extract entries asynchronously:
|
||||
```csharp
|
||||
using (Stream stream = File.OpenRead("archive.zip"))
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
// Async extraction
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
@"C:\temp",
|
||||
new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Extract all entries to directory asynchronously:
|
||||
```csharp
|
||||
using (Stream stream = File.OpenRead("archive.tar.gz"))
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
@"C:\temp",
|
||||
new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Open entry stream asynchronously:
|
||||
```csharp
|
||||
using (var archive = ZipArchive.Open("archive.zip"))
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
|
||||
{
|
||||
using (var entryStream = await entry.OpenEntryStreamAsync(cancellationToken))
|
||||
{
|
||||
// Process stream asynchronously
|
||||
await entryStream.CopyToAsync(outputStream, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Async Writing Examples
|
||||
|
||||
Write files asynchronously:
|
||||
```csharp
|
||||
using (Stream stream = File.OpenWrite("output.zip"))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
|
||||
{
|
||||
await writer.WriteAsync("file1.txt", fileStream, DateTime.Now, cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
Write all files from directory asynchronously:
|
||||
```csharp
|
||||
using (Stream stream = File.OpenWrite("output.tar.gz"))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)))
|
||||
{
|
||||
await writer.WriteAllAsync(@"D:\files", "*", SearchOption.AllDirectories, cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
All async methods support `CancellationToken` for graceful cancellation of long-running operations.
|
||||
|
||||
## Want to contribute?
|
||||
|
||||
I'm always looking for help or ideas. Please submit code or email with ideas. Unfortunately, just letting me know you'd like to help is not enough because I really have no overall plan of what needs to be done. I'll definitely accept code submissions and add you as a member of the project!
|
||||
|
||||
## TODOs (always lots)
|
||||
|
||||
* RAR 5 decryption crc check support
|
||||
* 7Zip writing
|
||||
* Zip64 (Need writing and extend Reading)
|
||||
* Multi-volume Zip support.
|
||||
* ZStandard writing
|
||||
|
||||
## Version Log
|
||||
|
||||
* [Releases](https://github.com/adamhathcock/sharpcompress/releases)
|
||||
|
||||
### Version 0.18
|
||||
|
||||
* [Now on Github releases](https://github.com/adamhathcock/sharpcompress/releases/tag/0.18)
|
||||
|
||||
### Version 0.17.1
|
||||
|
||||
* Fix - [Bug Fix for .NET Core on Windows](https://github.com/adamhathcock/sharpcompress/pull/257)
|
||||
|
||||
### Version 0.17.0
|
||||
|
||||
* New - Full LZip support! Can read and write LZip files and Tars inside LZip files. [Make LZip a first class citizen. #241](https://github.com/adamhathcock/sharpcompress/issues/241)
|
||||
* New - XZ read support! Can read XZ files and Tars inside XZ files. [XZ in SharpCompress #91](https://github.com/adamhathcock/sharpcompress/issues/94)
|
||||
* Fix - [Regression - zip file writing on seekable streams always assumed stream start was 0. Introduced with Zip64 writing.](https://github.com/adamhathcock/sharpcompress/issues/244)
|
||||
* Fix - [Zip files with post-data descriptors can be properly skipped via decompression](https://github.com/adamhathcock/sharpcompress/issues/162)
|
||||
|
||||
### Version 0.16.2
|
||||
|
||||
* Fix [.NET 3.5 should support files and cryptography (was a regression from 0.16.0)](https://github.com/adamhathcock/sharpcompress/pull/251)
|
||||
* Fix [Zip per entry compression customization wrote the wrong method into the zip archive](https://github.com/adamhathcock/sharpcompress/pull/249)
|
||||
|
||||
### Version 0.16.1
|
||||
|
||||
* Fix [Preserve compression method when getting a compressed stream](https://github.com/adamhathcock/sharpcompress/pull/235)
|
||||
* Fix [RAR entry key normalization fix](https://github.com/adamhathcock/sharpcompress/issues/201)
|
||||
|
||||
### Version 0.16.0
|
||||
|
||||
* Breaking - [Progress Event Tracking rethink](https://github.com/adamhathcock/sharpcompress/pull/226)
|
||||
* Update to VS2017 - [VS2017](https://github.com/adamhathcock/sharpcompress/pull/231) - Framework targets have been changed.
|
||||
* New - [Add Zip64 writing](https://github.com/adamhathcock/sharpcompress/pull/211)
|
||||
* [Fix invalid/mismatching Zip version flags.](https://github.com/adamhathcock/sharpcompress/issues/164) - This allows nuget/System.IO.Packaging to read zip files generated by SharpCompress
|
||||
* [Fix 7Zip directory hiding](https://github.com/adamhathcock/sharpcompress/pull/215/files)
|
||||
* [Verify RAR CRC headers](https://github.com/adamhathcock/sharpcompress/pull/220)
|
||||
|
||||
### Version 0.15.2
|
||||
|
||||
* [Fix invalid headers](https://github.com/adamhathcock/sharpcompress/pull/210) - fixes an issue creating large-ish zip archives that was introduced with zip64 reading.
|
||||
|
||||
### Version 0.15.1
|
||||
|
||||
* [Zip64 extending information and ZipReader](https://github.com/adamhathcock/sharpcompress/pull/206)
|
||||
|
||||
### Version 0.15.0
|
||||
|
||||
* [Add zip64 support for ZipArchive extraction](https://github.com/adamhathcock/sharpcompress/pull/205)
|
||||
|
||||
### Version 0.14.1
|
||||
|
||||
* [.NET Assemblies aren't strong named](https://github.com/adamhathcock/sharpcompress/issues/158)
|
||||
* [Pkware encryption for Zip files didn't allow for multiple reads of an entry](https://github.com/adamhathcock/sharpcompress/issues/197)
|
||||
* [GZip Entry couldn't be read multiple times](https://github.com/adamhathcock/sharpcompress/issues/198)
|
||||
|
||||
### Version 0.14.0
|
||||
|
||||
* [Support for LZip reading in for Tars](https://github.com/adamhathcock/sharpcompress/pull/191)
|
||||
|
||||
### Version 0.13.1
|
||||
|
||||
* [Fix null password on ReaderFactory. Fix null options on SevenZipArchive](https://github.com/adamhathcock/sharpcompress/pull/188)
|
||||
* [Make PpmdProperties lazy to avoid unnecessary allocations.](https://github.com/adamhathcock/sharpcompress/pull/185)
|
||||
|
||||
### Version 0.13.0
|
||||
|
||||
* Breaking change: Big refactor of Options on API.
|
||||
* 7Zip supports Deflate
|
||||
|
||||
### Version 0.12.4
|
||||
|
||||
* Forward only zip issue fix https://github.com/adamhathcock/sharpcompress/issues/160
|
||||
* Try to fix frameworks again by copying targets from JSON.NET
|
||||
|
||||
### Version 0.12.3
|
||||
|
||||
* 7Zip fixes https://github.com/adamhathcock/sharpcompress/issues/73
|
||||
* Maybe all profiles will work with project.json now
|
||||
|
||||
### Version 0.12.2
|
||||
|
||||
* Support Profile 259 again
|
||||
|
||||
### Version 0.12.1
|
||||
|
||||
* Support Silverlight 5
|
||||
|
||||
### Version 0.12.0
|
||||
|
||||
* .NET Core RTM!
|
||||
* Bug fix for Tar long paths
|
||||
|
||||
### Version 0.11.6
|
||||
|
||||
* Bug fix for global header in Tar
|
||||
* Writers now have a leaveOpen `bool` overload. They won't close streams if not-requested to.
|
||||
|
||||
### Version 0.11.5
|
||||
|
||||
* Bug fix in Skip method
|
||||
|
||||
### Version 0.11.4
|
||||
|
||||
* SharpCompress is now endian neutral (matters for Mono platforms)
|
||||
* Fix for Inflate (need to change implementation)
|
||||
* Fixes for RAR detection
|
||||
|
||||
### Version 0.11.1
|
||||
|
||||
* Added Cancel on IReader
|
||||
* Removed .NET 2.0 support and LinqBridge dependency
|
||||
|
||||
### Version 0.11
|
||||
|
||||
* Been over a year, contains mainly fixes from contributors!
|
||||
* Possible breaking change: ArchiveEncoding is UTF8 by default now.
|
||||
* TAR supports writing long names using longlink
|
||||
* RAR Protect Header added
|
||||
|
||||
### Version 0.10.3
|
||||
|
||||
* Finally fixed Disposal issue when creating a new archive with the Archive API
|
||||
|
||||
### Version 0.10.2
|
||||
|
||||
* Fixed Rar Header reading for invalid extended time headers.
|
||||
* Windows Store assembly is now strong named
|
||||
* Known issues with Long Tar names being worked on
|
||||
* Updated to VS2013
|
||||
* Portable targets SL5 and Windows Phone 8 (up from SL4 and WP7)
|
||||
|
||||
### Version 0.10.1
|
||||
|
||||
* Fixed 7Zip extraction performance problem
|
||||
|
||||
### Version 0.10:
|
||||
|
||||
* Added support for RAR Decryption (thanks to https://github.com/hrasyid)
|
||||
* Embedded some BouncyCastle crypto classes to allow RAR Decryption and Winzip AES Decryption in Portable and Windows Store DLLs
|
||||
* Built in Release (I think)
|
||||
## Notes
|
||||
|
||||
XZ implementation based on: https://github.com/sambott/XZ.NET by @sambott
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{CDB425
|
||||
.editorconfig = .editorconfig
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
NuGet.config = NuGet.config
|
||||
.github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml
|
||||
.github\workflows\nuget-release.yml = .github\workflows\nuget-release.yml
|
||||
USAGE.md = USAGE.md
|
||||
README.md = README.md
|
||||
FORMATS.md = FORMATS.md
|
||||
|
||||
40
USAGE.md
40
USAGE.md
@@ -113,38 +113,26 @@ using (var archive = RarArchive.Open("Test.rar"))
|
||||
}
|
||||
```
|
||||
|
||||
### Extract solid Rar or 7Zip archives with manual progress reporting
|
||||
### Extract solid Rar or 7Zip archives with progress reporting
|
||||
|
||||
`ExtractAllEntries` only works for solid archives (Rar) or 7Zip archives. For optimal performance with these archive types, use this method:
|
||||
|
||||
```C#
|
||||
using (var archive = RarArchive.Open("archive.rar")) // Must be solid Rar or 7Zip
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
var progress = new Progress<ProgressReport>(report =>
|
||||
{
|
||||
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
|
||||
Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%");
|
||||
});
|
||||
|
||||
using (var archive = RarArchive.Open("archive.rar", new ReaderOptions { Progress = progress })) // Must be solid Rar or 7Zip
|
||||
{
|
||||
archive.WriteToDirectory(@"D:\output", new ExtractionOptions()
|
||||
{
|
||||
// Calculate total size for progress reporting
|
||||
double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size);
|
||||
long completed = 0;
|
||||
|
||||
using (var reader = archive.ExtractAllEntries())
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
reader.WriteEntryToDirectory(@"D:\output", new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
});
|
||||
|
||||
completed += reader.Entry.Size;
|
||||
double progress = completed / totalSize;
|
||||
Console.WriteLine($"Progress: {progress:P}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ static async Task<(string version, bool isPrerelease)> GetVersion()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not tagged - create prerelease version based on next minor version
|
||||
// Not tagged - create prerelease version
|
||||
var allTags = (await GetGitOutput("tag", "--list"))
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(tag => Regex.IsMatch(tag.Trim(), @"^\d+\.\d+\.\d+$"))
|
||||
@@ -240,8 +240,22 @@ static async Task<(string version, bool isPrerelease)> GetVersion()
|
||||
var lastTag = allTags.OrderBy(tag => Version.Parse(tag)).LastOrDefault() ?? "0.0.0";
|
||||
var lastVersion = Version.Parse(lastTag);
|
||||
|
||||
// Increment minor version for next release
|
||||
var nextVersion = new Version(lastVersion.Major, lastVersion.Minor + 1, 0);
|
||||
// Determine version increment based on branch
|
||||
var currentBranch = await GetCurrentBranch();
|
||||
Version nextVersion;
|
||||
|
||||
if (currentBranch == "release")
|
||||
{
|
||||
// Release branch: increment patch version
|
||||
nextVersion = new Version(lastVersion.Major, lastVersion.Minor, lastVersion.Build + 1);
|
||||
Console.WriteLine($"Building prerelease for release branch (patch increment)");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Master or other branches: increment minor version
|
||||
nextVersion = new Version(lastVersion.Major, lastVersion.Minor + 1, 0);
|
||||
Console.WriteLine($"Building prerelease for {currentBranch} branch (minor increment)");
|
||||
}
|
||||
|
||||
// Use commit count since the last version tag if available; otherwise, fall back to total count
|
||||
var revListArgs = allTags.Any() ? $"--count {lastTag}..HEAD" : "--count HEAD";
|
||||
@@ -253,6 +267,28 @@ static async Task<(string version, bool isPrerelease)> GetVersion()
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<string> GetCurrentBranch()
|
||||
{
|
||||
// In GitHub Actions, GITHUB_REF_NAME contains the branch name
|
||||
var githubRefName = Environment.GetEnvironmentVariable("GITHUB_REF_NAME");
|
||||
if (!string.IsNullOrEmpty(githubRefName))
|
||||
{
|
||||
return githubRefName;
|
||||
}
|
||||
|
||||
// Fallback to git command for local builds
|
||||
try
|
||||
{
|
||||
var (output, _) = await ReadAsync("git", "branch --show-current");
|
||||
return output.Trim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Warning: Could not determine current branch: {ex.Message}");
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<string> GetGitOutput(string command, string args)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -14,11 +14,45 @@
|
||||
"resolved": "1.1.9",
|
||||
"contentHash": "AfK5+ECWYTP7G3AAdnU8IfVj+QpGjrh9GC2mpdcJzCvtQ4pnerAGwHsxJ9D4/RnhDUz2DSzd951O/lQjQby2Sw=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"SimpleExec": {
|
||||
"type": "Direct",
|
||||
"requested": "[12.1.0, )",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "PcCSAlMcKr5yTd571MgEMoGmoSr+omwziq2crB47lKP740lrmjuBocAUXHj+Q6LR6aUDFyhszot2wbtFJTClkA=="
|
||||
"requested": "[13.0.0, )",
|
||||
"resolved": "13.0.0",
|
||||
"contentHash": "zcCR1pupa1wI1VqBULRiQKeHKKZOuJhi/K+4V5oO+rHJZlaOD53ViFo1c3PavDoMAfSn/FAXGAWpPoF57rwhYg=="
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,22 +166,14 @@ public static class ArchiveFactory
|
||||
);
|
||||
}
|
||||
|
||||
public static bool IsArchive(
|
||||
string filePath,
|
||||
out ArchiveType? type,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public static bool IsArchive(string filePath, out ArchiveType? type)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream s = File.OpenRead(filePath);
|
||||
return IsArchive(s, out type, bufferSize);
|
||||
return IsArchive(s, out type);
|
||||
}
|
||||
|
||||
public static bool IsArchive(
|
||||
Stream stream,
|
||||
out ArchiveType? type,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public static bool IsArchive(Stream stream, out ArchiveType? type)
|
||||
{
|
||||
type = null;
|
||||
stream.NotNull(nameof(stream));
|
||||
|
||||
@@ -14,11 +14,8 @@ class AutoArchiveFactory : IArchiveFactory
|
||||
|
||||
public IEnumerable<string> GetSupportedExtensions() => throw new NotSupportedException();
|
||||
|
||||
public bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => throw new NotSupportedException();
|
||||
public bool IsArchive(Stream stream, string? password = null) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public FileInfo? GetFilePart(int index, FileInfo part1) => throw new NotSupportedException();
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ namespace SharpCompress.Archives;
|
||||
|
||||
public static class IArchiveEntryExtensions
|
||||
{
|
||||
private const int BufferSize = 81920;
|
||||
|
||||
/// <param name="archiveEntry">The archive entry to extract.</param>
|
||||
extension(IArchiveEntry archiveEntry)
|
||||
{
|
||||
@@ -28,7 +26,7 @@ public static class IArchiveEntryExtensions
|
||||
|
||||
using var entryStream = archiveEntry.OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
|
||||
sourceStream.CopyTo(streamToWriteTo, BufferSize);
|
||||
sourceStream.CopyTo(streamToWriteTo, Constants.BufferSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,7 +49,7 @@ public static class IArchiveEntryExtensions
|
||||
using var entryStream = await archiveEntry.OpenEntryStreamAsync(cancellationToken);
|
||||
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
|
||||
await sourceStream
|
||||
.CopyToAsync(streamToWriteTo, BufferSize, cancellationToken)
|
||||
.CopyToAsync(streamToWriteTo, Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,10 +212,31 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
public override long TotalSize =>
|
||||
_database?._packSizes.Aggregate(0L, (total, packSize) => total + packSize) ?? 0;
|
||||
|
||||
private sealed class SevenZipReader : AbstractReader<SevenZipEntry, SevenZipVolume>
|
||||
internal sealed class SevenZipReader : AbstractReader<SevenZipEntry, SevenZipVolume>
|
||||
{
|
||||
private readonly SevenZipArchive _archive;
|
||||
private SevenZipEntry? _currentEntry;
|
||||
private Stream? _currentFolderStream;
|
||||
private CFolder? _currentFolder;
|
||||
|
||||
/// <summary>
|
||||
/// Enables internal diagnostics for tests.
|
||||
/// When disabled (default), diagnostics properties return null to avoid exposing internal state.
|
||||
/// </summary>
|
||||
internal bool DiagnosticsEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current folder instance used to decide whether the solid folder stream should be reused.
|
||||
/// Only available when <see cref="DiagnosticsEnabled"/> is true.
|
||||
/// </summary>
|
||||
internal object? DiagnosticsCurrentFolder => DiagnosticsEnabled ? _currentFolder : null;
|
||||
|
||||
/// <summary>
|
||||
/// Current shared folder stream instance.
|
||||
/// Only available when <see cref="DiagnosticsEnabled"/> is true.
|
||||
/// </summary>
|
||||
internal Stream? DiagnosticsCurrentFolderStream =>
|
||||
DiagnosticsEnabled ? _currentFolderStream : null;
|
||||
|
||||
internal SevenZipReader(ReaderOptions readerOptions, SevenZipArchive archive)
|
||||
: base(readerOptions, ArchiveType.SevenZip) => this._archive = archive;
|
||||
@@ -231,9 +252,10 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
_currentEntry = dir;
|
||||
yield return dir;
|
||||
}
|
||||
// For non-directory entries, yield them without creating shared streams
|
||||
// Each call to GetEntryStream() will create a fresh decompression stream
|
||||
// to avoid state corruption issues with async operations
|
||||
// For solid archives (entries in the same folder share a compressed stream),
|
||||
// we must iterate entries sequentially and maintain the folder stream state
|
||||
// across entries in the same folder to avoid recreating the decompression
|
||||
// stream for each file, which breaks contiguous streaming.
|
||||
foreach (var entry in entries.Where(x => !x.IsDirectory))
|
||||
{
|
||||
_currentEntry = entry;
|
||||
@@ -243,19 +265,53 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
|
||||
protected override EntryStream GetEntryStream()
|
||||
{
|
||||
// Create a fresh decompression stream for each file (no state sharing).
|
||||
// However, the LZMA decoder has bugs in its async implementation that cause
|
||||
// state corruption even on fresh streams. The SyncOnlyStream wrapper
|
||||
// works around these bugs by forcing async operations to use sync equivalents.
|
||||
//
|
||||
// TODO: Fix the LZMA decoder async bugs (in LzmaStream, Decoder, OutWindow)
|
||||
// so this wrapper is no longer necessary.
|
||||
var entry = _currentEntry.NotNull("currentEntry is not null");
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
return CreateEntryStream(Stream.Null);
|
||||
}
|
||||
return CreateEntryStream(new SyncOnlyStream(entry.FilePart.GetCompressedStream()));
|
||||
|
||||
var filePart = (SevenZipFilePart)entry.FilePart;
|
||||
if (!filePart.Header.HasStream)
|
||||
{
|
||||
// Entries with no underlying stream (e.g., empty files or anti-items)
|
||||
// should return an empty stream, matching previous behavior.
|
||||
return CreateEntryStream(Stream.Null);
|
||||
}
|
||||
|
||||
var folder = filePart.Folder;
|
||||
// Check if we're starting a new folder - dispose old folder stream if needed
|
||||
if (folder != _currentFolder)
|
||||
{
|
||||
_currentFolderStream?.Dispose();
|
||||
_currentFolderStream = null;
|
||||
_currentFolder = folder;
|
||||
}
|
||||
|
||||
// Create the folder stream once per folder
|
||||
if (_currentFolderStream is null)
|
||||
{
|
||||
_currentFolderStream = _archive._database!.GetFolderStream(
|
||||
_archive.Volumes.Single().Stream,
|
||||
folder!,
|
||||
_archive._database.PasswordProvider
|
||||
);
|
||||
}
|
||||
|
||||
// Wrap with SyncOnlyStream to work around LZMA async bugs
|
||||
// Return a ReadOnlySubStream that reads from the shared folder stream
|
||||
return CreateEntryStream(
|
||||
new SyncOnlyStream(
|
||||
new ReadOnlySubStream(_currentFolderStream, entry.Size, leaveOpen: true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_currentFolderStream?.Dispose();
|
||||
_currentFolderStream = null;
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
using (var entryStream = entry.OpenEntryStream())
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
entryStream.CopyTo(memoryStream);
|
||||
entryStream.CopyTo(memoryStream, Constants.BufferSize);
|
||||
memoryStream.Position = 0;
|
||||
var bytes = memoryStream.ToArray();
|
||||
|
||||
|
||||
@@ -124,38 +124,27 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
);
|
||||
}
|
||||
|
||||
public static bool IsZipFile(
|
||||
string filePath,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => IsZipFile(new FileInfo(filePath), password, bufferSize);
|
||||
public static bool IsZipFile(string filePath, string? password = null) =>
|
||||
IsZipFile(new FileInfo(filePath), password);
|
||||
|
||||
public static bool IsZipFile(
|
||||
FileInfo fileInfo,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public static bool IsZipFile(FileInfo fileInfo, string? password = null)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using Stream stream = fileInfo.OpenRead();
|
||||
return IsZipFile(stream, password, bufferSize);
|
||||
return IsZipFile(stream, password);
|
||||
}
|
||||
|
||||
public static bool IsZipFile(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public static bool IsZipFile(Stream stream, string? password = null)
|
||||
{
|
||||
var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null);
|
||||
try
|
||||
{
|
||||
if (stream is not SharpCompressStream)
|
||||
{
|
||||
stream = new SharpCompressStream(stream, bufferSize: bufferSize);
|
||||
stream = new SharpCompressStream(stream, bufferSize: Constants.BufferSize);
|
||||
}
|
||||
|
||||
var header = headerFactory
|
||||
@@ -177,18 +166,14 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsZipMulti(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public static bool IsZipMulti(Stream stream, string? password = null)
|
||||
{
|
||||
var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null);
|
||||
try
|
||||
{
|
||||
if (stream is not SharpCompressStream)
|
||||
{
|
||||
stream = new SharpCompressStream(stream, bufferSize: bufferSize);
|
||||
stream = new SharpCompressStream(stream, bufferSize: Constants.BufferSize);
|
||||
}
|
||||
|
||||
var header = headerFactory
|
||||
@@ -229,7 +214,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
if (streams.Count() > 1) //test part 2 - true = multipart not split
|
||||
{
|
||||
streams[1].Position += 4; //skip the POST_DATA_DESCRIPTOR to prevent an exception
|
||||
var isZip = IsZipFile(streams[1], ReaderOptions.Password, ReaderOptions.BufferSize);
|
||||
var isZip = IsZipFile(streams[1], ReaderOptions.Password);
|
||||
streams[1].Position -= 4;
|
||||
if (isZip)
|
||||
{
|
||||
|
||||
61
src/SharpCompress/Common/Ace/AceCrc.cs
Normal file
61
src/SharpCompress/Common/Ace/AceCrc.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.Common.Ace
|
||||
{
|
||||
public class AceCrc
|
||||
{
|
||||
// CRC-32 lookup table (standard polynomial 0xEDB88320, reflected)
|
||||
private static readonly uint[] Crc32Table = GenerateTable();
|
||||
|
||||
private static uint[] GenerateTable()
|
||||
{
|
||||
var table = new uint[256];
|
||||
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
uint crc = (uint)i;
|
||||
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
if ((crc & 1) != 0)
|
||||
crc = (crc >> 1) ^ 0xEDB88320u;
|
||||
else
|
||||
crc >>= 1;
|
||||
}
|
||||
|
||||
table[i] = crc;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate ACE CRC-32 checksum.
|
||||
/// ACE CRC-32 uses standard CRC-32 polynomial (0xEDB88320, reflected)
|
||||
/// with init=0xFFFFFFFF but NO final XOR.
|
||||
/// </summary>
|
||||
public static uint AceCrc32(ReadOnlySpan<byte> data)
|
||||
{
|
||||
uint crc = 0xFFFFFFFFu;
|
||||
|
||||
foreach (byte b in data)
|
||||
{
|
||||
crc = (crc >> 8) ^ Crc32Table[(crc ^ b) & 0xFF];
|
||||
}
|
||||
|
||||
return crc; // No final XOR for ACE
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ACE CRC-16 is the lower 16 bits of the ACE CRC-32.
|
||||
/// </summary>
|
||||
public static ushort AceCrc16(ReadOnlySpan<byte> data)
|
||||
{
|
||||
return (ushort)(AceCrc32(data) & 0xFFFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/SharpCompress/Common/Ace/AceEntry.cs
Normal file
68
src/SharpCompress/Common/Ace/AceEntry.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Ace.Headers;
|
||||
|
||||
namespace SharpCompress.Common.Ace
|
||||
{
|
||||
public class AceEntry : Entry
|
||||
{
|
||||
private readonly AceFilePart _filePart;
|
||||
|
||||
internal AceEntry(AceFilePart filePart)
|
||||
{
|
||||
_filePart = filePart;
|
||||
}
|
||||
|
||||
public override long Crc
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_filePart == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return _filePart.Header.Crc32;
|
||||
}
|
||||
}
|
||||
|
||||
public override string? Key => _filePart?.Header.Filename;
|
||||
|
||||
public override string? LinkTarget => null;
|
||||
|
||||
public override long CompressedSize => _filePart?.Header.PackedSize ?? 0;
|
||||
|
||||
public override CompressionType CompressionType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_filePart.Header.CompressionType == Headers.CompressionType.Stored)
|
||||
{
|
||||
return CompressionType.None;
|
||||
}
|
||||
return CompressionType.AceLZ77;
|
||||
}
|
||||
}
|
||||
|
||||
public override long Size => _filePart?.Header.OriginalSize ?? 0;
|
||||
|
||||
public override DateTime? LastModifiedTime => _filePart.Header.DateTime;
|
||||
|
||||
public override DateTime? CreatedTime => null;
|
||||
|
||||
public override DateTime? LastAccessedTime => null;
|
||||
|
||||
public override DateTime? ArchivedTime => null;
|
||||
|
||||
public override bool IsEncrypted => _filePart.Header.IsFileEncrypted;
|
||||
|
||||
public override bool IsDirectory => _filePart.Header.IsDirectory;
|
||||
|
||||
public override bool IsSplitAfter => false;
|
||||
|
||||
internal override IEnumerable<FilePart> Parts => _filePart.Empty();
|
||||
}
|
||||
}
|
||||
52
src/SharpCompress/Common/Ace/AceFilePart.cs
Normal file
52
src/SharpCompress/Common/Ace/AceFilePart.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Ace.Headers;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Common.Ace
|
||||
{
|
||||
public class AceFilePart : FilePart
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
internal AceFileHeader Header { get; set; }
|
||||
|
||||
internal AceFilePart(AceFileHeader localAceHeader, Stream seekableStream)
|
||||
: base(localAceHeader.ArchiveEncoding)
|
||||
{
|
||||
_stream = seekableStream;
|
||||
Header = localAceHeader;
|
||||
}
|
||||
|
||||
internal override string? FilePartName => Header.Filename;
|
||||
|
||||
internal override Stream GetCompressedStream()
|
||||
{
|
||||
if (_stream != null)
|
||||
{
|
||||
Stream compressedStream;
|
||||
switch (Header.CompressionType)
|
||||
{
|
||||
case Headers.CompressionType.Stored:
|
||||
compressedStream = new ReadOnlySubStream(
|
||||
_stream,
|
||||
Header.DataStartPosition,
|
||||
Header.PackedSize
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(
|
||||
"CompressionMethod: " + Header.CompressionQuality
|
||||
);
|
||||
}
|
||||
return compressedStream;
|
||||
}
|
||||
return _stream.NotNull();
|
||||
}
|
||||
|
||||
internal override Stream? GetRawStream() => _stream;
|
||||
}
|
||||
}
|
||||
35
src/SharpCompress/Common/Ace/AceVolume.cs
Normal file
35
src/SharpCompress/Common/Ace/AceVolume.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Arj;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Common.Ace
|
||||
{
|
||||
public class AceVolume : Volume
|
||||
{
|
||||
public AceVolume(Stream stream, ReaderOptions readerOptions, int index = 0)
|
||||
: base(stream, readerOptions, index) { }
|
||||
|
||||
public override bool IsFirstVolume
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ArjArchive is part of a multi-part archive.
|
||||
/// </summary>
|
||||
public override bool IsMultiVolume
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
internal IEnumerable<AceFilePart> GetVolumeFileParts()
|
||||
{
|
||||
return new List<AceFilePart>();
|
||||
}
|
||||
}
|
||||
}
|
||||
171
src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs
Normal file
171
src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Xml.Linq;
|
||||
using SharpCompress.Common.Arc;
|
||||
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// ACE file entry header
|
||||
/// </summary>
|
||||
public sealed class AceFileHeader : AceHeader
|
||||
{
|
||||
public long DataStartPosition { get; private set; }
|
||||
public long PackedSize { get; set; }
|
||||
public long OriginalSize { get; set; }
|
||||
public DateTime DateTime { get; set; }
|
||||
public int Attributes { get; set; }
|
||||
public uint Crc32 { get; set; }
|
||||
public CompressionType CompressionType { get; set; }
|
||||
public CompressionQuality CompressionQuality { get; set; }
|
||||
public ushort Parameters { get; set; }
|
||||
public string Filename { get; set; } = string.Empty;
|
||||
public List<byte> Comment { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// File data offset in the archive
|
||||
/// </summary>
|
||||
public ulong DataOffset { get; set; }
|
||||
|
||||
public bool IsDirectory => (Attributes & 0x10) != 0;
|
||||
|
||||
public bool IsContinuedFromPrev =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.CONTINUED_PREV) != 0;
|
||||
|
||||
public bool IsContinuedToNext =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.CONTINUED_NEXT) != 0;
|
||||
|
||||
public int DictionarySize
|
||||
{
|
||||
get
|
||||
{
|
||||
int bits = Parameters & 0x0F;
|
||||
return bits < 10 ? 1024 : 1 << bits;
|
||||
}
|
||||
}
|
||||
|
||||
public AceFileHeader(ArchiveEncoding archiveEncoding)
|
||||
: base(archiveEncoding, AceHeaderType.FILE) { }
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next file entry header from the stream.
|
||||
/// Returns null if no more entries or end of archive.
|
||||
/// Supports both ACE 1.0 and ACE 2.0 formats.
|
||||
/// </summary>
|
||||
public override AceHeader? Read(Stream stream)
|
||||
{
|
||||
var headerData = ReadHeader(stream);
|
||||
if (headerData.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
int offset = 0;
|
||||
|
||||
// Header type (1 byte)
|
||||
HeaderType = headerData[offset++];
|
||||
|
||||
// Skip recovery record headers (ACE 2.0 feature)
|
||||
if (HeaderType == (byte)SharpCompress.Common.Ace.Headers.AceHeaderType.RECOVERY32)
|
||||
{
|
||||
// Skip to next header
|
||||
return null;
|
||||
}
|
||||
|
||||
if (HeaderType != (byte)SharpCompress.Common.Ace.Headers.AceHeaderType.FILE)
|
||||
{
|
||||
// Unknown header type - skip
|
||||
return null;
|
||||
}
|
||||
|
||||
// Header flags (2 bytes)
|
||||
HeaderFlags = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2;
|
||||
|
||||
// Packed size (4 bytes)
|
||||
PackedSize = BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// Original size (4 bytes)
|
||||
OriginalSize = BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// File date/time in DOS format (4 bytes)
|
||||
var dosDateTime = BitConverter.ToUInt32(headerData, offset);
|
||||
DateTime = ConvertDosDateTime(dosDateTime);
|
||||
offset += 4;
|
||||
|
||||
// File attributes (4 bytes)
|
||||
Attributes = (int)BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// CRC32 (4 bytes)
|
||||
Crc32 = BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// Compression type (1 byte)
|
||||
byte compressionType = headerData[offset++];
|
||||
CompressionType = GetCompressionType(compressionType);
|
||||
|
||||
// Compression quality/parameter (1 byte)
|
||||
byte compressionQuality = headerData[offset++];
|
||||
CompressionQuality = GetCompressionQuality(compressionQuality);
|
||||
|
||||
// Parameters (2 bytes)
|
||||
Parameters = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2;
|
||||
|
||||
// Reserved (2 bytes) - skip
|
||||
offset += 2;
|
||||
|
||||
// Filename length (2 bytes)
|
||||
var filenameLength = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2;
|
||||
|
||||
// Filename
|
||||
if (offset + filenameLength <= headerData.Length)
|
||||
{
|
||||
Filename = ArchiveEncoding.Decode(headerData, offset, filenameLength);
|
||||
offset += filenameLength;
|
||||
}
|
||||
|
||||
// Handle comment if present
|
||||
if ((HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.COMMENT) != 0)
|
||||
{
|
||||
// Comment length (2 bytes)
|
||||
if (offset + 2 <= headerData.Length)
|
||||
{
|
||||
ushort commentLength = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2 + commentLength; // Skip comment
|
||||
}
|
||||
}
|
||||
|
||||
// Store the data start position
|
||||
DataStartPosition = stream.Position;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public CompressionType GetCompressionType(byte value) =>
|
||||
value switch
|
||||
{
|
||||
0 => CompressionType.Stored,
|
||||
1 => CompressionType.Lz77,
|
||||
2 => CompressionType.Blocked,
|
||||
_ => CompressionType.Unknown,
|
||||
};
|
||||
|
||||
public CompressionQuality GetCompressionQuality(byte value) =>
|
||||
value switch
|
||||
{
|
||||
0 => CompressionQuality.None,
|
||||
1 => CompressionQuality.Fastest,
|
||||
2 => CompressionQuality.Fast,
|
||||
3 => CompressionQuality.Normal,
|
||||
4 => CompressionQuality.Good,
|
||||
5 => CompressionQuality.Best,
|
||||
_ => CompressionQuality.Unknown,
|
||||
};
|
||||
}
|
||||
}
|
||||
153
src/SharpCompress/Common/Ace/Headers/AceHeader.cs
Normal file
153
src/SharpCompress/Common/Ace/Headers/AceHeader.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Common.Arj.Headers;
|
||||
using SharpCompress.Crypto;
|
||||
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Header type constants
|
||||
/// </summary>
|
||||
public enum AceHeaderType
|
||||
{
|
||||
MAIN = 0,
|
||||
FILE = 1,
|
||||
RECOVERY32 = 2,
|
||||
RECOVERY64A = 3,
|
||||
RECOVERY64B = 4,
|
||||
}
|
||||
|
||||
public abstract class AceHeader
|
||||
{
|
||||
// ACE signature: bytes at offset 7 should be "**ACE**"
|
||||
private static readonly byte[] AceSignature =
|
||||
[
|
||||
(byte)'*',
|
||||
(byte)'*',
|
||||
(byte)'A',
|
||||
(byte)'C',
|
||||
(byte)'E',
|
||||
(byte)'*',
|
||||
(byte)'*',
|
||||
];
|
||||
|
||||
public AceHeader(ArchiveEncoding archiveEncoding, AceHeaderType type)
|
||||
{
|
||||
AceHeaderType = type;
|
||||
ArchiveEncoding = archiveEncoding;
|
||||
}
|
||||
|
||||
public ArchiveEncoding ArchiveEncoding { get; }
|
||||
public AceHeaderType AceHeaderType { get; }
|
||||
|
||||
public ushort HeaderFlags { get; set; }
|
||||
public ushort HeaderCrc { get; set; }
|
||||
public ushort HeaderSize { get; set; }
|
||||
public byte HeaderType { get; set; }
|
||||
|
||||
public bool IsFileEncrypted =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.FILE_ENCRYPTED) != 0;
|
||||
public bool Is64Bit =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.MEMORY_64BIT) != 0;
|
||||
|
||||
public bool IsSolid =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.SOLID_MAIN) != 0;
|
||||
|
||||
public bool IsMultiVolume =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.MULTIVOLUME) != 0;
|
||||
|
||||
public abstract AceHeader? Read(Stream reader);
|
||||
|
||||
public byte[] ReadHeader(Stream stream)
|
||||
{
|
||||
// Read header CRC (2 bytes) and header size (2 bytes)
|
||||
var headerBytes = new byte[4];
|
||||
if (stream.Read(headerBytes, 0, 4) != 4)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
HeaderCrc = BitConverter.ToUInt16(headerBytes, 0); // CRC for validation
|
||||
HeaderSize = BitConverter.ToUInt16(headerBytes, 2);
|
||||
if (HeaderSize == 0)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
// Read the header data
|
||||
var body = new byte[HeaderSize];
|
||||
if (stream.Read(body, 0, HeaderSize) != HeaderSize)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
// Verify crc
|
||||
var checksum = AceCrc.AceCrc16(body);
|
||||
if (checksum != HeaderCrc)
|
||||
{
|
||||
throw new InvalidDataException("Header checksum is invalid");
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
public static bool IsArchive(Stream stream)
|
||||
{
|
||||
// ACE files have a specific signature
|
||||
// First two bytes are typically 0x60 0xEA (signature bytes)
|
||||
// At offset 7, there should be "**ACE**" (7 bytes)
|
||||
var bytes = new byte[14];
|
||||
if (stream.Read(bytes, 0, 14) != 14)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for "**ACE**" at offset 7
|
||||
return CheckMagicBytes(bytes, 7);
|
||||
}
|
||||
|
||||
protected static bool CheckMagicBytes(byte[] headerBytes, int offset)
|
||||
{
|
||||
// Check for "**ACE**" at specified offset
|
||||
for (int i = 0; i < AceSignature.Length; i++)
|
||||
{
|
||||
if (headerBytes[offset + i] != AceSignature[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected DateTime ConvertDosDateTime(uint dosDateTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
int second = (int)(dosDateTime & 0x1F) * 2;
|
||||
int minute = (int)((dosDateTime >> 5) & 0x3F);
|
||||
int hour = (int)((dosDateTime >> 11) & 0x1F);
|
||||
int day = (int)((dosDateTime >> 16) & 0x1F);
|
||||
int month = (int)((dosDateTime >> 21) & 0x0F);
|
||||
int year = (int)((dosDateTime >> 25) & 0x7F) + 1980;
|
||||
|
||||
if (
|
||||
day < 1
|
||||
|| day > 31
|
||||
|| month < 1
|
||||
|| month > 12
|
||||
|| hour > 23
|
||||
|| minute > 59
|
||||
|| second > 59
|
||||
)
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
|
||||
return new DateTime(year, month, day, hour, minute, second);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs
Normal file
97
src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SharpCompress.Common.Ace.Headers;
|
||||
using SharpCompress.Common.Zip.Headers;
|
||||
using SharpCompress.Crypto;
|
||||
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// ACE main archive header
|
||||
/// </summary>
|
||||
public sealed class AceMainHeader : AceHeader
|
||||
{
|
||||
public byte ExtractVersion { get; set; }
|
||||
public byte CreatorVersion { get; set; }
|
||||
public HostOS HostOS { get; set; }
|
||||
public byte VolumeNumber { get; set; }
|
||||
public DateTime DateTime { get; set; }
|
||||
public string Advert { get; set; } = string.Empty;
|
||||
public List<byte> Comment { get; set; } = new();
|
||||
public byte AceVersion { get; private set; }
|
||||
|
||||
public AceMainHeader(ArchiveEncoding archiveEncoding)
|
||||
: base(archiveEncoding, AceHeaderType.MAIN) { }
|
||||
|
||||
/// <summary>
|
||||
/// Reads the main archive header from the stream.
|
||||
/// Returns header if this is a valid ACE archive.
|
||||
/// Supports both ACE 1.0 and ACE 2.0 formats.
|
||||
/// </summary>
|
||||
public override AceHeader? Read(Stream stream)
|
||||
{
|
||||
var headerData = ReadHeader(stream);
|
||||
if (headerData.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
int offset = 0;
|
||||
|
||||
// Header type should be 0 for main header
|
||||
if (headerData[offset++] != HeaderType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Header flags (2 bytes)
|
||||
HeaderFlags = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2;
|
||||
|
||||
// Skip signature "**ACE**" (7 bytes)
|
||||
if (!CheckMagicBytes(headerData, offset))
|
||||
{
|
||||
throw new InvalidDataException("Invalid ACE archive signature.");
|
||||
}
|
||||
offset += 7;
|
||||
|
||||
// ACE version (1 byte) - 10 for ACE 1.0, 20 for ACE 2.0
|
||||
AceVersion = headerData[offset++];
|
||||
ExtractVersion = headerData[offset++];
|
||||
|
||||
// Host OS (1 byte)
|
||||
if (offset < headerData.Length)
|
||||
{
|
||||
var hostOsByte = headerData[offset++];
|
||||
HostOS = hostOsByte <= 11 ? (HostOS)hostOsByte : HostOS.Unknown;
|
||||
}
|
||||
// Volume number (1 byte)
|
||||
VolumeNumber = headerData[offset++];
|
||||
|
||||
// Creation date/time (4 bytes)
|
||||
var dosDateTime = BitConverter.ToUInt32(headerData, offset);
|
||||
DateTime = ConvertDosDateTime(dosDateTime);
|
||||
offset += 4;
|
||||
|
||||
// Reserved fields (8 bytes)
|
||||
if (offset + 8 <= headerData.Length)
|
||||
{
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
// Skip additional fields based on flags
|
||||
// Handle comment if present
|
||||
if ((HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.COMMENT) != 0)
|
||||
{
|
||||
if (offset + 2 <= headerData.Length)
|
||||
{
|
||||
ushort commentLength = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2 + commentLength;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/SharpCompress/Common/Ace/Headers/CompressionQuality.cs
Normal file
16
src/SharpCompress/Common/Ace/Headers/CompressionQuality.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Compression quality
|
||||
/// </summary>
|
||||
public enum CompressionQuality
|
||||
{
|
||||
None,
|
||||
Fastest,
|
||||
Fast,
|
||||
Normal,
|
||||
Good,
|
||||
Best,
|
||||
Unknown,
|
||||
}
|
||||
}
|
||||
13
src/SharpCompress/Common/Ace/Headers/CompressionType.cs
Normal file
13
src/SharpCompress/Common/Ace/Headers/CompressionType.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Compression types
|
||||
/// </summary>
|
||||
public enum CompressionType
|
||||
{
|
||||
Stored,
|
||||
Lz77,
|
||||
Blocked,
|
||||
Unknown,
|
||||
}
|
||||
}
|
||||
33
src/SharpCompress/Common/Ace/Headers/HeaderFlags.cs
Normal file
33
src/SharpCompress/Common/Ace/Headers/HeaderFlags.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Header flags (main + file, overlapping meanings)
|
||||
/// </summary>
|
||||
public static class HeaderFlags
|
||||
{
|
||||
// Shared / low bits
|
||||
public const ushort ADDSIZE = 0x0001; // extra size field present
|
||||
public const ushort COMMENT = 0x0002; // comment present
|
||||
public const ushort MEMORY_64BIT = 0x0004;
|
||||
public const ushort AV_STRING = 0x0008; // AV string present
|
||||
public const ushort SOLID = 0x0010; // solid file
|
||||
public const ushort LOCKED = 0x0020;
|
||||
public const ushort PROTECTED = 0x0040;
|
||||
|
||||
// Main header specific
|
||||
public const ushort V20FORMAT = 0x0100;
|
||||
public const ushort SFX = 0x0200;
|
||||
public const ushort LIMITSFXJR = 0x0400;
|
||||
public const ushort MULTIVOLUME = 0x0800;
|
||||
public const ushort ADVERT = 0x1000;
|
||||
public const ushort RECOVERY = 0x2000;
|
||||
public const ushort LOCKED_MAIN = 0x4000;
|
||||
public const ushort SOLID_MAIN = 0x8000;
|
||||
|
||||
// File header specific (same bits, different meaning)
|
||||
public const ushort NTSECURITY = 0x0400;
|
||||
public const ushort CONTINUED_PREV = 0x1000;
|
||||
public const ushort CONTINUED_NEXT = 0x2000;
|
||||
public const ushort FILE_ENCRYPTED = 0x4000; // file encrypted (file header)
|
||||
}
|
||||
}
|
||||
22
src/SharpCompress/Common/Ace/Headers/HostOS.cs
Normal file
22
src/SharpCompress/Common/Ace/Headers/HostOS.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Host OS type
|
||||
/// </summary>
|
||||
public enum HostOS
|
||||
{
|
||||
MsDos = 0,
|
||||
Os2,
|
||||
Windows,
|
||||
Unix,
|
||||
MacOs,
|
||||
WinNt,
|
||||
Primos,
|
||||
AppleGs,
|
||||
Atari,
|
||||
Vax,
|
||||
Amiga,
|
||||
Next,
|
||||
Unknown,
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@ public enum ArchiveType
|
||||
GZip,
|
||||
Arc,
|
||||
Arj,
|
||||
Ace,
|
||||
}
|
||||
|
||||
@@ -34,14 +34,13 @@ namespace SharpCompress.Common.Arj.Headers
|
||||
public byte[] ReadHeader(Stream stream)
|
||||
{
|
||||
// check for magic bytes
|
||||
Span<byte> magic = stackalloc byte[2];
|
||||
var magic = new byte[2];
|
||||
if (stream.Read(magic) != 2)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
var magicValue = (ushort)(magic[0] | magic[1] << 8);
|
||||
if (magicValue != ARJ_MAGIC)
|
||||
if (!CheckMagicBytes(magic))
|
||||
{
|
||||
throw new InvalidDataException("Not an ARJ file (wrong magic bytes)");
|
||||
}
|
||||
@@ -138,5 +137,22 @@ namespace SharpCompress.Common.Arj.Headers
|
||||
? (FileType)value
|
||||
: Headers.FileType.Unknown;
|
||||
}
|
||||
|
||||
public static bool IsArchive(Stream stream)
|
||||
{
|
||||
var bytes = new byte[2];
|
||||
if (stream.Read(bytes, 0, 2) != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return CheckMagicBytes(bytes);
|
||||
}
|
||||
|
||||
protected static bool CheckMagicBytes(byte[] headerBytes)
|
||||
{
|
||||
var magicValue = (ushort)(headerBytes[0] | headerBytes[1] << 8);
|
||||
return magicValue == ARJ_MAGIC;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,4 +30,5 @@ public enum CompressionType
|
||||
Distilled,
|
||||
ZStandard,
|
||||
ArjLZ77,
|
||||
AceLZ77,
|
||||
}
|
||||
|
||||
10
src/SharpCompress/Common/Constants.cs
Normal file
10
src/SharpCompress/Common/Constants.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// The default buffer size for stream operations, matching .NET's Stream.CopyTo default of 81920 bytes.
|
||||
/// This can be modified globally at runtime.
|
||||
/// </summary>
|
||||
public static int BufferSize { get; set; } = 81920;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ internal class SevenZipFilePart : FilePart
|
||||
{
|
||||
folderStream.Skip(skipSize);
|
||||
}
|
||||
return new ReadOnlySubStream(folderStream, Header.Size);
|
||||
return new ReadOnlySubStream(folderStream, Header.Size, leaveOpen: false);
|
||||
}
|
||||
|
||||
public CompressionType CompressionType
|
||||
|
||||
@@ -20,7 +20,7 @@ internal class WinzipAesEncryptionData
|
||||
{
|
||||
_keySize = keySize;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
var rfc2898 = new Rfc2898DeriveBytes(password, salt, RFC2898_ITERATIONS);
|
||||
KeyBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
IvBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
|
||||
@@ -30,6 +30,7 @@ public sealed class BZip2Stream : Stream, IStreamStack
|
||||
|
||||
private readonly Stream stream;
|
||||
private bool isDisposed;
|
||||
private readonly bool leaveOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Create a BZip2Stream
|
||||
@@ -37,19 +38,30 @@ public sealed class BZip2Stream : Stream, IStreamStack
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="compressionMode">Compression Mode</param>
|
||||
/// <param name="decompressConcatenated">Decompress Concatenated</param>
|
||||
public BZip2Stream(Stream stream, CompressionMode compressionMode, bool decompressConcatenated)
|
||||
/// <param name="leaveOpen">Leave the stream open after disposing</param>
|
||||
public BZip2Stream(
|
||||
Stream stream,
|
||||
CompressionMode compressionMode,
|
||||
bool decompressConcatenated,
|
||||
bool leaveOpen = false
|
||||
)
|
||||
{
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugConstruct(typeof(BZip2Stream));
|
||||
#endif
|
||||
this.leaveOpen = leaveOpen;
|
||||
Mode = compressionMode;
|
||||
if (Mode == CompressionMode.Compress)
|
||||
{
|
||||
this.stream = new CBZip2OutputStream(stream);
|
||||
this.stream = new CBZip2OutputStream(stream, 9, leaveOpen);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.stream = new CBZip2InputStream(stream, decompressConcatenated);
|
||||
this.stream = new CBZip2InputStream(
|
||||
stream,
|
||||
decompressConcatenated,
|
||||
leaveOpen: leaveOpen
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,7 @@ internal class CBZip2InputStream : Stream, IStreamStack
|
||||
private int computedBlockCRC,
|
||||
computedCombinedCRC;
|
||||
private readonly bool decompressConcatenated;
|
||||
private readonly bool leaveOpen;
|
||||
|
||||
private int i2,
|
||||
count,
|
||||
@@ -181,9 +182,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
|
||||
private char z;
|
||||
private bool isDisposed;
|
||||
|
||||
public CBZip2InputStream(Stream zStream, bool decompressConcatenated)
|
||||
public CBZip2InputStream(Stream zStream, bool decompressConcatenated, bool leaveOpen = false)
|
||||
{
|
||||
this.decompressConcatenated = decompressConcatenated;
|
||||
this.leaveOpen = leaveOpen;
|
||||
ll8 = null;
|
||||
tt = null;
|
||||
BsSetStream(zStream);
|
||||
@@ -207,7 +209,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
|
||||
this.DebugDispose(typeof(CBZip2InputStream));
|
||||
#endif
|
||||
base.Dispose(disposing);
|
||||
bsStream?.Dispose();
|
||||
if (!leaveOpen)
|
||||
{
|
||||
bsStream?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal static int[][] InitIntArray(int n1, int n2)
|
||||
@@ -398,7 +403,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
|
||||
|
||||
private void BsFinishedWithStream()
|
||||
{
|
||||
bsStream?.Dispose();
|
||||
if (!leaveOpen)
|
||||
{
|
||||
bsStream?.Dispose();
|
||||
}
|
||||
bsStream = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -341,12 +341,14 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
|
||||
|
||||
private int currentChar = -1;
|
||||
private int runLength;
|
||||
private readonly bool leaveOpen;
|
||||
|
||||
public CBZip2OutputStream(Stream inStream)
|
||||
: this(inStream, 9) { }
|
||||
public CBZip2OutputStream(Stream inStream, bool leaveOpen = false)
|
||||
: this(inStream, 9, leaveOpen) { }
|
||||
|
||||
public CBZip2OutputStream(Stream inStream, int inBlockSize)
|
||||
public CBZip2OutputStream(Stream inStream, int inBlockSize, bool leaveOpen = false)
|
||||
{
|
||||
this.leaveOpen = leaveOpen;
|
||||
block = null;
|
||||
quadrant = null;
|
||||
zptr = null;
|
||||
@@ -481,7 +483,10 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
|
||||
this.DebugDispose(typeof(CBZip2OutputStream));
|
||||
#endif
|
||||
Dispose();
|
||||
bsStream?.Dispose();
|
||||
if (!leaveOpen)
|
||||
{
|
||||
bsStream?.Dispose();
|
||||
}
|
||||
bsStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,7 +586,13 @@ internal class ZlibBaseStream : Stream, IStreamStack
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
_stream.Flush();
|
||||
// Only flush the underlying stream when in write mode
|
||||
// Flushing input streams during read operations is not meaningful
|
||||
// and can cause issues with forward-only/non-seekable streams
|
||||
if (_streamMode == StreamMode.Writer)
|
||||
{
|
||||
_stream.Flush();
|
||||
}
|
||||
//rewind the buffer
|
||||
((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused
|
||||
z.AvailableBytesIn = 0;
|
||||
@@ -594,7 +600,13 @@ internal class ZlibBaseStream : Stream, IStreamStack
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
// Only flush the underlying stream when in write mode
|
||||
// Flushing input streams during read operations is not meaningful
|
||||
// and can cause issues with forward-only/non-seekable streams
|
||||
if (_streamMode == StreamMode.Writer)
|
||||
{
|
||||
await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
//rewind the buffer
|
||||
((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused
|
||||
z.AvailableBytesIn = 0;
|
||||
|
||||
@@ -153,7 +153,7 @@ internal class OutWindow : IDisposable
|
||||
_pendingLen = rem;
|
||||
}
|
||||
|
||||
public async Task CopyPendingAsync(CancellationToken cancellationToken = default)
|
||||
public async ValueTask CopyPendingAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_pendingLen < 1)
|
||||
{
|
||||
@@ -206,7 +206,7 @@ internal class OutWindow : IDisposable
|
||||
_pendingDist = distance;
|
||||
}
|
||||
|
||||
public async Task CopyBlockAsync(
|
||||
public async ValueTask CopyBlockAsync(
|
||||
int distance,
|
||||
int len,
|
||||
CancellationToken cancellationToken = default
|
||||
@@ -253,7 +253,7 @@ internal class OutWindow : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PutByteAsync(byte b, CancellationToken cancellationToken = default)
|
||||
public async ValueTask PutByteAsync(byte b, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_buffer[_pos++] = b;
|
||||
_total++;
|
||||
@@ -369,6 +369,28 @@ internal class OutWindow : IDisposable
|
||||
return size;
|
||||
}
|
||||
|
||||
public int Read(Memory<byte> buffer, int offset, int count)
|
||||
{
|
||||
if (_streamPos >= _pos)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var size = _pos - _streamPos;
|
||||
if (size > count)
|
||||
{
|
||||
size = count;
|
||||
}
|
||||
_buffer.AsMemory(_streamPos, size).CopyTo(buffer.Slice(offset, size));
|
||||
_streamPos += size;
|
||||
if (_streamPos >= _windowSize)
|
||||
{
|
||||
_pos = 0;
|
||||
_streamPos = 0;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
public int ReadByte()
|
||||
{
|
||||
if (_streamPos >= _pos)
|
||||
|
||||
@@ -45,10 +45,14 @@ public sealed class LZipStream : Stream, IStreamStack
|
||||
private bool _finished;
|
||||
|
||||
private long _writeCount;
|
||||
private readonly Stream? _originalStream;
|
||||
private readonly bool _leaveOpen;
|
||||
|
||||
public LZipStream(Stream stream, CompressionMode mode)
|
||||
public LZipStream(Stream stream, CompressionMode mode, bool leaveOpen = false)
|
||||
{
|
||||
Mode = mode;
|
||||
_originalStream = stream;
|
||||
_leaveOpen = leaveOpen;
|
||||
|
||||
if (mode == CompressionMode.Decompress)
|
||||
{
|
||||
@@ -58,7 +62,7 @@ public sealed class LZipStream : Stream, IStreamStack
|
||||
throw new InvalidFormatException("Not an LZip stream");
|
||||
}
|
||||
var properties = GetProperties(dSize);
|
||||
_stream = new LzmaStream(properties, stream);
|
||||
_stream = new LzmaStream(properties, stream, leaveOpen: leaveOpen);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -125,6 +129,10 @@ public sealed class LZipStream : Stream, IStreamStack
|
||||
{
|
||||
Finish();
|
||||
_stream.Dispose();
|
||||
if (Mode == CompressionMode.Compress && !_leaveOpen)
|
||||
{
|
||||
_originalStream?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Compressors.LZMA.LZ;
|
||||
using SharpCompress.Compressors.LZMA.RangeCoder;
|
||||
|
||||
@@ -475,7 +476,7 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream
|
||||
return false;
|
||||
}
|
||||
|
||||
internal async System.Threading.Tasks.Task<bool> CodeAsync(
|
||||
internal async ValueTask<bool> CodeAsync(
|
||||
int dictionarySize,
|
||||
OutWindow outWindow,
|
||||
RangeCoder.Decoder rangeDecoder,
|
||||
|
||||
@@ -35,6 +35,23 @@ public class LzmaStream : Stream, IStreamStack
|
||||
private readonly Stream _inputStream;
|
||||
private readonly long _inputSize;
|
||||
private readonly long _outputSize;
|
||||
private readonly bool _leaveOpen;
|
||||
|
||||
// Maximum dictionary size to prevent OutOfMemoryException: 512MB
|
||||
// This is a reasonable limit that balances memory usage with decompression capabilities.
|
||||
// The LZMA spec allows up to 4GB, but such large dictionaries are rarely necessary and
|
||||
// can cause OutOfMemoryException in memory-constrained environments (e.g., containers).
|
||||
//
|
||||
// Most 7z archives use much smaller dictionary sizes:
|
||||
// - Default: 32MB for solid archives, 1MB for non-solid
|
||||
// - Common maximum: 64-128MB
|
||||
// - Archives requiring >512MB dictionary are extremely rare
|
||||
//
|
||||
// If you encounter this limit with a legitimate archive, consider:
|
||||
// - Recreating the archive with a smaller dictionary size (if you control creation)
|
||||
// - Increasing available memory for the application
|
||||
// - Filing an issue to request a configurable limit option
|
||||
private const int MaxDictionarySize = 512 * 1024 * 1024; // 512MB
|
||||
|
||||
private readonly int _dictionarySize;
|
||||
private readonly OutWindow _outWindow = new();
|
||||
@@ -56,14 +73,49 @@ public class LzmaStream : Stream, IStreamStack
|
||||
private readonly Encoder _encoder;
|
||||
private bool _isDisposed;
|
||||
|
||||
public LzmaStream(byte[] properties, Stream inputStream)
|
||||
: this(properties, inputStream, -1, -1, null, properties.Length < 5) { }
|
||||
private static void ValidateDictionarySize(int dictionarySize)
|
||||
{
|
||||
// Validate dictionary size
|
||||
if (dictionarySize < 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid dictionary size ({dictionarySize}). The archive may be corrupted or use an unsupported format."
|
||||
);
|
||||
}
|
||||
|
||||
public LzmaStream(byte[] properties, Stream inputStream, long inputSize)
|
||||
: this(properties, inputStream, inputSize, -1, null, properties.Length < 5) { }
|
||||
// Cap dictionary size to prevent OutOfMemoryException
|
||||
if (dictionarySize > MaxDictionarySize)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Dictionary size ({dictionarySize} bytes, {dictionarySize / (1024.0 * 1024.0):F2} MB) exceeds maximum allowed size ({MaxDictionarySize} bytes, {MaxDictionarySize / (1024.0 * 1024.0):F2} MB). "
|
||||
+ "This archive may have been created with an extremely large dictionary size setting. "
|
||||
+ "Consider recreating the archive with a smaller dictionary size."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public LzmaStream(byte[] properties, Stream inputStream, long inputSize, long outputSize)
|
||||
: this(properties, inputStream, inputSize, outputSize, null, properties.Length < 5) { }
|
||||
public LzmaStream(byte[] properties, Stream inputStream, bool leaveOpen = false)
|
||||
: this(properties, inputStream, -1, -1, null, properties.Length < 5, leaveOpen) { }
|
||||
|
||||
public LzmaStream(byte[] properties, Stream inputStream, long inputSize, bool leaveOpen = false)
|
||||
: this(properties, inputStream, inputSize, -1, null, properties.Length < 5, leaveOpen) { }
|
||||
|
||||
public LzmaStream(
|
||||
byte[] properties,
|
||||
Stream inputStream,
|
||||
long inputSize,
|
||||
long outputSize,
|
||||
bool leaveOpen = false
|
||||
)
|
||||
: this(
|
||||
properties,
|
||||
inputStream,
|
||||
inputSize,
|
||||
outputSize,
|
||||
null,
|
||||
properties.Length < 5,
|
||||
leaveOpen
|
||||
) { }
|
||||
|
||||
public LzmaStream(
|
||||
byte[] properties,
|
||||
@@ -71,13 +123,15 @@ public class LzmaStream : Stream, IStreamStack
|
||||
long inputSize,
|
||||
long outputSize,
|
||||
Stream presetDictionary,
|
||||
bool isLzma2
|
||||
bool isLzma2,
|
||||
bool leaveOpen = false
|
||||
)
|
||||
{
|
||||
_inputStream = inputStream;
|
||||
_inputSize = inputSize;
|
||||
_outputSize = outputSize;
|
||||
_isLzma2 = isLzma2;
|
||||
_leaveOpen = leaveOpen;
|
||||
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugConstruct(typeof(LzmaStream));
|
||||
@@ -86,6 +140,8 @@ public class LzmaStream : Stream, IStreamStack
|
||||
if (!isLzma2)
|
||||
{
|
||||
_dictionarySize = BinaryPrimitives.ReadInt32LittleEndian(properties.AsSpan(1));
|
||||
ValidateDictionarySize(_dictionarySize);
|
||||
|
||||
_outWindow.Create(_dictionarySize);
|
||||
if (presetDictionary != null)
|
||||
{
|
||||
@@ -105,6 +161,7 @@ public class LzmaStream : Stream, IStreamStack
|
||||
{
|
||||
_dictionarySize = 2 | (properties[0] & 1);
|
||||
_dictionarySize <<= (properties[0] >> 1) + 11;
|
||||
ValidateDictionarySize(_dictionarySize);
|
||||
|
||||
_outWindow.Create(_dictionarySize);
|
||||
if (presetDictionary != null)
|
||||
@@ -179,7 +236,10 @@ public class LzmaStream : Stream, IStreamStack
|
||||
{
|
||||
_position = _encoder.Code(null, true);
|
||||
}
|
||||
_inputStream?.Dispose();
|
||||
if (!_leaveOpen)
|
||||
{
|
||||
_inputStream?.Dispose();
|
||||
}
|
||||
_outWindow.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
@@ -425,7 +485,7 @@ public class LzmaStream : Stream, IStreamStack
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DecodeChunkHeaderAsync(CancellationToken cancellationToken = default)
|
||||
private async ValueTask DecodeChunkHeaderAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var controlBuffer = new byte[1];
|
||||
await _inputStream
|
||||
@@ -632,6 +692,119 @@ public class LzmaStream : Stream, IStreamStack
|
||||
return total;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (_endReached)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var total = 0;
|
||||
var offset = 0;
|
||||
var count = buffer.Length;
|
||||
while (total < count)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_availableBytes == 0)
|
||||
{
|
||||
if (_isLzma2)
|
||||
{
|
||||
await DecodeChunkHeaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_endReached = true;
|
||||
}
|
||||
if (_endReached)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var toProcess = count - total;
|
||||
if (toProcess > _availableBytes)
|
||||
{
|
||||
toProcess = (int)_availableBytes;
|
||||
}
|
||||
|
||||
_outWindow.SetLimit(toProcess);
|
||||
if (_uncompressedChunk)
|
||||
{
|
||||
_inputPosition += await _outWindow
|
||||
.CopyStreamAsync(_inputStream, toProcess, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (
|
||||
await _decoder
|
||||
.CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
&& _outputSize < 0
|
||||
)
|
||||
{
|
||||
_availableBytes = _outWindow.AvailableBytes;
|
||||
}
|
||||
|
||||
var read = _outWindow.Read(buffer, offset, toProcess);
|
||||
total += read;
|
||||
offset += read;
|
||||
_position += read;
|
||||
_availableBytes -= read;
|
||||
|
||||
if (_availableBytes == 0 && !_uncompressedChunk)
|
||||
{
|
||||
if (
|
||||
!_rangeDecoder.IsFinished
|
||||
|| (_rangeDecoderLimit >= 0 && _rangeDecoder._total != _rangeDecoderLimit)
|
||||
)
|
||||
{
|
||||
_outWindow.SetLimit(toProcess + 1);
|
||||
if (
|
||||
!await _decoder
|
||||
.CodeAsync(
|
||||
_dictionarySize,
|
||||
_outWindow,
|
||||
_rangeDecoder,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false)
|
||||
)
|
||||
{
|
||||
_rangeDecoder.ReleaseStream();
|
||||
throw new DataErrorException();
|
||||
}
|
||||
}
|
||||
|
||||
_rangeDecoder.ReleaseStream();
|
||||
|
||||
_inputPosition += _rangeDecoder._total;
|
||||
if (_outWindow.HasPending)
|
||||
{
|
||||
throw new DataErrorException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_endReached)
|
||||
{
|
||||
if (_inputSize >= 0 && _inputPosition != _inputSize)
|
||||
{
|
||||
throw new DataErrorException();
|
||||
}
|
||||
if (_outputSize >= 0 && _position != _outputSize)
|
||||
{
|
||||
throw new DataErrorException();
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
|
||||
@@ -134,7 +134,7 @@ internal class RarStream : Stream, IStreamStack
|
||||
fetch = false;
|
||||
}
|
||||
_position += outTotal;
|
||||
if (count > 0 && outTotal == 0 && _position != Length)
|
||||
if (count > 0 && outTotal == 0 && _position < Length)
|
||||
{
|
||||
// sanity check, eg if we try to decompress a redir entry
|
||||
throw new InvalidOperationException(
|
||||
@@ -179,7 +179,7 @@ internal class RarStream : Stream, IStreamStack
|
||||
fetch = false;
|
||||
}
|
||||
_position += outTotal;
|
||||
if (count > 0 && outTotal == 0 && _position != Length)
|
||||
if (count > 0 && outTotal == 0 && _position < Length)
|
||||
{
|
||||
// sanity check, eg if we try to decompress a redir entry
|
||||
throw new InvalidOperationException(
|
||||
|
||||
33
src/SharpCompress/Factories/AceFactory.cs
Normal file
33
src/SharpCompress/Factories/AceFactory.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Ace.Headers;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Ace;
|
||||
|
||||
namespace SharpCompress.Factories
|
||||
{
|
||||
public class AceFactory : Factory, IReaderFactory
|
||||
{
|
||||
public override string Name => "Ace";
|
||||
|
||||
public override ArchiveType? KnownArchiveType => ArchiveType.Ace;
|
||||
|
||||
public override IEnumerable<string> GetSupportedExtensions()
|
||||
{
|
||||
yield return "ace";
|
||||
}
|
||||
|
||||
public override bool IsArchive(Stream stream, string? password = null)
|
||||
{
|
||||
return AceHeader.IsArchive(stream);
|
||||
}
|
||||
|
||||
public IReader OpenReader(Stream stream, ReaderOptions? options) =>
|
||||
AceReader.Open(stream, options);
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,7 @@ namespace SharpCompress.Factories
|
||||
yield return "arc";
|
||||
}
|
||||
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public override bool IsArchive(Stream stream, string? password = null)
|
||||
{
|
||||
//You may have to use some(paranoid) checks to ensure that you actually are
|
||||
//processing an ARC file, since other archivers also adopted the idea of putting
|
||||
|
||||
@@ -22,18 +22,9 @@ namespace SharpCompress.Factories
|
||||
yield return "arj";
|
||||
}
|
||||
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public override bool IsArchive(Stream stream, string? password = null)
|
||||
{
|
||||
var arjHeader = new ArjMainHeader(new ArchiveEncoding());
|
||||
if (arjHeader.Read(stream) == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return ArjHeader.IsArchive(stream);
|
||||
}
|
||||
|
||||
public IReader OpenReader(Stream stream, ReaderOptions? options) =>
|
||||
|
||||
@@ -19,6 +19,7 @@ public abstract class Factory : IFactory
|
||||
RegisterFactory(new TarFactory());
|
||||
RegisterFactory(new ArcFactory());
|
||||
RegisterFactory(new ArjFactory());
|
||||
RegisterFactory(new AceFactory());
|
||||
}
|
||||
|
||||
private static readonly HashSet<Factory> _factories = new();
|
||||
@@ -50,11 +51,7 @@ public abstract class Factory : IFactory
|
||||
public abstract IEnumerable<string> GetSupportedExtensions();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
);
|
||||
public abstract bool IsArchive(Stream stream, string? password = null);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual FileInfo? GetFilePart(int index, FileInfo part1) => null;
|
||||
@@ -81,7 +78,7 @@ public abstract class Factory : IFactory
|
||||
{
|
||||
long pos = ((IStreamStack)stream).GetPosition();
|
||||
|
||||
if (IsArchive(stream, options.Password, options.BufferSize))
|
||||
if (IsArchive(stream, options.Password))
|
||||
{
|
||||
((IStreamStack)stream).StackSeek(pos);
|
||||
reader = readerFactory.OpenReader(stream, options);
|
||||
|
||||
@@ -40,11 +40,8 @@ public class GZipFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => GZipArchive.IsGZipFile(stream);
|
||||
public override bool IsArchive(Stream stream, string? password = null) =>
|
||||
GZipArchive.IsGZipFile(stream);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -36,11 +36,7 @@ public interface IFactory
|
||||
/// </summary>
|
||||
/// <param name="stream">A stream, pointing to the beginning of the archive.</param>
|
||||
/// <param name="password">optional password</param>
|
||||
bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
);
|
||||
bool IsArchive(Stream stream, string? password = null);
|
||||
|
||||
/// <summary>
|
||||
/// From a passed in archive (zip, rar, 7z, 001), return all parts.
|
||||
|
||||
@@ -29,11 +29,8 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => RarArchive.IsRarFile(stream);
|
||||
public override bool IsArchive(Stream stream, string? password = null) =>
|
||||
RarArchive.IsRarFile(stream);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override FileInfo? GetFilePart(int index, FileInfo part1) =>
|
||||
|
||||
@@ -28,11 +28,8 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => SevenZipArchive.IsSevenZipFile(stream);
|
||||
public override bool IsArchive(Stream stream, string? password = null) =>
|
||||
SevenZipArchive.IsSevenZipFile(stream);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -53,11 +53,8 @@ public class TarFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => TarArchive.IsTarFile(stream);
|
||||
public override bool IsArchive(Stream stream, string? password = null) =>
|
||||
TarArchive.IsTarFile(stream);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ internal class ZStandardFactory : Factory
|
||||
yield return "zstd";
|
||||
}
|
||||
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = 65536
|
||||
) => ZStandardStream.IsZStandard(stream);
|
||||
public override bool IsArchive(Stream stream, string? password = null) =>
|
||||
ZStandardStream.IsZStandard(stream);
|
||||
}
|
||||
|
||||
@@ -39,11 +39,7 @@ public class ZipFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public override bool IsArchive(Stream stream, string? password = null)
|
||||
{
|
||||
var startPosition = stream.CanSeek ? stream.Position : -1;
|
||||
|
||||
@@ -51,10 +47,10 @@ public class ZipFactory
|
||||
|
||||
if (stream is not SharpCompressStream) // wrap to provide buffer bef
|
||||
{
|
||||
stream = new SharpCompressStream(stream, bufferSize: bufferSize);
|
||||
stream = new SharpCompressStream(stream, bufferSize: Constants.BufferSize);
|
||||
}
|
||||
|
||||
if (ZipArchive.IsZipFile(stream, password, bufferSize))
|
||||
if (ZipArchive.IsZipFile(stream, password))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -69,7 +65,7 @@ public class ZipFactory
|
||||
stream.Position = startPosition;
|
||||
|
||||
//test the zip (last) file of a multipart zip
|
||||
if (ZipArchive.IsZipMulti(stream, password, bufferSize))
|
||||
if (ZipArchive.IsZipMulti(stream, password))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
@@ -26,14 +29,25 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(BufferedSubStream));
|
||||
#endif
|
||||
if (disposing) { }
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
|
||||
if (disposing && _cache is not null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_cache);
|
||||
_cache = null;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private int _cacheOffset;
|
||||
private int _cacheLength;
|
||||
private readonly byte[] _cache = new byte[32 << 10];
|
||||
private byte[]? _cache = ArrayPool<byte>.Shared.Rent(81920);
|
||||
private long origin;
|
||||
private bool _isDisposed;
|
||||
|
||||
private long BytesLeftToRead { get; set; }
|
||||
|
||||
@@ -55,19 +69,58 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
|
||||
|
||||
private void RefillCache()
|
||||
{
|
||||
var count = (int)Math.Min(BytesLeftToRead, _cache.Length);
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(BufferedSubStream));
|
||||
}
|
||||
|
||||
var count = (int)Math.Min(BytesLeftToRead, _cache!.Length);
|
||||
_cacheOffset = 0;
|
||||
if (count == 0)
|
||||
{
|
||||
_cacheLength = 0;
|
||||
return;
|
||||
}
|
||||
Stream.Position = origin;
|
||||
|
||||
// Only seek if we're not already at the correct position
|
||||
// This avoids expensive seek operations when reading sequentially
|
||||
if (Stream.CanSeek && Stream.Position != origin)
|
||||
{
|
||||
Stream.Position = origin;
|
||||
}
|
||||
|
||||
_cacheLength = Stream.Read(_cache, 0, count);
|
||||
origin += _cacheLength;
|
||||
BytesLeftToRead -= _cacheLength;
|
||||
}
|
||||
|
||||
private async ValueTask RefillCacheAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(BufferedSubStream));
|
||||
}
|
||||
|
||||
var count = (int)Math.Min(BytesLeftToRead, _cache!.Length);
|
||||
_cacheOffset = 0;
|
||||
if (count == 0)
|
||||
{
|
||||
_cacheLength = 0;
|
||||
return;
|
||||
}
|
||||
// Only seek if we're not already at the correct position
|
||||
// This avoids expensive seek operations when reading sequentially
|
||||
if (Stream.CanSeek && Stream.Position != origin)
|
||||
{
|
||||
Stream.Position = origin;
|
||||
}
|
||||
_cacheLength = await Stream
|
||||
.ReadAsync(_cache, 0, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
origin += _cacheLength;
|
||||
BytesLeftToRead -= _cacheLength;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (count > Length)
|
||||
@@ -83,7 +136,7 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
|
||||
}
|
||||
|
||||
count = Math.Min(count, _cacheLength - _cacheOffset);
|
||||
Buffer.BlockCopy(_cache, _cacheOffset, buffer, offset, count);
|
||||
Buffer.BlockCopy(_cache!, _cacheOffset, buffer, offset, count);
|
||||
_cacheOffset += count;
|
||||
}
|
||||
|
||||
@@ -101,9 +154,64 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
|
||||
}
|
||||
}
|
||||
|
||||
return _cache[_cacheOffset++];
|
||||
return _cache![_cacheOffset++];
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (count > Length)
|
||||
{
|
||||
count = (int)Length;
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
if (_cacheOffset == _cacheLength)
|
||||
{
|
||||
await RefillCacheAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
count = Math.Min(count, _cacheLength - _cacheOffset);
|
||||
Buffer.BlockCopy(_cache!, _cacheOffset, buffer, offset, count);
|
||||
_cacheOffset += count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var count = buffer.Length;
|
||||
if (count > Length)
|
||||
{
|
||||
count = (int)Length;
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
if (_cacheOffset == _cacheLength)
|
||||
{
|
||||
await RefillCacheAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
count = Math.Min(count, _cacheLength - _cacheOffset);
|
||||
_cache!.AsSpan(_cacheOffset, count).CopyTo(buffer.Span);
|
||||
_cacheOffset += count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
@@ -16,11 +16,11 @@ internal class ReadOnlySubStream : SharpCompressStream, IStreamStack
|
||||
|
||||
private long _position;
|
||||
|
||||
public ReadOnlySubStream(Stream stream, long bytesToRead)
|
||||
: this(stream, null, bytesToRead) { }
|
||||
public ReadOnlySubStream(Stream stream, long bytesToRead, bool leaveOpen = true)
|
||||
: this(stream, null, bytesToRead, leaveOpen) { }
|
||||
|
||||
public ReadOnlySubStream(Stream stream, long? origin, long bytesToRead)
|
||||
: base(stream, leaveOpen: true, throwOnDispose: false)
|
||||
public ReadOnlySubStream(Stream stream, long? origin, long bytesToRead, bool leaveOpen = true)
|
||||
: base(stream, leaveOpen, throwOnDispose: false)
|
||||
{
|
||||
if (origin != null && stream.Position != origin.Value)
|
||||
{
|
||||
|
||||
@@ -138,8 +138,6 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
#endif
|
||||
}
|
||||
|
||||
internal bool IsRecording { get; private set; }
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
#if DEBUG_STREAMS
|
||||
@@ -208,11 +206,11 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
{
|
||||
ValidateBufferState();
|
||||
|
||||
// Fill buffer if needed
|
||||
// Fill buffer if needed, handling short reads from underlying stream
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
_bufferedLength = Stream.Read(_buffer!, 0, _bufferSize);
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = FillBuffer(_buffer!, 0, _bufferSize);
|
||||
}
|
||||
int available = _bufferedLength - _bufferPosition;
|
||||
int toRead = Math.Min(count, available);
|
||||
@@ -224,11 +222,8 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
return toRead;
|
||||
}
|
||||
// If buffer exhausted, refill
|
||||
int r = Stream.Read(_buffer!, 0, _bufferSize);
|
||||
if (r == 0)
|
||||
return 0;
|
||||
_bufferedLength = r;
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = FillBuffer(_buffer!, 0, _bufferSize);
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
return 0;
|
||||
@@ -252,6 +247,31 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills the buffer by reading from the underlying stream, handling short reads.
|
||||
/// Implements the ReadFully pattern: reads in a loop until buffer is full or EOF is reached.
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to fill</param>
|
||||
/// <param name="offset">Offset in buffer (always 0 in current usage)</param>
|
||||
/// <param name="count">Number of bytes to read</param>
|
||||
/// <returns>Total number of bytes read (may be less than count if EOF is reached)</returns>
|
||||
private int FillBuffer(byte[] buffer, int offset, int count)
|
||||
{
|
||||
// Implement ReadFully pattern but return the actual count read
|
||||
// This is the same logic as Utility.ReadFully but returns count instead of bool
|
||||
var total = 0;
|
||||
int read;
|
||||
while ((read = Stream.Read(buffer, offset + total, count - total)) > 0)
|
||||
{
|
||||
total += read;
|
||||
if (total >= count)
|
||||
{
|
||||
return total;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
if (_bufferingEnabled)
|
||||
@@ -259,7 +279,6 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
ValidateBufferState();
|
||||
}
|
||||
|
||||
long orig = _internalPosition;
|
||||
long targetPos;
|
||||
// Calculate the absolute target position based on origin
|
||||
switch (origin)
|
||||
@@ -327,13 +346,12 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
{
|
||||
ValidateBufferState();
|
||||
|
||||
// Fill buffer if needed
|
||||
// Fill buffer if needed, handling short reads from underlying stream
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
_bufferedLength = await Stream
|
||||
.ReadAsync(_buffer!, 0, _bufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = await FillBufferAsync(_buffer!, 0, _bufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
int available = _bufferedLength - _bufferPosition;
|
||||
int toRead = Math.Min(count, available);
|
||||
@@ -345,13 +363,9 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
return toRead;
|
||||
}
|
||||
// If buffer exhausted, refill
|
||||
int r = await Stream
|
||||
.ReadAsync(_buffer!, 0, _bufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (r == 0)
|
||||
return 0;
|
||||
_bufferedLength = r;
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = await FillBufferAsync(_buffer!, 0, _bufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
return 0;
|
||||
@@ -372,6 +386,38 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async version of FillBuffer. Implements the ReadFullyAsync pattern.
|
||||
/// Reads in a loop until buffer is full or EOF is reached.
|
||||
/// </summary>
|
||||
private async Task<int> FillBufferAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
// Implement ReadFullyAsync pattern but return the actual count read
|
||||
// This is the same logic as Utility.ReadFullyAsync but returns count instead of bool
|
||||
var total = 0;
|
||||
int read;
|
||||
while (
|
||||
(
|
||||
read = await Stream
|
||||
.ReadAsync(buffer, offset + total, count - total, cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
) > 0
|
||||
)
|
||||
{
|
||||
total += read;
|
||||
if (total >= count)
|
||||
{
|
||||
return total;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
@@ -402,13 +448,15 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
{
|
||||
ValidateBufferState();
|
||||
|
||||
// Fill buffer if needed
|
||||
// Fill buffer if needed, handling short reads from underlying stream
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
_bufferedLength = await Stream
|
||||
.ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = await FillBufferMemoryAsync(
|
||||
_buffer.AsMemory(0, _bufferSize),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
int available = _bufferedLength - _bufferPosition;
|
||||
int toRead = Math.Min(buffer.Length, available);
|
||||
@@ -420,13 +468,12 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
return toRead;
|
||||
}
|
||||
// If buffer exhausted, refill
|
||||
int r = await Stream
|
||||
.ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (r == 0)
|
||||
return 0;
|
||||
_bufferedLength = r;
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = await FillBufferMemoryAsync(
|
||||
_buffer.AsMemory(0, _bufferSize),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
return 0;
|
||||
@@ -445,6 +492,35 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async version of FillBuffer for Memory{byte}. Implements the ReadFullyAsync pattern.
|
||||
/// Reads in a loop until buffer is full or EOF is reached.
|
||||
/// </summary>
|
||||
private async ValueTask<int> FillBufferMemoryAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
// Implement ReadFullyAsync pattern but return the actual count read
|
||||
var total = 0;
|
||||
int read;
|
||||
while (
|
||||
(
|
||||
read = await Stream
|
||||
.ReadAsync(buffer.Slice(total), cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
) > 0
|
||||
)
|
||||
{
|
||||
total += read;
|
||||
if (total >= buffer.Length)
|
||||
{
|
||||
return total;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public override async ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
|
||||
@@ -222,8 +222,26 @@ public class SourceStream : Stream, IStreamStack
|
||||
SetStream(0);
|
||||
while (_prevSize + Current.Length < pos)
|
||||
{
|
||||
_prevSize += Current.Length;
|
||||
SetStream(_stream + 1);
|
||||
var currentLength = Current.Length;
|
||||
_prevSize += currentLength;
|
||||
|
||||
if (!SetStream(_stream + 1))
|
||||
{
|
||||
// No more streams available, cannot seek to requested position
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot seek to position {pos}. End of stream reached at position {_prevSize}."
|
||||
);
|
||||
}
|
||||
|
||||
// Safety check: if we have a zero-length stream and we're still not
|
||||
// making progress toward the target position, we're in an invalid state
|
||||
if (currentLength == 0 && Current.Length == 0)
|
||||
{
|
||||
// Both old and new stream have zero length - cannot make progress
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot seek to position {pos}. Encountered zero-length streams at position {_prevSize}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader
|
||||
{
|
||||
using Stream s = OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
sourceStream.CopyTo(writeStream, 81920);
|
||||
sourceStream.CopyTo(writeStream, Constants.BufferSize);
|
||||
}
|
||||
|
||||
internal async Task WriteAsync(Stream writeStream, CancellationToken cancellationToken)
|
||||
@@ -270,11 +270,15 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
using Stream s = OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false);
|
||||
await sourceStream
|
||||
.CopyToAsync(writeStream, Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
await using Stream s = OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false);
|
||||
await sourceStream
|
||||
.CopyToAsync(writeStream, Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
115
src/SharpCompress/Readers/Ace/AceReader.cs
Normal file
115
src/SharpCompress/Readers/Ace/AceReader.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Ace;
|
||||
using SharpCompress.Common.Ace.Headers;
|
||||
using SharpCompress.Common.Arj;
|
||||
|
||||
namespace SharpCompress.Readers.Ace
|
||||
{
|
||||
/// <summary>
|
||||
/// Reader for ACE archives.
|
||||
/// ACE is a proprietary archive format. This implementation supports both ACE 1.0 and ACE 2.0 formats
|
||||
/// and can read archive metadata and extract uncompressed (stored) entries.
|
||||
/// Compressed entries require proprietary decompression algorithms that are not publicly documented.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ACE 2.0 additions over ACE 1.0:
|
||||
/// - Improved LZ77 compression (compression type 2)
|
||||
/// - Recovery record support
|
||||
/// - Additional header flags
|
||||
/// </remarks>
|
||||
public abstract class AceReader : AbstractReader<AceEntry, AceVolume>
|
||||
{
|
||||
private readonly ArchiveEncoding _archiveEncoding;
|
||||
|
||||
internal AceReader(ReaderOptions options)
|
||||
: base(options, ArchiveType.Ace)
|
||||
{
|
||||
_archiveEncoding = Options.ArchiveEncoding;
|
||||
}
|
||||
|
||||
private AceReader(Stream stream, ReaderOptions options)
|
||||
: this(options) { }
|
||||
|
||||
/// <summary>
|
||||
/// Derived class must create or manage the Volume itself.
|
||||
/// AbstractReader.Volume is get-only, so it cannot be set here.
|
||||
/// </summary>
|
||||
public override AceVolume? Volume => _volume;
|
||||
|
||||
private AceVolume? _volume;
|
||||
|
||||
/// <summary>
|
||||
/// Opens an AceReader for non-seeking usage with a single volume.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream containing the ACE archive.</param>
|
||||
/// <param name="options">Reader options.</param>
|
||||
/// <returns>An AceReader instance.</returns>
|
||||
public static AceReader Open(Stream stream, ReaderOptions? options = null)
|
||||
{
|
||||
stream.NotNull(nameof(stream));
|
||||
return new SingleVolumeAceReader(stream, options ?? new ReaderOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens an AceReader for Non-seeking usage with multiple volumes
|
||||
/// </summary>
|
||||
/// <param name="streams"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
public static AceReader Open(IEnumerable<Stream> streams, ReaderOptions? options = null)
|
||||
{
|
||||
streams.NotNull(nameof(streams));
|
||||
return new MultiVolumeAceReader(streams, options ?? new ReaderOptions());
|
||||
}
|
||||
|
||||
protected abstract void ValidateArchive(AceVolume archive);
|
||||
|
||||
protected override IEnumerable<AceEntry> GetEntries(Stream stream)
|
||||
{
|
||||
var mainHeaderReader = new AceMainHeader(_archiveEncoding);
|
||||
var mainHeader = mainHeaderReader.Read(stream);
|
||||
if (mainHeader == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (mainHeader?.IsMultiVolume == true)
|
||||
{
|
||||
throw new MultiVolumeExtractionException(
|
||||
"Multi volumes are currently not supported"
|
||||
);
|
||||
}
|
||||
|
||||
if (_volume == null)
|
||||
{
|
||||
_volume = new AceVolume(stream, Options, 0);
|
||||
ValidateArchive(_volume);
|
||||
}
|
||||
|
||||
var localHeaderReader = new AceFileHeader(_archiveEncoding);
|
||||
while (true)
|
||||
{
|
||||
var localHeader = localHeaderReader.Read(stream);
|
||||
if (localHeader?.IsFileEncrypted == true)
|
||||
{
|
||||
throw new CryptographicException(
|
||||
"Password protected archives are currently not supported"
|
||||
);
|
||||
}
|
||||
if (localHeader == null)
|
||||
break;
|
||||
|
||||
yield return new AceEntry(new AceFilePart((AceFileHeader)localHeader, stream));
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<FilePart> CreateFilePartEnumerableForCurrentEntry() =>
|
||||
Entry.Parts;
|
||||
}
|
||||
}
|
||||
117
src/SharpCompress/Readers/Ace/MultiVolumeAceReader.cs
Normal file
117
src/SharpCompress/Readers/Ace/MultiVolumeAceReader.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Ace;
|
||||
|
||||
namespace SharpCompress.Readers.Ace
|
||||
{
|
||||
internal class MultiVolumeAceReader : AceReader
|
||||
{
|
||||
private readonly IEnumerator<Stream> streams;
|
||||
private Stream tempStream;
|
||||
|
||||
internal MultiVolumeAceReader(IEnumerable<Stream> streams, ReaderOptions options)
|
||||
: base(options) => this.streams = streams.GetEnumerator();
|
||||
|
||||
protected override void ValidateArchive(AceVolume archive) { }
|
||||
|
||||
protected override Stream RequestInitialStream()
|
||||
{
|
||||
if (streams.MoveNext())
|
||||
{
|
||||
return streams.Current;
|
||||
}
|
||||
throw new MultiVolumeExtractionException(
|
||||
"No stream provided when requested by MultiVolumeAceReader"
|
||||
);
|
||||
}
|
||||
|
||||
internal override bool NextEntryForCurrentStream()
|
||||
{
|
||||
if (!base.NextEntryForCurrentStream())
|
||||
{
|
||||
// if we're got another stream to try to process then do so
|
||||
return streams.MoveNext() && LoadStreamForReading(streams.Current);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override IEnumerable<FilePart> CreateFilePartEnumerableForCurrentEntry()
|
||||
{
|
||||
var enumerator = new MultiVolumeStreamEnumerator(this, streams, tempStream);
|
||||
tempStream = null;
|
||||
return enumerator;
|
||||
}
|
||||
|
||||
private class MultiVolumeStreamEnumerator : IEnumerable<FilePart>, IEnumerator<FilePart>
|
||||
{
|
||||
private readonly MultiVolumeAceReader reader;
|
||||
private readonly IEnumerator<Stream> nextReadableStreams;
|
||||
private Stream tempStream;
|
||||
private bool isFirst = true;
|
||||
|
||||
internal MultiVolumeStreamEnumerator(
|
||||
MultiVolumeAceReader r,
|
||||
IEnumerator<Stream> nextReadableStreams,
|
||||
Stream tempStream
|
||||
)
|
||||
{
|
||||
reader = r;
|
||||
this.nextReadableStreams = nextReadableStreams;
|
||||
this.tempStream = tempStream;
|
||||
}
|
||||
|
||||
public IEnumerator<FilePart> GetEnumerator() => this;
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => this;
|
||||
|
||||
public FilePart Current { get; private set; }
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
Current = reader.Entry.Parts.First();
|
||||
isFirst = false; //first stream already to go
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!reader.Entry.IsSplitAfter)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (tempStream != null)
|
||||
{
|
||||
reader.LoadStreamForReading(tempStream);
|
||||
tempStream = null;
|
||||
}
|
||||
else if (!nextReadableStreams.MoveNext())
|
||||
{
|
||||
throw new MultiVolumeExtractionException(
|
||||
"No stream provided when requested by MultiVolumeAceReader"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.LoadStreamForReading(nextReadableStreams.Current);
|
||||
}
|
||||
|
||||
Current = reader.Entry.Parts.First();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs
Normal file
31
src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Ace;
|
||||
|
||||
namespace SharpCompress.Readers.Ace
|
||||
{
|
||||
internal class SingleVolumeAceReader : AceReader
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
|
||||
internal SingleVolumeAceReader(Stream stream, ReaderOptions options)
|
||||
: base(options)
|
||||
{
|
||||
stream.NotNull(nameof(stream));
|
||||
_stream = stream;
|
||||
}
|
||||
|
||||
protected override Stream RequestInitialStream() => _stream;
|
||||
|
||||
protected override void ValidateArchive(AceVolume archive)
|
||||
{
|
||||
if (archive.IsMultiVolume)
|
||||
{
|
||||
throw new MultiVolumeExtractionException(
|
||||
"Streamed archive is a Multi-volume archive. Use a different AceReader method to extract."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ public static class ReaderFactory
|
||||
}
|
||||
|
||||
throw new InvalidFormatException(
|
||||
"Cannot determine compressed stream type. Supported Reader Formats: Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard"
|
||||
"Cannot determine compressed stream type. Supported Reader Formats: Ace, Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,14 @@ namespace SharpCompress.Readers;
|
||||
|
||||
public class ReaderOptions : OptionsBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The default buffer size for stream operations.
|
||||
/// This value (65536 bytes) is preserved for backward compatibility.
|
||||
/// New code should use Constants.BufferSize instead (81920 bytes), which matches .NET's Stream.CopyTo default.
|
||||
/// </summary>
|
||||
[Obsolete(
|
||||
"Use Constants.BufferSize instead. This constant will be removed in a future version."
|
||||
)]
|
||||
public const int DefaultBufferSize = 0x10000;
|
||||
|
||||
/// <summary>
|
||||
@@ -16,7 +24,7 @@ public class ReaderOptions : OptionsBase
|
||||
|
||||
public bool DisableCheckIncomplete { get; set; }
|
||||
|
||||
public int BufferSize { get; set; } = DefaultBufferSize;
|
||||
public int BufferSize { get; set; } = Constants.BufferSize;
|
||||
|
||||
/// <summary>
|
||||
/// Provide a hint for the extension of the archive being read, can speed up finding the correct decoder. Should be without the leading period in the form like: tar.gz or zip
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<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.0.0</VersionPrefix>
|
||||
<AssemblyVersion>0.0.0.0</AssemblyVersion>
|
||||
<FileVersion>0.0.0.0</FileVersion>
|
||||
<Authors>Adam Hathcock</Authors>
|
||||
<TargetFrameworks>net48;net8.0;net10.0</TargetFrameworks>
|
||||
<TargetFrameworks>net48;netstandard2.0;net8.0;net10.0</TargetFrameworks>
|
||||
<AssemblyName>SharpCompress</AssemblyName>
|
||||
<AssemblyOriginatorKeyFile>../../SharpCompress.snk</AssemblyOriginatorKeyFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
@@ -17,7 +17,7 @@
|
||||
<Copyright>Copyright (c) 2025 Adam Hathcock</Copyright>
|
||||
<GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
|
||||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
|
||||
<Description>SharpCompress is a compression library for NET 4.8/NET 8.0/NET 10.0 that can unrar, decompress 7zip, decompress xz, zip/unzip, tar/untar lzip/unlzip, bzip2/unbzip2 and gzip/ungzip with forward-only reading and file random access APIs. Write support for zip/tar/bzip2/gzip is implemented.</Description>
|
||||
<Description>SharpCompress is a compression library for NET 4.8/NET Standard 2.0/NET 8.0/NET 10.0 that can unrar, decompress 7zip, decompress xz, zip/unzip, tar/untar lzip/unlzip, bzip2/unbzip2 and gzip/ungzip with forward-only reading and file random access APIs. Write support for zip/tar/bzip2/gzip is implemented.</Description>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<DebugType>embedded</DebugType>
|
||||
@@ -30,25 +30,13 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net8.0' Or '$(TargetFramework)' == 'net10.0' ">
|
||||
<IsTrimmable>true</IsTrimmable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
|
||||
<DefineConstants>$(DefineConstants);DEBUG_STREAMS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0|AnyCPU'">
|
||||
<DefineConstants>$(DefineConstants);DEBUG_STREAMS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' Or '$(TargetFramework)' == 'net10.0' ">
|
||||
<PackageReference Include="Microsoft.NET.ILLink.Tasks" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net48' ">
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'netstandard2.0' ">
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" />
|
||||
<PackageReference Include="System.Buffers" />
|
||||
<PackageReference Include="System.Memory" />
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
|
||||
@@ -11,8 +11,6 @@ namespace SharpCompress;
|
||||
|
||||
internal static class Utility
|
||||
{
|
||||
//80kb is a good industry standard temporary buffer size
|
||||
private const int TEMP_BUFFER_SIZE = 81920;
|
||||
private static readonly HashSet<char> invalidChars = new(Path.GetInvalidFileNameChars());
|
||||
|
||||
public static ReadOnlyCollection<T> ToReadOnly<T>(this IList<T> items) => new(items);
|
||||
@@ -71,48 +69,16 @@ internal static class Utility
|
||||
return;
|
||||
}
|
||||
|
||||
using var buffer = MemoryPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
while (advanceAmount > 0)
|
||||
{
|
||||
var toRead = (int)Math.Min(buffer.Memory.Length, advanceAmount);
|
||||
var read = source.Read(buffer.Memory.Slice(0, toRead).Span);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
advanceAmount -= read;
|
||||
}
|
||||
using var readOnlySubStream = new IO.ReadOnlySubStream(source, advanceAmount);
|
||||
readOnlySubStream.CopyTo(Stream.Null);
|
||||
}
|
||||
|
||||
public static void Skip(this Stream source)
|
||||
{
|
||||
using var buffer = MemoryPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
while (source.Read(buffer.Memory.Span) > 0) { }
|
||||
}
|
||||
public static void Skip(this Stream source) => source.CopyTo(Stream.Null);
|
||||
|
||||
public static async Task SkipAsync(
|
||||
this Stream source,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
public static Task SkipAsync(this Stream source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var read = await source
|
||||
.ReadAsync(array, 0, array.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(array);
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return source.CopyToAsync(Stream.Null);
|
||||
}
|
||||
|
||||
public static DateTime DosDateToDateTime(ushort iDate, ushort iTime)
|
||||
@@ -183,7 +149,7 @@ internal static class Utility
|
||||
|
||||
public static long TransferTo(this Stream source, Stream destination, long maxLength)
|
||||
{
|
||||
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
var array = ArrayPool<byte>.Shared.Rent(Common.Constants.BufferSize);
|
||||
try
|
||||
{
|
||||
var maxReadSize = array.Length;
|
||||
@@ -222,7 +188,7 @@ internal static class Utility
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
var array = ArrayPool<byte>.Shared.Rent(Common.Constants.BufferSize);
|
||||
try
|
||||
{
|
||||
var maxReadSize = array.Length;
|
||||
@@ -300,7 +266,7 @@ internal static class Utility
|
||||
return;
|
||||
}
|
||||
|
||||
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
var array = ArrayPool<byte>.Shared.Rent(Common.Constants.BufferSize);
|
||||
try
|
||||
{
|
||||
while (advanceAmount > 0)
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class GZipWriter : AbstractWriter
|
||||
stream.FileName = filename;
|
||||
stream.LastModified = modificationTime;
|
||||
var progressStream = WrapWithProgress(source, filename);
|
||||
progressStream.CopyTo(stream);
|
||||
progressStream.CopyTo(stream, Constants.BufferSize);
|
||||
_wroteToStream = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.Compressors.PPMd;
|
||||
using SharpCompress.Compressors.ZStandard;
|
||||
using SharpCompress.IO;
|
||||
using Constants = SharpCompress.Common.Constants;
|
||||
|
||||
namespace SharpCompress.Writers.Zip;
|
||||
|
||||
@@ -87,7 +88,7 @@ public class ZipWriter : AbstractWriter
|
||||
{
|
||||
using var output = WriteToStream(entryPath, zipWriterEntryOptions);
|
||||
var progressStream = WrapWithProgress(source, entryPath);
|
||||
progressStream.CopyTo(output);
|
||||
progressStream.CopyTo(output, Constants.BufferSize);
|
||||
}
|
||||
|
||||
public Stream WriteToStream(string entryPath, ZipWriterEntryOptions options)
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"System.Buffers": {
|
||||
@@ -60,8 +60,8 @@
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net48": {
|
||||
"type": "Transitive",
|
||||
@@ -70,8 +70,8 @@
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
@@ -97,6 +97,110 @@
|
||||
"contentHash": "+RJT4qaekpZ7DDLhf+LTjq+E48jieKiY9ulJ+BoxKmZblIJfIJT8Ufcaa/clQqnYvWs8jugfGSMu8ylS0caG0w=="
|
||||
}
|
||||
},
|
||||
".NETStandard,Version=v2.0": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.0, )",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "vFuwSLj9QJBbNR0NeNO4YVASUbokxs+i/xbuu8B+Fs4FAZg5QaFa6eGrMaRqTzzNI5tAb97T7BhSxtLckFyiRA==",
|
||||
"dependencies": {
|
||||
"System.Threading.Tasks.Extensions": "4.6.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.3, )",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.6.1, )",
|
||||
"resolved": "4.6.1",
|
||||
"contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.6.3, )",
|
||||
"resolved": "4.6.3",
|
||||
"contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.6.1",
|
||||
"System.Numerics.Vectors": "4.6.1",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
|
||||
}
|
||||
},
|
||||
"System.Text.Encoding.CodePages": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.0, )",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "QLP54mIATaBpjGlsZIxga38VPk1G9js0Kw651B+bvrXi2kSgGZYrxJSpM3whhTZCBK4HEBHX3fzfDQMw7CXHGQ==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.6.3",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.6.1",
|
||||
"contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.1.2",
|
||||
"contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.6.3",
|
||||
"contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"net10.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
@@ -104,53 +208,81 @@
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
}
|
||||
},
|
||||
"net8.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.0, )",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA=="
|
||||
"requested": "[8.0.22, )",
|
||||
"resolved": "8.0.22",
|
||||
"contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,25 @@
|
||||
"JetBrains.Profiler.Api": "1.4.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"JetBrains.FormatRipper": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.4.0",
|
||||
@@ -33,6 +52,21 @@
|
||||
"JetBrains.HabitatDetector": "1.4.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
},
|
||||
"sharpcompress": {
|
||||
"type": "Project"
|
||||
}
|
||||
|
||||
61
tests/SharpCompress.Test/Ace/AceReaderTests.cs
Normal file
61
tests/SharpCompress.Test/Ace/AceReaderTests.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Ace;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Ace
|
||||
{
|
||||
public class AceReaderTests : ReaderTests
|
||||
{
|
||||
public AceReaderTests()
|
||||
{
|
||||
UseExtensionInsteadOfNameToVerify = true;
|
||||
UseCaseInsensitiveToVerify = true;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ace_Uncompressed_Read() => Read("Ace.store.ace", CompressionType.None);
|
||||
|
||||
[Fact]
|
||||
public void Ace_Encrypted_Read()
|
||||
{
|
||||
var exception = Assert.Throws<CryptographicException>(() => Read("Ace.encrypted.ace"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Ace.method1.ace", CompressionType.AceLZ77)]
|
||||
[InlineData("Ace.method1-solid.ace", CompressionType.AceLZ77)]
|
||||
[InlineData("Ace.method2.ace", CompressionType.AceLZ77)]
|
||||
[InlineData("Ace.method2-solid.ace", CompressionType.AceLZ77)]
|
||||
public void Ace_Unsupported_ShouldThrow(string fileName, CompressionType compressionType)
|
||||
{
|
||||
var exception = Assert.Throws<NotSupportedException>(() =>
|
||||
Read(fileName, compressionType)
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Ace.store.largefile.ace", CompressionType.None)]
|
||||
public void Ace_LargeFileTest_Read(string fileName, CompressionType compressionType)
|
||||
{
|
||||
ReadForBufferBoundaryCheck(fileName, compressionType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ace_Multi_Reader()
|
||||
{
|
||||
var exception = Assert.Throws<MultiVolumeExtractionException>(() =>
|
||||
DoMultiReader(
|
||||
["Ace.store.split.ace", "Ace.store.split.c01"],
|
||||
streams => AceReader.Open(streams)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,14 +45,17 @@ namespace SharpCompress.Test.Arj
|
||||
public void Arj_Multi_Reader()
|
||||
{
|
||||
var exception = Assert.Throws<MultiVolumeExtractionException>(() =>
|
||||
DoArj_Multi_Reader([
|
||||
"Arj.store.split.arj",
|
||||
"Arj.store.split.a01",
|
||||
"Arj.store.split.a02",
|
||||
"Arj.store.split.a03",
|
||||
"Arj.store.split.a04",
|
||||
"Arj.store.split.a05",
|
||||
])
|
||||
DoMultiReader(
|
||||
[
|
||||
"Arj.store.split.arj",
|
||||
"Arj.store.split.a01",
|
||||
"Arj.store.split.a02",
|
||||
"Arj.store.split.a03",
|
||||
"Arj.store.split.a04",
|
||||
"Arj.store.split.a05",
|
||||
],
|
||||
streams => ArjReader.Open(streams)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,26 +77,5 @@ namespace SharpCompress.Test.Arj
|
||||
{
|
||||
ReadForBufferBoundaryCheck(fileName, compressionType);
|
||||
}
|
||||
|
||||
private void DoArj_Multi_Reader(string[] archives)
|
||||
{
|
||||
using (
|
||||
var reader = ArjReader.Open(
|
||||
archives
|
||||
.Select(s => Path.Combine(TEST_ARCHIVES_PATH, s))
|
||||
.Select(p => File.OpenRead(p))
|
||||
)
|
||||
)
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
reader.WriteEntryToDirectory(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
VerifyFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
@@ -31,8 +31,8 @@ public class ForwardOnlyStream : SharpCompressStream, IStreamStack
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public ForwardOnlyStream(Stream stream, int bufferSize = ReaderOptions.DefaultBufferSize)
|
||||
: base(stream, bufferSize: bufferSize)
|
||||
public ForwardOnlyStream(Stream stream, int? bufferSize = null)
|
||||
: base(stream, bufferSize: bufferSize ?? Constants.BufferSize)
|
||||
{
|
||||
this.stream = stream;
|
||||
#if DEBUG_STREAMS
|
||||
|
||||
73
tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs
Normal file
73
tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// A stream wrapper that throws NotSupportedException on Flush() calls.
|
||||
/// This is used to test that archive iteration handles streams that don't support flushing.
|
||||
/// </summary>
|
||||
public class ThrowOnFlushStream : Stream
|
||||
{
|
||||
private readonly Stream inner;
|
||||
|
||||
public ThrowOnFlushStream(Stream inner)
|
||||
{
|
||||
this.inner = inner;
|
||||
}
|
||||
|
||||
public override bool CanRead => inner.CanRead;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush() => throw new NotSupportedException("Flush not supported");
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken) =>
|
||||
throw new NotSupportedException("FlushAsync not supported");
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
inner.Read(buffer, offset, count);
|
||||
|
||||
public override Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
) => inner.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
) => inner.ReadAsync(buffer, cancellationToken);
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
inner.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
65
tests/SharpCompress.Test/Mocks/TruncatedStream.cs
Normal file
65
tests/SharpCompress.Test/Mocks/TruncatedStream.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// A stream wrapper that truncates the underlying stream after reading a specified number of bytes.
|
||||
/// Used for testing error handling when streams end prematurely.
|
||||
/// </summary>
|
||||
public class TruncatedStream : Stream
|
||||
{
|
||||
private readonly Stream baseStream;
|
||||
private readonly long truncateAfterBytes;
|
||||
private long bytesRead;
|
||||
|
||||
public TruncatedStream(Stream baseStream, long truncateAfterBytes)
|
||||
{
|
||||
this.baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
|
||||
this.truncateAfterBytes = truncateAfterBytes;
|
||||
bytesRead = 0;
|
||||
}
|
||||
|
||||
public override bool CanRead => baseStream.CanRead;
|
||||
public override bool CanSeek => baseStream.CanSeek;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => baseStream.Length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => baseStream.Position;
|
||||
set => baseStream.Position = value;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (bytesRead >= truncateAfterBytes)
|
||||
{
|
||||
// Simulate premature end of stream
|
||||
return 0;
|
||||
}
|
||||
|
||||
var maxBytesToRead = (int)Math.Min(count, truncateAfterBytes - bytesRead);
|
||||
var actualBytesRead = baseStream.Read(buffer, offset, maxBytesToRead);
|
||||
bytesRead += actualBytesRead;
|
||||
return actualBytesRead;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => baseStream.Seek(offset, origin);
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public override void Flush() => baseStream.Flush();
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
baseStream?.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Archives;
|
||||
@@ -5,6 +6,7 @@ using SharpCompress.Archives.Rar;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Compressors.LZMA.Utilites;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Rar;
|
||||
@@ -642,4 +644,110 @@ public class RarArchiveTests : ArchiveTests
|
||||
);
|
||||
Assert.True(passwordProtectedFilesArchive.IsEncrypted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test for issue: InvalidOperationException when extracting RAR files.
|
||||
/// This test verifies the fix for the validation logic that was changed from
|
||||
/// (_position != Length) to (_position < Length).
|
||||
/// The old logic would throw an exception when position exceeded expected length,
|
||||
/// but the new logic only throws when decompression ends prematurely (position < expected).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rar_StreamValidation_OnlyThrowsOnPrematureEnd()
|
||||
{
|
||||
// Test normal extraction - should NOT throw InvalidOperationException
|
||||
// even if actual decompressed size differs from header
|
||||
var testFiles = new[] { "Rar.rar", "Rar5.rar", "Rar4.rar", "Rar2.rar" };
|
||||
|
||||
foreach (var testFile in testFiles)
|
||||
{
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));
|
||||
using var archive = RarArchive.Open(stream);
|
||||
|
||||
// Extract all entries and read them completely
|
||||
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var ms = new MemoryStream();
|
||||
|
||||
// This should complete without throwing InvalidOperationException
|
||||
// The fix ensures we only throw when position < expected length, not when position >= expected
|
||||
entryStream.CopyTo(ms);
|
||||
|
||||
// Verify we read some data
|
||||
Assert.True(
|
||||
ms.Length > 0,
|
||||
$"Failed to extract data from {entry.Key} in {testFile}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Negative test case: Verifies that InvalidOperationException IS thrown when
|
||||
/// a RAR stream ends prematurely (position < expected length).
|
||||
/// This tests the validation condition (_position < Length) works correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rar_StreamValidation_ThrowsOnTruncatedStream()
|
||||
{
|
||||
// This test verifies the exception is thrown when decompression ends prematurely
|
||||
// by using a truncated stream that stops reading after a small number of bytes
|
||||
var testFile = "Rar.rar";
|
||||
using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));
|
||||
|
||||
// Wrap the file stream with a truncated stream that will stop reading early
|
||||
// This simulates a corrupted or truncated RAR file
|
||||
using var truncatedStream = new TruncatedStream(fileStream, 1000);
|
||||
|
||||
// Opening the archive should work, but extracting should throw
|
||||
// when we try to read beyond the truncated data
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
using var archive = RarArchive.Open(truncatedStream);
|
||||
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var ms = new MemoryStream();
|
||||
// This should throw InvalidOperationException when it can't read all expected bytes
|
||||
entryStream.CopyTo(ms);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify the exception message matches our expectation
|
||||
Assert.Contains("unpacked file size does not match header", exception.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case for malformed RAR archives that previously caused infinite loops.
|
||||
/// This test verifies that attempting to read entries from a potentially malformed
|
||||
/// 512-byte RAR archive throws an InvalidOperationException instead of looping infinitely.
|
||||
/// See: https://github.com/adamhathcock/sharpcompress/issues/1176
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rar_MalformedArchive_NoInfiniteLoop()
|
||||
{
|
||||
var testFile = "Rar.malformed_512byte.rar";
|
||||
var readerOptions = new ReaderOptions { LookForHeader = true };
|
||||
|
||||
// This should throw InvalidOperationException, not hang in an infinite loop
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
using var fileStream = File.Open(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, testFile),
|
||||
FileMode.Open
|
||||
);
|
||||
using var archive = RarArchive.Open(fileStream, readerOptions);
|
||||
|
||||
// Attempting to enumerate entries should throw an exception
|
||||
// instead of looping infinitely
|
||||
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
|
||||
{
|
||||
// This line should not be reached due to the exception
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the exception is related to seeking beyond available data
|
||||
Assert.Contains("Cannot seek to position", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
@@ -220,4 +221,26 @@ public abstract class ReaderTests : TestBase
|
||||
Assert.Equal(expected.Pop(), reader.Entry.Key);
|
||||
}
|
||||
}
|
||||
|
||||
protected void DoMultiReader(
|
||||
string[] archives,
|
||||
Func<IEnumerable<Stream>, IDisposable> readerFactory
|
||||
)
|
||||
{
|
||||
using var reader = readerFactory(
|
||||
archives.Select(s => Path.Combine(TEST_ARCHIVES_PATH, s)).Select(File.OpenRead)
|
||||
);
|
||||
|
||||
dynamic dynReader = reader;
|
||||
|
||||
while (dynReader.MoveToNextEntry())
|
||||
{
|
||||
dynReader.WriteEntryToDirectory(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
|
||||
VerifyFiles();
|
||||
}
|
||||
}
|
||||
|
||||
139
tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs
Normal file
139
tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.SevenZip;
|
||||
|
||||
#if !NETFRAMEWORK
|
||||
public class SevenZipArchiveAsyncTests : ArchiveTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SevenZipArchive_LZMA_AsyncStreamExtraction()
|
||||
{
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA.7z");
|
||||
using var stream = File.OpenRead(testArchive);
|
||||
using var archive = ArchiveFactory.Open(stream);
|
||||
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
|
||||
var targetDir = Path.GetDirectoryName(targetPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
|
||||
{
|
||||
Directory.CreateDirectory(targetDir);
|
||||
}
|
||||
|
||||
using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
|
||||
await using var targetStream = File.Create(targetPath);
|
||||
await sourceStream.CopyToAsync(targetStream, CancellationToken.None);
|
||||
}
|
||||
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SevenZipArchive_LZMA2_AsyncStreamExtraction()
|
||||
{
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA2.7z");
|
||||
using var stream = File.OpenRead(testArchive);
|
||||
using var archive = ArchiveFactory.Open(stream);
|
||||
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
|
||||
var targetDir = Path.GetDirectoryName(targetPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
|
||||
{
|
||||
Directory.CreateDirectory(targetDir);
|
||||
}
|
||||
|
||||
using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
|
||||
await using var targetStream = File.Create(targetPath);
|
||||
await sourceStream.CopyToAsync(targetStream, CancellationToken.None);
|
||||
}
|
||||
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SevenZipArchive_Solid_AsyncStreamExtraction()
|
||||
{
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z");
|
||||
using var stream = File.OpenRead(testArchive);
|
||||
using var archive = ArchiveFactory.Open(stream);
|
||||
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
|
||||
var targetDir = Path.GetDirectoryName(targetPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
|
||||
{
|
||||
Directory.CreateDirectory(targetDir);
|
||||
}
|
||||
|
||||
using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
|
||||
await using var targetStream = File.Create(targetPath);
|
||||
await sourceStream.CopyToAsync(targetStream, CancellationToken.None);
|
||||
}
|
||||
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SevenZipArchive_BZip2_AsyncStreamExtraction()
|
||||
{
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.BZip2.7z");
|
||||
using var stream = File.OpenRead(testArchive);
|
||||
using var archive = ArchiveFactory.Open(stream);
|
||||
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
|
||||
var targetDir = Path.GetDirectoryName(targetPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
|
||||
{
|
||||
Directory.CreateDirectory(targetDir);
|
||||
}
|
||||
|
||||
using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
|
||||
await using var targetStream = File.Create(targetPath);
|
||||
await sourceStream.CopyToAsync(targetStream, CancellationToken.None);
|
||||
}
|
||||
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SevenZipArchive_PPMd_AsyncStreamExtraction()
|
||||
{
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.PPMd.7z");
|
||||
using var stream = File.OpenRead(testArchive);
|
||||
using var archive = ArchiveFactory.Open(stream);
|
||||
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
|
||||
var targetDir = Path.GetDirectoryName(targetPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
|
||||
{
|
||||
Directory.CreateDirectory(targetDir);
|
||||
}
|
||||
|
||||
using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
|
||||
await using var targetStream = File.Create(targetPath);
|
||||
await sourceStream.CopyToAsync(targetStream, CancellationToken.None);
|
||||
}
|
||||
|
||||
VerifyFiles();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -251,4 +251,98 @@ public class SevenZipArchiveTests : ArchiveTests
|
||||
);
|
||||
Assert.False(nonSolidArchive.IsSolid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SevenZipArchive_Solid_ExtractAllEntries_Contiguous()
|
||||
{
|
||||
// This test verifies that solid archives iterate entries as contiguous streams
|
||||
// rather than recreating the decompression stream for each entry
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z");
|
||||
using var archive = SevenZipArchive.Open(testArchive);
|
||||
Assert.True(archive.IsSolid);
|
||||
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
reader.WriteEntryToDirectory(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SevenZipArchive_Solid_VerifyStreamReuse()
|
||||
{
|
||||
// This test verifies that the folder stream is reused within each folder
|
||||
// and not recreated for each entry in solid archives
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z");
|
||||
using var archive = SevenZipArchive.Open(testArchive);
|
||||
Assert.True(archive.IsSolid);
|
||||
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
|
||||
var sevenZipReader = Assert.IsType<SevenZipArchive.SevenZipReader>(reader);
|
||||
sevenZipReader.DiagnosticsEnabled = true;
|
||||
|
||||
Stream? currentFolderStreamInstance = null;
|
||||
object? currentFolder = null;
|
||||
var entryCount = 0;
|
||||
var entriesInCurrentFolder = 0;
|
||||
var streamRecreationsWithinFolder = 0;
|
||||
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
// Extract the entry to trigger GetEntryStream
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
var buffer = new byte[4096];
|
||||
while (entryStream.Read(buffer, 0, buffer.Length) > 0)
|
||||
{
|
||||
// Read the stream to completion
|
||||
}
|
||||
|
||||
entryCount++;
|
||||
|
||||
var folderStream = sevenZipReader.DiagnosticsCurrentFolderStream;
|
||||
var folder = sevenZipReader.DiagnosticsCurrentFolder;
|
||||
|
||||
Assert.NotNull(folderStream); // Folder stream should exist
|
||||
|
||||
// Check if we're in a new folder
|
||||
if (currentFolder == null || !ReferenceEquals(currentFolder, folder))
|
||||
{
|
||||
// Starting a new folder
|
||||
currentFolder = folder;
|
||||
currentFolderStreamInstance = folderStream;
|
||||
entriesInCurrentFolder = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Same folder - verify stream wasn't recreated
|
||||
entriesInCurrentFolder++;
|
||||
|
||||
if (!ReferenceEquals(currentFolderStreamInstance, folderStream))
|
||||
{
|
||||
// Stream was recreated within the same folder - this is the bug we're testing for!
|
||||
streamRecreationsWithinFolder++;
|
||||
}
|
||||
|
||||
currentFolderStreamInstance = folderStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify we actually tested multiple entries
|
||||
Assert.True(entryCount > 1, "Test should have multiple entries to verify stream reuse");
|
||||
|
||||
// The critical check: within a single folder, the stream should NEVER be recreated
|
||||
Assert.Equal(0, streamRecreationsWithinFolder); // Folder stream should remain the same for all entries in the same folder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<AssemblyOriginatorKeyFile>SharpCompress.Test.snk</AssemblyOriginatorKeyFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0|AnyCPU'">
|
||||
<DefineConstants>$(DefineConstants);DEBUG_STREAMS</DefineConstants>
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net48' ">
|
||||
<DefineConstants>$(DefineConstants);LEGACY_DOTNET</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
|
||||
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
|
||||
@@ -23,9 +23,8 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(VersionlessImplicitFrameworkDefine)' != 'NETFRAMEWORK' ">
|
||||
<ItemGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
|
||||
<PackageReference Include="Mono.Posix.NETStandard" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
202
tests/SharpCompress.Test/Streams/DisposalTests.cs
Normal file
202
tests/SharpCompress.Test/Streams/DisposalTests.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Compressors;
|
||||
using SharpCompress.Compressors.BZip2;
|
||||
using SharpCompress.Compressors.Deflate;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.Compressors.Lzw;
|
||||
using SharpCompress.Compressors.PPMd;
|
||||
using SharpCompress.Compressors.Reduce;
|
||||
using SharpCompress.Compressors.ZStandard;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Streams;
|
||||
|
||||
public class DisposalTests
|
||||
{
|
||||
private void VerifyStreamDisposal(
|
||||
Func<Stream, bool, Stream> createStream,
|
||||
bool supportsLeaveOpen = true
|
||||
)
|
||||
{
|
||||
// 1. Test Dispose behavior (should dispose inner stream)
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
// createStream(stream, leaveOpen: false)
|
||||
var stream = createStream(innerStream, false);
|
||||
stream.Dispose();
|
||||
|
||||
// Some streams might not support disposal of inner stream (e.g. PpmdStream apparently)
|
||||
// But for those that satisfy the pattern, we assert true.
|
||||
Assert.True(
|
||||
innerStream.IsDisposed,
|
||||
"Stream should have been disposed when leaveOpen=false"
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Test LeaveOpen behavior (should NOT dispose inner stream)
|
||||
if (supportsLeaveOpen)
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
// createStream(stream, leaveOpen: true)
|
||||
var stream = createStream(innerStream, true);
|
||||
stream.Dispose();
|
||||
Assert.False(
|
||||
innerStream.IsDisposed,
|
||||
"Stream should NOT have been disposed when leaveOpen=true"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyAlwaysDispose(Func<Stream, Stream> createStream)
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
var stream = createStream(innerStream);
|
||||
stream.Dispose();
|
||||
Assert.True(innerStream.IsDisposed, "Stream should have been disposed (AlwaysDispose)");
|
||||
}
|
||||
|
||||
private void VerifyNeverDispose(Func<Stream, Stream> createStream)
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
var stream = createStream(innerStream);
|
||||
stream.Dispose();
|
||||
Assert.False(innerStream.IsDisposed, "Stream should NOT have been disposed (NeverDispose)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceStream_Disposal()
|
||||
{
|
||||
VerifyStreamDisposal(
|
||||
(stream, leaveOpen) =>
|
||||
new SourceStream(
|
||||
stream,
|
||||
i => null,
|
||||
new ReaderOptions { LeaveStreamOpen = leaveOpen }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProgressReportingStream_Disposal()
|
||||
{
|
||||
VerifyStreamDisposal(
|
||||
(stream, leaveOpen) =>
|
||||
new ProgressReportingStream(
|
||||
stream,
|
||||
new Progress<ProgressReport>(),
|
||||
"",
|
||||
0,
|
||||
leaveOpen: leaveOpen
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataDescriptorStream_Disposal()
|
||||
{
|
||||
// DataDescriptorStream DOES dispose inner stream
|
||||
VerifyAlwaysDispose(stream => new DataDescriptorStream(stream));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeflateStream_Disposal()
|
||||
{
|
||||
// DeflateStream in SharpCompress always disposes inner stream
|
||||
VerifyAlwaysDispose(stream => new DeflateStream(stream, CompressionMode.Compress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GZipStream_Disposal()
|
||||
{
|
||||
// GZipStream in SharpCompress always disposes inner stream
|
||||
VerifyAlwaysDispose(stream => new GZipStream(stream, CompressionMode.Compress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LzwStream_Disposal()
|
||||
{
|
||||
VerifyStreamDisposal(
|
||||
(stream, leaveOpen) =>
|
||||
{
|
||||
var lzw = new LzwStream(stream);
|
||||
lzw.IsStreamOwner = !leaveOpen;
|
||||
return lzw;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PpmdStream_Disposal()
|
||||
{
|
||||
// PpmdStream seems to not dispose inner stream based on code analysis
|
||||
// It takes PpmdProperties which we need to mock or create.
|
||||
var props = new PpmdProperties();
|
||||
VerifyNeverDispose(stream => new PpmdStream(props, stream, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LzmaStream_Disposal()
|
||||
{
|
||||
// LzmaStream always disposes inner stream
|
||||
// Need to provide valid properties to avoid crash in constructor (invalid window size)
|
||||
// 5 bytes: 1 byte properties + 4 bytes dictionary size (little endian)
|
||||
// Dictionary size = 1024 (0x400) -> 00 04 00 00
|
||||
var lzmaProps = new byte[] { 0, 0, 4, 0, 0 };
|
||||
VerifyAlwaysDispose(stream => new LzmaStream(lzmaProps, stream));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LZipStream_Disposal()
|
||||
{
|
||||
// LZipStream now supports leaveOpen parameter
|
||||
// Use Compress mode to avoid need for valid input header
|
||||
VerifyStreamDisposal(
|
||||
(stream, leaveOpen) => new LZipStream(stream, CompressionMode.Compress, leaveOpen)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BZip2Stream_Disposal()
|
||||
{
|
||||
// BZip2Stream now supports leaveOpen parameter
|
||||
VerifyStreamDisposal(
|
||||
(stream, leaveOpen) =>
|
||||
new BZip2Stream(stream, CompressionMode.Compress, false, leaveOpen)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReduceStream_Disposal()
|
||||
{
|
||||
// ReduceStream does not dispose inner stream
|
||||
VerifyNeverDispose(stream => new ReduceStream(stream, 0, 0, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZStandard_CompressionStream_Disposal()
|
||||
{
|
||||
VerifyStreamDisposal(
|
||||
(stream, leaveOpen) =>
|
||||
new CompressionStream(stream, level: 0, bufferSize: 0, leaveOpen: leaveOpen)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZStandard_DecompressionStream_Disposal()
|
||||
{
|
||||
VerifyStreamDisposal(
|
||||
(stream, leaveOpen) =>
|
||||
new DecompressionStream(
|
||||
stream,
|
||||
bufferSize: 0,
|
||||
checkEndOfStream: false,
|
||||
leaveOpen: leaveOpen
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
226
tests/SharpCompress.Test/Streams/LeaveOpenBehaviorTests.cs
Normal file
226
tests/SharpCompress.Test/Streams/LeaveOpenBehaviorTests.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SharpCompress.Compressors;
|
||||
using SharpCompress.Compressors.BZip2;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Streams;
|
||||
|
||||
public class LeaveOpenBehaviorTests
|
||||
{
|
||||
private static byte[] CreateTestData() =>
|
||||
Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");
|
||||
|
||||
[Fact]
|
||||
public void BZip2Stream_Compress_LeaveOpen_False()
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
using (
|
||||
var bzip2 = new BZip2Stream(
|
||||
innerStream,
|
||||
CompressionMode.Compress,
|
||||
false,
|
||||
leaveOpen: false
|
||||
)
|
||||
)
|
||||
{
|
||||
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
bzip2.Finish();
|
||||
}
|
||||
|
||||
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BZip2Stream_Compress_LeaveOpen_True()
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
byte[] compressed;
|
||||
using (
|
||||
var bzip2 = new BZip2Stream(
|
||||
innerStream,
|
||||
CompressionMode.Compress,
|
||||
false,
|
||||
leaveOpen: true
|
||||
)
|
||||
)
|
||||
{
|
||||
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
bzip2.Finish();
|
||||
}
|
||||
|
||||
Assert.False(
|
||||
innerStream.IsDisposed,
|
||||
"Inner stream should NOT be disposed when leaveOpen=true"
|
||||
);
|
||||
|
||||
// Should be able to read the compressed data
|
||||
innerStream.Position = 0;
|
||||
compressed = new byte[innerStream.Length];
|
||||
innerStream.Read(compressed, 0, compressed.Length);
|
||||
Assert.True(compressed.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BZip2Stream_Decompress_LeaveOpen_False()
|
||||
{
|
||||
// First compress some data
|
||||
var memStream = new MemoryStream();
|
||||
using (var bzip2 = new BZip2Stream(memStream, CompressionMode.Compress, false, true))
|
||||
{
|
||||
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
bzip2.Finish();
|
||||
}
|
||||
|
||||
memStream.Position = 0;
|
||||
using var innerStream = new TestStream(memStream);
|
||||
var decompressed = new byte[CreateTestData().Length];
|
||||
|
||||
using (
|
||||
var bzip2 = new BZip2Stream(
|
||||
innerStream,
|
||||
CompressionMode.Decompress,
|
||||
false,
|
||||
leaveOpen: false
|
||||
)
|
||||
)
|
||||
{
|
||||
bzip2.Read(decompressed, 0, decompressed.Length);
|
||||
}
|
||||
|
||||
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
|
||||
Assert.Equal(CreateTestData(), decompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BZip2Stream_Decompress_LeaveOpen_True()
|
||||
{
|
||||
// First compress some data
|
||||
var memStream = new MemoryStream();
|
||||
using (var bzip2 = new BZip2Stream(memStream, CompressionMode.Compress, false, true))
|
||||
{
|
||||
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
bzip2.Finish();
|
||||
}
|
||||
|
||||
memStream.Position = 0;
|
||||
using var innerStream = new TestStream(memStream);
|
||||
var decompressed = new byte[CreateTestData().Length];
|
||||
|
||||
using (
|
||||
var bzip2 = new BZip2Stream(
|
||||
innerStream,
|
||||
CompressionMode.Decompress,
|
||||
false,
|
||||
leaveOpen: true
|
||||
)
|
||||
)
|
||||
{
|
||||
bzip2.Read(decompressed, 0, decompressed.Length);
|
||||
}
|
||||
|
||||
Assert.False(
|
||||
innerStream.IsDisposed,
|
||||
"Inner stream should NOT be disposed when leaveOpen=true"
|
||||
);
|
||||
Assert.Equal(CreateTestData(), decompressed);
|
||||
|
||||
// Should still be able to use the stream
|
||||
innerStream.Position = 0;
|
||||
Assert.True(innerStream.CanRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LZipStream_Compress_LeaveOpen_False()
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
using (var lzip = new LZipStream(innerStream, CompressionMode.Compress, leaveOpen: false))
|
||||
{
|
||||
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
lzip.Finish();
|
||||
}
|
||||
|
||||
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LZipStream_Compress_LeaveOpen_True()
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
byte[] compressed;
|
||||
using (var lzip = new LZipStream(innerStream, CompressionMode.Compress, leaveOpen: true))
|
||||
{
|
||||
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
lzip.Finish();
|
||||
}
|
||||
|
||||
Assert.False(
|
||||
innerStream.IsDisposed,
|
||||
"Inner stream should NOT be disposed when leaveOpen=true"
|
||||
);
|
||||
|
||||
// Should be able to read the compressed data
|
||||
innerStream.Position = 0;
|
||||
compressed = new byte[innerStream.Length];
|
||||
innerStream.Read(compressed, 0, compressed.Length);
|
||||
Assert.True(compressed.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LZipStream_Decompress_LeaveOpen_False()
|
||||
{
|
||||
// First compress some data
|
||||
var memStream = new MemoryStream();
|
||||
using (var lzip = new LZipStream(memStream, CompressionMode.Compress, true))
|
||||
{
|
||||
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
lzip.Finish();
|
||||
}
|
||||
|
||||
memStream.Position = 0;
|
||||
using var innerStream = new TestStream(memStream);
|
||||
var decompressed = new byte[CreateTestData().Length];
|
||||
|
||||
using (var lzip = new LZipStream(innerStream, CompressionMode.Decompress, leaveOpen: false))
|
||||
{
|
||||
lzip.Read(decompressed, 0, decompressed.Length);
|
||||
}
|
||||
|
||||
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
|
||||
Assert.Equal(CreateTestData(), decompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LZipStream_Decompress_LeaveOpen_True()
|
||||
{
|
||||
// First compress some data
|
||||
var memStream = new MemoryStream();
|
||||
using (var lzip = new LZipStream(memStream, CompressionMode.Compress, true))
|
||||
{
|
||||
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
lzip.Finish();
|
||||
}
|
||||
|
||||
memStream.Position = 0;
|
||||
using var innerStream = new TestStream(memStream);
|
||||
var decompressed = new byte[CreateTestData().Length];
|
||||
|
||||
using (var lzip = new LZipStream(innerStream, CompressionMode.Decompress, leaveOpen: true))
|
||||
{
|
||||
lzip.Read(decompressed, 0, decompressed.Length);
|
||||
}
|
||||
|
||||
Assert.False(
|
||||
innerStream.IsDisposed,
|
||||
"Inner stream should NOT be disposed when leaveOpen=true"
|
||||
);
|
||||
Assert.Equal(CreateTestData(), decompressed);
|
||||
|
||||
// Should still be able to use the stream
|
||||
innerStream.Position = 0;
|
||||
Assert.True(innerStream.CanRead);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Streams;
|
||||
@@ -64,7 +65,14 @@ public class SharpCompressStreamTests
|
||||
{
|
||||
createData(ms);
|
||||
|
||||
using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000))
|
||||
using (
|
||||
SharpCompressStream scs = new SharpCompressStream(
|
||||
new ForwardOnlyStream(ms),
|
||||
true,
|
||||
false,
|
||||
0x10000
|
||||
)
|
||||
)
|
||||
{
|
||||
IStreamStack stack = (IStreamStack)scs;
|
||||
|
||||
@@ -89,4 +97,25 @@ public class SharpCompressStreamTests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BufferedSubStream_DoubleDispose_DoesNotCorruptArrayPool()
|
||||
{
|
||||
// This test verifies that calling Dispose multiple times on BufferedSubStream
|
||||
// doesn't return the same array to the pool twice, which would cause pool corruption
|
||||
byte[] data = new byte[0x10000];
|
||||
using (MemoryStream ms = new MemoryStream(data))
|
||||
{
|
||||
var stream = new BufferedSubStream(ms, 0, data.Length);
|
||||
|
||||
// First disposal
|
||||
stream.Dispose();
|
||||
|
||||
// Second disposal should not throw or corrupt the pool
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
// If we got here without an exception, the test passed
|
||||
Assert.True(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,4 +251,106 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
}
|
||||
Assert.Equal(8, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_Deflate_Async()
|
||||
{
|
||||
// Since version 0.41.0: EntryStream.DisposeAsync() should not throw NotSupportedException
|
||||
// when FlushAsync() fails on non-seekable streams (Deflate compression)
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
|
||||
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
// This should not throw, even if internal FlushAsync() fails
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#else
|
||||
await using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#endif
|
||||
// Read some data
|
||||
var buffer = new byte[1024];
|
||||
await entryStream.ReadAsync(buffer, 0, buffer.Length);
|
||||
// DisposeAsync should not throw NotSupportedException
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_LZMA_Async()
|
||||
{
|
||||
// Since version 0.41.0: EntryStream.DisposeAsync() should not throw NotSupportedException
|
||||
// when FlushAsync() fails on non-seekable streams (LZMA compression)
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
|
||||
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
// This should not throw, even if internal FlushAsync() fails
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#else
|
||||
await using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#endif
|
||||
// Read some data
|
||||
var buffer = new byte[1024];
|
||||
await entryStream.ReadAsync(buffer, 0, buffer.Length);
|
||||
// DisposeAsync should not throw NotSupportedException
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask Archive_Iteration_DoesNotBreak_WhenFlushThrows_Deflate_Async()
|
||||
{
|
||||
// Regression test: since 0.41.0, archive iteration would silently break
|
||||
// when the input stream throws NotSupportedException in Flush().
|
||||
// Only the first entry would be returned, then iteration would stop without exception.
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using Stream stream = new ThrowOnFlushStream(fileStream);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
var count = 0;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Should iterate through all entries, not just the first one
|
||||
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask Archive_Iteration_DoesNotBreak_WhenFlushThrows_LZMA_Async()
|
||||
{
|
||||
// Regression test: since 0.41.0, archive iteration would silently break
|
||||
// when the input stream throws NotSupportedException in Flush().
|
||||
// Only the first entry would be returned, then iteration would stop without exception.
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using Stream stream = new ThrowOnFlushStream(fileStream);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
var count = 0;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Should iterate through all entries, not just the first one
|
||||
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,4 +436,98 @@ public class ZipReaderTests : ReaderTests
|
||||
Assert.Equal(archiveKeys.OrderBy(k => k), readerKeys.OrderBy(k => k));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_Deflate()
|
||||
{
|
||||
// Since version 0.41.0: EntryStream.Dispose() should not throw NotSupportedException
|
||||
// when Flush() fails on non-seekable streams (Deflate compression)
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
|
||||
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
// This should not throw, even if internal Flush() fails
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
// Read some data
|
||||
var buffer = new byte[1024];
|
||||
entryStream.Read(buffer, 0, buffer.Length);
|
||||
// Dispose should not throw NotSupportedException
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_LZMA()
|
||||
{
|
||||
// Since version 0.41.0: EntryStream.Dispose() should not throw NotSupportedException
|
||||
// when Flush() fails on non-seekable streams (LZMA compression)
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
|
||||
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
// This should not throw, even if internal Flush() fails
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
// Read some data
|
||||
var buffer = new byte[1024];
|
||||
entryStream.Read(buffer, 0, buffer.Length);
|
||||
// Dispose should not throw NotSupportedException
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Archive_Iteration_DoesNotBreak_WhenFlushThrows_Deflate()
|
||||
{
|
||||
// Regression test: since 0.41.0, archive iteration would silently break
|
||||
// when the input stream throws NotSupportedException in Flush().
|
||||
// Only the first entry would be returned, then iteration would stop without exception.
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using Stream stream = new ThrowOnFlushStream(fileStream);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
var count = 0;
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Should iterate through all entries, not just the first one
|
||||
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Archive_Iteration_DoesNotBreak_WhenFlushThrows_LZMA()
|
||||
{
|
||||
// Regression test: since 0.41.0, archive iteration would silently break
|
||||
// when the input stream throws NotSupportedException in Flush().
|
||||
// Only the first entry would be returned, then iteration would stop without exception.
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using Stream stream = new ThrowOnFlushStream(fileStream);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
var count = 0;
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Should iterate through all entries, not just the first one
|
||||
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
|
||||
}
|
||||
}
|
||||
|
||||
117
tests/SharpCompress.Test/Zip/ZipShortReadTests.cs
Normal file
117
tests/SharpCompress.Test/Zip/ZipShortReadTests.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SharpCompress.Readers;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ZIP reading with streams that return short reads.
|
||||
/// Reproduces the regression where ZIP parsing fails depending on Stream.Read chunking patterns.
|
||||
/// </summary>
|
||||
public class ZipShortReadTests : ReaderTests
|
||||
{
|
||||
/// <summary>
|
||||
/// A non-seekable stream that returns controlled short reads.
|
||||
/// Simulates real-world network/multipart streams that legally return fewer bytes than requested.
|
||||
/// </summary>
|
||||
private sealed class PatternReadStream : Stream
|
||||
{
|
||||
private readonly MemoryStream _inner;
|
||||
private readonly int _firstReadSize;
|
||||
private readonly int _chunkSize;
|
||||
private bool _firstReadDone;
|
||||
|
||||
public PatternReadStream(byte[] bytes, int firstReadSize, int chunkSize)
|
||||
{
|
||||
_inner = new MemoryStream(bytes, writable: false);
|
||||
_firstReadSize = firstReadSize;
|
||||
_chunkSize = chunkSize;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
int limit = !_firstReadDone ? _firstReadSize : _chunkSize;
|
||||
_firstReadDone = true;
|
||||
|
||||
int toRead = Math.Min(count, limit);
|
||||
return _inner.Read(buffer, offset, toRead);
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => throw new NotSupportedException();
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush() => throw new NotSupportedException();
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that ZIP reading works correctly with short reads on non-seekable streams.
|
||||
/// Uses a test archive and different chunking patterns.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("Zip.deflate.zip", 1000, 4096)]
|
||||
[InlineData("Zip.deflate.zip", 999, 4096)]
|
||||
[InlineData("Zip.deflate.zip", 100, 4096)]
|
||||
[InlineData("Zip.deflate.zip", 50, 512)]
|
||||
[InlineData("Zip.deflate.zip", 1, 1)] // Extreme case: 1 byte at a time
|
||||
[InlineData("Zip.deflate.dd.zip", 1000, 4096)]
|
||||
[InlineData("Zip.deflate.dd.zip", 999, 4096)]
|
||||
[InlineData("Zip.zip64.zip", 3816, 4096)]
|
||||
[InlineData("Zip.zip64.zip", 3815, 4096)] // Similar to the issue pattern
|
||||
public void Zip_Reader_Handles_Short_Reads(string zipFile, int firstReadSize, int chunkSize)
|
||||
{
|
||||
// Use an existing test ZIP file
|
||||
var zipPath = Path.Combine(TEST_ARCHIVES_PATH, zipFile);
|
||||
if (!File.Exists(zipPath))
|
||||
{
|
||||
return; // Skip if file doesn't exist
|
||||
}
|
||||
|
||||
var bytes = File.ReadAllBytes(zipPath);
|
||||
|
||||
// Baseline with MemoryStream (seekable, no short reads)
|
||||
var baseline = ReadEntriesFromStream(new MemoryStream(bytes, writable: false));
|
||||
Assert.NotEmpty(baseline);
|
||||
|
||||
// Non-seekable stream with controlled short read pattern
|
||||
var chunked = ReadEntriesFromStream(new PatternReadStream(bytes, firstReadSize, chunkSize));
|
||||
Assert.Equal(baseline, chunked);
|
||||
}
|
||||
|
||||
private List<string> ReadEntriesFromStream(Stream stream)
|
||||
{
|
||||
var names = new List<string>();
|
||||
using var reader = ReaderFactory.Open(stream, new ReaderOptions { LeaveStreamOpen = true });
|
||||
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (reader.Entry.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
names.Add(reader.Entry.Key!);
|
||||
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
entryStream.CopyTo(Stream.Null);
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,16 @@
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"Mono.Posix.NETStandard": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.0, )",
|
||||
@@ -55,6 +65,11 @@
|
||||
"Microsoft.TestPlatform.ObjectModel": "17.13.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.CodeCoverage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "18.0.1",
|
||||
@@ -65,6 +80,11 @@
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
},
|
||||
"Microsoft.TestPlatform.ObjectModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.13.0",
|
||||
@@ -222,6 +242,16 @@
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"Mono.Posix.NETStandard": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.0, )",
|
||||
@@ -245,6 +275,11 @@
|
||||
"resolved": "3.1.5",
|
||||
"contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA=="
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.CodeCoverage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "18.0.1",
|
||||
@@ -255,6 +290,11 @@
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
},
|
||||
"Microsoft.TestPlatform.ObjectModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "18.0.1",
|
||||
|
||||
BIN
tests/TestArchives/Archives/Ace.encrypted.ace
Normal file
BIN
tests/TestArchives/Archives/Ace.encrypted.ace
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Ace.method1-solid.ace
Normal file
BIN
tests/TestArchives/Archives/Ace.method1-solid.ace
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Ace.method1.ace
Normal file
BIN
tests/TestArchives/Archives/Ace.method1.ace
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Ace.method2-solid.ace
Normal file
BIN
tests/TestArchives/Archives/Ace.method2-solid.ace
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Ace.method2.ace
Normal file
BIN
tests/TestArchives/Archives/Ace.method2.ace
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Ace.store.ace
Normal file
BIN
tests/TestArchives/Archives/Ace.store.ace
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Ace.store.largefile.ace
Normal file
BIN
tests/TestArchives/Archives/Ace.store.largefile.ace
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Ace.store.split.ace
Normal file
BIN
tests/TestArchives/Archives/Ace.store.split.ace
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Ace.store.split.c00
Normal file
BIN
tests/TestArchives/Archives/Ace.store.split.c00
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Rar.malformed_512byte.rar
Normal file
BIN
tests/TestArchives/Archives/Rar.malformed_512byte.rar
Normal file
Binary file not shown.
Reference in New Issue
Block a user