mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-04 05:25:00 +00:00
Compare commits
121 Commits
copilot/ad
...
release
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
72b3948f43 | ||
|
|
5d47bfaeb6 | ||
|
|
b2f2ea65ba | ||
|
|
7afa468e15 | ||
|
|
29cb1fed12 | ||
|
|
29f8b512c4 | ||
|
|
9794c8ba72 | ||
|
|
f1b305f682 | ||
|
|
091a800c9d | ||
|
|
975f5f4b4c | ||
|
|
9d6cd930ea | ||
|
|
d5913e8371 | ||
|
|
7f71f76f6e | ||
|
|
caa82a6146 | ||
|
|
bcf7137073 | ||
|
|
9238cf1128 | ||
|
|
2f874ace51 | ||
|
|
2feabed297 | ||
|
|
9001e28b36 | ||
|
|
24d651d7ae |
@@ -3,11 +3,11 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"commands": [
|
||||
"csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
155
.github/workflows/NUGET_RELEASE.md
vendored
Normal file
155
.github/workflows/NUGET_RELEASE.md
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
# NuGet Release Workflow
|
||||
|
||||
This document describes the automated NuGet release workflow for SharpCompress.
|
||||
|
||||
## Overview
|
||||
|
||||
The `nuget-release.yml` workflow automatically builds, tests, and publishes SharpCompress packages to NuGet.org when:
|
||||
- Changes are pushed to the `master` or `release` branch
|
||||
- A version tag (format: `MAJOR.MINOR.PATCH`) is pushed
|
||||
|
||||
The workflow runs on both Windows and Ubuntu, but only the Windows build publishes to NuGet.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Version Determination
|
||||
|
||||
The workflow automatically determines the version based on whether the commit is tagged using C# code in the build project:
|
||||
|
||||
1. **Tagged Release (Stable)**:
|
||||
- If the current commit has a version tag (e.g., `0.42.1`)
|
||||
- Uses the tag as the version number
|
||||
- Published as a stable release
|
||||
|
||||
2. **Untagged Release (Prerelease)**:
|
||||
- If the current commit is NOT tagged
|
||||
- Creates a prerelease version based on the next minor version
|
||||
- Format: `{NEXT_MINOR_VERSION}-beta.{COMMIT_COUNT}`
|
||||
- Example: `0.43.0-beta.123` (if last tag is 0.42.x)
|
||||
- Published as a prerelease to NuGet.org (Windows build only)
|
||||
|
||||
### Workflow Steps
|
||||
|
||||
The workflow runs on a matrix of operating systems (Windows and Ubuntu):
|
||||
|
||||
1. **Checkout**: Fetches the repository with full history for version detection
|
||||
2. **Setup .NET**: Installs .NET 10.0
|
||||
3. **Determine Version**: Runs `determine-version` build target to check for tags and determine version
|
||||
4. **Update Version**: Runs `update-version` build target to update the version in the project file
|
||||
5. **Build and Test**: Runs the full build and test suite on both platforms
|
||||
6. **Upload Artifacts**: Uploads the generated `.nupkg` files as workflow artifacts (separate for each OS)
|
||||
7. **Push to NuGet**: (Windows only) Runs `push-to-nuget` build target to publish the package to NuGet.org using the API key
|
||||
|
||||
All version detection, file updates, and publishing logic is implemented in C# in the `build/Program.cs` file using build targets.
|
||||
|
||||
## Setup Requirements
|
||||
|
||||
### 1. NuGet API Key Secret
|
||||
|
||||
The workflow requires a `NUGET_API_KEY` secret to be configured in the repository settings:
|
||||
|
||||
1. Go to https://www.nuget.org/account/apikeys
|
||||
2. Create a new API key with "Push" permission for the SharpCompress package
|
||||
3. In GitHub, go to: **Settings** → **Secrets and variables** → **Actions**
|
||||
4. Create a new secret named `NUGET_API_KEY` with the API key value
|
||||
|
||||
### 2. Branch Protection (Recommended)
|
||||
|
||||
Consider enabling branch protection rules for the `release` branch to ensure:
|
||||
- Code reviews are required before merging
|
||||
- Status checks pass before merging
|
||||
- Only authorized users can push to the branch
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Stable Release
|
||||
|
||||
There are two ways to trigger a stable release:
|
||||
|
||||
**Method 1: Push tag to trigger workflow**
|
||||
1. Ensure all changes are committed on the `master` or `release` branch
|
||||
2. Create and push a version tag:
|
||||
```bash
|
||||
git checkout master # or release
|
||||
git tag 0.43.0
|
||||
git push origin 0.43.0
|
||||
```
|
||||
3. The workflow will automatically trigger, build, test, and publish `SharpCompress 0.43.0` to NuGet.org (Windows build)
|
||||
|
||||
**Method 2: Tag after pushing to branch**
|
||||
1. Ensure all changes are merged and pushed to the `master` or `release` branch
|
||||
2. Create and push a version tag on the already-pushed commit:
|
||||
```bash
|
||||
git checkout master # or release
|
||||
git tag 0.43.0
|
||||
git push origin 0.43.0
|
||||
```
|
||||
3. The workflow will automatically trigger, build, test, and publish `SharpCompress 0.43.0` to NuGet.org (Windows build)
|
||||
|
||||
### Creating a Prerelease
|
||||
|
||||
1. Push changes to the `master` or `release` branch without tagging:
|
||||
```bash
|
||||
git checkout master # or release
|
||||
git push origin master # or release
|
||||
```
|
||||
2. The workflow will automatically:
|
||||
- Build and test the project on both Windows and Ubuntu
|
||||
- Publish a prerelease version like `0.43.0-beta.456` to NuGet.org (Windows build)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Workflow Fails to Push to NuGet
|
||||
|
||||
- **Check the API Key**: Ensure `NUGET_API_KEY` is set correctly in repository secrets
|
||||
- **Check API Key Permissions**: Verify the API key has "Push" permission for SharpCompress
|
||||
- **Check API Key Expiration**: NuGet API keys may expire; create a new one if needed
|
||||
|
||||
### Version Conflict
|
||||
|
||||
If you see "Package already exists" errors:
|
||||
- The workflow uses `--skip-duplicate` flag to handle this gracefully
|
||||
- If you need to republish the same version, delete it from NuGet.org first (if allowed)
|
||||
|
||||
### Build or Test Failures
|
||||
|
||||
- The workflow will not push to NuGet if build or tests fail
|
||||
- Check the workflow logs in GitHub Actions for details
|
||||
- Fix the issues and push again
|
||||
|
||||
## Manual Package Creation
|
||||
|
||||
If you need to create a package manually without publishing:
|
||||
|
||||
```bash
|
||||
dotnet run --project build/build.csproj -- publish
|
||||
```
|
||||
|
||||
The package will be created in the `artifacts/` directory.
|
||||
|
||||
## Build Targets
|
||||
|
||||
The workflow uses the following C# build targets defined in `build/Program.cs`:
|
||||
|
||||
- **determine-version**: Detects version from git tags and outputs VERSION and PRERELEASE variables
|
||||
- **update-version**: Updates VersionPrefix, AssemblyVersion, and FileVersion in the project file
|
||||
- **push-to-nuget**: Pushes the generated NuGet packages to NuGet.org (requires NUGET_API_KEY)
|
||||
|
||||
These targets can be run manually for testing:
|
||||
|
||||
```bash
|
||||
# Determine the version
|
||||
dotnet run --project build/build.csproj -- determine-version
|
||||
|
||||
# Update version in project file
|
||||
VERSION=0.43.0 dotnet run --project build/build.csproj -- update-version
|
||||
|
||||
# Push to NuGet (requires NUGET_API_KEY environment variable)
|
||||
NUGET_API_KEY=your-key dotnet run --project build/build.csproj -- push-to-nuget
|
||||
```
|
||||
|
||||
## Related Files
|
||||
|
||||
- `.github/workflows/nuget-release.yml` - The workflow definition
|
||||
- `build/Program.cs` - Build script with version detection and publishing logic
|
||||
- `src/SharpCompress/SharpCompress.csproj` - Project file with version information
|
||||
120
.github/workflows/TESTING.md
vendored
Normal file
120
.github/workflows/TESTING.md
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
# Testing Guide for NuGet Release Workflow
|
||||
|
||||
This document describes how to test the NuGet release workflow.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Since this workflow publishes to NuGet.org and requires repository secrets, testing should be done carefully. The workflow runs on both Windows and Ubuntu, but only the Windows build publishes to NuGet.
|
||||
|
||||
## Pre-Testing Checklist
|
||||
|
||||
- [x] Workflow YAML syntax validated
|
||||
- [x] Version determination logic tested locally
|
||||
- [x] Version update logic tested locally
|
||||
- [x] Build script works (`dotnet run --project build/build.csproj`)
|
||||
|
||||
## Manual Testing Steps
|
||||
|
||||
### 1. Test Prerelease Publishing (Recommended First Test)
|
||||
|
||||
This tests the workflow on untagged commits to the master or release branch.
|
||||
|
||||
**Steps:**
|
||||
1. Ensure `NUGET_API_KEY` secret is configured in repository settings
|
||||
2. Create a test commit on the `master` or `release` branch (e.g., update a comment or README)
|
||||
3. Push to the `master` or `release` branch
|
||||
4. Monitor the GitHub Actions workflow at: https://github.com/adamhathcock/sharpcompress/actions
|
||||
5. Verify:
|
||||
- Workflow triggers and runs successfully on both Windows and Ubuntu
|
||||
- Version is determined correctly (e.g., `0.43.0-beta.XXX` if last tag is 0.42.x)
|
||||
- Build and tests pass on both platforms
|
||||
- Package artifacts are uploaded for both platforms
|
||||
- Package is pushed to NuGet.org as prerelease (Windows build only)
|
||||
|
||||
**Expected Outcome:**
|
||||
- A new prerelease package appears on NuGet.org: https://www.nuget.org/packages/SharpCompress/
|
||||
- Package version follows pattern: `{NEXT_MINOR_VERSION}-beta.{COMMIT_COUNT}`
|
||||
|
||||
### 2. Test Tagged Release Publishing
|
||||
|
||||
This tests the workflow when a version tag is pushed.
|
||||
|
||||
**Steps:**
|
||||
1. Prepare the `master` or `release` branch with all desired changes
|
||||
2. Create a version tag (must be a pure semantic version like `MAJOR.MINOR.PATCH`):
|
||||
```bash
|
||||
git checkout master # or release
|
||||
git tag 0.42.2
|
||||
git push origin 0.42.2
|
||||
```
|
||||
3. Monitor the GitHub Actions workflow
|
||||
4. Verify:
|
||||
- Workflow triggers and runs successfully on both Windows and Ubuntu
|
||||
- Version is determined as the tag (e.g., `0.42.2`)
|
||||
- Build and tests pass on both platforms
|
||||
- Package artifacts are uploaded for both platforms
|
||||
- Package is pushed to NuGet.org as stable release (Windows build only)
|
||||
|
||||
**Expected Outcome:**
|
||||
- A new stable release package appears on NuGet.org
|
||||
- Package version matches the tag
|
||||
|
||||
### 3. Test Duplicate Package Handling
|
||||
|
||||
This tests the `--skip-duplicate` flag behavior.
|
||||
|
||||
**Steps:**
|
||||
1. Push to the `release` branch without making changes
|
||||
2. Monitor the workflow
|
||||
3. Verify:
|
||||
- Workflow runs but NuGet push is skipped with "duplicate" message
|
||||
- No errors occur
|
||||
|
||||
### 4. Test Build Failure Handling
|
||||
|
||||
This tests that failed builds don't publish packages.
|
||||
|
||||
**Steps:**
|
||||
1. Introduce a breaking change in a test or code
|
||||
2. Push to the `release` branch
|
||||
3. Verify:
|
||||
- Workflow runs and detects the failure
|
||||
- Build or test step fails
|
||||
- NuGet push step is skipped
|
||||
- No package is published
|
||||
|
||||
## Verification
|
||||
|
||||
After each test, verify:
|
||||
|
||||
1. **GitHub Actions Logs**: Check the workflow logs for any errors or warnings
|
||||
2. **NuGet.org**: Verify the package appears with correct version and metadata
|
||||
3. **Artifacts**: Download and inspect the uploaded artifacts
|
||||
|
||||
## Rollback/Cleanup
|
||||
|
||||
If testing produces unwanted packages:
|
||||
|
||||
1. **Prerelease packages**: Can be unlisted on NuGet.org (Settings → Unlist)
|
||||
2. **Stable packages**: Cannot be deleted, only unlisted (use test versions)
|
||||
3. **Tags**: Can be deleted with:
|
||||
```bash
|
||||
git tag -d 0.42.2
|
||||
git push origin :refs/tags/0.42.2
|
||||
```
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- NuGet.org does not allow re-uploading the same version
|
||||
- Deleted packages on NuGet.org reserve the version number
|
||||
- The workflow requires the `NUGET_API_KEY` secret to be set
|
||||
|
||||
## Success Criteria
|
||||
|
||||
The workflow is considered successful if:
|
||||
|
||||
- ✅ Prerelease versions are published correctly with beta suffix
|
||||
- ✅ Tagged versions are published as stable releases
|
||||
- ✅ Build and test failures prevent publishing
|
||||
- ✅ Duplicate packages are handled gracefully
|
||||
- ✅ Workflow logs are clear and informative
|
||||
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/*
|
||||
61
.github/workflows/nuget-release.yml
vendored
Normal file
61
.github/workflows/nuget-release.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: NuGet Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release'
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for versioning
|
||||
|
||||
- uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
# Determine version using C# build target
|
||||
- name: Determine Version
|
||||
id: version
|
||||
run: dotnet run --project build/build.csproj -- determine-version
|
||||
|
||||
# Update version in project file using C# build target
|
||||
- name: Update Version in Project
|
||||
run: dotnet run --project build/build.csproj -- update-version
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
|
||||
# Build and test
|
||||
- name: Build and Test
|
||||
run: dotnet run --project build/build.csproj
|
||||
|
||||
# Upload artifacts for verification
|
||||
- name: Upload NuGet Package
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-nuget-package
|
||||
path: artifacts/*.nupkg
|
||||
|
||||
# Push to NuGet.org using C# build target (Windows only, not on PRs)
|
||||
- name: Push to NuGet
|
||||
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 }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,6 +16,7 @@ tests/TestArchives/*/Scratch2
|
||||
.vs
|
||||
tools
|
||||
.idea/
|
||||
artifacts/
|
||||
|
||||
.DS_Store
|
||||
*.snupkg
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
200
build/Program.cs
200
build/Program.cs
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using GlobExpressions;
|
||||
using static Bullseye.Targets;
|
||||
using static SimpleExec.Command;
|
||||
@@ -13,6 +16,9 @@ const string Test = "test";
|
||||
const string Format = "format";
|
||||
const string CheckFormat = "check-format";
|
||||
const string Publish = "publish";
|
||||
const string DetermineVersion = "determine-version";
|
||||
const string UpdateVersion = "update-version";
|
||||
const string PushToNuGet = "push-to-nuget";
|
||||
|
||||
Target(
|
||||
Clean,
|
||||
@@ -99,6 +105,200 @@ Target(
|
||||
}
|
||||
);
|
||||
|
||||
Target(
|
||||
DetermineVersion,
|
||||
async () =>
|
||||
{
|
||||
var (version, isPrerelease) = await GetVersion();
|
||||
Console.WriteLine($"VERSION={version}");
|
||||
Console.WriteLine($"PRERELEASE={isPrerelease.ToString().ToLower()}");
|
||||
|
||||
// Write to environment file for GitHub Actions
|
||||
var githubOutput = Environment.GetEnvironmentVariable("GITHUB_OUTPUT");
|
||||
if (!string.IsNullOrEmpty(githubOutput))
|
||||
{
|
||||
File.AppendAllText(githubOutput, $"version={version}\n");
|
||||
File.AppendAllText(githubOutput, $"prerelease={isPrerelease.ToString().ToLower()}\n");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Target(
|
||||
UpdateVersion,
|
||||
async () =>
|
||||
{
|
||||
var version = Environment.GetEnvironmentVariable("VERSION");
|
||||
if (string.IsNullOrEmpty(version))
|
||||
{
|
||||
var (detectedVersion, _) = await GetVersion();
|
||||
version = detectedVersion;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Updating project file with version: {version}");
|
||||
|
||||
var projectPath = "src/SharpCompress/SharpCompress.csproj";
|
||||
var content = File.ReadAllText(projectPath);
|
||||
|
||||
// Get base version (without prerelease suffix)
|
||||
var baseVersion = version.Split('-')[0];
|
||||
|
||||
// Update VersionPrefix
|
||||
content = Regex.Replace(
|
||||
content,
|
||||
@"<VersionPrefix>[^<]*</VersionPrefix>",
|
||||
$"<VersionPrefix>{version}</VersionPrefix>"
|
||||
);
|
||||
|
||||
// Update AssemblyVersion
|
||||
content = Regex.Replace(
|
||||
content,
|
||||
@"<AssemblyVersion>[^<]*</AssemblyVersion>",
|
||||
$"<AssemblyVersion>{baseVersion}</AssemblyVersion>"
|
||||
);
|
||||
|
||||
// Update FileVersion
|
||||
content = Regex.Replace(
|
||||
content,
|
||||
@"<FileVersion>[^<]*</FileVersion>",
|
||||
$"<FileVersion>{baseVersion}</FileVersion>"
|
||||
);
|
||||
|
||||
File.WriteAllText(projectPath, content);
|
||||
Console.WriteLine($"Updated VersionPrefix to: {version}");
|
||||
Console.WriteLine($"Updated AssemblyVersion and FileVersion to: {baseVersion}");
|
||||
}
|
||||
);
|
||||
|
||||
Target(
|
||||
PushToNuGet,
|
||||
() =>
|
||||
{
|
||||
var apiKey = Environment.GetEnvironmentVariable("NUGET_API_KEY");
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"NUGET_API_KEY environment variable is not set. Skipping NuGet push."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var packages = Directory.GetFiles("artifacts", "*.nupkg");
|
||||
if (packages.Length == 0)
|
||||
{
|
||||
Console.WriteLine("No packages found in artifacts directory.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var package in packages)
|
||||
{
|
||||
Console.WriteLine($"Pushing {package} to NuGet.org");
|
||||
try
|
||||
{
|
||||
// Note: API key is passed via command line argument which is standard practice for dotnet nuget push
|
||||
// The key is already in an environment variable and not displayed in normal output
|
||||
Run(
|
||||
"dotnet",
|
||||
$"nuget push \"{package}\" --api-key {apiKey} --source https://api.nuget.org/v3/index.json --skip-duplicate"
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to push {package}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Target("default", [Publish], () => Console.WriteLine("Done!"));
|
||||
|
||||
await RunTargetsAndExitAsync(args);
|
||||
|
||||
static async Task<(string version, bool isPrerelease)> GetVersion()
|
||||
{
|
||||
// Check if current commit has a version tag
|
||||
var currentTag = (await GetGitOutput("tag", "--points-at HEAD"))
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault(tag => Regex.IsMatch(tag.Trim(), @"^\d+\.\d+\.\d+$"));
|
||||
|
||||
if (!string.IsNullOrEmpty(currentTag))
|
||||
{
|
||||
// Tagged release - use the tag as version
|
||||
var version = currentTag.Trim();
|
||||
Console.WriteLine($"Building tagged release version: {version}");
|
||||
return (version, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not tagged - create prerelease version
|
||||
var allTags = (await GetGitOutput("tag", "--list"))
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(tag => Regex.IsMatch(tag.Trim(), @"^\d+\.\d+\.\d+$"))
|
||||
.Select(tag => tag.Trim())
|
||||
.ToList();
|
||||
|
||||
var lastTag = allTags.OrderBy(tag => Version.Parse(tag)).LastOrDefault() ?? "0.0.0";
|
||||
var lastVersion = Version.Parse(lastTag);
|
||||
|
||||
// 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";
|
||||
var commitCount = (await GetGitOutput("rev-list", revListArgs)).Trim();
|
||||
|
||||
var version = $"{nextVersion}-beta.{commitCount}";
|
||||
Console.WriteLine($"Building prerelease version: {version}");
|
||||
return (version, true);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
// Use SimpleExec's Read to execute git commands in a cross-platform way
|
||||
var (output, _) = await ReadAsync("git", $"{command} {args}");
|
||||
return output;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Git command failed: git {command} {args}\n{ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 @@ public class LzmaStream : Stream, IStreamStack
|
||||
private readonly Stream _inputStream;
|
||||
private readonly long _inputSize;
|
||||
private readonly long _outputSize;
|
||||
private readonly bool _leaveOpen;
|
||||
|
||||
private readonly int _dictionarySize;
|
||||
private readonly OutWindow _outWindow = new();
|
||||
@@ -56,14 +57,28 @@ 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) { }
|
||||
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)
|
||||
: this(properties, inputStream, inputSize, -1, null, properties.Length < 5) { }
|
||||
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)
|
||||
: this(properties, inputStream, inputSize, outputSize, null, properties.Length < 5) { }
|
||||
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 +86,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));
|
||||
@@ -179,7 +196,10 @@ public class LzmaStream : Stream, IStreamStack
|
||||
{
|
||||
_position = _encoder.Code(null, true);
|
||||
}
|
||||
_inputStream?.Dispose();
|
||||
if (!_leaveOpen)
|
||||
{
|
||||
_inputStream?.Dispose();
|
||||
}
|
||||
_outWindow.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
@@ -425,7 +445,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 +652,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,60 +97,192 @@
|
||||
"contentHash": "+RJT4qaekpZ7DDLhf+LTjq+E48jieKiY9ulJ+BoxKmZblIJfIJT8Ufcaa/clQqnYvWs8jugfGSMu8ylS0caG0w=="
|
||||
}
|
||||
},
|
||||
"net10.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
".NETStandard,Version=v2.0": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.0, )",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA=="
|
||||
"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": "[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"
|
||||
}
|
||||
},
|
||||
"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": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
"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": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
"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",
|
||||
"requested": "[10.0.2, )",
|
||||
"resolved": "10.0.2",
|
||||
"contentHash": "sXdDtMf2qcnbygw9OdE535c2lxSxrZP8gO4UhDJ0xiJbl1wIqXS1OTcTDFTIJPOFd6Mhcm8gPEthqWGUxBsTqw=="
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
}
|
||||
},
|
||||
"net8.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.0, )",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA=="
|
||||
"requested": "[8.0.23, )",
|
||||
"resolved": "8.0.23",
|
||||
"contentHash": "GqHiB1HbbODWPbY/lc5xLQH8siEEhNA0ptpJCC6X6adtAYNEzu5ZlqV3YHA3Gh7fuEwgA8XqVwMtH2KNtuQM1Q=="
|
||||
},
|
||||
"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