mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-04 13:34:59 +00:00
Compare commits
501 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9fc680548 | ||
|
|
7dcc13c1f0 | ||
|
|
56d3091688 | ||
|
|
a0af0604d1 | ||
|
|
875c2d7694 | ||
|
|
8c95f863cb | ||
|
|
ddf37e82c2 | ||
|
|
a82fda98d7 | ||
|
|
44e4b1804e | ||
|
|
4ca1a7713e | ||
|
|
9caf7be928 | ||
|
|
bf4217fde6 | ||
|
|
d5a8c37113 | ||
|
|
21ce9a38e6 | ||
|
|
7732fbb698 | ||
|
|
97879f18b6 | ||
|
|
d74454f7e9 | ||
|
|
5c947bccc7 | ||
|
|
fbdefc17c1 | ||
|
|
1425c6ff0d | ||
|
|
e038aea694 | ||
|
|
87ccbf329d | ||
|
|
9dcf384263 | ||
|
|
be045c4f15 | ||
|
|
fd968b3f78 | ||
|
|
833dd7b3a2 | ||
|
|
b9258ad496 | ||
|
|
0678318dde | ||
|
|
7116c0d098 | ||
|
|
2fde8436fb | ||
|
|
61ecd6475f | ||
|
|
64b209a772 | ||
|
|
48dbdbfed5 | ||
|
|
cf50311b9c | ||
|
|
e4d8582a2a | ||
|
|
b8e5ee45eb | ||
|
|
9f20a9e7d2 | ||
|
|
201521d814 | ||
|
|
18bb3cba11 | ||
|
|
af951d6f6a | ||
|
|
e5fe92bf90 | ||
|
|
b1aca7c305 | ||
|
|
c0a0cc4a44 | ||
|
|
7a49eb9e93 | ||
|
|
5aa0610882 | ||
|
|
41ed4c8186 | ||
|
|
90a33ce6b0 | ||
|
|
12574798e1 | ||
|
|
83b11254db | ||
|
|
b25493fd29 | ||
|
|
bb66100486 | ||
|
|
3ebf97dd49 | ||
|
|
bfcdeb3784 | ||
|
|
feece3d788 | ||
|
|
94adb77e9e | ||
|
|
909d36c237 | ||
|
|
e1c8aa226d | ||
|
|
2327679f23 | ||
|
|
574d9f970c | ||
|
|
235096a2eb | ||
|
|
a739fdc544 | ||
|
|
6196e26044 | ||
|
|
46a4064989 | ||
|
|
72b3948f43 | ||
|
|
5d47bfaeb6 | ||
|
|
b2f2ea65ba | ||
|
|
7afa468e15 | ||
|
|
29cb1fed12 | ||
|
|
29f8b512c4 | ||
|
|
9794c8ba72 | ||
|
|
f1b305f682 | ||
|
|
091a800c9d | ||
|
|
975f5f4b4c | ||
|
|
9d6cd930ea | ||
|
|
d5913e8371 | ||
|
|
7f71f76f6e | ||
|
|
caa82a6146 | ||
|
|
bcf7137073 | ||
|
|
9238cf1128 | ||
|
|
2f874ace51 | ||
|
|
2feabed297 | ||
|
|
9001e28b36 | ||
|
|
24d651d7ae | ||
|
|
5667595587 | ||
|
|
32d5b61c4a | ||
|
|
128c9e639f | ||
|
|
5e3f01dc03 | ||
|
|
c97c05a3a7 | ||
|
|
b2beea9c4e | ||
|
|
41fbaa1c28 | ||
|
|
d9274cf794 | ||
|
|
583b048046 | ||
|
|
ead5916eae | ||
|
|
d15ab92da3 | ||
|
|
1ab30f2af5 | ||
|
|
4dbe0b91f1 | ||
|
|
a972d3784e | ||
|
|
6991900eb0 | ||
|
|
d614beb9eb | ||
|
|
253a46d458 | ||
|
|
32b1ec32c6 | ||
|
|
eb2cba09b2 | ||
|
|
e79dceb67e | ||
|
|
87c38d6dab | ||
|
|
9e98d9c45c | ||
|
|
0e9a4b0511 | ||
|
|
eae25aff64 | ||
|
|
b8c06ff36e | ||
|
|
6cf2e054bf | ||
|
|
95749234f5 | ||
|
|
b976961434 | ||
|
|
e1aa727513 | ||
|
|
1f71ce1be2 | ||
|
|
cf13de6ac1 | ||
|
|
c2e01798f8 | ||
|
|
8fc8295a89 | ||
|
|
d392991764 | ||
|
|
e57e87090f | ||
|
|
c701bbbee3 | ||
|
|
2f0eb0bd4b | ||
|
|
17bde8da8a | ||
|
|
99d355e6ca | ||
|
|
c790fd21a4 | ||
|
|
bee51af48b | ||
|
|
ca743eae22 | ||
|
|
93504cf82f | ||
|
|
6d3e4e842b | ||
|
|
54b64a8c3b | ||
|
|
0e59bf39f4 | ||
|
|
8b95e0a76d | ||
|
|
48a2ad7b57 | ||
|
|
cfc6651fff | ||
|
|
b23827a8db | ||
|
|
3f9986c13c | ||
|
|
224989f19b | ||
|
|
c7010b75c1 | ||
|
|
00cfeee56e | ||
|
|
aaa97e2ce2 | ||
|
|
1d52618137 | ||
|
|
34309f17f4 | ||
|
|
220ba67faa | ||
|
|
230f96e8e8 | ||
|
|
930c8899d2 | ||
|
|
7c0cef7dd8 | ||
|
|
951ebb3fa2 | ||
|
|
2a4d098b41 | ||
|
|
5839b87f98 | ||
|
|
425a2bd680 | ||
|
|
939c2497c8 | ||
|
|
8995ba56b8 | ||
|
|
e941ab60ca | ||
|
|
48860f1349 | ||
|
|
94b2c5c593 | ||
|
|
f1d8ae5a22 | ||
|
|
e44d2093e5 | ||
|
|
9fa1201a4c | ||
|
|
7800808648 | ||
|
|
2789e86d21 | ||
|
|
afd1e39b88 | ||
|
|
aec4c738ef | ||
|
|
63ecc8c842 | ||
|
|
5f6d583521 | ||
|
|
0341984f10 | ||
|
|
fe757486ae | ||
|
|
46d480c9a1 | ||
|
|
8fe3cba7a8 | ||
|
|
aa19f4da8b | ||
|
|
a08f95326c | ||
|
|
b4c5437c92 | ||
|
|
8680e3b39e | ||
|
|
1b3002c8df | ||
|
|
394fd2e7db | ||
|
|
d83af56d28 | ||
|
|
28c93d6841 | ||
|
|
5f52fc2176 | ||
|
|
8fba579e3a | ||
|
|
40b1aadeb2 | ||
|
|
40e72ad199 | ||
|
|
618b4bbb83 | ||
|
|
1eaf3e6294 | ||
|
|
fd453e946d | ||
|
|
c294071015 | ||
|
|
c2f6055e33 | ||
|
|
5161f4df33 | ||
|
|
3396f8fe00 | ||
|
|
9291f58091 | ||
|
|
85f3b17c42 | ||
|
|
2a3086a0d7 | ||
|
|
41c3cc1a18 | ||
|
|
1b1df86a11 | ||
|
|
e0660e7775 | ||
|
|
99a6c4de88 | ||
|
|
ffa765bd97 | ||
|
|
b1696524b3 | ||
|
|
14d432e22d | ||
|
|
6a37c55085 | ||
|
|
9c1c6fff9f | ||
|
|
db8c6f4bcb | ||
|
|
ff17ecda7d | ||
|
|
692058677c | ||
|
|
1e90d69912 | ||
|
|
64a1cc68e1 | ||
|
|
0fdf9c74a8 | ||
|
|
20353f35ff | ||
|
|
e2df7894f9 | ||
|
|
7af029b5de | ||
|
|
8fc5ca5a71 | ||
|
|
aa0356de9f | ||
|
|
e44a43d2b1 | ||
|
|
8997f00b9b | ||
|
|
c5da416764 | ||
|
|
840e58fc03 | ||
|
|
7f911c5219 | ||
|
|
a887390c23 | ||
|
|
f4dddcec8e | ||
|
|
0d9d82d7e6 | ||
|
|
3a6d24b1d9 | ||
|
|
b9b159be4c | ||
|
|
40212083a5 | ||
|
|
d3428b066e | ||
|
|
94c64b2a45 | ||
|
|
0d671a0bb2 | ||
|
|
d34a47c148 | ||
|
|
5aa216bd21 | ||
|
|
8af47548fe | ||
|
|
131bd2b7b8 | ||
|
|
1993673a22 | ||
|
|
30e036f9ec | ||
|
|
0f374b27cf | ||
|
|
0d487df61b | ||
|
|
c082d4203b | ||
|
|
d4380b6bb6 | ||
|
|
095c871174 | ||
|
|
6d73c5b295 | ||
|
|
cc4d28193c | ||
|
|
9433e06b93 | ||
|
|
a92aaa51d5 | ||
|
|
d41908adeb | ||
|
|
81ca15b567 | ||
|
|
b81d0fd730 | ||
|
|
3a1bb187e8 | ||
|
|
3fee14a070 | ||
|
|
5bf789ac65 | ||
|
|
be06049db3 | ||
|
|
a0435f6a60 | ||
|
|
2321e2c90b | ||
|
|
97e98d8629 | ||
|
|
d96e7362d2 | ||
|
|
7dd46fe5ed | ||
|
|
04c044cb2b | ||
|
|
cc10a12fbc | ||
|
|
8b0a1c699f | ||
|
|
15ca7c9807 | ||
|
|
2b4da7e39b | ||
|
|
31f81f38af | ||
|
|
72cf77b7c7 | ||
|
|
0fe48c647e | ||
|
|
7b06652bff | ||
|
|
434ce05416 | ||
|
|
0698031ed4 | ||
|
|
51237a34eb | ||
|
|
b8264a8131 | ||
|
|
cad923018e | ||
|
|
db94b49941 | ||
|
|
72d15d9cbf | ||
|
|
e0186eadc0 | ||
|
|
4cfa5b04af | ||
|
|
f2c54b1f8b | ||
|
|
d7d0bc6582 | ||
|
|
dd9dc2500b | ||
|
|
4efb109da8 | ||
|
|
bcf9a6bdf1 | ||
|
|
e3a25ecdc0 | ||
|
|
783521928d | ||
|
|
9a876abd31 | ||
|
|
97f58b412e | ||
|
|
4c61628078 | ||
|
|
99a8b0f750 | ||
|
|
a9017d7c25 | ||
|
|
d9e4b26648 | ||
|
|
0d03bafe49 | ||
|
|
fee15a31f9 | ||
|
|
997d3910d4 | ||
|
|
a3918cc0d7 | ||
|
|
f056986b07 | ||
|
|
59c1f02f98 | ||
|
|
3a71a2b1f8 | ||
|
|
2ef1215b49 | ||
|
|
130ac83076 | ||
|
|
dd606a0702 | ||
|
|
84cd772f50 | ||
|
|
fa1d7af22f | ||
|
|
a771ba3bc0 | ||
|
|
8b612c658d | ||
|
|
7dd0da5fd7 | ||
|
|
f7b3525c4e | ||
|
|
de83bdae48 | ||
|
|
d90b610767 | ||
|
|
2d41de6b72 | ||
|
|
f391c3caf3 | ||
|
|
9bdf150676 | ||
|
|
0c199609eb | ||
|
|
6eff9d3753 | ||
|
|
7ab16457c7 | ||
|
|
e7ad8132b5 | ||
|
|
da87e45534 | ||
|
|
2ffaef5563 | ||
|
|
55cb350d2c | ||
|
|
7fa271a1b4 | ||
|
|
c53ca372f2 | ||
|
|
75bc8501f4 | ||
|
|
1e22b47fe1 | ||
|
|
74e2dca207 | ||
|
|
a669de24b7 | ||
|
|
e1e9c449e9 | ||
|
|
60e1dc0239 | ||
|
|
10eb94fd82 | ||
|
|
ccc8587e5f | ||
|
|
53c96193c1 | ||
|
|
d4f11e00b1 | ||
|
|
321233b82c | ||
|
|
eb188051d4 | ||
|
|
a136084e11 | ||
|
|
bc06f3179d | ||
|
|
ee84d971b2 | ||
|
|
264d80ef4c | ||
|
|
dba68187ac | ||
|
|
ca4a1936b3 | ||
|
|
77c8d31a90 | ||
|
|
ab7196f86c | ||
|
|
88b3a66bf9 | ||
|
|
ea77666b4a | ||
|
|
db98e5f39b | ||
|
|
df59c5cb9d | ||
|
|
e786e95358 | ||
|
|
75ada5623c | ||
|
|
ad5c655c45 | ||
|
|
65e607454e | ||
|
|
f238be6003 | ||
|
|
dc31e4c5fa | ||
|
|
665d8cd266 | ||
|
|
8324114e84 | ||
|
|
b83e6ee4ce | ||
|
|
58bab0d310 | ||
|
|
1af51aaaba | ||
|
|
a09327b831 | ||
|
|
16543bf74c | ||
|
|
aa4cd373ac | ||
|
|
351e294362 | ||
|
|
a0c5b1cd9d | ||
|
|
df2ed1e584 | ||
|
|
b354f7a3a5 | ||
|
|
bb53d1e1c6 | ||
|
|
2aabd8d0e1 | ||
|
|
aca97c2c6c | ||
|
|
8e7d959cf4 | ||
|
|
b23f031db9 | ||
|
|
1ba529a9d5 | ||
|
|
3d29c183ef | ||
|
|
8a108b590d | ||
|
|
bca0f67344 | ||
|
|
f3dad51134 | ||
|
|
f51840829c | ||
|
|
aa1c0d0870 | ||
|
|
dee5ee6589 | ||
|
|
b799f479c4 | ||
|
|
b4352fefa5 | ||
|
|
77d06fb60e | ||
|
|
00b647457c | ||
|
|
153d10a35c | ||
|
|
06713c641e | ||
|
|
210978ec2d | ||
|
|
42f7d43139 | ||
|
|
19967f5ad7 | ||
|
|
a1de3eb47d | ||
|
|
e88841bdec | ||
|
|
c8e4915f8e | ||
|
|
a93a3f0598 | ||
|
|
084f81fc8d | ||
|
|
d148f36e87 | ||
|
|
150d9c35b7 | ||
|
|
e11198616e | ||
|
|
2f27f1e6f9 | ||
|
|
5392ca9794 | ||
|
|
46672eb583 | ||
|
|
79653eee80 | ||
|
|
16ad86c52a | ||
|
|
6b7c6be5f5 | ||
|
|
fda1c2cc79 | ||
|
|
ef2fee0ee3 | ||
|
|
e287d0811d | ||
|
|
a7164f3c9f | ||
|
|
c55060039a | ||
|
|
c68d8deddd | ||
|
|
f6eabc5db1 | ||
|
|
72d5884db6 | ||
|
|
3595c89c79 | ||
|
|
9ebbc718c5 | ||
|
|
e862480b86 | ||
|
|
1f3d8fe6f1 | ||
|
|
41ae036ab4 | ||
|
|
588d176b96 | ||
|
|
f8697120a0 | ||
|
|
1a767105e6 | ||
|
|
4067b6ed2c | ||
|
|
b272dbfd1f | ||
|
|
48be7bbf86 | ||
|
|
51e22cea71 | ||
|
|
2241e27e68 | ||
|
|
11c90ae879 | ||
|
|
cf55125202 | ||
|
|
9cefb85905 | ||
|
|
fc672da0e0 | ||
|
|
25b297b142 | ||
|
|
ab03c12fa8 | ||
|
|
3095c805ad | ||
|
|
9c18daafb8 | ||
|
|
16182417fb | ||
|
|
9af35201e4 | ||
|
|
f21b982955 | ||
|
|
b3a20d05c5 | ||
|
|
4cd024a2b2 | ||
|
|
63d08ebfd2 | ||
|
|
c696197b03 | ||
|
|
738a72228b | ||
|
|
90641f4488 | ||
|
|
a4cc7eaf9b | ||
|
|
fdca728fdc | ||
|
|
d2c4ae8cdf | ||
|
|
f3d3ac30a6 | ||
|
|
f8cc4ade8a | ||
|
|
b3975b7bbd | ||
|
|
4f1b61f5bc | ||
|
|
beeb37b4fd | ||
|
|
43aa2bad22 | ||
|
|
1b2ba921bb | ||
|
|
f543da0ea8 | ||
|
|
e60c9efa84 | ||
|
|
c52fc6f240 | ||
|
|
ee136b024a | ||
|
|
699bc5f34b | ||
|
|
9eed8e842c | ||
|
|
6d652a12ee | ||
|
|
e043e06656 | ||
|
|
14b52599f4 | ||
|
|
e3e2c0c567 | ||
|
|
4fc5d60f03 | ||
|
|
c37a9e0f82 | ||
|
|
fed17ebb96 | ||
|
|
eeac678872 | ||
|
|
f9ed0f2df9 | ||
|
|
0ddbacac85 | ||
|
|
f0d28aa5cf | ||
|
|
cc84f6fee4 | ||
|
|
00e6eef369 | ||
|
|
1ae71907bc | ||
|
|
3ff688fba2 | ||
|
|
bb59b3d456 | ||
|
|
186ea74ada | ||
|
|
c108f2dcf3 | ||
|
|
4cca232d83 | ||
|
|
1db511e9cb | ||
|
|
76afa7d3bf | ||
|
|
3513f7b1cd | ||
|
|
4531fe39e6 | ||
|
|
8d276a85bc | ||
|
|
5f0d042bc3 | ||
|
|
408f07e3c4 | ||
|
|
d1a540c90c | ||
|
|
00df8e930e | ||
|
|
3b768b1b77 | ||
|
|
42a7ececa0 | ||
|
|
e8867de049 | ||
|
|
a1dfa3dfa3 | ||
|
|
83917d4f79 | ||
|
|
513cd4f905 | ||
|
|
eda0309df3 | ||
|
|
74e27c028e | ||
|
|
36c06c4089 | ||
|
|
249b8a9cdd | ||
|
|
62bee15f00 | ||
|
|
d8797b69e4 | ||
|
|
084fe72b02 | ||
|
|
c823acaa3f | ||
|
|
e0d6cd9cb7 | ||
|
|
01021e102b | ||
|
|
6de738ff17 | ||
|
|
c0612547eb | ||
|
|
e960907698 | ||
|
|
84e03b1b27 | ||
|
|
f1a80da34b | ||
|
|
5a5a55e556 | ||
|
|
e1f132b45b | ||
|
|
087011aede | ||
|
|
1430bf9b31 | ||
|
|
4e5de817ef | ||
|
|
5d6b94f8c3 | ||
|
|
8dfbe56f42 | ||
|
|
df79d983d7 | ||
|
|
6c23a28826 | ||
|
|
f72289570a |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "1.1.2",
|
||||
"version": "1.2.5",
|
||||
"commands": [
|
||||
"csharpier"
|
||||
],
|
||||
|
||||
7
.copilot-agent.yml
Normal file
7
.copilot-agent.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
enabled: true
|
||||
agent:
|
||||
name: copilot-coding-agent
|
||||
allow:
|
||||
- paths: ["src/**/*", "tests/**/*", "README.md", "AGENTS.md"]
|
||||
actions: ["create", "modify"]
|
||||
require_review_before_merge: true
|
||||
17
.github/agents/copilot-agent.yml
vendored
Normal file
17
.github/agents/copilot-agent.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
enabled: true
|
||||
agent:
|
||||
name: copilot-coding-agent
|
||||
allow:
|
||||
- paths: ["src/**/*", "tests/**/*", "README.md", "AGENTS.md"]
|
||||
actions: ["create", "modify", "delete"]
|
||||
require_review_before_merge: true
|
||||
required_approvals: 1
|
||||
allowed_merge_strategies:
|
||||
- squash
|
||||
- merge
|
||||
auto_merge_on_green: false
|
||||
run_workflows: true
|
||||
notes: |
|
||||
- This manifest expresses the policy for the Copilot coding agent in this repository.
|
||||
- It does NOT install or authorize the agent; a repository admin must install the Copilot coding agent app and grant the repository the necessary permissions (contents: write, pull_requests: write, checks: write, actions: write/read, issues: write) to allow the agent to act.
|
||||
- Keep allow paths narrow and prefer require_review_before_merge during initial rollout.
|
||||
25
.github/prompts/plan-async.prompt.md
vendored
Normal file
25
.github/prompts/plan-async.prompt.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Plan: Implement Missing Async Functionality in SharpCompress
|
||||
|
||||
SharpCompress has async support for low-level stream operations and Reader/Writer APIs, but critical entry points (Archive.Open, factory methods, initialization) remain synchronous. This plan adds async overloads for all user-facing I/O operations and fixes existing async bugs, enabling full end-to-end async workflows.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Add async factory methods** to [ArchiveFactory.cs](src/SharpCompress/Factories/ArchiveFactory.cs), [ReaderFactory.cs](src/SharpCompress/Factories/ReaderFactory.cs), and [WriterFactory.cs](src/SharpCompress/Factories/WriterFactory.cs) with `OpenAsync` and `CreateAsync` overloads accepting `CancellationToken`
|
||||
|
||||
2. **Implement async Open methods** on concrete archive types ([ZipArchive.cs](src/SharpCompress/Archives/Zip/ZipArchive.cs), [TarArchive.cs](src/SharpCompress/Archives/Tar/TarArchive.cs), [RarArchive.cs](src/SharpCompress/Archives/Rar/RarArchive.cs), [GZipArchive.cs](src/SharpCompress/Archives/GZip/GZipArchive.cs), [SevenZipArchive.cs](src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs)) and reader types ([ZipReader.cs](src/SharpCompress/Readers/Zip/ZipReader.cs), [TarReader.cs](src/SharpCompress/Readers/Tar/TarReader.cs), etc.)
|
||||
|
||||
3. **Convert archive initialization logic to async** including header reading, volume loading, and format signature detection across archive constructors and internal initialization methods
|
||||
|
||||
4. **Fix LZMA decoder async bugs** in [LzmaStream.cs](src/SharpCompress/Compressors/LZMA/LzmaStream.cs), [Decoder.cs](src/SharpCompress/Compressors/LZMA/Decoder.cs), and [OutWindow.cs](src/SharpCompress/Compressors/LZMA/OutWindow.cs) to enable true async 7Zip support and remove `NonDisposingStream` workaround
|
||||
|
||||
5. **Complete Rar async implementation** by converting `UnpackV2017` methods to async in [UnpackV2017.cs](src/SharpCompress/Compressors/Rar/UnpackV2017.cs) and updating Rar20 decompression
|
||||
|
||||
6. **Add comprehensive async tests** covering all new async entry points, cancellation scenarios, and concurrent operations across all archive formats in test files
|
||||
|
||||
## Further Considerations
|
||||
|
||||
1. **Breaking changes** - Should new async methods be added alongside existing sync methods (non-breaking), or should sync methods eventually be deprecated? Recommend additive approach for backward compatibility.
|
||||
|
||||
2. **Performance impact** - Header parsing for formats like Zip/Tar is often small; consider whether truly async parsing adds value vs sync parsing wrapped in Task, or make it conditional based on stream type (network vs file).
|
||||
|
||||
3. **7Zip complexity** - The LZMA async bug fix (Step 4) may be challenging due to state management in the decoder; consider whether to scope it separately or implement a simpler workaround that maintains correctness.
|
||||
123
.github/prompts/plan-for-next.prompt.md
vendored
Normal file
123
.github/prompts/plan-for-next.prompt.md
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
# Plan: Modernize SharpCompress Public API
|
||||
|
||||
Based on comprehensive analysis, the API has several inconsistencies around factory patterns, async support, format capabilities, and options classes. Most improvements can be done incrementally without breaking changes.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Standardize factory patterns** by deprecating format-specific static `Open` methods in [Archives/Zip/ZipArchive.cs](src/SharpCompress/Archives/Zip/ZipArchive.cs), [Archives/Tar/TarArchive.cs](src/SharpCompress/Archives/Tar/TarArchive.cs), etc. in favor of centralized [Factories/ArchiveFactory.cs](src/SharpCompress/Factories/ArchiveFactory.cs)
|
||||
|
||||
2. **Complete async implementation** in [Writers/Zip/ZipWriter.cs](src/SharpCompress/Writers/Zip/ZipWriter.cs) and other writers that currently use sync-over-async, implementing true async I/O throughout the writer hierarchy
|
||||
|
||||
3. **Unify options classes** by making [Common/ExtractionOptions.cs](src/SharpCompress/Common/ExtractionOptions.cs) inherit from `OptionsBase` and adding progress reporting to extraction methods consistently
|
||||
|
||||
4. **Clarify GZip semantics** in [Archives/GZip/GZipArchive.cs](src/SharpCompress/Archives/GZip/GZipArchive.cs) by adding XML documentation explaining single-entry limitation and relationship to GZip compression used in Tar.gz
|
||||
|
||||
## Further Considerations
|
||||
|
||||
1. **Breaking changes roadmap** - Should we plan a major version (2.0) to remove deprecated factory methods, clean up `ArchiveType` enum (remove Arc/Arj or add full support), and consolidate naming patterns?
|
||||
|
||||
2. **Progress reporting consistency** - Should `IProgress<ArchiveExtractionProgress<IEntry>>` be added to all extraction extension methods or consolidated into options classes?
|
||||
|
||||
## Detailed Analysis
|
||||
|
||||
### Factory Pattern Issues
|
||||
|
||||
Three different factory patterns exist with overlapping functionality:
|
||||
|
||||
1. **Static Factories**: ArchiveFactory, ReaderFactory, WriterFactory
|
||||
2. **Instance Factories**: IArchiveFactory, IReaderFactory, IWriterFactory
|
||||
3. **Format-specific static methods**: Each archive class has static `Open` methods
|
||||
|
||||
**Example confusion:**
|
||||
```csharp
|
||||
// Three ways to open a Zip archive - which is recommended?
|
||||
var archive1 = ArchiveFactory.Open("file.zip");
|
||||
var archive2 = ZipArchive.Open("file.zip");
|
||||
var archive3 = ArchiveFactory.AutoFactory.Open(fileInfo, options);
|
||||
```
|
||||
|
||||
### Async Support Gaps
|
||||
|
||||
Base `IWriter` interface has async methods, but writer implementations provide minimal async support. Most writers just call synchronous methods:
|
||||
|
||||
```csharp
|
||||
public virtual async Task WriteAsync(...)
|
||||
{
|
||||
// Default implementation calls synchronous version
|
||||
Write(filename, source, modificationTime);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
```
|
||||
|
||||
Real async implementations only in:
|
||||
- `TarWriter` - Proper async implementation
|
||||
- Most other writers use sync-over-async
|
||||
|
||||
### GZip Archive Special Case
|
||||
|
||||
GZip is treated as both a compression format and an archive format, but only supports single-entry archives:
|
||||
|
||||
```csharp
|
||||
protected override GZipArchiveEntry CreateEntryInternal(...)
|
||||
{
|
||||
if (Entries.Any())
|
||||
{
|
||||
throw new InvalidFormatException("Only one entry is allowed in a GZip Archive");
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Options Class Hierarchy
|
||||
|
||||
```
|
||||
OptionsBase (LeaveStreamOpen, ArchiveEncoding)
|
||||
├─ ReaderOptions (LookForHeader, Password, DisableCheckIncomplete, BufferSize, ExtensionHint, Progress)
|
||||
├─ WriterOptions (CompressionType, CompressionLevel, Progress)
|
||||
│ ├─ ZipWriterOptions (ArchiveComment, UseZip64)
|
||||
│ ├─ TarWriterOptions (FinalizeArchiveOnClose, HeaderFormat)
|
||||
│ └─ GZipWriterOptions (no additional properties)
|
||||
└─ ExtractionOptions (standalone - Overwrite, ExtractFullPath, PreserveFileTime, PreserveAttributes)
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
- `ExtractionOptions` doesn't inherit from `OptionsBase` - no encoding support during extraction
|
||||
- Progress reporting inconsistency between readers and extraction
|
||||
- Obsolete properties (`ChecksumIsValid`, `Version`) with unclear migration path
|
||||
|
||||
### Implementation Priorities
|
||||
|
||||
**High Priority (Non-Breaking):**
|
||||
1. Add API usage guide (Archive vs Reader, factory recommendations, async best practices)
|
||||
2. Fix progress reporting consistency
|
||||
3. Complete async implementation in writers
|
||||
|
||||
**Medium Priority (Next Major Version):**
|
||||
1. Unify factory pattern - deprecate format-specific static `Open` methods
|
||||
2. Clean up options classes - make `ExtractionOptions` inherit from `OptionsBase`
|
||||
3. Clarify archive types - remove Arc/Arj from `ArchiveType` enum or add full support
|
||||
4. Standardize naming across archive types
|
||||
|
||||
**Low Priority:**
|
||||
1. Add BZip2 archive support similar to GZipArchive
|
||||
2. Complete obsolete property cleanup with migration guide
|
||||
|
||||
### Backward Compatibility Strategy
|
||||
|
||||
**Safe (Non-Breaking) Changes:**
|
||||
- Add new methods to interfaces (use default implementations)
|
||||
- Add new options properties (with defaults)
|
||||
- Add new factory methods
|
||||
- Improve async implementations
|
||||
- Add progress reporting support
|
||||
|
||||
**Breaking Changes to Avoid:**
|
||||
- ❌ Removing format-specific `Open` methods (deprecate instead)
|
||||
- ❌ Changing `LeaveStreamOpen` default (currently `true`)
|
||||
- ❌ Removing obsolete properties before major version bump
|
||||
- ❌ Changing return types or signatures of existing methods
|
||||
|
||||
**Deprecation Pattern:**
|
||||
- Use `[Obsolete]` for one major version
|
||||
- Use `[EditorBrowsable(EditorBrowsableState.Never)]` in next major version
|
||||
- Remove in following major version
|
||||
155
.github/workflows/NUGET_RELEASE.md
vendored
Normal file
155
.github/workflows/NUGET_RELEASE.md
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
# NuGet Release Workflow
|
||||
|
||||
This document describes the automated NuGet release workflow for SharpCompress.
|
||||
|
||||
## Overview
|
||||
|
||||
The `nuget-release.yml` workflow automatically builds, tests, and publishes SharpCompress packages to NuGet.org when:
|
||||
- Changes are pushed to the `master` or `release` branch
|
||||
- A version tag (format: `MAJOR.MINOR.PATCH`) is pushed
|
||||
|
||||
The workflow runs on both Windows and Ubuntu, but only the Windows build publishes to NuGet.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Version Determination
|
||||
|
||||
The workflow automatically determines the version based on whether the commit is tagged using C# code in the build project:
|
||||
|
||||
1. **Tagged Release (Stable)**:
|
||||
- If the current commit has a version tag (e.g., `0.42.1`)
|
||||
- Uses the tag as the version number
|
||||
- Published as a stable release
|
||||
|
||||
2. **Untagged Release (Prerelease)**:
|
||||
- If the current commit is NOT tagged
|
||||
- Creates a prerelease version based on the next minor version
|
||||
- Format: `{NEXT_MINOR_VERSION}-beta.{COMMIT_COUNT}`
|
||||
- Example: `0.43.0-beta.123` (if last tag is 0.42.x)
|
||||
- Published as a prerelease to NuGet.org (Windows build only)
|
||||
|
||||
### Workflow Steps
|
||||
|
||||
The workflow runs on a matrix of operating systems (Windows and Ubuntu):
|
||||
|
||||
1. **Checkout**: Fetches the repository with full history for version detection
|
||||
2. **Setup .NET**: Installs .NET 10.0
|
||||
3. **Determine Version**: Runs `determine-version` build target to check for tags and determine version
|
||||
4. **Update Version**: Runs `update-version` build target to update the version in the project file
|
||||
5. **Build and Test**: Runs the full build and test suite on both platforms
|
||||
6. **Upload Artifacts**: Uploads the generated `.nupkg` files as workflow artifacts (separate for each OS)
|
||||
7. **Push to NuGet**: (Windows only) Runs `push-to-nuget` build target to publish the package to NuGet.org using the API key
|
||||
|
||||
All version detection, file updates, and publishing logic is implemented in C# in the `build/Program.cs` file using build targets.
|
||||
|
||||
## Setup Requirements
|
||||
|
||||
### 1. NuGet API Key Secret
|
||||
|
||||
The workflow requires a `NUGET_API_KEY` secret to be configured in the repository settings:
|
||||
|
||||
1. Go to https://www.nuget.org/account/apikeys
|
||||
2. Create a new API key with "Push" permission for the SharpCompress package
|
||||
3. In GitHub, go to: **Settings** → **Secrets and variables** → **Actions**
|
||||
4. Create a new secret named `NUGET_API_KEY` with the API key value
|
||||
|
||||
### 2. Branch Protection (Recommended)
|
||||
|
||||
Consider enabling branch protection rules for the `release` branch to ensure:
|
||||
- Code reviews are required before merging
|
||||
- Status checks pass before merging
|
||||
- Only authorized users can push to the branch
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Stable Release
|
||||
|
||||
There are two ways to trigger a stable release:
|
||||
|
||||
**Method 1: Push tag to trigger workflow**
|
||||
1. Ensure all changes are committed on the `master` or `release` branch
|
||||
2. Create and push a version tag:
|
||||
```bash
|
||||
git checkout master # or release
|
||||
git tag 0.43.0
|
||||
git push origin 0.43.0
|
||||
```
|
||||
3. The workflow will automatically trigger, build, test, and publish `SharpCompress 0.43.0` to NuGet.org (Windows build)
|
||||
|
||||
**Method 2: Tag after pushing to branch**
|
||||
1. Ensure all changes are merged and pushed to the `master` or `release` branch
|
||||
2. Create and push a version tag on the already-pushed commit:
|
||||
```bash
|
||||
git checkout master # or release
|
||||
git tag 0.43.0
|
||||
git push origin 0.43.0
|
||||
```
|
||||
3. The workflow will automatically trigger, build, test, and publish `SharpCompress 0.43.0` to NuGet.org (Windows build)
|
||||
|
||||
### Creating a Prerelease
|
||||
|
||||
1. Push changes to the `master` or `release` branch without tagging:
|
||||
```bash
|
||||
git checkout master # or release
|
||||
git push origin master # or release
|
||||
```
|
||||
2. The workflow will automatically:
|
||||
- Build and test the project on both Windows and Ubuntu
|
||||
- Publish a prerelease version like `0.43.0-beta.456` to NuGet.org (Windows build)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Workflow Fails to Push to NuGet
|
||||
|
||||
- **Check the API Key**: Ensure `NUGET_API_KEY` is set correctly in repository secrets
|
||||
- **Check API Key Permissions**: Verify the API key has "Push" permission for SharpCompress
|
||||
- **Check API Key Expiration**: NuGet API keys may expire; create a new one if needed
|
||||
|
||||
### Version Conflict
|
||||
|
||||
If you see "Package already exists" errors:
|
||||
- The workflow uses `--skip-duplicate` flag to handle this gracefully
|
||||
- If you need to republish the same version, delete it from NuGet.org first (if allowed)
|
||||
|
||||
### Build or Test Failures
|
||||
|
||||
- The workflow will not push to NuGet if build or tests fail
|
||||
- Check the workflow logs in GitHub Actions for details
|
||||
- Fix the issues and push again
|
||||
|
||||
## Manual Package Creation
|
||||
|
||||
If you need to create a package manually without publishing:
|
||||
|
||||
```bash
|
||||
dotnet run --project build/build.csproj -- publish
|
||||
```
|
||||
|
||||
The package will be created in the `artifacts/` directory.
|
||||
|
||||
## Build Targets
|
||||
|
||||
The workflow uses the following C# build targets defined in `build/Program.cs`:
|
||||
|
||||
- **determine-version**: Detects version from git tags and outputs VERSION and PRERELEASE variables
|
||||
- **update-version**: Updates VersionPrefix, AssemblyVersion, and FileVersion in the project file
|
||||
- **push-to-nuget**: Pushes the generated NuGet packages to NuGet.org (requires NUGET_API_KEY)
|
||||
|
||||
These targets can be run manually for testing:
|
||||
|
||||
```bash
|
||||
# Determine the version
|
||||
dotnet run --project build/build.csproj -- determine-version
|
||||
|
||||
# Update version in project file
|
||||
VERSION=0.43.0 dotnet run --project build/build.csproj -- update-version
|
||||
|
||||
# Push to NuGet (requires NUGET_API_KEY environment variable)
|
||||
NUGET_API_KEY=your-key dotnet run --project build/build.csproj -- push-to-nuget
|
||||
```
|
||||
|
||||
## Related Files
|
||||
|
||||
- `.github/workflows/nuget-release.yml` - The workflow definition
|
||||
- `build/Program.cs` - Build script with version detection and publishing logic
|
||||
- `src/SharpCompress/SharpCompress.csproj` - Project file with version information
|
||||
120
.github/workflows/TESTING.md
vendored
Normal file
120
.github/workflows/TESTING.md
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
# Testing Guide for NuGet Release Workflow
|
||||
|
||||
This document describes how to test the NuGet release workflow.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Since this workflow publishes to NuGet.org and requires repository secrets, testing should be done carefully. The workflow runs on both Windows and Ubuntu, but only the Windows build publishes to NuGet.
|
||||
|
||||
## Pre-Testing Checklist
|
||||
|
||||
- [x] Workflow YAML syntax validated
|
||||
- [x] Version determination logic tested locally
|
||||
- [x] Version update logic tested locally
|
||||
- [x] Build script works (`dotnet run --project build/build.csproj`)
|
||||
|
||||
## Manual Testing Steps
|
||||
|
||||
### 1. Test Prerelease Publishing (Recommended First Test)
|
||||
|
||||
This tests the workflow on untagged commits to the master or release branch.
|
||||
|
||||
**Steps:**
|
||||
1. Ensure `NUGET_API_KEY` secret is configured in repository settings
|
||||
2. Create a test commit on the `master` or `release` branch (e.g., update a comment or README)
|
||||
3. Push to the `master` or `release` branch
|
||||
4. Monitor the GitHub Actions workflow at: https://github.com/adamhathcock/sharpcompress/actions
|
||||
5. Verify:
|
||||
- Workflow triggers and runs successfully on both Windows and Ubuntu
|
||||
- Version is determined correctly (e.g., `0.43.0-beta.XXX` if last tag is 0.42.x)
|
||||
- Build and tests pass on both platforms
|
||||
- Package artifacts are uploaded for both platforms
|
||||
- Package is pushed to NuGet.org as prerelease (Windows build only)
|
||||
|
||||
**Expected Outcome:**
|
||||
- A new prerelease package appears on NuGet.org: https://www.nuget.org/packages/SharpCompress/
|
||||
- Package version follows pattern: `{NEXT_MINOR_VERSION}-beta.{COMMIT_COUNT}`
|
||||
|
||||
### 2. Test Tagged Release Publishing
|
||||
|
||||
This tests the workflow when a version tag is pushed.
|
||||
|
||||
**Steps:**
|
||||
1. Prepare the `master` or `release` branch with all desired changes
|
||||
2. Create a version tag (must be a pure semantic version like `MAJOR.MINOR.PATCH`):
|
||||
```bash
|
||||
git checkout master # or release
|
||||
git tag 0.42.2
|
||||
git push origin 0.42.2
|
||||
```
|
||||
3. Monitor the GitHub Actions workflow
|
||||
4. Verify:
|
||||
- Workflow triggers and runs successfully on both Windows and Ubuntu
|
||||
- Version is determined as the tag (e.g., `0.42.2`)
|
||||
- Build and tests pass on both platforms
|
||||
- Package artifacts are uploaded for both platforms
|
||||
- Package is pushed to NuGet.org as stable release (Windows build only)
|
||||
|
||||
**Expected Outcome:**
|
||||
- A new stable release package appears on NuGet.org
|
||||
- Package version matches the tag
|
||||
|
||||
### 3. Test Duplicate Package Handling
|
||||
|
||||
This tests the `--skip-duplicate` flag behavior.
|
||||
|
||||
**Steps:**
|
||||
1. Push to the `release` branch without making changes
|
||||
2. Monitor the workflow
|
||||
3. Verify:
|
||||
- Workflow runs but NuGet push is skipped with "duplicate" message
|
||||
- No errors occur
|
||||
|
||||
### 4. Test Build Failure Handling
|
||||
|
||||
This tests that failed builds don't publish packages.
|
||||
|
||||
**Steps:**
|
||||
1. Introduce a breaking change in a test or code
|
||||
2. Push to the `release` branch
|
||||
3. Verify:
|
||||
- Workflow runs and detects the failure
|
||||
- Build or test step fails
|
||||
- NuGet push step is skipped
|
||||
- No package is published
|
||||
|
||||
## Verification
|
||||
|
||||
After each test, verify:
|
||||
|
||||
1. **GitHub Actions Logs**: Check the workflow logs for any errors or warnings
|
||||
2. **NuGet.org**: Verify the package appears with correct version and metadata
|
||||
3. **Artifacts**: Download and inspect the uploaded artifacts
|
||||
|
||||
## Rollback/Cleanup
|
||||
|
||||
If testing produces unwanted packages:
|
||||
|
||||
1. **Prerelease packages**: Can be unlisted on NuGet.org (Settings → Unlist)
|
||||
2. **Stable packages**: Cannot be deleted, only unlisted (use test versions)
|
||||
3. **Tags**: Can be deleted with:
|
||||
```bash
|
||||
git tag -d 0.42.2
|
||||
git push origin :refs/tags/0.42.2
|
||||
```
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- NuGet.org does not allow re-uploading the same version
|
||||
- Deleted packages on NuGet.org reserve the version number
|
||||
- The workflow requires the `NUGET_API_KEY` secret to be set
|
||||
|
||||
## Success Criteria
|
||||
|
||||
The workflow is considered successful if:
|
||||
|
||||
- ✅ Prerelease versions are published correctly with beta suffix
|
||||
- ✅ Tagged versions are published as stable releases
|
||||
- ✅ Build and test failures prevent publishing
|
||||
- ✅ Duplicate packages are handled gracefully
|
||||
- ✅ Workflow logs are clear and informative
|
||||
25
.github/workflows/dotnetcore.yml
vendored
25
.github/workflows/dotnetcore.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: SharpCompress
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened, ready_for_review ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- run: dotnet run --project build/build.csproj
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}-sharpcompress.nupkg
|
||||
path: artifacts/*
|
||||
61
.github/workflows/nuget-release.yml
vendored
Normal file
61
.github/workflows/nuget-release.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: NuGet Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release'
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for versioning
|
||||
|
||||
- uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
# Determine version using C# build target
|
||||
- name: Determine Version
|
||||
id: version
|
||||
run: dotnet run --project build/build.csproj -- determine-version
|
||||
|
||||
# Update version in project file using C# build target
|
||||
- name: Update Version in Project
|
||||
run: dotnet run --project build/build.csproj -- update-version
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
|
||||
# Build and test
|
||||
- name: Build and Test
|
||||
run: dotnet run --project build/build.csproj
|
||||
|
||||
# Upload artifacts for verification
|
||||
- name: Upload NuGet Package
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-nuget-package
|
||||
path: artifacts/*.nupkg
|
||||
|
||||
# Push to NuGet.org using C# build target (Windows only, not on PRs)
|
||||
- name: Push to NuGet
|
||||
if: success() && matrix.os == 'windows-latest' && github.event_name != 'pull_request'
|
||||
run: dotnet run --project build/build.csproj -- push-to-nuget
|
||||
env:
|
||||
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -11,11 +11,12 @@ TestResults/
|
||||
packages/*/
|
||||
project.lock.json
|
||||
tests/TestArchives/Scratch
|
||||
tests/TestArchives/*/Scratch
|
||||
tests/TestArchives/*/Scratch2
|
||||
.vs
|
||||
tools
|
||||
.vscode
|
||||
.idea/
|
||||
artifacts/
|
||||
|
||||
.DS_Store
|
||||
*.snupkg
|
||||
/tests/TestArchives/6d23a38c-f064-4ef1-ad89-b942396f53b9/Scratch
|
||||
|
||||
9
.vscode/extensions.json
vendored
Normal file
9
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-dotnettools.csharp",
|
||||
"ms-dotnettools.vscode-dotnet-runtime",
|
||||
"csharpier.csharpier-vscode",
|
||||
"formulahendry.dotnet-test-explorer"
|
||||
]
|
||||
}
|
||||
97
.vscode/launch.json
vendored
Normal file
97
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Tests (net10.0)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net10.0",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Debug Specific Test (net10.0)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net10.0",
|
||||
"--no-build",
|
||||
"--filter",
|
||||
"FullyQualifiedName~${input:testName}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Debug Performance Tests",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/tests/SharpCompress.Performance/SharpCompress.Performance.csproj",
|
||||
"--no-build"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Debug Build Script",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/build/build.csproj",
|
||||
"--",
|
||||
"${input:buildTarget}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "testName",
|
||||
"type": "promptString",
|
||||
"description": "Enter test name or pattern (e.g., TestMethodName or ClassName)",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"id": "buildTarget",
|
||||
"type": "pickString",
|
||||
"description": "Select build target",
|
||||
"options": [
|
||||
"clean",
|
||||
"restore",
|
||||
"build",
|
||||
"test",
|
||||
"format",
|
||||
"publish",
|
||||
"default"
|
||||
],
|
||||
"default": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
29
.vscode/settings.json
vendored
Normal file
29
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"dotnet.defaultSolution": "SharpCompress.sln",
|
||||
"files.exclude": {
|
||||
"**/bin": true,
|
||||
"**/obj": true
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/bin/**": true,
|
||||
"**/obj/**": true,
|
||||
"**/artifacts/**": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/bin": true,
|
||||
"**/obj": true,
|
||||
"**/artifacts": true
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"[csharp]": {
|
||||
"editor.defaultFormatter": "csharpier.csharpier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
}
|
||||
},
|
||||
"csharpier.enableDebugLogs": false,
|
||||
"omnisharp.enableRoslynAnalyzers": true,
|
||||
"omnisharp.enableEditorConfigSupport": true,
|
||||
"dotnet-test-explorer.testProjectPath": "tests/**/*.csproj"
|
||||
}
|
||||
178
.vscode/tasks.json
vendored
Normal file
178
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/SharpCompress.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "build-release",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/SharpCompress.sln",
|
||||
"-c",
|
||||
"Release",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "build-library",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/src/SharpCompress/SharpCompress.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "restore",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"restore",
|
||||
"${workspaceFolder}/SharpCompress.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "clean",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"clean",
|
||||
"${workspaceFolder}/SharpCompress.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "test",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test-net10",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net10.0",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "test",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test-net48",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net48",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "test",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "format",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"csharpier",
|
||||
"."
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "format-check",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"csharpier",
|
||||
"check",
|
||||
"."
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "run-build-script",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/build/build.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "pack",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"pack",
|
||||
"${workspaceFolder}/src/SharpCompress/SharpCompress.csproj",
|
||||
"-c",
|
||||
"Release",
|
||||
"-o",
|
||||
"${workspaceFolder}/artifacts/"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"dependsOn": "build-release"
|
||||
},
|
||||
{
|
||||
"label": "performance-tests",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/tests/SharpCompress.Performance/SharpCompress.Performance.csproj",
|
||||
"-c",
|
||||
"Release"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
165
AGENTS.md
165
AGENTS.md
@@ -1,19 +1,24 @@
|
||||
---
|
||||
description: 'Guidelines for building C# applications'
|
||||
description: 'Guidelines for building SharpCompress - A C# compression library'
|
||||
applyTo: '**/*.cs'
|
||||
---
|
||||
|
||||
# C# Development
|
||||
# SharpCompress Development
|
||||
|
||||
## About SharpCompress
|
||||
SharpCompress is a pure C# compression library supporting multiple archive formats (Zip, Tar, GZip, BZip2, 7Zip, Rar, LZip, XZ, ZStandard) for .NET Framework 4.62, .NET Standard 2.1, .NET 6.0, and .NET 8.0. The library provides both seekable Archive APIs and forward-only Reader/Writer APIs for streaming scenarios.
|
||||
|
||||
## C# Instructions
|
||||
- Always use the latest version C#, currently C# 13 features.
|
||||
- Write clear and concise comments for each function.
|
||||
- Follow the existing code style and patterns in the codebase.
|
||||
|
||||
## General Instructions
|
||||
- Make only high confidence suggestions when reviewing code changes.
|
||||
- Write code with good maintainability practices, including comments on why certain design decisions were made.
|
||||
- Handle edge cases and write clear exception handling.
|
||||
- For libraries or external dependencies, mention their usage and purpose in comments.
|
||||
- Preserve backward compatibility when making changes to public APIs.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
@@ -23,20 +28,74 @@ applyTo: '**/*.cs'
|
||||
|
||||
## Code Formatting
|
||||
|
||||
- Use CSharpier for all code formatting to ensure consistent style across the project.
|
||||
- Install CSharpier globally: `dotnet tool install -g csharpier`
|
||||
- Format files with: `dotnet csharpier format .`
|
||||
- Configure your IDE to format on save using CSharpier.
|
||||
- CSharpier configuration can be customized via `.csharpierrc` file in the project root.
|
||||
- Trust CSharpier's opinionated formatting decisions to maintain consistency.
|
||||
**Copilot agents: You MUST run the `format` task after making code changes to ensure consistency.**
|
||||
|
||||
- Use CSharpier for code formatting to ensure consistent style across the project
|
||||
- CSharpier is configured as a local tool in `.config/dotnet-tools.json`
|
||||
|
||||
### Commands
|
||||
|
||||
1. **Restore tools** (first time only):
|
||||
```bash
|
||||
dotnet tool restore
|
||||
```
|
||||
|
||||
2. **Check if files are formatted correctly** (doesn't modify files):
|
||||
```bash
|
||||
dotnet csharpier check .
|
||||
```
|
||||
- Exit code 0: All files are properly formatted
|
||||
- Exit code 1: Some files need formatting (will show which files and differences)
|
||||
|
||||
3. **Format files** (modifies files):
|
||||
```bash
|
||||
dotnet csharpier format .
|
||||
```
|
||||
- Formats all files in the project to match CSharpier style
|
||||
- Run from project root directory
|
||||
|
||||
4. **Configure your IDE** to format on save using CSharpier for the best experience
|
||||
|
||||
### Additional Notes
|
||||
- The project also uses `.editorconfig` for editor settings (indentation, encoding, etc.)
|
||||
- Let CSharpier handle code style while `.editorconfig` handles editor behavior
|
||||
- Always run `dotnet csharpier check .` before committing to verify formatting
|
||||
|
||||
## Project Setup and Structure
|
||||
|
||||
- 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`
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
src/SharpCompress/
|
||||
├── Archives/ # IArchive implementations (Zip, Tar, Rar, 7Zip, GZip)
|
||||
├── Readers/ # IReader implementations (forward-only)
|
||||
├── Writers/ # IWriter implementations (forward-only)
|
||||
├── Compressors/ # Low-level compression streams (BZip2, Deflate, LZMA, etc.)
|
||||
├── Factories/ # Format detection and factory pattern
|
||||
├── Common/ # Shared types (ArchiveType, Entry, Options)
|
||||
├── Crypto/ # Encryption implementations
|
||||
└── IO/ # Stream utilities and wrappers
|
||||
|
||||
tests/SharpCompress.Test/
|
||||
├── Zip/, Tar/, Rar/, SevenZip/, GZip/, BZip2/ # Format-specific tests
|
||||
├── TestBase.cs # Base test class with helper methods
|
||||
└── TestArchives/ # Test data (not checked into main test project)
|
||||
```
|
||||
|
||||
### Factory Pattern
|
||||
All format types implement factory interfaces (`IArchiveFactory`, `IReaderFactory`, `IWriterFactory`) for auto-detection:
|
||||
- `ReaderFactory.Open()` - Auto-detects format by probing stream
|
||||
- `WriterFactory.Open()` - Creates writer for specified `ArchiveType`
|
||||
- Factories located in: `src/SharpCompress/Factories/`
|
||||
|
||||
## Nullable Reference Types
|
||||
|
||||
@@ -44,21 +103,79 @@ 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.
|
||||
- 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.
|
||||
|
||||
## Performance Optimization
|
||||
### Test Organization
|
||||
- Base class: `TestBase` - Provides `TEST_ARCHIVES_PATH`, `SCRATCH_FILES_PATH`, temp directory management
|
||||
- Framework: xUnit with AwesomeAssertions
|
||||
- Test archives: `tests/TestArchives/` - Use existing archives, don't create new ones unnecessarily
|
||||
- Match naming style of nearby test files
|
||||
|
||||
- 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.
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Don't mix Archive and Reader APIs** - Archive needs seekable stream, Reader doesn't
|
||||
2. **Solid archives (Rar, 7Zip)** - Use `ExtractAllEntries()` for best performance, not individual entry extraction
|
||||
3. **Stream disposal** - Always set `LeaveStreamOpen` explicitly when needed (default is to close)
|
||||
4. **Tar + non-seekable stream** - Must provide file size or it will throw
|
||||
5. **Multi-framework differences** - Some features differ between .NET Framework and modern .NET (e.g., Mono.Posix)
|
||||
6. **Format detection** - Use `ReaderFactory.Open()` for auto-detection, test with actual archive files
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Bullseye" Version="6.0.0" />
|
||||
<PackageVersion Include="AwesomeAssertions" Version="9.2.0" />
|
||||
<PackageVersion Include="Bullseye" Version="6.1.0" />
|
||||
<PackageVersion Include="AwesomeAssertions" Version="9.3.0" />
|
||||
<PackageVersion Include="Glob" Version="1.1.9" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageVersion Include="JetBrains.Profiler.SelfApi" Version="2.5.15" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageVersion Include="SimpleExec" Version="12.0.0" />
|
||||
<PackageVersion Include="SimpleExec" Version="13.0.0" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Buffers" Version="4.6.1" />
|
||||
<PackageVersion Include="System.Memory" Version="4.6.3" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
<PackageVersion Include="xunit.SkippableFact" Version="1.5.23" />
|
||||
<PackageVersion Include="ZstdSharp.Port" Version="0.8.6" />
|
||||
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
26
FORMATS.md
26
FORMATS.md
@@ -10,7 +10,10 @@
|
||||
|
||||
| Archive Format | Compression Format(s) | Compress/Decompress | Archive API | Reader API | Writer API |
|
||||
| ---------------------- | ------------------------------------------------- | ------------------- | --------------- | ---------- | ------------- |
|
||||
| Rar | Rar | Decompress (1) | RarArchive | RarReader | N/A |
|
||||
| Ace | None | Decompress | N/A | AceReader | N/A |
|
||||
| Arc | None, Packed, Squeezed, Crunched | Decompress | N/A | ArcReader | N/A |
|
||||
| Arj | None | Decompress | N/A | ArjReader | N/A |
|
||||
| Rar | Rar | Decompress | RarArchive | RarReader | N/A |
|
||||
| Zip (2) | None, Shrink, Reduce, Implode, DEFLATE, Deflate64, BZip2, LZMA/LZMA2, PPMd | Both | ZipArchive | ZipReader | ZipWriter |
|
||||
| Tar | None | Both | TarArchive | TarReader | TarWriter (3) |
|
||||
| Tar.GZip | DEFLATE | Both | TarArchive | TarReader | TarWriter (3) |
|
||||
@@ -22,11 +25,28 @@
|
||||
| 7Zip (4) | LZMA, LZMA2, BZip2, PPMd, BCJ, BCJ2, Deflate | Decompress | SevenZipArchive | N/A | N/A |
|
||||
|
||||
1. SOLID Rars are only supported in the RarReader API.
|
||||
2. Zip format supports pkware and WinzipAES encryption. However, encrypted LZMA is not supported. Zip64 reading/writing is supported but only with seekable streams as the Zip spec doesn't support Zip64 data in post data descriptors. Deflate64 is only supported for reading.
|
||||
2. Zip format supports pkware and WinzipAES encryption. However, encrypted LZMA is not supported. Zip64 reading/writing is supported but only with seekable streams as the Zip spec doesn't support Zip64 data in post data descriptors. Deflate64 is only supported for reading. See [Zip Format Notes](#zip-format-notes) for details on multi-volume archives and streaming behavior.
|
||||
3. The Tar format requires a file size in the header. If no size is specified to the TarWriter and the stream is not seekable, then an exception will be thrown.
|
||||
4. The 7Zip format doesn't allow for reading as a forward-only stream so 7Zip is only supported through the Archive API
|
||||
4. The 7Zip format doesn't allow for reading as a forward-only stream so 7Zip is only supported through the Archive API. See [7Zip Format Notes](#7zip-format-notes) for details on async extraction behavior.
|
||||
5. LZip has no support for extra data like the file name or timestamp. There is a default filename used when looking at the entry Key on the archive.
|
||||
|
||||
### Zip Format Notes
|
||||
|
||||
- Multi-volume/split ZIP archives require ZipArchive (seekable streams) as ZipReader cannot seek across volume files.
|
||||
- ZipReader processes entries from LocalEntry headers (which include directory entries ending with `/`) and intentionally skips DirectoryEntry headers from the central directory, as they are redundant in streaming mode - all entry data comes from LocalEntry headers which ZipReader has already processed.
|
||||
|
||||
### 7Zip Format Notes
|
||||
|
||||
- **Async Extraction Performance**: When using async extraction methods (e.g., `ExtractAllEntries()` with `MoveToNextEntryAsync()`), each file creates its own decompression stream to avoid state corruption in the LZMA decoder. This is less efficient than synchronous extraction, which can reuse a single decompression stream for multiple files in the same folder.
|
||||
|
||||
**Performance Impact**: For archives with many small files in the same compression folder, async extraction will be slower than synchronous extraction because it must:
|
||||
1. Create a new LZMA decoder for each file
|
||||
2. Skip through the decompressed data to reach each file's starting position
|
||||
|
||||
**Recommendation**: For best performance with 7Zip archives, use synchronous extraction methods (`MoveToNextEntry()` and `WriteEntryToDirectory()`) when possible. Use async methods only when you need to avoid blocking the thread (e.g., in UI applications or async-only contexts).
|
||||
|
||||
**Technical Details**: 7Zip archives group files into "folders" (compression units), where all files in a folder share one continuous LZMA-compressed stream. The LZMA decoder maintains internal state (dictionary window, decoder positions) that assumes sequential, non-interruptible processing. Async operations can yield control during awaits, which would corrupt this shared state. To avoid this, async extraction creates a fresh decoder stream for each file.
|
||||
|
||||
## Compression Streams
|
||||
|
||||
For those who want to directly compress/decompress bits. The single file formats are represented here as well. However, BZip2, LZip and XZ have no metadata (GZip has a little) so using them without something like a Tar file makes little sense.
|
||||
|
||||
142
OLD_CHANGELOG.md
Normal file
142
OLD_CHANGELOG.md
Normal file
@@ -0,0 +1,142 @@
|
||||
|
||||
# Version Log
|
||||
|
||||
* [Releases](https://github.com/adamhathcock/sharpcompress/releases)
|
||||
|
||||
## Version 0.18
|
||||
|
||||
* [Now on Github releases](https://github.com/adamhathcock/sharpcompress/releases/tag/0.18)
|
||||
|
||||
## Version 0.17.1
|
||||
|
||||
* Fix - [Bug Fix for .NET Core on Windows](https://github.com/adamhathcock/sharpcompress/pull/257)
|
||||
|
||||
## Version 0.17.0
|
||||
|
||||
* New - Full LZip support! Can read and write LZip files and Tars inside LZip files. [Make LZip a first class citizen. #241](https://github.com/adamhathcock/sharpcompress/issues/241)
|
||||
* New - XZ read support! Can read XZ files and Tars inside XZ files. [XZ in SharpCompress #91](https://github.com/adamhathcock/sharpcompress/issues/94)
|
||||
* Fix - [Regression - zip file writing on seekable streams always assumed stream start was 0. Introduced with Zip64 writing.](https://github.com/adamhathcock/sharpcompress/issues/244)
|
||||
* Fix - [Zip files with post-data descriptors can be properly skipped via decompression](https://github.com/adamhathcock/sharpcompress/issues/162)
|
||||
|
||||
## Version 0.16.2
|
||||
|
||||
* Fix [.NET 3.5 should support files and cryptography (was a regression from 0.16.0)](https://github.com/adamhathcock/sharpcompress/pull/251)
|
||||
* Fix [Zip per entry compression customization wrote the wrong method into the zip archive](https://github.com/adamhathcock/sharpcompress/pull/249)
|
||||
|
||||
## Version 0.16.1
|
||||
|
||||
* Fix [Preserve compression method when getting a compressed stream](https://github.com/adamhathcock/sharpcompress/pull/235)
|
||||
* Fix [RAR entry key normalization fix](https://github.com/adamhathcock/sharpcompress/issues/201)
|
||||
|
||||
## Version 0.16.0
|
||||
|
||||
* Breaking - [Progress Event Tracking rethink](https://github.com/adamhathcock/sharpcompress/pull/226)
|
||||
* Update to VS2017 - [VS2017](https://github.com/adamhathcock/sharpcompress/pull/231) - Framework targets have been changed.
|
||||
* New - [Add Zip64 writing](https://github.com/adamhathcock/sharpcompress/pull/211)
|
||||
* [Fix invalid/mismatching Zip version flags.](https://github.com/adamhathcock/sharpcompress/issues/164) - This allows nuget/System.IO.Packaging to read zip files generated by SharpCompress
|
||||
* [Fix 7Zip directory hiding](https://github.com/adamhathcock/sharpcompress/pull/215/files)
|
||||
* [Verify RAR CRC headers](https://github.com/adamhathcock/sharpcompress/pull/220)
|
||||
|
||||
## Version 0.15.2
|
||||
|
||||
* [Fix invalid headers](https://github.com/adamhathcock/sharpcompress/pull/210) - fixes an issue creating large-ish zip archives that was introduced with zip64 reading.
|
||||
|
||||
## Version 0.15.1
|
||||
|
||||
* [Zip64 extending information and ZipReader](https://github.com/adamhathcock/sharpcompress/pull/206)
|
||||
|
||||
## Version 0.15.0
|
||||
|
||||
* [Add zip64 support for ZipArchive extraction](https://github.com/adamhathcock/sharpcompress/pull/205)
|
||||
|
||||
## Version 0.14.1
|
||||
|
||||
* [.NET Assemblies aren't strong named](https://github.com/adamhathcock/sharpcompress/issues/158)
|
||||
* [Pkware encryption for Zip files didn't allow for multiple reads of an entry](https://github.com/adamhathcock/sharpcompress/issues/197)
|
||||
* [GZip Entry couldn't be read multiple times](https://github.com/adamhathcock/sharpcompress/issues/198)
|
||||
|
||||
## Version 0.14.0
|
||||
|
||||
* [Support for LZip reading in for Tars](https://github.com/adamhathcock/sharpcompress/pull/191)
|
||||
|
||||
## Version 0.13.1
|
||||
|
||||
* [Fix null password on ReaderFactory. Fix null options on SevenZipArchive](https://github.com/adamhathcock/sharpcompress/pull/188)
|
||||
* [Make PpmdProperties lazy to avoid unnecessary allocations.](https://github.com/adamhathcock/sharpcompress/pull/185)
|
||||
|
||||
## Version 0.13.0
|
||||
|
||||
* Breaking change: Big refactor of Options on API.
|
||||
* 7Zip supports Deflate
|
||||
|
||||
## Version 0.12.4
|
||||
|
||||
* Forward only zip issue fix https://github.com/adamhathcock/sharpcompress/issues/160
|
||||
* Try to fix frameworks again by copying targets from JSON.NET
|
||||
|
||||
## Version 0.12.3
|
||||
|
||||
* 7Zip fixes https://github.com/adamhathcock/sharpcompress/issues/73
|
||||
* Maybe all profiles will work with project.json now
|
||||
|
||||
## Version 0.12.2
|
||||
|
||||
* Support Profile 259 again
|
||||
|
||||
## Version 0.12.1
|
||||
|
||||
* Support Silverlight 5
|
||||
|
||||
## Version 0.12.0
|
||||
|
||||
* .NET Core RTM!
|
||||
* Bug fix for Tar long paths
|
||||
|
||||
## Version 0.11.6
|
||||
|
||||
* Bug fix for global header in Tar
|
||||
* Writers now have a leaveOpen `bool` overload. They won't close streams if not-requested to.
|
||||
|
||||
## Version 0.11.5
|
||||
|
||||
* Bug fix in Skip method
|
||||
|
||||
## Version 0.11.4
|
||||
|
||||
* SharpCompress is now endian neutral (matters for Mono platforms)
|
||||
* Fix for Inflate (need to change implementation)
|
||||
* Fixes for RAR detection
|
||||
|
||||
## Version 0.11.1
|
||||
|
||||
* Added Cancel on IReader
|
||||
* Removed .NET 2.0 support and LinqBridge dependency
|
||||
|
||||
## Version 0.11
|
||||
|
||||
* Been over a year, contains mainly fixes from contributors!
|
||||
* Possible breaking change: ArchiveEncoding is UTF8 by default now.
|
||||
* TAR supports writing long names using longlink
|
||||
* RAR Protect Header added
|
||||
|
||||
## Version 0.10.3
|
||||
|
||||
* Finally fixed Disposal issue when creating a new archive with the Archive API
|
||||
|
||||
## Version 0.10.2
|
||||
|
||||
* Fixed Rar Header reading for invalid extended time headers.
|
||||
* Windows Store assembly is now strong named
|
||||
* Known issues with Long Tar names being worked on
|
||||
* Updated to VS2013
|
||||
* Portable targets SL5 and Windows Phone 8 (up from SL4 and WP7)
|
||||
|
||||
## Version 0.10.1
|
||||
|
||||
* Fixed 7Zip extraction performance problem
|
||||
|
||||
## Version 0.10:
|
||||
|
||||
* Added support for RAR Decryption (thanks to https://github.com/hrasyid)
|
||||
* Embedded some BouncyCastle crypto classes to allow RAR Decryption and Winzip AES Decryption in Portable and Windows Store DLLs
|
||||
* Built in Release (I think)
|
||||
154
README.md
154
README.md
@@ -1,9 +1,11 @@
|
||||
# SharpCompress
|
||||
|
||||
SharpCompress is a compression library in pure C# for .NET Framework 4.62, .NET Standard 2.1, .NET 6.0 and NET 8.0 that can unrar, un7zip, unzip, untar unbzip2, ungzip, unlzip, unzstd with forward-only reading and file random access APIs. Write support for zip/tar/bzip2/gzip/lzip are implemented.
|
||||
SharpCompress is a compression library in pure C# for .NET Framework 4.8, .NET 8.0 and .NET 10.0 that can unrar, un7zip, unzip, untar unbzip2, ungzip, unlzip, unzstd, unarc and unarj with forward-only reading and file random access APIs. Write support for zip/tar/bzip2/gzip/lzip are implemented.
|
||||
|
||||
The major feature is support for non-seekable streams so large files can be processed on the fly (i.e. download stream).
|
||||
|
||||
**NEW:** All I/O operations now support async/await for improved performance and scalability. See the [USAGE.md](USAGE.md#async-examples) for examples.
|
||||
|
||||
GitHub Actions Build -
|
||||
[](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml)
|
||||
[](https://dndocs.com/d/sharpcompress/api/index.html)
|
||||
@@ -36,155 +38,7 @@ Please do not email me directly to ask for help. If you think there is a real is
|
||||
|
||||
I'm always looking for help or ideas. Please submit code or email with ideas. Unfortunately, just letting me know you'd like to help is not enough because I really have no overall plan of what needs to be done. I'll definitely accept code submissions and add you as a member of the project!
|
||||
|
||||
## TODOs (always lots)
|
||||
|
||||
* RAR 5 decryption crc check support
|
||||
* 7Zip writing
|
||||
* Zip64 (Need writing and extend Reading)
|
||||
* Multi-volume Zip support.
|
||||
* ZStandard writing
|
||||
|
||||
## Version Log
|
||||
|
||||
* [Releases](https://github.com/adamhathcock/sharpcompress/releases)
|
||||
|
||||
### Version 0.18
|
||||
|
||||
* [Now on Github releases](https://github.com/adamhathcock/sharpcompress/releases/tag/0.18)
|
||||
|
||||
### Version 0.17.1
|
||||
|
||||
* Fix - [Bug Fix for .NET Core on Windows](https://github.com/adamhathcock/sharpcompress/pull/257)
|
||||
|
||||
### Version 0.17.0
|
||||
|
||||
* New - Full LZip support! Can read and write LZip files and Tars inside LZip files. [Make LZip a first class citizen. #241](https://github.com/adamhathcock/sharpcompress/issues/241)
|
||||
* New - XZ read support! Can read XZ files and Tars inside XZ files. [XZ in SharpCompress #91](https://github.com/adamhathcock/sharpcompress/issues/94)
|
||||
* Fix - [Regression - zip file writing on seekable streams always assumed stream start was 0. Introduced with Zip64 writing.](https://github.com/adamhathcock/sharpcompress/issues/244)
|
||||
* Fix - [Zip files with post-data descriptors can be properly skipped via decompression](https://github.com/adamhathcock/sharpcompress/issues/162)
|
||||
|
||||
### Version 0.16.2
|
||||
|
||||
* Fix [.NET 3.5 should support files and cryptography (was a regression from 0.16.0)](https://github.com/adamhathcock/sharpcompress/pull/251)
|
||||
* Fix [Zip per entry compression customization wrote the wrong method into the zip archive](https://github.com/adamhathcock/sharpcompress/pull/249)
|
||||
|
||||
### Version 0.16.1
|
||||
|
||||
* Fix [Preserve compression method when getting a compressed stream](https://github.com/adamhathcock/sharpcompress/pull/235)
|
||||
* Fix [RAR entry key normalization fix](https://github.com/adamhathcock/sharpcompress/issues/201)
|
||||
|
||||
### Version 0.16.0
|
||||
|
||||
* Breaking - [Progress Event Tracking rethink](https://github.com/adamhathcock/sharpcompress/pull/226)
|
||||
* Update to VS2017 - [VS2017](https://github.com/adamhathcock/sharpcompress/pull/231) - Framework targets have been changed.
|
||||
* New - [Add Zip64 writing](https://github.com/adamhathcock/sharpcompress/pull/211)
|
||||
* [Fix invalid/mismatching Zip version flags.](https://github.com/adamhathcock/sharpcompress/issues/164) - This allows nuget/System.IO.Packaging to read zip files generated by SharpCompress
|
||||
* [Fix 7Zip directory hiding](https://github.com/adamhathcock/sharpcompress/pull/215/files)
|
||||
* [Verify RAR CRC headers](https://github.com/adamhathcock/sharpcompress/pull/220)
|
||||
|
||||
### Version 0.15.2
|
||||
|
||||
* [Fix invalid headers](https://github.com/adamhathcock/sharpcompress/pull/210) - fixes an issue creating large-ish zip archives that was introduced with zip64 reading.
|
||||
|
||||
### Version 0.15.1
|
||||
|
||||
* [Zip64 extending information and ZipReader](https://github.com/adamhathcock/sharpcompress/pull/206)
|
||||
|
||||
### Version 0.15.0
|
||||
|
||||
* [Add zip64 support for ZipArchive extraction](https://github.com/adamhathcock/sharpcompress/pull/205)
|
||||
|
||||
### Version 0.14.1
|
||||
|
||||
* [.NET Assemblies aren't strong named](https://github.com/adamhathcock/sharpcompress/issues/158)
|
||||
* [Pkware encryption for Zip files didn't allow for multiple reads of an entry](https://github.com/adamhathcock/sharpcompress/issues/197)
|
||||
* [GZip Entry couldn't be read multiple times](https://github.com/adamhathcock/sharpcompress/issues/198)
|
||||
|
||||
### Version 0.14.0
|
||||
|
||||
* [Support for LZip reading in for Tars](https://github.com/adamhathcock/sharpcompress/pull/191)
|
||||
|
||||
### Version 0.13.1
|
||||
|
||||
* [Fix null password on ReaderFactory. Fix null options on SevenZipArchive](https://github.com/adamhathcock/sharpcompress/pull/188)
|
||||
* [Make PpmdProperties lazy to avoid unnecessary allocations.](https://github.com/adamhathcock/sharpcompress/pull/185)
|
||||
|
||||
### Version 0.13.0
|
||||
|
||||
* Breaking change: Big refactor of Options on API.
|
||||
* 7Zip supports Deflate
|
||||
|
||||
### Version 0.12.4
|
||||
|
||||
* Forward only zip issue fix https://github.com/adamhathcock/sharpcompress/issues/160
|
||||
* Try to fix frameworks again by copying targets from JSON.NET
|
||||
|
||||
### Version 0.12.3
|
||||
|
||||
* 7Zip fixes https://github.com/adamhathcock/sharpcompress/issues/73
|
||||
* Maybe all profiles will work with project.json now
|
||||
|
||||
### Version 0.12.2
|
||||
|
||||
* Support Profile 259 again
|
||||
|
||||
### Version 0.12.1
|
||||
|
||||
* Support Silverlight 5
|
||||
|
||||
### Version 0.12.0
|
||||
|
||||
* .NET Core RTM!
|
||||
* Bug fix for Tar long paths
|
||||
|
||||
### Version 0.11.6
|
||||
|
||||
* Bug fix for global header in Tar
|
||||
* Writers now have a leaveOpen `bool` overload. They won't close streams if not-requested to.
|
||||
|
||||
### Version 0.11.5
|
||||
|
||||
* Bug fix in Skip method
|
||||
|
||||
### Version 0.11.4
|
||||
|
||||
* SharpCompress is now endian neutral (matters for Mono platforms)
|
||||
* Fix for Inflate (need to change implementation)
|
||||
* Fixes for RAR detection
|
||||
|
||||
### Version 0.11.1
|
||||
|
||||
* Added Cancel on IReader
|
||||
* Removed .NET 2.0 support and LinqBridge dependency
|
||||
|
||||
### Version 0.11
|
||||
|
||||
* Been over a year, contains mainly fixes from contributors!
|
||||
* Possible breaking change: ArchiveEncoding is UTF8 by default now.
|
||||
* TAR supports writing long names using longlink
|
||||
* RAR Protect Header added
|
||||
|
||||
### Version 0.10.3
|
||||
|
||||
* Finally fixed Disposal issue when creating a new archive with the Archive API
|
||||
|
||||
### Version 0.10.2
|
||||
|
||||
* Fixed Rar Header reading for invalid extended time headers.
|
||||
* Windows Store assembly is now strong named
|
||||
* Known issues with Long Tar names being worked on
|
||||
* Updated to VS2013
|
||||
* Portable targets SL5 and Windows Phone 8 (up from SL4 and WP7)
|
||||
|
||||
### Version 0.10.1
|
||||
|
||||
* Fixed 7Zip extraction performance problem
|
||||
|
||||
### Version 0.10:
|
||||
|
||||
* Added support for RAR Decryption (thanks to https://github.com/hrasyid)
|
||||
* Embedded some BouncyCastle crypto classes to allow RAR Decryption and Winzip AES Decryption in Portable and Windows Store DLLs
|
||||
* Built in Release (I think)
|
||||
## Notes
|
||||
|
||||
XZ implementation based on: https://github.com/sambott/XZ.NET by @sambott
|
||||
|
||||
|
||||
@@ -20,9 +20,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{CDB425
|
||||
.editorconfig = .editorconfig
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
NuGet.config = NuGet.config
|
||||
.github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml
|
||||
.github\workflows\nuget-release.yml = .github\workflows\nuget-release.yml
|
||||
USAGE.md = USAGE.md
|
||||
README.md = README.md
|
||||
FORMATS.md = FORMATS.md
|
||||
AGENTS.md = AGENTS.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCompress.Performance", "tests\SharpCompress.Performance\SharpCompress.Performance.csproj", "{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -41,6 +47,10 @@ Global
|
||||
{D4D613CB-5E94-47FB-85BE-B8423D20C545}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D4D613CB-5E94-47FB-85BE-B8423D20C545}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D4D613CB-5E94-47FB-85BE-B8423D20C545}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -48,5 +58,6 @@ Global
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{FD19DDD8-72B2-4024-8665-0D1F7A2AA998} = {3C5BE746-03E5-4895-9988-0B57F162F86C}
|
||||
{F2B1A1EB-0FA6-40D0-8908-E13247C7226F} = {0F0901FF-E8D9-426A-B5A2-17C7F47C1529}
|
||||
{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17} = {0F0901FF-E8D9-426A-B5A2-17C7F47C1529}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
179
USAGE.md
179
USAGE.md
@@ -1,5 +1,18 @@
|
||||
# SharpCompress Usage
|
||||
|
||||
## Async/Await Support
|
||||
|
||||
SharpCompress now provides full async/await support for all I/O operations. All `Read`, `Write`, and extraction operations have async equivalents ending in `Async` that accept an optional `CancellationToken`. This enables better performance and scalability for I/O-bound operations.
|
||||
|
||||
**Key Async Methods:**
|
||||
- `reader.WriteEntryToAsync(stream, cancellationToken)` - Extract entry asynchronously
|
||||
- `reader.WriteAllToDirectoryAsync(path, options, cancellationToken)` - Extract all asynchronously
|
||||
- `writer.WriteAsync(filename, stream, modTime, cancellationToken)` - Write entry asynchronously
|
||||
- `writer.WriteAllAsync(directory, pattern, searchOption, cancellationToken)` - Write directory asynchronously
|
||||
- `entry.OpenEntryStreamAsync(cancellationToken)` - Open entry stream asynchronously
|
||||
|
||||
See [Async Examples](#async-examples) section below for usage patterns.
|
||||
|
||||
## Stream Rules (changed with 0.21)
|
||||
|
||||
When dealing with Streams, the rule should be that you don't close a stream you didn't create. This, in effect, should mean you should always put a Stream in a using block to dispose it.
|
||||
@@ -74,20 +87,17 @@ memoryStream.Position = 0;
|
||||
### Extract all files from a rar file to a directory using RarArchive
|
||||
|
||||
Note: Extracting a solid rar or 7z file needs to be done in sequential order to get acceptable decompression speed.
|
||||
It is explicitly recommended to use `ExtractAllEntries` when extracting an entire `IArchive` instead of iterating over all its `Entries`.
|
||||
Alternatively, use `IArchive.WriteToDirectory`.
|
||||
`ExtractAllEntries` is primarily intended for solid archives (like solid Rar) or 7Zip archives, where sequential extraction provides the best performance. For general/simple extraction with any supported archive type, use `archive.WriteToDirectory()` instead.
|
||||
|
||||
```C#
|
||||
using (var archive = RarArchive.Open("Test.rar"))
|
||||
{
|
||||
using (var reader = archive.ExtractAllEntries())
|
||||
// Simple extraction with RarArchive; this WriteToDirectory pattern works for all archive types
|
||||
archive.WriteToDirectory(@"D:\temp", new ExtractionOptions()
|
||||
{
|
||||
reader.WriteAllToDirectory(@"D:\temp", new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
});
|
||||
}
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -103,6 +113,29 @@ using (var archive = RarArchive.Open("Test.rar"))
|
||||
}
|
||||
```
|
||||
|
||||
### Extract solid Rar or 7Zip archives with progress reporting
|
||||
|
||||
`ExtractAllEntries` only works for solid archives (Rar) or 7Zip archives. For optimal performance with these archive types, use this method:
|
||||
|
||||
```C#
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
var progress = new Progress<ProgressReport>(report =>
|
||||
{
|
||||
Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%");
|
||||
});
|
||||
|
||||
using (var archive = RarArchive.Open("archive.rar", new ReaderOptions { Progress = progress })) // Must be solid Rar or 7Zip
|
||||
{
|
||||
archive.WriteToDirectory(@"D:\output", new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Use ReaderFactory to autodetect archive type and Open the entry stream
|
||||
|
||||
```C#
|
||||
@@ -172,3 +205,131 @@ foreach(var entry in tr.Entries)
|
||||
Console.WriteLine($"{entry.Key}");
|
||||
}
|
||||
```
|
||||
|
||||
## Async Examples
|
||||
|
||||
### Async Reader Examples
|
||||
|
||||
**Extract single entry asynchronously:**
|
||||
```C#
|
||||
using (Stream stream = File.OpenRead("archive.zip"))
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
using (var entryStream = reader.OpenEntryStream())
|
||||
{
|
||||
using (var outputStream = File.Create("output.bin"))
|
||||
{
|
||||
await reader.WriteEntryToAsync(outputStream, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Extract all entries asynchronously:**
|
||||
```C#
|
||||
using (Stream stream = File.OpenRead("archive.tar.gz"))
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
@"D:\temp",
|
||||
new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
},
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Open and process entry stream asynchronously:**
|
||||
```C#
|
||||
using (var archive = ZipArchive.Open("archive.zip"))
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
|
||||
{
|
||||
using (var entryStream = await entry.OpenEntryStreamAsync(cancellationToken))
|
||||
{
|
||||
// Process the decompressed stream asynchronously
|
||||
await ProcessStreamAsync(entryStream, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Async Writer Examples
|
||||
|
||||
**Write single file asynchronously:**
|
||||
```C#
|
||||
using (Stream archiveStream = File.OpenWrite("output.zip"))
|
||||
using (var writer = WriterFactory.Open(archiveStream, ArchiveType.Zip, CompressionType.Deflate))
|
||||
{
|
||||
using (Stream fileStream = File.OpenRead("input.txt"))
|
||||
{
|
||||
await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Write entire directory asynchronously:**
|
||||
```C#
|
||||
using (Stream stream = File.OpenWrite("backup.tar.gz"))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)))
|
||||
{
|
||||
await writer.WriteAllAsync(
|
||||
@"D:\files",
|
||||
"*",
|
||||
SearchOption.AllDirectories,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Write with progress tracking and cancellation:**
|
||||
```C#
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
// Set timeout or cancel from UI
|
||||
cts.CancelAfter(TimeSpan.FromMinutes(5));
|
||||
|
||||
using (Stream stream = File.OpenWrite("archive.zip"))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
|
||||
{
|
||||
try
|
||||
{
|
||||
await writer.WriteAllAsync(@"D:\data", "*", SearchOption.AllDirectories, cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine("Operation was cancelled");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Archive Async Examples
|
||||
|
||||
**Extract from archive asynchronously:**
|
||||
```C#
|
||||
using (var archive = ZipArchive.Open("archive.zip"))
|
||||
{
|
||||
// Simple async extraction - works for all archive types
|
||||
await archive.WriteToDirectoryAsync(
|
||||
@"C:\output",
|
||||
new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits of Async Operations:**
|
||||
- Non-blocking I/O for better application responsiveness
|
||||
- Improved scalability for server applications
|
||||
- Support for cancellation via CancellationToken
|
||||
- Better resource utilization in async/await contexts
|
||||
- Compatible with modern .NET async patterns
|
||||
|
||||
177
build/Program.cs
177
build/Program.cs
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using GlobExpressions;
|
||||
using static Bullseye.Targets;
|
||||
using static SimpleExec.Command;
|
||||
@@ -11,7 +14,11 @@ const string Restore = "restore";
|
||||
const string Build = "build";
|
||||
const string Test = "test";
|
||||
const string Format = "format";
|
||||
const string CheckFormat = "check-format";
|
||||
const string Publish = "publish";
|
||||
const string DetermineVersion = "determine-version";
|
||||
const string UpdateVersion = "update-version";
|
||||
const string PushToNuGet = "push-to-nuget";
|
||||
|
||||
Target(
|
||||
Clean,
|
||||
@@ -42,12 +49,20 @@ Target(
|
||||
Target(
|
||||
Format,
|
||||
() =>
|
||||
{
|
||||
Run("dotnet", "tool restore");
|
||||
Run("dotnet", "csharpier format .");
|
||||
}
|
||||
);
|
||||
Target(
|
||||
CheckFormat,
|
||||
() =>
|
||||
{
|
||||
Run("dotnet", "tool restore");
|
||||
Run("dotnet", "csharpier check .");
|
||||
}
|
||||
);
|
||||
Target(Restore, [Format], () => Run("dotnet", "restore"));
|
||||
Target(Restore, [CheckFormat], () => Run("dotnet", "restore"));
|
||||
|
||||
Target(
|
||||
Build,
|
||||
@@ -61,7 +76,7 @@ Target(
|
||||
Target(
|
||||
Test,
|
||||
[Build],
|
||||
["net8.0", "net48"],
|
||||
["net10.0", "net48"],
|
||||
framework =>
|
||||
{
|
||||
IEnumerable<string> GetFiles(string d)
|
||||
@@ -90,6 +105,164 @@ Target(
|
||||
}
|
||||
);
|
||||
|
||||
Target(
|
||||
DetermineVersion,
|
||||
async () =>
|
||||
{
|
||||
var (version, isPrerelease) = await GetVersion();
|
||||
Console.WriteLine($"VERSION={version}");
|
||||
Console.WriteLine($"PRERELEASE={isPrerelease.ToString().ToLower()}");
|
||||
|
||||
// Write to environment file for GitHub Actions
|
||||
var githubOutput = Environment.GetEnvironmentVariable("GITHUB_OUTPUT");
|
||||
if (!string.IsNullOrEmpty(githubOutput))
|
||||
{
|
||||
File.AppendAllText(githubOutput, $"version={version}\n");
|
||||
File.AppendAllText(githubOutput, $"prerelease={isPrerelease.ToString().ToLower()}\n");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Target(
|
||||
UpdateVersion,
|
||||
async () =>
|
||||
{
|
||||
var version = Environment.GetEnvironmentVariable("VERSION");
|
||||
if (string.IsNullOrEmpty(version))
|
||||
{
|
||||
var (detectedVersion, _) = await GetVersion();
|
||||
version = detectedVersion;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Updating project file with version: {version}");
|
||||
|
||||
var projectPath = "src/SharpCompress/SharpCompress.csproj";
|
||||
var content = File.ReadAllText(projectPath);
|
||||
|
||||
// Get base version (without prerelease suffix)
|
||||
var baseVersion = version.Split('-')[0];
|
||||
|
||||
// Update VersionPrefix
|
||||
content = Regex.Replace(
|
||||
content,
|
||||
@"<VersionPrefix>[^<]*</VersionPrefix>",
|
||||
$"<VersionPrefix>{version}</VersionPrefix>"
|
||||
);
|
||||
|
||||
// Update AssemblyVersion
|
||||
content = Regex.Replace(
|
||||
content,
|
||||
@"<AssemblyVersion>[^<]*</AssemblyVersion>",
|
||||
$"<AssemblyVersion>{baseVersion}</AssemblyVersion>"
|
||||
);
|
||||
|
||||
// Update FileVersion
|
||||
content = Regex.Replace(
|
||||
content,
|
||||
@"<FileVersion>[^<]*</FileVersion>",
|
||||
$"<FileVersion>{baseVersion}</FileVersion>"
|
||||
);
|
||||
|
||||
File.WriteAllText(projectPath, content);
|
||||
Console.WriteLine($"Updated VersionPrefix to: {version}");
|
||||
Console.WriteLine($"Updated AssemblyVersion and FileVersion to: {baseVersion}");
|
||||
}
|
||||
);
|
||||
|
||||
Target(
|
||||
PushToNuGet,
|
||||
() =>
|
||||
{
|
||||
var apiKey = Environment.GetEnvironmentVariable("NUGET_API_KEY");
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"NUGET_API_KEY environment variable is not set. Skipping NuGet push."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var packages = Directory.GetFiles("artifacts", "*.nupkg");
|
||||
if (packages.Length == 0)
|
||||
{
|
||||
Console.WriteLine("No packages found in artifacts directory.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var package in packages)
|
||||
{
|
||||
Console.WriteLine($"Pushing {package} to NuGet.org");
|
||||
try
|
||||
{
|
||||
// Note: API key is passed via command line argument which is standard practice for dotnet nuget push
|
||||
// The key is already in an environment variable and not displayed in normal output
|
||||
Run(
|
||||
"dotnet",
|
||||
$"nuget push \"{package}\" --api-key {apiKey} --source https://api.nuget.org/v3/index.json --skip-duplicate"
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to push {package}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Target("default", [Publish], () => Console.WriteLine("Done!"));
|
||||
|
||||
await RunTargetsAndExitAsync(args);
|
||||
|
||||
static async Task<(string version, bool isPrerelease)> GetVersion()
|
||||
{
|
||||
// Check if current commit has a version tag
|
||||
var currentTag = (await GetGitOutput("tag", "--points-at HEAD"))
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault(tag => Regex.IsMatch(tag.Trim(), @"^\d+\.\d+\.\d+$"));
|
||||
|
||||
if (!string.IsNullOrEmpty(currentTag))
|
||||
{
|
||||
// Tagged release - use the tag as version
|
||||
var version = currentTag.Trim();
|
||||
Console.WriteLine($"Building tagged release version: {version}");
|
||||
return (version, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not tagged - create prerelease version based on next minor version
|
||||
var allTags = (await GetGitOutput("tag", "--list"))
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(tag => Regex.IsMatch(tag.Trim(), @"^\d+\.\d+\.\d+$"))
|
||||
.Select(tag => tag.Trim())
|
||||
.ToList();
|
||||
|
||||
var lastTag = allTags.OrderBy(tag => Version.Parse(tag)).LastOrDefault() ?? "0.0.0";
|
||||
var lastVersion = Version.Parse(lastTag);
|
||||
|
||||
// Increment minor version for next release
|
||||
var nextVersion = new Version(lastVersion.Major, lastVersion.Minor + 1, 0);
|
||||
|
||||
// Use commit count since the last version tag if available; otherwise, fall back to total count
|
||||
var revListArgs = allTags.Any() ? $"--count {lastTag}..HEAD" : "--count HEAD";
|
||||
var commitCount = (await GetGitOutput("rev-list", revListArgs)).Trim();
|
||||
|
||||
var version = $"{nextVersion}-beta.{commitCount}";
|
||||
Console.WriteLine($"Building prerelease version: {version}");
|
||||
return (version, true);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<string> GetGitOutput(string command, string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use SimpleExec's Read to execute git commands in a cross-platform way
|
||||
var (output, _) = await ReadAsync("git", $"{command} {args}");
|
||||
return output;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Git command failed: git {command} {args}\n{ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bullseye" />
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net8.0": {
|
||||
"net10.0": {
|
||||
"Bullseye": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.0, )",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "vgwwXfzs7jJrskWH7saHRMgPzziq/e86QZNWY1MnMxd7e+De7E7EX4K3C7yrvaK9y02SJoLxNxcLG/q5qUAghw=="
|
||||
"requested": "[6.1.0, )",
|
||||
"resolved": "6.1.0",
|
||||
"contentHash": "fltnAJDe0BEX5eymXGUq+il2rSUA0pHqUonNDRH2TrvRu8SkU17mYG0IVpdmG2ibtfhdjNrv4CuTCxHOwcozCA=="
|
||||
},
|
||||
"Glob": {
|
||||
"type": "Direct",
|
||||
@@ -16,9 +16,9 @@
|
||||
},
|
||||
"SimpleExec": {
|
||||
"type": "Direct",
|
||||
"requested": "[12.0.0, )",
|
||||
"resolved": "12.0.0",
|
||||
"contentHash": "ptxlWtxC8vM6Y6e3h9ZTxBBkOWnWrm/Sa1HT+2i1xcXY3Hx2hmKDZP5RShPf8Xr9D+ivlrXNy57ktzyH8kyt+Q=="
|
||||
"requested": "[13.0.0, )",
|
||||
"resolved": "13.0.0",
|
||||
"contentHash": "zcCR1pupa1wI1VqBULRiQKeHKKZOuJhi/K+4V5oO+rHJZlaOD53ViFo1c3PavDoMAfSn/FAXGAWpPoF57rwhYg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100",
|
||||
"version": "10.0.100",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtractionListener
|
||||
public abstract class AbstractArchive<TEntry, TVolume> : IArchive
|
||||
where TEntry : IArchiveEntry
|
||||
where TVolume : IVolume
|
||||
{
|
||||
@@ -17,11 +17,6 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
private bool _disposed;
|
||||
private readonly SourceStream? _sourceStream;
|
||||
|
||||
public event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>>? EntryExtractionBegin;
|
||||
public event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>>? EntryExtractionEnd;
|
||||
|
||||
public event EventHandler<CompressedBytesReadEventArgs>? CompressedBytesRead;
|
||||
public event EventHandler<FilePartExtractionBeginEventArgs>? FilePartExtractionBegin;
|
||||
protected ReaderOptions ReaderOptions { get; }
|
||||
|
||||
internal AbstractArchive(ArchiveType type, SourceStream sourceStream)
|
||||
@@ -43,12 +38,6 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
|
||||
public ArchiveType Type { get; }
|
||||
|
||||
void IArchiveExtractionListener.FireEntryExtractionBegin(IArchiveEntry entry) =>
|
||||
EntryExtractionBegin?.Invoke(this, new ArchiveExtractionEventArgs<IArchiveEntry>(entry));
|
||||
|
||||
void IArchiveExtractionListener.FireEntryExtractionEnd(IArchiveEntry entry) =>
|
||||
EntryExtractionEnd?.Invoke(this, new ArchiveExtractionEventArgs<IArchiveEntry>(entry));
|
||||
|
||||
private static Stream CheckStreams(Stream stream)
|
||||
{
|
||||
if (!stream.CanSeek || !stream.CanRead)
|
||||
@@ -99,38 +88,12 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
}
|
||||
}
|
||||
|
||||
void IArchiveExtractionListener.EnsureEntriesLoaded()
|
||||
private void EnsureEntriesLoaded()
|
||||
{
|
||||
_lazyEntries.EnsureFullyLoaded();
|
||||
_lazyVolumes.EnsureFullyLoaded();
|
||||
}
|
||||
|
||||
void IExtractionListener.FireCompressedBytesRead(
|
||||
long currentPartCompressedBytes,
|
||||
long compressedReadBytes
|
||||
) =>
|
||||
CompressedBytesRead?.Invoke(
|
||||
this,
|
||||
new CompressedBytesReadEventArgs(
|
||||
currentFilePartCompressedBytesRead: currentPartCompressedBytes,
|
||||
compressedBytesRead: compressedReadBytes
|
||||
)
|
||||
);
|
||||
|
||||
void IExtractionListener.FireFilePartExtractionBegin(
|
||||
string name,
|
||||
long size,
|
||||
long compressedSize
|
||||
) =>
|
||||
FilePartExtractionBegin?.Invoke(
|
||||
this,
|
||||
new FilePartExtractionBeginEventArgs(
|
||||
compressedSize: compressedSize,
|
||||
size: size,
|
||||
name: name
|
||||
)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Use this method to extract all entries in an archive in order.
|
||||
/// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be
|
||||
@@ -144,7 +107,13 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
/// <returns></returns>
|
||||
public IReader ExtractAllEntries()
|
||||
{
|
||||
((IArchiveExtractionListener)this).EnsureEntriesLoaded();
|
||||
if (!IsSolid && Type != ArchiveType.SevenZip)
|
||||
{
|
||||
throw new SharpCompressException(
|
||||
"ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)."
|
||||
);
|
||||
}
|
||||
EnsureEntriesLoaded();
|
||||
return CreateReaderForSolidExtraction();
|
||||
}
|
||||
|
||||
@@ -155,6 +124,11 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
/// </summary>
|
||||
public virtual bool IsSolid => false;
|
||||
|
||||
/// <summary>
|
||||
/// Archive is ENCRYPTED (this means the Archive has password-protected files).
|
||||
/// </summary>
|
||||
public virtual bool IsEncrypted => false;
|
||||
|
||||
/// <summary>
|
||||
/// The archive can find all the parts of the archive needed to fully extract the archive. This forces the parsing of the entire archive.
|
||||
/// </summary>
|
||||
@@ -162,7 +136,7 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
{
|
||||
get
|
||||
{
|
||||
((IArchiveExtractionListener)this).EnsureEntriesLoaded();
|
||||
EnsureEntriesLoaded();
|
||||
return Entries.All(x => x.IsComplete);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public static class ArchiveFactory
|
||||
/// <param name="options"></param>
|
||||
public static IArchive Open(string filePath, ReaderOptions? options = null)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty(nameof(filePath));
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return Open(new FileInfo(filePath), options);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public static class ArchiveFactory
|
||||
/// <param name="options"></param>
|
||||
public static IArchive Open(IEnumerable<FileInfo> fileInfos, ReaderOptions? options = null)
|
||||
{
|
||||
fileInfos.CheckNotNull(nameof(fileInfos));
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var filesArray = fileInfos.ToArray();
|
||||
if (filesArray.Length == 0)
|
||||
{
|
||||
@@ -81,7 +81,7 @@ public static class ArchiveFactory
|
||||
return Open(fileInfo, options);
|
||||
}
|
||||
|
||||
fileInfo.CheckNotNull(nameof(fileInfo));
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
options ??= new ReaderOptions { LeaveStreamOpen = false };
|
||||
|
||||
return FindFactory<IMultiArchiveFactory>(fileInfo).Open(filesArray, options);
|
||||
@@ -94,7 +94,7 @@ public static class ArchiveFactory
|
||||
/// <param name="options"></param>
|
||||
public static IArchive Open(IEnumerable<Stream> streams, ReaderOptions? options = null)
|
||||
{
|
||||
streams.CheckNotNull(nameof(streams));
|
||||
streams.NotNull(nameof(streams));
|
||||
var streamsArray = streams.ToArray();
|
||||
if (streamsArray.Length == 0)
|
||||
{
|
||||
@@ -107,7 +107,7 @@ public static class ArchiveFactory
|
||||
return Open(firstStream, options);
|
||||
}
|
||||
|
||||
firstStream.CheckNotNull(nameof(firstStream));
|
||||
firstStream.NotNull(nameof(firstStream));
|
||||
options ??= new ReaderOptions();
|
||||
|
||||
return FindFactory<IMultiArchiveFactory>(firstStream).Open(streamsArray, options);
|
||||
@@ -129,7 +129,7 @@ public static class ArchiveFactory
|
||||
private static T FindFactory<T>(FileInfo finfo)
|
||||
where T : IFactory
|
||||
{
|
||||
finfo.CheckNotNull(nameof(finfo));
|
||||
finfo.NotNull(nameof(finfo));
|
||||
using Stream stream = finfo.OpenRead();
|
||||
return FindFactory<T>(stream);
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public static class ArchiveFactory
|
||||
private static T FindFactory<T>(Stream stream)
|
||||
where T : IFactory
|
||||
{
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
stream.NotNull(nameof(stream));
|
||||
if (!stream.CanRead || !stream.CanSeek)
|
||||
{
|
||||
throw new ArgumentException("Stream should be readable and seekable");
|
||||
@@ -172,7 +172,7 @@ public static class ArchiveFactory
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty(nameof(filePath));
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream s = File.OpenRead(filePath);
|
||||
return IsArchive(s, out type, bufferSize);
|
||||
}
|
||||
@@ -184,7 +184,7 @@ public static class ArchiveFactory
|
||||
)
|
||||
{
|
||||
type = null;
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
stream.NotNull(nameof(stream));
|
||||
|
||||
if (!stream.CanRead || !stream.CanSeek)
|
||||
{
|
||||
@@ -215,7 +215,7 @@ public static class ArchiveFactory
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<string> GetFileParts(string part1)
|
||||
{
|
||||
part1.CheckNotNullOrEmpty(nameof(part1));
|
||||
part1.NotNullOrEmpty(nameof(part1));
|
||||
return GetFileParts(new FileInfo(part1)).Select(a => a.FullName);
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ public static class ArchiveFactory
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<FileInfo> GetFileParts(FileInfo part1)
|
||||
{
|
||||
part1.CheckNotNull(nameof(part1));
|
||||
part1.NotNull(nameof(part1));
|
||||
yield return part1;
|
||||
|
||||
foreach (var factory in Factory.Factories.OfType<IFactory>())
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.GZip;
|
||||
using SharpCompress.IO;
|
||||
@@ -21,7 +23,7 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static GZipArchive Open(string filePath, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty(nameof(filePath));
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
|
||||
}
|
||||
|
||||
@@ -32,7 +34,7 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static GZipArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
fileInfo.CheckNotNull(nameof(fileInfo));
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
return new GZipArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
@@ -52,7 +54,7 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.CheckNotNull(nameof(fileInfos));
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var files = fileInfos.ToArray();
|
||||
return new GZipArchive(
|
||||
new SourceStream(
|
||||
@@ -70,7 +72,7 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static GZipArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
streams.CheckNotNull(nameof(streams));
|
||||
streams.NotNull(nameof(streams));
|
||||
var strms = streams.ToArray();
|
||||
return new GZipArchive(
|
||||
new SourceStream(
|
||||
@@ -88,7 +90,7 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static GZipArchive Open(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
stream.NotNull(nameof(stream));
|
||||
|
||||
if (stream is not { CanSeek: true })
|
||||
{
|
||||
@@ -136,6 +138,16 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
SaveTo(stream, new WriterOptions(CompressionType.GZip));
|
||||
}
|
||||
|
||||
public Task SaveToAsync(string filePath, CancellationToken cancellationToken = default) =>
|
||||
SaveToAsync(new FileInfo(filePath), cancellationToken);
|
||||
|
||||
public async Task SaveToAsync(FileInfo fileInfo, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
|
||||
await SaveToAsync(stream, new WriterOptions(CompressionType.GZip), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static bool IsGZipFile(Stream stream)
|
||||
{
|
||||
// read the header on the first read
|
||||
@@ -173,6 +185,11 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
return new GZipWritableArchiveEntry(this, source, filePath, size, modified, closeStream);
|
||||
}
|
||||
|
||||
protected override GZipArchiveEntry CreateDirectoryEntry(
|
||||
string directoryPath,
|
||||
DateTime? modified
|
||||
) => throw new NotSupportedException("GZip archives do not support directory entries.");
|
||||
|
||||
protected override void SaveTo(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
@@ -196,6 +213,28 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task SaveToAsync(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
IEnumerable<GZipArchiveEntry> oldEntries,
|
||||
IEnumerable<GZipArchiveEntry> newEntries,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (Entries.Count > 1)
|
||||
{
|
||||
throw new InvalidFormatException("Only one entry is allowed in a GZip Archive");
|
||||
}
|
||||
using var writer = new GZipWriter(stream, new GZipWriterOptions(options));
|
||||
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
await writer
|
||||
.WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<GZipArchiveEntry> LoadEntries(IEnumerable<GZipVolume> volumes)
|
||||
{
|
||||
var stream = volumes.Single().Stream;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.GZip;
|
||||
|
||||
namespace SharpCompress.Archives.GZip;
|
||||
@@ -13,13 +15,20 @@ public class GZipArchiveEntry : GZipEntry, IArchiveEntry
|
||||
{
|
||||
//this is to reset the stream to be read multiple times
|
||||
var part = (GZipFilePart)Parts.Single();
|
||||
if (part.GetRawStream().Position != part.EntryStartPosition)
|
||||
var rawStream = part.GetRawStream();
|
||||
if (rawStream.CanSeek && rawStream.Position != part.EntryStartPosition)
|
||||
{
|
||||
part.GetRawStream().Position = part.EntryStartPosition;
|
||||
rawStream.Position = part.EntryStartPosition;
|
||||
}
|
||||
return Parts.Single().GetCompressedStream().NotNull();
|
||||
}
|
||||
|
||||
public virtual Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// GZip synchronous implementation is fast enough, just wrap it
|
||||
return Task.FromResult(OpenEntryStream());
|
||||
}
|
||||
|
||||
#region IArchiveEntry Members
|
||||
|
||||
public IArchive Archive { get; }
|
||||
|
||||
@@ -7,12 +7,6 @@ namespace SharpCompress.Archives;
|
||||
|
||||
public interface IArchive : IDisposable
|
||||
{
|
||||
event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>> EntryExtractionBegin;
|
||||
event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>> EntryExtractionEnd;
|
||||
|
||||
event EventHandler<CompressedBytesReadEventArgs> CompressedBytesRead;
|
||||
event EventHandler<FilePartExtractionBeginEventArgs> FilePartExtractionBegin;
|
||||
|
||||
IEnumerable<IArchiveEntry> Entries { get; }
|
||||
IEnumerable<IVolume> Volumes { get; }
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
@@ -11,6 +13,12 @@ public interface IArchiveEntry : IEntry
|
||||
/// </summary>
|
||||
Stream OpenEntryStream();
|
||||
|
||||
/// <summary>
|
||||
/// Opens the current entry as a stream that will decompress as it is read asynchronously.
|
||||
/// Read the entire stream or use SkipEntry on EntryStream.
|
||||
/// </summary>
|
||||
Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// The archive can find all the parts of the archive needed to extract this entry.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
|
||||
@@ -6,61 +9,153 @@ namespace SharpCompress.Archives;
|
||||
|
||||
public static class IArchiveEntryExtensions
|
||||
{
|
||||
public static void WriteTo(this IArchiveEntry archiveEntry, Stream streamToWriteTo)
|
||||
private const int BufferSize = 81920;
|
||||
|
||||
/// <param name="archiveEntry">The archive entry to extract.</param>
|
||||
extension(IArchiveEntry archiveEntry)
|
||||
{
|
||||
if (archiveEntry.IsDirectory)
|
||||
/// <summary>
|
||||
/// Extract entry to the specified stream.
|
||||
/// </summary>
|
||||
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public void WriteTo(Stream streamToWriteTo, IProgress<ProgressReport>? progress = null)
|
||||
{
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
if (archiveEntry.IsDirectory)
|
||||
{
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
}
|
||||
|
||||
using var entryStream = archiveEntry.OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
|
||||
sourceStream.CopyTo(streamToWriteTo, BufferSize);
|
||||
}
|
||||
|
||||
var streamListener = (IArchiveExtractionListener)archiveEntry.Archive;
|
||||
streamListener.EnsureEntriesLoaded();
|
||||
streamListener.FireEntryExtractionBegin(archiveEntry);
|
||||
streamListener.FireFilePartExtractionBegin(
|
||||
archiveEntry.Key ?? "Key",
|
||||
archiveEntry.Size,
|
||||
archiveEntry.CompressedSize
|
||||
);
|
||||
var entryStream = archiveEntry.OpenEntryStream();
|
||||
using (entryStream)
|
||||
/// <summary>
|
||||
/// Extract entry to the specified stream asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public async Task WriteToAsync(
|
||||
Stream streamToWriteTo,
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using Stream s = new ListeningStream(streamListener, entryStream);
|
||||
s.TransferTo(streamToWriteTo);
|
||||
if (archiveEntry.IsDirectory)
|
||||
{
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
}
|
||||
|
||||
using var entryStream = await archiveEntry.OpenEntryStreamAsync(cancellationToken);
|
||||
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
|
||||
await sourceStream
|
||||
.CopyToAsync(streamToWriteTo, BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
streamListener.FireEntryExtractionEnd(archiveEntry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory, retaining filename
|
||||
/// </summary>
|
||||
public static void WriteToDirectory(
|
||||
this IArchiveEntry entry,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToDirectory(
|
||||
entry,
|
||||
destinationDirectory,
|
||||
options,
|
||||
entry.WriteToFile
|
||||
);
|
||||
private static Stream WrapWithProgress(
|
||||
Stream source,
|
||||
IArchiveEntry entry,
|
||||
IProgress<ProgressReport>? progress
|
||||
)
|
||||
{
|
||||
if (progress is null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file
|
||||
/// </summary>
|
||||
public static void WriteToFile(
|
||||
this IArchiveEntry entry,
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options = null
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToFile(
|
||||
entry,
|
||||
destinationFileName,
|
||||
options,
|
||||
(x, fm) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
entry.WriteTo(fs);
|
||||
}
|
||||
var entryPath = entry.Key ?? string.Empty;
|
||||
var totalBytes = GetEntrySizeSafe(entry);
|
||||
return new ProgressReportingStream(
|
||||
source,
|
||||
progress,
|
||||
entryPath,
|
||||
totalBytes,
|
||||
leaveOpen: true
|
||||
);
|
||||
}
|
||||
|
||||
private static long? GetEntrySizeSafe(IArchiveEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
var size = entry.Size;
|
||||
return size >= 0 ? size : null;
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extension(IArchiveEntry entry)
|
||||
{
|
||||
/// <summary>
|
||||
/// Extract to specific directory, retaining filename
|
||||
/// </summary>
|
||||
public void WriteToDirectory(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToDirectory(
|
||||
entry,
|
||||
destinationDirectory,
|
||||
options,
|
||||
entry.WriteToFile
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory asynchronously, retaining filename
|
||||
/// </summary>
|
||||
public Task WriteToDirectoryAsync(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToDirectoryAsync(
|
||||
entry,
|
||||
destinationDirectory,
|
||||
options,
|
||||
entry.WriteToFileAsync,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file
|
||||
/// </summary>
|
||||
public void WriteToFile(string destinationFileName, ExtractionOptions? options = null) =>
|
||||
ExtractionMethods.WriteEntryToFile(
|
||||
entry,
|
||||
destinationFileName,
|
||||
options,
|
||||
(x, fm) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
entry.WriteTo(fs);
|
||||
}
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file asynchronously
|
||||
/// </summary>
|
||||
public Task WriteToFileAsync(
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToFileAsync(
|
||||
entry,
|
||||
destinationFileName,
|
||||
options,
|
||||
async (x, fm, ct) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
await entry.WriteToAsync(fs, null, ct).ConfigureAwait(false);
|
||||
},
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
@@ -10,78 +10,159 @@ namespace SharpCompress.Archives;
|
||||
|
||||
public static class IArchiveExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extract to specific directory, retaining filename
|
||||
/// </summary>
|
||||
public static void WriteToDirectory(
|
||||
this IArchive archive,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
)
|
||||
{
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
reader.WriteAllToDirectory(destinationDirectory, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the archive to the destination directory. Directories will be created as needed.
|
||||
/// </summary>
|
||||
/// <param name="archive">The archive to extract.</param>
|
||||
/// <param name="destination">The folder to extract into.</param>
|
||||
/// <param name="progressReport">Optional progress report callback.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token.</param>
|
||||
public static void ExtractToDirectory(
|
||||
this IArchive archive,
|
||||
string destination,
|
||||
Action<double>? progressReport = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
extension(IArchive archive)
|
||||
{
|
||||
// Prepare for progress reporting
|
||||
var totalBytes = archive.TotalUncompressSize;
|
||||
var bytesRead = 0L;
|
||||
|
||||
// Tracking for created directories.
|
||||
var seenDirectories = new HashSet<string>();
|
||||
|
||||
// Extract
|
||||
var entries = archive.ExtractAllEntries();
|
||||
while (entries.MoveToNextEntry())
|
||||
/// <summary>
|
||||
/// Extract to specific directory with progress reporting
|
||||
/// </summary>
|
||||
/// <param name="destinationDirectory">The folder to extract into.</param>
|
||||
/// <param name="options">Extraction options.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public void WriteToDirectory(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
IProgress<ProgressReport>? progress = null
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var entry = entries.Entry;
|
||||
if (entry.IsDirectory)
|
||||
// For solid archives (Rar, 7Zip), use the optimized reader-based approach
|
||||
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
|
||||
{
|
||||
var dirPath = Path.Combine(destination, entry.Key.NotNull("Entry Key is null"));
|
||||
if (
|
||||
Path.GetDirectoryName(dirPath + "/") is { } emptyDirectory
|
||||
&& seenDirectories.Add(dirPath)
|
||||
)
|
||||
{
|
||||
Directory.CreateDirectory(emptyDirectory);
|
||||
}
|
||||
continue;
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
reader.WriteAllToDirectory(destinationDirectory, options);
|
||||
}
|
||||
|
||||
// Create each directory if not already created
|
||||
var path = Path.Combine(destination, entry.Key.NotNull("Entry Key is null"));
|
||||
if (Path.GetDirectoryName(path) is { } directory)
|
||||
else
|
||||
{
|
||||
if (!Directory.Exists(directory) && !seenDirectories.Contains(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
seenDirectories.Add(directory);
|
||||
}
|
||||
// For non-solid archives, extract entries directly
|
||||
archive.WriteToDirectoryInternal(destinationDirectory, options, progress);
|
||||
}
|
||||
}
|
||||
|
||||
// Write file
|
||||
using var fs = File.OpenWrite(path);
|
||||
entries.WriteEntryTo(fs);
|
||||
private void WriteToDirectoryInternal(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
IProgress<ProgressReport>? progress
|
||||
)
|
||||
{
|
||||
// Prepare for progress reporting
|
||||
var totalBytes = archive.TotalUncompressSize;
|
||||
var bytesRead = 0L;
|
||||
|
||||
// Update progress
|
||||
bytesRead += entry.Size;
|
||||
progressReport?.Invoke(bytesRead / (double)totalBytes);
|
||||
// Tracking for created directories.
|
||||
var seenDirectories = new HashSet<string>();
|
||||
|
||||
// Extract
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
var dirPath = Path.Combine(
|
||||
destinationDirectory,
|
||||
entry.Key.NotNull("Entry Key is null")
|
||||
);
|
||||
if (
|
||||
Path.GetDirectoryName(dirPath + "/") is { } parentDirectory
|
||||
&& seenDirectories.Add(dirPath)
|
||||
)
|
||||
{
|
||||
Directory.CreateDirectory(parentDirectory);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the entry's WriteToDirectory method which respects ExtractionOptions
|
||||
entry.WriteToDirectory(destinationDirectory, options);
|
||||
|
||||
// Update progress
|
||||
bytesRead += entry.Size;
|
||||
progress?.Report(
|
||||
new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory asynchronously with progress reporting and cancellation support
|
||||
/// </summary>
|
||||
/// <param name="destinationDirectory">The folder to extract into.</param>
|
||||
/// <param name="options">Extraction options.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token.</param>
|
||||
public async Task WriteToDirectoryAsync(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// For solid archives (Rar, 7Zip), use the optimized reader-based approach
|
||||
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
|
||||
{
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
destinationDirectory,
|
||||
options,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For non-solid archives, extract entries directly
|
||||
await archive.WriteToDirectoryAsyncInternal(
|
||||
destinationDirectory,
|
||||
options,
|
||||
progress,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteToDirectoryAsyncInternal(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
IProgress<ProgressReport>? progress,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
// Prepare for progress reporting
|
||||
var totalBytes = archive.TotalUncompressSize;
|
||||
var bytesRead = 0L;
|
||||
|
||||
// Tracking for created directories.
|
||||
var seenDirectories = new HashSet<string>();
|
||||
|
||||
// Extract
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
var dirPath = Path.Combine(
|
||||
destinationDirectory,
|
||||
entry.Key.NotNull("Entry Key is null")
|
||||
);
|
||||
if (
|
||||
Path.GetDirectoryName(dirPath + "/") is { } parentDirectory
|
||||
&& seenDirectories.Add(dirPath)
|
||||
)
|
||||
{
|
||||
Directory.CreateDirectory(parentDirectory);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the entry's WriteToDirectoryAsync method which respects ExtractionOptions
|
||||
await entry
|
||||
.WriteToDirectoryAsync(destinationDirectory, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Update progress
|
||||
bytesRead += entry.Size;
|
||||
progress?.Report(
|
||||
new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
internal interface IArchiveExtractionListener : IExtractionListener
|
||||
{
|
||||
void EnsureEntriesLoaded();
|
||||
void FireEntryExtractionBegin(IArchiveEntry entry);
|
||||
void FireEntryExtractionEnd(IArchiveEntry entry);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -84,6 +84,8 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
|
||||
public override bool IsSolid => Volumes.First().IsSolidArchive;
|
||||
|
||||
public override bool IsEncrypted => Entries.First(x => !x.IsDirectory).IsEncrypted;
|
||||
|
||||
public virtual int MinVersion => Volumes.First().MinVersion;
|
||||
public virtual int MaxVersion => Volumes.First().MaxVersion;
|
||||
|
||||
@@ -95,7 +97,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
/// <param name="options"></param>
|
||||
public static RarArchive Open(string filePath, ReaderOptions? options = null)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty(nameof(filePath));
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
@@ -113,7 +115,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
/// <param name="options"></param>
|
||||
public static RarArchive Open(FileInfo fileInfo, ReaderOptions? options = null)
|
||||
{
|
||||
fileInfo.CheckNotNull(nameof(fileInfo));
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
@@ -130,7 +132,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
/// <param name="options"></param>
|
||||
public static RarArchive Open(Stream stream, ReaderOptions? options = null)
|
||||
{
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
stream.NotNull(nameof(stream));
|
||||
|
||||
if (stream is not { CanSeek: true })
|
||||
{
|
||||
@@ -150,7 +152,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.CheckNotNull(nameof(fileInfos));
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var files = fileInfos.ToArray();
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
@@ -168,7 +170,7 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static RarArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
streams.CheckNotNull(nameof(streams));
|
||||
streams.NotNull(nameof(streams));
|
||||
var strms = streams.ToArray();
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Rar;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
@@ -68,20 +70,50 @@ 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)
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream = new RarStream(
|
||||
archive.UnpackV2017.Value,
|
||||
FileHeader,
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
|
||||
);
|
||||
}
|
||||
|
||||
return new RarStream(
|
||||
archive.UnpackV2017.Value,
|
||||
FileHeader,
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
|
||||
);
|
||||
stream.Initialize();
|
||||
return stream;
|
||||
}
|
||||
|
||||
public async Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RarStream stream;
|
||||
if (IsRarV3)
|
||||
{
|
||||
stream = new RarStream(
|
||||
archive.UnpackV1.Value,
|
||||
FileHeader,
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream = new RarStream(
|
||||
archive.UnpackV2017.Value,
|
||||
FileHeader,
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
|
||||
);
|
||||
}
|
||||
|
||||
await stream.InitializeAsync(cancellationToken);
|
||||
return stream;
|
||||
}
|
||||
|
||||
public bool IsComplete
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.SevenZip;
|
||||
using SharpCompress.Compressors.LZMA.Utilites;
|
||||
@@ -21,7 +23,7 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
/// <param name="readerOptions"></param>
|
||||
public static SevenZipArchive Open(string filePath, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty("filePath");
|
||||
filePath.NotNullOrEmpty("filePath");
|
||||
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
|
||||
}
|
||||
|
||||
@@ -32,7 +34,7 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
/// <param name="readerOptions"></param>
|
||||
public static SevenZipArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
fileInfo.CheckNotNull("fileInfo");
|
||||
fileInfo.NotNull("fileInfo");
|
||||
return new SevenZipArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
@@ -52,7 +54,7 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.CheckNotNull(nameof(fileInfos));
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var files = fileInfos.ToArray();
|
||||
return new SevenZipArchive(
|
||||
new SourceStream(
|
||||
@@ -73,7 +75,7 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
streams.CheckNotNull(nameof(streams));
|
||||
streams.NotNull(nameof(streams));
|
||||
var strms = streams.ToArray();
|
||||
return new SevenZipArchive(
|
||||
new SourceStream(
|
||||
@@ -91,7 +93,7 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
/// <param name="readerOptions"></param>
|
||||
public static SevenZipArchive Open(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
stream.CheckNotNull("stream");
|
||||
stream.NotNull("stream");
|
||||
|
||||
if (stream is not { CanSeek: true })
|
||||
{
|
||||
@@ -205,15 +207,15 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
.GroupBy(x => x.FilePart.Folder)
|
||||
.Any(folder => folder.Count() > 1);
|
||||
|
||||
public override bool IsEncrypted => Entries.First(x => !x.IsDirectory).IsEncrypted;
|
||||
|
||||
public override long TotalSize =>
|
||||
_database?._packSizes.Aggregate(0L, (total, packSize) => total + packSize) ?? 0;
|
||||
|
||||
private sealed class SevenZipReader : AbstractReader<SevenZipEntry, SevenZipVolume>
|
||||
{
|
||||
private readonly SevenZipArchive _archive;
|
||||
private CFolder? _currentFolder;
|
||||
private Stream? _currentStream;
|
||||
private CFileItem? _currentItem;
|
||||
private SevenZipEntry? _currentEntry;
|
||||
|
||||
internal SevenZipReader(ReaderOptions readerOptions, SevenZipArchive archive)
|
||||
: base(readerOptions, ArchiveType.SevenZip) => this._archive = archive;
|
||||
@@ -226,40 +228,135 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
stream.Position = 0;
|
||||
foreach (var dir in entries.Where(x => x.IsDirectory))
|
||||
{
|
||||
_currentEntry = dir;
|
||||
yield return dir;
|
||||
}
|
||||
foreach (
|
||||
var group in entries.Where(x => !x.IsDirectory).GroupBy(x => x.FilePart.Folder)
|
||||
)
|
||||
// For non-directory entries, yield them without creating shared streams
|
||||
// Each call to GetEntryStream() will create a fresh decompression stream
|
||||
// to avoid state corruption issues with async operations
|
||||
foreach (var entry in entries.Where(x => !x.IsDirectory))
|
||||
{
|
||||
_currentFolder = group.Key;
|
||||
if (group.Key is null)
|
||||
{
|
||||
_currentStream = Stream.Null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentStream = _archive._database?.GetFolderStream(
|
||||
stream,
|
||||
_currentFolder,
|
||||
new PasswordProvider(Options.Password)
|
||||
);
|
||||
}
|
||||
foreach (var entry in group)
|
||||
{
|
||||
_currentItem = entry.FilePart.Header;
|
||||
yield return entry;
|
||||
}
|
||||
_currentEntry = entry;
|
||||
yield return entry;
|
||||
}
|
||||
}
|
||||
|
||||
protected override EntryStream GetEntryStream() =>
|
||||
CreateEntryStream(
|
||||
new ReadOnlySubStream(
|
||||
_currentStream.NotNull("currentStream is not null"),
|
||||
_currentItem?.Size ?? 0
|
||||
)
|
||||
);
|
||||
protected override EntryStream GetEntryStream()
|
||||
{
|
||||
// Create a fresh decompression stream for each file (no state sharing).
|
||||
// However, the LZMA decoder has bugs in its async implementation that cause
|
||||
// state corruption even on fresh streams. The SyncOnlyStream wrapper
|
||||
// works around these bugs by forcing async operations to use sync equivalents.
|
||||
//
|
||||
// TODO: Fix the LZMA decoder async bugs (in LzmaStream, Decoder, OutWindow)
|
||||
// so this wrapper is no longer necessary.
|
||||
var entry = _currentEntry.NotNull("currentEntry is not null");
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
return CreateEntryStream(Stream.Null);
|
||||
}
|
||||
return CreateEntryStream(new SyncOnlyStream(entry.FilePart.GetCompressedStream()));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WORKAROUND: Forces async operations to use synchronous equivalents.
|
||||
/// This is necessary because the LZMA decoder has bugs in its async implementation
|
||||
/// that cause state corruption (IndexOutOfRangeException, DataErrorException).
|
||||
///
|
||||
/// The proper fix would be to repair the LZMA decoder's async methods
|
||||
/// (LzmaStream.ReadAsync, Decoder.CodeAsync, OutWindow async operations),
|
||||
/// but that requires deep changes to the decoder state machine.
|
||||
/// </summary>
|
||||
private sealed class SyncOnlyStream : Stream
|
||||
{
|
||||
private readonly Stream _baseStream;
|
||||
|
||||
public SyncOnlyStream(Stream baseStream) => _baseStream = baseStream;
|
||||
|
||||
public override bool CanRead => _baseStream.CanRead;
|
||||
public override bool CanSeek => _baseStream.CanSeek;
|
||||
public override bool CanWrite => _baseStream.CanWrite;
|
||||
public override long Length => _baseStream.Length;
|
||||
public override long Position
|
||||
{
|
||||
get => _baseStream.Position;
|
||||
set => _baseStream.Position = value;
|
||||
}
|
||||
|
||||
public override void Flush() => _baseStream.Flush();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
_baseStream.Read(buffer, offset, count);
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) =>
|
||||
_baseStream.Seek(offset, origin);
|
||||
|
||||
public override void SetLength(long value) => _baseStream.SetLength(value);
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
_baseStream.Write(buffer, offset, count);
|
||||
|
||||
// Force async operations to use sync equivalents to avoid LZMA decoder bugs
|
||||
public override Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(_baseStream.Read(buffer, offset, count));
|
||||
}
|
||||
|
||||
public override Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_baseStream.Write(buffer, offset, count);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_baseStream.Flush();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new ValueTask<int>(_baseStream.Read(buffer.Span));
|
||||
}
|
||||
|
||||
public override ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_baseStream.Write(buffer.Span);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_baseStream.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
private class PasswordProvider : IPasswordProvider
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.SevenZip;
|
||||
|
||||
namespace SharpCompress.Archives.SevenZip;
|
||||
@@ -10,6 +12,9 @@ public class SevenZipArchiveEntry : SevenZipEntry, IArchiveEntry
|
||||
|
||||
public Stream OpenEntryStream() => FilePart.GetCompressedStream();
|
||||
|
||||
public Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(OpenEntryStream());
|
||||
|
||||
public IArchive Archive { get; }
|
||||
|
||||
public bool IsComplete => true;
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Tar;
|
||||
using SharpCompress.Common.Tar.Headers;
|
||||
@@ -22,7 +24,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static TarArchive Open(string filePath, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty(nameof(filePath));
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
|
||||
}
|
||||
|
||||
@@ -33,7 +35,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static TarArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
fileInfo.CheckNotNull(nameof(fileInfo));
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
return new TarArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
@@ -53,7 +55,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.CheckNotNull(nameof(fileInfos));
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var files = fileInfos.ToArray();
|
||||
return new TarArchive(
|
||||
new SourceStream(
|
||||
@@ -71,7 +73,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static TarArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
streams.CheckNotNull(nameof(streams));
|
||||
streams.NotNull(nameof(streams));
|
||||
var strms = streams.ToArray();
|
||||
return new TarArchive(
|
||||
new SourceStream(
|
||||
@@ -89,7 +91,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static TarArchive Open(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
stream.NotNull(nameof(stream));
|
||||
|
||||
if (stream is not { CanSeek: true })
|
||||
{
|
||||
@@ -178,7 +180,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
using (var entryStream = entry.OpenEntryStream())
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
entryStream.TransferTo(memoryStream);
|
||||
entryStream.CopyTo(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
var bytes = memoryStream.ToArray();
|
||||
|
||||
@@ -222,6 +224,11 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
closeStream
|
||||
);
|
||||
|
||||
protected override TarArchiveEntry CreateDirectoryEntry(
|
||||
string directoryPath,
|
||||
DateTime? modified
|
||||
) => new TarWritableArchiveEntry(this, directoryPath, modified);
|
||||
|
||||
protected override void SaveTo(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
@@ -230,15 +237,62 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
)
|
||||
{
|
||||
using var writer = new TarWriter(stream, new TarWriterOptions(options));
|
||||
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
|
||||
foreach (var entry in oldEntries.Concat(newEntries))
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
writer.Write(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
entry.LastModifiedTime,
|
||||
entry.Size
|
||||
);
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
writer.WriteDirectory(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entry.LastModifiedTime
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
writer.Write(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
entry.LastModifiedTime,
|
||||
entry.Size
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task SaveToAsync(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
IEnumerable<TarArchiveEntry> oldEntries,
|
||||
IEnumerable<TarArchiveEntry> newEntries,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var writer = new TarWriter(stream, new TarWriterOptions(options));
|
||||
foreach (var entry in oldEntries.Concat(newEntries))
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
await writer
|
||||
.WriteDirectoryAsync(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entry.LastModifiedTime,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
await writer
|
||||
.WriteAsync(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
entry.LastModifiedTime,
|
||||
entry.Size,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Tar;
|
||||
|
||||
@@ -12,6 +14,10 @@ public class TarArchiveEntry : TarEntry, IArchiveEntry
|
||||
|
||||
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
|
||||
|
||||
public virtual Task<Stream> OpenEntryStreamAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
) => Task.FromResult(OpenEntryStream());
|
||||
|
||||
#region IArchiveEntry Members
|
||||
|
||||
public IArchive Archive { get; }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Zip;
|
||||
using SharpCompress.Common.Zip.Headers;
|
||||
@@ -43,7 +45,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static ZipArchive Open(string filePath, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty(nameof(filePath));
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
|
||||
}
|
||||
|
||||
@@ -54,7 +56,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static ZipArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
fileInfo.CheckNotNull(nameof(fileInfo));
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
return new ZipArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
@@ -74,7 +76,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.CheckNotNull(nameof(fileInfos));
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var files = fileInfos.ToArray();
|
||||
return new ZipArchive(
|
||||
new SourceStream(
|
||||
@@ -92,7 +94,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static ZipArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
streams.CheckNotNull(nameof(streams));
|
||||
streams.NotNull(nameof(streams));
|
||||
var strms = streams.ToArray();
|
||||
return new ZipArchive(
|
||||
new SourceStream(
|
||||
@@ -110,7 +112,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static ZipArchive Open(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
stream.NotNull(nameof(stream));
|
||||
|
||||
if (stream is not { CanSeek: true })
|
||||
{
|
||||
@@ -306,14 +308,59 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
)
|
||||
{
|
||||
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
|
||||
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
|
||||
foreach (var entry in oldEntries.Concat(newEntries))
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
writer.Write(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
entry.LastModifiedTime
|
||||
);
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
writer.WriteDirectory(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entry.LastModifiedTime
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
writer.Write(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
entry.LastModifiedTime
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task SaveToAsync(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
IEnumerable<ZipArchiveEntry> oldEntries,
|
||||
IEnumerable<ZipArchiveEntry> newEntries,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
|
||||
foreach (var entry in oldEntries.Concat(newEntries))
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
await writer
|
||||
.WriteDirectoryAsync(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entry.LastModifiedTime,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
await writer
|
||||
.WriteAsync(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +372,11 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
bool closeStream
|
||||
) => new ZipWritableArchiveEntry(this, source, filePath, size, modified, closeStream);
|
||||
|
||||
protected override ZipArchiveEntry CreateDirectoryEntry(
|
||||
string directoryPath,
|
||||
DateTime? modified
|
||||
) => new ZipWritableArchiveEntry(this, directoryPath, modified);
|
||||
|
||||
public static ZipArchive Create() => new();
|
||||
|
||||
protected override IReader CreateReaderForSolidExtraction()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Zip;
|
||||
|
||||
namespace SharpCompress.Archives.Zip;
|
||||
@@ -11,6 +13,10 @@ public class ZipArchiveEntry : ZipEntry, IArchiveEntry
|
||||
|
||||
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
|
||||
|
||||
public virtual Task<Stream> OpenEntryStreamAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
) => Task.FromResult(OpenEntryStream());
|
||||
|
||||
#region IArchiveEntry Members
|
||||
|
||||
public IArchive Archive { get; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// CLSCompliant(false) is required because ZStandard integration uses unsafe code
|
||||
[assembly: CLSCompliant(false)]
|
||||
[assembly: InternalsVisibleTo(
|
||||
"SharpCompress.Test,PublicKey=0024000004800000940000000602000000240000525341310004000001000100158bebf1433f76dffc356733c138babea7a47536c65ed8009b16372c6f4edbb20554db74a62687f56b97c20a6ce8c4b123280279e33c894e7b3aa93ab3c573656fde4db576cfe07dba09619ead26375b25d2c4a8e43f7be257d712b0dd2eb546f67adb09281338618a58ac834fc038dd7e2740a7ab3591826252e4f4516306dc"
|
||||
|
||||
61
src/SharpCompress/Common/Ace/AceCrc.cs
Normal file
61
src/SharpCompress/Common/Ace/AceCrc.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.Common.Ace
|
||||
{
|
||||
public class AceCrc
|
||||
{
|
||||
// CRC-32 lookup table (standard polynomial 0xEDB88320, reflected)
|
||||
private static readonly uint[] Crc32Table = GenerateTable();
|
||||
|
||||
private static uint[] GenerateTable()
|
||||
{
|
||||
var table = new uint[256];
|
||||
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
uint crc = (uint)i;
|
||||
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
if ((crc & 1) != 0)
|
||||
crc = (crc >> 1) ^ 0xEDB88320u;
|
||||
else
|
||||
crc >>= 1;
|
||||
}
|
||||
|
||||
table[i] = crc;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate ACE CRC-32 checksum.
|
||||
/// ACE CRC-32 uses standard CRC-32 polynomial (0xEDB88320, reflected)
|
||||
/// with init=0xFFFFFFFF but NO final XOR.
|
||||
/// </summary>
|
||||
public static uint AceCrc32(ReadOnlySpan<byte> data)
|
||||
{
|
||||
uint crc = 0xFFFFFFFFu;
|
||||
|
||||
foreach (byte b in data)
|
||||
{
|
||||
crc = (crc >> 8) ^ Crc32Table[(crc ^ b) & 0xFF];
|
||||
}
|
||||
|
||||
return crc; // No final XOR for ACE
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ACE CRC-16 is the lower 16 bits of the ACE CRC-32.
|
||||
/// </summary>
|
||||
public static ushort AceCrc16(ReadOnlySpan<byte> data)
|
||||
{
|
||||
return (ushort)(AceCrc32(data) & 0xFFFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/SharpCompress/Common/Ace/AceEntry.cs
Normal file
68
src/SharpCompress/Common/Ace/AceEntry.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Ace.Headers;
|
||||
|
||||
namespace SharpCompress.Common.Ace
|
||||
{
|
||||
public class AceEntry : Entry
|
||||
{
|
||||
private readonly AceFilePart _filePart;
|
||||
|
||||
internal AceEntry(AceFilePart filePart)
|
||||
{
|
||||
_filePart = filePart;
|
||||
}
|
||||
|
||||
public override long Crc
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_filePart == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return _filePart.Header.Crc32;
|
||||
}
|
||||
}
|
||||
|
||||
public override string? Key => _filePart?.Header.Filename;
|
||||
|
||||
public override string? LinkTarget => null;
|
||||
|
||||
public override long CompressedSize => _filePart?.Header.PackedSize ?? 0;
|
||||
|
||||
public override CompressionType CompressionType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_filePart.Header.CompressionType == Headers.CompressionType.Stored)
|
||||
{
|
||||
return CompressionType.None;
|
||||
}
|
||||
return CompressionType.AceLZ77;
|
||||
}
|
||||
}
|
||||
|
||||
public override long Size => _filePart?.Header.OriginalSize ?? 0;
|
||||
|
||||
public override DateTime? LastModifiedTime => _filePart.Header.DateTime;
|
||||
|
||||
public override DateTime? CreatedTime => null;
|
||||
|
||||
public override DateTime? LastAccessedTime => null;
|
||||
|
||||
public override DateTime? ArchivedTime => null;
|
||||
|
||||
public override bool IsEncrypted => _filePart.Header.IsFileEncrypted;
|
||||
|
||||
public override bool IsDirectory => _filePart.Header.IsDirectory;
|
||||
|
||||
public override bool IsSplitAfter => false;
|
||||
|
||||
internal override IEnumerable<FilePart> Parts => _filePart.Empty();
|
||||
}
|
||||
}
|
||||
52
src/SharpCompress/Common/Ace/AceFilePart.cs
Normal file
52
src/SharpCompress/Common/Ace/AceFilePart.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Ace.Headers;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Common.Ace
|
||||
{
|
||||
public class AceFilePart : FilePart
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
internal AceFileHeader Header { get; set; }
|
||||
|
||||
internal AceFilePart(AceFileHeader localAceHeader, Stream seekableStream)
|
||||
: base(localAceHeader.ArchiveEncoding)
|
||||
{
|
||||
_stream = seekableStream;
|
||||
Header = localAceHeader;
|
||||
}
|
||||
|
||||
internal override string? FilePartName => Header.Filename;
|
||||
|
||||
internal override Stream GetCompressedStream()
|
||||
{
|
||||
if (_stream != null)
|
||||
{
|
||||
Stream compressedStream;
|
||||
switch (Header.CompressionType)
|
||||
{
|
||||
case Headers.CompressionType.Stored:
|
||||
compressedStream = new ReadOnlySubStream(
|
||||
_stream,
|
||||
Header.DataStartPosition,
|
||||
Header.PackedSize
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(
|
||||
"CompressionMethod: " + Header.CompressionQuality
|
||||
);
|
||||
}
|
||||
return compressedStream;
|
||||
}
|
||||
return _stream.NotNull();
|
||||
}
|
||||
|
||||
internal override Stream? GetRawStream() => _stream;
|
||||
}
|
||||
}
|
||||
35
src/SharpCompress/Common/Ace/AceVolume.cs
Normal file
35
src/SharpCompress/Common/Ace/AceVolume.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Arj;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Common.Ace
|
||||
{
|
||||
public class AceVolume : Volume
|
||||
{
|
||||
public AceVolume(Stream stream, ReaderOptions readerOptions, int index = 0)
|
||||
: base(stream, readerOptions, index) { }
|
||||
|
||||
public override bool IsFirstVolume
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ArjArchive is part of a multi-part archive.
|
||||
/// </summary>
|
||||
public override bool IsMultiVolume
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
internal IEnumerable<AceFilePart> GetVolumeFileParts()
|
||||
{
|
||||
return new List<AceFilePart>();
|
||||
}
|
||||
}
|
||||
}
|
||||
171
src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs
Normal file
171
src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Xml.Linq;
|
||||
using SharpCompress.Common.Arc;
|
||||
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// ACE file entry header
|
||||
/// </summary>
|
||||
public sealed class AceFileHeader : AceHeader
|
||||
{
|
||||
public long DataStartPosition { get; private set; }
|
||||
public long PackedSize { get; set; }
|
||||
public long OriginalSize { get; set; }
|
||||
public DateTime DateTime { get; set; }
|
||||
public int Attributes { get; set; }
|
||||
public uint Crc32 { get; set; }
|
||||
public CompressionType CompressionType { get; set; }
|
||||
public CompressionQuality CompressionQuality { get; set; }
|
||||
public ushort Parameters { get; set; }
|
||||
public string Filename { get; set; } = string.Empty;
|
||||
public List<byte> Comment { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// File data offset in the archive
|
||||
/// </summary>
|
||||
public ulong DataOffset { get; set; }
|
||||
|
||||
public bool IsDirectory => (Attributes & 0x10) != 0;
|
||||
|
||||
public bool IsContinuedFromPrev =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.CONTINUED_PREV) != 0;
|
||||
|
||||
public bool IsContinuedToNext =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.CONTINUED_NEXT) != 0;
|
||||
|
||||
public int DictionarySize
|
||||
{
|
||||
get
|
||||
{
|
||||
int bits = Parameters & 0x0F;
|
||||
return bits < 10 ? 1024 : 1 << bits;
|
||||
}
|
||||
}
|
||||
|
||||
public AceFileHeader(ArchiveEncoding archiveEncoding)
|
||||
: base(archiveEncoding, AceHeaderType.FILE) { }
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next file entry header from the stream.
|
||||
/// Returns null if no more entries or end of archive.
|
||||
/// Supports both ACE 1.0 and ACE 2.0 formats.
|
||||
/// </summary>
|
||||
public override AceHeader? Read(Stream stream)
|
||||
{
|
||||
var headerData = ReadHeader(stream);
|
||||
if (headerData.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
int offset = 0;
|
||||
|
||||
// Header type (1 byte)
|
||||
HeaderType = headerData[offset++];
|
||||
|
||||
// Skip recovery record headers (ACE 2.0 feature)
|
||||
if (HeaderType == (byte)SharpCompress.Common.Ace.Headers.AceHeaderType.RECOVERY32)
|
||||
{
|
||||
// Skip to next header
|
||||
return null;
|
||||
}
|
||||
|
||||
if (HeaderType != (byte)SharpCompress.Common.Ace.Headers.AceHeaderType.FILE)
|
||||
{
|
||||
// Unknown header type - skip
|
||||
return null;
|
||||
}
|
||||
|
||||
// Header flags (2 bytes)
|
||||
HeaderFlags = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2;
|
||||
|
||||
// Packed size (4 bytes)
|
||||
PackedSize = BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// Original size (4 bytes)
|
||||
OriginalSize = BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// File date/time in DOS format (4 bytes)
|
||||
var dosDateTime = BitConverter.ToUInt32(headerData, offset);
|
||||
DateTime = ConvertDosDateTime(dosDateTime);
|
||||
offset += 4;
|
||||
|
||||
// File attributes (4 bytes)
|
||||
Attributes = (int)BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// CRC32 (4 bytes)
|
||||
Crc32 = BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// Compression type (1 byte)
|
||||
byte compressionType = headerData[offset++];
|
||||
CompressionType = GetCompressionType(compressionType);
|
||||
|
||||
// Compression quality/parameter (1 byte)
|
||||
byte compressionQuality = headerData[offset++];
|
||||
CompressionQuality = GetCompressionQuality(compressionQuality);
|
||||
|
||||
// Parameters (2 bytes)
|
||||
Parameters = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2;
|
||||
|
||||
// Reserved (2 bytes) - skip
|
||||
offset += 2;
|
||||
|
||||
// Filename length (2 bytes)
|
||||
var filenameLength = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2;
|
||||
|
||||
// Filename
|
||||
if (offset + filenameLength <= headerData.Length)
|
||||
{
|
||||
Filename = ArchiveEncoding.Decode(headerData, offset, filenameLength);
|
||||
offset += filenameLength;
|
||||
}
|
||||
|
||||
// Handle comment if present
|
||||
if ((HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.COMMENT) != 0)
|
||||
{
|
||||
// Comment length (2 bytes)
|
||||
if (offset + 2 <= headerData.Length)
|
||||
{
|
||||
ushort commentLength = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2 + commentLength; // Skip comment
|
||||
}
|
||||
}
|
||||
|
||||
// Store the data start position
|
||||
DataStartPosition = stream.Position;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public CompressionType GetCompressionType(byte value) =>
|
||||
value switch
|
||||
{
|
||||
0 => CompressionType.Stored,
|
||||
1 => CompressionType.Lz77,
|
||||
2 => CompressionType.Blocked,
|
||||
_ => CompressionType.Unknown,
|
||||
};
|
||||
|
||||
public CompressionQuality GetCompressionQuality(byte value) =>
|
||||
value switch
|
||||
{
|
||||
0 => CompressionQuality.None,
|
||||
1 => CompressionQuality.Fastest,
|
||||
2 => CompressionQuality.Fast,
|
||||
3 => CompressionQuality.Normal,
|
||||
4 => CompressionQuality.Good,
|
||||
5 => CompressionQuality.Best,
|
||||
_ => CompressionQuality.Unknown,
|
||||
};
|
||||
}
|
||||
}
|
||||
153
src/SharpCompress/Common/Ace/Headers/AceHeader.cs
Normal file
153
src/SharpCompress/Common/Ace/Headers/AceHeader.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Common.Arj.Headers;
|
||||
using SharpCompress.Crypto;
|
||||
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Header type constants
|
||||
/// </summary>
|
||||
public enum AceHeaderType
|
||||
{
|
||||
MAIN = 0,
|
||||
FILE = 1,
|
||||
RECOVERY32 = 2,
|
||||
RECOVERY64A = 3,
|
||||
RECOVERY64B = 4,
|
||||
}
|
||||
|
||||
public abstract class AceHeader
|
||||
{
|
||||
// ACE signature: bytes at offset 7 should be "**ACE**"
|
||||
private static readonly byte[] AceSignature =
|
||||
[
|
||||
(byte)'*',
|
||||
(byte)'*',
|
||||
(byte)'A',
|
||||
(byte)'C',
|
||||
(byte)'E',
|
||||
(byte)'*',
|
||||
(byte)'*',
|
||||
];
|
||||
|
||||
public AceHeader(ArchiveEncoding archiveEncoding, AceHeaderType type)
|
||||
{
|
||||
AceHeaderType = type;
|
||||
ArchiveEncoding = archiveEncoding;
|
||||
}
|
||||
|
||||
public ArchiveEncoding ArchiveEncoding { get; }
|
||||
public AceHeaderType AceHeaderType { get; }
|
||||
|
||||
public ushort HeaderFlags { get; set; }
|
||||
public ushort HeaderCrc { get; set; }
|
||||
public ushort HeaderSize { get; set; }
|
||||
public byte HeaderType { get; set; }
|
||||
|
||||
public bool IsFileEncrypted =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.FILE_ENCRYPTED) != 0;
|
||||
public bool Is64Bit =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.MEMORY_64BIT) != 0;
|
||||
|
||||
public bool IsSolid =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.SOLID_MAIN) != 0;
|
||||
|
||||
public bool IsMultiVolume =>
|
||||
(HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.MULTIVOLUME) != 0;
|
||||
|
||||
public abstract AceHeader? Read(Stream reader);
|
||||
|
||||
public byte[] ReadHeader(Stream stream)
|
||||
{
|
||||
// Read header CRC (2 bytes) and header size (2 bytes)
|
||||
var headerBytes = new byte[4];
|
||||
if (stream.Read(headerBytes, 0, 4) != 4)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
HeaderCrc = BitConverter.ToUInt16(headerBytes, 0); // CRC for validation
|
||||
HeaderSize = BitConverter.ToUInt16(headerBytes, 2);
|
||||
if (HeaderSize == 0)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
// Read the header data
|
||||
var body = new byte[HeaderSize];
|
||||
if (stream.Read(body, 0, HeaderSize) != HeaderSize)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
// Verify crc
|
||||
var checksum = AceCrc.AceCrc16(body);
|
||||
if (checksum != HeaderCrc)
|
||||
{
|
||||
throw new InvalidDataException("Header checksum is invalid");
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
public static bool IsArchive(Stream stream)
|
||||
{
|
||||
// ACE files have a specific signature
|
||||
// First two bytes are typically 0x60 0xEA (signature bytes)
|
||||
// At offset 7, there should be "**ACE**" (7 bytes)
|
||||
var bytes = new byte[14];
|
||||
if (stream.Read(bytes, 0, 14) != 14)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for "**ACE**" at offset 7
|
||||
return CheckMagicBytes(bytes, 7);
|
||||
}
|
||||
|
||||
protected static bool CheckMagicBytes(byte[] headerBytes, int offset)
|
||||
{
|
||||
// Check for "**ACE**" at specified offset
|
||||
for (int i = 0; i < AceSignature.Length; i++)
|
||||
{
|
||||
if (headerBytes[offset + i] != AceSignature[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected DateTime ConvertDosDateTime(uint dosDateTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
int second = (int)(dosDateTime & 0x1F) * 2;
|
||||
int minute = (int)((dosDateTime >> 5) & 0x3F);
|
||||
int hour = (int)((dosDateTime >> 11) & 0x1F);
|
||||
int day = (int)((dosDateTime >> 16) & 0x1F);
|
||||
int month = (int)((dosDateTime >> 21) & 0x0F);
|
||||
int year = (int)((dosDateTime >> 25) & 0x7F) + 1980;
|
||||
|
||||
if (
|
||||
day < 1
|
||||
|| day > 31
|
||||
|| month < 1
|
||||
|| month > 12
|
||||
|| hour > 23
|
||||
|| minute > 59
|
||||
|| second > 59
|
||||
)
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
|
||||
return new DateTime(year, month, day, hour, minute, second);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs
Normal file
97
src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SharpCompress.Common.Ace.Headers;
|
||||
using SharpCompress.Common.Zip.Headers;
|
||||
using SharpCompress.Crypto;
|
||||
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// ACE main archive header
|
||||
/// </summary>
|
||||
public sealed class AceMainHeader : AceHeader
|
||||
{
|
||||
public byte ExtractVersion { get; set; }
|
||||
public byte CreatorVersion { get; set; }
|
||||
public HostOS HostOS { get; set; }
|
||||
public byte VolumeNumber { get; set; }
|
||||
public DateTime DateTime { get; set; }
|
||||
public string Advert { get; set; } = string.Empty;
|
||||
public List<byte> Comment { get; set; } = new();
|
||||
public byte AceVersion { get; private set; }
|
||||
|
||||
public AceMainHeader(ArchiveEncoding archiveEncoding)
|
||||
: base(archiveEncoding, AceHeaderType.MAIN) { }
|
||||
|
||||
/// <summary>
|
||||
/// Reads the main archive header from the stream.
|
||||
/// Returns header if this is a valid ACE archive.
|
||||
/// Supports both ACE 1.0 and ACE 2.0 formats.
|
||||
/// </summary>
|
||||
public override AceHeader? Read(Stream stream)
|
||||
{
|
||||
var headerData = ReadHeader(stream);
|
||||
if (headerData.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
int offset = 0;
|
||||
|
||||
// Header type should be 0 for main header
|
||||
if (headerData[offset++] != HeaderType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Header flags (2 bytes)
|
||||
HeaderFlags = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2;
|
||||
|
||||
// Skip signature "**ACE**" (7 bytes)
|
||||
if (!CheckMagicBytes(headerData, offset))
|
||||
{
|
||||
throw new InvalidDataException("Invalid ACE archive signature.");
|
||||
}
|
||||
offset += 7;
|
||||
|
||||
// ACE version (1 byte) - 10 for ACE 1.0, 20 for ACE 2.0
|
||||
AceVersion = headerData[offset++];
|
||||
ExtractVersion = headerData[offset++];
|
||||
|
||||
// Host OS (1 byte)
|
||||
if (offset < headerData.Length)
|
||||
{
|
||||
var hostOsByte = headerData[offset++];
|
||||
HostOS = hostOsByte <= 11 ? (HostOS)hostOsByte : HostOS.Unknown;
|
||||
}
|
||||
// Volume number (1 byte)
|
||||
VolumeNumber = headerData[offset++];
|
||||
|
||||
// Creation date/time (4 bytes)
|
||||
var dosDateTime = BitConverter.ToUInt32(headerData, offset);
|
||||
DateTime = ConvertDosDateTime(dosDateTime);
|
||||
offset += 4;
|
||||
|
||||
// Reserved fields (8 bytes)
|
||||
if (offset + 8 <= headerData.Length)
|
||||
{
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
// Skip additional fields based on flags
|
||||
// Handle comment if present
|
||||
if ((HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.COMMENT) != 0)
|
||||
{
|
||||
if (offset + 2 <= headerData.Length)
|
||||
{
|
||||
ushort commentLength = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2 + commentLength;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/SharpCompress/Common/Ace/Headers/CompressionQuality.cs
Normal file
16
src/SharpCompress/Common/Ace/Headers/CompressionQuality.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Compression quality
|
||||
/// </summary>
|
||||
public enum CompressionQuality
|
||||
{
|
||||
None,
|
||||
Fastest,
|
||||
Fast,
|
||||
Normal,
|
||||
Good,
|
||||
Best,
|
||||
Unknown,
|
||||
}
|
||||
}
|
||||
13
src/SharpCompress/Common/Ace/Headers/CompressionType.cs
Normal file
13
src/SharpCompress/Common/Ace/Headers/CompressionType.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Compression types
|
||||
/// </summary>
|
||||
public enum CompressionType
|
||||
{
|
||||
Stored,
|
||||
Lz77,
|
||||
Blocked,
|
||||
Unknown,
|
||||
}
|
||||
}
|
||||
33
src/SharpCompress/Common/Ace/Headers/HeaderFlags.cs
Normal file
33
src/SharpCompress/Common/Ace/Headers/HeaderFlags.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Header flags (main + file, overlapping meanings)
|
||||
/// </summary>
|
||||
public static class HeaderFlags
|
||||
{
|
||||
// Shared / low bits
|
||||
public const ushort ADDSIZE = 0x0001; // extra size field present
|
||||
public const ushort COMMENT = 0x0002; // comment present
|
||||
public const ushort MEMORY_64BIT = 0x0004;
|
||||
public const ushort AV_STRING = 0x0008; // AV string present
|
||||
public const ushort SOLID = 0x0010; // solid file
|
||||
public const ushort LOCKED = 0x0020;
|
||||
public const ushort PROTECTED = 0x0040;
|
||||
|
||||
// Main header specific
|
||||
public const ushort V20FORMAT = 0x0100;
|
||||
public const ushort SFX = 0x0200;
|
||||
public const ushort LIMITSFXJR = 0x0400;
|
||||
public const ushort MULTIVOLUME = 0x0800;
|
||||
public const ushort ADVERT = 0x1000;
|
||||
public const ushort RECOVERY = 0x2000;
|
||||
public const ushort LOCKED_MAIN = 0x4000;
|
||||
public const ushort SOLID_MAIN = 0x8000;
|
||||
|
||||
// File header specific (same bits, different meaning)
|
||||
public const ushort NTSECURITY = 0x0400;
|
||||
public const ushort CONTINUED_PREV = 0x1000;
|
||||
public const ushort CONTINUED_NEXT = 0x2000;
|
||||
public const ushort FILE_ENCRYPTED = 0x4000; // file encrypted (file header)
|
||||
}
|
||||
}
|
||||
22
src/SharpCompress/Common/Ace/Headers/HostOS.cs
Normal file
22
src/SharpCompress/Common/Ace/Headers/HostOS.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace SharpCompress.Common.Ace.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Host OS type
|
||||
/// </summary>
|
||||
public enum HostOS
|
||||
{
|
||||
MsDos = 0,
|
||||
Os2,
|
||||
Windows,
|
||||
Unix,
|
||||
MacOs,
|
||||
WinNt,
|
||||
Primos,
|
||||
AppleGs,
|
||||
Atari,
|
||||
Vax,
|
||||
Amiga,
|
||||
Next,
|
||||
Unknown,
|
||||
}
|
||||
}
|
||||
@@ -7,53 +7,54 @@ using System.Threading.Tasks;
|
||||
using SharpCompress.Common.GZip;
|
||||
using SharpCompress.Common.Tar;
|
||||
|
||||
namespace SharpCompress.Common.Arc;
|
||||
|
||||
public class ArcEntry : Entry
|
||||
namespace SharpCompress.Common.Arc
|
||||
{
|
||||
private readonly ArcFilePart? _filePart;
|
||||
|
||||
internal ArcEntry(ArcFilePart? filePart)
|
||||
public class ArcEntry : Entry
|
||||
{
|
||||
_filePart = filePart;
|
||||
}
|
||||
private readonly ArcFilePart? _filePart;
|
||||
|
||||
public override long Crc
|
||||
{
|
||||
get
|
||||
internal ArcEntry(ArcFilePart? filePart)
|
||||
{
|
||||
if (_filePart == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return _filePart.Header.Crc16;
|
||||
_filePart = filePart;
|
||||
}
|
||||
|
||||
public override long Crc
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_filePart == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return _filePart.Header.Crc16;
|
||||
}
|
||||
}
|
||||
|
||||
public override string? Key => _filePart?.Header.Name;
|
||||
|
||||
public override string? LinkTarget => null;
|
||||
|
||||
public override long CompressedSize => _filePart?.Header.CompressedSize ?? 0;
|
||||
|
||||
public override CompressionType CompressionType =>
|
||||
_filePart?.Header.CompressionMethod ?? CompressionType.Unknown;
|
||||
|
||||
public override long Size => throw new NotImplementedException();
|
||||
|
||||
public override DateTime? LastModifiedTime => null;
|
||||
|
||||
public override DateTime? CreatedTime => null;
|
||||
|
||||
public override DateTime? LastAccessedTime => null;
|
||||
|
||||
public override DateTime? ArchivedTime => null;
|
||||
|
||||
public override bool IsEncrypted => false;
|
||||
|
||||
public override bool IsDirectory => false;
|
||||
|
||||
public override bool IsSplitAfter => false;
|
||||
|
||||
internal override IEnumerable<FilePart> Parts => _filePart.Empty();
|
||||
}
|
||||
|
||||
public override string? Key => _filePart?.Header.Name;
|
||||
|
||||
public override string? LinkTarget => null;
|
||||
|
||||
public override long CompressedSize => _filePart?.Header.CompressedSize ?? 0;
|
||||
|
||||
public override CompressionType CompressionType =>
|
||||
_filePart?.Header.CompressionMethod ?? CompressionType.Unknown;
|
||||
|
||||
public override long Size => throw new NotImplementedException();
|
||||
|
||||
public override DateTime? LastModifiedTime => null;
|
||||
|
||||
public override DateTime? CreatedTime => null;
|
||||
|
||||
public override DateTime? LastAccessedTime => null;
|
||||
|
||||
public override DateTime? ArchivedTime => null;
|
||||
|
||||
public override bool IsEncrypted => false;
|
||||
|
||||
public override bool IsDirectory => false;
|
||||
|
||||
public override bool IsSplitAfter => false;
|
||||
|
||||
internal override IEnumerable<FilePart> Parts => _filePart.Empty();
|
||||
}
|
||||
|
||||
@@ -3,73 +3,74 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpCompress.Common.Arc;
|
||||
|
||||
public class ArcEntryHeader
|
||||
namespace SharpCompress.Common.Arc
|
||||
{
|
||||
public ArchiveEncoding ArchiveEncoding { get; }
|
||||
public CompressionType CompressionMethod { get; private set; }
|
||||
public string? Name { get; private set; }
|
||||
public long CompressedSize { get; private set; }
|
||||
public DateTime DateTime { get; private set; }
|
||||
public int Crc16 { get; private set; }
|
||||
public long OriginalSize { get; private set; }
|
||||
public long DataStartPosition { get; private set; }
|
||||
|
||||
public ArcEntryHeader(ArchiveEncoding archiveEncoding)
|
||||
public class ArcEntryHeader
|
||||
{
|
||||
this.ArchiveEncoding = archiveEncoding;
|
||||
}
|
||||
public ArchiveEncoding ArchiveEncoding { get; }
|
||||
public CompressionType CompressionMethod { get; private set; }
|
||||
public string? Name { get; private set; }
|
||||
public long CompressedSize { get; private set; }
|
||||
public DateTime DateTime { get; private set; }
|
||||
public int Crc16 { get; private set; }
|
||||
public long OriginalSize { get; private set; }
|
||||
public long DataStartPosition { get; private set; }
|
||||
|
||||
public ArcEntryHeader? ReadHeader(Stream stream)
|
||||
{
|
||||
byte[] headerBytes = new byte[29];
|
||||
if (stream.Read(headerBytes, 0, headerBytes.Length) != headerBytes.Length)
|
||||
public ArcEntryHeader(ArchiveEncoding archiveEncoding)
|
||||
{
|
||||
return null;
|
||||
this.ArchiveEncoding = archiveEncoding;
|
||||
}
|
||||
DataStartPosition = stream.Position;
|
||||
return LoadFrom(headerBytes);
|
||||
}
|
||||
|
||||
public ArcEntryHeader LoadFrom(byte[] headerBytes)
|
||||
{
|
||||
CompressionMethod = GetCompressionType(headerBytes[1]);
|
||||
|
||||
// Read name
|
||||
int nameEnd = Array.IndexOf(headerBytes, (byte)0, 1); // Find null terminator
|
||||
Name = Encoding.UTF8.GetString(headerBytes, 2, nameEnd > 0 ? nameEnd - 2 : 12);
|
||||
|
||||
int offset = 15;
|
||||
CompressedSize = BitConverter.ToUInt32(headerBytes, offset);
|
||||
offset += 4;
|
||||
uint rawDateTime = BitConverter.ToUInt32(headerBytes, offset);
|
||||
DateTime = ConvertToDateTime(rawDateTime);
|
||||
offset += 4;
|
||||
Crc16 = BitConverter.ToUInt16(headerBytes, offset);
|
||||
offset += 2;
|
||||
OriginalSize = BitConverter.ToUInt32(headerBytes, offset);
|
||||
return this;
|
||||
}
|
||||
|
||||
private CompressionType GetCompressionType(byte value)
|
||||
{
|
||||
return value switch
|
||||
public ArcEntryHeader? ReadHeader(Stream stream)
|
||||
{
|
||||
1 or 2 => CompressionType.None,
|
||||
3 => CompressionType.RLE90,
|
||||
4 => CompressionType.Squeezed,
|
||||
5 or 6 or 7 or 8 => CompressionType.Crunched,
|
||||
9 => CompressionType.Squashed,
|
||||
10 => CompressionType.Crushed,
|
||||
11 => CompressionType.Distilled,
|
||||
_ => CompressionType.Unknown,
|
||||
};
|
||||
}
|
||||
byte[] headerBytes = new byte[29];
|
||||
if (stream.Read(headerBytes, 0, headerBytes.Length) != headerBytes.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
DataStartPosition = stream.Position;
|
||||
return LoadFrom(headerBytes);
|
||||
}
|
||||
|
||||
public static DateTime ConvertToDateTime(long rawDateTime)
|
||||
{
|
||||
// Convert Unix timestamp to DateTime (UTC)
|
||||
return DateTimeOffset.FromUnixTimeSeconds(rawDateTime).UtcDateTime;
|
||||
public ArcEntryHeader LoadFrom(byte[] headerBytes)
|
||||
{
|
||||
CompressionMethod = GetCompressionType(headerBytes[1]);
|
||||
|
||||
// Read name
|
||||
int nameEnd = Array.IndexOf(headerBytes, (byte)0, 1); // Find null terminator
|
||||
Name = Encoding.UTF8.GetString(headerBytes, 2, nameEnd > 0 ? nameEnd - 2 : 12);
|
||||
|
||||
int offset = 15;
|
||||
CompressedSize = BitConverter.ToUInt32(headerBytes, offset);
|
||||
offset += 4;
|
||||
uint rawDateTime = BitConverter.ToUInt32(headerBytes, offset);
|
||||
DateTime = ConvertToDateTime(rawDateTime);
|
||||
offset += 4;
|
||||
Crc16 = BitConverter.ToUInt16(headerBytes, offset);
|
||||
offset += 2;
|
||||
OriginalSize = BitConverter.ToUInt32(headerBytes, offset);
|
||||
return this;
|
||||
}
|
||||
|
||||
private CompressionType GetCompressionType(byte value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
1 or 2 => CompressionType.None,
|
||||
3 => CompressionType.Packed,
|
||||
4 => CompressionType.Squeezed,
|
||||
5 or 6 or 7 or 8 => CompressionType.Crunched,
|
||||
9 => CompressionType.Squashed,
|
||||
10 => CompressionType.Crushed,
|
||||
11 => CompressionType.Distilled,
|
||||
_ => CompressionType.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
public static DateTime ConvertToDateTime(long rawDateTime)
|
||||
{
|
||||
// Convert Unix timestamp to DateTime (UTC)
|
||||
return DateTimeOffset.FromUnixTimeSeconds(rawDateTime).UtcDateTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,55 +13,71 @@ using SharpCompress.Compressors.RLE90;
|
||||
using SharpCompress.Compressors.Squeezed;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Common.Arc;
|
||||
|
||||
public class ArcFilePart : FilePart
|
||||
namespace SharpCompress.Common.Arc
|
||||
{
|
||||
private readonly Stream? _stream;
|
||||
|
||||
internal ArcFilePart(ArcEntryHeader localArcHeader, Stream? seekableStream)
|
||||
: base(localArcHeader.ArchiveEncoding)
|
||||
public class ArcFilePart : FilePart
|
||||
{
|
||||
_stream = seekableStream;
|
||||
Header = localArcHeader;
|
||||
}
|
||||
private readonly Stream? _stream;
|
||||
|
||||
internal ArcEntryHeader Header { get; set; }
|
||||
|
||||
internal override string? FilePartName => Header.Name;
|
||||
|
||||
internal override Stream GetCompressedStream()
|
||||
{
|
||||
if (_stream != null)
|
||||
internal ArcFilePart(ArcEntryHeader localArcHeader, Stream? seekableStream)
|
||||
: base(localArcHeader.ArchiveEncoding)
|
||||
{
|
||||
Stream compressedStream;
|
||||
switch (Header.CompressionMethod)
|
||||
{
|
||||
case CompressionType.None:
|
||||
compressedStream = new ReadOnlySubStream(
|
||||
_stream,
|
||||
Header.DataStartPosition,
|
||||
Header.CompressedSize
|
||||
);
|
||||
break;
|
||||
case CompressionType.RLE90:
|
||||
compressedStream = new RunLength90Stream(_stream, (int)Header.CompressedSize);
|
||||
break;
|
||||
case CompressionType.Squeezed:
|
||||
compressedStream = new SqueezeStream(_stream, (int)Header.CompressedSize);
|
||||
break;
|
||||
case CompressionType.Crunched:
|
||||
compressedStream = new ArcLzwStream(_stream, (int)Header.CompressedSize, true);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(
|
||||
"CompressionMethod: " + Header.CompressionMethod
|
||||
);
|
||||
}
|
||||
return compressedStream;
|
||||
_stream = seekableStream;
|
||||
Header = localArcHeader;
|
||||
}
|
||||
return _stream.NotNull();
|
||||
}
|
||||
|
||||
internal override Stream? GetRawStream() => _stream;
|
||||
internal ArcEntryHeader Header { get; set; }
|
||||
|
||||
internal override string? FilePartName => Header.Name;
|
||||
|
||||
internal override Stream GetCompressedStream()
|
||||
{
|
||||
if (_stream != null)
|
||||
{
|
||||
Stream compressedStream;
|
||||
switch (Header.CompressionMethod)
|
||||
{
|
||||
case CompressionType.None:
|
||||
compressedStream = new ReadOnlySubStream(
|
||||
_stream,
|
||||
Header.DataStartPosition,
|
||||
Header.CompressedSize
|
||||
);
|
||||
break;
|
||||
case CompressionType.Packed:
|
||||
compressedStream = new RunLength90Stream(
|
||||
_stream,
|
||||
(int)Header.CompressedSize
|
||||
);
|
||||
break;
|
||||
case CompressionType.Squeezed:
|
||||
compressedStream = new SqueezeStream(_stream, (int)Header.CompressedSize);
|
||||
break;
|
||||
case CompressionType.Crunched:
|
||||
if (Header.OriginalSize > 128 * 1024)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"CompressionMethod: "
|
||||
+ Header.CompressionMethod
|
||||
+ " with size > 128KB"
|
||||
);
|
||||
}
|
||||
compressedStream = new ArcLzwStream(
|
||||
_stream,
|
||||
(int)Header.CompressedSize,
|
||||
true
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(
|
||||
"CompressionMethod: " + Header.CompressionMethod
|
||||
);
|
||||
}
|
||||
return compressedStream;
|
||||
}
|
||||
return _stream.NotNull();
|
||||
}
|
||||
|
||||
internal override Stream? GetRawStream() => _stream;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Common.Arc;
|
||||
|
||||
public class ArcVolume : Volume
|
||||
namespace SharpCompress.Common.Arc
|
||||
{
|
||||
public ArcVolume(Stream stream, ReaderOptions readerOptions, int index = 0)
|
||||
: base(stream, readerOptions, index) { }
|
||||
public class ArcVolume : Volume
|
||||
{
|
||||
public ArcVolume(Stream stream, ReaderOptions readerOptions, int index = 0)
|
||||
: base(stream, readerOptions, index) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public class ArchiveExtractionEventArgs<T> : EventArgs
|
||||
{
|
||||
internal ArchiveExtractionEventArgs(T entry) => Item = entry;
|
||||
|
||||
public T Item { get; }
|
||||
}
|
||||
@@ -8,4 +8,6 @@ public enum ArchiveType
|
||||
SevenZip,
|
||||
GZip,
|
||||
Arc,
|
||||
Arj,
|
||||
Ace,
|
||||
}
|
||||
|
||||
58
src/SharpCompress/Common/Arj/ArjEntry.cs
Normal file
58
src/SharpCompress/Common/Arj/ArjEntry.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
72
src/SharpCompress/Common/Arj/ArjFilePart.cs
Normal file
72
src/SharpCompress/Common/Arj/ArjFilePart.cs
Normal 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.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.CompressedMost:
|
||||
case CompressionMethod.Compressed:
|
||||
case CompressionMethod.CompressedFaster:
|
||||
if (Header.OriginalSize > 128 * 1024)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"CompressionMethod: "
|
||||
+ Header.CompressionMethod
|
||||
+ " with size > 128KB"
|
||||
);
|
||||
}
|
||||
compressedStream = new LhaStream<Lh7DecoderCfg>(
|
||||
_stream,
|
||||
(int)Header.OriginalSize
|
||||
);
|
||||
break;
|
||||
case CompressionMethod.CompressedFastest:
|
||||
compressedStream = new LHDecoderStream(_stream, (int)Header.OriginalSize);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(
|
||||
"CompressionMethod: " + Header.CompressionMethod
|
||||
);
|
||||
}
|
||||
return compressedStream;
|
||||
}
|
||||
return _stream.NotNull();
|
||||
}
|
||||
|
||||
internal override Stream GetRawStream() => _stream;
|
||||
}
|
||||
}
|
||||
36
src/SharpCompress/Common/Arj/ArjVolume.cs
Normal file
36
src/SharpCompress/Common/Arj/ArjVolume.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/SharpCompress/Common/Arj/Headers/ArjHeader.cs
Normal file
158
src/SharpCompress/Common/Arj/Headers/ArjHeader.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
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
|
||||
var magic = new byte[2];
|
||||
if (stream.Read(magic) != 2)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
if (!CheckMagicBytes(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;
|
||||
}
|
||||
|
||||
public static bool IsArchive(Stream stream)
|
||||
{
|
||||
var bytes = new byte[2];
|
||||
if (stream.Read(bytes, 0, 2) != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return CheckMagicBytes(bytes);
|
||||
}
|
||||
|
||||
protected static bool CheckMagicBytes(byte[] headerBytes)
|
||||
{
|
||||
var magicValue = (ushort)(headerBytes[0] | headerBytes[1] << 8);
|
||||
return magicValue == ARJ_MAGIC;
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs
Normal file
161
src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
138
src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs
Normal file
138
src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/SharpCompress/Common/Arj/Headers/CompressionMethod.cs
Normal file
20
src/SharpCompress/Common/Arj/Headers/CompressionMethod.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
37
src/SharpCompress/Common/Arj/Headers/DosDateTime.cs
Normal file
37
src/SharpCompress/Common/Arj/Headers/DosDateTime.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
13
src/SharpCompress/Common/Arj/Headers/FileType.cs
Normal file
13
src/SharpCompress/Common/Arj/Headers/FileType.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
19
src/SharpCompress/Common/Arj/Headers/HostOS.cs
Normal file
19
src/SharpCompress/Common/Arj/Headers/HostOS.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public sealed class CompressedBytesReadEventArgs : EventArgs
|
||||
{
|
||||
public CompressedBytesReadEventArgs(
|
||||
long compressedBytesRead,
|
||||
long currentFilePartCompressedBytesRead
|
||||
)
|
||||
{
|
||||
CompressedBytesRead = compressedBytesRead;
|
||||
CurrentFilePartCompressedBytesRead = currentFilePartCompressedBytesRead;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compressed bytes read for the current entry
|
||||
/// </summary>
|
||||
public long CompressedBytesRead { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current file part read for Multipart files (e.g. Rar)
|
||||
/// </summary>
|
||||
public long CurrentFilePartCompressedBytesRead { get; }
|
||||
}
|
||||
@@ -23,10 +23,12 @@ public enum CompressionType
|
||||
Reduce4,
|
||||
Explode,
|
||||
Squeezed,
|
||||
RLE90,
|
||||
Packed,
|
||||
Crunched,
|
||||
Squashed,
|
||||
Crushed,
|
||||
Distilled,
|
||||
ZStandard,
|
||||
ArjLZ77,
|
||||
AceLZ77,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
@@ -51,8 +53,22 @@ public class EntryStream : Stream, IStreamStack
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously skip the rest of the entry stream.
|
||||
/// </summary>
|
||||
public async Task SkipEntryAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this.SkipAsync(cancellationToken).ConfigureAwait(false);
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
if (!(_completed || _reader.Cancelled))
|
||||
{
|
||||
SkipEntry();
|
||||
@@ -70,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
|
||||
@@ -83,6 +93,39 @@ public class EntryStream : Stream, IStreamStack
|
||||
_stream.Dispose();
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
if (!(_completed || _reader.Cancelled))
|
||||
{
|
||||
await SkipEntryAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
//Need a safe standard approach to this - it's okay for compression to overreads. Handling needs to be standardised
|
||||
if (_stream is IStreamStack ss)
|
||||
{
|
||||
if (ss.BaseStream() is SharpCompress.Compressors.Deflate.DeflateStream deflateStream)
|
||||
{
|
||||
await deflateStream.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
else if (ss.BaseStream() is SharpCompress.Compressors.LZMA.LzmaStream lzmaStream)
|
||||
{
|
||||
await lzmaStream.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(EntryStream));
|
||||
#endif
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
await _stream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
@@ -91,6 +134,8 @@ public class EntryStream : Stream, IStreamStack
|
||||
|
||||
public override void Flush() { }
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public override long Length => _stream.Length;
|
||||
|
||||
public override long Position
|
||||
@@ -109,6 +154,38 @@ public class EntryStream : Stream, IStreamStack
|
||||
return read;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var read = await _stream
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (read <= 0)
|
||||
{
|
||||
_completed = true;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var read = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
if (read <= 0)
|
||||
{
|
||||
_completed = true;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
var value = _stream.ReadByte();
|
||||
@@ -125,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();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
internal static class ExtractionMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the appropriate StringComparison for path checks based on the file system.
|
||||
/// Windows uses case-insensitive file systems, while Unix-like systems use case-sensitive file systems.
|
||||
/// </summary>
|
||||
private static StringComparison PathComparison =>
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory, retaining filename
|
||||
/// </summary>
|
||||
@@ -46,7 +58,7 @@ internal static class ExtractionMethods
|
||||
|
||||
if (!Directory.Exists(destdir))
|
||||
{
|
||||
if (!destdir.StartsWith(fullDestinationDirectoryPath, StringComparison.Ordinal))
|
||||
if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison))
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Entry is trying to create a directory outside of the destination directory."
|
||||
@@ -66,12 +78,7 @@ internal static class ExtractionMethods
|
||||
{
|
||||
destinationFileName = Path.GetFullPath(destinationFileName);
|
||||
|
||||
if (
|
||||
!destinationFileName.StartsWith(
|
||||
fullDestinationDirectoryPath,
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
)
|
||||
if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison))
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Entry is trying to write a file outside of the destination directory."
|
||||
@@ -116,4 +123,111 @@ internal static class ExtractionMethods
|
||||
entry.PreserveExtractionOptions(destinationFileName, options);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WriteEntryToDirectoryAsync(
|
||||
IEntry entry,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
Func<string, ExtractionOptions?, CancellationToken, Task> writeAsync,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
string destinationFileName;
|
||||
var fullDestinationDirectoryPath = Path.GetFullPath(destinationDirectory);
|
||||
|
||||
//check for trailing slash.
|
||||
if (
|
||||
fullDestinationDirectoryPath[fullDestinationDirectoryPath.Length - 1]
|
||||
!= Path.DirectorySeparatorChar
|
||||
)
|
||||
{
|
||||
fullDestinationDirectoryPath += Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(fullDestinationDirectoryPath))
|
||||
{
|
||||
throw new ExtractionException(
|
||||
$"Directory does not exist to extract to: {fullDestinationDirectoryPath}"
|
||||
);
|
||||
}
|
||||
|
||||
options ??= new ExtractionOptions() { Overwrite = true };
|
||||
|
||||
var file = Path.GetFileName(entry.Key.NotNull("Entry Key is null")).NotNull("File is null");
|
||||
file = Utility.ReplaceInvalidFileNameChars(file);
|
||||
if (options.ExtractFullPath)
|
||||
{
|
||||
var folder = Path.GetDirectoryName(entry.Key.NotNull("Entry Key is null"))
|
||||
.NotNull("Directory is null");
|
||||
var destdir = Path.GetFullPath(Path.Combine(fullDestinationDirectoryPath, folder));
|
||||
|
||||
if (!Directory.Exists(destdir))
|
||||
{
|
||||
if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison))
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Entry is trying to create a directory outside of the destination directory."
|
||||
);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(destdir);
|
||||
}
|
||||
destinationFileName = Path.Combine(destdir, file);
|
||||
}
|
||||
else
|
||||
{
|
||||
destinationFileName = Path.Combine(fullDestinationDirectoryPath, file);
|
||||
}
|
||||
|
||||
if (!entry.IsDirectory)
|
||||
{
|
||||
destinationFileName = Path.GetFullPath(destinationFileName);
|
||||
|
||||
if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison))
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Entry is trying to write a file outside of the destination directory."
|
||||
);
|
||||
}
|
||||
await writeAsync(destinationFileName, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (options.ExtractFullPath && !Directory.Exists(destinationFileName))
|
||||
{
|
||||
Directory.CreateDirectory(destinationFileName);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WriteEntryToFileAsync(
|
||||
IEntry entry,
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options,
|
||||
Func<string, FileMode, CancellationToken, Task> openAndWriteAsync,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (entry.LinkTarget != null)
|
||||
{
|
||||
if (options?.WriteSymbolicLink is null)
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Entry is a symbolic link but ExtractionOptions.WriteSymbolicLink delegate is null"
|
||||
);
|
||||
}
|
||||
options.WriteSymbolicLink(destinationFileName, entry.LinkTarget);
|
||||
}
|
||||
else
|
||||
{
|
||||
var fm = FileMode.Create;
|
||||
options ??= new ExtractionOptions() { Overwrite = true };
|
||||
|
||||
if (!options.Overwrite)
|
||||
{
|
||||
fm = FileMode.CreateNew;
|
||||
}
|
||||
|
||||
await openAndWriteAsync(destinationFileName, fm, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
entry.PreserveExtractionOptions(destinationFileName, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public sealed class FilePartExtractionBeginEventArgs : EventArgs
|
||||
{
|
||||
public FilePartExtractionBeginEventArgs(string name, long size, long compressedSize)
|
||||
{
|
||||
Name = name;
|
||||
Size = size;
|
||||
CompressedSize = compressedSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// File name for the part for the current entry
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Uncompressed size of the current entry in the part
|
||||
/// </summary>
|
||||
public long Size { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compressed size of the current entry in the part
|
||||
/// </summary>
|
||||
public long CompressedSize { get; }
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public interface IExtractionListener
|
||||
{
|
||||
void FireFilePartExtractionBegin(string name, long size, long compressedSize);
|
||||
void FireCompressedBytesRead(long currentPartCompressedBytes, long compressedReadBytes);
|
||||
}
|
||||
43
src/SharpCompress/Common/ProgressReport.cs
Normal file
43
src/SharpCompress/Common/ProgressReport.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents progress information for compression or extraction operations.
|
||||
/// </summary>
|
||||
public sealed class ProgressReport
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProgressReport"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entryPath">The path of the entry being processed.</param>
|
||||
/// <param name="bytesTransferred">Number of bytes transferred so far.</param>
|
||||
/// <param name="totalBytes">Total bytes to be transferred, or null if unknown.</param>
|
||||
public ProgressReport(string entryPath, long bytesTransferred, long? totalBytes)
|
||||
{
|
||||
EntryPath = entryPath;
|
||||
BytesTransferred = bytesTransferred;
|
||||
TotalBytes = totalBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the entry being processed.
|
||||
/// </summary>
|
||||
public string EntryPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of bytes transferred so far.
|
||||
/// </summary>
|
||||
public long BytesTransferred { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of bytes to be transferred, or null if unknown.
|
||||
/// </summary>
|
||||
public long? TotalBytes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the progress percentage (0-100), or null if total bytes is unknown.
|
||||
/// </summary>
|
||||
public double? PercentComplete =>
|
||||
TotalBytes.HasValue && TotalBytes.Value > 0
|
||||
? (double)BytesTransferred / TotalBytes.Value * 100
|
||||
: null;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public sealed class ReaderExtractionEventArgs<T> : EventArgs
|
||||
{
|
||||
internal ReaderExtractionEventArgs(T entry, ReaderProgress? readerProgress = null)
|
||||
{
|
||||
Item = entry;
|
||||
ReaderProgress = readerProgress;
|
||||
}
|
||||
|
||||
public T Item { get; }
|
||||
|
||||
public ReaderProgress? ReaderProgress { get; }
|
||||
}
|
||||
@@ -55,7 +55,7 @@ internal class SevenZipFilePart : FilePart
|
||||
{
|
||||
folderStream.Skip(skipSize);
|
||||
}
|
||||
return new ReadOnlySubStream(folderStream, Header.Size);
|
||||
return new ReadOnlySubStream(folderStream, Header.Size, leaveOpen: false);
|
||||
}
|
||||
|
||||
public CompressionType CompressionType
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
@@ -9,8 +10,16 @@ internal sealed class TarHeader
|
||||
{
|
||||
internal static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public TarHeader(ArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
|
||||
public TarHeader(
|
||||
ArchiveEncoding archiveEncoding,
|
||||
TarHeaderWriteFormat writeFormat = TarHeaderWriteFormat.GNU_TAR_LONG_LINK
|
||||
)
|
||||
{
|
||||
ArchiveEncoding = archiveEncoding;
|
||||
WriteFormat = writeFormat;
|
||||
}
|
||||
|
||||
internal TarHeaderWriteFormat WriteFormat { get; set; }
|
||||
internal string? Name { get; set; }
|
||||
internal string? LinkName { get; set; }
|
||||
|
||||
@@ -25,7 +34,119 @@ internal sealed class TarHeader
|
||||
|
||||
internal const int BLOCK_SIZE = 512;
|
||||
|
||||
// Maximum size for long name/link headers to prevent memory exhaustion attacks
|
||||
// This is generous enough for most real-world scenarios (32KB)
|
||||
private const int MAX_LONG_NAME_SIZE = 32768;
|
||||
|
||||
internal void Write(Stream output)
|
||||
{
|
||||
switch (WriteFormat)
|
||||
{
|
||||
case TarHeaderWriteFormat.GNU_TAR_LONG_LINK:
|
||||
WriteGnuTarLongLink(output);
|
||||
break;
|
||||
case TarHeaderWriteFormat.USTAR:
|
||||
WriteUstar(output);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("This should be impossible...");
|
||||
}
|
||||
}
|
||||
|
||||
internal void WriteUstar(Stream output)
|
||||
{
|
||||
var buffer = new byte[BLOCK_SIZE];
|
||||
|
||||
WriteOctalBytes(511, buffer, 100, 8); // file mode
|
||||
WriteOctalBytes(0, buffer, 108, 8); // owner ID
|
||||
WriteOctalBytes(0, buffer, 116, 8); // group ID
|
||||
|
||||
//ArchiveEncoding.UTF8.GetBytes("magic").CopyTo(buffer, 257);
|
||||
var nameByteCount = ArchiveEncoding
|
||||
.GetEncoding()
|
||||
.GetByteCount(Name.NotNull("Name is null"));
|
||||
|
||||
if (nameByteCount > 100)
|
||||
{
|
||||
// if name is longer, try to split it into name and namePrefix
|
||||
|
||||
string fullName = Name.NotNull("Name is null");
|
||||
|
||||
// find all directory separators
|
||||
List<int> dirSeps = new List<int>();
|
||||
for (int i = 0; i < fullName.Length; i++)
|
||||
{
|
||||
if (fullName[i] == Path.DirectorySeparatorChar)
|
||||
{
|
||||
dirSeps.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// find the right place to split the name
|
||||
int splitIndex = -1;
|
||||
for (int i = 0; i < dirSeps.Count; i++)
|
||||
{
|
||||
int count = ArchiveEncoding
|
||||
.GetEncoding()
|
||||
.GetByteCount(fullName.Substring(0, dirSeps[i]));
|
||||
if (count < 155)
|
||||
{
|
||||
splitIndex = dirSeps[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (splitIndex == -1)
|
||||
{
|
||||
throw new Exception(
|
||||
$"Tar header USTAR format can not fit file name \"{fullName}\" of length {nameByteCount}! Directory separator not found! Try using GNU Tar format instead!"
|
||||
);
|
||||
}
|
||||
|
||||
string namePrefix = fullName.Substring(0, splitIndex);
|
||||
string name = fullName.Substring(splitIndex + 1);
|
||||
|
||||
if (this.ArchiveEncoding.GetEncoding().GetByteCount(namePrefix) >= 155)
|
||||
throw new Exception(
|
||||
$"Tar header USTAR format can not fit file name \"{fullName}\" of length {nameByteCount}! Try using GNU Tar format instead!"
|
||||
);
|
||||
|
||||
if (this.ArchiveEncoding.GetEncoding().GetByteCount(name) >= 100)
|
||||
throw new Exception(
|
||||
$"Tar header USTAR format can not fit file name \"{fullName}\" of length {nameByteCount}! Try using GNU Tar format instead!"
|
||||
);
|
||||
|
||||
// write name prefix
|
||||
WriteStringBytes(ArchiveEncoding.Encode(namePrefix), buffer, 345, 100);
|
||||
// write partial name
|
||||
WriteStringBytes(ArchiveEncoding.Encode(name), buffer, 100);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteStringBytes(ArchiveEncoding.Encode(Name.NotNull("Name is null")), buffer, 100);
|
||||
}
|
||||
|
||||
WriteOctalBytes(Size, buffer, 124, 12);
|
||||
var time = (long)(LastModifiedTime.ToUniversalTime() - EPOCH).TotalSeconds;
|
||||
WriteOctalBytes(time, buffer, 136, 12);
|
||||
buffer[156] = (byte)EntryType;
|
||||
|
||||
// write ustar magic field
|
||||
WriteStringBytes(Encoding.ASCII.GetBytes("ustar"), buffer, 257, 6);
|
||||
// write ustar version "00"
|
||||
buffer[263] = 0x30;
|
||||
buffer[264] = 0x30;
|
||||
|
||||
var crc = RecalculateChecksum(buffer);
|
||||
WriteOctalBytes(crc, buffer, 148, 8);
|
||||
|
||||
output.Write(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
internal void WriteGnuTarLongLink(Stream output)
|
||||
{
|
||||
var buffer = new byte[BLOCK_SIZE];
|
||||
|
||||
@@ -81,7 +202,7 @@ internal sealed class TarHeader
|
||||
0,
|
||||
100 - ArchiveEncoding.GetEncoding().GetMaxByteCount(1)
|
||||
);
|
||||
Write(output);
|
||||
WriteGnuTarLongLink(output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +307,15 @@ internal sealed class TarHeader
|
||||
private string ReadLongName(BinaryReader reader, byte[] buffer)
|
||||
{
|
||||
var size = ReadSize(buffer);
|
||||
|
||||
// Validate size to prevent memory exhaustion from malformed headers
|
||||
if (size < 0 || size > MAX_LONG_NAME_SIZE)
|
||||
{
|
||||
throw new InvalidFormatException(
|
||||
$"Long name size {size} is invalid or exceeds maximum allowed size of {MAX_LONG_NAME_SIZE} bytes"
|
||||
);
|
||||
}
|
||||
|
||||
var nameLength = (int)size;
|
||||
var nameBytes = reader.ReadBytes(nameLength);
|
||||
var remainingBytesToRead = BLOCK_SIZE - (nameLength % BLOCK_SIZE);
|
||||
@@ -228,6 +358,18 @@ internal sealed class TarHeader
|
||||
buffer.Slice(i, length - i).Clear();
|
||||
}
|
||||
|
||||
private static void WriteStringBytes(
|
||||
ReadOnlySpan<byte> name,
|
||||
Span<byte> buffer,
|
||||
int offset,
|
||||
int length
|
||||
)
|
||||
{
|
||||
name.CopyTo(buffer.Slice(offset));
|
||||
var i = Math.Min(length, name.Length);
|
||||
buffer.Slice(offset + i, length - i).Clear();
|
||||
}
|
||||
|
||||
private static void WriteStringBytes(string name, byte[] buffer, int offset, int length)
|
||||
{
|
||||
int i;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SharpCompress.Common.Tar.Headers;
|
||||
|
||||
public enum TarHeaderWriteFormat
|
||||
{
|
||||
GNU_TAR_LONG_LINK,
|
||||
USTAR,
|
||||
}
|
||||
@@ -66,6 +66,36 @@ internal class TarReadOnlySubStream : SharpCompressStream, IStreamStack
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async System.Threading.Tasks.ValueTask DisposeAsync()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(TarReadOnlySubStream));
|
||||
#endif
|
||||
// Ensure we read all remaining blocks for this entry.
|
||||
await Stream.SkipAsync(BytesLeftToRead).ConfigureAwait(false);
|
||||
_amountRead += BytesLeftToRead;
|
||||
|
||||
// If the last block wasn't a full 512 bytes, skip the remaining padding bytes.
|
||||
var bytesInLastBlock = _amountRead % 512;
|
||||
|
||||
if (bytesInLastBlock != 0)
|
||||
{
|
||||
await Stream.SkipAsync(512 - bytesInLastBlock).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Call base Dispose instead of base DisposeAsync to avoid double disposal
|
||||
base.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
#endif
|
||||
|
||||
private long BytesLeftToRead { get; set; }
|
||||
|
||||
public override bool CanRead => true;
|
||||
@@ -76,6 +106,10 @@ internal class TarReadOnlySubStream : SharpCompressStream, IStreamStack
|
||||
|
||||
public override void Flush() { }
|
||||
|
||||
public override System.Threading.Tasks.Task FlushAsync(
|
||||
System.Threading.CancellationToken cancellationToken
|
||||
) => System.Threading.Tasks.Task.CompletedTask;
|
||||
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
@@ -114,6 +148,48 @@ internal class TarReadOnlySubStream : SharpCompressStream, IStreamStack
|
||||
return value;
|
||||
}
|
||||
|
||||
public override async System.Threading.Tasks.Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
System.Threading.CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (BytesLeftToRead < count)
|
||||
{
|
||||
count = (int)BytesLeftToRead;
|
||||
}
|
||||
var read = await Stream
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (read > 0)
|
||||
{
|
||||
BytesLeftToRead -= read;
|
||||
_amountRead += read;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async System.Threading.Tasks.ValueTask<int> ReadAsync(
|
||||
System.Memory<byte> buffer,
|
||||
System.Threading.CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (BytesLeftToRead < buffer.Length)
|
||||
{
|
||||
buffer = buffer.Slice(0, (int)BytesLeftToRead);
|
||||
}
|
||||
var read = await Stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
if (read > 0)
|
||||
{
|
||||
BytesLeftToRead -= read;
|
||||
_amountRead += read;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
@@ -91,8 +91,15 @@ internal abstract class ZipFileEntry : ZipHeader
|
||||
|
||||
protected void LoadExtra(byte[] extra)
|
||||
{
|
||||
for (var i = 0; i < extra.Length - 4; )
|
||||
for (var i = 0; i < extra.Length; )
|
||||
{
|
||||
// Ensure we have at least a header (2-byte ID + 2-byte length)
|
||||
if (i + 4 > extra.Length)
|
||||
{
|
||||
// Incomplete header — stop parsing extras
|
||||
break;
|
||||
}
|
||||
|
||||
var type = (ExtraDataType)BinaryPrimitives.ReadUInt16LittleEndian(extra.AsSpan(i));
|
||||
if (!Enum.IsDefined(typeof(ExtraDataType), type))
|
||||
{
|
||||
@@ -106,7 +113,17 @@ internal abstract class ZipFileEntry : ZipHeader
|
||||
if (length > extra.Length)
|
||||
{
|
||||
// bad extras block
|
||||
return;
|
||||
break; // allow processing optional other blocks
|
||||
}
|
||||
// Some ZIP files contain vendor-specific or malformed extra fields where the declared
|
||||
// data length extends beyond the remaining buffer. This adjustment ensures that
|
||||
// we only read data within bounds (i + 4 + length <= extra.Length)
|
||||
// The example here is: 41 43 18 00 41 52 43 30 46 EB FF FF 51 29 03 C6 03 00 00 00 00 00 00 00 00
|
||||
// No existing zip utility uses 0x4341 ('AC')
|
||||
if (i + 4 + length > extra.Length)
|
||||
{
|
||||
// incomplete or corrupt field
|
||||
break; // allow processing optional other blocks
|
||||
}
|
||||
|
||||
var data = new byte[length];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpCompress.Common.Zip;
|
||||
|
||||
@@ -19,8 +20,24 @@ internal class WinzipAesEncryptionData
|
||||
{
|
||||
_keySize = keySize;
|
||||
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
#if NETFRAMEWORK
|
||||
var rfc2898 = new Rfc2898DeriveBytes(password, salt, RFC2898_ITERATIONS);
|
||||
KeyBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
IvBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
var generatedVerifyValue = rfc2898.GetBytes(2);
|
||||
#elif NET10_0_OR_GREATER
|
||||
var derivedKeySize = (KeySizeInBytes * 2) + 2;
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
var derivedKey = Rfc2898DeriveBytes.Pbkdf2(
|
||||
passwordBytes,
|
||||
salt,
|
||||
RFC2898_ITERATIONS,
|
||||
HashAlgorithmName.SHA1,
|
||||
derivedKeySize
|
||||
);
|
||||
KeyBytes = derivedKey.AsSpan(0, KeySizeInBytes).ToArray();
|
||||
IvBytes = derivedKey.AsSpan(KeySizeInBytes, KeySizeInBytes).ToArray();
|
||||
var generatedVerifyValue = derivedKey.AsSpan((KeySizeInBytes * 2), 2).ToArray();
|
||||
#else
|
||||
var rfc2898 = new Rfc2898DeriveBytes(
|
||||
password,
|
||||
@@ -28,11 +45,10 @@ internal class WinzipAesEncryptionData
|
||||
RFC2898_ITERATIONS,
|
||||
HashAlgorithmName.SHA1
|
||||
);
|
||||
#endif
|
||||
|
||||
KeyBytes = rfc2898.GetBytes(KeySizeInBytes); // 16 or 24 or 32 ???
|
||||
KeyBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
IvBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
var generatedVerifyValue = rfc2898.GetBytes(2);
|
||||
#endif
|
||||
|
||||
var verify = BinaryPrimitives.ReadInt16LittleEndian(passwordVerifyValue);
|
||||
var generated = BinaryPrimitives.ReadInt16LittleEndian(generatedVerifyValue);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
72
src/SharpCompress/Compressors/Arj/BitReader.cs
Normal file
72
src/SharpCompress/Compressors/Arj/BitReader.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
[CLSCompliant(true)]
|
||||
public class BitReader
|
||||
{
|
||||
private readonly Stream _input;
|
||||
private int _bitBuffer; // currently buffered bits
|
||||
private int _bitCount; // number of bits in buffer
|
||||
|
||||
public BitReader(Stream input)
|
||||
{
|
||||
_input = input ?? throw new ArgumentNullException(nameof(input));
|
||||
_bitBuffer = 0;
|
||||
_bitCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a single bit from the stream. Returns 0 or 1.
|
||||
/// </summary>
|
||||
public int ReadBit()
|
||||
{
|
||||
if (_bitCount == 0)
|
||||
{
|
||||
int nextByte = _input.ReadByte();
|
||||
if (nextByte < 0)
|
||||
{
|
||||
throw new EndOfStreamException("No more data available in BitReader.");
|
||||
}
|
||||
|
||||
_bitBuffer = nextByte;
|
||||
_bitCount = 8;
|
||||
}
|
||||
|
||||
int bit = (_bitBuffer >> (_bitCount - 1)) & 1;
|
||||
_bitCount--;
|
||||
return bit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads n bits (up to 32) from the stream.
|
||||
/// </summary>
|
||||
public int ReadBits(int count)
|
||||
{
|
||||
if (count < 0 || count > 32)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(count),
|
||||
"Count must be between 0 and 32."
|
||||
);
|
||||
}
|
||||
|
||||
int result = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
result = (result << 1) | ReadBit();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets any buffered bits.
|
||||
/// </summary>
|
||||
public void AlignToByte()
|
||||
{
|
||||
_bitCount = 0;
|
||||
_bitBuffer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/SharpCompress/Compressors/Arj/HistoryIterator.cs
Normal file
43
src/SharpCompress/Compressors/Arj/HistoryIterator.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
/// <summary>
|
||||
/// Iterator that reads & pushes values back into the ring buffer.
|
||||
/// </summary>
|
||||
public class HistoryIterator : IEnumerator<byte>
|
||||
{
|
||||
private int _index;
|
||||
private readonly IRingBuffer _ring;
|
||||
|
||||
public HistoryIterator(IRingBuffer ring, int startIndex)
|
||||
{
|
||||
_ring = ring;
|
||||
_index = startIndex;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
Current = _ring[_index];
|
||||
_index = unchecked(_index + 1);
|
||||
|
||||
// Push value back into the ring buffer
|
||||
_ring.Push(Current);
|
||||
|
||||
return true; // iterator is infinite
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public byte Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
218
src/SharpCompress/Compressors/Arj/HuffmanTree.cs
Normal file
218
src/SharpCompress/Compressors/Arj/HuffmanTree.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
[CLSCompliant(true)]
|
||||
public enum NodeType
|
||||
{
|
||||
Leaf,
|
||||
Branch,
|
||||
}
|
||||
|
||||
[CLSCompliant(true)]
|
||||
public sealed class TreeEntry
|
||||
{
|
||||
public readonly NodeType Type;
|
||||
public readonly int LeafValue;
|
||||
public readonly int BranchIndex;
|
||||
|
||||
public const int MAX_INDEX = 4096;
|
||||
|
||||
private TreeEntry(NodeType type, int leafValue, int branchIndex)
|
||||
{
|
||||
Type = type;
|
||||
LeafValue = leafValue;
|
||||
BranchIndex = branchIndex;
|
||||
}
|
||||
|
||||
public static TreeEntry Leaf(int value)
|
||||
{
|
||||
return new TreeEntry(NodeType.Leaf, value, -1);
|
||||
}
|
||||
|
||||
public static TreeEntry Branch(int index)
|
||||
{
|
||||
if (index >= MAX_INDEX)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(index),
|
||||
"Branch index exceeds MAX_INDEX"
|
||||
);
|
||||
}
|
||||
return new TreeEntry(NodeType.Branch, 0, index);
|
||||
}
|
||||
}
|
||||
|
||||
[CLSCompliant(true)]
|
||||
public sealed class HuffTree
|
||||
{
|
||||
private readonly List<TreeEntry> _tree;
|
||||
|
||||
public HuffTree(int capacity = 0)
|
||||
{
|
||||
_tree = new List<TreeEntry>(capacity);
|
||||
}
|
||||
|
||||
public void SetSingle(int value)
|
||||
{
|
||||
_tree.Clear();
|
||||
_tree.Add(TreeEntry.Leaf(value));
|
||||
}
|
||||
|
||||
public void BuildTree(byte[] lengths, int count)
|
||||
{
|
||||
if (lengths == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(lengths));
|
||||
}
|
||||
|
||||
if (count < 0 || count > lengths.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
|
||||
if (count > TreeEntry.MAX_INDEX / 2)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Count exceeds maximum allowed: {TreeEntry.MAX_INDEX / 2}"
|
||||
);
|
||||
}
|
||||
byte[] slice = new byte[count];
|
||||
Array.Copy(lengths, slice, count);
|
||||
|
||||
BuildTree(slice);
|
||||
}
|
||||
|
||||
public void BuildTree(byte[] valueLengths)
|
||||
{
|
||||
if (valueLengths == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(valueLengths));
|
||||
}
|
||||
|
||||
if (valueLengths.Length > TreeEntry.MAX_INDEX / 2)
|
||||
{
|
||||
throw new InvalidOperationException("Too many code lengths");
|
||||
}
|
||||
|
||||
_tree.Clear();
|
||||
|
||||
int maxAllocated = 1; // start with a single (root) node
|
||||
|
||||
for (byte currentLen = 1; ; currentLen++)
|
||||
{
|
||||
// add missing branches up to current limit
|
||||
int maxLimit = maxAllocated;
|
||||
|
||||
for (int i = _tree.Count; i < maxLimit; i++)
|
||||
{
|
||||
// TreeEntry.Branch may throw if index too large
|
||||
try
|
||||
{
|
||||
_tree.Add(TreeEntry.Branch(maxAllocated));
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
_tree.Clear();
|
||||
throw new InvalidOperationException("Branch index exceeds limit", e);
|
||||
}
|
||||
|
||||
// each branch node allocates two children
|
||||
maxAllocated += 2;
|
||||
}
|
||||
|
||||
// fill tree with leaves found in the lengths table at the current length
|
||||
bool moreLeaves = false;
|
||||
|
||||
for (int value = 0; value < valueLengths.Length; value++)
|
||||
{
|
||||
byte len = valueLengths[value];
|
||||
if (len == currentLen)
|
||||
{
|
||||
_tree.Add(TreeEntry.Leaf(value));
|
||||
}
|
||||
else if (len > currentLen)
|
||||
{
|
||||
moreLeaves = true; // there are more leaves to process
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check (too many leaves)
|
||||
if (_tree.Count > maxAllocated)
|
||||
{
|
||||
throw new InvalidOperationException("Too many leaves");
|
||||
}
|
||||
|
||||
// stop when no longer finding longer codes
|
||||
if (!moreLeaves)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ensure tree is complete
|
||||
if (_tree.Count != maxAllocated)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Missing some leaves: tree count = {_tree.Count}, expected = {maxAllocated}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public int ReadEntry(BitReader reader)
|
||||
{
|
||||
if (_tree.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Tree not initialized");
|
||||
}
|
||||
|
||||
TreeEntry node = _tree[0];
|
||||
while (true)
|
||||
{
|
||||
if (node.Type == NodeType.Leaf)
|
||||
{
|
||||
return node.LeafValue;
|
||||
}
|
||||
|
||||
int bit = reader.ReadBit();
|
||||
int index = node.BranchIndex + bit;
|
||||
|
||||
if (index >= _tree.Count)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid branch index during read");
|
||||
}
|
||||
|
||||
node = _tree[index];
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
|
||||
void FormatStep(int index, string prefix)
|
||||
{
|
||||
var node = _tree[index];
|
||||
if (node.Type == NodeType.Leaf)
|
||||
{
|
||||
result.AppendLine($"{prefix} -> {node.LeafValue}");
|
||||
}
|
||||
else
|
||||
{
|
||||
FormatStep(node.BranchIndex, prefix + "0");
|
||||
FormatStep(node.BranchIndex + 1, prefix + "1");
|
||||
}
|
||||
}
|
||||
|
||||
if (_tree.Count > 0)
|
||||
{
|
||||
FormatStep(0, "");
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/SharpCompress/Compressors/Arj/ILhaDecoderConfig.cs
Normal file
9
src/SharpCompress/Compressors/Arj/ILhaDecoderConfig.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
public interface ILhaDecoderConfig
|
||||
{
|
||||
int HistoryBits { get; }
|
||||
int OffsetBits { get; }
|
||||
RingBuffer RingBuffer { get; }
|
||||
}
|
||||
}
|
||||
17
src/SharpCompress/Compressors/Arj/IRingBuffer.cs
Normal file
17
src/SharpCompress/Compressors/Arj/IRingBuffer.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
public interface IRingBuffer
|
||||
{
|
||||
int BufferSize { get; }
|
||||
|
||||
int Cursor { get; }
|
||||
void SetCursor(int pos);
|
||||
|
||||
void Push(byte value);
|
||||
|
||||
HistoryIterator IterFromOffset(int offset);
|
||||
HistoryIterator IterFromPos(int pos);
|
||||
|
||||
byte this[int index] { get; }
|
||||
}
|
||||
}
|
||||
191
src/SharpCompress/Compressors/Arj/LHDecoderStream.cs
Normal file
191
src/SharpCompress/Compressors/Arj/LHDecoderStream.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
9
src/SharpCompress/Compressors/Arj/Lh5DecoderCfg.cs
Normal file
9
src/SharpCompress/Compressors/Arj/Lh5DecoderCfg.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SharpCompress.Compressors.Arj
|
||||
{
|
||||
public class Lh5DecoderCfg : ILhaDecoderConfig
|
||||
{
|
||||
public int HistoryBits => 14;
|
||||
public int OffsetBits => 4;
|
||||
public RingBuffer RingBuffer { get; } = new RingBuffer(1 << 14);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user