Compare commits

...

145 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
dd242cb1c5 Fix ExtractToDirectory to properly truncate existing files
Changed File.OpenWrite to File.Create to ensure files are truncated when extracting over existing files. File.OpenWrite does not truncate, leaving old data at the end if the new file is shorter.

Added test Zip_Deflate_ArchiveExtractToDirectory_Overwrite to verify the fix.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-14 08:54:45 +00:00
copilot-swe-agent[bot]
0ecbe02ede Initial plan 2025-11-14 08:35:27 +00:00
Adam Hathcock
bcf9a6bdf1 Merge pull request #1017 from Morilli/fix-streamstack
Fix some IStreamStack and SharpCompressStream functions
2025-11-14 08:33:38 +00:00
Morilli
e3a25ecdc0 make formatting worse with csharpier 2025-11-14 03:54:26 +01:00
Morilli
783521928d fix SharpCompressStream BufferSize setter and Seek as well
the BufferSize setter was completely broken and trashed the `_internalPosition` value on set, for `Seek` this is just an off-by-one fix allowing seeking to immediately past the last byte in the buffer
2025-11-14 03:37:08 +01:00
Morilli
9a876abd31 fix IStreamStack.StackSeek with buffering streams 2025-11-14 03:37:08 +01:00
Morilli
97f58b412e Add test for StackSeek behavior 2025-11-14 03:37:08 +01:00
Adam Hathcock
99a8b0f750 Merge pull request #1007 from TwanVanDongen/master
ArjReader throws exception for password protected archives.
2025-11-06 09:58:24 +00:00
Twan van Dongen
a9017d7c25 ArjReader throws exception for password protected archives. 2025-11-06 09:32:12 +01:00
Adam Hathcock
d9e4b26648 Merge pull request #1006 from TwanVanDongen/master
ARJ multi-part archive handling improved
2025-11-05 08:37:54 +00:00
Twan van Dongen
0d03bafe49 Merg conflicts resolved 2025-11-05 08:19:30 +01:00
Twan
fee15a31f9 Merge branch 'adamhathcock:master' into master 2025-11-05 08:12:53 +01:00
Twan van Dongen
997d3910d4 CSharpier appliedArjReader thrown exception for multi-part archives. Method4 (decodefastest) refactored to support Stream. 2025-11-05 08:05:30 +01:00
Twan van Dongen
a3918cc0d7 ArjReader thrown exception for multi-part archives. Method4 (decodefastest) refactored to support Stream. 2025-11-05 08:02:20 +01:00
Adam Hathcock
f056986b07 Merge pull request #1005 from TwanVanDongen/master
Refactor SqueezeStream for CLS Compliance, Streaming, and Generic Test Coverage
2025-11-04 15:50:36 +00:00
Twan van Dongen
59c1f02f98 Still difficult to run CSharpier... 2025-11-02 19:23:30 +01:00
Twan van Dongen
3a71a2b1f8 Ran CSharpier. 2025-11-02 19:22:01 +01:00
Twan van Dongen
2ef1215b49 Merge branch 'master' of https://github.com/TwanVanDongen/sharpcompress 2025-11-02 19:20:47 +01:00
Twan van Dongen
130ac83076 Revert "CSharpier...Refactors the SqueezeStream class to ensure full CLS compliance and proper stream behavior. It replaces the previous one-shot decoding logic with a true streaming implementation by piping Huffman-decoded output into the existing RunLength90Stream, enabling real-time decompression."
This reverts commit dd606a0702.
2025-11-02 19:20:34 +01:00
Twan van Dongen
dd606a0702 CSharpier...Refactors the SqueezeStream class to ensure full CLS compliance and proper stream behavior. It replaces the previous one-shot decoding logic with a true streaming implementation by piping Huffman-decoded output into the existing RunLength90Stream, enabling real-time decompression. 2025-11-02 19:15:17 +01:00
Twan van Dongen
84cd772f50 Refactors the SqueezeStream class to ensure full CLS compliance and proper stream behavior. It replaces the previous one-shot decoding logic with a true streaming implementation by piping Huffman-decoded output into the existing RunLength90Stream, enabling real-time decompression. 2025-11-02 19:09:59 +01:00
Adam Hathcock
8b612c658d Merge pull request #1003 from adamhathcock/adam/async-lzma
async lzma
2025-11-01 10:34:54 +00:00
Adam Hathcock
f7b3525c4e fix tests and fmt 2025-11-01 10:25:14 +00:00
Adam Hathcock
de83bdae48 Merge remote-tracking branch 'origin/master' into adam/async-lzma 2025-11-01 10:21:25 +00:00
Adam Hathcock
d90b610767 Merge pull request #994 from TwanVanDongen/master
Adding the ARJ (Archived by Robert Jung) format
2025-11-01 10:20:54 +00:00
Adam Hathcock
2d41de6b72 add async tests 2025-11-01 09:57:26 +00:00
Adam Hathcock
f391c3caf3 make an async code 2025-11-01 09:42:17 +00:00
Twan van Dongen
9bdf150676 Merge branch 'master' of https://github.com/TwanVanDongen/sharpcompress 2025-10-31 16:17:26 +01:00
Twan van Dongen
0c199609eb CSharpier...Merge branch 'master' of https://github.com/TwanVanDongen/sharpcompress 2025-10-31 16:17:23 +01:00
Twan van Dongen
6eff9d3753 Merge branch 'master' of https://github.com/TwanVanDongen/sharpcompress 2025-10-31 16:15:51 +01:00
Twan
7ab16457c7 Added ARJ's compressmion method4 (compressed fastest).
Refactored TestBase to support archives with mixed compression algorithms.Merge branch 'adamhathcock:master' into master
2025-10-31 16:14:48 +01:00
Twan
e7ad8132b5 Merge branch 'adamhathcock:master' into master 2025-10-31 14:33:14 +01:00
Adam Hathcock
55cb350d2c remove needless variable 2025-10-31 11:44:32 +00:00
Adam Hathcock
7fa271a1b4 fix ILLink 2025-10-31 11:43:35 +00:00
Adam Hathcock
c53ca372f2 don't use pools in tests 2025-10-31 11:39:57 +00:00
Adam Hathcock
75bc8501f4 Merge pull request #997 from adamhathcock/copilot/fix-ziparchive-linux-issue
Fix ArchiveFactory.Open double-wrapping causing "Cannot determine compressed stream type" on Linux
2025-10-31 11:24:59 +00:00
Adam Hathcock
1e22b47fe1 some fixes 2025-10-31 11:23:30 +00:00
Adam Hathcock
74e2dca207 fmt 2025-10-31 11:15:42 +00:00
Adam Hathcock
a669de24b7 Merge remote-tracking branch 'origin/master' into adam/async-lzma 2025-10-31 11:13:17 +00:00
Adam Hathcock
e1e9c449e9 use proper async versions 2025-10-31 11:12:46 +00:00
Adam Hathcock
60e1dc0239 review fixes 2025-10-31 11:10:55 +00:00
Adam Hathcock
10eb94fd82 Merge pull request #1002 from adamhathcock/adam/async-bzip2
async bzip2 and add
2025-10-31 11:09:39 +00:00
Adam Hathcock
ccc8587e5f review fixes 2025-10-31 10:55:33 +00:00
Adam Hathcock
53c96193c1 format 2025-10-30 16:40:59 +00:00
Adam Hathcock
d4f11e00b1 some more async changes 2025-10-30 16:31:45 +00:00
Adam Hathcock
321233b82c some async implementations 2025-10-30 15:08:57 +00:00
Adam Hathcock
eb188051d4 fmt 2025-10-30 14:53:11 +00:00
Adam Hathcock
a136084e11 add adc async 2025-10-30 14:52:51 +00:00
Adam Hathcock
bc06f3179d add basics for async bzip2 2025-10-30 14:42:46 +00:00
Adam Hathcock
ee84d971b2 Merge remote-tracking branch 'origin/master' into copilot/fix-ziparchive-linux-issue
# Conflicts:
#	src/SharpCompress/packages.lock.json
2025-10-30 14:30:31 +00:00
Twan
264d80ef4c Merge branch 'master' into master 2025-10-30 07:53:09 +01:00
Adam Hathcock
dba68187ac Merge pull request #996 from adamhathcock/adam/async-rar-ai 2025-10-29 14:26:15 +00:00
Adam Hathcock
ca4a1936b3 Merge remote-tracking branch 'origin/adam/async-rar-ai' into adam/async-rar-ai 2025-10-29 14:11:02 +00:00
Adam Hathcock
77c8d31a90 Merge pull request #1000 from adamhathcock/copilot/sub-pr-996
Fix Windows test failures due to ArrayPool buffer sizing
2025-10-29 14:10:39 +00:00
Adam Hathcock
ab7196f86c some review fixes 2025-10-29 14:07:12 +00:00
copilot-swe-agent[bot]
88b3a66bf9 Fix Windows test failures in SharpCompressStreamTests
ArrayPool.Rent() can return buffers larger than requested. The tests were using test.Length (the actual buffer size) instead of the requested size (0x1000), causing failures on Windows where ArrayPool returns larger buffers than on Linux.

Fixed by:
- Using explicit size (0x1000) instead of test.Length in Read() calls
- Using test.Take(0x1000) instead of test when comparing arrays

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-29 13:40:44 +00:00
copilot-swe-agent[bot]
ea77666b4a Final verification: All tests pass, no security issues
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-29 13:38:38 +00:00
copilot-swe-agent[bot]
db98e5f39b Fix ArchiveFactory.Open to avoid double-wrapping SharpCompressStream
Use SharpCompressStream.Create instead of constructor to properly handle
streams that are already wrapped. This prevents potential buffering issues
when opening ZIP files, particularly on Linux systems.

Added tests to verify both raw FileStream and pre-wrapped stream scenarios.

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-29 13:34:53 +00:00
copilot-swe-agent[bot]
df59c5cb9d Plan: Fix potential ZipArchive.IsZipFile failure on Linux
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-29 13:26:00 +00:00
copilot-swe-agent[bot]
e786e95358 Initial plan 2025-10-29 13:25:48 +00:00
Adam Hathcock
75ada5623c add async tests for compress stream 2025-10-29 13:23:50 +00:00
copilot-swe-agent[bot]
ad5c655c45 Initial plan 2025-10-29 13:09:05 +00:00
Adam Hathcock
65e607454e add async for UnpWriteBufAsync and remove comments 2025-10-29 13:07:43 +00:00
Adam Hathcock
f238be6003 add arraypool usage in init 2025-10-29 10:35:03 +00:00
Adam Hathcock
dc31e4c5fa fmt 2025-10-29 10:27:30 +00:00
Adam Hathcock
665d8cd266 ran out of tokens, things work but need one more conversion 2025-10-29 10:23:57 +00:00
Adam Hathcock
8324114e84 remove unused code 2025-10-29 10:11:34 +00:00
Adam Hathcock
b83e6ee4ce fix async usage 2025-10-29 09:55:11 +00:00
Adam Hathcock
58bab0d310 async unpack2.0 2025-10-29 09:53:14 +00:00
Adam Hathcock
1af51aaaba async unpack1.5 2025-10-29 09:50:12 +00:00
Adam Hathcock
a09327b831 unpack50async 2025-10-29 09:46:10 +00:00
Adam Hathcock
16543bf74c async UnpReadBufAsync 2025-10-29 09:38:55 +00:00
Adam Hathcock
aa4cd373ac added more async methods 2025-10-29 09:28:34 +00:00
Adam Hathcock
351e294362 convert unpack15 and unpack20 2025-10-29 09:23:42 +00:00
Adam Hathcock
a0c5b1cd9d add archive tests 2025-10-29 09:08:08 +00:00
Adam Hathcock
df2ed1e584 fix RarStream 2025-10-29 09:00:23 +00:00
Adam Hathcock
b354f7a3a5 fix logic mistake 2025-10-29 08:47:18 +00:00
Adam Hathcock
bb53d1e1c6 entrystream fixes and fmt 2025-10-29 08:41:05 +00:00
Twan van Dongen
2aabd8d0e1 Initial implementation of the ARJ (Archived by Robert Jung) format, supporting 'store' for single file archives. Multi-file archives and all compression algorithms still require implementations. 2025-10-28 20:38:29 +01:00
Adam Hathcock
aca97c2c6c add rarcrc tests 2025-10-28 16:48:05 +00:00
Adam Hathcock
8e7d959cf4 add async creations 2025-10-28 16:07:16 +00:00
Adam Hathcock
b23f031db9 add async reads 2025-10-28 15:52:18 +00:00
Adam Hathcock
1ba529a9d5 first pass of async files 2025-10-28 15:29:41 +00:00
Adam Hathcock
3d29c183ef basic async usage 2025-10-28 15:00:11 +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
142 changed files with 11921 additions and 1343 deletions

View File

@@ -3,11 +3,13 @@
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.yml: general information for this project
- 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:
- Do not change any other files in the repository.
- If build/test paths are different, update the workflow accordingly; this workflow targets SharpCompress.sln and the SharpCompress.Tests test project.
- 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.

107
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,21 +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 .`
- **ALWAYS run `dotnet csharpier format .` after making code changes before committing.**
- 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
@@ -45,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

@@ -14,6 +14,7 @@
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="ZstdSharp.Port" Version="0.8.6" />
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="8.0.21" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
</ItemGroup>

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

@@ -20,7 +20,7 @@ public static class ArchiveFactory
public static IArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
readerOptions ??= new ReaderOptions();
stream = new SharpCompressStream(stream, bufferSize: readerOptions.BufferSize);
stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize);
return FindFactory<IArchiveFactory>(stream).Open(stream, readerOptions);
}

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

@@ -15,9 +15,10 @@ 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();
}

View File

@@ -1,4 +1,6 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
@@ -30,6 +32,34 @@ public static class IArchiveEntryExtensions
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);
}
/// <summary>
/// Extract to specific directory, retaining filename
/// </summary>
@@ -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

@@ -74,7 +74,7 @@ public static class IArchiveExtensions
}
// Write file
using var fs = File.OpenWrite(path);
using var fs = File.Create(path);
entry.WriteTo(fs);
// Update progress

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

@@ -70,24 +70,51 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
public Stream OpenEntryStream()
{
RarStream stream;
if (IsRarV3)
{
return new RarStream(
stream = new RarStream(
archive.UnpackV1.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
);
}
else
{
stream = new RarStream(
archive.UnpackV2017.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
);
}
return new RarStream(
archive.UnpackV2017.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
);
stream.Initialize();
return stream;
}
public Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default) =>
Task.FromResult(OpenEntryStream());
public async Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
{
RarStream stream;
if (IsRarV3)
{
stream = new RarStream(
archive.UnpackV1.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
);
}
else
{
stream = new RarStream(
archive.UnpackV2017.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
);
}
await stream.InitializeAsync(cancellationToken);
return stream;
}
public bool IsComplete
{

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

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

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

@@ -8,4 +8,5 @@ public enum ArchiveType
SevenZip,
GZip,
Arc,
Arj,
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common.Arc;
using SharpCompress.Common.Arj.Headers;
namespace SharpCompress.Common.Arj
{
public class ArjEntry : Entry
{
private readonly ArjFilePart _filePart;
internal ArjEntry(ArjFilePart filePart)
{
_filePart = filePart;
}
public override long Crc => _filePart.Header.OriginalCrc32;
public override string? Key => _filePart?.Header.Name;
public override string? LinkTarget => null;
public override long CompressedSize => _filePart?.Header.CompressedSize ?? 0;
public override CompressionType CompressionType
{
get
{
if (_filePart.Header.CompressionMethod == CompressionMethod.Stored)
{
return CompressionType.None;
}
return CompressionType.ArjLZ77;
}
}
public override long Size => _filePart?.Header.OriginalSize ?? 0;
public override DateTime? LastModifiedTime => _filePart.Header.DateTimeModified.DateTime;
public override DateTime? CreatedTime => _filePart.Header.DateTimeCreated.DateTime;
public override DateTime? LastAccessedTime => _filePart.Header.DateTimeAccessed.DateTime;
public override DateTime? ArchivedTime => null;
public override bool IsEncrypted => false;
public override bool IsDirectory => _filePart.Header.FileType == FileType.Directory;
public override bool IsSplitAfter => false;
internal override IEnumerable<FilePart> Parts => _filePart.Empty();
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common.Arj.Headers;
using SharpCompress.Compressors.Arj;
using SharpCompress.IO;
namespace SharpCompress.Common.Arj
{
public class ArjFilePart : FilePart
{
private readonly Stream _stream;
internal ArjLocalHeader Header { get; set; }
internal ArjFilePart(ArjLocalHeader localArjHeader, Stream seekableStream)
: base(localArjHeader.ArchiveEncoding)
{
_stream = seekableStream;
Header = localArjHeader;
}
internal override string? FilePartName => Header.Name;
internal override Stream GetCompressedStream()
{
if (_stream != null)
{
Stream compressedStream;
switch (Header.CompressionMethod)
{
case CompressionMethod.Stored:
compressedStream = new ReadOnlySubStream(
_stream,
Header.DataStartPosition,
Header.CompressedSize
);
break;
case CompressionMethod.CompressedFastest:
compressedStream = new LHDecoderStream(_stream, (int)Header.OriginalSize);
break;
default:
throw new NotSupportedException(
"CompressionMethod: " + Header.CompressionMethod
);
}
return compressedStream;
}
return _stream.NotNull();
}
internal override Stream GetRawStream() => _stream;
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common.Rar;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.Readers;
namespace SharpCompress.Common.Arj
{
public class ArjVolume : Volume
{
public ArjVolume(Stream stream, ReaderOptions readerOptions, int index = 0)
: base(stream, readerOptions, index) { }
public override bool IsFirstVolume
{
get { return true; }
}
/// <summary>
/// ArjArchive is part of a multi-part archive.
/// </summary>
public override bool IsMultiVolume
{
get { return false; }
}
internal IEnumerable<ArjFilePart> GetVolumeFileParts()
{
return new List<ArjFilePart>();
}
}
}

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.Crypto;
namespace SharpCompress.Common.Arj.Headers
{
public enum ArjHeaderType
{
MainHeader,
LocalHeader,
}
public abstract class ArjHeader
{
private const int FIRST_HDR_SIZE = 34;
private const ushort ARJ_MAGIC = 0xEA60;
public ArjHeader(ArjHeaderType type)
{
ArjHeaderType = type;
}
public ArjHeaderType ArjHeaderType { get; }
public byte Flags { get; set; }
public FileType FileType { get; set; }
public abstract ArjHeader? Read(Stream reader);
public byte[] ReadHeader(Stream stream)
{
// check for magic bytes
Span<byte> magic = stackalloc byte[2];
if (stream.Read(magic) != 2)
{
return Array.Empty<byte>();
}
var magicValue = (ushort)(magic[0] | magic[1] << 8);
if (magicValue != ARJ_MAGIC)
{
throw new InvalidDataException("Not an ARJ file (wrong magic bytes)");
}
// read header_size
byte[] headerBytes = new byte[2];
stream.Read(headerBytes, 0, 2);
var headerSize = (ushort)(headerBytes[0] | headerBytes[1] << 8);
if (headerSize < 1)
{
return Array.Empty<byte>();
}
var body = new byte[headerSize];
var read = stream.Read(body, 0, headerSize);
if (read < headerSize)
{
return Array.Empty<byte>();
}
byte[] crc = new byte[4];
read = stream.Read(crc, 0, 4);
var checksum = Crc32Stream.Compute(body);
// Compute the hash value
if (checksum != BitConverter.ToUInt32(crc, 0))
{
throw new InvalidDataException("Header checksum is invalid");
}
return body;
}
protected List<byte[]> ReadExtendedHeaders(Stream reader)
{
List<byte[]> extendedHeader = new List<byte[]>();
byte[] buffer = new byte[2];
while (true)
{
int bytesRead = reader.Read(buffer, 0, 2);
if (bytesRead < 2)
{
throw new EndOfStreamException(
"Unexpected end of stream while reading extended header size."
);
}
var extHeaderSize = (ushort)(buffer[0] | (buffer[1] << 8));
if (extHeaderSize == 0)
{
return extendedHeader;
}
byte[] header = new byte[extHeaderSize];
bytesRead = reader.Read(header, 0, extHeaderSize);
if (bytesRead < extHeaderSize)
{
throw new EndOfStreamException(
"Unexpected end of stream while reading extended header data."
);
}
byte[] crc = new byte[4];
bytesRead = reader.Read(crc, 0, 4);
if (bytesRead < 4)
{
throw new EndOfStreamException(
"Unexpected end of stream while reading extended header CRC."
);
}
var checksum = Crc32Stream.Compute(header);
if (checksum != BitConverter.ToUInt32(crc, 0))
{
throw new InvalidDataException("Extended header checksum is invalid");
}
extendedHeader.Add(header);
}
}
// Flag helpers
public bool IsGabled => (Flags & 0x01) != 0;
public bool IsAnsiPage => (Flags & 0x02) != 0;
public bool IsVolume => (Flags & 0x04) != 0;
public bool IsArjProtected => (Flags & 0x08) != 0;
public bool IsPathSym => (Flags & 0x10) != 0;
public bool IsBackup => (Flags & 0x20) != 0;
public bool IsSecured => (Flags & 0x40) != 0;
public bool IsAltName => (Flags & 0x80) != 0;
public static FileType FileTypeFromByte(byte value)
{
return Enum.IsDefined(typeof(FileType), value)
? (FileType)value
: Headers.FileType.Unknown;
}
}
}

View File

@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace SharpCompress.Common.Arj.Headers
{
public class ArjLocalHeader : ArjHeader
{
public ArchiveEncoding ArchiveEncoding { get; }
public long DataStartPosition { get; protected set; }
public byte ArchiverVersionNumber { get; set; }
public byte MinVersionToExtract { get; set; }
public HostOS HostOS { get; set; }
public CompressionMethod CompressionMethod { get; set; }
public DosDateTime DateTimeModified { get; set; } = new DosDateTime(0);
public long CompressedSize { get; set; }
public long OriginalSize { get; set; }
public long OriginalCrc32 { get; set; }
public int FileSpecPosition { get; set; }
public int FileAccessMode { get; set; }
public byte FirstChapter { get; set; }
public byte LastChapter { get; set; }
public long ExtendedFilePosition { get; set; }
public DosDateTime DateTimeAccessed { get; set; } = new DosDateTime(0);
public DosDateTime DateTimeCreated { get; set; } = new DosDateTime(0);
public long OriginalSizeEvenForVolumes { get; set; }
public string Name { get; set; } = string.Empty;
public string Comment { get; set; } = string.Empty;
private const byte StdHdrSize = 30;
private const byte R9HdrSize = 46;
public ArjLocalHeader(ArchiveEncoding archiveEncoding)
: base(ArjHeaderType.LocalHeader)
{
ArchiveEncoding =
archiveEncoding ?? throw new ArgumentNullException(nameof(archiveEncoding));
}
public override ArjHeader? Read(Stream stream)
{
var body = ReadHeader(stream);
if (body.Length > 0)
{
ReadExtendedHeaders(stream);
var header = LoadFrom(body);
header.DataStartPosition = stream.Position;
return header;
}
return null;
}
public ArjLocalHeader LoadFrom(byte[] headerBytes)
{
int offset = 0;
int ReadInt16()
{
if (offset + 1 >= headerBytes.Length)
{
throw new EndOfStreamException();
}
var v = headerBytes[offset] & 0xFF | (headerBytes[offset + 1] & 0xFF) << 8;
offset += 2;
return v;
}
long ReadInt32()
{
if (offset + 3 >= headerBytes.Length)
{
throw new EndOfStreamException();
}
long v =
headerBytes[offset] & 0xFF
| (headerBytes[offset + 1] & 0xFF) << 8
| (headerBytes[offset + 2] & 0xFF) << 16
| (headerBytes[offset + 3] & 0xFF) << 24;
offset += 4;
return v;
}
byte headerSize = headerBytes[offset++];
ArchiverVersionNumber = headerBytes[offset++];
MinVersionToExtract = headerBytes[offset++];
HostOS hostOS = (HostOS)headerBytes[offset++];
Flags = headerBytes[offset++];
CompressionMethod = CompressionMethodFromByte(headerBytes[offset++]);
FileType = FileTypeFromByte(headerBytes[offset++]);
offset++; // Skip 1 byte
var rawTimestamp = ReadInt32();
DateTimeModified =
rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : new DosDateTime(0);
CompressedSize = ReadInt32();
OriginalSize = ReadInt32();
OriginalCrc32 = ReadInt32();
FileSpecPosition = ReadInt16();
FileAccessMode = ReadInt16();
FirstChapter = headerBytes[offset++];
LastChapter = headerBytes[offset++];
ExtendedFilePosition = 0;
OriginalSizeEvenForVolumes = 0;
if (headerSize > StdHdrSize)
{
ExtendedFilePosition = ReadInt32();
if (headerSize >= R9HdrSize)
{
rawTimestamp = ReadInt32();
DateTimeAccessed =
rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : new DosDateTime(0);
rawTimestamp = ReadInt32();
DateTimeCreated =
rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : new DosDateTime(0);
OriginalSizeEvenForVolumes = ReadInt32();
}
}
Name = Encoding.ASCII.GetString(
headerBytes,
offset,
Array.IndexOf(headerBytes, (byte)0, offset) - offset
);
offset += Name.Length + 1;
Comment = Encoding.ASCII.GetString(
headerBytes,
offset,
Array.IndexOf(headerBytes, (byte)0, offset) - offset
);
offset += Comment.Length + 1;
return this;
}
public static CompressionMethod CompressionMethodFromByte(byte value)
{
return value switch
{
0 => CompressionMethod.Stored,
1 => CompressionMethod.CompressedMost,
2 => CompressionMethod.Compressed,
3 => CompressionMethod.CompressedFaster,
4 => CompressionMethod.CompressedFastest,
8 => CompressionMethod.NoDataNoCrc,
9 => CompressionMethod.NoData,
_ => CompressionMethod.Unknown,
};
}
}
}

View File

@@ -0,0 +1,138 @@
using System;
using System.IO;
using System.Text;
using SharpCompress.Compressors.Deflate;
using SharpCompress.Crypto;
namespace SharpCompress.Common.Arj.Headers
{
public class ArjMainHeader : ArjHeader
{
private const int FIRST_HDR_SIZE = 34;
private const ushort ARJ_MAGIC = 0xEA60;
public ArchiveEncoding ArchiveEncoding { get; }
public int ArchiverVersionNumber { get; private set; }
public int MinVersionToExtract { get; private set; }
public HostOS HostOs { get; private set; }
public int SecurityVersion { get; private set; }
public DosDateTime CreationDateTime { get; private set; } = new DosDateTime(0);
public long CompressedSize { get; private set; }
public long ArchiveSize { get; private set; }
public long SecurityEnvelope { get; private set; }
public int FileSpecPosition { get; private set; }
public int SecurityEnvelopeLength { get; private set; }
public int EncryptionVersion { get; private set; }
public int LastChapter { get; private set; }
public int ArjProtectionFactor { get; private set; }
public int Flags2 { get; private set; }
public string Name { get; private set; } = string.Empty;
public string Comment { get; private set; } = string.Empty;
public ArjMainHeader(ArchiveEncoding archiveEncoding)
: base(ArjHeaderType.MainHeader)
{
ArchiveEncoding =
archiveEncoding ?? throw new ArgumentNullException(nameof(archiveEncoding));
}
public override ArjHeader? Read(Stream stream)
{
var body = ReadHeader(stream);
ReadExtendedHeaders(stream);
return LoadFrom(body);
}
public ArjMainHeader LoadFrom(byte[] headerBytes)
{
var offset = 1;
byte ReadByte()
{
if (offset >= headerBytes.Length)
{
throw new EndOfStreamException();
}
return (byte)(headerBytes[offset++] & 0xFF);
}
int ReadInt16()
{
if (offset + 1 >= headerBytes.Length)
{
throw new EndOfStreamException();
}
var v = headerBytes[offset] & 0xFF | (headerBytes[offset + 1] & 0xFF) << 8;
offset += 2;
return v;
}
long ReadInt32()
{
if (offset + 3 >= headerBytes.Length)
{
throw new EndOfStreamException();
}
long v =
headerBytes[offset] & 0xFF
| (headerBytes[offset + 1] & 0xFF) << 8
| (headerBytes[offset + 2] & 0xFF) << 16
| (headerBytes[offset + 3] & 0xFF) << 24;
offset += 4;
return v;
}
string ReadNullTerminatedString(byte[] x, int startIndex)
{
var result = new StringBuilder();
int i = startIndex;
while (i < x.Length && x[i] != 0)
{
result.Append((char)x[i]);
i++;
}
// Skip the null terminator
i++;
if (i < x.Length)
{
byte[] remainder = new byte[x.Length - i];
Array.Copy(x, i, remainder, 0, remainder.Length);
x = remainder;
}
return result.ToString();
}
ArchiverVersionNumber = ReadByte();
MinVersionToExtract = ReadByte();
var hostOsByte = ReadByte();
HostOs = hostOsByte <= 11 ? (HostOS)hostOsByte : HostOS.Unknown;
Flags = ReadByte();
SecurityVersion = ReadByte();
FileType = FileTypeFromByte(ReadByte());
offset++; // skip reserved
CreationDateTime = new DosDateTime((int)ReadInt32());
CompressedSize = ReadInt32();
ArchiveSize = ReadInt32();
SecurityEnvelope = ReadInt32();
FileSpecPosition = ReadInt16();
SecurityEnvelopeLength = ReadInt16();
EncryptionVersion = ReadByte();
LastChapter = ReadByte();
Name = ReadNullTerminatedString(headerBytes, offset);
Comment = ReadNullTerminatedString(headerBytes, offset + 1 + Name.Length);
return this;
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SharpCompress.Common.Arj.Headers
{
public enum CompressionMethod
{
Stored = 0,
CompressedMost = 1,
Compressed = 2,
CompressedFaster = 3,
CompressedFastest = 4,
NoDataNoCrc = 8,
NoData = 9,
Unknown,
}
}

View File

@@ -0,0 +1,37 @@
using System;
namespace SharpCompress.Common.Arj.Headers
{
public class DosDateTime
{
public DateTime DateTime { get; }
public DosDateTime(long dosValue)
{
// Ensure only the lower 32 bits are used
int value = unchecked((int)(dosValue & 0xFFFFFFFF));
var date = (value >> 16) & 0xFFFF;
var time = value & 0xFFFF;
var day = date & 0x1F;
var month = (date >> 5) & 0x0F;
var year = ((date >> 9) & 0x7F) + 1980;
var second = (time & 0x1F) * 2;
var minute = (time >> 5) & 0x3F;
var hour = (time >> 11) & 0x1F;
try
{
DateTime = new DateTime(year, month, day, hour, minute, second);
}
catch
{
DateTime = DateTime.MinValue;
}
}
public override string ToString() => DateTime.ToString("yyyy-MM-dd HH:mm:ss");
}
}

View File

@@ -0,0 +1,13 @@
namespace SharpCompress.Common.Arj.Headers
{
public enum FileType : byte
{
Binary = 0,
Text7Bit = 1,
CommentHeader = 2,
Directory = 3,
VolumeLabel = 4,
ChapterLabel = 5,
Unknown = 255,
}
}

View File

@@ -0,0 +1,19 @@
namespace SharpCompress.Common.Arj.Headers
{
public enum HostOS
{
MsDos = 0,
PrimOS = 1,
Unix = 2,
Amiga = 3,
MacOs = 4,
OS2 = 5,
AppleGS = 6,
AtariST = 7,
NeXT = 8,
VaxVMS = 9,
Win95 = 10,
Win32 = 11,
Unknown = 255,
}
}

View File

@@ -29,4 +29,5 @@ public enum CompressionType
Crushed,
Distilled,
ZStandard,
ArjLZ77,
}

View File

@@ -64,6 +64,11 @@ public class EntryStream : Stream, IStreamStack
protected override void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!(_completed || _reader.Cancelled))
{
SkipEntry();
@@ -81,12 +86,6 @@ public class EntryStream : Stream, IStreamStack
lzmaStream.Flush(); //Lzma over reads. Knock it back
}
}
if (_isDisposed)
{
return;
}
_isDisposed = true;
#if DEBUG_STREAMS
this.DebugDispose(typeof(EntryStream));
#endif
@@ -97,6 +96,11 @@ public class EntryStream : Stream, IStreamStack
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask DisposeAsync()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!(_completed || _reader.Cancelled))
{
await SkipEntryAsync().ConfigureAwait(false);
@@ -114,12 +118,6 @@ public class EntryStream : Stream, IStreamStack
await lzmaStream.FlushAsync().ConfigureAwait(false);
}
}
if (_isDisposed)
{
return;
}
_isDisposed = true;
#if DEBUG_STREAMS
this.DebugDispose(typeof(EntryStream));
#endif
@@ -204,4 +202,11 @@ public class EntryStream : Stream, IStreamStack
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
public override Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
) => throw new NotSupportedException();
}

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -7,6 +8,15 @@ 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>
@@ -48,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."
@@ -68,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."
@@ -158,7 +163,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."
@@ -178,12 +183,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."

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

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

@@ -24,10 +24,29 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Compressors.ADC;
/// <summary>
/// Result of an ADC decompression operation
/// </summary>
public class AdcDecompressResult
{
/// <summary>
/// Number of bytes read from input
/// </summary>
public int BytesRead { get; set; }
/// <summary>
/// Decompressed output buffer
/// </summary>
public byte[]? Output { get; set; }
}
/// <summary>
/// Provides static methods for decompressing Apple Data Compression data
/// </summary>
@@ -78,6 +97,173 @@ public static class ADCBase
public static int Decompress(byte[] input, out byte[]? output, int bufferSize = 262144) =>
Decompress(new MemoryStream(input), out output, bufferSize);
/// <summary>
/// Decompresses a byte buffer asynchronously that's compressed with ADC
/// </summary>
/// <param name="input">Compressed buffer</param>
/// <param name="bufferSize">Max size for decompressed data</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result containing bytes read and decompressed data</returns>
public static async Task<AdcDecompressResult> DecompressAsync(
byte[] input,
int bufferSize = 262144,
CancellationToken cancellationToken = default
) => await DecompressAsync(new MemoryStream(input), bufferSize, cancellationToken);
/// <summary>
/// Decompresses a stream asynchronously that's compressed with ADC
/// </summary>
/// <param name="input">Stream containing compressed data</param>
/// <param name="bufferSize">Max size for decompressed data</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result containing bytes read and decompressed data</returns>
public static async Task<AdcDecompressResult> DecompressAsync(
Stream input,
int bufferSize = 262144,
CancellationToken cancellationToken = default
)
{
var result = new AdcDecompressResult();
if (input is null || input.Length == 0)
{
result.BytesRead = 0;
result.Output = null;
return result;
}
var start = (int)input.Position;
var position = (int)input.Position;
int chunkSize;
int offset;
int chunkType;
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
var outPosition = 0;
var full = false;
byte[] temp = ArrayPool<byte>.Shared.Rent(3);
try
{
while (position < input.Length)
{
cancellationToken.ThrowIfCancellationRequested();
var readByte = input.ReadByte();
if (readByte == -1)
{
break;
}
chunkType = GetChunkType((byte)readByte);
switch (chunkType)
{
case PLAIN:
chunkSize = GetChunkSize((byte)readByte);
if (outPosition + chunkSize > bufferSize)
{
full = true;
break;
}
var readCount = await input.ReadAsync(
buffer,
outPosition,
chunkSize,
cancellationToken
);
outPosition += readCount;
position += readCount + 1;
break;
case TWO_BYTE:
chunkSize = GetChunkSize((byte)readByte);
temp[0] = (byte)readByte;
temp[1] = (byte)input.ReadByte();
offset = GetOffset(temp.AsSpan(0, 2));
if (outPosition + chunkSize > bufferSize)
{
full = true;
break;
}
if (offset == 0)
{
var lastByte = buffer[outPosition - 1];
for (var i = 0; i < chunkSize; i++)
{
buffer[outPosition] = lastByte;
outPosition++;
}
position += 2;
}
else
{
for (var i = 0; i < chunkSize; i++)
{
buffer[outPosition] = buffer[outPosition - offset - 1];
outPosition++;
}
position += 2;
}
break;
case THREE_BYTE:
chunkSize = GetChunkSize((byte)readByte);
temp[0] = (byte)readByte;
temp[1] = (byte)input.ReadByte();
temp[2] = (byte)input.ReadByte();
offset = GetOffset(temp.AsSpan(0, 3));
if (outPosition + chunkSize > bufferSize)
{
full = true;
break;
}
if (offset == 0)
{
var lastByte = buffer[outPosition - 1];
for (var i = 0; i < chunkSize; i++)
{
buffer[outPosition] = lastByte;
outPosition++;
}
position += 3;
}
else
{
for (var i = 0; i < chunkSize; i++)
{
buffer[outPosition] = buffer[outPosition - offset - 1];
outPosition++;
}
position += 3;
}
break;
}
if (full)
{
break;
}
}
var output = new byte[outPosition];
Array.Copy(buffer, output, outPosition);
result.BytesRead = position - start;
result.Output = output;
return result;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
ArrayPool<byte>.Shared.Return(temp);
}
}
/// <summary>
/// Decompresses a stream that's compressed with ADC
/// </summary>

View File

@@ -28,6 +28,8 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
namespace SharpCompress.Compressors.ADC;
@@ -187,6 +189,76 @@ public sealed class ADCStream : Stream, IStreamStack
return copied;
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
)
{
if (count == 0)
{
return 0;
}
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));
}
if (_outBuffer is null)
{
var result = await ADCBase.DecompressAsync(
_stream,
cancellationToken: cancellationToken
);
_outBuffer = result.Output;
_outPosition = 0;
}
var inPosition = offset;
var toCopy = count;
var copied = 0;
while (_outPosition + toCopy >= _outBuffer.Length)
{
cancellationToken.ThrowIfCancellationRequested();
var piece = _outBuffer.Length - _outPosition;
Array.Copy(_outBuffer, _outPosition, buffer, inPosition, piece);
inPosition += piece;
copied += piece;
_position += piece;
toCopy -= piece;
var result = await ADCBase.DecompressAsync(
_stream,
cancellationToken: cancellationToken
);
_outBuffer = result.Output;
_outPosition = 0;
if (result.BytesRead == 0 || _outBuffer is null || _outBuffer.Length == 0)
{
return copied;
}
}
Array.Copy(_outBuffer, _outPosition, buffer, inPosition, toCopy);
_outPosition += toCopy;
_position += toCopy;
copied += toCopy;
return copied;
}
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();

View File

@@ -0,0 +1,60 @@
using System;
using System.IO;
namespace SharpCompress.Compressors.Arj
{
[CLSCompliant(true)]
public sealed class BitReader
{
private readonly Stream _stream;
private int _bitBuffer;
private int _bitsRemaining;
private bool _disposed;
public BitReader(Stream input)
{
_stream = input ?? throw new ArgumentNullException(nameof(input));
if (!input.CanRead)
throw new ArgumentException("Stream must be readable.", nameof(input));
}
public int ReadBits(int count)
{
if (_disposed)
throw new ObjectDisposedException(nameof(BitReader));
if (count <= 0 || count > 32)
throw new ArgumentOutOfRangeException(
nameof(count),
"Bit count must be between 1 and 32."
);
int result = 0;
for (int i = 0; i < count; i++)
{
if (_bitsRemaining == 0)
{
int nextByte = _stream.ReadByte();
if (nextByte == -1)
throw new EndOfStreamException();
_bitBuffer = nextByte;
_bitsRemaining = 8;
}
// hoogste bit eerst
result = (result << 1) | ((_bitBuffer >> 7) & 1);
_bitBuffer <<= 1;
_bitsRemaining--;
}
return result;
}
public void AlignToByte()
{
_bitsRemaining = 0;
_bitBuffer = 0;
}
}
}

View File

@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.IO;
using SharpCompress.IO;
namespace SharpCompress.Compressors.Arj
{
[CLSCompliant(true)]
public sealed class LHDecoderStream : Stream, IStreamStack
{
#if DEBUG_STREAMS
long IStreamStack.InstanceId { get; set; }
#endif
int IStreamStack.DefaultBufferSize { get; set; }
Stream IStreamStack.BaseStream() => _stream;
int IStreamStack.BufferSize
{
get => 0;
set { }
}
int IStreamStack.BufferPosition
{
get => 0;
set { }
}
void IStreamStack.SetPosition(long position) { }
private readonly BitReader _bitReader;
private readonly Stream _stream;
// Buffer containing *all* bytes decoded so far.
private readonly List<byte> _buffer = new();
private long _readPosition;
private readonly int _originalSize;
private bool _finishedDecoding;
private bool _disposed;
private const int THRESHOLD = 3;
public LHDecoderStream(Stream compressedStream, int originalSize)
{
_stream = compressedStream ?? throw new ArgumentNullException(nameof(compressedStream));
if (!compressedStream.CanRead)
throw new ArgumentException(
"compressedStream must be readable.",
nameof(compressedStream)
);
_bitReader = new BitReader(compressedStream);
_originalSize = originalSize;
_readPosition = 0;
_finishedDecoding = (originalSize == 0);
}
public Stream BaseStream => _stream;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => _originalSize;
public override long Position
{
get => _readPosition;
set => throw new NotSupportedException();
}
/// <summary>
/// Decodes a single element (literal or back-reference) and appends it to _buffer.
/// Returns true if data was added, or false if all input has already been decoded.
/// </summary>
private bool DecodeNext()
{
if (_buffer.Count >= _originalSize)
{
_finishedDecoding = true;
return false;
}
int len = DecodeVal(0, 7);
if (len == 0)
{
byte nextChar = (byte)_bitReader.ReadBits(8);
_buffer.Add(nextChar);
}
else
{
int repCount = len + THRESHOLD - 1;
int backPtr = DecodeVal(9, 13);
if (backPtr >= _buffer.Count)
throw new InvalidDataException("Invalid back_ptr in LH stream");
int srcIndex = _buffer.Count - 1 - backPtr;
for (int j = 0; j < repCount && _buffer.Count < _originalSize; j++)
{
byte b = _buffer[srcIndex];
_buffer.Add(b);
srcIndex++;
// srcIndex may grow; it's allowed (source region can overlap destination)
}
}
if (_buffer.Count >= _originalSize)
{
_finishedDecoding = true;
}
return true;
}
private int DecodeVal(int from, int to)
{
int add = 0;
int bit = from;
while (bit < to && _bitReader.ReadBits(1) == 1)
{
add |= 1 << bit;
bit++;
}
int res = bit > 0 ? _bitReader.ReadBits(bit) : 0;
return res + add;
}
/// <summary>
/// Reads decompressed bytes into buffer[offset..offset+count].
/// The method decodes additional data on demand when needed.
/// </summary>
public override int Read(byte[] buffer, int offset, int count)
{
if (_disposed)
throw new ObjectDisposedException(nameof(LHDecoderStream));
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (offset < 0 || count < 0 || offset + count > buffer.Length)
throw new ArgumentOutOfRangeException("offset/count");
if (_readPosition >= _originalSize)
return 0; // EOF
int totalRead = 0;
while (totalRead < count && _readPosition < _originalSize)
{
if (_readPosition >= _buffer.Count)
{
bool had = DecodeNext();
if (!had)
{
break;
}
}
int available = _buffer.Count - (int)_readPosition;
if (available <= 0)
{
if (!_finishedDecoding)
{
continue;
}
break;
}
int toCopy = Math.Min(available, count - totalRead);
_buffer.CopyTo((int)_readPosition, buffer, offset + totalRead, toCopy);
_readPosition += toCopy;
totalRead += toCopy;
}
return totalRead;
}
public override void Flush() => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
}
}

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
namespace SharpCompress.Compressors.BZip2;
@@ -96,13 +98,37 @@ public sealed class BZip2Stream : Stream, IStreamStack
public override void SetLength(long value) => stream.SetLength(value);
#if !NETFRAMEWORK&& !NETSTANDARD2_0
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override int Read(Span<byte> buffer) => stream.Read(buffer);
public override void Write(ReadOnlySpan<byte> buffer) => stream.Write(buffer);
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
) => await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
public override async ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default
) => await stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
#endif
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
) => await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
public override async Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
) => await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
public override void Write(byte[] buffer, int offset, int count) =>
stream.Write(buffer, offset, count);

View File

@@ -2,6 +2,8 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
/*
@@ -1127,6 +1129,28 @@ internal class CBZip2InputStream : Stream, IStreamStack
return k;
}
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
)
{
var c = -1;
int k;
for (k = 0; k < count; ++k)
{
cancellationToken.ThrowIfCancellationRequested();
c = ReadByte();
if (c == -1)
{
break;
}
buffer[k + offset] = (byte)c;
}
return Task.FromResult(k);
}
public override long Seek(long offset, SeekOrigin origin) => 0;
public override void SetLength(long value) { }

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
/*
@@ -2022,6 +2024,21 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
}
}
public override Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
)
{
for (var k = 0; k < count; ++k)
{
cancellationToken.ThrowIfCancellationRequested();
WriteByte(buffer[k + offset]);
}
return Task.CompletedTask;
}
public override bool CanRead => false;
public override bool CanSeek => false;

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

@@ -2,6 +2,8 @@ using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Compressors.LZMA.Utilites;
using SharpCompress.IO;
@@ -283,5 +285,70 @@ internal sealed class AesDecoderStream : DecoderStream2, IStreamStack
return count;
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
)
{
if (count == 0 || mWritten == mLimit)
{
return 0;
}
if (mUnderflow > 0)
{
return HandleUnderflow(buffer, offset, count);
}
// Need at least 16 bytes to proceed.
if (mEnding - mOffset < 16)
{
Buffer.BlockCopy(mBuffer, mOffset, mBuffer, 0, mEnding - mOffset);
mEnding -= mOffset;
mOffset = 0;
do
{
cancellationToken.ThrowIfCancellationRequested();
var read = await mStream
.ReadAsync(mBuffer, mEnding, mBuffer.Length - mEnding, cancellationToken)
.ConfigureAwait(false);
if (read == 0)
{
// We are not done decoding and have less than 16 bytes.
throw new EndOfStreamException();
}
mEnding += read;
} while (mEnding - mOffset < 16);
}
// We shouldn't return more data than we are limited to.
if (count > mLimit - mWritten)
{
count = (int)(mLimit - mWritten);
}
// We cannot transform less than 16 bytes into the target buffer,
// but we also cannot return zero, so we need to handle this.
if (count < 16)
{
return HandleUnderflow(buffer, offset, count);
}
if (count > mEnding - mOffset)
{
count = mEnding - mOffset;
}
// Otherwise we transform directly into the target buffer.
var processed = mDecoder.TransformBlock(mBuffer, mOffset, count & ~15, buffer, offset);
mOffset += processed;
mWritten += processed;
return processed;
}
#endregion
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
namespace SharpCompress.Compressors.LZMA;
@@ -191,6 +193,18 @@ internal class Bcj2DecoderStream : DecoderStream2, IStreamStack
return count;
}
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
// Bcj2DecoderStream uses complex state machine with multiple streams
return Task.FromResult(Read(buffer, offset, count));
}
public override int ReadByte()
{
if (_mFinished)

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

@@ -3,6 +3,8 @@
using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Compressors.LZMA.LZ;
@@ -85,6 +87,12 @@ internal class OutWindow : IDisposable
_stream = null;
}
public async Task ReleaseStreamAsync(CancellationToken cancellationToken = default)
{
await FlushAsync(cancellationToken).ConfigureAwait(false);
_stream = null;
}
private void Flush()
{
if (_stream is null)
@@ -104,6 +112,27 @@ internal class OutWindow : IDisposable
_streamPos = _pos;
}
private async Task FlushAsync(CancellationToken cancellationToken = default)
{
if (_stream is null)
{
return;
}
var size = _pos - _streamPos;
if (size == 0)
{
return;
}
await _stream
.WriteAsync(_buffer, _streamPos, size, cancellationToken)
.ConfigureAwait(false);
if (_pos >= _windowSize)
{
_pos = 0;
}
_streamPos = _pos;
}
public void CopyPending()
{
if (_pendingLen < 1)
@@ -124,6 +153,26 @@ internal class OutWindow : IDisposable
_pendingLen = rem;
}
public async Task CopyPendingAsync(CancellationToken cancellationToken = default)
{
if (_pendingLen < 1)
{
return;
}
var rem = _pendingLen;
var pos = (_pendingDist < _pos ? _pos : _pos + _windowSize) - _pendingDist - 1;
while (rem > 0 && HasSpace)
{
if (pos >= _windowSize)
{
pos = 0;
}
await PutByteAsync(_buffer[pos++], cancellationToken).ConfigureAwait(false);
rem--;
}
_pendingLen = rem;
}
public void CopyBlock(int distance, int len)
{
var rem = len;
@@ -157,6 +206,43 @@ internal class OutWindow : IDisposable
_pendingDist = distance;
}
public async Task CopyBlockAsync(
int distance,
int len,
CancellationToken cancellationToken = default
)
{
var rem = len;
var pos = (distance < _pos ? _pos : _pos + _windowSize) - distance - 1;
var targetSize = HasSpace ? (int)Math.Min(rem, _limit - _total) : 0;
var sizeUntilWindowEnd = Math.Min(_windowSize - _pos, _windowSize - pos);
var sizeUntilOverlap = Math.Abs(pos - _pos);
var fastSize = Math.Min(Math.Min(sizeUntilWindowEnd, sizeUntilOverlap), targetSize);
if (fastSize >= 2)
{
_buffer.AsSpan(pos, fastSize).CopyTo(_buffer.AsSpan(_pos, fastSize));
_pos += fastSize;
pos += fastSize;
_total += fastSize;
if (_pos >= _windowSize)
{
await FlushAsync(cancellationToken).ConfigureAwait(false);
}
rem -= fastSize;
}
while (rem > 0 && HasSpace)
{
if (pos >= _windowSize)
{
pos = 0;
}
await PutByteAsync(_buffer[pos++], cancellationToken).ConfigureAwait(false);
rem--;
}
_pendingLen = rem;
_pendingDist = distance;
}
public void PutByte(byte b)
{
_buffer[_pos++] = b;
@@ -167,6 +253,16 @@ internal class OutWindow : IDisposable
}
}
public async Task PutByteAsync(byte b, CancellationToken cancellationToken = default)
{
_buffer[_pos++] = b;
_total++;
if (_pos >= _windowSize)
{
await FlushAsync(cancellationToken).ConfigureAwait(false);
}
}
public byte GetByte(int distance)
{
var pos = _pos - distance - 1;
@@ -207,6 +303,44 @@ internal class OutWindow : IDisposable
return len - size;
}
public async Task<int> CopyStreamAsync(
Stream stream,
int len,
CancellationToken cancellationToken = default
)
{
var size = len;
while (size > 0 && _pos < _windowSize && _total < _limit)
{
cancellationToken.ThrowIfCancellationRequested();
var curSize = _windowSize - _pos;
if (curSize > _limit - _total)
{
curSize = (int)(_limit - _total);
}
if (curSize > size)
{
curSize = size;
}
var numReadBytes = await stream
.ReadAsync(_buffer, _pos, curSize, cancellationToken)
.ConfigureAwait(false);
if (numReadBytes == 0)
{
throw new DataErrorException();
}
size -= numReadBytes;
_pos += numReadBytes;
_total += numReadBytes;
if (_pos >= _windowSize)
{
await FlushAsync(cancellationToken).ConfigureAwait(false);
}
}
return len - size;
}
public void SetLimit(long size) => _limit = _total + size;
public bool HasSpace => _pos < _windowSize && _total < _limit;

View File

@@ -1,6 +1,8 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Crypto;
using SharpCompress.IO;
@@ -157,6 +159,11 @@ public sealed class LZipStream : Stream, IStreamStack
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
) => _stream.ReadAsync(buffer, cancellationToken);
public override int Read(Span<byte> buffer) => _stream.Read(buffer);
public override void Write(ReadOnlySpan<byte> buffer)
@@ -179,6 +186,25 @@ public sealed class LZipStream : Stream, IStreamStack
++_writeCount;
}
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
) => _stream.ReadAsync(buffer, offset, count, cancellationToken);
public override async Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();
await _stream.WriteAsync(buffer, offset, count, cancellationToken);
_writeCount += count;
}
#endregion
/// <summary>

View File

@@ -1,6 +1,7 @@
#nullable disable
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using SharpCompress.Compressors.LZMA.LZ;
using SharpCompress.Compressors.LZMA.RangeCoder;
@@ -199,6 +200,9 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream
}
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
[MemberNotNull(nameof(_outWindow))]
#endif
private void CreateDictionary()
{
if (_dictionarySize < 0)
@@ -309,6 +313,42 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream
_outWindow = null;
}
public async System.Threading.Tasks.Task CodeAsync(
Stream inStream,
Stream outStream,
long inSize,
long outSize,
ICodeProgress progress,
System.Threading.CancellationToken cancellationToken = default
)
{
if (_outWindow is null)
{
CreateDictionary();
}
_outWindow.Init(outStream);
if (outSize > 0)
{
_outWindow.SetLimit(outSize);
}
else
{
_outWindow.SetLimit(long.MaxValue - _outWindow.Total);
}
var rangeDecoder = new RangeCoder.Decoder();
rangeDecoder.Init(inStream);
await CodeAsync(_dictionarySize, _outWindow, rangeDecoder, cancellationToken)
.ConfigureAwait(false);
await _outWindow.ReleaseStreamAsync(cancellationToken).ConfigureAwait(false);
rangeDecoder.ReleaseStream();
_outWindow.Dispose();
_outWindow = null;
}
internal bool Code(int dictionarySize, OutWindow outWindow, RangeCoder.Decoder rangeDecoder)
{
var dictionarySizeCheck = Math.Max(dictionarySize, 1);
@@ -435,6 +475,143 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream
return false;
}
internal async System.Threading.Tasks.Task<bool> CodeAsync(
int dictionarySize,
OutWindow outWindow,
RangeCoder.Decoder rangeDecoder,
System.Threading.CancellationToken cancellationToken = default
)
{
var dictionarySizeCheck = Math.Max(dictionarySize, 1);
await outWindow.CopyPendingAsync(cancellationToken).ConfigureAwait(false);
while (outWindow.HasSpace)
{
cancellationToken.ThrowIfCancellationRequested();
var posState = (uint)outWindow.Total & _posStateMask;
if (
_isMatchDecoders[(_state._index << Base.K_NUM_POS_STATES_BITS_MAX) + posState]
.Decode(rangeDecoder) == 0
)
{
byte b;
var prevByte = outWindow.GetByte(0);
if (!_state.IsCharState())
{
b = _literalDecoder.DecodeWithMatchByte(
rangeDecoder,
(uint)outWindow.Total,
prevByte,
outWindow.GetByte((int)_rep0)
);
}
else
{
b = _literalDecoder.DecodeNormal(rangeDecoder, (uint)outWindow.Total, prevByte);
}
await outWindow.PutByteAsync(b, cancellationToken).ConfigureAwait(false);
_state.UpdateChar();
}
else
{
uint len;
if (_isRepDecoders[_state._index].Decode(rangeDecoder) == 1)
{
if (_isRepG0Decoders[_state._index].Decode(rangeDecoder) == 0)
{
if (
_isRep0LongDecoders[
(_state._index << Base.K_NUM_POS_STATES_BITS_MAX) + posState
]
.Decode(rangeDecoder) == 0
)
{
_state.UpdateShortRep();
await outWindow
.PutByteAsync(outWindow.GetByte((int)_rep0), cancellationToken)
.ConfigureAwait(false);
continue;
}
}
else
{
uint distance;
if (_isRepG1Decoders[_state._index].Decode(rangeDecoder) == 0)
{
distance = _rep1;
}
else
{
if (_isRepG2Decoders[_state._index].Decode(rangeDecoder) == 0)
{
distance = _rep2;
}
else
{
distance = _rep3;
_rep3 = _rep2;
}
_rep2 = _rep1;
}
_rep1 = _rep0;
_rep0 = distance;
}
len = _repLenDecoder.Decode(rangeDecoder, posState) + Base.K_MATCH_MIN_LEN;
_state.UpdateRep();
}
else
{
_rep3 = _rep2;
_rep2 = _rep1;
_rep1 = _rep0;
len = Base.K_MATCH_MIN_LEN + _lenDecoder.Decode(rangeDecoder, posState);
_state.UpdateMatch();
var posSlot = _posSlotDecoder[Base.GetLenToPosState(len)].Decode(rangeDecoder);
if (posSlot >= Base.K_START_POS_MODEL_INDEX)
{
var numDirectBits = (int)((posSlot >> 1) - 1);
_rep0 = ((2 | (posSlot & 1)) << numDirectBits);
if (posSlot < Base.K_END_POS_MODEL_INDEX)
{
_rep0 += BitTreeDecoder.ReverseDecode(
_posDecoders,
_rep0 - posSlot - 1,
rangeDecoder,
numDirectBits
);
}
else
{
_rep0 += (
rangeDecoder.DecodeDirectBits(numDirectBits - Base.K_NUM_ALIGN_BITS)
<< Base.K_NUM_ALIGN_BITS
);
_rep0 += _posAlignDecoder.ReverseDecode(rangeDecoder);
}
}
else
{
_rep0 = posSlot;
}
}
if (_rep0 >= outWindow.Total || _rep0 >= dictionarySizeCheck)
{
if (_rep0 == 0xFFFFFFFF)
{
return true;
}
throw new DataErrorException();
}
await outWindow
.CopyBlockAsync((int)_rep0, (int)len, cancellationToken)
.ConfigureAwait(false);
}
}
return false;
}
public void SetDecoderProperties(byte[] properties)
{
if (properties.Length < 1)
@@ -470,29 +647,4 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream
}
_outWindow.Train(stream);
}
/*
public override bool CanRead { get { return true; }}
public override bool CanWrite { get { return true; }}
public override bool CanSeek { get { return true; }}
public override long Length { get { return 0; }}
public override long Position
{
get { return 0; }
set { }
}
public override void Flush() { }
public override int Read(byte[] buffer, int offset, int count)
{
return 0;
}
public override void Write(byte[] buffer, int offset, int count)
{
}
public override long Seek(long offset, System.IO.SeekOrigin origin)
{
return 0;
}
public override void SetLength(long value) {}
*/
}

View File

@@ -3,6 +3,8 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Compressors.LZMA.LZ;
using SharpCompress.IO;
@@ -423,6 +425,82 @@ public class LzmaStream : Stream, IStreamStack
}
}
private async Task DecodeChunkHeaderAsync(CancellationToken cancellationToken = default)
{
var controlBuffer = new byte[1];
await _inputStream.ReadAsync(controlBuffer, 0, 1, cancellationToken).ConfigureAwait(false);
var control = controlBuffer[0];
_inputPosition++;
if (control == 0x00)
{
_endReached = true;
return;
}
if (control >= 0xE0 || control == 0x01)
{
_needProps = true;
_needDictReset = false;
_outWindow.Reset();
}
else if (_needDictReset)
{
throw new DataErrorException();
}
if (control >= 0x80)
{
_uncompressedChunk = false;
_availableBytes = (control & 0x1F) << 16;
var buffer = new byte[2];
await _inputStream.ReadAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false);
_availableBytes += (buffer[0] << 8) + buffer[1] + 1;
_inputPosition += 2;
await _inputStream.ReadAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false);
_rangeDecoderLimit = (buffer[0] << 8) + buffer[1] + 1;
_inputPosition += 2;
if (control >= 0xC0)
{
_needProps = false;
await _inputStream
.ReadAsync(controlBuffer, 0, 1, cancellationToken)
.ConfigureAwait(false);
Properties[0] = controlBuffer[0];
_inputPosition++;
_decoder = new Decoder();
_decoder.SetDecoderProperties(Properties);
}
else if (_needProps)
{
throw new DataErrorException();
}
else if (control >= 0xA0)
{
_decoder = new Decoder();
_decoder.SetDecoderProperties(Properties);
}
_rangeDecoder.Init(_inputStream);
}
else if (control > 0x02)
{
throw new DataErrorException();
}
else
{
_uncompressedChunk = true;
var buffer = new byte[2];
await _inputStream.ReadAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false);
_availableBytes = (buffer[0] << 8) + buffer[1] + 1;
_inputPosition += 2;
}
}
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
@@ -435,5 +513,128 @@ public class LzmaStream : Stream, IStreamStack
}
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (_endReached)
{
return 0;
}
var total = 0;
while (total < count)
{
cancellationToken.ThrowIfCancellationRequested();
if (_availableBytes == 0)
{
if (_isLzma2)
{
await DecodeChunkHeaderAsync(cancellationToken).ConfigureAwait(false);
}
else
{
_endReached = true;
}
if (_endReached)
{
break;
}
}
var toProcess = count - total;
if (toProcess > _availableBytes)
{
toProcess = (int)_availableBytes;
}
_outWindow.SetLimit(toProcess);
if (_uncompressedChunk)
{
_inputPosition += await _outWindow
.CopyStreamAsync(_inputStream, toProcess, cancellationToken)
.ConfigureAwait(false);
}
else if (
await _decoder
.CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken)
.ConfigureAwait(false)
&& _outputSize < 0
)
{
_availableBytes = _outWindow.AvailableBytes;
}
var read = _outWindow.Read(buffer, offset, toProcess);
total += read;
offset += read;
_position += read;
_availableBytes -= read;
if (_availableBytes == 0 && !_uncompressedChunk)
{
if (
!_rangeDecoder.IsFinished
|| (_rangeDecoderLimit >= 0 && _rangeDecoder._total != _rangeDecoderLimit)
)
{
_outWindow.SetLimit(toProcess + 1);
if (
!await _decoder
.CodeAsync(
_dictionarySize,
_outWindow,
_rangeDecoder,
cancellationToken
)
.ConfigureAwait(false)
)
{
_rangeDecoder.ReleaseStream();
throw new DataErrorException();
}
}
_rangeDecoder.ReleaseStream();
_inputPosition += _rangeDecoder._total;
if (_outWindow.HasPending)
{
throw new DataErrorException();
}
}
}
if (_endReached)
{
if (_inputSize >= 0 && _inputPosition != _inputSize)
{
throw new DataErrorException();
}
if (_outputSize >= 0 && _position != _outputSize)
{
throw new DataErrorException();
}
}
return total;
}
public override Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();
Write(buffer, offset, count);
return Task.CompletedTask;
}
public byte[] Properties { get; } = new byte[5];
}

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
namespace SharpCompress.Compressors.LZMA.Utilites;
@@ -101,4 +103,22 @@ internal class CrcBuilderStream : Stream, IStreamStack
_mCrc = Crc.Update(_mCrc, buffer, offset, count);
_mTarget.Write(buffer, offset, count);
}
public override async Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
if (_mFinished)
{
throw new InvalidOperationException("CRC calculation has been finished.");
}
Processed += count;
_mCrc = Crc.Update(_mCrc, buffer, offset, count);
await _mTarget.WriteAsync(buffer, offset, count, cancellationToken);
}
}

View File

@@ -1,6 +1,9 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Compressors.LZMA.Utilites;
@@ -11,7 +14,7 @@ public class CrcCheckStream : Stream
private uint _mCurrentCrc;
private bool _mClosed;
private readonly long[] _mBytes = new long[256];
private readonly long[] _mBytes = ArrayPool<long>.Shared.Rent(256);
private long _mLength;
public CrcCheckStream(uint crc)
@@ -65,6 +68,7 @@ public class CrcCheckStream : Stream
finally
{
base.Dispose(disposing);
ArrayPool<long>.Shared.Return(_mBytes);
}
}
@@ -101,4 +105,16 @@ public class CrcCheckStream : Stream
_mCurrentCrc = Crc.Update(_mCurrentCrc, buffer, offset, count);
}
public override Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();
Write(buffer, offset, count);
return Task.CompletedTask;
}
}

View File

@@ -1,13 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.IO;
namespace SharpCompress.Compressors.RLE90
{
/// <summary>
/// Real-time streaming RLE90 decompression stream.
/// Decompresses bytes on demand without buffering the entire file in memory.
/// </summary>
public class RunLength90Stream : Stream, IStreamStack
{
#if DEBUG_STREAMS
@@ -31,13 +31,19 @@ namespace SharpCompress.Compressors.RLE90
void IStreamStack.SetPosition(long position) { }
private readonly Stream _stream;
private readonly int _compressedSize;
private int _bytesReadFromSource;
private const byte DLE = 0x90;
private int _compressedSize;
private bool _processed = false;
private bool _inDleMode;
private byte _lastByte;
private int _repeatCount;
private bool _endOfCompressedData;
public RunLength90Stream(Stream stream, int compressedSize)
{
_stream = stream;
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
_compressedSize = compressedSize;
#if DEBUG_STREAMS
this.DebugConstruct(typeof(RunLength90Stream));
@@ -53,44 +59,93 @@ namespace SharpCompress.Compressors.RLE90
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotImplementedException();
public override long Length => throw new NotSupportedException();
public override long Position
{
get => _stream.Position;
set => throw new NotImplementedException();
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush() => throw new NotImplementedException();
public override void Flush() => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count)
{
if (_processed)
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 && !_endOfCompressedData)
{
return 0;
// Handle pending repeat bytes first
if (_repeatCount > 0)
{
int toWrite = Math.Min(_repeatCount, count - bytesWritten);
for (int i = 0; i < toWrite; i++)
{
buffer[offset + bytesWritten++] = _lastByte;
}
_repeatCount -= toWrite;
continue;
}
// Try to read the next byte from compressed data
if (_bytesReadFromSource >= _compressedSize)
{
_endOfCompressedData = true;
break;
}
int next = _stream.ReadByte();
if (next == -1)
{
_endOfCompressedData = true;
break;
}
_bytesReadFromSource++;
byte c = (byte)next;
if (_inDleMode)
{
_inDleMode = false;
if (c == 0)
{
buffer[offset + bytesWritten++] = DLE;
_lastByte = DLE;
}
else
{
_repeatCount = c - 1;
// Well handle these repeats in next loop iteration.
}
}
else if (c == DLE)
{
_inDleMode = true;
}
else
{
buffer[offset + bytesWritten++] = c;
_lastByte = c;
}
}
_processed = true;
using var binaryReader = new BinaryReader(_stream);
byte[] compressedBuffer = binaryReader.ReadBytes(_compressedSize);
var unpacked = RLE.UnpackRLE(compressedBuffer);
unpacked.CopyTo(buffer);
return unpacked.Count;
return bytesWritten;
}
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotImplementedException();
public override void SetLength(long value) => throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotImplementedException();
}
}

View File

@@ -1,4 +1,6 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Rar.Headers;
namespace SharpCompress.Compressors.Rar;
@@ -8,6 +10,14 @@ internal interface IRarUnpack
void DoUnpack(FileHeader fileHeader, Stream readStream, Stream writeStream);
void DoUnpack();
Task DoUnpackAsync(
FileHeader fileHeader,
Stream readStream,
Stream writeStream,
CancellationToken cancellationToken
);
Task DoUnpackAsync(CancellationToken cancellationToken);
// eg u/i pause/resume button
bool Suspended { get; set; }

View File

@@ -150,6 +150,136 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack
return totalRead;
}
public override async System.Threading.Tasks.Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
System.Threading.CancellationToken cancellationToken
)
{
var totalRead = 0;
var currentOffset = offset;
var currentCount = count;
while (currentCount > 0)
{
var readSize = currentCount;
if (currentCount > maxPosition - currentPosition)
{
readSize = (int)(maxPosition - currentPosition);
}
var read = await currentStream
.ReadAsync(buffer, currentOffset, readSize, cancellationToken)
.ConfigureAwait(false);
if (read < 0)
{
throw new EndOfStreamException();
}
currentPosition += read;
currentOffset += read;
currentCount -= read;
totalRead += read;
if (
((maxPosition - currentPosition) == 0)
&& filePartEnumerator.Current.FileHeader.IsSplitAfter
)
{
if (filePartEnumerator.Current.FileHeader.R4Salt != null)
{
throw new InvalidFormatException(
"Sharpcompress currently does not support multi-volume decryption."
);
}
var fileName = filePartEnumerator.Current.FileHeader.FileName;
if (!filePartEnumerator.MoveNext())
{
throw new InvalidFormatException(
"Multi-part rar file is incomplete. Entry expects a new volume: "
+ fileName
);
}
InitializeNextFilePart();
}
else
{
break;
}
}
currentPartTotalReadBytes += totalRead;
currentEntryTotalReadBytes += totalRead;
streamListener.FireCompressedBytesRead(
currentPartTotalReadBytes,
currentEntryTotalReadBytes
);
return totalRead;
}
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public override async System.Threading.Tasks.ValueTask<int> ReadAsync(
Memory<byte> buffer,
System.Threading.CancellationToken cancellationToken = default
)
{
var totalRead = 0;
var currentOffset = 0;
var currentCount = buffer.Length;
while (currentCount > 0)
{
var readSize = currentCount;
if (currentCount > maxPosition - currentPosition)
{
readSize = (int)(maxPosition - currentPosition);
}
var read = await currentStream
.ReadAsync(buffer.Slice(currentOffset, readSize), cancellationToken)
.ConfigureAwait(false);
if (read < 0)
{
throw new EndOfStreamException();
}
currentPosition += read;
currentOffset += read;
currentCount -= read;
totalRead += read;
if (
((maxPosition - currentPosition) == 0)
&& filePartEnumerator.Current.FileHeader.IsSplitAfter
)
{
if (filePartEnumerator.Current.FileHeader.R4Salt != null)
{
throw new InvalidFormatException(
"Sharpcompress currently does not support multi-volume decryption."
);
}
var fileName = filePartEnumerator.Current.FileHeader.FileName;
if (!filePartEnumerator.MoveNext())
{
throw new InvalidFormatException(
"Multi-part rar file is incomplete. Entry expects a new volume: "
+ fileName
);
}
InitializeNextFilePart();
}
else
{
break;
}
}
currentPartTotalReadBytes += totalRead;
currentEntryTotalReadBytes += totalRead;
streamListener.FireCompressedBytesRead(
currentPartTotalReadBytes,
currentEntryTotalReadBytes
);
return totalRead;
}
#endif
public override bool CanRead => true;
public override bool CanSeek => false;

View File

@@ -1,6 +1,8 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.IO;
@@ -103,7 +105,7 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack
byte[] _hash = { };
public RarBLAKE2spStream(
private RarBLAKE2spStream(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream
@@ -121,6 +123,29 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack
ResetCrc();
}
public static RarBLAKE2spStream Create(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream
)
{
var stream = new RarBLAKE2spStream(unpack, fileHeader, readStream);
stream.Initialize();
return stream;
}
public static async Task<RarBLAKE2spStream> CreateAsync(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
CancellationToken cancellationToken = default
)
{
var stream = new RarBLAKE2spStream(unpack, fileHeader, readStream);
await stream.InitializeAsync(cancellationToken);
return stream;
}
protected override void Dispose(bool disposing)
{
#if DEBUG_STREAMS
@@ -333,4 +358,59 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack
return result;
}
public override async System.Threading.Tasks.Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
System.Threading.CancellationToken cancellationToken
)
{
var result = await base.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
if (result != 0)
{
Update(_blake2sp, new ReadOnlySpan<byte>(buffer, offset, result), result);
}
else
{
_hash = Final(_blake2sp);
if (!disableCRCCheck && !(GetCrc().SequenceEqual(readStream.CurrentCrc)) && count != 0)
{
// NOTE: we use the last FileHeader in a multipart volume to check CRC
throw new InvalidFormatException("file crc mismatch");
}
}
return result;
}
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public override async System.Threading.Tasks.ValueTask<int> ReadAsync(
Memory<byte> buffer,
System.Threading.CancellationToken cancellationToken = default
)
{
var result = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
if (result != 0)
{
Update(_blake2sp, buffer.Span.Slice(0, result), result);
}
else
{
_hash = Final(_blake2sp);
if (
!disableCRCCheck
&& !(GetCrc().SequenceEqual(readStream.CurrentCrc))
&& buffer.Length != 0
)
{
// NOTE: we use the last FileHeader in a multipart volume to check CRC
throw new InvalidFormatException("file crc mismatch");
}
}
return result;
}
#endif
}

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.IO;
@@ -31,7 +33,7 @@ internal class RarCrcStream : RarStream, IStreamStack
private uint currentCrc;
private readonly bool disableCRC;
public RarCrcStream(
private RarCrcStream(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream
@@ -46,6 +48,29 @@ internal class RarCrcStream : RarStream, IStreamStack
ResetCrc();
}
public static RarCrcStream Create(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream
)
{
var stream = new RarCrcStream(unpack, fileHeader, readStream);
stream.Initialize();
return stream;
}
public static async Task<RarCrcStream> CreateAsync(
IRarUnpack unpack,
FileHeader fileHeader,
MultiVolumeReadOnlyStream readStream,
CancellationToken cancellationToken = default
)
{
var stream = new RarCrcStream(unpack, fileHeader, readStream);
await stream.InitializeAsync(cancellationToken);
return stream;
}
protected override void Dispose(bool disposing)
{
#if DEBUG_STREAMS
@@ -77,4 +102,56 @@ internal class RarCrcStream : RarStream, IStreamStack
return result;
}
public override async System.Threading.Tasks.Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
System.Threading.CancellationToken cancellationToken
)
{
var result = await base.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
if (result != 0)
{
currentCrc = RarCRC.CheckCrc(currentCrc, buffer, offset, result);
}
else if (
!disableCRC
&& GetCrc() != BitConverter.ToUInt32(readStream.CurrentCrc, 0)
&& count != 0
)
{
// NOTE: we use the last FileHeader in a multipart volume to check CRC
throw new InvalidFormatException("file crc mismatch");
}
return result;
}
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public override async System.Threading.Tasks.ValueTask<int> ReadAsync(
Memory<byte> buffer,
System.Threading.CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
var result = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
if (result != 0)
{
currentCrc = RarCRC.CheckCrc(currentCrc, buffer.Span, 0, result);
}
else if (
!disableCRC
&& GetCrc() != BitConverter.ToUInt32(readStream.CurrentCrc, 0)
&& buffer.Length != 0
)
{
// NOTE: we use the last FileHeader in a multipart volume to check CRC
throw new InvalidFormatException("file crc mismatch");
}
return result;
}
#endif
}

View File

@@ -3,6 +3,8 @@
using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.IO;
@@ -56,13 +58,24 @@ internal class RarStream : Stream, IStreamStack
#if DEBUG_STREAMS
this.DebugConstruct(typeof(RarStream));
#endif
}
public void Initialize()
{
fetch = true;
unpack.DoUnpack(fileHeader, readStream, this);
fetch = false;
_position = 0;
}
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
fetch = true;
await unpack.DoUnpackAsync(fileHeader, readStream, this, cancellationToken);
fetch = false;
_position = 0;
}
protected override void Dispose(bool disposing)
{
if (!isDisposed)
@@ -131,6 +144,73 @@ internal class RarStream : Stream, IStreamStack
return outTotal;
}
public override async System.Threading.Tasks.Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
System.Threading.CancellationToken cancellationToken
) => await ReadImplAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
private async System.Threading.Tasks.Task<int> ReadImplAsync(
byte[] buffer,
int offset,
int count,
System.Threading.CancellationToken cancellationToken
)
{
outTotal = 0;
if (tmpCount > 0)
{
var toCopy = tmpCount < count ? tmpCount : count;
Buffer.BlockCopy(tmpBuffer, tmpOffset, buffer, offset, toCopy);
tmpOffset += toCopy;
tmpCount -= toCopy;
offset += toCopy;
count -= toCopy;
outTotal += toCopy;
}
if (count > 0 && unpack.DestSize > 0)
{
outBuffer = buffer;
outOffset = offset;
outCount = count;
fetch = true;
await unpack.DoUnpackAsync(cancellationToken).ConfigureAwait(false);
fetch = false;
}
_position += outTotal;
if (count > 0 && outTotal == 0 && _position != Length)
{
// sanity check, eg if we try to decompress a redir entry
throw new InvalidOperationException(
$"unpacked file size does not match header: expected {Length} found {_position}"
);
}
return outTotal;
}
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public override async System.Threading.Tasks.ValueTask<int> ReadAsync(
Memory<byte> buffer,
System.Threading.CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
var array = System.Buffers.ArrayPool<byte>.Shared.Rent(buffer.Length);
try
{
var bytesRead = await ReadImplAsync(array, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false);
new ReadOnlySpan<byte>(array, 0, bytesRead).CopyTo(buffer.Span);
return bytesRead;
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(array);
}
}
#endif
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
@@ -165,6 +245,18 @@ internal class RarStream : Stream, IStreamStack
}
}
public override System.Threading.Tasks.Task WriteAsync(
byte[] buffer,
int offset,
int count,
System.Threading.CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();
Write(buffer, offset, count);
return System.Threading.Tasks.Task.CompletedTask;
}
private void EnsureBufferCapacity(int count)
{
if (this.tmpBuffer.Length < this.tmpCount + count)

View File

@@ -155,6 +155,25 @@ internal sealed partial class Unpack : BitInput, IRarUnpack
DoUnpack();
}
public async System.Threading.Tasks.Task DoUnpackAsync(
FileHeader fileHeader,
Stream readStream,
Stream writeStream,
System.Threading.CancellationToken cancellationToken = default
)
{
destUnpSize = fileHeader.UncompressedSize;
this.fileHeader = fileHeader;
this.readStream = readStream;
this.writeStream = writeStream;
if (!fileHeader.IsSolid)
{
Init(null);
}
suspended = false;
await DoUnpackAsync(cancellationToken).ConfigureAwait(false);
}
public void DoUnpack()
{
if (fileHeader.CompressionMethod == 0)
@@ -189,6 +208,42 @@ internal sealed partial class Unpack : BitInput, IRarUnpack
}
}
public async System.Threading.Tasks.Task DoUnpackAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
if (fileHeader.CompressionMethod == 0)
{
await UnstoreFileAsync(cancellationToken).ConfigureAwait(false);
return;
}
switch (fileHeader.CompressionAlgorithm)
{
case 15: // rar 1.5 compression
await unpack15Async(fileHeader.IsSolid, cancellationToken).ConfigureAwait(false);
break;
case 20: // rar 2.x compression
case 26: // files larger than 2GB
await unpack20Async(fileHeader.IsSolid, cancellationToken).ConfigureAwait(false);
break;
case 29: // rar 3.x compression
case 36: // alternative hash
await Unpack29Async(fileHeader.IsSolid, cancellationToken).ConfigureAwait(false);
break;
case 50: // rar 5.x compression
await Unpack5Async(fileHeader.IsSolid, cancellationToken).ConfigureAwait(false);
break;
default:
throw new InvalidFormatException(
"unknown rar compression version " + fileHeader.CompressionAlgorithm
);
}
}
private void UnstoreFile()
{
Span<byte> buffer = stackalloc byte[(int)Math.Min(0x10000, destUnpSize)];
@@ -205,6 +260,26 @@ internal sealed partial class Unpack : BitInput, IRarUnpack
} while (!suspended && destUnpSize > 0);
}
private async System.Threading.Tasks.Task UnstoreFileAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
var buffer = new byte[(int)Math.Min(0x10000, destUnpSize)];
do
{
var code = await readStream
.ReadAsync(buffer, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false);
if (code == 0 || code == -1)
{
break;
}
code = code < destUnpSize ? code : (int)destUnpSize;
await writeStream.WriteAsync(buffer, 0, code, cancellationToken).ConfigureAwait(false);
destUnpSize -= code;
} while (!suspended && destUnpSize > 0);
}
private void Unpack29(bool solid)
{
Span<int> DDecode = stackalloc int[PackDef.DC];
@@ -483,6 +558,281 @@ internal sealed partial class Unpack : BitInput, IRarUnpack
UnpWriteBuf();
}
private async System.Threading.Tasks.Task Unpack29Async(
bool solid,
System.Threading.CancellationToken cancellationToken = default
)
{
int[] DDecode = new int[PackDef.DC];
byte[] DBits = new byte[PackDef.DC];
int Bits;
if (DDecode[1] == 0)
{
int Dist = 0,
BitLength = 0,
Slot = 0;
for (var I = 0; I < DBitLengthCounts.Length; I++, BitLength++)
{
var count = DBitLengthCounts[I];
for (var J = 0; J < count; J++, Slot++, Dist += (1 << BitLength))
{
DDecode[Slot] = Dist;
DBits[Slot] = (byte)BitLength;
}
}
}
FileExtracted = true;
if (!suspended)
{
UnpInitData(solid);
if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
return;
}
if ((!solid || !tablesRead) && !ReadTables())
{
return;
}
}
if (ppmError)
{
return;
}
while (true)
{
unpPtr &= PackDef.MAXWINMASK;
if (inAddr > readBorder)
{
if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
break;
}
}
if (((wrPtr - unpPtr) & PackDef.MAXWINMASK) < 260 && wrPtr != unpPtr)
{
await UnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
if (destUnpSize < 0)
{
return;
}
if (suspended)
{
FileExtracted = false;
return;
}
}
if (unpBlockType == BlockTypes.BLOCK_PPM)
{
var ch = ppm.DecodeChar();
if (ch == -1)
{
ppmError = true;
break;
}
if (ch == PpmEscChar)
{
var nextCh = ppm.DecodeChar();
if (nextCh == 0)
{
if (!ReadTables())
{
break;
}
continue;
}
if (nextCh == 2 || nextCh == -1)
{
break;
}
if (nextCh == 3)
{
if (!ReadVMCode())
{
break;
}
continue;
}
if (nextCh == 4)
{
uint Distance = 0,
Length = 0;
var failed = false;
for (var I = 0; I < 4 && !failed; I++)
{
var ch2 = ppm.DecodeChar();
if (ch2 == -1)
{
failed = true;
}
else if (I == 3)
{
Length = (uint)ch2;
}
else
{
Distance = (Distance << 8) + (uint)ch2;
}
}
if (failed)
{
break;
}
CopyString(Length + 32, Distance + 2);
continue;
}
if (nextCh == 5)
{
var length = ppm.DecodeChar();
if (length == -1)
{
break;
}
CopyString((uint)(length + 4), 1);
continue;
}
}
window[unpPtr++] = (byte)ch;
continue;
}
var Number = this.decodeNumber(LD);
if (Number < 256)
{
window[unpPtr++] = (byte)Number;
continue;
}
if (Number >= 271)
{
var Length = LDecode[Number -= 271] + 3;
if ((Bits = LBits[Number]) > 0)
{
Length += GetBits() >> (16 - Bits);
AddBits(Bits);
}
var DistNumber = this.decodeNumber(DD);
var Distance = DDecode[DistNumber] + 1;
if ((Bits = DBits[DistNumber]) > 0)
{
if (DistNumber > 9)
{
if (Bits > 4)
{
Distance += (GetBits() >> (20 - Bits)) << 4;
AddBits(Bits - 4);
}
if (lowDistRepCount > 0)
{
lowDistRepCount--;
Distance += prevLowDist;
}
else
{
var LowDist = this.decodeNumber(LDD);
if (LowDist == 16)
{
lowDistRepCount = PackDef.LOW_DIST_REP_COUNT - 1;
Distance += prevLowDist;
}
else
{
Distance += LowDist;
prevLowDist = (int)LowDist;
}
}
}
else
{
Distance += GetBits() >> (16 - Bits);
AddBits(Bits);
}
}
if (Distance >= 0x2000)
{
Length++;
if (Distance >= 0x40000)
{
Length++;
}
}
InsertOldDist(Distance);
lastLength = Length;
CopyString(Length, Distance);
continue;
}
if (Number == 256)
{
if (!ReadEndOfBlock())
{
break;
}
continue;
}
if (Number == 257)
{
if (!ReadVMCode())
{
break;
}
continue;
}
if (Number == 258)
{
if (lastLength != 0)
{
CopyString(lastLength, oldDist[0]);
}
continue;
}
if (Number < 263)
{
var DistNum = Number - 259;
var Distance = (uint)oldDist[DistNum];
for (var I = DistNum; I > 0; I--)
{
oldDist[I] = oldDist[I - 1];
}
oldDist[0] = (int)Distance;
var LengthNumber = this.decodeNumber(RD);
var Length = LDecode[LengthNumber] + 2;
if ((Bits = LBits[LengthNumber]) > 0)
{
Length += GetBits() >> (16 - Bits);
AddBits(Bits);
}
lastLength = Length;
CopyString((uint)Length, Distance);
continue;
}
if (Number < 272)
{
var Distance = SDDecode[Number -= 263] + 1;
if ((Bits = SDBits[Number]) > 0)
{
Distance += GetBits() >> (16 - Bits);
AddBits(Bits);
}
InsertOldDist((uint)Distance);
lastLength = 2;
CopyString(2, (uint)Distance);
}
}
await UnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
}
private void UnpWriteBuf()
{
var WrittenBorder = wrPtr;
@@ -1339,6 +1689,256 @@ internal sealed partial class Unpack : BitInput, IRarUnpack
}
}
private async System.Threading.Tasks.Task UnpWriteBufAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
var WrittenBorder = wrPtr;
var WriteSize = (unpPtr - WrittenBorder) & PackDef.MAXWINMASK;
for (var I = 0; I < prgStack.Count; I++)
{
var flt = prgStack[I];
if (flt is null)
{
continue;
}
if (flt.NextWindow)
{
flt.NextWindow = false;
continue;
}
var BlockStart = flt.BlockStart;
var BlockLength = flt.BlockLength;
if (((BlockStart - WrittenBorder) & PackDef.MAXWINMASK) < WriteSize)
{
if (WrittenBorder != BlockStart)
{
await UnpWriteAreaAsync(WrittenBorder, BlockStart, cancellationToken)
.ConfigureAwait(false);
WrittenBorder = BlockStart;
WriteSize = (unpPtr - WrittenBorder) & PackDef.MAXWINMASK;
}
if (BlockLength <= WriteSize)
{
var BlockEnd = (BlockStart + BlockLength) & PackDef.MAXWINMASK;
if (BlockStart < BlockEnd || BlockEnd == 0)
{
rarVM.setMemory(0, window, BlockStart, BlockLength);
}
else
{
var FirstPartLength = PackDef.MAXWINSIZE - BlockStart;
rarVM.setMemory(0, window, BlockStart, FirstPartLength);
rarVM.setMemory(FirstPartLength, window, 0, BlockEnd);
}
var ParentPrg = filters[flt.ParentFilter].Program;
var Prg = flt.Program;
if (ParentPrg.GlobalData.Count > RarVM.VM_FIXEDGLOBALSIZE)
{
Prg.GlobalData.Clear();
for (
var i = 0;
i < ParentPrg.GlobalData.Count - RarVM.VM_FIXEDGLOBALSIZE;
i++
)
{
Prg.GlobalData[RarVM.VM_FIXEDGLOBALSIZE + i] = ParentPrg.GlobalData[
RarVM.VM_FIXEDGLOBALSIZE + i
];
}
}
ExecuteCode(Prg);
if (Prg.GlobalData.Count > RarVM.VM_FIXEDGLOBALSIZE)
{
if (ParentPrg.GlobalData.Count < Prg.GlobalData.Count)
{
ParentPrg.GlobalData.SetSize(Prg.GlobalData.Count);
}
for (var i = 0; i < Prg.GlobalData.Count - RarVM.VM_FIXEDGLOBALSIZE; i++)
{
ParentPrg.GlobalData[RarVM.VM_FIXEDGLOBALSIZE + i] = Prg.GlobalData[
RarVM.VM_FIXEDGLOBALSIZE + i
];
}
}
else
{
ParentPrg.GlobalData.Clear();
}
var FilteredDataOffset = Prg.FilteredDataOffset;
var FilteredDataSize = Prg.FilteredDataSize;
var FilteredData = ArrayPool<byte>.Shared.Rent(FilteredDataSize);
try
{
Array.Copy(
rarVM.Mem,
FilteredDataOffset,
FilteredData,
0,
FilteredDataSize
);
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;
}
rarVM.setMemory(0, FilteredData, 0, FilteredDataSize);
var pPrg = filters[NextFilter.ParentFilter].Program;
var NextPrg = NextFilter.Program;
if (pPrg.GlobalData.Count > RarVM.VM_FIXEDGLOBALSIZE)
{
NextPrg.GlobalData.SetSize(pPrg.GlobalData.Count);
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)
{
if (pPrg.GlobalData.Count < NextPrg.GlobalData.Count)
{
pPrg.GlobalData.SetSize(NextPrg.GlobalData.Count);
}
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;
}
await writeStream
.WriteAsync(FilteredData, 0, FilteredDataSize, cancellationToken)
.ConfigureAwait(false);
writtenFileSize += FilteredDataSize;
destUnpSize -= FilteredDataSize;
WrittenBorder = BlockEnd;
WriteSize = (unpPtr - WrittenBorder) & PackDef.MAXWINMASK;
}
finally
{
ArrayPool<byte>.Shared.Return(FilteredData);
}
}
else
{
for (var J = I; J < prgStack.Count; J++)
{
var filt = prgStack[J];
if (filt != null && filt.NextWindow)
{
filt.NextWindow = false;
}
}
wrPtr = WrittenBorder;
return;
}
}
}
await UnpWriteAreaAsync(WrittenBorder, unpPtr, cancellationToken).ConfigureAwait(false);
wrPtr = unpPtr;
}
private async System.Threading.Tasks.Task UnpWriteAreaAsync(
int startPtr,
int endPtr,
System.Threading.CancellationToken cancellationToken = default
)
{
if (endPtr < startPtr)
{
await UnpWriteDataAsync(
window,
startPtr,
-startPtr & PackDef.MAXWINMASK,
cancellationToken
)
.ConfigureAwait(false);
await UnpWriteDataAsync(window, 0, endPtr, cancellationToken).ConfigureAwait(false);
}
else
{
await UnpWriteDataAsync(window, startPtr, endPtr - startPtr, cancellationToken)
.ConfigureAwait(false);
}
}
private async System.Threading.Tasks.Task UnpWriteDataAsync(
byte[] data,
int offset,
int size,
System.Threading.CancellationToken cancellationToken = default
)
{
if (destUnpSize < 0)
{
return;
}
var writeSize = size;
if (writeSize > destUnpSize)
{
writeSize = (int)destUnpSize;
}
await writeStream
.WriteAsync(data, offset, writeSize, cancellationToken)
.ConfigureAwait(false);
writtenFileSize += size;
destUnpSize -= size;
}
private void CleanUp()
{
if (ppm != null)

View File

@@ -316,6 +316,110 @@ internal partial class Unpack
oldUnpWriteBuf();
}
private async System.Threading.Tasks.Task unpack15Async(
bool solid,
System.Threading.CancellationToken cancellationToken = default
)
{
if (suspended)
{
unpPtr = wrPtr;
}
else
{
UnpInitData(solid);
oldUnpInitData(solid);
await unpReadBufAsync(cancellationToken).ConfigureAwait(false);
if (!solid)
{
initHuff();
unpPtr = 0;
}
else
{
unpPtr = wrPtr;
}
--destUnpSize;
}
if (destUnpSize >= 0)
{
getFlagsBuf();
FlagsCnt = 8;
}
while (destUnpSize >= 0)
{
unpPtr &= PackDef.MAXWINMASK;
if (
inAddr > readTop - 30
&& !await unpReadBufAsync(cancellationToken).ConfigureAwait(false)
)
{
break;
}
if (((wrPtr - unpPtr) & PackDef.MAXWINMASK) < 270 && wrPtr != unpPtr)
{
await oldUnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
if (suspended)
{
return;
}
}
if (StMode != 0)
{
huffDecode();
continue;
}
if (--FlagsCnt < 0)
{
getFlagsBuf();
FlagsCnt = 7;
}
if ((FlagBuf & 0x80) != 0)
{
FlagBuf <<= 1;
if (Nlzb > Nhfb)
{
longLZ();
}
else
{
huffDecode();
}
}
else
{
FlagBuf <<= 1;
if (--FlagsCnt < 0)
{
getFlagsBuf();
FlagsCnt = 7;
}
if ((FlagBuf & 0x80) != 0)
{
FlagBuf <<= 1;
if (Nlzb > Nhfb)
{
huffDecode();
}
else
{
longLZ();
}
}
else
{
FlagBuf <<= 1;
shortLZ();
}
}
}
await oldUnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
}
private bool unpReadBuf()
{
var dataSize = readTop - inAddr;
@@ -351,6 +455,40 @@ internal partial class Unpack
return (readCode != -1);
}
private async System.Threading.Tasks.Task<bool> unpReadBufAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
var dataSize = readTop - inAddr;
if (dataSize < 0)
{
return (false);
}
if (inAddr > MAX_SIZE / 2)
{
if (dataSize > 0)
{
Array.Copy(InBuf, inAddr, InBuf, 0, dataSize);
}
inAddr = 0;
readTop = dataSize;
}
else
{
dataSize = readTop;
}
var readCode = await readStream
.ReadAsync(InBuf, dataSize, (MAX_SIZE - dataSize) & ~0xf, cancellationToken)
.ConfigureAwait(false);
if (readCode > 0)
{
readTop += readCode;
}
readBorder = readTop - 30;
return (readCode != -1);
}
private int getShortLen1(int pos) => pos == 1 ? Buf60 + 3 : ShortLen1[pos];
private int getShortLen2(int pos) => pos == 3 ? Buf60 + 3 : ShortLen2[pos];
@@ -814,4 +952,26 @@ internal partial class Unpack
}
wrPtr = unpPtr;
}
private async System.Threading.Tasks.Task oldUnpWriteBufAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
if (unpPtr < wrPtr)
{
await writeStream
.WriteAsync(window, wrPtr, -wrPtr & PackDef.MAXWINMASK, cancellationToken)
.ConfigureAwait(false);
await writeStream
.WriteAsync(window, 0, unpPtr, cancellationToken)
.ConfigureAwait(false);
}
else
{
await writeStream
.WriteAsync(window, wrPtr, unpPtr - wrPtr, cancellationToken)
.ConfigureAwait(false);
}
wrPtr = unpPtr;
}
}

View File

@@ -368,6 +368,163 @@ internal partial class Unpack
oldUnpWriteBuf();
}
private async System.Threading.Tasks.Task unpack20Async(
bool solid,
System.Threading.CancellationToken cancellationToken = default
)
{
int Bits;
if (suspended)
{
unpPtr = wrPtr;
}
else
{
UnpInitData(solid);
if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
return;
}
if (!solid)
{
if (!await ReadTables20Async(cancellationToken).ConfigureAwait(false))
{
return;
}
}
--destUnpSize;
}
while (destUnpSize >= 0)
{
unpPtr &= PackDef.MAXWINMASK;
if (inAddr > readTop - 30)
{
if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
break;
}
}
if (((wrPtr - unpPtr) & PackDef.MAXWINMASK) < 270 && wrPtr != unpPtr)
{
await oldUnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
if (suspended)
{
return;
}
}
if (UnpAudioBlock != 0)
{
var AudioNumber = this.decodeNumber(MD[UnpCurChannel]);
if (AudioNumber == 256)
{
if (!await ReadTables20Async(cancellationToken).ConfigureAwait(false))
{
break;
}
continue;
}
window[unpPtr++] = DecodeAudio(AudioNumber);
if (++UnpCurChannel == UnpChannels)
{
UnpCurChannel = 0;
}
--destUnpSize;
continue;
}
var Number = this.decodeNumber(LD);
if (Number < 256)
{
window[unpPtr++] = (byte)Number;
--destUnpSize;
continue;
}
if (Number > 269)
{
var Length = LDecode[Number -= 270] + 3;
if ((Bits = LBits[Number]) > 0)
{
Length += Utility.URShift(GetBits(), (16 - Bits));
AddBits(Bits);
}
var DistNumber = this.decodeNumber(DD);
var Distance = DDecode[DistNumber] + 1;
if ((Bits = DBits[DistNumber]) > 0)
{
Distance += Utility.URShift(GetBits(), (16 - Bits));
AddBits(Bits);
}
if (Distance >= 0x2000)
{
Length++;
if (Distance >= 0x40000L)
{
Length++;
}
}
CopyString20(Length, Distance);
continue;
}
if (Number == 269)
{
if (!await ReadTables20Async(cancellationToken).ConfigureAwait(false))
{
break;
}
continue;
}
if (Number == 256)
{
CopyString20(lastLength, lastDist);
continue;
}
if (Number < 261)
{
var Distance = oldDist[(oldDistPtr - (Number - 256)) & 3];
var LengthNumber = this.decodeNumber(RD);
var Length = LDecode[LengthNumber] + 2;
if ((Bits = LBits[LengthNumber]) > 0)
{
Length += Utility.URShift(GetBits(), (16 - Bits));
AddBits(Bits);
}
if (Distance >= 0x101)
{
Length++;
if (Distance >= 0x2000)
{
Length++;
if (Distance >= 0x40000)
{
Length++;
}
}
}
CopyString20(Length, Distance);
continue;
}
if (Number < 270)
{
var Distance = SDDecode[Number -= 261] + 1;
if ((Bits = SDBits[Number]) > 0)
{
Distance += Utility.URShift(GetBits(), (16 - Bits));
AddBits(Bits);
}
CopyString20(2, Distance);
}
}
ReadLastTables();
await oldUnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
}
private void CopyString20(int Length, int Distance)
{
lastDist = oldDist[oldDistPtr++ & 3] = Distance;
@@ -534,6 +691,120 @@ internal partial class Unpack
return (true);
}
private async System.Threading.Tasks.Task<bool> ReadTables20Async(
System.Threading.CancellationToken cancellationToken = default
)
{
byte[] BitLength = new byte[PackDef.BC20];
byte[] Table = new byte[PackDef.MC20 * 4];
int TableSize,
N,
I;
if (inAddr > readTop - 25)
{
if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
return (false);
}
}
var BitField = GetBits();
UnpAudioBlock = (BitField & 0x8000);
if (0 == (BitField & 0x4000))
{
new Span<byte>(UnpOldTable20).Clear();
}
AddBits(2);
if (UnpAudioBlock != 0)
{
UnpChannels = ((Utility.URShift(BitField, 12)) & 3) + 1;
if (UnpCurChannel >= UnpChannels)
{
UnpCurChannel = 0;
}
AddBits(2);
TableSize = PackDef.MC20 * UnpChannels;
}
else
{
TableSize = PackDef.NC20 + PackDef.DC20 + PackDef.RC20;
}
for (I = 0; I < PackDef.BC20; I++)
{
BitLength[I] = (byte)(Utility.URShift(GetBits(), 12));
AddBits(4);
}
UnpackUtility.makeDecodeTables(BitLength, 0, BD, PackDef.BC20);
I = 0;
while (I < TableSize)
{
if (inAddr > readTop - 5)
{
if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
return (false);
}
}
var Number = this.decodeNumber(BD);
if (Number < 16)
{
Table[I] = (byte)((Number + UnpOldTable20[I]) & 0xf);
I++;
}
else if (Number == 16)
{
N = (Utility.URShift(GetBits(), 14)) + 3;
AddBits(2);
while (N-- > 0 && I < TableSize)
{
Table[I] = Table[I - 1];
I++;
}
}
else
{
if (Number == 17)
{
N = (Utility.URShift(GetBits(), 13)) + 3;
AddBits(3);
}
else
{
N = (Utility.URShift(GetBits(), 9)) + 11;
AddBits(7);
}
while (N-- > 0 && I < TableSize)
{
Table[I++] = 0;
}
}
}
if (inAddr > readTop)
{
return (true);
}
if (UnpAudioBlock != 0)
{
for (I = 0; I < UnpChannels; I++)
{
UnpackUtility.makeDecodeTables(Table, I * PackDef.MC20, MD[I], PackDef.MC20);
}
}
else
{
UnpackUtility.makeDecodeTables(Table, 0, LD, PackDef.NC20);
UnpackUtility.makeDecodeTables(Table, PackDef.NC20, DD, PackDef.DC20);
UnpackUtility.makeDecodeTables(Table, PackDef.NC20 + PackDef.DC20, RD, PackDef.RC20);
}
for (var i = 0; i < UnpOldTable20.Length; i++)
{
UnpOldTable20[i] = Table[i];
}
return (true);
}
private void unpInitData20(bool Solid)
{
if (!Solid)

View File

@@ -479,6 +479,354 @@ internal partial class Unpack
return ReadCode != -1;
}
private async System.Threading.Tasks.Task<bool> UnpReadBufAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
var DataSize = ReadTop - Inp.InAddr; // Data left to process.
if (DataSize < 0)
{
return false;
}
BlockHeader.BlockSize -= Inp.InAddr - BlockHeader.BlockStart;
if (Inp.InAddr > MAX_SIZE / 2)
{
if (DataSize > 0)
{
Array.Copy(InBuf, inAddr, InBuf, 0, DataSize);
}
Inp.InAddr = 0;
ReadTop = DataSize;
}
else
{
DataSize = ReadTop;
}
var ReadCode = 0;
if (MAX_SIZE != DataSize)
{
ReadCode = await readStream
.ReadAsync(InBuf, DataSize, MAX_SIZE - DataSize, cancellationToken)
.ConfigureAwait(false);
}
if (ReadCode > 0) // Can be also -1.
{
ReadTop += ReadCode;
}
ReadBorder = ReadTop - 30;
BlockHeader.BlockStart = Inp.InAddr;
if (BlockHeader.BlockSize != -1) // '-1' means not defined yet.
{
ReadBorder = Math.Min(ReadBorder, BlockHeader.BlockStart + BlockHeader.BlockSize - 1);
}
return ReadCode != -1;
}
public async System.Threading.Tasks.Task Unpack5Async(
bool Solid,
System.Threading.CancellationToken cancellationToken = default
)
{
FileExtracted = true;
if (!Suspended)
{
UnpInitData(Solid);
if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
return;
}
// Check TablesRead5 to be sure that we read tables at least once
// regardless of current block header TablePresent flag.
// So we can safefly use these tables below.
if (
!await ReadBlockHeaderAsync(cancellationToken).ConfigureAwait(false)
|| !ReadTables()
|| !TablesRead5
)
{
return;
}
}
while (true)
{
UnpPtr &= MaxWinMask;
if (Inp.InAddr >= ReadBorder)
{
var FileDone = false;
// We use 'while', because for empty block containing only Huffman table,
// we'll be on the block border once again just after reading the table.
while (
Inp.InAddr > BlockHeader.BlockStart + BlockHeader.BlockSize - 1
|| Inp.InAddr == BlockHeader.BlockStart + BlockHeader.BlockSize - 1
&& Inp.InBit >= BlockHeader.BlockBitSize
)
{
if (BlockHeader.LastBlockInFile)
{
FileDone = true;
break;
}
if (
!await ReadBlockHeaderAsync(cancellationToken).ConfigureAwait(false)
|| !ReadTables()
)
{
return;
}
}
if (FileDone || !await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
break;
}
}
if (
((WriteBorder - UnpPtr) & MaxWinMask) < PackDef.MAX_LZ_MATCH + 3
&& WriteBorder != UnpPtr
)
{
UnpWriteBuf();
if (WrittenFileSize > DestUnpSize)
{
return;
}
if (Suspended)
{
FileExtracted = false;
return;
}
}
//uint MainSlot=DecodeNumber(Inp,LD);
var MainSlot = this.DecodeNumber(LD);
if (MainSlot < 256)
{
// if (Fragmented)
// FragWindow[UnpPtr++]=(byte)MainSlot;
// else
Window[UnpPtr++] = (byte)MainSlot;
continue;
}
if (MainSlot >= 262)
{
var Length = SlotToLength(MainSlot - 262);
//uint DBits,Distance=1,DistSlot=DecodeNumber(Inp,&BlockTables.DD);
int DBits;
uint Distance = 1,
DistSlot = this.DecodeNumber(DD);
if (DistSlot < 4)
{
DBits = 0;
Distance += DistSlot;
}
else
{
//DBits=DistSlot/2 - 1;
DBits = (int)((DistSlot / 2) - 1);
Distance += (2 | (DistSlot & 1)) << DBits;
}
if (DBits > 0)
{
if (DBits >= 4)
{
if (DBits > 4)
{
Distance += ((Inp.getbits() >> (36 - DBits)) << 4);
Inp.AddBits(DBits - 4);
}
//uint LowDist=DecodeNumber(Inp,&BlockTables.LDD);
var LowDist = this.DecodeNumber(LDD);
Distance += LowDist;
}
else
{
Distance += Inp.getbits() >> (32 - DBits);
Inp.AddBits(DBits);
}
}
if (Distance > 0x100)
{
Length++;
if (Distance > 0x2000)
{
Length++;
if (Distance > 0x40000)
{
Length++;
}
}
}
InsertOldDist(Distance);
LastLength = Length;
// if (Fragmented)
// FragWindow.CopyString(Length,Distance,UnpPtr,MaxWinMask);
// else
CopyString(Length, Distance);
continue;
}
if (MainSlot == 256)
{
var Filter = new UnpackFilter();
if (
!await ReadFilterAsync(Filter, cancellationToken).ConfigureAwait(false)
|| !AddFilter(Filter)
)
{
break;
}
continue;
}
if (MainSlot == 257)
{
if (LastLength != 0)
// if (Fragmented)
// FragWindow.CopyString(LastLength,OldDist[0],UnpPtr,MaxWinMask);
// else
//CopyString(LastLength,OldDist[0]);
{
CopyString(LastLength, OldDistN(0));
}
continue;
}
if (MainSlot < 262)
{
//uint DistNum=MainSlot-258;
var DistNum = (int)(MainSlot - 258);
//uint Distance=OldDist[DistNum];
var Distance = OldDistN(DistNum);
//for (uint I=DistNum;I>0;I--)
for (var I = DistNum; I > 0; I--)
//OldDistN[I]=OldDistN(I-1);
{
SetOldDistN(I, OldDistN(I - 1));
}
//OldDistN[0]=Distance;
SetOldDistN(0, Distance);
var LengthSlot = this.DecodeNumber(RD);
var Length = SlotToLength(LengthSlot);
LastLength = Length;
// if (Fragmented)
// FragWindow.CopyString(Length,Distance,UnpPtr,MaxWinMask);
// else
CopyString(Length, Distance);
continue;
}
}
UnpWriteBuf();
}
private async System.Threading.Tasks.Task<bool> ReadBlockHeaderAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
Header.HeaderSize = 0;
if (!Inp.ExternalBuffer && Inp.InAddr > ReadTop - 7)
{
if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
return false;
}
}
//Inp.faddbits((8-Inp.InBit)&7);
Inp.faddbits((uint)((8 - Inp.InBit) & 7));
var BlockFlags = (byte)(Inp.fgetbits() >> 8);
Inp.faddbits(8);
//uint ByteCount=((BlockFlags>>3)&3)+1; // Block size byte count.
var ByteCount = (uint)(((BlockFlags >> 3) & 3) + 1); // Block size byte count.
if (ByteCount == 4)
{
return false;
}
//Header.HeaderSize=2+ByteCount;
Header.HeaderSize = (int)(2 + ByteCount);
Header.BlockBitSize = (BlockFlags & 7) + 1;
var SavedCheckSum = (byte)(Inp.fgetbits() >> 8);
Inp.faddbits(8);
var BlockSize = 0;
//for (uint I=0;I<ByteCount;I++)
for (var I = 0; I < ByteCount; I++)
{
//BlockSize+=(Inp.fgetbits()>>8)<<(I*8);
BlockSize += (int)(Inp.fgetbits() >> 8) << (I * 8);
Inp.AddBits(8);
}
Header.BlockSize = BlockSize;
var CheckSum = (byte)(0x5a ^ BlockFlags ^ BlockSize ^ (BlockSize >> 8) ^ (BlockSize >> 16));
if (CheckSum != SavedCheckSum)
{
return false;
}
Header.BlockStart = Inp.InAddr;
ReadBorder = Math.Min(ReadBorder, Header.BlockStart + Header.BlockSize - 1);
Header.LastBlockInFile = (BlockFlags & 0x40) != 0;
Header.TablePresent = (BlockFlags & 0x80) != 0;
return true;
}
private async System.Threading.Tasks.Task<bool> ReadFilterAsync(
UnpackFilter Filter,
System.Threading.CancellationToken cancellationToken = default
)
{
if (!Inp.ExternalBuffer && Inp.InAddr > ReadTop - 16)
{
if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
return false;
}
}
Filter.uBlockStart = ReadFilterData();
Filter.uBlockLength = ReadFilterData();
if (Filter.BlockLength > MAX_FILTER_BLOCK_SIZE)
{
Filter.BlockLength = 0;
}
//Filter.Type=Inp.fgetbits()>>13;
Filter.Type = (byte)(Inp.fgetbits() >> 13);
Inp.faddbits(3);
if (Filter.Type == (byte)FilterType.FILTER_DELTA)
{
//Filter.Channels=(Inp.fgetbits()>>11)+1;
Filter.Channels = (byte)((Inp.fgetbits() >> 11) + 1);
Inp.faddbits(5);
}
return true;
}
//?
// void UnpWriteBuf()
// {
@@ -814,116 +1162,5 @@ internal partial class Unpack
Header.TablePresent = (BlockFlags & 0x80) != 0;
return true;
}
//?
// bool ReadTables(BitInput Inp, ref UnpackBlockHeader Header, ref UnpackBlockTables Tables)
// {
// if (!Header.TablePresent)
// return true;
//
// if (!Inp.ExternalBuffer && Inp.InAddr>ReadTop-25)
// if (!UnpReadBuf())
// return false;
//
// byte BitLength[BC];
// for (uint I=0;I<BC;I++)
// {
// uint Length=(byte)(Inp.fgetbits() >> 12);
// Inp.faddbits(4);
// if (Length==15)
// {
// uint ZeroCount=(byte)(Inp.fgetbits() >> 12);
// Inp.faddbits(4);
// if (ZeroCount==0)
// BitLength[I]=15;
// else
// {
// ZeroCount+=2;
// while (ZeroCount-- > 0 && I<ASIZE(BitLength))
// BitLength[I++]=0;
// I--;
// }
// }
// else
// BitLength[I]=Length;
// }
//
// MakeDecodeTables(BitLength,&Tables.BD,BC);
//
// byte Table[HUFF_TABLE_SIZE];
// const uint TableSize=HUFF_TABLE_SIZE;
// for (uint I=0;I<TableSize;)
// {
// if (!Inp.ExternalBuffer && Inp.InAddr>ReadTop-5)
// if (!UnpReadBuf())
// return false;
// uint Number=DecodeNumber(Inp,&Tables.BD);
// if (Number<16)
// {
// Table[I]=Number;
// I++;
// }
// else
// if (Number<18)
// {
// uint N;
// if (Number==16)
// {
// N=(Inp.fgetbits() >> 13)+3;
// Inp.faddbits(3);
// }
// else
// {
// N=(Inp.fgetbits() >> 9)+11;
// Inp.faddbits(7);
// }
// if (I==0)
// {
// // We cannot have "repeat previous" code at the first position.
// // Multiple such codes would shift Inp position without changing I,
// // which can lead to reading beyond of Inp boundary in mutithreading
// // mode, where Inp.ExternalBuffer disables bounds check and we just
// // reserve a lot of buffer space to not need such check normally.
// return false;
// }
// else
// while (N-- > 0 && I<TableSize)
// {
// Table[I]=Table[I-1];
// I++;
// }
// }
// else
// {
// uint N;
// if (Number==18)
// {
// N=(Inp.fgetbits() >> 13)+3;
// Inp.faddbits(3);
// }
// else
// {
// N=(Inp.fgetbits() >> 9)+11;
// Inp.faddbits(7);
// }
// while (N-- > 0 && I<TableSize)
// Table[I++]=0;
// }
// }
// TablesRead5=true;
// if (!Inp.ExternalBuffer && Inp.InAddr>ReadTop)
// return false;
// MakeDecodeTables(&Table[0],&Tables.LD,NC);
// MakeDecodeTables(&Table[NC],&Tables.DD,DC);
// MakeDecodeTables(&Table[NC+DC],&Tables.LDD,LDC);
// MakeDecodeTables(&Table[NC+DC+LDC],&Tables.RD,RC);
// return true;
// }
//?
// void InitFilters()
// {
// Filters.SoftReset();
// }
}
#endif

View File

@@ -29,9 +29,28 @@ internal partial class Unpack : IRarUnpack
// NOTE: caller has logic to check for -1 for error we throw instead.
readStream.Read(buf, offset, count);
private async System.Threading.Tasks.Task<int> UnpIO_UnpReadAsync(
byte[] buf,
int offset,
int count,
System.Threading.CancellationToken cancellationToken = default
) =>
// NOTE: caller has logic to check for -1 for error we throw instead.
await readStream.ReadAsync(buf, offset, count, cancellationToken).ConfigureAwait(false);
private void UnpIO_UnpWrite(byte[] buf, size_t offset, uint count) =>
writeStream.Write(buf, checked((int)offset), checked((int)count));
private async System.Threading.Tasks.Task UnpIO_UnpWriteAsync(
byte[] buf,
size_t offset,
uint count,
System.Threading.CancellationToken cancellationToken = default
) =>
await writeStream
.WriteAsync(buf, checked((int)offset), checked((int)count), cancellationToken)
.ConfigureAwait(false);
public void DoUnpack(FileHeader fileHeader, Stream readStream, Stream writeStream)
{
// as of 12/2017 .NET limits array indexing to using a signed integer
@@ -53,6 +72,25 @@ internal partial class Unpack : IRarUnpack
DoUnpack();
}
public async System.Threading.Tasks.Task DoUnpackAsync(
FileHeader fileHeader,
Stream readStream,
Stream writeStream,
System.Threading.CancellationToken cancellationToken = default
)
{
DestUnpSize = fileHeader.UncompressedSize;
this.fileHeader = fileHeader;
this.readStream = readStream;
this.writeStream = writeStream;
if (!fileHeader.IsStored)
{
Init(fileHeader.WindowSize, fileHeader.IsSolid);
}
Suspended = false;
await DoUnpackAsync(cancellationToken).ConfigureAwait(false);
}
public void DoUnpack()
{
if (fileHeader.IsStored)
@@ -65,6 +103,27 @@ internal partial class Unpack : IRarUnpack
}
}
public async System.Threading.Tasks.Task DoUnpackAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
if (fileHeader.IsStored)
{
await UnstoreFileAsync(cancellationToken).ConfigureAwait(false);
}
else
{
// TODO: When compression methods are converted to async, call them here
// For now, fall back to synchronous version
await DoUnpackAsync(
fileHeader.CompressionAlgorithm,
fileHeader.IsSolid,
cancellationToken
)
.ConfigureAwait(false);
}
}
private void UnstoreFile()
{
Span<byte> b = stackalloc byte[(int)Math.Min(0x10000, DestUnpSize)];
@@ -80,6 +139,25 @@ internal partial class Unpack : IRarUnpack
} while (!Suspended);
}
private async System.Threading.Tasks.Task UnstoreFileAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
var buffer = new byte[(int)Math.Min(0x10000, DestUnpSize)];
do
{
var n = await readStream
.ReadAsync(buffer, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false);
if (n == 0)
{
break;
}
await writeStream.WriteAsync(buffer, 0, n, cancellationToken).ConfigureAwait(false);
DestUnpSize -= n;
} while (!Suspended);
}
public bool Suspended { get; set; }
public long DestSize => DestUnpSize;

View File

@@ -200,6 +200,102 @@ internal partial class Unpack
UnpWriteBuf20();
}
private async System.Threading.Tasks.Task Unpack15Async(
bool Solid,
System.Threading.CancellationToken cancellationToken = default
)
{
UnpInitData(Solid);
UnpInitData15(Solid);
await UnpReadBufAsync(cancellationToken).ConfigureAwait(false);
if (!Solid)
{
InitHuff();
UnpPtr = 0;
}
else
{
UnpPtr = WrPtr;
}
--DestUnpSize;
if (DestUnpSize >= 0)
{
GetFlagsBuf();
FlagsCnt = 8;
}
while (DestUnpSize >= 0)
{
UnpPtr &= MaxWinMask;
if (
Inp.InAddr > ReadTop - 30
&& !await UnpReadBufAsync(cancellationToken).ConfigureAwait(false)
)
{
break;
}
if (((WrPtr - UnpPtr) & MaxWinMask) < 270 && WrPtr != UnpPtr)
{
await UnpWriteBuf20Async(cancellationToken).ConfigureAwait(false);
}
if (StMode != 0)
{
HuffDecode();
continue;
}
if (--FlagsCnt < 0)
{
GetFlagsBuf();
FlagsCnt = 7;
}
if ((FlagBuf & 0x80) != 0)
{
FlagBuf <<= 1;
if (Nlzb > Nhfb)
{
LongLZ();
}
else
{
HuffDecode();
}
}
else
{
FlagBuf <<= 1;
if (--FlagsCnt < 0)
{
GetFlagsBuf();
FlagsCnt = 7;
}
if ((FlagBuf & 0x80) != 0)
{
FlagBuf <<= 1;
if (Nlzb > Nhfb)
{
HuffDecode();
}
else
{
LongLZ();
}
}
else
{
FlagBuf <<= 1;
ShortLZ();
}
}
}
await UnpWriteBuf20Async(cancellationToken).ConfigureAwait(false);
}
//#define GetShortLen1(pos) ((pos)==1 ? Buf60+3:ShortLen1[pos])
private uint GetShortLen1(uint pos) => ((pos) == 1 ? (uint)(Buf60 + 3) : ShortLen1[pos]);

View File

@@ -349,6 +349,170 @@ internal partial class Unpack
UnpWriteBuf20();
}
private async System.Threading.Tasks.Task Unpack20Async(
bool Solid,
System.Threading.CancellationToken cancellationToken = default
)
{
uint Bits;
if (Suspended)
{
UnpPtr = WrPtr;
}
else
{
UnpInitData(Solid);
if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
return;
}
if (
(!Solid || !TablesRead2)
&& !await ReadTables20Async(cancellationToken).ConfigureAwait(false)
)
{
return;
}
--DestUnpSize;
}
while (DestUnpSize >= 0)
{
UnpPtr &= MaxWinMask;
if (Inp.InAddr > ReadTop - 30)
{
if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
break;
}
}
if (((WrPtr - UnpPtr) & MaxWinMask) < 270 && WrPtr != UnpPtr)
{
await UnpWriteBuf20Async(cancellationToken).ConfigureAwait(false);
if (Suspended)
{
return;
}
}
if (UnpAudioBlock)
{
var AudioNumber = DecodeNumber(Inp, MD[UnpCurChannel]);
if (AudioNumber == 256)
{
if (!await ReadTables20Async(cancellationToken).ConfigureAwait(false))
{
break;
}
continue;
}
Window[UnpPtr++] = DecodeAudio((int)AudioNumber);
if (++UnpCurChannel == UnpChannels)
{
UnpCurChannel = 0;
}
--DestUnpSize;
continue;
}
var Number = DecodeNumber(Inp, BlockTables.LD);
if (Number < 256)
{
Window[UnpPtr++] = (byte)Number;
--DestUnpSize;
continue;
}
if (Number > 269)
{
var Length = (uint)(LDecode[Number -= 270] + 3);
if ((Bits = LBits[Number]) > 0)
{
Length += Inp.getbits() >> (int)(16 - Bits);
Inp.addbits(Bits);
}
var DistNumber = DecodeNumber(Inp, BlockTables.DD);
var Distance = DDecode[DistNumber] + 1;
if ((Bits = DBits[DistNumber]) > 0)
{
Distance += Inp.getbits() >> (int)(16 - Bits);
Inp.addbits(Bits);
}
if (Distance >= 0x2000)
{
Length++;
if (Distance >= 0x40000L)
{
Length++;
}
}
CopyString20(Length, Distance);
continue;
}
if (Number == 269)
{
if (!await ReadTables20Async(cancellationToken).ConfigureAwait(false))
{
break;
}
continue;
}
if (Number == 256)
{
CopyString20(LastLength, LastDist);
continue;
}
if (Number < 261)
{
var Distance = OldDist[(OldDistPtr - (Number - 256)) & 3];
var LengthNumber = DecodeNumber(Inp, BlockTables.RD);
var Length = (uint)(LDecode[LengthNumber] + 2);
if ((Bits = LBits[LengthNumber]) > 0)
{
Length += Inp.getbits() >> (int)(16 - Bits);
Inp.addbits(Bits);
}
if (Distance >= 0x101)
{
Length++;
if (Distance >= 0x2000)
{
Length++;
if (Distance >= 0x40000)
{
Length++;
}
}
}
CopyString20(Length, Distance);
continue;
}
if (Number < 270)
{
var Distance = (uint)(SDDecode[Number -= 261] + 1);
if ((Bits = SDBits[Number]) > 0)
{
Distance += Inp.getbits() >> (int)(16 - Bits);
Inp.addbits(Bits);
}
CopyString20(2, Distance);
continue;
}
}
ReadLastTables();
await UnpWriteBuf20Async(cancellationToken).ConfigureAwait(false);
}
private void UnpWriteBuf20()
{
if (UnpPtr != WrPtr)
@@ -370,6 +534,36 @@ internal partial class Unpack
WrPtr = UnpPtr;
}
private async System.Threading.Tasks.Task UnpWriteBuf20Async(
System.Threading.CancellationToken cancellationToken = default
)
{
if (UnpPtr != WrPtr)
{
UnpSomeRead = true;
}
if (UnpPtr < WrPtr)
{
await UnpIO_UnpWriteAsync(
Window,
WrPtr,
(uint)(-(int)WrPtr & MaxWinMask),
cancellationToken
)
.ConfigureAwait(false);
await UnpIO_UnpWriteAsync(Window, 0, UnpPtr, cancellationToken).ConfigureAwait(false);
UnpAllBuf = true;
}
else
{
await UnpIO_UnpWriteAsync(Window, WrPtr, UnpPtr - WrPtr, cancellationToken)
.ConfigureAwait(false);
}
WrPtr = UnpPtr;
}
private bool ReadTables20()
{
Span<byte> BitLength = stackalloc byte[checked((int)BC20)];
@@ -490,6 +684,130 @@ internal partial class Unpack
return true;
}
private async System.Threading.Tasks.Task<bool> ReadTables20Async(
System.Threading.CancellationToken cancellationToken = default
)
{
byte[] BitLength = new byte[checked((int)BC20)];
byte[] Table = new byte[checked((int)MC20 * 4)];
if (Inp.InAddr > ReadTop - 25)
{
if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
return false;
}
}
var BitField = Inp.getbits();
UnpAudioBlock = (BitField & 0x8000) != 0;
if ((BitField & 0x4000) != 0)
{
Array.Clear(UnpOldTable20, 0, UnpOldTable20.Length);
}
Inp.addbits(2);
uint TableSize;
if (UnpAudioBlock)
{
UnpChannels = ((BitField >> 12) & 3) + 1;
if (UnpCurChannel >= UnpChannels)
{
UnpCurChannel = 0;
}
Inp.addbits(2);
TableSize = MC20 * UnpChannels;
}
else
{
TableSize = NC20 + DC20 + RC20;
}
for (int I = 0; I < checked((int)BC20); I++)
{
BitLength[I] = (byte)(Inp.getbits() >> 12);
Inp.addbits(4);
}
MakeDecodeTables(BitLength, 0, BlockTables.BD, BC20);
for (int I = 0; I < checked((int)TableSize); )
{
if (Inp.InAddr > ReadTop - 5)
{
if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
return false;
}
}
var Number = DecodeNumber(Inp, BlockTables.BD);
if (Number < 16)
{
Table[I] = (byte)((Number + UnpOldTable20[I]) & 0xF);
I++;
}
else if (Number < 18)
{
uint N;
if (Number == 16)
{
N = (Inp.getbits() >> 14) + 3;
Inp.addbits(2);
}
else
{
N = (Inp.getbits() >> 13) + 11;
Inp.addbits(3);
}
if (I == 0)
{
return false;
}
while (N-- > 0 && I < checked((int)TableSize))
{
Table[I] = Table[I - 1];
I++;
}
}
else
{
uint N;
if (Number == 18)
{
N = (Inp.getbits() >> 13) + 3;
Inp.addbits(3);
}
else
{
N = (Inp.getbits() >> 9) + 11;
Inp.addbits(7);
}
while (N-- > 0 && I < checked((int)TableSize))
{
Table[I++] = 0;
}
}
}
if (UnpAudioBlock)
{
for (int I = 0; I < UnpChannels; I++)
{
MakeDecodeTables(Table, (int)(I * MC20), MD[I], MC20);
}
}
else
{
MakeDecodeTables(Table, 0, BlockTables.LD, NC20);
MakeDecodeTables(Table, (int)NC20, BlockTables.DD, DC20);
MakeDecodeTables(Table, (int)(NC20 + DC20), BlockTables.RD, RC20);
}
Array.Copy(Table, 0, this.UnpOldTable20, 0, UnpOldTable20.Length);
return true;
}
private void ReadLastTables()
{
if (ReadTop >= Inp.InAddr + 5)

View File

@@ -1,793 +0,0 @@
#if !Rar2017_64bit
#else
using nint = System.Int64;
using nuint = System.UInt64;
using size_t = System.UInt64;
#endif
//using static SharpCompress.Compressors.Rar.UnpackV2017.Unpack.Unpack30Local;
/*
namespace SharpCompress.Compressors.Rar.UnpackV2017
{
internal partial class Unpack
{
#if !RarV2017_RAR5ONLY
// We use it instead of direct PPM.DecodeChar call to be sure that
// we reset PPM structures in case of corrupt data. It is important,
// because these structures can be invalid after PPM.DecodeChar returned -1.
int SafePPMDecodeChar()
{
int Ch=PPM.DecodeChar();
if (Ch==-1) // Corrupt PPM data found.
{
PPM.CleanUp(); // Reset possibly corrupt PPM data structures.
UnpBlockType=BLOCK_LZ; // Set faster and more fail proof LZ mode.
}
return(Ch);
}
internal static class Unpack30Local {
public static readonly byte[] LDecode={0,1,2,3,4,5,6,7,8,10,12,14,16,20,24,28,32,40,48,56,64,80,96,112,128,160,192,224};
public static readonly byte[] LBits= {0,0,0,0,0,0,0,0,1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5};
public static readonly int[] DDecode = new int[DC];
public static readonly byte[] DBits = new byte[DC];
public static readonly int[] DBitLengthCounts= {4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,14,0,12};
public static readonly byte[] SDDecode={0,4,8,16,32,64,128,192};
public static readonly byte[] SDBits= {2,2,3, 4, 5, 6, 6, 6};
}
void Unpack29(bool Solid)
{
uint Bits;
if (DDecode[1]==0)
{
int Dist=0,BitLength=0,Slot=0;
for (int I=0;I<DBitLengthCounts.Length;I++,BitLength++)
for (int J=0;J<DBitLengthCounts[I];J++,Slot++,Dist+=(1<<BitLength))
{
DDecode[Slot]=Dist;
DBits[Slot]=(byte)BitLength;
}
}
FileExtracted=true;
if (!Suspended)
{
UnpInitData(Solid);
if (!UnpReadBuf30())
return;
if ((!Solid || !TablesRead3) && !ReadTables30())
return;
}
while (true)
{
UnpPtr&=MaxWinMask;
if (Inp.InAddr>ReadBorder)
{
if (!UnpReadBuf30())
break;
}
if (((WrPtr-UnpPtr) & MaxWinMask)<260 && WrPtr!=UnpPtr)
{
UnpWriteBuf30();
if (WrittenFileSize>DestUnpSize)
return;
if (Suspended)
{
FileExtracted=false;
return;
}
}
if (UnpBlockType==BLOCK_PPM)
{
// Here speed is critical, so we do not use SafePPMDecodeChar,
// because sometimes even the inline function can introduce
// some additional penalty.
int Ch=PPM.DecodeChar();
if (Ch==-1) // Corrupt PPM data found.
{
PPM.CleanUp(); // Reset possibly corrupt PPM data structures.
UnpBlockType=BLOCK_LZ; // Set faster and more fail proof LZ mode.
break;
}
if (Ch==PPMEscChar)
{
int NextCh=SafePPMDecodeChar();
if (NextCh==0) // End of PPM encoding.
{
if (!ReadTables30())
break;
continue;
}
if (NextCh==-1) // Corrupt PPM data found.
break;
if (NextCh==2) // End of file in PPM mode.
break;
if (NextCh==3) // Read VM code.
{
if (!ReadVMCodePPM())
break;
continue;
}
if (NextCh==4) // LZ inside of PPM.
{
uint Distance=0,Length;
bool Failed=false;
for (int I=0;I<4 && !Failed;I++)
{
int _Ch=SafePPMDecodeChar();
if (_Ch==-1)
Failed=true;
else
if (I==3)
Length=(byte)_Ch;
else
Distance=(Distance<<8)+(byte)_Ch;
}
if (Failed)
break;
CopyString(Length+32,Distance+2);
continue;
}
if (NextCh==5) // One byte distance match (RLE) inside of PPM.
{
int Length=SafePPMDecodeChar();
if (Length==-1)
break;
CopyString((uint)(Length+4),1);
continue;
}
// If we are here, NextCh must be 1, what means that current byte
// is equal to our 'escape' byte, so we just store it to Window.
}
Window[UnpPtr++]=(byte)Ch;
continue;
}
uint Number=DecodeNumber(Inp,BlockTables.LD);
if (Number<256)
{
Window[UnpPtr++]=(byte)Number;
continue;
}
if (Number>=271)
{
uint Length=(uint)(LDecode[Number-=271]+3);
if ((Bits=LBits[Number])>0)
{
Length+=Inp.getbits()>>(int)(16-Bits);
Inp.addbits(Bits);
}
uint DistNumber=DecodeNumber(Inp,BlockTables.DD);
uint Distance=(uint)(DDecode[DistNumber]+1);
if ((Bits=DBits[DistNumber])>0)
{
if (DistNumber>9)
{
if (Bits>4)
{
Distance+=((Inp.getbits()>>(int)(20-Bits))<<4);
Inp.addbits(Bits-4);
}
if (LowDistRepCount>0)
{
LowDistRepCount--;
Distance+=(uint)PrevLowDist;
}
else
{
uint LowDist=DecodeNumber(Inp,BlockTables.LDD);
if (LowDist==16)
{
LowDistRepCount=(int)(LOW_DIST_REP_COUNT-1);
Distance+=(uint)PrevLowDist;
}
else
{
Distance+=LowDist;
PrevLowDist=(int)LowDist;
}
}
}
else
{
Distance+=Inp.getbits()>>(int)(16-Bits);
Inp.addbits(Bits);
}
}
if (Distance>=0x2000)
{
Length++;
if (Distance>=0x40000)
Length++;
}
InsertOldDist(Distance);
LastLength=Length;
CopyString(Length,Distance);
continue;
}
if (Number==256)
{
if (!ReadEndOfBlock())
break;
continue;
}
if (Number==257)
{
if (!ReadVMCode())
break;
continue;
}
if (Number==258)
{
if (LastLength!=0)
CopyString(LastLength,OldDist[0]);
continue;
}
if (Number<263)
{
uint DistNum=Number-259;
uint Distance=OldDist[DistNum];
for (uint I=DistNum;I>0;I--)
OldDist[I]=OldDist[I-1];
OldDist[0]=Distance;
uint LengthNumber=DecodeNumber(Inp,BlockTables.RD);
int Length=LDecode[LengthNumber]+2;
if ((Bits=LBits[LengthNumber])>0)
{
Length+=(int)(Inp.getbits()>>(int)(16-Bits));
Inp.addbits(Bits);
}
LastLength=(uint)Length;
CopyString((uint)Length,Distance);
continue;
}
if (Number<272)
{
uint Distance=(uint)(SDDecode[Number-=263]+1);
if ((Bits=SDBits[Number])>0)
{
Distance+=Inp.getbits()>>(int)(16-Bits);
Inp.addbits(Bits);
}
InsertOldDist(Distance);
LastLength=2;
CopyString(2,Distance);
continue;
}
}
UnpWriteBuf30();
}
// Return 'false' to quit unpacking the current file or 'true' to continue.
bool ReadEndOfBlock()
{
uint BitField=Inp.getbits();
bool NewTable,NewFile=false;
// "1" - no new file, new table just here.
// "00" - new file, no new table.
// "01" - new file, new table (in beginning of next file).
if ((BitField & 0x8000)!=0)
{
NewTable=true;
Inp.addbits(1);
}
else
{
NewFile=true;
NewTable=(BitField & 0x4000)!=0;
Inp.addbits(2);
}
TablesRead3=!NewTable;
// Quit immediately if "new file" flag is set. If "new table" flag
// is present, we'll read the table in beginning of next file
// based on 'TablesRead3' 'false' value.
if (NewFile)
return false;
return ReadTables30(); // Quit only if we failed to read tables.
}
bool ReadVMCode()
{
// Entire VM code is guaranteed to fully present in block defined
// by current Huffman table. Compressor checks that VM code does not cross
// Huffman block boundaries.
uint FirstByte=Inp.getbits()>>8;
Inp.addbits(8);
uint Length=(FirstByte & 7)+1;
if (Length==7)
{
Length=(Inp.getbits()>>8)+7;
Inp.addbits(8);
}
else
if (Length==8)
{
Length=Inp.getbits();
Inp.addbits(16);
}
if (Length==0)
return false;
Array<byte> VMCode(Length);
for (uint I=0;I<Length;I++)
{
// Try to read the new buffer if only one byte is left.
// But if we read all bytes except the last, one byte is enough.
if (Inp.InAddr>=ReadTop-1 && !UnpReadBuf30() && I<Length-1)
return false;
VMCode[I]=Inp.getbits()>>8;
Inp.addbits(8);
}
return AddVMCode(FirstByte,&VMCode[0],Length);
}
bool ReadVMCodePPM()
{
uint FirstByte=(uint)SafePPMDecodeChar();
if ((int)FirstByte==-1)
return false;
uint Length=(FirstByte & 7)+1;
if (Length==7)
{
int B1=SafePPMDecodeChar();
if (B1==-1)
return false;
Length=B1+7;
}
else
if (Length==8)
{
int B1=SafePPMDecodeChar();
if (B1==-1)
return false;
int B2=SafePPMDecodeChar();
if (B2==-1)
return false;
Length=B1*256+B2;
}
if (Length==0)
return false;
Array<byte> VMCode(Length);
for (uint I=0;I<Length;I++)
{
int Ch=SafePPMDecodeChar();
if (Ch==-1)
return false;
VMCode[I]=Ch;
}
return AddVMCode(FirstByte,&VMCode[0],Length);
}
bool AddVMCode(uint FirstByte,byte[] Code,int CodeSize)
{
VMCodeInp.InitBitInput();
//x memcpy(VMCodeInp.InBuf,Code,Min(BitInput.MAX_SIZE,CodeSize));
Array.Copy(Code, 0, VMCodeInp.InBuf, 0, Math.Min(BitInput.MAX_SIZE,CodeSize));
VM.Init();
uint FiltPos;
if ((FirstByte & 0x80)!=0)
{
FiltPos=RarVM.ReadData(VMCodeInp);
if (FiltPos==0)
InitFilters30(false);
else
FiltPos--;
}
else
FiltPos=(uint)this.LastFilter; // Use the same filter as last time.
if (FiltPos>Filters30.Count || FiltPos>OldFilterLengths.Count)
return false;
LastFilter=(int)FiltPos;
bool NewFilter=(FiltPos==Filters30.Count);
UnpackFilter30 StackFilter=new UnpackFilter30(); // New filter for PrgStack.
UnpackFilter30 Filter;
if (NewFilter) // New filter code, never used before since VM reset.
{
if (FiltPos>MAX3_UNPACK_FILTERS)
{
// Too many different filters, corrupt archive.
//delete StackFilter;
return false;
}
Filters30.Add(1);
Filters30[Filters30.Count-1]=Filter=new UnpackFilter30();
StackFilter.ParentFilter=(uint)(Filters30.Count-1);
// Reserve one item to store the data block length of our new filter
// entry. We'll set it to real block length below, after reading it.
// But we need to initialize it now, because when processing corrupt
// data, we can access this item even before we set it to real value.
OldFilterLengths.Add(0);
}
else // Filter was used in the past.
{
Filter=Filters30[(int)FiltPos];
StackFilter.ParentFilter=FiltPos;
}
int EmptyCount=0;
for (int I=0;I<PrgStack.Count;I++)
{
PrgStack[I-EmptyCount]=PrgStack[I];
if (PrgStack[I]==null)
EmptyCount++;
if (EmptyCount>0)
PrgStack[I]=null;
}
if (EmptyCount==0)
{
if (PrgStack.Count>MAX3_UNPACK_FILTERS)
{
//delete StackFilter;
return false;
}
PrgStack.Add(1);
EmptyCount=1;
}
size_t StackPos=(uint)(this.PrgStack.Count-EmptyCount);
PrgStack[(int)StackPos]=StackFilter;
uint BlockStart=RarVM.ReadData(VMCodeInp);
if ((FirstByte & 0x40)!=0)
BlockStart+=258;
StackFilter.BlockStart=(uint)((BlockStart+UnpPtr)&MaxWinMask);
if ((FirstByte & 0x20)!=0)
{
StackFilter.BlockLength=RarVM.ReadData(VMCodeInp);
// Store the last data block length for current filter.
OldFilterLengths[(int)FiltPos]=(int)StackFilter.BlockLength;
}
else
{
// Set the data block size to same value as the previous block size
// for same filter. It is possible for corrupt data to access a new
// and not filled yet item of OldFilterLengths array here. This is why
// we set new OldFilterLengths items to zero above.
StackFilter.BlockLength=FiltPos<OldFilterLengths.Count ? OldFilterLengths[(int)FiltPos]:0;
}
StackFilter.NextWindow=WrPtr!=UnpPtr && ((WrPtr-UnpPtr)&MaxWinMask)<=BlockStart;
// DebugLog("\nNextWindow: UnpPtr=%08x WrPtr=%08x BlockStart=%08x",UnpPtr,WrPtr,BlockStart);
memset(StackFilter.Prg.InitR,0,sizeof(StackFilter.Prg.InitR));
StackFilter.Prg.InitR[4]=StackFilter.BlockLength;
if ((FirstByte & 0x10)!=0) // Set registers to optional parameters if any.
{
uint InitMask=VMCodeInp.fgetbits()>>9;
VMCodeInp.faddbits(7);
for (int I=0;I<7;I++)
if ((InitMask & (1<<I)) != 0)
StackFilter.Prg.InitR[I]=RarVM.ReadData(VMCodeInp);
}
if (NewFilter)
{
uint VMCodeSize=RarVM.ReadData(VMCodeInp);
if (VMCodeSize>=0x10000 || VMCodeSize==0)
return false;
Array<byte> VMCode(VMCodeSize);
for (uint I=0;I<VMCodeSize;I++)
{
if (VMCodeInp.Overflow(3))
return false;
VMCode[I]=VMCodeInp.fgetbits()>>8;
VMCodeInp.faddbits(8);
}
VM.Prepare(&VMCode[0],VMCodeSize,&Filter->Prg);
}
StackFilter.Prg.Type=Filter.Prg.Type;
return true;
}
bool UnpReadBuf30()
{
int DataSize=ReadTop-Inp.InAddr; // Data left to process.
if (DataSize<0)
return false;
if (Inp.InAddr>BitInput.MAX_SIZE/2)
{
// If we already processed more than half of buffer, let's move
// remaining data into beginning to free more space for new data
// and ensure that calling function does not cross the buffer border
// even if we did not read anything here. Also it ensures that read size
// is not less than CRYPT_BLOCK_SIZE, so we can align it without risk
// to make it zero.
if (DataSize>0)
//x memmove(Inp.InBuf,Inp.InBuf+Inp.InAddr,DataSize);
Array.Copy(Inp.InBuf,Inp.InAddr,Inp.InBuf,0,DataSize);
Inp.InAddr=0;
ReadTop=DataSize;
}
else
DataSize=ReadTop;
int ReadCode=UnpIO_UnpRead(Inp.InBuf,DataSize,BitInput.MAX_SIZE-DataSize);
if (ReadCode>0)
ReadTop+=ReadCode;
ReadBorder=ReadTop-30;
return ReadCode!=-1;
}
void UnpWriteBuf30()
{
uint WrittenBorder=(uint)WrPtr;
uint WriteSize=(uint)((UnpPtr-WrittenBorder)&MaxWinMask);
for (int I=0;I<PrgStack.Count;I++)
{
// Here we apply filters to data which we need to write.
// We always copy data to virtual machine memory before processing.
// We cannot process them just in place in Window buffer, because
// these data can be used for future string matches, so we must
// preserve them in original form.
UnpackFilter30 flt=PrgStack[I];
if (flt==null)
continue;
if (flt.NextWindow)
{
flt.NextWindow=false;
continue;
}
uint BlockStart=flt.BlockStart;
uint BlockLength=flt.BlockLength;
if (((BlockStart-WrittenBorder)&MaxWinMask)<WriteSize)
{
if (WrittenBorder!=BlockStart)
{
UnpWriteArea(WrittenBorder,BlockStart);
WrittenBorder=BlockStart;
WriteSize=(uint)((UnpPtr-WrittenBorder)&MaxWinMask);
}
if (BlockLength<=WriteSize)
{
uint BlockEnd=(BlockStart+BlockLength)&MaxWinMask;
if (BlockStart<BlockEnd || BlockEnd==0)
VM.SetMemory(0,Window+BlockStart,BlockLength);
else
{
uint FirstPartLength=uint(MaxWinSize-BlockStart);
VM.SetMemory(0,Window+BlockStart,FirstPartLength);
VM.SetMemory(FirstPartLength,Window,BlockEnd);
}
VM_PreparedProgram *ParentPrg=&Filters30[flt->ParentFilter]->Prg;
VM_PreparedProgram *Prg=&flt->Prg;
ExecuteCode(Prg);
byte[] FilteredData=Prg.FilteredData;
uint FilteredDataSize=Prg.FilteredDataSize;
delete PrgStack[I];
PrgStack[I]=null;
while (I+1<PrgStack.Count)
{
UnpackFilter30 NextFilter=PrgStack[I+1];
// It is required to check NextWindow here.
if (NextFilter==null || NextFilter.BlockStart!=BlockStart ||
NextFilter.BlockLength!=FilteredDataSize || NextFilter.NextWindow)
break;
// Apply several filters to same data block.
VM.SetMemory(0,FilteredData,FilteredDataSize);
VM_PreparedProgram *ParentPrg=&Filters30[NextFilter.ParentFilter]->Prg;
VM_PreparedProgram *NextPrg=&NextFilter->Prg;
ExecuteCode(NextPrg);
FilteredData=NextPrg.FilteredData;
FilteredDataSize=NextPrg.FilteredDataSize;
I++;
delete PrgStack[I];
PrgStack[I]=null;
}
UnpIO_UnpWrite(FilteredData,0,FilteredDataSize);
UnpSomeRead=true;
WrittenFileSize+=FilteredDataSize;
WrittenBorder=BlockEnd;
WriteSize=(uint)((UnpPtr-WrittenBorder)&MaxWinMask);
}
else
{
// Current filter intersects the window write border, so we adjust
// the window border to process this filter next time, not now.
for (size_t J=I;J<PrgStack.Count;J++)
{
UnpackFilter30 flt=PrgStack[J];
if (flt!=null && flt.NextWindow)
flt.NextWindow=false;
}
WrPtr=WrittenBorder;
return;
}
}
}
UnpWriteArea(WrittenBorder,UnpPtr);
WrPtr=UnpPtr;
}
void ExecuteCode(VM_PreparedProgram *Prg)
{
Prg->InitR[6]=(uint)WrittenFileSize;
VM.Execute(Prg);
}
bool ReadTables30()
{
byte[] BitLength = new byte[BC];
byte[] Table = new byte[HUFF_TABLE_SIZE30];
if (Inp.InAddr>ReadTop-25)
if (!UnpReadBuf30())
return(false);
Inp.faddbits((uint)((8-Inp.InBit)&7));
uint BitField=Inp.fgetbits();
if ((BitField & 0x8000) != 0)
{
UnpBlockType=BLOCK_PPM;
return(PPM.DecodeInit(this,PPMEscChar));
}
UnpBlockType=BLOCK_LZ;
PrevLowDist=0;
LowDistRepCount=0;
if ((BitField & 0x4000) == 0)
Utility.Memset(UnpOldTable,0,UnpOldTable.Length);
Inp.faddbits(2);
for (uint I=0;I<BC;I++)
{
uint Length=(byte)(Inp.fgetbits() >> 12);
Inp.faddbits(4);
if (Length==15)
{
uint ZeroCount=(byte)(Inp.fgetbits() >> 12);
Inp.faddbits(4);
if (ZeroCount==0)
BitLength[I]=15;
else
{
ZeroCount+=2;
while (ZeroCount-- > 0 && I<BitLength.Length)
BitLength[I++]=0;
I--;
}
}
else
BitLength[I]=(byte)Length;
}
MakeDecodeTables(BitLength,0,BlockTables.BD,BC30);
const uint TableSize=HUFF_TABLE_SIZE30;
for (uint I=0;I<TableSize;)
{
if (Inp.InAddr>ReadTop-5)
if (!UnpReadBuf30())
return(false);
uint Number=DecodeNumber(Inp,BlockTables.BD);
if (Number<16)
{
Table[I]=(byte)((Number+this.UnpOldTable[I]) & 0xf);
I++;
}
else
if (Number<18)
{
uint N;
if (Number==16)
{
N=(Inp.fgetbits() >> 13)+3;
Inp.faddbits(3);
}
else
{
N=(Inp.fgetbits() >> 9)+11;
Inp.faddbits(7);
}
if (I==0)
return false; // We cannot have "repeat previous" code at the first position.
else
while (N-- > 0 && I<TableSize)
{
Table[I]=Table[I-1];
I++;
}
}
else
{
uint N;
if (Number==18)
{
N=(Inp.fgetbits() >> 13)+3;
Inp.faddbits(3);
}
else
{
N=(Inp.fgetbits() >> 9)+11;
Inp.faddbits(7);
}
while (N-- > 0 && I<TableSize)
Table[I++]=0;
}
}
TablesRead3=true;
if (Inp.InAddr>ReadTop)
return false;
MakeDecodeTables(Table,0,BlockTables.LD,NC30);
MakeDecodeTables(Table,(int)NC30,BlockTables.DD,DC30);
MakeDecodeTables(Table,(int)(NC30+DC30),BlockTables.LDD,LDC30);
MakeDecodeTables(Table,(int)(NC30+DC30+LDC30),BlockTables.RD,RC30);
//x memcpy(UnpOldTable,Table,sizeof(UnpOldTable));
Array.Copy(Table,0,UnpOldTable,0,UnpOldTable.Length);
return true;
}
#endif
void UnpInitData30(bool Solid)
{
if (!Solid)
{
TablesRead3=false;
Utility.Memset(UnpOldTable, 0, UnpOldTable.Length);
PPMEscChar=2;
UnpBlockType=BLOCK_LZ;
}
InitFilters30(Solid);
}
void InitFilters30(bool Solid)
{
if (!Solid)
{
//OldFilterLengths.SoftReset();
OldFilterLengths.Clear();
LastFilter=0;
//for (size_t I=0;I<Filters30.Count;I++)
// delete Filters30[I];
//Filters30.SoftReset();
Filters30.Clear();
}
//for (size_t I=0;I<PrgStack.Count;I++)
// delete PrgStack[I];
//PrgStack.SoftReset();
PrgStack.Clear();
}
}
}
*/

View File

@@ -222,6 +222,180 @@ internal partial class Unpack
UnpWriteBuf();
}
private async System.Threading.Tasks.Task Unpack5Async(
bool Solid,
System.Threading.CancellationToken cancellationToken = default
)
{
FileExtracted = true;
if (!Suspended)
{
UnpInitData(Solid);
if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
return;
}
if (
!ReadBlockHeader(Inp, ref BlockHeader)
|| !ReadTables(Inp, ref BlockHeader, ref BlockTables)
|| !TablesRead5
)
{
return;
}
}
while (true)
{
UnpPtr &= MaxWinMask;
if (Inp.InAddr >= ReadBorder)
{
var FileDone = false;
while (
Inp.InAddr > BlockHeader.BlockStart + BlockHeader.BlockSize - 1
|| Inp.InAddr == BlockHeader.BlockStart + BlockHeader.BlockSize - 1
&& Inp.InBit >= BlockHeader.BlockBitSize
)
{
if (BlockHeader.LastBlockInFile)
{
FileDone = true;
break;
}
if (
!ReadBlockHeader(Inp, ref BlockHeader)
|| !ReadTables(Inp, ref BlockHeader, ref BlockTables)
)
{
return;
}
}
if (FileDone || !await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
{
break;
}
}
if (((WriteBorder - UnpPtr) & MaxWinMask) < MAX_LZ_MATCH + 3 && WriteBorder != UnpPtr)
{
await UnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
if (WrittenFileSize > DestUnpSize)
{
return;
}
}
uint MainSlot = DecodeNumber(Inp, BlockTables.LD);
if (MainSlot < 256)
{
if (Fragmented)
{
FragWindow[UnpPtr++] = (byte)MainSlot;
}
else
{
Window[UnpPtr++] = (byte)MainSlot;
}
continue;
}
if (MainSlot >= 262)
{
uint Length = SlotToLength(Inp, MainSlot - 262);
uint DBits,
Distance = 1,
DistSlot = DecodeNumber(Inp, BlockTables.DD);
if (DistSlot < 4)
{
DBits = 0;
Distance += DistSlot;
}
else
{
DBits = (DistSlot / 2) - 1;
Distance += (2 | (DistSlot & 1)) << (int)DBits;
}
if (DBits > 0)
{
if (DBits >= 4)
{
if (DBits > 4)
{
Distance += ((Inp.getbits() >> (int)(20 - DBits)) << 4);
Inp.addbits(DBits - 4);
}
uint LowDist = DecodeNumber(Inp, BlockTables.LDD);
Distance += LowDist;
}
else
{
Distance += Inp.getbits() >> (int)(16 - DBits);
Inp.addbits(DBits);
}
}
if (Distance > 0x100)
{
Length++;
if (Distance > 0x2000)
{
Length++;
if (Distance > 0x40000)
{
Length++;
}
}
}
InsertOldDist(Distance);
LastLength = Length;
CopyString(Length, Distance);
continue;
}
if (MainSlot == 256)
{
var Filter = new UnpackFilter();
if (!ReadFilter(Inp, Filter) || !AddFilter(Filter))
{
break;
}
continue;
}
if (MainSlot == 257)
{
if (LastLength != 0)
{
CopyString((uint)LastLength, (uint)OldDist[0]);
}
continue;
}
if (MainSlot < 262)
{
uint DistNum = MainSlot - 258;
uint Distance = (uint)OldDist[(int)DistNum];
for (var I = (int)DistNum; I > 0; I--)
{
OldDist[I] = OldDist[I - 1];
}
OldDist[0] = Distance;
uint LengthSlot = DecodeNumber(Inp, BlockTables.RD);
uint Length = SlotToLength(Inp, LengthSlot);
LastLength = Length;
CopyString(Length, Distance);
continue;
}
}
await UnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
}
private uint ReadFilterData(BitInput Inp)
{
var ByteCount = (Inp.fgetbits() >> 14) + 1;
@@ -339,6 +513,58 @@ internal partial class Unpack
return ReadCode != -1;
}
private async System.Threading.Tasks.Task<bool> UnpReadBufAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
var DataSize = ReadTop - Inp.InAddr; // Data left to process.
if (DataSize < 0)
{
return false;
}
BlockHeader.BlockSize -= Inp.InAddr - BlockHeader.BlockStart;
if (Inp.InAddr > MAX_SIZE / 2)
{
if (DataSize > 0)
{
Buffer.BlockCopy(Inp.InBuf, Inp.InAddr, Inp.InBuf, 0, DataSize);
}
Inp.InAddr = 0;
ReadTop = DataSize;
}
else
{
DataSize = ReadTop;
}
var ReadCode = 0;
if (MAX_SIZE != DataSize)
{
ReadCode = await UnpIO_UnpReadAsync(
Inp.InBuf,
DataSize,
MAX_SIZE - DataSize,
cancellationToken
)
.ConfigureAwait(false);
}
if (ReadCode > 0) // Can be also -1.
{
ReadTop += ReadCode;
}
ReadBorder = ReadTop - 30;
BlockHeader.BlockStart = Inp.InAddr;
if (BlockHeader.BlockSize != -1) // '-1' means not defined yet.
{
ReadBorder = Math.Min(ReadBorder, BlockHeader.BlockStart + BlockHeader.BlockSize - 1);
}
return ReadCode != -1;
}
private void UnpWriteBuf()
{
var WrittenBorder = WrPtr;
@@ -533,6 +759,163 @@ internal partial class Unpack
}
}
private async System.Threading.Tasks.Task UnpWriteBufAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
var WrittenBorder = WrPtr;
var FullWriteSize = (UnpPtr - WrittenBorder) & MaxWinMask;
var WriteSizeLeft = FullWriteSize;
var NotAllFiltersProcessed = false;
for (var I = 0; I < Filters.Count; I++)
{
var flt = Filters[I];
if (flt.Type == FILTER_NONE)
{
continue;
}
if (flt.NextWindow)
{
if (((flt.BlockStart - WrPtr) & MaxWinMask) <= FullWriteSize)
{
flt.NextWindow = false;
}
continue;
}
var BlockStart = flt.BlockStart;
var BlockLength = flt.BlockLength;
if (((BlockStart - WrittenBorder) & MaxWinMask) < WriteSizeLeft)
{
if (WrittenBorder != BlockStart)
{
await UnpWriteAreaAsync(WrittenBorder, BlockStart, cancellationToken)
.ConfigureAwait(false);
WrittenBorder = BlockStart;
WriteSizeLeft = (UnpPtr - WrittenBorder) & MaxWinMask;
}
if (BlockLength <= WriteSizeLeft)
{
if (BlockLength > 0)
{
var BlockEnd = (BlockStart + BlockLength) & MaxWinMask;
FilterSrcMemory = EnsureCapacity(
FilterSrcMemory,
checked((int)BlockLength)
);
var Mem = FilterSrcMemory;
if (BlockStart < BlockEnd || BlockEnd == 0)
{
if (Fragmented)
{
FragWindow.CopyData(Mem, 0, BlockStart, BlockLength);
}
else
{
Buffer.BlockCopy(Window, (int)BlockStart, Mem, 0, (int)BlockLength);
}
}
else
{
var FirstPartLength = MaxWinSize - BlockStart;
if (Fragmented)
{
FragWindow.CopyData(Mem, 0, BlockStart, FirstPartLength);
FragWindow.CopyData(Mem, FirstPartLength, 0, BlockEnd);
}
else
{
Buffer.BlockCopy(
Window,
(int)BlockStart,
Mem,
0,
(int)FirstPartLength
);
Buffer.BlockCopy(
Window,
0,
Mem,
(int)FirstPartLength,
(int)BlockEnd
);
}
}
var OutMem = ApplyFilter(Mem, BlockLength, flt);
Filters[I].Type = FILTER_NONE;
if (OutMem != null)
{
await UnpIO_UnpWriteAsync(OutMem, 0, BlockLength, cancellationToken)
.ConfigureAwait(false);
WrittenFileSize += BlockLength;
}
WrittenBorder = BlockEnd;
WriteSizeLeft = (UnpPtr - WrittenBorder) & MaxWinMask;
}
}
else
{
NotAllFiltersProcessed = true;
for (var J = I; J < Filters.Count; J++)
{
var fltj = Filters[J];
if (
fltj.Type != FILTER_NONE
&& fltj.NextWindow == false
&& ((fltj.BlockStart - WrPtr) & MaxWinMask) < FullWriteSize
)
{
fltj.NextWindow = true;
}
}
break;
}
}
}
var EmptyCount = 0;
for (var I = 0; I < Filters.Count; I++)
{
if (EmptyCount > 0)
{
Filters[I - EmptyCount] = Filters[I];
}
if (Filters[I].Type == FILTER_NONE)
{
EmptyCount++;
}
}
if (EmptyCount > 0)
{
Filters.RemoveRange(Filters.Count - EmptyCount, EmptyCount);
}
if (!NotAllFiltersProcessed)
{
await UnpWriteAreaAsync(WrittenBorder, UnpPtr, cancellationToken).ConfigureAwait(false);
WrPtr = UnpPtr;
}
WriteBorder = (UnpPtr + Math.Min(MaxWinSize, UNPACK_MAX_WRITE)) & MaxWinMask;
if (
WriteBorder == UnpPtr
|| WrPtr != UnpPtr
&& ((WrPtr - UnpPtr) & MaxWinMask) < ((WriteBorder - UnpPtr) & MaxWinMask)
)
{
WriteBorder = WrPtr;
}
}
private byte[] ApplyFilter(byte[] __d, uint DataSize, UnpackFilter Flt)
{
var Data = 0;
@@ -664,6 +1047,48 @@ internal partial class Unpack
}
}
private async System.Threading.Tasks.Task UnpWriteAreaAsync(
size_t StartPtr,
size_t EndPtr,
System.Threading.CancellationToken cancellationToken = default
)
{
if (EndPtr != StartPtr)
{
UnpSomeRead = true;
}
if (EndPtr < StartPtr)
{
UnpAllBuf = true;
}
if (Fragmented)
{
var SizeToWrite = (EndPtr - StartPtr) & MaxWinMask;
while (SizeToWrite > 0)
{
var BlockSize = FragWindow.GetBlockSize(StartPtr, SizeToWrite);
FragWindow.GetBuffer(StartPtr, out var __buffer, out var __offset);
await UnpWriteDataAsync(__buffer, __offset, BlockSize, cancellationToken)
.ConfigureAwait(false);
SizeToWrite -= BlockSize;
StartPtr = (StartPtr + BlockSize) & MaxWinMask;
}
}
else if (EndPtr < StartPtr)
{
await UnpWriteDataAsync(Window, StartPtr, MaxWinSize - StartPtr, cancellationToken)
.ConfigureAwait(false);
await UnpWriteDataAsync(Window, 0, EndPtr, cancellationToken).ConfigureAwait(false);
}
else
{
await UnpWriteDataAsync(Window, StartPtr, EndPtr - StartPtr, cancellationToken)
.ConfigureAwait(false);
}
}
private void UnpWriteData(byte[] Data, size_t offset, size_t Size)
{
if (WrittenFileSize >= DestUnpSize)
@@ -682,6 +1107,29 @@ internal partial class Unpack
WrittenFileSize += Size;
}
private async System.Threading.Tasks.Task UnpWriteDataAsync(
byte[] Data,
size_t offset,
size_t Size,
System.Threading.CancellationToken cancellationToken = default
)
{
if (WrittenFileSize >= DestUnpSize)
{
return;
}
var WriteSize = Size;
var LeftToWrite = DestUnpSize - WrittenFileSize;
if (WriteSize > LeftToWrite)
{
WriteSize = (size_t)LeftToWrite;
}
await UnpIO_UnpWriteAsync(Data, offset, WriteSize, cancellationToken).ConfigureAwait(false);
WrittenFileSize += Size;
}
private void UnpInitData50(bool Solid)
{
if (!Solid)

View File

@@ -1,16 +1,11 @@
#nullable disable
using System;
using System.Buffers;
using SharpCompress.Common;
using static SharpCompress.Compressors.Rar.UnpackV2017.PackDef;
using static SharpCompress.Compressors.Rar.UnpackV2017.UnpackGlobal;
#if !Rar2017_64bit
using size_t = System.UInt32;
#else
using nint = System.Int64;
using nuint = System.UInt64;
using size_t = System.UInt64;
#endif
namespace SharpCompress.Compressors.Rar.UnpackV2017;
@@ -42,11 +37,9 @@ internal sealed partial class Unpack : BitInput
// It prevents crash if first DoUnpack call is later made with wrong
// (true) 'Solid' value.
UnpInitData(false);
#if !RarV2017_SFX_MODULE
// RAR 1.5 decompression initialization
UnpInitData15(false);
InitHuff();
#endif
}
// later: may need Dispose() if we support thread pool
@@ -110,7 +103,7 @@ internal sealed partial class Unpack : BitInput
throw new InvalidFormatException("Grow && Fragmented");
}
var NewWindow = Fragmented ? null : new byte[WinSize];
var NewWindow = Fragmented ? null : ArrayPool<byte>.Shared.Rent((int)WinSize);
if (NewWindow == null)
{
@@ -126,6 +119,7 @@ internal sealed partial class Unpack : BitInput
if (Window != null) // If allocated by preceding files.
{
//free(Window);
ArrayPool<byte>.Shared.Return(Window);
Window = null;
}
FragWindow.Init(WinSize);
@@ -170,7 +164,6 @@ internal sealed partial class Unpack : BitInput
// just for extra safety.
switch (Method)
{
#if !RarV2017_SFX_MODULE
case 15: // rar 1.5 compression
if (!Fragmented)
{
@@ -186,33 +179,64 @@ internal sealed partial class Unpack : BitInput
}
break;
#endif
#if !RarV2017_RAR5ONLY
case 29: // rar 3.x compression
if (!Fragmented)
{
throw new NotImplementedException();
}
break;
case 50: // RAR 5.0 compression algorithm.
Unpack5(Solid);
break;
#if !Rar2017_NOSTRICT
default:
throw new InvalidFormatException("unknown compression method " + Method);
#endif
}
}
private async System.Threading.Tasks.Task DoUnpackAsync(
uint Method,
bool Solid,
System.Threading.CancellationToken cancellationToken = default
)
{
// Methods <50 will crash in Fragmented mode when accessing NULL Window.
// They cannot be called in such mode now, but we check it below anyway
// just for extra safety.
switch (Method)
{
#if !RarV2017_SFX_MODULE
case 15: // rar 1.5 compression
if (!Fragmented)
{
await Unpack15Async(Solid, cancellationToken).ConfigureAwait(false);
}
break;
case 20: // rar 2.x compression
case 26: // files larger than 2GB
if (!Fragmented)
{
await Unpack20Async(Solid, cancellationToken).ConfigureAwait(false);
}
break;
#endif
#if !RarV2017_RAR5ONLY
case 29: // rar 3.x compression
if (!Fragmented)
{
// TODO: Create Unpack29Async when ready
throw new NotImplementedException();
}
break;
#endif
case 50: // RAR 5.0 compression algorithm.
/*#if RarV2017_RAR_SMP
if (MaxUserThreads > 1)
{
// We do not use the multithreaded unpack routine to repack RAR archives
// in 'suspended' mode, because unlike the single threaded code it can
// write more than one dictionary for same loop pass. So we would need
// larger buffers of unknown size. Also we do not support multithreading
// in fragmented window mode.
if (!Fragmented)
{
Unpack5MT(Solid);
break;
}
}
#endif*/
Unpack5(Solid);
// RAR 5.0 has full async support via UnpReadBufAsync and UnpWriteBuf
await Unpack5Async(Solid, cancellationToken).ConfigureAwait(false);
break;
#if !Rar2017_NOSTRICT
default:

View File

@@ -1,14 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Compressors.RLE90;
using SharpCompress.IO;
namespace SharpCompress.Compressors.Squeezed
{
[CLSCompliant(true)]
public class SqueezeStream : Stream, IStreamStack
{
#if DEBUG_STREAMS
@@ -35,12 +34,15 @@ namespace SharpCompress.Compressors.Squeezed
private readonly int _compressedSize;
private const int NUMVALS = 257;
private const int SPEOF = 256;
private bool _processed = false;
private Stream _decodedStream;
public SqueezeStream(Stream stream, int compressedSize)
{
_stream = stream;
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
_compressedSize = compressedSize;
_decodedStream = BuildDecodedStream();
#if DEBUG_STREAMS
this.DebugConstruct(typeof(SqueezeStream));
#endif
@@ -51,52 +53,46 @@ namespace SharpCompress.Compressors.Squeezed
#if DEBUG_STREAMS
this.DebugDispose(typeof(SqueezeStream));
#endif
_decodedStream?.Dispose();
base.Dispose(disposing);
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotImplementedException();
public override long Length => throw new NotSupportedException();
public override long Position
{
get => _stream.Position;
set => throw new NotImplementedException();
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush() => throw new NotImplementedException();
public override void Flush() => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count)
{
if (_processed)
{
return 0;
}
_processed = true;
using var binaryReader = new BinaryReader(_stream);
return _decodedStream.Read(buffer, offset, count);
}
// Read numnodes (equivalent to convert_u16!(numnodes, buf))
var numnodes = binaryReader.ReadUInt16();
private Stream BuildDecodedStream()
{
var binaryReader = new BinaryReader(_stream, Encoding.Default, leaveOpen: true);
int numnodes = binaryReader.ReadUInt16();
// Validation: numnodes should be within bounds
if (numnodes >= NUMVALS)
if (numnodes >= NUMVALS || numnodes == 0)
{
throw new InvalidDataException(
$"Invalid number of nodes {numnodes} (max {NUMVALS - 1})"
);
return new MemoryStream(Array.Empty<byte>());
}
// Handle the case where no nodes exist
if (numnodes == 0)
{
return 0;
}
// Build dnode (tree of nodes)
var dnode = new int[numnodes, 2];
for (int j = 0; j < numnodes; j++)
{
@@ -104,42 +100,27 @@ namespace SharpCompress.Compressors.Squeezed
dnode[j, 1] = binaryReader.ReadInt16();
}
// Initialize BitReader for reading bits
var bitReader = new BitReader(_stream);
var decoded = new List<byte>();
var huffmanDecoded = new MemoryStream();
int i = 0;
// Decode the buffer using the dnode tree
while (true)
{
i = dnode[i, bitReader.ReadBit() ? 1 : 0];
if (i < 0)
{
i = (short)-(i + 1);
i = -(i + 1);
if (i == SPEOF)
{
break;
}
else
{
decoded.Add((byte)i);
i = 0;
}
huffmanDecoded.WriteByte((byte)i);
i = 0;
}
}
// Unpack the decoded buffer using the RLE class
var unpacked = RLE.UnpackRLE(decoded.ToArray());
unpacked.CopyTo(buffer, 0);
return unpacked.Count();
huffmanDecoded.Position = 0;
return new RunLength90Stream(huffmanDecoded, (int)huffmanDecoded.Length);
}
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotImplementedException();
public override void SetLength(long value) => throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotImplementedException();
}
}

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

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Arj.Headers;
using SharpCompress.Readers;
using SharpCompress.Readers.Arj;
namespace SharpCompress.Factories
{
public class ArjFactory : Factory, IReaderFactory
{
public override string Name => "Arj";
public override ArchiveType? KnownArchiveType => ArchiveType.Arj;
public override IEnumerable<string> GetSupportedExtensions()
{
yield return "arj";
}
public override bool IsArchive(
Stream stream,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize
)
{
var arjHeader = new ArjMainHeader(new ArchiveEncoding());
if (arjHeader.Read(stream) == null)
{
return false;
}
return true;
}
public IReader OpenReader(Stream stream, ReaderOptions? options) =>
ArjReader.Open(stream, options);
}
}

View File

@@ -18,6 +18,7 @@ public abstract class Factory : IFactory
RegisterFactory(new GZipFactory());
RegisterFactory(new TarFactory());
RegisterFactory(new ArcFactory());
RegisterFactory(new ArjFactory());
}
private static readonly HashSet<Factory> _factories = new();

View File

@@ -143,7 +143,6 @@ namespace SharpCompress.IO
var stack = new List<IStreamStack>();
Stream? current = stream as Stream;
int lastBufferingIndex = -1;
int firstSeekableIndex = -1;
Stream? firstSeekableStream = null;
// Traverse the stack, collecting info
@@ -161,7 +160,6 @@ namespace SharpCompress.IO
// Find the first seekable stream (closest to the root)
if (current != null && current.CanSeek)
{
firstSeekableIndex = stack.Count;
firstSeekableStream = current;
}
@@ -169,16 +167,17 @@ namespace SharpCompress.IO
if (lastBufferingIndex != -1)
{
var bufferingStream = stack[lastBufferingIndex];
if (position >= 0 && position < bufferingStream.BufferSize)
var targetBufferPosition =
position - bufferingStream.GetPosition() + bufferingStream.BufferPosition;
if (targetBufferPosition >= 0 && targetBufferPosition <= bufferingStream.BufferSize)
{
bufferingStream.BufferPosition = (int)position;
bufferingStream.BufferPosition = (int)targetBufferPosition;
return position;
}
else
{
// If position is not in buffer, reset buffer and proceed as non-buffering
bufferingStream.BufferPosition = 0;
}
// If position is not in buffer, reset buffer and proceed as non-buffering
bufferingStream.BufferPosition = 0;
// Continue to seek as if no buffer is present
}

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

@@ -79,7 +79,7 @@ public class SharpCompressStream : Stream, IStreamStack
{
if (value < 0 || value > _bufferedLength)
throw new ArgumentOutOfRangeException(nameof(value));
_internalPosition = value;
_internalPosition = _internalPosition - _bufferPosition + value;
_bufferPosition = value;
ValidateBufferState(); // Add here
}
@@ -90,8 +90,6 @@ public class SharpCompressStream : Stream, IStreamStack
public Stream Stream { get; }
//private MemoryStream _bufferStream = new();
private bool _readOnly; //some archive detection requires seek to be disabled to cause it to exception to try the next arc type
//private bool _isRewound;
@@ -293,7 +291,7 @@ public class SharpCompressStream : Stream, IStreamStack
long bufferPos = _internalPosition - _bufferPosition;
if (targetPos >= bufferPos && targetPos < bufferPos + _bufferedLength)
if (targetPos >= bufferPos && targetPos <= bufferPos + _bufferedLength)
{
_bufferPosition = (int)(targetPos - bufferPos); //repoint within the buffer
_internalPosition = targetPos;

View File

@@ -96,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();
@@ -129,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();
@@ -151,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)
@@ -232,6 +294,21 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
return stream;
}
public async Task<EntryStream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
)
{
if (_wroteCurrentEntry)
{
throw new ArgumentException(
"WriteEntryToAsync or OpenEntryStreamAsync can only be called once."
);
}
var stream = await GetEntryStreamAsync(cancellationToken).ConfigureAwait(false);
_wroteCurrentEntry = true;
return stream;
}
/// <summary>
/// Retains a reference to the entry stream, so we can check whether it completed later.
/// </summary>
@@ -241,6 +318,10 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
protected virtual EntryStream GetEntryStream() =>
CreateEntryStream(Entry.Parts.First().GetCompressedStream());
protected virtual Task<EntryStream> GetEntryStreamAsync(
CancellationToken cancellationToken = default
) => Task.FromResult(GetEntryStream());
#endregion
IEntry IReader.Entry => Entry;

View File

@@ -30,7 +30,7 @@ namespace SharpCompress.Readers.Arc
protected override IEnumerable<ArcEntry> GetEntries(Stream stream)
{
ArcEntryHeader headerReader = new ArcEntryHeader(new ArchiveEncoding());
ArcEntryHeader headerReader = new ArcEntryHeader(Options.ArchiveEncoding);
ArcEntryHeader? header;
while ((header = headerReader.ReadHeader(stream)) != null)
{

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Common.Arj;
using SharpCompress.Common.Arj.Headers;
using SharpCompress.Readers.Rar;
namespace SharpCompress.Readers.Arj
{
public abstract class ArjReader : AbstractReader<ArjEntry, ArjVolume>
{
internal ArjReader(ReaderOptions options)
: base(options, ArchiveType.Arj) { }
/// <summary>
/// Derived class must create or manage the Volume itself.
/// AbstractReader.Volume is get-only, so it cannot be set here.
/// </summary>
public override ArjVolume? Volume => _volume;
private ArjVolume? _volume;
/// <summary>
/// Opens an ArjReader for Non-seeking usage with a single volume
/// </summary>
/// <param name="stream"></param>
/// <param name="options"></param>
/// <returns></returns>
public static ArjReader Open(Stream stream, ReaderOptions? options = null)
{
stream.NotNull(nameof(stream));
return new SingleVolumeArjReader(stream, options ?? new ReaderOptions());
}
/// <summary>
/// Opens an ArjReader for Non-seeking usage with multiple volumes
/// </summary>
/// <param name="streams"></param>
/// <param name="options"></param>
/// <returns></returns>
public static ArjReader Open(IEnumerable<Stream> streams, ReaderOptions? options = null)
{
streams.NotNull(nameof(streams));
return new MultiVolumeArjReader(streams, options ?? new ReaderOptions());
}
protected abstract void ValidateArchive(ArjVolume archive);
protected override IEnumerable<ArjEntry> GetEntries(Stream stream)
{
var encoding = new ArchiveEncoding();
var mainHeaderReader = new ArjMainHeader(encoding);
var localHeaderReader = new ArjLocalHeader(encoding);
var mainHeader = mainHeaderReader.Read(stream);
if (mainHeader?.IsVolume == true)
{
throw new MultiVolumeExtractionException(
"Multi volumes are currently not supported"
);
}
if (mainHeader?.IsGabled == true)
{
throw new CryptographicException(
"Password protected archives are currently not supported"
);
}
if (_volume == null)
{
_volume = new ArjVolume(stream, Options, 0);
ValidateArchive(_volume);
}
while (true)
{
var localHeader = localHeaderReader.Read(stream);
if (localHeader == null)
break;
yield return new ArjEntry(new ArjFilePart((ArjLocalHeader)localHeader, stream));
}
}
protected virtual IEnumerable<FilePart> CreateFilePartEnumerableForCurrentEntry() =>
Entry.Parts;
}
}

View File

@@ -0,0 +1,117 @@
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Arj;
using SharpCompress.Readers.Rar;
namespace SharpCompress.Readers.Arj;
internal class MultiVolumeArjReader : ArjReader
{
private readonly IEnumerator<Stream> streams;
private Stream tempStream;
internal MultiVolumeArjReader(IEnumerable<Stream> streams, ReaderOptions options)
: base(options) => this.streams = streams.GetEnumerator();
protected override void ValidateArchive(ArjVolume archive) { }
protected override Stream RequestInitialStream()
{
if (streams.MoveNext())
{
return streams.Current;
}
throw new MultiVolumeExtractionException(
"No stream provided when requested by MultiVolumeArjReader"
);
}
internal override bool NextEntryForCurrentStream()
{
if (!base.NextEntryForCurrentStream())
{
// if we're got another stream to try to process then do so
return streams.MoveNext() && LoadStreamForReading(streams.Current);
}
return true;
}
protected override IEnumerable<FilePart> CreateFilePartEnumerableForCurrentEntry()
{
var enumerator = new MultiVolumeStreamEnumerator(this, streams, tempStream);
tempStream = null;
return enumerator;
}
private class MultiVolumeStreamEnumerator : IEnumerable<FilePart>, IEnumerator<FilePart>
{
private readonly MultiVolumeArjReader reader;
private readonly IEnumerator<Stream> nextReadableStreams;
private Stream tempStream;
private bool isFirst = true;
internal MultiVolumeStreamEnumerator(
MultiVolumeArjReader r,
IEnumerator<Stream> nextReadableStreams,
Stream tempStream
)
{
reader = r;
this.nextReadableStreams = nextReadableStreams;
this.tempStream = tempStream;
}
public IEnumerator<FilePart> GetEnumerator() => this;
IEnumerator IEnumerable.GetEnumerator() => this;
public FilePart Current { get; private set; }
public void Dispose() { }
object IEnumerator.Current => Current;
public bool MoveNext()
{
if (isFirst)
{
Current = reader.Entry.Parts.First();
isFirst = false; //first stream already to go
return true;
}
if (!reader.Entry.IsSplitAfter)
{
return false;
}
if (tempStream != null)
{
reader.LoadStreamForReading(tempStream);
tempStream = null;
}
else if (!nextReadableStreams.MoveNext())
{
throw new MultiVolumeExtractionException(
"No stream provided when requested by MultiVolumeArjReader"
);
}
else
{
reader.LoadStreamForReading(nextReadableStreams.Current);
}
Current = reader.Entry.Parts.First();
return true;
}
public void Reset() { }
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Common.Arj;
namespace SharpCompress.Readers.Arj
{
internal class SingleVolumeArjReader : ArjReader
{
private readonly Stream _stream;
internal SingleVolumeArjReader(Stream stream, ReaderOptions options)
: base(options)
{
stream.NotNull(nameof(stream));
_stream = stream;
}
protected override Stream RequestInitialStream() => _stream;
protected override void ValidateArchive(ArjVolume archive)
{
if (archive.IsMultiVolume)
{
throw new MultiVolumeExtractionException(
"Streamed archive is a Multi-volume archive. Use a different ArjReader method to extract."
);
}
}
}
}

View File

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

@@ -113,16 +113,54 @@ public abstract class RarReader : AbstractReader<RarReaderEntry, RarVolume>
);
if (Entry.IsRarV3)
{
return CreateEntryStream(new RarCrcStream(UnpackV1.Value, Entry.FileHeader, stream));
return CreateEntryStream(RarCrcStream.Create(UnpackV1.Value, Entry.FileHeader, stream));
}
if (Entry.FileHeader.FileCrc?.Length > 5)
{
return CreateEntryStream(
new RarBLAKE2spStream(UnpackV2017.Value, Entry.FileHeader, stream)
RarBLAKE2spStream.Create(UnpackV2017.Value, Entry.FileHeader, stream)
);
}
return CreateEntryStream(new RarCrcStream(UnpackV2017.Value, Entry.FileHeader, stream));
return CreateEntryStream(RarCrcStream.Create(UnpackV2017.Value, Entry.FileHeader, stream));
}
protected override async System.Threading.Tasks.Task<EntryStream> GetEntryStreamAsync(
System.Threading.CancellationToken cancellationToken = default
)
{
if (Entry.IsRedir)
{
throw new InvalidOperationException("no stream for redirect entry");
}
var stream = new MultiVolumeReadOnlyStream(
CreateFilePartEnumerableForCurrentEntry().Cast<RarFilePart>(),
this
);
if (Entry.IsRarV3)
{
return CreateEntryStream(
await RarCrcStream
.CreateAsync(UnpackV1.Value, Entry.FileHeader, stream, cancellationToken)
.ConfigureAwait(false)
);
}
if (Entry.FileHeader.FileCrc?.Length > 5)
{
return CreateEntryStream(
await RarBLAKE2spStream
.CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken)
.ConfigureAwait(false)
);
}
return CreateEntryStream(
await RarCrcStream
.CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken)
.ConfigureAwait(false)
);
}
}

View File

@@ -70,7 +70,7 @@ public static class ReaderFactory
}
throw new InvalidFormatException(
"Cannot determine compressed stream type. Supported Reader Formats: Arc, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard"
"Cannot determine compressed stream type. Supported Reader Formats: Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard"
);
}
}

View File

@@ -38,6 +38,9 @@
<PackageReference Include="ZstdSharp.Port" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.NET.ILLink.Tasks" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'net481' ">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Text.Encoding.CodePages" />

View File

@@ -37,6 +37,20 @@ public abstract class AbstractWriter(ArchiveType type, WriterOptions writerOptio
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

@@ -50,4 +50,7 @@ public sealed class GZipWriter : AbstractWriter
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

@@ -16,4 +16,10 @@ public interface IWriter : IDisposable
DateTime? modificationTime,
CancellationToken cancellationToken = default
);
void WriteDirectory(string directoryName, DateTime? modificationTime);
Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
);
}

View File

@@ -55,6 +55,9 @@ public static class IWriterExtensions
}
}
public static void WriteDirectory(this IWriter writer, string directoryName) =>
writer.WriteDirectory(directoryName, null);
// Async extensions
public static Task WriteAsync(
this IWriter writer,
@@ -121,4 +124,10 @@ public static class IWriterExtensions
.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;
@@ -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 Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
// Synchronous implementation is sufficient for directory entries
WriteDirectory(directoryName, modificationTime);
return Task.CompletedTask;
}
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

@@ -335,9 +335,9 @@
"net8.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.17, )",
"resolved": "8.0.17",
"contentHash": "x5/y4l8AtshpBOrCZdlE4txw8K3e3s9meBFeZeR3l8hbbku2V7kK6ojhXvrbjg1rk3G+JqL1BI26gtgc1ZrdUw=="
"requested": "[8.0.21, )",
"resolved": "8.0.21",
"contentHash": "s8H5PZQs50OcNkaB6Si54+v3GWM7vzs6vxFRMlD3aXsbM+aPCtod62gmK0BYWou9diGzmo56j8cIf/PziijDqQ=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",

View File

@@ -0,0 +1,61 @@
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Compressors.ADC;
using Xunit;
namespace SharpCompress.Test;
public class AdcAsyncTest : TestBase
{
[Fact]
public async Task TestAdcStreamAsyncWholeChunk()
{
using var decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin"));
var decompressed = new byte[decFs.Length];
decFs.Read(decompressed, 0, decompressed.Length);
using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin"));
using var decStream = new ADCStream(cmpFs);
var test = new byte[262144];
await decStream.ReadAsync(test, 0, test.Length);
Assert.Equal(decompressed, test);
}
[Fact]
public async Task TestAdcStreamAsync()
{
using var decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin"));
var decompressed = new byte[decFs.Length];
decFs.Read(decompressed, 0, decompressed.Length);
using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin"));
using var decStream = new ADCStream(cmpFs);
using var decMs = new MemoryStream();
var test = new byte[512];
var count = 0;
do
{
count = await decStream.ReadAsync(test, 0, test.Length);
decMs.Write(test, 0, count);
} while (count > 0);
Assert.Equal(decompressed, decMs.ToArray());
}
[Fact]
public async Task TestAdcStreamAsyncWithCancellation()
{
using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin"));
using var decStream = new ADCStream(cmpFs);
var test = new byte[512];
using var cts = new System.Threading.CancellationTokenSource();
// Read should complete without cancellation
var bytesRead = await decStream.ReadAsync(test, 0, test.Length, cts.Token);
Assert.True(bytesRead > 0);
}
}

View File

@@ -23,36 +23,9 @@ namespace SharpCompress.Test.Arc
public void Arc_Uncompressed_Read() => Read("Arc.uncompressed.arc", CompressionType.None);
[Fact]
public void Arc_Squeezed_Read()
{
ProcessArchive("Arc.squeezed.arc");
}
public void Arc_Squeezed_Read() => Read("Arc.squeezed.arc");
[Fact]
public void Arc_Crunched_Read()
{
ProcessArchive("Arc.crunched.arc");
}
private void ProcessArchive(string archiveName)
{
// Process a given archive by its name
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)))
using (IReader reader = ArcReader.Open(stream))
{
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
reader.WriteEntryToDirectory(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
}
VerifyFilesByExtension();
}
public void Arc_Crunched_Read() => Read("Arc.crunched.arc");
}
}

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;
@@ -569,4 +570,88 @@ 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();
}
}
[Fact]
public void ArchiveFactory_Open_WithPreWrappedStream()
{
// Test that ArchiveFactory.Open works correctly with a stream that's already wrapped
// This addresses the issue where ZIP files fail to open on Linux
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.noEmptyDirs.zip");
// Open with a pre-wrapped stream
using (var fileStream = File.OpenRead(testArchive))
using (var wrappedStream = SharpCompressStream.Create(fileStream, bufferSize: 32768))
using (var archive = ArchiveFactory.Open(wrappedStream))
{
Assert.Equal(ArchiveType.Zip, archive.Type);
Assert.Equal(3, archive.Entries.Count());
}
}
[Fact]
public void ArchiveFactory_Open_WithRawFileStream()
{
// Test that ArchiveFactory.Open works correctly with a raw FileStream
// This is the common use case reported in the issue
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.noEmptyDirs.zip");
using (var stream = File.OpenRead(testArchive))
using (var archive = ArchiveFactory.Open(stream))
{
Assert.Equal(ArchiveType.Zip, archive.Type);
Assert.Equal(3, archive.Entries.Count());
}
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Readers.Arj;
using Xunit;
namespace SharpCompress.Test.Arj
{
public class ArjReaderTests : ReaderTests
{
public ArjReaderTests()
{
UseExtensionInsteadOfNameToVerify = true;
UseCaseInsensitiveToVerify = true;
}
[Fact]
public void Arj_Uncompressed_Read() => Read("Arj.store.arj", CompressionType.None);
[Fact]
public void Arj_Method4_Read() => Read("Arj.method4.arj");
[Fact]
public void Arj_Encrypted_Read()
{
var exception = Assert.Throws<CryptographicException>(() => Read("Arj.encrypted.arj"));
}
[Fact]
public void Arj_Multi_Reader()
{
var exception = Assert.Throws<MultiVolumeExtractionException>(() =>
DoArj_Multi_Reader(
[
"Arj.store.split.arj",
"Arj.store.split.a01",
"Arj.store.split.a02",
"Arj.store.split.a03",
"Arj.store.split.a04",
"Arj.store.split.a05",
]
)
);
}
private void DoArj_Multi_Reader(string[] archives)
{
using (
var reader = ArjReader.Open(
archives
.Select(s => Path.Combine(TEST_ARCHIVES_PATH, s))
.Select(p => File.OpenRead(p))
)
)
{
while (reader.MoveToNextEntry())
{
reader.WriteEntryToDirectory(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
VerifyFiles();
}
}
}

View File

@@ -0,0 +1,231 @@
using System;
using System.Buffers;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Compressors.BZip2;
using Xunit;
namespace SharpCompress.Test.BZip2;
public class BZip2StreamAsyncTests
{
private byte[] CreateTestData(int size)
{
var data = new byte[size];
// Create compressible data with repetitive pattern
for (int i = 0; i < size; i++)
{
data[i] = (byte)('A' + (i % 26));
}
return data;
}
[Fact]
public async Task BZip2CompressDecompressAsyncTest()
{
var testData = CreateTestData(10000);
byte[] compressed;
// Compress
using (var memoryStream = new MemoryStream())
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Compress,
false
)
)
{
await bzip2Stream.WriteAsync(testData, 0, testData.Length);
(bzip2Stream as BZip2Stream)?.Finish();
}
compressed = memoryStream.ToArray();
}
// Verify compression occurred
Assert.True(compressed.Length > 0);
Assert.True(compressed.Length < testData.Length);
// Decompress
byte[] decompressed;
using (var memoryStream = new MemoryStream(compressed))
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Decompress,
false
)
)
{
decompressed = new byte[testData.Length];
var totalRead = 0;
int bytesRead;
while (
(
bytesRead = await bzip2Stream.ReadAsync(
decompressed,
totalRead,
testData.Length - totalRead
)
) > 0
)
{
totalRead += bytesRead;
}
}
}
// Verify decompression
Assert.Equal(testData, decompressed);
}
[Fact]
public async Task BZip2ReadAsyncWithCancellationTest()
{
var testData = Encoding.ASCII.GetBytes(new string('A', 5000)); // Repetitive data compresses well
byte[] compressed;
// Compress
using (var memoryStream = new MemoryStream())
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Compress,
false
)
)
{
await bzip2Stream.WriteAsync(testData, 0, testData.Length);
(bzip2Stream as BZip2Stream)?.Finish();
}
compressed = memoryStream.ToArray();
}
// Decompress with cancellation support
using (var memoryStream = new MemoryStream(compressed))
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Decompress,
false
)
)
{
var buffer = new byte[1024];
using var cts = new System.Threading.CancellationTokenSource();
// Read should complete without cancellation
var bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length, cts.Token);
Assert.True(bytesRead > 0);
}
}
}
[Fact]
public async Task BZip2MultipleAsyncWritesTest()
{
using (var memoryStream = new MemoryStream())
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Compress,
false
)
)
{
var data1 = Encoding.ASCII.GetBytes("Hello ");
var data2 = Encoding.ASCII.GetBytes("World");
var data3 = Encoding.ASCII.GetBytes("!");
await bzip2Stream.WriteAsync(data1, 0, data1.Length);
await bzip2Stream.WriteAsync(data2, 0, data2.Length);
await bzip2Stream.WriteAsync(data3, 0, data3.Length);
(bzip2Stream as BZip2Stream)?.Finish();
}
var compressed = memoryStream.ToArray();
Assert.True(compressed.Length > 0);
// Decompress and verify
using (var readStream = new MemoryStream(compressed))
{
using (
var bzip2Stream = new BZip2Stream(
readStream,
SharpCompress.Compressors.CompressionMode.Decompress,
false
)
)
{
var result = new StringBuilder();
var buffer = new byte[256];
int bytesRead;
while ((bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
result.Append(Encoding.ASCII.GetString(buffer, 0, bytesRead));
}
Assert.Equal("Hello World!", result.ToString());
}
}
}
}
[Fact]
public async Task BZip2LargeDataAsyncTest()
{
var largeData = CreateTestData(100000);
// Compress
byte[] compressed;
using (var memoryStream = new MemoryStream())
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Compress,
false
)
)
{
await bzip2Stream.WriteAsync(largeData, 0, largeData.Length);
(bzip2Stream as BZip2Stream)?.Finish();
}
compressed = memoryStream.ToArray();
}
// Decompress
byte[] decompressed;
using (var memoryStream = new MemoryStream(compressed))
{
using (
var bzip2Stream = new BZip2Stream(
memoryStream,
SharpCompress.Compressors.CompressionMode.Decompress,
false
)
)
{
decompressed = new byte[largeData.Length];
var totalRead = 0;
int bytesRead;
var buffer = new byte[4096];
while ((bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
Array.Copy(buffer, 0, decompressed, totalRead, bytesRead);
totalRead += bytesRead;
}
}
}
// Verify
Assert.Equal(largeData, decompressed);
}
}

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

@@ -6,6 +6,8 @@ using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Compressors;
using SharpCompress.Compressors.Deflate;
using SharpCompress.Readers;
using SharpCompress.Writers;
using Xunit;
@@ -191,12 +193,7 @@ public class AsyncTests : TestBase
// Test async write with GZipStream
using (var fileStream = File.Create(compressedPath))
using (
var gzipStream = new Compressors.Deflate.GZipStream(
fileStream,
Compressors.CompressionMode.Compress
)
)
using (var gzipStream = new GZipStream(fileStream, CompressionMode.Compress))
{
await gzipStream.WriteAsync(testData, 0, testData.Length);
await gzipStream.FlushAsync();
@@ -207,12 +204,7 @@ public class AsyncTests : TestBase
// Test async read with GZipStream
using (var fileStream = File.OpenRead(compressedPath))
using (
var gzipStream = new Compressors.Deflate.GZipStream(
fileStream,
Compressors.CompressionMode.Decompress
)
)
using (var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress))
{
var decompressed = new byte[testData.Length];
var totalRead = 0;

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

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