mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-05 13:34:59 +00:00
Compare commits
315 Commits
copilot/fi
...
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 | ||
|
|
5667595587 | ||
|
|
32d5b61c4a | ||
|
|
128c9e639f | ||
|
|
5e3f01dc03 | ||
|
|
c97c05a3a7 | ||
|
|
b2beea9c4e | ||
|
|
41fbaa1c28 | ||
|
|
d9274cf794 | ||
|
|
583b048046 | ||
|
|
ead5916eae | ||
|
|
d15ab92da3 | ||
|
|
1ab30f2af5 | ||
|
|
4dbe0b91f1 | ||
|
|
a972d3784e | ||
|
|
6991900eb0 | ||
|
|
d614beb9eb | ||
|
|
253a46d458 | ||
|
|
32b1ec32c6 | ||
|
|
eb2cba09b2 | ||
|
|
e79dceb67e | ||
|
|
87c38d6dab | ||
|
|
9e98d9c45c | ||
|
|
0e9a4b0511 | ||
|
|
eae25aff64 | ||
|
|
b8c06ff36e | ||
|
|
6cf2e054bf | ||
|
|
95749234f5 | ||
|
|
b976961434 | ||
|
|
e1aa727513 | ||
|
|
1f71ce1be2 | ||
|
|
cf13de6ac1 | ||
|
|
c2e01798f8 | ||
|
|
8fc8295a89 | ||
|
|
d392991764 | ||
|
|
e57e87090f | ||
|
|
c701bbbee3 | ||
|
|
2f0eb0bd4b | ||
|
|
17bde8da8a | ||
|
|
99d355e6ca | ||
|
|
c790fd21a4 | ||
|
|
bee51af48b | ||
|
|
ca743eae22 | ||
|
|
93504cf82f | ||
|
|
6d3e4e842b | ||
|
|
54b64a8c3b | ||
|
|
0e59bf39f4 | ||
|
|
8b95e0a76d | ||
|
|
48a2ad7b57 | ||
|
|
cfc6651fff | ||
|
|
b23827a8db | ||
|
|
3f9986c13c | ||
|
|
224989f19b | ||
|
|
c7010b75c1 | ||
|
|
00cfeee56e | ||
|
|
aaa97e2ce2 | ||
|
|
1d52618137 | ||
|
|
34309f17f4 | ||
|
|
220ba67faa | ||
|
|
230f96e8e8 | ||
|
|
930c8899d2 | ||
|
|
7c0cef7dd8 | ||
|
|
951ebb3fa2 | ||
|
|
2a4d098b41 | ||
|
|
5839b87f98 | ||
|
|
425a2bd680 | ||
|
|
939c2497c8 | ||
|
|
8995ba56b8 | ||
|
|
e941ab60ca | ||
|
|
48860f1349 | ||
|
|
94b2c5c593 | ||
|
|
f1d8ae5a22 | ||
|
|
e44d2093e5 | ||
|
|
9fa1201a4c | ||
|
|
7800808648 | ||
|
|
2789e86d21 | ||
|
|
afd1e39b88 | ||
|
|
aec4c738ef | ||
|
|
63ecc8c842 | ||
|
|
5f6d583521 | ||
|
|
0341984f10 | ||
|
|
fe757486ae | ||
|
|
46d480c9a1 | ||
|
|
8fe3cba7a8 | ||
|
|
aa19f4da8b | ||
|
|
a08f95326c | ||
|
|
b4c5437c92 | ||
|
|
8680e3b39e | ||
|
|
1b3002c8df | ||
|
|
394fd2e7db | ||
|
|
d83af56d28 | ||
|
|
28c93d6841 | ||
|
|
5f52fc2176 | ||
|
|
8fba579e3a | ||
|
|
40b1aadeb2 | ||
|
|
40e72ad199 | ||
|
|
618b4bbb83 | ||
|
|
1eaf3e6294 | ||
|
|
fd453e946d | ||
|
|
c294071015 | ||
|
|
c2f6055e33 | ||
|
|
5161f4df33 | ||
|
|
3396f8fe00 | ||
|
|
9291f58091 | ||
|
|
85f3b17c42 | ||
|
|
2a3086a0d7 | ||
|
|
41c3cc1a18 | ||
|
|
1b1df86a11 | ||
|
|
e0660e7775 | ||
|
|
99a6c4de88 | ||
|
|
ffa765bd97 | ||
|
|
b1696524b3 | ||
|
|
14d432e22d | ||
|
|
6a37c55085 | ||
|
|
9c1c6fff9f | ||
|
|
db8c6f4bcb | ||
|
|
ff17ecda7d | ||
|
|
692058677c | ||
|
|
1e90d69912 | ||
|
|
64a1cc68e1 | ||
|
|
0fdf9c74a8 | ||
|
|
20353f35ff | ||
|
|
e2df7894f9 | ||
|
|
7af029b5de | ||
|
|
8fc5ca5a71 | ||
|
|
aa0356de9f | ||
|
|
e44a43d2b1 | ||
|
|
8997f00b9b | ||
|
|
c5da416764 | ||
|
|
840e58fc03 | ||
|
|
7f911c5219 | ||
|
|
a887390c23 | ||
|
|
f4dddcec8e | ||
|
|
0d9d82d7e6 | ||
|
|
3a6d24b1d9 | ||
|
|
b9b159be4c | ||
|
|
40212083a5 | ||
|
|
d3428b066e | ||
|
|
94c64b2a45 | ||
|
|
0d671a0bb2 | ||
|
|
d34a47c148 | ||
|
|
5aa216bd21 | ||
|
|
8af47548fe | ||
|
|
131bd2b7b8 | ||
|
|
1993673a22 | ||
|
|
30e036f9ec | ||
|
|
0f374b27cf | ||
|
|
0d487df61b | ||
|
|
c082d4203b | ||
|
|
d4380b6bb6 | ||
|
|
095c871174 | ||
|
|
6d73c5b295 | ||
|
|
cc4d28193c | ||
|
|
9433e06b93 | ||
|
|
a92aaa51d5 | ||
|
|
d41908adeb | ||
|
|
81ca15b567 | ||
|
|
b81d0fd730 | ||
|
|
3a1bb187e8 | ||
|
|
3fee14a070 | ||
|
|
5bf789ac65 | ||
|
|
be06049db3 | ||
|
|
a0435f6a60 | ||
|
|
2321e2c90b | ||
|
|
97e98d8629 | ||
|
|
d96e7362d2 | ||
|
|
7dd46fe5ed | ||
|
|
04c044cb2b | ||
|
|
cc10a12fbc | ||
|
|
8b0a1c699f | ||
|
|
15ca7c9807 | ||
|
|
2b4da7e39b | ||
|
|
31f81f38af | ||
|
|
72cf77b7c7 | ||
|
|
0fe48c647e | ||
|
|
7b06652bff | ||
|
|
434ce05416 | ||
|
|
0698031ed4 | ||
|
|
51237a34eb | ||
|
|
b8264a8131 | ||
|
|
cad923018e | ||
|
|
db94b49941 | ||
|
|
72d15d9cbf | ||
|
|
e0186eadc0 | ||
|
|
4cfa5b04af | ||
|
|
f2c54b1f8b | ||
|
|
d7d0bc6582 | ||
|
|
dd9dc2500b | ||
|
|
4efb109da8 | ||
|
|
4c61628078 | ||
|
|
fa1d7af22f | ||
|
|
a771ba3bc0 | ||
|
|
7dd0da5fd7 | ||
|
|
da87e45534 | ||
|
|
2ffaef5563 |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "1.1.2",
|
||||
"version": "1.2.5",
|
||||
"commands": [
|
||||
"csharpier"
|
||||
],
|
||||
|
||||
15
.github/COPILOT_AGENT_README.md
vendored
15
.github/COPILOT_AGENT_README.md
vendored
@@ -1,15 +0,0 @@
|
||||
# Copilot Coding Agent Configuration
|
||||
|
||||
This repository includes a minimal opt-in configuration and CI workflow to allow the GitHub Copilot coding agent to open and validate PRs.
|
||||
|
||||
- .copilot-agent.yml: opt-in config for automated agents
|
||||
- .github/agents/copilot-agent.yml: detailed agent policy configuration
|
||||
- .github/workflows/dotnetcore.yml: CI runs on PRs touching the solution, source, or tests to validate changes
|
||||
- AGENTS.md: general instructions for Copilot coding agent with project-specific guidelines
|
||||
|
||||
Maintainers can adjust the allowed paths or disable the agent by editing or removing .copilot-agent.yml.
|
||||
|
||||
Notes:
|
||||
- The agent can create, modify, and delete files within the allowed paths (src, tests, README.md, AGENTS.md)
|
||||
- All changes require review before merge
|
||||
- If build/test paths are different, update the workflow accordingly; this workflow targets SharpCompress.sln and the SharpCompress.Test test project.
|
||||
25
.github/prompts/plan-async.prompt.md
vendored
Normal file
25
.github/prompts/plan-async.prompt.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Plan: Implement Missing Async Functionality in SharpCompress
|
||||
|
||||
SharpCompress has async support for low-level stream operations and Reader/Writer APIs, but critical entry points (Archive.Open, factory methods, initialization) remain synchronous. This plan adds async overloads for all user-facing I/O operations and fixes existing async bugs, enabling full end-to-end async workflows.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Add async factory methods** to [ArchiveFactory.cs](src/SharpCompress/Factories/ArchiveFactory.cs), [ReaderFactory.cs](src/SharpCompress/Factories/ReaderFactory.cs), and [WriterFactory.cs](src/SharpCompress/Factories/WriterFactory.cs) with `OpenAsync` and `CreateAsync` overloads accepting `CancellationToken`
|
||||
|
||||
2. **Implement async Open methods** on concrete archive types ([ZipArchive.cs](src/SharpCompress/Archives/Zip/ZipArchive.cs), [TarArchive.cs](src/SharpCompress/Archives/Tar/TarArchive.cs), [RarArchive.cs](src/SharpCompress/Archives/Rar/RarArchive.cs), [GZipArchive.cs](src/SharpCompress/Archives/GZip/GZipArchive.cs), [SevenZipArchive.cs](src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs)) and reader types ([ZipReader.cs](src/SharpCompress/Readers/Zip/ZipReader.cs), [TarReader.cs](src/SharpCompress/Readers/Tar/TarReader.cs), etc.)
|
||||
|
||||
3. **Convert archive initialization logic to async** including header reading, volume loading, and format signature detection across archive constructors and internal initialization methods
|
||||
|
||||
4. **Fix LZMA decoder async bugs** in [LzmaStream.cs](src/SharpCompress/Compressors/LZMA/LzmaStream.cs), [Decoder.cs](src/SharpCompress/Compressors/LZMA/Decoder.cs), and [OutWindow.cs](src/SharpCompress/Compressors/LZMA/OutWindow.cs) to enable true async 7Zip support and remove `NonDisposingStream` workaround
|
||||
|
||||
5. **Complete Rar async implementation** by converting `UnpackV2017` methods to async in [UnpackV2017.cs](src/SharpCompress/Compressors/Rar/UnpackV2017.cs) and updating Rar20 decompression
|
||||
|
||||
6. **Add comprehensive async tests** covering all new async entry points, cancellation scenarios, and concurrent operations across all archive formats in test files
|
||||
|
||||
## Further Considerations
|
||||
|
||||
1. **Breaking changes** - Should new async methods be added alongside existing sync methods (non-breaking), or should sync methods eventually be deprecated? Recommend additive approach for backward compatibility.
|
||||
|
||||
2. **Performance impact** - Header parsing for formats like Zip/Tar is often small; consider whether truly async parsing adds value vs sync parsing wrapped in Task, or make it conditional based on stream type (network vs file).
|
||||
|
||||
3. **7Zip complexity** - The LZMA async bug fix (Step 4) may be challenging due to state management in the decoder; consider whether to scope it separately or implement a simpler workaround that maintains correctness.
|
||||
123
.github/prompts/plan-for-next.prompt.md
vendored
Normal file
123
.github/prompts/plan-for-next.prompt.md
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
# Plan: Modernize SharpCompress Public API
|
||||
|
||||
Based on comprehensive analysis, the API has several inconsistencies around factory patterns, async support, format capabilities, and options classes. Most improvements can be done incrementally without breaking changes.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Standardize factory patterns** by deprecating format-specific static `Open` methods in [Archives/Zip/ZipArchive.cs](src/SharpCompress/Archives/Zip/ZipArchive.cs), [Archives/Tar/TarArchive.cs](src/SharpCompress/Archives/Tar/TarArchive.cs), etc. in favor of centralized [Factories/ArchiveFactory.cs](src/SharpCompress/Factories/ArchiveFactory.cs)
|
||||
|
||||
2. **Complete async implementation** in [Writers/Zip/ZipWriter.cs](src/SharpCompress/Writers/Zip/ZipWriter.cs) and other writers that currently use sync-over-async, implementing true async I/O throughout the writer hierarchy
|
||||
|
||||
3. **Unify options classes** by making [Common/ExtractionOptions.cs](src/SharpCompress/Common/ExtractionOptions.cs) inherit from `OptionsBase` and adding progress reporting to extraction methods consistently
|
||||
|
||||
4. **Clarify GZip semantics** in [Archives/GZip/GZipArchive.cs](src/SharpCompress/Archives/GZip/GZipArchive.cs) by adding XML documentation explaining single-entry limitation and relationship to GZip compression used in Tar.gz
|
||||
|
||||
## Further Considerations
|
||||
|
||||
1. **Breaking changes roadmap** - Should we plan a major version (2.0) to remove deprecated factory methods, clean up `ArchiveType` enum (remove Arc/Arj or add full support), and consolidate naming patterns?
|
||||
|
||||
2. **Progress reporting consistency** - Should `IProgress<ArchiveExtractionProgress<IEntry>>` be added to all extraction extension methods or consolidated into options classes?
|
||||
|
||||
## Detailed Analysis
|
||||
|
||||
### Factory Pattern Issues
|
||||
|
||||
Three different factory patterns exist with overlapping functionality:
|
||||
|
||||
1. **Static Factories**: ArchiveFactory, ReaderFactory, WriterFactory
|
||||
2. **Instance Factories**: IArchiveFactory, IReaderFactory, IWriterFactory
|
||||
3. **Format-specific static methods**: Each archive class has static `Open` methods
|
||||
|
||||
**Example confusion:**
|
||||
```csharp
|
||||
// Three ways to open a Zip archive - which is recommended?
|
||||
var archive1 = ArchiveFactory.Open("file.zip");
|
||||
var archive2 = ZipArchive.Open("file.zip");
|
||||
var archive3 = ArchiveFactory.AutoFactory.Open(fileInfo, options);
|
||||
```
|
||||
|
||||
### Async Support Gaps
|
||||
|
||||
Base `IWriter` interface has async methods, but writer implementations provide minimal async support. Most writers just call synchronous methods:
|
||||
|
||||
```csharp
|
||||
public virtual async Task WriteAsync(...)
|
||||
{
|
||||
// Default implementation calls synchronous version
|
||||
Write(filename, source, modificationTime);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
```
|
||||
|
||||
Real async implementations only in:
|
||||
- `TarWriter` - Proper async implementation
|
||||
- Most other writers use sync-over-async
|
||||
|
||||
### GZip Archive Special Case
|
||||
|
||||
GZip is treated as both a compression format and an archive format, but only supports single-entry archives:
|
||||
|
||||
```csharp
|
||||
protected override GZipArchiveEntry CreateEntryInternal(...)
|
||||
{
|
||||
if (Entries.Any())
|
||||
{
|
||||
throw new InvalidFormatException("Only one entry is allowed in a GZip Archive");
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Options Class Hierarchy
|
||||
|
||||
```
|
||||
OptionsBase (LeaveStreamOpen, ArchiveEncoding)
|
||||
├─ ReaderOptions (LookForHeader, Password, DisableCheckIncomplete, BufferSize, ExtensionHint, Progress)
|
||||
├─ WriterOptions (CompressionType, CompressionLevel, Progress)
|
||||
│ ├─ ZipWriterOptions (ArchiveComment, UseZip64)
|
||||
│ ├─ TarWriterOptions (FinalizeArchiveOnClose, HeaderFormat)
|
||||
│ └─ GZipWriterOptions (no additional properties)
|
||||
└─ ExtractionOptions (standalone - Overwrite, ExtractFullPath, PreserveFileTime, PreserveAttributes)
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
- `ExtractionOptions` doesn't inherit from `OptionsBase` - no encoding support during extraction
|
||||
- Progress reporting inconsistency between readers and extraction
|
||||
- Obsolete properties (`ChecksumIsValid`, `Version`) with unclear migration path
|
||||
|
||||
### Implementation Priorities
|
||||
|
||||
**High Priority (Non-Breaking):**
|
||||
1. Add API usage guide (Archive vs Reader, factory recommendations, async best practices)
|
||||
2. Fix progress reporting consistency
|
||||
3. Complete async implementation in writers
|
||||
|
||||
**Medium Priority (Next Major Version):**
|
||||
1. Unify factory pattern - deprecate format-specific static `Open` methods
|
||||
2. Clean up options classes - make `ExtractionOptions` inherit from `OptionsBase`
|
||||
3. Clarify archive types - remove Arc/Arj from `ArchiveType` enum or add full support
|
||||
4. Standardize naming across archive types
|
||||
|
||||
**Low Priority:**
|
||||
1. Add BZip2 archive support similar to GZipArchive
|
||||
2. Complete obsolete property cleanup with migration guide
|
||||
|
||||
### Backward Compatibility Strategy
|
||||
|
||||
**Safe (Non-Breaking) Changes:**
|
||||
- Add new methods to interfaces (use default implementations)
|
||||
- Add new options properties (with defaults)
|
||||
- Add new factory methods
|
||||
- Improve async implementations
|
||||
- Add progress reporting support
|
||||
|
||||
**Breaking Changes to Avoid:**
|
||||
- ❌ Removing format-specific `Open` methods (deprecate instead)
|
||||
- ❌ Changing `LeaveStreamOpen` default (currently `true`)
|
||||
- ❌ Removing obsolete properties before major version bump
|
||||
- ❌ Changing return types or signatures of existing methods
|
||||
|
||||
**Deprecation Pattern:**
|
||||
- Use `[Obsolete]` for one major version
|
||||
- Use `[EditorBrowsable(EditorBrowsableState.Never)]` in next major version
|
||||
- Remove in following major version
|
||||
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@v5
|
||||
- uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- run: dotnet run --project build/build.csproj
|
||||
- uses: actions/upload-artifact@v5
|
||||
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 }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -11,11 +11,12 @@ TestResults/
|
||||
packages/*/
|
||||
project.lock.json
|
||||
tests/TestArchives/Scratch
|
||||
tests/TestArchives/*/Scratch
|
||||
tests/TestArchives/*/Scratch2
|
||||
.vs
|
||||
tools
|
||||
.vscode
|
||||
.idea/
|
||||
artifacts/
|
||||
|
||||
.DS_Store
|
||||
*.snupkg
|
||||
/tests/TestArchives/6d23a38c-f064-4ef1-ad89-b942396f53b9/Scratch
|
||||
|
||||
9
.vscode/extensions.json
vendored
Normal file
9
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-dotnettools.csharp",
|
||||
"ms-dotnettools.vscode-dotnet-runtime",
|
||||
"csharpier.csharpier-vscode",
|
||||
"formulahendry.dotnet-test-explorer"
|
||||
]
|
||||
}
|
||||
97
.vscode/launch.json
vendored
Normal file
97
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Tests (net10.0)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net10.0",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Debug Specific Test (net10.0)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net10.0",
|
||||
"--no-build",
|
||||
"--filter",
|
||||
"FullyQualifiedName~${input:testName}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Debug Performance Tests",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/tests/SharpCompress.Performance/SharpCompress.Performance.csproj",
|
||||
"--no-build"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Debug Build Script",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/build/build.csproj",
|
||||
"--",
|
||||
"${input:buildTarget}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "testName",
|
||||
"type": "promptString",
|
||||
"description": "Enter test name or pattern (e.g., TestMethodName or ClassName)",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"id": "buildTarget",
|
||||
"type": "pickString",
|
||||
"description": "Select build target",
|
||||
"options": [
|
||||
"clean",
|
||||
"restore",
|
||||
"build",
|
||||
"test",
|
||||
"format",
|
||||
"publish",
|
||||
"default"
|
||||
],
|
||||
"default": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
29
.vscode/settings.json
vendored
Normal file
29
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"dotnet.defaultSolution": "SharpCompress.sln",
|
||||
"files.exclude": {
|
||||
"**/bin": true,
|
||||
"**/obj": true
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/bin/**": true,
|
||||
"**/obj/**": true,
|
||||
"**/artifacts/**": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/bin": true,
|
||||
"**/obj": true,
|
||||
"**/artifacts": true
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"[csharp]": {
|
||||
"editor.defaultFormatter": "csharpier.csharpier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
}
|
||||
},
|
||||
"csharpier.enableDebugLogs": false,
|
||||
"omnisharp.enableRoslynAnalyzers": true,
|
||||
"omnisharp.enableEditorConfigSupport": true,
|
||||
"dotnet-test-explorer.testProjectPath": "tests/**/*.csproj"
|
||||
}
|
||||
178
.vscode/tasks.json
vendored
Normal file
178
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/SharpCompress.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "build-release",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/SharpCompress.sln",
|
||||
"-c",
|
||||
"Release",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "build-library",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/src/SharpCompress/SharpCompress.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "restore",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"restore",
|
||||
"${workspaceFolder}/SharpCompress.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "clean",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"clean",
|
||||
"${workspaceFolder}/SharpCompress.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "test",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test-net10",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net10.0",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "test",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test-net48",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net48",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "test",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "format",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"csharpier",
|
||||
"."
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "format-check",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"csharpier",
|
||||
"check",
|
||||
"."
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "run-build-script",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/build/build.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "pack",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"pack",
|
||||
"${workspaceFolder}/src/SharpCompress/SharpCompress.csproj",
|
||||
"-c",
|
||||
"Release",
|
||||
"-o",
|
||||
"${workspaceFolder}/artifacts/"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"dependsOn": "build-release"
|
||||
},
|
||||
{
|
||||
"label": "performance-tests",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/tests/SharpCompress.Performance/SharpCompress.Performance.csproj",
|
||||
"-c",
|
||||
"Release"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
71
AGENTS.md
71
AGENTS.md
@@ -28,14 +28,38 @@ SharpCompress is a pure C# compression library supporting multiple archive forma
|
||||
|
||||
## Code Formatting
|
||||
|
||||
**Copilot agents: You MUST run the `format` task after making code changes to ensure consistency.**
|
||||
|
||||
- Use CSharpier for code formatting to ensure consistent style across the project
|
||||
- CSharpier is configured as a local tool in `.config/dotnet-tools.json`
|
||||
- Restore tools with: `dotnet tool restore`
|
||||
- Format files from the project root with: `dotnet csharpier .`
|
||||
- **Run `dotnet csharpier .` from the project root after making code changes before committing**
|
||||
- Configure your IDE to format on save using CSharpier for the best experience
|
||||
|
||||
### Commands
|
||||
|
||||
1. **Restore tools** (first time only):
|
||||
```bash
|
||||
dotnet tool restore
|
||||
```
|
||||
|
||||
2. **Check if files are formatted correctly** (doesn't modify files):
|
||||
```bash
|
||||
dotnet csharpier check .
|
||||
```
|
||||
- Exit code 0: All files are properly formatted
|
||||
- Exit code 1: Some files need formatting (will show which files and differences)
|
||||
|
||||
3. **Format files** (modifies files):
|
||||
```bash
|
||||
dotnet csharpier format .
|
||||
```
|
||||
- Formats all files in the project to match CSharpier style
|
||||
- Run from project root directory
|
||||
|
||||
4. **Configure your IDE** to format on save using CSharpier for the best experience
|
||||
|
||||
### Additional Notes
|
||||
- The project also uses `.editorconfig` for editor settings (indentation, encoding, etc.)
|
||||
- Let CSharpier handle code style while `.editorconfig` handles editor behavior
|
||||
- Always run `dotnet csharpier check .` before committing to verify formatting
|
||||
|
||||
## Project Setup and Structure
|
||||
|
||||
@@ -49,6 +73,30 @@ SharpCompress is a pure C# compression library supporting multiple archive forma
|
||||
- Use `dotnet test` to run tests
|
||||
- Solution file: `SharpCompress.sln`
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
src/SharpCompress/
|
||||
├── Archives/ # IArchive implementations (Zip, Tar, Rar, 7Zip, GZip)
|
||||
├── Readers/ # IReader implementations (forward-only)
|
||||
├── Writers/ # IWriter implementations (forward-only)
|
||||
├── Compressors/ # Low-level compression streams (BZip2, Deflate, LZMA, etc.)
|
||||
├── Factories/ # Format detection and factory pattern
|
||||
├── Common/ # Shared types (ArchiveType, Entry, Options)
|
||||
├── Crypto/ # Encryption implementations
|
||||
└── IO/ # Stream utilities and wrappers
|
||||
|
||||
tests/SharpCompress.Test/
|
||||
├── Zip/, Tar/, Rar/, SevenZip/, GZip/, BZip2/ # Format-specific tests
|
||||
├── TestBase.cs # Base test class with helper methods
|
||||
└── TestArchives/ # Test data (not checked into main test project)
|
||||
```
|
||||
|
||||
### Factory Pattern
|
||||
All format types implement factory interfaces (`IArchiveFactory`, `IReaderFactory`, `IWriterFactory`) for auto-detection:
|
||||
- `ReaderFactory.Open()` - Auto-detects format by probing stream
|
||||
- `WriterFactory.Open()` - Creates writer for specified `ArchiveType`
|
||||
- Factories located in: `src/SharpCompress/Factories/`
|
||||
|
||||
## Nullable Reference Types
|
||||
|
||||
- Declare variables non-nullable, and check for `null` at entry points.
|
||||
@@ -116,3 +164,18 @@ SharpCompress supports multiple archive and compression formats:
|
||||
- Use test archives from `tests/TestArchives` directory for consistency.
|
||||
- Test stream disposal and `LeaveStreamOpen` behavior.
|
||||
- Test edge cases: empty archives, large files, corrupted archives, encrypted archives.
|
||||
|
||||
### Test Organization
|
||||
- Base class: `TestBase` - Provides `TEST_ARCHIVES_PATH`, `SCRATCH_FILES_PATH`, temp directory management
|
||||
- Framework: xUnit with AwesomeAssertions
|
||||
- Test archives: `tests/TestArchives/` - Use existing archives, don't create new ones unnecessarily
|
||||
- Match naming style of nearby test files
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Don't mix Archive and Reader APIs** - Archive needs seekable stream, Reader doesn't
|
||||
2. **Solid archives (Rar, 7Zip)** - Use `ExtractAllEntries()` for best performance, not individual entry extraction
|
||||
3. **Stream disposal** - Always set `LeaveStreamOpen` explicitly when needed (default is to close)
|
||||
4. **Tar + non-seekable stream** - Must provide file size or it will throw
|
||||
5. **Multi-framework differences** - Some features differ between .NET Framework and modern .NET (e.g., Mono.Posix)
|
||||
6. **Format detection** - Use `ReaderFactory.Open()` for auto-detection, test with actual archive files
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Bullseye" Version="6.0.0" />
|
||||
<PackageVersion Include="AwesomeAssertions" Version="9.2.1" />
|
||||
<PackageVersion Include="Bullseye" Version="6.1.0" />
|
||||
<PackageVersion Include="AwesomeAssertions" Version="9.3.0" />
|
||||
<PackageVersion Include="Glob" Version="1.1.9" />
|
||||
<PackageVersion Include="JetBrains.Profiler.SelfApi" Version="2.5.14" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageVersion Include="JetBrains.Profiler.SelfApi" Version="2.5.15" />
|
||||
<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.0.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="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
<PackageVersion Include="ZstdSharp.Port" Version="0.8.6" />
|
||||
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="8.0.21" />
|
||||
<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>
|
||||
|
||||
26
FORMATS.md
26
FORMATS.md
@@ -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) |
|
||||
@@ -22,11 +25,28 @@
|
||||
| 7Zip (4) | LZMA, LZMA2, BZip2, PPMd, BCJ, BCJ2, Deflate | Decompress | SevenZipArchive | N/A | N/A |
|
||||
|
||||
1. SOLID Rars are only supported in the RarReader API.
|
||||
2. Zip format supports pkware and WinzipAES encryption. However, encrypted LZMA is not supported. Zip64 reading/writing is supported but only with seekable streams as the Zip spec doesn't support Zip64 data in post data descriptors. Deflate64 is only supported for reading.
|
||||
2. Zip format supports pkware and WinzipAES encryption. However, encrypted LZMA is not supported. Zip64 reading/writing is supported but only with seekable streams as the Zip spec doesn't support Zip64 data in post data descriptors. Deflate64 is only supported for reading. See [Zip Format Notes](#zip-format-notes) for details on multi-volume archives and streaming behavior.
|
||||
3. The Tar format requires a file size in the header. If no size is specified to the TarWriter and the stream is not seekable, then an exception will be thrown.
|
||||
4. The 7Zip format doesn't allow for reading as a forward-only stream so 7Zip is only supported through the Archive API
|
||||
4. The 7Zip format doesn't allow for reading as a forward-only stream so 7Zip is only supported through the Archive API. See [7Zip Format Notes](#7zip-format-notes) for details on async extraction behavior.
|
||||
5. LZip has no support for extra data like the file name or timestamp. There is a default filename used when looking at the entry Key on the archive.
|
||||
|
||||
### Zip Format Notes
|
||||
|
||||
- Multi-volume/split ZIP archives require ZipArchive (seekable streams) as ZipReader cannot seek across volume files.
|
||||
- ZipReader processes entries from LocalEntry headers (which include directory entries ending with `/`) and intentionally skips DirectoryEntry headers from the central directory, as they are redundant in streaming mode - all entry data comes from LocalEntry headers which ZipReader has already processed.
|
||||
|
||||
### 7Zip Format Notes
|
||||
|
||||
- **Async Extraction Performance**: When using async extraction methods (e.g., `ExtractAllEntries()` with `MoveToNextEntryAsync()`), each file creates its own decompression stream to avoid state corruption in the LZMA decoder. This is less efficient than synchronous extraction, which can reuse a single decompression stream for multiple files in the same folder.
|
||||
|
||||
**Performance Impact**: For archives with many small files in the same compression folder, async extraction will be slower than synchronous extraction because it must:
|
||||
1. Create a new LZMA decoder for each file
|
||||
2. Skip through the decompressed data to reach each file's starting position
|
||||
|
||||
**Recommendation**: For best performance with 7Zip archives, use synchronous extraction methods (`MoveToNextEntry()` and `WriteEntryToDirectory()`) when possible. Use async methods only when you need to avoid blocking the thread (e.g., in UI applications or async-only contexts).
|
||||
|
||||
**Technical Details**: 7Zip archives group files into "folders" (compression units), where all files in a folder share one continuous LZMA-compressed stream. The LZMA decoder maintains internal state (dictionary window, decoder positions) that assumes sequential, non-interruptible processing. Async operations can yield control during awaits, which would corrupt this shared state. To avoid this, async extraction creates a fresh decoder stream for each file.
|
||||
|
||||
## Compression Streams
|
||||
|
||||
For those who want to directly compress/decompress bits. The single file formats are represented here as well. However, BZip2, LZip and XZ have no metadata (GZip has a little) so using them without something like a Tar file makes little sense.
|
||||
|
||||
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)
|
||||
230
README.md
230
README.md
@@ -1,10 +1,10 @@
|
||||
# SharpCompress
|
||||
|
||||
SharpCompress is a compression library in pure C# for .NET Framework 4.62, .NET Standard 2.1, .NET 6.0 and NET 8.0 that can unrar, un7zip, unzip, untar unbzip2, ungzip, unlzip, unzstd with forward-only reading and file random access APIs. Write support for zip/tar/bzip2/gzip/lzip are implemented.
|
||||
SharpCompress is a compression library in pure C# for .NET Framework 4.8, .NET 8.0 and .NET 10.0 that can unrar, un7zip, unzip, untar unbzip2, ungzip, unlzip, unzstd, unarc and unarj with forward-only reading and file random access APIs. Write support for zip/tar/bzip2/gzip/lzip are implemented.
|
||||
|
||||
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
|
||||
|
||||
52
USAGE.md
52
USAGE.md
@@ -87,20 +87,17 @@ memoryStream.Position = 0;
|
||||
### Extract all files from a rar file to a directory using RarArchive
|
||||
|
||||
Note: Extracting a solid rar or 7z file needs to be done in sequential order to get acceptable decompression speed.
|
||||
It is explicitly recommended to use `ExtractAllEntries` when extracting an entire `IArchive` instead of iterating over all its `Entries`.
|
||||
Alternatively, use `IArchive.WriteToDirectory`.
|
||||
`ExtractAllEntries` is primarily intended for solid archives (like solid Rar) or 7Zip archives, where sequential extraction provides the best performance. For general/simple extraction with any supported archive type, use `archive.WriteToDirectory()` instead.
|
||||
|
||||
```C#
|
||||
using (var archive = RarArchive.Open("Test.rar"))
|
||||
{
|
||||
using (var reader = archive.ExtractAllEntries())
|
||||
// Simple extraction with RarArchive; this WriteToDirectory pattern works for all archive types
|
||||
archive.WriteToDirectory(@"D:\temp", new ExtractionOptions()
|
||||
{
|
||||
reader.WriteAllToDirectory(@"D:\temp", new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
});
|
||||
}
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -116,6 +113,29 @@ using (var archive = RarArchive.Open("Test.rar"))
|
||||
}
|
||||
```
|
||||
|
||||
### 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 SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
var progress = new Progress<ProgressReport>(report =>
|
||||
{
|
||||
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()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Use ReaderFactory to autodetect archive type and Open the entry stream
|
||||
|
||||
```C#
|
||||
@@ -298,14 +318,12 @@ using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.
|
||||
```C#
|
||||
using (var archive = ZipArchive.Open("archive.zip"))
|
||||
{
|
||||
using (var reader = archive.ExtractAllEntries())
|
||||
{
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
@"C:\output",
|
||||
new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
// Simple async extraction - works for all archive types
|
||||
await archive.WriteToDirectoryAsync(
|
||||
@"C:\output",
|
||||
new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
213
build/Program.cs
213
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;
|
||||
@@ -11,7 +14,11 @@ const string Restore = "restore";
|
||||
const string Build = "build";
|
||||
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,
|
||||
@@ -42,12 +49,20 @@ Target(
|
||||
Target(
|
||||
Format,
|
||||
() =>
|
||||
{
|
||||
Run("dotnet", "tool restore");
|
||||
Run("dotnet", "csharpier format .");
|
||||
}
|
||||
);
|
||||
Target(
|
||||
CheckFormat,
|
||||
() =>
|
||||
{
|
||||
Run("dotnet", "tool restore");
|
||||
Run("dotnet", "csharpier check .");
|
||||
}
|
||||
);
|
||||
Target(Restore, [Format], () => Run("dotnet", "restore"));
|
||||
Target(Restore, [CheckFormat], () => Run("dotnet", "restore"));
|
||||
|
||||
Target(
|
||||
Build,
|
||||
@@ -61,7 +76,7 @@ Target(
|
||||
Target(
|
||||
Test,
|
||||
[Build],
|
||||
["net8.0", "net48"],
|
||||
["net10.0", "net48"],
|
||||
framework =>
|
||||
{
|
||||
IEnumerable<string> GetFiles(string d)
|
||||
@@ -90,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bullseye" />
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net8.0": {
|
||||
"net10.0": {
|
||||
"Bullseye": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "vgwwXfzs7jJrskWH7saHRMgPzziq/e86QZNWY1MnMxd7e+De7E7EX4K3C7yrvaK9y02SJoLxNxcLG/q5qUAghw=="
|
||||
"requested": "[6.1.0, )",
|
||||
"resolved": "6.1.0",
|
||||
"contentHash": "fltnAJDe0BEX5eymXGUq+il2rSUA0pHqUonNDRH2TrvRu8SkU17mYG0IVpdmG2ibtfhdjNrv4CuTCxHOwcozCA=="
|
||||
},
|
||||
"Glob": {
|
||||
"type": "Direct",
|
||||
@@ -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.0.0, )",
|
||||
"resolved": "12.0.0",
|
||||
"contentHash": "ptxlWtxC8vM6Y6e3h9ZTxBBkOWnWrm/Sa1HT+2i1xcXY3Hx2hmKDZP5RShPf8Xr9D+ivlrXNy57ktzyH8kyt+Q=="
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100",
|
||||
"version": "10.0.100",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtractionListener
|
||||
public abstract class AbstractArchive<TEntry, TVolume> : IArchive
|
||||
where TEntry : IArchiveEntry
|
||||
where TVolume : IVolume
|
||||
{
|
||||
@@ -17,11 +17,6 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
private bool _disposed;
|
||||
private readonly SourceStream? _sourceStream;
|
||||
|
||||
public event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>>? EntryExtractionBegin;
|
||||
public event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>>? EntryExtractionEnd;
|
||||
|
||||
public event EventHandler<CompressedBytesReadEventArgs>? CompressedBytesRead;
|
||||
public event EventHandler<FilePartExtractionBeginEventArgs>? FilePartExtractionBegin;
|
||||
protected ReaderOptions ReaderOptions { get; }
|
||||
|
||||
internal AbstractArchive(ArchiveType type, SourceStream sourceStream)
|
||||
@@ -43,12 +38,6 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
|
||||
public ArchiveType Type { get; }
|
||||
|
||||
void IArchiveExtractionListener.FireEntryExtractionBegin(IArchiveEntry entry) =>
|
||||
EntryExtractionBegin?.Invoke(this, new ArchiveExtractionEventArgs<IArchiveEntry>(entry));
|
||||
|
||||
void IArchiveExtractionListener.FireEntryExtractionEnd(IArchiveEntry entry) =>
|
||||
EntryExtractionEnd?.Invoke(this, new ArchiveExtractionEventArgs<IArchiveEntry>(entry));
|
||||
|
||||
private static Stream CheckStreams(Stream stream)
|
||||
{
|
||||
if (!stream.CanSeek || !stream.CanRead)
|
||||
@@ -99,38 +88,12 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
}
|
||||
}
|
||||
|
||||
void IArchiveExtractionListener.EnsureEntriesLoaded()
|
||||
private void EnsureEntriesLoaded()
|
||||
{
|
||||
_lazyEntries.EnsureFullyLoaded();
|
||||
_lazyVolumes.EnsureFullyLoaded();
|
||||
}
|
||||
|
||||
void IExtractionListener.FireCompressedBytesRead(
|
||||
long currentPartCompressedBytes,
|
||||
long compressedReadBytes
|
||||
) =>
|
||||
CompressedBytesRead?.Invoke(
|
||||
this,
|
||||
new CompressedBytesReadEventArgs(
|
||||
currentFilePartCompressedBytesRead: currentPartCompressedBytes,
|
||||
compressedBytesRead: compressedReadBytes
|
||||
)
|
||||
);
|
||||
|
||||
void IExtractionListener.FireFilePartExtractionBegin(
|
||||
string name,
|
||||
long size,
|
||||
long compressedSize
|
||||
) =>
|
||||
FilePartExtractionBegin?.Invoke(
|
||||
this,
|
||||
new FilePartExtractionBeginEventArgs(
|
||||
compressedSize: compressedSize,
|
||||
size: size,
|
||||
name: name
|
||||
)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Use this method to extract all entries in an archive in order.
|
||||
/// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be
|
||||
@@ -146,11 +109,11 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
{
|
||||
if (!IsSolid && Type != ArchiveType.SevenZip)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
throw new SharpCompressException(
|
||||
"ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)."
|
||||
);
|
||||
}
|
||||
((IArchiveExtractionListener)this).EnsureEntriesLoaded();
|
||||
EnsureEntriesLoaded();
|
||||
return CreateReaderForSolidExtraction();
|
||||
}
|
||||
|
||||
@@ -161,6 +124,11 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
/// </summary>
|
||||
public virtual bool IsSolid => false;
|
||||
|
||||
/// <summary>
|
||||
/// Archive is ENCRYPTED (this means the Archive has password-protected files).
|
||||
/// </summary>
|
||||
public virtual bool IsEncrypted => false;
|
||||
|
||||
/// <summary>
|
||||
/// The archive can find all the parts of the archive needed to fully extract the archive. This forces the parsing of the entire archive.
|
||||
/// </summary>
|
||||
@@ -168,7 +136,7 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
{
|
||||
get
|
||||
{
|
||||
((IArchiveExtractionListener)this).EnsureEntriesLoaded();
|
||||
EnsureEntriesLoaded();
|
||||
return Entries.All(x => x.IsComplete);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -7,12 +7,6 @@ namespace SharpCompress.Archives;
|
||||
|
||||
public interface IArchive : IDisposable
|
||||
{
|
||||
event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>> EntryExtractionBegin;
|
||||
event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>> EntryExtractionEnd;
|
||||
|
||||
event EventHandler<CompressedBytesReadEventArgs> CompressedBytesRead;
|
||||
event EventHandler<FilePartExtractionBeginEventArgs> FilePartExtractionBegin;
|
||||
|
||||
IEnumerable<IArchiveEntry> Entries { get; }
|
||||
IEnumerable<IVolume> Volumes { get; }
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,126 +9,151 @@ namespace SharpCompress.Archives;
|
||||
|
||||
public static class IArchiveEntryExtensions
|
||||
{
|
||||
public static void WriteTo(this IArchiveEntry archiveEntry, Stream streamToWriteTo)
|
||||
/// <param name="archiveEntry">The archive entry to extract.</param>
|
||||
extension(IArchiveEntry archiveEntry)
|
||||
{
|
||||
if (archiveEntry.IsDirectory)
|
||||
/// <summary>
|
||||
/// Extract entry to the specified stream.
|
||||
/// </summary>
|
||||
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public void WriteTo(Stream streamToWriteTo, IProgress<ProgressReport>? progress = null)
|
||||
{
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
if (archiveEntry.IsDirectory)
|
||||
{
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
}
|
||||
|
||||
using var entryStream = archiveEntry.OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
|
||||
sourceStream.CopyTo(streamToWriteTo, Constants.BufferSize);
|
||||
}
|
||||
|
||||
var streamListener = (IArchiveExtractionListener)archiveEntry.Archive;
|
||||
streamListener.EnsureEntriesLoaded();
|
||||
streamListener.FireEntryExtractionBegin(archiveEntry);
|
||||
streamListener.FireFilePartExtractionBegin(
|
||||
archiveEntry.Key ?? "Key",
|
||||
archiveEntry.Size,
|
||||
archiveEntry.CompressedSize
|
||||
);
|
||||
var entryStream = archiveEntry.OpenEntryStream();
|
||||
using (entryStream)
|
||||
/// <summary>
|
||||
/// Extract entry to the specified stream asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public async Task WriteToAsync(
|
||||
Stream streamToWriteTo,
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using Stream s = new ListeningStream(streamListener, entryStream);
|
||||
s.CopyTo(streamToWriteTo);
|
||||
if (archiveEntry.IsDirectory)
|
||||
{
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
}
|
||||
|
||||
using var entryStream = await archiveEntry.OpenEntryStreamAsync(cancellationToken);
|
||||
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
|
||||
await sourceStream
|
||||
.CopyToAsync(streamToWriteTo, Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
streamListener.FireEntryExtractionEnd(archiveEntry);
|
||||
}
|
||||
|
||||
public static async Task WriteToAsync(
|
||||
this IArchiveEntry archiveEntry,
|
||||
Stream streamToWriteTo,
|
||||
CancellationToken cancellationToken = default
|
||||
private static Stream WrapWithProgress(
|
||||
Stream source,
|
||||
IArchiveEntry entry,
|
||||
IProgress<ProgressReport>? progress
|
||||
)
|
||||
{
|
||||
if (archiveEntry.IsDirectory)
|
||||
if (progress is null)
|
||||
{
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
return source;
|
||||
}
|
||||
|
||||
var streamListener = (IArchiveExtractionListener)archiveEntry.Archive;
|
||||
streamListener.EnsureEntriesLoaded();
|
||||
streamListener.FireEntryExtractionBegin(archiveEntry);
|
||||
streamListener.FireFilePartExtractionBegin(
|
||||
archiveEntry.Key ?? "Key",
|
||||
archiveEntry.Size,
|
||||
archiveEntry.CompressedSize
|
||||
var entryPath = entry.Key ?? string.Empty;
|
||||
var totalBytes = GetEntrySizeSafe(entry);
|
||||
return new ProgressReportingStream(
|
||||
source,
|
||||
progress,
|
||||
entryPath,
|
||||
totalBytes,
|
||||
leaveOpen: true
|
||||
);
|
||||
var entryStream = archiveEntry.OpenEntryStream();
|
||||
using (entryStream)
|
||||
{
|
||||
using Stream s = new ListeningStream(streamListener, entryStream);
|
||||
await s.CopyToAsync(streamToWriteTo, 81920, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
streamListener.FireEntryExtractionEnd(archiveEntry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory, retaining filename
|
||||
/// </summary>
|
||||
public static void WriteToDirectory(
|
||||
this IArchiveEntry entry,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToDirectory(
|
||||
entry,
|
||||
destinationDirectory,
|
||||
options,
|
||||
entry.WriteToFile
|
||||
);
|
||||
private static long? GetEntrySizeSafe(IArchiveEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
var size = entry.Size;
|
||||
return size >= 0 ? size : null;
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory asynchronously, retaining filename
|
||||
/// </summary>
|
||||
public static Task WriteToDirectoryAsync(
|
||||
this IArchiveEntry entry,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToDirectoryAsync(
|
||||
entry,
|
||||
destinationDirectory,
|
||||
options,
|
||||
(x, opt) => entry.WriteToFileAsync(x, opt, cancellationToken),
|
||||
cancellationToken
|
||||
);
|
||||
extension(IArchiveEntry entry)
|
||||
{
|
||||
/// <summary>
|
||||
/// Extract to specific directory, retaining filename
|
||||
/// </summary>
|
||||
public void WriteToDirectory(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToDirectory(
|
||||
entry,
|
||||
destinationDirectory,
|
||||
options,
|
||||
entry.WriteToFile
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file
|
||||
/// </summary>
|
||||
public static void WriteToFile(
|
||||
this IArchiveEntry entry,
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options = null
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToFile(
|
||||
entry,
|
||||
destinationFileName,
|
||||
options,
|
||||
(x, fm) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
entry.WriteTo(fs);
|
||||
}
|
||||
);
|
||||
/// <summary>
|
||||
/// Extract to specific directory asynchronously, retaining filename
|
||||
/// </summary>
|
||||
public Task WriteToDirectoryAsync(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToDirectoryAsync(
|
||||
entry,
|
||||
destinationDirectory,
|
||||
options,
|
||||
entry.WriteToFileAsync,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file asynchronously
|
||||
/// </summary>
|
||||
public static Task WriteToFileAsync(
|
||||
this IArchiveEntry entry,
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToFileAsync(
|
||||
entry,
|
||||
destinationFileName,
|
||||
options,
|
||||
async (x, fm) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
await entry.WriteToAsync(fs, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
);
|
||||
/// <summary>
|
||||
/// Extract to specific file
|
||||
/// </summary>
|
||||
public void WriteToFile(string destinationFileName, ExtractionOptions? options = null) =>
|
||||
ExtractionMethods.WriteEntryToFile(
|
||||
entry,
|
||||
destinationFileName,
|
||||
options,
|
||||
(x, fm) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
entry.WriteTo(fs);
|
||||
}
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file asynchronously
|
||||
/// </summary>
|
||||
public Task WriteToFileAsync(
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToFileAsync(
|
||||
entry,
|
||||
destinationFileName,
|
||||
options,
|
||||
async (x, fm, ct) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
await entry.WriteToAsync(fs, null, ct).ConfigureAwait(false);
|
||||
},
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
@@ -10,76 +10,159 @@ namespace SharpCompress.Archives;
|
||||
|
||||
public static class IArchiveExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extract to specific directory, retaining filename
|
||||
/// </summary>
|
||||
public static void WriteToDirectory(
|
||||
this IArchive archive,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
)
|
||||
{
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
reader.WriteAllToDirectory(destinationDirectory, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the archive to the destination directory. Directories will be created as needed.
|
||||
/// </summary>
|
||||
/// <param name="archive">The archive to extract.</param>
|
||||
/// <param name="destination">The folder to extract into.</param>
|
||||
/// <param name="progressReport">Optional progress report callback.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token.</param>
|
||||
public static void ExtractToDirectory(
|
||||
this IArchive archive,
|
||||
string destination,
|
||||
Action<double>? progressReport = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
extension(IArchive archive)
|
||||
{
|
||||
// Prepare for progress reporting
|
||||
var totalBytes = archive.TotalUncompressSize;
|
||||
var bytesRead = 0L;
|
||||
|
||||
// Tracking for created directories.
|
||||
var seenDirectories = new HashSet<string>();
|
||||
|
||||
// Extract
|
||||
foreach (var entry in archive.Entries)
|
||||
/// <summary>
|
||||
/// Extract to specific directory with progress reporting
|
||||
/// </summary>
|
||||
/// <param name="destinationDirectory">The folder to extract into.</param>
|
||||
/// <param name="options">Extraction options.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public void WriteToDirectory(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
IProgress<ProgressReport>? progress = null
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (entry.IsDirectory)
|
||||
// For solid archives (Rar, 7Zip), use the optimized reader-based approach
|
||||
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
|
||||
{
|
||||
var dirPath = Path.Combine(destination, entry.Key.NotNull("Entry Key is null"));
|
||||
if (
|
||||
Path.GetDirectoryName(dirPath + "/") is { } emptyDirectory
|
||||
&& seenDirectories.Add(dirPath)
|
||||
)
|
||||
{
|
||||
Directory.CreateDirectory(emptyDirectory);
|
||||
}
|
||||
continue;
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
reader.WriteAllToDirectory(destinationDirectory, options);
|
||||
}
|
||||
|
||||
// Create each directory if not already created
|
||||
var path = Path.Combine(destination, entry.Key.NotNull("Entry Key is null"));
|
||||
if (Path.GetDirectoryName(path) is { } directory)
|
||||
else
|
||||
{
|
||||
if (!Directory.Exists(directory) && !seenDirectories.Contains(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
seenDirectories.Add(directory);
|
||||
}
|
||||
// For non-solid archives, extract entries directly
|
||||
archive.WriteToDirectoryInternal(destinationDirectory, options, progress);
|
||||
}
|
||||
}
|
||||
|
||||
// Write file
|
||||
using var fs = File.Create(path);
|
||||
entry.WriteTo(fs);
|
||||
private void WriteToDirectoryInternal(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
IProgress<ProgressReport>? progress
|
||||
)
|
||||
{
|
||||
// Prepare for progress reporting
|
||||
var totalBytes = archive.TotalUncompressSize;
|
||||
var bytesRead = 0L;
|
||||
|
||||
// Update progress
|
||||
bytesRead += entry.Size;
|
||||
progressReport?.Invoke(bytesRead / (double)totalBytes);
|
||||
// Tracking for created directories.
|
||||
var seenDirectories = new HashSet<string>();
|
||||
|
||||
// Extract
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
var dirPath = Path.Combine(
|
||||
destinationDirectory,
|
||||
entry.Key.NotNull("Entry Key is null")
|
||||
);
|
||||
if (
|
||||
Path.GetDirectoryName(dirPath + "/") is { } parentDirectory
|
||||
&& seenDirectories.Add(dirPath)
|
||||
)
|
||||
{
|
||||
Directory.CreateDirectory(parentDirectory);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the entry's WriteToDirectory method which respects ExtractionOptions
|
||||
entry.WriteToDirectory(destinationDirectory, options);
|
||||
|
||||
// Update progress
|
||||
bytesRead += entry.Size;
|
||||
progress?.Report(
|
||||
new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory asynchronously with progress reporting and cancellation support
|
||||
/// </summary>
|
||||
/// <param name="destinationDirectory">The folder to extract into.</param>
|
||||
/// <param name="options">Extraction options.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token.</param>
|
||||
public async Task WriteToDirectoryAsync(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// For solid archives (Rar, 7Zip), use the optimized reader-based approach
|
||||
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
|
||||
{
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
destinationDirectory,
|
||||
options,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For non-solid archives, extract entries directly
|
||||
await archive.WriteToDirectoryAsyncInternal(
|
||||
destinationDirectory,
|
||||
options,
|
||||
progress,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteToDirectoryAsyncInternal(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
IProgress<ProgressReport>? progress,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
// Prepare for progress reporting
|
||||
var totalBytes = archive.TotalUncompressSize;
|
||||
var bytesRead = 0L;
|
||||
|
||||
// Tracking for created directories.
|
||||
var seenDirectories = new HashSet<string>();
|
||||
|
||||
// Extract
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
var dirPath = Path.Combine(
|
||||
destinationDirectory,
|
||||
entry.Key.NotNull("Entry Key is null")
|
||||
);
|
||||
if (
|
||||
Path.GetDirectoryName(dirPath + "/") is { } parentDirectory
|
||||
&& seenDirectories.Add(dirPath)
|
||||
)
|
||||
{
|
||||
Directory.CreateDirectory(parentDirectory);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the entry's WriteToDirectoryAsync method which respects ExtractionOptions
|
||||
await entry
|
||||
.WriteToDirectoryAsync(destinationDirectory, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Update progress
|
||||
bytesRead += entry.Size;
|
||||
progress?.Report(
|
||||
new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
internal interface IArchiveExtractionListener : IExtractionListener
|
||||
{
|
||||
void EnsureEntriesLoaded();
|
||||
void FireEntryExtractionBegin(IArchiveEntry entry);
|
||||
void FireEntryExtractionEnd(IArchiveEntry entry);
|
||||
}
|
||||
@@ -84,6 +84,8 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
|
||||
public override bool IsSolid => Volumes.First().IsSolidArchive;
|
||||
|
||||
public override bool IsEncrypted => Entries.First(x => !x.IsDirectory).IsEncrypted;
|
||||
|
||||
public virtual int MinVersion => Volumes.First().MinVersion;
|
||||
public virtual int MaxVersion => Volumes.First().MaxVersion;
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
|
||||
stream = new RarStream(
|
||||
archive.UnpackV1.Value,
|
||||
FileHeader,
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
|
||||
);
|
||||
}
|
||||
else
|
||||
@@ -84,7 +84,7 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
|
||||
stream = new RarStream(
|
||||
archive.UnpackV2017.Value,
|
||||
FileHeader,
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
|
||||
stream = new RarStream(
|
||||
archive.UnpackV1.Value,
|
||||
FileHeader,
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
|
||||
);
|
||||
}
|
||||
else
|
||||
@@ -108,7 +108,7 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
|
||||
stream = new RarStream(
|
||||
archive.UnpackV2017.Value,
|
||||
FileHeader,
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.SevenZip;
|
||||
using SharpCompress.Compressors.LZMA.Utilites;
|
||||
@@ -205,15 +207,36 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
.GroupBy(x => x.FilePart.Folder)
|
||||
.Any(folder => folder.Count() > 1);
|
||||
|
||||
public override bool IsEncrypted => Entries.First(x => !x.IsDirectory).IsEncrypted;
|
||||
|
||||
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;
|
||||
private Stream? _currentStream;
|
||||
private CFileItem? _currentItem;
|
||||
|
||||
/// <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;
|
||||
@@ -226,40 +249,170 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
stream.Position = 0;
|
||||
foreach (var dir in entries.Where(x => x.IsDirectory))
|
||||
{
|
||||
_currentEntry = dir;
|
||||
yield return dir;
|
||||
}
|
||||
foreach (
|
||||
var group in entries.Where(x => !x.IsDirectory).GroupBy(x => x.FilePart.Folder)
|
||||
)
|
||||
// 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))
|
||||
{
|
||||
_currentFolder = group.Key;
|
||||
if (group.Key is null)
|
||||
{
|
||||
_currentStream = Stream.Null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentStream = _archive._database?.GetFolderStream(
|
||||
stream,
|
||||
_currentFolder,
|
||||
new PasswordProvider(Options.Password)
|
||||
);
|
||||
}
|
||||
foreach (var entry in group)
|
||||
{
|
||||
_currentItem = entry.FilePart.Header;
|
||||
yield return entry;
|
||||
}
|
||||
_currentEntry = entry;
|
||||
yield return entry;
|
||||
}
|
||||
}
|
||||
|
||||
protected override EntryStream GetEntryStream() =>
|
||||
CreateEntryStream(
|
||||
new ReadOnlySubStream(
|
||||
_currentStream.NotNull("currentStream is not null"),
|
||||
_currentItem?.Size ?? 0
|
||||
protected override EntryStream GetEntryStream()
|
||||
{
|
||||
var entry = _currentEntry.NotNull("currentEntry is not null");
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
return CreateEntryStream(Stream.Null);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WORKAROUND: Forces async operations to use synchronous equivalents.
|
||||
/// This is necessary because the LZMA decoder has bugs in its async implementation
|
||||
/// that cause state corruption (IndexOutOfRangeException, DataErrorException).
|
||||
///
|
||||
/// The proper fix would be to repair the LZMA decoder's async methods
|
||||
/// (LzmaStream.ReadAsync, Decoder.CodeAsync, OutWindow async operations),
|
||||
/// but that requires deep changes to the decoder state machine.
|
||||
/// </summary>
|
||||
private sealed class SyncOnlyStream : Stream
|
||||
{
|
||||
private readonly Stream _baseStream;
|
||||
|
||||
public SyncOnlyStream(Stream baseStream) => _baseStream = baseStream;
|
||||
|
||||
public override bool CanRead => _baseStream.CanRead;
|
||||
public override bool CanSeek => _baseStream.CanSeek;
|
||||
public override bool CanWrite => _baseStream.CanWrite;
|
||||
public override long Length => _baseStream.Length;
|
||||
public override long Position
|
||||
{
|
||||
get => _baseStream.Position;
|
||||
set => _baseStream.Position = value;
|
||||
}
|
||||
|
||||
public override void Flush() => _baseStream.Flush();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
_baseStream.Read(buffer, offset, count);
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) =>
|
||||
_baseStream.Seek(offset, origin);
|
||||
|
||||
public override void SetLength(long value) => _baseStream.SetLength(value);
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
_baseStream.Write(buffer, offset, count);
|
||||
|
||||
// Force async operations to use sync equivalents to avoid LZMA decoder bugs
|
||||
public override Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(_baseStream.Read(buffer, offset, count));
|
||||
}
|
||||
|
||||
public override Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_baseStream.Write(buffer, offset, count);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_baseStream.Flush();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new ValueTask<int>(_baseStream.Read(buffer.Span));
|
||||
}
|
||||
|
||||
public override ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_baseStream.Write(buffer.Span);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_baseStream.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
private class PasswordProvider : IPasswordProvider
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: CLSCompliant(true)]
|
||||
// CLSCompliant(false) is required because ZStandard integration uses unsafe code
|
||||
[assembly: CLSCompliant(false)]
|
||||
[assembly: InternalsVisibleTo(
|
||||
"SharpCompress.Test,PublicKey=0024000004800000940000000602000000240000525341310004000001000100158bebf1433f76dffc356733c138babea7a47536c65ed8009b16372c6f4edbb20554db74a62687f56b97c20a6ce8c4b123280279e33c894e7b3aa93ab3c573656fde4db576cfe07dba09619ead26375b25d2c4a8e43f7be257d712b0dd2eb546f67adb09281338618a58ac834fc038dd7e2740a7ab3591826252e4f4516306dc"
|
||||
)]
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ namespace SharpCompress.Common.Arc
|
||||
return value switch
|
||||
{
|
||||
1 or 2 => CompressionType.None,
|
||||
3 => CompressionType.RLE90,
|
||||
3 => CompressionType.Packed,
|
||||
4 => CompressionType.Squeezed,
|
||||
5 or 6 or 7 or 8 => CompressionType.Crunched,
|
||||
9 => CompressionType.Squashed,
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace SharpCompress.Common.Arc
|
||||
Header.CompressedSize
|
||||
);
|
||||
break;
|
||||
case CompressionType.RLE90:
|
||||
case CompressionType.Packed:
|
||||
compressedStream = new RunLength90Stream(
|
||||
_stream,
|
||||
(int)Header.CompressedSize
|
||||
@@ -54,6 +54,14 @@ namespace SharpCompress.Common.Arc
|
||||
compressedStream = new SqueezeStream(_stream, (int)Header.CompressedSize);
|
||||
break;
|
||||
case CompressionType.Crunched:
|
||||
if (Header.OriginalSize > 128 * 1024)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"CompressionMethod: "
|
||||
+ Header.CompressionMethod
|
||||
+ " with size > 128KB"
|
||||
);
|
||||
}
|
||||
compressedStream = new ArcLzwStream(
|
||||
_stream,
|
||||
(int)Header.CompressedSize,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public class ArchiveExtractionEventArgs<T> : EventArgs
|
||||
{
|
||||
internal ArchiveExtractionEventArgs(T entry) => Item = entry;
|
||||
|
||||
public T Item { get; }
|
||||
}
|
||||
@@ -9,4 +9,5 @@ public enum ArchiveType
|
||||
GZip,
|
||||
Arc,
|
||||
Arj,
|
||||
Ace,
|
||||
}
|
||||
|
||||
@@ -38,6 +38,22 @@ namespace SharpCompress.Common.Arj
|
||||
Header.CompressedSize
|
||||
);
|
||||
break;
|
||||
case CompressionMethod.CompressedMost:
|
||||
case CompressionMethod.Compressed:
|
||||
case CompressionMethod.CompressedFaster:
|
||||
if (Header.OriginalSize > 128 * 1024)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"CompressionMethod: "
|
||||
+ Header.CompressionMethod
|
||||
+ " with size > 128KB"
|
||||
);
|
||||
}
|
||||
compressedStream = new LhaStream<Lh7DecoderCfg>(
|
||||
_stream,
|
||||
(int)Header.OriginalSize
|
||||
);
|
||||
break;
|
||||
case CompressionMethod.CompressedFastest:
|
||||
compressedStream = new LHDecoderStream(_stream, (int)Header.OriginalSize);
|
||||
break;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public sealed class CompressedBytesReadEventArgs : EventArgs
|
||||
{
|
||||
public CompressedBytesReadEventArgs(
|
||||
long compressedBytesRead,
|
||||
long currentFilePartCompressedBytesRead
|
||||
)
|
||||
{
|
||||
CompressedBytesRead = compressedBytesRead;
|
||||
CurrentFilePartCompressedBytesRead = currentFilePartCompressedBytesRead;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compressed bytes read for the current entry
|
||||
/// </summary>
|
||||
public long CompressedBytesRead { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current file part read for Multipart files (e.g. Rar)
|
||||
/// </summary>
|
||||
public long CurrentFilePartCompressedBytesRead { get; }
|
||||
}
|
||||
@@ -23,11 +23,12 @@ public enum CompressionType
|
||||
Reduce4,
|
||||
Explode,
|
||||
Squeezed,
|
||||
RLE90,
|
||||
Packed,
|
||||
Crunched,
|
||||
Squashed,
|
||||
Crushed,
|
||||
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;
|
||||
}
|
||||
@@ -128,7 +128,7 @@ internal static class ExtractionMethods
|
||||
IEntry entry,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
Func<string, ExtractionOptions?, Task> writeAsync,
|
||||
Func<string, ExtractionOptions?, CancellationToken, Task> writeAsync,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
@@ -189,7 +189,7 @@ internal static class ExtractionMethods
|
||||
"Entry is trying to write a file outside of the destination directory."
|
||||
);
|
||||
}
|
||||
await writeAsync(destinationFileName, options).ConfigureAwait(false);
|
||||
await writeAsync(destinationFileName, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (options.ExtractFullPath && !Directory.Exists(destinationFileName))
|
||||
{
|
||||
@@ -201,7 +201,7 @@ internal static class ExtractionMethods
|
||||
IEntry entry,
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options,
|
||||
Func<string, FileMode, Task> openAndWriteAsync,
|
||||
Func<string, FileMode, CancellationToken, Task> openAndWriteAsync,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
@@ -225,7 +225,8 @@ internal static class ExtractionMethods
|
||||
fm = FileMode.CreateNew;
|
||||
}
|
||||
|
||||
await openAndWriteAsync(destinationFileName, fm).ConfigureAwait(false);
|
||||
await openAndWriteAsync(destinationFileName, fm, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
entry.PreserveExtractionOptions(destinationFileName, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public sealed class FilePartExtractionBeginEventArgs : EventArgs
|
||||
{
|
||||
public FilePartExtractionBeginEventArgs(string name, long size, long compressedSize)
|
||||
{
|
||||
Name = name;
|
||||
Size = size;
|
||||
CompressedSize = compressedSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// File name for the part for the current entry
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Uncompressed size of the current entry in the part
|
||||
/// </summary>
|
||||
public long Size { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compressed size of the current entry in the part
|
||||
/// </summary>
|
||||
public long CompressedSize { get; }
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public interface IExtractionListener
|
||||
{
|
||||
void FireFilePartExtractionBegin(string name, long size, long compressedSize);
|
||||
void FireCompressedBytesRead(long currentPartCompressedBytes, long compressedReadBytes);
|
||||
}
|
||||
43
src/SharpCompress/Common/ProgressReport.cs
Normal file
43
src/SharpCompress/Common/ProgressReport.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents progress information for compression or extraction operations.
|
||||
/// </summary>
|
||||
public sealed class ProgressReport
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProgressReport"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entryPath">The path of the entry being processed.</param>
|
||||
/// <param name="bytesTransferred">Number of bytes transferred so far.</param>
|
||||
/// <param name="totalBytes">Total bytes to be transferred, or null if unknown.</param>
|
||||
public ProgressReport(string entryPath, long bytesTransferred, long? totalBytes)
|
||||
{
|
||||
EntryPath = entryPath;
|
||||
BytesTransferred = bytesTransferred;
|
||||
TotalBytes = totalBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the entry being processed.
|
||||
/// </summary>
|
||||
public string EntryPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of bytes transferred so far.
|
||||
/// </summary>
|
||||
public long BytesTransferred { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of bytes to be transferred, or null if unknown.
|
||||
/// </summary>
|
||||
public long? TotalBytes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the progress percentage (0-100), or null if total bytes is unknown.
|
||||
/// </summary>
|
||||
public double? PercentComplete =>
|
||||
TotalBytes.HasValue && TotalBytes.Value > 0
|
||||
? (double)BytesTransferred / TotalBytes.Value * 100
|
||||
: null;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public sealed class ReaderExtractionEventArgs<T> : EventArgs
|
||||
{
|
||||
internal ReaderExtractionEventArgs(T entry, ReaderProgress? readerProgress = null)
|
||||
{
|
||||
Item = entry;
|
||||
ReaderProgress = readerProgress;
|
||||
}
|
||||
|
||||
public T Item { get; }
|
||||
|
||||
public ReaderProgress? ReaderProgress { get; }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
@@ -9,8 +10,16 @@ internal sealed class TarHeader
|
||||
{
|
||||
internal static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public TarHeader(ArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
|
||||
public TarHeader(
|
||||
ArchiveEncoding archiveEncoding,
|
||||
TarHeaderWriteFormat writeFormat = TarHeaderWriteFormat.GNU_TAR_LONG_LINK
|
||||
)
|
||||
{
|
||||
ArchiveEncoding = archiveEncoding;
|
||||
WriteFormat = writeFormat;
|
||||
}
|
||||
|
||||
internal TarHeaderWriteFormat WriteFormat { get; set; }
|
||||
internal string? Name { get; set; }
|
||||
internal string? LinkName { get; set; }
|
||||
|
||||
@@ -25,7 +34,119 @@ internal sealed class TarHeader
|
||||
|
||||
internal const int BLOCK_SIZE = 512;
|
||||
|
||||
// Maximum size for long name/link headers to prevent memory exhaustion attacks
|
||||
// This is generous enough for most real-world scenarios (32KB)
|
||||
private const int MAX_LONG_NAME_SIZE = 32768;
|
||||
|
||||
internal void Write(Stream output)
|
||||
{
|
||||
switch (WriteFormat)
|
||||
{
|
||||
case TarHeaderWriteFormat.GNU_TAR_LONG_LINK:
|
||||
WriteGnuTarLongLink(output);
|
||||
break;
|
||||
case TarHeaderWriteFormat.USTAR:
|
||||
WriteUstar(output);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("This should be impossible...");
|
||||
}
|
||||
}
|
||||
|
||||
internal void WriteUstar(Stream output)
|
||||
{
|
||||
var buffer = new byte[BLOCK_SIZE];
|
||||
|
||||
WriteOctalBytes(511, buffer, 100, 8); // file mode
|
||||
WriteOctalBytes(0, buffer, 108, 8); // owner ID
|
||||
WriteOctalBytes(0, buffer, 116, 8); // group ID
|
||||
|
||||
//ArchiveEncoding.UTF8.GetBytes("magic").CopyTo(buffer, 257);
|
||||
var nameByteCount = ArchiveEncoding
|
||||
.GetEncoding()
|
||||
.GetByteCount(Name.NotNull("Name is null"));
|
||||
|
||||
if (nameByteCount > 100)
|
||||
{
|
||||
// if name is longer, try to split it into name and namePrefix
|
||||
|
||||
string fullName = Name.NotNull("Name is null");
|
||||
|
||||
// find all directory separators
|
||||
List<int> dirSeps = new List<int>();
|
||||
for (int i = 0; i < fullName.Length; i++)
|
||||
{
|
||||
if (fullName[i] == Path.DirectorySeparatorChar)
|
||||
{
|
||||
dirSeps.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// find the right place to split the name
|
||||
int splitIndex = -1;
|
||||
for (int i = 0; i < dirSeps.Count; i++)
|
||||
{
|
||||
int count = ArchiveEncoding
|
||||
.GetEncoding()
|
||||
.GetByteCount(fullName.Substring(0, dirSeps[i]));
|
||||
if (count < 155)
|
||||
{
|
||||
splitIndex = dirSeps[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (splitIndex == -1)
|
||||
{
|
||||
throw new Exception(
|
||||
$"Tar header USTAR format can not fit file name \"{fullName}\" of length {nameByteCount}! Directory separator not found! Try using GNU Tar format instead!"
|
||||
);
|
||||
}
|
||||
|
||||
string namePrefix = fullName.Substring(0, splitIndex);
|
||||
string name = fullName.Substring(splitIndex + 1);
|
||||
|
||||
if (this.ArchiveEncoding.GetEncoding().GetByteCount(namePrefix) >= 155)
|
||||
throw new Exception(
|
||||
$"Tar header USTAR format can not fit file name \"{fullName}\" of length {nameByteCount}! Try using GNU Tar format instead!"
|
||||
);
|
||||
|
||||
if (this.ArchiveEncoding.GetEncoding().GetByteCount(name) >= 100)
|
||||
throw new Exception(
|
||||
$"Tar header USTAR format can not fit file name \"{fullName}\" of length {nameByteCount}! Try using GNU Tar format instead!"
|
||||
);
|
||||
|
||||
// write name prefix
|
||||
WriteStringBytes(ArchiveEncoding.Encode(namePrefix), buffer, 345, 100);
|
||||
// write partial name
|
||||
WriteStringBytes(ArchiveEncoding.Encode(name), buffer, 100);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteStringBytes(ArchiveEncoding.Encode(Name.NotNull("Name is null")), buffer, 100);
|
||||
}
|
||||
|
||||
WriteOctalBytes(Size, buffer, 124, 12);
|
||||
var time = (long)(LastModifiedTime.ToUniversalTime() - EPOCH).TotalSeconds;
|
||||
WriteOctalBytes(time, buffer, 136, 12);
|
||||
buffer[156] = (byte)EntryType;
|
||||
|
||||
// write ustar magic field
|
||||
WriteStringBytes(Encoding.ASCII.GetBytes("ustar"), buffer, 257, 6);
|
||||
// write ustar version "00"
|
||||
buffer[263] = 0x30;
|
||||
buffer[264] = 0x30;
|
||||
|
||||
var crc = RecalculateChecksum(buffer);
|
||||
WriteOctalBytes(crc, buffer, 148, 8);
|
||||
|
||||
output.Write(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
internal void WriteGnuTarLongLink(Stream output)
|
||||
{
|
||||
var buffer = new byte[BLOCK_SIZE];
|
||||
|
||||
@@ -81,7 +202,7 @@ internal sealed class TarHeader
|
||||
0,
|
||||
100 - ArchiveEncoding.GetEncoding().GetMaxByteCount(1)
|
||||
);
|
||||
Write(output);
|
||||
WriteGnuTarLongLink(output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +307,15 @@ internal sealed class TarHeader
|
||||
private string ReadLongName(BinaryReader reader, byte[] buffer)
|
||||
{
|
||||
var size = ReadSize(buffer);
|
||||
|
||||
// Validate size to prevent memory exhaustion from malformed headers
|
||||
if (size < 0 || size > MAX_LONG_NAME_SIZE)
|
||||
{
|
||||
throw new InvalidFormatException(
|
||||
$"Long name size {size} is invalid or exceeds maximum allowed size of {MAX_LONG_NAME_SIZE} bytes"
|
||||
);
|
||||
}
|
||||
|
||||
var nameLength = (int)size;
|
||||
var nameBytes = reader.ReadBytes(nameLength);
|
||||
var remainingBytesToRead = BLOCK_SIZE - (nameLength % BLOCK_SIZE);
|
||||
@@ -228,6 +358,18 @@ internal sealed class TarHeader
|
||||
buffer.Slice(i, length - i).Clear();
|
||||
}
|
||||
|
||||
private static void WriteStringBytes(
|
||||
ReadOnlySpan<byte> name,
|
||||
Span<byte> buffer,
|
||||
int offset,
|
||||
int length
|
||||
)
|
||||
{
|
||||
name.CopyTo(buffer.Slice(offset));
|
||||
var i = Math.Min(length, name.Length);
|
||||
buffer.Slice(offset + i, length - i).Clear();
|
||||
}
|
||||
|
||||
private static void WriteStringBytes(string name, byte[] buffer, int offset, int length)
|
||||
{
|
||||
int i;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SharpCompress.Common.Tar.Headers;
|
||||
|
||||
public enum TarHeaderWriteFormat
|
||||
{
|
||||
GNU_TAR_LONG_LINK,
|
||||
USTAR,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpCompress.Common.Zip;
|
||||
|
||||
@@ -21,6 +22,22 @@ internal class WinzipAesEncryptionData
|
||||
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
var rfc2898 = new Rfc2898DeriveBytes(password, salt, RFC2898_ITERATIONS);
|
||||
KeyBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
IvBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
var generatedVerifyValue = rfc2898.GetBytes(2);
|
||||
#elif NET10_0_OR_GREATER
|
||||
var derivedKeySize = (KeySizeInBytes * 2) + 2;
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
var derivedKey = Rfc2898DeriveBytes.Pbkdf2(
|
||||
passwordBytes,
|
||||
salt,
|
||||
RFC2898_ITERATIONS,
|
||||
HashAlgorithmName.SHA1,
|
||||
derivedKeySize
|
||||
);
|
||||
KeyBytes = derivedKey.AsSpan(0, KeySizeInBytes).ToArray();
|
||||
IvBytes = derivedKey.AsSpan(KeySizeInBytes, KeySizeInBytes).ToArray();
|
||||
var generatedVerifyValue = derivedKey.AsSpan((KeySizeInBytes * 2), 2).ToArray();
|
||||
#else
|
||||
var rfc2898 = new Rfc2898DeriveBytes(
|
||||
password,
|
||||
@@ -28,11 +45,10 @@ internal class WinzipAesEncryptionData
|
||||
RFC2898_ITERATIONS,
|
||||
HashAlgorithmName.SHA1
|
||||
);
|
||||
#endif
|
||||
|
||||
KeyBytes = rfc2898.GetBytes(KeySizeInBytes); // 16 or 24 or 32 ???
|
||||
KeyBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
IvBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
var generatedVerifyValue = rfc2898.GetBytes(2);
|
||||
#endif
|
||||
|
||||
var verify = BinaryPrimitives.ReadInt16LittleEndian(passwordVerifyValue);
|
||||
var generated = BinaryPrimitives.ReadInt16LittleEndian(generatedVerifyValue);
|
||||
|
||||
@@ -13,8 +13,8 @@ using SharpCompress.Compressors.PPMd;
|
||||
using SharpCompress.Compressors.Reduce;
|
||||
using SharpCompress.Compressors.Shrink;
|
||||
using SharpCompress.Compressors.Xz;
|
||||
using SharpCompress.Compressors.ZStandard;
|
||||
using SharpCompress.IO;
|
||||
using ZstdSharp;
|
||||
|
||||
namespace SharpCompress.Common.Zip;
|
||||
|
||||
|
||||
@@ -4,56 +4,68 @@ using System.IO;
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
[CLSCompliant(true)]
|
||||
public sealed class BitReader
|
||||
public class BitReader
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
private int _bitBuffer;
|
||||
private int _bitsRemaining;
|
||||
private bool _disposed;
|
||||
private readonly Stream _input;
|
||||
private int _bitBuffer; // currently buffered bits
|
||||
private int _bitCount; // number of bits in buffer
|
||||
|
||||
public BitReader(Stream input)
|
||||
{
|
||||
_stream = input ?? throw new ArgumentNullException(nameof(input));
|
||||
if (!input.CanRead)
|
||||
throw new ArgumentException("Stream must be readable.", nameof(input));
|
||||
_input = input ?? throw new ArgumentNullException(nameof(input));
|
||||
_bitBuffer = 0;
|
||||
_bitCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a single bit from the stream. Returns 0 or 1.
|
||||
/// </summary>
|
||||
public int ReadBit()
|
||||
{
|
||||
if (_bitCount == 0)
|
||||
{
|
||||
int nextByte = _input.ReadByte();
|
||||
if (nextByte < 0)
|
||||
{
|
||||
throw new EndOfStreamException("No more data available in BitReader.");
|
||||
}
|
||||
|
||||
_bitBuffer = nextByte;
|
||||
_bitCount = 8;
|
||||
}
|
||||
|
||||
int bit = (_bitBuffer >> (_bitCount - 1)) & 1;
|
||||
_bitCount--;
|
||||
return bit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads n bits (up to 32) from the stream.
|
||||
/// </summary>
|
||||
public int ReadBits(int count)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(BitReader));
|
||||
|
||||
if (count <= 0 || count > 32)
|
||||
if (count < 0 || count > 32)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(count),
|
||||
"Bit count must be between 1 and 32."
|
||||
"Count must be between 0 and 32."
|
||||
);
|
||||
}
|
||||
|
||||
int result = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (_bitsRemaining == 0)
|
||||
{
|
||||
int nextByte = _stream.ReadByte();
|
||||
if (nextByte == -1)
|
||||
throw new EndOfStreamException();
|
||||
|
||||
_bitBuffer = nextByte;
|
||||
_bitsRemaining = 8;
|
||||
}
|
||||
|
||||
// hoogste bit eerst
|
||||
result = (result << 1) | ((_bitBuffer >> 7) & 1);
|
||||
_bitBuffer <<= 1;
|
||||
_bitsRemaining--;
|
||||
result = (result << 1) | ReadBit();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets any buffered bits.
|
||||
/// </summary>
|
||||
public void AlignToByte()
|
||||
{
|
||||
_bitsRemaining = 0;
|
||||
_bitCount = 0;
|
||||
_bitBuffer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
43
src/SharpCompress/Compressors/Arj/HistoryIterator.cs
Normal file
43
src/SharpCompress/Compressors/Arj/HistoryIterator.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
/// <summary>
|
||||
/// Iterator that reads & pushes values back into the ring buffer.
|
||||
/// </summary>
|
||||
public class HistoryIterator : IEnumerator<byte>
|
||||
{
|
||||
private int _index;
|
||||
private readonly IRingBuffer _ring;
|
||||
|
||||
public HistoryIterator(IRingBuffer ring, int startIndex)
|
||||
{
|
||||
_ring = ring;
|
||||
_index = startIndex;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
Current = _ring[_index];
|
||||
_index = unchecked(_index + 1);
|
||||
|
||||
// Push value back into the ring buffer
|
||||
_ring.Push(Current);
|
||||
|
||||
return true; // iterator is infinite
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public byte Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
218
src/SharpCompress/Compressors/Arj/HuffmanTree.cs
Normal file
218
src/SharpCompress/Compressors/Arj/HuffmanTree.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
[CLSCompliant(true)]
|
||||
public enum NodeType
|
||||
{
|
||||
Leaf,
|
||||
Branch,
|
||||
}
|
||||
|
||||
[CLSCompliant(true)]
|
||||
public sealed class TreeEntry
|
||||
{
|
||||
public readonly NodeType Type;
|
||||
public readonly int LeafValue;
|
||||
public readonly int BranchIndex;
|
||||
|
||||
public const int MAX_INDEX = 4096;
|
||||
|
||||
private TreeEntry(NodeType type, int leafValue, int branchIndex)
|
||||
{
|
||||
Type = type;
|
||||
LeafValue = leafValue;
|
||||
BranchIndex = branchIndex;
|
||||
}
|
||||
|
||||
public static TreeEntry Leaf(int value)
|
||||
{
|
||||
return new TreeEntry(NodeType.Leaf, value, -1);
|
||||
}
|
||||
|
||||
public static TreeEntry Branch(int index)
|
||||
{
|
||||
if (index >= MAX_INDEX)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(index),
|
||||
"Branch index exceeds MAX_INDEX"
|
||||
);
|
||||
}
|
||||
return new TreeEntry(NodeType.Branch, 0, index);
|
||||
}
|
||||
}
|
||||
|
||||
[CLSCompliant(true)]
|
||||
public sealed class HuffTree
|
||||
{
|
||||
private readonly List<TreeEntry> _tree;
|
||||
|
||||
public HuffTree(int capacity = 0)
|
||||
{
|
||||
_tree = new List<TreeEntry>(capacity);
|
||||
}
|
||||
|
||||
public void SetSingle(int value)
|
||||
{
|
||||
_tree.Clear();
|
||||
_tree.Add(TreeEntry.Leaf(value));
|
||||
}
|
||||
|
||||
public void BuildTree(byte[] lengths, int count)
|
||||
{
|
||||
if (lengths == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(lengths));
|
||||
}
|
||||
|
||||
if (count < 0 || count > lengths.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
|
||||
if (count > TreeEntry.MAX_INDEX / 2)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Count exceeds maximum allowed: {TreeEntry.MAX_INDEX / 2}"
|
||||
);
|
||||
}
|
||||
byte[] slice = new byte[count];
|
||||
Array.Copy(lengths, slice, count);
|
||||
|
||||
BuildTree(slice);
|
||||
}
|
||||
|
||||
public void BuildTree(byte[] valueLengths)
|
||||
{
|
||||
if (valueLengths == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(valueLengths));
|
||||
}
|
||||
|
||||
if (valueLengths.Length > TreeEntry.MAX_INDEX / 2)
|
||||
{
|
||||
throw new InvalidOperationException("Too many code lengths");
|
||||
}
|
||||
|
||||
_tree.Clear();
|
||||
|
||||
int maxAllocated = 1; // start with a single (root) node
|
||||
|
||||
for (byte currentLen = 1; ; currentLen++)
|
||||
{
|
||||
// add missing branches up to current limit
|
||||
int maxLimit = maxAllocated;
|
||||
|
||||
for (int i = _tree.Count; i < maxLimit; i++)
|
||||
{
|
||||
// TreeEntry.Branch may throw if index too large
|
||||
try
|
||||
{
|
||||
_tree.Add(TreeEntry.Branch(maxAllocated));
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
_tree.Clear();
|
||||
throw new InvalidOperationException("Branch index exceeds limit", e);
|
||||
}
|
||||
|
||||
// each branch node allocates two children
|
||||
maxAllocated += 2;
|
||||
}
|
||||
|
||||
// fill tree with leaves found in the lengths table at the current length
|
||||
bool moreLeaves = false;
|
||||
|
||||
for (int value = 0; value < valueLengths.Length; value++)
|
||||
{
|
||||
byte len = valueLengths[value];
|
||||
if (len == currentLen)
|
||||
{
|
||||
_tree.Add(TreeEntry.Leaf(value));
|
||||
}
|
||||
else if (len > currentLen)
|
||||
{
|
||||
moreLeaves = true; // there are more leaves to process
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check (too many leaves)
|
||||
if (_tree.Count > maxAllocated)
|
||||
{
|
||||
throw new InvalidOperationException("Too many leaves");
|
||||
}
|
||||
|
||||
// stop when no longer finding longer codes
|
||||
if (!moreLeaves)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ensure tree is complete
|
||||
if (_tree.Count != maxAllocated)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Missing some leaves: tree count = {_tree.Count}, expected = {maxAllocated}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public int ReadEntry(BitReader reader)
|
||||
{
|
||||
if (_tree.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Tree not initialized");
|
||||
}
|
||||
|
||||
TreeEntry node = _tree[0];
|
||||
while (true)
|
||||
{
|
||||
if (node.Type == NodeType.Leaf)
|
||||
{
|
||||
return node.LeafValue;
|
||||
}
|
||||
|
||||
int bit = reader.ReadBit();
|
||||
int index = node.BranchIndex + bit;
|
||||
|
||||
if (index >= _tree.Count)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid branch index during read");
|
||||
}
|
||||
|
||||
node = _tree[index];
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
|
||||
void FormatStep(int index, string prefix)
|
||||
{
|
||||
var node = _tree[index];
|
||||
if (node.Type == NodeType.Leaf)
|
||||
{
|
||||
result.AppendLine($"{prefix} -> {node.LeafValue}");
|
||||
}
|
||||
else
|
||||
{
|
||||
FormatStep(node.BranchIndex, prefix + "0");
|
||||
FormatStep(node.BranchIndex + 1, prefix + "1");
|
||||
}
|
||||
}
|
||||
|
||||
if (_tree.Count > 0)
|
||||
{
|
||||
FormatStep(0, "");
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/SharpCompress/Compressors/Arj/ILhaDecoderConfig.cs
Normal file
9
src/SharpCompress/Compressors/Arj/ILhaDecoderConfig.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
public interface ILhaDecoderConfig
|
||||
{
|
||||
int HistoryBits { get; }
|
||||
int OffsetBits { get; }
|
||||
RingBuffer RingBuffer { get; }
|
||||
}
|
||||
}
|
||||
17
src/SharpCompress/Compressors/Arj/IRingBuffer.cs
Normal file
17
src/SharpCompress/Compressors/Arj/IRingBuffer.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
public interface IRingBuffer
|
||||
{
|
||||
int BufferSize { get; }
|
||||
|
||||
int Cursor { get; }
|
||||
void SetCursor(int pos);
|
||||
|
||||
void Push(byte value);
|
||||
|
||||
HistoryIterator IterFromOffset(int offset);
|
||||
HistoryIterator IterFromPos(int pos);
|
||||
|
||||
byte this[int index] { get; }
|
||||
}
|
||||
}
|
||||
9
src/SharpCompress/Compressors/Arj/Lh5DecoderCfg.cs
Normal file
9
src/SharpCompress/Compressors/Arj/Lh5DecoderCfg.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
public class Lh5DecoderCfg : ILhaDecoderConfig
|
||||
{
|
||||
public int HistoryBits => 14;
|
||||
public int OffsetBits => 4;
|
||||
public RingBuffer RingBuffer { get; } = new RingBuffer(1 << 14);
|
||||
}
|
||||
}
|
||||
9
src/SharpCompress/Compressors/Arj/Lh7DecoderCfg.cs
Normal file
9
src/SharpCompress/Compressors/Arj/Lh7DecoderCfg.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
public class Lh7DecoderCfg : ILhaDecoderConfig
|
||||
{
|
||||
public int HistoryBits => 17;
|
||||
public int OffsetBits => 5;
|
||||
public RingBuffer RingBuffer { get; } = new RingBuffer(1 << 17);
|
||||
}
|
||||
}
|
||||
363
src/SharpCompress/Compressors/Arj/LhaStream.cs
Normal file
363
src/SharpCompress/Compressors/Arj/LhaStream.cs
Normal file
@@ -0,0 +1,363 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
[CLSCompliant(true)]
|
||||
public sealed class LhaStream<C> : Stream, IStreamStack
|
||||
where C : ILhaDecoderConfig, new()
|
||||
{
|
||||
private readonly BitReader _bitReader;
|
||||
private readonly Stream _stream;
|
||||
|
||||
private readonly HuffTree _commandTree;
|
||||
private readonly HuffTree _offsetTree;
|
||||
private int _remainingCommands;
|
||||
private (int offset, int count)? _copyProgress;
|
||||
private readonly RingBuffer _ringBuffer;
|
||||
private readonly C _config = new C();
|
||||
|
||||
private const int NUM_COMMANDS = 510;
|
||||
private const int NUM_TEMP_CODELEN = 20;
|
||||
|
||||
private readonly int _originalSize;
|
||||
private int _producedBytes = 0;
|
||||
|
||||
#if DEBUG_STREAMS
|
||||
long IStreamStack.InstanceId { get; set; }
|
||||
#endif
|
||||
int IStreamStack.DefaultBufferSize { get; set; }
|
||||
|
||||
Stream IStreamStack.BaseStream() => _stream;
|
||||
|
||||
int IStreamStack.BufferSize
|
||||
{
|
||||
get => 0;
|
||||
set { }
|
||||
}
|
||||
int IStreamStack.BufferPosition
|
||||
{
|
||||
get => 0;
|
||||
set { }
|
||||
}
|
||||
|
||||
void IStreamStack.SetPosition(long position) { }
|
||||
|
||||
public LhaStream(Stream compressedStream, int originalSize)
|
||||
{
|
||||
_stream = compressedStream ?? throw new ArgumentNullException(nameof(compressedStream));
|
||||
_bitReader = new BitReader(compressedStream);
|
||||
_ringBuffer = _config.RingBuffer;
|
||||
_commandTree = new HuffTree(NUM_COMMANDS * 2);
|
||||
_offsetTree = new HuffTree(NUM_TEMP_CODELEN * 2);
|
||||
_remainingCommands = 0;
|
||||
_copyProgress = null;
|
||||
_originalSize = originalSize;
|
||||
}
|
||||
|
||||
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() { }
|
||||
|
||||
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();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (buffer == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
}
|
||||
if (offset < 0 || count < 0 || (offset + count) > buffer.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
if (_producedBytes >= _originalSize)
|
||||
{
|
||||
return 0; // EOF
|
||||
}
|
||||
if (count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int bytesRead = FillBuffer(buffer);
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
private byte ReadCodeLength()
|
||||
{
|
||||
byte len = (byte)_bitReader.ReadBits(3);
|
||||
if (len == 7)
|
||||
{
|
||||
while (_bitReader.ReadBit() != 0)
|
||||
{
|
||||
len++;
|
||||
if (len > 255)
|
||||
{
|
||||
throw new InvalidOperationException("Code length overflow");
|
||||
}
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
private int ReadCodeSkip(int skipRange)
|
||||
{
|
||||
int bits;
|
||||
int increment;
|
||||
|
||||
switch (skipRange)
|
||||
{
|
||||
case 0:
|
||||
return 1;
|
||||
case 1:
|
||||
bits = 4;
|
||||
increment = 3; // 3..=18
|
||||
break;
|
||||
default:
|
||||
bits = 9;
|
||||
increment = 20; // 20..=531
|
||||
break;
|
||||
}
|
||||
|
||||
int skip = _bitReader.ReadBits(bits);
|
||||
return skip + increment;
|
||||
}
|
||||
|
||||
private void ReadTempTree()
|
||||
{
|
||||
byte[] codeLengths = new byte[NUM_TEMP_CODELEN];
|
||||
|
||||
// number of codes to read (5 bits)
|
||||
int numCodes = _bitReader.ReadBits(5);
|
||||
|
||||
// single code only
|
||||
if (numCodes == 0)
|
||||
{
|
||||
int code = _bitReader.ReadBits(5);
|
||||
_offsetTree.SetSingle((byte)code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (numCodes > NUM_TEMP_CODELEN)
|
||||
{
|
||||
throw new Exception("temporary codelen table has invalid size");
|
||||
}
|
||||
|
||||
// read actual lengths
|
||||
int count = Math.Min(3, numCodes);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
codeLengths[i] = (byte)ReadCodeLength();
|
||||
}
|
||||
|
||||
// 2-bit skip value follows
|
||||
int skip = _bitReader.ReadBits(2);
|
||||
|
||||
if (3 + skip > numCodes)
|
||||
{
|
||||
throw new Exception("temporary codelen table has invalid size");
|
||||
}
|
||||
|
||||
for (int i = 3 + skip; i < numCodes; i++)
|
||||
{
|
||||
codeLengths[i] = (byte)ReadCodeLength();
|
||||
}
|
||||
|
||||
_offsetTree.BuildTree(codeLengths, numCodes);
|
||||
}
|
||||
|
||||
private void ReadCommandTree()
|
||||
{
|
||||
byte[] codeLengths = new byte[NUM_COMMANDS];
|
||||
|
||||
// number of codes to read (9 bits)
|
||||
int numCodes = _bitReader.ReadBits(9);
|
||||
|
||||
// single code only
|
||||
if (numCodes == 0)
|
||||
{
|
||||
int code = _bitReader.ReadBits(9);
|
||||
_commandTree.SetSingle((ushort)code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (numCodes > NUM_COMMANDS)
|
||||
{
|
||||
throw new Exception("commands codelen table has invalid size");
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
while (index < numCodes)
|
||||
{
|
||||
for (int n = 0; n < numCodes - index; n++)
|
||||
{
|
||||
int code = _offsetTree.ReadEntry(_bitReader);
|
||||
|
||||
if (code >= 0 && code <= 2) // skip range
|
||||
{
|
||||
int skipCount = ReadCodeSkip(code);
|
||||
index += n + skipCount;
|
||||
goto outerLoop;
|
||||
}
|
||||
else
|
||||
{
|
||||
codeLengths[index + n] = (byte)(code - 2);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
outerLoop:
|
||||
;
|
||||
}
|
||||
|
||||
_commandTree.BuildTree(codeLengths, numCodes);
|
||||
}
|
||||
|
||||
private void ReadOffsetTree()
|
||||
{
|
||||
int numCodes = _bitReader.ReadBits(_config.OffsetBits);
|
||||
if (numCodes == 0)
|
||||
{
|
||||
int code = _bitReader.ReadBits(_config.OffsetBits);
|
||||
_offsetTree.SetSingle(code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (numCodes > _config.HistoryBits)
|
||||
{
|
||||
throw new InvalidDataException("Offset code table too large");
|
||||
}
|
||||
|
||||
byte[] codeLengths = new byte[NUM_TEMP_CODELEN];
|
||||
for (int i = 0; i < numCodes; i++)
|
||||
{
|
||||
codeLengths[i] = (byte)ReadCodeLength();
|
||||
}
|
||||
|
||||
_offsetTree.BuildTree(codeLengths, numCodes);
|
||||
}
|
||||
|
||||
private void BeginNewBlock()
|
||||
{
|
||||
ReadTempTree();
|
||||
ReadCommandTree();
|
||||
ReadOffsetTree();
|
||||
}
|
||||
|
||||
private int ReadCommand() => _commandTree.ReadEntry(_bitReader);
|
||||
|
||||
private int ReadOffset()
|
||||
{
|
||||
int bits = _offsetTree.ReadEntry(_bitReader);
|
||||
if (bits <= 1)
|
||||
{
|
||||
return bits;
|
||||
}
|
||||
|
||||
int res = _bitReader.ReadBits(bits - 1);
|
||||
return res | (1 << (bits - 1));
|
||||
}
|
||||
|
||||
private int CopyFromHistory(byte[] target, int targetIndex, int offset, int count)
|
||||
{
|
||||
var historyIter = _ringBuffer.IterFromOffset(offset);
|
||||
int copied = 0;
|
||||
|
||||
while (
|
||||
copied < count && historyIter.MoveNext() && (targetIndex + copied) < target.Length
|
||||
)
|
||||
{
|
||||
target[targetIndex + copied] = historyIter.Current;
|
||||
copied++;
|
||||
}
|
||||
|
||||
if (copied < count)
|
||||
{
|
||||
_copyProgress = (offset, count - copied);
|
||||
}
|
||||
|
||||
return copied;
|
||||
}
|
||||
|
||||
public int FillBuffer(byte[] buffer)
|
||||
{
|
||||
int bufLen = buffer.Length;
|
||||
int bufIndex = 0;
|
||||
|
||||
// stop when we reached original size
|
||||
if (_producedBytes >= _originalSize)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// calculate limit, so that we don't go over the original size
|
||||
int remaining = (int)Math.Min(bufLen, _originalSize - _producedBytes);
|
||||
|
||||
while (bufIndex < remaining)
|
||||
{
|
||||
if (_copyProgress.HasValue)
|
||||
{
|
||||
var (offset, count) = _copyProgress.Value;
|
||||
int copied = CopyFromHistory(
|
||||
buffer,
|
||||
bufIndex,
|
||||
offset,
|
||||
(int)Math.Min(count, remaining - bufIndex)
|
||||
);
|
||||
bufIndex += copied;
|
||||
_copyProgress = null;
|
||||
}
|
||||
|
||||
if (_remainingCommands == 0)
|
||||
{
|
||||
_remainingCommands = _bitReader.ReadBits(16);
|
||||
if (bufIndex + _remainingCommands > remaining)
|
||||
{
|
||||
break;
|
||||
}
|
||||
BeginNewBlock();
|
||||
}
|
||||
|
||||
_remainingCommands--;
|
||||
|
||||
int command = ReadCommand();
|
||||
|
||||
if (command >= 0 && command <= 0xFF)
|
||||
{
|
||||
byte value = (byte)command;
|
||||
buffer[bufIndex++] = value;
|
||||
_ringBuffer.Push(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
int count = command - 0x100 + 3;
|
||||
int offset = ReadOffset();
|
||||
int copyCount = (int)Math.Min(count, remaining - bufIndex);
|
||||
bufIndex += CopyFromHistory(buffer, bufIndex, offset, copyCount);
|
||||
}
|
||||
}
|
||||
|
||||
_producedBytes += bufIndex;
|
||||
return bufIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/SharpCompress/Compressors/Arj/RingBuffer.cs
Normal file
67
src/SharpCompress/Compressors/Arj/RingBuffer.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
/// <summary>
|
||||
/// A fixed-size ring buffer where N must be a power of two.
|
||||
/// </summary>
|
||||
public class RingBuffer : IRingBuffer
|
||||
{
|
||||
private readonly byte[] _buffer;
|
||||
private int _cursor;
|
||||
|
||||
public int BufferSize { get; }
|
||||
|
||||
public int Cursor => _cursor;
|
||||
|
||||
private readonly int _mask;
|
||||
|
||||
public RingBuffer(int size)
|
||||
{
|
||||
if ((size & (size - 1)) != 0)
|
||||
{
|
||||
throw new ArgumentException("RingArrayBuffer size must be a power of two");
|
||||
}
|
||||
|
||||
BufferSize = size;
|
||||
_buffer = new byte[size];
|
||||
_cursor = 0;
|
||||
_mask = size - 1;
|
||||
|
||||
// Fill with spaces
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
_buffer[i] = (byte)' ';
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCursor(int pos)
|
||||
{
|
||||
_cursor = pos & _mask;
|
||||
}
|
||||
|
||||
public void Push(byte value)
|
||||
{
|
||||
int index = _cursor;
|
||||
_buffer[index & _mask] = value;
|
||||
_cursor = (index + 1) & _mask;
|
||||
}
|
||||
|
||||
public byte this[int index] => _buffer[index & _mask];
|
||||
|
||||
public HistoryIterator IterFromOffset(int offset)
|
||||
{
|
||||
int masked = (offset & _mask) + 1;
|
||||
int startIndex = _cursor + BufferSize - masked;
|
||||
return new HistoryIterator(this, startIndex);
|
||||
}
|
||||
|
||||
public HistoryIterator IterFromPos(int pos)
|
||||
{
|
||||
int startIndex = pos & _mask;
|
||||
return new HistoryIterator(this, startIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -544,6 +549,12 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
|
||||
|
||||
private void EndBlock()
|
||||
{
|
||||
// Skip block processing for empty input (no data written)
|
||||
if (last < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
blockCRC = mCrc.GetFinalCRC();
|
||||
combinedCRC = (combinedCRC << 1) | (int)(((uint)combinedCRC) >> 31);
|
||||
combinedCRC ^= blockCRC;
|
||||
|
||||
@@ -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,10 +445,12 @@ 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.ReadAsync(controlBuffer, 0, 1, cancellationToken).ConfigureAwait(false);
|
||||
await _inputStream
|
||||
.ReadExactlyAsync(controlBuffer, 0, 1, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var control = controlBuffer[0];
|
||||
_inputPosition++;
|
||||
|
||||
@@ -455,11 +477,15 @@ public class LzmaStream : Stream, IStreamStack
|
||||
|
||||
_availableBytes = (control & 0x1F) << 16;
|
||||
var buffer = new byte[2];
|
||||
await _inputStream.ReadAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false);
|
||||
await _inputStream
|
||||
.ReadExactlyAsync(buffer, 0, 2, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_availableBytes += (buffer[0] << 8) + buffer[1] + 1;
|
||||
_inputPosition += 2;
|
||||
|
||||
await _inputStream.ReadAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false);
|
||||
await _inputStream
|
||||
.ReadExactlyAsync(buffer, 0, 2, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_rangeDecoderLimit = (buffer[0] << 8) + buffer[1] + 1;
|
||||
_inputPosition += 2;
|
||||
|
||||
@@ -467,7 +493,7 @@ public class LzmaStream : Stream, IStreamStack
|
||||
{
|
||||
_needProps = false;
|
||||
await _inputStream
|
||||
.ReadAsync(controlBuffer, 0, 1, cancellationToken)
|
||||
.ReadExactlyAsync(controlBuffer, 0, 1, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
Properties[0] = controlBuffer[0];
|
||||
_inputPosition++;
|
||||
@@ -495,7 +521,9 @@ public class LzmaStream : Stream, IStreamStack
|
||||
{
|
||||
_uncompressedChunk = true;
|
||||
var buffer = new byte[2];
|
||||
await _inputStream.ReadAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false);
|
||||
await _inputStream
|
||||
.ReadExactlyAsync(buffer, 0, 2, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_availableBytes = (buffer[0] << 8) + buffer[1] + 1;
|
||||
_inputPosition += 2;
|
||||
}
|
||||
@@ -624,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,
|
||||
|
||||
@@ -7,7 +7,7 @@ using SharpCompress.Compressors.Deflate;
|
||||
using SharpCompress.Compressors.Filters;
|
||||
using SharpCompress.Compressors.LZMA.Utilites;
|
||||
using SharpCompress.Compressors.PPMd;
|
||||
using ZstdSharp;
|
||||
using SharpCompress.Compressors.ZStandard;
|
||||
|
||||
namespace SharpCompress.Compressors.LZMA;
|
||||
|
||||
|
||||
@@ -37,18 +37,8 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
|
||||
private IEnumerator<RarFilePart> filePartEnumerator;
|
||||
private Stream currentStream;
|
||||
|
||||
private readonly IExtractionListener streamListener;
|
||||
|
||||
private long currentPartTotalReadBytes;
|
||||
private long currentEntryTotalReadBytes;
|
||||
|
||||
internal MultiVolumeReadOnlyStream(
|
||||
IEnumerable<RarFilePart> parts,
|
||||
IExtractionListener streamListener
|
||||
)
|
||||
internal MultiVolumeReadOnlyStream(IEnumerable<RarFilePart> parts)
|
||||
{
|
||||
this.streamListener = streamListener;
|
||||
|
||||
filePartEnumerator = parts.GetEnumerator();
|
||||
filePartEnumerator.MoveNext();
|
||||
InitializeNextFilePart();
|
||||
@@ -81,15 +71,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
|
||||
currentPosition = 0;
|
||||
currentStream = filePartEnumerator.Current.GetCompressedStream();
|
||||
|
||||
currentPartTotalReadBytes = 0;
|
||||
|
||||
CurrentCrc = filePartEnumerator.Current.FileHeader.FileCrc;
|
||||
|
||||
streamListener.FireFilePartExtractionBegin(
|
||||
filePartEnumerator.Current.FilePartName,
|
||||
filePartEnumerator.Current.FileHeader.CompressedSize,
|
||||
filePartEnumerator.Current.FileHeader.UncompressedSize
|
||||
);
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
@@ -141,12 +123,6 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentPartTotalReadBytes += totalRead;
|
||||
currentEntryTotalReadBytes += totalRead;
|
||||
streamListener.FireCompressedBytesRead(
|
||||
currentPartTotalReadBytes,
|
||||
currentEntryTotalReadBytes
|
||||
);
|
||||
return totalRead;
|
||||
}
|
||||
|
||||
@@ -206,12 +182,6 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentPartTotalReadBytes += totalRead;
|
||||
currentEntryTotalReadBytes += totalRead;
|
||||
streamListener.FireCompressedBytesRead(
|
||||
currentPartTotalReadBytes,
|
||||
currentEntryTotalReadBytes
|
||||
);
|
||||
return totalRead;
|
||||
}
|
||||
|
||||
@@ -270,12 +240,6 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentPartTotalReadBytes += totalRead;
|
||||
currentEntryTotalReadBytes += totalRead;
|
||||
streamListener.FireCompressedBytesRead(
|
||||
currentPartTotalReadBytes,
|
||||
currentEntryTotalReadBytes
|
||||
);
|
||||
return totalRead;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -27,7 +27,7 @@ internal sealed partial class Unpack : BitInput, IRarUnpack
|
||||
if (!disposed)
|
||||
{
|
||||
base.Dispose();
|
||||
if (!externalWindow)
|
||||
if (!externalWindow && window is not null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(window);
|
||||
window = null;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.Compressors.Xz;
|
||||
|
||||
@@ -30,6 +32,28 @@ public static class BinaryUtils
|
||||
internal static uint ReadLittleEndianUInt32(this Stream stream) =>
|
||||
unchecked((uint)ReadLittleEndianInt32(stream));
|
||||
|
||||
public static async Task<int> ReadLittleEndianInt32Async(
|
||||
this Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var bytes = new byte[4];
|
||||
var read = await stream.ReadFullyAsync(bytes, cancellationToken).ConfigureAwait(false);
|
||||
if (!read)
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
return BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
}
|
||||
|
||||
internal static async Task<uint> ReadLittleEndianUInt32Async(
|
||||
this Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
unchecked(
|
||||
(uint)await ReadLittleEndianInt32Async(stream, cancellationToken).ConfigureAwait(false)
|
||||
);
|
||||
|
||||
internal static byte[] ToBigEndianBytes(this uint uint32)
|
||||
{
|
||||
var result = BitConverter.GetBytes(uint32);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Compressors.Xz;
|
||||
@@ -39,4 +41,75 @@ internal static class MultiByteIntegers
|
||||
}
|
||||
return Output;
|
||||
}
|
||||
|
||||
public static async Task<ulong> ReadXZIntegerAsync(
|
||||
this BinaryReader reader,
|
||||
CancellationToken cancellationToken = default,
|
||||
int MaxBytes = 9
|
||||
)
|
||||
{
|
||||
if (MaxBytes <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(MaxBytes));
|
||||
}
|
||||
|
||||
if (MaxBytes > 9)
|
||||
{
|
||||
MaxBytes = 9;
|
||||
}
|
||||
|
||||
var LastByte = await ReadByteAsync(reader, cancellationToken).ConfigureAwait(false);
|
||||
var Output = (ulong)LastByte & 0x7F;
|
||||
|
||||
var i = 0;
|
||||
while ((LastByte & 0x80) != 0)
|
||||
{
|
||||
if (++i >= MaxBytes)
|
||||
{
|
||||
throw new InvalidFormatException();
|
||||
}
|
||||
|
||||
LastByte = await ReadByteAsync(reader, cancellationToken).ConfigureAwait(false);
|
||||
if (LastByte == 0)
|
||||
{
|
||||
throw new InvalidFormatException();
|
||||
}
|
||||
|
||||
Output |= ((ulong)(LastByte & 0x7F)) << (i * 7);
|
||||
}
|
||||
return Output;
|
||||
}
|
||||
|
||||
public static async Task<byte> ReadByteAsync(
|
||||
this BinaryReader reader,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var buffer = new byte[1];
|
||||
var bytesRead = await reader
|
||||
.BaseStream.ReadAsync(buffer, 0, 1, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (bytesRead != 1)
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
return buffer[0];
|
||||
}
|
||||
|
||||
public static async Task<byte[]> ReadBytesAsync(
|
||||
this BinaryReader reader,
|
||||
int count,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var buffer = new byte[count];
|
||||
var bytesRead = await reader
|
||||
.BaseStream.ReadAsync(buffer, 0, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (bytesRead != count)
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Compressors.Xz.Filters;
|
||||
|
||||
@@ -72,6 +74,49 @@ public sealed class XZBlock : XZReadOnlyStream
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var bytesRead = 0;
|
||||
if (!HeaderIsLoaded)
|
||||
{
|
||||
await LoadHeaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!_streamConnected)
|
||||
{
|
||||
ConnectStream();
|
||||
}
|
||||
|
||||
if (!_endOfStream)
|
||||
{
|
||||
bytesRead = await _decomStream
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (bytesRead != count)
|
||||
{
|
||||
_endOfStream = true;
|
||||
}
|
||||
|
||||
if (_endOfStream && !_paddingSkipped)
|
||||
{
|
||||
await SkipPaddingAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_endOfStream && !_crcChecked)
|
||||
{
|
||||
await CheckCrcAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
private void SkipPadding()
|
||||
{
|
||||
var bytes = (BaseStream.Position - _startPosition) % 4;
|
||||
@@ -87,6 +132,23 @@ public sealed class XZBlock : XZReadOnlyStream
|
||||
_paddingSkipped = true;
|
||||
}
|
||||
|
||||
private async Task SkipPaddingAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = (BaseStream.Position - _startPosition) % 4;
|
||||
if (bytes > 0)
|
||||
{
|
||||
var paddingBytes = new byte[4 - bytes];
|
||||
await BaseStream
|
||||
.ReadAsync(paddingBytes, 0, paddingBytes.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (paddingBytes.Any(b => b != 0))
|
||||
{
|
||||
throw new InvalidFormatException("Padding bytes were non-null");
|
||||
}
|
||||
}
|
||||
_paddingSkipped = true;
|
||||
}
|
||||
|
||||
private void CheckCrc()
|
||||
{
|
||||
var crc = new byte[_checkSize];
|
||||
@@ -96,6 +158,15 @@ public sealed class XZBlock : XZReadOnlyStream
|
||||
_crcChecked = true;
|
||||
}
|
||||
|
||||
private async Task CheckCrcAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var crc = new byte[_checkSize];
|
||||
await BaseStream.ReadAsync(crc, 0, _checkSize, cancellationToken).ConfigureAwait(false);
|
||||
// Actually do a check (and read in the bytes
|
||||
// into the function throughout the stream read).
|
||||
_crcChecked = true;
|
||||
}
|
||||
|
||||
private void ConnectStream()
|
||||
{
|
||||
_decomStream = BaseStream;
|
||||
@@ -123,6 +194,21 @@ public sealed class XZBlock : XZReadOnlyStream
|
||||
HeaderIsLoaded = true;
|
||||
}
|
||||
|
||||
private async Task LoadHeaderAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await ReadHeaderSizeAsync(cancellationToken).ConfigureAwait(false);
|
||||
var headerCache = await CacheHeaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using (var cache = new MemoryStream(headerCache))
|
||||
using (var cachedReader = new BinaryReader(cache))
|
||||
{
|
||||
cachedReader.BaseStream.Position = 1; // skip the header size byte
|
||||
ReadBlockFlags(cachedReader);
|
||||
ReadFilters(cachedReader);
|
||||
}
|
||||
HeaderIsLoaded = true;
|
||||
}
|
||||
|
||||
private void ReadHeaderSize()
|
||||
{
|
||||
_blockHeaderSizeByte = (byte)BaseStream.ReadByte();
|
||||
@@ -132,6 +218,17 @@ public sealed class XZBlock : XZReadOnlyStream
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReadHeaderSizeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var buffer = new byte[1];
|
||||
await BaseStream.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false);
|
||||
_blockHeaderSizeByte = buffer[0];
|
||||
if (_blockHeaderSizeByte == 0)
|
||||
{
|
||||
throw new XZIndexMarkerReachedException();
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] CacheHeader()
|
||||
{
|
||||
var blockHeaderWithoutCrc = new byte[BlockHeaderSize - 4];
|
||||
@@ -139,7 +236,7 @@ public sealed class XZBlock : XZReadOnlyStream
|
||||
var read = BaseStream.Read(blockHeaderWithoutCrc, 1, BlockHeaderSize - 5);
|
||||
if (read != BlockHeaderSize - 5)
|
||||
{
|
||||
throw new EndOfStreamException("Reached end of stream unexectedly");
|
||||
throw new EndOfStreamException("Reached end of stream unexpectedly");
|
||||
}
|
||||
|
||||
var crc = BaseStream.ReadLittleEndianUInt32();
|
||||
@@ -152,6 +249,30 @@ public sealed class XZBlock : XZReadOnlyStream
|
||||
return blockHeaderWithoutCrc;
|
||||
}
|
||||
|
||||
private async Task<byte[]> CacheHeaderAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var blockHeaderWithoutCrc = new byte[BlockHeaderSize - 4];
|
||||
blockHeaderWithoutCrc[0] = _blockHeaderSizeByte;
|
||||
var read = await BaseStream
|
||||
.ReadAsync(blockHeaderWithoutCrc, 1, BlockHeaderSize - 5, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (read != BlockHeaderSize - 5)
|
||||
{
|
||||
throw new EndOfStreamException("Reached end of stream unexpectedly");
|
||||
}
|
||||
|
||||
var crc = await BaseStream
|
||||
.ReadLittleEndianUInt32Async(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var calcCrc = Crc32.Compute(blockHeaderWithoutCrc);
|
||||
if (crc != calcCrc)
|
||||
{
|
||||
throw new InvalidFormatException("Block header corrupt");
|
||||
}
|
||||
|
||||
return blockHeaderWithoutCrc;
|
||||
}
|
||||
|
||||
private void ReadBlockFlags(BinaryReader reader)
|
||||
{
|
||||
var blockFlags = reader.ReadByte();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
|
||||
@@ -27,6 +29,16 @@ public class XZFooter
|
||||
return footer;
|
||||
}
|
||||
|
||||
public static async Task<XZFooter> FromStreamAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var footer = new XZFooter(new BinaryReader(stream, Encoding.UTF8, true));
|
||||
await footer.ProcessAsync(cancellationToken).ConfigureAwait(false);
|
||||
return footer;
|
||||
}
|
||||
|
||||
public void Process()
|
||||
{
|
||||
var crc = _reader.ReadLittleEndianUInt32();
|
||||
@@ -49,4 +61,29 @@ public class XZFooter
|
||||
throw new InvalidFormatException("Magic footer missing");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ProcessAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var crc = await _reader
|
||||
.BaseStream.ReadLittleEndianUInt32Async(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var footerBytes = await _reader.ReadBytesAsync(6, cancellationToken).ConfigureAwait(false);
|
||||
var myCrc = Crc32.Compute(footerBytes);
|
||||
if (crc != myCrc)
|
||||
{
|
||||
throw new InvalidFormatException("Footer corrupt");
|
||||
}
|
||||
|
||||
using (var stream = new MemoryStream(footerBytes))
|
||||
using (var reader = new BinaryReader(stream))
|
||||
{
|
||||
BackwardSize = (reader.ReadLittleEndianUInt32() + 1) * 4;
|
||||
StreamFlags = reader.ReadBytes(2);
|
||||
}
|
||||
var magBy = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false);
|
||||
if (!magBy.AsSpan().SequenceEqual(_magicBytes))
|
||||
{
|
||||
throw new InvalidFormatException("Magic footer missing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
|
||||
@@ -23,12 +25,28 @@ public class XZHeader
|
||||
return header;
|
||||
}
|
||||
|
||||
public static async Task<XZHeader> FromStreamAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var header = new XZHeader(new BinaryReader(stream, Encoding.UTF8, true));
|
||||
await header.ProcessAsync(cancellationToken).ConfigureAwait(false);
|
||||
return header;
|
||||
}
|
||||
|
||||
public void Process()
|
||||
{
|
||||
CheckMagicBytes(_reader.ReadBytes(6));
|
||||
ProcessStreamFlags();
|
||||
}
|
||||
|
||||
public async Task ProcessAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
CheckMagicBytes(await _reader.ReadBytesAsync(6, cancellationToken).ConfigureAwait(false));
|
||||
await ProcessStreamFlagsAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void ProcessStreamFlags()
|
||||
{
|
||||
var streamFlags = _reader.ReadBytes(2);
|
||||
@@ -47,6 +65,26 @@ public class XZHeader
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessStreamFlagsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var streamFlags = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false);
|
||||
var crc = await _reader
|
||||
.BaseStream.ReadLittleEndianUInt32Async(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var calcCrc = Crc32.Compute(streamFlags);
|
||||
if (crc != calcCrc)
|
||||
{
|
||||
throw new InvalidFormatException("Stream header corrupt");
|
||||
}
|
||||
|
||||
BlockCheckType = (CheckType)(streamFlags[1] & 0x0F);
|
||||
var futureUse = (byte)(streamFlags[1] & 0xF0);
|
||||
if (futureUse != 0 || streamFlags[0] != 0)
|
||||
{
|
||||
throw new InvalidFormatException("Unknown XZ Stream Version");
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckMagicBytes(byte[] header)
|
||||
{
|
||||
if (!header.SequenceEqual(MagicHeader))
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
|
||||
@@ -39,6 +41,20 @@ public class XZIndex
|
||||
return index;
|
||||
}
|
||||
|
||||
public static async Task<XZIndex> FromStreamAsync(
|
||||
Stream stream,
|
||||
bool indexMarkerAlreadyVerified,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var index = new XZIndex(
|
||||
new BinaryReader(stream, Encoding.UTF8, true),
|
||||
indexMarkerAlreadyVerified
|
||||
);
|
||||
await index.ProcessAsync(cancellationToken).ConfigureAwait(false);
|
||||
return index;
|
||||
}
|
||||
|
||||
public void Process()
|
||||
{
|
||||
if (!_indexMarkerAlreadyVerified)
|
||||
@@ -55,6 +71,26 @@ public class XZIndex
|
||||
VerifyCrc32();
|
||||
}
|
||||
|
||||
public async Task ProcessAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_indexMarkerAlreadyVerified)
|
||||
{
|
||||
await VerifyIndexMarkerAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
NumberOfRecords = await _reader.ReadXZIntegerAsync(cancellationToken).ConfigureAwait(false);
|
||||
for (ulong i = 0; i < NumberOfRecords; i++)
|
||||
{
|
||||
Records.Add(
|
||||
await XZIndexRecord
|
||||
.FromBinaryReaderAsync(_reader, cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
);
|
||||
}
|
||||
await SkipPaddingAsync(cancellationToken).ConfigureAwait(false);
|
||||
await VerifyCrc32Async(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void VerifyIndexMarker()
|
||||
{
|
||||
var marker = _reader.ReadByte();
|
||||
@@ -64,6 +100,15 @@ public class XZIndex
|
||||
}
|
||||
}
|
||||
|
||||
private async Task VerifyIndexMarkerAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var marker = await _reader.ReadByteAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (marker != 0)
|
||||
{
|
||||
throw new InvalidFormatException("Not an index block");
|
||||
}
|
||||
}
|
||||
|
||||
private void SkipPadding()
|
||||
{
|
||||
var bytes = (int)(_reader.BaseStream.Position - StreamStartPosition) % 4;
|
||||
@@ -77,9 +122,32 @@ public class XZIndex
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SkipPaddingAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = (int)(_reader.BaseStream.Position - StreamStartPosition) % 4;
|
||||
if (bytes > 0)
|
||||
{
|
||||
var paddingBytes = await _reader
|
||||
.ReadBytesAsync(4 - bytes, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (paddingBytes.Any(b => b != 0))
|
||||
{
|
||||
throw new InvalidFormatException("Padding bytes were non-null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyCrc32()
|
||||
{
|
||||
var crc = _reader.ReadLittleEndianUInt32();
|
||||
// TODO verify this matches
|
||||
}
|
||||
|
||||
private async Task VerifyCrc32Async(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var crc = await _reader
|
||||
.BaseStream.ReadLittleEndianUInt32Async(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
// TODO verify this matches
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.Compressors.Xz;
|
||||
|
||||
@@ -18,4 +20,16 @@ public class XZIndexRecord
|
||||
record.UncompressedSize = br.ReadXZInteger();
|
||||
return record;
|
||||
}
|
||||
|
||||
public static async Task<XZIndexRecord> FromBinaryReaderAsync(
|
||||
BinaryReader br,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var record = new XZIndexRecord();
|
||||
record.UnpaddedSize = await br.ReadXZIntegerAsync(cancellationToken).ConfigureAwait(false);
|
||||
record.UncompressedSize = await br.ReadXZIntegerAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
|
||||
@@ -104,6 +106,35 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var bytesRead = 0;
|
||||
if (_endOfStream)
|
||||
{
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
if (!HeaderIsRead)
|
||||
{
|
||||
await ReadHeaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
bytesRead = await ReadBlocksAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (bytesRead < count)
|
||||
{
|
||||
_endOfStream = true;
|
||||
await ReadIndexAsync(cancellationToken).ConfigureAwait(false);
|
||||
await ReadFooterAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
private void ReadHeader()
|
||||
{
|
||||
Header = XZHeader.FromStream(BaseStream);
|
||||
@@ -111,12 +142,31 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack
|
||||
HeaderIsRead = true;
|
||||
}
|
||||
|
||||
private async Task ReadHeaderAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Header = await XZHeader
|
||||
.FromStreamAsync(BaseStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
AssertBlockCheckTypeIsSupported();
|
||||
HeaderIsRead = true;
|
||||
}
|
||||
|
||||
private void ReadIndex() => Index = XZIndex.FromStream(BaseStream, true);
|
||||
|
||||
// TODO veryfy Index
|
||||
private async Task ReadIndexAsync(CancellationToken cancellationToken = default) =>
|
||||
Index = await XZIndex
|
||||
.FromStreamAsync(BaseStream, true, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// TODO verify Index
|
||||
private void ReadFooter() => Footer = XZFooter.FromStream(BaseStream);
|
||||
|
||||
// TODO verify footer
|
||||
private async Task ReadFooterAsync(CancellationToken cancellationToken = default) =>
|
||||
Footer = await XZFooter
|
||||
.FromStreamAsync(BaseStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
private int ReadBlocks(byte[] buffer, int offset, int count)
|
||||
{
|
||||
var bytesRead = 0;
|
||||
@@ -152,6 +202,48 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
private async Task<int> ReadBlocksAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var bytesRead = 0;
|
||||
if (_currentBlock is null)
|
||||
{
|
||||
NextBlock();
|
||||
}
|
||||
|
||||
for (; ; )
|
||||
{
|
||||
try
|
||||
{
|
||||
if (bytesRead >= count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var remaining = count - bytesRead;
|
||||
var newOffset = offset + bytesRead;
|
||||
var justRead = await _currentBlock
|
||||
.ReadAsync(buffer, newOffset, remaining, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (justRead < remaining)
|
||||
{
|
||||
NextBlock();
|
||||
}
|
||||
|
||||
bytesRead += justRead;
|
||||
}
|
||||
catch (XZIndexMarkerReachedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
private void NextBlock() =>
|
||||
_currentBlock = new XZBlock(BaseStream, Header.BlockCheckType, Header.BlockCheckSize);
|
||||
}
|
||||
|
||||
311
src/SharpCompress/Compressors/ZStandard/BitOperations.cs
Normal file
311
src/SharpCompress/Compressors/ZStandard/BitOperations.cs
Normal file
@@ -0,0 +1,311 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#if !NETCOREAPP3_0_OR_GREATER
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using static SharpCompress.Compressors.ZStandard.UnsafeHelper;
|
||||
|
||||
// Some routines inspired by the Stanford Bit Twiddling Hacks by Sean Eron Anderson:
|
||||
// http://graphics.stanford.edu/~seander/bithacks.html
|
||||
|
||||
namespace System.Numerics
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility methods for intrinsic bit-twiddling operations.
|
||||
/// The methods use hardware intrinsics when available on the underlying platform,
|
||||
/// otherwise they use optimized software fallbacks.
|
||||
/// </summary>
|
||||
public static unsafe class BitOperations
|
||||
{
|
||||
// hack: should be public because of inline
|
||||
public static readonly byte* TrailingZeroCountDeBruijn = GetArrayPointer(
|
||||
new byte[]
|
||||
{
|
||||
00,
|
||||
01,
|
||||
28,
|
||||
02,
|
||||
29,
|
||||
14,
|
||||
24,
|
||||
03,
|
||||
30,
|
||||
22,
|
||||
20,
|
||||
15,
|
||||
25,
|
||||
17,
|
||||
04,
|
||||
08,
|
||||
31,
|
||||
27,
|
||||
13,
|
||||
23,
|
||||
21,
|
||||
19,
|
||||
16,
|
||||
07,
|
||||
26,
|
||||
12,
|
||||
18,
|
||||
06,
|
||||
11,
|
||||
05,
|
||||
10,
|
||||
09,
|
||||
}
|
||||
);
|
||||
|
||||
// hack: should be public because of inline
|
||||
public static readonly byte* Log2DeBruijn = GetArrayPointer(
|
||||
new byte[]
|
||||
{
|
||||
00,
|
||||
09,
|
||||
01,
|
||||
10,
|
||||
13,
|
||||
21,
|
||||
02,
|
||||
29,
|
||||
11,
|
||||
14,
|
||||
16,
|
||||
18,
|
||||
22,
|
||||
25,
|
||||
03,
|
||||
30,
|
||||
08,
|
||||
12,
|
||||
20,
|
||||
28,
|
||||
15,
|
||||
17,
|
||||
24,
|
||||
07,
|
||||
19,
|
||||
27,
|
||||
23,
|
||||
06,
|
||||
26,
|
||||
05,
|
||||
04,
|
||||
31,
|
||||
}
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the integer (floor) log of the specified value, base 2.
|
||||
/// Note that by convention, input value 0 returns 0 since log(0) is undefined.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Log2(uint value)
|
||||
{
|
||||
// The 0->0 contract is fulfilled by setting the LSB to 1.
|
||||
// Log(1) is 0, and setting the LSB for values > 1 does not change the log2 result.
|
||||
value |= 1;
|
||||
|
||||
// value lzcnt actual expected
|
||||
// ..0001 31 31-31 0
|
||||
// ..0010 30 31-30 1
|
||||
// 0010.. 2 31-2 29
|
||||
// 0100.. 1 31-1 30
|
||||
// 1000.. 0 31-0 31
|
||||
|
||||
// Fallback contract is 0->0
|
||||
// No AggressiveInlining due to large method size
|
||||
// Has conventional contract 0->0 (Log(0) is undefined)
|
||||
|
||||
// Fill trailing zeros with ones, eg 00010010 becomes 00011111
|
||||
value |= value >> 01;
|
||||
value |= value >> 02;
|
||||
value |= value >> 04;
|
||||
value |= value >> 08;
|
||||
value |= value >> 16;
|
||||
|
||||
// uint.MaxValue >> 27 is always in range [0 - 31] so we use Unsafe.AddByteOffset to avoid bounds check
|
||||
return Log2DeBruijn[
|
||||
// Using deBruijn sequence, k=2, n=5 (2^5=32) : 0b_0000_0111_1100_0100_1010_1100_1101_1101u
|
||||
(int)((value * 0x07C4ACDDu) >> 27)
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the integer (floor) log of the specified value, base 2.
|
||||
/// Note that by convention, input value 0 returns 0 since log(0) is undefined.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int Log2(ulong value)
|
||||
{
|
||||
value |= 1;
|
||||
|
||||
uint hi = (uint)(value >> 32);
|
||||
|
||||
if (hi == 0)
|
||||
{
|
||||
return Log2((uint)value);
|
||||
}
|
||||
|
||||
return 32 + Log2(hi);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Count the number of trailing zero bits in an integer value.
|
||||
/// Similar in behavior to the x86 instruction TZCNT.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int TrailingZeroCount(int value) => TrailingZeroCount((uint)value);
|
||||
|
||||
/// <summary>
|
||||
/// Count the number of trailing zero bits in an integer value.
|
||||
/// Similar in behavior to the x86 instruction TZCNT.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int TrailingZeroCount(uint value)
|
||||
{
|
||||
// Unguarded fallback contract is 0->0, BSF contract is 0->undefined
|
||||
if (value == 0)
|
||||
{
|
||||
return 32;
|
||||
}
|
||||
|
||||
// uint.MaxValue >> 27 is always in range [0 - 31] so we use Unsafe.AddByteOffset to avoid bounds check
|
||||
return TrailingZeroCountDeBruijn[
|
||||
// Using deBruijn sequence, k=2, n=5 (2^5=32) : 0b_0000_0111_0111_1100_1011_0101_0011_0001u
|
||||
(int)(((value & (uint)-(int)value) * 0x077CB531u) >> 27)
|
||||
]; // Multi-cast mitigates redundant conv.u8
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Count the number of trailing zero bits in a mask.
|
||||
/// Similar in behavior to the x86 instruction TZCNT.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int TrailingZeroCount(long value) => TrailingZeroCount((ulong)value);
|
||||
|
||||
/// <summary>
|
||||
/// Count the number of trailing zero bits in a mask.
|
||||
/// Similar in behavior to the x86 instruction TZCNT.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int TrailingZeroCount(ulong value)
|
||||
{
|
||||
uint lo = (uint)value;
|
||||
|
||||
if (lo == 0)
|
||||
{
|
||||
return 32 + TrailingZeroCount((uint)(value >> 32));
|
||||
}
|
||||
|
||||
return TrailingZeroCount(lo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates the specified value left by the specified number of bits.
|
||||
/// Similar in behavior to the x86 instruction ROL.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to rotate.</param>
|
||||
/// <param name="offset">The number of bits to rotate by.
|
||||
/// Any value outside the range [0..31] is treated as congruent mod 32.</param>
|
||||
/// <returns>The rotated value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint RotateLeft(uint value, int offset) =>
|
||||
(value << offset) | (value >> (32 - offset));
|
||||
|
||||
/// <summary>
|
||||
/// Rotates the specified value left by the specified number of bits.
|
||||
/// Similar in behavior to the x86 instruction ROL.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to rotate.</param>
|
||||
/// <param name="offset">The number of bits to rotate by.
|
||||
/// Any value outside the range [0..63] is treated as congruent mod 64.</param>
|
||||
/// <returns>The rotated value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong RotateLeft(ulong value, int offset) =>
|
||||
(value << offset) | (value >> (64 - offset));
|
||||
|
||||
/// <summary>
|
||||
/// Rotates the specified value right by the specified number of bits.
|
||||
/// Similar in behavior to the x86 instruction ROR.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to rotate.</param>
|
||||
/// <param name="offset">The number of bits to rotate by.
|
||||
/// Any value outside the range [0..31] is treated as congruent mod 32.</param>
|
||||
/// <returns>The rotated value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint RotateRight(uint value, int offset) =>
|
||||
(value >> offset) | (value << (32 - offset));
|
||||
|
||||
/// <summary>
|
||||
/// Rotates the specified value right by the specified number of bits.
|
||||
/// Similar in behavior to the x86 instruction ROR.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to rotate.</param>
|
||||
/// <param name="offset">The number of bits to rotate by.
|
||||
/// Any value outside the range [0..63] is treated as congruent mod 64.</param>
|
||||
/// <returns>The rotated value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong RotateRight(ulong value, int offset) =>
|
||||
(value >> offset) | (value << (64 - offset));
|
||||
|
||||
/// <summary>
|
||||
/// Count the number of leading zero bits in a mask.
|
||||
/// Similar in behavior to the x86 instruction LZCNT.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int LeadingZeroCount(uint value)
|
||||
{
|
||||
// Unguarded fallback contract is 0->31, BSR contract is 0->undefined
|
||||
if (value == 0)
|
||||
{
|
||||
return 32;
|
||||
}
|
||||
|
||||
// No AggressiveInlining due to large method size
|
||||
// Has conventional contract 0->0 (Log(0) is undefined)
|
||||
|
||||
// Fill trailing zeros with ones, eg 00010010 becomes 00011111
|
||||
value |= value >> 01;
|
||||
value |= value >> 02;
|
||||
value |= value >> 04;
|
||||
value |= value >> 08;
|
||||
value |= value >> 16;
|
||||
|
||||
// uint.MaxValue >> 27 is always in range [0 - 31] so we use Unsafe.AddByteOffset to avoid bounds check
|
||||
return 31
|
||||
^ Log2DeBruijn[
|
||||
// uint|long -> IntPtr cast on 32-bit platforms does expensive overflow checks not needed here
|
||||
(int)((value * 0x07C4ACDDu) >> 27)
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Count the number of leading zero bits in a mask.
|
||||
/// Similar in behavior to the x86 instruction LZCNT.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int LeadingZeroCount(ulong value)
|
||||
{
|
||||
uint hi = (uint)(value >> 32);
|
||||
|
||||
if (hi == 0)
|
||||
{
|
||||
return 32 + LeadingZeroCount((uint)value);
|
||||
}
|
||||
|
||||
return LeadingZeroCount(hi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
301
src/SharpCompress/Compressors/ZStandard/CompressionStream.cs
Normal file
301
src/SharpCompress/Compressors/ZStandard/CompressionStream.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Compressors.ZStandard.Unsafe;
|
||||
|
||||
namespace SharpCompress.Compressors.ZStandard;
|
||||
|
||||
public class CompressionStream : Stream
|
||||
{
|
||||
private readonly Stream innerStream;
|
||||
private readonly byte[] outputBuffer;
|
||||
private readonly bool preserveCompressor;
|
||||
private readonly bool leaveOpen;
|
||||
private Compressor? compressor;
|
||||
private ZSTD_outBuffer_s output;
|
||||
|
||||
public CompressionStream(
|
||||
Stream stream,
|
||||
int level = Compressor.DefaultCompressionLevel,
|
||||
int bufferSize = 0,
|
||||
bool leaveOpen = true
|
||||
)
|
||||
: this(stream, new Compressor(level), bufferSize, false, leaveOpen) { }
|
||||
|
||||
public CompressionStream(
|
||||
Stream stream,
|
||||
Compressor compressor,
|
||||
int bufferSize = 0,
|
||||
bool preserveCompressor = true,
|
||||
bool leaveOpen = true
|
||||
)
|
||||
{
|
||||
if (stream == null)
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
if (!stream.CanWrite)
|
||||
throw new ArgumentException("Stream is not writable", nameof(stream));
|
||||
|
||||
if (bufferSize < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(bufferSize));
|
||||
|
||||
innerStream = stream;
|
||||
this.compressor = compressor;
|
||||
this.preserveCompressor = preserveCompressor;
|
||||
this.leaveOpen = leaveOpen;
|
||||
|
||||
var outputBufferSize =
|
||||
bufferSize > 0
|
||||
? bufferSize
|
||||
: (int)Unsafe.Methods.ZSTD_CStreamOutSize().EnsureZstdSuccess();
|
||||
outputBuffer = ArrayPool<byte>.Shared.Rent(outputBufferSize);
|
||||
output = new ZSTD_outBuffer_s { pos = 0, size = (nuint)outputBufferSize };
|
||||
}
|
||||
|
||||
public void SetParameter(ZSTD_cParameter parameter, int value)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
compressor.NotNull().SetParameter(parameter, value);
|
||||
}
|
||||
|
||||
public int GetParameter(ZSTD_cParameter parameter)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
return compressor.NotNull().GetParameter(parameter);
|
||||
}
|
||||
|
||||
public void LoadDictionary(byte[] dict)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
compressor.NotNull().LoadDictionary(dict);
|
||||
}
|
||||
|
||||
~CompressionStream() => Dispose(false);
|
||||
|
||||
#if !NETSTANDARD2_0 && !NETFRAMEWORK
|
||||
public override async ValueTask DisposeAsync()
|
||||
#else
|
||||
public async Task DisposeAsync()
|
||||
#endif
|
||||
{
|
||||
if (compressor == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await FlushInternalAsync(ZSTD_EndDirective.ZSTD_e_end).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReleaseUnmanagedResources();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (compressor == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (disposing)
|
||||
FlushInternal(ZSTD_EndDirective.ZSTD_e_end);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReleaseUnmanagedResources();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseUnmanagedResources()
|
||||
{
|
||||
if (!preserveCompressor)
|
||||
{
|
||||
compressor.NotNull().Dispose();
|
||||
}
|
||||
compressor = null;
|
||||
|
||||
if (outputBuffer != null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(outputBuffer);
|
||||
}
|
||||
|
||||
if (!leaveOpen)
|
||||
{
|
||||
innerStream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Flush() => FlushInternal(ZSTD_EndDirective.ZSTD_e_flush);
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken) =>
|
||||
await FlushInternalAsync(ZSTD_EndDirective.ZSTD_e_flush, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
private void FlushInternal(ZSTD_EndDirective directive) => WriteInternal(null, directive);
|
||||
|
||||
private async Task FlushInternalAsync(
|
||||
ZSTD_EndDirective directive,
|
||||
CancellationToken cancellationToken = default
|
||||
) => await WriteInternalAsync(null, directive, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
Write(new ReadOnlySpan<byte>(buffer, offset, count));
|
||||
|
||||
#if !NETSTANDARD2_0 && !NETFRAMEWORK
|
||||
public override void Write(ReadOnlySpan<byte> buffer) =>
|
||||
WriteInternal(buffer, ZSTD_EndDirective.ZSTD_e_continue);
|
||||
#else
|
||||
public void Write(ReadOnlySpan<byte> buffer) =>
|
||||
WriteInternal(buffer, ZSTD_EndDirective.ZSTD_e_continue);
|
||||
#endif
|
||||
|
||||
private void WriteInternal(ReadOnlySpan<byte> buffer, ZSTD_EndDirective directive)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
|
||||
var input = new ZSTD_inBuffer_s
|
||||
{
|
||||
pos = 0,
|
||||
size = buffer != null ? (nuint)buffer.Length : 0,
|
||||
};
|
||||
nuint remaining;
|
||||
do
|
||||
{
|
||||
output.pos = 0;
|
||||
remaining = CompressStream(ref input, buffer, directive);
|
||||
|
||||
var written = (int)output.pos;
|
||||
if (written > 0)
|
||||
innerStream.Write(outputBuffer, 0, written);
|
||||
} while (
|
||||
directive == ZSTD_EndDirective.ZSTD_e_continue ? input.pos < input.size : remaining > 0
|
||||
);
|
||||
}
|
||||
|
||||
#if !NETSTANDARD2_0 && !NETFRAMEWORK
|
||||
private async ValueTask WriteInternalAsync(
|
||||
ReadOnlyMemory<byte>? buffer,
|
||||
ZSTD_EndDirective directive,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
#else
|
||||
private async Task WriteInternalAsync(
|
||||
ReadOnlyMemory<byte>? buffer,
|
||||
ZSTD_EndDirective directive,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
#endif
|
||||
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
|
||||
var input = new ZSTD_inBuffer_s
|
||||
{
|
||||
pos = 0,
|
||||
size = buffer.HasValue ? (nuint)buffer.Value.Length : 0,
|
||||
};
|
||||
nuint remaining;
|
||||
do
|
||||
{
|
||||
output.pos = 0;
|
||||
remaining = CompressStream(
|
||||
ref input,
|
||||
buffer.HasValue ? buffer.Value.Span : null,
|
||||
directive
|
||||
);
|
||||
|
||||
var written = (int)output.pos;
|
||||
if (written > 0)
|
||||
await innerStream
|
||||
.WriteAsync(outputBuffer, 0, written, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
} while (
|
||||
directive == ZSTD_EndDirective.ZSTD_e_continue ? input.pos < input.size : remaining > 0
|
||||
);
|
||||
}
|
||||
|
||||
#if !NETSTANDARD2_0 && !NETFRAMEWORK
|
||||
|
||||
public override Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
) => WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken).AsTask();
|
||||
|
||||
public override async ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await WriteInternalAsync(buffer, ZSTD_EndDirective.ZSTD_e_continue, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
|
||||
public override Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
) => WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken);
|
||||
|
||||
public async Task WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await WriteInternalAsync(buffer, ZSTD_EndDirective.ZSTD_e_continue, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
|
||||
internal unsafe nuint CompressStream(
|
||||
ref ZSTD_inBuffer_s input,
|
||||
ReadOnlySpan<byte> inputBuffer,
|
||||
ZSTD_EndDirective directive
|
||||
)
|
||||
{
|
||||
fixed (byte* inputBufferPtr = inputBuffer)
|
||||
fixed (byte* outputBufferPtr = outputBuffer)
|
||||
{
|
||||
input.src = inputBufferPtr;
|
||||
output.dst = outputBufferPtr;
|
||||
return compressor
|
||||
.NotNull()
|
||||
.CompressStream(ref input, ref output, directive)
|
||||
.EnsureZstdSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanRead => false;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => 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 int Read(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
private void EnsureNotDisposed()
|
||||
{
|
||||
if (compressor == null)
|
||||
throw new ObjectDisposedException(nameof(CompressionStream));
|
||||
}
|
||||
|
||||
public void SetPledgedSrcSize(ulong pledgedSrcSize)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
compressor.NotNull().SetPledgedSrcSize(pledgedSrcSize);
|
||||
}
|
||||
}
|
||||
204
src/SharpCompress/Compressors/ZStandard/Compressor.cs
Normal file
204
src/SharpCompress/Compressors/ZStandard/Compressor.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using SharpCompress.Compressors.ZStandard.Unsafe;
|
||||
|
||||
namespace SharpCompress.Compressors.ZStandard;
|
||||
|
||||
public unsafe class Compressor : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum negative compression level allowed
|
||||
/// </summary>
|
||||
public static int MinCompressionLevel => Unsafe.Methods.ZSTD_minCLevel();
|
||||
|
||||
/// <summary>
|
||||
/// Maximum compression level available
|
||||
/// </summary>
|
||||
public static int MaxCompressionLevel => Unsafe.Methods.ZSTD_maxCLevel();
|
||||
|
||||
/// <summary>
|
||||
/// Default compression level
|
||||
/// </summary>
|
||||
/// <see cref="Unsafe.Methods.ZSTD_defaultCLevel"/>
|
||||
public const int DefaultCompressionLevel = 3;
|
||||
|
||||
private int level = DefaultCompressionLevel;
|
||||
|
||||
private readonly SafeCctxHandle handle;
|
||||
|
||||
public int Level
|
||||
{
|
||||
get => level;
|
||||
set
|
||||
{
|
||||
if (level != value)
|
||||
{
|
||||
level = value;
|
||||
SetParameter(ZSTD_cParameter.ZSTD_c_compressionLevel, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetParameter(ZSTD_cParameter parameter, int value)
|
||||
{
|
||||
using var cctx = handle.Acquire();
|
||||
Unsafe.Methods.ZSTD_CCtx_setParameter(cctx, parameter, value).EnsureZstdSuccess();
|
||||
}
|
||||
|
||||
public int GetParameter(ZSTD_cParameter parameter)
|
||||
{
|
||||
using var cctx = handle.Acquire();
|
||||
int value;
|
||||
Unsafe.Methods.ZSTD_CCtx_getParameter(cctx, parameter, &value).EnsureZstdSuccess();
|
||||
return value;
|
||||
}
|
||||
|
||||
public void LoadDictionary(byte[] dict)
|
||||
{
|
||||
var dictReadOnlySpan = new ReadOnlySpan<byte>(dict);
|
||||
LoadDictionary(dictReadOnlySpan);
|
||||
}
|
||||
|
||||
public void LoadDictionary(ReadOnlySpan<byte> dict)
|
||||
{
|
||||
using var cctx = handle.Acquire();
|
||||
fixed (byte* dictPtr = dict)
|
||||
Unsafe
|
||||
.Methods.ZSTD_CCtx_loadDictionary(cctx, dictPtr, (nuint)dict.Length)
|
||||
.EnsureZstdSuccess();
|
||||
}
|
||||
|
||||
public Compressor(int level = DefaultCompressionLevel)
|
||||
{
|
||||
handle = SafeCctxHandle.Create();
|
||||
Level = level;
|
||||
}
|
||||
|
||||
public static int GetCompressBound(int length) =>
|
||||
(int)Unsafe.Methods.ZSTD_compressBound((nuint)length);
|
||||
|
||||
public static ulong GetCompressBoundLong(ulong length) =>
|
||||
Unsafe.Methods.ZSTD_compressBound((nuint)length);
|
||||
|
||||
public Span<byte> Wrap(ReadOnlySpan<byte> src)
|
||||
{
|
||||
var dest = new byte[GetCompressBound(src.Length)];
|
||||
var length = Wrap(src, dest);
|
||||
return new Span<byte>(dest, 0, length);
|
||||
}
|
||||
|
||||
public int Wrap(byte[] src, byte[] dest, int offset) =>
|
||||
Wrap(src, new Span<byte>(dest, offset, dest.Length - offset));
|
||||
|
||||
public int Wrap(ReadOnlySpan<byte> src, Span<byte> dest)
|
||||
{
|
||||
fixed (byte* srcPtr = src)
|
||||
fixed (byte* destPtr = dest)
|
||||
{
|
||||
using var cctx = handle.Acquire();
|
||||
return (int)
|
||||
Unsafe
|
||||
.Methods.ZSTD_compress2(
|
||||
cctx,
|
||||
destPtr,
|
||||
(nuint)dest.Length,
|
||||
srcPtr,
|
||||
(nuint)src.Length
|
||||
)
|
||||
.EnsureZstdSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
public int Wrap(ArraySegment<byte> src, ArraySegment<byte> dest) =>
|
||||
Wrap((ReadOnlySpan<byte>)src, dest);
|
||||
|
||||
public int Wrap(
|
||||
byte[] src,
|
||||
int srcOffset,
|
||||
int srcLength,
|
||||
byte[] dst,
|
||||
int dstOffset,
|
||||
int dstLength
|
||||
) =>
|
||||
Wrap(
|
||||
new ReadOnlySpan<byte>(src, srcOffset, srcLength),
|
||||
new Span<byte>(dst, dstOffset, dstLength)
|
||||
);
|
||||
|
||||
public bool TryWrap(byte[] src, byte[] dest, int offset, out int written) =>
|
||||
TryWrap(src, new Span<byte>(dest, offset, dest.Length - offset), out written);
|
||||
|
||||
public bool TryWrap(ReadOnlySpan<byte> src, Span<byte> dest, out int written)
|
||||
{
|
||||
fixed (byte* srcPtr = src)
|
||||
fixed (byte* destPtr = dest)
|
||||
{
|
||||
nuint returnValue;
|
||||
using (var cctx = handle.Acquire())
|
||||
{
|
||||
returnValue = Unsafe.Methods.ZSTD_compress2(
|
||||
cctx,
|
||||
destPtr,
|
||||
(nuint)dest.Length,
|
||||
srcPtr,
|
||||
(nuint)src.Length
|
||||
);
|
||||
}
|
||||
|
||||
if (returnValue == unchecked(0 - (nuint)ZSTD_ErrorCode.ZSTD_error_dstSize_tooSmall))
|
||||
{
|
||||
written = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
returnValue.EnsureZstdSuccess();
|
||||
written = (int)returnValue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWrap(ArraySegment<byte> src, ArraySegment<byte> dest, out int written) =>
|
||||
TryWrap((ReadOnlySpan<byte>)src, dest, out written);
|
||||
|
||||
public bool TryWrap(
|
||||
byte[] src,
|
||||
int srcOffset,
|
||||
int srcLength,
|
||||
byte[] dst,
|
||||
int dstOffset,
|
||||
int dstLength,
|
||||
out int written
|
||||
) =>
|
||||
TryWrap(
|
||||
new ReadOnlySpan<byte>(src, srcOffset, srcLength),
|
||||
new Span<byte>(dst, dstOffset, dstLength),
|
||||
out written
|
||||
);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
handle.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
internal nuint CompressStream(
|
||||
ref ZSTD_inBuffer_s input,
|
||||
ref ZSTD_outBuffer_s output,
|
||||
ZSTD_EndDirective directive
|
||||
)
|
||||
{
|
||||
fixed (ZSTD_inBuffer_s* inputPtr = &input)
|
||||
fixed (ZSTD_outBuffer_s* outputPtr = &output)
|
||||
{
|
||||
using var cctx = handle.Acquire();
|
||||
return Unsafe
|
||||
.Methods.ZSTD_compressStream2(cctx, outputPtr, inputPtr, directive)
|
||||
.EnsureZstdSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPledgedSrcSize(ulong pledgedSrcSize)
|
||||
{
|
||||
using var cctx = handle.Acquire();
|
||||
Unsafe.Methods.ZSTD_CCtx_setPledgedSrcSize(cctx, pledgedSrcSize).EnsureZstdSuccess();
|
||||
}
|
||||
}
|
||||
8
src/SharpCompress/Compressors/ZStandard/Constants.cs
Normal file
8
src/SharpCompress/Compressors/ZStandard/Constants.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SharpCompress.Compressors.ZStandard;
|
||||
|
||||
internal class Constants
|
||||
{
|
||||
//NOTE: https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcallowverylargeobjects-element#remarks
|
||||
//NOTE: https://github.com/dotnet/runtime/blob/v5.0.0-rtm.20519.4/src/libraries/System.Private.CoreLib/src/System/Array.cs#L27
|
||||
public const ulong MaxByteArrayLength = 0x7FFFFFC7;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user