Compare commits

...

148 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
238ed748fc Revert "Document Copilot instructions setup status"
This reverts commit be6aefc8c4.
2025-10-29 13:22:55 +00:00
copilot-swe-agent[bot]
be6aefc8c4 Document Copilot instructions setup status
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-29 13:20:10 +00:00
copilot-swe-agent[bot]
b8867e7e54 Initial plan 2025-10-29 13:14:08 +00:00
Adam Hathcock
8a108b590d Merge pull request #993 from adamhathcock/adam/macos-fixes
make test linux only
2025-10-28 14:52:05 +00:00
Adam Hathcock
bca0f67344 make test linux only 2025-10-28 12:18:05 +00:00
Adam Hathcock
f3dad51134 Merge pull request #991 from adamhathcock/async-reader-methods
Add more Async tests and complete Zip tests
2025-10-28 11:50:03 +00:00
Adam Hathcock
f51840829c Merge branch 'master' into async-reader-methods 2025-10-28 11:39:35 +00:00
Adam Hathcock
aa1c0d0870 Merge pull request #988 from adamhathcock/copilot/fix-file-write-error
Fix extraction failure on Windows due to case-sensitive path comparison
2025-10-28 11:39:17 +00:00
Adam Hathcock
dee5ee6589 Merge pull request #989 from adamhathcock/copilot/add-support-empty-directories
Add support for empty directory entries in archives
2025-10-28 11:38:41 +00:00
Adam Hathcock
b799f479c4 Merge branch 'master' into copilot/add-support-empty-directories 2025-10-28 11:35:56 +00:00
copilot-swe-agent[bot]
b4352fefa5 Fix code formatting per CSharpier standards
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-28 11:32:39 +00:00
Adam Hathcock
77d06fb60e Merge branch 'master' into copilot/fix-file-write-error 2025-10-28 11:32:05 +00:00
Adam Hathcock
00b647457c Merge pull request #990 from adamhathcock/copilot/add-common-exception-type
Make all library exceptions inherit from SharpCompressException
2025-10-28 11:31:32 +00:00
Adam Hathcock
153d10a35c add async to forward only streams 2025-10-28 11:30:12 +00:00
Adam Hathcock
06713c641e async deflate 64 2025-10-28 11:26:31 +00:00
copilot-swe-agent[bot]
210978ec2d Format code with CSharpier
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-28 11:13:51 +00:00
Adam Hathcock
42f7d43139 enable zip64 tests that pass 2025-10-28 11:07:53 +00:00
Adam Hathcock
19967f5ad7 allow forward only write 2025-10-28 11:01:27 +00:00
Adam Hathcock
a1de3eb47d add async tests and clean up deflate64stream 2025-10-28 11:01:12 +00:00
copilot-swe-agent[bot]
e88841bdec Add support for empty directory entries in archives
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-28 10:34:03 +00:00
copilot-swe-agent[bot]
c8e4915f8e Final progress report
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-28 10:29:36 +00:00
copilot-swe-agent[bot]
a93a3f0598 Address code review feedback - fix formatting
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-28 10:26:31 +00:00
copilot-swe-agent[bot]
084f81fc8d Format code with CSharpier 2025-10-28 10:23:57 +00:00
copilot-swe-agent[bot]
d148f36e87 Add support for empty directory entries in archives
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-28 10:22:58 +00:00
copilot-swe-agent[bot]
150d9c35b7 Complete fix for case-sensitive path comparison on Windows
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-28 10:22:56 +00:00
copilot-swe-agent[bot]
e11198616e Address code review feedback: use RuntimeInformation for platform detection
- Replace Environment.OSVersion.Platform with RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
- Clarify test comment about platform-specific behavior
- Add using System.Runtime.InteropServices for RuntimeInformation

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-28 10:20:05 +00:00
copilot-swe-agent[bot]
2f27f1e6f9 Complete exception hierarchy implementation
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-28 10:18:42 +00:00
copilot-swe-agent[bot]
5392ca9794 Fix case-sensitive path comparison on Windows for file extraction
- Add PathComparison property that uses OrdinalIgnoreCase on Windows and Ordinal on Unix
- Update all path comparison checks in ExtractionMethods to use PathComparison
- Add comprehensive tests for extraction with case-insensitive paths
- Ensure security check for path traversal still works correctly

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-28 10:16:07 +00:00
copilot-swe-agent[bot]
46672eb583 Update exceptions to inherit from SharpCompressException
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-28 10:15:40 +00:00
Adam Hathcock
79653eee80 Merge remote-tracking branch 'origin/master' into async-reader-methods 2025-10-28 10:11:11 +00:00
Adam Hathcock
16ad86c52a add async implementations to readonlysubstream 2025-10-28 10:09:46 +00:00
copilot-swe-agent[bot]
6b7c6be5f5 Initial plan 2025-10-28 10:02:58 +00:00
copilot-swe-agent[bot]
fda1c2cc79 Initial plan 2025-10-28 10:01:37 +00:00
copilot-swe-agent[bot]
ef2fee0ee3 Initial plan 2025-10-28 10:00:50 +00:00
Adam Hathcock
e287d0811d minor clean up 2025-10-28 09:58:01 +00:00
Adam Hathcock
a7164f3c9f Merge pull request #987 from adamhathcock/copilot/fix-gzip-extract-not-supported-exception
Fix GZip extraction NotSupportedException for non-seekable streams
2025-10-27 14:26:04 +00:00
Adam Hathcock
c55060039a Merge branch 'master' into copilot/fix-gzip-extract-not-supported-exception 2025-10-27 14:21:49 +00:00
Adam Hathcock
c68d8deddd add async tests 2025-10-27 12:34:24 +00:00
Adam Hathcock
f6eabc5db1 Merge remote-tracking branch 'origin/master' into async-reader-methods
# Conflicts:
#	src/SharpCompress/packages.lock.json
2025-10-27 12:24:14 +00:00
Adam Hathcock
72d5884db6 added async reader overloads 2025-10-27 12:23:54 +00:00
Adam Hathcock
3595c89c79 Merge pull request #983 from adamhathcock/copilot/set-up-copilot-instructions
Configure Copilot coding agent instructions for SharpCompress
2025-10-27 12:13:50 +00:00
Adam Hathcock
9ebbc718c5 Merge branch 'master' into copilot/fix-gzip-extract-not-supported-exception 2025-10-27 12:11:32 +00:00
Adam Hathcock
e862480b86 Merge branch 'master' into copilot/set-up-copilot-instructions 2025-10-27 12:09:55 +00:00
Adam Hathcock
1f3d8fe6f1 Merge pull request #986 from adamhathcock/copilot/fix-compressiontype-none-bug
Support CompressionType.None for uncompressed 7z files
2025-10-27 12:06:52 +00:00
Adam Hathcock
41ae036ab4 Merge branch 'master' into copilot/set-up-copilot-instructions 2025-10-27 12:04:58 +00:00
copilot-swe-agent[bot]
588d176b96 Final verification - all tests pass
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 11:15:16 +00:00
copilot-swe-agent[bot]
f8697120a0 Add support for CompressionType.None for uncompressed 7z files
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 11:11:38 +00:00
copilot-swe-agent[bot]
1a767105e6 Add explanatory comment for EntryStartPosition initialization
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 11:09:52 +00:00
copilot-swe-agent[bot]
4067b6ed2c Fix GZip extraction for non-seekable streams
- Modified GZipFilePart to only access stream.Position when stream.CanSeek is true
- Modified GZipArchiveEntry.OpenEntryStream to check CanSeek before accessing Position
- Added test case GZip_Archive_NonSeekableStream to verify non-seekable stream support
- All existing tests pass

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 11:08:38 +00:00
copilot-swe-agent[bot]
b272dbfd1f Clarify CSharpier should be run from project root
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 11:07:02 +00:00
copilot-swe-agent[bot]
48be7bbf86 Correct formatting instructions to use CSharpier
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 11:05:47 +00:00
copilot-swe-agent[bot]
51e22cea71 Initial plan for fixing GZip non-seekable stream support
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 11:02:53 +00:00
copilot-swe-agent[bot]
2241e27e68 Initial exploration: Understanding the issue
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 11:02:29 +00:00
copilot-swe-agent[bot]
11c90ae879 Update Copilot configuration to reflect actual project setup
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 11:00:02 +00:00
copilot-swe-agent[bot]
cf55125202 Initial plan 2025-10-27 10:58:07 +00:00
copilot-swe-agent[bot]
9cefb85905 Enhance AGENTS.md with SharpCompress-specific guidelines
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 10:57:22 +00:00
copilot-swe-agent[bot]
fc672da0e0 Initial plan 2025-10-27 10:55:53 +00:00
Adam Hathcock
25b297b142 Merge pull request #980 from adamhathcock/async-gzip-tests
adds more async tests and overloads to make things writable and async
2025-10-27 10:54:42 +00:00
Adam Hathcock
ab03c12fa8 add more tests 2025-10-27 10:52:03 +00:00
copilot-swe-agent[bot]
3095c805ad Initial plan 2025-10-27 10:48:24 +00:00
Adam Hathcock
9c18daafb8 Merge remote-tracking branch 'origin/async-gzip-tests' into async-gzip-tests 2025-10-27 10:46:15 +00:00
Adam Hathcock
16182417fb add tar specific tests 2025-10-27 10:46:08 +00:00
Adam Hathcock
9af35201e4 Merge branch 'master' into async-gzip-tests 2025-10-27 10:31:51 +00:00
Adam Hathcock
f21b982955 adds more async tests and overloads to make things writable and async 2025-10-27 10:31:10 +00:00
Adam Hathcock
b3a20d05c5 Merge pull request #978 from adamhathcock/copilot/enhance-stream-io-async-support
Add comprehensive async/await support for Stream I/O operations
2025-10-27 10:23:08 +00:00
Adam Hathcock
4cd024a2b2 Merge remote-tracking branch 'origin/master' into copilot/enhance-stream-io-async-support 2025-10-27 10:20:06 +00:00
Adam Hathcock
63d08ebfd2 update agents 2025-10-27 10:19:57 +00:00
Adam Hathcock
c696197b03 formatting 2025-10-27 10:19:24 +00:00
Adam Hathcock
738a72228b added fixes and more async tests 2025-10-27 10:15:06 +00:00
Adam Hathcock
90641f4488 Merge pull request #979 from adamhathcock/dependabot/github_actions/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5
2025-10-27 10:06:02 +00:00
Adam Hathcock
a4cc7eaf9b fully use async for zlibbase 2025-10-27 09:51:39 +00:00
Adam Hathcock
fdca728fdc add some dispose async 2025-10-27 09:47:15 +00:00
dependabot[bot]
d2c4ae8cdf Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 09:43:01 +00:00
Adam Hathcock
f3d3ac30a6 add gubbins 2025-10-27 09:39:08 +00:00
Adam Hathcock
f8cc4ade8a format 2025-10-27 09:37:00 +00:00
copilot-swe-agent[bot]
b3975b7bbd Add async tests for EntryStream and compression streams
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 09:28:34 +00:00
copilot-swe-agent[bot]
4f1b61f5bc Add async support to DeflateStream and GZipStream
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 09:20:37 +00:00
copilot-swe-agent[bot]
beeb37b4fd Add async support to EntryStream, ZlibStream, and ZlibBaseStream
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 09:11:29 +00:00
copilot-swe-agent[bot]
43aa2bad22 Integrate async/await support from PR #976 as baseline
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 09:00:38 +00:00
copilot-swe-agent[bot]
1b2ba921bb Initial plan 2025-10-27 08:48:01 +00:00
Adam Hathcock
f543da0ea8 Merge pull request #977 from adamhathcock/copilot/add-copilot-agent-config
Add Copilot agent manifest and usage documentation
2025-10-27 08:42:20 +00:00
copilot-swe-agent[bot]
e60c9efa84 Add copilot agent configuration and documentation
- Create .github/agents/copilot-agent.yml with agent manifest
- Replace AGENTS.md with agent usage and command documentation

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-25 18:16:47 +00:00
copilot-swe-agent[bot]
c52fc6f240 Initial plan 2025-10-25 18:12:45 +00:00
Adam Hathcock
ee136b024a Merge pull request #974 from adamhathcock/adam/enable-agent
chore: add Copilot coding agent config and CI workflow
2025-10-25 16:09:51 +01:00
Adam Hathcock
699bc5f34b chore: add Copilot coding agent config and CI workflow 2025-10-25 16:05:09 +01:00
Adam Hathcock
9eed8e842c Merge pull request #972 from TwanVanDongen/master
Handle vendor-specific and malformed ZIP extra fields safely
2025-10-25 13:53:10 +01:00
Twan van Dongen
6d652a12ee And again forgot to apply CSharpierAdds bounds checks to prevent exceptions when extra fields are truncated or non-standard (e.g., 0x4341 "AC"/ARC0). Stops parsing gracefully, allowing other fields to be processed. 2025-10-24 17:18:37 +02:00
Adam Hathcock
e043e06656 Merge pull request #969 from adamhathcock/adam/perf
Add JB perf testing project.
2025-10-23 14:34:43 +01:00
Adam Hathcock
14b52599f4 Update src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-23 14:20:54 +01:00
Adam Hathcock
e3e2c0c567 Update tests/SharpCompress.Performance/LargeMemoryStream.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-23 14:19:16 +01:00
Adam Hathcock
4fc5d60f03 reduce visibility 2025-10-23 14:16:39 +01:00
Adam Hathcock
c37a9e0f82 Merge remote-tracking branch 'origin/adam/perf' into adam/perf 2025-10-23 13:50:31 +01:00
Adam Hathcock
fed17ebb96 fmt 2025-10-23 13:50:07 +01:00
Adam Hathcock
eeac678872 More usage of pool and better copy 2025-10-23 13:49:54 +01:00
Adam Hathcock
f9ed0f2df9 Update tests/SharpCompress.Performance/Program.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-23 11:47:42 +01:00
Adam Hathcock
0ddbacac85 Update src/SharpCompress/Compressors/Rar/UnpackV1/UnpackUtility.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-23 11:47:27 +01:00
Adam Hathcock
f0d28aa5cf fmt 2025-10-23 11:43:38 +01:00
Adam Hathcock
cc84f6fee4 more making rar faster 2025-10-23 11:43:21 +01:00
Adam Hathcock
00e6eef369 used AI to optimize some copys and shifting 2025-10-23 11:18:50 +01:00
Adam Hathcock
1ae71907bc don't need to clear everything 2025-10-23 10:53:54 +01:00
Adam Hathcock
3ff688fba2 clear and null check 2025-10-23 10:48:18 +01:00
Adam Hathcock
bb59b3d456 add pool to LZMA out window 2025-10-23 09:54:52 +01:00
Adam Hathcock
186ea74ada add some fixes for rar to pool memory 2025-10-23 09:40:15 +01:00
Adam Hathcock
c108f2dcf3 add perf testing project using JB memory and cpu 2025-10-23 09:39:57 +01:00
Adam Hathcock
4cca232d83 Merge pull request #959 from adamhathcock/adam/xz-wrapped-often
Removed wrappers that weren't needed (probably)
2025-10-22 11:54:47 +01:00
Adam Hathcock
1db511e9cb Merge branch 'master' into adam/xz-wrapped-often 2025-10-22 11:51:46 +01:00
Adam Hathcock
76afa7d3bf Merge pull request #968 from adamhathcock/adam/rework-deps
rework dependencies to be correct for frameworks and update
2025-10-22 11:51:30 +01:00
Adam Hathcock
3513f7b1cd Update src/SharpCompress/SharpCompress.csproj
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-22 10:51:12 +01:00
Adam Hathcock
4531fe39e6 Merge branch 'master' into adam/rework-deps 2025-10-22 10:48:16 +01:00
Adam Hathcock
8d276a85bc rework dependencies to be correct for frameworks and update 2025-10-22 10:47:43 +01:00
Adam Hathcock
5f0d042bc3 Merge pull request #967 from adamhathcock/adam/reduce-custom-utilities
Reduce custom utilities for arrays/bytes
2025-10-22 10:41:10 +01:00
Adam Hathcock
408f07e3c4 Merge branch 'master' into adam/reduce-custom-utilities 2025-10-22 10:38:01 +01:00
Adam Hathcock
d1a540c90c use windows instead of skippable fact 2025-10-22 10:32:47 +01:00
Adam Hathcock
00df8e930e add windows only compile constant 2025-10-22 10:30:40 +01:00
Adam Hathcock
3b768b1b77 Merge pull request #961 from adamhathcock/dependabot/nuget/AwesomeAssertions-9.2.1
Bump AwesomeAssertions from 9.2.0 to 9.2.1
2025-10-22 10:25:01 +01:00
Adam Hathcock
42a7ececa0 Merge branch 'master' into adam/xz-wrapped-often 2025-10-22 10:22:36 +01:00
Adam Hathcock
e8867de049 Merge branch 'master' into dependabot/nuget/AwesomeAssertions-9.2.1 2025-10-22 10:21:59 +01:00
Adam Hathcock
a1dfa3dfa3 xplat tests for path characters 2025-10-22 10:21:22 +01:00
Adam Hathcock
83917d4f79 Merge remote-tracking branch 'origin/master' into adam/reduce-custom-utilities 2025-10-22 10:17:20 +01:00
Adam Hathcock
513cd4f905 some AI suggestions 2025-10-22 10:16:45 +01:00
Adam Hathcock
eda0309df3 Merge pull request #966 from adamhathcock/adam/reduce-stackalloc
Remove a dynamically created stackalloc
2025-10-22 10:13:14 +01:00
Adam Hathcock
74e27c028e fix the span length 2025-10-22 10:10:07 +01:00
Adam Hathcock
36c06c4089 ugh, this is used because it shadows a field 2025-10-22 09:32:19 +01:00
Adam Hathcock
249b8a9cdd add AI generated tests 2025-10-22 09:28:07 +01:00
Adam Hathcock
62bee15f00 fmt 2025-10-22 09:19:30 +01:00
Adam Hathcock
d8797b69e4 remove do while 2025-10-22 09:19:09 +01:00
Adam Hathcock
084fe72b02 Consolidate not null 2025-10-22 09:17:13 +01:00
Adam Hathcock
c823acaa3f optimize ReadFully and Skip 2025-10-22 09:10:16 +01:00
Adam Hathcock
e0d6cd9cb7 Try to reduce custom functions for array/byte management 2025-10-22 09:00:21 +01:00
Adam Hathcock
01021e102b remove some extra stackallocs 2025-10-22 08:36:03 +01:00
Adam Hathcock
6de738ff17 reduce dynamic stackallocs in unpackv1 2025-10-22 08:32:19 +01:00
Adam Hathcock
c0612547eb Merge pull request #964 from adamhathcock/adam/extract-all-solid-only
Only allow extract all on archives that are solid (some rars and 7zip only)
2025-10-21 14:08:23 +01:00
Adam Hathcock
e960907698 Update src/SharpCompress/Archives/AbstractArchive.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 13:55:56 +01:00
Adam Hathcock
84e03b1b27 Allow 7zip files of all sizes? 2025-10-21 10:28:58 +01:00
Adam Hathcock
f1a80da34b fix tests that use extract all wrongly 2025-10-21 09:56:29 +01:00
Adam Hathcock
5a5a55e556 fmt 2025-10-21 09:22:35 +01:00
Adam Hathcock
e1f132b45b Only allow extract all on archives that are solid (some rars and 7zip only) 2025-10-21 09:21:46 +01:00
dependabot[bot]
087011aede Bump AwesomeAssertions from 9.2.0 to 9.2.1
---
updated-dependencies:
- dependency-name: AwesomeAssertions
  dependency-version: 9.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 10:44:17 +00:00
Adam Hathcock
1430bf9b31 fmt 2025-10-15 09:54:13 +01:00
Adam Hathcock
4e5de817ef Removed too many wrappers
# Conflicts:
#	src/SharpCompress/Compressors/Xz/XZIndex.cs
2025-10-15 09:53:46 +01:00
Adam Hathcock
5d6b94f8c3 Merge pull request #952 from adamhathcock/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-10-14 08:25:53 +01:00
Adam Hathcock
8dfbe56f42 Merge branch 'master' into dependabot/github_actions/actions/checkout-5 2025-10-14 08:23:18 +01:00
Adam Hathcock
df79d983d7 Merge pull request #957 from adamhathcock/dependabot/github_actions/actions/setup-dotnet-5
Bump actions/setup-dotnet from 4 to 5
2025-10-14 08:22:47 +01:00
dependabot[bot]
6c23a28826 Bump actions/setup-dotnet from 4 to 5
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 16:21:25 +00:00
dependabot[bot]
f72289570a Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 16:12:51 +00:00
Adam Hathcock
51bc9dc20e Merge pull request #950 from adamhathcock/adamhathcock-patch-1
Configure Dependabot for NuGet updates
2025-10-13 16:52:07 +01:00
Adam Hathcock
e45ac6bfa9 Update .github/dependabot.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-13 16:48:55 +01:00
Adam Hathcock
44d1cbdb0c Configure Dependabot for NuGet updates
Added NuGet package ecosystem configuration for Dependabot.
2025-10-13 16:47:46 +01:00
121 changed files with 8036 additions and 1006 deletions

7
.copilot-agent.yml Normal file
View File

@@ -0,0 +1,7 @@
enabled: true
agent:
name: copilot-coding-agent
allow:
- paths: ["src/**/*", "tests/**/*", "README.md", "AGENTS.md"]
actions: ["create", "modify"]
require_review_before_merge: true

15
.github/COPILOT_AGENT_README.md vendored Normal file
View File

@@ -0,0 +1,15 @@
# 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.

17
.github/agents/copilot-agent.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
enabled: true
agent:
name: copilot-coding-agent
allow:
- paths: ["src/**/*", "tests/**/*", "README.md", "AGENTS.md"]
actions: ["create", "modify", "delete"]
require_review_before_merge: true
required_approvals: 1
allowed_merge_strategies:
- squash
- merge
auto_merge_on_green: false
run_workflows: true
notes: |
- This manifest expresses the policy for the Copilot coding agent in this repository.
- It does NOT install or authorize the agent; a repository admin must install the Copilot coding agent app and grant the repository the necessary permissions (contents: write, pull_requests: write, checks: write, actions: write/read, issues: write) to allow the agent to act.
- Keep allow paths narrow and prefer require_review_before_merge during initial rollout.

View File

@@ -1,6 +1,13 @@
version: 2
updates:
- package-ecosystem: "github-actions" # search for actions - there are other options available
directory: "/" # search in .github/workflows under root `/`
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly" # check for action update every week
interval: "weekly"
- package-ecosystem: "nuget"
directory: "/" # change to "/src/YourProject" if .csproj files are in subfolders
schedule:
interval: "weekly"
open-pull-requests-limit: 5
# optional: target-branch: "master"

View File

@@ -14,12 +14,12 @@ jobs:
os: [windows-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
- uses: actions/checkout@v5
- uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.0.x
- run: dotnet run --project build/build.csproj
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: ${{ matrix.os }}-sharpcompress.nupkg
path: artifacts/*

106
AGENTS.md
View File

@@ -1,19 +1,24 @@
---
description: 'Guidelines for building C# applications'
description: 'Guidelines for building SharpCompress - A C# compression library'
applyTo: '**/*.cs'
---
# C# Development
# SharpCompress Development
## About SharpCompress
SharpCompress is a pure C# compression library supporting multiple archive formats (Zip, Tar, GZip, BZip2, 7Zip, Rar, LZip, XZ, ZStandard) for .NET Framework 4.62, .NET Standard 2.1, .NET 6.0, and .NET 8.0. The library provides both seekable Archive APIs and forward-only Reader/Writer APIs for streaming scenarios.
## C# Instructions
- Always use the latest version C#, currently C# 13 features.
- Write clear and concise comments for each function.
- Follow the existing code style and patterns in the codebase.
## General Instructions
- 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.
- For libraries or external dependencies, mention their usage and purpose in comments.
- Preserve backward compatibility when making changes to public APIs.
## Naming Conventions
@@ -23,20 +28,26 @@ applyTo: '**/*.cs'
## Code Formatting
- Use CSharpier for all code formatting to ensure consistent style across the project.
- Install CSharpier globally: `dotnet tool install -g csharpier`
- Format files with: `dotnet csharpier format .`
- Configure your IDE to format on save using CSharpier.
- CSharpier configuration can be customized via `.csharpierrc` file in the project root.
- Trust CSharpier's opinionated formatting decisions to maintain 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
- The project also uses `.editorconfig` for editor settings (indentation, encoding, etc.)
- Let CSharpier handle code style while `.editorconfig` handles editor behavior
## Project Setup and Structure
- Guide users through creating a new .NET project with the appropriate templates.
- Explain the purpose of each generated file and folder to build understanding of the project structure.
- Demonstrate how to organize code using feature folders or domain-driven design principles.
- Show proper separation of concerns with models, services, and data access layers.
- Explain the Program.cs and configuration system in ASP.NET Core 9 including environment-specific settings.
- The project targets multiple frameworks: .NET Framework 4.62, .NET Standard 2.1, .NET 6.0, and .NET 8.0
- Main library is in `src/SharpCompress/`
- Tests are in `tests/SharpCompress.Test/`
- Performance tests are in `tests/SharpCompress.Performance/`
- Test archives are in `tests/TestArchives/`
- Build project is in `build/`
- Use `dotnet build` to build the solution
- Use `dotnet test` to run tests
- Solution file: `SharpCompress.sln`
## Nullable Reference Types
@@ -44,21 +55,64 @@ applyTo: '**/*.cs'
- Always use `is null` or `is not null` instead of `== null` or `!= null`.
- Trust the C# null annotations and don't add null checks when the type system says a value cannot be null.
## SharpCompress-Specific Guidelines
### Supported Formats
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
### Stream Handling Rules
- **Disposal**: As of version 0.21, SharpCompress closes wrapped streams by default
- Use `ReaderOptions` or `WriterOptions` with `LeaveStreamOpen = true` to control stream disposal
- Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal
- Always dispose of readers, writers, and archives in `using` blocks
- For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs
### Async/Await Patterns
- All I/O operations support async/await with `CancellationToken`
- Async methods follow the naming convention: `MethodNameAsync`
- Key async methods:
- `WriteEntryToAsync` - Extract entry asynchronously
- `WriteAllToDirectoryAsync` - Extract all entries asynchronously
- `WriteAsync` - Write entry asynchronously
- `WriteAllAsync` - Write directory asynchronously
- `OpenEntryStreamAsync` - Open entry stream asynchronously
- Always provide `CancellationToken` parameter in async methods
### Archive APIs vs Reader/Writer APIs
- **Archive API**: Use for random access with seekable streams (e.g., `ZipArchive`, `TarArchive`)
- **Reader API**: Use for forward-only reading on non-seekable streams (e.g., `ZipReader`, `TarReader`)
- **Writer API**: Use for forward-only writing on streams (e.g., `ZipWriter`, `TarWriter`)
- 7Zip only supports Archive API due to format limitations
### Tar-Specific Considerations
- Tar format requires file size in the header
- If no size is specified to TarWriter and the stream is not seekable, an exception will be thrown
- Tar combined with compression (GZip, BZip2, LZip, XZ) is supported
### Zip-Specific Considerations
- Supports Zip64 for large files (seekable streams only)
- Supports PKWare and WinZip AES encryption
- Multiple compression methods: None, Shrink, Reduce, Implode, DEFLATE, Deflate64, BZip2, LZMA, PPMd
- Encrypted LZMA is not supported
### Performance Considerations
- For large files, use Reader/Writer APIs with non-seekable streams to avoid loading entire file in memory
- Leverage async I/O for better scalability
- Consider compression level trade-offs (speed vs. size)
- Use appropriate buffer sizes for stream operations
## Testing
- Always include test cases for critical paths of the application.
- Guide users through creating unit tests.
- Test with multiple archive formats when making changes to core functionality.
- Include tests for both Archive and Reader/Writer APIs when applicable.
- Test async operations with cancellation tokens.
- Do not emit "Act", "Arrange" or "Assert" comments.
- Copy existing style in nearby files for test method names and capitalization.
- Explain integration testing approaches for API endpoints.
- Demonstrate how to mock dependencies for effective testing.
- Show how to test authentication and authorization logic.
- Explain test-driven development principles as applied to API development.
## Performance Optimization
- Guide users on implementing caching strategies (in-memory, distributed, response caching).
- Explain asynchronous programming patterns and why they matter for API performance.
- Demonstrate pagination, filtering, and sorting for large data sets.
- Show how to implement compression and other performance optimizations.
- Explain how to measure and benchmark API performance.
- 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.

View File

@@ -1,18 +1,18 @@
<Project>
<ItemGroup>
<PackageVersion Include="Bullseye" Version="6.0.0" />
<PackageVersion Include="AwesomeAssertions" Version="9.2.0" />
<PackageVersion Include="AwesomeAssertions" Version="9.2.1" />
<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="17.13.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageVersion Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageVersion Include="SimpleExec" Version="12.0.0" />
<PackageVersion Include="System.Buffers" Version="4.6.0" />
<PackageVersion Include="System.Memory" Version="4.6.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="xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="ZstdSharp.Port" Version="0.8.6" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />

View File

@@ -4,6 +4,8 @@ SharpCompress is a compression library in pure C# for .NET Framework 4.62, .NET
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.
GitHub Actions Build -
[![SharpCompress](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml/badge.svg)](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml)
[![Static Badge](https://img.shields.io/badge/API%20Docs-DNDocs-190088?logo=readme&logoColor=white)](https://dndocs.com/d/sharpcompress/api/index.html)
@@ -32,6 +34,82 @@ 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!

View File

@@ -21,8 +21,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{CDB425
Directory.Packages.props = Directory.Packages.props
NuGet.config = NuGet.config
.github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml
USAGE.md = USAGE.md
README.md = README.md
FORMATS.md = FORMATS.md
AGENTS.md = AGENTS.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCompress.Performance", "tests\SharpCompress.Performance\SharpCompress.Performance.csproj", "{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -41,6 +47,10 @@ Global
{D4D613CB-5E94-47FB-85BE-B8423D20C545}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4D613CB-5E94-47FB-85BE-B8423D20C545}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4D613CB-5E94-47FB-85BE-B8423D20C545}.Release|Any CPU.Build.0 = Release|Any CPU
{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -48,5 +58,6 @@ Global
GlobalSection(NestedProjects) = preSolution
{FD19DDD8-72B2-4024-8665-0D1F7A2AA998} = {3C5BE746-03E5-4895-9988-0B57F162F86C}
{F2B1A1EB-0FA6-40D0-8908-E13247C7226F} = {0F0901FF-E8D9-426A-B5A2-17C7F47C1529}
{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17} = {0F0901FF-E8D9-426A-B5A2-17C7F47C1529}
EndGlobalSection
EndGlobal

143
USAGE.md
View File

@@ -1,5 +1,18 @@
# SharpCompress Usage
## Async/Await Support
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.
**Key Async Methods:**
- `reader.WriteEntryToAsync(stream, cancellationToken)` - Extract entry asynchronously
- `reader.WriteAllToDirectoryAsync(path, options, cancellationToken)` - Extract all asynchronously
- `writer.WriteAsync(filename, stream, modTime, cancellationToken)` - Write entry asynchronously
- `writer.WriteAllAsync(directory, pattern, searchOption, cancellationToken)` - Write directory asynchronously
- `entry.OpenEntryStreamAsync(cancellationToken)` - Open entry stream asynchronously
See [Async Examples](#async-examples) section below for usage patterns.
## Stream Rules (changed with 0.21)
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.
@@ -172,3 +185,133 @@ foreach(var entry in tr.Entries)
Console.WriteLine($"{entry.Key}");
}
```
## Async Examples
### Async Reader Examples
**Extract single entry asynchronously:**
```C#
using (Stream stream = File.OpenRead("archive.zip"))
using (var reader = ReaderFactory.Open(stream))
{
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
using (var entryStream = reader.OpenEntryStream())
{
using (var outputStream = File.Create("output.bin"))
{
await reader.WriteEntryToAsync(outputStream, cancellationToken);
}
}
}
}
}
```
**Extract all entries asynchronously:**
```C#
using (Stream stream = File.OpenRead("archive.tar.gz"))
using (var reader = ReaderFactory.Open(stream))
{
await reader.WriteAllToDirectoryAsync(
@"D:\temp",
new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
},
cancellationToken
);
}
```
**Open and process entry stream asynchronously:**
```C#
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 the decompressed stream asynchronously
await ProcessStreamAsync(entryStream, cancellationToken);
}
}
}
```
### Async Writer Examples
**Write single file asynchronously:**
```C#
using (Stream archiveStream = File.OpenWrite("output.zip"))
using (var writer = WriterFactory.Open(archiveStream, ArchiveType.Zip, CompressionType.Deflate))
{
using (Stream fileStream = File.OpenRead("input.txt"))
{
await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken);
}
}
```
**Write entire directory asynchronously:**
```C#
using (Stream stream = File.OpenWrite("backup.tar.gz"))
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)))
{
await writer.WriteAllAsync(
@"D:\files",
"*",
SearchOption.AllDirectories,
cancellationToken
);
}
```
**Write with progress tracking and cancellation:**
```C#
var cts = new CancellationTokenSource();
// Set timeout or cancel from UI
cts.CancelAfter(TimeSpan.FromMinutes(5));
using (Stream stream = File.OpenWrite("archive.zip"))
using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
{
try
{
await writer.WriteAllAsync(@"D:\data", "*", SearchOption.AllDirectories, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled");
}
}
```
### Archive Async Examples
**Extract from archive asynchronously:**
```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
);
}
}
```
**Benefits of Async Operations:**
- Non-blocking I/O for better application responsiveness
- Improved scalability for server applications
- Support for cancellation via CancellationToken
- Better resource utilization in async/await contexts
- Compatible with modern .NET async patterns

View File

@@ -144,6 +144,12 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
/// <returns></returns>
public IReader ExtractAllEntries()
{
if (!IsSolid && Type != ArchiveType.SevenZip)
{
throw new InvalidOperationException(
"ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)."
);
}
((IArchiveExtractionListener)this).EnsureEntriesLoaded();
return CreateReaderForSolidExtraction();
}

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.IO;
using SharpCompress.Writers;
@@ -94,6 +96,9 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
DateTime? modified
) => AddEntry(key, source, closeStream, size, modified);
IArchiveEntry IWritableArchive.AddDirectoryEntry(string key, DateTime? modified) =>
AddDirectoryEntry(key, modified);
public TEntry AddEntry(
string key,
Stream source,
@@ -134,6 +139,22 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
return false;
}
public TEntry AddDirectoryEntry(string key, DateTime? modified = null)
{
if (key.Length > 0 && key[0] is '/' or '\\')
{
key = key.Substring(1);
}
if (DoesKeyMatchExisting(key))
{
throw new ArchiveException("Cannot add entry with duplicate key: " + key);
}
var entry = CreateDirectoryEntry(key, modified);
newEntries.Add(entry);
RebuildModifiedCollection();
return entry;
}
public void SaveTo(Stream stream, WriterOptions options)
{
//reset streams of new entries
@@ -141,6 +162,18 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
SaveTo(stream, options, OldEntries, newEntries);
}
public async Task SaveToAsync(
Stream stream,
WriterOptions options,
CancellationToken cancellationToken = default
)
{
//reset streams of new entries
newEntries.Cast<IWritableArchiveEntry>().ForEach(x => x.Stream.Seek(0, SeekOrigin.Begin));
await SaveToAsync(stream, options, OldEntries, newEntries, cancellationToken)
.ConfigureAwait(false);
}
protected TEntry CreateEntry(
string key,
Stream source,
@@ -166,6 +199,8 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
bool closeStream
);
protected abstract TEntry CreateDirectoryEntry(string key, DateTime? modified);
protected abstract void SaveTo(
Stream stream,
WriterOptions options,
@@ -173,6 +208,14 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
IEnumerable<TEntry> newEntries
);
protected abstract Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<TEntry> oldEntries,
IEnumerable<TEntry> newEntries,
CancellationToken cancellationToken = default
);
public override void Dispose()
{
base.Dispose();

View File

@@ -45,7 +45,7 @@ public static class ArchiveFactory
/// <param name="options"></param>
public static IArchive Open(string filePath, ReaderOptions? options = null)
{
filePath.CheckNotNullOrEmpty(nameof(filePath));
filePath.NotNullOrEmpty(nameof(filePath));
return Open(new FileInfo(filePath), options);
}
@@ -68,7 +68,7 @@ public static class ArchiveFactory
/// <param name="options"></param>
public static IArchive Open(IEnumerable<FileInfo> fileInfos, ReaderOptions? options = null)
{
fileInfos.CheckNotNull(nameof(fileInfos));
fileInfos.NotNull(nameof(fileInfos));
var filesArray = fileInfos.ToArray();
if (filesArray.Length == 0)
{
@@ -81,7 +81,7 @@ public static class ArchiveFactory
return Open(fileInfo, options);
}
fileInfo.CheckNotNull(nameof(fileInfo));
fileInfo.NotNull(nameof(fileInfo));
options ??= new ReaderOptions { LeaveStreamOpen = false };
return FindFactory<IMultiArchiveFactory>(fileInfo).Open(filesArray, options);
@@ -94,7 +94,7 @@ public static class ArchiveFactory
/// <param name="options"></param>
public static IArchive Open(IEnumerable<Stream> streams, ReaderOptions? options = null)
{
streams.CheckNotNull(nameof(streams));
streams.NotNull(nameof(streams));
var streamsArray = streams.ToArray();
if (streamsArray.Length == 0)
{
@@ -107,7 +107,7 @@ public static class ArchiveFactory
return Open(firstStream, options);
}
firstStream.CheckNotNull(nameof(firstStream));
firstStream.NotNull(nameof(firstStream));
options ??= new ReaderOptions();
return FindFactory<IMultiArchiveFactory>(firstStream).Open(streamsArray, options);
@@ -129,7 +129,7 @@ public static class ArchiveFactory
private static T FindFactory<T>(FileInfo finfo)
where T : IFactory
{
finfo.CheckNotNull(nameof(finfo));
finfo.NotNull(nameof(finfo));
using Stream stream = finfo.OpenRead();
return FindFactory<T>(stream);
}
@@ -137,7 +137,7 @@ public static class ArchiveFactory
private static T FindFactory<T>(Stream stream)
where T : IFactory
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
if (!stream.CanRead || !stream.CanSeek)
{
throw new ArgumentException("Stream should be readable and seekable");
@@ -172,7 +172,7 @@ public static class ArchiveFactory
int bufferSize = ReaderOptions.DefaultBufferSize
)
{
filePath.CheckNotNullOrEmpty(nameof(filePath));
filePath.NotNullOrEmpty(nameof(filePath));
using Stream s = File.OpenRead(filePath);
return IsArchive(s, out type, bufferSize);
}
@@ -184,7 +184,7 @@ public static class ArchiveFactory
)
{
type = null;
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
if (!stream.CanRead || !stream.CanSeek)
{
@@ -215,7 +215,7 @@ public static class ArchiveFactory
/// <returns></returns>
public static IEnumerable<string> GetFileParts(string part1)
{
part1.CheckNotNullOrEmpty(nameof(part1));
part1.NotNullOrEmpty(nameof(part1));
return GetFileParts(new FileInfo(part1)).Select(a => a.FullName);
}
@@ -226,7 +226,7 @@ public static class ArchiveFactory
/// <returns></returns>
public static IEnumerable<FileInfo> GetFileParts(FileInfo part1)
{
part1.CheckNotNull(nameof(part1));
part1.NotNull(nameof(part1));
yield return part1;
foreach (var factory in Factory.Factories.OfType<IFactory>())

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.GZip;
using SharpCompress.IO;
@@ -21,7 +23,7 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
/// <param name="readerOptions"></param>
public static GZipArchive Open(string filePath, ReaderOptions? readerOptions = null)
{
filePath.CheckNotNullOrEmpty(nameof(filePath));
filePath.NotNullOrEmpty(nameof(filePath));
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
@@ -32,7 +34,7 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
/// <param name="readerOptions"></param>
public static GZipArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.CheckNotNull(nameof(fileInfo));
fileInfo.NotNull(nameof(fileInfo));
return new GZipArchive(
new SourceStream(
fileInfo,
@@ -52,7 +54,7 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
ReaderOptions? readerOptions = null
)
{
fileInfos.CheckNotNull(nameof(fileInfos));
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new GZipArchive(
new SourceStream(
@@ -70,7 +72,7 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
/// <param name="readerOptions"></param>
public static GZipArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
{
streams.CheckNotNull(nameof(streams));
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new GZipArchive(
new SourceStream(
@@ -88,7 +90,7 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
/// <param name="readerOptions"></param>
public static GZipArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
@@ -136,6 +138,16 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
SaveTo(stream, new WriterOptions(CompressionType.GZip));
}
public Task SaveToAsync(string filePath, CancellationToken cancellationToken = default) =>
SaveToAsync(new FileInfo(filePath), cancellationToken);
public async Task SaveToAsync(FileInfo fileInfo, CancellationToken cancellationToken = default)
{
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
await SaveToAsync(stream, new WriterOptions(CompressionType.GZip), cancellationToken)
.ConfigureAwait(false);
}
public static bool IsGZipFile(Stream stream)
{
// read the header on the first read
@@ -173,6 +185,11 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
return new GZipWritableArchiveEntry(this, source, filePath, size, modified, closeStream);
}
protected override GZipArchiveEntry CreateDirectoryEntry(
string directoryPath,
DateTime? modified
) => throw new NotSupportedException("GZip archives do not support directory entries.");
protected override void SaveTo(
Stream stream,
WriterOptions options,
@@ -196,6 +213,28 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
}
}
protected override async Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<GZipArchiveEntry> oldEntries,
IEnumerable<GZipArchiveEntry> newEntries,
CancellationToken cancellationToken = default
)
{
if (Entries.Count > 1)
{
throw new InvalidFormatException("Only one entry is allowed in a GZip Archive");
}
using var writer = new GZipWriter(stream, new GZipWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken)
.ConfigureAwait(false);
}
}
protected override IEnumerable<GZipArchiveEntry> LoadEntries(IEnumerable<GZipVolume> volumes)
{
var stream = volumes.Single().Stream;

View File

@@ -1,5 +1,7 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.GZip;
namespace SharpCompress.Archives.GZip;
@@ -13,13 +15,20 @@ public class GZipArchiveEntry : GZipEntry, IArchiveEntry
{
//this is to reset the stream to be read multiple times
var part = (GZipFilePart)Parts.Single();
if (part.GetRawStream().Position != part.EntryStartPosition)
var rawStream = part.GetRawStream();
if (rawStream.CanSeek && rawStream.Position != part.EntryStartPosition)
{
part.GetRawStream().Position = part.EntryStartPosition;
rawStream.Position = part.EntryStartPosition;
}
return Parts.Single().GetCompressedStream().NotNull();
}
public virtual Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
{
// GZip synchronous implementation is fast enough, just wrap it
return Task.FromResult(OpenEntryStream());
}
#region IArchiveEntry Members
public IArchive Archive { get; }

View File

@@ -1,4 +1,6 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.Archives;
@@ -11,6 +13,12 @@ public interface IArchiveEntry : IEntry
/// </summary>
Stream OpenEntryStream();
/// <summary>
/// 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);
/// <summary>
/// The archive can find all the parts of the archive needed to extract this entry.
/// </summary>

View File

@@ -1,4 +1,6 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
@@ -25,7 +27,35 @@ public static class IArchiveEntryExtensions
using (entryStream)
{
using Stream s = new ListeningStream(streamListener, entryStream);
s.TransferTo(streamToWriteTo);
s.CopyTo(streamToWriteTo);
}
streamListener.FireEntryExtractionEnd(archiveEntry);
}
public static async Task WriteToAsync(
this IArchiveEntry archiveEntry,
Stream streamToWriteTo,
CancellationToken cancellationToken = default
)
{
if (archiveEntry.IsDirectory)
{
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
}
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)
{
using Stream s = new ListeningStream(streamListener, entryStream);
await s.CopyToAsync(streamToWriteTo, 81920, cancellationToken).ConfigureAwait(false);
}
streamListener.FireEntryExtractionEnd(archiveEntry);
}
@@ -45,6 +75,23 @@ public static class IArchiveEntryExtensions
entry.WriteToFile
);
/// <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
);
/// <summary>
/// Extract to specific file
/// </summary>
@@ -63,4 +110,24 @@ public static class IArchiveEntryExtensions
entry.WriteTo(fs);
}
);
/// <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);
}
);
}

View File

@@ -45,12 +45,10 @@ public static class IArchiveExtensions
var seenDirectories = new HashSet<string>();
// Extract
var entries = archive.ExtractAllEntries();
while (entries.MoveToNextEntry())
foreach (var entry in archive.Entries)
{
cancellationToken.ThrowIfCancellationRequested();
var entry = entries.Entry;
if (entry.IsDirectory)
{
var dirPath = Path.Combine(destination, entry.Key.NotNull("Entry Key is null"));
@@ -77,7 +75,7 @@ public static class IArchiveExtensions
// Write file
using var fs = File.OpenWrite(path);
entries.WriteEntryTo(fs);
entry.WriteTo(fs);
// Update progress
bytesRead += entry.Size;

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Writers;
namespace SharpCompress.Archives;
@@ -16,8 +18,16 @@ public interface IWritableArchive : IArchive
DateTime? modified = null
);
IArchiveEntry AddDirectoryEntry(string key, DateTime? modified = null);
void SaveTo(Stream stream, WriterOptions options);
Task SaveToAsync(
Stream stream,
WriterOptions options,
CancellationToken cancellationToken = default
);
/// <summary>
/// Use this to pause entry rebuilding when adding large collections of entries. Dispose when complete. A using statement is recommended.
/// </summary>

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Writers;
namespace SharpCompress.Archives;
@@ -42,6 +44,24 @@ public static class IWritableArchiveExtensions
writableArchive.SaveTo(stream, options);
}
public static Task SaveToAsync(
this IWritableArchive writableArchive,
string filePath,
WriterOptions options,
CancellationToken cancellationToken = default
) => writableArchive.SaveToAsync(new FileInfo(filePath), options, cancellationToken);
public static async Task SaveToAsync(
this IWritableArchive writableArchive,
FileInfo fileInfo,
WriterOptions options,
CancellationToken cancellationToken = default
)
{
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
await writableArchive.SaveToAsync(stream, options, cancellationToken).ConfigureAwait(false);
}
public static void AddAllFromDirectory(
this IWritableArchive writableArchive,
string filePath,

View File

@@ -95,7 +95,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
/// <param name="options"></param>
public static RarArchive Open(string filePath, ReaderOptions? options = null)
{
filePath.CheckNotNullOrEmpty(nameof(filePath));
filePath.NotNullOrEmpty(nameof(filePath));
var fileInfo = new FileInfo(filePath);
return new RarArchive(
new SourceStream(
@@ -113,7 +113,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
/// <param name="options"></param>
public static RarArchive Open(FileInfo fileInfo, ReaderOptions? options = null)
{
fileInfo.CheckNotNull(nameof(fileInfo));
fileInfo.NotNull(nameof(fileInfo));
return new RarArchive(
new SourceStream(
fileInfo,
@@ -130,7 +130,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
/// <param name="options"></param>
public static RarArchive Open(Stream stream, ReaderOptions? options = null)
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
@@ -150,7 +150,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
ReaderOptions? readerOptions = null
)
{
fileInfos.CheckNotNull(nameof(fileInfos));
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new RarArchive(
new SourceStream(
@@ -168,7 +168,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
/// <param name="readerOptions"></param>
public static RarArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
{
streams.CheckNotNull(nameof(streams));
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new RarArchive(
new SourceStream(

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;
@@ -84,6 +86,9 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
);
}
public Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default) =>
Task.FromResult(OpenEntryStream());
public bool IsComplete
{
get

View File

@@ -21,7 +21,7 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
/// <param name="readerOptions"></param>
public static SevenZipArchive Open(string filePath, ReaderOptions? readerOptions = null)
{
filePath.CheckNotNullOrEmpty("filePath");
filePath.NotNullOrEmpty("filePath");
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
@@ -32,7 +32,7 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
/// <param name="readerOptions"></param>
public static SevenZipArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.CheckNotNull("fileInfo");
fileInfo.NotNull("fileInfo");
return new SevenZipArchive(
new SourceStream(
fileInfo,
@@ -52,7 +52,7 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
ReaderOptions? readerOptions = null
)
{
fileInfos.CheckNotNull(nameof(fileInfos));
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new SevenZipArchive(
new SourceStream(
@@ -73,7 +73,7 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
ReaderOptions? readerOptions = null
)
{
streams.CheckNotNull(nameof(streams));
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new SevenZipArchive(
new SourceStream(
@@ -91,7 +91,7 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
/// <param name="readerOptions"></param>
public static SevenZipArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
stream.CheckNotNull("stream");
stream.NotNull("stream");
if (stream is not { CanSeek: true })
{

View File

@@ -1,4 +1,6 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.SevenZip;
namespace SharpCompress.Archives.SevenZip;
@@ -10,6 +12,9 @@ public class SevenZipArchiveEntry : SevenZipEntry, IArchiveEntry
public Stream OpenEntryStream() => FilePart.GetCompressedStream();
public Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default) =>
Task.FromResult(OpenEntryStream());
public IArchive Archive { get; }
public bool IsComplete => true;

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.Tar;
using SharpCompress.Common.Tar.Headers;
@@ -22,7 +24,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
/// <param name="readerOptions"></param>
public static TarArchive Open(string filePath, ReaderOptions? readerOptions = null)
{
filePath.CheckNotNullOrEmpty(nameof(filePath));
filePath.NotNullOrEmpty(nameof(filePath));
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
@@ -33,7 +35,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
/// <param name="readerOptions"></param>
public static TarArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.CheckNotNull(nameof(fileInfo));
fileInfo.NotNull(nameof(fileInfo));
return new TarArchive(
new SourceStream(
fileInfo,
@@ -53,7 +55,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
ReaderOptions? readerOptions = null
)
{
fileInfos.CheckNotNull(nameof(fileInfos));
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new TarArchive(
new SourceStream(
@@ -71,7 +73,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
/// <param name="readerOptions"></param>
public static TarArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
{
streams.CheckNotNull(nameof(streams));
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new TarArchive(
new SourceStream(
@@ -89,7 +91,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
/// <param name="readerOptions"></param>
public static TarArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
@@ -178,7 +180,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
using (var entryStream = entry.OpenEntryStream())
{
using var memoryStream = new MemoryStream();
entryStream.TransferTo(memoryStream);
entryStream.CopyTo(memoryStream);
memoryStream.Position = 0;
var bytes = memoryStream.ToArray();
@@ -222,6 +224,11 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
closeStream
);
protected override TarArchiveEntry CreateDirectoryEntry(
string directoryPath,
DateTime? modified
) => new TarWritableArchiveEntry(this, directoryPath, modified);
protected override void SaveTo(
Stream stream,
WriterOptions options,
@@ -230,15 +237,62 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
)
{
using var writer = new TarWriter(stream, new TarWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
foreach (var entry in oldEntries.Concat(newEntries))
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size
);
if (entry.IsDirectory)
{
writer.WriteDirectory(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime
);
}
else
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size
);
}
}
}
protected override async Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<TarArchiveEntry> oldEntries,
IEnumerable<TarArchiveEntry> newEntries,
CancellationToken cancellationToken = default
)
{
using var writer = new TarWriter(stream, new TarWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries))
{
if (entry.IsDirectory)
{
await writer
.WriteDirectoryAsync(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime,
cancellationToken
)
.ConfigureAwait(false);
}
else
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size,
cancellationToken
)
.ConfigureAwait(false);
}
}
}

View File

@@ -1,5 +1,7 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Tar;
@@ -12,6 +14,10 @@ public class TarArchiveEntry : TarEntry, IArchiveEntry
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
public virtual Task<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
) => Task.FromResult(OpenEntryStream());
#region IArchiveEntry Members
public IArchive Archive { get; }

View File

@@ -9,7 +9,8 @@ namespace SharpCompress.Archives.Tar;
internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiveEntry
{
private readonly bool closeStream;
private readonly Stream stream;
private readonly Stream? stream;
private readonly bool isDirectory;
internal TarWritableArchiveEntry(
TarArchive archive,
@@ -27,6 +28,22 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
Size = size;
LastModifiedTime = lastModified;
this.closeStream = closeStream;
isDirectory = false;
}
internal TarWritableArchiveEntry(
TarArchive archive,
string directoryPath,
DateTime? lastModified
)
: base(archive, null, CompressionType.None)
{
stream = null;
Key = directoryPath;
Size = 0;
LastModifiedTime = lastModified;
closeStream = false;
isDirectory = true;
}
public override long Crc => 0;
@@ -47,15 +64,19 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
public override bool IsEncrypted => false;
public override bool IsDirectory => false;
public override bool IsDirectory => isDirectory;
public override bool IsSplitAfter => false;
internal override IEnumerable<FilePart> Parts => throw new NotImplementedException();
Stream IWritableArchiveEntry.Stream => stream;
Stream IWritableArchiveEntry.Stream => stream ?? Stream.Null;
public override Stream OpenEntryStream()
{
if (stream is null)
{
return Stream.Null;
}
//ensure new stream is at the start, this could be reset
stream.Seek(0, SeekOrigin.Begin);
return SharpCompressStream.Create(stream, leaveOpen: true);
@@ -63,7 +84,7 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
internal override void Close()
{
if (closeStream)
if (closeStream && stream is not null)
{
stream.Dispose();
}

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.Zip;
using SharpCompress.Common.Zip.Headers;
@@ -43,7 +45,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
/// <param name="readerOptions"></param>
public static ZipArchive Open(string filePath, ReaderOptions? readerOptions = null)
{
filePath.CheckNotNullOrEmpty(nameof(filePath));
filePath.NotNullOrEmpty(nameof(filePath));
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
@@ -54,7 +56,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
/// <param name="readerOptions"></param>
public static ZipArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.CheckNotNull(nameof(fileInfo));
fileInfo.NotNull(nameof(fileInfo));
return new ZipArchive(
new SourceStream(
fileInfo,
@@ -74,7 +76,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
ReaderOptions? readerOptions = null
)
{
fileInfos.CheckNotNull(nameof(fileInfos));
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new ZipArchive(
new SourceStream(
@@ -92,7 +94,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
/// <param name="readerOptions"></param>
public static ZipArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
{
streams.CheckNotNull(nameof(streams));
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new ZipArchive(
new SourceStream(
@@ -110,7 +112,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
/// <param name="readerOptions"></param>
public static ZipArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
@@ -306,14 +308,59 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
)
{
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
foreach (var entry in oldEntries.Concat(newEntries))
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime
);
if (entry.IsDirectory)
{
writer.WriteDirectory(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime
);
}
else
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime
);
}
}
}
protected override async Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<ZipArchiveEntry> oldEntries,
IEnumerable<ZipArchiveEntry> newEntries,
CancellationToken cancellationToken = default
)
{
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries))
{
if (entry.IsDirectory)
{
await writer
.WriteDirectoryAsync(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime,
cancellationToken
)
.ConfigureAwait(false);
}
else
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(
entry.Key.NotNull("Entry Key is null"),
entryStream,
cancellationToken
)
.ConfigureAwait(false);
}
}
}
@@ -325,6 +372,11 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
bool closeStream
) => new ZipWritableArchiveEntry(this, source, filePath, size, modified, closeStream);
protected override ZipArchiveEntry CreateDirectoryEntry(
string directoryPath,
DateTime? modified
) => new ZipWritableArchiveEntry(this, directoryPath, modified);
public static ZipArchive Create() => new();
protected override IReader CreateReaderForSolidExtraction()

View File

@@ -1,5 +1,7 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Zip;
namespace SharpCompress.Archives.Zip;
@@ -11,6 +13,10 @@ public class ZipArchiveEntry : ZipEntry, IArchiveEntry
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
public virtual Task<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
) => Task.FromResult(OpenEntryStream());
#region IArchiveEntry Members
public IArchive Archive { get; }

View File

@@ -9,7 +9,8 @@ namespace SharpCompress.Archives.Zip;
internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
{
private readonly bool closeStream;
private readonly Stream stream;
private readonly Stream? stream;
private readonly bool isDirectory;
private bool isDisposed;
internal ZipWritableArchiveEntry(
@@ -27,6 +28,22 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
Size = size;
LastModifiedTime = lastModified;
this.closeStream = closeStream;
isDirectory = false;
}
internal ZipWritableArchiveEntry(
ZipArchive archive,
string directoryPath,
DateTime? lastModified
)
: base(archive, null)
{
stream = null;
Key = directoryPath;
Size = 0;
LastModifiedTime = lastModified;
closeStream = false;
isDirectory = true;
}
public override long Crc => 0;
@@ -47,16 +64,20 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
public override bool IsEncrypted => false;
public override bool IsDirectory => false;
public override bool IsDirectory => isDirectory;
public override bool IsSplitAfter => false;
internal override IEnumerable<FilePart> Parts => throw new NotImplementedException();
Stream IWritableArchiveEntry.Stream => stream;
Stream IWritableArchiveEntry.Stream => stream ?? Stream.Null;
public override Stream OpenEntryStream()
{
if (stream is null)
{
return Stream.Null;
}
//ensure new stream is at the start, this could be reset
stream.Seek(0, SeekOrigin.Begin);
return SharpCompressStream.Create(stream, leaveOpen: true);
@@ -64,7 +85,7 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
internal override void Close()
{
if (closeStream && !isDisposed)
if (closeStream && !isDisposed && stream is not null)
{
stream.Dispose();
isDisposed = true;

View File

@@ -1,6 +1,8 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
using SharpCompress.Readers;
@@ -51,6 +53,15 @@ public class EntryStream : Stream, IStreamStack
_completed = true;
}
/// <summary>
/// Asynchronously skip the rest of the entry stream.
/// </summary>
public async Task SkipEntryAsync(CancellationToken cancellationToken = default)
{
await this.SkipAsync(cancellationToken).ConfigureAwait(false);
_completed = true;
}
protected override void Dispose(bool disposing)
{
if (!(_completed || _reader.Cancelled))
@@ -83,6 +94,40 @@ public class EntryStream : Stream, IStreamStack
_stream.Dispose();
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask DisposeAsync()
{
if (!(_completed || _reader.Cancelled))
{
await SkipEntryAsync().ConfigureAwait(false);
}
//Need a safe standard approach to this - it's okay for compression to overreads. Handling needs to be standardised
if (_stream is IStreamStack ss)
{
if (ss.BaseStream() is SharpCompress.Compressors.Deflate.DeflateStream deflateStream)
{
await deflateStream.FlushAsync().ConfigureAwait(false);
}
else if (ss.BaseStream() is SharpCompress.Compressors.LZMA.LzmaStream lzmaStream)
{
await lzmaStream.FlushAsync().ConfigureAwait(false);
}
}
if (_isDisposed)
{
return;
}
_isDisposed = true;
#if DEBUG_STREAMS
this.DebugDispose(typeof(EntryStream));
#endif
await base.DisposeAsync().ConfigureAwait(false);
await _stream.DisposeAsync().ConfigureAwait(false);
}
#endif
public override bool CanRead => true;
public override bool CanSeek => false;
@@ -91,6 +136,8 @@ public class EntryStream : Stream, IStreamStack
public override void Flush() { }
public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public override long Length => _stream.Length;
public override long Position
@@ -109,6 +156,38 @@ public class EntryStream : Stream, IStreamStack
return read;
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
var read = await _stream
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
if (read <= 0)
{
_completed = true;
}
return read;
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
var read = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
if (read <= 0)
{
_completed = true;
}
return read;
}
#endif
public override int ReadByte()
{
var value = _stream.ReadByte();

View File

@@ -1,10 +1,22 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Common;
internal static class ExtractionMethods
{
/// <summary>
/// Gets the appropriate StringComparison for path checks based on the file system.
/// Windows uses case-insensitive file systems, while Unix-like systems use case-sensitive file systems.
/// </summary>
private static StringComparison PathComparison =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
/// <summary>
/// Extract to specific directory, retaining filename
/// </summary>
@@ -46,7 +58,7 @@ internal static class ExtractionMethods
if (!Directory.Exists(destdir))
{
if (!destdir.StartsWith(fullDestinationDirectoryPath, StringComparison.Ordinal))
if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison))
{
throw new ExtractionException(
"Entry is trying to create a directory outside of the destination directory."
@@ -66,12 +78,7 @@ internal static class ExtractionMethods
{
destinationFileName = Path.GetFullPath(destinationFileName);
if (
!destinationFileName.StartsWith(
fullDestinationDirectoryPath,
StringComparison.Ordinal
)
)
if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison))
{
throw new ExtractionException(
"Entry is trying to write a file outside of the destination directory."
@@ -116,4 +123,110 @@ internal static class ExtractionMethods
entry.PreserveExtractionOptions(destinationFileName, options);
}
}
public static async Task WriteEntryToDirectoryAsync(
IEntry entry,
string destinationDirectory,
ExtractionOptions? options,
Func<string, ExtractionOptions?, Task> writeAsync,
CancellationToken cancellationToken = default
)
{
string destinationFileName;
var fullDestinationDirectoryPath = Path.GetFullPath(destinationDirectory);
//check for trailing slash.
if (
fullDestinationDirectoryPath[fullDestinationDirectoryPath.Length - 1]
!= Path.DirectorySeparatorChar
)
{
fullDestinationDirectoryPath += Path.DirectorySeparatorChar;
}
if (!Directory.Exists(fullDestinationDirectoryPath))
{
throw new ExtractionException(
$"Directory does not exist to extract to: {fullDestinationDirectoryPath}"
);
}
options ??= new ExtractionOptions() { Overwrite = true };
var file = Path.GetFileName(entry.Key.NotNull("Entry Key is null")).NotNull("File is null");
file = Utility.ReplaceInvalidFileNameChars(file);
if (options.ExtractFullPath)
{
var folder = Path.GetDirectoryName(entry.Key.NotNull("Entry Key is null"))
.NotNull("Directory is null");
var destdir = Path.GetFullPath(Path.Combine(fullDestinationDirectoryPath, folder));
if (!Directory.Exists(destdir))
{
if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison))
{
throw new ExtractionException(
"Entry is trying to create a directory outside of the destination directory."
);
}
Directory.CreateDirectory(destdir);
}
destinationFileName = Path.Combine(destdir, file);
}
else
{
destinationFileName = Path.Combine(fullDestinationDirectoryPath, file);
}
if (!entry.IsDirectory)
{
destinationFileName = Path.GetFullPath(destinationFileName);
if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison))
{
throw new ExtractionException(
"Entry is trying to write a file outside of the destination directory."
);
}
await writeAsync(destinationFileName, options).ConfigureAwait(false);
}
else if (options.ExtractFullPath && !Directory.Exists(destinationFileName))
{
Directory.CreateDirectory(destinationFileName);
}
}
public static async Task WriteEntryToFileAsync(
IEntry entry,
string destinationFileName,
ExtractionOptions? options,
Func<string, FileMode, Task> openAndWriteAsync,
CancellationToken cancellationToken = default
)
{
if (entry.LinkTarget != null)
{
if (options?.WriteSymbolicLink is null)
{
throw new ExtractionException(
"Entry is a symbolic link but ExtractionOptions.WriteSymbolicLink delegate is null"
);
}
options.WriteSymbolicLink(destinationFileName, entry.LinkTarget);
}
else
{
var fm = FileMode.Create;
options ??= new ExtractionOptions() { Overwrite = true };
if (!options.Overwrite)
{
fm = FileMode.CreateNew;
}
await openAndWriteAsync(destinationFileName, fm).ConfigureAwait(false);
entry.PreserveExtractionOptions(destinationFileName, options);
}
}
}

View File

@@ -24,8 +24,14 @@ internal sealed class GZipFilePart : FilePart
stream.Position = stream.Length - 8;
ReadTrailer();
stream.Position = position;
EntryStartPosition = position;
}
else
{
// For non-seekable streams, we can't read the trailer or track position.
// Set to 0 since the stream will be read sequentially from its current position.
EntryStartPosition = 0;
}
EntryStartPosition = stream.Position;
}
internal long EntryStartPosition { get; }

View File

@@ -67,6 +67,7 @@ internal class SevenZipFilePart : FilePart
}
}
private const uint K_COPY = 0x0;
private const uint K_LZMA2 = 0x21;
private const uint K_LZMA = 0x030101;
private const uint K_PPMD = 0x030401;
@@ -82,6 +83,7 @@ internal class SevenZipFilePart : FilePart
var coder = Folder.NotNull()._coders.First();
return coder._methodId._id switch
{
K_COPY => CompressionType.None,
K_LZMA or K_LZMA2 => CompressionType.LZMA,
K_PPMD => CompressionType.PPMd,
K_B_ZIP2 => CompressionType.BZip2,

View File

@@ -66,6 +66,36 @@ internal class TarReadOnlySubStream : SharpCompressStream, IStreamStack
base.Dispose(disposing);
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async System.Threading.Tasks.ValueTask DisposeAsync()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
#if DEBUG_STREAMS
this.DebugDispose(typeof(TarReadOnlySubStream));
#endif
// Ensure we read all remaining blocks for this entry.
await Stream.SkipAsync(BytesLeftToRead).ConfigureAwait(false);
_amountRead += BytesLeftToRead;
// If the last block wasn't a full 512 bytes, skip the remaining padding bytes.
var bytesInLastBlock = _amountRead % 512;
if (bytesInLastBlock != 0)
{
await Stream.SkipAsync(512 - bytesInLastBlock).ConfigureAwait(false);
}
// Call base Dispose instead of base DisposeAsync to avoid double disposal
base.Dispose(true);
GC.SuppressFinalize(this);
}
#endif
private long BytesLeftToRead { get; set; }
public override bool CanRead => true;
@@ -76,6 +106,10 @@ internal class TarReadOnlySubStream : SharpCompressStream, IStreamStack
public override void Flush() { }
public override System.Threading.Tasks.Task FlushAsync(
System.Threading.CancellationToken cancellationToken
) => System.Threading.Tasks.Task.CompletedTask;
public override long Length => throw new NotSupportedException();
public override long Position
@@ -114,6 +148,48 @@ internal class TarReadOnlySubStream : SharpCompressStream, IStreamStack
return value;
}
public override async System.Threading.Tasks.Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
System.Threading.CancellationToken cancellationToken
)
{
if (BytesLeftToRead < count)
{
count = (int)BytesLeftToRead;
}
var read = await Stream
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
if (read > 0)
{
BytesLeftToRead -= read;
_amountRead += read;
}
return read;
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async System.Threading.Tasks.ValueTask<int> ReadAsync(
System.Memory<byte> buffer,
System.Threading.CancellationToken cancellationToken = default
)
{
if (BytesLeftToRead < buffer.Length)
{
buffer = buffer.Slice(0, (int)BytesLeftToRead);
}
var read = await Stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
if (read > 0)
{
BytesLeftToRead -= read;
_amountRead += read;
}
return read;
}
#endif
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();

View File

@@ -91,8 +91,15 @@ internal abstract class ZipFileEntry : ZipHeader
protected void LoadExtra(byte[] extra)
{
for (var i = 0; i < extra.Length - 4; )
for (var i = 0; i < extra.Length; )
{
// Ensure we have at least a header (2-byte ID + 2-byte length)
if (i + 4 > extra.Length)
{
// Incomplete header — stop parsing extras
break;
}
var type = (ExtraDataType)BinaryPrimitives.ReadUInt16LittleEndian(extra.AsSpan(i));
if (!Enum.IsDefined(typeof(ExtraDataType), type))
{
@@ -106,7 +113,17 @@ internal abstract class ZipFileEntry : ZipHeader
if (length > extra.Length)
{
// bad extras block
return;
break; // allow processing optional other blocks
}
// Some ZIP files contain vendor-specific or malformed extra fields where the declared
// data length extends beyond the remaining buffer. This adjustment ensures that
// we only read data within bounds (i + 4 + length <= extra.Length)
// The example here is: 41 43 18 00 41 52 43 30 46 EB FF FF 51 29 03 C6 03 00 00 00 00 00 00 00 00
// No existing zip utility uses 0x4341 ('AC')
if (i + 4 + length > extra.Length)
{
// incomplete or corrupt field
break; // allow processing optional other blocks
}
var data = new byte[length];

View File

@@ -36,11 +36,10 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory
throw new ArgumentException("Stream must be a SharpCompressStream", nameof(stream));
}
}
SharpCompressStream rewindableStream = (SharpCompressStream)stream;
var rewindableStream = (SharpCompressStream)stream;
while (true)
{
ZipHeader? header;
var reader = new BinaryReader(rewindableStream);
uint headerBytes = 0;
if (
@@ -155,7 +154,7 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory
}
_lastEntryHeader = null;
header = ReadHeader(headerBytes, reader);
var header = ReadHeader(headerBytes, reader);
if (header is null)
{
yield break;

View File

@@ -28,6 +28,7 @@ using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
namespace SharpCompress.Compressors.Deflate;
@@ -289,6 +290,34 @@ public class DeflateStream : Stream, IStreamStack
_baseStream.Flush();
}
public override async Task FlushAsync(CancellationToken cancellationToken)
{
if (_disposed)
{
throw new ObjectDisposedException("DeflateStream");
}
await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
_disposed = true;
if (_baseStream != null)
{
await _baseStream.DisposeAsync().ConfigureAwait(false);
}
#if DEBUG_STREAMS
this.DebugDispose(typeof(DeflateStream));
#endif
await base.DisposeAsync().ConfigureAwait(false);
}
#endif
/// <summary>
/// Read data from the stream.
/// </summary>
@@ -325,6 +354,36 @@ public class DeflateStream : Stream, IStreamStack
return _baseStream.Read(buffer, offset, count);
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (_disposed)
{
throw new ObjectDisposedException("DeflateStream");
}
return await _baseStream
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (_disposed)
{
throw new ObjectDisposedException("DeflateStream");
}
return await _baseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
}
#endif
public override int ReadByte()
{
if (_disposed)
@@ -386,6 +445,36 @@ public class DeflateStream : Stream, IStreamStack
_baseStream.Write(buffer, offset, count);
}
public override async Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (_disposed)
{
throw new ObjectDisposedException("DeflateStream");
}
await _baseStream
.WriteAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (_disposed)
{
throw new ObjectDisposedException("DeflateStream");
}
await _baseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
}
#endif
public override void WriteByte(byte value)
{
if (_disposed)

View File

@@ -30,6 +30,8 @@ using System;
using System.Buffers.Binary;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
namespace SharpCompress.Compressors.Deflate;
@@ -257,6 +259,15 @@ public class GZipStream : Stream, IStreamStack
BaseStream.Flush();
}
public override async Task FlushAsync(CancellationToken cancellationToken)
{
if (_disposed)
{
throw new ObjectDisposedException("GZipStream");
}
await BaseStream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Read and decompress data from the source stream.
/// </summary>
@@ -309,6 +320,54 @@ public class GZipStream : Stream, IStreamStack
return n;
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (_disposed)
{
throw new ObjectDisposedException("GZipStream");
}
var n = await BaseStream
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
if (!_firstReadDone)
{
_firstReadDone = true;
FileName = BaseStream._GzipFileName;
Comment = BaseStream._GzipComment;
LastModified = BaseStream._GzipMtime;
}
return n;
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (_disposed)
{
throw new ObjectDisposedException("GZipStream");
}
var n = await BaseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
if (!_firstReadDone)
{
_firstReadDone = true;
FileName = BaseStream._GzipFileName;
Comment = BaseStream._GzipComment;
LastModified = BaseStream._GzipMtime;
}
return n;
}
#endif
/// <summary>
/// Calling this method always throws a <see cref="NotImplementedException"/>.
/// </summary>
@@ -368,6 +427,77 @@ public class GZipStream : Stream, IStreamStack
BaseStream.Write(buffer, offset, count);
}
public override async Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (_disposed)
{
throw new ObjectDisposedException("GZipStream");
}
if (BaseStream._streamMode == ZlibBaseStream.StreamMode.Undefined)
{
if (BaseStream._wantCompress)
{
// first write in compression, therefore, emit the GZIP header
_headerByteCount = EmitHeader();
}
else
{
throw new InvalidOperationException();
}
}
await BaseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (_disposed)
{
throw new ObjectDisposedException("GZipStream");
}
if (BaseStream._streamMode == ZlibBaseStream.StreamMode.Undefined)
{
if (BaseStream._wantCompress)
{
// first write in compression, therefore, emit the GZIP header
_headerByteCount = EmitHeader();
}
else
{
throw new InvalidOperationException();
}
}
await BaseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
}
public override async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
_disposed = true;
if (BaseStream != null)
{
await BaseStream.DisposeAsync().ConfigureAwait(false);
}
#if DEBUG_STREAMS
this.DebugDispose(typeof(GZipStream));
#endif
await base.DisposeAsync().ConfigureAwait(false);
}
#endif
#endregion Stream methods
public string? Comment

View File

@@ -31,6 +31,8 @@ using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Tar.Headers;
using SharpCompress.IO;
@@ -197,6 +199,69 @@ internal class ZlibBaseStream : Stream, IStreamStack
} while (!done);
}
public override async Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
// workitem 7159
// calculate the CRC on the unccompressed data (before writing)
if (crc != null)
{
crc.SlurpBlock(buffer, offset, count);
}
if (_streamMode == StreamMode.Undefined)
{
_streamMode = StreamMode.Writer;
}
else if (_streamMode != StreamMode.Writer)
{
throw new ZlibException("Cannot Write after Reading.");
}
if (count == 0)
{
return;
}
// first reference of z property will initialize the private var _z
z.InputBuffer = buffer;
_z.NextIn = offset;
_z.AvailableBytesIn = count;
var done = false;
do
{
_z.OutputBuffer = workingBuffer;
_z.NextOut = 0;
_z.AvailableBytesOut = _workingBuffer.Length;
var rc = (_wantCompress) ? _z.Deflate(_flushMode) : _z.Inflate(_flushMode);
if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END)
{
throw new ZlibException((_wantCompress ? "de" : "in") + "flating: " + _z.Message);
}
await _stream
.WriteAsync(
_workingBuffer,
0,
_workingBuffer.Length - _z.AvailableBytesOut,
cancellationToken
)
.ConfigureAwait(false);
done = _z.AvailableBytesIn == 0 && _z.AvailableBytesOut != 0;
// If GZIP and de-compress, we're done when 8 bytes remain.
if (_flavor == ZlibStreamFlavor.GZIP && !_wantCompress)
{
done = (_z.AvailableBytesIn == 8 && _z.AvailableBytesOut != 0);
}
} while (!done);
}
private void finish()
{
if (_z is null)
@@ -335,6 +400,111 @@ internal class ZlibBaseStream : Stream, IStreamStack
}
}
private async Task finishAsync(CancellationToken cancellationToken = default)
{
if (_z is null)
{
return;
}
if (_streamMode == StreamMode.Writer)
{
var done = false;
do
{
_z.OutputBuffer = workingBuffer;
_z.NextOut = 0;
_z.AvailableBytesOut = _workingBuffer.Length;
var rc =
(_wantCompress) ? _z.Deflate(FlushType.Finish) : _z.Inflate(FlushType.Finish);
if (rc != ZlibConstants.Z_STREAM_END && rc != ZlibConstants.Z_OK)
{
var verb = (_wantCompress ? "de" : "in") + "flating";
if (_z.Message is null)
{
throw new ZlibException(String.Format("{0}: (rc = {1})", verb, rc));
}
throw new ZlibException(verb + ": " + _z.Message);
}
if (_workingBuffer.Length - _z.AvailableBytesOut > 0)
{
await _stream
.WriteAsync(
_workingBuffer,
0,
_workingBuffer.Length - _z.AvailableBytesOut,
cancellationToken
)
.ConfigureAwait(false);
}
done = _z.AvailableBytesIn == 0 && _z.AvailableBytesOut != 0;
// If GZIP and de-compress, we're done when 8 bytes remain.
if (_flavor == ZlibStreamFlavor.GZIP && !_wantCompress)
{
done = (_z.AvailableBytesIn == 8 && _z.AvailableBytesOut != 0);
}
} while (!done);
await FlushAsync(cancellationToken).ConfigureAwait(false);
// workitem 7159
if (_flavor == ZlibStreamFlavor.GZIP)
{
if (_wantCompress)
{
// Emit the GZIP trailer: CRC32 and size mod 2^32
byte[] intBuf = new byte[4];
BinaryPrimitives.WriteInt32LittleEndian(intBuf, crc.Crc32Result);
await _stream.WriteAsync(intBuf, 0, 4, cancellationToken).ConfigureAwait(false);
var c2 = (int)(crc.TotalBytesRead & 0x00000000FFFFFFFF);
BinaryPrimitives.WriteInt32LittleEndian(intBuf, c2);
await _stream.WriteAsync(intBuf, 0, 4, cancellationToken).ConfigureAwait(false);
}
else
{
throw new ZlibException("Writing with decompression is not supported.");
}
}
}
// workitem 7159
else if (_streamMode == StreamMode.Reader)
{
if (_flavor == ZlibStreamFlavor.GZIP)
{
if (!_wantCompress)
{
// workitem 8501: handle edge case (decompress empty stream)
if (_z.TotalBytesOut == 0L)
{
return;
}
// Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32
byte[] trailer = new byte[8];
// workitem 8679
if (_z.AvailableBytesIn != 8)
{
// Make sure we have read to the end of the stream
_z.InputBuffer.AsSpan(_z.NextIn, _z.AvailableBytesIn).CopyTo(trailer);
var bytesNeeded = 8 - _z.AvailableBytesIn;
var bytesRead = await _stream
.ReadAsync(trailer, _z.AvailableBytesIn, bytesNeeded, cancellationToken)
.ConfigureAwait(false);
}
}
else
{
throw new ZlibException("Reading with compression is not supported.");
}
}
}
}
private void end()
{
if (z is null)
@@ -382,6 +552,38 @@ internal class ZlibBaseStream : Stream, IStreamStack
}
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask DisposeAsync()
{
if (isDisposed)
{
return;
}
isDisposed = true;
#if DEBUG_STREAMS
this.DebugDispose(typeof(ZlibBaseStream));
#endif
await base.DisposeAsync().ConfigureAwait(false);
if (_stream is null)
{
return;
}
try
{
await finishAsync().ConfigureAwait(false);
}
finally
{
end();
if (_stream != null)
{
await _stream.DisposeAsync().ConfigureAwait(false);
_stream = null;
}
}
}
#endif
public override void Flush()
{
_stream.Flush();
@@ -390,6 +592,14 @@ internal class ZlibBaseStream : Stream, IStreamStack
z.AvailableBytesIn = 0;
}
public override async Task FlushAsync(CancellationToken cancellationToken)
{
await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
//rewind the buffer
((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused
z.AvailableBytesIn = 0;
}
public override Int64 Seek(Int64 offset, SeekOrigin origin) =>
throw new NotSupportedException();
@@ -436,6 +646,31 @@ internal class ZlibBaseStream : Stream, IStreamStack
return _encoding.GetString(buffer, 0, buffer.Length);
}
private async Task<string> ReadZeroTerminatedStringAsync(CancellationToken cancellationToken)
{
var list = new List<byte>();
var done = false;
do
{
// workitem 7740
var n = await _stream.ReadAsync(_buf1, 0, 1, cancellationToken).ConfigureAwait(false);
if (n != 1)
{
throw new ZlibException("Unexpected EOF reading GZIP header.");
}
if (_buf1[0] == 0)
{
done = true;
}
else
{
list.Add(_buf1[0]);
}
} while (!done);
var buffer = list.ToArray();
return _encoding.GetString(buffer, 0, buffer.Length);
}
private int _ReadAndValidateGzipHeader()
{
var totalBytesRead = 0;
@@ -494,6 +729,68 @@ internal class ZlibBaseStream : Stream, IStreamStack
return totalBytesRead;
}
private async Task<int> _ReadAndValidateGzipHeaderAsync(CancellationToken cancellationToken)
{
var totalBytesRead = 0;
// read the header on the first read
byte[] header = new byte[10];
var n = await _stream.ReadAsync(header, 0, 10, cancellationToken).ConfigureAwait(false);
// workitem 8501: handle edge case (decompress empty stream)
if (n == 0)
{
return 0;
}
if (n != 10)
{
throw new ZlibException("Not a valid GZIP stream.");
}
if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
{
throw new ZlibException("Bad GZIP header.");
}
var timet = BinaryPrimitives.ReadInt32LittleEndian(header.AsSpan(4));
_GzipMtime = TarHeader.EPOCH.AddSeconds(timet);
totalBytesRead += n;
if ((header[3] & 0x04) == 0x04)
{
// read and discard extra field
n = await _stream.ReadAsync(header, 0, 2, cancellationToken).ConfigureAwait(false); // 2-byte length field
totalBytesRead += n;
var extraLength = (short)(header[0] + header[1] * 256);
var extra = new byte[extraLength];
n = await _stream
.ReadAsync(extra, 0, extra.Length, cancellationToken)
.ConfigureAwait(false);
if (n != extraLength)
{
throw new ZlibException("Unexpected end-of-file reading GZIP header.");
}
totalBytesRead += n;
}
if ((header[3] & 0x08) == 0x08)
{
_GzipFileName = await ReadZeroTerminatedStringAsync(cancellationToken)
.ConfigureAwait(false);
}
if ((header[3] & 0x10) == 0x010)
{
_GzipComment = await ReadZeroTerminatedStringAsync(cancellationToken)
.ConfigureAwait(false);
}
if ((header[3] & 0x02) == 0x02)
{
await _stream.ReadAsync(_buf1, 0, 1, cancellationToken).ConfigureAwait(false); // CRC16, ignore
}
return totalBytesRead;
}
public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count)
{
// According to MS documentation, any implementation of the IO.Stream.Read function must:
@@ -678,6 +975,220 @@ internal class ZlibBaseStream : Stream, IStreamStack
return rc;
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
// According to MS documentation, any implementation of the IO.Stream.Read function must:
// (a) throw an exception if offset & count reference an invalid part of the buffer,
// or if count < 0, or if buffer is null
// (b) return 0 only upon EOF, or if count = 0
// (c) if not EOF, then return at least 1 byte, up to <count> bytes
if (_streamMode == StreamMode.Undefined)
{
if (!_stream.CanRead)
{
throw new ZlibException("The stream is not readable.");
}
// for the first read, set up some controls.
_streamMode = StreamMode.Reader;
// (The first reference to _z goes through the private accessor which
// may initialize it.)
z.AvailableBytesIn = 0;
if (_flavor == ZlibStreamFlavor.GZIP)
{
_gzipHeaderByteCount = await _ReadAndValidateGzipHeaderAsync(cancellationToken)
.ConfigureAwait(false);
// workitem 8501: handle edge case (decompress empty stream)
if (_gzipHeaderByteCount == 0)
{
return 0;
}
}
}
if (_streamMode != StreamMode.Reader)
{
throw new ZlibException("Cannot Read after Writing.");
}
var rc = 0;
// set up the output of the deflate/inflate codec:
_z.OutputBuffer = buffer;
_z.NextOut = offset;
_z.AvailableBytesOut = count;
if (count == 0)
{
return 0;
}
if (nomoreinput && _wantCompress)
{
// no more input data available; therefore we flush to
// try to complete the read
rc = _z.Deflate(FlushType.Finish);
if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END)
{
throw new ZlibException(
String.Format("Deflating: rc={0} msg={1}", rc, _z.Message)
);
}
rc = (count - _z.AvailableBytesOut);
// calculate CRC after reading
if (crc != null)
{
crc.SlurpBlock(buffer, offset, rc);
}
return rc;
}
if (buffer is null)
{
throw new ArgumentNullException(nameof(buffer));
}
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
if (offset < buffer.GetLowerBound(0))
{
throw new ArgumentOutOfRangeException(nameof(offset));
}
if ((offset + count) > buffer.GetLength(0))
{
throw new ArgumentOutOfRangeException(nameof(count));
}
// This is necessary in case _workingBuffer has been resized. (new byte[])
// (The first reference to _workingBuffer goes through the private accessor which
// may initialize it.)
_z.InputBuffer = workingBuffer;
do
{
// need data in _workingBuffer in order to deflate/inflate. Here, we check if we have any.
if ((_z.AvailableBytesIn == 0) && (!nomoreinput))
{
// No data available, so try to Read data from the captive stream.
_z.NextIn = 0;
_z.AvailableBytesIn = await _stream
.ReadAsync(_workingBuffer, 0, _workingBuffer.Length, cancellationToken)
.ConfigureAwait(false);
if (_z.AvailableBytesIn == 0)
{
nomoreinput = true;
}
}
// we have data in InputBuffer; now compress or decompress as appropriate
rc = (_wantCompress) ? _z.Deflate(_flushMode) : _z.Inflate(_flushMode);
if (nomoreinput && (rc == ZlibConstants.Z_BUF_ERROR))
{
return 0;
}
if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END)
{
throw new ZlibException(
String.Format(
"{0}flating: rc={1} msg={2}",
(_wantCompress ? "de" : "in"),
rc,
_z.Message
)
);
}
if (
(nomoreinput || rc == ZlibConstants.Z_STREAM_END) && (_z.AvailableBytesOut == count)
)
{
break; // nothing more to read
}
} //while (_z.AvailableBytesOut == count && rc == ZlibConstants.Z_OK);
while (_z.AvailableBytesOut > 0 && !nomoreinput && rc == ZlibConstants.Z_OK);
// workitem 8557
// is there more room in output?
if (_z.AvailableBytesOut > 0)
{
if (rc == ZlibConstants.Z_OK && _z.AvailableBytesIn == 0)
{
// deferred
}
// are we completely done reading?
if (nomoreinput)
{
// and in compression?
if (_wantCompress)
{
// no more input data available; therefore we flush to
// try to complete the read
rc = _z.Deflate(FlushType.Finish);
if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END)
{
throw new ZlibException(
String.Format("Deflating: rc={0} msg={1}", rc, _z.Message)
);
}
}
}
}
rc = (count - _z.AvailableBytesOut);
// calculate CRC after reading
if (crc != null)
{
crc.SlurpBlock(buffer, offset, rc);
}
if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress)
{
//rewind the buffer
((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused
z.AvailableBytesIn = 0;
}
return rc;
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
// Use ArrayPool to rent a buffer and delegate to byte[] ReadAsync
byte[] array = System.Buffers.ArrayPool<byte>.Shared.Rent(buffer.Length);
try
{
int read = await ReadAsync(array, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false);
array.AsSpan(0, read).CopyTo(buffer.Span);
return read;
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(array);
}
}
#endif
public override Boolean CanRead => _stream.CanRead;
public override Boolean CanSeek => _stream.CanSeek;

View File

@@ -28,6 +28,8 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
namespace SharpCompress.Compressors.Deflate;
@@ -266,6 +268,34 @@ public class ZlibStream : Stream, IStreamStack
_baseStream.Flush();
}
public override async Task FlushAsync(CancellationToken cancellationToken)
{
if (_disposed)
{
throw new ObjectDisposedException("ZlibStream");
}
await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
_disposed = true;
if (_baseStream != null)
{
await _baseStream.DisposeAsync().ConfigureAwait(false);
}
#if DEBUG_STREAMS
this.DebugDispose(typeof(ZlibStream));
#endif
await base.DisposeAsync().ConfigureAwait(false);
}
#endif
/// <summary>
/// Read data from the stream.
/// </summary>
@@ -301,6 +331,36 @@ public class ZlibStream : Stream, IStreamStack
return _baseStream.Read(buffer, offset, count);
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (_disposed)
{
throw new ObjectDisposedException("ZlibStream");
}
return await _baseStream
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (_disposed)
{
throw new ObjectDisposedException("ZlibStream");
}
return await _baseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
}
#endif
public override int ReadByte()
{
if (_disposed)
@@ -355,6 +415,36 @@ public class ZlibStream : Stream, IStreamStack
_baseStream.Write(buffer, offset, count);
}
public override async Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (_disposed)
{
throw new ObjectDisposedException("ZlibStream");
}
await _baseStream
.WriteAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (_disposed)
{
throw new ObjectDisposedException("ZlibStream");
}
await _baseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
}
#endif
public override void WriteByte(byte value)
{
if (_disposed)

View File

@@ -2,12 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Zip;
using SharpCompress.IO;
@@ -39,7 +39,6 @@ public sealed class Deflate64Stream : Stream, IStreamStack
private const int DEFAULT_BUFFER_SIZE = 8192;
private Stream _stream;
private CompressionMode _mode;
private InflaterManaged _inflater;
private byte[] _buffer;
@@ -62,61 +61,23 @@ public sealed class Deflate64Stream : Stream, IStreamStack
throw new ArgumentException("Deflate64: input stream is not readable", nameof(stream));
}
InitializeInflater(stream, ZipCompressionMethod.Deflate64);
#if DEBUG_STREAMS
this.DebugConstruct(typeof(Deflate64Stream));
#endif
}
/// <summary>
/// Sets up this DeflateManagedStream to be used for Inflation/Decompression
/// </summary>
private void InitializeInflater(
Stream stream,
ZipCompressionMethod method = ZipCompressionMethod.Deflate
)
{
Debug.Assert(stream != null);
Debug.Assert(
method == ZipCompressionMethod.Deflate || method == ZipCompressionMethod.Deflate64
);
if (!stream.CanRead)
{
throw new ArgumentException("Deflate64: input stream is not readable", nameof(stream));
}
_inflater = new InflaterManaged(method == ZipCompressionMethod.Deflate64);
_inflater = new InflaterManaged(true);
_stream = stream;
_mode = CompressionMode.Decompress;
_buffer = new byte[DEFAULT_BUFFER_SIZE];
#if DEBUG_STREAMS
this.DebugConstruct(typeof(Deflate64Stream));
#endif
}
public override bool CanRead
{
get
{
if (_stream is null)
{
return false;
}
public override bool CanRead => _stream.CanRead;
return (_mode == CompressionMode.Decompress && _stream.CanRead);
}
}
public override bool CanWrite
{
get
{
if (_stream is null)
{
return false;
}
return (_mode == CompressionMode.Compress && _stream.CanWrite);
}
}
public override bool CanWrite => false;
public override bool CanSeek => false;
@@ -138,7 +99,6 @@ public sealed class Deflate64Stream : Stream, IStreamStack
public override int Read(byte[] array, int offset, int count)
{
EnsureDecompressionMode();
ValidateParameters(array, offset, count);
EnsureNotDisposed();
@@ -185,6 +145,106 @@ public sealed class Deflate64Stream : Stream, IStreamStack
return count - remainingCount;
}
public override async Task<int> ReadAsync(
byte[] array,
int offset,
int count,
CancellationToken cancellationToken
)
{
ValidateParameters(array, offset, count);
EnsureNotDisposed();
int bytesRead;
var currentOffset = offset;
var remainingCount = count;
while (true)
{
bytesRead = _inflater.Inflate(array, currentOffset, remainingCount);
currentOffset += bytesRead;
remainingCount -= bytesRead;
if (remainingCount == 0)
{
break;
}
if (_inflater.Finished())
{
// if we finished decompressing, we can't have anything left in the outputwindow.
Debug.Assert(
_inflater.AvailableOutput == 0,
"We should have copied all stuff out!"
);
break;
}
var bytes = await _stream
.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken)
.ConfigureAwait(false);
if (bytes <= 0)
{
break;
}
else if (bytes > _buffer.Length)
{
// The stream is either malicious or poorly implemented and returned a number of
// bytes larger than the buffer supplied to it.
throw new InvalidFormatException("Deflate64: invalid data");
}
_inflater.SetInput(_buffer, 0, bytes);
}
return count - remainingCount;
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
EnsureNotDisposed();
// InflaterManaged doesn't have a Span-based Inflate method, so we need to work with arrays
// For large buffers, we could rent from ArrayPool, but for simplicity we'll use the buffer's array if available
if (
System.Runtime.InteropServices.MemoryMarshal.TryGetArray<byte>(
buffer,
out var arraySegment
)
)
{
// Fast path: the Memory<byte> is backed by an array
return await ReadAsync(
arraySegment.Array!,
arraySegment.Offset,
arraySegment.Count,
cancellationToken
)
.ConfigureAwait(false);
}
else
{
// Slow path: rent a temporary array
var tempBuffer = System.Buffers.ArrayPool<byte>.Shared.Rent(buffer.Length);
try
{
var bytesRead = await ReadAsync(tempBuffer, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false);
tempBuffer.AsMemory(0, bytesRead).CopyTo(buffer);
return bytesRead;
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(tempBuffer);
}
}
}
#endif
private void ValidateParameters(byte[] array, int offset, int count)
{
if (array is null)
@@ -220,26 +280,6 @@ public sealed class Deflate64Stream : Stream, IStreamStack
private static void ThrowStreamClosedException() =>
throw new ObjectDisposedException(null, "Deflate64: stream has been disposed");
private void EnsureDecompressionMode()
{
if (_mode != CompressionMode.Decompress)
{
ThrowCannotReadFromDeflateManagedStreamException();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowCannotReadFromDeflateManagedStreamException() =>
throw new InvalidOperationException("Deflate64: cannot read from this stream");
private void EnsureCompressionMode()
{
if (_mode != CompressionMode.Compress)
{
ThrowCannotWriteToDeflateManagedStreamException();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowCannotWriteToDeflateManagedStreamException() =>
throw new InvalidOperationException("Deflate64: cannot write to this stream");
@@ -281,20 +321,17 @@ public sealed class Deflate64Stream : Stream, IStreamStack
#endif
if (disposing)
{
_stream?.Dispose();
_stream.Dispose();
}
}
finally
{
_stream = null;
try
{
_inflater?.Dispose();
_inflater.Dispose();
}
finally
{
_inflater = null;
base.Dispose(disposing);
}
}

View File

@@ -1,12 +1,13 @@
using System;
using System.IO;
using SharpCompress.Common;
namespace SharpCompress.Compressors.LZMA;
/// <summary>
/// The exception that is thrown when an error in input stream occurs during decoding.
/// </summary>
internal class DataErrorException : Exception
internal class DataErrorException : SharpCompressException
{
public DataErrorException()
: base("Data Error") { }
@@ -15,7 +16,7 @@ internal class DataErrorException : Exception
/// <summary>
/// The exception that is thrown when the value of an argument is outside the allowable range.
/// </summary>
internal class InvalidParamException : Exception
internal class InvalidParamException : SharpCompressException
{
public InvalidParamException()
: base("Invalid Parameter") { }

View File

@@ -1,11 +1,12 @@
#nullable disable
using System;
using System.Buffers;
using System.IO;
namespace SharpCompress.Compressors.LZMA.LZ;
internal class OutWindow
internal class OutWindow : IDisposable
{
private byte[] _buffer;
private int _windowSize;
@@ -15,19 +16,22 @@ internal class OutWindow
private int _pendingDist;
private Stream _stream;
public long _total;
public long _limit;
private long _total;
private long _limit;
public long Total => _total;
public void Create(int windowSize)
{
if (_windowSize != windowSize)
{
_buffer = new byte[windowSize];
}
else
{
_buffer[windowSize - 1] = 0;
if (_buffer is not null)
{
ArrayPool<byte>.Shared.Return(_buffer);
}
_buffer = ArrayPool<byte>.Shared.Rent(windowSize);
}
_buffer[windowSize - 1] = 0;
_windowSize = windowSize;
_pos = 0;
_streamPos = 0;
@@ -36,7 +40,22 @@ internal class OutWindow
_limit = 0;
}
public void Reset() => Create(_windowSize);
public void Dispose()
{
ReleaseStream();
if (_buffer is null)
{
return;
}
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = null;
}
public void Reset()
{
ReleaseStream();
Create(_windowSize);
}
public void Init(Stream stream)
{
@@ -66,7 +85,7 @@ internal class OutWindow
_stream = null;
}
public void Flush()
private void Flush()
{
if (_stream is null)
{

View File

@@ -294,7 +294,7 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream
}
else
{
_outWindow.SetLimit(long.MaxValue - _outWindow._total);
_outWindow.SetLimit(long.MaxValue - _outWindow.Total);
}
var rangeDecoder = new RangeCoder.Decoder();
@@ -305,6 +305,7 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream
_outWindow.ReleaseStream();
rangeDecoder.ReleaseStream();
_outWindow.Dispose();
_outWindow = null;
}
@@ -316,7 +317,7 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream
while (outWindow.HasSpace)
{
var posState = (uint)outWindow._total & _posStateMask;
var posState = (uint)outWindow.Total & _posStateMask;
if (
_isMatchDecoders[(_state._index << Base.K_NUM_POS_STATES_BITS_MAX) + posState]
.Decode(rangeDecoder) == 0
@@ -328,18 +329,14 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream
{
b = _literalDecoder.DecodeWithMatchByte(
rangeDecoder,
(uint)outWindow._total,
(uint)outWindow.Total,
prevByte,
outWindow.GetByte((int)_rep0)
);
}
else
{
b = _literalDecoder.DecodeNormal(
rangeDecoder,
(uint)outWindow._total,
prevByte
);
b = _literalDecoder.DecodeNormal(rangeDecoder, (uint)outWindow.Total, prevByte);
}
outWindow.PutByte(b);
_state.UpdateChar();
@@ -424,7 +421,7 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream
_rep0 = posSlot;
}
}
if (_rep0 >= outWindow._total || _rep0 >= dictionarySizeCheck)
if (_rep0 >= outWindow.Total || _rep0 >= dictionarySizeCheck)
{
if (_rep0 == 0xFFFFFFFF)
{

View File

@@ -178,6 +178,7 @@ public class LzmaStream : Stream, IStreamStack
_position = _encoder.Code(null, true);
}
_inputStream?.Dispose();
_outWindow.Dispose();
}
base.Dispose(disposing);
}

View File

@@ -13,7 +13,7 @@ using SharpCompress.Compressors.Rar.VM;
namespace SharpCompress.Compressors.Rar.UnpackV1;
internal sealed partial class Unpack : BitInput, IRarUnpack, IDisposable
internal sealed partial class Unpack : BitInput, IRarUnpack
{
private readonly BitInput Inp;
private bool disposed;
@@ -22,15 +22,17 @@ internal sealed partial class Unpack : BitInput, IRarUnpack, IDisposable
// to ease in porting Unpack50.cs
Inp = this;
public void Dispose()
public override void Dispose()
{
if (!disposed)
{
base.Dispose();
if (!externalWindow)
{
ArrayPool<byte>.Shared.Return(window);
window = null;
}
rarVM.Dispose();
disposed = true;
}
}
@@ -574,104 +576,111 @@ internal sealed partial class Unpack : BitInput, IRarUnpack, IDisposable
var FilteredDataOffset = Prg.FilteredDataOffset;
var FilteredDataSize = Prg.FilteredDataSize;
var FilteredData = new byte[FilteredDataSize];
for (var i = 0; i < FilteredDataSize; i++)
var FilteredData = ArrayPool<byte>.Shared.Rent(FilteredDataSize);
try
{
FilteredData[i] = rarVM.Mem[FilteredDataOffset + i];
Array.Copy(
rarVM.Mem,
FilteredDataOffset,
FilteredData,
0,
FilteredDataSize
);
// Prg.GlobalData.get(FilteredDataOffset
// +
// i);
}
prgStack[I] = null;
while (I + 1 < prgStack.Count)
{
var NextFilter = prgStack[I + 1];
if (
NextFilter is null
|| NextFilter.BlockStart != BlockStart
|| NextFilter.BlockLength != FilteredDataSize
|| NextFilter.NextWindow
)
{
break;
}
// apply several filters to same data block
rarVM.setMemory(0, FilteredData, 0, FilteredDataSize);
// .SetMemory(0,FilteredData,FilteredDataSize);
var pPrg = filters[NextFilter.ParentFilter].Program;
var NextPrg = NextFilter.Program;
if (pPrg.GlobalData.Count > RarVM.VM_FIXEDGLOBALSIZE)
{
// copy global data from previous script execution
// if any
// NextPrg->GlobalData.Alloc(ParentPrg->GlobalData.Size());
NextPrg.GlobalData.SetSize(pPrg.GlobalData.Count);
// memcpy(&NextPrg->GlobalData[VM_FIXEDGLOBALSIZE],&ParentPrg->GlobalData[VM_FIXEDGLOBALSIZE],ParentPrg->GlobalData.Size()-VM_FIXEDGLOBALSIZE);
for (
var i = 0;
i < pPrg.GlobalData.Count - RarVM.VM_FIXEDGLOBALSIZE;
i++
)
{
NextPrg.GlobalData[RarVM.VM_FIXEDGLOBALSIZE + i] = pPrg.GlobalData[
RarVM.VM_FIXEDGLOBALSIZE + i
];
}
}
ExecuteCode(NextPrg);
if (NextPrg.GlobalData.Count > RarVM.VM_FIXEDGLOBALSIZE)
{
// save global data for next script execution
if (pPrg.GlobalData.Count < NextPrg.GlobalData.Count)
{
pPrg.GlobalData.SetSize(NextPrg.GlobalData.Count);
}
// memcpy(&ParentPrg->GlobalData[VM_FIXEDGLOBALSIZE],&NextPrg->GlobalData[VM_FIXEDGLOBALSIZE],NextPrg->GlobalData.Size()-VM_FIXEDGLOBALSIZE);
for (
var i = 0;
i < NextPrg.GlobalData.Count - RarVM.VM_FIXEDGLOBALSIZE;
i++
)
{
pPrg.GlobalData[RarVM.VM_FIXEDGLOBALSIZE + i] = NextPrg.GlobalData[
RarVM.VM_FIXEDGLOBALSIZE + i
];
}
}
else
{
pPrg.GlobalData.Clear();
}
FilteredDataOffset = NextPrg.FilteredDataOffset;
FilteredDataSize = NextPrg.FilteredDataSize;
FilteredData = new byte[FilteredDataSize];
for (var i = 0; i < FilteredDataSize; i++)
{
FilteredData[i] = NextPrg.GlobalData[FilteredDataOffset + i];
}
I++;
prgStack[I] = null;
while (I + 1 < prgStack.Count)
{
var NextFilter = prgStack[I + 1];
if (
NextFilter is null
|| NextFilter.BlockStart != BlockStart
|| NextFilter.BlockLength != FilteredDataSize
|| NextFilter.NextWindow
)
{
break;
}
// apply several filters to same data block
rarVM.setMemory(0, FilteredData, 0, FilteredDataSize);
// .SetMemory(0,FilteredData,FilteredDataSize);
var pPrg = filters[NextFilter.ParentFilter].Program;
var NextPrg = NextFilter.Program;
if (pPrg.GlobalData.Count > RarVM.VM_FIXEDGLOBALSIZE)
{
// copy global data from previous script execution
// if any
// NextPrg->GlobalData.Alloc(ParentPrg->GlobalData.Size());
NextPrg.GlobalData.SetSize(pPrg.GlobalData.Count);
// memcpy(&NextPrg->GlobalData[VM_FIXEDGLOBALSIZE],&ParentPrg->GlobalData[VM_FIXEDGLOBALSIZE],ParentPrg->GlobalData.Size()-VM_FIXEDGLOBALSIZE);
for (
var i = 0;
i < pPrg.GlobalData.Count - RarVM.VM_FIXEDGLOBALSIZE;
i++
)
{
NextPrg.GlobalData[RarVM.VM_FIXEDGLOBALSIZE + i] =
pPrg.GlobalData[RarVM.VM_FIXEDGLOBALSIZE + i];
}
}
ExecuteCode(NextPrg);
if (NextPrg.GlobalData.Count > RarVM.VM_FIXEDGLOBALSIZE)
{
// save global data for next script execution
if (pPrg.GlobalData.Count < NextPrg.GlobalData.Count)
{
pPrg.GlobalData.SetSize(NextPrg.GlobalData.Count);
}
// memcpy(&ParentPrg->GlobalData[VM_FIXEDGLOBALSIZE],&NextPrg->GlobalData[VM_FIXEDGLOBALSIZE],NextPrg->GlobalData.Size()-VM_FIXEDGLOBALSIZE);
for (
var i = 0;
i < NextPrg.GlobalData.Count - RarVM.VM_FIXEDGLOBALSIZE;
i++
)
{
pPrg.GlobalData[RarVM.VM_FIXEDGLOBALSIZE + i] =
NextPrg.GlobalData[RarVM.VM_FIXEDGLOBALSIZE + i];
}
}
else
{
pPrg.GlobalData.Clear();
}
FilteredDataOffset = NextPrg.FilteredDataOffset;
FilteredDataSize = NextPrg.FilteredDataSize;
if (FilteredData.Length < FilteredDataSize)
{
ArrayPool<byte>.Shared.Return(FilteredData);
FilteredData = ArrayPool<byte>.Shared.Rent(FilteredDataSize);
}
for (var i = 0; i < FilteredDataSize; i++)
{
FilteredData[i] = NextPrg.GlobalData[FilteredDataOffset + i];
}
I++;
prgStack[I] = null;
}
writeStream.Write(FilteredData, 0, FilteredDataSize);
writtenFileSize += FilteredDataSize;
destUnpSize -= FilteredDataSize;
WrittenBorder = BlockEnd;
WriteSize = (unpPtr - WrittenBorder) & PackDef.MAXWINMASK;
}
finally
{
ArrayPool<byte>.Shared.Return(FilteredData);
}
writeStream.Write(FilteredData, 0, FilteredDataSize);
unpSomeRead = true;
writtenFileSize += FilteredDataSize;
destUnpSize -= FilteredDataSize;
WrittenBorder = BlockEnd;
WriteSize = (unpPtr - WrittenBorder) & PackDef.MAXWINMASK;
}
else
{
@@ -695,15 +704,10 @@ internal sealed partial class Unpack : BitInput, IRarUnpack, IDisposable
private void UnpWriteArea(int startPtr, int endPtr)
{
if (endPtr != startPtr)
{
unpSomeRead = true;
}
if (endPtr < startPtr)
{
UnpWriteData(window, startPtr, -startPtr & PackDef.MAXWINMASK);
UnpWriteData(window, 0, endPtr);
unpAllBuf = true;
}
else
{
@@ -757,19 +761,27 @@ internal sealed partial class Unpack : BitInput, IRarUnpack, IDisposable
// System.out.println("copyString(" + length + ", " + distance + ")");
var destPtr = unpPtr - distance;
var safeZone = PackDef.MAXWINSIZE - 260;
// System.out.println(unpPtr+":"+distance);
if (destPtr >= 0 && destPtr < PackDef.MAXWINSIZE - 260 && unpPtr < PackDef.MAXWINSIZE - 260)
// Fast path: use Array.Copy for bulk operations when in safe zone
if (destPtr >= 0 && destPtr < safeZone && unpPtr < safeZone && distance >= length)
{
window[unpPtr++] = window[destPtr++];
while (--length > 0)
// Non-overlapping copy: can use Array.Copy directly
Array.Copy(window, destPtr, window, unpPtr, length);
unpPtr += length;
}
else if (destPtr >= 0 && destPtr < safeZone && unpPtr < safeZone)
{
// Overlapping copy in safe zone: use byte-by-byte to handle self-referential copies
for (int i = 0; i < length; i++)
{
window[unpPtr++] = window[destPtr++];
window[unpPtr + i] = window[destPtr + i];
}
unpPtr += length;
}
else
{
// Slow path with wraparound mask
while (length-- != 0)
{
window[unpPtr] = window[destPtr++ & PackDef.MAXWINMASK];
@@ -1028,7 +1040,7 @@ internal sealed partial class Unpack : BitInput, IRarUnpack, IDisposable
vmCode.Add((byte)(GetBits() >> 8));
AddBits(8);
}
return (AddVMCode(FirstByte, vmCode, Length));
return AddVMCode(FirstByte, vmCode);
}
private bool ReadVMCodePPM()
@@ -1073,12 +1085,12 @@ internal sealed partial class Unpack : BitInput, IRarUnpack, IDisposable
}
vmCode.Add((byte)Ch); // VMCode[I]=Ch;
}
return (AddVMCode(FirstByte, vmCode, Length));
return AddVMCode(FirstByte, vmCode);
}
private bool AddVMCode(int firstByte, List<byte> vmCode, int length)
private bool AddVMCode(int firstByte, List<byte> vmCode)
{
var Inp = new BitInput();
using var Inp = new BitInput();
Inp.InitBitInput();
// memcpy(Inp.InBuf,Code,Min(BitInput::MAX_SIZE,CodeSize));
@@ -1086,7 +1098,6 @@ internal sealed partial class Unpack : BitInput, IRarUnpack, IDisposable
{
Inp.InBuf[i] = vmCode[i];
}
rarVM.init();
int FiltPos;
if ((firstByte & 0x80) != 0)
@@ -1199,19 +1210,28 @@ internal sealed partial class Unpack : BitInput, IRarUnpack, IDisposable
{
return (false);
}
Span<byte> VMCode = stackalloc byte[VMCodeSize];
for (var I = 0; I < VMCodeSize; I++)
{
if (Inp.Overflow(3))
{
return (false);
}
VMCode[I] = (byte)(Inp.GetBits() >> 8);
Inp.AddBits(8);
}
// VM.Prepare(&VMCode[0],VMCodeSize,&Filter->Prg);
rarVM.prepare(VMCode, VMCodeSize, Filter.Program);
var VMCode = ArrayPool<byte>.Shared.Rent(VMCodeSize);
try
{
for (var I = 0; I < VMCodeSize; I++)
{
if (Inp.Overflow(3))
{
return (false);
}
VMCode[I] = (byte)(Inp.GetBits() >> 8);
Inp.AddBits(8);
}
// VM.Prepare(&VMCode[0],VMCodeSize,&Filter->Prg);
rarVM.prepare(VMCode.AsSpan(0, VMCodeSize), Filter.Program);
}
finally
{
ArrayPool<byte>.Shared.Return(VMCode);
}
}
StackFilter.Program.AltCommands = Filter.Program.Commands; // StackFilter->Prg.AltCmd=&Filter->Prg.Cmd[0];
StackFilter.Program.CommandCount = Filter.Program.CommandCount;

View File

@@ -19,14 +19,9 @@ internal partial class Unpack
private bool suspended;
internal bool unpAllBuf;
//private ComprDataIO unpIO;
private Stream readStream;
private Stream writeStream;
internal bool unpSomeRead;
private int readTop;
private long destUnpSize;
@@ -808,15 +803,10 @@ internal partial class Unpack
private void oldUnpWriteBuf()
{
if (unpPtr != wrPtr)
{
unpSomeRead = true;
}
if (unpPtr < wrPtr)
{
writeStream.Write(window, wrPtr, -wrPtr & PackDef.MAXWINMASK);
writeStream.Write(window, 0, unpPtr);
unpAllBuf = true;
}
else
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;
using SharpCompress.Compressors.Rar.VM;
namespace SharpCompress.Compressors.Rar.UnpackV1;
@@ -9,167 +10,15 @@ internal static class UnpackUtility
internal static uint DecodeNumber(this BitInput input, Decode.Decode dec) =>
(uint)input.decodeNumber(dec);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int decodeNumber(this BitInput input, Decode.Decode dec)
{
int bits;
long bitField = input.GetBits() & 0xfffe;
// if (bitField < dec.getDecodeLen()[8]) {
// if (bitField < dec.getDecodeLen()[4]) {
// if (bitField < dec.getDecodeLen()[2]) {
// if (bitField < dec.getDecodeLen()[1]) {
// bits = 1;
// } else {
// bits = 2;
// }
// } else {
// if (bitField < dec.getDecodeLen()[3]) {
// bits = 3;
// } else {
// bits = 4;
// }
// }
// } else {
// if (bitField < dec.getDecodeLen()[6]) {
// if (bitField < dec.getDecodeLen()[5])
// bits = 5;
// else
// bits = 6;
// } else {
// if (bitField < dec.getDecodeLen()[7]) {
// bits = 7;
// } else {
// bits = 8;
// }
// }
// }
// } else {
// if (bitField < dec.getDecodeLen()[12]) {
// if (bitField < dec.getDecodeLen()[10])
// if (bitField < dec.getDecodeLen()[9])
// bits = 9;
// else
// bits = 10;
// else if (bitField < dec.getDecodeLen()[11])
// bits = 11;
// else
// bits = 12;
// } else {
// if (bitField < dec.getDecodeLen()[14]) {
// if (bitField < dec.getDecodeLen()[13]) {
// bits = 13;
// } else {
// bits = 14;
// }
// } else {
// bits = 15;
// }
// }
// }
// addbits(bits);
// int N = dec.getDecodePos()[bits]
// + (((int) bitField - dec.getDecodeLen()[bits - 1]) >>> (16 - bits));
// if (N >= dec.getMaxNum()) {
// N = 0;
// }
// return (dec.getDecodeNum()[N]);
var decodeLen = dec.DecodeLen;
if (bitField < decodeLen[8])
{
if (bitField < decodeLen[4])
{
if (bitField < decodeLen[2])
{
if (bitField < decodeLen[1])
{
bits = 1;
}
else
{
bits = 2;
}
}
else
{
if (bitField < decodeLen[3])
{
bits = 3;
}
else
{
bits = 4;
}
}
}
else
{
if (bitField < decodeLen[6])
{
if (bitField < decodeLen[5])
{
bits = 5;
}
else
{
bits = 6;
}
}
else
{
if (bitField < decodeLen[7])
{
bits = 7;
}
else
{
bits = 8;
}
}
}
}
else
{
if (bitField < decodeLen[12])
{
if (bitField < decodeLen[10])
{
if (bitField < decodeLen[9])
{
bits = 9;
}
else
{
bits = 10;
}
}
else if (bitField < decodeLen[11])
{
bits = 11;
}
else
{
bits = 12;
}
}
else
{
if (bitField < decodeLen[14])
{
if (bitField < decodeLen[13])
{
bits = 13;
}
else
{
bits = 14;
}
}
else
{
bits = 15;
}
}
}
// Binary search to find the bit length - faster than nested ifs
int bits = FindDecodeBits(bitField, decodeLen);
input.AddBits(bits);
var N =
dec.DecodePos[bits]
@@ -181,6 +30,52 @@ internal static class UnpackUtility
return (dec.DecodeNum[N]);
}
/// <summary>
/// Fast binary search to find which bit length matches the bitField.
/// Optimized with cached array access to minimize memory lookups.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int FindDecodeBits(long bitField, int[] decodeLen)
{
// Cache critical values to reduce array access overhead
long len4 = decodeLen[4];
long len8 = decodeLen[8];
long len12 = decodeLen[12];
if (bitField < len8)
{
if (bitField < len4)
{
long len2 = decodeLen[2];
if (bitField < len2)
{
return bitField < decodeLen[1] ? 1 : 2;
}
return bitField < decodeLen[3] ? 3 : 4;
}
long len6 = decodeLen[6];
if (bitField < len6)
{
return bitField < decodeLen[5] ? 5 : 6;
}
return bitField < decodeLen[7] ? 7 : 8;
}
if (bitField < len12)
{
long len10 = decodeLen[10];
if (bitField < len10)
{
return bitField < decodeLen[9] ? 9 : 10;
}
return bitField < decodeLen[11] ? 11 : 12;
}
long len14 = decodeLen[14];
return bitField < len14 ? (bitField < decodeLen[13] ? 13 : 14) : 15;
}
internal static void makeDecodeTables(
Span<byte> lenTab,
int offset,
@@ -194,8 +89,7 @@ internal static class UnpackUtility
long M,
N;
new Span<int>(dec.DecodeNum).Clear(); // memset(Dec->DecodeNum,0,Size*sizeof(*Dec->DecodeNum));
new Span<int>(dec.DecodeNum).Clear();
for (i = 0; i < size; i++)
{
lenCount[lenTab[offset + i] & 0xF]++;

View File

@@ -413,7 +413,7 @@ internal partial class Unpack
else
//x memcpy(Mem,Window+BlockStart,BlockLength);
{
Utility.Copy(Window, BlockStart, Mem, 0, BlockLength);
Buffer.BlockCopy(Window, (int)BlockStart, Mem, 0, (int)BlockLength);
}
}
else
@@ -427,9 +427,21 @@ internal partial class Unpack
else
{
//x memcpy(Mem,Window+BlockStart,FirstPartLength);
Utility.Copy(Window, BlockStart, Mem, 0, FirstPartLength);
Buffer.BlockCopy(
Window,
(int)BlockStart,
Mem,
0,
(int)FirstPartLength
);
//x memcpy(Mem+FirstPartLength,Window,BlockEnd);
Utility.Copy(Window, 0, Mem, FirstPartLength, BlockEnd);
Buffer.BlockCopy(
Window,
0,
Mem,
(int)FirstPartLength,
(int)BlockEnd
);
}
}

View File

@@ -1,6 +1,9 @@
using System;
using System.Buffers;
namespace SharpCompress.Compressors.Rar.VM;
internal class BitInput
internal class BitInput : IDisposable
{
/// <summary> the max size of the input</summary>
internal const int MAX_SIZE = 0x8000;
@@ -20,9 +23,11 @@ internal class BitInput
set => inBit = value;
}
public bool ExternalBuffer;
private byte[] _privateBuffer = ArrayPool<byte>.Shared.Rent(MAX_SIZE);
private bool _disposed;
/// <summary> </summary>
internal BitInput() => InBuf = new byte[MAX_SIZE];
internal BitInput() => InBuf = _privateBuffer;
internal byte[] InBuf { get; }
@@ -87,4 +92,14 @@ internal class BitInput
/// <returns> true if an Oververflow would occur
/// </returns>
internal bool Overflow(int IncPtr) => (inAddr + IncPtr >= MAX_SIZE);
public virtual void Dispose()
{
if (_disposed)
{
return;
}
ArrayPool<byte>.Shared.Return(_privateBuffer);
_disposed = true;
}
}

View File

@@ -1,6 +1,5 @@
#nullable disable
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
@@ -16,7 +15,9 @@ internal sealed class RarVM : BitInput
// Mem.set_Renamed(offset + 3, Byte.valueOf((sbyte) ((Utility.URShift(value_Renamed, 24)) & 0xff)));
//}
internal byte[] Mem { get; private set; }
internal byte[] Mem => _memory.NotNull();
private byte[]? _memory = ArrayPool<byte>.Shared.Rent(VM_MEMSIZE + 4);
public const int VM_MEMSIZE = 0x40000;
@@ -40,11 +41,18 @@ internal sealed class RarVM : BitInput
private int IP;
internal RarVM() =>
//InitBlock();
Mem = null;
internal RarVM() { }
internal void init() => Mem ??= new byte[VM_MEMSIZE + 4];
public override void Dispose()
{
base.Dispose();
if (_memory is null)
{
return;
}
ArrayPool<byte>.Shared.Return(_memory);
_memory = null;
}
private bool IsVMMem(byte[] mem) => Mem == mem;
@@ -776,9 +784,10 @@ internal sealed class RarVM : BitInput
}
}
public void prepare(ReadOnlySpan<byte> code, int codeSize, VMPreparedProgram prg)
public void prepare(ReadOnlySpan<byte> code, VMPreparedProgram prg)
{
InitBitInput();
var codeSize = code.Length;
var cpLength = Math.Min(MAX_SIZE, codeSize);
// memcpy(inBuf,Code,Min(CodeSize,BitInput::MAX_SIZE));
@@ -795,7 +804,7 @@ internal sealed class RarVM : BitInput
prg.CommandCount = 0;
if (xorSum == code[0])
{
var filterType = IsStandardFilter(code, codeSize);
var filterType = IsStandardFilter(code);
if (filterType != VMStandardFilters.VMSF_NONE)
{
var curCmd = new VMPreparedCommand();
@@ -1105,7 +1114,7 @@ internal sealed class RarVM : BitInput
}
}
private VMStandardFilters IsStandardFilter(ReadOnlySpan<byte> code, int codeSize)
private VMStandardFilters IsStandardFilter(ReadOnlySpan<byte> code)
{
VMStandardFilterSignature[] stdList =
{
@@ -1130,6 +1139,7 @@ internal sealed class RarVM : BitInput
private void ExecuteStandardFilter(VMStandardFilters filterType)
{
var mem = Mem;
switch (filterType)
{
case VMStandardFilters.VMSF_E8:
@@ -1148,7 +1158,7 @@ internal sealed class RarVM : BitInput
);
for (var curPos = 0; curPos < dataSize - 4; )
{
var curByte = Mem[curPos++];
var curByte = mem[curPos++];
if (curByte == 0xe8 || curByte == cmpByte2)
{
// #ifdef PRESENT_INT32
@@ -1164,19 +1174,19 @@ internal sealed class RarVM : BitInput
// SET_VALUE(false,Data,Addr-Offset);
// #else
var offset = curPos + fileOffset;
long Addr = GetValue(false, Mem, curPos);
long Addr = GetValue(false, mem, curPos);
if ((Addr & unchecked((int)0x80000000)) != 0)
{
if (((Addr + offset) & unchecked((int)0x80000000)) == 0)
{
SetValue(false, Mem, curPos, (int)Addr + fileSize);
SetValue(false, mem, curPos, (int)Addr + fileSize);
}
}
else
{
if (((Addr - fileSize) & unchecked((int)0x80000000)) != 0)
{
SetValue(false, Mem, curPos, (int)(Addr - offset));
SetValue(false, mem, curPos, (int)(Addr - offset));
}
}
@@ -1204,7 +1214,7 @@ internal sealed class RarVM : BitInput
while (curPos < dataSize - 21)
{
var Byte = (Mem[curPos] & 0x1f) - 0x10;
var Byte = (mem[curPos] & 0x1f) - 0x10;
if (Byte >= 0)
{
var cmdMask = Masks[Byte];
@@ -1250,7 +1260,7 @@ internal sealed class RarVM : BitInput
var channels = R[0] & unchecked((int)0xFFffFFff);
var srcPos = 0;
var border = (dataSize * 2) & unchecked((int)0xFFffFFff);
SetValue(false, Mem, VM_GLOBALMEMADDR + 0x20, dataSize);
SetValue(false, mem, VM_GLOBALMEMADDR + 0x20, dataSize);
if (dataSize >= VM_GLOBALMEMADDR / 2)
{
break;
@@ -1268,7 +1278,7 @@ internal sealed class RarVM : BitInput
destPos += channels
)
{
Mem[destPos] = (PrevByte = (byte)(PrevByte - Mem[srcPos++]));
mem[destPos] = (PrevByte = (byte)(PrevByte - mem[srcPos++]));
}
}
}
@@ -1283,7 +1293,7 @@ internal sealed class RarVM : BitInput
var channels = 3;
var srcPos = 0;
var destDataPos = dataSize;
SetValue(false, Mem, VM_GLOBALMEMADDR + 0x20, dataSize);
SetValue(false, mem, VM_GLOBALMEMADDR + 0x20, dataSize);
if (dataSize >= VM_GLOBALMEMADDR / 2 || posR < 0)
{
break;
@@ -1299,8 +1309,8 @@ internal sealed class RarVM : BitInput
if (upperPos >= 3)
{
var upperDataPos = destDataPos + upperPos;
var upperByte = Mem[upperDataPos] & 0xff;
var upperLeftByte = Mem[upperDataPos - 3] & 0xff;
var upperByte = mem[upperDataPos] & 0xff;
var upperLeftByte = mem[upperDataPos - 3] & 0xff;
predicted = prevByte + upperByte - upperLeftByte;
var pa = Math.Abs((int)(predicted - prevByte));
var pb = Math.Abs((int)(predicted - upperByte));
@@ -1326,15 +1336,15 @@ internal sealed class RarVM : BitInput
predicted = prevByte;
}
prevByte = ((predicted - Mem[srcPos++]) & 0xff) & 0xff;
Mem[destDataPos + i] = (byte)(prevByte & 0xff);
prevByte = ((predicted - mem[srcPos++]) & 0xff) & 0xff;
mem[destDataPos + i] = (byte)(prevByte & 0xff);
}
}
for (int i = posR, border = dataSize - 2; i < border; i += 3)
{
var G = Mem[destDataPos + i + 1];
Mem[destDataPos + i] = (byte)(Mem[destDataPos + i] + G);
Mem[destDataPos + i + 2] = (byte)(Mem[destDataPos + i + 2] + G);
var G = mem[destDataPos + i + 1];
mem[destDataPos + i] = (byte)(mem[destDataPos + i] + G);
mem[destDataPos + i + 2] = (byte)(mem[destDataPos + i + 2] + G);
}
}
break;
@@ -1347,7 +1357,7 @@ internal sealed class RarVM : BitInput
var destDataPos = dataSize;
//byte *SrcData=Mem,*DestData=SrcData+DataSize;
SetValue(false, Mem, VM_GLOBALMEMADDR + 0x20, dataSize);
SetValue(false, mem, VM_GLOBALMEMADDR + 0x20, dataSize);
if (dataSize >= VM_GLOBALMEMADDR / 2)
{
break;
@@ -1377,10 +1387,10 @@ internal sealed class RarVM : BitInput
var predicted = (8 * prevByte) + (K1 * D1) + (K2 * D2) + (K3 * D3);
predicted = Utility.URShift(predicted, 3) & 0xff;
long curByte = Mem[srcPos++];
long curByte = mem[srcPos++];
predicted -= curByte;
Mem[destDataPos + i] = (byte)predicted;
mem[destDataPos + i] = (byte)predicted;
prevDelta = (byte)(predicted - prevByte);
//fix java byte
@@ -1480,15 +1490,15 @@ internal sealed class RarVM : BitInput
}
while (srcPos < dataSize)
{
var curByte = Mem[srcPos++];
if (curByte == 2 && (curByte = Mem[srcPos++]) != 2)
var curByte = mem[srcPos++];
if (curByte == 2 && (curByte = mem[srcPos++]) != 2)
{
curByte = (byte)(curByte - 32);
}
Mem[destPos++] = curByte;
mem[destPos++] = curByte;
}
SetValue(false, Mem, VM_GLOBALMEMADDR + 0x1c, destPos - dataSize);
SetValue(false, Mem, VM_GLOBALMEMADDR + 0x20, dataSize);
SetValue(false, mem, VM_GLOBALMEMADDR + 0x1c, destPos - dataSize);
SetValue(false, mem, VM_GLOBALMEMADDR + 0x20, dataSize);
}
break;
}
@@ -1528,15 +1538,14 @@ internal sealed class RarVM : BitInput
{
if (pos < VM_MEMSIZE)
{
//&& data!=Mem+Pos)
//memmove(Mem+Pos,Data,Min(DataSize,VM_MEMSIZE-Pos));
for (var i = 0; i < Math.Min(data.Length - offset, dataSize); i++)
// Use Array.Copy for fast bulk memory operations instead of byte-by-byte loop
// Calculate how much data can actually fit in VM memory
int copyLength = Math.Min(dataSize, VM_MEMSIZE - pos);
copyLength = Math.Min(copyLength, data.Length - offset);
if (copyLength > 0)
{
if ((VM_MEMSIZE - pos) < i)
{
break;
}
Mem[pos + i] = data[offset + i];
Array.Copy(data, offset, Mem, pos, copyLength);
}
}
}

View File

@@ -22,9 +22,7 @@ public class XZFooter
public static XZFooter FromStream(Stream stream)
{
var footer = new XZFooter(
new BinaryReader(SharpCompressStream.Create(stream, leaveOpen: true), Encoding.UTF8)
);
var footer = new XZFooter(new BinaryReader(stream, Encoding.UTF8, true));
footer.Process();
return footer;
}

View File

@@ -18,9 +18,7 @@ public class XZHeader
public static XZHeader FromStream(Stream stream)
{
var header = new XZHeader(
new BinaryReader(SharpCompressStream.Create(stream, leaveOpen: true), Encoding.UTF8)
);
var header = new XZHeader(new BinaryReader(stream, Encoding.UTF8, true));
header.Process();
return header;
}

View File

@@ -32,7 +32,7 @@ public class XZIndex
public static XZIndex FromStream(Stream stream, bool indexMarkerAlreadyVerified)
{
var index = new XZIndex(
new BinaryReader(SharpCompressStream.Create(stream, leaveOpen: true), Encoding.UTF8),
new BinaryReader(stream, Encoding.UTF8, true),
indexMarkerAlreadyVerified
);
index.Process();

View File

@@ -1,5 +1,5 @@
using System;
using SharpCompress.Common;
namespace SharpCompress.Compressors.Xz;
public class XZIndexMarkerReachedException : Exception { }
public class XZIndexMarkerReachedException : SharpCompressException { }

View File

@@ -34,7 +34,7 @@ public abstract class Factory : IFactory
/// <exception cref="ArgumentNullException"><paramref name="factory"/> must not be null.</exception>
public static void RegisterFactory(Factory factory)
{
factory.CheckNotNull(nameof(factory));
factory.NotNull(nameof(factory));
_factories.Add(factory);
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.IO;
@@ -93,6 +95,47 @@ internal class ReadOnlySubStream : SharpCompressStream, IStreamStack
}
#endif
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (BytesLeftToRead < count)
{
count = (int)BytesLeftToRead;
}
var read = await Stream
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
if (read > 0)
{
BytesLeftToRead -= read;
_position += read;
}
return read;
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
var sliceLen = BytesLeftToRead < buffer.Length ? BytesLeftToRead : buffer.Length;
var read = await Stream
.ReadAsync(buffer.Slice(0, (int)sliceLen), cancellationToken)
.ConfigureAwait(false);
if (read > 0)
{
BytesLeftToRead -= read;
_position += read;
}
return read;
}
#endif
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();

View File

@@ -1,8 +1,10 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.IO;
@@ -44,7 +46,11 @@ public class SharpCompressStream : Stream, IStreamStack
_bufferingEnabled = _bufferSize > 0;
if (_bufferingEnabled)
{
_buffer = new byte[_bufferSize];
if (_buffer is not null)
{
ArrayPool<byte>.Shared.Return(_buffer);
}
_buffer = ArrayPool<byte>.Shared.Rent(_bufferSize);
_bufferPosition = 0;
_bufferedLength = 0;
if (_bufferingEnabled)
@@ -173,6 +179,11 @@ public class SharpCompressStream : Stream, IStreamStack
if (disposing)
{
Stream.Dispose();
if (_buffer != null)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = null;
}
}
}
@@ -316,20 +327,146 @@ public class SharpCompressStream : Stream, IStreamStack
_internalPosition += count;
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (count == 0)
return 0;
if (_bufferingEnabled)
{
ValidateBufferState();
// Fill buffer if needed
if (_bufferedLength == 0)
{
_bufferedLength = await Stream
.ReadAsync(_buffer!, 0, _bufferSize, cancellationToken)
.ConfigureAwait(false);
_bufferPosition = 0;
}
int available = _bufferedLength - _bufferPosition;
int toRead = Math.Min(count, available);
if (toRead > 0)
{
Array.Copy(_buffer!, _bufferPosition, buffer, offset, toRead);
_bufferPosition += toRead;
_internalPosition += toRead;
return toRead;
}
// If buffer exhausted, refill
int r = await Stream
.ReadAsync(_buffer!, 0, _bufferSize, cancellationToken)
.ConfigureAwait(false);
if (r == 0)
return 0;
_bufferedLength = r;
_bufferPosition = 0;
if (_bufferedLength == 0)
{
return 0;
}
toRead = Math.Min(count, _bufferedLength);
Array.Copy(_buffer!, 0, buffer, offset, toRead);
_bufferPosition = toRead;
_internalPosition += toRead;
return toRead;
}
else
{
int read = await Stream
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
_internalPosition += read;
return read;
}
}
public override async Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
await Stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
_internalPosition += count;
}
public override async Task FlushAsync(CancellationToken cancellationToken)
{
await Stream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
//public override int Read(Span<byte> buffer)
//{
// int bytesRead = Stream.Read(buffer);
// _internalPosition += bytesRead;
// return bytesRead;
//}
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (buffer.Length == 0)
return 0;
// public override void Write(ReadOnlySpan<byte> buffer)
// {
// Stream.Write(buffer);
// _internalPosition += buffer.Length;
// }
if (_bufferingEnabled)
{
ValidateBufferState();
// Fill buffer if needed
if (_bufferedLength == 0)
{
_bufferedLength = await Stream
.ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken)
.ConfigureAwait(false);
_bufferPosition = 0;
}
int available = _bufferedLength - _bufferPosition;
int toRead = Math.Min(buffer.Length, available);
if (toRead > 0)
{
_buffer.AsSpan(_bufferPosition, toRead).CopyTo(buffer.Span);
_bufferPosition += toRead;
_internalPosition += toRead;
return toRead;
}
// If buffer exhausted, refill
int r = await Stream
.ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken)
.ConfigureAwait(false);
if (r == 0)
return 0;
_bufferedLength = r;
_bufferPosition = 0;
if (_bufferedLength == 0)
{
return 0;
}
toRead = Math.Min(buffer.Length, _bufferedLength);
_buffer.AsSpan(0, toRead).CopyTo(buffer.Span);
_bufferPosition = toRead;
_internalPosition += toRead;
return toRead;
}
else
{
int read = await Stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
_internalPosition += read;
return read;
}
}
public override async ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default
)
{
await Stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
_internalPosition += buffer.Length;
}
#endif
}

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.Readers;
namespace SharpCompress.IO;
@@ -238,6 +240,105 @@ public class SourceStream : Stream, IStreamStack
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotImplementedException();
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (count <= 0)
{
return 0;
}
var total = count;
var r = -1;
while (count != 0 && r != 0)
{
r = await Current
.ReadAsync(
buffer,
offset,
(int)Math.Min(count, Current.Length - Current.Position),
cancellationToken
)
.ConfigureAwait(false);
count -= r;
offset += r;
if (!IsVolumes && count != 0 && Current.Position == Current.Length)
{
var length = Current.Length;
// Load next file if present
if (!SetStream(_stream + 1))
{
break;
}
// Current stream switched
// Add length of previous stream
_prevSize += length;
Current.Seek(0, SeekOrigin.Begin);
r = -1; //BugFix: reset to allow loop if count is still not 0 - was breaking split zipx (lzma xz etc)
}
}
return total - count;
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (buffer.Length <= 0)
{
return 0;
}
var total = buffer.Length;
var count = buffer.Length;
var offset = 0;
var r = -1;
while (count != 0 && r != 0)
{
r = await Current
.ReadAsync(
buffer.Slice(offset, (int)Math.Min(count, Current.Length - Current.Position)),
cancellationToken
)
.ConfigureAwait(false);
count -= r;
offset += r;
if (!IsVolumes && count != 0 && Current.Position == Current.Length)
{
var length = Current.Length;
// Load next file if present
if (!SetStream(_stream + 1))
{
break;
}
// Current stream switched
// Add length of previous stream
_prevSize += length;
Current.Seek(0, SeekOrigin.Begin);
r = -1;
}
}
return total - count;
}
#endif
public override void Close()
{
if (IsFileMode || !ReaderOptions.LeaveStreamOpen) //close if file mode or options specify it

View File

@@ -4,7 +4,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
namespace SharpCompress.Helpers;
namespace SharpCompress;
internal sealed class LazyReadOnlyCollection<T> : ICollection<T>
{

View File

@@ -4,20 +4,19 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
namespace SharpCompress.Helpers;
namespace SharpCompress;
internal static class NotNullExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEnumerable<T> Empty<T>(this IEnumerable<T>? source) =>
source ?? Enumerable.Empty<T>();
public static IEnumerable<T> Empty<T>(this IEnumerable<T>? source) => source ?? [];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEnumerable<T> Empty<T>(this T? source)
{
if (source is null)
{
return Enumerable.Empty<T>();
return [];
}
return source.AsEnumerable();
}
@@ -68,4 +67,15 @@ internal static class NotNullExtensions
return obj.Value;
}
#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string NotNullOrEmpty(this string obj, string name)
{
obj.NotNull(name);
if (obj.Length == 0)
{
throw new ArgumentException("String is empty.", name);
}
return obj;
}
}

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;
namespace SharpCompress.Readers;
@@ -94,6 +96,33 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
return false;
}
public async Task<bool> MoveToNextEntryAsync(CancellationToken cancellationToken = default)
{
if (_completed)
{
return false;
}
if (Cancelled)
{
throw new ReaderCancelledException("Reader has been cancelled.");
}
if (_entriesForCurrentReadStream is null)
{
return LoadStreamForReading(RequestInitialStream());
}
if (!_wroteCurrentEntry)
{
await SkipEntryAsync(cancellationToken).ConfigureAwait(false);
}
_wroteCurrentEntry = false;
if (NextEntryForCurrentStream())
{
return true;
}
_completed = true;
return false;
}
protected bool LoadStreamForReading(Stream stream)
{
_entriesForCurrentReadStream?.Dispose();
@@ -127,6 +156,14 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
}
}
private async Task SkipEntryAsync(CancellationToken cancellationToken)
{
if (!Entry.IsDirectory)
{
await SkipAsync(cancellationToken).ConfigureAwait(false);
}
}
private void Skip()
{
var part = Entry.Parts.First();
@@ -149,6 +186,33 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
s.SkipEntry();
}
private async Task SkipAsync(CancellationToken cancellationToken)
{
var part = Entry.Parts.First();
if (!Entry.IsSplitAfter && !Entry.IsSolid && Entry.CompressedSize > 0)
{
//not solid and has a known compressed size then we can skip raw bytes.
var rawStream = part.GetRawStream();
if (rawStream != null)
{
var bytesToAdvance = Entry.CompressedSize;
await rawStream.SkipAsync(bytesToAdvance, cancellationToken).ConfigureAwait(false);
part.Skipped = true;
return;
}
}
//don't know the size so we have to try to decompress to skip
#if NETFRAMEWORK || NETSTANDARD2_0
using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
#else
await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
#endif
}
public void WriteEntryTo(Stream writableStream)
{
if (_wroteCurrentEntry)
@@ -171,6 +235,33 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
_wroteCurrentEntry = true;
}
public async Task WriteEntryToAsync(
Stream writableStream,
CancellationToken cancellationToken = default
)
{
if (_wroteCurrentEntry)
{
throw new ArgumentException(
"WriteEntryToAsync or OpenEntryStream can only be called once."
);
}
if (writableStream is null)
{
throw new ArgumentNullException(nameof(writableStream));
}
if (!writableStream.CanWrite)
{
throw new ArgumentException(
"A writable Stream was required. Use Cancel if that was intended."
);
}
await WriteAsync(writableStream, cancellationToken).ConfigureAwait(false);
_wroteCurrentEntry = true;
}
internal void Write(Stream writeStream)
{
var streamListener = this as IReaderExtractionListener;
@@ -178,6 +269,20 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
s.TransferTo(writeStream, Entry, streamListener);
}
internal async Task WriteAsync(Stream writeStream, CancellationToken cancellationToken)
{
var streamListener = this as IReaderExtractionListener;
#if NETFRAMEWORK || NETSTANDARD2_0
using Stream s = OpenEntryStream();
await s.TransferToAsync(writeStream, Entry, streamListener, cancellationToken)
.ConfigureAwait(false);
#else
await using Stream s = OpenEntryStream();
await s.TransferToAsync(writeStream, Entry, streamListener, cancellationToken)
.ConfigureAwait(false);
#endif
}
public EntryStream OpenEntryStream()
{
if (_wroteCurrentEntry)
@@ -189,6 +294,19 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
return stream;
}
public Task<EntryStream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
{
if (_wroteCurrentEntry)
{
throw new ArgumentException(
"WriteEntryToAsync or OpenEntryStreamAsync can only be called once."
);
}
var stream = GetEntryStream();
_wroteCurrentEntry = true;
return Task.FromResult(stream);
}
/// <summary>
/// Retains a reference to the entry stream, so we can check whether it completed later.
/// </summary>

View File

@@ -24,7 +24,7 @@ namespace SharpCompress.Readers.Arc
/// <returns></returns>
public static ArcReader Open(Stream stream, ReaderOptions? options = null)
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
return new ArcReader(stream, options ?? new ReaderOptions());
}

View File

@@ -22,7 +22,7 @@ public class GZipReader : AbstractReader<GZipEntry, GZipVolume>
/// <returns></returns>
public static GZipReader Open(Stream stream, ReaderOptions? options = null)
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
return new GZipReader(stream, options ?? new ReaderOptions());
}

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.Readers;
@@ -21,6 +23,13 @@ public interface IReader : IDisposable
/// <param name="writableStream"></param>
void WriteEntryTo(Stream writableStream);
/// <summary>
/// Decompresses the current entry to the stream asynchronously. This cannot be called twice for the current entry.
/// </summary>
/// <param name="writableStream"></param>
/// <param name="cancellationToken"></param>
Task WriteEntryToAsync(Stream writableStream, CancellationToken cancellationToken = default);
bool Cancelled { get; }
void Cancel();
@@ -30,9 +39,23 @@ public interface IReader : IDisposable
/// <returns></returns>
bool MoveToNextEntry();
/// <summary>
/// Moves to the next entry asynchronously by reading more data from the underlying stream. This skips if data has not been read.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<bool> MoveToNextEntryAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Opens the current entry as a stream that will decompress as it is read.
/// Read the entire stream or use SkipEntry on EntryStream.
/// </summary>
EntryStream OpenEntryStream();
/// <summary>
/// Opens the current entry asynchronously as a stream that will decompress as it is read.
/// Read the entire stream or use SkipEntry on EntryStream.
/// </summary>
/// <param name="cancellationToken"></param>
Task<EntryStream> OpenEntryStreamAsync(CancellationToken cancellationToken = default);
}

View File

@@ -1,4 +1,6 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.Readers;
@@ -65,4 +67,64 @@ public static class IReaderExtensions
reader.WriteEntryTo(fs);
}
);
/// <summary>
/// Extract to specific directory asynchronously, retaining filename
/// </summary>
public static async Task WriteEntryToDirectoryAsync(
this IReader reader,
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
await ExtractionMethods
.WriteEntryToDirectoryAsync(
reader.Entry,
destinationDirectory,
options,
(fileName, opts) => reader.WriteEntryToFileAsync(fileName, opts, cancellationToken),
cancellationToken
)
.ConfigureAwait(false);
/// <summary>
/// Extract to specific file asynchronously
/// </summary>
public static async Task WriteEntryToFileAsync(
this IReader reader,
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
await ExtractionMethods
.WriteEntryToFileAsync(
reader.Entry,
destinationFileName,
options,
async (x, fm) =>
{
using var fs = File.Open(destinationFileName, fm);
await reader.WriteEntryToAsync(fs, cancellationToken).ConfigureAwait(false);
},
cancellationToken
)
.ConfigureAwait(false);
/// <summary>
/// Extract all remaining unread entries to specific directory asynchronously, retaining filename
/// </summary>
public static async Task WriteAllToDirectoryAsync(
this IReader reader,
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
)
{
while (reader.MoveToNextEntry())
{
await reader
.WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken)
.ConfigureAwait(false);
}
}
}

View File

@@ -40,6 +40,29 @@ public abstract class RarReader : AbstractReader<RarReaderEntry, RarVolume>
public override RarVolume? Volume => volume;
public static RarReader Open(string filePath, ReaderOptions? options = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return Open(new FileInfo(filePath), options);
}
public static RarReader Open(FileInfo fileInfo, ReaderOptions? options = null)
{
options ??= new ReaderOptions { LeaveStreamOpen = false };
return Open(fileInfo.OpenRead(), options);
}
public static RarReader Open(IEnumerable<string> filePaths, ReaderOptions? options = null)
{
return Open(filePaths.Select(x => new FileInfo(x)), options);
}
public static RarReader Open(IEnumerable<FileInfo> fileInfos, ReaderOptions? options = null)
{
options ??= new ReaderOptions { LeaveStreamOpen = false };
return Open(fileInfos.Select(x => x.OpenRead()), options);
}
/// <summary>
/// Opens a RarReader for Non-seeking usage with a single volume
/// </summary>
@@ -48,7 +71,7 @@ public abstract class RarReader : AbstractReader<RarReaderEntry, RarVolume>
/// <returns></returns>
public static RarReader Open(Stream stream, ReaderOptions? options = null)
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
return new SingleVolumeRarReader(stream, options ?? new ReaderOptions());
}
@@ -60,7 +83,7 @@ public abstract class RarReader : AbstractReader<RarReaderEntry, RarVolume>
/// <returns></returns>
public static RarReader Open(IEnumerable<Stream> streams, ReaderOptions? options = null)
{
streams.CheckNotNull(nameof(streams));
streams.NotNull(nameof(streams));
return new MultiVolumeRarReader(streams, options ?? new ReaderOptions());
}

View File

@@ -9,6 +9,18 @@ namespace SharpCompress.Readers;
public static class ReaderFactory
{
public static IReader Open(string filePath, ReaderOptions? options = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return Open(new FileInfo(filePath), options);
}
public static IReader Open(FileInfo fileInfo, ReaderOptions? options = null)
{
options ??= new ReaderOptions { LeaveStreamOpen = false };
return Open(fileInfo.OpenRead(), options);
}
/// <summary>
/// Opens a Reader for Non-seeking usage
/// </summary>
@@ -17,7 +29,7 @@ public static class ReaderFactory
/// <returns></returns>
public static IReader Open(Stream stream, ReaderOptions? options = null)
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
options ??= new ReaderOptions() { LeaveStreamOpen = false };
var bStream = new SharpCompressStream(stream, bufferSize: options.BufferSize);

View File

@@ -55,7 +55,7 @@ public class TarReader : AbstractReader<TarEntry, TarVolume>
/// <returns></returns>
public static TarReader Open(Stream stream, ReaderOptions? options = null)
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
options = options ?? new ReaderOptions();
var rewindableStream = new SharpCompressStream(stream);

View File

@@ -44,7 +44,7 @@ public class ZipReader : AbstractReader<ZipEntry, ZipVolume>
/// <returns></returns>
public static ZipReader Open(Stream stream, ReaderOptions? options = null)
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
return new ZipReader(stream, options ?? new ReaderOptions());
}
@@ -54,7 +54,7 @@ public class ZipReader : AbstractReader<ZipEntry, ZipVolume>
IEnumerable<ZipEntry> entries
)
{
stream.CheckNotNull(nameof(stream));
stream.NotNull(nameof(stream));
return new ZipReader(stream, options ?? new ReaderOptions(), entries);
}

View File

@@ -35,9 +35,14 @@
<DefineConstants>$(DefineConstants);DEBUG_STREAMS</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Buffers" />
<PackageReference Include="ZstdSharp.Port" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'net481' ">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Text.Encoding.CodePages" />
<PackageReference Include="System.Buffers" />
<PackageReference Include="System.Memory" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">

View File

@@ -1,16 +1,21 @@
global using SharpCompress.Helpers;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Readers;
namespace SharpCompress.Helpers;
namespace SharpCompress;
internal static class Utility
{
//80kb is a good industry standard temporary buffer size
private const int TEMP_BUFFER_SIZE = 81920;
private static readonly HashSet<char> invalidChars = new(Path.GetInvalidFileNameChars());
public static ReadOnlyCollection<T> ToReadOnly<T>(this IList<T> items) => new(items);
/// <summary>
@@ -19,14 +24,7 @@ internal static class Utility
/// <param name="number">Number to operate on</param>
/// <param name="bits">Amount of bits to shift</param>
/// <returns>The resulting number from the shift operation</returns>
public static int URShift(int number, int bits)
{
if (number >= 0)
{
return number >> bits;
}
return (number >> bits) + (2 << ~bits);
}
public static int URShift(int number, int bits) => (int)((uint)number >> bits);
/// <summary>
/// Performs an unsigned bitwise right shift with the specified number
@@ -34,14 +32,7 @@ internal static class Utility
/// <param name="number">Number to operate on</param>
/// <param name="bits">Amount of bits to shift</param>
/// <returns>The resulting number from the shift operation</returns>
public static long URShift(long number, int bits)
{
if (number >= 0)
{
return number >> bits;
}
return (number >> bits) + (2L << ~bits);
}
public static long URShift(long number, int bits) => (long)((ulong)number >> bits);
public static void SetSize(this List<byte> list, int count)
{
@@ -68,60 +59,11 @@ internal static class Utility
}
}
public static void Copy(
Array sourceArray,
long sourceIndex,
Array destinationArray,
long destinationIndex,
long length
)
{
if (sourceIndex > int.MaxValue || sourceIndex < int.MinValue)
{
throw new ArgumentOutOfRangeException(nameof(sourceIndex));
}
if (destinationIndex > int.MaxValue || destinationIndex < int.MinValue)
{
throw new ArgumentOutOfRangeException(nameof(destinationIndex));
}
if (length > int.MaxValue || length < int.MinValue)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
Array.Copy(
sourceArray,
(int)sourceIndex,
destinationArray,
(int)destinationIndex,
(int)length
);
}
public static IEnumerable<T> AsEnumerable<T>(this T item)
{
yield return item;
}
public static void CheckNotNull(this object obj, string name)
{
if (obj is null)
{
throw new ArgumentNullException(name);
}
}
public static void CheckNotNullOrEmpty(this string obj, string name)
{
obj.CheckNotNull(name);
if (obj.Length == 0)
{
throw new ArgumentException("String is empty.", name);
}
}
public static void Skip(this Stream source, long advanceAmount)
{
if (source.CanSeek)
@@ -130,79 +72,48 @@ internal static class Utility
return;
}
var buffer = GetTransferByteArray();
try
using var buffer = MemoryPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
while (advanceAmount > 0)
{
var read = 0;
var readCount = 0;
do
var toRead = (int)Math.Min(buffer.Memory.Length, advanceAmount);
var read = source.Read(buffer.Memory.Slice(0, toRead).Span);
if (read <= 0)
{
readCount = buffer.Length;
if (readCount > advanceAmount)
{
readCount = (int)advanceAmount;
}
read = source.Read(buffer, 0, readCount);
if (read <= 0)
{
break;
}
advanceAmount -= read;
if (advanceAmount == 0)
{
break;
}
} while (true);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
break;
}
advanceAmount -= read;
}
}
public static void Skip(this Stream source)
{
var buffer = GetTransferByteArray();
try
{
do { } while (source.Read(buffer, 0, buffer.Length) == buffer.Length);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
using var buffer = MemoryPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
while (source.Read(buffer.Memory.Span) > 0) { }
}
public static bool Find(this Stream source, byte[] array)
public static async Task SkipAsync(
this Stream source,
CancellationToken cancellationToken = default
)
{
var buffer = GetTransferByteArray();
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
try
{
var count = 0;
var len = source.Read(buffer, 0, buffer.Length);
do
while (true)
{
for (var i = 0; i < len; i++)
var read = await source
.ReadAsync(array, 0, array.Length, cancellationToken)
.ConfigureAwait(false);
if (read <= 0)
{
if (array[count] == buffer[i])
{
count++;
if (count == array.Length)
{
source.Position = source.Position - len + i - array.Length + 1;
return true;
}
}
break;
}
} while ((len = source.Read(buffer, 0, buffer.Length)) > 0);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
ArrayPool<byte>.Shared.Return(array);
}
return false;
}
public static DateTime DosDateToDateTime(ushort iDate, ushort iTime)
@@ -271,31 +182,12 @@ internal static class Utility
return sTime.AddSeconds(unixtime);
}
public static long TransferTo(this Stream source, Stream destination)
{
var array = GetTransferByteArray();
try
{
long total = 0;
while (ReadTransferBlock(source, array, out var count))
{
destination.Write(array, 0, count);
total += count;
}
return total;
}
finally
{
ArrayPool<byte>.Shared.Return(array);
}
}
public static long TransferTo(this Stream source, Stream destination, long maxLength)
{
var array = GetTransferByteArray();
var maxReadSize = array.Length;
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
try
{
var maxReadSize = array.Length;
long total = 0;
var remaining = maxLength;
if (remaining < maxReadSize)
@@ -331,12 +223,13 @@ internal static class Utility
IReaderExtractionListener readerExtractionListener
)
{
var array = GetTransferByteArray();
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
try
{
var iterations = 0;
long total = 0;
while (ReadTransferBlock(source, array, out var count))
int count;
while ((count = source.Read(array, 0, array.Length)) != 0)
{
total += count;
destination.Write(array, 0, count);
@@ -351,12 +244,93 @@ internal static class Utility
}
}
private static bool ReadTransferBlock(Stream source, byte[] array, out int count) =>
(count = source.Read(array, 0, array.Length)) != 0;
private static bool ReadTransferBlock(Stream source, byte[] array, int size, out int count)
public static async Task<long> TransferToAsync(
this Stream source,
Stream destination,
long maxLength,
CancellationToken cancellationToken = default
)
{
if (size > array.Length)
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
try
{
var maxReadSize = array.Length;
long total = 0;
var remaining = maxLength;
if (remaining < maxReadSize)
{
maxReadSize = (int)remaining;
}
while (
await ReadTransferBlockAsync(source, array, maxReadSize, cancellationToken)
.ConfigureAwait(false)
is var (success, count)
&& success
)
{
await destination
.WriteAsync(array, 0, count, cancellationToken)
.ConfigureAwait(false);
total += count;
if (remaining - count < 0)
{
break;
}
remaining -= count;
if (remaining < maxReadSize)
{
maxReadSize = (int)remaining;
}
}
return total;
}
finally
{
ArrayPool<byte>.Shared.Return(array);
}
}
public static async Task<long> TransferToAsync(
this Stream source,
Stream destination,
Common.Entry entry,
IReaderExtractionListener readerExtractionListener,
CancellationToken cancellationToken = default
)
{
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
try
{
var iterations = 0;
long total = 0;
int count;
while (
(
count = await source
.ReadAsync(array, 0, array.Length, cancellationToken)
.ConfigureAwait(false)
) != 0
)
{
total += count;
await destination
.WriteAsync(array, 0, count, cancellationToken)
.ConfigureAwait(false);
iterations++;
readerExtractionListener.FireEntryExtractionProgress(entry, total, iterations);
}
return total;
}
finally
{
ArrayPool<byte>.Shared.Return(array);
}
}
private static bool ReadTransferBlock(Stream source, byte[] array, int maxSize, out int count)
{
var size = maxSize;
if (maxSize > array.Length)
{
size = array.Length;
}
@@ -364,8 +338,84 @@ internal static class Utility
return count != 0;
}
private static byte[] GetTransferByteArray() => ArrayPool<byte>.Shared.Rent(81920);
private static async Task<(bool success, int count)> ReadTransferBlockAsync(
Stream source,
byte[] array,
int maxSize,
CancellationToken cancellationToken
)
{
var size = maxSize;
if (maxSize > array.Length)
{
size = array.Length;
}
var count = await source.ReadAsync(array, 0, size, cancellationToken).ConfigureAwait(false);
return (count != 0, count);
}
public static async Task SkipAsync(
this Stream source,
long advanceAmount,
CancellationToken cancellationToken = default
)
{
if (source.CanSeek)
{
source.Position += advanceAmount;
return;
}
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
try
{
while (advanceAmount > 0)
{
var toRead = (int)Math.Min(array.Length, advanceAmount);
var read = await source
.ReadAsync(array, 0, toRead, cancellationToken)
.ConfigureAwait(false);
if (read <= 0)
{
break;
}
advanceAmount -= read;
}
}
finally
{
ArrayPool<byte>.Shared.Return(array);
}
}
#if NET60_OR_GREATER
public static bool ReadFully(this Stream stream, byte[] buffer)
{
try
{
stream.ReadExactly(buffer);
return true;
}
catch (EndOfStreamException)
{
return false;
}
}
public static bool ReadFully(this Stream stream, Span<byte> buffer)
{
try
{
stream.ReadExactly(buffer);
return true;
}
catch (EndOfStreamException)
{
return false;
}
}
#else
public static bool ReadFully(this Stream stream, byte[] buffer)
{
var total = 0;
@@ -395,6 +445,7 @@ internal static class Utility
}
return (total >= buffer.Length);
}
#endif
public static string TrimNulls(this string source) => source.Replace('\0', ' ').Trim();
@@ -439,7 +490,6 @@ internal static class Utility
public static string ReplaceInvalidFileNameChars(string fileName)
{
var invalidChars = new HashSet<char>(Path.GetInvalidFileNameChars());
var sb = new StringBuilder(fileName.Length);
foreach (var c in fileName)
{

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.Writers;
@@ -22,6 +24,33 @@ public abstract class AbstractWriter(ArchiveType type, WriterOptions writerOptio
public abstract void Write(string filename, Stream source, DateTime? modificationTime);
public virtual async Task WriteAsync(
string filename,
Stream source,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
// Default implementation calls synchronous version
// Derived classes should override for true async behavior
Write(filename, source, modificationTime);
await Task.CompletedTask.ConfigureAwait(false);
}
public abstract void WriteDirectory(string directoryName, DateTime? modificationTime);
public virtual async Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
// Default implementation calls synchronous version
// Derived classes should override for true async behavior
WriteDirectory(directoryName, modificationTime);
await Task.CompletedTask.ConfigureAwait(false);
}
protected virtual void Dispose(bool isDisposing)
{
if (isDisposing)

View File

@@ -47,7 +47,10 @@ public sealed class GZipWriter : AbstractWriter
var stream = (GZipStream)OutputStream;
stream.FileName = filename;
stream.LastModified = modificationTime;
source.TransferTo(stream);
source.CopyTo(stream);
_wroteToStream = true;
}
public override void WriteDirectory(string directoryName, DateTime? modificationTime) =>
throw new NotSupportedException("GZip archives do not support directory entries.");
}

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.Writers;
@@ -8,4 +10,16 @@ public interface IWriter : IDisposable
{
ArchiveType WriterType { get; }
void Write(string filename, Stream source, DateTime? modificationTime);
Task WriteAsync(
string filename,
Stream source,
DateTime? modificationTime,
CancellationToken cancellationToken = default
);
void WriteDirectory(string directoryName, DateTime? modificationTime);
Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
);
}

View File

@@ -1,6 +1,8 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Writers;
@@ -52,4 +54,80 @@ public static class IWriterExtensions
writer.Write(file.Substring(directory.Length), file);
}
}
public static void WriteDirectory(this IWriter writer, string directoryName) =>
writer.WriteDirectory(directoryName, null);
// Async extensions
public static Task WriteAsync(
this IWriter writer,
string entryPath,
Stream source,
CancellationToken cancellationToken = default
) => writer.WriteAsync(entryPath, source, null, cancellationToken);
public static async Task WriteAsync(
this IWriter writer,
string entryPath,
FileInfo source,
CancellationToken cancellationToken = default
)
{
if (!source.Exists)
{
throw new ArgumentException("Source does not exist: " + source.FullName);
}
using var stream = source.OpenRead();
await writer
.WriteAsync(entryPath, stream, source.LastWriteTime, cancellationToken)
.ConfigureAwait(false);
}
public static Task WriteAsync(
this IWriter writer,
string entryPath,
string source,
CancellationToken cancellationToken = default
) => writer.WriteAsync(entryPath, new FileInfo(source), cancellationToken);
public static Task WriteAllAsync(
this IWriter writer,
string directory,
string searchPattern = "*",
SearchOption option = SearchOption.TopDirectoryOnly,
CancellationToken cancellationToken = default
) => writer.WriteAllAsync(directory, searchPattern, null, option, cancellationToken);
public static async Task WriteAllAsync(
this IWriter writer,
string directory,
string searchPattern = "*",
Func<string, bool>? fileSearchFunc = null,
SearchOption option = SearchOption.TopDirectoryOnly,
CancellationToken cancellationToken = default
)
{
if (!Directory.Exists(directory))
{
throw new ArgumentException("Directory does not exist: " + directory);
}
fileSearchFunc ??= n => true;
foreach (
var file in Directory
.EnumerateFiles(directory, searchPattern, option)
.Where(fileSearchFunc)
)
{
await writer
.WriteAsync(file.Substring(directory.Length), file, cancellationToken)
.ConfigureAwait(false);
}
}
public static Task WriteDirectoryAsync(
this IWriter writer,
string directoryName,
CancellationToken cancellationToken = default
) => writer.WriteDirectoryAsync(directoryName, null, cancellationToken);
}

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Tar.Headers;
using SharpCompress.Compressors;
@@ -72,6 +74,44 @@ public class TarWriter : AbstractWriter
return filename.Trim('/');
}
private string NormalizeDirectoryName(string directoryName)
{
directoryName = NormalizeFilename(directoryName);
// Ensure directory name ends with '/' for tar format
if (!string.IsNullOrEmpty(directoryName) && !directoryName.EndsWith('/'))
{
directoryName += '/';
}
return directoryName;
}
public override void WriteDirectory(string directoryName, DateTime? modificationTime)
{
var normalizedName = NormalizeDirectoryName(directoryName);
if (string.IsNullOrEmpty(normalizedName))
{
return; // Skip empty or root directory
}
var header = new TarHeader(WriterOptions.ArchiveEncoding);
header.LastModifiedTime = modificationTime ?? TarHeader.EPOCH;
header.Name = normalizedName;
header.Size = 0;
header.EntryType = EntryType.Directory;
header.Write(OutputStream);
}
public override async Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
// Synchronous implementation is sufficient for header-only write
WriteDirectory(directoryName, modificationTime);
await Task.CompletedTask.ConfigureAwait(false);
}
public void Write(string filename, Stream source, DateTime? modificationTime, long? size)
{
if (!source.CanSeek && size is null)
@@ -91,6 +131,40 @@ public class TarWriter : AbstractWriter
PadTo512(size.Value);
}
public override async Task WriteAsync(
string filename,
Stream source,
DateTime? modificationTime,
CancellationToken cancellationToken = default
) => await WriteAsync(filename, source, modificationTime, null, cancellationToken);
public async Task WriteAsync(
string filename,
Stream source,
DateTime? modificationTime,
long? size,
CancellationToken cancellationToken = default
)
{
if (!source.CanSeek && size is null)
{
throw new ArgumentException("Seekable stream is required if no size is given.");
}
var realSize = size ?? source.Length;
var header = new TarHeader(WriterOptions.ArchiveEncoding);
header.LastModifiedTime = modificationTime ?? TarHeader.EPOCH;
header.Name = NormalizeFilename(filename);
header.Size = realSize;
header.Write(OutputStream);
var written = await source
.TransferToAsync(OutputStream, realSize, cancellationToken)
.ConfigureAwait(false);
PadTo512(written);
}
private void PadTo512(long size)
{
var zeros = unchecked((int)(((size + 511L) & ~511L) - size));

View File

@@ -3,6 +3,8 @@ using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Zip;
using SharpCompress.Common.Zip.Headers;
@@ -83,7 +85,7 @@ public class ZipWriter : AbstractWriter
public void Write(string entryPath, Stream source, ZipWriterEntryOptions zipWriterEntryOptions)
{
using var output = WriteToStream(entryPath, zipWriterEntryOptions);
source.TransferTo(output);
source.CopyTo(output);
}
public Stream WriteToStream(string entryPath, ZipWriterEntryOptions options)
@@ -135,6 +137,73 @@ public class ZipWriter : AbstractWriter
return filename.Trim('/');
}
private string NormalizeDirectoryName(string directoryName)
{
directoryName = NormalizeFilename(directoryName);
// Ensure directory name ends with '/' for zip format
if (!string.IsNullOrEmpty(directoryName) && !directoryName.EndsWith('/'))
{
directoryName += '/';
}
return directoryName;
}
public override void WriteDirectory(string directoryName, DateTime? modificationTime)
{
var normalizedName = NormalizeDirectoryName(directoryName);
if (string.IsNullOrEmpty(normalizedName))
{
return; // Skip empty or root directory
}
var options = new ZipWriterEntryOptions { ModificationDateTime = modificationTime };
WriteDirectoryEntry(normalizedName, options);
}
public override async Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
// Synchronous implementation is sufficient for directory entries
WriteDirectory(directoryName, modificationTime);
await Task.CompletedTask.ConfigureAwait(false);
}
private void WriteDirectoryEntry(string directoryPath, ZipWriterEntryOptions options)
{
var compression = ZipCompressionMethod.None;
options.ModificationDateTime ??= DateTime.Now;
options.EntryComment ??= string.Empty;
var entry = new ZipCentralDirectoryEntry(
compression,
directoryPath,
(ulong)streamPosition,
WriterOptions.ArchiveEncoding
)
{
Comment = options.EntryComment,
ModificationTime = options.ModificationDateTime,
Crc = 0,
Compressed = 0,
Decompressed = 0,
};
// Use the archive default setting for zip64 and allow overrides
var useZip64 = isZip64;
if (options.EnableZip64.HasValue)
{
useZip64 = options.EnableZip64.Value;
}
var headersize = (uint)WriteHeader(directoryPath, options, entry, useZip64);
streamPosition += headersize;
entries.Add(entry);
}
private int WriteHeader(
string filename,
ZipWriterEntryOptions zipWriterEntryOptions,

View File

@@ -2,6 +2,15 @@
"version": 2,
"dependencies": {
".NETFramework,Version=v4.8": {
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==",
"dependencies": {
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.0.3, )",
@@ -23,9 +32,30 @@
},
"System.Buffers": {
"type": "Direct",
"requested": "[4.6.0, )",
"resolved": "4.6.0",
"contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA=="
"requested": "[4.6.1, )",
"resolved": "4.6.1",
"contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
},
"System.Memory": {
"type": "Direct",
"requested": "[4.6.3, )",
"resolved": "4.6.3",
"contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
"dependencies": {
"System.Buffers": "4.6.1",
"System.Numerics.Vectors": "4.6.1",
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
},
"System.Text.Encoding.CodePages": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==",
"dependencies": {
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"ZstdSharp.Port": {
"type": "Direct",
@@ -55,13 +85,13 @@
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
"resolved": "4.6.1",
"contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
"resolved": "6.1.2",
"contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
},
"System.Threading.Tasks.Extensions": {
"type": "Transitive",
@@ -70,29 +100,18 @@
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
}
},
".NETFramework,Version=v4.8.1": {
"Microsoft.Bcl.AsyncInterfaces": {
"type": "CentralTransitive",
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "5.0.0",
"contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==",
"resolved": "8.0.0",
"contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==",
"dependencies": {
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"System.Memory": {
"type": "CentralTransitive",
"requested": "[4.6.0, )",
"resolved": "4.5.5",
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.5.0",
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
}
},
".NETFramework,Version=v4.8.1": {
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.0.3, )",
@@ -114,9 +133,30 @@
},
"System.Buffers": {
"type": "Direct",
"requested": "[4.6.0, )",
"resolved": "4.6.0",
"contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA=="
"requested": "[4.6.1, )",
"resolved": "4.6.1",
"contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
},
"System.Memory": {
"type": "Direct",
"requested": "[4.6.3, )",
"resolved": "4.6.3",
"contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
"dependencies": {
"System.Buffers": "4.6.1",
"System.Numerics.Vectors": "4.6.1",
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
},
"System.Text.Encoding.CodePages": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==",
"dependencies": {
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"ZstdSharp.Port": {
"type": "Direct",
@@ -146,13 +186,13 @@
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
"resolved": "4.6.1",
"contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
"resolved": "6.1.2",
"contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
},
"System.Threading.Tasks.Extensions": {
"type": "Transitive",
@@ -161,26 +201,6 @@
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "CentralTransitive",
"requested": "[8.0.0, )",
"resolved": "5.0.0",
"contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==",
"dependencies": {
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"System.Memory": {
"type": "CentralTransitive",
"requested": "[4.6.0, )",
"resolved": "4.5.5",
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.5.0",
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
}
},
".NETStandard,Version=v2.0": {
@@ -193,15 +213,6 @@
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"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, )",
@@ -221,21 +232,15 @@
"Microsoft.NETCore.Platforms": "1.1.0"
}
},
"System.Buffers": {
"type": "Direct",
"requested": "[4.6.0, )",
"resolved": "4.6.0",
"contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA=="
},
"System.Memory": {
"type": "Direct",
"requested": "[4.6.0, )",
"resolved": "4.6.0",
"contentHash": "OEkbBQoklHngJ8UD8ez2AERSk2g+/qpAaSWWCBFbpH727HxDq5ydVkuncBaKcKfwRqXGWx64dS6G1SUScMsitg==",
"requested": "[4.6.3, )",
"resolved": "4.6.3",
"contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
"dependencies": {
"System.Buffers": "4.6.0",
"System.Numerics.Vectors": "4.6.0",
"System.Runtime.CompilerServices.Unsafe": "6.1.0"
"System.Buffers": "4.6.1",
"System.Numerics.Vectors": "4.6.1",
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
},
"System.Text.Encoding.CodePages": {
@@ -269,11 +274,6 @@
"resolved": "1.1.0",
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
},
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
@@ -281,13 +281,13 @@
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.6.0",
"contentHash": "t+SoieZsRuEyiw/J+qXUbolyO219tKQQI0+2/YI+Qv7YdGValA6WiuokrNKqjrTNsy5ABWU11bdKOzUdheteXg=="
"resolved": "4.6.1",
"contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.1.0",
"contentHash": "5o/HZxx6RVqYlhKSq8/zronDkALJZUT2Vz0hx43f0gwe8mwlM0y2nYlqdBwLMzr262Bwvpikeb/yEwkAa5PADg=="
"resolved": "6.1.2",
"contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
},
"System.Threading.Tasks.Extensions": {
"type": "Transitive",
@@ -296,18 +296,15 @@
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
"System.Buffers": {
"type": "CentralTransitive",
"requested": "[4.6.1, )",
"resolved": "4.6.1",
"contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
}
},
"net6.0": {
"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, )",
@@ -318,12 +315,6 @@
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"System.Buffers": {
"type": "Direct",
"requested": "[4.6.0, )",
"resolved": "4.6.0",
"contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA=="
},
"ZstdSharp.Port": {
"type": "Direct",
"requested": "[0.8.6, )",
@@ -335,11 +326,6 @@
"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",
@@ -353,15 +339,6 @@
"resolved": "8.0.17",
"contentHash": "x5/y4l8AtshpBOrCZdlE4txw8K3e3s9meBFeZeR3l8hbbku2V7kK6ojhXvrbjg1rk3G+JqL1BI26gtgc1ZrdUw=="
},
"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, )",
@@ -372,12 +349,6 @@
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"System.Buffers": {
"type": "Direct",
"requested": "[4.6.0, )",
"resolved": "4.6.0",
"contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA=="
},
"ZstdSharp.Port": {
"type": "Direct",
"requested": "[0.8.6, )",
@@ -389,11 +360,6 @@
"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",

View File

@@ -0,0 +1,49 @@
using System;
using JetBrains.Profiler.SelfApi;
namespace SharpCompress.Test;
public static class JetbrainsProfiler
{
private sealed class CpuClass : IDisposable
{
public CpuClass(string snapshotPath)
{
DotTrace.Init();
var config2 = new DotTrace.Config();
config2.SaveToDir(snapshotPath);
DotTrace.Attach(config2);
DotTrace.StartCollectingData();
}
public void Dispose()
{
DotTrace.StopCollectingData();
DotTrace.SaveData();
DotTrace.Detach();
}
}
private sealed class MemoryClass : IDisposable
{
public MemoryClass(string snapshotPath)
{
DotMemory.Init();
var config = new DotMemory.Config();
config.UseLogLevelVerbose();
config.SaveToDir(snapshotPath);
DotMemory.Attach(config);
DotMemory.GetSnapshot("Before");
}
public void Dispose()
{
DotMemory.GetSnapshot("After");
DotMemory.Detach();
}
}
public static IDisposable Cpu(string snapshotPath) => new CpuClass(snapshotPath);
public static IDisposable Memory(string snapshotPath) => new MemoryClass(snapshotPath);
}

View File

@@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace SharpCompress.Performance;
/// <summary>
/// A Stream implementation backed by a List of byte arrays that supports large position values.
/// This allows handling streams larger than typical 32-bit or even standard 64-bit constraints
/// by chunking data into multiple byte array segments.
/// </summary>
public class LargeMemoryStream : Stream
{
private readonly List<byte[]> _chunks;
private readonly int _chunkSize;
private long _position;
private bool _isDisposed;
/// <summary>
/// Initializes a new instance of the LargeMemoryStream class.
/// </summary>
/// <param name="chunkSize">The size of each chunk in the backing byte array list. Defaults to 1MB.</param>
public LargeMemoryStream(int chunkSize = 1024 * 1024)
{
if (chunkSize <= 0)
throw new ArgumentException("Chunk size must be greater than zero.", nameof(chunkSize));
_chunks = new List<byte[]>();
_chunkSize = chunkSize;
_position = 0;
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => true;
public override long Length
{
get
{
ThrowIfDisposed();
if (_chunks.Count == 0)
return 0;
long length = (long)(_chunks.Count - 1) * _chunkSize;
length += _chunks[_chunks.Count - 1].Length;
return length;
}
}
public override long Position
{
get
{
ThrowIfDisposed();
return _position;
}
set
{
ThrowIfDisposed();
if (value < 0)
throw new ArgumentOutOfRangeException(
nameof(value),
"Position cannot be negative."
);
_position = value;
}
}
public override void Flush()
{
ThrowIfDisposed();
// No-op for in-memory stream
}
public override int Read(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (offset < 0 || count < 0 || offset + count > buffer.Length)
throw new ArgumentOutOfRangeException();
long length = Length;
if (_position >= length)
return 0;
int bytesToRead = (int)Math.Min(count, length - _position);
int bytesRead = 0;
while (bytesRead < bytesToRead)
{
long chunkIndex = _position / _chunkSize;
int chunkOffset = (int)(_position % _chunkSize);
if (chunkIndex >= _chunks.Count)
break;
byte[] chunk = _chunks[(int)chunkIndex];
int availableInChunk = chunk.Length - chunkOffset;
int bytesToCopyFromChunk = Math.Min(availableInChunk, bytesToRead - bytesRead);
Array.Copy(chunk, chunkOffset, buffer, offset + bytesRead, bytesToCopyFromChunk);
_position += bytesToCopyFromChunk;
bytesRead += bytesToCopyFromChunk;
}
return bytesRead;
}
public override void Write(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (offset < 0 || count < 0 || offset + count > buffer.Length)
throw new ArgumentOutOfRangeException();
int bytesWritten = 0;
while (bytesWritten < count)
{
long chunkIndex = _position / _chunkSize;
int chunkOffset = (int)(_position % _chunkSize);
// Ensure we have enough chunks
while (_chunks.Count <= chunkIndex)
{
_chunks.Add(new byte[_chunkSize]);
}
byte[] chunk = _chunks[(int)chunkIndex];
int availableInChunk = chunk.Length - chunkOffset;
int bytesToCopyToChunk = Math.Min(availableInChunk, count - bytesWritten);
Array.Copy(buffer, offset + bytesWritten, chunk, chunkOffset, bytesToCopyToChunk);
_position += bytesToCopyToChunk;
bytesWritten += bytesToCopyToChunk;
}
}
public override long Seek(long offset, SeekOrigin origin)
{
ThrowIfDisposed();
long newPosition = origin switch
{
SeekOrigin.Begin => offset,
SeekOrigin.Current => _position + offset,
SeekOrigin.End => Length + offset,
_ => throw new ArgumentOutOfRangeException(nameof(origin)),
};
if (newPosition < 0)
throw new ArgumentOutOfRangeException(
nameof(offset),
"Cannot seek before the beginning of the stream."
);
_position = newPosition;
return _position;
}
public override void SetLength(long value)
{
ThrowIfDisposed();
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "Length cannot be negative.");
long currentLength = Length;
if (value < currentLength)
{
// Truncate
long chunkIndex = (value + _chunkSize - 1) / _chunkSize;
if (chunkIndex > 0)
chunkIndex--;
_chunks.RemoveRange((int)(chunkIndex + 1), _chunks.Count - (int)(chunkIndex + 1));
if (chunkIndex < _chunks.Count)
{
int lastChunkSize = (int)(value - chunkIndex * _chunkSize);
var x = _chunks[(int)chunkIndex];
Array.Resize(ref x, lastChunkSize);
}
if (_position > value)
_position = value;
}
else if (value > currentLength)
{
// Extend with zeros
long chunkIndex = currentLength / _chunkSize;
int chunkOffset = (int)(currentLength % _chunkSize);
while ((long)_chunks.Count * _chunkSize < value)
{
_chunks.Add(new byte[_chunkSize]);
}
// Resize the last chunk if needed
if (_chunks.Count > 0)
{
long lastChunkNeededSize = value - (long)(_chunks.Count - 1) * _chunkSize;
if (lastChunkNeededSize < _chunkSize)
{
var x = _chunks[^1];
Array.Resize(ref x, (int)lastChunkNeededSize);
}
}
}
}
/// <summary>
/// Gets the number of chunks in the backing list.
/// </summary>
public int ChunkCount => _chunks.Count;
/// <summary>
/// Gets the size of each chunk in bytes.
/// </summary>
public int ChunkSize => _chunkSize;
/// <summary>
/// Converts the stream contents to a single byte array.
/// This may consume significant memory for large streams.
/// </summary>
public byte[] ToArray()
{
ThrowIfDisposed();
long length = Length;
byte[] result = new byte[length];
long currentPosition = _position;
try
{
_position = 0;
int totalRead = 0;
while (totalRead < length)
{
int bytesToRead = (int)Math.Min(length - totalRead, int.MaxValue);
int bytesRead = Read(result, totalRead, bytesToRead);
if (bytesRead == 0)
break;
totalRead += bytesRead;
}
}
finally
{
_position = currentPosition;
}
return result;
}
private void ThrowIfDisposed()
{
if (_isDisposed)
throw new ObjectDisposedException(GetType().Name);
}
protected override void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_chunks.Clear();
}
_isDisposed = true;
}
base.Dispose(disposing);
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Performance;
using SharpCompress.Readers;
using SharpCompress.Test;
var index = AppDomain.CurrentDomain.BaseDirectory.IndexOf(
"SharpCompress.Performance",
StringComparison.OrdinalIgnoreCase
);
var path = AppDomain.CurrentDomain.BaseDirectory.Substring(0, index);
var SOLUTION_BASE_PATH = Path.GetDirectoryName(path) ?? throw new ArgumentNullException();
var TEST_ARCHIVES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "Archives");
//using var _ = JetbrainsProfiler.Memory($"/Users/adam/temp/");
using (var __ = JetbrainsProfiler.Cpu($"/Users/adam/temp/"))
{
var testArchives = new[]
{
"Rar.Audio_program.rar",
//"64bitstream.zip.7z",
//"TarWithSymlink.tar.gz"
};
var arcs = testArchives.Select(a => Path.Combine(TEST_ARCHIVES_PATH, a)).ToArray();
for (int i = 0; i < 50; i++)
{
using var found = ArchiveFactory.Open(arcs[0]);
foreach (var entry in found.Entries.Where(entry => !entry.IsDirectory))
{
Console.WriteLine($"Extracting {entry.Key}");
using var entryStream = entry.OpenEntryStream();
entryStream.CopyTo(Stream.Null);
}
/*using var found = ReaderFactory.Open(arcs[0]);
while (found.MoveToNextEntry())
{
var entry = found.Entry;
if (entry.IsDirectory)
continue;
Console.WriteLine($"Extracting {entry.Key}");
found.WriteEntryTo(Stream.Null);
}*/
}
Console.WriteLine("Still running...");
}
await Task.Delay(500);

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Profiler.SelfApi" />
<ProjectReference Include="..\..\src\SharpCompress\SharpCompress.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,50 @@
{
"version": 2,
"dependencies": {
"net8.0": {
"JetBrains.Profiler.SelfApi": {
"type": "Direct",
"requested": "[2.5.14, )",
"resolved": "2.5.14",
"contentHash": "9+NcTe49B2M8/MOledSxKZkQKqavFf5xXZw4JL4bVu/KYiw6OOaD6cDQmNGSO18yUP/WoBXsXGKmZ9VOpmyadw==",
"dependencies": {
"JetBrains.HabitatDetector": "1.4.5",
"JetBrains.Profiler.Api": "1.4.10"
}
},
"JetBrains.FormatRipper": {
"type": "Transitive",
"resolved": "2.4.0",
"contentHash": "k5eGab1DArJH0k94ZO9oxDxg8go1KvR1oPGPzyVvfplEHetgrc2hGZ6Cken8fVsdS/Xp3hMnHd9L5MXb7JJM4A=="
},
"JetBrains.HabitatDetector": {
"type": "Transitive",
"resolved": "1.4.5",
"contentHash": "5kb1G32O8fmlS2QnJLycEnHbq9ukuDUHQll4mqOAPLEE1JEJcz12W6cTt1CMpQY3n/6R0jZAhmBvaJm2zixvow==",
"dependencies": {
"JetBrains.FormatRipper": "2.4.0"
}
},
"JetBrains.Profiler.Api": {
"type": "Transitive",
"resolved": "1.4.10",
"contentHash": "XBynPGDiWB6uWoiVwkki3uUsXqc66lRC1YX8LWYWc579ioJSB5OzZ8KsRK2q+eawj3OxrkeCsgXlb6mwBkCebQ==",
"dependencies": {
"JetBrains.HabitatDetector": "1.4.5"
}
},
"sharpcompress": {
"type": "Project",
"dependencies": {
"ZstdSharp.Port": "[0.8.6, )"
}
},
"ZstdSharp.Port": {
"type": "CentralTransitive",
"requested": "[0.8.6, )",
"resolved": "0.8.6",
"contentHash": "iP4jVLQoQmUjMU88g1WObiNr6YKZGvh4aOXn3yOJsHqZsflwRsxZPcIBvNXgjXO3vQKSLctXGLTpcBPLnWPS8A=="
}
}
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Compressors.Xz;
@@ -255,16 +256,10 @@ public class ArchiveTests : ReaderTests
protected void ArchiveExtractToDirectory(
string testArchive,
ReaderOptions? readerOptions = null
) => ArchiveExtractToDirectory(ArchiveFactory.AutoFactory, testArchive, readerOptions);
protected void ArchiveExtractToDirectory(
IArchiveFactory archiveFactory,
string testArchive,
ReaderOptions? readerOptions = null
)
{
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
using (var archive = archiveFactory.Open(new FileInfo(testArchive), readerOptions))
using (var archive = ArchiveFactory.Open(new FileInfo(testArchive), readerOptions))
{
archive.ExtractToDirectory(SCRATCH_FILES_PATH);
}
@@ -342,13 +337,12 @@ public class ArchiveTests : ReaderTests
{
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
using var archive = ArchiveFactory.Open(testArchive);
using var reader = archive.ExtractAllEntries();
while (reader.MoveToNextEntry())
foreach (var entry in archive.Entries)
{
if (!reader.Entry.IsDirectory)
if (!entry.IsDirectory)
{
var memory = new MemoryStream();
reader.WriteEntryTo(memory);
entry.WriteTo(memory);
memory.Position = 0;
@@ -576,4 +570,56 @@ public class ArchiveTests : ReaderTests
return (extractedData, crc);
}
protected async Task ArchiveStreamReadAsync(
string testArchive,
ReaderOptions? readerOptions = null
)
{
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
await ArchiveStreamReadAsync(
ArchiveFactory.AutoFactory,
readerOptions,
new[] { testArchive }
);
}
protected async Task ArchiveStreamReadAsync(
IArchiveFactory archiveFactory,
ReaderOptions? readerOptions,
IEnumerable<string> testArchives
)
{
foreach (var path in testArchives)
{
using (
var stream = SharpCompressStream.Create(
File.OpenRead(path),
leaveOpen: true,
throwOnDispose: true
)
)
using (var archive = archiveFactory.Open(stream, readerOptions))
{
try
{
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
await entry.WriteToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
catch (IndexOutOfRangeException)
{
//SevenZipArchive_BZip2_Split test needs this
stream.ThrowOnDispose = false;
throw;
}
stream.ThrowOnDispose = false;
}
VerifyFiles();
}
}
}

View File

@@ -0,0 +1,116 @@
using System;
using SharpCompress.Common;
using SharpCompress.Compressors.Deflate;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Compressors.Xz;
using Xunit;
namespace SharpCompress.Test;
public class ExceptionHierarchyTests
{
[Fact]
public void AllSharpCompressExceptions_InheritFromSharpCompressException()
{
// Verify that ArchiveException inherits from SharpCompressException
Assert.True(typeof(SharpCompressException).IsAssignableFrom(typeof(ArchiveException)));
// Verify that ExtractionException inherits from SharpCompressException
Assert.True(typeof(SharpCompressException).IsAssignableFrom(typeof(ExtractionException)));
// Verify that InvalidFormatException inherits from SharpCompressException (through ExtractionException)
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(InvalidFormatException))
);
// Verify that CryptographicException inherits from SharpCompressException
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(CryptographicException))
);
// Verify that IncompleteArchiveException inherits from SharpCompressException (through ArchiveException)
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(IncompleteArchiveException))
);
// Verify that ReaderCancelledException inherits from SharpCompressException
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(ReaderCancelledException))
);
// Verify that MultipartStreamRequiredException inherits from SharpCompressException (through ExtractionException)
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(
typeof(MultipartStreamRequiredException)
)
);
// Verify that MultiVolumeExtractionException inherits from SharpCompressException (through ExtractionException)
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(MultiVolumeExtractionException))
);
// Verify that ZlibException inherits from SharpCompressException
Assert.True(typeof(SharpCompressException).IsAssignableFrom(typeof(ZlibException)));
// Verify that XZIndexMarkerReachedException inherits from SharpCompressException
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(XZIndexMarkerReachedException))
);
}
[Fact]
public void SharpCompressException_CanBeCaughtByBaseType()
{
// Test that a derived exception can be caught as SharpCompressException
var exception = new InvalidFormatException("Test message");
var caughtException = false;
try
{
throw exception;
}
catch (SharpCompressException ex)
{
caughtException = true;
Assert.Same(exception, ex);
}
Assert.True(caughtException, "Exception should have been caught as SharpCompressException");
}
[Fact]
public void InternalLzmaExceptions_InheritFromSharpCompressException()
{
// Use reflection to verify internal exception types
var dataErrorExceptionType = Type.GetType(
"SharpCompress.Compressors.LZMA.DataErrorException, SharpCompress"
);
Assert.NotNull(dataErrorExceptionType);
Assert.True(typeof(SharpCompressException).IsAssignableFrom(dataErrorExceptionType));
var invalidParamExceptionType = Type.GetType(
"SharpCompress.Compressors.LZMA.InvalidParamException, SharpCompress"
);
Assert.NotNull(invalidParamExceptionType);
Assert.True(typeof(SharpCompressException).IsAssignableFrom(invalidParamExceptionType));
}
[Fact]
public void ExceptionConstructors_WorkCorrectly()
{
// Test parameterless constructor
var ex1 = new SharpCompressException();
Assert.NotNull(ex1);
// Test message constructor
var ex2 = new SharpCompressException("Test message");
Assert.Equal("Test message", ex2.Message);
// Test message and inner exception constructor
var inner = new InvalidOperationException("Inner");
var ex3 = new SharpCompressException("Test message", inner);
Assert.Equal("Test message", ex3.Message);
Assert.Same(inner, ex3.InnerException);
}
}

View File

@@ -0,0 +1,99 @@
using System;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Xunit;
namespace SharpCompress.Test;
public class ExtractionTests : TestBase
{
[Fact]
public void Extraction_ShouldHandleCaseInsensitivePathsOnWindows()
{
// This test validates that extraction succeeds when Path.GetFullPath returns paths
// with casing that matches the platform's file system behavior. On Windows,
// Path.GetFullPath can return different casing than the actual directory on disk
// (e.g., "system32" vs "System32"), and the extraction should succeed because
// Windows file systems are case-insensitive. On Unix-like systems, this test
// verifies that the case-sensitive comparison is used correctly.
var testArchive = Path.Combine(SCRATCH2_FILES_PATH, "test-extraction.zip");
var extractPath = SCRATCH_FILES_PATH;
// Create a simple test archive with a single file
using (var stream = File.Create(testArchive))
{
using var writer = (ZipWriter)
WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate);
// Create a test file to add to the archive
var testFilePath = Path.Combine(SCRATCH2_FILES_PATH, "testfile.txt");
File.WriteAllText(testFilePath, "Test content");
writer.Write("testfile.txt", testFilePath);
}
// Extract the archive - this should succeed regardless of path casing
using (var stream = File.OpenRead(testArchive))
{
using var reader = ReaderFactory.Open(stream);
// This should not throw an exception even if Path.GetFullPath returns
// a path with different casing than the actual directory
var exception = Record.Exception(() =>
reader.WriteAllToDirectory(
extractPath,
new ExtractionOptions { ExtractFullPath = false, Overwrite = true }
)
);
Assert.Null(exception);
}
// Verify the file was extracted successfully
var extractedFile = Path.Combine(extractPath, "testfile.txt");
Assert.True(File.Exists(extractedFile));
Assert.Equal("Test content", File.ReadAllText(extractedFile));
}
[Fact]
public void Extraction_ShouldPreventPathTraversalAttacks()
{
// This test ensures that the security check still works to prevent
// path traversal attacks (e.g., using "../" to escape the destination directory)
var testArchive = Path.Combine(SCRATCH2_FILES_PATH, "test-traversal.zip");
var extractPath = SCRATCH_FILES_PATH;
// Create a test archive with a path traversal attempt
using (var stream = File.Create(testArchive))
{
using var writer = (ZipWriter)
WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate);
var testFilePath = Path.Combine(SCRATCH2_FILES_PATH, "testfile2.txt");
File.WriteAllText(testFilePath, "Test content");
// Try to write with a path that attempts to escape the destination directory
writer.Write("../../evil.txt", testFilePath);
}
// Extract the archive - this should throw an exception for path traversal
using (var stream = File.OpenRead(testArchive))
{
using var reader = ReaderFactory.Open(stream);
var exception = Assert.Throws<ExtractionException>(() =>
reader.WriteAllToDirectory(
extractPath,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
)
);
Assert.Contains("outside of the destination", exception.Message);
}
}
}

View File

@@ -0,0 +1,238 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Writers;
using Xunit;
namespace SharpCompress.Test.GZip;
public class AsyncTests : TestBase
{
[Fact]
public async Task Reader_Async_Extract_All()
{
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
#if NETFRAMEWORK
using var stream = File.OpenRead(testArchive);
#else
await using var stream = File.OpenRead(testArchive);
#endif
using var reader = ReaderFactory.Open(stream);
await reader.WriteAllToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
// Just verify some files were extracted
var extractedFiles = Directory.GetFiles(
SCRATCH_FILES_PATH,
"*",
SearchOption.AllDirectories
);
Assert.True(extractedFiles.Length > 0, "No files were extracted");
}
[Fact]
public async Task Reader_Async_Extract_Single_Entry()
{
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
#if NETFRAMEWORK
using var stream = File.OpenRead(testArchive);
#else
await using var stream = File.OpenRead(testArchive);
#endif
using var reader = ReaderFactory.Open(stream);
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
var outputPath = Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key!);
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
#if NETFRAMEWORK
using var outputStream = File.Create(outputPath);
#else
await using var outputStream = File.Create(outputPath);
#endif
await reader.WriteEntryToAsync(outputStream);
break; // Just test one entry
}
}
}
[Fact]
public async Task Archive_Entry_Async_Open_Stream()
{
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
using var archive = ArchiveFactory.Open(testArchive);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory).Take(1))
{
#if NETFRAMEWORK
using var entryStream = await entry.OpenEntryStreamAsync();
#else
await using var entryStream = await entry.OpenEntryStreamAsync();
#endif
Assert.NotNull(entryStream);
Assert.True(entryStream.CanRead);
// Read some data to verify it works
var buffer = new byte[1024];
var read = await entryStream.ReadAsync(buffer, 0, buffer.Length);
Assert.True(read > 0);
}
}
[Fact]
public async Task Writer_Async_Write_Single_File()
{
var outputPath = Path.Combine(SCRATCH_FILES_PATH, "async_test.zip");
using (var stream = File.Create(outputPath))
using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
{
var testFile = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
using var fileStream = File.OpenRead(testFile);
await writer.WriteAsync("test_entry.bin", fileStream, new DateTime(2023, 1, 1));
}
// Verify the archive was created and contains the entry
Assert.True(File.Exists(outputPath));
using var archive = ZipArchive.Open(outputPath);
Assert.Single(archive.Entries.Where(e => !e.IsDirectory));
}
[Fact]
public async Task Async_With_Cancellation_Token()
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(10000); // 10 seconds should be plenty
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
using var stream = File.OpenRead(testArchive);
using var reader = ReaderFactory.Open(stream);
await reader.WriteAllToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
cts.Token
);
// Just verify some files were extracted
var extractedFiles = Directory.GetFiles(
SCRATCH_FILES_PATH,
"*",
SearchOption.AllDirectories
);
Assert.True(extractedFiles.Length > 0, "No files were extracted");
}
[Fact]
public async Task Stream_Extensions_Async()
{
var testFile = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
using var inputStream = File.OpenRead(testFile);
var outputPath = Path.Combine(SCRATCH_FILES_PATH, "async_copy.bin");
using var outputStream = File.Create(outputPath);
// Test the async extension method
var buffer = new byte[8192];
int bytesRead;
while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await outputStream.WriteAsync(buffer, 0, bytesRead);
}
Assert.True(File.Exists(outputPath));
Assert.True(new FileInfo(outputPath).Length > 0);
}
[Fact]
public async Task EntryStream_ReadAsync_Works()
{
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
using var stream = File.OpenRead(testArchive);
using var reader = ReaderFactory.Open(stream);
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
using var entryStream = reader.OpenEntryStream();
var buffer = new byte[4096];
var totalRead = 0;
int bytesRead;
// Test ReadAsync on EntryStream
while ((bytesRead = await entryStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
totalRead += bytesRead;
}
Assert.True(totalRead > 0, "Should have read some data from entry stream");
break; // Test just one entry
}
}
}
[Fact]
public async Task CompressionStream_Async_ReadWrite()
{
var testData = new byte[1024];
new Random(42).NextBytes(testData);
var compressedPath = Path.Combine(SCRATCH_FILES_PATH, "async_compressed.gz");
// Test async write with GZipStream
using (var fileStream = File.Create(compressedPath))
using (
var gzipStream = new Compressors.Deflate.GZipStream(
fileStream,
Compressors.CompressionMode.Compress
)
)
{
await gzipStream.WriteAsync(testData, 0, testData.Length);
await gzipStream.FlushAsync();
}
Assert.True(File.Exists(compressedPath));
Assert.True(new FileInfo(compressedPath).Length > 0);
// Test async read with GZipStream
using (var fileStream = File.OpenRead(compressedPath))
using (
var gzipStream = new Compressors.Deflate.GZipStream(
fileStream,
Compressors.CompressionMode.Decompress
)
)
{
var decompressed = new byte[testData.Length];
var totalRead = 0;
int bytesRead;
while (
totalRead < decompressed.Length
&& (
bytesRead = await gzipStream.ReadAsync(
decompressed,
totalRead,
decompressed.Length - totalRead
)
) > 0
)
{
totalRead += bytesRead;
}
Assert.Equal(testData.Length, totalRead);
Assert.Equal(testData, decompressed);
}
}
}

View File

@@ -0,0 +1,127 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.GZip;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipArchiveAsyncTests : ArchiveTests
{
public GZipArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async Task GZip_Archive_Generic_Async()
{
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")))
using (var archive = ArchiveFactory.Open(stream))
{
var entry = archive.Entries.First();
await entry.WriteToFileAsync(Path.Combine(SCRATCH_FILES_PATH, entry.Key.NotNull()));
var size = entry.Size;
var scratch = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"));
var test = new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
Assert.Equal(size, scratch.Length);
Assert.Equal(size, test.Length);
}
CompareArchivesByPath(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"),
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")
);
}
[Fact]
public async Task GZip_Archive_Async()
{
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")))
using (var archive = GZipArchive.Open(stream))
{
var entry = archive.Entries.First();
await entry.WriteToFileAsync(Path.Combine(SCRATCH_FILES_PATH, entry.Key.NotNull()));
var size = entry.Size;
var scratch = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"));
var test = new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
Assert.Equal(size, scratch.Length);
Assert.Equal(size, test.Length);
}
CompareArchivesByPath(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"),
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")
);
}
[Fact]
public async Task GZip_Archive_NoAdd_Async()
{
var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg");
using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
using var archive = GZipArchive.Open(stream);
Assert.Throws<InvalidFormatException>(() => archive.AddEntry("jpg\\test.jpg", jpg));
await archive.SaveToAsync(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"));
}
[Fact]
public async Task GZip_Archive_Multiple_Reads_Async()
{
var inputStream = new MemoryStream();
using (var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")))
{
await fileStream.CopyToAsync(inputStream);
inputStream.Position = 0;
}
using var archive = GZipArchive.Open(inputStream);
var archiveEntry = archive.Entries.First();
MemoryStream tarStream;
using (var entryStream = archiveEntry.OpenEntryStream())
{
tarStream = new MemoryStream();
await entryStream.CopyToAsync(tarStream);
}
var size = tarStream.Length;
using (var entryStream = archiveEntry.OpenEntryStream())
{
tarStream = new MemoryStream();
await entryStream.CopyToAsync(tarStream);
}
Assert.Equal(size, tarStream.Length);
using (var entryStream = archiveEntry.OpenEntryStream())
{
var result = TarArchive.IsTarFile(entryStream);
Assert.True(result);
}
Assert.Equal(size, tarStream.Length);
using (var entryStream = archiveEntry.OpenEntryStream())
{
tarStream = new MemoryStream();
await entryStream.CopyToAsync(tarStream);
}
Assert.Equal(size, tarStream.Length);
}
[Fact]
public void TestGzCrcWithMostSignificantBitNotNegative_Async()
{
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
using var archive = GZipArchive.Open(stream);
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
Assert.InRange(entry.Crc, 0L, 0xFFFFFFFFL);
}
}
[Fact]
public void TestGzArchiveTypeGzip_Async()
{
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
using var archive = GZipArchive.Open(stream);
Assert.Equal(archive.Type, ArchiveType.GZip);
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.IO;
using SharpCompress.Archives.GZip;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipArchiveDirectoryTests : TestBase
{
[Fact]
public void GZipArchive_AddDirectoryEntry_ThrowsNotSupportedException()
{
using var archive = GZipArchive.Create();
Assert.Throws<NotSupportedException>(() =>
archive.AddDirectoryEntry("test-dir", DateTime.Now)
);
}
}

View File

@@ -1,3 +1,4 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives;
@@ -124,4 +125,60 @@ public class GZipArchiveTests : ArchiveTests
using var archive = GZipArchive.Open(stream);
Assert.Equal(archive.Type, ArchiveType.GZip);
}
[Fact]
public void GZip_Archive_NonSeekableStream()
{
// Test that GZip extraction works with non-seekable streams (like HttpBaseStream)
using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
var buffer = new MemoryStream();
fileStream.CopyTo(buffer);
buffer.Position = 0;
// Create a non-seekable wrapper around the MemoryStream
using var nonSeekableStream = new NonSeekableStream(buffer);
using var reader = SharpCompress.Readers.GZip.GZipReader.Open(nonSeekableStream);
// Verify we can move to the first entry and read it without exceptions
Assert.True(reader.MoveToNextEntry());
Assert.NotNull(reader.Entry);
// Extract and verify the entry can be read
using var outputStream = new MemoryStream();
reader.WriteEntryTo(outputStream);
Assert.True(outputStream.Length > 0);
}
// Helper class to simulate a non-seekable stream like HttpBaseStream
private class NonSeekableStream : Stream
{
private readonly Stream _baseStream;
public NonSeekableStream(Stream baseStream) => _baseStream = baseStream;
public override bool CanRead => _baseStream.CanRead;
public override bool CanSeek => false; // Simulate non-seekable stream
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush() => _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) =>
throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
}
}

View File

@@ -0,0 +1,99 @@
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Readers.GZip;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipReaderAsyncTests : ReaderTests
{
public GZipReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async Task GZip_Reader_Generic_Async() =>
await ReadAsync("Tar.tar.gz", CompressionType.GZip);
[Fact]
public async Task GZip_Reader_Generic2_Async()
{
//read only as GZip item
using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
using var reader = GZipReader.Open(new SharpCompressStream(stream));
while (reader.MoveToNextEntry())
{
Assert.NotEqual(0, reader.Entry.Size);
Assert.NotEqual(0, reader.Entry.Crc);
// Use async overload for reading the entry
if (!reader.Entry.IsDirectory)
{
using var entryStream = reader.OpenEntryStream();
using var ms = new MemoryStream();
await entryStream.CopyToAsync(ms);
}
}
}
protected async Task ReadAsync(
string testArchive,
CompressionType expectedCompression,
ReaderOptions? options = null
)
{
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
options ??= new ReaderOptions() { BufferSize = 0x20000 };
options.LeaveStreamOpen = true;
await ReadImplAsync(testArchive, expectedCompression, options);
options.LeaveStreamOpen = false;
await ReadImplAsync(testArchive, expectedCompression, options);
VerifyFiles();
}
private async Task ReadImplAsync(
string testArchive,
CompressionType expectedCompression,
ReaderOptions options
)
{
using var file = File.OpenRead(testArchive);
using var protectedStream = SharpCompressStream.Create(
new ForwardOnlyStream(file, options.BufferSize),
leaveOpen: true,
throwOnDispose: true,
bufferSize: options.BufferSize
);
using var testStream = new TestStream(protectedStream);
using (var reader = ReaderFactory.Open(testStream, options))
{
await UseReaderAsync(reader, expectedCompression);
protectedStream.ThrowOnDispose = false;
Assert.False(testStream.IsDisposed, $"{nameof(testStream)} prematurely closed");
}
var message =
$"{nameof(options.LeaveStreamOpen)} is set to '{options.LeaveStreamOpen}', so {nameof(testStream.IsDisposed)} should be set to '{!testStream.IsDisposed}', but is set to {testStream.IsDisposed}";
Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message);
}
private async Task UseReaderAsync(IReader reader, CompressionType expectedCompression)
{
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(expectedCompression, reader.Entry.CompressionType);
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
}
}

View File

@@ -0,0 +1,83 @@
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Writers;
using SharpCompress.Writers.GZip;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipWriterAsyncTests : WriterTests
{
public GZipWriterAsyncTests()
: base(ArchiveType.GZip) => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async Task GZip_Writer_Generic_Async()
{
using (
Stream stream = File.Open(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
FileMode.OpenOrCreate,
FileAccess.Write
)
)
using (var writer = WriterFactory.Open(stream, ArchiveType.GZip, CompressionType.GZip))
{
await writer.WriteAsync("Tar.tar", Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
}
CompareArchivesByPath(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")
);
}
[Fact]
public async Task GZip_Writer_Async()
{
using (
Stream stream = File.Open(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
FileMode.OpenOrCreate,
FileAccess.Write
)
)
using (var writer = new GZipWriter(stream))
{
await writer.WriteAsync("Tar.tar", Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
}
CompareArchivesByPath(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")
);
}
[Fact]
public void GZip_Writer_Generic_Bad_Compression_Async() =>
Assert.Throws<InvalidFormatException>(() =>
{
using Stream stream = File.OpenWrite(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"));
using var writer = WriterFactory.Open(stream, ArchiveType.GZip, CompressionType.BZip2);
});
[Fact]
public async Task GZip_Writer_Entry_Path_With_Dir_Async()
{
using (
Stream stream = File.Open(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
FileMode.OpenOrCreate,
FileAccess.Write
)
)
using (var writer = new GZipWriter(stream))
{
var path = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
await writer.WriteAsync(path, path);
}
CompareArchivesByPath(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")
);
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Writers.GZip;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipWriterDirectoryTests : TestBase
{
[Fact]
public void GZipWriter_WriteDirectory_ThrowsNotSupportedException()
{
using var memoryStream = new MemoryStream();
using var writer = new GZipWriter(memoryStream, new GZipWriterOptions());
Assert.Throws<NotSupportedException>(() => writer.WriteDirectory("test-dir", DateTime.Now));
}
}

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
using SharpCompress.Readers;
@@ -57,7 +59,7 @@ public class ForwardOnlyStream : SharpCompressStream, IStreamStack
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override bool CanWrite => true;
public override void Flush() { }
@@ -72,10 +74,41 @@ public class ForwardOnlyStream : SharpCompressStream, IStreamStack
public override int Read(byte[] buffer, int offset, int count) =>
stream.Read(buffer, offset, count);
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
) => stream.ReadAsync(buffer, offset, count, cancellationToken);
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
) => stream.ReadAsync(buffer, cancellationToken);
#endif
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
stream.Write(buffer, offset, count);
public override Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
) => stream.WriteAsync(buffer, offset, count, cancellationToken);
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default
) => stream.WriteAsync(buffer, cancellationToken);
#endif
public override Task FlushAsync(CancellationToken cancellationToken) =>
stream.FlushAsync(cancellationToken);
}

View File

@@ -223,7 +223,7 @@ public class RarReaderTests : ReaderTests
var destinationFileName = Path.Combine(destdir, file);
using var fs = File.OpenWrite(destinationFileName);
entryStream.TransferTo(fs);
entryStream.CopyTo(fs);
}
}
}
@@ -407,10 +407,16 @@ public class RarReaderTests : ReaderTests
Path.Combine("exe", "test.exe"),
}
);
using var archive = RarArchive.Open(
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part01.rar")
using var reader = RarReader.Open(
[
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part01.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part02.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part03.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part04.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part05.rar"),
Path.Combine(TEST_ARCHIVES_PATH, "Rar.multi.part06.rar"),
]
);
using var reader = archive.ExtractAllEntries();
while (reader.MoveToNextEntry())
{
Assert.Equal(expectedOrder.Pop(), reader.Entry.Key);

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
@@ -72,6 +74,72 @@ public abstract class ReaderTests : TestBase
}
}
protected async Task ReadAsync(
string testArchive,
CompressionType expectedCompression,
ReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
options ??= new ReaderOptions() { BufferSize = 0x20000 };
options.LeaveStreamOpen = true;
await ReadImplAsync(testArchive, expectedCompression, options, cancellationToken);
options.LeaveStreamOpen = false;
await ReadImplAsync(testArchive, expectedCompression, options, cancellationToken);
VerifyFiles();
}
private async Task ReadImplAsync(
string testArchive,
CompressionType expectedCompression,
ReaderOptions options,
CancellationToken cancellationToken = default
)
{
using var file = File.OpenRead(testArchive);
using var protectedStream = SharpCompressStream.Create(
new ForwardOnlyStream(file, options.BufferSize),
leaveOpen: true,
throwOnDispose: true,
bufferSize: options.BufferSize
);
using var testStream = new TestStream(protectedStream);
using (var reader = ReaderFactory.Open(testStream, options))
{
await UseReaderAsync(reader, expectedCompression, cancellationToken);
protectedStream.ThrowOnDispose = false;
Assert.False(testStream.IsDisposed, $"{nameof(testStream)} prematurely closed");
}
var message =
$"{nameof(options.LeaveStreamOpen)} is set to '{options.LeaveStreamOpen}', so {nameof(testStream.IsDisposed)} should be set to '{!testStream.IsDisposed}', but is set to {testStream.IsDisposed}";
Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message);
}
public async Task UseReaderAsync(
IReader reader,
CompressionType expectedCompression,
CancellationToken cancellationToken = default
)
{
while (await reader.MoveToNextEntryAsync(cancellationToken))
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(expectedCompression, reader.Entry.CompressionType);
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
cancellationToken
);
}
}
}
protected void Iterate(
string testArchive,
string fileOrder,

View File

@@ -122,6 +122,25 @@ public class SevenZipArchiveTests : ArchiveTests
//"7Zip.BZip2.split.006",
//"7Zip.BZip2.split.007"
[Fact]
public void SevenZipArchive_Copy_StreamRead() => ArchiveStreamRead("7Zip.Copy.7z");
[Fact]
public void SevenZipArchive_Copy_PathRead() => ArchiveFileRead("7Zip.Copy.7z");
[Fact]
public void SevenZipArchive_Copy_CompressionType()
{
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "7Zip.Copy.7z")))
using (var archive = SevenZipArchive.Open(stream))
{
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
Assert.Equal(CompressionType.None, entry.CompressionType);
}
}
}
[Fact]
public void SevenZipArchive_ZSTD_StreamRead() => ArchiveStreamRead("7Zip.ZSTD.7z");

View File

@@ -9,6 +9,12 @@
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
<DefineConstants>$(DefineConstants);DEBUG_STREAMS</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
<DefineConstants>$(DefineConstants);LINUX</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SharpCompress\SharpCompress.csproj" />
</ItemGroup>
@@ -17,7 +23,6 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="xunit" />
<PackageReference Include="Xunit.SkippableFact" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition=" '$(VersionlessImplicitFrameworkDefine)' != 'NETFRAMEWORK' ">

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