Compare commits

...

295 Commits

Author SHA1 Message Date
Adam Hathcock
5c4b83e501 Merge remote-tracking branch 'origin/master' into copilot/add-performance-benchmarking
# Conflicts:
#	tests/SharpCompress.Performance/packages.lock.json
2026-01-14 14:52:21 +00:00
copilot-swe-agent[bot]
80ac10a5fe Merge latest master branch and resolve conflicts
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-12 14:11:57 +00:00
Adam Hathcock
65dba509e0 Merge pull request #1121 from adamhathcock/adam/async
More async for ZipReader and ZipWriter
2026-01-12 14:07:37 +00:00
Adam Hathcock
3807c3ce2a Merge pull request #1127 from adamhathcock/copilot/sub-pr-1121-yet-again
Fix async test method naming in ZipArchiveAsyncTests
2026-01-12 12:10:51 +00:00
Adam Hathcock
d2f328af01 Merge pull request #1126 from adamhathcock/copilot/sub-pr-1121-another-one
Fix typo in TestBase.cs comment
2026-01-12 12:10:11 +00:00
Adam Hathcock
c3ffcf4fe8 Merge pull request #1125 from adamhathcock/copilot/sub-pr-1121-again
[WIP] Update ZipReader and ZipWriter based on review feedback
2026-01-12 12:09:42 +00:00
copilot-swe-agent[bot]
95c409d979 Change File.Create to File.OpenWrite in TarReaderAsyncTests for consistency
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-12 12:09:12 +00:00
Adam Hathcock
dadf9e71bb Merge pull request #1124 from adamhathcock/copilot/sub-pr-1121
[WIP] Address feedback on async implementation for ZipReader and ZipWriter
2026-01-12 12:09:10 +00:00
copilot-swe-agent[bot]
f4b1780d8a Rename async test methods to use _Async suffix instead of _Sync
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-12 12:08:23 +00:00
copilot-swe-agent[bot]
921cff00a5 Fix async test method naming: rename Sync to Async
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-12 12:08:09 +00:00
copilot-swe-agent[bot]
64a09eb0f8 Fix typo in TestBase.cs comment: 'akways' -> 'always'
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-12 12:06:47 +00:00
copilot-swe-agent[bot]
3a636531e8 Initial plan 2026-01-12 12:05:34 +00:00
copilot-swe-agent[bot]
292da90184 Initial plan 2026-01-12 12:05:22 +00:00
copilot-swe-agent[bot]
90c8ff8650 Initial plan 2026-01-12 12:05:11 +00:00
copilot-swe-agent[bot]
0f37049aad Initial plan 2026-01-12 12:05:04 +00:00
Adam Hathcock
3fb07d129f Use async dispose always 2026-01-12 10:19:01 +00:00
Adam Hathcock
8d0ac5062f Merge pull request #1123 from adamhathcock/adam/add-more-docs
Add more documentation
2026-01-11 12:20:55 +00:00
Adam Hathcock
b2d1505e5c remove section on allocations 2026-01-11 12:20:40 +00:00
Adam Hathcock
a35e65ee42 use ifdefs for creating files? 2026-01-08 16:52:23 +00:00
Adam Hathcock
d1fcf31f7e fmt 2026-01-08 16:31:11 +00:00
Adam Hathcock
17cd934b5b use async methods where we can 2026-01-08 16:24:11 +00:00
Adam Hathcock
ae614cd3fe update references 2026-01-08 16:14:40 +00:00
Adam Hathcock
47037a4b9d docs: Add explicit guideline that agents should never commit to git
Agents should stage files and leave committing to the user. Only create
commits when the user explicitly requests them.
2026-01-08 15:49:10 +00:00
Adam Hathcock
507b1e35d8 docs: Remove broken references to non-existent files
- Remove CONTRIBUTING.md reference from ARCHITECTURE.md
- Remove ERRORS.md reference from API.md
- Remove TROUBLESHOOTING.md reference from ENCODING.md
- Remove TROUBLESHOOTING.md reference from PERFORMANCE.md

All markdown files now reference only existing documentation.
2026-01-08 15:47:57 +00:00
Adam Hathcock
2839e1d33f Update docs/ENCODING.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-08 15:39:47 +00:00
Adam Hathcock
ef0b9d525c merge conflicts 2026-01-08 15:37:55 +00:00
Adam Hathcock
01e6e04a78 Merge branch 'master' into adam/async
# Conflicts:
#	src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs
#	src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs
2026-01-08 15:36:41 +00:00
Adam Hathcock
8c876c70af Add more documentation 2026-01-08 15:34:48 +00:00
Adam Hathcock
a7d6d6493e add version numbers again to get them 2026-01-08 14:22:54 +00:00
Adam Hathcock
b6cc95af73 Merge pull request #1122 from adamhathcock/adam/netstandard-20-readd
Readd netstandard 2.0
2026-01-08 14:03:30 +00:00
Adam Hathcock
bdcc1d32c2 fix scratch dir creation 2026-01-08 14:01:35 +00:00
Adam Hathcock
90d91cc7c2 Merge pull request #1117 from adamhathcock/adam/rework-archive-encoding
Change ArchiveEncoding to interface.
2026-01-08 13:39:30 +00:00
Adam Hathcock
ec83cf588f Readd netstandard 2.0 2026-01-08 13:33:36 +00:00
Adam Hathcock
4f0a2e3c95 disable zip64 tests 2026-01-08 12:55:16 +00:00
Adam Hathcock
3747a27109 Task to ValueTask 2026-01-08 12:35:12 +00:00
Adam Hathcock
b501bac54a better names for new interfaces 2026-01-08 12:02:26 +00:00
Adam Hathcock
7aec98d652 read async interface for reader 2026-01-08 11:28:15 +00:00
Adam Hathcock
406b198e0e can't dispose before returning 2026-01-08 10:24:33 +00:00
Adam Hathcock
8e42296c3a switch Task to ValueTask 2026-01-08 10:22:53 +00:00
Adam Hathcock
60e5220bd0 fmt 2026-01-08 09:41:48 +00:00
Adam Hathcock
0f37cbfd0b archive async path uses new async interface 2026-01-08 09:39:04 +00:00
Adam Hathcock
541fd136d5 IArchiveAsync 2026-01-08 09:14:46 +00:00
Adam Hathcock
60d42ca9c3 fmt 2026-01-07 16:38:48 +00:00
Adam Hathcock
5c947bccc7 Merge branch 'adam/update-docs' 2026-01-07 16:18:51 +00:00
Adam Hathcock
fbdefc17c1 updates from review 2026-01-07 16:18:27 +00:00
Adam Hathcock
1425c6ff0d Merge pull request #1120 from adamhathcock/adam/update-docs
Update docs
2026-01-07 16:12:51 +00:00
Adam Hathcock
e038aea694 move old changelog 2026-01-07 16:10:55 +00:00
Adam Hathcock
87ccbf329d moved examples to USAGE 2026-01-07 15:56:38 +00:00
Adam Hathcock
9dcf384263 update for progress reporting 2026-01-07 15:30:26 +00:00
Adam Hathcock
ac0716ddeb write testing 2026-01-07 15:01:04 +00:00
Adam Hathcock
b9792ca491 fix async zip decompression 2026-01-07 14:54:32 +00:00
Adam Hathcock
c3fd42057a Pass more Zip tests 2026-01-07 14:47:20 +00:00
Adam Hathcock
39d85ff4f6 conflicts from merge 2026-01-07 12:18:14 +00:00
Adam Hathcock
fbce3e77ba Merge branch 'master' into adam/async
# Conflicts:
#	src/SharpCompress/Utility.cs
2026-01-07 12:11:19 +00:00
Adam Hathcock
66e9de2685 fixed comment 2026-01-07 11:26:42 +00:00
Adam Hathcock
321520408b fmt 2026-01-07 11:12:02 +00:00
Adam Hathcock
68451bd75f Use explicit enum, add comments 2026-01-07 11:10:15 +00:00
Adam Hathcock
486fdf118b move to own files and refactor UTF8 usage 2026-01-07 10:39:18 +00:00
Adam Hathcock
bd3cda0617 Some restoring of functionality 2026-01-07 10:32:02 +00:00
Adam Hathcock
725503d1ce Change ArchiveEncoding to interface. Simplify class. Question what to do about Forced and complex access 2026-01-07 08:44:12 +00:00
Adam Hathcock
be045c4f15 Merge pull request #1114 from adamhathcock/copilot/fix-7z-file-decompression-error
Fix async decompression of .7z files by implementing Memory<byte> ReadAsync overload
2026-01-07 08:16:51 +00:00
Adam Hathcock
fd968b3f78 Update src/SharpCompress/IO/ReadOnlySubStream.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-06 16:33:03 +00:00
Adam Hathcock
833dd7b3a2 fix tests and fmt 2026-01-06 15:33:43 +00:00
Adam Hathcock
b9258ad496 use more ValueTask methods but types are still created because of state machine suspension 2026-01-06 15:26:49 +00:00
copilot-swe-agent[bot]
0678318dde Fix async decompression by implementing Memory<byte> ReadAsync overload
The issue was that .NET 10's ReadExactlyAsync calls the Memory<byte> overload of ReadAsync, which wasn't implemented in BufferedSubStream. This caused it to fall back to the base Stream implementation that uses synchronous reads, leading to cache state corruption.

Solution: Added ValueTask<int> ReadAsync(Memory<byte>, CancellationToken) overload for modern .NET versions.

All tests now passing including LZMA2 and Solid archives.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-06 14:18:15 +00:00
copilot-swe-agent[bot]
7116c0d098 Add async support to BufferedSubStream for 7zip decompression
- Implemented ReadAsync and RefillCacheAsync methods in BufferedSubStream
- Added async test cases for SevenZipArchive (LZMA, LZMA2, Solid, BZip2, PPMd)
- Tests show LZMA, BZip2, and PPMd working correctly
- LZMA2 and Solid archives still failing with Data Error - investigating cache state management

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-06 14:13:48 +00:00
copilot-swe-agent[bot]
2fde8436fb Initial plan 2026-01-06 14:00:03 +00:00
Adam Hathcock
61ecd6475f Merge pull request #1113 from adamhathcock/adam/fix-readonly-dispose
Fix a usage of ReadOnly that use dispose in 7Zip
2026-01-06 13:22:50 +00:00
Adam Hathcock
64b209a772 add disposal tests and fix lzipstream 2026-01-06 13:13:34 +00:00
Adam Hathcock
48dbdbfed5 fmt 2026-01-06 12:49:26 +00:00
Adam Hathcock
cf50311b9c Skip should use framework stuff 2026-01-06 12:45:13 +00:00
Adam Hathcock
e4d8582a2a 7zip streams always want to be disposed 2026-01-06 12:42:48 +00:00
copilot-swe-agent[bot]
a92ce90252 Fix path validation and add iteration cleanup to prevent file reuse
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-05 17:38:17 +00:00
copilot-swe-agent[bot]
e519f61f0f Address code review feedback: fix exception handling and initialization order
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-05 17:36:08 +00:00
copilot-swe-agent[bot]
49f2271253 Format code with CSharpier
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-05 17:33:06 +00:00
copilot-swe-agent[bot]
5b1d11bc1d Add WriteBenchmarks, BaselineComparisonBenchmarks, and comprehensive documentation
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-05 17:31:32 +00:00
copilot-swe-agent[bot]
aa3a40d968 Add BenchmarkDotNet integration with Archive and Reader benchmarks
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-05 17:27:50 +00:00
Adam Hathcock
b825e15406 Merge pull request #1100 from adamhathcock/copilot/fix-read-method-implementations
Consolidate stream extension methods and simplify with framework methods
2026-01-05 17:24:54 +00:00
copilot-swe-agent[bot]
6125654b2e Initial plan 2026-01-05 17:21:29 +00:00
Adam Hathcock
b8e5ee45eb Merge pull request #1109 from adamhathcock/dependabot/nuget/build/SimpleExec-13.0.0
Bump SimpleExec from 12.1.0 to 13.0.0
2026-01-05 17:17:55 +00:00
Adam Hathcock
9f20a9e7d2 Merge pull request #1110 from TwanVanDongen/master
Formats.md updated to reflect additions of Ace, Arc and Arj
2026-01-05 17:14:26 +00:00
Twan
201521d814 Merge branch 'adamhathcock:master' into master 2026-01-05 18:09:55 +01:00
Twan van Dongen
18bb3cba11 Added descriptions for archives Ace, Arc and Arj 2026-01-05 18:08:53 +01:00
Adam Hathcock
af951d6f6a Merge pull request #1102 from TwanVanDongen/master
Add support for ACE archives
2026-01-05 16:37:35 +00:00
Adam Hathcock
e5fe92bf90 Merge pull request #1108 from adamhathcock/dependabot/nuget/dot-config/csharpier-1.2.5
Bump csharpier from 1.2.4 to 1.2.5
2026-01-05 16:15:12 +00:00
dependabot[bot]
b1aca7c305 Bump SimpleExec from 12.1.0 to 13.0.0
---
updated-dependencies:
- dependency-name: SimpleExec
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 09:32:51 +00:00
dependabot[bot]
c0a0cc4a44 Bump csharpier from 1.2.4 to 1.2.5
---
updated-dependencies:
- dependency-name: csharpier
  dependency-version: 1.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 09:32:17 +00:00
Twan van Dongen
7a49eb9e93 Archives containing encrypted content throws exception. 2026-01-04 19:10:57 +01:00
Adam Hathcock
5aa0610882 Merge pull request #1104 from adamhathcock/copilot/fix-invalidoperationexception-rar
Fix InvalidOperationException when RAR uncompressed size exceeds header value
2026-01-04 12:18:38 +00:00
copilot-swe-agent[bot]
41ed4c8186 Add negative test case for premature stream termination
Added Rar_StreamValidation_ThrowsOnTruncatedStream test that verifies InvalidOperationException IS thrown when a RAR stream ends prematurely (position < expected length). Created TruncatedStream mock to simulate corrupted/truncated RAR files. This test validates the exception condition (_position < Length) works correctly.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-04 11:47:57 +00:00
copilot-swe-agent[bot]
90a33ce6b0 Remove duplicate test that doesn't verify exception case
Removed Rar_StreamValidation_CorrectExceptionBehavior test as it duplicated the validation in Rar_StreamValidation_OnlyThrowsOnPrematureEnd without actually testing the exception case. The remaining test adequately validates that the fix works correctly across multiple RAR formats.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-04 11:36:25 +00:00
copilot-swe-agent[bot]
12574798e1 Update test to document exception behavior for RAR stream validation
Renamed and enhanced the test to better document the fix. Added second test (Rar_StreamValidation_CorrectExceptionBehavior) that explicitly validates the difference between old and new behavior. Tests verify that InvalidOperationException is only thrown when position < expected length (premature termination), not when position >= expected length (which can be valid for some RAR files).

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-04 09:55:18 +00:00
copilot-swe-agent[bot]
83b11254db Add test for InvalidOperationException fix in RAR extraction
Added Rar_ExtractionCompletesWithoutInvalidOperationException test that verifies RAR extraction completes successfully without throwing InvalidOperationException when reading streams to EOF. The test validates the fix works across RAR, RAR5, RAR4, and RAR2 formats by reading all entries completely and ensuring no exceptions are thrown.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-04 09:33:31 +00:00
copilot-swe-agent[bot]
b25493fd29 Fix InvalidOperationException when RAR unpacks more data than header specifies
Changed validation condition from `_position != Length` to `_position < Length` in RarStream.Read() and RarStream.ReadImplAsync() to only throw when unpacking ends prematurely, not when more data is unpacked than the header specifies. This allows successful extraction of RAR files where the actual uncompressed size exceeds the header size.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-04 09:23:47 +00:00
copilot-swe-agent[bot]
bb66100486 Initial plan 2026-01-04 09:10:50 +00:00
copilot-swe-agent[bot]
9bd86f64c9 Replace manual TransferTo implementation with Stream.CopyTo framework methods
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-03 18:41:02 +00:00
Twan van Dongen
3ebf97dd49 MultiVolume not supported, tests provided by split archive. 2026-01-03 19:09:43 +01:00
Twan van Dongen
bfcdeb3784 Ace largefile test added 2026-01-03 18:48:54 +01:00
copilot-swe-agent[bot]
77015224f6 Add input validation for ReadBytesAsync count parameter
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-03 17:19:55 +00:00
copilot-swe-agent[bot]
372ecb77d0 Use threshold-based ArrayPool strategy for BinaryReaderExtensions
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-03 17:17:59 +00:00
copilot-swe-agent[bot]
05642cbdc6 Use ArrayPool for temporary buffers in BinaryReaderExtensions
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-03 17:12:43 +00:00
Twan van Dongen
feece3d788 Missed some CSharpier edits 2026-01-03 18:08:01 +01:00
Twan van Dongen
94adb77e9e Merge branch 'master' of https://github.com/TwanVanDongen/sharpcompress 2026-01-03 18:01:13 +01:00
Twan van Dongen
909d36c237 more subtle check of magic bytes for ARJ archives 2026-01-03 17:59:17 +01:00
Twan van Dongen
e1c8aa226d Add ACE archive support (read-only, stored entries) 2026-01-03 17:18:00 +01:00
copilot-swe-agent[bot]
1a71c01fd4 Consolidate ReadExact and ReadFully methods into Utility.cs
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-03 15:58:27 +00:00
Adam Hathcock
54640548ed Consolidate reads 2026-01-03 15:47:10 +00:00
Adam Hathcock
2327679f23 Merge pull request #1098 from adamhathcock/adam/remove-old-release
remove old release
2026-01-03 14:27:13 +00:00
Adam Hathcock
574d9f970c Merge pull request #1099 from adamhathcock/copilot/sub-pr-1098
Configure nuget-release workflow to validate PRs without publishing
2026-01-03 14:22:13 +00:00
copilot-swe-agent[bot]
235096a2eb Configure nuget-release.yml to run on PRs without publishing
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-03 14:15:50 +00:00
copilot-swe-agent[bot]
a739fdc544 Initial plan 2026-01-03 14:13:34 +00:00
Adam Hathcock
6196e26044 also remove from sln 2026-01-03 13:57:42 +00:00
Adam Hathcock
46a4064989 remove old bulid 2026-01-03 13:54:51 +00:00
Adam Hathcock
72b3948f43 Merge pull request #1095 from adamhathcock/copilot/add-github-action-nuget-release
Add GitHub Actions workflow for automated NuGet releases with multi-platform builds
2026-01-03 13:49:38 +00:00
copilot-swe-agent[bot]
5d47bfaeb6 Add tag push trigger to workflow
- Workflow now triggers on tag pushes in addition to branch pushes
- Tags must match pattern: [0-9]+.[0-9]+.[0-9]+ (e.g., 0.43.0)
- Updated documentation to explain both triggering methods
- Allows publishing by pushing tags directly without requiring branch push

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-03 13:36:19 +00:00
Adam Hathcock
b2f2ea65ba fmt 2026-01-03 13:31:15 +00:00
Adam Hathcock
7afa468e15 Update build/Program.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-03 13:29:29 +00:00
Adam Hathcock
29cb1fed12 Update build/Program.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-03 13:29:05 +00:00
Adam Hathcock
29f8b512c4 Potential fix for code scanning alert no. 7: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-01-03 13:23:13 +00:00
copilot-swe-agent[bot]
9794c8ba72 Remove GitHub release creation - will be done manually
- Removed create-release build target from build/Program.cs
- Removed CreateRelease constant
- Removed "Create GitHub Release" step from workflow
- Removed permissions: contents: write since no longer creating releases
- Updated NUGET_RELEASE.md to remove all references to GitHub release creation
- Updated TESTING.md to remove GitHub release verification steps
- GitHub releases will now be created manually instead of automatically

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-03 13:19:23 +00:00
copilot-swe-agent[bot]
f1b305f682 Add matrix build and master branch support to release workflow
- Workflow now triggers on both master and release branches
- Added matrix strategy for windows-latest and ubuntu-latest
- Both OS platforms build and test the project
- Only Windows build publishes to NuGet and creates GitHub releases
- Upload artifacts are now OS-specific (windows-latest-nuget-package, ubuntu-latest-nuget-package)
- Updated documentation to reflect multi-platform builds and master branch support

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-03 12:54:05 +00:00
Adam Hathcock
091a800c9d Potential fix for code scanning alert no. 6: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-01-03 12:36:50 +00:00
Adam Hathcock
975f5f4b4c make things async 2026-01-03 12:35:34 +00:00
copilot-swe-agent[bot]
9d6cd930ea Use next minor version with beta suffix for prereleases
- Changed prerelease versioning to increment minor version instead of using last tag
- Changed suffix from "preview" to "beta"
- Format is now {NEXT_MINOR_VERSION}-beta.{COMMIT_COUNT}
- Example: 0.43.0-beta.123 (if last tag is 0.42.x)
- Updated documentation in NUGET_RELEASE.md and TESTING.md

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-03 11:23:32 +00:00
copilot-swe-agent[bot]
ea02d31096 Add IsArchiveAsync overloads for Zip and GZip factories
- Added IsArchiveAsync interface method to IFactory
- Implemented async versions of IsZipFile, IsZipMulti, IsGZipFile
- Updated ZipFactory and GZipFactory to override IsArchiveAsync
- Updated ReaderFactory.OpenAsync to use IsArchiveAsync
- Fixed Zip_Reader_Disposal_Test2_Async to use ReaderFactory.OpenAsync
- Fixed TestStream to properly forward ReadAsync calls
- Removed BufferedStream wrapping from AsyncBinaryReader as it uses sync Read
- Added default implementation in Factory base class

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-02 18:10:54 +00:00
copilot-swe-agent[bot]
d5913e8371 Fix TESTING.md to use valid version tag format
- Changed example from 0.42.2-test to 0.42.2 to match regex pattern
- Added note that tags must be pure semantic versions (MAJOR.MINOR.PATCH)
- Addresses bot review feedback about documentation inconsistency

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-02 18:04:13 +00:00
copilot-swe-agent[bot]
7f71f76f6e Remove SHA from prerelease version, use only commit count
- Changed prerelease version format from {LAST_TAG}-preview.{COMMIT_COUNT}+{SHA} to {LAST_TAG}-preview.{COMMIT_COUNT}
- Updated documentation in NUGET_RELEASE.md and TESTING.md to reflect the change
- Removed git rev-parse call for short SHA since it's no longer needed

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-02 18:03:18 +00:00
Adam Hathcock
d04830ba90 Add some OpenAsync 2026-01-02 17:52:18 +00:00
copilot-swe-agent[bot]
caa82a6146 Remove duplicate entry in documentation
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-01 11:49:41 +00:00
copilot-swe-agent[bot]
bcf7137073 Fix code review issues: use cross-platform git execution
- Replaced bash-specific Process execution with SimpleExec's ReadAsync
- Fixed git command execution to work on Windows and Linux
- Added comment about API key handling in push-to-nuget target
- Removed unused System.Diagnostics import

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-01 11:48:25 +00:00
copilot-swe-agent[bot]
9238cf1128 Move bash logic to C# build targets
- Added determine-version, update-version, push-to-nuget, and create-release build targets
- All version detection and publishing logic now in build/Program.cs
- Workflow calls C# build targets instead of bash scripts
- Updated documentation to reflect C# implementation

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-01 11:44:20 +00:00
copilot-swe-agent[bot]
2f874ace51 Add comment clarifying sort -V usage
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-01 10:48:32 +00:00
copilot-swe-agent[bot]
2feabed297 Remove redundant NUGET_API_KEY environment variable
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-01 10:47:04 +00:00
copilot-swe-agent[bot]
9001e28b36 Add GitHub Actions workflow for NuGet releases
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-01 10:44:59 +00:00
copilot-swe-agent[bot]
24d651d7ae Initial plan 2026-01-01 10:37:43 +00:00
Adam Hathcock
8533b09091 start of implementing zip reading async 2025-12-31 14:53:55 +00:00
Adam Hathcock
44b7955d85 reader tests 2025-12-31 14:43:15 +00:00
Adam Hathcock
038b9f18c6 Merge remote-tracking branch 'origin/master' into copilot/add-buffered-stream-async-read 2025-12-31 14:24:31 +00:00
Adam Hathcock
5667595587 Merge pull request #1091 from adamhathcock/adam/update-deps2
Update dependencies
2025-12-31 14:23:58 +00:00
copilot-swe-agent[bot]
6e0e20ba6e Fix zip64_locator to use ReadUInt32Async instead of ReadUInt16Async
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-30 11:50:41 +00:00
copilot-swe-agent[bot]
ec31cb9987 Fix Zip headers to support both sync and async reading
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-30 11:47:31 +00:00
Adam Hathcock
32d5b61c4a Merge pull request #1093 from adamhathcock/copilot/sub-pr-1091 2025-12-30 11:32:05 +00:00
copilot-swe-agent[bot]
128c9e639f Add back System.Buffers and System.Memory packages
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-30 11:30:12 +00:00
copilot-swe-agent[bot]
5e3f01dc03 Initial plan 2025-12-30 11:26:39 +00:00
copilot-swe-agent[bot]
39a0b4ce78 Use BufferedStream for async reading in AsyncBinaryReader
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-30 11:23:55 +00:00
Adam Hathcock
af719707bf Merge branch 'adam/async-binary-reader' into copilot/add-buffered-stream-async-read 2025-12-30 11:17:16 +00:00
copilot-swe-agent[bot]
8415a19912 Initial plan 2025-12-30 11:15:28 +00:00
Adam Hathcock
1607d2768e Merge branch 'master' into adam/async-binary-reader 2025-12-30 11:13:14 +00:00
Adam Hathcock
c97c05a3a7 Update dependencies 2025-12-30 11:11:40 +00:00
Adam Hathcock
b2beea9c4e Merge pull request #1077 from adamhathcock/copilot/provide-interface-for-entries
Remove ExtractAllEntries restriction for non-SOLID archives
2025-12-23 15:47:52 +00:00
Adam Hathcock
41fbaa1c28 Merge pull request #1085 from adamhathcock/adam/edit-md-files
add some markdown files for planning
2025-12-23 15:38:56 +00:00
Adam Hathcock
d9274cf794 Update tests/SharpCompress.Test/ExtractAllEntriesTests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 15:38:17 +00:00
Adam Hathcock
583b048046 Update USAGE.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 15:38:00 +00:00
Adam Hathcock
ead5916eae Update USAGE.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 15:37:28 +00:00
Adam Hathcock
d15ab92da3 add some markdown files for planning 2025-12-23 15:37:04 +00:00
Adam Hathcock
1ab30f2af5 Merge pull request #1084 from adamhathcock/copilot/fix-handled-system-exception
Avoid NotSupportedException overhead in SharpCompressStream for non-seekable streams
2025-12-23 15:15:15 +00:00
Adam Hathcock
4dbe0b91f1 Update src/SharpCompress/IO/SharpCompressStream.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 15:06:24 +00:00
Adam Hathcock
a972d3784e Merge pull request #1082 from adamhathcock/dependabot/nuget/JetBrains.Profiler.SelfApi-2.5.15
Bump JetBrains.Profiler.SelfApi from 2.5.14 to 2.5.15
2025-12-23 15:04:17 +00:00
copilot-swe-agent[bot]
6991900eb0 Remove try/catch blocks, just check CanSeek as requested
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-23 15:01:41 +00:00
copilot-swe-agent[bot]
d614beb9eb Add explanatory comments for CanSeek checks and try-catch blocks
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-23 14:41:44 +00:00
copilot-swe-agent[bot]
253a46d458 Fix NotSupportedException in SharpCompressStream by checking CanSeek
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-23 14:38:11 +00:00
copilot-swe-agent[bot]
32b1ec32c6 Initial plan 2025-12-23 14:32:38 +00:00
Adam Hathcock
eb2cba09b2 update usage 2025-12-23 09:34:55 +00:00
Adam Hathcock
e79dceb67e check should be there 2025-12-23 09:31:51 +00:00
Adam Hathcock
87c38d6dab fix ordering and token passing 2025-12-23 09:22:38 +00:00
dependabot[bot]
9e98d9c45c Bump JetBrains.Profiler.SelfApi from 2.5.14 to 2.5.15
---
updated-dependencies:
- dependency-name: JetBrains.Profiler.SelfApi
  dependency-version: 2.5.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 09:39:01 +00:00
Adam Hathcock
0e9a4b0511 Merge remote-tracking branch 'origin/master' into copilot/provide-interface-for-entries
# Conflicts:
#	src/SharpCompress/Archives/AbstractArchive.cs
2025-12-19 15:41:51 +00:00
Adam Hathcock
eae25aff64 Merge pull request #1076 from adamhathcock/adam/extract-all-test
add extract all test
2025-12-19 14:58:33 +00:00
Adam Hathcock
b8c06ff36e oops 2025-12-19 14:39:37 +00:00
Adam Hathcock
6cf2e054bf fmt 2025-12-19 14:33:24 +00:00
Adam Hathcock
95749234f5 Use proper polyfill for net48 2025-12-19 14:33:06 +00:00
Adam Hathcock
b976961434 Merge fixes 2025-12-19 14:22:09 +00:00
Adam Hathcock
e1aa727513 Merge remote-tracking branch 'origin/master' into adam/extract-all-test
# Conflicts:
#	src/SharpCompress/Archives/IArchiveExtensions.cs
2025-12-19 14:16:29 +00:00
Adam Hathcock
1f71ce1be2 Merge pull request #1080 from adamhathcock/copilot/refactor-write-to-directory-consistency
Standardize extraction API to WriteToDirectory with IProgress support
2025-12-19 14:15:43 +00:00
Adam Hathcock
cf13de6ac1 Merge pull request #1081 from adamhathcock/copilot/sub-pr-1076-again
Fix async LZMA extraction bug for 7Zip archives
2025-12-19 14:14:16 +00:00
Adam Hathcock
c2e01798f8 refactor archive extensions 2025-12-19 14:09:01 +00:00
copilot-swe-agent[bot]
8fc8295a89 Add TODO and explanation for SyncOnlyStream workaround
The SyncOnlyStream wrapper is necessary because the LZMA decoder has bugs in its async implementation that cause state corruption (IndexOutOfRangeException, DataErrorException) even with fresh, non-shared streams.

Without this wrapper, async operations on LZMA streams fail. The proper fix would be to repair the async bugs in LzmaStream.ReadAsync, Decoder.CodeAsync, and OutWindow async operations, but that requires deep changes to the decoder state machine.

Added detailed comments explaining this is a workaround and where the real fix should go.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 13:47:49 +00:00
copilot-swe-agent[bot]
d392991764 Remove CancellationToken from synchronous WriteToDirectory overload
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 13:40:22 +00:00
copilot-swe-agent[bot]
e57e87090f Implement option 2: Avoid async LZMA state corruption for 7Zip extraction
- Each 7Zip file now gets a fresh decompression stream instead of reusing shared streams
- Added SyncOnlyStream wrapper to force async operations to use synchronous equivalents
- This avoids LZMA decoder state corruption bugs in async operations
- Performance trade-off: slower async extraction for 7Zip but correctness guaranteed
- Documented behavior and performance implications in FORMATS.md

All 14 ExtractAll tests now pass (both async and sync)

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 13:33:04 +00:00
copilot-swe-agent[bot]
c701bbbee3 Run CSharpier formatting and document check-format process
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 13:31:35 +00:00
copilot-swe-agent[bot]
2f0eb0bd4b Replace Action<double> with IProgress<ProgressReport> for progress reporting
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 13:25:36 +00:00
copilot-swe-agent[bot]
17bde8da8a Merge branch 'master' of https://github.com/adamhathcock/sharpcompress into copilot/refactor-write-to-directory-consistency 2025-12-19 13:10:33 +00:00
Adam Hathcock
99d355e6ca Merge remote-tracking branch 'origin/master' into copilot/sub-pr-1076-again 2025-12-19 13:05:33 +00:00
Adam Hathcock
c790fd21a4 reading a single byte shouldn't be async 2025-12-19 13:05:04 +00:00
Adam Hathcock
bee51af48b Merge pull request #1044 from adamhathcock/copilot/add-progress-reporting
Unified progress reporting for compression and extraction operations
2025-12-19 12:54:55 +00:00
Adam Hathcock
ca743eae22 fix for running net 10 tests 2025-12-19 12:17:08 +00:00
copilot-swe-agent[bot]
93504cf82f Add sync test and attempt to fix async LZMA extraction bug
- Restored original async ExtractAllEntries test with using statement
- Added new ExtractAllEntriesSync test (all tests pass)
- Fixed potential partial read bug in LzmaStream.DecodeChunkHeaderAsync
  - Added ReadFullyAsync helper to ensure complete reads
  - ReadAsync is not guaranteed to return all requested bytes
- Async tests for 7Zip still failing with Data Error
  - Issue appears related to LZMA2 stream state management
  - _needDictReset flag not being cleared correctly in async flow
  - Further investigation needed

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 12:15:54 +00:00
Adam Hathcock
6d3e4e842b Merge remote-tracking branch 'origin/master' into copilot/add-progress-reporting 2025-12-19 12:13:34 +00:00
copilot-swe-agent[bot]
54b64a8c3b Fix misleading variable name: emptyDirectory -> parentDirectory
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 12:06:36 +00:00
copilot-swe-agent[bot]
0e59bf39f4 Add test for IArchive.WriteToDirectoryAsync
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 12:04:47 +00:00
copilot-swe-agent[bot]
8b95e0a76d Standardize on WriteToDirectory naming and add async support
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 11:57:01 +00:00
copilot-swe-agent[bot]
48a2ad7b57 Fix ExtractAll test to use synchronous extraction methods for 7Zip archives
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 11:55:00 +00:00
copilot-swe-agent[bot]
cfc6651fff Update documentation to reflect ExtractAllEntries universal compatibility
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 11:49:35 +00:00
copilot-swe-agent[bot]
b23827a8db Initial plan 2025-12-19 11:48:49 +00:00
copilot-swe-agent[bot]
3f9986c13c Initial plan 2025-12-19 11:47:53 +00:00
copilot-swe-agent[bot]
224989f19b Remove restriction on ExtractAllEntries and add comprehensive tests
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 11:45:21 +00:00
Adam Hathcock
c7010b75c1 fix the test targets 2025-12-19 11:41:08 +00:00
Adam Hathcock
00cfeee56e Fix logic to match ExtractAllEntries 2025-12-19 11:37:04 +00:00
copilot-swe-agent[bot]
aaa97e2ce2 Merge master branch - add ZStandard compression support and TarHeaderWriteFormat
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-19 11:36:44 +00:00
copilot-swe-agent[bot]
1d52618137 Initial plan 2025-12-19 11:32:17 +00:00
Adam Hathcock
34309f17f4 fmt 2025-12-18 15:33:26 +00:00
Adam Hathcock
220ba67faa add extract all test 2025-12-18 15:26:35 +00:00
Adam Hathcock
230f96e8e8 Merge pull request #1052 from adamhathcock/copilot/move-zstdsharp-into-sharpcompress
Move ZstdSharp into SharpCompress - Complete Integration
2025-12-18 14:48:03 +00:00
Adam Hathcock
930c8899d2 Merge remote-tracking branch 'origin/master' into copilot/move-zstdsharp-into-sharpcompress
# Conflicts:
#	src/SharpCompress/packages.lock.json
#	tests/SharpCompress.Test/packages.lock.json
2025-12-18 12:43:19 +00:00
copilot-swe-agent[bot]
7c0cef7dd8 Clean up unused using statements in ZstandardConstants.cs
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-18 12:32:48 +00:00
copilot-swe-agent[bot]
951ebb3fa2 Complete ZstdSharp integration: Add all Unsafe implementation files and remove ZstdSharp.Port dependency
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
2a4d098b41 Initial plan 2025-12-18 12:30:38 +00:00
dependabot[bot]
5839b87f98 Bump csharpier from 1.2.1 to 1.2.3
---
updated-dependencies:
- dependency-name: csharpier
  dependency-version: 1.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-18 12:30:38 +00:00
dependabot[bot]
425a2bd680 Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-18 12:30:38 +00:00
drone1400
939c2497c8 Reformatted modified files with csharpier 2025-12-18 12:30:38 +00:00
drone1400
8995ba56b8 Add alternative option for writing TAR archives with USTAR header format
- TarWriterOptions now has a property that allows the user to select writing the TAR using the USTAR header format
- if unspecified, will default to the original modern GNU TAR header format
- default behavior is unchanged
2025-12-18 12:30:38 +00:00
HeroponRikIBestest
e941ab60ca Csharpier 2025-12-18 12:30:38 +00:00
HeroponRikIBestest
48860f1349 Improve logic 2025-12-18 12:30:38 +00:00
HeroponRikIBestest
94b2c5c593 Add tests 2025-12-18 12:30:38 +00:00
HeroponRikIBestest
f1d8ae5a22 Add archive-level IsEncrypted flag 2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
e44d2093e5 Consolidate agent instructions into AGENTS.md
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
9fa1201a4c Initial plan 2025-12-18 12:30:38 +00:00
Adam Hathcock
7800808648 first pass of instructions...consolidate? 2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
2789e86d21 Update launch.json to use net10.0 instead of net8.0
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
afd1e39b88 Initial plan 2025-12-18 12:30:38 +00:00
Adam Hathcock
aec4c738ef add vscode config 2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
63ecc8c842 Apply code review feedback: improve documentation structure and add explicit DirectoryEntry case
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-18 12:30:38 +00:00
Adam Hathcock
5f6d583521 Update tests/SharpCompress.Test/Zip/ZipReaderTests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
0341984f10 Add documentation about ZipReader directory and central directory handling
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
fe757486ae Add multi-volume ZIP documentation to FORMATS.md
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
46d480c9a1 Changes before error encountered
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
8fe3cba7a8 Initial plan 2025-12-18 12:30:38 +00:00
Adam Hathcock
aa19f4da8b update dependencies 2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
a08f95326c Remove netstandard2.0 and net481 targets, keep net48, net8.0, net10.0
- Updated target frameworks to net48;net8.0;net10.0 only
- Removed netstandard2.0 and net481 package conditions
- Updated conditional compilation to use NETFRAMEWORK only
- Updated package description and README

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
b4c5437c92 Fix .NET 10.0 formatting in README
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
8680e3b39e Drop .NET 6 support and add .NET 10 support
- Updated SharpCompress.csproj target frameworks from net48;net481;netstandard2.0;net6.0;net8.0 to net48;net481;netstandard2.0;net8.0;net10.0
- Updated test and build projects to use .NET 10
- Updated global.json to .NET 10 SDK
- Updated CI workflow to use .NET 10
- Fixed deprecated Rfc2898DeriveBytes constructor for .NET 10 (SYSLIB0060)
- Updated package description and README to reflect new supported frameworks
- Updated package versions for .NET 10 compatibility

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-18 12:30:38 +00:00
copilot-swe-agent[bot]
1b3002c8df Initial plan 2025-12-18 12:30:38 +00:00
Adam Hathcock
394fd2e7db Merge pull request #1072 from adamhathcock/dependabot/nuget/dot-config/csharpier-1.2.3
Bump csharpier from 1.2.1 to 1.2.3
2025-12-15 11:02:17 +00:00
Adam Hathcock
d83af56d28 Merge pull request #1071 from adamhathcock/dependabot/github_actions/actions/upload-artifact-6
Bump actions/upload-artifact from 5 to 6
2025-12-15 10:58:08 +00:00
dependabot[bot]
28c93d6841 Bump csharpier from 1.2.1 to 1.2.3
---
updated-dependencies:
- dependency-name: csharpier
  dependency-version: 1.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 09:42:59 +00:00
dependabot[bot]
5f52fc2176 Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 09:02:14 +00:00
Adam Hathcock
8fba579e3a Merge pull request #1063 from drone1400/tar-ustar-header-format-support
Add alternative option for writing TAR archives with USTAR header format
2025-12-09 08:37:18 +00:00
drone1400
40b1aadeb2 Reformatted modified files with csharpier 2025-12-08 17:49:58 +02:00
Adam Hathcock
40e72ad199 fix AI edit 2025-12-08 11:11:51 +00:00
Adam Hathcock
618b4bbb83 try to tell agents to format 2025-12-08 11:04:08 +00:00
Adam Hathcock
1eaf3e6294 format with csharpier 2025-12-08 11:00:29 +00:00
Adam Hathcock
fd453e946d Update src/SharpCompress/IO/ProgressReportingStream.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 10:55:24 +00:00
Adam Hathcock
c294071015 Update src/SharpCompress/Archives/IArchiveEntryExtensions.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 10:55:06 +00:00
Adam Hathcock
c2f6055e33 format 2025-12-08 10:26:45 +00:00
drone1400
5161f4df33 Add alternative option for writing TAR archives with USTAR header format
- TarWriterOptions now has a property that allows the user to select writing the TAR using the USTAR header format
- if unspecified, will default to the original modern GNU TAR header format
- default behavior is unchanged
2025-12-06 17:33:01 +02:00
copilot-swe-agent[bot]
3396f8fe00 Refactor to use ProgressReportingStream for progress tracking
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-05 11:55:29 +00:00
copilot-swe-agent[bot]
9291f58091 Merge master and add comprehensive tests for archive and reader progress
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-12-05 11:44:17 +00:00
copilot-swe-agent[bot]
85f3b17c42 Merge remote-tracking branch 'origin/master' into copilot/add-progress-reporting 2025-12-05 11:33:39 +00:00
Adam Hathcock
2a3086a0d7 Merge pull request #1060 from HeroponRikiBestest/rar-7z-password 2025-12-03 19:58:47 +00:00
HeroponRikIBestest
41c3cc1a18 Csharpier 2025-12-03 12:05:16 -05:00
HeroponRikIBestest
1b1df86a11 Improve logic 2025-12-02 10:02:49 -05:00
HeroponRikIBestest
e0660e7775 Add tests 2025-12-02 09:55:24 -05:00
HeroponRikIBestest
99a6c4de88 Add archive-level IsEncrypted flag 2025-12-02 09:47:06 -05:00
Adam Hathcock
ffa765bd97 Merge pull request #1057 from adamhathcock/adam/add-copilot-instructions 2025-11-30 13:48:55 +00:00
Adam Hathcock
b1696524b3 Merge pull request #1058 from adamhathcock/copilot/sub-pr-1057 2025-11-30 13:48:31 +00:00
copilot-swe-agent[bot]
14d432e22d Pass progress as parameter to WriteTo/WriteToAsync instead of storing on archive
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-30 13:04:40 +00:00
copilot-swe-agent[bot]
6a37c55085 Consolidate agent instructions into AGENTS.md
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-30 12:52:22 +00:00
copilot-swe-agent[bot]
9c1c6fff9f Initial plan 2025-11-30 12:49:36 +00:00
Adam Hathcock
db8c6f4bcb first pass of instructions...consolidate? 2025-11-30 12:47:57 +00:00
Adam Hathcock
ff17ecda7d Merge pull request #1055 from adamhathcock/adam/vscode-fixes
add vscode config
2025-11-30 12:47:16 +00:00
Adam Hathcock
692058677c Merge pull request #1056 from adamhathcock/copilot/sub-pr-1055
Fix launch.json debug configurations to use net10.0
2025-11-30 12:41:38 +00:00
copilot-swe-agent[bot]
1e90d69912 Update launch.json to use net10.0 instead of net8.0
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-30 12:22:31 +00:00
copilot-swe-agent[bot]
64a1cc68e1 Initial plan 2025-11-30 12:20:37 +00:00
copilot-swe-agent[bot]
0fdf9c74a8 Address code review: Replace dynamic with IArchiveProgressInfo interface
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-30 12:07:17 +00:00
Adam Hathcock
20353f35ff add vscode config 2025-11-30 12:05:08 +00:00
copilot-swe-agent[bot]
e2df7894f9 Remove IArchiveExtractionListener and add IProgress support to Archive Entry extraction
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-30 12:00:54 +00:00
copilot-swe-agent[bot]
7af029b5de Address code review: properly handle zero-sized entries in progress reporting
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 17:48:50 +00:00
copilot-swe-agent[bot]
8fc5ca5a71 Unify progress reporting: remove IExtractionListener and add IProgress support for reading
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 17:40:10 +00:00
copilot-swe-agent[bot]
aa0356de9f Changes before error encountered
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 15:55:21 +00:00
Adam Hathcock
e44a43d2b1 Merge pull request #1054 from adamhathcock/copilot/fix-zipreader-directory-entry
Document ZipReader DirectoryEntry behavior and add verification test
2025-11-29 15:46:51 +00:00
Adam Hathcock
8997f00b9b Merge pull request #1049 from adamhathcock/copilot/drop-dotnet-6-support
Drop .NET 6, .NET Standard 2.0, .NET 4.8.1, add .NET 10 support
2025-11-29 15:46:27 +00:00
copilot-swe-agent[bot]
c5da416764 Apply code review feedback: improve documentation structure and add explicit DirectoryEntry case
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 15:39:36 +00:00
Adam Hathcock
840e58fc03 Update tests/SharpCompress.Test/Zip/ZipReaderTests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-29 15:36:46 +00:00
copilot-swe-agent[bot]
7f911c5219 Add documentation about ZipReader directory and central directory handling
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 15:29:07 +00:00
copilot-swe-agent[bot]
a887390c23 Add multi-volume ZIP documentation to FORMATS.md
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 15:25:35 +00:00
copilot-swe-agent[bot]
f4dddcec8e Changes before error encountered
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 14:06:03 +00:00
copilot-swe-agent[bot]
0d9d82d7e6 Initial plan 2025-11-29 13:42:43 +00:00
copilot-swe-agent[bot]
3a6d24b1d9 Add ZStandard frame and dictionary types
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 13:36:01 +00:00
copilot-swe-agent[bot]
b9b159be4c Add ZStandard compression parameter types and enums
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 13:34:18 +00:00
copilot-swe-agent[bot]
40212083a5 Add core ZStandard infrastructure: UnsafeHelper, ThrowHelper, and Unsafe Methods
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 13:28:16 +00:00
copilot-swe-agent[bot]
d3428b066e Fix XML documentation comments in buffer structs
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 13:14:52 +00:00
copilot-swe-agent[bot]
94c64b2a45 Add initial ZStandard infrastructure types for ZstdSharp integration
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 13:11:19 +00:00
copilot-swe-agent[bot]
0d671a0bb2 Initial plan 2025-11-29 13:03:20 +00:00
Adam Hathcock
d34a47c148 update dependencies 2025-11-29 11:56:20 +00:00
copilot-swe-agent[bot]
5aa216bd21 Remove netstandard2.0 and net481 targets, keep net48, net8.0, net10.0
- Updated target frameworks to net48;net8.0;net10.0 only
- Removed netstandard2.0 and net481 package conditions
- Updated conditional compilation to use NETFRAMEWORK only
- Updated package description and README

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 11:42:05 +00:00
copilot-swe-agent[bot]
8af47548fe Fix .NET 10.0 formatting in README
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 11:04:54 +00:00
copilot-swe-agent[bot]
131bd2b7b8 Drop .NET 6 support and add .NET 10 support
- Updated SharpCompress.csproj target frameworks from net48;net481;netstandard2.0;net6.0;net8.0 to net48;net481;netstandard2.0;net8.0;net10.0
- Updated test and build projects to use .NET 10
- Updated global.json to .NET 10 SDK
- Updated CI workflow to use .NET 10
- Fixed deprecated Rfc2898DeriveBytes constructor for .NET 10 (SYSLIB0060)
- Updated package description and README to reflect new supported frameworks
- Updated package versions for .NET 10 compatibility

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 11:03:48 +00:00
copilot-swe-agent[bot]
1993673a22 Initial plan 2025-11-29 10:42:07 +00:00
copilot-swe-agent[bot]
0f374b27cf Address code review: ProgressReportingStream now throws on writes
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-27 19:17:10 +00:00
copilot-swe-agent[bot]
0d487df61b Add IProgress support for compression operations with tests
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-27 19:15:10 +00:00
copilot-swe-agent[bot]
c082d4203b Changes before error encountered
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-27 16:58:32 +00:00
copilot-swe-agent[bot]
d4380b6bb6 Initial plan 2025-11-27 16:48:09 +00:00
Adam Hathcock
fb76bd82f2 first commit of async reader 2025-11-26 08:09:20 +00:00
Adam Hathcock
3bdaba46a9 fmt 2025-11-25 15:39:43 +00:00
Adam Hathcock
7c3c94ed7f Add ArcReaderAsync tests 2025-11-25 14:44:03 +00:00
472 changed files with 68395 additions and 2442 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.2.1",
"version": "1.2.5",
"commands": [
"csharpier"
],

View File

@@ -368,6 +368,9 @@ dotnet_diagnostic.NX0001.severity = error
dotnet_diagnostic.NX0002.severity = silent
dotnet_diagnostic.NX0003.severity = silent
dotnet_diagnostic.VSTHRD110.severity = error
dotnet_diagnostic.VSTHRD107.severity = error
##########################################
# Styles
##########################################

View File

@@ -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
View 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
View 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
View 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
View 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

View File

@@ -1,25 +0,0 @@
name: SharpCompress
on:
push:
branches:
- 'master'
pull_request:
types: [ opened, synchronize, reopened, ready_for_review ]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
dotnet-version: 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
View 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 }}

6
.gitignore vendored
View File

@@ -15,8 +15,12 @@ tests/TestArchives/*/Scratch
tests/TestArchives/*/Scratch2
.vs
tools
.vscode
.idea/
artifacts/
.DS_Store
*.snupkg
# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts/
**/BenchmarkDotNet.Artifacts/

9
.vscode/extensions.json vendored Normal file
View 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
View 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
View 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
View 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"
}
]
}

View File

@@ -14,6 +14,7 @@ SharpCompress is a pure C# compression library supporting multiple archive forma
- Follow the existing code style and patterns in the codebase.
## General Instructions
- **Agents should NEVER commit to git** - Agents should stage files and leave committing to the user. Only create commits when the user explicitly requests them.
- Make only high confidence suggestions when reviewing code changes.
- Write code with good maintainability practices, including comments on why certain design decisions were made.
- Handle edge cases and write clear exception handling.
@@ -28,14 +29,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 +74,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.
@@ -62,7 +111,7 @@ SharpCompress supports multiple archive and compression formats:
- **Archive Formats**: Zip, Tar, 7Zip, Rar (read-only)
- **Compression**: DEFLATE, BZip2, LZMA/LZMA2, PPMd, ZStandard (decompress only), Deflate64 (decompress only)
- **Combined Formats**: Tar.GZip, Tar.BZip2, Tar.LZip, Tar.XZ, Tar.ZStandard
- See FORMATS.md for complete format support matrix
- See [docs/FORMATS.md](docs/FORMATS.md) for complete format support matrix
### Stream Handling Rules
- **Disposal**: As of version 0.21, SharpCompress closes wrapped streams by default
@@ -116,3 +165,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

View File

@@ -1,21 +1,24 @@
<Project>
<ItemGroup>
<PackageVersion Include="Bullseye" Version="6.0.0" />
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<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="8.0.0" />
<GlobalPackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
<GlobalPackageReference
Include="Microsoft.VisualStudio.Threading.Analyzers"
Version="17.14.15"
/>
</ItemGroup>
</Project>

232
README.md
View File

@@ -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, unarc and unarj 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](docs/USAGE.md#async-examples) for examples.
GitHub Actions Build -
[![SharpCompress](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml/badge.svg)](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml)
@@ -14,7 +14,7 @@ GitHub Actions Build -
Post Issues on Github!
Check the [Supported Formats](FORMATS.md) and [Basic Usage.](USAGE.md)
Check the [Supported Formats](docs/FORMATS.md) and [Basic Usage.](docs/USAGE.md)
## Recommended Formats
@@ -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

View File

@@ -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

View File

@@ -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,164 @@ 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 based on next minor 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);
// Increment minor version for next release
var nextVersion = new Version(lastVersion.Major, lastVersion.Minor + 1, 0);
// 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> 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);
}
}

View File

@@ -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" />

View File

@@ -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,51 @@
"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": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"Microsoft.VisualStudio.Threading.Analyzers": {
"type": "Direct",
"requested": "[17.14.15, )",
"resolved": "17.14.15",
"contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw=="
},
"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": "8.0.0",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
},
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
}
}
}

489
docs/API.md Normal file
View File

@@ -0,0 +1,489 @@
# API Quick Reference
Quick reference for commonly used SharpCompress APIs.
## Factory Methods
### Opening Archives
```csharp
// Auto-detect format
using (var reader = ReaderFactory.Open(stream))
{
// Works with Zip, Tar, GZip, Rar, 7Zip, etc.
}
// Specific format - Archive API
using (var archive = ZipArchive.Open("file.zip"))
using (var archive = TarArchive.Open("file.tar"))
using (var archive = RarArchive.Open("file.rar"))
using (var archive = SevenZipArchive.Open("file.7z"))
using (var archive = GZipArchive.Open("file.gz"))
// With options
var options = new ReaderOptions
{
Password = "password",
LeaveStreamOpen = true,
ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
};
using (var archive = ZipArchive.Open("encrypted.zip", options))
```
### Creating Archives
```csharp
// Writer Factory
using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
{
// Write entries
}
// Specific writer
using (var archive = ZipArchive.Create())
using (var archive = TarArchive.Create())
using (var archive = GZipArchive.Create())
// With options
var options = new WriterOptions(CompressionType.Deflate)
{
CompressionLevel = 9,
LeaveStreamOpen = false
};
using (var archive = ZipArchive.Create())
{
archive.SaveTo("output.zip", options);
}
```
---
## Archive API Methods
### Reading/Extracting
```csharp
using (var archive = ZipArchive.Open("file.zip"))
{
// Get all entries
IEnumerable<IEntry> entries = archive.Entries;
// Find specific entry
var entry = archive.Entries.FirstOrDefault(e => e.Key == "file.txt");
// Extract all
archive.WriteToDirectory(@"C:\output", new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true
});
// Extract single entry
var entry = archive.Entries.First();
entry.WriteToFile(@"C:\output\file.txt");
entry.WriteToFile(@"C:\output\file.txt", new ExtractionOptions { Overwrite = true });
// Get entry stream
using (var stream = entry.OpenEntryStream())
{
stream.CopyTo(outputStream);
}
}
// Async variants
await archive.WriteToDirectoryAsync(@"C:\output", options, cancellationToken);
using (var stream = await entry.OpenEntryStreamAsync(cancellationToken))
{
// ...
}
```
### Entry Properties
```csharp
foreach (var entry in archive.Entries)
{
string name = entry.Key; // Entry name/path
long size = entry.Size; // Uncompressed size
long compressedSize = entry.CompressedSize;
bool isDir = entry.IsDirectory;
DateTime? modTime = entry.LastModifiedTime;
CompressionType compression = entry.CompressionType;
}
```
### Creating Archives
```csharp
using (var archive = ZipArchive.Create())
{
// Add file
archive.AddEntry("file.txt", "C:\\source\\file.txt");
// Add multiple files
archive.AddAllFromDirectory("C:\\source");
archive.AddAllFromDirectory("C:\\source", "*.txt"); // Pattern
// Save to file
archive.SaveTo("output.zip", CompressionType.Deflate);
// Save to stream
archive.SaveTo(outputStream, new WriterOptions(CompressionType.Deflate)
{
CompressionLevel = 9,
LeaveStreamOpen = true
});
}
```
---
## Reader API Methods
### Forward-Only Reading
```csharp
using (var stream = File.OpenRead("file.zip"))
using (var reader = ReaderFactory.Open(stream))
{
while (reader.MoveToNextEntry())
{
IEntry entry = reader.Entry;
if (!entry.IsDirectory)
{
// Extract entry
reader.WriteEntryToDirectory(@"C:\output");
reader.WriteEntryToFile(@"C:\output\file.txt");
// Or get stream
using (var entryStream = reader.OpenEntryStream())
{
entryStream.CopyTo(outputStream);
}
}
}
}
// Async variants
while (await reader.MoveToNextEntryAsync())
{
await reader.WriteEntryToFileAsync(@"C:\output\" + reader.Entry.Key, cancellationToken);
}
// Async extraction
await reader.WriteAllToDirectoryAsync(@"C:\output",
new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
cancellationToken);
```
---
## Writer API Methods
### Creating Archives (Streaming)
```csharp
using (var stream = File.Create("output.zip"))
using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
{
// Write single file
using (var fileStream = File.OpenRead("source.txt"))
{
writer.Write("entry.txt", fileStream, DateTime.Now);
}
// Write directory
writer.WriteAll("C:\\source", "*", SearchOption.AllDirectories);
writer.WriteAll("C:\\source", "*.txt", SearchOption.TopDirectoryOnly);
// Async variants
using (var fileStream = File.OpenRead("source.txt"))
{
await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken);
}
await writer.WriteAllAsync("C:\\source", "*", SearchOption.AllDirectories, cancellationToken);
}
```
---
## Common Options
### ReaderOptions
```csharp
var options = new ReaderOptions
{
Password = "password", // For encrypted archives
LeaveStreamOpen = true, // Don't close wrapped stream
ArchiveEncoding = new ArchiveEncoding // Custom character encoding
{
Default = Encoding.GetEncoding(932)
}
};
using (var archive = ZipArchive.Open("file.zip", options))
{
// ...
}
```
### WriterOptions
```csharp
var options = new WriterOptions(CompressionType.Deflate)
{
CompressionLevel = 9, // 0-9 for Deflate
LeaveStreamOpen = true, // Don't close stream
};
archive.SaveTo("output.zip", options);
```
### ExtractionOptions
```csharp
var options = new ExtractionOptions
{
ExtractFullPath = true, // Recreate directory structure
Overwrite = true, // Overwrite existing files
PreserveFileTime = true // Keep original timestamps
};
archive.WriteToDirectory(@"C:\output", options);
```
---
## Compression Types
### Available Compressions
```csharp
// For creating archives
CompressionType.None // No compression (store)
CompressionType.Deflate // DEFLATE (default for ZIP/GZip)
CompressionType.BZip2 // BZip2
CompressionType.LZMA // LZMA (for 7Zip, LZip, XZ)
CompressionType.PPMd // PPMd (for ZIP)
CompressionType.Rar // RAR compression (read-only)
// For Tar archives
// Use CompressionType in TarWriter constructor
using (var writer = TarWriter(stream, CompressionType.GZip)) // Tar.GZip
using (var writer = TarWriter(stream, CompressionType.BZip2)) // Tar.BZip2
```
### Archive Types
```csharp
ArchiveType.Zip
ArchiveType.Tar
ArchiveType.GZip
ArchiveType.BZip2
ArchiveType.Rar
ArchiveType.SevenZip
ArchiveType.XZ
ArchiveType.ZStandard
```
---
## Patterns & Examples
### Extract with Error Handling
```csharp
try
{
using (var archive = ZipArchive.Open("archive.zip",
new ReaderOptions { Password = "password" }))
{
archive.WriteToDirectory(@"C:\output", new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true
});
}
}
catch (PasswordRequiredException)
{
Console.WriteLine("Password required");
}
catch (InvalidArchiveException)
{
Console.WriteLine("Archive is invalid");
}
catch (SharpCompressException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
```
### Extract with Progress
```csharp
var progress = new Progress<ProgressReport>(report =>
{
Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%");
});
var options = new ReaderOptions { Progress = progress };
using (var archive = ZipArchive.Open("archive.zip", options))
{
archive.WriteToDirectory(@"C:\output");
}
```
### Async Extract with Cancellation
```csharp
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMinutes(5));
try
{
using (var archive = ZipArchive.Open("archive.zip"))
{
await archive.WriteToDirectoryAsync(@"C:\output",
new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
cts.Token);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Extraction cancelled");
}
```
### Create with Custom Compression
```csharp
using (var archive = ZipArchive.Create())
{
archive.AddAllFromDirectory(@"D:\source");
// Fastest
archive.SaveTo("fast.zip", new WriterOptions(CompressionType.Deflate)
{
CompressionLevel = 1
});
// Balanced (default)
archive.SaveTo("normal.zip", CompressionType.Deflate);
// Best compression
archive.SaveTo("best.zip", new WriterOptions(CompressionType.Deflate)
{
CompressionLevel = 9
});
}
```
### Stream Processing (No File I/O)
```csharp
using (var outputStream = new MemoryStream())
using (var archive = ZipArchive.Create())
{
// Add content from memory
using (var contentStream = new MemoryStream(Encoding.UTF8.GetBytes("Hello")))
{
archive.AddEntry("file.txt", contentStream);
}
// Save to memory
archive.SaveTo(outputStream, CompressionType.Deflate);
// Get bytes
byte[] archiveBytes = outputStream.ToArray();
}
```
### Extract Specific Files
```csharp
using (var archive = ZipArchive.Open("archive.zip"))
{
var filesToExtract = new[] { "file1.txt", "file2.txt" };
foreach (var entry in archive.Entries.Where(e => filesToExtract.Contains(e.Key)))
{
entry.WriteToFile(@"C:\output\" + entry.Key);
}
}
```
### List Archive Contents
```csharp
using (var archive = ZipArchive.Open("archive.zip"))
{
foreach (var entry in archive.Entries)
{
if (entry.IsDirectory)
Console.WriteLine($"[DIR] {entry.Key}");
else
Console.WriteLine($"[FILE] {entry.Key} ({entry.Size} bytes)");
}
}
```
---
## Common Mistakes
### ✗ Wrong - Stream not disposed
```csharp
var stream = File.OpenRead("archive.zip");
var archive = ZipArchive.Open(stream);
archive.WriteToDirectory(@"C:\output");
// stream not disposed - leaked resource
```
### ✓ Correct - Using blocks
```csharp
using (var stream = File.OpenRead("archive.zip"))
using (var archive = ZipArchive.Open(stream))
{
archive.WriteToDirectory(@"C:\output");
}
// Both properly disposed
```
### ✗ Wrong - Mixing API styles
```csharp
// Loading entire archive then iterating
using (var archive = ZipArchive.Open("large.zip"))
{
var entries = archive.Entries.ToList(); // Loads all in memory
foreach (var e in entries)
{
e.WriteToFile(...); // Then extracts each
}
}
```
### ✓ Correct - Use Reader for large files
```csharp
// Streaming iteration
using (var stream = File.OpenRead("large.zip"))
using (var reader = ReaderFactory.Open(stream))
{
while (reader.MoveToNextEntry())
{
reader.WriteEntryToDirectory(@"C:\output");
}
}
```
---
## Related Documentation
- [USAGE.md](USAGE.md) - Complete code examples
- [FORMATS.md](FORMATS.md) - Supported formats
- [PERFORMANCE.md](PERFORMANCE.md) - API selection guide

659
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,659 @@
# SharpCompress Architecture Guide
This guide explains the internal architecture and design patterns of SharpCompress for contributors.
## Overview
SharpCompress is organized into three main layers:
```
┌─────────────────────────────────────────┐
│ User-Facing APIs (Top Layer) │
│ Archive, Reader, Writer Factories │
├─────────────────────────────────────────┤
│ Format-Specific Implementations │
│ ZipArchive, TarReader, GZipWriter, │
│ RarArchive, SevenZipArchive, etc. │
├─────────────────────────────────────────┤
│ Compression & Crypto (Bottom Layer) │
│ Deflate, LZMA, BZip2, AES, CRC32 │
└─────────────────────────────────────────┘
```
---
## Directory Structure
### `src/SharpCompress/`
#### `Archives/` - Archive Implementations
Contains `IArchive` implementations for seekable, random-access APIs.
**Key Files:**
- `AbstractArchive.cs` - Base class for all archives
- `IArchive.cs` - Archive interface definition
- `ArchiveFactory.cs` - Factory for opening archives
- Format-specific: `ZipArchive.cs`, `TarArchive.cs`, `RarArchive.cs`, `SevenZipArchive.cs`, `GZipArchive.cs`
**Use Archive API when:**
- Stream is seekable (file, memory)
- Need random access to entries
- Archive fits in memory
- Simplicity is important
#### `Readers/` - Reader Implementations
Contains `IReader` implementations for forward-only, non-seekable APIs.
**Key Files:**
- `AbstractReader.cs` - Base reader class
- `IReader.cs` - Reader interface
- `ReaderFactory.cs` - Auto-detection factory
- `ReaderOptions.cs` - Configuration for readers
- Format-specific: `ZipReader.cs`, `TarReader.cs`, `GZipReader.cs`, `RarReader.cs`, etc.
**Use Reader API when:**
- Stream is non-seekable (network, pipe, compressed)
- Processing large files
- Memory is limited
- Forward-only processing is acceptable
#### `Writers/` - Writer Implementations
Contains `IWriter` implementations for forward-only writing.
**Key Files:**
- `AbstractWriter.cs` - Base writer class
- `IWriter.cs` - Writer interface
- `WriterFactory.cs` - Factory for creating writers
- `WriterOptions.cs` - Configuration for writers
- Format-specific: `ZipWriter.cs`, `TarWriter.cs`, `GZipWriter.cs`
#### `Factories/` - Format Detection
Factory classes for auto-detecting archive format and creating appropriate readers/writers.
**Key Files:**
- `Factory.cs` - Base factory class
- `IFactory.cs` - Factory interface
- Format-specific: `ZipFactory.cs`, `TarFactory.cs`, `RarFactory.cs`, etc.
**How It Works:**
1. `ReaderFactory.Open(stream)` probes stream signatures
2. Identifies format by magic bytes
3. Creates appropriate reader instance
4. Returns generic `IReader` interface
#### `Common/` - Shared Types
Common types, options, and enumerations used across formats.
**Key Files:**
- `IEntry.cs` - Entry interface (file within archive)
- `Entry.cs` - Entry implementation
- `ArchiveType.cs` - Enum for archive formats
- `CompressionType.cs` - Enum for compression methods
- `ArchiveEncoding.cs` - Character encoding configuration
- `ExtractionOptions.cs` - Extraction configuration
- Format-specific headers: `Zip/Headers/`, `Tar/Headers/`, `Rar/Headers/`, etc.
#### `Compressors/` - Compression Algorithms
Low-level compression streams implementing specific algorithms.
**Algorithms:**
- `Deflate/` - DEFLATE compression (Zip default)
- `BZip2/` - BZip2 compression
- `LZMA/` - LZMA compression (7Zip, XZ, LZip)
- `PPMd/` - Prediction by Partial Matching (Zip, 7Zip)
- `ZStandard/` - ZStandard compression (decompression only)
- `Xz/` - XZ format (decompression only)
- `Rar/` - RAR-specific unpacking
- `Arj/`, `Arc/`, `Ace/` - Legacy format decompression
- `Filters/` - BCJ/BCJ2 filters for executable compression
**Each Compressor:**
- Implements a `Stream` subclass
- Provides both compression and decompression
- Some are read-only (decompression only)
#### `Crypto/` - Encryption & Hashing
Cryptographic functions and stream wrappers.
**Key Files:**
- `Crc32Stream.cs` - CRC32 calculation wrapper
- `BlockTransformer.cs` - Block cipher transformations
- AES, PKWare, WinZip encryption implementations
#### `IO/` - Stream Utilities
Stream wrappers and utilities.
**Key Classes:**
- `SharpCompressStream` - Base stream class
- `ProgressReportingStream` - Progress tracking wrapper
- `MarkingBinaryReader` - Binary reader with position marks
- `BufferedSubStream` - Buffered read-only substream
- `ReadOnlySubStream` - Read-only view of parent stream
- `NonDisposingStream` - Prevents wrapped stream disposal
---
## Design Patterns
### 1. Factory Pattern
**Purpose:** Auto-detect format and create appropriate reader/writer.
**Example:**
```csharp
// User calls factory
using (var reader = ReaderFactory.Open(stream)) // Returns IReader
{
while (reader.MoveToNextEntry())
{
// Process entry
}
}
// Behind the scenes:
// 1. Factory.Open() probes stream signatures
// 2. Detects format (Zip, Tar, Rar, etc.)
// 3. Creates appropriate reader (ZipReader, TarReader, etc.)
// 4. Returns as generic IReader interface
```
**Files:**
- `src/SharpCompress/Factories/ReaderFactory.cs`
- `src/SharpCompress/Factories/WriterFactory.cs`
- `src/SharpCompress/Factories/ArchiveFactory.cs`
### 2. Strategy Pattern
**Purpose:** Encapsulate compression algorithms as swappable strategies.
**Example:**
```csharp
// Different compression strategies
CompressionType.Deflate // DEFLATE
CompressionType.BZip2 // BZip2
CompressionType.LZMA // LZMA
CompressionType.PPMd // PPMd
// Writer uses strategy pattern
var archive = ZipArchive.Create();
archive.SaveTo("output.zip", CompressionType.Deflate); // Use Deflate
archive.SaveTo("output.bz2", CompressionType.BZip2); // Use BZip2
```
**Files:**
- `src/SharpCompress/Compressors/` - Strategy implementations
### 3. Decorator Pattern
**Purpose:** Wrap streams with additional functionality.
**Example:**
```csharp
// Progress reporting decorator
var progressStream = new ProgressReportingStream(baseStream, progressReporter);
progressStream.Read(buffer, 0, buffer.Length); // Reports progress
// Non-disposing decorator
var nonDisposingStream = new NonDisposingStream(baseStream);
using (var compressor = new DeflateStream(nonDisposingStream))
{
// baseStream won't be disposed when compressor is disposed
}
```
**Files:**
- `src/SharpCompress/IO/ProgressReportingStream.cs`
- `src/SharpCompress/IO/NonDisposingStream.cs`
### 4. Template Method Pattern
**Purpose:** Define algorithm skeleton in base class, let subclasses fill details.
**Example:**
```csharp
// AbstractArchive defines common archive operations
public abstract class AbstractArchive : IArchive
{
// Template methods
public virtual void WriteToDirectory(string destinationDirectory, ExtractionOptions options)
{
// Common extraction logic
foreach (var entry in Entries)
{
// Call subclass method
entry.WriteToFile(destinationPath, options);
}
}
// Subclasses override format-specific details
protected abstract Entry CreateEntry(EntryData data);
}
```
**Files:**
- `src/SharpCompress/Archives/AbstractArchive.cs`
- `src/SharpCompress/Readers/AbstractReader.cs`
### 5. Iterator Pattern
**Purpose:** Provide sequential access to entries.
**Example:**
```csharp
// Archive API - provides collection
IEnumerable<IEntry> entries = archive.Entries;
foreach (var entry in entries)
{
// Random access - entries already in memory
}
// Reader API - provides iterator
IReader reader = ReaderFactory.Open(stream);
while (reader.MoveToNextEntry())
{
// Forward-only iteration - one entry at a time
var entry = reader.Entry;
}
```
---
## Key Interfaces
### IArchive - Random Access API
```csharp
public interface IArchive : IDisposable
{
IEnumerable<IEntry> Entries { get; }
void WriteToDirectory(string destinationDirectory,
ExtractionOptions options = null);
IEntry FirstOrDefault(Func<IEntry, bool> predicate);
// ... format-specific methods
}
```
**Implementations:** `ZipArchive`, `TarArchive`, `RarArchive`, `SevenZipArchive`, `GZipArchive`
### IReader - Forward-Only API
```csharp
public interface IReader : IDisposable
{
IEntry Entry { get; }
bool MoveToNextEntry();
void WriteEntryToDirectory(string destinationDirectory,
ExtractionOptions options = null);
Stream OpenEntryStream();
// ... async variants
}
```
**Implementations:** `ZipReader`, `TarReader`, `RarReader`, `GZipReader`, etc.
### IWriter - Writing API
```csharp
public interface IWriter : IDisposable
{
void Write(string entryPath, Stream source,
DateTime? modificationTime = null);
void WriteAll(string sourceDirectory, string searchPattern,
SearchOption searchOption);
// ... async variants
}
```
**Implementations:** `ZipWriter`, `TarWriter`, `GZipWriter`
### IEntry - Archive Entry
```csharp
public interface IEntry
{
string Key { get; }
uint Size { get; }
uint CompressedSize { get; }
bool IsDirectory { get; }
DateTime? LastModifiedTime { get; }
CompressionType CompressionType { get; }
void WriteToFile(string fullPath, ExtractionOptions options = null);
void WriteToStream(Stream destinationStream);
Stream OpenEntryStream();
// ... async variants
}
```
---
## Adding Support for a New Format
### Step 1: Understand the Format
- Research format specification
- Understand compression/encryption used
- Study existing similar formats in codebase
### Step 2: Create Format Structure Classes
**Create:** `src/SharpCompress/Common/NewFormat/`
```csharp
// Headers and data structures
public class NewFormatHeader
{
public uint Magic { get; set; }
public ushort Version { get; set; }
// ... other fields
public static NewFormatHeader Read(BinaryReader reader)
{
// Deserialize from binary
}
}
public class NewFormatEntry
{
public string FileName { get; set; }
public uint CompressedSize { get; set; }
public uint UncompressedSize { get; set; }
// ... other fields
}
```
### Step 3: Create Archive Implementation
**Create:** `src/SharpCompress/Archives/NewFormat/NewFormatArchive.cs`
```csharp
public class NewFormatArchive : AbstractArchive
{
private NewFormatHeader _header;
private List<NewFormatEntry> _entries;
public static NewFormatArchive Open(Stream stream)
{
var archive = new NewFormatArchive();
archive._header = NewFormatHeader.Read(stream);
archive.LoadEntries(stream);
return archive;
}
public override IEnumerable<IEntry> Entries => _entries.Select(e => new Entry(e));
protected override Stream OpenEntryStream(Entry entry)
{
// Return decompressed stream for entry
}
// ... other abstract method implementations
}
```
### Step 4: Create Reader Implementation
**Create:** `src/SharpCompress/Readers/NewFormat/NewFormatReader.cs`
```csharp
public class NewFormatReader : AbstractReader
{
private NewFormatHeader _header;
private BinaryReader _reader;
public NewFormatReader(Stream stream)
{
_reader = new BinaryReader(stream);
_header = NewFormatHeader.Read(_reader);
}
public override bool MoveToNextEntry()
{
// Read next entry header
if (!_reader.BaseStream.CanRead) return false;
var entryData = NewFormatEntry.Read(_reader);
// ... set this.Entry
return entryData != null;
}
// ... other abstract method implementations
}
```
### Step 5: Create Factory
**Create:** `src/SharpCompress/Factories/NewFormatFactory.cs`
```csharp
public class NewFormatFactory : Factory, IArchiveFactory, IReaderFactory
{
// Archive format magic bytes (signature)
private static readonly byte[] NewFormatSignature = new byte[] { 0x4E, 0x46 }; // "NF"
public static NewFormatFactory Instance { get; } = new();
public IArchive CreateArchive(Stream stream)
=> NewFormatArchive.Open(stream);
public IReader CreateReader(Stream stream, ReaderOptions options)
=> new NewFormatReader(stream) { Options = options };
public bool Matches(Stream stream, ReadOnlySpan<byte> signature)
=> signature.StartsWith(NewFormatSignature);
}
```
### Step 6: Register Factory
**Update:** `src/SharpCompress/Factories/ArchiveFactory.cs`
```csharp
private static readonly IFactory[] Factories =
{
ZipFactory.Instance,
TarFactory.Instance,
RarFactory.Instance,
SevenZipFactory.Instance,
GZipFactory.Instance,
NewFormatFactory.Instance, // Add here
// ... other factories
};
```
### Step 7: Add Tests
**Create:** `tests/SharpCompress.Test/NewFormat/NewFormatTests.cs`
```csharp
public class NewFormatTests : TestBase
{
[Fact]
public void NewFormat_Extracts_Successfully()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "archive.newformat");
using (var archive = NewFormatArchive.Open(archivePath))
{
archive.WriteToDirectory(SCRATCH_FILES_PATH);
// Assert extraction
}
}
[Fact]
public void NewFormat_Reader_Works()
{
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "archive.newformat");
using (var stream = File.OpenRead(archivePath))
using (var reader = new NewFormatReader(stream))
{
Assert.True(reader.MoveToNextEntry());
Assert.NotNull(reader.Entry);
}
}
}
```
### Step 8: Add Test Archives
Place test files in `tests/TestArchives/Archives/NewFormat/` directory.
### Step 9: Document
Update `docs/FORMATS.md` with format support information.
---
## Compression Algorithm Implementation
### Creating a New Compression Stream
**Example:** Creating `CustomStream` for a custom compression algorithm
```csharp
public class CustomStream : Stream
{
private readonly Stream _baseStream;
private readonly bool _leaveOpen;
public CustomStream(Stream baseStream, bool leaveOpen = false)
{
_baseStream = baseStream;
_leaveOpen = leaveOpen;
}
public override int Read(byte[] buffer, int offset, int count)
{
// Decompress data from _baseStream into buffer
// Return number of decompressed bytes
}
public override void Write(byte[] buffer, int offset, int count)
{
// Compress data from buffer into _baseStream
}
protected override void Dispose(bool disposing)
{
if (disposing && !_leaveOpen)
{
_baseStream?.Dispose();
}
base.Dispose(disposing);
}
}
```
---
## Stream Handling Best Practices
### Disposal Pattern
```csharp
// Correct: Nested using blocks
using (var fileStream = File.OpenRead("archive.zip"))
using (var archive = ZipArchive.Open(fileStream))
{
archive.WriteToDirectory(@"C:\output");
}
// Both archive and fileStream properly disposed
// Correct: Using with options
var options = new ReaderOptions { LeaveStreamOpen = true };
var stream = File.OpenRead("archive.zip");
using (var archive = ZipArchive.Open(stream, options))
{
archive.WriteToDirectory(@"C:\output");
}
stream.Dispose(); // Manually dispose if LeaveStreamOpen = true
```
### NonDisposingStream Wrapper
```csharp
// Prevent unwanted stream closure
var baseStream = File.OpenRead("data.bin");
var nonDisposing = new NonDisposingStream(baseStream);
using (var compressor = new DeflateStream(nonDisposing))
{
// Compressor won't close baseStream when disposed
}
// baseStream still usable
baseStream.Position = 0; // Works
baseStream.Dispose(); // Manual disposal
```
---
## Performance Considerations
### Memory Efficiency
1. **Avoid loading entire archive in memory** - Use Reader API for large files
2. **Process entries sequentially** - Especially for solid archives
3. **Use appropriate buffer sizes** - Larger buffers for network I/O
4. **Dispose streams promptly** - Free resources when done
### Algorithm Selection
1. **Archive API** - Fast for small archives with random access
2. **Reader API** - Efficient for large files or streaming
3. **Solid archives** - Sequential extraction much faster
4. **Compression levels** - Trade-off between speed and size
---
## Testing Guidelines
### Test Coverage
1. **Happy path** - Normal extraction works
2. **Edge cases** - Empty archives, single file, many files
3. **Corrupted data** - Handle gracefully
4. **Error cases** - Missing passwords, unsupported compression
5. **Async operations** - Both sync and async code paths
### Test Archives
- Use `tests/TestArchives/` for test data
- Create format-specific subdirectories
- Include encrypted, corrupted, and edge case archives
- Don't recreate existing archives
### Test Patterns
```csharp
[Fact]
public void Archive_Extraction_Works()
{
// Arrange
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "test.zip");
// Act
using (var archive = ZipArchive.Open(testArchive))
{
archive.WriteToDirectory(SCRATCH_FILES_PATH);
}
// Assert
Assert.True(File.Exists(Path.Combine(SCRATCH_FILES_PATH, "file.txt")));
}
```
---
## Related Documentation
- [AGENTS.md](../AGENTS.md) - Development guidelines
- [FORMATS.md](FORMATS.md) - Supported formats

610
docs/ENCODING.md Normal file
View File

@@ -0,0 +1,610 @@
# SharpCompress Character Encoding Guide
This guide explains how SharpCompress handles character encoding for archive entries (filenames, comments, etc.).
## Overview
Most archive formats store filenames and metadata as bytes. SharpCompress must convert these bytes to strings using the appropriate character encoding.
**Common Problem:** Archives created on systems with non-UTF8 encodings (especially Japanese, Chinese systems) appear with corrupted filenames when extracted on systems that assume UTF8.
---
## ArchiveEncoding Class
### Basic Usage
```csharp
using SharpCompress.Common;
using SharpCompress.Readers;
// Configure encoding before opening archive
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding(932) // cp932 for Japanese
}
};
using (var archive = ZipArchive.Open("japanese.zip", options))
{
foreach (var entry in archive.Entries)
{
Console.WriteLine(entry.Key); // Now shows correct characters
}
}
```
### ArchiveEncoding Properties
| Property | Purpose |
|----------|---------|
| `Default` | Default encoding for filenames (fallback) |
| `CustomDecoder` | Custom decoding function for special cases |
### Setting for Different APIs
**Archive API:**
```csharp
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
};
using (var archive = ZipArchive.Open("file.zip", options))
{
// Use archive with correct encoding
}
```
**Reader API:**
```csharp
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
};
using (var stream = File.OpenRead("file.zip"))
using (var reader = ReaderFactory.Open(stream, options))
{
while (reader.MoveToNextEntry())
{
// Filenames decoded correctly
}
}
```
---
## Common Encodings
### Asian Encodings
#### cp932 (Japanese)
```csharp
// Windows-31J, Shift-JIS variant used on Japanese Windows
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding(932)
}
};
using (var archive = ZipArchive.Open("japanese.zip", options))
{
// Correctly decodes Japanese filenames
}
```
**When to use:**
- Archives from Japanese Windows systems
- Files with Japanese characters in names
#### gb2312 (Simplified Chinese)
```csharp
// Simplified Chinese
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("gb2312")
}
};
```
#### gbk (Extended Simplified Chinese)
```csharp
// Extended Simplified Chinese (more characters than gb2312)
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("gbk")
}
};
```
#### big5 (Traditional Chinese)
```csharp
// Traditional Chinese (Taiwan, Hong Kong)
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("big5")
}
};
```
#### euc-jp (Japanese, Unix)
```csharp
// Extended Unix Code for Japanese
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("eucjp")
}
};
```
#### euc-kr (Korean)
```csharp
// Extended Unix Code for Korean
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("euc-kr")
}
};
```
### Western European Encodings
#### iso-8859-1 (Latin-1)
```csharp
// Western European (includes accented characters)
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("iso-8859-1")
}
};
```
**When to use:**
- Archives from French, German, Spanish systems
- Files with accented characters (é, ñ, ü, etc.)
#### cp1252 (Windows-1252)
```csharp
// Windows Western European
// Very similar to iso-8859-1 but with additional printable characters
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("cp1252")
}
};
```
**When to use:**
- Archives from older Western European Windows systems
- Files with smart quotes and other Windows-specific characters
#### iso-8859-15 (Latin-9)
```csharp
// Western European with Euro symbol support
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("iso-8859-15")
}
};
```
### Cyrillic Encodings
#### cp1251 (Windows Cyrillic)
```csharp
// Russian, Serbian, Bulgarian, etc.
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("cp1251")
}
};
```
#### koi8-r (KOI8 Russian)
```csharp
// Russian (Unix standard)
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("koi8-r")
}
};
```
### UTF Encodings (Modern)
#### UTF-8 (Default)
```csharp
// Modern standard - usually correct for new archives
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.UTF8
}
};
```
#### UTF-16
```csharp
// Unicode - rarely used in archives
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.Unicode
}
};
```
---
## Encoding Auto-Detection
SharpCompress attempts to auto-detect encoding, but this isn't always reliable:
```csharp
// Auto-detection (default)
using (var archive = ZipArchive.Open("file.zip")) // Uses UTF8 by default
{
// May show corrupted characters if archive uses different encoding
}
// Explicit encoding (more reliable)
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
};
using (var archive = ZipArchive.Open("file.zip", options))
{
// Correct characters displayed
}
```
### When Manual Override is Needed
| Situation | Solution |
|-----------|----------|
| Archive shows corrupted characters | Specify the encoding explicitly |
| Archives from specific region | Use that region's encoding |
| Mixed encodings in archive | Use CustomDecoder |
| Testing with international files | Try different encodings |
---
## Custom Decoder
For complex scenarios where a single encoding isn't sufficient:
### Basic Custom Decoder
```csharp
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
CustomDecoder = (data, offset, length) =>
{
// Custom decoding logic
var bytes = new byte[length];
Array.Copy(data, offset, bytes, 0, length);
// Try UTF8 first
try
{
return Encoding.UTF8.GetString(bytes);
}
catch
{
// Fallback to cp932 if UTF8 fails
return Encoding.GetEncoding(932).GetString(bytes);
}
}
}
};
using (var archive = ZipArchive.Open("mixed.zip", options))
{
foreach (var entry in archive.Entries)
{
Console.WriteLine(entry.Key); // Uses custom decoder
}
}
```
### Advanced: Detect Encoding by Content
```csharp
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
CustomDecoder = DetectAndDecode
}
};
private static string DetectAndDecode(byte[] data, int offset, int length)
{
var bytes = new byte[length];
Array.Copy(data, offset, bytes, 0, length);
// Try UTF8 (most modern archives)
try
{
var str = Encoding.UTF8.GetString(bytes);
// Verify it decoded correctly (no replacement characters)
if (!str.Contains('\uFFFD'))
return str;
}
catch { }
// Try cp932 (Japanese)
try
{
var str = Encoding.GetEncoding(932).GetString(bytes);
if (!str.Contains('\uFFFD'))
return str;
}
catch { }
// Fallback to iso-8859-1 (always succeeds)
return Encoding.GetEncoding("iso-8859-1").GetString(bytes);
}
```
---
## Code Examples
### Extract Archive with Japanese Filenames
```csharp
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding(932) // cp932
}
};
using (var archive = ZipArchive.Open("japanese_files.zip", options))
{
archive.WriteToDirectory(@"C:\output", new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true
});
}
// Files extracted with correct Japanese names
```
### Extract Archive with Western European Filenames
```csharp
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("iso-8859-1")
}
};
using (var archive = ZipArchive.Open("french_files.zip", options))
{
archive.WriteToDirectory(@"C:\output");
}
// Accented characters (é, è, ê, etc.) display correctly
```
### Extract Archive with Chinese Filenames
```csharp
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("gbk") // Simplified Chinese
}
};
using (var archive = ZipArchive.Open("chinese_files.zip", options))
{
archive.WriteToDirectory(@"C:\output");
}
```
### Extract Archive with Russian Filenames
```csharp
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("cp1251") // Windows Cyrillic
}
};
using (var archive = ZipArchive.Open("russian_files.zip", options))
{
archive.WriteToDirectory(@"C:\output");
}
```
### Reader API with Encoding
```csharp
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding(932)
}
};
using (var stream = File.OpenRead("japanese.zip"))
using (var reader = ReaderFactory.Open(stream, options))
{
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
Console.WriteLine(reader.Entry.Key); // Correct characters
reader.WriteEntryToDirectory(@"C:\output");
}
}
}
```
---
## Creating Archives with Correct Encoding
When creating archives, SharpCompress uses UTF8 by default (recommended):
```csharp
// Create with UTF8 (default, recommended)
using (var archive = ZipArchive.Create())
{
archive.AddAllFromDirectory(@"D:\my_files");
archive.SaveTo("output.zip", CompressionType.Deflate);
// Archives created with UTF8 encoding
}
```
If you need to create archives for systems that expect specific encodings:
```csharp
// Note: SharpCompress Writer API uses UTF8 for encoding
// To create archives with other encodings, consider:
// 1. Let users on those systems create archives
// 2. Use system tools (7-Zip, WinRAR) with desired encoding
// 3. Post-process archives if absolutely necessary
// For now, recommend modern UTF8-based archives
```
---
## Troubleshooting Encoding Issues
### Filenames Show Question Marks (?)
```
✗ Wrong encoding detected
test文件.txt → test???.txt
```
**Solution:** Specify correct encoding explicitly
```csharp
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
Default = Encoding.GetEncoding("gbk") // Try different encodings
}
};
```
### Filenames Show Replacement Character (￿)
```
✗ Invalid bytes for selected encoding
café.txt → caf￿.txt
```
**Solution:**
1. Try a different encoding (see Common Encodings table)
2. Use CustomDecoder with fallback encoding
3. Archive might be corrupted
### Mixed Encodings in Single Archive
```csharp
// Use CustomDecoder to handle mixed encodings
var options = new ReaderOptions
{
ArchiveEncoding = new ArchiveEncoding
{
CustomDecoder = (data, offset, length) =>
{
// Try multiple encodings in priority order
var bytes = new byte[length];
Array.Copy(data, offset, bytes, 0, length);
foreach (var encoding in new[]
{
Encoding.UTF8,
Encoding.GetEncoding(932),
Encoding.GetEncoding("iso-8859-1")
})
{
try
{
var str = encoding.GetString(bytes);
if (!str.Contains('\uFFFD'))
return str;
}
catch { }
}
// Final fallback
return Encoding.GetEncoding("iso-8859-1").GetString(bytes);
}
}
};
```
---
## Encoding Reference Table
| Encoding | Code | Use Case |
|----------|------|----------|
| UTF-8 | (default) | Modern archives, recommended |
| cp932 | 932 | Japanese Windows |
| gb2312 | "gb2312" | Simplified Chinese |
| gbk | "gbk" | Extended Simplified Chinese |
| big5 | "big5" | Traditional Chinese |
| iso-8859-1 | "iso-8859-1" | Western European |
| cp1252 | "cp1252" | Windows Western European |
| cp1251 | "cp1251" | Russian/Cyrillic |
| euc-jp | "euc-jp" | Japanese Unix |
| euc-kr | "euc-kr" | Korean |
---
## Best Practices
1. **Use UTF-8 for new archives** - Most modern systems support it
2. **Ask the archive creator** - When receiving archives with corrupted names
3. **Provide encoding options** - If your app handles user archives
4. **Document your assumption** - Tell users what encoding you're using
5. **Test with international files** - Before releasing production code
---
## Related Documentation
- [USAGE.md](USAGE.md#extract-zip-which-has-non-utf8-encoded-filenamycp932) - Usage examples

View File

@@ -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
docs/OLD_CHANGELOG.md Normal file
View 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)

474
docs/PERFORMANCE.md Normal file
View File

@@ -0,0 +1,474 @@
# SharpCompress Performance Guide
This guide helps you optimize SharpCompress for performance in various scenarios.
## API Selection Guide
### Archive API vs Reader API
Choose the right API based on your use case:
| Aspect | Archive API | Reader API |
|--------|------------|-----------|
| **Stream Type** | Seekable only | Non-seekable OK |
| **Memory Usage** | All entries in memory | One entry at a time |
| **Random Access** | ✓ Yes | ✗ No |
| **Best For** | Small-to-medium archives | Large or streaming data |
| **Performance** | Fast for random access | Better for large files |
### Archive API (Fast for Random Access)
```csharp
// Use when:
// - Archive fits in memory
// - You need random access to entries
// - Stream is seekable (file, MemoryStream)
using (var archive = ZipArchive.Open("archive.zip"))
{
// Random access - all entries available
var specific = archive.Entries.FirstOrDefault(e => e.Key == "file.txt");
if (specific != null)
{
specific.WriteToFile(@"C:\output\file.txt");
}
}
```
**Performance Characteristics:**
- ✓ Instant entry lookup
- ✓ Parallel extraction possible
- ✗ Entire archive in memory
- ✗ Can't process while downloading
### Reader API (Best for Large Files)
```csharp
// Use when:
// - Processing large archives (>100 MB)
// - Streaming from network/pipe
// - Memory is constrained
// - Forward-only processing is acceptable
using (var stream = File.OpenRead("large.zip"))
using (var reader = ReaderFactory.Open(stream))
{
while (reader.MoveToNextEntry())
{
// Process one entry at a time
reader.WriteEntryToDirectory(@"C:\output");
}
}
```
**Performance Characteristics:**
- ✓ Minimal memory footprint
- ✓ Works with non-seekable streams
- ✓ Can process while downloading
- ✗ Forward-only (no random access)
- ✗ Entry lookup requires iteration
---
## Buffer Sizing
### Understanding Buffers
SharpCompress uses internal buffers for reading compressed data. Buffer size affects:
- **Speed:** Larger buffers = fewer I/O operations = faster
- **Memory:** Larger buffers = higher memory usage
### Recommended Buffer Sizes
| Scenario | Size | Notes |
|----------|------|-------|
| Embedded/IoT devices | 4-8 KB | Minimal memory usage |
| Memory-constrained | 16-32 KB | Conservative default |
| Standard use (default) | 64 KB | Recommended default |
| Large file streaming | 256 KB | Better throughput |
| High-speed SSD | 512 KB - 1 MB | Maximum throughput |
### How Buffer Size Affects Performance
```csharp
// SharpCompress manages buffers internally
// You can't directly set buffer size, but you can:
// 1. Use Stream.CopyTo with explicit buffer size
using (var entryStream = reader.OpenEntryStream())
using (var fileStream = File.Create(@"C:\output\file.txt"))
{
// 64 KB buffer (default)
entryStream.CopyTo(fileStream);
// Or specify larger buffer for faster copy
entryStream.CopyTo(fileStream, bufferSize: 262144); // 256 KB
}
// 2. Use custom buffer for writing
using (var entryStream = reader.OpenEntryStream())
using (var fileStream = File.Create(@"C:\output\file.txt"))
{
byte[] buffer = new byte[262144]; // 256 KB
int bytesRead;
while ((bytesRead = entryStream.Read(buffer, 0, buffer.Length)) > 0)
{
fileStream.Write(buffer, 0, bytesRead);
}
}
```
---
## Streaming Large Files
### Non-Seekable Stream Patterns
For processing archives from downloads or pipes:
```csharp
// Download stream (non-seekable)
using (var httpStream = await httpClient.GetStreamAsync(url))
using (var reader = ReaderFactory.Open(httpStream))
{
// Process entries as they arrive
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
reader.WriteEntryToDirectory(@"C:\output");
}
}
}
```
**Performance Tips:**
- Don't try to buffer the entire stream
- Process entries immediately
- Use async APIs for better responsiveness
### Download-Then-Extract vs Streaming
Choose based on your constraints:
| Approach | When to Use |
|----------|------------|
| **Download then extract** | Moderate size, need random access |
| **Stream during download** | Large files, bandwidth limited, memory constrained |
```csharp
// Download then extract (requires disk space)
var archivePath = await DownloadFile(url, @"C:\temp\archive.zip");
using (var archive = ZipArchive.Open(archivePath))
{
archive.WriteToDirectory(@"C:\output");
}
// Stream during download (on-the-fly extraction)
using (var httpStream = await httpClient.GetStreamAsync(url))
using (var reader = ReaderFactory.Open(httpStream))
{
while (reader.MoveToNextEntry())
{
reader.WriteEntryToDirectory(@"C:\output");
}
}
```
---
## Solid Archive Optimization
### Why Solid Archives Are Slow
Solid archives (Rar, 7Zip) group files together in a single compressed stream:
```
Solid Archive Layout:
[Header] [Compressed Stream] [Footer]
├─ File1 compressed data
├─ File2 compressed data
├─ File3 compressed data
└─ File4 compressed data
```
Extracting File3 requires decompressing File1 and File2 first.
### Sequential vs Random Extraction
**Random Extraction (Slow):**
```csharp
using (var archive = RarArchive.Open("solid.rar"))
{
foreach (var entry in archive.Entries)
{
entry.WriteToFile(@"C:\output\" + entry.Key); // ✗ Slow!
// Each entry triggers full decompression from start
}
}
```
**Sequential Extraction (Fast):**
```csharp
using (var archive = RarArchive.Open("solid.rar"))
{
// Method 1: Use WriteToDirectory (recommended)
archive.WriteToDirectory(@"C:\output", new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true
});
// Method 2: Use ExtractAllEntries
archive.ExtractAllEntries();
// Method 3: Use Reader API (also sequential)
using (var reader = RarReader.Open(File.OpenRead("solid.rar")))
{
while (reader.MoveToNextEntry())
{
reader.WriteEntryToDirectory(@"C:\output");
}
}
}
```
**Performance Impact:**
- Random extraction: O(n²) - very slow for many files
- Sequential extraction: O(n) - 10-100x faster
### Best Practices for Solid Archives
1. **Always extract sequentially** when possible
2. **Use Reader API** for large solid archives
3. **Process entries in order** from the archive
4. **Consider using 7Zip command-line** for scripted extractions
---
## Compression Level Trade-offs
### Deflate/GZip Levels
```csharp
// Level 1 = Fastest, largest size
// Level 6 = Default (balanced)
// Level 9 = Slowest, best compression
// Write with different compression levels
using (var archive = ZipArchive.Create())
{
archive.AddAllFromDirectory(@"D:\data");
// Fast compression (level 1)
archive.SaveTo("fast.zip", new WriterOptions(CompressionType.Deflate)
{
CompressionLevel = 1
});
// Default compression (level 6)
archive.SaveTo("default.zip", CompressionType.Deflate);
// Best compression (level 9)
archive.SaveTo("best.zip", new WriterOptions(CompressionType.Deflate)
{
CompressionLevel = 9
});
}
```
**Speed vs Size:**
| Level | Speed | Size | Use Case |
|-------|-------|------|----------|
| 1 | 10x | 90% | Network, streaming |
| 6 | 1x | 75% | Default (good balance) |
| 9 | 0.1x | 65% | Archival, static storage |
### BZip2 Block Size
```csharp
// BZip2 block size affects memory and compression
// 100K to 900K (default 900K)
// Smaller block size = lower memory, faster
// Larger block size = better compression, slower
using (var archive = TarArchive.Create())
{
archive.AddAllFromDirectory(@"D:\data");
// These are preset in WriterOptions via CompressionLevel
archive.SaveTo("archive.tar.bz2", CompressionType.BZip2);
}
```
### LZMA Settings
LZMA compression is very powerful but memory-intensive:
```csharp
// LZMA (7Zip, .tar.lzma):
// - Dictionary size: 16 KB to 1 GB (default 32 MB)
// - Faster preset: smaller dictionary
// - Better compression: larger dictionary
// Preset via CompressionType
using (var archive = TarArchive.Create())
{
archive.AddAllFromDirectory(@"D:\data");
archive.SaveTo("archive.tar.xz", CompressionType.LZMA); // Default settings
}
```
---
## Async Performance
### When Async Helps
Async is beneficial when:
- **Long I/O operations** (network, slow disks)
- **UI responsiveness** needed (Windows Forms, WPF, Blazor)
- **Server applications** (ASP.NET, multiple concurrent operations)
```csharp
// Async extraction (non-blocking)
using (var archive = ZipArchive.Open("archive.zip"))
{
await archive.WriteToDirectoryAsync(
@"C:\output",
new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
cancellationToken
);
}
// Thread can handle other work while I/O happens
```
### When Async Doesn't Help
Async doesn't improve performance for:
- **CPU-bound operations** (already fast)
- **Local SSD I/O** (I/O is fast enough)
- **Single-threaded scenarios** (no parallelism benefit)
```csharp
// Sync extraction (simpler, same performance on fast I/O)
using (var archive = ZipArchive.Open("archive.zip"))
{
archive.WriteToDirectory(
@"C:\output",
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
// Simple and fast - no async needed
```
### Cancellation Pattern
```csharp
var cts = new CancellationTokenSource();
// Cancel after 5 minutes
cts.CancelAfter(TimeSpan.FromMinutes(5));
try
{
using (var archive = ZipArchive.Open("archive.zip"))
{
await archive.WriteToDirectoryAsync(
@"C:\output",
new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
cts.Token
);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Extraction cancelled");
// Clean up partial extraction if needed
}
```
---
## Practical Performance Tips
### 1. Choose the Right API
| Scenario | API | Why |
|----------|-----|-----|
| Small archives | Archive | Faster random access |
| Large archives | Reader | Lower memory |
| Streaming | Reader | Works on non-seekable streams |
| Download streams | Reader | Async extraction while downloading |
### 2. Batch Operations
```csharp
// ✗ Slow - opens each archive separately
foreach (var file in files)
{
using (var archive = ZipArchive.Open("archive.zip"))
{
archive.WriteToDirectory(@"C:\output");
}
}
// ✓ Better - process multiple entries at once
using (var archive = ZipArchive.Open("archive.zip"))
{
archive.WriteToDirectory(@"C:\output");
}
```
### 3. Profile Your Code
```csharp
var sw = Stopwatch.StartNew();
using (var archive = ZipArchive.Open("large.zip"))
{
archive.WriteToDirectory(@"C:\output");
}
sw.Stop();
Console.WriteLine($"Extraction took {sw.ElapsedMilliseconds}ms");
// Measure memory before/after
var beforeMem = GC.GetTotalMemory(true);
// ... do work ...
var afterMem = GC.GetTotalMemory(true);
Console.WriteLine($"Memory used: {(afterMem - beforeMem) / 1024 / 1024}MB");
```
---
## Troubleshooting Performance
### Extraction is Slow
1. **Check if solid archive** → Use sequential extraction
2. **Check API** → Reader API might be faster for large files
3. **Check compression level** → Higher levels are slower to decompress
4. **Check I/O** → Network drives are much slower than SSD
5. **Check buffer size** → May need larger buffers for network
### High Memory Usage
1. **Use Reader API** instead of Archive API
2. **Process entries immediately** rather than buffering
3. **Reduce compression level** if writing
4. **Check for memory leaks** in your code
### CPU Usage at 100%
1. **Normal for compression** - especially with high compression levels
2. **Consider lower level** for faster processing
3. **Reduce parallelism** if processing multiple archives
4. **Check if awaiting properly** in async code
---
## Related Documentation
- [PERFORMANCE.md](USAGE.md) - Usage examples with performance considerations
- [FORMATS.md](FORMATS.md) - Format-specific performance notes

View File

@@ -1,6 +1,6 @@
# SharpCompress Usage
## Async/Await Support
## Async/Await Support (Beta)
SharpCompress now provides full async/await support for all I/O operations. All `Read`, `Write`, and extraction operations have async equivalents ending in `Async` that accept an optional `CancellationToken`. This enables better performance and scalability for I/O-bound operations.
@@ -13,7 +13,7 @@ SharpCompress now provides full async/await support for all I/O operations. All
See [Async Examples](#async-examples) section below for usage patterns.
## Stream Rules (changed with 0.21)
## Stream Rules
When dealing with Streams, the rule should be that you don't close a stream you didn't create. This, in effect, should mean you should always put a Stream in a using block to dispose it.
@@ -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
);
}
```

View File

@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.100",
"version": "10.0.100",
"rollForward": "latestFeature"
}
}

View File

@@ -1,14 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtractionListener
public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IAsyncArchive
where TEntry : IArchiveEntry
where TVolume : IVolume
{
@@ -17,11 +16,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)
@@ -31,6 +25,12 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
_sourceStream = sourceStream;
_lazyVolumes = new LazyReadOnlyCollection<TVolume>(LoadVolumes(_sourceStream));
_lazyEntries = new LazyReadOnlyCollection<TEntry>(LoadEntries(Volumes));
_lazyVolumesAsync = new LazyAsyncReadOnlyCollection<TVolume>(
LoadVolumesAsync(_sourceStream)
);
_lazyEntriesAsync = new LazyAsyncReadOnlyCollection<TEntry>(
LoadEntriesAsync(_lazyVolumesAsync)
);
}
internal AbstractArchive(ArchiveType type)
@@ -39,25 +39,16 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
ReaderOptions = new();
_lazyVolumes = new LazyReadOnlyCollection<TVolume>(Enumerable.Empty<TVolume>());
_lazyEntries = new LazyReadOnlyCollection<TEntry>(Enumerable.Empty<TEntry>());
_lazyVolumesAsync = new LazyAsyncReadOnlyCollection<TVolume>(
AsyncEnumerableEx.Empty<TVolume>()
);
_lazyEntriesAsync = new LazyAsyncReadOnlyCollection<TEntry>(
AsyncEnumerableEx.Empty<TEntry>()
);
}
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)
{
throw new ArchiveException("Archive streams must be Readable and Seekable");
}
return stream;
}
/// <summary>
/// Returns an ReadOnlyCollection of all the RarArchiveEntries across the one or many parts of the RarArchive.
/// </summary>
@@ -83,6 +74,19 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
protected abstract IEnumerable<TVolume> LoadVolumes(SourceStream sourceStream);
protected abstract IEnumerable<TEntry> LoadEntries(IEnumerable<TVolume> volumes);
protected virtual IAsyncEnumerable<TVolume> LoadVolumesAsync(SourceStream sourceStream) =>
LoadVolumes(sourceStream).ToAsyncEnumerable();
protected virtual async IAsyncEnumerable<TEntry> LoadEntriesAsync(
IAsyncEnumerable<TVolume> volumes
)
{
foreach (var item in LoadEntries(await volumes.ToListAsync()))
{
yield return item;
}
}
IEnumerable<IArchiveEntry> IArchive.Entries => Entries.Cast<IArchiveEntry>();
IEnumerable<IVolume> IArchive.Volumes => _lazyVolumes.Cast<IVolume>();
@@ -99,38 +103,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,21 +124,27 @@ 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();
}
protected abstract IReader CreateReaderForSolidExtraction();
protected abstract ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync();
/// <summary>
/// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files).
/// </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,8 +152,71 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
{
get
{
((IArchiveExtractionListener)this).EnsureEntriesLoaded();
EnsureEntriesLoaded();
return Entries.All(x => x.IsComplete);
}
}
#region Async Support
private readonly LazyAsyncReadOnlyCollection<TVolume> _lazyVolumesAsync;
private readonly LazyAsyncReadOnlyCollection<TEntry> _lazyEntriesAsync;
public virtual async ValueTask DisposeAsync()
{
if (!_disposed)
{
await foreach (var v in _lazyVolumesAsync)
{
v.Dispose();
}
foreach (var v in _lazyEntriesAsync.GetLoaded().Cast<Entry>())
{
v.Close();
}
_sourceStream?.Dispose();
_disposed = true;
}
}
private async ValueTask EnsureEntriesLoadedAsync()
{
await _lazyEntriesAsync.EnsureFullyLoaded();
await _lazyVolumesAsync.EnsureFullyLoaded();
}
public virtual IAsyncEnumerable<TEntry> EntriesAsync => _lazyEntriesAsync;
IAsyncEnumerable<IArchiveEntry> IAsyncArchive.EntriesAsync =>
EntriesAsync.Cast<TEntry, IArchiveEntry>();
public IAsyncEnumerable<IVolume> VolumesAsync => _lazyVolumesAsync.Cast<TVolume, IVolume>();
public async ValueTask<IAsyncReader> ExtractAllEntriesAsync()
{
if (!IsSolid && Type != ArchiveType.SevenZip)
{
throw new SharpCompressException(
"ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)."
);
}
await EnsureEntriesLoadedAsync();
return await CreateReaderForSolidExtractionAsync();
}
public virtual ValueTask<bool> IsSolidAsync() => new(false);
public async ValueTask<bool> IsCompleteAsync()
{
await EnsureEntriesLoadedAsync();
return await EntriesAsync.All(x => x.IsComplete);
}
public async ValueTask<long> TotalSizeAsync() =>
await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.CompressedSize);
public async ValueTask<long> TotalUncompressSizeAsync() =>
await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.Size);
#endregion
}

View File

@@ -162,7 +162,7 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
SaveTo(stream, options, OldEntries, newEntries);
}
public async Task SaveToAsync(
public async ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
CancellationToken cancellationToken = default
@@ -208,7 +208,7 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
IEnumerable<TEntry> newEntries
);
protected abstract Task SaveToAsync(
protected abstract ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<TEntry> oldEntries,

View File

@@ -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.Factories;
using SharpCompress.IO;
@@ -24,6 +26,28 @@ public static class ArchiveFactory
return FindFactory<IArchiveFactory>(stream).Open(stream, readerOptions);
}
/// <summary>
/// Opens an Archive for random access asynchronously
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async ValueTask<IAsyncArchive> OpenAsync(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
readerOptions ??= new ReaderOptions();
stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize);
var factory = await FindFactoryAsync<IArchiveFactory>(stream, cancellationToken)
.ConfigureAwait(false);
return await factory
.OpenAsync(stream, readerOptions, cancellationToken)
.ConfigureAwait(false);
}
public static IWritableArchive Create(ArchiveType type)
{
var factory = Factory
@@ -49,6 +73,22 @@ public static class ArchiveFactory
return Open(new FileInfo(filePath), options);
}
/// <summary>
/// Opens an Archive from a filepath asynchronously.
/// </summary>
/// <param name="filePath"></param>
/// <param name="options"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
string filePath,
ReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenAsync(new FileInfo(filePath), options, cancellationToken);
}
/// <summary>
/// Constructor with a FileInfo object to an existing file.
/// </summary>
@@ -61,6 +101,25 @@ public static class ArchiveFactory
return FindFactory<IArchiveFactory>(fileInfo).Open(fileInfo, options);
}
/// <summary>
/// Opens an Archive from a FileInfo object asynchronously.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="options"></param>
/// <param name="cancellationToken"></param>
public static async ValueTask<IAsyncArchive> OpenAsync(
FileInfo fileInfo,
ReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
options ??= new ReaderOptions { LeaveStreamOpen = false };
var factory = await FindFactoryAsync<IArchiveFactory>(fileInfo, cancellationToken)
.ConfigureAwait(false);
return await factory.OpenAsync(fileInfo, options, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Constructor with IEnumerable FileInfo objects, multi and split support.
/// </summary>
@@ -87,6 +146,40 @@ public static class ArchiveFactory
return FindFactory<IMultiArchiveFactory>(fileInfo).Open(filesArray, options);
}
/// <summary>
/// Opens a multi-part archive from files asynchronously.
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="options"></param>
/// <param name="cancellationToken"></param>
public static async ValueTask<IAsyncArchive> OpenAsync(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
fileInfos.NotNull(nameof(fileInfos));
var filesArray = fileInfos.ToArray();
if (filesArray.Length == 0)
{
throw new InvalidOperationException("No files to open");
}
var fileInfo = filesArray[0];
if (filesArray.Length == 1)
{
return await OpenAsync(fileInfo, options, cancellationToken).ConfigureAwait(false);
}
fileInfo.NotNull(nameof(fileInfo));
options ??= new ReaderOptions { LeaveStreamOpen = false };
var factory = FindFactory<IMultiArchiveFactory>(fileInfo);
return await factory
.OpenAsync(filesArray, options, cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Constructor with IEnumerable FileInfo objects, multi and split support.
/// </summary>
@@ -113,6 +206,41 @@ public static class ArchiveFactory
return FindFactory<IMultiArchiveFactory>(firstStream).Open(streamsArray, options);
}
/// <summary>
/// Opens a multi-part archive from streams asynchronously.
/// </summary>
/// <param name="streams"></param>
/// <param name="options"></param>
/// <param name="cancellationToken"></param>
public static async ValueTask<IAsyncArchive> OpenAsync(
IEnumerable<Stream> streams,
ReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
streams.NotNull(nameof(streams));
var streamsArray = streams.ToArray();
if (streamsArray.Length == 0)
{
throw new InvalidOperationException("No streams");
}
var firstStream = streamsArray[0];
if (streamsArray.Length == 1)
{
return await OpenAsync(firstStream, options, cancellationToken).ConfigureAwait(false);
}
firstStream.NotNull(nameof(firstStream));
options ??= new ReaderOptions();
var factory = FindFactory<IMultiArchiveFactory>(firstStream);
return await factory
.OpenAsync(streamsArray, options, cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Extract to specific directory, retaining filename
/// </summary>
@@ -166,6 +294,52 @@ public static class ArchiveFactory
);
}
private static async ValueTask<T> FindFactoryAsync<T>(
FileInfo finfo,
CancellationToken cancellationToken
)
where T : IFactory
{
finfo.NotNull(nameof(finfo));
using Stream stream = finfo.OpenRead();
return await FindFactoryAsync<T>(stream, cancellationToken);
}
private static async ValueTask<T> FindFactoryAsync<T>(
Stream stream,
CancellationToken cancellationToken
)
where T : IFactory
{
stream.NotNull(nameof(stream));
if (!stream.CanRead || !stream.CanSeek)
{
throw new ArgumentException("Stream should be readable and seekable");
}
var factories = Factory.Factories.OfType<T>();
var startPosition = stream.Position;
foreach (var factory in factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
if (await factory.IsArchiveAsync(stream, cancellationToken: cancellationToken))
{
stream.Seek(startPosition, SeekOrigin.Begin);
return factory;
}
}
var extensions = string.Join(", ", factories.Select(item => item.Name));
throw new InvalidOperationException(
$"Cannot determine compressed stream type. Supported Archive Formats: {extensions}"
);
}
public static bool IsArchive(
string filePath,
out ArchiveType? type,

View File

@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
class AutoArchiveFactory : IArchiveFactory
internal class AutoArchiveFactory : IArchiveFactory
{
public string Name => nameof(AutoArchiveFactory);
@@ -20,11 +22,30 @@ class AutoArchiveFactory : IArchiveFactory
int bufferSize = ReaderOptions.DefaultBufferSize
) => throw new NotSupportedException();
public ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize,
CancellationToken cancellationToken = default
) => throw new NotSupportedException();
public FileInfo? GetFilePart(int index, FileInfo part1) => throw new NotSupportedException();
public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) =>
ArchiveFactory.Open(stream, readerOptions);
public async ValueTask<IAsyncArchive> OpenAsync(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
) => await ArchiveFactory.OpenAsync(stream, readerOptions, cancellationToken);
public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>
ArchiveFactory.Open(fileInfo, readerOptions);
public async ValueTask<IAsyncArchive> OpenAsync(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
) => await ArchiveFactory.OpenAsync(fileInfo, readerOptions, cancellationToken);
}

View File

@@ -102,6 +102,70 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
);
}
/// <summary>
/// Opens a GZipArchive asynchronously from a stream.
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(stream, readerOptions));
}
/// <summary>
/// Opens a GZipArchive asynchronously from a FileInfo.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(fileInfo, readerOptions));
}
/// <summary>
/// Opens a GZipArchive asynchronously from multiple streams.
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(streams, readerOptions));
}
/// <summary>
/// Opens a GZipArchive asynchronously from multiple FileInfo objects.
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(fileInfos, readerOptions));
}
public static GZipArchive Create() => new();
/// <summary>
@@ -138,10 +202,13 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
SaveTo(stream, new WriterOptions(CompressionType.GZip));
}
public Task SaveToAsync(string filePath, CancellationToken cancellationToken = default) =>
public ValueTask SaveToAsync(string filePath, CancellationToken cancellationToken = default) =>
SaveToAsync(new FileInfo(filePath), cancellationToken);
public async Task SaveToAsync(FileInfo fileInfo, CancellationToken cancellationToken = default)
public async ValueTask SaveToAsync(
FileInfo fileInfo,
CancellationToken cancellationToken = default
)
{
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
await SaveToAsync(stream, new WriterOptions(CompressionType.GZip), cancellationToken)
@@ -167,6 +234,28 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
return true;
}
public static async ValueTask<bool> IsGZipFileAsync(
Stream stream,
CancellationToken cancellationToken = default
)
{
// read the header on the first read
byte[] header = new byte[10];
// workitem 8501: handle edge case (decompress empty stream)
if (!await stream.ReadFullyAsync(header, cancellationToken).ConfigureAwait(false))
{
return false;
}
if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
{
return false;
}
return true;
}
internal GZipArchive()
: base(ArchiveType.GZip) { }
@@ -213,7 +302,7 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
}
}
protected override async Task SaveToAsync(
protected override async ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<GZipArchiveEntry> oldEntries,
@@ -250,4 +339,11 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
stream.Position = 0;
return GZipReader.Open(stream);
}
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync()
{
var stream = Volumes.Single().Stream;
stream.Position = 0;
return new(GZipReader.Open(stream));
}
}

View File

@@ -23,10 +23,12 @@ public class GZipArchiveEntry : GZipEntry, IArchiveEntry
return Parts.Single().GetCompressedStream().NotNull();
}
public virtual Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
public async ValueTask<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
)
{
// GZip synchronous implementation is fast enough, just wrap it
return Task.FromResult(OpenEntryStream());
return OpenEntryStream();
}
#region IArchiveEntry Members

View File

@@ -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; }

View File

@@ -17,7 +17,7 @@ public interface IArchiveEntry : IEntry
/// Opens the current entry as a stream that will decompress as it is read asynchronously.
/// Read the entire stream or use SkipEntry on EntryStream.
/// </summary>
Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default);
ValueTask<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default);
/// <summary>
/// The archive can find all the parts of the archive needed to extract this entry.

View File

@@ -1,3 +1,4 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -8,127 +9,157 @@ namespace SharpCompress.Archives;
public static class IArchiveEntryExtensions
{
public static void WriteTo(this IArchiveEntry archiveEntry, Stream streamToWriteTo)
private const int BufferSize = 81920;
/// <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, 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 ValueTask 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, 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 async ValueTask WriteToDirectoryAsync(
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
await ExtractionMethods
.WriteEntryToDirectoryAsync(
entry,
destinationDirectory,
options,
entry.WriteToFileAsync,
cancellationToken
)
.ConfigureAwait(false);
/// <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);
},
cancellationToken
);
/// <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 async ValueTask WriteToFileAsync(
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
await 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
)
.ConfigureAwait(false);
}
}

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using SharpCompress.Common;
using SharpCompress.Readers;
@@ -10,76 +8,75 @@ 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.OpenWrite(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)
);
}
}
}
}

View File

@@ -1,10 +0,0 @@
using SharpCompress.Common;
namespace SharpCompress.Archives;
internal interface IArchiveExtractionListener : IExtractionListener
{
void EnsureEntriesLoaded();
void FireEntryExtractionBegin(IArchiveEntry entry);
void FireEntryExtractionEnd(IArchiveEntry entry);
}

View File

@@ -1,4 +1,6 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Factories;
using SharpCompress.Readers;
@@ -26,10 +28,34 @@ public interface IArchiveFactory : IFactory
/// <param name="readerOptions">reading options.</param>
IArchive Open(Stream stream, ReaderOptions? readerOptions = null);
/// <summary>
/// Opens an Archive for random access asynchronously.
/// </summary>
/// <param name="stream">An open, readable and seekable stream.</param>
/// <param name="readerOptions">reading options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask<IAsyncArchive> OpenAsync(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);
/// <summary>
/// Constructor with a FileInfo object to an existing file.
/// </summary>
/// <param name="fileInfo">the file to open.</param>
/// <param name="readerOptions">reading options.</param>
IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null);
/// <summary>
/// Opens an Archive from a FileInfo object asynchronously.
/// </summary>
/// <param name="fileInfo">the file to open.</param>
/// <param name="readerOptions">reading options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask<IAsyncArchive> OpenAsync(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
public interface IAsyncArchive : IAsyncDisposable
{
IAsyncEnumerable<IArchiveEntry> EntriesAsync { get; }
IAsyncEnumerable<IVolume> VolumesAsync { get; }
ArchiveType Type { get; }
/// <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
/// extracted sequentially for the best performance.
/// </summary>
ValueTask<IAsyncReader> ExtractAllEntriesAsync();
/// <summary>
/// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files).
/// Rar Archives can be SOLID while all 7Zip archives are considered SOLID.
/// </summary>
ValueTask<bool> IsSolidAsync();
/// <summary>
/// This checks to see if all the known entries have IsComplete = true
/// </summary>
ValueTask<bool> IsCompleteAsync();
/// <summary>
/// The total size of the files compressed in the archive.
/// </summary>
ValueTask<long> TotalSizeAsync();
/// <summary>
/// The total size of the files as uncompressed in the archive.
/// </summary>
ValueTask<long> TotalUncompressSizeAsync();
}

View File

@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
public static class IAsyncArchiveExtensions
{
/// <summary>
/// Extract to specific directory asynchronously with progress reporting and cancellation support
/// </summary>
/// <param name="archive">The archive to extract.</param>
/// <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 static async Task WriteToDirectoryAsync(
this IAsyncArchive archive,
string destinationDirectory,
ExtractionOptions? options = null,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
)
{
// For solid archives (Rar, 7Zip), use the optimized reader-based approach
if (await archive.IsSolidAsync() || archive.Type == ArchiveType.SevenZip)
{
await using var reader = await archive.ExtractAllEntriesAsync();
await reader.WriteAllToDirectoryAsync(destinationDirectory, options, cancellationToken);
}
else
{
// For non-solid archives, extract entries directly
await archive.WriteToDirectoryAsyncInternal(
destinationDirectory,
options,
progress,
cancellationToken
);
}
}
private static async Task WriteToDirectoryAsyncInternal(
this IAsyncArchive archive,
string destinationDirectory,
ExtractionOptions? options,
IProgress<ProgressReport>? progress,
CancellationToken cancellationToken
)
{
// Prepare for progress reporting
var totalBytes = await archive.TotalUncompressSizeAsync();
var bytesRead = 0L;
// Tracking for created directories.
var seenDirectories = new HashSet<string>();
// Extract
await foreach (var entry in archive.EntriesAsync.WithCancellation(cancellationToken))
{
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));
}
}
}

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Factories;
using SharpCompress.Readers;
@@ -27,10 +29,34 @@ public interface IMultiArchiveFactory : IFactory
/// <param name="readerOptions">reading options.</param>
IArchive Open(IReadOnlyList<Stream> streams, ReaderOptions? readerOptions = null);
/// <summary>
/// Opens a multi-part archive from streams asynchronously.
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions">reading options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);
/// <summary>
/// Constructor with IEnumerable Stream objects, multi and split support.
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions">reading options.</param>
IArchive Open(IReadOnlyList<FileInfo> fileInfos, ReaderOptions? readerOptions = null);
/// <summary>
/// Opens a multi-part archive from files asynchronously.
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions">reading options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);
}

View File

@@ -22,7 +22,7 @@ public interface IWritableArchive : IArchive
void SaveTo(Stream stream, WriterOptions options);
Task SaveToAsync(
ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
CancellationToken cancellationToken = default

View File

@@ -44,14 +44,14 @@ public static class IWritableArchiveExtensions
writableArchive.SaveTo(stream, options);
}
public static Task SaveToAsync(
public static ValueTask SaveToAsync(
this IWritableArchive writableArchive,
string filePath,
WriterOptions options,
CancellationToken cancellationToken = default
) => writableArchive.SaveToAsync(new FileInfo(filePath), options, cancellationToken);
public static async Task SaveToAsync(
public static async ValueTask SaveToAsync(
this IWritableArchive writableArchive,
FileInfo fileInfo,
WriterOptions options,

View File

@@ -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.Rar;
using SharpCompress.Common.Rar.Headers;
@@ -65,7 +67,13 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
return new StreamRarArchiveVolume(sourceStream, ReaderOptions, i++).AsEnumerable();
}
protected override IReader CreateReaderForSolidExtraction()
protected override IReader CreateReaderForSolidExtraction() =>
CreateReaderForSolidExtractionInternal();
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync() =>
new(CreateReaderForSolidExtractionInternal());
private RarReader CreateReaderForSolidExtractionInternal()
{
if (this.IsMultipartVolume())
{
@@ -84,6 +92,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;
@@ -179,6 +189,70 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
);
}
/// <summary>
/// Opens a RarArchive asynchronously from a stream.
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(stream, readerOptions));
}
/// <summary>
/// Opens a RarArchive asynchronously from a FileInfo.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(fileInfo, readerOptions));
}
/// <summary>
/// Opens a RarArchive asynchronously from multiple streams.
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(streams, readerOptions));
}
/// <summary>
/// Opens a RarArchive asynchronously from multiple FileInfo objects.
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(fileInfos, readerOptions));
}
public static bool IsRarFile(string filePath) => IsRarFile(new FileInfo(filePath));
public static bool IsRarFile(FileInfo fileInfo)

View File

@@ -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>())
);
}
@@ -92,7 +92,9 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
return stream;
}
public async Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
public async ValueTask<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
)
{
RarStream stream;
if (IsRarV3)
@@ -100,7 +102,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 +110,7 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
stream = new RarStream(
archive.UnpackV2017.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
);
}

View File

@@ -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;
@@ -103,6 +105,70 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
);
}
/// <summary>
/// Opens a SevenZipArchive asynchronously from a stream.
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(stream, readerOptions));
}
/// <summary>
/// Opens a SevenZipArchive asynchronously from a FileInfo.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(fileInfo, readerOptions));
}
/// <summary>
/// Opens a SevenZipArchive asynchronously from multiple streams.
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(streams, readerOptions));
}
/// <summary>
/// Opens a SevenZipArchive asynchronously from multiple FileInfo objects.
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(fileInfos, readerOptions));
}
/// <summary>
/// Constructor with a SourceStream able to handle FileInfo and Streams.
/// </summary>
@@ -199,21 +265,24 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
protected override IReader CreateReaderForSolidExtraction() =>
new SevenZipReader(ReaderOptions, this);
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync() =>
new(new SevenZipReader(ReaderOptions, this));
public override bool IsSolid =>
Entries
.Where(x => !x.IsDirectory)
.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>
{
private readonly SevenZipArchive _archive;
private CFolder? _currentFolder;
private Stream? _currentStream;
private CFileItem? _currentItem;
private SevenZipEntry? _currentEntry;
internal SevenZipReader(ReaderOptions readerOptions, SevenZipArchive archive)
: base(readerOptions, ArchiveType.SevenZip) => this._archive = archive;
@@ -226,40 +295,135 @@ 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 non-directory entries, yield them without creating shared streams
// Each call to GetEntryStream() will create a fresh decompression stream
// to avoid state corruption issues with async operations
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()
{
// Create a fresh decompression stream for each file (no state sharing).
// However, the LZMA decoder has bugs in its async implementation that cause
// state corruption even on fresh streams. The SyncOnlyStream wrapper
// works around these bugs by forcing async operations to use sync equivalents.
//
// TODO: Fix the LZMA decoder async bugs (in LzmaStream, Decoder, OutWindow)
// so this wrapper is no longer necessary.
var entry = _currentEntry.NotNull("currentEntry is not null");
if (entry.IsDirectory)
{
return CreateEntryStream(Stream.Null);
}
return CreateEntryStream(new SyncOnlyStream(entry.FilePart.GetCompressedStream()));
}
}
/// <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

View File

@@ -12,8 +12,9 @@ public class SevenZipArchiveEntry : SevenZipEntry, IArchiveEntry
public Stream OpenEntryStream() => FilePart.GetCompressedStream();
public Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default) =>
Task.FromResult(OpenEntryStream());
public async ValueTask<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
) => OpenEntryStream();
public IArchive Archive { get; }

View File

@@ -103,6 +103,70 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
);
}
/// <summary>
/// Opens a TarArchive asynchronously from a stream.
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(stream, readerOptions));
}
/// <summary>
/// Opens a TarArchive asynchronously from a FileInfo.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(fileInfo, readerOptions));
}
/// <summary>
/// Opens a TarArchive asynchronously from multiple streams.
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(streams, readerOptions));
}
/// <summary>
/// Opens a TarArchive asynchronously from multiple FileInfo objects.
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(fileInfos, readerOptions));
}
public static bool IsTarFile(string filePath) => IsTarFile(new FileInfo(filePath));
public static bool IsTarFile(FileInfo fileInfo)
@@ -259,7 +323,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
}
}
protected override async Task SaveToAsync(
protected override async ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<TarArchiveEntry> oldEntries,
@@ -302,4 +366,11 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
stream.Position = 0;
return TarReader.Open(stream);
}
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync()
{
var stream = Volumes.Single().Stream;
stream.Position = 0;
return new(TarReader.Open(stream));
}
}

View File

@@ -14,9 +14,9 @@ public class TarArchiveEntry : TarEntry, IArchiveEntry
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
public virtual Task<Stream> OpenEntryStreamAsync(
public async ValueTask<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
) => Task.FromResult(OpenEntryStream());
) => OpenEntryStream();
#region IArchiveEntry Members

View File

@@ -124,6 +124,70 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
);
}
/// <summary>
/// Opens a ZipArchive asynchronously from a stream.
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(stream, readerOptions));
}
/// <summary>
/// Opens a ZipArchive asynchronously from a FileInfo.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(fileInfo, readerOptions));
}
/// <summary>
/// Opens a ZipArchive asynchronously from multiple streams.
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(streams, readerOptions));
}
/// <summary>
/// Opens a ZipArchive asynchronously from multiple FileInfo objects.
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions"></param>
/// <param name="cancellationToken"></param>
public static ValueTask<IAsyncArchive> OpenAsync(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(Open(fileInfos, readerOptions));
}
public static bool IsZipFile(
string filePath,
string? password = null,
@@ -199,7 +263,95 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
if (stream.CanSeek) //could be multipart. Test for central directory - might not be z64 safe
{
var z = new SeekableZipHeaderFactory(password, new ArchiveEncoding());
var x = z.ReadSeekableHeader(stream).FirstOrDefault();
var x = z.ReadSeekableHeader(stream, useSync: true).FirstOrDefault();
return x?.ZipHeaderType == ZipHeaderType.DirectoryEntry;
}
else
{
return false;
}
}
return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType);
}
catch (CryptographicException)
{
return true;
}
catch
{
return false;
}
}
public static async ValueTask<bool> IsZipFileAsync(
Stream stream,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null);
try
{
if (stream is not SharpCompressStream)
{
stream = new SharpCompressStream(stream, bufferSize: bufferSize);
}
var header = await headerFactory
.ReadStreamHeaderAsync(stream)
.Where(x => x.ZipHeaderType != ZipHeaderType.Split)
.FirstOrDefaultAsync();
if (header is null)
{
return false;
}
return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType);
}
catch (CryptographicException)
{
return true;
}
catch
{
return false;
}
}
public static async ValueTask<bool> IsZipMultiAsync(
Stream stream,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null);
try
{
if (stream is not SharpCompressStream)
{
stream = new SharpCompressStream(stream, bufferSize: bufferSize);
}
var header = headerFactory
.ReadStreamHeader(stream)
.FirstOrDefault(x => x.ZipHeaderType != ZipHeaderType.Split);
if (header is null)
{
if (stream.CanSeek) //could be multipart. Test for central directory - might not be z64 safe
{
var z = new SeekableZipHeaderFactory(password, new ArchiveEncoding());
ZipHeader? x = null;
await foreach (
var h in z.ReadSeekableHeaderAsync(stream)
.WithCancellation(cancellationToken)
)
{
x = h;
break;
}
return x?.ZipHeaderType == ZipHeaderType.DirectoryEntry;
}
else
@@ -254,7 +406,9 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
protected override IEnumerable<ZipArchiveEntry> LoadEntries(IEnumerable<ZipVolume> volumes)
{
var vols = volumes.ToArray();
foreach (var h in headerFactory.NotNull().ReadSeekableHeader(vols.Last().Stream))
foreach (
var h in headerFactory.NotNull().ReadSeekableHeader(vols.Last().Stream, useSync: true)
)
{
if (h != null)
{
@@ -298,6 +452,59 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
}
}
protected override async IAsyncEnumerable<ZipArchiveEntry> LoadEntriesAsync(
IAsyncEnumerable<ZipVolume> volumes
)
{
var vols = await volumes.ToListAsync();
var volsArray = vols.ToArray();
await foreach (
var h in headerFactory.NotNull().ReadSeekableHeaderAsync(volsArray.Last().Stream)
)
{
if (h != null)
{
switch (h.ZipHeaderType)
{
case ZipHeaderType.DirectoryEntry:
{
var deh = (DirectoryEntryHeader)h;
Stream s;
if (
deh.RelativeOffsetOfEntryHeader + deh.CompressedSize
> volsArray[deh.DiskNumberStart].Stream.Length
)
{
var v = volsArray.Skip(deh.DiskNumberStart).ToArray();
s = new SourceStream(
v[0].Stream,
i => i < v.Length ? v[i].Stream : null,
new ReaderOptions() { LeaveStreamOpen = true }
);
}
else
{
s = volsArray[deh.DiskNumberStart].Stream;
}
yield return new ZipArchiveEntry(
this,
new SeekableZipFilePart(headerFactory.NotNull(), deh, s)
);
}
break;
case ZipHeaderType.DirectoryEnd:
{
var bytes = ((DirectoryEndHeader)h).Comment ?? Array.Empty<byte>();
volsArray.Last().Comment = ReaderOptions.ArchiveEncoding.Decode(bytes);
yield break;
}
}
}
}
}
public void SaveTo(Stream stream) => SaveTo(stream, new WriterOptions(CompressionType.Deflate));
protected override void SaveTo(
@@ -329,7 +536,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
}
}
protected override async Task SaveToAsync(
protected override async ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<ZipArchiveEntry> oldEntries,
@@ -385,4 +592,11 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
((IStreamStack)stream).StackSeek(0);
return ZipReader.Open(stream, ReaderOptions, Entries);
}
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync()
{
var stream = Volumes.Single().Stream;
stream.Position = 0;
return new(ZipReader.Open(stream));
}
}

View File

@@ -13,9 +13,17 @@ public class ZipArchiveEntry : ZipEntry, IArchiveEntry
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
public virtual Task<Stream> OpenEntryStreamAsync(
public async ValueTask<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
) => Task.FromResult(OpenEntryStream());
)
{
var part = Parts.Single();
if (part is SeekableZipFilePart seekablePart)
{
return (await seekablePart.GetCompressedStreamAsync(cancellationToken)).NotNull();
}
return OpenEntryStream();
}
#region IArchiveEntry Members

View File

@@ -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"
)]

View 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);
}
}
}

View 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();
}
}

View 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;
}
}

View 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>();
}
}
}

View 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(IArchiveEncoding 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,
};
}
}

View 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(IArchiveEncoding archiveEncoding, AceHeaderType type)
{
AceHeaderType = type;
ArchiveEncoding = archiveEncoding;
}
public IArchiveEncoding 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;
}
}
}
}

View 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(IArchiveEncoding 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;
}
}
}

View File

@@ -0,0 +1,16 @@
namespace SharpCompress.Common.Ace.Headers
{
/// <summary>
/// Compression quality
/// </summary>
public enum CompressionQuality
{
None,
Fastest,
Fast,
Normal,
Good,
Best,
Unknown,
}
}

View File

@@ -0,0 +1,13 @@
namespace SharpCompress.Common.Ace.Headers
{
/// <summary>
/// Compression types
/// </summary>
public enum CompressionType
{
Stored,
Lz77,
Blocked,
Unknown,
}
}

View 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)
}
}

View 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,
}
}

View File

@@ -7,7 +7,7 @@ namespace SharpCompress.Common.Arc
{
public class ArcEntryHeader
{
public ArchiveEncoding ArchiveEncoding { get; }
public IArchiveEncoding ArchiveEncoding { get; }
public CompressionType CompressionMethod { get; private set; }
public string? Name { get; private set; }
public long CompressedSize { get; private set; }
@@ -16,7 +16,7 @@ namespace SharpCompress.Common.Arc
public long OriginalSize { get; private set; }
public long DataStartPosition { get; private set; }
public ArcEntryHeader(ArchiveEncoding archiveEncoding)
public ArcEntryHeader(IArchiveEncoding archiveEncoding)
{
this.ArchiveEncoding = archiveEncoding;
}

View File

@@ -3,55 +3,11 @@ using System.Text;
namespace SharpCompress.Common;
public class ArchiveEncoding
public class ArchiveEncoding : IArchiveEncoding
{
/// <summary>
/// Default encoding to use when archive format doesn't specify one.
/// </summary>
public Encoding? Default { get; set; }
/// <summary>
/// ArchiveEncoding used by encryption schemes which don't comply with RFC 2898.
/// </summary>
public Encoding? Password { get; set; }
/// <summary>
/// Set this encoding when you want to force it for all encoding operations.
/// </summary>
public Encoding Default { get; set; } = Encoding.Default;
public Encoding Password { get; set; } = Encoding.Default;
public Encoding UTF8 { get; set; } = Encoding.UTF8;
public Encoding? Forced { get; set; }
/// <summary>
/// Set this when you want to use a custom method for all decoding operations.
/// </summary>
/// <returns>string Func(bytes, index, length)</returns>
public Func<byte[], int, int, string>? CustomDecoder { get; set; }
public ArchiveEncoding()
: this(Encoding.Default, Encoding.Default) { }
public ArchiveEncoding(Encoding def, Encoding password)
{
Default = def;
Password = password;
}
#if !NETFRAMEWORK
static ArchiveEncoding() => Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
#endif
public string Decode(byte[] bytes) => Decode(bytes, 0, bytes.Length);
public string Decode(byte[] bytes, int start, int length) =>
GetDecoder().Invoke(bytes, start, length);
public string DecodeUTF8(byte[] bytes) => Encoding.UTF8.GetString(bytes, 0, bytes.Length);
public byte[] Encode(string str) => GetEncoding().GetBytes(str);
public Encoding GetEncoding() => Forced ?? Default ?? Encoding.UTF8;
public Encoding GetPasswordEncoding() => Password ?? Encoding.UTF8;
public Func<byte[], int, int, string> GetDecoder() =>
CustomDecoder ?? ((bytes, index, count) => GetEncoding().GetString(bytes, index, count));
public Func<byte[], int, int, EncodingType, string>? CustomDecoder { get; set; }
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Text;
namespace SharpCompress.Common;
/// <summary>
/// Specifies the type of encoding to use.
/// </summary>
public enum EncodingType
{
/// <summary>
/// Uses the default encoding.
/// </summary>
Default,
/// <summary>
/// Uses UTF-8 encoding.
/// </summary>
UTF8,
}
/// <summary>
/// Provides extension methods for archive encoding.
/// </summary>
public static class ArchiveEncodingExtensions
{
#if !NETFRAMEWORK
/// <summary>
/// Registers the code pages encoding provider.
/// </summary>
static ArchiveEncodingExtensions() =>
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
#endif
extension(IArchiveEncoding encoding)
{
/// <summary>
/// Gets the encoding based on the archive encoding settings.
/// </summary>
/// <param name="useUtf8">Whether to use UTF-8.</param>
/// <returns>The encoding.</returns>
public Encoding GetEncoding(bool useUtf8 = false) =>
encoding.Forced ?? (useUtf8 ? encoding.UTF8 : encoding.Default);
/// <summary>
/// Gets the decoder function for the archive encoding.
/// </summary>
/// <returns>The decoder function.</returns>
public Func<byte[], int, int, EncodingType, string> GetDecoder() =>
encoding.CustomDecoder
?? (
(bytes, index, count, type) =>
encoding.GetEncoding(type == EncodingType.UTF8).GetString(bytes, index, count)
);
/// <summary>
/// Encodes a string using the default encoding.
/// </summary>
/// <param name="str">The string to encode.</param>
/// <returns>The encoded bytes.</returns>
public byte[] Encode(string str) => encoding.Default.GetBytes(str);
/// <summary>
/// Decodes bytes using the specified encoding type.
/// </summary>
/// <param name="bytes">The bytes to decode.</param>
/// <param name="type">The encoding type.</param>
/// <returns>The decoded string.</returns>
public string Decode(byte[] bytes, EncodingType type = EncodingType.Default) =>
encoding.Decode(bytes, 0, bytes.Length, type);
/// <summary>
/// Decodes a portion of bytes using the specified encoding type.
/// </summary>
/// <param name="bytes">The bytes to decode.</param>
/// <param name="start">The start index.</param>
/// <param name="length">The length.</param>
/// <param name="type">The encoding type.</param>
/// <returns>The decoded string.</returns>
public string Decode(
byte[] bytes,
int start,
int length,
EncodingType type = EncodingType.Default
) => encoding.GetDecoder()(bytes, start, length, type);
}
}

View File

@@ -1,10 +0,0 @@
using System;
namespace SharpCompress.Common;
public class ArchiveExtractionEventArgs<T> : EventArgs
{
internal ArchiveExtractionEventArgs(T entry) => Item = entry;
public T Item { get; }
}

View File

@@ -9,4 +9,5 @@ public enum ArchiveType
GZip,
Arc,
Arj,
Ace,
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Common
{
public sealed class AsyncBinaryReader : IDisposable
{
private readonly Stream _stream;
private readonly Stream _originalStream;
private readonly bool _leaveOpen;
private readonly byte[] _buffer = new byte[8];
private bool _disposed;
public AsyncBinaryReader(Stream stream, bool leaveOpen = false, int bufferSize = 4096)
{
_originalStream = stream ?? throw new ArgumentNullException(nameof(stream));
_leaveOpen = leaveOpen;
// Use the stream directly without wrapping in BufferedStream
// BufferedStream uses synchronous Read internally which doesn't work with async-only streams
// SharpCompress uses SharpCompressStream for buffering which supports true async reads
_stream = stream;
}
public Stream BaseStream => _stream;
public async ValueTask<byte> ReadByteAsync(CancellationToken ct = default)
{
await _stream.ReadExactAsync(_buffer, 0, 1, ct).ConfigureAwait(false);
return _buffer[0];
}
public async ValueTask<ushort> ReadUInt16Async(CancellationToken ct = default)
{
await _stream.ReadExactAsync(_buffer, 0, 2, ct).ConfigureAwait(false);
return BinaryPrimitives.ReadUInt16LittleEndian(_buffer);
}
public async ValueTask<uint> ReadUInt32Async(CancellationToken ct = default)
{
await _stream.ReadExactAsync(_buffer, 0, 4, ct).ConfigureAwait(false);
return BinaryPrimitives.ReadUInt32LittleEndian(_buffer);
}
public async ValueTask<ulong> ReadUInt64Async(CancellationToken ct = default)
{
await _stream.ReadExactAsync(_buffer, 0, 8, ct).ConfigureAwait(false);
return BinaryPrimitives.ReadUInt64LittleEndian(_buffer);
}
public async ValueTask<byte[]> ReadBytesAsync(int count, CancellationToken ct = default)
{
var result = new byte[count];
await _stream.ReadExactAsync(result, 0, count, ct).ConfigureAwait(false);
return result;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
// Dispose the original stream if we own it
if (!_leaveOpen)
{
_originalStream.Dispose();
}
}
#if NET6_0_OR_GREATER
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
_disposed = true;
// Dispose the original stream if we own it
if (!_leaveOpen)
{
await _originalStream.DisposeAsync().ConfigureAwait(false);
}
}
#endif
}
}

View File

@@ -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; }
}

View File

@@ -30,4 +30,5 @@ public enum CompressionType
Distilled,
ZStandard,
ArjLZ77,
AceLZ77,
}

View File

@@ -56,7 +56,7 @@ public class EntryStream : Stream, IStreamStack
/// <summary>
/// Asynchronously skip the rest of the entry stream.
/// </summary>
public async Task SkipEntryAsync(CancellationToken cancellationToken = default)
public async ValueTask SkipEntryAsync(CancellationToken cancellationToken = default)
{
await this.SkipAsync(cancellationToken).ConfigureAwait(false);
_completed = true;

View File

@@ -124,11 +124,11 @@ internal static class ExtractionMethods
}
}
public static async Task WriteEntryToDirectoryAsync(
public static async ValueTask WriteEntryToDirectoryAsync(
IEntry entry,
string destinationDirectory,
ExtractionOptions? options,
Func<string, ExtractionOptions?, Task> writeAsync,
Func<string, ExtractionOptions?, CancellationToken, ValueTask> 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))
{
@@ -197,11 +197,11 @@ internal static class ExtractionMethods
}
}
public static async Task WriteEntryToFileAsync(
public static async ValueTask WriteEntryToFileAsync(
IEntry entry,
string destinationFileName,
ExtractionOptions? options,
Func<string, FileMode, Task> openAndWriteAsync,
Func<string, FileMode, CancellationToken, ValueTask> 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);
}
}

View File

@@ -1,12 +1,14 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Common;
public abstract class FilePart
{
protected FilePart(ArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
protected FilePart(IArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
internal ArchiveEncoding ArchiveEncoding { get; }
internal IArchiveEncoding ArchiveEncoding { get; }
internal abstract string? FilePartName { get; }
public int Index { get; set; }
@@ -14,4 +16,8 @@ public abstract class FilePart
internal abstract Stream? GetCompressedStream();
internal abstract Stream? GetRawStream();
internal bool Skipped { get; set; }
internal virtual ValueTask<Stream?> GetCompressedStreamAsync(
CancellationToken cancellationToken = default
) => new(GetCompressedStream());
}

View File

@@ -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; }
}

View File

@@ -13,7 +13,7 @@ internal sealed class GZipFilePart : FilePart
private string? _name;
private readonly Stream _stream;
internal GZipFilePart(Stream stream, ArchiveEncoding archiveEncoding)
internal GZipFilePart(Stream stream, IArchiveEncoding archiveEncoding)
: base(archiveEncoding)
{
_stream = stream;

View File

@@ -0,0 +1,36 @@
using System;
using System.Text;
namespace SharpCompress.Common;
/// <summary>
/// Defines the encoding settings for archives.
/// </summary>
public interface IArchiveEncoding
{
/// <summary>
/// Default encoding to use when archive format doesn't specify one. Required and defaults to Encoding.Default.
/// </summary>
public Encoding Default { get; set; }
/// <summary>
/// ArchiveEncoding used by encryption schemes which don't comply with RFC 2898. Required and defaults to Encoding.Default.
/// </summary>
public Encoding Password { get; set; }
/// <summary>
/// Default encoding to use when archive format specifies UTF-8 encoding. Required and defaults to Encoding.UTF8.
/// </summary>
public Encoding UTF8 { get; set; }
/// <summary>
/// Set this encoding when you want to force it for all encoding operations.
/// </summary>
public Encoding? Forced { get; set; }
/// <summary>
/// Set this when you want to use a custom method for all decoding operations.
/// </summary>
/// <returns>string Func(bytes, index, length, EncodingType)</returns>
public Func<byte[], int, int, EncodingType, string>? CustomDecoder { get; set; }
}

View File

@@ -1,7 +0,0 @@
namespace SharpCompress.Common;
public interface IExtractionListener
{
void FireFilePartExtractionBegin(string name, long size, long compressedSize);
void FireCompressedBytesRead(long currentPartCompressedBytes, long compressedReadBytes);
}

View File

@@ -7,5 +7,5 @@ public class OptionsBase
/// </summary>
public bool LeaveStreamOpen { get; set; } = true;
public ArchiveEncoding ArchiveEncoding { get; set; } = new();
public IArchiveEncoding ArchiveEncoding { get; set; } = new ArchiveEncoding();
}

View 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;
}

View File

@@ -13,7 +13,7 @@ internal class RarHeader : IRarHeader
internal static RarHeader? TryReadBase(
RarCrcBinaryReader reader,
bool isRar5,
ArchiveEncoding archiveEncoding
IArchiveEncoding archiveEncoding
)
{
try
@@ -26,7 +26,7 @@ internal class RarHeader : IRarHeader
}
}
private RarHeader(RarCrcBinaryReader reader, bool isRar5, ArchiveEncoding archiveEncoding)
private RarHeader(RarCrcBinaryReader reader, bool isRar5, IArchiveEncoding archiveEncoding)
{
_headerType = HeaderType.Null;
_isRar5 = isRar5;
@@ -115,7 +115,7 @@ internal class RarHeader : IRarHeader
protected int HeaderSize { get; }
internal ArchiveEncoding ArchiveEncoding { get; }
internal IArchiveEncoding ArchiveEncoding { get; }
/// <summary>
/// Extra header size.

View File

@@ -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; }
}

View File

@@ -15,7 +15,7 @@ internal class SevenZipFilePart : FilePart
ArchiveDatabase database,
int index,
CFileItem fileEntry,
ArchiveEncoding archiveEncoding
IArchiveEncoding archiveEncoding
)
: base(archiveEncoding)
{
@@ -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

View File

@@ -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(
IArchiveEncoding 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; }
@@ -21,7 +30,7 @@ internal sealed class TarHeader
internal DateTime LastModifiedTime { get; set; }
internal EntryType EntryType { get; set; }
internal Stream? PackedStream { get; set; }
internal ArchiveEncoding ArchiveEncoding { get; }
internal IArchiveEncoding ArchiveEncoding { get; }
internal const int BLOCK_SIZE = 512;
@@ -30,6 +39,114 @@ internal sealed class TarHeader
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];
@@ -85,7 +202,7 @@ internal sealed class TarHeader
0,
100 - ArchiveEncoding.GetEncoding().GetMaxByteCount(1)
);
Write(output);
WriteGnuTarLongLink(output);
}
}
@@ -241,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;

View File

@@ -0,0 +1,7 @@
namespace SharpCompress.Common.Tar.Headers;
public enum TarHeaderWriteFormat
{
GNU_TAR_LONG_LINK,
USTAR,
}

View File

@@ -54,7 +54,7 @@ public class TarEntry : Entry
StreamingMode mode,
Stream stream,
CompressionType compressionType,
ArchiveEncoding archiveEncoding
IArchiveEncoding archiveEncoding
)
{
foreach (var header in TarHeaderFactory.ReadHeader(mode, stream, archiveEncoding))

View File

@@ -10,7 +10,7 @@ internal static class TarHeaderFactory
internal static IEnumerable<TarHeader?> ReadHeader(
StreamingMode mode,
Stream stream,
ArchiveEncoding archiveEncoding
IArchiveEncoding archiveEncoding
)
{
while (true)

View File

@@ -1,4 +1,5 @@
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -19,6 +20,18 @@ internal class DirectoryEndHeader : ZipHeader
Comment = reader.ReadBytes(CommentLength);
}
internal override async ValueTask Read(AsyncBinaryReader reader)
{
VolumeNumber = await reader.ReadUInt16Async();
FirstVolumeWithDirectory = await reader.ReadUInt16Async();
TotalNumberOfEntriesInDisk = await reader.ReadUInt16Async();
TotalNumberOfEntries = await reader.ReadUInt16Async();
DirectorySize = await reader.ReadUInt32Async();
DirectoryStartOffsetRelativeToDisk = await reader.ReadUInt32Async();
CommentLength = await reader.ReadUInt16Async();
Comment = await reader.ReadBytesAsync(CommentLength);
}
public ushort VolumeNumber { get; private set; }
public ushort FirstVolumeWithDirectory { get; private set; }

View File

@@ -1,11 +1,12 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
internal class DirectoryEntryHeader : ZipFileEntry
{
public DirectoryEntryHeader(ArchiveEncoding archiveEncoding)
public DirectoryEntryHeader(IArchiveEncoding archiveEncoding)
: base(ZipHeaderType.DirectoryEntry, archiveEncoding) { }
internal override void Read(BinaryReader reader)
@@ -31,7 +32,37 @@ internal class DirectoryEntryHeader : ZipFileEntry
var extra = reader.ReadBytes(extraLength);
var comment = reader.ReadBytes(commentLength);
// According to .ZIP File Format Specification
ProcessReadData(name, extra, comment);
}
internal override async ValueTask Read(AsyncBinaryReader reader)
{
Version = await reader.ReadUInt16Async();
VersionNeededToExtract = await reader.ReadUInt16Async();
Flags = (HeaderFlags)await reader.ReadUInt16Async();
CompressionMethod = (ZipCompressionMethod)await reader.ReadUInt16Async();
OriginalLastModifiedTime = LastModifiedTime = await reader.ReadUInt16Async();
OriginalLastModifiedDate = LastModifiedDate = await reader.ReadUInt16Async();
Crc = await reader.ReadUInt32Async();
CompressedSize = await reader.ReadUInt32Async();
UncompressedSize = await reader.ReadUInt32Async();
var nameLength = await reader.ReadUInt16Async();
var extraLength = await reader.ReadUInt16Async();
var commentLength = await reader.ReadUInt16Async();
DiskNumberStart = await reader.ReadUInt16Async();
InternalFileAttributes = await reader.ReadUInt16Async();
ExternalFileAttributes = await reader.ReadUInt32Async();
RelativeOffsetOfEntryHeader = await reader.ReadUInt32Async();
var name = await reader.ReadBytesAsync(nameLength);
var extra = await reader.ReadBytesAsync(extraLength);
var comment = await reader.ReadBytesAsync(commentLength);
ProcessReadData(name, extra, comment);
}
private void ProcessReadData(byte[] name, byte[] extra, byte[] comment)
{
//
// For example: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
//
@@ -41,8 +72,8 @@ internal class DirectoryEntryHeader : ZipFileEntry
if (Flags.HasFlag(HeaderFlags.Efs))
{
Name = ArchiveEncoding.DecodeUTF8(name);
Comment = ArchiveEncoding.DecodeUTF8(comment);
Name = ArchiveEncoding.Decode(name, EncodingType.UTF8);
Comment = ArchiveEncoding.Decode(comment, EncodingType.UTF8);
}
else
{

View File

@@ -1,4 +1,5 @@
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -8,4 +9,6 @@ internal class IgnoreHeader : ZipHeader
: base(type) { }
internal override void Read(BinaryReader reader) { }
internal override ValueTask Read(AsyncBinaryReader reader) => default;
}

View File

@@ -1,13 +1,12 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
internal class LocalEntryHeader : ZipFileEntry
internal class LocalEntryHeader(IArchiveEncoding archiveEncoding)
: ZipFileEntry(ZipHeaderType.LocalEntry, archiveEncoding)
{
public LocalEntryHeader(ArchiveEncoding archiveEncoding)
: base(ZipHeaderType.LocalEntry, archiveEncoding) { }
internal override void Read(BinaryReader reader)
{
Version = reader.ReadUInt16();
@@ -23,7 +22,29 @@ internal class LocalEntryHeader : ZipFileEntry
var name = reader.ReadBytes(nameLength);
var extra = reader.ReadBytes(extraLength);
// According to .ZIP File Format Specification
ProcessReadData(name, extra);
}
internal override async ValueTask Read(AsyncBinaryReader reader)
{
Version = await reader.ReadUInt16Async();
Flags = (HeaderFlags)await reader.ReadUInt16Async();
CompressionMethod = (ZipCompressionMethod)await reader.ReadUInt16Async();
OriginalLastModifiedTime = LastModifiedTime = await reader.ReadUInt16Async();
OriginalLastModifiedDate = LastModifiedDate = await reader.ReadUInt16Async();
Crc = await reader.ReadUInt32Async();
CompressedSize = await reader.ReadUInt32Async();
UncompressedSize = await reader.ReadUInt32Async();
var nameLength = await reader.ReadUInt16Async();
var extraLength = await reader.ReadUInt16Async();
var name = await reader.ReadBytesAsync(nameLength);
var extra = await reader.ReadBytesAsync(extraLength);
ProcessReadData(name, extra);
}
private void ProcessReadData(byte[] name, byte[] extra)
{
//
// For example: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
//
@@ -33,7 +54,7 @@ internal class LocalEntryHeader : ZipFileEntry
if (Flags.HasFlag(HeaderFlags.Efs))
{
Name = ArchiveEncoding.DecodeUTF8(name);
Name = ArchiveEncoding.Decode(name, EncodingType.UTF8);
}
else
{

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -9,4 +10,7 @@ internal class SplitHeader : ZipHeader
: base(ZipHeaderType.Split) { }
internal override void Read(BinaryReader reader) => throw new NotImplementedException();
internal override ValueTask Read(AsyncBinaryReader reader) =>
throw new NotImplementedException();
}

View File

@@ -1,4 +1,5 @@
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -26,6 +27,25 @@ internal class Zip64DirectoryEndHeader : ZipHeader
);
}
internal override async ValueTask Read(AsyncBinaryReader reader)
{
SizeOfDirectoryEndRecord = (long)await reader.ReadUInt64Async();
VersionMadeBy = await reader.ReadUInt16Async();
VersionNeededToExtract = await reader.ReadUInt16Async();
VolumeNumber = await reader.ReadUInt32Async();
FirstVolumeWithDirectory = await reader.ReadUInt32Async();
TotalNumberOfEntriesInDisk = (long)await reader.ReadUInt64Async();
TotalNumberOfEntries = (long)await reader.ReadUInt64Async();
DirectorySize = (long)await reader.ReadUInt64Async();
DirectoryStartOffsetRelativeToDisk = (long)await reader.ReadUInt64Async();
DataSector = await reader.ReadBytesAsync(
(int)(
SizeOfDirectoryEndRecord
- SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS
)
);
}
private const int SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS = 44;
public long SizeOfDirectoryEndRecord { get; private set; }

View File

@@ -1,12 +1,10 @@
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
internal class Zip64DirectoryEndLocatorHeader : ZipHeader
internal class Zip64DirectoryEndLocatorHeader() : ZipHeader(ZipHeaderType.Zip64DirectoryEndLocator)
{
public Zip64DirectoryEndLocatorHeader()
: base(ZipHeaderType.Zip64DirectoryEndLocator) { }
internal override void Read(BinaryReader reader)
{
FirstVolumeWithDirectory = reader.ReadUInt32();
@@ -14,6 +12,13 @@ internal class Zip64DirectoryEndLocatorHeader : ZipHeader
TotalNumberOfVolumes = reader.ReadUInt32();
}
internal override async ValueTask Read(AsyncBinaryReader reader)
{
FirstVolumeWithDirectory = await reader.ReadUInt32Async();
RelativeOffsetOfTheEndOfDirectoryRecord = (long)await reader.ReadUInt64Async();
TotalNumberOfVolumes = await reader.ReadUInt32Async();
}
public uint FirstVolumeWithDirectory { get; private set; }
public long RelativeOffsetOfTheEndOfDirectoryRecord { get; private set; }

View File

@@ -2,18 +2,14 @@ using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
internal abstract class ZipFileEntry : ZipHeader
internal abstract class ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiveEncoding)
: ZipHeader(type)
{
protected ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding)
: base(type)
{
Extra = new List<ExtraData>();
ArchiveEncoding = archiveEncoding;
}
internal bool IsDirectory
{
get
@@ -30,7 +26,7 @@ internal abstract class ZipFileEntry : ZipHeader
internal Stream? PackedStream { get; set; }
internal ArchiveEncoding ArchiveEncoding { get; }
internal IArchiveEncoding ArchiveEncoding { get; } = archiveEncoding;
internal string? Name { get; set; }
@@ -44,7 +40,7 @@ internal abstract class ZipFileEntry : ZipHeader
internal long UncompressedSize { get; set; }
internal List<ExtraData> Extra { get; set; }
internal List<ExtraData> Extra { get; set; } = new();
public string? Password { get; set; }
@@ -63,6 +59,24 @@ internal abstract class ZipFileEntry : ZipHeader
return encryptionData;
}
internal async ValueTask<PkwareTraditionalEncryptionData> ComposeEncryptionDataAsync(
Stream archiveStream,
CancellationToken cancellationToken = default
)
{
if (archiveStream is null)
{
throw new ArgumentNullException(nameof(archiveStream));
}
var buffer = new byte[12];
await archiveStream.ReadFullyAsync(buffer, 0, 12, cancellationToken).ConfigureAwait(false);
var encryptionData = PkwareTraditionalEncryptionData.ForRead(Password!, this, buffer);
return encryptionData;
}
internal WinzipAesEncryptionData? WinzipAesEncryptionData { get; set; }
/// <summary>

View File

@@ -1,18 +1,14 @@
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
internal abstract class ZipHeader
internal abstract class ZipHeader(ZipHeaderType type)
{
protected ZipHeader(ZipHeaderType type)
{
ZipHeaderType = type;
HasData = true;
}
internal ZipHeaderType ZipHeaderType { get; }
internal ZipHeaderType ZipHeaderType { get; } = type;
internal abstract void Read(BinaryReader reader);
internal abstract ValueTask Read(AsyncBinaryReader reader);
internal bool HasData { get; set; }
internal bool HasData { get; set; } = true;
}

Some files were not shown because too many files have changed in this diff Show More