Compare commits
1766 commits
master
...
fix-zip64-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c59aa78928 | ||
|
|
0e20a10961 | ||
|
|
de8ee30b17 | ||
|
|
37a22a8697 | ||
|
|
49d00034e4 | ||
|
|
727d2a79f9 | ||
|
|
e8ee054757 | ||
|
|
808524d917 | ||
|
|
e9e05520d6 | ||
|
|
2866bfbd54 | ||
|
|
c36a916c84 | ||
|
|
78a2218982 | ||
|
|
2d041e5c76 | ||
|
|
160ba0938a | ||
|
|
7814013995 | ||
|
|
668d5999f0 | ||
|
|
59d6b82ac5 | ||
|
|
4e3dcabbd0 | ||
|
|
323831cfa7 | ||
|
|
9684e53084 | ||
|
|
68a74e0f45 | ||
|
|
61ae3f21c7 | ||
|
|
0fdd9e6fc6 | ||
|
|
21b9c33c95 | ||
|
|
de6e2bfee2 | ||
|
|
3a73cfe925 | ||
|
|
5de1193aab | ||
|
|
31fcb8526c | ||
|
|
2c9f7650a0 | ||
|
|
cd55ed2744 | ||
|
|
27cc668147 | ||
|
|
b4b13b7c08 | ||
|
|
a14a5c7afd | ||
|
|
4034a5471b | ||
|
|
051b302def | ||
|
|
4c6f773d2b | ||
|
|
dbfe9a1d61 | ||
|
|
aec708361a | ||
|
|
5bfb67e177 | ||
|
|
fbfbd81fe4 | ||
|
|
f2c42487f3 | ||
|
|
697703c4b6 | ||
|
|
22413268ec | ||
|
|
4d89d856b1 | ||
|
|
98b9d86a5c | ||
|
|
07319f61bf | ||
|
|
c83ab22098 | ||
|
|
4ed093a492 | ||
|
|
02c6123825 | ||
|
|
34504f5124 | ||
|
|
0a727e16e8 | ||
|
|
017d545bc3 | ||
|
|
87a90dbb4f | ||
|
|
1d9fd83a73 | ||
|
|
0f6f1f2b53 | ||
|
|
ca32d49957 | ||
|
|
d544e4ec2a | ||
|
|
762580caf0 | ||
|
|
7a6a8d53f9 | ||
|
|
938995ea43 | ||
|
|
ee64868919 | ||
|
|
e7eb1097fa | ||
|
|
bb7ab319c8 | ||
|
|
49d3452c4f | ||
|
|
eb5efc27c9 | ||
|
|
d6efe32cda | ||
|
|
7c5a7dccb1 | ||
|
|
ae8d983b72 | ||
|
|
01fe281eb0 | ||
|
|
afea217934 | ||
|
|
d5a8f98500 | ||
|
|
3f79edd31e | ||
|
|
22e32f2a10 | ||
|
|
5adae487c7 | ||
|
|
76ed18fc76 | ||
|
|
163dfb7682 | ||
|
|
f982fa3abe | ||
|
|
67ceab7443 | ||
|
|
0518137a1e | ||
|
|
3ddfaaa899 | ||
|
|
aa34c22f39 | ||
|
|
a984a3e30a | ||
|
|
0a3c36dc80 | ||
|
|
851aa391c6 | ||
|
|
c0b42d4806 | ||
|
|
55df0d30d9 | ||
|
|
98b4ffc807 | ||
|
|
4334078d4a | ||
|
|
35c9cdd232 | ||
|
|
9bc2a8677b | ||
|
|
c49c1f46ef | ||
|
|
55a6daea7a | ||
|
|
7569436cdd | ||
|
|
b36a8e95dd | ||
|
|
1af93977ba | ||
|
|
34034e843b | ||
|
|
a5ef90ee22 | ||
|
|
48ff579bc8 | ||
|
|
5185cbb765 | ||
|
|
a702e4faf1 | ||
|
|
673cb3637a | ||
|
|
192a947524 | ||
|
|
9699907289 | ||
|
|
63bfa185f7 | ||
|
|
576d431378 | ||
|
|
f8c6142ce3 | ||
|
|
a7d5949604 | ||
|
|
9d87274c5a | ||
|
|
1e3346208f | ||
|
|
2154419801 | ||
|
|
20a656c1a5 | ||
|
|
7b73b0b5de | ||
|
|
2d5c2210e7 | ||
|
|
b8376cb765 | ||
|
|
7e9525f478 | ||
|
|
9dbe3c7aca | ||
|
|
a6b644f3ec | ||
|
|
63b104f2e7 | ||
|
|
9faf6fe757 | ||
|
|
da20f512aa | ||
|
|
d3fb640aaa | ||
|
|
270b018739 | ||
|
|
9e2aac2521 | ||
|
|
063e0daa15 | ||
|
|
4816f09e6c | ||
|
|
b681f96077 | ||
|
|
564f136845 | ||
|
|
c66742d4c4 | ||
|
|
fa296525c8 | ||
|
|
f34ca7c67c | ||
|
|
21f3ec70d9 | ||
|
|
55393ca73c | ||
|
|
2352f7918c | ||
|
|
ccb3aafb30 | ||
|
|
a05465a13d | ||
|
|
b9ddd4294e | ||
|
|
1e6da68c49 | ||
|
|
f92d86fbf3 | ||
|
|
005c269bbe | ||
|
|
117eaa8167 | ||
|
|
69ef2b2a3b | ||
|
|
6e59c7d7bb | ||
|
|
7de1f07597 | ||
|
|
5f16216a3f | ||
|
|
2c35d2d242 | ||
|
|
bd0fa73cce | ||
|
|
8d6f1014f0 | ||
|
|
3b29de3954 | ||
|
|
2021a06626 | ||
|
|
25a5b7beeb | ||
|
|
ee02924b14 | ||
|
|
48afb4da92 | ||
|
|
2f3d11ca61 | ||
|
|
de36344863 | ||
|
|
6172bd3e52 | ||
|
|
29f06b753a | ||
|
|
2aec75a3bc | ||
|
|
29744b36be | ||
|
|
818735dc2b | ||
|
|
998853c91d | ||
|
|
14ad6a8685 | ||
|
|
06a70fdd1e | ||
|
|
afc2730548 | ||
|
|
56bf786ecb | ||
|
|
a3772608f3 | ||
|
|
8a8fb98768 | ||
|
|
698257de29 | ||
|
|
c629dc5903 | ||
|
|
01273b73aa | ||
|
|
2b7faac10a | ||
|
|
4f41b6f793 | ||
|
|
9156a75c56 | ||
|
|
2c0a15e0f0 | ||
|
|
0352740ea1 | ||
|
|
fc096e1996 | ||
|
|
18fade571e | ||
|
|
896dfd6537 | ||
|
|
2d4d9c285a | ||
|
|
d961301907 | ||
|
|
472765b5e3 | ||
|
|
aae3502a8b | ||
|
|
ac8203e957 | ||
|
|
4fc08534c2 | ||
|
|
d2c06e95d6 | ||
|
|
f118935bb8 | ||
|
|
6c59326628 | ||
|
|
a176ffee20 | ||
|
|
76300b9c97 | ||
|
|
50179007f4 | ||
|
|
41432ab062 | ||
|
|
c9a68593ea | ||
|
|
5d14c96fb0 | ||
|
|
2bae46e28a | ||
|
|
be4b6cdd7f | ||
|
|
3ed94dd462 | ||
|
|
d3ff2ebe0a | ||
|
|
f4935b042f | ||
|
|
5d75f8c36d | ||
|
|
d10b93242c | ||
|
|
6a402a3b50 | ||
|
|
1f699f3552 | ||
|
|
4f1f32b508 | ||
|
|
3af023a4a8 | ||
|
|
72b81b8609 | ||
|
|
d99b406e09 | ||
|
|
a026806b1c | ||
|
|
a351b56737 | ||
|
|
6a7e0b2f10 | ||
|
|
f5e27faefc | ||
|
|
1bd7bf2169 | ||
|
|
8d193db721 | ||
|
|
ea950457f5 | ||
|
|
23840b7a0f | ||
|
|
37c0f5aec5 | ||
|
|
2a62f4dfe2 | ||
|
|
823afd4fd5 | ||
|
|
1ba1966e1c | ||
|
|
6105373d7f | ||
|
|
8bd386cde2 | ||
|
|
f8485b0e8c | ||
|
|
a286d104e9 | ||
|
|
49a7e15a8b | ||
|
|
e0ccbbb7c7 | ||
|
|
891d1268fb | ||
|
|
a59f4f330d | ||
|
|
ad3c5dafd7 | ||
|
|
b36689af20 | ||
|
|
e29670e55f | ||
|
|
14b0fd012c | ||
|
|
51acd43b16 | ||
|
|
bf075d291b | ||
|
|
f77f372d9f | ||
|
|
23e72452e3 | ||
|
|
20932b81b5 | ||
|
|
da30b5c805 | ||
|
|
b2b036a2be | ||
|
|
ef67c0c3bd | ||
|
|
4d774c6a7a | ||
|
|
2487d8995c | ||
|
|
d1e6173b50 | ||
|
|
e7cacdd7ee | ||
|
|
fb78598516 | ||
|
|
ec46584e54 | ||
|
|
4f09c6dc00 | ||
|
|
bfe78249fc | ||
|
|
c25fec313e | ||
|
|
8c4fed373b | ||
|
|
c01dd5cd90 | ||
|
|
5e7644bd18 | ||
|
|
6b9a3f8b69 | ||
|
|
db9bdb20bd | ||
|
|
2bf007542a | ||
|
|
a26198a6b0 | ||
|
|
9db27dfc2b | ||
|
|
b326b719bc | ||
|
|
817bd2a95e | ||
|
|
5758b08236 | ||
|
|
d532204f6b | ||
|
|
01fee28759 | ||
|
|
bd0d1cfba8 | ||
|
|
9ce6ec8d6f | ||
|
|
669fbed62b | ||
|
|
c4c646c4dd | ||
|
|
16903bdecb | ||
|
|
58bec11b2c | ||
|
|
24e812ce76 | ||
|
|
ea6946c48d | ||
|
|
512f101d26 | ||
|
|
fefeed0601 | ||
|
|
265d99095b | ||
|
|
7178a9e599 | ||
|
|
a3a7098ed2 | ||
|
|
243bf00308 | ||
|
|
f038652d3b | ||
|
|
f608957c7a | ||
|
|
4540c58e98 | ||
|
|
d7f9d25b74 | ||
|
|
d56b676ee9 | ||
|
|
1bfc11adfa | ||
|
|
4d6478454d | ||
|
|
9611c4c3de | ||
|
|
642371f87e | ||
|
|
63e83dff5d | ||
|
|
5fc0d691cf | ||
|
|
b54c42028c | ||
|
|
03aef9e4c1 | ||
|
|
51859f9a2b | ||
|
|
b05a16d007 | ||
|
|
e6a179bdb5 | ||
|
|
f13ab3a0e7 | ||
|
|
ebd784cfb2 | ||
|
|
15d6d3c641 | ||
|
|
542f71f0de | ||
|
|
c6e2221b24 | ||
|
|
04d1a10114 | ||
|
|
b0e9736005 | ||
|
|
bf32c85933 | ||
|
|
9050a7a64a | ||
|
|
f34ed8bd94 | ||
|
|
b719edfa57 | ||
|
|
7c0e3d584a | ||
|
|
5a8119e562 | ||
|
|
1d07f4e1ca | ||
|
|
3abf0dc3f3 | ||
|
|
3fb7e02653 | ||
|
|
0910f20c90 | ||
|
|
8fbb2688d9 | ||
|
|
d17b74e608 | ||
|
|
ba1cd66336 | ||
|
|
10b6e903c1 | ||
|
|
4445b8e01e | ||
|
|
fc426c73e0 | ||
|
|
c1b132c215 | ||
|
|
30dfa72622 | ||
|
|
75d2b70f20 | ||
|
|
55c2e75b1d | ||
|
|
6e8ae5d342 | ||
|
|
50e8c66c27 | ||
|
|
c4acf1774d | ||
|
|
28319f1510 | ||
|
|
d0bfdfd6ab | ||
|
|
5a0d6dd333 | ||
|
|
0778b13086 | ||
|
|
a53a88b9dc | ||
|
|
852a077085 | ||
|
|
543c33e26b | ||
|
|
9c214a658c | ||
|
|
d512655c5c | ||
|
|
3e35d7e6b4 | ||
|
|
2093ceb700 | ||
|
|
02fa306b46 | ||
|
|
d4277f5907 | ||
|
|
d82cf8f911 | ||
|
|
97b8a93a0c | ||
|
|
bcf628568c | ||
|
|
be0e298b1a | ||
|
|
cac77aaeda | ||
|
|
aec0fca5f8 | ||
|
|
8d24dff967 | ||
|
|
0d913f6bbc | ||
|
|
f766213d06 | ||
|
|
19d11b0882 | ||
|
|
f0db93c8c2 | ||
|
|
6893a68559 | ||
|
|
5e8ea67e21 | ||
|
|
d15b728390 | ||
|
|
992a37646e | ||
|
|
ff92c51c68 | ||
|
|
a0708ecf01 | ||
|
|
d5dbaea89a | ||
|
|
caac8ef802 | ||
|
|
268ee191e8 | ||
|
|
c5200c299c | ||
|
|
0b68146bfa | ||
|
|
eb844e8016 | ||
|
|
b0eea45f3b | ||
|
|
855d4b4ce2 | ||
|
|
0c340fa509 | ||
|
|
0cb9e5e970 | ||
|
|
d4af7f1ca3 | ||
|
|
f533b6321e | ||
|
|
33c95d77ed | ||
|
|
7f286da74e | ||
|
|
5c1547c284 | ||
|
|
eca7bcb515 | ||
|
|
17776d0659 | ||
|
|
8088f45d18 | ||
|
|
be63c35876 | ||
|
|
ead253afe8 | ||
|
|
4ab03c0093 | ||
|
|
8e715a3077 | ||
|
|
2a613a8427 | ||
|
|
07062e198c | ||
|
|
1fddd102dc | ||
|
|
b7f41acd19 | ||
|
|
71d993fab1 | ||
|
|
13fdb4d7ca | ||
|
|
4546a3acd2 | ||
|
|
cdb13da054 | ||
|
|
a2a1b2e8fd | ||
|
|
fb70f06fd4 | ||
|
|
31c6eb3b5c | ||
|
|
eb7e55ac7d | ||
|
|
eb5394a194 | ||
|
|
7e0091cba5 | ||
|
|
0db412d710 | ||
|
|
8669948221 | ||
|
|
5263f43c29 | ||
|
|
eb5c5faa99 | ||
|
|
67bbdeab3d | ||
|
|
7a9c6b56b7 | ||
|
|
cd1684b81c | ||
|
|
26d49198f0 | ||
|
|
18eb140017 | ||
|
|
d138999445 | ||
|
|
7cef629a06 | ||
|
|
76352df852 | ||
|
|
69a434b0e7 | ||
|
|
39c9d68c4f | ||
|
|
da5cc69a06 | ||
|
|
6c2e27870d | ||
|
|
d667288a87 | ||
|
|
165239c971 | ||
|
|
d68b9d6a86 | ||
|
|
359b1093bc | ||
|
|
ebb8f16e44 | ||
|
|
6931a78bed | ||
|
|
c7dac12cd9 | ||
|
|
a505808549 | ||
|
|
a92f82ef28 | ||
|
|
ae0b4f5c4c | ||
|
|
0e72f5ad9d | ||
|
|
0c21681717 | ||
|
|
f2a9171dd8 | ||
|
|
0e2c33dd78 | ||
|
|
158460bc77 | ||
|
|
b5bd4cbf53 | ||
|
|
2dfe535e0b | ||
|
|
92c04a9ba4 | ||
|
|
5f3031db4a | ||
|
|
c81e78b5bb | ||
|
|
fb707aa676 | ||
|
|
147dbc878a | ||
|
|
06bd6d9bed | ||
|
|
7f6272807d | ||
|
|
89d948b4e1 | ||
|
|
51c42b89b4 | ||
|
|
5a319ffe2c | ||
|
|
bae660381c | ||
|
|
b2f1d007c6 | ||
|
|
33e9c78626 | ||
|
|
6f50545c31 | ||
|
|
ab1dd45e9c | ||
|
|
cd5da3da5d | ||
|
|
218af5a8b3 | ||
|
|
e786c00767 | ||
|
|
103ae60631 | ||
|
|
98d0f1913e | ||
|
|
8aa93f4e34 | ||
|
|
3689b893db | ||
|
|
fbec7dc083 | ||
|
|
7cf7623438 | ||
|
|
8a54f253d5 | ||
|
|
d0baa16502 | ||
|
|
5e4094952a | ||
|
|
99d8eb9265 | ||
|
|
a9a0201ae9 | ||
|
|
5fe248eb45 | ||
|
|
4178c382e1 | ||
|
|
775a9bf144 | ||
|
|
c4fb32a56d | ||
|
|
54a00e2614 | ||
|
|
19ed4d16db | ||
|
|
d220532b16 | ||
|
|
6b035cb76e | ||
|
|
a3e3d9d0aa | ||
|
|
f0da1b3a93 | ||
|
|
04c3b84fc0 | ||
|
|
a9f2d3cf7f | ||
|
|
57cd2f12d0 | ||
|
|
b2066fc022 | ||
|
|
4639748461 | ||
|
|
42e118db68 | ||
|
|
8b375b9179 | ||
|
|
e4ad307413 | ||
|
|
e7609329d7 | ||
|
|
64a8a9c1dc | ||
|
|
51aa6d782a | ||
|
|
e040e5e449 | ||
|
|
3aca691ea0 | ||
|
|
a0a7da9254 | ||
|
|
960920993a | ||
|
|
e1df6557d1 | ||
|
|
34f4314b86 | ||
|
|
bd99c1ab27 | ||
|
|
2e364ac0eb | ||
|
|
0dcfbf56c6 | ||
|
|
399572d9d6 | ||
|
|
e3c3b50ac1 | ||
|
|
e21631526b | ||
|
|
cf0ad9b323 | ||
|
|
938692ef33 | ||
|
|
84c49f152e | ||
|
|
04dd177f19 | ||
|
|
2e074e18d4 | ||
|
|
4f04122eb8 | ||
|
|
4475c2af73 | ||
|
|
ed52ed1e73 | ||
|
|
b67b4fd57f | ||
|
|
1ba438d4c7 | ||
|
|
77c5fbd739 | ||
|
|
756cb7bd9d | ||
|
|
a8c06386a3 | ||
|
|
b3038010d9 | ||
|
|
ed6c774f08 | ||
|
|
c37209618d | ||
|
|
0048452efa | ||
|
|
c4a28e7cfb | ||
|
|
29197f2142 | ||
|
|
9c7d27d1e0 | ||
|
|
2a4081362e | ||
|
|
d5cab8172b | ||
|
|
4084b347d4 | ||
|
|
5437d9ff8c | ||
|
|
7b746c49cf | ||
|
|
1da178a4be | ||
|
|
2b74807f5e | ||
|
|
38d295b089 | ||
|
|
690e1d8f6b | ||
|
|
c48388ead2 | ||
|
|
cc6e410be8 | ||
|
|
87cab8e16a | ||
|
|
e85752ca1d | ||
|
|
2860896640 | ||
|
|
8b6d96c9ca | ||
|
|
c96acf18dc | ||
|
|
6eec6faff2 | ||
|
|
118fbbea64 | ||
|
|
bbc664ddcc | ||
|
|
2fa8196105 | ||
|
|
d4f1dafabb | ||
|
|
f2483be1da | ||
|
|
7914b7ddaf | ||
|
|
0848a1b940 | ||
|
|
ca262920c8 | ||
|
|
2541c09973 | ||
|
|
2f9b6422c3 | ||
|
|
c21c7eb5ee | ||
|
|
4e4de625ae | ||
|
|
3fb8387419 | ||
|
|
240468f968 | ||
|
|
778a930266 | ||
|
|
db544211f5 | ||
|
|
d12b539720 | ||
|
|
d26db95aff | ||
|
|
7231b7b35c | ||
|
|
73fd2d70ba | ||
|
|
c026e02390 | ||
|
|
9676d6ccc9 | ||
|
|
fad7cf3b1d | ||
|
|
17cb952772 | ||
|
|
923217ef0e | ||
|
|
0d81d7f243 | ||
|
|
2070ab282f | ||
|
|
faacff414d | ||
|
|
642b8bddb8 | ||
|
|
27f7221902 | ||
|
|
eb738b44a8 | ||
|
|
57c0d00b37 | ||
|
|
43276b32b7 | ||
|
|
94b275c41b | ||
|
|
ae4ae799b9 | ||
|
|
9def35f78a | ||
|
|
7e8005a9d8 | ||
|
|
b93ed79ef3 | ||
|
|
c7b8021c2e | ||
|
|
3af0e1091c | ||
|
|
1323c96bc8 | ||
|
|
94716a5ba9 | ||
|
|
f67168f479 | ||
|
|
7e54f91bfa | ||
|
|
3ab4478275 | ||
|
|
8759cf08ff | ||
|
|
8cff7cb551 | ||
|
|
1b0ec2410d | ||
|
|
22d15f73f0 | ||
|
|
4e0d78d6c8 | ||
|
|
63a1927838 | ||
|
|
3d745bfa05 | ||
|
|
ce26d50792 | ||
|
|
01e162fcc4 | ||
|
|
443f7b8b0c | ||
|
|
08d64ee8a1 | ||
|
|
eedc7c7a0f | ||
|
|
3198b32008 | ||
|
|
dff17a95e8 | ||
|
|
236ee215b9 | ||
|
|
6af612fd54 | ||
|
|
361e695380 | ||
|
|
8a8784a974 | ||
|
|
ddc01527bd | ||
|
|
e6ad44def8 | ||
|
|
df63e152c1 | ||
|
|
ad7e64ba43 | ||
|
|
8737b7a38e | ||
|
|
13199fcfd1 | ||
|
|
9b8e3d8530 | ||
|
|
0b15b60506 | ||
|
|
46e2ea8507 | ||
|
|
62b8fc92d1 | ||
|
|
cb27b117b4 | ||
|
|
6112b2d1d9 | ||
|
|
0e2f8068a6 | ||
|
|
227e70926b | ||
|
|
86e412cf77 | ||
|
|
037b6842bf | ||
|
|
895dd02830 | ||
|
|
7112dba345 | ||
|
|
0767292bb0 | ||
|
|
b40e1a002a | ||
|
|
c096164486 | ||
|
|
d92def91b0 | ||
|
|
b48e938c98 | ||
|
|
4ed1f89866 | ||
|
|
525bcea989 | ||
|
|
6c3f7c86da | ||
|
|
595a97bd62 | ||
|
|
c9db03335b | ||
|
|
659f5d7834 | ||
|
|
42f6c77419 | ||
|
|
bcaec86514 | ||
|
|
1ca914823f | ||
|
|
be8841075a | ||
|
|
a94e319935 | ||
|
|
d60abc3f45 | ||
|
|
b994f0ab55 | ||
|
|
e2cb9f39ab | ||
|
|
58459bda12 | ||
|
|
8dfd5349f0 | ||
|
|
c770bc4788 | ||
|
|
24b4ef8780 | ||
|
|
6ddcbf2bc9 | ||
|
|
8d5d686b79 | ||
|
|
f4369e540a | ||
|
|
c219eb4abb | ||
|
|
9a7bdd39e8 | ||
|
|
484bc740d7 | ||
|
|
8a67d501a8 | ||
|
|
3c87242bd0 | ||
|
|
999124e68e | ||
|
|
db2f5c9cb9 | ||
|
|
af08a7cd54 | ||
|
|
72eaf66f05 | ||
|
|
8a3be35d67 | ||
|
|
d59e4c2a0d | ||
|
|
71655e04c4 | ||
|
|
a706a9d725 | ||
|
|
970934a40b | ||
|
|
a9c28a7b62 | ||
|
|
4d31436740 | ||
|
|
c82744c51c | ||
|
|
f0eaddc6a6 | ||
|
|
d6156f0f1e | ||
|
|
3c88c7fdd5 | ||
|
|
d11f6aefb0 | ||
|
|
010a38bb73 | ||
|
|
53f12d75db | ||
|
|
6c866324b2 | ||
|
|
a114155189 | ||
|
|
014bbc3ea4 | ||
|
|
d52facd4ab | ||
|
|
0a50386ada | ||
|
|
f64fa53ed1 | ||
|
|
335db1eb9e | ||
|
|
27fe2d807e | ||
|
|
27cf2795ef | ||
|
|
979c8d9234 | ||
|
|
04eabb7866 | ||
|
|
f4eccea20c | ||
|
|
fc63217dd0 | ||
|
|
b9fc680548 | ||
|
|
7dcc13c1f0 | ||
|
|
56d3091688 | ||
|
|
a0af0604d1 | ||
|
|
875c2d7694 | ||
|
|
8c95f863cb | ||
|
|
ddf37e82c2 | ||
|
|
a82fda98d7 | ||
|
|
44e4b1804e | ||
|
|
984ea8f46f | ||
|
|
4d84394417 | ||
|
|
507074cf72 | ||
|
|
f364b68e09 | ||
|
|
244acc0c9e | ||
|
|
def0bce221 | ||
|
|
d0823db595 | ||
|
|
73704bcd7e | ||
|
|
86c3b93fa5 | ||
|
|
e89fb211ce | ||
|
|
55100cb37a | ||
|
|
14fd880dac | ||
|
|
4ca1a7713e | ||
|
|
9caf7be928 | ||
|
|
bf4217fde6 | ||
|
|
de3cda9034 | ||
|
|
f1102dc980 | ||
|
|
f2bb81d611 | ||
|
|
41e0c151de | ||
|
|
d0f44839ff | ||
|
|
414cad1241 | ||
|
|
abe0087cfd | ||
|
|
060b1ed5dd | ||
|
|
fbc168fafe | ||
|
|
d5a8c37113 | ||
|
|
21ce9a38e6 | ||
|
|
7732fbb698 | ||
|
|
44402414a6 | ||
|
|
11b92d102a | ||
|
|
16831e1e6e | ||
|
|
3b83d08e2a | ||
|
|
b622a2ce73 | ||
|
|
c5814502f6 | ||
|
|
d9be6389ca | ||
|
|
336a8f2876 | ||
|
|
b4f949ba9b | ||
|
|
9403c12793 | ||
|
|
77c1cebefc | ||
|
|
caa7acdbc5 | ||
|
|
1522e64797 | ||
|
|
5152e3197e | ||
|
|
ae4f2c08fd | ||
|
|
9628f2dda1 | ||
|
|
65208a30c1 | ||
|
|
4c838db876 | ||
|
|
d1f6fd9af1 | ||
|
|
61c6f8403a | ||
|
|
a8f47237d7 | ||
|
|
7cbdc5b46c | ||
|
|
8b74243e79 | ||
|
|
f77a2aabab | ||
|
|
e6fb704780 | ||
|
|
c5d7407919 | ||
|
|
b9ed2b09c1 | ||
|
|
db0bb8a30d | ||
|
|
85d82e5c86 | ||
|
|
1a87075f33 | ||
|
|
8df9232171 | ||
|
|
7b7eba8cd9 | ||
|
|
169364f6ae | ||
|
|
c38f74d34c | ||
|
|
895699d22e | ||
|
|
cf901c2784 | ||
|
|
e1bbc65f5b | ||
|
|
f6faaa83ec | ||
|
|
4d3ae3a97f | ||
|
|
cc47fde57f | ||
|
|
a8d5b8e86b | ||
|
|
0a9c5bfe15 | ||
|
|
ff0769e988 | ||
|
|
3987733079 | ||
|
|
b26d38b7e4 | ||
|
|
2175cb299d | ||
|
|
8abb972f87 | ||
|
|
05bf22f518 | ||
|
|
3b5ee481c5 | ||
|
|
b54617238b | ||
|
|
44174e7b03 | ||
|
|
ecd9317ab3 | ||
|
|
884f0b702e | ||
|
|
2e95832bea | ||
|
|
97879f18b6 | ||
|
|
d74454f7e9 | ||
|
|
ce01cc7ce1 | ||
|
|
9454466be7 | ||
|
|
0e4a159998 | ||
|
|
4998676476 | ||
|
|
f359f553b3 | ||
|
|
08118f7286 | ||
|
|
408d2e6663 | ||
|
|
4c4b727bd7 | ||
|
|
8e54b10b7f | ||
|
|
f99e421115 | ||
|
|
82d56b9678 | ||
|
|
447d35267f | ||
|
|
763805e03a | ||
|
|
cd70a7760e | ||
|
|
ec7c359341 | ||
|
|
cc59c1960a | ||
|
|
1cc80e7675 | ||
|
|
cfe59fc515 | ||
|
|
2180df3318 | ||
|
|
29f4c7fe2e | ||
|
|
d5f9815561 | ||
|
|
6e5e47f041 | ||
|
|
b0fde2b8c7 | ||
|
|
4b9b20de42 | ||
|
|
f7c91bb26f | ||
|
|
4b34dd61d3 | ||
|
|
c958d184d0 | ||
|
|
0de5c59a77 | ||
|
|
3b10be53b5 | ||
|
|
5336eb6fe6 | ||
|
|
9fa686b8f9 | ||
|
|
2012077fb0 | ||
|
|
302cf2e14f | ||
|
|
b9fccbd691 | ||
|
|
bbbbc8810a | ||
|
|
c7da19f3a5 | ||
|
|
e919930cf6 | ||
|
|
2906529080 | ||
|
|
75cc36849b | ||
|
|
63e124e72f | ||
|
|
394d982168 | ||
|
|
f4ce4cbad8 | ||
|
|
491beabe03 | ||
|
|
f5d83c0e33 | ||
|
|
d2cb792d91 | ||
|
|
52fef492a5 | ||
|
|
a5300f3383 | ||
|
|
cab3e7d498 | ||
|
|
405dbb30cd | ||
|
|
9bb670ad19 | ||
|
|
bbba2e6c7a | ||
|
|
0b2158f74c | ||
|
|
5c06b8c48f | ||
|
|
810df8a18b | ||
|
|
63736efcac | ||
|
|
3e219fa9ec | ||
|
|
33b6447c18 | ||
|
|
ec310c87de | ||
|
|
c55a383112 | ||
|
|
227fec66ad | ||
|
|
38eec23e07 | ||
|
|
437271c6a2 | ||
|
|
81a2060c75 | ||
|
|
5e90cfd6c5 | ||
|
|
2d597e6e43 | ||
|
|
a410f73bf3 | ||
|
|
b41296194f | ||
|
|
bf7416753a | ||
|
|
7fbd751d27 | ||
|
|
85b28dfe68 | ||
|
|
779fba5deb | ||
|
|
2756b1f6f8 | ||
|
|
7b76858ae1 | ||
|
|
84b5b5a717 | ||
|
|
ebfa16f09f | ||
|
|
c1d240b516 | ||
|
|
5c4719f4a9 | ||
|
|
95d2278d8b | ||
|
|
e63ee57ef0 | ||
|
|
775efa1b26 | ||
|
|
3677b4b193 | ||
|
|
c32f4b4f2a | ||
|
|
8d34f88ca6 | ||
|
|
ca4cf25a1f | ||
|
|
4fa976b478 | ||
|
|
767f3a4985 | ||
|
|
ddc08e068e | ||
|
|
a1a86cdde8 | ||
|
|
fc85f1fa2c | ||
|
|
0b8081f320 | ||
|
|
0b5371d986 | ||
|
|
cdca909d84 | ||
|
|
ec7d2e357d | ||
|
|
1c0183ef11 | ||
|
|
9cf2b3129c | ||
|
|
9a4e864f5e | ||
|
|
4df952db1b | ||
|
|
1b4cedfa13 | ||
|
|
6d6103afd6 | ||
|
|
d727d76299 | ||
|
|
0502ff545e | ||
|
|
fce4a96718 | ||
|
|
38203fb950 | ||
|
|
65dba509e0 | ||
|
|
0615d17b8b | ||
|
|
c1f8580d89 | ||
|
|
c5a6f900df | ||
|
|
3807c3ce2a | ||
|
|
d2f328af01 | ||
|
|
c3ffcf4fe8 | ||
|
|
95c409d979 | ||
|
|
dadf9e71bb | ||
|
|
05ebf22009 | ||
|
|
f4b1780d8a | ||
|
|
921cff00a5 | ||
|
|
64a09eb0f8 | ||
|
|
3a636531e8 | ||
|
|
292da90184 | ||
|
|
90c8ff8650 | ||
|
|
0f37049aad | ||
|
|
3fb07d129f | ||
|
|
8d0ac5062f | ||
|
|
b2d1505e5c | ||
|
|
a35e65ee42 | ||
|
|
d1fcf31f7e | ||
|
|
17cd934b5b | ||
|
|
ae614cd3fe | ||
|
|
47037a4b9d | ||
|
|
507b1e35d8 | ||
|
|
2839e1d33f | ||
|
|
ef0b9d525c | ||
|
|
01e6e04a78 | ||
|
|
8c876c70af | ||
|
|
a7d6d6493e | ||
|
|
b6cc95af73 | ||
|
|
bdcc1d32c2 | ||
|
|
90d91cc7c2 | ||
|
|
ec83cf588f | ||
|
|
4f0a2e3c95 | ||
|
|
3747a27109 | ||
|
|
b501bac54a | ||
|
|
7aec98d652 | ||
|
|
406b198e0e | ||
|
|
8e42296c3a | ||
|
|
60e5220bd0 | ||
|
|
0f37cbfd0b | ||
|
|
541fd136d5 | ||
|
|
60d42ca9c3 | ||
|
|
5c947bccc7 | ||
|
|
fbdefc17c1 | ||
|
|
1425c6ff0d | ||
|
|
e038aea694 | ||
|
|
87ccbf329d | ||
|
|
9dcf384263 | ||
|
|
ac0716ddeb | ||
|
|
b9792ca491 | ||
|
|
c3fd42057a | ||
|
|
39d85ff4f6 | ||
|
|
fbce3e77ba | ||
|
|
66e9de2685 | ||
|
|
321520408b | ||
|
|
68451bd75f | ||
|
|
486fdf118b | ||
|
|
bd3cda0617 | ||
|
|
725503d1ce | ||
|
|
be045c4f15 | ||
|
|
fd968b3f78 | ||
|
|
833dd7b3a2 | ||
|
|
b9258ad496 | ||
|
|
0678318dde | ||
|
|
7116c0d098 | ||
|
|
2fde8436fb | ||
|
|
61ecd6475f | ||
|
|
64b209a772 | ||
|
|
48dbdbfed5 | ||
|
|
cf50311b9c | ||
|
|
e4d8582a2a | ||
|
|
b825e15406 | ||
|
|
b8e5ee45eb | ||
|
|
9f20a9e7d2 | ||
|
|
201521d814 | ||
|
|
18bb3cba11 | ||
|
|
af951d6f6a | ||
|
|
e5fe92bf90 | ||
|
|
b1aca7c305 | ||
|
|
c0a0cc4a44 | ||
|
|
7a49eb9e93 | ||
|
|
5aa0610882 | ||
|
|
41ed4c8186 | ||
|
|
90a33ce6b0 | ||
|
|
12574798e1 | ||
|
|
83b11254db | ||
|
|
b25493fd29 | ||
|
|
bb66100486 | ||
|
|
9bd86f64c9 | ||
|
|
3ebf97dd49 | ||
|
|
bfcdeb3784 | ||
|
|
77015224f6 | ||
|
|
372ecb77d0 | ||
|
|
05642cbdc6 | ||
|
|
feece3d788 | ||
|
|
94adb77e9e | ||
|
|
909d36c237 | ||
|
|
e1c8aa226d | ||
|
|
1a71c01fd4 | ||
|
|
54640548ed | ||
|
|
2327679f23 | ||
|
|
574d9f970c | ||
|
|
235096a2eb | ||
|
|
a739fdc544 | ||
|
|
6196e26044 | ||
|
|
46a4064989 | ||
|
|
72b3948f43 | ||
|
|
5d47bfaeb6 | ||
|
|
b2f2ea65ba | ||
|
|
7afa468e15 | ||
|
|
29cb1fed12 | ||
|
|
29f8b512c4 | ||
|
|
9794c8ba72 | ||
|
|
f1b305f682 | ||
|
|
091a800c9d | ||
|
|
975f5f4b4c | ||
|
|
9d6cd930ea | ||
|
|
ea02d31096 | ||
|
|
d5913e8371 | ||
|
|
7f71f76f6e | ||
|
|
d04830ba90 | ||
|
|
caa82a6146 | ||
|
|
bcf7137073 | ||
|
|
9238cf1128 | ||
|
|
2f874ace51 | ||
|
|
2feabed297 | ||
|
|
9001e28b36 | ||
|
|
24d651d7ae | ||
|
|
8533b09091 | ||
|
|
44b7955d85 | ||
|
|
038b9f18c6 | ||
|
|
5667595587 | ||
|
|
6e0e20ba6e | ||
|
|
ec31cb9987 | ||
|
|
32d5b61c4a | ||
|
|
128c9e639f | ||
|
|
5e3f01dc03 | ||
|
|
39a0b4ce78 | ||
|
|
af719707bf | ||
|
|
8415a19912 | ||
|
|
1607d2768e | ||
|
|
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 | ||
|
|
fb76bd82f2 | ||
|
|
3bdaba46a9 | ||
|
|
9433e06b93 | ||
|
|
a92aaa51d5 | ||
|
|
7c3c94ed7f | ||
|
|
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 | ||
|
|
51bc9dc20e | ||
|
|
e45ac6bfa9 | ||
|
|
44d1cbdb0c | ||
|
|
3fa7e854d3 | ||
|
|
0f5c080be6 | ||
|
|
871009ef8d | ||
|
|
89a565c6c4 | ||
|
|
6ed60e4d5f | ||
|
|
4dab1df3ae | ||
|
|
7b7aa2cdf0 | ||
|
|
d1e1a65f32 | ||
|
|
9d332b4ac5 | ||
|
|
fc2462e281 | ||
|
|
fb2cddabf0 | ||
|
|
33481a9465 | ||
|
|
45de87cb97 | ||
|
|
553c533ada | ||
|
|
634e562b93 | ||
|
|
c789ead590 | ||
|
|
d23560a441 | ||
|
|
9f306966db | ||
|
|
8eea2cef97 | ||
|
|
3abbb89c2e | ||
|
|
76de7d54c7 | ||
|
|
35aee6e066 | ||
|
|
8a0152fe7c | ||
|
|
7769435fc8 | ||
|
|
ae311ea66e | ||
|
|
d15816f162 | ||
|
|
5cbb31c559 | ||
|
|
b7f5b36f2b | ||
|
|
e9dd413de6 | ||
|
|
c5de3d8cc1 | ||
|
|
624c385e27 | ||
|
|
893890c985 | ||
|
|
e12efc8b52 | ||
|
|
015dfa41b1 | ||
|
|
5e72ce5fbe | ||
|
|
236e653176 | ||
|
|
3946c3ba03 | ||
|
|
5bdd39e6eb | ||
|
|
46267c0531 | ||
|
|
7b96eb7704 | ||
|
|
60d5955101 | ||
|
|
587675e488 | ||
|
|
c0ae319c63 | ||
|
|
d810f8d8db | ||
|
|
a88390a546 | ||
|
|
8575e5615d | ||
|
|
5fe9516c09 | ||
|
|
938775789d | ||
|
|
f37bebe51f | ||
|
|
21f14cd3f2 | ||
|
|
4e7baeb2c9 | ||
|
|
d78a682dd8 | ||
|
|
44021b7abc | ||
|
|
7f9e543213 | ||
|
|
c489e5a59b | ||
|
|
8de30db7f9 | ||
|
|
415f0c6774 | ||
|
|
aa9cf195a5 | ||
|
|
6412fc8135 | ||
|
|
8b1ba9a00c | ||
|
|
b02584ef9e | ||
|
|
88cd6bfd1a | ||
|
|
89a3da14c9 | ||
|
|
619d987492 | ||
|
|
744e410a1a | ||
|
|
6e51967993 | ||
|
|
7989ab2e28 | ||
|
|
a3570a568d | ||
|
|
c0cd998836 | ||
|
|
1a452acd1c | ||
|
|
6c54083b08 | ||
|
|
76105ebdaf | ||
|
|
2c452201fa | ||
|
|
9fc7fc73f9 | ||
|
|
e7417e35ba | ||
|
|
19eb4c7cba | ||
|
|
1f39a0c9da | ||
|
|
a480a8893c | ||
|
|
d3a9e341a5 | ||
|
|
95caffe607 | ||
|
|
fdeca61284 | ||
|
|
45dc653191 | ||
|
|
6e48302b7b | ||
|
|
a7918d7b11 | ||
|
|
ec21253af9 | ||
|
|
97cdda8663 | ||
|
|
35ac2b9d71 | ||
|
|
14affd8ffa | ||
|
|
c48409c903 | ||
|
|
227f66f299 | ||
|
|
c2d9bf94d1 | ||
|
|
8f03841161 | ||
|
|
344a1ed912 | ||
|
|
ff71993b31 | ||
|
|
18bb773b2c | ||
|
|
7f905c7940 | ||
|
|
8a4ba6fc56 | ||
|
|
e0b275c01c | ||
|
|
5926db85df | ||
|
|
a4715a10e7 | ||
|
|
de0f5c0fcb | ||
|
|
88d85ce6ac | ||
|
|
131c171d3e | ||
|
|
c5ddef6ef7 | ||
|
|
f36486d006 | ||
|
|
eaf466c5c3 | ||
|
|
b41bcc349e | ||
|
|
825c61bdcd | ||
|
|
f13f49bd71 | ||
|
|
88f5d4544b | ||
|
|
b7432a20f0 | ||
|
|
ab57c85e66 | ||
|
|
c7c41bc0d8 | ||
|
|
c66f9b06a4 | ||
|
|
a3ac7e7ca7 | ||
|
|
187b762f8a | ||
|
|
3a7fbdfa52 | ||
|
|
bbeb46b37f | ||
|
|
5450b9a700 | ||
|
|
353b28647c | ||
|
|
2411f4f870 | ||
|
|
34cd059423 | ||
|
|
4b4ec12c87 | ||
|
|
441147c0dc | ||
|
|
08ceac9f46 | ||
|
|
6bc690b50b | ||
|
|
c41888b338 | ||
|
|
6d3c980b39 | ||
|
|
eb5db176fa | ||
|
|
a77c03fcaf | ||
|
|
04b25011e9 | ||
|
|
96b34aec10 | ||
|
|
d560b46c85 | ||
|
|
94789ce455 | ||
|
|
55797e5873 | ||
|
|
675cab3074 | ||
|
|
8d63ab646e | ||
|
|
37a2fa1cdc | ||
|
|
79ed9650b3 | ||
|
|
b6dc58164e | ||
|
|
f9a974c1fe | ||
|
|
91e672befb | ||
|
|
c14d18b9df | ||
|
|
d2cfc1844c | ||
|
|
2fb3243a1a | ||
|
|
6f3124a84f | ||
|
|
59eb5bd9c4 | ||
|
|
ad2b6bb4f7 | ||
|
|
51d64ee10e | ||
|
|
1159efc6cc | ||
|
|
3cfb76a56f | ||
|
|
7a1ad6688a | ||
|
|
4f4af6e3fd | ||
|
|
2736b79097 | ||
|
|
8da2ffa433 | ||
|
|
5bf3d6dc32 | ||
|
|
30d4380afe | ||
|
|
c8b5aad482 | ||
|
|
fa1d440947 | ||
|
|
a89fc3a276 | ||
|
|
3875f62453 | ||
|
|
23e1447ab6 | ||
|
|
91364c6a7c | ||
|
|
a070493fba | ||
|
|
ca0a6ab72c | ||
|
|
10e0562a82 | ||
|
|
44998a2a2b | ||
|
|
f8e033e560 | ||
|
|
5842da84af | ||
|
|
10cc270170 | ||
|
|
f51bdd56aa | ||
|
|
7f79c49f93 | ||
|
|
e4920255f0 | ||
|
|
f8e8273d39 | ||
|
|
ceddab98d1 | ||
|
|
0faa176791 | ||
|
|
9b0e0ee536 | ||
|
|
881d2756db | ||
|
|
ab5af40999 | ||
|
|
6aeef8dcf9 | ||
|
|
5d62196dde | ||
|
|
7fe27ac310 | ||
|
|
1e300349ce | ||
|
|
6b01a7b08e | ||
|
|
34d948df18 | ||
|
|
27091c4f1d | ||
|
|
970a3d7f2a | ||
|
|
2bedbbfc54 | ||
|
|
8de33f0db3 | ||
|
|
df4eab67dc | ||
|
|
2d13bc0046 | ||
|
|
704a0cb35d | ||
|
|
06a983e445 | ||
|
|
2d10df8b87 | ||
|
|
baf66db9dc | ||
|
|
3545693999 | ||
|
|
84fb99c2c8 | ||
|
|
21e2983ae1 | ||
|
|
004e0941d5 | ||
|
|
188a426dde | ||
|
|
6fcfae8bfe | ||
|
|
9515350f52 | ||
|
|
6b88f82656 | ||
|
|
e42d953f47 | ||
|
|
471a3f63fe | ||
|
|
9c257faf26 | ||
|
|
d18cad6d76 | ||
|
|
061273be22 | ||
|
|
b89de6caad | ||
|
|
9bc0a1d7c7 | ||
|
|
eee518b7fa | ||
|
|
b7b78edaa3 | ||
|
|
3eaac68ab4 | ||
|
|
a7672190e9 | ||
|
|
4e4e89b6eb | ||
|
|
33dd519f56 | ||
|
|
5c1149aa8b | ||
|
|
9061e92af6 | ||
|
|
49f5ceaa9b | ||
|
|
525b309d37 | ||
|
|
bdb3a787fc | ||
|
|
a9601ef848 | ||
|
|
6fc4b045fd | ||
|
|
446852c7d0 | ||
|
|
c635f00899 | ||
|
|
1393629bc5 | ||
|
|
49ce17b759 | ||
|
|
74888021c8 | ||
|
|
9483856439 | ||
|
|
dbbc7c8132 | ||
|
|
5d9c99508d | ||
|
|
e4d5b56951 | ||
|
|
e31238d121 | ||
|
|
a283d99e1b | ||
|
|
8cb621ebed | ||
|
|
b203d165f5 | ||
|
|
c695e1136d | ||
|
|
d847202308 | ||
|
|
9d24e0a4b8 | ||
|
|
745fe1eb9f | ||
|
|
3e52b85e9d | ||
|
|
d26f020b50 | ||
|
|
095b5f702c | ||
|
|
9622853b8d | ||
|
|
b94e75fabe | ||
|
|
23dd041e2e | ||
|
|
c73ca21b4d | ||
|
|
7ebdc85ad2 | ||
|
|
99e2c8c90d | ||
|
|
f24bfdf945 | ||
|
|
7963233702 | ||
|
|
b550df2038 | ||
|
|
fb55624f5f | ||
|
|
e96366f489 | ||
|
|
900190cf54 | ||
|
|
2af744b474 | ||
|
|
11153084e2 | ||
|
|
4b9c814bfc | ||
|
|
1b5d3a3b6e | ||
|
|
373637e6a7 | ||
|
|
cb223217c1 | ||
|
|
eab97a3f8b | ||
|
|
fdfaa8ab45 | ||
|
|
2321d9dbee | ||
|
|
bf74dd887a | ||
|
|
3612035894 | ||
|
|
6553e9b0cd | ||
|
|
09f2410170 | ||
|
|
fb73d8c0a7 | ||
|
|
f2b0368078 | ||
|
|
02301ecf6d | ||
|
|
bcb61ee3e4 | ||
|
|
6a824429d0 | ||
|
|
6a52f9097f | ||
|
|
3fa85fc516 | ||
|
|
498d132d8a | ||
|
|
b6340f1458 | ||
|
|
4afc7ae2e4 | ||
|
|
95975a4c33 | ||
|
|
198a0673a2 | ||
|
|
94d1503c64 | ||
|
|
5f13e245f0 | ||
|
|
2715ae645d | ||
|
|
0299232cb5 | ||
|
|
93e181cfd9 | ||
|
|
8072eb1212 | ||
|
|
226ce340f2 | ||
|
|
ab5535eba3 | ||
|
|
8da2499495 | ||
|
|
c057ffb153 | ||
|
|
fe13d29549 | ||
|
|
225aaab4f4 | ||
|
|
14c973558b | ||
|
|
f515ff36b6 | ||
|
|
ed57cfd2f9 | ||
|
|
d69559e9c7 | ||
|
|
396717efd1 | ||
|
|
284fa24464 | ||
|
|
0a20b9179a | ||
|
|
a0d5037885 | ||
|
|
4477833b1d | ||
|
|
e0a5ed4bdb | ||
|
|
46d4b26eba | ||
|
|
f7c6edf849 | ||
|
|
6c157def4b | ||
|
|
741712f89f | ||
|
|
4f749da628 | ||
|
|
8b02795d69 | ||
|
|
f8a0069a5d | ||
|
|
388bbe047e | ||
|
|
2d4ce30e58 | ||
|
|
d4fb17cf66 | ||
|
|
372a2c8375 | ||
|
|
8f27121f21 | ||
|
|
b986bf675f | ||
|
|
80718a461b | ||
|
|
2d14ecf58b | ||
|
|
32aa9877c0 | ||
|
|
cee3a9c11d | ||
|
|
b78643f2d8 | ||
|
|
30a31de45b | ||
|
|
e4c4db534c | ||
|
|
4f7a0d3ad0 | ||
|
|
ea3a96eead | ||
|
|
c0e01ac132 | ||
|
|
28ea50bca4 | ||
|
|
619e44b30f | ||
|
|
d678275dee | ||
|
|
08eed53595 | ||
|
|
ff40f7d262 | ||
|
|
3c1ae51dae | ||
|
|
8a59fc9aaf | ||
|
|
b7ea9dd841 | ||
|
|
0320db6b4a | ||
|
|
18c7f58093 | ||
|
|
7f6f7b1436 | ||
|
|
ca49176b97 | ||
|
|
67be0cd9d7 | ||
|
|
902fadef83 | ||
|
|
2777b6411f | ||
|
|
e3235d7f04 | ||
|
|
dc89c8858e | ||
|
|
d28a278d63 | ||
|
|
7080c2abd0 | ||
|
|
43f86bcab8 | ||
|
|
7d9c875c4d | ||
|
|
ed4099eb12 |
1146 changed files with 161553 additions and 18922 deletions
22
.agents/skills/rar-format/SKILL.md
Normal file
22
.agents/skills/rar-format/SKILL.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
name: rar-format
|
||||
description: Reference the RAR/RAR5 archive container format and UnRAR source behavior. Use when an AI agent needs to answer questions or make code changes involving RAR signatures, RAR5 vint fields, block headers, file/service headers, extra records, solid archives, multivolume parts, encryption headers, recovery records, quick-open data, or SharpCompress Rar parsing and extraction behavior.
|
||||
---
|
||||
|
||||
# Rar Format
|
||||
|
||||
Use this skill for RAR container-format work. It provides a local, SharpCompress-oriented reference for RAR 5.0 archive blocks, older RAR header compatibility, UnRAR source behavior, and current SharpCompress Rar implementation boundaries.
|
||||
|
||||
## Reference
|
||||
|
||||
- Read [references/rar-format.md](references/rar-format.md) when the task depends on RAR binary layout, RAR4 vs RAR5 signatures, RAR5 vint encoding, block/header flags, file and service header fields, extra records, encrypted headers, solid archives, split volumes, redirection records, or current SharpCompress Rar support boundaries.
|
||||
- Treat the reference as an implementation guide, not a standards replacement. It summarizes `reference/RAR 5.0 archive format.htm` and selected files under `reference/unrar/`, especially `headers5.hpp`, `headers.hpp`, `arcread.cpp`, `rawread.*`, `crypt*.cpp`, `qopen.*`, `volume.*`, `recvol*.cpp`, `unpack*.cpp`, and `blake2sp.*`.
|
||||
- Prefer the SharpCompress support matrix in the reference over generic RAR assumptions when changing code. RAR is a read-only format in SharpCompress, and metadata/service-header support is intentionally partial.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify which layer is involved: signature detection, RAR4 headers, RAR5 headers, vint parsing, block flags, extra records, file/service metadata, encrypted headers, multivolume handling, decompression, reader/archive API behavior, or tests.
|
||||
2. Open the relevant section in `references/rar-format.md` and use the source-file pointers before changing code.
|
||||
3. For header parsing changes, cross-check sync and async implementations: `MarkHeader.cs`, `RarHeader.cs`, `RarHeaderFactory.cs`, `FileHeader.cs`, `Flags.cs`, and their async counterparts.
|
||||
4. For extraction changes, verify split and solid archive behavior with `RarArchive`, `RarReader`, `RarStream`, and the unpacker files under `src/SharpCompress/Compressors/Rar/`.
|
||||
5. For support claims, keep unsupported or partial features explicit: RAR writing, pre-RAR4 archives, complete service-header semantics, quick-open use, recovery reconstruction, and encryption/version constraints.
|
||||
459
.agents/skills/rar-format/references/rar-format.md
Normal file
459
.agents/skills/rar-format/references/rar-format.md
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
# RAR Format Reference
|
||||
|
||||
This reference summarizes the RAR archive container format for SharpCompress work. It is locally authored from the RAR 5.0 format document, UnRAR source code, and the current SharpCompress implementation.
|
||||
|
||||
Primary local references:
|
||||
|
||||
- `reference/RAR 5.0 archive format.htm`
|
||||
- `reference/unrar/headers5.hpp`
|
||||
- `reference/unrar/headers.hpp`
|
||||
- `reference/unrar/arcread.cpp`
|
||||
- `reference/unrar/rawread.cpp`
|
||||
- `reference/unrar/rawread.hpp`
|
||||
- `reference/unrar/crypt.cpp`
|
||||
- `reference/unrar/crypt5.cpp`
|
||||
- `reference/unrar/qopen.cpp`
|
||||
- `reference/unrar/qopen.hpp`
|
||||
- `reference/unrar/volume.cpp`
|
||||
- `reference/unrar/volume.hpp`
|
||||
- `reference/unrar/recvol.cpp`
|
||||
- `reference/unrar/recvol5.cpp`
|
||||
- `reference/unrar/unpack.cpp`
|
||||
- `reference/unrar/unpack15.cpp`
|
||||
- `reference/unrar/unpack20.cpp`
|
||||
- `reference/unrar/unpack30.cpp`
|
||||
- `reference/unrar/unpack50.cpp`
|
||||
- `reference/unrar/blake2sp.cpp`
|
||||
- `reference/unrar/blake2sp.hpp`
|
||||
|
||||
Primary SharpCompress references:
|
||||
|
||||
- `docs/FORMATS.md`
|
||||
- `src/SharpCompress/Factories/RarFactory.cs`
|
||||
- `src/SharpCompress/Archives/Rar/RarArchive.cs`
|
||||
- `src/SharpCompress/Archives/Rar/RarArchiveEntry.cs`
|
||||
- `src/SharpCompress/Archives/Rar/RarArchiveVolumeFactory.cs`
|
||||
- `src/SharpCompress/Readers/Rar/RarReader.cs`
|
||||
- `src/SharpCompress/Readers/Rar/MultiVolumeRarReader.cs`
|
||||
- `src/SharpCompress/Common/Rar/RarEntry.cs`
|
||||
- `src/SharpCompress/Common/Rar/RarFilePart.cs`
|
||||
- `src/SharpCompress/Common/Rar/Rar5CryptoInfo.cs`
|
||||
- `src/SharpCompress/Common/Rar/RarCryptoBinaryReader.cs`
|
||||
- `src/SharpCompress/Common/Rar/RarCryptoWrapper.cs`
|
||||
- `src/SharpCompress/Common/Rar/Headers/MarkHeader.cs`
|
||||
- `src/SharpCompress/Common/Rar/Headers/RarHeader.cs`
|
||||
- `src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs`
|
||||
- `src/SharpCompress/Common/Rar/Headers/FileHeader.cs`
|
||||
- `src/SharpCompress/Common/Rar/Headers/Flags.cs`
|
||||
- `src/SharpCompress/Compressors/Rar/`
|
||||
- `tests/SharpCompress.Test/Rar/`
|
||||
|
||||
## Contents
|
||||
|
||||
- [Format Overview](#format-overview)
|
||||
- [Signatures And SFX](#signatures-and-sfx)
|
||||
- [RAR5 Vint Encoding](#rar5-vint-encoding)
|
||||
- [RAR5 General Block Format](#rar5-general-block-format)
|
||||
- [RAR5 Header Types](#rar5-header-types)
|
||||
- [RAR5 Common Header Flags](#rar5-common-header-flags)
|
||||
- [RAR5 Main Archive Header](#rar5-main-archive-header)
|
||||
- [RAR5 File And Service Headers](#rar5-file-and-service-headers)
|
||||
- [RAR5 Extra Area Records](#rar5-extra-area-records)
|
||||
- [RAR5 Compression Info](#rar5-compression-info)
|
||||
- [Encryption](#encryption)
|
||||
- [Checksums And Hashes](#checksums-and-hashes)
|
||||
- [Service Headers](#service-headers)
|
||||
- [RAR4 Compatibility](#rar4-compatibility)
|
||||
- [Multivolume And Solid Archives](#multivolume-and-solid-archives)
|
||||
- [SharpCompress Support Matrix](#sharpcompress-support-matrix)
|
||||
- [SharpCompress Read Behavior](#sharpcompress-read-behavior)
|
||||
- [Known Limitations](#known-limitations)
|
||||
- [Test Fixtures](#test-fixtures)
|
||||
|
||||
## Format Overview
|
||||
|
||||
RAR is a block-oriented archive format. SharpCompress supports RAR as a read-only archive/reader format and delegates decompression to its RAR unpacker implementation under `src/SharpCompress/Compressors/Rar/`.
|
||||
|
||||
RAR 5.0 general layout:
|
||||
|
||||
```text
|
||||
self-extracting module (optional)
|
||||
RAR 5.0 signature
|
||||
archive encryption header (optional)
|
||||
main archive header
|
||||
archive comment service header (optional)
|
||||
file header 1
|
||||
service headers for preceding file (optional)
|
||||
...
|
||||
file header N
|
||||
service headers for preceding file (optional)
|
||||
recovery record (optional)
|
||||
end of archive header
|
||||
```
|
||||
|
||||
RAR has no ZIP-style central directory. Readers scan blocks in sequence. Random access is limited by solid compression, split volumes, and the need to process preceding compressed data for solid archives.
|
||||
|
||||
## Signatures And SFX
|
||||
|
||||
RAR signatures:
|
||||
|
||||
| Format | Bytes | SharpCompress behavior |
|
||||
| --- | --- | --- |
|
||||
| Old pre-RAR4 marker | `52 45 7e 5e` | Explicitly unsupported |
|
||||
| RAR4 marker | `52 61 72 21 1a 07 00` | Supported read path |
|
||||
| RAR5 marker | `52 61 72 21 1a 07 01 00` | Supported read path |
|
||||
|
||||
RAR archives can be preceded by a self-extracting module. `MarkHeader.Read` scans for the marker when `ReaderOptions.LookForHeader` is enabled. SharpCompress uses a maximum SFX scan size from the UnRAR implementation notes.
|
||||
|
||||
## RAR5 Vint Encoding
|
||||
|
||||
RAR5 uses variable-length integers, called `vint` in the format document.
|
||||
|
||||
Rules:
|
||||
|
||||
- Each byte contributes 7 data bits.
|
||||
- The high bit is the continuation flag.
|
||||
- If the high bit is `0`, the byte is the last byte in the sequence.
|
||||
- The first byte contains the least significant 7 bits.
|
||||
- RAR currently uses vint for up to 64-bit integers, so values can occupy up to 10 bytes.
|
||||
- Writers can preallocate more bytes than needed by using leading `0x80` bytes, which encode zero with continuation set.
|
||||
|
||||
SharpCompress reads these through `ReadRarVInt*` helpers on marking readers. For RAR5 header size, SharpCompress limits the size field to 3 vint bytes to match the current format implementation limit of a 2 MB maximum header size.
|
||||
|
||||
## RAR5 General Block Format
|
||||
|
||||
RAR5 blocks share a common header shape:
|
||||
|
||||
| Field | Size | Notes |
|
||||
| --- | --- | --- |
|
||||
| Header CRC32 | `uint32` | CRC32 of header data starting at header size through optional extra area |
|
||||
| Header size | `vint` | Size from header type through optional extra area; current max 3 bytes for 2 MB headers |
|
||||
| Header type | `vint` | See [RAR5 Header Types](#rar5-header-types) |
|
||||
| Header flags | `vint` | Common flags for all headers |
|
||||
| Extra area size | `vint` | Present only when common flag `0x0001` is set |
|
||||
| Data size | `vint` | Present only when common flag `0x0002` is set |
|
||||
| Type-specific fields | variable | Depends on header type |
|
||||
| Extra area | variable | Present only when common flag `0x0001` is set |
|
||||
| Data area | variable | Present only when common flag `0x0002` is set; not included in header CRC/size |
|
||||
|
||||
SharpCompress reads the common fields in `RarHeader.Initialize`, then creates typed headers in `RarHeaderFactory`.
|
||||
|
||||
## RAR5 Header Types
|
||||
|
||||
RAR5 header type values:
|
||||
|
||||
| Type | Meaning | SharpCompress code path |
|
||||
| --- | --- | --- |
|
||||
| `1` | Main archive header | `ArchiveHeader` |
|
||||
| `2` | File header | `FileHeader` with `HeaderType.File` |
|
||||
| `3` | Service header | `FileHeader` with `HeaderType.Service` |
|
||||
| `4` | Archive encryption header | `ArchiveCryptHeader` |
|
||||
| `5` | End of archive header | `EndArchiveHeader` |
|
||||
|
||||
SharpCompress constants live in `HeaderCodeV` in `Flags.cs`.
|
||||
|
||||
## RAR5 Common Header Flags
|
||||
|
||||
Common RAR5 header flags from `headers5.hpp` and `Flags.cs`:
|
||||
|
||||
| Flag | Meaning |
|
||||
| --- | --- |
|
||||
| `0x0001` | Extra area is present |
|
||||
| `0x0002` | Data area is present |
|
||||
| `0x0004` | Unknown blocks with this flag must be skipped when updating |
|
||||
| `0x0008` | Data area continues from previous volume |
|
||||
| `0x0010` | Data area continues in next volume |
|
||||
| `0x0020` | Block depends on preceding file block |
|
||||
| `0x0040` | Preserve child block if host block is modified |
|
||||
|
||||
SharpCompress exposes split status through `FileHeader.IsSplitBefore`, `FileHeader.IsSplitAfter`, and `RarEntry.IsSplitAfter`.
|
||||
|
||||
## RAR5 Main Archive Header
|
||||
|
||||
Main archive header fields after the common block fields:
|
||||
|
||||
| Field | Size | Notes |
|
||||
| --- | --- | --- |
|
||||
| Archive flags | `vint` | Volume, solid, recovery, locked flags |
|
||||
| Volume number | `vint` | Present only when archive flag `0x0002` is set |
|
||||
| Extra area | variable | Optional records, currently locator is defined |
|
||||
|
||||
Main archive flags:
|
||||
|
||||
| Flag | Meaning |
|
||||
| --- | --- |
|
||||
| `0x0001` | Volume, archive is part of a multivolume set |
|
||||
| `0x0002` | Volume number field is present |
|
||||
| `0x0004` | Solid archive |
|
||||
| `0x0008` | Recovery record is present |
|
||||
| `0x0010` | Locked archive |
|
||||
|
||||
Main header extra record types:
|
||||
|
||||
| Type | Name | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `0x01` | Locator | Optional offsets to quick-open and recovery-record blocks |
|
||||
|
||||
SharpCompress parses archive flags in `ArchiveHeader` and uses volume/solid information in archive and reader flows.
|
||||
|
||||
## RAR5 File And Service Headers
|
||||
|
||||
File and service headers share the same base layout. Header type `2` is a file header and header type `3` is a service header.
|
||||
|
||||
Fields after common block fields:
|
||||
|
||||
| Field | Size | Notes |
|
||||
| --- | --- | --- |
|
||||
| File flags | `vint` | Directory, time, CRC, unknown unpacked size |
|
||||
| Unpacked size | `vint` | Present even when unknown-size flag is set, but ignored then |
|
||||
| Attributes | `vint` | OS-specific file attributes |
|
||||
| mtime | `uint32` | Unix time, present only when file flag `0x0002` is set |
|
||||
| Data CRC32 | `uint32` | Present only when file flag `0x0004` is set |
|
||||
| Compression information | `vint` | Algorithm version, solid flag, method, dictionary size |
|
||||
| Host OS | `vint` | `0` Windows, `1` Unix |
|
||||
| Name length | `vint` | Byte count |
|
||||
| Name | variable | UTF-8, no trailing zero |
|
||||
| Extra area | variable | Optional file/service extra records |
|
||||
| Data area | variable | File data or service data |
|
||||
|
||||
RAR5 file flags:
|
||||
|
||||
| Flag | Meaning |
|
||||
| --- | --- |
|
||||
| `0x0001` | Directory filesystem object |
|
||||
| `0x0002` | Unix mtime field is present |
|
||||
| `0x0004` | CRC32 field is present |
|
||||
| `0x0008` | Unpacked size is unknown; extract until compression stream ends |
|
||||
|
||||
SharpCompress parses these in `FileHeader.ReadFromReaderV5`.
|
||||
|
||||
## RAR5 Extra Area Records
|
||||
|
||||
Each extra record has this shape:
|
||||
|
||||
```text
|
||||
record size vint size from type through record data
|
||||
record type vint
|
||||
record data variable
|
||||
```
|
||||
|
||||
File and service header extra record types:
|
||||
|
||||
| Type | Name | SharpCompress behavior |
|
||||
| --- | --- | --- |
|
||||
| `0x01` | File encryption | Parses `Rar5CryptoInfo` |
|
||||
| `0x02` | File hash | Reads BLAKE2sp digest when present |
|
||||
| `0x03` | File time | Reads high precision mtime/ctime/atime |
|
||||
| `0x04` | File version | Currently skipped/drained |
|
||||
| `0x05` | Redirection | Parses symlink/junction/hard link/file copy metadata |
|
||||
| `0x06` | Unix owner | Currently skipped/drained |
|
||||
| `0x07` | Service data | Currently skipped/drained except service header data handling |
|
||||
|
||||
Unknown records must be skipped without interrupting normal operation. SharpCompress drains unhandled extra record bytes after each record.
|
||||
|
||||
Redirection types:
|
||||
|
||||
| Value | Meaning |
|
||||
| --- | --- |
|
||||
| `0x0001` | Unix symlink |
|
||||
| `0x0002` | Windows symlink |
|
||||
| `0x0003` | Windows junction |
|
||||
| `0x0004` | Hard link |
|
||||
| `0x0005` | File copy |
|
||||
|
||||
`RarEntry.IsRedir` and `RarEntry.RedirTargetName` expose part of this metadata.
|
||||
|
||||
## RAR5 Compression Info
|
||||
|
||||
RAR5 file compression information is a packed vint bit field:
|
||||
|
||||
| Bits/mask | Meaning |
|
||||
| --- | --- |
|
||||
| `0x003f` | Compression algorithm version, currently 0 in RAR5; SharpCompress stores it as value + 50 |
|
||||
| `0x0040` | Solid flag; dictionary continues from preceding files |
|
||||
| `0x0380` | Compression method, values 0-5 are used; 0 is store/no compression |
|
||||
| `0x3c00` | Minimum dictionary size: 0 means 128 KB, 1 means 256 KB, through 15 meaning 4096 MB |
|
||||
|
||||
SharpCompress maps these to `FileHeader.CompressionAlgorithm`, `FileHeader.IsSolid`, `FileHeader.CompressionMethod`, and `FileHeader.WindowSize`.
|
||||
|
||||
RAR4 and older algorithm values are distinct. `RarEntry.IsRarV3` currently treats algorithm values `15`, `20`, `26`, `29`, and `36` as legacy RAR code paths.
|
||||
|
||||
## Encryption
|
||||
|
||||
RAR5 archive encryption header fields:
|
||||
|
||||
| Field | Size | Notes |
|
||||
| --- | --- | --- |
|
||||
| Encryption version | `vint` | Current supported version is 0, AES-256 |
|
||||
| Encryption flags | `vint` | `0x0001` means password check data is present |
|
||||
| KDF count | 1 byte | Binary logarithm of PBKDF2 iteration count |
|
||||
| Salt | 16 bytes | Header encryption salt |
|
||||
| Check value | 12 bytes | Optional password check plus checksum |
|
||||
|
||||
When archive headers are encrypted, each following header starts with a 16-byte AES-256 initialization vector followed by encrypted header data aligned to 16 bytes.
|
||||
|
||||
RAR5 file encryption extra record fields include encryption version, flags, KDF count, 16-byte salt, 16-byte IV, and optional 12-byte password check data.
|
||||
|
||||
SharpCompress behavior:
|
||||
|
||||
- Archive encryption header is parsed by `ArchiveCryptHeader`.
|
||||
- File encryption extra records are parsed into `Rar5CryptoInfo`.
|
||||
- Header decryption uses `RarCryptoBinaryReader` with `CryptKey5`.
|
||||
- File data decryption uses `RarCryptoWrapper` around the packed stream.
|
||||
- Password is required through `ReaderOptions.Password`; missing password throws `CryptographicException`.
|
||||
- RAR4 encrypted file data uses RAR3 crypto classes and salt handling.
|
||||
|
||||
## Checksums And Hashes
|
||||
|
||||
RAR5 checksum/hash locations:
|
||||
|
||||
- Header CRC32 covers header data starting at the header size field through optional extra area. It does not include the data area.
|
||||
- File header can contain CRC32 of unpacked data when file flag `0x0004` is set.
|
||||
- File hash extra record type `0x02` can contain BLAKE2sp hash data. The defined RAR5 hash type value is `0x00` for BLAKE2sp.
|
||||
- For split files, hashes can apply to packed data for non-final volume parts.
|
||||
|
||||
SharpCompress verifies header CRC in `RarHeader.VerifyHeaderCrc`. RAR CRC logic is in `RarCrcBinaryReader`, `AsyncRarCrcBinaryReader`, and `RarCRC`.
|
||||
|
||||
## Service Headers
|
||||
|
||||
RAR5 service headers use the file-header structure with header type `3`. Known service names from the RAR5 document and UnRAR source include:
|
||||
|
||||
| Name | Meaning |
|
||||
| --- | --- |
|
||||
| `CMT` | Archive comment |
|
||||
| `QO` | Quick-open data |
|
||||
| `ACL` | NTFS permissions |
|
||||
| `STM` | NTFS alternate data stream |
|
||||
| `RR` | Recovery record |
|
||||
|
||||
SharpCompress handles `CMT` specially by exposing its packed stream in `RarHeaderFactory`; most other service data is skipped or only partially modeled.
|
||||
|
||||
Quick-open caution from the RAR5 format document:
|
||||
|
||||
- Quick-open data can store copies of headers for faster listing.
|
||||
- If quick-open data is used to display names, extraction must use the same source. Otherwise malicious archives could show one name and extract another.
|
||||
- SharpCompress should not use quick-open data for one path and ordinary headers for another without carefully preserving this invariant.
|
||||
|
||||
## RAR4 Compatibility
|
||||
|
||||
SharpCompress supports RAR4-style headers as well as RAR5.
|
||||
|
||||
RAR4 header codes from UnRAR and `Flags.cs`:
|
||||
|
||||
| Code | Meaning |
|
||||
| --- | --- |
|
||||
| `0x72` | Mark header |
|
||||
| `0x73` | Archive/main header |
|
||||
| `0x74` | File header |
|
||||
| `0x75` | Comment header |
|
||||
| `0x76` | AV header |
|
||||
| `0x77` | Old subheader |
|
||||
| `0x78` | Protect/recovery header |
|
||||
| `0x79` | Sign header |
|
||||
| `0x7a` | New subheader/service header |
|
||||
| `0x7b` | End archive header |
|
||||
|
||||
RAR4 file-header behavior differs from RAR5:
|
||||
|
||||
- Fixed-size base fields rather than RAR5 vint-heavy layout.
|
||||
- Optional large-file high-size fields when `LARGE` flag is set.
|
||||
- File names can use older RAR Unicode name encoding.
|
||||
- DOS timestamps and RAR4 extended time flags are used.
|
||||
- Directory status is encoded through the window mask.
|
||||
- Path separators differ: RAR4 can use backslashes as separators, while RAR5 uses `/` as the universal separator.
|
||||
|
||||
SharpCompress parses this in `FileHeader.ReadFromReaderV4` and maps paths through `ConvertPathV4`.
|
||||
|
||||
## Multivolume And Solid Archives
|
||||
|
||||
RAR supports multivolume archives and solid compression.
|
||||
|
||||
Relevant flags:
|
||||
|
||||
- Main archive `0x0001`: archive is part of a volume set.
|
||||
- Main archive `0x0002`: RAR5 volume number field is present.
|
||||
- Common block `0x0008`: data continues from previous volume.
|
||||
- Common block `0x0010`: data continues in next volume.
|
||||
- RAR4 file flags `SPLIT_BEFORE` and `SPLIT_AFTER` indicate split file data.
|
||||
- RAR5 compression info `0x0040` indicates solid compression.
|
||||
|
||||
SharpCompress behavior:
|
||||
|
||||
- `RarFactory` implements `IMultiArchiveFactory`.
|
||||
- `RarArchiveVolumeFactory` resolves volume file parts.
|
||||
- `RarEntry.IsSplitAfter` exposes split status.
|
||||
- Solid archives should be extracted sequentially. Prefer `ExtractAllEntries()` for solid RAR archives rather than extracting arbitrary entries independently.
|
||||
|
||||
## SharpCompress Support Matrix
|
||||
|
||||
| Feature | Support | Notes |
|
||||
| --- | --- | --- |
|
||||
| RAR read | Yes | Archive and Reader APIs |
|
||||
| RAR write | No | RAR is read-only |
|
||||
| RAR4 headers | Yes | Supported parsing path |
|
||||
| RAR5 headers | Yes | Supported parsing path |
|
||||
| Pre-RAR4 marker | No | `MarkHeader` throws unsupported format |
|
||||
| RAR5 vint fields | Yes | Reader helpers parse vint values |
|
||||
| File/service extra records | Partial | Encryption/hash/time/redirection modeled; others mostly skipped |
|
||||
| Solid archives | Yes, with sequencing constraints | Use sequential extraction |
|
||||
| Multivolume archives | Yes | Archive/multi-volume factory paths |
|
||||
| Encrypted archives | Partial | Password required; format/version constraints apply |
|
||||
| Archive comments | Partial | `CMT` service header handled specially |
|
||||
| Quick-open records | Not a public feature | Avoid inconsistent listing/extraction behavior |
|
||||
| Recovery records | Skipped/partial | Not recovery reconstruction API |
|
||||
|
||||
## SharpCompress Read Behavior
|
||||
|
||||
Header flow:
|
||||
|
||||
1. `MarkHeader.Read` scans for RAR4 or RAR5 signature and rejects old pre-RAR4 signatures.
|
||||
2. `RarHeaderFactory.ReadHeaders` records whether the archive is RAR5.
|
||||
3. `RarHeader.TryReadBase` reads common header fields and CRC state.
|
||||
4. `RarHeaderFactory` creates typed headers based on header code.
|
||||
5. File data is either skipped, wrapped in `ReadOnlySubStream`, or wrapped in decryption stream depending on mode and encryption metadata.
|
||||
|
||||
Seekable mode:
|
||||
|
||||
- File data positions are recorded in `FileHeader.DataStartPosition`.
|
||||
- Streams can seek over packed data while collecting headers.
|
||||
|
||||
Streaming mode:
|
||||
|
||||
- File data is exposed as a substream for file headers.
|
||||
- Non-file service data is skipped unless specially handled.
|
||||
- Consumers must process entries in archive order.
|
||||
|
||||
Decompression:
|
||||
|
||||
- `RarStream` wraps the packed stream and an `IRarUnpack` implementation.
|
||||
- `RarStream` is non-seekable.
|
||||
- `RarStream.Initialize` starts unpacking based on the parsed `FileHeader`.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
Keep these limitations explicit in code comments, docs, and tests:
|
||||
|
||||
- No RAR writing support.
|
||||
- No support for pre-RAR4 archives.
|
||||
- Service headers are not fully modeled by public APIs.
|
||||
- Quick-open records are not a general public feature and have security-sensitive listing/extraction consistency requirements.
|
||||
- Recovery record reconstruction is not exposed as a full repair feature.
|
||||
- RAR5 Unix owner records and service data records are mostly skipped unless a specific behavior is implemented.
|
||||
- Redirection metadata is surfaced, but extraction semantics for all link types should be treated carefully and tested for security.
|
||||
- Encrypted archive support depends on password, encryption version, and KDF limits.
|
||||
- Solid and multivolume archives must be tested with sequential extraction scenarios.
|
||||
|
||||
## Test Fixtures
|
||||
|
||||
Representative RAR test files live under `tests/TestArchives/Archives/` and related test archive folders. Use existing fixtures when possible instead of adding new binary archives.
|
||||
|
||||
Representative test files:
|
||||
|
||||
- `tests/SharpCompress.Test/Rar/RarArchiveTests.cs`
|
||||
- `tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs`
|
||||
- `tests/SharpCompress.Test/Rar/RarReaderTests.cs`
|
||||
- `tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs`
|
||||
- `tests/SharpCompress.Test/Rar/RarHeaderFactoryTest.cs`
|
||||
- `tests/SharpCompress.Test/Rar/RarCRCTest.cs`
|
||||
|
||||
When changing RAR behavior, include both Archive and Reader API coverage where applicable. For solid or multivolume behavior, prefer tests that extract entries sequentially and verify stream ownership and volume transitions.
|
||||
21
.agents/skills/sevenzip-format/SKILL.md
Normal file
21
.agents/skills/sevenzip-format/SKILL.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
name: sevenzip-format
|
||||
description: Reference the 7z/7zip archive container format. Use when an AI agent needs to answer questions or make code changes involving 7z signatures, headers, encoded headers, NID/property IDs, packed streams, folders/coders, bind pairs, substreams, file metadata properties, or SharpCompress 7Zip parsing behavior.
|
||||
---
|
||||
|
||||
# Sevenzip Format
|
||||
|
||||
Use this skill for 7z container-format work. It provides a local Markdown conversion of the LZMA SDK `7zFormat.txt` reference.
|
||||
|
||||
## Reference
|
||||
|
||||
- Read [references/7z-format.md](references/7z-format.md) when the task depends on 7z binary layout, header property IDs, stream/folder relationships, metadata fields, or encoded headers.
|
||||
- Treat the reference as the LZMA SDK 7z format description version 4.59. It describes the container grammar, not compression method internals; method-specific codec details are outside this skill.
|
||||
- Preserve source field names and numeric IDs when mapping the spec to code. The converted reference keeps source spelling inside syntax blocks where exact matching may matter.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify which part of the 7z container is involved: signature/start header, packed streams, coders/folders, substreams, files info, or encoded headers.
|
||||
2. Open the relevant section in `references/7z-format.md` and use the table of contents to avoid loading unrelated details.
|
||||
3. When implementing or reviewing parsing logic, pay special attention to optional blocks marked with `[]`, 7z's variable-length `UINT64` encoding, and little-endian `REAL_UINT64` fields.
|
||||
4. Cross-check behavior against SharpCompress tests and existing parser conventions before changing public API or stream behavior.
|
||||
478
.agents/skills/sevenzip-format/references/7z-format.md
Normal file
478
.agents/skills/sevenzip-format/references/7z-format.md
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
# 7z Format Description (4.59)
|
||||
|
||||
Source: https://github.com/jljusten/LZMA-SDK/blob/master/DOC/7zFormat.txt
|
||||
|
||||
Raw download used for this conversion: https://raw.githubusercontent.com/jljusten/LZMA-SDK/master/DOC/7zFormat.txt
|
||||
|
||||
Downloaded and converted on 2026-05-23.
|
||||
|
||||
This is a Markdown conversion of the LZMA SDK plaintext 7z archive format description. Pseudo-grammar blocks preserve the source field names and spelling.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Format Structure Overview](#format-structure-overview)
|
||||
- [Notes About Notation and Encoding](#notes-about-notation-and-encoding)
|
||||
- [Property IDs](#property-ids)
|
||||
- [7z Format Headers](#7z-format-headers)
|
||||
- [SignatureHeader](#signatureheader)
|
||||
- [ArchiveProperties](#archiveproperties)
|
||||
- [Digests](#digests-numstreams)
|
||||
- [PackInfo](#packinfo)
|
||||
- [Folder](#folder)
|
||||
- [Coders Info](#coders-info)
|
||||
- [SubStreams Info](#substreams-info)
|
||||
- [Streams Info](#streams-info)
|
||||
- [FilesInfo](#filesinfo)
|
||||
- [Header](#header)
|
||||
- [HeaderInfo](#headerinfo)
|
||||
|
||||
## Overview
|
||||
|
||||
This file contains a description of the 7z archive format.
|
||||
|
||||
A 7z archive can contain files compressed with any method. See `Methods.txt` in the LZMA SDK for descriptions of defined compression methods.
|
||||
|
||||
## Format Structure Overview
|
||||
|
||||
Some fields can be optional.
|
||||
|
||||
### Archive Structure
|
||||
|
||||
```text
|
||||
SignatureHeader
|
||||
[PackedStreams]
|
||||
[PackedStreamsForHeaders]
|
||||
[
|
||||
Header
|
||||
or
|
||||
{
|
||||
Packed Header
|
||||
HeaderInfo
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Header Structure
|
||||
|
||||
```text
|
||||
{
|
||||
ArchiveProperties
|
||||
AdditionalStreams
|
||||
{
|
||||
PackInfo
|
||||
{
|
||||
PackPos
|
||||
NumPackStreams
|
||||
Sizes[NumPackStreams]
|
||||
CRCs[NumPackStreams]
|
||||
}
|
||||
CodersInfo
|
||||
{
|
||||
NumFolders
|
||||
Folders[NumFolders]
|
||||
{
|
||||
NumCoders
|
||||
CodersInfo[NumCoders]
|
||||
{
|
||||
ID
|
||||
NumInStreams;
|
||||
NumOutStreams;
|
||||
PropertiesSize
|
||||
Properties[PropertiesSize]
|
||||
}
|
||||
NumBindPairs
|
||||
BindPairsInfo[NumBindPairs]
|
||||
{
|
||||
InIndex;
|
||||
OutIndex;
|
||||
}
|
||||
PackedIndices
|
||||
}
|
||||
UnPackSize[Folders][Folders.NumOutstreams]
|
||||
CRCs[NumFolders]
|
||||
}
|
||||
SubStreamsInfo
|
||||
{
|
||||
NumUnPackStreamsInFolders[NumFolders];
|
||||
UnPackSizes[]
|
||||
CRCs[]
|
||||
}
|
||||
}
|
||||
MainStreamsInfo
|
||||
{
|
||||
(Same as in AdditionalStreams)
|
||||
}
|
||||
FilesInfo
|
||||
{
|
||||
NumFiles
|
||||
Properties[]
|
||||
{
|
||||
ID
|
||||
Size
|
||||
Data
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### HeaderInfo Structure
|
||||
|
||||
```text
|
||||
{
|
||||
(Same as in AdditionalStreams)
|
||||
}
|
||||
```
|
||||
|
||||
## Notes About Notation and Encoding
|
||||
|
||||
7z uses little-endian encoding.
|
||||
|
||||
Optional headers are marked as:
|
||||
|
||||
```text
|
||||
[]
|
||||
Header
|
||||
[]
|
||||
```
|
||||
|
||||
`REAL_UINT64` means a real `UINT64`.
|
||||
|
||||
`UINT64` means a real `UINT64` encoded with the following scheme. The size of the encoding sequence depends on the first byte:
|
||||
|
||||
| First byte (binary) | Extra bytes | Value |
|
||||
| --- | --- | --- |
|
||||
| `0xxxxxxx` | none | `( xxxxxxx )` |
|
||||
| `10xxxxxx` | `BYTE y[1]` | `( xxxxxx << (8 * 1)) + y` |
|
||||
| `110xxxxx` | `BYTE y[2]` | `( xxxxx << (8 * 2)) + y` |
|
||||
| `...` | `...` | `...` |
|
||||
| `1111110x` | `BYTE y[6]` | `( x << (8 * 6)) + y` |
|
||||
| `11111110` | `BYTE y[7]` | `y` |
|
||||
| `11111111` | `BYTE y[8]` | `y` |
|
||||
|
||||
## Property IDs
|
||||
|
||||
| ID | Name |
|
||||
| --- | --- |
|
||||
| `0x00` | `kEnd` |
|
||||
| `0x01` | `kHeader` |
|
||||
| `0x02` | `kArchiveProperties` |
|
||||
| `0x03` | `kAdditionalStreamsInfo` |
|
||||
| `0x04` | `kMainStreamsInfo` |
|
||||
| `0x05` | `kFilesInfo` |
|
||||
| `0x06` | `kPackInfo` |
|
||||
| `0x07` | `kUnPackInfo` |
|
||||
| `0x08` | `kSubStreamsInfo` |
|
||||
| `0x09` | `kSize` |
|
||||
| `0x0A` | `kCRC` |
|
||||
| `0x0B` | `kFolder` |
|
||||
| `0x0C` | `kCodersUnPackSize` |
|
||||
| `0x0D` | `kNumUnPackStream` |
|
||||
| `0x0E` | `kEmptyStream` |
|
||||
| `0x0F` | `kEmptyFile` |
|
||||
| `0x10` | `kAnti` |
|
||||
| `0x11` | `kName` |
|
||||
| `0x12` | `kCTime` |
|
||||
| `0x13` | `kATime` |
|
||||
| `0x14` | `kMTime` |
|
||||
| `0x15` | `kWinAttributes` |
|
||||
| `0x16` | `kComment` |
|
||||
| `0x17` | `kEncodedHeader` |
|
||||
| `0x18` | `kStartPos` |
|
||||
| `0x19` | `kDummy` |
|
||||
|
||||
## 7z Format Headers
|
||||
|
||||
### SignatureHeader
|
||||
|
||||
```text
|
||||
BYTE kSignature[6] = {'7', 'z', 0xBC, 0xAF, 0x27, 0x1C};
|
||||
|
||||
ArchiveVersion
|
||||
{
|
||||
BYTE Major; // now = 0
|
||||
BYTE Minor; // now = 2
|
||||
};
|
||||
|
||||
UINT32 StartHeaderCRC;
|
||||
|
||||
StartHeader
|
||||
{
|
||||
REAL_UINT64 NextHeaderOffset
|
||||
REAL_UINT64 NextHeaderSize
|
||||
UINT32 NextHeaderCRC
|
||||
}
|
||||
```
|
||||
|
||||
### ArchiveProperties
|
||||
|
||||
```text
|
||||
BYTE NID::kArchiveProperties (0x02)
|
||||
for (;;)
|
||||
{
|
||||
BYTE PropertyType;
|
||||
if (aType == 0)
|
||||
break;
|
||||
UINT64 PropertySize;
|
||||
BYTE PropertyData[PropertySize];
|
||||
}
|
||||
```
|
||||
|
||||
### Digests (NumStreams)
|
||||
|
||||
```text
|
||||
BYTE AllAreDefined
|
||||
if (AllAreDefined == 0)
|
||||
{
|
||||
for(NumStreams)
|
||||
BIT Defined
|
||||
}
|
||||
UINT32 CRCs[NumDefined]
|
||||
```
|
||||
|
||||
### PackInfo
|
||||
|
||||
```text
|
||||
BYTE NID::kPackInfo (0x06)
|
||||
UINT64 PackPos
|
||||
UINT64 NumPackStreams
|
||||
|
||||
[]
|
||||
BYTE NID::kSize (0x09)
|
||||
UINT64 PackSizes[NumPackStreams]
|
||||
[]
|
||||
|
||||
[]
|
||||
BYTE NID::kCRC (0x0A)
|
||||
PackStreamDigests[NumPackStreams]
|
||||
[]
|
||||
|
||||
BYTE NID::kEnd
|
||||
```
|
||||
|
||||
### Folder
|
||||
|
||||
```text
|
||||
UINT64 NumCoders;
|
||||
for (NumCoders)
|
||||
{
|
||||
BYTE
|
||||
{
|
||||
0:3 CodecIdSize
|
||||
4: Is Complex Coder
|
||||
5: There Are Attributes
|
||||
6: Reserved
|
||||
7: There are more alternative methods. (Not used anymore, must be 0).
|
||||
}
|
||||
BYTE CodecId[CodecIdSize]
|
||||
if (Is Complex Coder)
|
||||
{
|
||||
UINT64 NumInStreams;
|
||||
UINT64 NumOutStreams;
|
||||
}
|
||||
if (There Are Attributes)
|
||||
{
|
||||
UINT64 PropertiesSize
|
||||
BYTE Properties[PropertiesSize]
|
||||
}
|
||||
}
|
||||
|
||||
NumBindPairs = NumOutStreamsTotal - 1;
|
||||
|
||||
for (NumBindPairs)
|
||||
{
|
||||
UINT64 InIndex;
|
||||
UINT64 OutIndex;
|
||||
}
|
||||
|
||||
NumPackedStreams = NumInStreamsTotal - NumBindPairs;
|
||||
if (NumPackedStreams > 1)
|
||||
for(NumPackedStreams)
|
||||
{
|
||||
UINT64 Index;
|
||||
};
|
||||
```
|
||||
|
||||
### Coders Info
|
||||
|
||||
```text
|
||||
BYTE NID::kUnPackInfo (0x07)
|
||||
|
||||
BYTE NID::kFolder (0x0B)
|
||||
UINT64 NumFolders
|
||||
BYTE External
|
||||
switch(External)
|
||||
{
|
||||
case 0:
|
||||
Folders[NumFolders]
|
||||
case 1:
|
||||
UINT64 DataStreamIndex
|
||||
}
|
||||
|
||||
BYTE ID::kCodersUnPackSize (0x0C)
|
||||
for(Folders)
|
||||
for(Folder.NumOutStreams)
|
||||
UINT64 UnPackSize;
|
||||
|
||||
[]
|
||||
BYTE NID::kCRC (0x0A)
|
||||
UnPackDigests[NumFolders]
|
||||
[]
|
||||
|
||||
BYTE NID::kEnd
|
||||
```
|
||||
|
||||
### SubStreams Info
|
||||
|
||||
```text
|
||||
BYTE NID::kSubStreamsInfo; (0x08)
|
||||
|
||||
[]
|
||||
BYTE NID::kNumUnPackStream; (0x0D)
|
||||
UINT64 NumUnPackStreamsInFolders[NumFolders];
|
||||
[]
|
||||
|
||||
[]
|
||||
BYTE NID::kSize (0x09)
|
||||
UINT64 UnPackSizes[]
|
||||
[]
|
||||
|
||||
[]
|
||||
BYTE NID::kCRC (0x0A)
|
||||
Digests[Number of streams with unknown CRC]
|
||||
[]
|
||||
|
||||
BYTE NID::kEnd
|
||||
```
|
||||
|
||||
### Streams Info
|
||||
|
||||
```text
|
||||
[]
|
||||
PackInfo
|
||||
[]
|
||||
|
||||
[]
|
||||
CodersInfo
|
||||
[]
|
||||
|
||||
[]
|
||||
SubStreamsInfo
|
||||
[]
|
||||
|
||||
BYTE NID::kEnd
|
||||
```
|
||||
|
||||
### FilesInfo
|
||||
|
||||
```text
|
||||
BYTE NID::kFilesInfo; (0x05)
|
||||
UINT64 NumFiles
|
||||
|
||||
for (;;)
|
||||
{
|
||||
BYTE PropertyType;
|
||||
if (aType == 0)
|
||||
break;
|
||||
|
||||
UINT64 Size;
|
||||
|
||||
switch(PropertyType)
|
||||
{
|
||||
kEmptyStream: (0x0E)
|
||||
for(NumFiles)
|
||||
BIT IsEmptyStream
|
||||
|
||||
kEmptyFile: (0x0F)
|
||||
for(EmptyStreams)
|
||||
BIT IsEmptyFile
|
||||
|
||||
kAnti: (0x10)
|
||||
for(EmptyStreams)
|
||||
BIT IsAntiFile
|
||||
|
||||
case kCTime: (0x12)
|
||||
case kATime: (0x13)
|
||||
case kMTime: (0x14)
|
||||
BYTE AllAreDefined
|
||||
if (AllAreDefined == 0)
|
||||
{
|
||||
for(NumFiles)
|
||||
BIT TimeDefined
|
||||
}
|
||||
BYTE External;
|
||||
if(External != 0)
|
||||
UINT64 DataIndex
|
||||
[]
|
||||
for(Definded Items)
|
||||
UINT64 Time
|
||||
[]
|
||||
|
||||
kNames: (0x11)
|
||||
BYTE External;
|
||||
if(External != 0)
|
||||
UINT64 DataIndex
|
||||
[]
|
||||
for(Files)
|
||||
{
|
||||
wchar_t Names[NameSize];
|
||||
wchar_t 0;
|
||||
}
|
||||
[]
|
||||
|
||||
kAttributes: (0x15)
|
||||
BYTE AllAreDefined
|
||||
if (AllAreDefined == 0)
|
||||
{
|
||||
for(NumFiles)
|
||||
BIT AttributesAreDefined
|
||||
}
|
||||
BYTE External;
|
||||
if(External != 0)
|
||||
UINT64 DataIndex
|
||||
[]
|
||||
for(Definded Attributes)
|
||||
UINT32 Attributes
|
||||
[]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Header
|
||||
|
||||
```text
|
||||
BYTE NID::kHeader (0x01)
|
||||
|
||||
[]
|
||||
ArchiveProperties
|
||||
[]
|
||||
|
||||
[]
|
||||
BYTE NID::kAdditionalStreamsInfo; (0x03)
|
||||
StreamsInfo
|
||||
[]
|
||||
|
||||
[]
|
||||
BYTE NID::kMainStreamsInfo; (0x04)
|
||||
StreamsInfo
|
||||
[]
|
||||
|
||||
[]
|
||||
FilesInfo
|
||||
[]
|
||||
|
||||
BYTE NID::kEnd
|
||||
```
|
||||
|
||||
### HeaderInfo
|
||||
|
||||
```text
|
||||
[]
|
||||
BYTE NID::kEncodedHeader; (0x17)
|
||||
StreamsInfo for Encoded Header
|
||||
[]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
End of document.
|
||||
22
.agents/skills/tar-format/SKILL.md
Normal file
22
.agents/skills/tar-format/SKILL.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
name: tar-format
|
||||
description: Reference the Tar/USTAR/PAX/GNU tar archive container format. Use when an AI agent needs to answer questions or make code changes involving tar headers, 512-byte blocks, checksums, typeflags, USTAR prefixes, PAX local/global extended headers, GNU long names/links, sparse entries, wrapper compression, or SharpCompress Tar parsing and writing behavior.
|
||||
---
|
||||
|
||||
# Tar Format
|
||||
|
||||
Use this skill for tar container-format work. It provides a local, SharpCompress-oriented reference for POSIX USTAR, POSIX PAX, GNU tar extensions, and the current SharpCompress Tar implementation.
|
||||
|
||||
## Reference
|
||||
|
||||
- Read [references/tar-format.md](references/tar-format.md) when the task depends on tar binary layout, header field offsets, typeflag behavior, checksum rules, PAX records, GNU long-name/link records, wrapper compression support, or current SharpCompress Tar support boundaries.
|
||||
- Treat the reference as an implementation guide, not a standards replacement. It cites POSIX and GNU tar sources, but it also documents SharpCompress-specific behavior and limitations.
|
||||
- Prefer the SharpCompress support matrix in the reference over generic tar assumptions when changing code. Tar dialect support is intentionally partial in some areas.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify which layer is involved: raw tar block/header parsing, POSIX USTAR fields, POSIX PAX metadata, GNU tar extensions, wrapper compression, reader/archive/writer API behavior, or tests.
|
||||
2. Open the relevant section in `references/tar-format.md` and use the source-file pointers before changing code.
|
||||
3. For parsing changes, cross-check `TarHeader.cs`, `TarHeader.Async.cs`, `EntryType.cs`, and matching sync/async tests.
|
||||
4. For writer changes, verify both sync and async file/directory paths and `TarWriterOptions.HeaderFormat` behavior.
|
||||
5. For support claims, keep unsupported features explicit: PAX write, sparse reconstruction, device/FIFO semantics, link-writing APIs, and writing `tar.xz`, `tar.zst`, or `tar.Z`.
|
||||
360
.agents/skills/tar-format/references/tar-format.md
Normal file
360
.agents/skills/tar-format/references/tar-format.md
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
# Tar Format Reference
|
||||
|
||||
This reference summarizes the Tar archive container format for SharpCompress work. It is locally authored from public format references and the current SharpCompress implementation.
|
||||
|
||||
Primary external references:
|
||||
|
||||
- POSIX `pax` and `ustar`: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html
|
||||
- GNU tar basic format: https://www.gnu.org/software/tar/manual/html_node/Standard.html
|
||||
- GNU tar extensions: https://www.gnu.org/software/tar/manual/html_node/Extensions.html
|
||||
|
||||
Primary SharpCompress references:
|
||||
|
||||
- `docs/TAR_SPEC.md`
|
||||
- `docs/TAR_GAP_ANALYSIS.md`
|
||||
- `src/SharpCompress/Factories/TarFactory.cs`
|
||||
- `src/SharpCompress/Factories/TarWrapper.cs`
|
||||
- `src/SharpCompress/Common/Tar/Headers/TarHeader.cs`
|
||||
- `src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs`
|
||||
- `src/SharpCompress/Common/Tar/Headers/EntryType.cs`
|
||||
- `src/SharpCompress/Writers/Tar/TarWriter.cs`
|
||||
- `src/SharpCompress/Writers/Tar/TarWriter.Async.cs`
|
||||
- `src/SharpCompress/Writers/Tar/TarWriterOptions.cs`
|
||||
- `tests/SharpCompress.Test/Tar/`
|
||||
|
||||
## Contents
|
||||
|
||||
- [Format Overview](#format-overview)
|
||||
- [USTAR Header Layout](#ustar-header-layout)
|
||||
- [Entry Type Flags](#entry-type-flags)
|
||||
- [Numeric Fields](#numeric-fields)
|
||||
- [Checksum](#checksum)
|
||||
- [Path Names](#path-names)
|
||||
- [PAX Extended Headers](#pax-extended-headers)
|
||||
- [GNU Tar Extensions](#gnu-tar-extensions)
|
||||
- [SharpCompress Support Matrix](#sharpcompress-support-matrix)
|
||||
- [SharpCompress Read Behavior](#sharpcompress-read-behavior)
|
||||
- [SharpCompress Write Behavior](#sharpcompress-write-behavior)
|
||||
- [Known Limitations](#known-limitations)
|
||||
- [Test Fixtures](#test-fixtures)
|
||||
|
||||
## Format Overview
|
||||
|
||||
A tar archive is a sequence of 512-byte blocks. Each archive member is represented by:
|
||||
|
||||
```text
|
||||
header block (512 bytes)
|
||||
payload blocks, padded to a 512-byte boundary
|
||||
```
|
||||
|
||||
The archive should end with two 512-byte blocks filled with zero bytes. Readers should be tolerant of missing end markers because real-world tar tools may produce archives without them.
|
||||
|
||||
The header contains file metadata and the payload size. Tar has no central directory. Streaming readers must consume or skip each payload and its padding before the next header can be parsed.
|
||||
|
||||
SharpCompress relies on this in `TarReadOnlySubStream`: disposing an entry stream consumes unread entry bytes plus 512-byte padding so the next header remains aligned.
|
||||
|
||||
## USTAR Header Layout
|
||||
|
||||
The POSIX USTAR header is exactly 512 bytes. Field offsets are byte offsets from the beginning of the header block.
|
||||
|
||||
| Field | Offset | Length | Notes |
|
||||
| ----- | ------ | ------ | ----- |
|
||||
| `name` | 0 | 100 | File name or final path component |
|
||||
| `mode` | 100 | 8 | Octal file mode |
|
||||
| `uid` | 108 | 8 | Octal owner id |
|
||||
| `gid` | 116 | 8 | Octal group id |
|
||||
| `size` | 124 | 12 | Octal payload size, or GNU base-256 in some archives |
|
||||
| `mtime` | 136 | 12 | Octal seconds since Unix epoch |
|
||||
| `chksum` | 148 | 8 | Header checksum |
|
||||
| `typeflag` | 156 | 1 | Entry type |
|
||||
| `linkname` | 157 | 100 | Link target for hard/symbolic links |
|
||||
| `magic` | 257 | 6 | Usually `ustar` followed by NUL |
|
||||
| `version` | 263 | 2 | Usually `00` |
|
||||
| `uname` | 265 | 32 | Owner name |
|
||||
| `gname` | 297 | 32 | Group name |
|
||||
| `devmajor` | 329 | 8 | Character/block device major number |
|
||||
| `devminor` | 337 | 8 | Character/block device minor number |
|
||||
| `prefix` | 345 | 155 | USTAR path prefix |
|
||||
| padding | 500 | 12 | Unused padding to 512 bytes |
|
||||
|
||||
SharpCompress parses the core fields in `TarHeader.Read` and `TarHeader.ReadAsync`. It reconstructs USTAR paths as `prefix + "/" + name` when `magic` is exactly `ustar` and `prefix` is non-empty.
|
||||
|
||||
## Entry Type Flags
|
||||
|
||||
Common POSIX typeflags:
|
||||
|
||||
| Typeflag | Meaning |
|
||||
| -------- | ------- |
|
||||
| NUL | Regular file, older tar form |
|
||||
| `0` | Regular file |
|
||||
| `1` | Hard link |
|
||||
| `2` | Symbolic link |
|
||||
| `3` | Character device |
|
||||
| `4` | Block device |
|
||||
| `5` | Directory |
|
||||
| `6` | FIFO |
|
||||
| `7` | Contiguous file, reserved by POSIX historical usage |
|
||||
| `x` | POSIX PAX local extended header for the following file |
|
||||
| `g` | POSIX PAX global extended header for following files |
|
||||
|
||||
GNU and other extension typeflags relevant to SharpCompress:
|
||||
|
||||
| Typeflag | Meaning |
|
||||
| -------- | ------- |
|
||||
| `K` | GNU long link target for the next real entry |
|
||||
| `L` | GNU long path name for the next real entry |
|
||||
| `S` | GNU sparse file |
|
||||
| `V` | GNU volume header |
|
||||
|
||||
SharpCompress declares these in `EntryType.cs`:
|
||||
|
||||
```text
|
||||
File = 0
|
||||
OldFile = '0'
|
||||
HardLink = '1'
|
||||
SymLink = '2'
|
||||
CharDevice = '3'
|
||||
BlockDevice = '4'
|
||||
Directory = '5'
|
||||
Fifo = '6'
|
||||
LongLink = 'K'
|
||||
LongName = 'L'
|
||||
SparseFile = 'S'
|
||||
VolumeHeader = 'V'
|
||||
LocalExtendedHeader = 'x'
|
||||
GlobalExtendedHeader = 'g'
|
||||
```
|
||||
|
||||
Declaration does not mean full semantic support. Sparse, device, FIFO, and volume-header semantics are not fully modeled by the public API.
|
||||
|
||||
## Numeric Fields
|
||||
|
||||
Standard tar numeric fields are ASCII octal values, usually NUL-terminated or space-padded depending on writer. Important fields include `mode`, `uid`, `gid`, `size`, `mtime`, `chksum`, `devmajor`, and `devminor`.
|
||||
|
||||
GNU tar can use base-256 binary encoding for values that exceed the octal field range:
|
||||
|
||||
- A leading byte with bit `0x80` indicates a positive binary value.
|
||||
- GNU documentation also describes `0xff` as a negative two's-complement marker.
|
||||
- The value bytes are big-endian.
|
||||
|
||||
SharpCompress currently handles binary `size` fields when bit `0x80` is set and writes large sizes in GNU long-link mode using a base-256-style binary `size` field. It also has an old GNU uid/gid quirk reader for fields beginning with `0x80 0x00`.
|
||||
|
||||
## Checksum
|
||||
|
||||
The checksum is the simple sum of all 512 header bytes, treating the 8-byte checksum field at offset 148 as spaces (`0x20`) during calculation.
|
||||
|
||||
SharpCompress checksum behavior:
|
||||
|
||||
- `RecalculateChecksum` fills the checksum field with eight spaces and sums bytes as unsigned values.
|
||||
- `checkChecksum` accepts both POSIX unsigned sums and signed-byte sums used by some historical tar implementations.
|
||||
- An all-zero block is treated as an empty/end marker case.
|
||||
|
||||
When editing parser code, keep checksum compatibility broad enough for old archives. When editing writer code, use POSIX unsigned checksum output.
|
||||
|
||||
## Path Names
|
||||
|
||||
Classic tar has a 100-byte `name` field. POSIX USTAR extends this with a 155-byte `prefix` field.
|
||||
|
||||
USTAR path reconstruction:
|
||||
|
||||
```text
|
||||
full path = prefix + "/" + name
|
||||
```
|
||||
|
||||
USTAR writer constraints:
|
||||
|
||||
- `name` must fit within the 100-byte name field.
|
||||
- `prefix` must fit within the 155-byte prefix field.
|
||||
- Splitting is normally done at a directory separator.
|
||||
|
||||
SharpCompress write behavior:
|
||||
|
||||
- `TarHeaderWriteFormat.USTAR` tries to split long paths into `prefix` and `name`.
|
||||
- If a path cannot fit, SharpCompress throws `InvalidFormatException` and tells callers to use GNU tar format.
|
||||
- `TarHeaderWriteFormat.GNU_TAR_LONG_LINK` writes GNU long-name metadata for names over 100 bytes.
|
||||
|
||||
SharpCompress path normalization in `TarWriter`:
|
||||
|
||||
- Converts backslashes to `/`.
|
||||
- Removes drive prefixes before `:`.
|
||||
- Trims leading and trailing `/` for file entries.
|
||||
- Ensures directory entries end with `/`.
|
||||
- Skips empty or root-equivalent directory names.
|
||||
|
||||
## PAX Extended Headers
|
||||
|
||||
POSIX PAX uses regular tar header blocks with special typeflags and a payload of UTF-8 key/value records.
|
||||
|
||||
PAX header typeflags:
|
||||
|
||||
- `x`: local extended header, applies to the next real file entry.
|
||||
- `g`: global extended header, applies to subsequent entries until overridden by another global or local header.
|
||||
|
||||
Each record has this form:
|
||||
|
||||
```text
|
||||
<decimal-length> <keyword>=<value>\n
|
||||
```
|
||||
|
||||
The decimal length includes every byte in the record, including the digits of the length itself, the space, key, equals sign, value, and newline.
|
||||
|
||||
SharpCompress PAX read support is intentionally limited to selected keys:
|
||||
|
||||
| Key | Effect |
|
||||
| --- | ------ |
|
||||
| `path` | Overrides entry path/name |
|
||||
| `linkpath` | Overrides hard/symbolic link target |
|
||||
| `size` | Overrides payload size |
|
||||
| `mtime` | Overrides modification time |
|
||||
| `uid` | Overrides owner id |
|
||||
| `gid` | Overrides group id |
|
||||
| `mode` | Overrides mode |
|
||||
|
||||
Local metadata overrides global metadata. Unknown PAX keys are ignored. PAX payload reads are capped at 65536 bytes to avoid memory exhaustion from malformed archives.
|
||||
|
||||
SharpCompress does not currently write PAX headers.
|
||||
|
||||
## GNU Tar Extensions
|
||||
|
||||
SharpCompress supports the most common GNU extensions needed for interoperability.
|
||||
|
||||
### Long Name and Long Link
|
||||
|
||||
GNU long-name and long-link records are synthetic entries that apply to the next real entry:
|
||||
|
||||
| Typeflag | Purpose |
|
||||
| -------- | ------- |
|
||||
| `L` | Long file name/path for next entry |
|
||||
| `K` | Long link target for next entry |
|
||||
|
||||
The payload contains the long name or link target, padded to a 512-byte boundary. SharpCompress caps long-name payload reads at 32768 bytes.
|
||||
|
||||
Writer behavior in `GNU_TAR_LONG_LINK` mode:
|
||||
|
||||
1. Write a synthetic `././@LongLink` header with `typeflag = 'L'` when the name exceeds 100 bytes.
|
||||
2. Write the long-name payload and 512-byte padding.
|
||||
3. Write the actual file or directory header.
|
||||
|
||||
### Sparse Files
|
||||
|
||||
GNU sparse tar uses `typeflag = 'S'` and additional sparse-map metadata. POSIX PAX sparse variants use keys such as `GNU.sparse.*`.
|
||||
|
||||
SharpCompress currently recognizes the sparse entry type enum value but does not reconstruct sparse holes or semantically expose sparse maps. Treat sparse support as unsupported unless implementing full reconstruction and tests.
|
||||
|
||||
### Base-256 Numeric Fields
|
||||
|
||||
GNU tar uses base-256 binary fields for out-of-range numeric values. SharpCompress reads binary `size` fields and writes large sizes in GNU mode.
|
||||
|
||||
## SharpCompress Support Matrix
|
||||
|
||||
Wrapper detection is defined in `TarWrapper.Wrappers`. Detection is content-based: wrapper detection is followed by a tar-header probe of the decompressed payload.
|
||||
|
||||
| Wrapper | Extensions | Read | Write |
|
||||
| ------- | ---------- | ---- | ----- |
|
||||
| Plain tar | `tar` | Yes | Yes |
|
||||
| Tar + GZip | `tar.gz`, `taz`, `tgz` | Yes | Yes |
|
||||
| Tar + BZip2 | `tar.bz2`, `tb2`, `tbz`, `tbz2`, `tz2` | Yes | Yes |
|
||||
| Tar + LZip | `tar.lz` | Yes | Yes |
|
||||
| Tar + XZ | `tar.xz`, `txz` | Yes | No |
|
||||
| Tar + ZStandard | `tar.zst`, `tar.zstd`, `tzst`, `tzstd` | Yes | No |
|
||||
| Tar + LZW compress | `tar.Z`, `tZ`, `taZ` | Yes | No |
|
||||
|
||||
Writer support currently accepts only these compression types:
|
||||
|
||||
- `CompressionType.None`
|
||||
- `CompressionType.GZip`
|
||||
- `CompressionType.BZip2`
|
||||
- `CompressionType.LZip`
|
||||
|
||||
Other compression types throw `InvalidFormatException`.
|
||||
|
||||
## SharpCompress Read Behavior
|
||||
|
||||
Reader API:
|
||||
|
||||
- `TarReader` is forward-only and supports non-seekable streams.
|
||||
- `ReaderFactory.OpenReader` can auto-detect tar and wrapper compression.
|
||||
- Entry streams must be consumed or disposed so the next header can be aligned.
|
||||
|
||||
Archive API:
|
||||
|
||||
- `TarArchive.OpenArchive(Stream)` and `TarArchive.OpenAsyncArchive(Stream)` require seekable streams.
|
||||
- File/path overloads own the opened file stream.
|
||||
- Compressed tar archive access follows streaming semantics over the decompressed stream rather than full random-access semantics.
|
||||
|
||||
Parsed metadata surfaced through entries includes:
|
||||
|
||||
- `Key`
|
||||
- `LinkTarget`
|
||||
- `Size`
|
||||
- `CompressedSize`
|
||||
- `LastModifiedTime`
|
||||
- `IsDirectory`
|
||||
- `Mode`
|
||||
- `UserID`
|
||||
- `GroupId`
|
||||
|
||||
Tar entries are always reported as unencrypted and CRC is always `0`.
|
||||
|
||||
## SharpCompress Write Behavior
|
||||
|
||||
`TarWriter` is forward-only. It writes a header, payload, payload padding, and finally two zero blocks on dispose when `FinalizeArchiveOnClose` is true.
|
||||
|
||||
Important writer rules:
|
||||
|
||||
- Tar requires payload size in the header.
|
||||
- If the source stream is non-seekable and no `size` is supplied, `TarWriter` throws `ArgumentException`.
|
||||
- `TarWriterOptions.HeaderFormat` defaults to `GNU_TAR_LONG_LINK`.
|
||||
- Current sync and async file and directory write paths honor `HeaderFormat`.
|
||||
- `USTAR` mode writes USTAR headers and throws when paths cannot fit.
|
||||
- `GNU_TAR_LONG_LINK` mode writes GNU long-name records for long paths.
|
||||
- The public writer supports regular files and directories, not links, devices, FIFOs, sparse maps, or PAX metadata.
|
||||
|
||||
Writer metadata is narrower than reader metadata. It writes name, size, last modified time, and file/directory type, and uses fixed mode/user/group defaults in the header.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
Keep these limitations explicit in code comments, docs, and tests:
|
||||
|
||||
- No PAX write support.
|
||||
- No sparse-file reconstruction or sparse write support.
|
||||
- No public API for writing symbolic links or hard links.
|
||||
- No device or FIFO metadata object model beyond internal type recognition.
|
||||
- No write support for `tar.xz`, `tar.zst`, or `tar.Z`.
|
||||
- PAX read support is limited to `path`, `linkpath`, `size`, `mtime`, `uid`, `gid`, and `mode`.
|
||||
- Unknown PAX keys are ignored.
|
||||
- Stream-based `TarArchive` open requires seekable input.
|
||||
|
||||
## Test Fixtures
|
||||
|
||||
Representative fixtures in `tests/TestArchives/Archives/`:
|
||||
|
||||
- `Tar.tar`
|
||||
- `Tar.tar.gz`
|
||||
- `Tar.tar.bz2`
|
||||
- `Tar.tar.lz`
|
||||
- `Tar.tar.xz`
|
||||
- `Tar.tar.zst`
|
||||
- `Tar.tar.Z`
|
||||
- `Tar.oldgnu.tar.gz`
|
||||
- `very long filename.tar`
|
||||
- `ustar with long names.tar`
|
||||
- `Tar.LongPathsWithLongNameExtension.tar`
|
||||
- `Tar.PaxLocalHeader.tar`
|
||||
- `Tar.PaxLocalHeader.Link.tar`
|
||||
- `Tar.PaxGlobalHeader.tar`
|
||||
- `Tar.PaxGlobalHeader.Link.tar`
|
||||
- `Tar.Empty.tar`
|
||||
- `TarCorrupted.tar`
|
||||
- `TarWithSymlink.tar.gz`
|
||||
|
||||
Representative test files:
|
||||
|
||||
- `tests/SharpCompress.Test/Tar/TarReaderTests.cs`
|
||||
- `tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs`
|
||||
- `tests/SharpCompress.Test/Tar/TarArchiveTests.cs`
|
||||
- `tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs`
|
||||
- `tests/SharpCompress.Test/Tar/TarWriterTests.cs`
|
||||
- `tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs`
|
||||
- `tests/SharpCompress.Test/Tar/TarWriterDirectoryTests.cs`
|
||||
- `tests/SharpCompress.Test/Tar/TarArchiveDirectoryTests.cs`
|
||||
22
.agents/skills/xz-lzma-format/SKILL.md
Normal file
22
.agents/skills/xz-lzma-format/SKILL.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
name: xz-lzma-format
|
||||
description: Reference the XZ container format and LZMA/LZMA2 decoder behavior. Use when an AI agent needs to answer questions or make code changes involving XZ headers, blocks, indexes, checks, CRC64/XZ, LZMA2 chunks, LZMA end markers, corrupt .xz test files, or SharpCompress XZ/LZMA parsing and decompression behavior.
|
||||
---
|
||||
|
||||
# XZ and LZMA Format
|
||||
|
||||
Use this skill for work at the boundary between the XZ container and the LZMA/LZMA2 compression streams. It captures the key details needed for SharpCompress XZ block parsing, XZ integrity checks, and LZMA2 decoder corruption handling.
|
||||
|
||||
## Reference
|
||||
|
||||
- Read [references/xz-lzma-format.md](references/xz-lzma-format.md) when the task depends on XZ binary layout, XZ block checks, CRC64/XZ parameters, LZMA2 chunk control bytes, LZMA end-of-payload markers, or XZ Utils bad-file expectations.
|
||||
- Treat XZ as a container around filter chains. Do not assume raw LZMA/LZMA2 behavior is equivalent to XZ stream validation.
|
||||
- Use the linked XZ Utils/liblzma sources in the reference when matching corruption behavior. The test corpus contains intentionally bad files that must throw even when they can produce all expected output bytes.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify the layer involved: XZ stream header/footer, block header, compressed data padding, block check, index, filter chain, LZMA2 chunk framing, or raw LZMA range decoding.
|
||||
2. Open `references/xz-lzma-format.md` and cross-check the relevant spec/source section before changing parser or decoder code.
|
||||
3. For XZ checksum work, verify the stream check type from the XZ header. Use CRC32, CRC64/XZ, SHA-256, or no check according to the header, not according to a test fixture assumption.
|
||||
4. For LZMA2 corruption work, compare SharpCompress behavior against the XZ Utils test corpus notes and liblzma decoder state model.
|
||||
5. Test both sync and async paths. Relevant files are `XZBlock.cs`, `XZBlock.Async.cs`, `XZStream.cs`, `XZStream.Async.cs`, `LzmaStream.cs`, `LzmaStream.Async.cs`, `LzmaDecoder.cs`, and `LzmaDecoder.Async.cs`.
|
||||
174
.agents/skills/xz-lzma-format/references/xz-lzma-format.md
Normal file
174
.agents/skills/xz-lzma-format/references/xz-lzma-format.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# XZ and LZMA/LZMA2 Reference
|
||||
|
||||
This reference summarizes the XZ container and LZMA/LZMA2 decoder facts that matter for SharpCompress maintenance. It is a local guide, not a full copy of the specs.
|
||||
|
||||
## Upstream References
|
||||
|
||||
- XZ file format specification: `https://raw.githubusercontent.com/tukaani-project/xz/master/doc/xz-file-format.txt`
|
||||
- liblzma LZMA2 decoder: `https://raw.githubusercontent.com/tukaani-project/xz/master/src/liblzma/lzma/lzma2_decoder.c`
|
||||
- liblzma LZMA decoder: `https://raw.githubusercontent.com/tukaani-project/xz/master/src/liblzma/lzma/lzma_decoder.c`
|
||||
- XZ Utils test file descriptions: `https://raw.githubusercontent.com/tukaani-project/xz/master/tests/files/README`
|
||||
- XZ range decoder reference, useful for `rc_is_finished` and normalization behavior: `https://raw.githubusercontent.com/tukaani-project/xz/master/src/liblzma/rangecoder/range_decoder.h`
|
||||
|
||||
## SharpCompress Pointers
|
||||
|
||||
- XZ container stream: `src/SharpCompress/Compressors/Xz/XZStream.cs`, `src/SharpCompress/Compressors/Xz/XZStream.Async.cs`
|
||||
- XZ block parsing/checks: `src/SharpCompress/Compressors/Xz/XZBlock.cs`, `src/SharpCompress/Compressors/Xz/XZBlock.Async.cs`
|
||||
- XZ header/footer/index: `XZHeader.cs`, `XZFooter.cs`, `XZIndex.cs`, `XZIndexRecord.cs`, and async counterparts.
|
||||
- XZ LZMA2 filter wrapper: `src/SharpCompress/Compressors/Xz/Filters/Lzma2Filter.cs`, `Lzma2Filter.Async.cs`
|
||||
- LZMA/LZMA2 stream and decoder: `src/SharpCompress/Compressors/LZMA/LzmaStream.cs`, `LzmaStream.Async.cs`, `LzmaDecoder.cs`, `LzmaDecoder.Async.cs`, `RangeCoder/RangeCoder.cs`, `RangeCoder/RangeCoder.Async.cs`
|
||||
- Core tests: `tests/SharpCompress.Test/Xz/*`, `tests/SharpCompress.Test/Streams/LzmaStreamTests.cs`, `tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs`
|
||||
- Corruption fixture discussed here: `tests/TestArchives/Archives/bad-1-lzma2-7.xz`
|
||||
|
||||
## XZ Container Structure
|
||||
|
||||
An XZ file is one or more XZ streams. A typical single stream is:
|
||||
|
||||
```text
|
||||
Stream Header -> Block(s) -> Index -> Stream Footer
|
||||
```
|
||||
|
||||
Important layout rules:
|
||||
|
||||
- XZ files and XZ streams are aligned to four-byte boundaries.
|
||||
- Stream header magic is `FD 37 7A 58 5A 00`.
|
||||
- Stream footer magic is `59 5A` (`YZ`).
|
||||
- Stream header/footer flags include the check type used for every block in the stream.
|
||||
- A block consists of `Block Header`, `Compressed Data`, `Block Padding`, and `Check`.
|
||||
- Block padding is 0-3 null bytes and makes the block size a multiple of four.
|
||||
- The Index contains one record per block: `Unpadded Size` and `Uncompressed Size`.
|
||||
|
||||
## Variable-Length Integers
|
||||
|
||||
XZ variable-length integers encode seven data bits per byte. The high bit means continuation. Current XZ limits the encoded integer to nine bytes/63 bits.
|
||||
|
||||
SharpCompress implementation:
|
||||
|
||||
- `MultiByteIntegers.ReadXZInteger` reads these values.
|
||||
- It rejects overlong encodings when a continuation byte is `0x00`.
|
||||
- Use this for block sizes, filter IDs, filter property sizes, index counts, and index record fields.
|
||||
|
||||
## XZ Checks Versus Raw LZMA
|
||||
|
||||
XZ checks are container-level integrity checks over uncompressed block data. Raw LZMA/LZMA2 decoding does not provide the same container-level CRC validation.
|
||||
|
||||
Supported XZ check IDs in SharpCompress:
|
||||
|
||||
- `0x00`: none, 0 bytes.
|
||||
- `0x01`: CRC32, 4 bytes.
|
||||
- `0x04`: CRC64/XZ, 8 bytes.
|
||||
- `0x0A`: SHA-256, 32 bytes.
|
||||
|
||||
CRC64/XZ details:
|
||||
|
||||
- Polynomial: reflected ECMA polynomial `0xC96C5795D7870F42`.
|
||||
- Initial value: `0xffffffffffffffff`.
|
||||
- Final XOR: `0xffffffffffffffff`.
|
||||
- Stored little-endian in the block check field.
|
||||
- Test vector for `"123456789"`: `0x995DC9BBDF1939FA`.
|
||||
|
||||
Common pitfall:
|
||||
|
||||
- CRC64/XZ is not the older `Iso3309Polynomial = 0xD800000000000000` path that existed in SharpCompress's generic `Crc64` helper. Using that produces wrong XZ block check values.
|
||||
|
||||
## XZ Block Check Handling
|
||||
|
||||
When reading an `XZBlock`:
|
||||
|
||||
1. Parse and CRC-validate the block header.
|
||||
2. Build the filter chain in reverse order from the List of Filter Flags.
|
||||
3. Read uncompressed bytes through the filter chain and update the selected check over the uncompressed bytes.
|
||||
4. At block end, skip/validate block padding.
|
||||
5. Read the check field and compare to the computed value.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- A short `Stream.Read` result does not universally mean EOF. Be careful when using `bytesRead != count` as an end-of-block signal.
|
||||
- Tests using `StreamReader.ReadToEnd()` often expose end-of-block behavior because they force padding/check validation.
|
||||
- The check type must match the XZ stream header. For example, a fixture may have one XZ stream using CRC32 and another using CRC64; do not hard-code CRC64 in block tests.
|
||||
|
||||
## LZMA2 Chunks
|
||||
|
||||
LZMA2 is the only LZMA-family filter defined for XZ (`Filter ID 0x21`). Raw LZMA is not an XZ filter.
|
||||
|
||||
LZMA2 control byte categories from liblzma:
|
||||
|
||||
- `0x00`: LZMA2 end marker.
|
||||
- `0x01` or `>= 0xE0`: dictionary reset; the next LZMA chunk must set new properties.
|
||||
- `>= 0x80`: LZMA chunk. The control byte and following two bytes encode uncompressed chunk size. The next two bytes encode compressed chunk size. Some control values also provide new LZMA properties.
|
||||
- `0x02`: uncompressed chunk without dictionary reset.
|
||||
- `0x01`: uncompressed chunk with dictionary reset.
|
||||
- `0x03..0x7F`: invalid/reserved control values.
|
||||
|
||||
For LZMA chunks, the LZMA2 decoder must track:
|
||||
|
||||
- Exact uncompressed chunk size.
|
||||
- Exact compressed chunk size.
|
||||
- Whether LZMA properties are needed.
|
||||
- Whether dictionary reset is required.
|
||||
- Whether the inner LZMA stream saw an LZMA end-of-payload marker.
|
||||
|
||||
## LZMA End-Of-Payload Marker In LZMA2
|
||||
|
||||
The XZ Utils bad-file corpus describes `bad-1-lzma2-7.xz` as:
|
||||
|
||||
```text
|
||||
bad-1-lzma2-7.xz has EOPM at LZMA level.
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
- The outer XZ container can be parsed.
|
||||
- The LZMA2 stream can produce all advertised uncompressed bytes.
|
||||
- The inner raw LZMA decoder still reaches an LZMA end-of-payload marker (`rep0 == uint.MaxValue`).
|
||||
- LZMA2 must reject this. End-of-payload markers are for raw LZMA cases with unknown size; they are not valid as an LZMA-level terminator inside an LZMA2 chunk.
|
||||
|
||||
liblzma behavior:
|
||||
|
||||
- `lzma2_decoder.c` calls the inner LZMA decoder with a known `uncompressed_size` and `allow_eopm = false`.
|
||||
- `lzma_decoder.c` treats EOPM as data error when EOPM is not valid.
|
||||
- The XZ Utils test README says all `bad-*` files must cause decoder errors.
|
||||
|
||||
SharpCompress maintenance guidance:
|
||||
|
||||
- If a bad LZMA2 fixture produces all output bytes but `xz --test` reports corrupt data, inspect the inner decoder state, not just output length or XZ block check.
|
||||
- In SharpCompress, `Decoder.HasEndMarker => _rep0 == uint.MaxValue` is the useful signal for the `bad-1-lzma2-7.xz` case.
|
||||
- Validate both sync and async paths; `Stream.CopyToAsync` can use byte-array or `Memory<byte>` read overloads depending on target framework and wrapper stream.
|
||||
|
||||
## Exception Expectations
|
||||
|
||||
`DataErrorException` is internal and derives from `SharpCompressException`. Some public XZ parsing failures throw `InvalidFormatException`; raw decoder corruption may surface as `SharpCompressException` via `DataErrorException` unless the wrapper maps it.
|
||||
|
||||
Testing guidance:
|
||||
|
||||
- Use exact `InvalidFormatException` when the code path is XZ header/footer/block/index/check validation.
|
||||
- Use `Assert.ThrowsAnyAsync<SharpCompressException>` or equivalent when the desired behavior is simply that corrupt LZMA/LZMA2 data is rejected.
|
||||
- Do not weaken tests to accept no exception for XZ Utils `bad-*` fixtures.
|
||||
|
||||
## Useful Commands
|
||||
|
||||
Use the system `xz` tool as an oracle when available:
|
||||
|
||||
```bash
|
||||
xz --test --verbose tests/TestArchives/Archives/bad-1-lzma2-7.xz
|
||||
xz --robot --list --verbose tests/TestArchives/Archives/bad-1-lzma2-7.xz
|
||||
```
|
||||
|
||||
Expected for `bad-1-lzma2-7.xz`:
|
||||
|
||||
```text
|
||||
xz: tests/TestArchives/Archives/bad-1-lzma2-7.xz: Compressed data is corrupt
|
||||
```
|
||||
|
||||
Targeted SharpCompress tests:
|
||||
|
||||
```bash
|
||||
dotnet test tests/SharpCompress.Test/SharpCompress.Test.csproj --framework net10.0 --filter "FullyQualifiedName~SharpCompress.Test.Streams.LzmaStream|FullyQualifiedName~SharpCompress.Test.Xz"
|
||||
```
|
||||
|
||||
## Current SharpCompress Gotchas
|
||||
|
||||
- XZ index CRC32 verification may still be incomplete; check `XZIndex.VerifyCrc32` before relying on index corruption detection.
|
||||
- XZ block/index size semantics are easy to confuse. `Unpadded Size` excludes block padding but includes header, compressed data, and check. `Uncompressed Size` is raw output size.
|
||||
- LZMA2 chunks include their own compressed and uncompressed chunk sizes; these are separate from XZ block header/index sizes.
|
||||
- `StreamReader.ReadToEnd()` and `TransferToAsync(Stream.Null, long.MaxValue)` are useful for forcing full-stream validation.
|
||||
22
.agents/skills/zip-format/SKILL.md
Normal file
22
.agents/skills/zip-format/SKILL.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
name: zip-format
|
||||
description: Reference the ZIP/ZIP64/PKWARE APPNOTE archive container format. Use when an AI agent needs to answer questions or make code changes involving ZIP local headers, central directory records, EOCD/Zip64 records, data descriptors, general purpose bit flags, compression method IDs, extra fields, encryption markers, split archives, or SharpCompress Zip parsing and writing behavior.
|
||||
---
|
||||
|
||||
# Zip Format
|
||||
|
||||
Use this skill for ZIP container-format work. It provides a local, SharpCompress-oriented reference for PKWARE APPNOTE ZIP records, ZIP64, compression method IDs, extra fields, and the current SharpCompress Zip implementation.
|
||||
|
||||
## Reference
|
||||
|
||||
- Read [references/zip-format.md](references/zip-format.md) when the task depends on ZIP binary layout, record signatures, local vs central directory metadata, data descriptor rules, Zip64 sentinel values, extra field parsing, compression method IDs, encryption markers, split archive handling, or current SharpCompress Zip support boundaries.
|
||||
- Treat the reference as an implementation guide, not a standards replacement. It summarizes PKWARE APPNOTE 6.3.10 from `https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT` and documents SharpCompress-specific behavior and limitations.
|
||||
- Prefer the SharpCompress support matrix in the reference over generic ZIP assumptions when changing code. ZIP is highly extensible, and SharpCompress intentionally supports only selected records, methods, and extra fields.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify which layer is involved: header discovery, local file headers, central directory headers, EOCD/Zip64 records, data descriptors, extra fields, compression methods, encryption, reader/archive/writer API behavior, or tests.
|
||||
2. Open the relevant section in `references/zip-format.md` and use the source-file pointers before changing code.
|
||||
3. For parsing changes, cross-check sync and async header readers: `ZipHeaderFactory.cs`, `ZipHeaderFactory.Async.cs`, `SeekableZipHeaderFactory.cs`, `StreamingZipHeaderFactory.cs`, `ZipFileEntry.cs`, `LocalEntryHeader.cs`, and `DirectoryEntryHeader.cs`.
|
||||
4. For writer changes, verify local header, post-data descriptor, central directory, Zip64, and sync/async paths together.
|
||||
5. For support claims, keep unsupported features explicit: central directory encryption, strong encryption records, XZ writing, non-standard compression methods, broad extra-field semantics, and non-seekable Zip64 writing.
|
||||
456
.agents/skills/zip-format/references/zip-format.md
Normal file
456
.agents/skills/zip-format/references/zip-format.md
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
# ZIP Format Reference
|
||||
|
||||
This reference summarizes the ZIP archive container format for SharpCompress work. It is locally authored from PKWARE APPNOTE and the current SharpCompress implementation.
|
||||
|
||||
Primary external reference:
|
||||
|
||||
- PKWARE APPNOTE.TXT - ZIP File Format Specification, version 6.3.10: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
||||
|
||||
Primary SharpCompress references:
|
||||
|
||||
- `docs/FORMATS.md`
|
||||
- `src/SharpCompress/Factories/ZipFactory.cs`
|
||||
- `src/SharpCompress/Archives/Zip/ZipArchive.cs`
|
||||
- `src/SharpCompress/Readers/Zip/ZipReader.cs`
|
||||
- `src/SharpCompress/Writers/Zip/ZipWriter.cs`
|
||||
- `src/SharpCompress/Writers/Zip/ZipWritingStream.cs`
|
||||
- `src/SharpCompress/Writers/Zip/ZipCentralDirectoryEntry.cs`
|
||||
- `src/SharpCompress/Common/Zip/ZipCompressionMethod.cs`
|
||||
- `src/SharpCompress/Common/Zip/ZipEntry.cs`
|
||||
- `src/SharpCompress/Common/Zip/ZipFilePart.cs`
|
||||
- `src/SharpCompress/Common/Zip/ZipHeaderFactory.cs`
|
||||
- `src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs`
|
||||
- `src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs`
|
||||
- `src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.Async.cs`
|
||||
- `src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs`
|
||||
- `src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs`
|
||||
- `src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs`
|
||||
- `src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs`
|
||||
- `src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs`
|
||||
- `src/SharpCompress/Common/Zip/Headers/LocalEntryHeaderExtraFactory.cs`
|
||||
- `tests/SharpCompress.Test/Zip/`
|
||||
|
||||
## Contents
|
||||
|
||||
- [Format Overview](#format-overview)
|
||||
- [Record Signatures](#record-signatures)
|
||||
- [Local File Header](#local-file-header)
|
||||
- [Central Directory Header](#central-directory-header)
|
||||
- [End Of Central Directory](#end-of-central-directory)
|
||||
- [Data Descriptors](#data-descriptors)
|
||||
- [General Purpose Bit Flags](#general-purpose-bit-flags)
|
||||
- [Compression Methods](#compression-methods)
|
||||
- [Extra Fields](#extra-fields)
|
||||
- [Zip64](#zip64)
|
||||
- [Names, Comments, And Encoding](#names-comments-and-encoding)
|
||||
- [Encryption](#encryption)
|
||||
- [Seekable And Streaming Reads](#seekable-and-streaming-reads)
|
||||
- [SharpCompress Support Matrix](#sharpcompress-support-matrix)
|
||||
- [SharpCompress Write Behavior](#sharpcompress-write-behavior)
|
||||
- [Known Limitations](#known-limitations)
|
||||
- [Test Fixtures](#test-fixtures)
|
||||
|
||||
## Format Overview
|
||||
|
||||
A ZIP archive stores each file as a local file record followed by compressed or stored payload bytes. Metadata is repeated in a central directory near the end of the archive, followed by the end of central directory record.
|
||||
|
||||
High-level APPNOTE layout:
|
||||
|
||||
```text
|
||||
[local file header 1]
|
||||
[encryption header 1]
|
||||
[file data 1]
|
||||
[data descriptor 1]
|
||||
...
|
||||
[local file header n]
|
||||
[encryption header n]
|
||||
[file data n]
|
||||
[data descriptor n]
|
||||
[archive decryption header]
|
||||
[archive extra data record]
|
||||
[central directory header 1]
|
||||
...
|
||||
[central directory header n]
|
||||
[zip64 end of central directory record]
|
||||
[zip64 end of central directory locator]
|
||||
[end of central directory record]
|
||||
```
|
||||
|
||||
All ordinary multi-byte ZIP fields are little-endian unless APPNOTE says otherwise. ZIP readers must identify records by signatures rather than by extension.
|
||||
|
||||
SharpCompress has two important read modes:
|
||||
|
||||
- Seekable Archive API reads the central directory first and then seeks to local headers for entry data.
|
||||
- Streaming Reader API processes local headers and payloads in order and cannot rely on central directory data unless it is already supplied by the caller/path flow.
|
||||
|
||||
## Record Signatures
|
||||
|
||||
Common ZIP signatures used by SharpCompress:
|
||||
|
||||
| Record | Signature | SharpCompress constant |
|
||||
| --- | --- | --- |
|
||||
| Local file header | `0x04034b50` | `ENTRY_HEADER_BYTES` |
|
||||
| Data descriptor | `0x08074b50` | `POST_DATA_DESCRIPTOR` |
|
||||
| Central directory file header | `0x02014b50` | `DIRECTORY_START_HEADER_BYTES` |
|
||||
| End of central directory | `0x06054b50` | `DIRECTORY_END_HEADER_BYTES` |
|
||||
| Digital signature | `0x05054b50` | `DIGITAL_SIGNATURE` |
|
||||
| Split archive marker | `0x30304b50` | `SPLIT_ARCHIVE_HEADER_BYTES` |
|
||||
| Zip64 end of central directory | `0x06064b50` | `ZIP64_END_OF_CENTRAL_DIRECTORY` |
|
||||
| Zip64 end of central directory locator | `0x07064b50` | `ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR` |
|
||||
|
||||
`ZipHeaderFactory.IsHeader` recognizes these signatures while streaming.
|
||||
|
||||
## Local File Header
|
||||
|
||||
The local file header immediately precedes optional encryption metadata and file data.
|
||||
|
||||
| Field | Size | Notes |
|
||||
| --- | --- | --- |
|
||||
| Signature | 4 | `0x04034b50` |
|
||||
| Version needed to extract | 2 | Feature/version indicator |
|
||||
| General purpose bit flag | 2 | Encryption, data descriptor, EFS, method-specific bits |
|
||||
| Compression method | 2 | See [Compression Methods](#compression-methods) |
|
||||
| Last mod file time | 2 | MS-DOS time |
|
||||
| Last mod file date | 2 | MS-DOS date |
|
||||
| CRC-32 | 4 | Zero when bit 3 defers values to data descriptor |
|
||||
| Compressed size | 4 | `0xffffffff` sentinel when Zip64 extra carries value |
|
||||
| Uncompressed size | 4 | `0xffffffff` sentinel when Zip64 extra carries value |
|
||||
| File name length | 2 | Byte count |
|
||||
| Extra field length | 2 | Byte count |
|
||||
| File name | variable | Not NUL-terminated |
|
||||
| Extra field | variable | Sequence of ID/length/data blocks |
|
||||
|
||||
SharpCompress reads this in `LocalEntryHeader.Read` and then decodes names, loads extra fields, applies Unicode path extra data, applies Zip64 extra data, and applies selected Unix time data.
|
||||
|
||||
## Central Directory Header
|
||||
|
||||
The central directory file header repeats entry metadata and adds fields needed for random access.
|
||||
|
||||
| Field | Size | Notes |
|
||||
| --- | --- | --- |
|
||||
| Signature | 4 | `0x02014b50` |
|
||||
| Version made by | 2 | Host/version metadata |
|
||||
| Version needed to extract | 2 | Feature/version indicator |
|
||||
| General purpose bit flag | 2 | Same semantic space as local header |
|
||||
| Compression method | 2 | See [Compression Methods](#compression-methods) |
|
||||
| Last mod file time | 2 | MS-DOS time |
|
||||
| Last mod file date | 2 | MS-DOS date |
|
||||
| CRC-32 | 4 | Entry CRC |
|
||||
| Compressed size | 4 | Zip64 sentinel possible |
|
||||
| Uncompressed size | 4 | Zip64 sentinel possible |
|
||||
| File name length | 2 | Byte count |
|
||||
| Extra field length | 2 | Byte count |
|
||||
| File comment length | 2 | Byte count |
|
||||
| Disk number start | 2 | Zip64 sentinel possible |
|
||||
| Internal file attributes | 2 | Host/application metadata |
|
||||
| External file attributes | 4 | Host-dependent file attributes |
|
||||
| Relative offset of local header | 4 | Zip64 sentinel possible |
|
||||
| File name | variable | Not NUL-terminated |
|
||||
| Extra field | variable | Sequence of ID/length/data blocks |
|
||||
| File comment | variable | Not NUL-terminated |
|
||||
|
||||
SharpCompress reads this in `DirectoryEntryHeader.Read`. Seekable ZIP reads use central directory entries to discover the archive contents, then `SeekableZipHeaderFactory.GetLocalHeader` seeks to local headers and copies central-directory-only metadata onto the local entry.
|
||||
|
||||
## End Of Central Directory
|
||||
|
||||
Every normal ZIP archive ends with exactly one EOCD record. The minimum EOCD length is 22 bytes, plus an optional ZIP file comment up to 65535 bytes.
|
||||
|
||||
EOCD fields:
|
||||
|
||||
| Field | Size | Notes |
|
||||
| --- | --- | --- |
|
||||
| Signature | 4 | `0x06054b50` |
|
||||
| Number of this disk | 2 | Split/spanned metadata |
|
||||
| Central directory start disk | 2 | Split/spanned metadata |
|
||||
| Entries on this disk | 2 | `0xffff` sentinel when Zip64 is needed |
|
||||
| Total entries | 2 | `0xffff` sentinel when Zip64 is needed |
|
||||
| Central directory size | 4 | `0xffffffff` sentinel when Zip64 is needed |
|
||||
| Central directory offset | 4 | `0xffffffff` sentinel when Zip64 is needed |
|
||||
| ZIP file comment length | 2 | Byte count |
|
||||
| ZIP file comment | variable | Archive-level comment |
|
||||
|
||||
`SeekableZipHeaderFactory.SeekBackToHeader` searches backwards from the end of the stream across the maximum EOCD/comment search window.
|
||||
|
||||
## Data Descriptors
|
||||
|
||||
When general purpose bit 3 is set, the local header CRC and size fields are placeholders. The actual values follow file data in a data descriptor.
|
||||
|
||||
Descriptor forms:
|
||||
|
||||
```text
|
||||
[optional signature 0x08074b50]
|
||||
crc-32 4 bytes
|
||||
compressed size 4 or 8 bytes
|
||||
uncompressed size 4 or 8 bytes
|
||||
```
|
||||
|
||||
APPNOTE says the signature was not originally assigned but is commonly used. SharpCompress handles descriptors with and without the signature in streaming reads.
|
||||
|
||||
Important SharpCompress behavior:
|
||||
|
||||
- `StreamingZipHeaderFactory` reads post-data descriptors after the previous entry stream has been consumed.
|
||||
- Streaming descriptor parsing has compatibility logic for 32-bit and 64-bit sizes.
|
||||
- `ZipWriter` writes a descriptor with signature for non-seekable output when Zip64 is not required.
|
||||
- `ZipWriter` intentionally rejects Zip64 on non-seekable streams.
|
||||
|
||||
## General Purpose Bit Flags
|
||||
|
||||
Selected flags relevant to SharpCompress:
|
||||
|
||||
| Bit | Meaning |
|
||||
| --- | --- |
|
||||
| 0 | Entry is encrypted |
|
||||
| 1 | Method-specific. For LZMA method 14, set means EOS marker is used. For Implode it has dictionary-size meaning. |
|
||||
| 2 | Method-specific. For Deflate/Deflate64 it encodes compression option; for Implode it has tree-count meaning. |
|
||||
| 3 | CRC and sizes are deferred to a data descriptor after file data |
|
||||
| 6 | Strong encryption |
|
||||
| 11 | Language encoding flag (EFS): file name and comment are UTF-8 |
|
||||
| 13 | Central directory encryption masks selected local header values |
|
||||
|
||||
SharpCompress decodes names/comments as UTF-8 when EFS is set. For ZIP LZMA writing on non-seekable output, it sets bit 1 for EOS marker behavior.
|
||||
|
||||
## Compression Methods
|
||||
|
||||
APPNOTE compression method IDs relevant to SharpCompress and nearby unsupported methods:
|
||||
|
||||
| ID | APPNOTE method | SharpCompress status |
|
||||
| --- | --- | --- |
|
||||
| 0 | Stored | Read/write |
|
||||
| 1 | Shrunk | Read |
|
||||
| 2 | Reduced factor 1 | Read |
|
||||
| 3 | Reduced factor 2 | Read |
|
||||
| 4 | Reduced factor 3 | Read |
|
||||
| 5 | Reduced factor 4 | Read |
|
||||
| 6 | Imploded | Read |
|
||||
| 8 | Deflated | Read/write |
|
||||
| 9 | Deflate64 | Read |
|
||||
| 12 | BZIP2 | Read/write |
|
||||
| 14 | LZMA | Read/write |
|
||||
| 93 | Zstandard | Read/write |
|
||||
| 95 | XZ | Read |
|
||||
| 98 | PPMd version I, Rev 1 | Read/write |
|
||||
| 99 | AE-x encryption marker | Read for WinZip AES handling |
|
||||
|
||||
SharpCompress declares these in `ZipCompressionMethod.cs`:
|
||||
|
||||
```text
|
||||
None = 0
|
||||
Shrink = 1
|
||||
Reduce1 = 2
|
||||
Reduce2 = 3
|
||||
Reduce3 = 4
|
||||
Reduce4 = 5
|
||||
Explode = 6
|
||||
Deflate = 8
|
||||
Deflate64 = 9
|
||||
BZip2 = 12
|
||||
LZMA = 14
|
||||
ZStandard = 93
|
||||
Xz = 95
|
||||
PPMd = 98
|
||||
WinzipAes = 0x63
|
||||
```
|
||||
|
||||
ZIP has method 14 for LZMA and method 95 for XZ. There is no separate APPNOTE ZIP compression method named LZMA2 in the method table. XZ commonly uses LZMA2 internally, but a ZIP entry using APPNOTE method 95 is an XZ-compressed ZIP entry, not a separate LZMA2 ZIP method.
|
||||
|
||||
`ZipFilePart.ToCompressionType` maps supported read methods to public `CompressionType` values and throws for unsupported methods before decompression. `ZipEntry.CompressionType` reports the public entry compression type, including `CompressionType.Xz` for method 95.
|
||||
|
||||
## Extra Fields
|
||||
|
||||
APPNOTE extra fields use this generic structure:
|
||||
|
||||
```text
|
||||
header id 2 bytes
|
||||
data size 2 bytes
|
||||
data variable
|
||||
```
|
||||
|
||||
SharpCompress parses extra fields in `ZipFileEntry.LoadExtra`. Unknown or unsupported extra fields become `NotImplementedExtraData` and are preserved only as raw data for internal parsing decisions.
|
||||
|
||||
Recognized extra fields:
|
||||
|
||||
| Header ID | Meaning | SharpCompress type |
|
||||
| --- | --- | --- |
|
||||
| `0x0001` | Zip64 extended information | `Zip64ExtendedInformationExtraField` |
|
||||
| `0x5455` | Extended timestamp / Unix time | `UnixTimeExtraField` |
|
||||
| `0x7075` | Info-ZIP Unicode path | `ExtraUnicodePathExtraField` |
|
||||
| `0x9901` | WinZip AES | Raw `ExtraData` used by AES logic |
|
||||
|
||||
Zip64 extra values appear only when the corresponding 16-bit or 32-bit field in the local or central directory is set to its maximum sentinel. Values must appear in APPNOTE order:
|
||||
|
||||
1. Original/uncompressed size
|
||||
2. Compressed size
|
||||
3. Relative header offset
|
||||
4. Disk start number
|
||||
|
||||
`Zip64ExtendedInformationExtraField.Process` enforces the required byte count for the sentinel fields being resolved.
|
||||
|
||||
## Zip64
|
||||
|
||||
Zip64 extends size, count, and offset fields beyond classic ZIP limits. Classic fields use sentinel values when the real value is stored elsewhere:
|
||||
|
||||
| Classic field size | Sentinel |
|
||||
| --- | --- |
|
||||
| 2 bytes | `0xffff` |
|
||||
| 4 bytes | `0xffffffff` |
|
||||
|
||||
Zip64 structures:
|
||||
|
||||
- Zip64 extended information extra field (`0x0001`) carries per-entry sizes and offsets.
|
||||
- Zip64 end of central directory record (`0x06064b50`) carries archive-level counts, central directory size, and central directory offset.
|
||||
- Zip64 end of central directory locator (`0x07064b50`) points to the Zip64 EOCD record.
|
||||
|
||||
SharpCompress read behavior:
|
||||
|
||||
- Seekable reads detect Zip64 through EOCD sentinel values and then locate the Zip64 EOCD locator and record.
|
||||
- Local and central entry readers apply Zip64 extra data when size/offset fields contain sentinels.
|
||||
|
||||
SharpCompress write behavior:
|
||||
|
||||
- `ZipWriterOptions.UseZip64` controls whether local headers reserve Zip64 extra data for entries.
|
||||
- `ZipWriter` emits Zip64 EOCD and locator when entry counts, central directory size, or offsets require them.
|
||||
- Non-seekable Zip64 writing is rejected because current post-data descriptor handling cannot safely represent the required Zip64 values in all cases.
|
||||
|
||||
## Names, Comments, And Encoding
|
||||
|
||||
APPNOTE file name and comment fields are length-prefixed byte sequences, not NUL-terminated strings.
|
||||
|
||||
Rules relevant to SharpCompress:
|
||||
|
||||
- If general purpose bit 11 (EFS) is set, file names and comments must be UTF-8.
|
||||
- If EFS is not set, SharpCompress uses the configured archive encoding.
|
||||
- If Info-ZIP Unicode path extra field `0x7075` is present and the caller did not force an encoding, SharpCompress uses the Unicode name from that extra field.
|
||||
- `ZipWriter.NormalizeFilename` converts backslashes to `/`, removes drive prefixes before `:`, and trims leading/trailing `/` for file entries.
|
||||
- `ZipWriter` ensures directory entries end with `/`.
|
||||
|
||||
## Encryption
|
||||
|
||||
APPNOTE defines traditional PKWARE encryption, strong encryption features, central directory encryption, and method 99 as an AE-x encryption marker.
|
||||
|
||||
SharpCompress behavior:
|
||||
|
||||
- Traditional PKWARE-encrypted ZIP entries can be read when a password is supplied.
|
||||
- WinZip AES entries use method 99 (`WinzipAes`) and extra field `0x9901`; SharpCompress reads the actual compression method from that extra data.
|
||||
- `ZipHeaderFactory.LoadHeader` rejects encrypted ZIP data that requires unsupported non-seekable handling.
|
||||
- Central directory encryption and broad strong encryption records are not general-purpose supported features. Keep this explicit in support claims.
|
||||
|
||||
## Seekable And Streaming Reads
|
||||
|
||||
Seekable read path:
|
||||
|
||||
- `SeekableZipHeaderFactory` searches backward for EOCD.
|
||||
- If EOCD indicates Zip64, it reads the Zip64 locator and Zip64 EOCD.
|
||||
- It iterates central directory file headers.
|
||||
- `GetLocalHeader` seeks to each entry's local header when entry data is needed.
|
||||
|
||||
Streaming read path:
|
||||
|
||||
- `StreamingZipHeaderFactory` reads local headers in archive order.
|
||||
- Entry data must be consumed or skipped before the next header can be parsed.
|
||||
- If bit 3 is set, descriptor values are read after entry data.
|
||||
- Directory entries may be inferred from names ending in `/`, or from zero-size names ending in `\` for older .NET-produced archives.
|
||||
|
||||
Archive API vs Reader API:
|
||||
|
||||
- `ZipArchive` is appropriate for seekable streams and multi-volume/split archives.
|
||||
- `ZipReader` is forward-only and does not seek across volume files.
|
||||
|
||||
## SharpCompress Support Matrix
|
||||
|
||||
Current ZIP support summary from `docs/FORMATS.md` and implementation:
|
||||
|
||||
| Feature | Read | Write | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Stored | Yes | Yes | Method 0 |
|
||||
| Deflate | Yes | Yes | Method 8 |
|
||||
| Deflate64 | Yes | No | Method 9 |
|
||||
| BZip2 | Yes | Yes | Method 12 |
|
||||
| LZMA | Yes | Yes | Method 14 |
|
||||
| PPMd | Yes | Yes | Method 98 |
|
||||
| ZStandard | Yes | Yes | Method 93 |
|
||||
| XZ | Yes | No | Method 95 |
|
||||
| Shrink | Yes | No | Legacy method 1 |
|
||||
| Reduce | Yes | No | Legacy methods 2-5 |
|
||||
| Implode | Yes | No | Legacy method 6 |
|
||||
| Zip64 | Yes | Yes | Writing requires seekable stream for Zip64 |
|
||||
| Data descriptors | Yes | Yes | Writer uses them for non-seekable non-Zip64 output |
|
||||
| Traditional PKWARE encryption | Yes | No | Password required |
|
||||
| WinZip AES | Yes | No | Method 99 + extra field `0x9901` |
|
||||
| Split/multi-volume ZIP | Yes | No | Use Archive API |
|
||||
|
||||
## SharpCompress Write Behavior
|
||||
|
||||
`ZipWriter` is forward-only for entry creation but writes a central directory on dispose. It tracks local header offsets and entry sizes as data is written.
|
||||
|
||||
Writer compression mapping in `ZipWriter.ToZipCompressionMethod` currently accepts:
|
||||
|
||||
- `CompressionType.None`
|
||||
- `CompressionType.Deflate`
|
||||
- `CompressionType.BZip2`
|
||||
- `CompressionType.LZMA`
|
||||
- `CompressionType.PPMd`
|
||||
- `CompressionType.ZStandard`
|
||||
|
||||
Other compression types throw `InvalidFormatException`, including `CompressionType.Xz`.
|
||||
|
||||
Important writer rules:
|
||||
|
||||
- Local headers are written before payload data.
|
||||
- Seekable output lets SharpCompress patch CRC and size fields after compression.
|
||||
- Non-seekable output uses post-data descriptors for non-Zip64 entries.
|
||||
- Zip64 on non-seekable output is rejected.
|
||||
- Zero-byte file entries are normalized to stored/no compression in the central directory.
|
||||
- Central directory entries are written on `Dispose` / `DisposeAsync`; callers must dispose writers to finalize archives.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
Keep these limitations explicit in code comments, docs, and tests:
|
||||
|
||||
- No separate ZIP LZMA2 method support. APPNOTE method 95 is XZ; XZ may use LZMA2 internally.
|
||||
- No XZ writing for ZIP entries.
|
||||
- No Deflate64, Shrink, Reduce, or Implode writing.
|
||||
- No general-purpose central directory encryption support.
|
||||
- No broad APPNOTE strong encryption record support beyond current traditional PKWARE and WinZip AES read paths.
|
||||
- No non-seekable Zip64 writing.
|
||||
- Unknown extra fields are not semantically modeled unless explicitly implemented.
|
||||
- ZipReader cannot seek across multi-volume/split archive parts; use ZipArchive for split archives.
|
||||
|
||||
## Test Fixtures
|
||||
|
||||
Representative fixtures in `tests/TestArchives/Archives/`:
|
||||
|
||||
- `Zip.none.zip`
|
||||
- `Zip.deflate.zip`
|
||||
- `Zip.deflate.dd.zip`
|
||||
- `Zip.deflate64.zip`
|
||||
- `Zip.bzip2.zip`
|
||||
- `Zip.lzma.zip`
|
||||
- `Zip.lzma.dd.zip`
|
||||
- `Zip.ppmd.zip`
|
||||
- `Zip.shrink.zip`
|
||||
- `Zip.reduce1.zip`
|
||||
- `Zip.reduce2.zip`
|
||||
- `Zip.reduce3.zip`
|
||||
- `Zip.reduce4.zip`
|
||||
- `Zip.implode.zip`
|
||||
- `Zip.zip64.zip`
|
||||
- `Zip.zstd.WinzipAES.mixed.zip`
|
||||
- `WinZip27_XZ.zipx`
|
||||
- `WinZip27_ZSTD.zipx`
|
||||
- `WinZip26.nocomp.multi.zip`
|
||||
- `WinZip26.nocomp.multi.zipx`
|
||||
- `Zip.UnicodePathExtra.zip`
|
||||
- `Zip.EntryComment.zip`
|
||||
|
||||
Representative test files:
|
||||
|
||||
- `tests/SharpCompress.Test/Zip/ZipArchiveTests.cs`
|
||||
- `tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs`
|
||||
- `tests/SharpCompress.Test/Zip/ZipReaderTests.cs`
|
||||
- `tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs`
|
||||
- `tests/SharpCompress.Test/Zip/ZipWriterTests.cs`
|
||||
- `tests/SharpCompress.Test/Zip/ZipWriterAsyncTests.cs`
|
||||
- `tests/SharpCompress.Test/Zip/Zip64Tests.cs`
|
||||
- `tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs`
|
||||
- `tests/SharpCompress.Test/Zip/Zip64VersionConsistencyTests.cs`
|
||||
- `tests/SharpCompress.Test/Zip/ZipFilePartTests.cs`
|
||||
|
|
@ -3,10 +3,11 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "0.25.0",
|
||||
"version": "1.3.0",
|
||||
"commands": [
|
||||
"dotnet-csharpier"
|
||||
]
|
||||
"csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
189
.editorconfig
189
.editorconfig
|
|
@ -70,7 +70,7 @@ indent_style = tab
|
|||
|
||||
[*.{cs,csx,cake,vb,vbx}]
|
||||
# Default Severity for all .NET Code Style rules below
|
||||
dotnet_analyzer_diagnostic.severity = warning
|
||||
dotnet_analyzer_diagnostic.severity = silent
|
||||
|
||||
##########################################
|
||||
# File Header (Uncomment to support file headers)
|
||||
|
|
@ -257,59 +257,73 @@ csharp_style_namespace_declarations = file_scoped
|
|||
##########################################
|
||||
|
||||
[*.{cs,csx,cake,vb,vbx}]
|
||||
dotnet_diagnostic.CA1000.severity = suggestion
|
||||
dotnet_diagnostic.CA1001.severity = error
|
||||
dotnet_diagnostic.CA1018.severity = error
|
||||
dotnet_diagnostic.CA1000.severity = error # do not declare static members on generic types
|
||||
dotnet_diagnostic.CA1001.severity = error # disposable field owners should be disposable
|
||||
dotnet_diagnostic.CA1018.severity = error # mark custom attributes with AttributeUsage
|
||||
dotnet_diagnostic.CA1036.severity = silent
|
||||
dotnet_diagnostic.CA1051.severity = suggestion
|
||||
dotnet_diagnostic.CA1068.severity = error
|
||||
dotnet_diagnostic.CA1069.severity = error
|
||||
dotnet_diagnostic.CA1304.severity = error
|
||||
dotnet_diagnostic.CA1305.severity = suggestion
|
||||
dotnet_diagnostic.CA1307.severity = suggestion
|
||||
dotnet_diagnostic.CA1309.severity = suggestion
|
||||
dotnet_diagnostic.CA1310.severity = error
|
||||
dotnet_diagnostic.CA1707.severity = suggestion
|
||||
dotnet_diagnostic.CA1708.severity = suggestion
|
||||
dotnet_diagnostic.CA1711.severity = suggestion
|
||||
dotnet_diagnostic.CA1716.severity = suggestion
|
||||
dotnet_diagnostic.CA1720.severity = suggestion
|
||||
dotnet_diagnostic.CA1725.severity = suggestion
|
||||
dotnet_diagnostic.CA1805.severity = suggestion
|
||||
dotnet_diagnostic.CA1816.severity = suggestion
|
||||
dotnet_diagnostic.CA1822.severity = suggestion
|
||||
dotnet_diagnostic.CA1825.severity = error
|
||||
dotnet_diagnostic.CA1051.severity = suggestion # do not declare visible instance fields
|
||||
dotnet_diagnostic.CA1068.severity = error # cancellation token parameters must come last
|
||||
dotnet_diagnostic.CA1069.severity = error # enums should not have duplicate values
|
||||
dotnet_diagnostic.CA1304.severity = error # specify CultureInfo for culture-sensitive operations
|
||||
dotnet_diagnostic.CA1305.severity = error # specify IFormatProvider
|
||||
dotnet_diagnostic.CA1307.severity = error # specify StringComparison for clarity
|
||||
dotnet_diagnostic.CA1309.severity = error # use ordinal StringComparison
|
||||
dotnet_diagnostic.CA1310.severity = error # specify StringComparison for correctness
|
||||
dotnet_diagnostic.CA1507.severity = error # use nameof in place of string literals
|
||||
dotnet_diagnostic.CA1513.severity = suggestion # use ObjectDisposedException throw helper
|
||||
dotnet_diagnostic.CA1707.severity = suggestion # identifiers should not contain underscores
|
||||
dotnet_diagnostic.CA1708.severity = suggestion # identifiers should differ by more than case
|
||||
dotnet_diagnostic.CA1711.severity = suggestion # identifiers should not have incorrect suffixes
|
||||
dotnet_diagnostic.CA1716.severity = suggestion # identifiers should not match language keywords
|
||||
dotnet_diagnostic.CA1720.severity = suggestion # identifiers should not contain type names
|
||||
dotnet_diagnostic.CA1725.severity = error # parameter names should match base declaration
|
||||
dotnet_diagnostic.CA1805.severity = suggestion # avoid unnecessary default value initialization
|
||||
dotnet_diagnostic.CA1816.severity = suggestion # call GC.SuppressFinalize correctly
|
||||
dotnet_diagnostic.CA1822.severity = suggestion # mark members static when possible
|
||||
dotnet_diagnostic.CA1825.severity = error # avoid zero-length array allocations
|
||||
dotnet_diagnostic.CA1826.severity = silent
|
||||
dotnet_diagnostic.CA1827.severity = error
|
||||
dotnet_diagnostic.CA1829.severity = suggestion
|
||||
dotnet_diagnostic.CA1834.severity = error
|
||||
dotnet_diagnostic.CA1845.severity = suggestion
|
||||
dotnet_diagnostic.CA1848.severity = suggestion
|
||||
dotnet_diagnostic.CA1852.severity = suggestion
|
||||
dotnet_diagnostic.CA2016.severity = suggestion
|
||||
dotnet_diagnostic.CA2201.severity = error
|
||||
dotnet_diagnostic.CA2206.severity = error
|
||||
dotnet_diagnostic.CA2208.severity = error
|
||||
dotnet_diagnostic.CA2211.severity = error
|
||||
dotnet_diagnostic.CA2249.severity = error
|
||||
dotnet_diagnostic.CA2251.severity = error
|
||||
dotnet_diagnostic.CA1827.severity = error # use Any() instead of Count()/LongCount() checks
|
||||
dotnet_diagnostic.CA1829.severity = error # use Length or Count property instead of LINQ Count()
|
||||
dotnet_diagnostic.CA1834.severity = error # prefer StringBuilder.Append(char) for single chars
|
||||
dotnet_diagnostic.CA1845.severity = error # use span-based string.Concat overloads
|
||||
dotnet_diagnostic.CA1848.severity = error # use LoggerMessage for high-performance logging
|
||||
dotnet_diagnostic.CA1852.severity = suggestion # seal types that are not intended for inheritance
|
||||
dotnet_diagnostic.CA1860.severity = silent
|
||||
dotnet_diagnostic.CA2016.severity = error # forward CancellationToken to invoked methods
|
||||
dotnet_diagnostic.CA2201.severity = error # do not throw reserved or overly general exceptions
|
||||
dotnet_diagnostic.CA2206.severity = error # enforce CA2206 usage guidance
|
||||
dotnet_diagnostic.CA2208.severity = error # instantiate ArgumentException types correctly
|
||||
dotnet_diagnostic.CA2211.severity = error # non-constant fields should not be visible
|
||||
dotnet_diagnostic.CA2249.severity = error # prefer string.Contains over string.IndexOf checks
|
||||
dotnet_diagnostic.CA2251.severity = error # use string.Equals over string.Compare equality checks
|
||||
dotnet_diagnostic.CA2252.severity = none
|
||||
dotnet_diagnostic.CA2254.severity = suggestion
|
||||
dotnet_diagnostic.CA2254.severity = error # logging message templates should be static expressions
|
||||
|
||||
dotnet_diagnostic.CS0169.severity = error
|
||||
dotnet_diagnostic.CS0219.severity = error
|
||||
dotnet_diagnostic.CS0649.severity = suggestion
|
||||
dotnet_diagnostic.CS1998.severity = error
|
||||
dotnet_diagnostic.CS8602.severity = error
|
||||
dotnet_diagnostic.CS8604.severity = error
|
||||
dotnet_diagnostic.CS8618.severity = error
|
||||
dotnet_diagnostic.CS0618.severity = error
|
||||
dotnet_diagnostic.CS1998.severity = error
|
||||
dotnet_diagnostic.CS4014.severity = error
|
||||
dotnet_diagnostic.CS8600.severity = error
|
||||
dotnet_diagnostic.CS8603.severity = error
|
||||
dotnet_diagnostic.CS8625.severity = error
|
||||
dotnet_diagnostic.CS8981.severity = suggestion
|
||||
; High volume analyzers requiring extensive refactoring - set to suggestion temporarily
|
||||
dotnet_diagnostic.CA1835.severity = suggestion # prefer Memory<T>-based async overloads
|
||||
dotnet_diagnostic.CA1510.severity = error # use ArgumentNullException.ThrowIfNull
|
||||
dotnet_diagnostic.CA1512.severity = error # use ArgumentOutOfRangeException throw helpers
|
||||
dotnet_diagnostic.CA1844.severity = suggestion # provide memory-based async stream overrides
|
||||
dotnet_diagnostic.CA1825.severity = error # avoid zero-length array allocations
|
||||
dotnet_diagnostic.CA1712.severity = suggestion # do not prefix enum values with type name
|
||||
dotnet_diagnostic.CA2022.severity = suggestion # avoid inexact reads with Stream.Read
|
||||
dotnet_diagnostic.CA1850.severity = error # prefer static HashData over ComputeHash
|
||||
dotnet_diagnostic.CA2263.severity = error # prefer generic overload when type is known
|
||||
dotnet_diagnostic.CA2012.severity = error # use ValueTasks correctly
|
||||
dotnet_diagnostic.CA1001.severity = error # disposable field owners should be disposable
|
||||
|
||||
dotnet_diagnostic.CS0169.severity = error # field is never used
|
||||
dotnet_diagnostic.CS0219.severity = error # variable assigned but never used
|
||||
dotnet_diagnostic.CS0649.severity = error # field is never assigned and remains default
|
||||
dotnet_diagnostic.CS1998.severity = error # async method lacks await operators
|
||||
dotnet_diagnostic.CS8602.severity = error # possible null reference dereference
|
||||
dotnet_diagnostic.CS8604.severity = error # possible null reference argument
|
||||
dotnet_diagnostic.CS8618.severity = error # non-nullable member is uninitialized
|
||||
dotnet_diagnostic.CS0618.severity = error # obsolete member usage
|
||||
dotnet_diagnostic.CS4014.severity = error # unawaited task call
|
||||
dotnet_diagnostic.CS8600.severity = error # possible null to non-nullable conversion
|
||||
dotnet_diagnostic.CS8603.severity = error # possible null reference return
|
||||
dotnet_diagnostic.CS8625.severity = error # cannot assign null to non-nullable reference
|
||||
|
||||
dotnet_diagnostic.BL0005.severity = suggestion
|
||||
|
||||
|
|
@ -317,9 +331,9 @@ dotnet_diagnostic.MVC1000.severity = suggestion
|
|||
|
||||
dotnet_diagnostic.RZ10012.severity = error
|
||||
|
||||
dotnet_diagnostic.IDE0004.severity = error # redundant cast
|
||||
dotnet_diagnostic.IDE0005.severity = error
|
||||
dotnet_diagnostic.IDE0007.severity = error # Use var
|
||||
dotnet_diagnostic.IDE0004.severity = suggestion # redundant cast
|
||||
dotnet_diagnostic.IDE0005.severity = suggestion
|
||||
dotnet_diagnostic.IDE0007.severity = suggestion # Use var
|
||||
dotnet_diagnostic.IDE0011.severity = error # Use braces on if statements
|
||||
dotnet_diagnostic.IDE0010.severity = silent # populate switch
|
||||
dotnet_diagnostic.IDE0017.severity = suggestion # initialization can be simplified
|
||||
|
|
@ -329,15 +343,15 @@ dotnet_diagnostic.IDE0023.severity = suggestion # use expression body for operat
|
|||
dotnet_diagnostic.IDE0024.severity = silent # expression body for operators
|
||||
dotnet_diagnostic.IDE0025.severity = suggestion # use expression body for properties
|
||||
dotnet_diagnostic.IDE0027.severity = suggestion # Use expression body for accessors
|
||||
dotnet_diagnostic.IDE0028.severity = silent
|
||||
dotnet_diagnostic.IDE0028.severity = silent # expression body for accessors
|
||||
dotnet_diagnostic.IDE0032.severity = suggestion # Use auto property
|
||||
dotnet_diagnostic.IDE0033.severity = error # prefer tuple name
|
||||
dotnet_diagnostic.IDE0037.severity = suggestion # simplify anonymous type
|
||||
dotnet_diagnostic.IDE0040.severity = error # modifiers required
|
||||
dotnet_diagnostic.IDE0040.severity = suggestion # modifiers required
|
||||
dotnet_diagnostic.IDE0041.severity = error # simplify null
|
||||
dotnet_diagnostic.IDE0042.severity = error # deconstruct variable
|
||||
dotnet_diagnostic.IDE0044.severity = suggestion # make field only when possible
|
||||
dotnet_diagnostic.IDE0047.severity = suggestion # paratemeter name
|
||||
dotnet_diagnostic.IDE0047.severity = suggestion # parameter name
|
||||
dotnet_diagnostic.IDE0051.severity = error # unused field
|
||||
dotnet_diagnostic.IDE0052.severity = error # unused member
|
||||
dotnet_diagnostic.IDE0053.severity = suggestion # lambda not needed
|
||||
|
|
@ -347,15 +361,76 @@ dotnet_diagnostic.IDE0060.severity = suggestion # unused parameters
|
|||
dotnet_diagnostic.IDE0061.severity = suggestion # local expression body
|
||||
dotnet_diagnostic.IDE0062.severity = suggestion # local to static
|
||||
dotnet_diagnostic.IDE0063.severity = error # simplify using
|
||||
|
||||
[src/**/*.cs]
|
||||
dotnet_diagnostic.VSTHRD002.severity = error # avoid sync waits on async operations
|
||||
dotnet_diagnostic.VSTHRD100.severity = error # avoid async void methods
|
||||
dotnet_diagnostic.VSTHRD101.severity = error # avoid unsupported async delegates
|
||||
dotnet_diagnostic.VSTHRD102.severity = error # implement internal logic asynchronously
|
||||
dotnet_diagnostic.VSTHRD103.severity = error # use async methods from async methods
|
||||
dotnet_diagnostic.VSTHRD104.severity = error # offer async alternatives when possible
|
||||
dotnet_diagnostic.VSTHRD107.severity = error # await task within using expression
|
||||
dotnet_diagnostic.VSTHRD110.severity = error # observe result of async calls
|
||||
dotnet_diagnostic.VSTHRD111.severity = error # use ConfigureAwait(bool)
|
||||
dotnet_diagnostic.VSTHRD112.severity = error # implement System.IAsyncDisposable
|
||||
dotnet_diagnostic.VSTHRD113.severity = error # check for System.IAsyncDisposable
|
||||
dotnet_diagnostic.VSTHRD114.severity = error # avoid returning null from Task methods
|
||||
dotnet_diagnostic.VSTHRD200.severity = suggestion # use Async suffix naming convention
|
||||
|
||||
[build/**/*.cs]
|
||||
dotnet_diagnostic.VSTHRD001.severity = none # avoid legacy thread switching methods (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD002.severity = none # avoid sync waits on async operations (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD003.severity = none # avoid awaiting foreign tasks (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD004.severity = none # await SwitchToMainThreadAsync (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD010.severity = none # invoke single-threaded types on main thread (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD011.severity = none # use AsyncLazy<T> (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD012.severity = none # provide JoinableTaskFactory where allowed (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD100.severity = none # avoid async void methods (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD101.severity = none # avoid unsupported async delegates (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD102.severity = none # implement internal logic asynchronously (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD103.severity = none # use async methods from async methods (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD104.severity = none # offer async alternatives when possible (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD105.severity = none # avoid TaskScheduler.Current assumptions (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD106.severity = none # use InvokeAsync for async events (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD107.severity = none # await task within using expression (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD108.severity = none # assert thread affinity unconditionally (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD109.severity = none # switch instead of assert in async methods (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD110.severity = none # observe result of async calls (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD111.severity = none # use ConfigureAwait(bool) (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD112.severity = none # implement System.IAsyncDisposable (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD113.severity = none # check for System.IAsyncDisposable (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD114.severity = none # avoid returning null from Task methods (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD115.severity = none # avoid explicit null SynchronizationContext in JTC (disabled for build scripts)
|
||||
dotnet_diagnostic.VSTHRD200.severity = none # use Async suffix naming convention (disabled for build scripts)
|
||||
|
||||
[tests/**/*.cs]
|
||||
dotnet_diagnostic.CA1861.severity = suggestion # avoid constant arrays as arguments
|
||||
dotnet_diagnostic.CA1305.severity = suggestion # specify IFormatProvider
|
||||
dotnet_diagnostic.CA1307.severity = suggestion # specify StringComparison for clarity
|
||||
dotnet_diagnostic.IDE0042.severity = suggestion
|
||||
dotnet_diagnostic.IDE0051.severity = suggestion
|
||||
dotnet_diagnostic.IDE0063.severity = suggestion
|
||||
dotnet_diagnostic.IDE0066.severity = suggestion # switch expression
|
||||
dotnet_diagnostic.IDE0072.severity = suggestion # Populate switch - forces population of all cases even when default specified
|
||||
dotnet_diagnostic.IDE0078.severity = suggestion # use pattern matching
|
||||
dotnet_diagnostic.IDE0090.severity = suggestion # new can be simplified
|
||||
dotnet_diagnostic.IDE0130.severity = error # namespace folder structure
|
||||
dotnet_diagnostic.IDE0130.severity = suggestion # namespace folder structure
|
||||
dotnet_diagnostic.IDE0160.severity = silent # Use block namespaces ARE NOT required
|
||||
dotnet_diagnostic.IDE0161.severity = error # Please use file namespaces
|
||||
dotnet_diagnostic.IDE0200.severity = suggestion # lambda not needed
|
||||
dotnet_diagnostic.IDE1006.severity = suggestion # Naming rule violation: These words cannot contain lower case characters
|
||||
dotnet_diagnostic.IDE0260.severity = suggestion # Use pattern matching
|
||||
dotnet_diagnostic.IDE0270.severity = suggestion # Null check simplifcation
|
||||
dotnet_diagnostic.IDE0290.severity = suggestion # Primary Constructor
|
||||
dotnet_diagnostic.IDE0300.severity = suggestion # Collection
|
||||
dotnet_diagnostic.IDE0305.severity = suggestion # Collection ToList
|
||||
|
||||
dotnet_diagnostic.NX0001.severity = error
|
||||
dotnet_diagnostic.NX0002.severity = silent
|
||||
dotnet_diagnostic.NX0003.severity = silent
|
||||
|
||||
dotnet_diagnostic.VSTHRD110.severity = error # observe result of async calls
|
||||
dotnet_diagnostic.VSTHRD107.severity = error # await task within using expression
|
||||
|
||||
##########################################
|
||||
# Styles
|
||||
|
|
|
|||
27
.github/copilot-instructions.md
vendored
Normal file
27
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<!-- rtk-instructions v2 -->
|
||||
# RTK — Token-Optimized CLI
|
||||
|
||||
**rtk** is a CLI proxy that filters and compresses command outputs, saving 60-90% tokens.
|
||||
|
||||
## Rule
|
||||
|
||||
Always prefix shell commands with `rtk`:
|
||||
|
||||
```bash
|
||||
# Instead of: Use:
|
||||
git status rtk git status
|
||||
git log -10 rtk git log -10
|
||||
cargo test rtk cargo test
|
||||
docker ps rtk docker ps
|
||||
kubectl get pods rtk kubectl pods
|
||||
```
|
||||
|
||||
## Meta commands (use directly)
|
||||
|
||||
```bash
|
||||
rtk gain # Token savings dashboard
|
||||
rtk gain --history # Per-command savings history
|
||||
rtk discover # Find missed rtk opportunities
|
||||
rtk proxy <cmd> # Run raw (no filtering) but track usage
|
||||
```
|
||||
<!-- /rtk-instructions -->
|
||||
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
|
|
@ -1,6 +1,19 @@
|
|||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "github-actions" # search for actions - there are other options available
|
||||
directory: "/" # search in .github/workflows under root `/`
|
||||
- package-ecosystem: "nuget"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly" # check for action update every week
|
||||
interval: "weekly"
|
||||
|
||||
open-pull-requests-limit: 2
|
||||
|
||||
groups:
|
||||
all-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types:
|
||||
- "version-update:semver-major"
|
||||
|
|
|
|||
12
.github/hooks/rtk-rewrite.json
vendored
Normal file
12
.github/hooks/rtk-rewrite.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "rtk hook copilot",
|
||||
"cwd": ".",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
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
|
||||
32
.github/workflows/dotnetcore.yml
vendored
32
.github/workflows/dotnetcore.yml
vendored
|
|
@ -1,32 +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@v3
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
- name: NuGet Caching
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('packages.lock.json', '*/packages.lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
- run: dotnet run --project build/build.csproj
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-sharpcompress.nupkg
|
||||
path: artifacts/*
|
||||
67
.github/workflows/nuget-release.yml
vendored
Normal file
67
.github/workflows/nuget-release.yml
vendored
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
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:
|
||||
global-json-file: global.json
|
||||
|
||||
# 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
|
||||
|
||||
- name: Validate AOT Smoke Test
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
dotnet publish tests/SharpCompress.AotSmoke/SharpCompress.AotSmoke.csproj --configuration Release --runtime linux-x64 --self-contained true --output artifacts/aot-smoke
|
||||
./artifacts/aot-smoke/SharpCompress.AotSmoke
|
||||
|
||||
# Upload artifacts for verification
|
||||
- name: Upload NuGet Package
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.os }}-nuget-package
|
||||
path: artifacts/*.nupkg
|
||||
|
||||
# Push to NuGet.org only for version tag pushes (Windows only)
|
||||
- name: Push to NuGet
|
||||
if: success() && matrix.os == 'windows-latest' && startsWith(github.ref, 'refs/tags/')
|
||||
run: dotnet run --project build/build.csproj -- push-to-nuget
|
||||
env:
|
||||
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
|
||||
50
.github/workflows/performance-benchmarks.yml
vendored
Normal file
50
.github/workflows/performance-benchmarks.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: Performance Benchmarks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Build Performance Project
|
||||
run: dotnet build tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release
|
||||
|
||||
- name: Run Benchmarks
|
||||
run: dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release --no-build -- --filter "*" --exporters json markdown --artifacts benchmark-results
|
||||
continue-on-error: true
|
||||
|
||||
- name: Display Benchmark Results
|
||||
if: always()
|
||||
run: dotnet run --project build/build.csproj -- display-benchmark-results
|
||||
|
||||
- name: Compare with Baseline
|
||||
if: always()
|
||||
run: dotnet run --project build/build.csproj -- compare-benchmark-results
|
||||
|
||||
- name: Upload Benchmark Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: benchmark-results
|
||||
path: benchmark-results/
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -4,18 +4,24 @@ _ReSharper.SharpCompress/
|
|||
bin/
|
||||
*.suo
|
||||
*.user
|
||||
TestArchives/Scratch/
|
||||
TestArchives/Scratch2/
|
||||
tests/TestArchives/Scratch/
|
||||
tests/TestArchives/Scratch2/
|
||||
TestResults/
|
||||
*.nupkg
|
||||
packages/*/
|
||||
project.lock.json
|
||||
tests/TestArchives/Scratch
|
||||
tests/TestArchives/*/Scratch
|
||||
tests/TestArchives/*/Scratch2
|
||||
.vs
|
||||
tools
|
||||
.vscode
|
||||
.idea/
|
||||
artifacts/
|
||||
BenchmarkDotNet.Artifacts/
|
||||
baseline-artifacts/
|
||||
profiler-snapshots/
|
||||
|
||||
.DS_Store
|
||||
*.snupkg
|
||||
/tests/TestArchives/6d23a38c-f064-4ef1-ad89-b942396f53b9/Scratch
|
||||
benchmark-results/
|
||||
.opencode/
|
||||
|
|
|
|||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
32
.vscode/settings.json
vendored
Normal file
32
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"dotnet.defaultSolution": "SharpCompress.slnx",
|
||||
"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",
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"dotnet csharpier": true
|
||||
}
|
||||
}
|
||||
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.slnx",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "build-release",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/SharpCompress.slnx",
|
||||
"-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.slnx"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "clean",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"clean",
|
||||
"${workspaceFolder}/SharpCompress.slnx"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
231
AGENTS.md
Normal file
231
AGENTS.md
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
---
|
||||
description: 'Guidelines for building SharpCompress - A C# compression library'
|
||||
applyTo: '**/*.cs'
|
||||
---
|
||||
|
||||
# SharpCompress Development
|
||||
|
||||
## About SharpCompress
|
||||
SharpCompress is a pure C# compression library supporting multiple archive formats (Zip, Tar, GZip, BZip2, 7Zip, Rar, LZip, XZ, ZStandard, Arc, Arj, Ace, LZW). The project currently targets .NET Framework 4.8, .NET Standard 2.0/2.1, .NET 6.0, .NET 8.0, and .NET 10.0. The library provides both seekable Archive APIs and forward-only Reader/Writer APIs for streaming scenarios.
|
||||
|
||||
## C# Instructions
|
||||
- Use language features supported by the current project toolchain (`LangVersion=latest`) and existing codebase patterns.
|
||||
- Add comments for non-obvious logic and important design decisions; avoid redundant comments.
|
||||
- Follow the existing code style and patterns in the codebase.
|
||||
|
||||
## General Instructions
|
||||
- **Do not commit or stage changes unless the user explicitly asks for it.**
|
||||
- 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.
|
||||
|
||||
### Workspace Hygiene
|
||||
- Do not edit generated or machine-local files unless required for the task (for example: `bin/`, `obj/`, `*.csproj.user`).
|
||||
- Avoid broad formatting-only diffs in unrelated files.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- Follow PascalCase for component names, method names, and public members.
|
||||
- Use camelCase for private fields and local variables.
|
||||
- Prefix interface names with "I" (e.g., IUserService).
|
||||
|
||||
## Code Formatting
|
||||
|
||||
**Copilot agents: You MUST run the `format` task after making code changes to ensure consistency.**
|
||||
|
||||
- Use CSharpier for code formatting to ensure consistent style across the project
|
||||
- CSharpier is configured as a local tool in `.config/dotnet-tools.json`
|
||||
|
||||
### 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
|
||||
|
||||
- The project targets multiple frameworks: .NET Framework 4.8, .NET Standard 2.0/2.1, .NET 6.0, .NET 8.0, and .NET 10.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.slnx`
|
||||
|
||||
### 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
|
||||
|
||||
tests/
|
||||
├── SharpCompress.Test/ # Unit/integration tests
|
||||
├── SharpCompress.Performance/ # Benchmark tests
|
||||
└── TestArchives/ # Test data archives
|
||||
```
|
||||
|
||||
### Factory Pattern
|
||||
Factory implementations can implement one or more interfaces (`IArchiveFactory`, `IReaderFactory`, `IWriterFactory`) depending on format capabilities:
|
||||
- `ArchiveFactory.OpenArchive()` - Opens archive API objects from seekable streams/files
|
||||
- `ArchiveFactory.OpenAsyncArchive()` - Opens async archive API objects for async archive use cases
|
||||
- `ReaderFactory.OpenReader()` - Auto-detects and opens forward-only readers
|
||||
- `ReaderFactory.OpenAsyncReader()` - Auto-detects and opens forward-only async readers
|
||||
- `WriterFactory.OpenWriter()` - Creates a writer for a specified `ArchiveType`
|
||||
- `WriterFactory.OpenAsyncWriter()` - Creates an async writer for async write scenarios
|
||||
- Factories located in: `src/SharpCompress/Factories/`
|
||||
|
||||
## Nullable Reference Types
|
||||
|
||||
- Declare variables non-nullable, and check for `null` at entry points.
|
||||
- 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), Ace (read-only), Arc (read-only), Arj (read-only), LZW (read-only)
|
||||
- **Compression**: DEFLATE, BZip2, LZMA/LZMA2, PPMd, ZStandard, LZip, XZ (decompress only), Deflate64 (decompress only), legacy Zip/Arc/Arj/Ace methods (read-only as applicable)
|
||||
- **Combined Formats**: Tar.GZip, Tar.BZip2, Tar.LZip, Tar.XZ (decompress only), Tar.ZStandard (decompress only), Tar.LZW (decompress only)
|
||||
- **ZIP ZStandard**: ZIP supports ZStandard reading and writing; Tar.ZStandard is decompress-only.
|
||||
- See [docs/FORMATS.md](docs/FORMATS.md) for complete format support matrix
|
||||
|
||||
### Stream Handling Rules
|
||||
- **Disposal semantics**: The default `ReaderOptions.LeaveStreamOpen` value is `false`, but effective stream ownership depends on which API overload you call
|
||||
- File-based overloads (e.g., `OpenArchive(string filePath)`) open the file internally and own that stream, so it is closed by default with the archive/reader
|
||||
- Do **not** rely on a specific `ReaderOptions` preset being used internally; some implementations may use `ReaderOptions.ForFilePath`, while others may use default `ReaderOptions` with the same ownership semantics
|
||||
- Several high-level overloads that accept a caller-provided `Stream` use external-stream semantics by default (for example, `ReaderFactory.OpenReader(Stream)` / `ArchiveFactory.OpenArchive(Stream)`), so the caller's stream is typically left open unless you opt into different ownership behavior
|
||||
- Do **not** assume every stream-based overload behaves identically; some APIs require you to pass stream ownership options explicitly
|
||||
- **For caller-provided streams**: When the overload accepts `ReaderOptions`, pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream
|
||||
- Example: `var options = new ReaderOptions { LeaveStreamOpen = true };`
|
||||
- Or: `var options = ReaderOptions.ForExternalStream;`
|
||||
- **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is needed beyond the archive/reader itself
|
||||
- Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal
|
||||
- Always dispose of readers, writers, and archives in `using` / `await 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`
|
||||
- For async archive scenarios, prefer `ArchiveFactory.OpenAsyncArchive(...)` over sync `OpenArchive(...)`.
|
||||
- For async forward-only read scenarios, prefer `ReaderFactory.OpenAsyncReader(...)` over sync `OpenReader(...)`.
|
||||
- For async write scenarios, prefer `WriterFactory.OpenAsyncWriter(...)` over sync `OpenWriter(...)`.
|
||||
- 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 is supported for reading with GZip, BZip2, LZip, XZ, ZStandard, and LZW
|
||||
- Tar writing supports uncompressed Tar, GZip, BZip2, and LZip wrappers
|
||||
|
||||
### 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
### Validation Expectations
|
||||
- Run targeted tests for the changed area first.
|
||||
- On non-Windows machines, avoid net48 test runs unless Mono is installed; use framework-specific validation such as `--framework net10.0` instead.
|
||||
- Run `dotnet csharpier format .` after code edits.
|
||||
- Run `dotnet csharpier check .` before handing off changes.
|
||||
|
||||
### 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
|
||||
|
||||
### Public API Change Checklist
|
||||
- Preserve existing public method signatures and behavior when possible.
|
||||
- If a breaking change is unavoidable, document it and provide a migration path.
|
||||
- Add or update tests that cover backward compatibility expectations.
|
||||
- Avoid exposing public `init` setters, positional records, `required` members, or other metadata that forces consumers onto newer C# language versions; validate older-consumer compatibility with tests when changing exported APIs.
|
||||
|
||||
### Public API Documentation Checklist
|
||||
- When adding, removing, renaming, or changing public APIs, update the public docs in the same change.
|
||||
- Check `docs/API.md` for new factory methods, options, interfaces, extension methods, enums, archive/reader/writer APIs, compression provider APIs, and examples.
|
||||
- Check `docs/USAGE.md` when the API change affects recommended usage patterns or requires a new example.
|
||||
- Check `docs/FORMATS.md` when the API change affects supported archive formats, compression methods, reader/archive/writer availability, or detection behavior.
|
||||
- Check `README.md` when the change affects the top-level support summary, target frameworks, major capabilities, or user-facing feature list.
|
||||
- For public API changes, verify examples compile conceptually against the actual public signatures and avoid documenting internal-only types.
|
||||
|
||||
### Stream Ownership and Position Checklist
|
||||
- Verify `LeaveStreamOpen` behavior for externally owned streams.
|
||||
- Validate behavior for both seekable and non-seekable streams.
|
||||
- Ensure stream position assumptions are explicit and tested.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Don't mix Archive and Reader APIs** - Archive needs seekable stream, Reader doesn't
|
||||
2. **Don't mix sync and async open paths** - For async workflows use `OpenAsyncArchive`/`OpenAsyncReader`/`OpenAsyncWriter`, not `OpenArchive`/`OpenReader`/`OpenWriter`
|
||||
3. **Solid archives (Rar, 7Zip)** - Use `ExtractAllEntries()` for best performance, not individual entry extraction
|
||||
4. **Stream disposal** - Always set `LeaveStreamOpen` explicitly when needed (default is to close)
|
||||
5. **Tar + non-seekable stream** - Must provide file size or it will throw
|
||||
6. **Format detection** - Use `ReaderFactory.OpenReader()` / `ReaderFactory.OpenAsyncReader()` for auto-detection, test with actual archive files
|
||||
|
|
@ -8,7 +8,9 @@
|
|||
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<RunAnalyzersDuringLiveAnalysis>False</RunAnalyzersDuringLiveAnalysis>
|
||||
<RunAnalyzersDuringBuild>False</RunAnalyzersDuringBuild>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
<NoWarn>${NoWarn};IDE0051</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
|
|||
23
Directory.Packages.props
Normal file
23
Directory.Packages.props
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<Project>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
|
||||
<PackageVersion Include="Bullseye" Version="6.1.0" />
|
||||
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
|
||||
<PackageVersion Include="Glob" Version="1.1.9" />
|
||||
<PackageVersion Include="JetBrains.Profiler.SelfApi" Version="2.5.18" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
||||
<PackageVersion Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageVersion Include="SimpleExec" Version="13.0.0" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
|
||||
<GlobalPackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
|
||||
<GlobalPackageReference
|
||||
Include="Microsoft.VisualStudio.Threading.Analyzers"
|
||||
Version="17.14.15"
|
||||
/>
|
||||
<GlobalPackageReference Include="PolySharp" Version="1.16.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
59
FORMATS.md
59
FORMATS.md
|
|
@ -1,59 +0,0 @@
|
|||
# Formats
|
||||
|
||||
## Accessing Archives
|
||||
|
||||
* Archive classes allow random access to a seekable stream.
|
||||
* Reader classes allow forward-only reading on a stream.
|
||||
* Writer classes allow forward-only Writing on a stream.
|
||||
|
||||
## Supported Format Table
|
||||
|
||||
| Archive Format | Compression Format(s) | Compress/Decompress | Archive API | Reader API | Writer API |
|
||||
| ---------------------- | ------------------------------------------------- | ------------------- | --------------- | ---------- | ------------- |
|
||||
| Rar | Rar | Decompress (1) | RarArchive | RarReader | N/A |
|
||||
| Zip (2) | None, 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) |
|
||||
| Tar.BZip2 | BZip2 | Both | TarArchive | TarReader | TarWriter (3) |
|
||||
| Tar.LZip | LZMA | Both | TarArchive | TarReader | TarWriter (3) |
|
||||
| Tar.XZ | LZMA2 | Decompress | TarArchive | TarReader | TarWriter (3) |
|
||||
| GZip (single file) | DEFLATE | Both | GZipArchive | GZipReader | GZipWriter |
|
||||
| 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.
|
||||
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
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
| Compressor | Compress/Decompress |
|
||||
| --------------- | ------------------- |
|
||||
| BZip2Stream | Both |
|
||||
| GZipStream | Both |
|
||||
| DeflateStream | Both |
|
||||
| Deflate64Stream | Decompress |
|
||||
| LZMAStream | Both |
|
||||
| PPMdStream | Both |
|
||||
| ADCStream | Decompress |
|
||||
| LZipStream | Both |
|
||||
| XZStream | Decompress |
|
||||
|
||||
## Archive Formats vs Compression
|
||||
|
||||
Sometimes the terminology gets mixed.
|
||||
|
||||
### Compression
|
||||
|
||||
DEFLATE, LZMA are pure compression algorithms
|
||||
|
||||
### Formats
|
||||
|
||||
Formats like Zip, 7Zip, Rar are archive formats only. They use other compression methods (e.g. DEFLATE, LZMA, etc.) or propriatory (e.g RAR)
|
||||
|
||||
### Overlap
|
||||
|
||||
GZip, BZip2 and LZip are single file archival formats. The overlap in the API happens because Tar uses the single file formats as "compression" methods and the API tries to hide this a bit.
|
||||
9
NuGet.config
Normal file
9
NuGet.config
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSourceMapping>
|
||||
<!-- key value for <packageSource> should match key values from <packageSources> element -->
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
169
README.md
169
README.md
|
|
@ -1,18 +1,24 @@
|
|||
# SharpCompress
|
||||
|
||||
SharpCompress is a compression library in pure C# for .NET Standard 2.0, 2.1, .NET Core 3.1 and .NET 5.0 that can unrar, un7zip, unzip, untar unbzip2, ungzip, unlzip 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 Standard 2.0/2.1, .NET 6.0, .NET 8.0, and .NET 10.0 that can unrar, un7zip, unzip, untar, unbzip2, ungzip, unlzip, unxz, unzstd, unarc, unarj, unace, and unlzw with forward-only reading and file random access APIs. Write support for zip, tar, bzip2, gzip, lzip, zstandard compression streams, and 7zip archives is 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](docs/USAGE.md#async-examples) for examples.
|
||||
|
||||
GitHub Actions Build -
|
||||
[](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml)
|
||||
[](https://www.robiniadocs.com/d/sharpcompress/api/SharpCompress.html)
|
||||
[](https://github.com/adamhathcock/sharpcompress/actions/workflows/nuget-release.yml)
|
||||
[](https://dndocs.com/d/sharpcompress/api/index.html)
|
||||
|
||||
## Need Help?
|
||||
|
||||
Post Issues on Github!
|
||||
|
||||
Check the [Supported Formats](FORMATS.md) and [Basic Usage.](USAGE.md)
|
||||
Check the [Supported Formats](docs/FORMATS.md), [API Reference](docs/API.md), and [Basic Usage](docs/USAGE.md).
|
||||
|
||||
## Custom Compression Providers
|
||||
|
||||
If you need to swap out SharpCompress’s built-in codecs, the `Providers` property (and `WithProviders(...)` extensions) on `ReaderOptions` and `WriterOptions` lets you supply a `CompressionProviderRegistry`. The selected registry is used by Reader/Writer APIs, Archive APIs, and async extraction paths, so the same provider choice is applied consistently across open/read/write flows. The default registry is already wired up, so customization is only necessary when you want to plug in alternatives such as `SystemGZipCompressionProvider` or a third-party `CompressionProvider`. See [docs/USAGE.md#custom-compression-providers](docs/USAGE.md#custom-compression-providers) for guided examples.
|
||||
|
||||
## Recommended Formats
|
||||
|
||||
|
|
@ -20,10 +26,12 @@ In general, I recommend GZip (Deflate)/BZip2 (BZip)/LZip (LZMA) as the simplicit
|
|||
|
||||
Zip is okay, but it's a very hap-hazard format and the variation in headers and implementations makes it hard to get correct. Uses Deflate by default but supports a lot of compression methods.
|
||||
|
||||
RAR is not recommended as it's a propriatory format and the compression is closed source. Use Tar/LZip for LZMA
|
||||
RAR is not recommended as it's a proprietary format and the compression is closed source. Use Tar/LZip for LZMA
|
||||
|
||||
7Zip and XZ both are overly complicated. 7Zip does not support streamable formats. XZ has known holes explained here: (http://www.nongnu.org/lzip/xz_inadequate.html) Use Tar/LZip for LZMA compression instead.
|
||||
|
||||
ZStandard is an efficient format that works well for streaming with a flexible compression level to tweak the speed/performance trade off you are looking for.
|
||||
|
||||
## A Simple Request
|
||||
|
||||
Hi everyone. I hope you're using SharpCompress and finding it useful. Please give me feedback on what you'd like to see changed especially as far as usability goes. New feature suggestions are always welcome as well. I would also like to know what projects SharpCompress is being used in. I like seeing how it is used to give me ideas for future versions. Thanks!
|
||||
|
|
@ -34,154 +42,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 support
|
||||
* 7Zip writing
|
||||
* Zip64 (Need writing and extend Reading)
|
||||
* Multi-volume Zip support.
|
||||
|
||||
## 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
|
||||
|
||||
|
|
@ -189,6 +50,8 @@ XZ BCJ filters support contributed by Louis-Michel Bergeron, on behalf of aDolus
|
|||
|
||||
7Zip implementation based on: https://code.google.com/p/managed-lzma/
|
||||
|
||||
Zstandard implementation from: https://github.com/oleg-st/ZstdSharp
|
||||
|
||||
LICENSE
|
||||
Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26430.6
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3C5BE746-03E5-4895-9988-0B57F162F86C}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0F0901FF-E8D9-426A-B5A2-17C7F47C1529}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpCompress", "src\SharpCompress\SharpCompress.csproj", "{FD19DDD8-72B2-4024-8665-0D1F7A2AA998}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpCompress.Test", "tests\SharpCompress.Test\SharpCompress.Test.csproj", "{F2B1A1EB-0FA6-40D0-8908-E13247C7226F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "build", "build\build.csproj", "{D4D613CB-5E94-47FB-85BE-B8423D20C545}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{CDB42573-7D22-4490-BA12-1B7FB99CE7FB}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
Directory.Build.props = Directory.Build.props
|
||||
global.json = global.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{FD19DDD8-72B2-4024-8665-0D1F7A2AA998}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FD19DDD8-72B2-4024-8665-0D1F7A2AA998}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FD19DDD8-72B2-4024-8665-0D1F7A2AA998}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FD19DDD8-72B2-4024-8665-0D1F7A2AA998}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F2B1A1EB-0FA6-40D0-8908-E13247C7226F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F2B1A1EB-0FA6-40D0-8908-E13247C7226F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F2B1A1EB-0FA6-40D0-8908-E13247C7226F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F2B1A1EB-0FA6-40D0-8908-E13247C7226F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D4D613CB-5E94-47FB-85BE-B8423D20C545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{FD19DDD8-72B2-4024-8665-0D1F7A2AA998} = {3C5BE746-03E5-4895-9988-0B57F162F86C}
|
||||
{F2B1A1EB-0FA6-40D0-8908-E13247C7226F} = {0F0901FF-E8D9-426A-B5A2-17C7F47C1529}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArgumentsStyleNamedExpression/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeBraces_005Fdowhile/@EntryIndexedValue">ERROR</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeBraces_005Ffixed/@EntryIndexedValue">ERROR</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeBraces_005Ffor/@EntryIndexedValue">ERROR</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeBraces_005Fforeach/@EntryIndexedValue">ERROR</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeBraces_005Fifelse/@EntryIndexedValue">ERROR</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeBraces_005Flock/@EntryIndexedValue">ERROR</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeBraces_005Fusing/@EntryIndexedValue">ERROR</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeBraces_005Fwhile/@EntryIndexedValue">ERROR</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodSupportsCancellation/@EntryIndexedValue">ERROR</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantExplicitParamsArrayCreation/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
|
||||
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Basic_0020Clean/@EntryIndexedValue"><?xml version="1.0" encoding="utf-16"?><Profile name="Basic Clean"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSRemoveCodeRedundancies>True</CSRemoveCodeRedundancies><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="False" RemoveRedundantParentheses="False" AddMissingParentheses="False" ArrangeBraces="True" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSReformatCode>True</CSReformatCode></Profile></s:String>
|
||||
|
||||
<s:String x:Key="/Default/CodeStyle/CodeCleanup/SilentCleanupProfile/@EntryValue">Basic Clean</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/APPLY_ON_COMPLETION/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ARGUMENTS_NAMED/@EntryValue">Named</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOR/@EntryValue">Required</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOREACH/@EntryValue">Required</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_IFELSE/@EntryValue">Required</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_WHILE/@EntryValue">Required</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_FIRST_ARG_BY_PAREN/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_LINQ_QUERY/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_ARGUMENT/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_ARRAY_AND_OBJECT_INITIALIZER/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_CALLS_CHAIN/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_EXPRESSION/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_EXTENDS_LIST/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_FOR_STMT/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_PARAMETER/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTIPLE_DECLARATION/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTLINE_TYPE_PARAMETER_CONSTRAINS/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTLINE_TYPE_PARAMETER_LIST/@EntryValue">True</s:Boolean>
|
||||
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AFTER_START_COMMENT/@EntryValue">0</s:Int64>
|
||||
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_BEFORE_SINGLE_LINE_COMMENT/@EntryValue">1</s:Int64>
|
||||
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_ATTRIBUTE_STYLE/@EntryValue">SEPARATE</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FIXED_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FOR_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FOREACH_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_IFELSE_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_USING_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_WHILE_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_ANONYMOUS_METHOD_BLOCK/@EntryValue">True</s:Boolean>
|
||||
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_BLANK_LINES_IN_CODE/@EntryValue">1</s:Int64>
|
||||
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_BLANK_LINES_IN_DECLARATIONS/@EntryValue">1</s:Int64>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSOR_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_CONSTRUCTOR_INITIALIZER_ON_SAME_LINE/@EntryValue">False</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE/@EntryValue">False</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ACCESSORHOLDER_ON_SINGLE_LINE/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ACCESSOR_ATTRIBUTE_ON_SAME_LINE/@EntryValue">False</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_EMBEDDED_STATEMENT_ON_SAME_LINE/@EntryValue">NEVER</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_INITIALIZER_ON_SINGLE_LINE/@EntryValue">True</s:Boolean>
|
||||
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_WHILE_ON_NEW_LINE/@EntryValue">True</s:Boolean>
|
||||
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SIMPLE_EMBEDDED_STATEMENT_STYLE/@EntryValue">LINE_BREAK</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_AFTER_TYPECAST_PARENTHESES/@EntryValue">False</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_AROUND_ARROW_OP/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_AROUND_MULTIPLICATIVE_OP/@EntryValue">True</s:Boolean>
|
||||
|
||||
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_SIZEOF_PARENTHESES/@EntryValue">False</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_TYPEOF_PARENTHESES/@EntryValue">False</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/STICK_COMMENT/@EntryValue">False</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARGUMENTS_STYLE/@EntryValue">CHOP_IF_LONG</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARRAY_INITIALIZER_STYLE/@EntryValue">CHOP_IF_LONG</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_EXTENDS_LIST_STYLE/@EntryValue">CHOP_IF_LONG</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LINES/@EntryValue">False</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_PARAMETERS_STYLE/@EntryValue">CHOP_IF_LONG</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForBuiltInTypes/@EntryValue">UseVarWhenEvident</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseVarWhenEvident</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseVarWhenEvident</s:String>
|
||||
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=StaticReadonly/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FCONSTANT/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FFUNCTION/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FVARIABLE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FCLASS/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FCONSTRUCTOR/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FFUNCTION/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FGLOBAL_005FVARIABLE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FLABEL/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FLOCAL_005FCONSTRUCTOR/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FLOCAL_005FVARIABLE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FOBJECT_005FPROPERTY_005FOF_005FFUNCTION/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FPARAMETER/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FCLASS/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FENUM/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FENUM_005FMEMBER/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FINTERFACE/@EntryIndexedValue"><Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE_005FEXPORTED/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE_005FLOCAL/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FMEMBER_005FACCESSOR/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FTYPE_005FMETHOD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FMEMBER_005FACCESSOR/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FTYPE_005FMETHOD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FMEMBER_005FACCESSOR/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FTYPE_005FMETHOD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FTYPE_005FPARAMETER/@EntryIndexedValue"><Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FHTML_005FCONTROL/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FTAG_005FNAME/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FTAG_005FPREFIX/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=NAMESPACE_005FALIAS/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=6af8f80e_002D9fdd_002D4223_002D8e02_002D473db916f9b2/@EntryIndexedValue"><SessionState ContinuousTestingIsOn="False" ContinuousTestingMode="0" FrameworkVersion="{x:Null}" IsLocked="False" Name="All tests from Solution" PlatformMonoPreference="{x:Null}" PlatformType="{x:Null}" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Solution />
|
||||
</SessionState></s:String></wpf:ResourceDictionary>
|
||||
24
SharpCompress.slnx
Normal file
24
SharpCompress.slnx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<Solution>
|
||||
<Folder Name="/Config/">
|
||||
<File Path=".editorconfig" />
|
||||
<File Path=".github/workflows/nuget-release.yml" />
|
||||
<File Path=".gitignore" />
|
||||
<File Path="AGENTS.md" />
|
||||
<File Path="Directory.Build.props" />
|
||||
<File Path="Directory.Packages.props" />
|
||||
<File Path="docs/TAR_GAP_ANALYSIS.md" />
|
||||
<File Path="docs/TAR_SPEC.md" />
|
||||
<File Path="global.json" />
|
||||
<File Path="NuGet.config" />
|
||||
<File Path="README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/SharpCompress/SharpCompress.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/SharpCompress.AotSmoke/SharpCompress.AotSmoke.csproj" />
|
||||
<Project Path="tests/SharpCompress.Performance/SharpCompress.Performance.csproj" />
|
||||
<Project Path="tests/SharpCompress.Test/SharpCompress.Test.csproj" />
|
||||
</Folder>
|
||||
<Project Path="build/build.csproj" />
|
||||
</Solution>
|
||||
158
USAGE.md
158
USAGE.md
|
|
@ -1,158 +0,0 @@
|
|||
# SharpCompress Usage
|
||||
|
||||
## 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.
|
||||
|
||||
However, the .NET Framework often has classes that will dispose streams by default to make things "easy" like the following:
|
||||
|
||||
```C#
|
||||
using (var reader = new StreamReader(File.Open("foo")))
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
In this example, reader should get disposed. However, stream rules should say the the `FileStream` created by `File.Open` should remain open. However, the .NET Framework closes it for you by default unless you override the constructor. In general, you should be writing Stream code like this:
|
||||
|
||||
```C#
|
||||
using (var fileStream = File.Open("foo"))
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
To deal with the "correct" rules as well as the expectations of users, I've decided to always close wrapped streams as of 0.21.
|
||||
|
||||
To be explicit though, consider always using the overloads that use `ReaderOptions` or `WriterOptions` and explicitly set `LeaveStreamOpen` the way you want.
|
||||
|
||||
If using Compression Stream classes directly and you don't want the wrapped stream to be closed. Use the `NonDisposingStream` as a wrapped to prevent the stream being disposed. The change in 0.21 simplified a lot even though the usage is a bit more convoluted.
|
||||
|
||||
## Samples
|
||||
|
||||
Also, look over the tests for more thorough [examples](https://github.com/adamhathcock/sharpcompress/tree/master/tests/SharpCompress.Test)
|
||||
|
||||
### Create Zip Archive from multiple files
|
||||
```C#
|
||||
using(var archive = ZipArchive.Create())
|
||||
{
|
||||
archive.AddEntry("file01.txt", "C:\\file01.txt");
|
||||
archive.AddEntry("file02.txt", "C:\\file02.txt");
|
||||
...
|
||||
|
||||
archive.SaveTo("C:\\temp.zip", CompressionType.Deflate);
|
||||
}
|
||||
```
|
||||
|
||||
### Create Zip Archive from all files in a directory to a file
|
||||
|
||||
```C#
|
||||
using (var archive = ZipArchive.Create())
|
||||
{
|
||||
archive.AddAllFromDirectory("D:\\temp");
|
||||
archive.SaveTo("C:\\temp.zip", CompressionType.Deflate);
|
||||
}
|
||||
```
|
||||
|
||||
### Create Zip Archive from all files in a directory and save in memory
|
||||
|
||||
```C#
|
||||
var memoryStream = new MemoryStream();
|
||||
using (var archive = ZipArchive.Create())
|
||||
{
|
||||
archive.AddAllFromDirectory("D:\\temp");
|
||||
archive.SaveTo(memoryStream, new WriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
LeaveStreamOpen = true
|
||||
});
|
||||
}
|
||||
//reset memoryStream to be usable now
|
||||
memoryStream.Position = 0;
|
||||
```
|
||||
|
||||
### Extract all files from a Rar file to a directory using RarArchive
|
||||
|
||||
```C#
|
||||
using (var archive = RarArchive.Open("Test.rar"))
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
entry.WriteToDirectory("D:\\temp", new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use ReaderFactory to autodetect archive type and Open the entry stream
|
||||
|
||||
```C#
|
||||
using (Stream stream = File.OpenRead("Tar.tar.bz2"))
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Console.WriteLine(reader.Entry.Key);
|
||||
reader.WriteEntryToDirectory(@"C:\temp", new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use ReaderFactory to autodetect archive type and Open the entry stream
|
||||
|
||||
```C#
|
||||
using (Stream stream = File.OpenRead("Tar.tar.bz2"))
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
using (var entryStream = reader.OpenEntryStream())
|
||||
{
|
||||
entryStream.CopyTo(...);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use WriterFactory to write all files from a directory in a streaming manner.
|
||||
|
||||
```C#
|
||||
using (Stream stream = File.OpenWrite("C:\\temp.tgz"))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)
|
||||
{
|
||||
LeaveOpenStream = true
|
||||
}))
|
||||
{
|
||||
writer.WriteAll("D:\\temp", "*", SearchOption.AllDirectories);
|
||||
}
|
||||
```
|
||||
|
||||
### Extract zip which has non-utf8 encoded filename(cp932)
|
||||
|
||||
```C#
|
||||
var opts = new SharpCompress.Readers.ReaderOptions();
|
||||
var encoding = Encoding.GetEncoding(932);
|
||||
opts.ArchiveEncoding = new SharpCompress.Common.ArchiveEncoding();
|
||||
opts.ArchiveEncoding.CustomDecoder = (data, x, y) =>
|
||||
{
|
||||
return encoding.GetString(data);
|
||||
};
|
||||
var tr = SharpCompress.Archives.Zip.ZipArchive.Open("test.zip", opts);
|
||||
foreach(var entry in tr.Entries)
|
||||
{
|
||||
Console.WriteLine($"{entry.Key}");
|
||||
}
|
||||
```
|
||||
637
build/Program.cs
637
build/Program.cs
|
|
@ -1,21 +1,33 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
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;
|
||||
|
||||
const string Clean = "clean";
|
||||
const string Restore = "restore";
|
||||
const string UpdateLocks = "update-locks";
|
||||
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";
|
||||
const string DisplayBenchmarkResults = "display-benchmark-results";
|
||||
const string CompareBenchmarkResults = "compare-benchmark-results";
|
||||
const string GenerateBaseline = "generate-baseline";
|
||||
|
||||
Target(
|
||||
Clean,
|
||||
ForEach("**/bin", "**/obj"),
|
||||
["**/bin", "**/obj"],
|
||||
dir =>
|
||||
{
|
||||
IEnumerable<string> GetDirectories(string d)
|
||||
|
|
@ -44,14 +56,23 @@ Target(
|
|||
() =>
|
||||
{
|
||||
Run("dotnet", "tool restore");
|
||||
Run("dotnet", "csharpier --check .");
|
||||
Run("dotnet", "csharpier format .");
|
||||
}
|
||||
);
|
||||
Target(Restore, DependsOn(Format), () => Run("dotnet", "restore"));
|
||||
Target(
|
||||
CheckFormat,
|
||||
() =>
|
||||
{
|
||||
Run("dotnet", "tool restore");
|
||||
Run("dotnet", "csharpier check .");
|
||||
}
|
||||
);
|
||||
Target(Restore, [CheckFormat], () => Run("dotnet", "restore --locked-mode"));
|
||||
Target(UpdateLocks, [CheckFormat], () => Run("dotnet", "restore --force-evaluate"));
|
||||
|
||||
Target(
|
||||
Build,
|
||||
DependsOn(Restore),
|
||||
[Restore],
|
||||
() =>
|
||||
{
|
||||
Run("dotnet", "build src/SharpCompress/SharpCompress.csproj -c Release --no-restore");
|
||||
|
|
@ -60,8 +81,8 @@ Target(
|
|||
|
||||
Target(
|
||||
Test,
|
||||
DependsOn(Build),
|
||||
ForEach("net7.0", "net462"),
|
||||
[Build],
|
||||
["net10.0", "net48"],
|
||||
framework =>
|
||||
{
|
||||
IEnumerable<string> GetFiles(string d)
|
||||
|
|
@ -69,7 +90,7 @@ Target(
|
|||
return Glob.Files(".", d);
|
||||
}
|
||||
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && framework == "net462")
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && framework == "net48")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -83,13 +104,611 @@ Target(
|
|||
|
||||
Target(
|
||||
Publish,
|
||||
DependsOn(Test),
|
||||
[Test],
|
||||
() =>
|
||||
{
|
||||
Run("dotnet", "pack src/SharpCompress/SharpCompress.csproj -c Release -o artifacts/");
|
||||
}
|
||||
);
|
||||
|
||||
Target("default", DependsOn(Publish), () => Console.WriteLine("Done!"));
|
||||
Target(
|
||||
DetermineVersion,
|
||||
async () =>
|
||||
{
|
||||
var (version, isPrerelease) = await GetVersion();
|
||||
Console.WriteLine($"VERSION={version}");
|
||||
Console.WriteLine(
|
||||
$"PRERELEASE={isPrerelease.ToString().ToLower(CultureInfo.InvariantCulture)}"
|
||||
);
|
||||
|
||||
// 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(CultureInfo.InvariantCulture)}\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(
|
||||
DisplayBenchmarkResults,
|
||||
() =>
|
||||
{
|
||||
var githubStepSummary = Environment.GetEnvironmentVariable("GITHUB_STEP_SUMMARY");
|
||||
var resultsDir = "benchmark-results/results";
|
||||
|
||||
if (!Directory.Exists(resultsDir))
|
||||
{
|
||||
Console.WriteLine("No benchmark results found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var markdownFiles = Directory
|
||||
.GetFiles(resultsDir, "*-report-github.md")
|
||||
.OrderBy(f => f)
|
||||
.ToList();
|
||||
|
||||
if (markdownFiles.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No benchmark markdown reports found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var output = new List<string> { "## Benchmark Results", "" };
|
||||
|
||||
foreach (var file in markdownFiles)
|
||||
{
|
||||
Console.WriteLine($"Processing {Path.GetFileName(file)}");
|
||||
var content = File.ReadAllText(file);
|
||||
output.Add(content);
|
||||
output.Add("");
|
||||
}
|
||||
|
||||
// Write to GitHub Step Summary if available
|
||||
if (!string.IsNullOrEmpty(githubStepSummary))
|
||||
{
|
||||
File.AppendAllLines(githubStepSummary, output);
|
||||
Console.WriteLine($"Benchmark results written to GitHub Step Summary");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write to console if not in GitHub Actions
|
||||
foreach (var line in output)
|
||||
{
|
||||
Console.WriteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Target(
|
||||
CompareBenchmarkResults,
|
||||
() =>
|
||||
{
|
||||
var githubStepSummary = Environment.GetEnvironmentVariable("GITHUB_STEP_SUMMARY");
|
||||
var baselinePath = "tests/SharpCompress.Performance/baseline-results.md";
|
||||
var resultsDir = "benchmark-results/results";
|
||||
|
||||
var output = new List<string> { "## Comparison with Baseline", "" };
|
||||
|
||||
if (!File.Exists(baselinePath))
|
||||
{
|
||||
Console.WriteLine("Baseline file not found");
|
||||
output.Add("⚠️ Baseline file not found. Run `generate-baseline` to create it.");
|
||||
WriteOutput(output, githubStepSummary);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(resultsDir))
|
||||
{
|
||||
Console.WriteLine("No current benchmark results found.");
|
||||
output.Add("⚠️ No current benchmark results found. Showing baseline only.");
|
||||
output.Add("");
|
||||
output.Add("### Baseline Results");
|
||||
output.AddRange(File.ReadAllLines(baselinePath));
|
||||
WriteOutput(output, githubStepSummary);
|
||||
return;
|
||||
}
|
||||
|
||||
var markdownFiles = Directory
|
||||
.GetFiles(resultsDir, "*-report-github.md")
|
||||
.OrderBy(f => f)
|
||||
.ToList();
|
||||
|
||||
if (markdownFiles.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No current benchmark markdown reports found.");
|
||||
output.Add("⚠️ No current benchmark results found. Showing baseline only.");
|
||||
output.Add("");
|
||||
output.Add("### Baseline Results");
|
||||
output.AddRange(File.ReadAllLines(baselinePath));
|
||||
WriteOutput(output, githubStepSummary);
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Parsing baseline results...");
|
||||
var baselineMetrics = ParseBenchmarkResults(File.ReadAllText(baselinePath));
|
||||
|
||||
Console.WriteLine("Parsing current results...");
|
||||
var currentText = string.Join("\n", markdownFiles.Select(f => File.ReadAllText(f)));
|
||||
var currentMetrics = ParseBenchmarkResults(currentText);
|
||||
|
||||
Console.WriteLine("Comparing results...");
|
||||
output.Add("### Performance Comparison");
|
||||
output.Add("");
|
||||
output.Add(
|
||||
"| Benchmark | Baseline Mean | Current Mean | Change | Baseline Memory | Current Memory | Change |"
|
||||
);
|
||||
output.Add(
|
||||
"|-----------|---------------|--------------|--------|-----------------|----------------|--------|"
|
||||
);
|
||||
|
||||
var hasRegressions = false;
|
||||
var hasImprovements = false;
|
||||
|
||||
foreach (var method in currentMetrics.Keys.Union(baselineMetrics.Keys).OrderBy(k => k))
|
||||
{
|
||||
var hasCurrent = currentMetrics.TryGetValue(method, out var current);
|
||||
var hasBaseline = baselineMetrics.TryGetValue(method, out var baseline);
|
||||
|
||||
if (!hasCurrent)
|
||||
{
|
||||
output.Add(
|
||||
$"| {method} | {baseline!.Mean} | ❌ Missing | N/A | {baseline.Memory} | N/A | N/A |"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasBaseline)
|
||||
{
|
||||
output.Add(
|
||||
$"| {method} | ❌ New | {current!.Mean} | N/A | N/A | {current.Memory} | N/A |"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var timeChange = CalculateChange(baseline!.MeanValue, current!.MeanValue);
|
||||
var memChange = CalculateChange(baseline.MemoryValue, current.MemoryValue);
|
||||
|
||||
var timeIcon =
|
||||
timeChange > 25 ? "🔴"
|
||||
: timeChange < -25 ? "🟢"
|
||||
: "⚪";
|
||||
var memIcon =
|
||||
memChange > 25 ? "🔴"
|
||||
: memChange < -25 ? "🟢"
|
||||
: "⚪";
|
||||
|
||||
if (timeChange > 25 || memChange > 25)
|
||||
{
|
||||
hasRegressions = true;
|
||||
}
|
||||
if (timeChange < -25 || memChange < -25)
|
||||
{
|
||||
hasImprovements = true;
|
||||
}
|
||||
|
||||
output.Add(
|
||||
$"| {method} | {baseline.Mean} | {current.Mean} | {timeIcon} {timeChange:+0.0;-0.0;0}% | {baseline.Memory} | {current.Memory} | {memIcon} {memChange:+0.0;-0.0;0}% |"
|
||||
);
|
||||
}
|
||||
|
||||
output.Add("");
|
||||
output.Add("**Legend:**");
|
||||
output.Add("- 🔴 Regression (>25% slower/more memory)");
|
||||
output.Add("- 🟢 Improvement (>25% faster/less memory)");
|
||||
output.Add("- ⚪ No significant change");
|
||||
|
||||
if (hasRegressions)
|
||||
{
|
||||
output.Add("");
|
||||
output.Add(
|
||||
"⚠️ **Warning**: Performance regressions detected. Review the changes carefully."
|
||||
);
|
||||
}
|
||||
else if (hasImprovements)
|
||||
{
|
||||
output.Add("");
|
||||
output.Add("✅ Performance improvements detected!");
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Add("");
|
||||
output.Add("✅ Performance is stable compared to baseline.");
|
||||
}
|
||||
|
||||
WriteOutput(output, githubStepSummary);
|
||||
}
|
||||
);
|
||||
|
||||
Target(
|
||||
GenerateBaseline,
|
||||
() =>
|
||||
{
|
||||
var perfProject = "tests/SharpCompress.Performance/SharpCompress.Performance.csproj";
|
||||
var baselinePath = "tests/SharpCompress.Performance/baseline-results.md";
|
||||
var artifactsDir = "baseline-artifacts";
|
||||
|
||||
Console.WriteLine("Building performance project...");
|
||||
Run("dotnet", $"build {perfProject} --configuration Release");
|
||||
|
||||
Console.WriteLine("Running benchmarks to generate baseline...");
|
||||
Run(
|
||||
"dotnet",
|
||||
$"run --project {perfProject} --configuration Release --no-build -- --filter \"*\" --exporters markdown --artifacts {artifactsDir}"
|
||||
);
|
||||
|
||||
var resultsDir = Path.Combine(artifactsDir, "results");
|
||||
if (!Directory.Exists(resultsDir))
|
||||
{
|
||||
Console.WriteLine("ERROR: No benchmark results generated.");
|
||||
return;
|
||||
}
|
||||
|
||||
var markdownFiles = Directory
|
||||
.GetFiles(resultsDir, "*-report-github.md")
|
||||
.OrderBy(f => f)
|
||||
.ToList();
|
||||
|
||||
if (markdownFiles.Count == 0)
|
||||
{
|
||||
Console.WriteLine("ERROR: No markdown reports found.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Combining {markdownFiles.Count} benchmark reports...");
|
||||
var baselineContent = new List<string>();
|
||||
|
||||
foreach (var file in markdownFiles)
|
||||
{
|
||||
var lines = File.ReadAllLines(file);
|
||||
baselineContent.AddRange(lines.Select(l => l.Trim()).Where(l => l.StartsWith('|')));
|
||||
}
|
||||
|
||||
File.WriteAllText(baselinePath, string.Join(Environment.NewLine, baselineContent));
|
||||
Console.WriteLine($"Baseline written to {baselinePath}");
|
||||
|
||||
// Clean up artifacts directory
|
||||
if (Directory.Exists(artifactsDir))
|
||||
{
|
||||
Directory.Delete(artifactsDir, true);
|
||||
Console.WriteLine("Cleaned up artifacts directory.");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Target("default", [Publish], () => Console.WriteLine("Done!"));
|
||||
|
||||
await RunTargetsAndExitAsync(args);
|
||||
|
||||
static async Task<(string version, bool isPrerelease)> GetVersion()
|
||||
{
|
||||
// Check if current commit has a version tag
|
||||
var currentTag = (await GetGitOutput("tag", "--points-at HEAD"))
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault(tag => Regex.IsMatch(tag.Trim(), @"^\d+\.\d+\.\d+$"));
|
||||
|
||||
if (!string.IsNullOrEmpty(currentTag))
|
||||
{
|
||||
// Tagged release - use the tag as version
|
||||
var version = currentTag.Trim();
|
||||
Console.WriteLine($"Building tagged release version: {version}");
|
||||
return (version, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not tagged - create prerelease version
|
||||
var allTags = (await GetGitOutput("tag", "--list"))
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(tag => Regex.IsMatch(tag.Trim(), @"^\d+\.\d+\.\d+$"))
|
||||
.Select(tag => tag.Trim())
|
||||
.ToList();
|
||||
|
||||
var lastTag = allTags.OrderBy(tag => Version.Parse(tag)).LastOrDefault() ?? "0.0.0";
|
||||
var lastVersion = Version.Parse(lastTag);
|
||||
|
||||
// Determine version increment based on branch
|
||||
var currentBranch = await GetCurrentBranch();
|
||||
Version nextVersion;
|
||||
|
||||
if (currentBranch == "release")
|
||||
{
|
||||
// Release branch: increment patch version
|
||||
nextVersion = new Version(lastVersion.Major, lastVersion.Minor, lastVersion.Build + 1);
|
||||
Console.WriteLine($"Building prerelease for release branch (patch increment)");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Master or other branches: increment minor version
|
||||
nextVersion = new Version(lastVersion.Major, lastVersion.Minor + 1, 0);
|
||||
Console.WriteLine($"Building prerelease for {currentBranch} branch (minor increment)");
|
||||
}
|
||||
|
||||
// Use commit count since the last version tag if available; otherwise, fall back to total count
|
||||
var revListArgs = allTags.Any() ? $"--count {lastTag}..HEAD" : "--count HEAD";
|
||||
var commitCount = (await GetGitOutput("rev-list", revListArgs)).Trim();
|
||||
|
||||
var version = $"{nextVersion}-beta.{commitCount}";
|
||||
Console.WriteLine($"Building prerelease version: {version}");
|
||||
return (version, true);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<string> GetCurrentBranch()
|
||||
{
|
||||
// In GitHub Actions, GITHUB_REF_NAME contains the branch name
|
||||
var githubRefName = Environment.GetEnvironmentVariable("GITHUB_REF_NAME");
|
||||
if (!string.IsNullOrEmpty(githubRefName))
|
||||
{
|
||||
return githubRefName;
|
||||
}
|
||||
|
||||
// Fallback to git command for local builds
|
||||
try
|
||||
{
|
||||
var (output, _) = await ReadAsync("git", "branch --show-current");
|
||||
return output.Trim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Warning: Could not determine current branch: {ex.Message}");
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<string> GetGitOutput(string command, string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use SimpleExec's Read to execute git commands in a cross-platform way
|
||||
var (output, _) = await ReadAsync("git", $"{command} {args}");
|
||||
return output;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Git command failed: git {command} {args}\n{ex.Message}",
|
||||
ex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void WriteOutput(List<string> output, string? githubStepSummary)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(githubStepSummary))
|
||||
{
|
||||
File.AppendAllLines(githubStepSummary, output);
|
||||
Console.WriteLine("Comparison written to GitHub Step Summary");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var line in output)
|
||||
{
|
||||
Console.WriteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, BenchmarkMetric> ParseBenchmarkResults(string markdown)
|
||||
{
|
||||
var metrics = new Dictionary<string, BenchmarkMetric>();
|
||||
var lines = markdown.Split('\n');
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
|
||||
// Look for table rows with benchmark data
|
||||
if (line.StartsWith('|') && line.Contains("'", StringComparison.Ordinal) && i > 0)
|
||||
{
|
||||
var parts = line.Split('|', StringSplitOptions.TrimEntries);
|
||||
if (parts.Length >= 5)
|
||||
{
|
||||
var method = parts[1].Replace("'", "'", StringComparison.Ordinal);
|
||||
var meanStr = parts[2];
|
||||
|
||||
// Find Allocated column - it's usually the last column or labeled "Allocated"
|
||||
string memoryStr = "N/A";
|
||||
for (int j = parts.Length - 2; j >= 2; j--)
|
||||
{
|
||||
if (
|
||||
parts[j].Contains("KB", StringComparison.Ordinal)
|
||||
|| parts[j].Contains("MB", StringComparison.Ordinal)
|
||||
|| parts[j].Contains("GB", StringComparison.Ordinal)
|
||||
|| parts[j].Contains('B', StringComparison.Ordinal)
|
||||
)
|
||||
{
|
||||
memoryStr = parts[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!method.Equals("Method", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrWhiteSpace(method)
|
||||
)
|
||||
{
|
||||
var metric = new BenchmarkMetric
|
||||
{
|
||||
Method = method,
|
||||
Mean = meanStr,
|
||||
MeanValue = ParseTimeValue(meanStr),
|
||||
Memory = memoryStr,
|
||||
MemoryValue = ParseMemoryValue(memoryStr),
|
||||
};
|
||||
metrics[method] = metric;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
static double ParseTimeValue(string timeStr)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(timeStr) || timeStr == "N/A" || timeStr == "NA")
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Remove thousands separators and parse
|
||||
timeStr = timeStr.Replace(",", "", StringComparison.Ordinal).Trim();
|
||||
|
||||
var match = Regex.Match(timeStr, @"([\d.]+)\s*(\w+)");
|
||||
if (!match.Success)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var value = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
|
||||
var unit = match.Groups[2].Value.ToLower(CultureInfo.InvariantCulture);
|
||||
|
||||
// Convert to microseconds for comparison
|
||||
return unit switch
|
||||
{
|
||||
"s" => value * 1_000_000,
|
||||
"ms" => value * 1_000,
|
||||
"μs" or "us" => value,
|
||||
"ns" => value / 1_000,
|
||||
_ => value,
|
||||
};
|
||||
}
|
||||
|
||||
static double ParseMemoryValue(string memStr)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(memStr) || memStr == "N/A" || memStr == "NA")
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
memStr = memStr.Replace(",", "", StringComparison.Ordinal).Trim();
|
||||
|
||||
var match = Regex.Match(memStr, @"([\d.]+)\s*(\w+)");
|
||||
if (!match.Success)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var value = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
|
||||
var unit = match.Groups[2].Value.ToUpper(CultureInfo.InvariantCulture);
|
||||
|
||||
// Convert to KB for comparison
|
||||
return unit switch
|
||||
{
|
||||
"GB" => value * 1_024 * 1_024,
|
||||
"MB" => value * 1_024,
|
||||
"KB" => value,
|
||||
"B" => value / 1_024,
|
||||
_ => value,
|
||||
};
|
||||
}
|
||||
|
||||
static double CalculateChange(double baseline, double current)
|
||||
{
|
||||
if (baseline == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return ((current - baseline) / baseline) * 100;
|
||||
}
|
||||
|
||||
record BenchmarkMetric
|
||||
{
|
||||
public string Method { get; init; } = "";
|
||||
public string Mean { get; init; } = "";
|
||||
public double MeanValue { get; init; }
|
||||
public string Memory { get; init; } = "";
|
||||
public double MemoryValue { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bullseye" Version="4.2.1" />
|
||||
<PackageReference Include="Glob" Version="1.1.9" />
|
||||
<PackageReference Include="SimpleExec" Version="11.0.0" />
|
||||
<PackageReference Include="Bullseye" />
|
||||
<PackageReference Include="Glob" />
|
||||
<PackageReference Include="SimpleExec" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
80
build/packages.lock.json
Normal file
80
build/packages.lock.json
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Bullseye": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.1.0, )",
|
||||
"resolved": "6.1.0",
|
||||
"contentHash": "fltnAJDe0BEX5eymXGUq+il2rSUA0pHqUonNDRH2TrvRu8SkU17mYG0IVpdmG2ibtfhdjNrv4CuTCxHOwcozCA=="
|
||||
},
|
||||
"Glob": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.1.9, )",
|
||||
"resolved": "1.1.9",
|
||||
"contentHash": "AfK5+ECWYTP7G3AAdnU8IfVj+QpGjrh9GC2mpdcJzCvtQ4pnerAGwHsxJ9D4/RnhDUz2DSzd951O/lQjQby2Sw=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.300, )",
|
||||
"resolved": "10.0.300",
|
||||
"contentHash": "QzCtLkXVb3l4IxcpvJCbzUwMLihAmLN6vVLjQGSzYSF8d2dvXxqJAZk83RV3gYnp2egz8jRMgSR2woY3vOahTA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.300",
|
||||
"Microsoft.SourceLink.Common": "10.0.300",
|
||||
"System.IO.Hashing": "10.0.8"
|
||||
}
|
||||
},
|
||||
"Microsoft.VisualStudio.Threading.Analyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[17.14.15, )",
|
||||
"resolved": "17.14.15",
|
||||
"contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw=="
|
||||
},
|
||||
"PolySharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.16.0, )",
|
||||
"resolved": "1.16.0",
|
||||
"contentHash": "3kdIIceBPumwjw279FuiVMfVENT2cGASXJgcigdySsbX2dJB8ofUgG6i47yqF/k1qu6fvNR3csrSekZPviR6kQ=="
|
||||
},
|
||||
"SimpleExec": {
|
||||
"type": "Direct",
|
||||
"requested": "[13.0.0, )",
|
||||
"resolved": "13.0.0",
|
||||
"contentHash": "zcCR1pupa1wI1VqBULRiQKeHKKZOuJhi/K+4V5oO+rHJZlaOD53ViFo1c3PavDoMAfSn/FAXGAWpPoF57rwhYg=="
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.300",
|
||||
"contentHash": "P0kaQwVZx4xIUe2FtrLyBadYNXuAljttJUPvjBYRuHhPE8L77L42KakLDkaADRiUrGspoLcMwayjrbQhYTr0zA==",
|
||||
"dependencies": {
|
||||
"System.IO.Hashing": "10.0.8"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.300",
|
||||
"contentHash": "0jlkXaUGjYlWTIVPve5MftjKHnT3SlAtq9BCLV4J9IjdPrxV/+4rMlBSjfr1khG8/GC6KGojjola8E1VvWF0qQ=="
|
||||
},
|
||||
"System.IO.Hashing": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.8",
|
||||
"contentHash": "+dJsbPJ3FyUbTZNplFj0RCKePFizmv6ewDV46JE9q/IVH4c3xTCftHfHelLsAKf0jryIPqgMb5GpS0x7TAY3mg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
810
docs/API.md
Normal file
810
docs/API.md
Normal file
|
|
@ -0,0 +1,810 @@
|
|||
# API Quick Reference
|
||||
|
||||
Quick reference for commonly used SharpCompress APIs.
|
||||
|
||||
## Factory Methods
|
||||
|
||||
### Opening Archives
|
||||
|
||||
```csharp
|
||||
// Auto-detect format
|
||||
using (var reader = ReaderFactory.OpenReader(stream))
|
||||
{
|
||||
// Works with Zip, Tar, GZip, Rar, 7Zip, etc.
|
||||
}
|
||||
|
||||
// Specific format - Archive API
|
||||
using (var archive = ZipArchive.OpenArchive("file.zip"))
|
||||
using (var archive = TarArchive.OpenArchive("file.tar"))
|
||||
using (var archive = RarArchive.OpenArchive("file.rar"))
|
||||
using (var archive = SevenZipArchive.OpenArchive("file.7z"))
|
||||
using (var archive = GZipArchive.OpenArchive("file.gz"))
|
||||
|
||||
// With fluent options (preferred)
|
||||
var options = ReaderOptions.ForEncryptedArchive("password")
|
||||
.WithArchiveEncoding(new ArchiveEncoding { Default = Encoding.GetEncoding(932) });
|
||||
using (var archive = ZipArchive.OpenArchive("encrypted.zip", options))
|
||||
|
||||
// Alternative: object initializer
|
||||
var options2 = new ReaderOptions
|
||||
{
|
||||
Password = "password",
|
||||
LeaveStreamOpen = true,
|
||||
ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
|
||||
};
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Auto-detect and open with the Archive API
|
||||
using (var archive = ArchiveFactory.OpenArchive("archive.zip"))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
Console.WriteLine(entry.Key);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect before opening
|
||||
if (ArchiveFactory.IsArchive("archive.zip", out var archiveType))
|
||||
{
|
||||
Console.WriteLine($"Detected {archiveType}");
|
||||
}
|
||||
|
||||
// Detect capabilities before choosing Archive API vs Reader API
|
||||
var info = ArchiveFactory.GetArchiveInformation("archive.arc");
|
||||
if (info is not null)
|
||||
{
|
||||
Console.WriteLine($"Type: {info.Type}");
|
||||
Console.WriteLine($"Supports random access: {info.SupportsRandomAccess}");
|
||||
}
|
||||
|
||||
var asyncInfo = await ArchiveFactory.GetArchiveInformationAsync(
|
||||
"archive.zip",
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
// Multi-volume archives
|
||||
var parts = ArchiveFactory.GetFileParts("archive.part1.rar")
|
||||
.Select(path => new FileInfo(path))
|
||||
.ToArray();
|
||||
using (var archive = ArchiveFactory.OpenArchive(parts))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
```
|
||||
|
||||
`ArchiveInformation.SupportsRandomAccess` is `true` when the detected format supports `IArchive` random access. It is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW, where `ReaderFactory.OpenReader` should be used instead.
|
||||
|
||||
### Creating Archives
|
||||
|
||||
```csharp
|
||||
// Writer Factory
|
||||
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, CompressionType.Deflate))
|
||||
{
|
||||
// Write entries
|
||||
}
|
||||
|
||||
// Specific writer
|
||||
using (var archive = ZipArchive.CreateArchive())
|
||||
using (var archive = TarArchive.CreateArchive())
|
||||
using (var archive = GZipArchive.CreateArchive())
|
||||
|
||||
// With fluent options (preferred)
|
||||
var options = WriterOptions.ForZip()
|
||||
.WithCompressionLevel(9)
|
||||
.WithLeaveStreamOpen(false)
|
||||
.WithBufferSize(131072);
|
||||
using (var archive = ZipArchive.CreateArchive())
|
||||
{
|
||||
archive.SaveTo("output.zip", options);
|
||||
}
|
||||
|
||||
// Alternative: constructor with object initializer
|
||||
var options2 = new WriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
CompressionLevel = 9,
|
||||
LeaveStreamOpen = false,
|
||||
BufferSize = 131072
|
||||
};
|
||||
```
|
||||
|
||||
`WriterOptions.BufferSize` controls stream copy buffers used while writing archive entries. If it is not set, SharpCompress falls back to `Constants.BufferSize`.
|
||||
|
||||
---
|
||||
|
||||
## Archive API Methods
|
||||
|
||||
### Reading/Extracting
|
||||
|
||||
```csharp
|
||||
using (var archive = ZipArchive.OpenArchive("file.zip"))
|
||||
{
|
||||
// Get all entries
|
||||
IEnumerable<IArchiveEntry> entries = archive.Entries;
|
||||
|
||||
// Find specific entry
|
||||
var entry = archive.Entries.FirstOrDefault(e => e.Key == "file.txt");
|
||||
|
||||
// Extract all
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
|
||||
// Extract single entry
|
||||
var firstEntry = archive.Entries.First();
|
||||
firstEntry.WriteToFile(@"C:\output\file.txt");
|
||||
|
||||
// Extract single entry to a stream with extraction options
|
||||
using (var outputStream = File.Create(@"C:\output\file.txt"))
|
||||
{
|
||||
firstEntry.WriteTo(
|
||||
outputStream,
|
||||
new ExtractionOptions { CheckCrc = false }
|
||||
);
|
||||
}
|
||||
|
||||
// Get entry stream
|
||||
using (var stream = entry.OpenEntryStream())
|
||||
{
|
||||
stream.CopyTo(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
// Async extraction (requires IAsyncArchive)
|
||||
await using (var asyncArchive = await ZipArchive.OpenAsyncArchive("file.zip"))
|
||||
{
|
||||
// Extract all entries asynchronously
|
||||
await asyncArchive.WriteToDirectoryAsync(
|
||||
@"C:\output",
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
// Open a specific entry stream asynchronously
|
||||
await using (var asyncArchive = await ZipArchive.OpenAsyncArchive("file.zip"))
|
||||
{
|
||||
await foreach (var entry in asyncArchive.EntriesAsync)
|
||||
{
|
||||
await using (var outputStream = File.Create(@"C:\output\" + entry.Key))
|
||||
{
|
||||
await entry.WriteToAsync(
|
||||
outputStream,
|
||||
new ExtractionOptions { CheckCrc = false },
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
using (var stream = await entry.OpenEntryStreamAsync(cancellationToken))
|
||||
{
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Entry Properties
|
||||
|
||||
```csharp
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
string name = entry.Key; // Entry name/path
|
||||
long size = entry.Size; // Uncompressed size
|
||||
long compressedSize = entry.CompressedSize;
|
||||
bool isDir = entry.IsDirectory;
|
||||
DateTime? modTime = entry.LastModifiedTime;
|
||||
CompressionType compression = entry.CompressionType;
|
||||
}
|
||||
```
|
||||
|
||||
### Creating Archives
|
||||
|
||||
```csharp
|
||||
using (var archive = ZipArchive.CreateArchive())
|
||||
{
|
||||
// Add file
|
||||
archive.AddEntry("file.txt", @"C:\source\file.txt");
|
||||
|
||||
// Add multiple files
|
||||
archive.AddAllFromDirectory(@"C:\source");
|
||||
archive.AddAllFromDirectory(@"C:\source", "*.txt"); // Pattern
|
||||
|
||||
// Save to file
|
||||
archive.SaveTo("output.zip", CompressionType.Deflate);
|
||||
|
||||
// Save to stream
|
||||
archive.SaveTo(outputStream, new WriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
CompressionLevel = 9,
|
||||
LeaveStreamOpen = true
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reader API Methods
|
||||
|
||||
### Forward-Only Reading
|
||||
|
||||
```csharp
|
||||
using (var stream = File.OpenRead("file.zip"))
|
||||
using (var reader = ReaderFactory.OpenReader(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
IArchiveEntry entry = reader.Entry;
|
||||
|
||||
if (!entry.IsDirectory)
|
||||
{
|
||||
// Extract entry
|
||||
reader.WriteEntryToDirectory(@"C:\output");
|
||||
reader.WriteEntryToFile(@"C:\output\file.txt");
|
||||
|
||||
// Or get stream
|
||||
using (var entryStream = reader.OpenEntryStream())
|
||||
{
|
||||
entryStream.CopyTo(outputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Async variants (use OpenAsyncReader to get IAsyncReader)
|
||||
using (var stream = File.OpenRead("file.zip"))
|
||||
await using (var reader = await ReaderFactory.OpenAsyncReader(stream))
|
||||
{
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
await reader.WriteEntryToFileAsync(
|
||||
@"C:\output\" + reader.Entry.Key,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
// Async extraction of all entries
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
@"C:\output",
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Writer API Methods
|
||||
|
||||
### Creating Archives (Streaming)
|
||||
|
||||
```csharp
|
||||
using (var stream = File.Create("output.zip"))
|
||||
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, CompressionType.Deflate))
|
||||
{
|
||||
// Write single file
|
||||
using (var fileStream = File.OpenRead("source.txt"))
|
||||
{
|
||||
writer.Write("entry.txt", fileStream, DateTime.Now);
|
||||
}
|
||||
|
||||
// Write directory
|
||||
writer.WriteAll("C:\\source", "*", SearchOption.AllDirectories);
|
||||
writer.WriteAll("C:\\source", "*.txt", SearchOption.TopDirectoryOnly);
|
||||
}
|
||||
|
||||
// Async variants: use OpenAsyncWriter to get IAsyncWriter
|
||||
await using var stream = File.Create("output.zip");
|
||||
await using var writer = await WriterFactory.OpenAsyncWriter(stream, ArchiveType.Zip, new WriterOptions(CompressionType.Deflate), cancellationToken);
|
||||
|
||||
using (var fileStream = File.OpenRead("source.txt"))
|
||||
{
|
||||
await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken);
|
||||
}
|
||||
|
||||
await writer.WriteAllAsync("C:\\source", "*", SearchOption.AllDirectories, cancellationToken);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Options
|
||||
|
||||
### ReaderOptions
|
||||
|
||||
Use preset properties and fluent helpers for common configurations:
|
||||
|
||||
```csharp
|
||||
// External stream with password and custom encoding
|
||||
var options = ReaderOptions.ForExternalStream
|
||||
.WithPassword("password")
|
||||
.WithArchiveEncoding(new ArchiveEncoding { Default = Encoding.GetEncoding(932) });
|
||||
|
||||
using (var archive = ZipArchive.OpenArchive("file.zip", options))
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
// Open-time presets
|
||||
var external = ReaderOptions.ForExternalStream;
|
||||
var owned = ReaderOptions.ForFilePath;
|
||||
var encrypted = ReaderOptions.ForEncryptedArchive("password");
|
||||
var encoded = ReaderOptions.ForEncoding(new ArchiveEncoding { Default = Encoding.UTF8 });
|
||||
var sfx = ReaderOptions.ForSelfExtractingArchive("password");
|
||||
|
||||
// Faster detection when the container is known
|
||||
var hinted = ReaderOptions.ForExternalStream.WithExtensionHint("tar.gz");
|
||||
|
||||
// Increase for non-seekable streams with large detection probes, such as SFX RAR
|
||||
var buffered = ReaderOptions.ForExternalStream.WithRewindableBufferSize(1_048_576);
|
||||
|
||||
// Extraction presets
|
||||
var safeOptions = ExtractionOptions.SafeExtract; // No overwrite
|
||||
var flatOptions = ExtractionOptions.FlatExtract; // No directory structure
|
||||
var metadataOptions = ExtractionOptions.PreserveMetadata; // Keep timestamps and attributes
|
||||
|
||||
// Tune extraction copy buffering
|
||||
var extractionOptions = new ExtractionOptions
|
||||
{
|
||||
BufferSize = 131072,
|
||||
CheckCrc = true, // Default: validate entry checksums when archive metadata provides one
|
||||
};
|
||||
|
||||
// Factory defaults:
|
||||
// - file path / FileInfo overloads use LeaveStreamOpen = false
|
||||
// - stream overloads use LeaveStreamOpen = true
|
||||
```
|
||||
|
||||
Alternative: traditional object initializer:
|
||||
|
||||
```csharp
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
Password = "password",
|
||||
LeaveStreamOpen = true,
|
||||
ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) },
|
||||
ExtensionHint = "zip",
|
||||
LookForHeader = true,
|
||||
DisableCheckIncomplete = false,
|
||||
BufferSize = 81920,
|
||||
RewindableBufferSize = 1_048_576,
|
||||
};
|
||||
|
||||
var extractionOptions = new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true,
|
||||
BufferSize = 131072,
|
||||
};
|
||||
```
|
||||
|
||||
### WriterOptions
|
||||
|
||||
Factory methods provide a clean, discoverable way to create writer options:
|
||||
|
||||
```csharp
|
||||
// Factory methods for common archive types
|
||||
var zipOptions = WriterOptions.ForZip() // ZIP with Deflate
|
||||
.WithCompressionLevel(9) // 0-9 for Deflate
|
||||
.WithLeaveStreamOpen(false); // Close stream when done
|
||||
|
||||
var tarOptions = WriterOptions.ForTar(CompressionType.GZip) // TAR with GZip
|
||||
.WithLeaveStreamOpen(false);
|
||||
|
||||
var gzipOptions = WriterOptions.ForGZip() // GZip file
|
||||
.WithCompressionLevel(6);
|
||||
|
||||
archive.SaveTo("output.zip", zipOptions);
|
||||
```
|
||||
|
||||
Use typed writer options when a format exposes extra settings:
|
||||
|
||||
```csharp
|
||||
// ZIP archive-level options
|
||||
var zipWriterOptions = new ZipWriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
ArchiveComment = "Created by SharpCompress",
|
||||
UseZip64 = true,
|
||||
CompressionLevel = 9,
|
||||
};
|
||||
|
||||
// ZIP per-entry options
|
||||
using (var writer = new ZipWriter(outputStream, zipWriterOptions))
|
||||
using (var source = File.OpenRead("source.txt"))
|
||||
{
|
||||
writer.Write("entry.txt", source, new ZipWriterEntryOptions
|
||||
{
|
||||
CompressionType = CompressionType.ZStandard,
|
||||
CompressionLevel = 3,
|
||||
EntryComment = "per-entry comment",
|
||||
ModificationDateTime = DateTime.UtcNow,
|
||||
EnableZip64 = true,
|
||||
});
|
||||
}
|
||||
|
||||
// TAR-specific options
|
||||
var tarOptions = new TarWriterOptions(CompressionType.GZip, finalizeArchiveOnClose: true)
|
||||
{
|
||||
HeaderFormat = TarHeaderWriteFormat.GNU_TAR_LONG_LINK,
|
||||
};
|
||||
|
||||
// GZip-specific options
|
||||
var gzipOptions = new GZipWriterOptions(compressionLevel: 9);
|
||||
|
||||
// 7z writing requires a seekable output stream and writes non-solid archives
|
||||
var sevenZipOptions = new SevenZipWriterOptions(CompressionType.LZMA2)
|
||||
{
|
||||
CompressHeader = true,
|
||||
LzmaProperties = new LzmaEncoderProperties(),
|
||||
};
|
||||
```
|
||||
|
||||
Alternative: traditional constructor with object initializer:
|
||||
|
||||
```csharp
|
||||
var options = new WriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
CompressionLevel = 9,
|
||||
LeaveStreamOpen = true,
|
||||
};
|
||||
archive.SaveTo("output.zip", options);
|
||||
```
|
||||
|
||||
### Extraction behavior
|
||||
|
||||
```csharp
|
||||
var options = new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true, // Recreate directory structure
|
||||
Overwrite = true, // Overwrite existing files
|
||||
PreserveFileTime = true, // Keep original timestamps
|
||||
CheckCrc = true // Validate payload checksums when available
|
||||
};
|
||||
|
||||
using (var archive = ZipArchive.OpenArchive("file.zip"))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output", options);
|
||||
}
|
||||
```
|
||||
|
||||
`CheckCrc` validates archive-level payload checksums when the format stores reliable metadata, such as ZIP CRC32 values. Formats without payload checksums skip this validation. Decompressor integrity checks that are required to decode a stream may still fail even when `CheckCrc` is disabled.
|
||||
|
||||
### Options matrix
|
||||
|
||||
```text
|
||||
ReaderOptions: open-time behavior (password, encoding, stream ownership)
|
||||
ExtractionOptions: extract-time behavior (overwrite, paths, timestamps, attributes, symlinks)
|
||||
WriterOptions: write-time behavior (compression type/level, encoding, stream ownership)
|
||||
ZipWriterEntryOptions: per-entry ZIP overrides (compression, level, timestamps, comments, zip64)
|
||||
```
|
||||
|
||||
### Compression Providers
|
||||
|
||||
`ReaderOptions` and `WriterOptions` expose a `Providers` registry that controls which `ICompressionProvider` implementations are used for each `CompressionType`. The registry defaults to `CompressionProviderRegistry.Default`, so you only need to set it if you want to swap in a custom provider (for example the `SystemGZipCompressionProvider` or `SystemDeflateCompressionProvider`). The selected registry is honored by Reader/Writer APIs, Archive APIs, and async entry-stream extraction paths.
|
||||
|
||||
```csharp
|
||||
var registry = CompressionProviderRegistry.Default
|
||||
.With(new SystemGZipCompressionProvider())
|
||||
.With(new SystemDeflateCompressionProvider());
|
||||
var readerOptions = ReaderOptions.ForFilePath.WithProviders(registry);
|
||||
var writerOptions = new WriterOptions(CompressionType.GZip)
|
||||
{
|
||||
CompressionLevel = 6,
|
||||
}.WithProviders(registry);
|
||||
|
||||
using var reader = ReaderFactory.OpenReader(input, readerOptions);
|
||||
using var writer = WriterFactory.OpenWriter(output, ArchiveType.GZip, writerOptions);
|
||||
```
|
||||
|
||||
Registry API summary:
|
||||
|
||||
```text
|
||||
CompressionProviderRegistry.Default: built-in provider registry
|
||||
CompressionProviderRegistry.Empty: empty registry, primarily useful for tests
|
||||
With(provider): returns a new registry with that provider added or replaced
|
||||
GetProvider(type): returns the registered ICompressionProvider, or null
|
||||
CreateCompressStream(...): creates a compression stream or throws if unsupported
|
||||
CreateDecompressStream(...): creates a decompression stream or throws if unsupported
|
||||
CreateCompressStreamAsync(...): async stream creation counterpart
|
||||
CreateDecompressStreamAsync(...): async stream creation counterpart
|
||||
GetCompressingProvider(type): returns ICompressionProviderHooks when available
|
||||
```
|
||||
|
||||
`ICompressionProvider` implementations declare their `CompressionType`, whether they support compression/decompression, and create sync/async streams. For simple providers, derive from `CompressionProviderBase`; it supplies default async implementations that delegate to the synchronous methods. For read-only codecs, derive from `DecompressionOnlyProviderBase`.
|
||||
|
||||
```csharp
|
||||
public sealed class CustomGZipProvider : CompressionProviderBase
|
||||
{
|
||||
public override CompressionType CompressionType => CompressionType.GZip;
|
||||
public override bool SupportsCompression => true;
|
||||
public override bool SupportsDecompression => true;
|
||||
|
||||
public override Stream CreateCompressStream(Stream destination, int compressionLevel) =>
|
||||
new GZipStream(destination, CompressionMode.Compress, leaveOpen: false);
|
||||
|
||||
public override Stream CreateDecompressStream(Stream source) =>
|
||||
new GZipStream(source, CompressionMode.Decompress, leaveOpen: false);
|
||||
}
|
||||
```
|
||||
|
||||
`CompressionContext` carries metadata that providers may need when creating streams, including `InputSize`, `OutputSize`, `Properties`, `CanSeek`, `FormatOptions`, and `ReaderOptions`. Use `CompressionContext.FromStream(stream)` to populate stream-derived values, `WithReaderOptions(...)` to attach reader metadata, and `ResolveArchiveEncoding()` to get the archive header encoding from the context.
|
||||
|
||||
When a format needs additional initialization/finalization data (LZMA, PPMd, etc.) the registry exposes `GetCompressingProvider` which returns the `ICompressionProviderHooks` contract; the rest of the API continues to flow through `Providers`, including pre/properties/post compression hook data.
|
||||
|
||||
---
|
||||
|
||||
## Compression Types
|
||||
|
||||
### Available Compressions
|
||||
|
||||
```csharp
|
||||
// For creating archives
|
||||
CompressionType.None // No compression (store)
|
||||
CompressionType.GZip // GZip wrapper/combined tar compression
|
||||
CompressionType.Deflate // DEFLATE (default for ZIP/GZip)
|
||||
CompressionType.Deflate64 // Deflate64
|
||||
CompressionType.BZip2 // BZip2
|
||||
CompressionType.LZMA // LZMA (for 7Zip, LZip, XZ)
|
||||
CompressionType.LZMA2 // LZMA2 (for 7Zip/XZ)
|
||||
CompressionType.BCJ // 7Zip branch converter filter
|
||||
CompressionType.BCJ2 // 7Zip branch converter filter
|
||||
CompressionType.LZip // LZip wrapper/combined tar compression
|
||||
CompressionType.Xz // XZ wrapper/combined tar compression
|
||||
CompressionType.PPMd // PPMd (for ZIP)
|
||||
CompressionType.Rar // RAR compression (read-only)
|
||||
CompressionType.Lzw // LZW, including .Z/tar.Z reading
|
||||
CompressionType.Shrink // ZIP shrink (read-only)
|
||||
CompressionType.Reduce1 // ZIP reduce methods (read-only)
|
||||
CompressionType.Reduce2
|
||||
CompressionType.Reduce3
|
||||
CompressionType.Reduce4
|
||||
CompressionType.Explode // ZIP implode/explode (read-only)
|
||||
CompressionType.Squeezed // ARC/ARJ legacy compression (read-only)
|
||||
CompressionType.Packed // ARC legacy compression (read-only)
|
||||
CompressionType.Crunched // ARC legacy compression (read-only)
|
||||
CompressionType.Squashed // Legacy compression (read-only)
|
||||
CompressionType.Crushed // Legacy compression (read-only)
|
||||
CompressionType.Distilled // Legacy compression (read-only)
|
||||
CompressionType.ZStandard // ZStandard
|
||||
CompressionType.ArjLZ77 // ARJ compression (read-only)
|
||||
CompressionType.AceLZ77 // ACE compression (read-only)
|
||||
CompressionType.Unknown
|
||||
|
||||
// For Tar archives with compression
|
||||
// Use WriterFactory to create compressed tar archives
|
||||
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Tar, CompressionType.GZip)) // Tar.GZip
|
||||
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Tar, CompressionType.BZip2)) // Tar.BZip2
|
||||
```
|
||||
|
||||
### Archive Types
|
||||
|
||||
```csharp
|
||||
ArchiveType.Zip
|
||||
ArchiveType.Tar
|
||||
ArchiveType.GZip
|
||||
ArchiveType.BZip2
|
||||
ArchiveType.Rar
|
||||
ArchiveType.SevenZip
|
||||
ArchiveType.Arc
|
||||
ArchiveType.Arj
|
||||
ArchiveType.Ace
|
||||
ArchiveType.Lzw
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patterns & Examples
|
||||
|
||||
### Extract with Error Handling
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
using (var archive = ZipArchive.OpenArchive("archive.zip",
|
||||
ReaderOptions.ForEncryptedArchive("password")))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
}
|
||||
catch (PasswordRequiredException)
|
||||
{
|
||||
Console.WriteLine("Password required");
|
||||
}
|
||||
catch (InvalidArchiveException)
|
||||
{
|
||||
Console.WriteLine("Archive is invalid");
|
||||
}
|
||||
catch (SharpCompressException ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
### Extract with Progress
|
||||
|
||||
```csharp
|
||||
var progress = new Progress<ProgressReport>(report =>
|
||||
{
|
||||
Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%");
|
||||
});
|
||||
|
||||
var options = ReaderOptions.ForFilePath.WithProgress(progress);
|
||||
using (var archive = ZipArchive.OpenArchive("archive.zip", options))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
```
|
||||
|
||||
### Async Extract with Cancellation
|
||||
|
||||
```csharp
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(TimeSpan.FromMinutes(5));
|
||||
|
||||
try
|
||||
{
|
||||
await using (var archive = await ZipArchive.OpenAsyncArchive("archive.zip"))
|
||||
{
|
||||
await archive.WriteToDirectoryAsync(
|
||||
@"C:\output",
|
||||
cancellationToken: cts.Token
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine("Extraction cancelled");
|
||||
}
|
||||
```
|
||||
|
||||
### Create with Custom Compression
|
||||
|
||||
```csharp
|
||||
using (var archive = ZipArchive.CreateArchive())
|
||||
{
|
||||
archive.AddAllFromDirectory(@"D:\source");
|
||||
|
||||
// Fastest
|
||||
archive.SaveTo("fast.zip", new WriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
CompressionLevel = 1
|
||||
});
|
||||
|
||||
// Balanced (default)
|
||||
archive.SaveTo("normal.zip", CompressionType.Deflate);
|
||||
|
||||
// Best compression
|
||||
archive.SaveTo("best.zip", new WriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
CompressionLevel = 9
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Stream Processing (No File I/O)
|
||||
|
||||
```csharp
|
||||
using (var outputStream = new MemoryStream())
|
||||
using (var archive = ZipArchive.CreateArchive())
|
||||
{
|
||||
// Add content from memory
|
||||
using (var contentStream = new MemoryStream(Encoding.UTF8.GetBytes("Hello")))
|
||||
{
|
||||
archive.AddEntry("file.txt", contentStream);
|
||||
}
|
||||
|
||||
// Save to memory
|
||||
archive.SaveTo(outputStream, CompressionType.Deflate);
|
||||
|
||||
// Get bytes
|
||||
byte[] archiveBytes = outputStream.ToArray();
|
||||
}
|
||||
```
|
||||
|
||||
### Buffered Forward-Only Streams
|
||||
|
||||
`SharpCompressStream` can wrap streams with buffering for forward-only scenarios:
|
||||
|
||||
```csharp
|
||||
// Wrap a non-seekable stream with buffering
|
||||
using (var bufferedStream = new SharpCompressStream(rawStream))
|
||||
{
|
||||
// Provides ring buffer functionality for reading ahead
|
||||
// and seeking within buffered data
|
||||
using (var reader = ReaderFactory.OpenReader(bufferedStream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
reader.WriteEntryToDirectory(@"C:\output");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Useful for:
|
||||
- Non-seekable streams (network streams, pipes)
|
||||
- Forward-only reading with limited look-ahead
|
||||
- Buffering unbuffered streams for better performance
|
||||
|
||||
### Extract Specific Files
|
||||
|
||||
```csharp
|
||||
using (var archive = ZipArchive.OpenArchive("archive.zip"))
|
||||
{
|
||||
var filesToExtract = new[] { "file1.txt", "file2.txt" };
|
||||
|
||||
foreach (var entry in archive.Entries.Where(e => filesToExtract.Contains(e.Key)))
|
||||
{
|
||||
entry.WriteToFile(@"C:\output\" + entry.Key);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### List Archive Contents
|
||||
|
||||
```csharp
|
||||
using (var archive = ZipArchive.OpenArchive("archive.zip"))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
Console.WriteLine($"[DIR] {entry.Key}");
|
||||
else
|
||||
Console.WriteLine($"[FILE] {entry.Key} ({entry.Size} bytes)");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### ✗ Wrong - Stream not disposed
|
||||
|
||||
```csharp
|
||||
var stream = File.OpenRead("archive.zip");
|
||||
var archive = ZipArchive.OpenArchive(stream);
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
// stream not disposed - leaked resource
|
||||
```
|
||||
|
||||
### ✓ Correct - Using blocks
|
||||
|
||||
```csharp
|
||||
using (var stream = File.OpenRead("archive.zip"))
|
||||
using (var archive = ZipArchive.OpenArchive(stream))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
// Both properly disposed
|
||||
```
|
||||
|
||||
### ✗ Wrong - Mixing API styles
|
||||
|
||||
```csharp
|
||||
// Loading entire archive then iterating
|
||||
using (var archive = ZipArchive.OpenArchive("large.zip"))
|
||||
{
|
||||
var entries = archive.Entries.ToList(); // Loads all in memory
|
||||
foreach (var e in entries)
|
||||
{
|
||||
e.WriteToFile(...); // Then extracts each
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ✓ Correct - Use Reader for large files
|
||||
|
||||
```csharp
|
||||
// Streaming iteration
|
||||
using (var stream = File.OpenRead("large.zip"))
|
||||
using (var reader = ReaderFactory.OpenReader(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
reader.WriteEntryToDirectory(@"C:\output");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [USAGE.md](USAGE.md) - Complete code examples
|
||||
- [FORMATS.md](FORMATS.md) - Supported formats
|
||||
- [PERFORMANCE.md](PERFORMANCE.md) - API selection guide
|
||||
658
docs/ARCHITECTURE.md
Normal file
658
docs/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,658 @@
|
|||
# SharpCompress Architecture Guide
|
||||
|
||||
This guide explains the internal architecture and design patterns of SharpCompress for contributors.
|
||||
|
||||
## Overview
|
||||
|
||||
SharpCompress is organized into three main layers:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ User-Facing APIs (Top Layer) │
|
||||
│ Archive, Reader, Writer Factories │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Format-Specific Implementations │
|
||||
│ ZipArchive, TarReader, GZipWriter, │
|
||||
│ RarArchive, SevenZipArchive, etc. │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Compression & Crypto (Bottom Layer) │
|
||||
│ Deflate, LZMA, BZip2, AES, CRC32 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
### `src/SharpCompress/`
|
||||
|
||||
#### `Archives/` - Archive Implementations
|
||||
Contains `IArchive` implementations for seekable, random-access APIs.
|
||||
|
||||
**Key Files:**
|
||||
- `AbstractArchive.cs` - Base class for all archives
|
||||
- `IArchive.cs` - Archive interface definition
|
||||
- `ArchiveFactory.cs` - Factory for opening archives
|
||||
- Format-specific: `ZipArchive.cs`, `TarArchive.cs`, `RarArchive.cs`, `SevenZipArchive.cs`, `GZipArchive.cs`
|
||||
|
||||
**Use Archive API when:**
|
||||
- Stream is seekable (file, memory)
|
||||
- Need random access to entries
|
||||
- Archive fits in memory
|
||||
- Simplicity is important
|
||||
|
||||
#### `Readers/` - Reader Implementations
|
||||
Contains `IReader` implementations for forward-only, non-seekable APIs.
|
||||
|
||||
**Key Files:**
|
||||
- `AbstractReader.cs` - Base reader class
|
||||
- `IReader.cs` - Reader interface
|
||||
- `ReaderFactory.cs` - Auto-detection factory
|
||||
- `ReaderOptions.cs` - Configuration for readers
|
||||
- Format-specific: `ZipReader.cs`, `TarReader.cs`, `GZipReader.cs`, `RarReader.cs`, etc.
|
||||
|
||||
**Use Reader API when:**
|
||||
- Stream is non-seekable (network, pipe, compressed)
|
||||
- Processing large files
|
||||
- Memory is limited
|
||||
- Forward-only processing is acceptable
|
||||
|
||||
#### `Writers/` - Writer Implementations
|
||||
Contains `IWriter` implementations for forward-only writing.
|
||||
|
||||
**Key Files:**
|
||||
- `AbstractWriter.cs` - Base writer class
|
||||
- `IWriter.cs` - Writer interface
|
||||
- `WriterFactory.cs` - Factory for creating writers
|
||||
- `WriterOptions.cs` - Configuration for writers
|
||||
- Format-specific: `ZipWriter.cs`, `TarWriter.cs`, `GZipWriter.cs`
|
||||
|
||||
#### `Factories/` - Format Detection
|
||||
Factory classes for auto-detecting archive format and creating appropriate readers/writers.
|
||||
|
||||
**Key Files:**
|
||||
- `Factory.cs` - Base factory class
|
||||
- `IFactory.cs` - Factory interface
|
||||
- Format-specific: `ZipFactory.cs`, `TarFactory.cs`, `RarFactory.cs`, etc.
|
||||
|
||||
**How It Works:**
|
||||
1. `ReaderFactory.OpenReader(stream)` probes stream signatures
|
||||
2. Identifies format by magic bytes
|
||||
3. Creates appropriate reader instance
|
||||
4. Returns generic `IReader` interface
|
||||
|
||||
#### `Common/` - Shared Types
|
||||
Common types, options, and enumerations used across formats.
|
||||
|
||||
**Key Files:**
|
||||
- `IEntry.cs` - Entry interface (file within archive)
|
||||
- `Entry.cs` - Entry implementation
|
||||
- `ArchiveType.cs` - Enum for archive formats
|
||||
- `CompressionType.cs` - Enum for compression methods
|
||||
- `ArchiveEncoding.cs` - Character encoding configuration
|
||||
- `IExtractionOptions.cs` - Interface for extraction configuration
|
||||
- `ExtractionOptions.cs` - Extraction behavior options for file extraction APIs
|
||||
- Format-specific headers: `Zip/Headers/`, `Tar/Headers/`, `Rar/Headers/`, etc.
|
||||
|
||||
#### `Compressors/` - Compression Algorithms
|
||||
Low-level compression streams implementing specific algorithms.
|
||||
|
||||
**Algorithms:**
|
||||
- `Deflate/` - DEFLATE compression (Zip default)
|
||||
- `BZip2/` - BZip2 compression
|
||||
- `LZMA/` - LZMA compression (7Zip, XZ, LZip)
|
||||
- `PPMd/` - Prediction by Partial Matching (Zip, 7Zip)
|
||||
- `ZStandard/` - ZStandard compression (decompression only)
|
||||
- `Xz/` - XZ format (decompression only)
|
||||
- `Rar/` - RAR-specific unpacking
|
||||
- `Arj/`, `Arc/`, `Ace/` - Legacy format decompression
|
||||
- `Filters/` - BCJ/BCJ2 filters for executable compression
|
||||
|
||||
**Each Compressor:**
|
||||
- Implements a `Stream` subclass
|
||||
- Provides both compression and decompression
|
||||
- Some are read-only (decompression only)
|
||||
|
||||
#### `Crypto/` - Encryption & Hashing
|
||||
Cryptographic functions and stream wrappers.
|
||||
|
||||
**Key Files:**
|
||||
- `Crc32Stream.cs` - CRC32 calculation wrapper
|
||||
- `BlockTransformer.cs` - Block cipher transformations
|
||||
- AES, PKWare, WinZip encryption implementations
|
||||
|
||||
#### `IO/` - Stream Utilities
|
||||
Stream wrappers and utilities.
|
||||
|
||||
**Key Classes:**
|
||||
- `SharpCompressStream` - Base stream class
|
||||
- `ProgressReportingStream` - Progress tracking wrapper
|
||||
- `MarkingBinaryReader` - Binary reader with position marks
|
||||
- `BufferedSubStream` - Buffered read-only substream
|
||||
- `ReadOnlySubStream` - Read-only view of parent stream
|
||||
- `NonDisposingStream` - Prevents wrapped stream disposal
|
||||
|
||||
---
|
||||
|
||||
## Design Patterns
|
||||
|
||||
### 1. Factory Pattern
|
||||
|
||||
**Purpose:** Auto-detect format and create appropriate reader/writer.
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
// User calls factory
|
||||
using (var reader = ReaderFactory.OpenReader(stream)) // Returns IReader
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
// Process entry
|
||||
}
|
||||
}
|
||||
|
||||
// Behind the scenes:
|
||||
// 1. Factory.Open() probes stream signatures
|
||||
// 2. Detects format (Zip, Tar, Rar, etc.)
|
||||
// 3. Creates appropriate reader (ZipReader, TarReader, etc.)
|
||||
// 4. Returns as generic IReader interface
|
||||
```
|
||||
|
||||
**Files:**
|
||||
- `src/SharpCompress/Factories/ReaderFactory.cs`
|
||||
- `src/SharpCompress/Factories/WriterFactory.cs`
|
||||
- `src/SharpCompress/Factories/ArchiveFactory.cs`
|
||||
|
||||
### 2. Strategy Pattern
|
||||
|
||||
**Purpose:** Encapsulate compression algorithms as swappable strategies.
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
// Different compression strategies
|
||||
CompressionType.Deflate // DEFLATE
|
||||
CompressionType.BZip2 // BZip2
|
||||
CompressionType.LZMA // LZMA
|
||||
CompressionType.PPMd // PPMd
|
||||
|
||||
// Writer uses strategy pattern
|
||||
var archive = ZipArchive.CreateArchive();
|
||||
archive.SaveTo("output.zip", CompressionType.Deflate); // Use Deflate
|
||||
archive.SaveTo("output.bz2", CompressionType.BZip2); // Use BZip2
|
||||
```
|
||||
|
||||
**Files:**
|
||||
- `src/SharpCompress/Compressors/` - Strategy implementations
|
||||
|
||||
### 3. Decorator Pattern
|
||||
|
||||
**Purpose:** Wrap streams with additional functionality.
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
// Progress reporting decorator
|
||||
var progressStream = new ProgressReportingStream(baseStream, progressReporter);
|
||||
progressStream.Read(buffer, 0, buffer.Length); // Reports progress
|
||||
|
||||
// Non-disposing decorator
|
||||
var nonDisposingStream = new NonDisposingStream(baseStream);
|
||||
using (var compressor = new DeflateStream(nonDisposingStream))
|
||||
{
|
||||
// baseStream won't be disposed when compressor is disposed
|
||||
}
|
||||
```
|
||||
|
||||
**Files:**
|
||||
- `src/SharpCompress/IO/ProgressReportingStream.cs`
|
||||
- `src/SharpCompress/IO/NonDisposingStream.cs`
|
||||
|
||||
### 4. Template Method Pattern
|
||||
|
||||
**Purpose:** Define algorithm skeleton in base class, let subclasses fill details.
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
// AbstractArchive defines common archive operations
|
||||
public abstract class AbstractArchive : IArchive
|
||||
{
|
||||
// Template methods
|
||||
public virtual void WriteToDirectory(string destinationDirectory)
|
||||
{
|
||||
// Common extraction logic
|
||||
foreach (var entry in Entries)
|
||||
{
|
||||
// Call subclass method
|
||||
entry.WriteToFile(destinationPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Subclasses override format-specific details
|
||||
protected abstract Entry CreateEntry(EntryData data);
|
||||
}
|
||||
```
|
||||
|
||||
**Files:**
|
||||
- `src/SharpCompress/Archives/AbstractArchive.cs`
|
||||
- `src/SharpCompress/Readers/AbstractReader.cs`
|
||||
|
||||
### 5. Iterator Pattern
|
||||
|
||||
**Purpose:** Provide sequential access to entries.
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
// Archive API - provides collection
|
||||
IEnumerable<IEntry> entries = archive.Entries;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
// Random access - entries already in memory
|
||||
}
|
||||
|
||||
// Reader API - provides iterator
|
||||
IReader reader = ReaderFactory.OpenReader(stream);
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
// Forward-only iteration - one entry at a time
|
||||
var entry = reader.Entry;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Interfaces
|
||||
|
||||
### IArchive - Random Access API
|
||||
|
||||
```csharp
|
||||
public interface IArchive : IDisposable
|
||||
{
|
||||
IEnumerable<IEntry> Entries { get; }
|
||||
|
||||
void WriteToDirectory(string destinationDirectory);
|
||||
|
||||
IEntry FirstOrDefault(Func<IEntry, bool> predicate);
|
||||
|
||||
// ... format-specific methods
|
||||
}
|
||||
```
|
||||
|
||||
**Implementations:** `ZipArchive`, `TarArchive`, `RarArchive`, `SevenZipArchive`, `GZipArchive`
|
||||
|
||||
### IReader - Forward-Only API
|
||||
|
||||
```csharp
|
||||
public interface IReader : IDisposable
|
||||
{
|
||||
IEntry Entry { get; }
|
||||
|
||||
bool MoveToNextEntry();
|
||||
|
||||
void WriteEntryToDirectory(string destinationDirectory);
|
||||
|
||||
Stream OpenEntryStream();
|
||||
|
||||
// ... async variants
|
||||
}
|
||||
```
|
||||
|
||||
**Implementations:** `ZipReader`, `TarReader`, `RarReader`, `GZipReader`, etc.
|
||||
|
||||
### IWriter - Writing API
|
||||
|
||||
```csharp
|
||||
public interface IWriter : IDisposable
|
||||
{
|
||||
void Write(string entryPath, Stream source,
|
||||
DateTime? modificationTime = null);
|
||||
|
||||
void WriteAll(string sourceDirectory, string searchPattern,
|
||||
SearchOption searchOption);
|
||||
|
||||
// ... async variants
|
||||
}
|
||||
```
|
||||
|
||||
**Implementations:** `ZipWriter`, `TarWriter`, `GZipWriter`
|
||||
|
||||
### IEntry - Archive Entry
|
||||
|
||||
```csharp
|
||||
public interface IEntry
|
||||
{
|
||||
string Key { get; }
|
||||
uint Size { get; }
|
||||
uint CompressedSize { get; }
|
||||
bool IsDirectory { get; }
|
||||
DateTime? LastModifiedTime { get; }
|
||||
CompressionType CompressionType { get; }
|
||||
|
||||
void WriteToFile(string fullPath);
|
||||
void WriteToStream(Stream destinationStream);
|
||||
Stream OpenEntryStream();
|
||||
|
||||
// ... async variants
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding Support for a New Format
|
||||
|
||||
### Step 1: Understand the Format
|
||||
- Research format specification
|
||||
- Understand compression/encryption used
|
||||
- Study existing similar formats in codebase
|
||||
|
||||
### Step 2: Create Format Structure Classes
|
||||
|
||||
**Create:** `src/SharpCompress/Common/NewFormat/`
|
||||
|
||||
```csharp
|
||||
// Headers and data structures
|
||||
public class NewFormatHeader
|
||||
{
|
||||
public uint Magic { get; set; }
|
||||
public ushort Version { get; set; }
|
||||
// ... other fields
|
||||
|
||||
public static NewFormatHeader Read(BinaryReader reader)
|
||||
{
|
||||
// Deserialize from binary
|
||||
}
|
||||
}
|
||||
|
||||
public class NewFormatEntry
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public uint CompressedSize { get; set; }
|
||||
public uint UncompressedSize { get; set; }
|
||||
// ... other fields
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create Archive Implementation
|
||||
|
||||
**Create:** `src/SharpCompress/Archives/NewFormat/NewFormatArchive.cs`
|
||||
|
||||
```csharp
|
||||
public class NewFormatArchive : AbstractArchive
|
||||
{
|
||||
private NewFormatHeader _header;
|
||||
private List<NewFormatEntry> _entries;
|
||||
|
||||
public static NewFormatArchive OpenArchive(Stream stream)
|
||||
{
|
||||
var archive = new NewFormatArchive();
|
||||
archive._header = NewFormatHeader.Read(stream);
|
||||
archive.LoadEntries(stream);
|
||||
return archive;
|
||||
}
|
||||
|
||||
public override IEnumerable<IEntry> Entries => _entries.Select(e => new Entry(e));
|
||||
|
||||
protected override Stream OpenEntryStream(Entry entry)
|
||||
{
|
||||
// Return decompressed stream for entry
|
||||
}
|
||||
|
||||
// ... other abstract method implementations
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Create Reader Implementation
|
||||
|
||||
**Create:** `src/SharpCompress/Readers/NewFormat/NewFormatReader.cs`
|
||||
|
||||
```csharp
|
||||
public class NewFormatReader : AbstractReader
|
||||
{
|
||||
private NewFormatHeader _header;
|
||||
private BinaryReader _reader;
|
||||
|
||||
public NewFormatReader(Stream stream)
|
||||
{
|
||||
_reader = new BinaryReader(stream);
|
||||
_header = NewFormatHeader.Read(_reader);
|
||||
}
|
||||
|
||||
public override bool MoveToNextEntry()
|
||||
{
|
||||
// Read next entry header
|
||||
if (!_reader.BaseStream.CanRead) return false;
|
||||
|
||||
var entryData = NewFormatEntry.Read(_reader);
|
||||
// ... set this.Entry
|
||||
return entryData != null;
|
||||
}
|
||||
|
||||
// ... other abstract method implementations
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Create Factory
|
||||
|
||||
**Create:** `src/SharpCompress/Factories/NewFormatFactory.cs`
|
||||
|
||||
```csharp
|
||||
public class NewFormatFactory : Factory, IArchiveFactory, IReaderFactory
|
||||
{
|
||||
// Archive format magic bytes (signature)
|
||||
private static readonly byte[] NewFormatSignature = new byte[] { 0x4E, 0x46 }; // "NF"
|
||||
|
||||
public static NewFormatFactory Instance { get; } = new();
|
||||
|
||||
public IArchive CreateArchive(Stream stream)
|
||||
=> NewFormatArchive.OpenArchive(stream);
|
||||
|
||||
public IReader CreateReader(Stream stream, ReaderOptions options)
|
||||
=> new NewFormatReader(stream) { Options = options };
|
||||
|
||||
public bool Matches(Stream stream, ReadOnlySpan<byte> signature)
|
||||
=> signature.StartsWith(NewFormatSignature);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Register Factory
|
||||
|
||||
**Update:** `src/SharpCompress/Factories/ArchiveFactory.cs`
|
||||
|
||||
```csharp
|
||||
private static readonly IFactory[] Factories =
|
||||
{
|
||||
ZipFactory.Instance,
|
||||
TarFactory.Instance,
|
||||
RarFactory.Instance,
|
||||
SevenZipFactory.Instance,
|
||||
GZipFactory.Instance,
|
||||
NewFormatFactory.Instance, // Add here
|
||||
// ... other factories
|
||||
};
|
||||
```
|
||||
|
||||
### Step 7: Add Tests
|
||||
|
||||
**Create:** `tests/SharpCompress.Test/NewFormat/NewFormatTests.cs`
|
||||
|
||||
```csharp
|
||||
public class NewFormatTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void NewFormat_Extracts_Successfully()
|
||||
{
|
||||
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "archive.newformat");
|
||||
using (var archive = NewFormatArchive.OpenArchive(archivePath))
|
||||
{
|
||||
archive.WriteToDirectory(SCRATCH_FILES_PATH);
|
||||
// Assert extraction
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewFormat_Reader_Works()
|
||||
{
|
||||
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "archive.newformat");
|
||||
using (var stream = File.OpenRead(archivePath))
|
||||
using (var reader = new NewFormatReader(stream))
|
||||
{
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
Assert.NotNull(reader.Entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 8: Add Test Archives
|
||||
|
||||
Place test files in `tests/TestArchives/Archives/NewFormat/` directory.
|
||||
|
||||
### Step 9: Document
|
||||
|
||||
Update `docs/FORMATS.md` with format support information.
|
||||
|
||||
---
|
||||
|
||||
## Compression Algorithm Implementation
|
||||
|
||||
### Creating a New Compression Stream
|
||||
|
||||
**Example:** Creating `CustomStream` for a custom compression algorithm
|
||||
|
||||
```csharp
|
||||
public class CustomStream : Stream
|
||||
{
|
||||
private readonly Stream _baseStream;
|
||||
private readonly bool _leaveOpen;
|
||||
|
||||
public CustomStream(Stream baseStream, bool leaveOpen = false)
|
||||
{
|
||||
_baseStream = baseStream;
|
||||
_leaveOpen = leaveOpen;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
// Decompress data from _baseStream into buffer
|
||||
// Return number of decompressed bytes
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
// Compress data from buffer into _baseStream
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && !_leaveOpen)
|
||||
{
|
||||
_baseStream?.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stream Handling Best Practices
|
||||
|
||||
### Disposal Pattern
|
||||
|
||||
```csharp
|
||||
// Correct: Nested using blocks
|
||||
using (var fileStream = File.OpenRead("archive.zip"))
|
||||
using (var archive = ZipArchive.OpenArchive(fileStream))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
// Both archive and fileStream properly disposed
|
||||
|
||||
// Correct: Using with options
|
||||
var options = new ReaderOptions { LeaveStreamOpen = true };
|
||||
var stream = File.OpenRead("archive.zip");
|
||||
using (var archive = ZipArchive.OpenArchive(stream, options))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
stream.Dispose(); // Manually dispose if LeaveStreamOpen = true
|
||||
```
|
||||
|
||||
### NonDisposingStream Wrapper
|
||||
|
||||
```csharp
|
||||
// Prevent unwanted stream closure
|
||||
var baseStream = File.OpenRead("data.bin");
|
||||
var nonDisposing = new NonDisposingStream(baseStream);
|
||||
|
||||
using (var compressor = new DeflateStream(nonDisposing))
|
||||
{
|
||||
// Compressor won't close baseStream when disposed
|
||||
}
|
||||
|
||||
// baseStream still usable
|
||||
baseStream.Position = 0; // Works
|
||||
baseStream.Dispose(); // Manual disposal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memory Efficiency
|
||||
|
||||
1. **Avoid loading entire archive in memory** - Use Reader API for large files
|
||||
2. **Process entries sequentially** - Especially for solid archives
|
||||
3. **Use appropriate buffer sizes** - Larger buffers for network I/O
|
||||
4. **Dispose streams promptly** - Free resources when done
|
||||
|
||||
### Algorithm Selection
|
||||
|
||||
1. **Archive API** - Fast for small archives with random access
|
||||
2. **Reader API** - Efficient for large files or streaming
|
||||
3. **Solid archives** - Sequential extraction much faster
|
||||
4. **Compression levels** - Trade-off between speed and size
|
||||
|
||||
---
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Test Coverage
|
||||
|
||||
1. **Happy path** - Normal extraction works
|
||||
2. **Edge cases** - Empty archives, single file, many files
|
||||
3. **Corrupted data** - Handle gracefully
|
||||
4. **Error cases** - Missing passwords, unsupported compression
|
||||
5. **Async operations** - Both sync and async code paths
|
||||
|
||||
### Test Archives
|
||||
|
||||
- Use `tests/TestArchives/` for test data
|
||||
- Create format-specific subdirectories
|
||||
- Include encrypted, corrupted, and edge case archives
|
||||
- Don't recreate existing archives
|
||||
|
||||
### Test Patterns
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void Archive_Extraction_Works()
|
||||
{
|
||||
// Arrange
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "test.zip");
|
||||
|
||||
// Act
|
||||
using (var archive = ZipArchive.OpenArchive(testArchive))
|
||||
{
|
||||
archive.WriteToDirectory(SCRATCH_FILES_PATH);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.True(File.Exists(Path.Combine(SCRATCH_FILES_PATH, "file.txt")));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [AGENTS.md](../AGENTS.md) - Development guidelines
|
||||
- [FORMATS.md](FORMATS.md) - Supported formats
|
||||
603
docs/ENCODING.md
Normal file
603
docs/ENCODING.md
Normal file
|
|
@ -0,0 +1,603 @@
|
|||
# SharpCompress Character Encoding Guide
|
||||
|
||||
This guide explains how SharpCompress handles character encoding for archive entries (filenames, comments, etc.).
|
||||
|
||||
## Overview
|
||||
|
||||
Most archive formats store filenames and metadata as bytes. SharpCompress must convert these bytes to strings using the appropriate character encoding.
|
||||
|
||||
**Common Problem:** Archives created on systems with non-UTF8 encodings (especially Japanese, Chinese systems) appear with corrupted filenames when extracted on systems that assume UTF8.
|
||||
|
||||
---
|
||||
|
||||
## ArchiveEncoding Class
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```csharp
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
// Configure encoding using fluent factory method (preferred)
|
||||
var options = ReaderOptions.ForEncoding(
|
||||
new ArchiveEncoding { Default = Encoding.GetEncoding(932) }); // cp932 for Japanese
|
||||
|
||||
using (var archive = ZipArchive.OpenArchive("japanese.zip", options))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
Console.WriteLine(entry.Key); // Now shows correct characters
|
||||
}
|
||||
}
|
||||
|
||||
// Alternative: object initializer
|
||||
var options2 = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
|
||||
};
|
||||
```
|
||||
|
||||
### ArchiveEncoding Properties
|
||||
|
||||
| Property | Purpose |
|
||||
|----------|---------|
|
||||
| `Default` | Default encoding for filenames (fallback) |
|
||||
| `CustomDecoder` | Custom decoding function for special cases |
|
||||
|
||||
### Setting for Different APIs
|
||||
|
||||
**Archive API:**
|
||||
```csharp
|
||||
var options = ReaderOptions.ForEncoding(
|
||||
new ArchiveEncoding { Default = Encoding.GetEncoding(932) });
|
||||
using (var archive = ZipArchive.OpenArchive("file.zip", options))
|
||||
{
|
||||
// Use archive with correct encoding
|
||||
}
|
||||
```
|
||||
|
||||
**Reader API:**
|
||||
```csharp
|
||||
var options = ReaderOptions.ForEncoding(
|
||||
new ArchiveEncoding { Default = Encoding.GetEncoding(932) });
|
||||
using (var stream = File.OpenRead("file.zip"))
|
||||
using (var reader = ReaderFactory.OpenReader(stream, options))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
// Filenames decoded correctly
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Encodings
|
||||
|
||||
### Asian Encodings
|
||||
|
||||
#### cp932 (Japanese)
|
||||
```csharp
|
||||
// Windows-31J, Shift-JIS variant used on Japanese Windows
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding(932)
|
||||
}
|
||||
};
|
||||
using (var archive = ZipArchive.OpenArchive("japanese.zip", options))
|
||||
{
|
||||
// Correctly decodes Japanese filenames
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Archives from Japanese Windows systems
|
||||
- Files with Japanese characters in names
|
||||
|
||||
#### gb2312 (Simplified Chinese)
|
||||
```csharp
|
||||
// Simplified Chinese
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("gb2312")
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### gbk (Extended Simplified Chinese)
|
||||
```csharp
|
||||
// Extended Simplified Chinese (more characters than gb2312)
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("gbk")
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### big5 (Traditional Chinese)
|
||||
```csharp
|
||||
// Traditional Chinese (Taiwan, Hong Kong)
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("big5")
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### euc-jp (Japanese, Unix)
|
||||
```csharp
|
||||
// Extended Unix Code for Japanese
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("eucjp")
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### euc-kr (Korean)
|
||||
```csharp
|
||||
// Extended Unix Code for Korean
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("euc-kr")
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Western European Encodings
|
||||
|
||||
#### iso-8859-1 (Latin-1)
|
||||
```csharp
|
||||
// Western European (includes accented characters)
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("iso-8859-1")
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Archives from French, German, Spanish systems
|
||||
- Files with accented characters (é, ñ, ü, etc.)
|
||||
|
||||
#### cp1252 (Windows-1252)
|
||||
```csharp
|
||||
// Windows Western European
|
||||
// Very similar to iso-8859-1 but with additional printable characters
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("cp1252")
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Archives from older Western European Windows systems
|
||||
- Files with smart quotes and other Windows-specific characters
|
||||
|
||||
#### iso-8859-15 (Latin-9)
|
||||
```csharp
|
||||
// Western European with Euro symbol support
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("iso-8859-15")
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Cyrillic Encodings
|
||||
|
||||
#### cp1251 (Windows Cyrillic)
|
||||
```csharp
|
||||
// Russian, Serbian, Bulgarian, etc.
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("cp1251")
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### koi8-r (KOI8 Russian)
|
||||
```csharp
|
||||
// Russian (Unix standard)
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("koi8-r")
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### UTF Encodings (Modern)
|
||||
|
||||
#### UTF-8 (Default)
|
||||
```csharp
|
||||
// Modern standard - usually correct for new archives
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.UTF8
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### UTF-16
|
||||
```csharp
|
||||
// Unicode - rarely used in archives
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.Unicode
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Encoding Auto-Detection
|
||||
|
||||
SharpCompress attempts to auto-detect encoding, but this isn't always reliable:
|
||||
|
||||
```csharp
|
||||
// Auto-detection (default)
|
||||
using (var archive = ZipArchive.OpenArchive("file.zip")) // Uses UTF8 by default
|
||||
{
|
||||
// May show corrupted characters if archive uses different encoding
|
||||
}
|
||||
|
||||
// Explicit encoding (more reliable)
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
|
||||
};
|
||||
using (var archive = ZipArchive.OpenArchive("file.zip", options))
|
||||
{
|
||||
// Correct characters displayed
|
||||
}
|
||||
```
|
||||
|
||||
### When Manual Override is Needed
|
||||
|
||||
| Situation | Solution |
|
||||
|-----------|----------|
|
||||
| Archive shows corrupted characters | Specify the encoding explicitly |
|
||||
| Archives from specific region | Use that region's encoding |
|
||||
| Mixed encodings in archive | Use CustomDecoder |
|
||||
| Testing with international files | Try different encodings |
|
||||
|
||||
---
|
||||
|
||||
## Custom Decoder
|
||||
|
||||
For complex scenarios where a single encoding isn't sufficient:
|
||||
|
||||
### Basic Custom Decoder
|
||||
|
||||
```csharp
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
CustomDecoder = (data, offset, length) =>
|
||||
{
|
||||
// Custom decoding logic
|
||||
var bytes = new byte[length];
|
||||
Array.Copy(data, offset, bytes, 0, length);
|
||||
|
||||
// Try UTF8 first
|
||||
try
|
||||
{
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback to cp932 if UTF8 fails
|
||||
return Encoding.GetEncoding(932).GetString(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
using (var archive = ZipArchive.OpenArchive("mixed.zip", options))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
Console.WriteLine(entry.Key); // Uses custom decoder
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced: Detect Encoding by Content
|
||||
|
||||
```csharp
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
CustomDecoder = DetectAndDecode
|
||||
}
|
||||
};
|
||||
|
||||
private static string DetectAndDecode(byte[] data, int offset, int length)
|
||||
{
|
||||
var bytes = new byte[length];
|
||||
Array.Copy(data, offset, bytes, 0, length);
|
||||
|
||||
// Try UTF8 (most modern archives)
|
||||
try
|
||||
{
|
||||
var str = Encoding.UTF8.GetString(bytes);
|
||||
// Verify it decoded correctly (no replacement characters)
|
||||
if (!str.Contains('\uFFFD'))
|
||||
return str;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Try cp932 (Japanese)
|
||||
try
|
||||
{
|
||||
var str = Encoding.GetEncoding(932).GetString(bytes);
|
||||
if (!str.Contains('\uFFFD'))
|
||||
return str;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Fallback to iso-8859-1 (always succeeds)
|
||||
return Encoding.GetEncoding("iso-8859-1").GetString(bytes);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Extract Archive with Japanese Filenames
|
||||
|
||||
```csharp
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding(932) // cp932
|
||||
}
|
||||
};
|
||||
|
||||
using (var archive = ZipArchive.OpenArchive("japanese_files.zip", options))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
// Files extracted with correct Japanese names
|
||||
```
|
||||
|
||||
### Extract Archive with Western European Filenames
|
||||
|
||||
```csharp
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("iso-8859-1")
|
||||
}
|
||||
};
|
||||
|
||||
using (var archive = ZipArchive.OpenArchive("french_files.zip", options))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
// Accented characters (é, è, ê, etc.) display correctly
|
||||
```
|
||||
|
||||
### Extract Archive with Chinese Filenames
|
||||
|
||||
```csharp
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("gbk") // Simplified Chinese
|
||||
}
|
||||
};
|
||||
|
||||
using (var archive = ZipArchive.OpenArchive("chinese_files.zip", options))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
```
|
||||
|
||||
### Extract Archive with Russian Filenames
|
||||
|
||||
```csharp
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("cp1251") // Windows Cyrillic
|
||||
}
|
||||
};
|
||||
|
||||
using (var archive = ZipArchive.OpenArchive("russian_files.zip", options))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
```
|
||||
|
||||
### Reader API with Encoding
|
||||
|
||||
```csharp
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding(932)
|
||||
}
|
||||
};
|
||||
|
||||
using (var stream = File.OpenRead("japanese.zip"))
|
||||
using (var reader = ReaderFactory.OpenReader(stream, options))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Console.WriteLine(reader.Entry.Key); // Correct characters
|
||||
reader.WriteEntryToDirectory(@"C:\output");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating Archives with Correct Encoding
|
||||
|
||||
When creating archives, SharpCompress uses UTF8 by default (recommended):
|
||||
|
||||
```csharp
|
||||
// Create with UTF8 (default, recommended)
|
||||
using (var archive = ZipArchive.CreateArchive())
|
||||
{
|
||||
archive.AddAllFromDirectory(@"D:\my_files");
|
||||
archive.SaveTo("output.zip", CompressionType.Deflate);
|
||||
// Archives created with UTF8 encoding
|
||||
}
|
||||
```
|
||||
|
||||
If you need to create archives for systems that expect specific encodings:
|
||||
|
||||
```csharp
|
||||
// Note: SharpCompress Writer API uses UTF8 for encoding
|
||||
// To create archives with other encodings, consider:
|
||||
// 1. Let users on those systems create archives
|
||||
// 2. Use system tools (7-Zip, WinRAR) with desired encoding
|
||||
// 3. Post-process archives if absolutely necessary
|
||||
|
||||
// For now, recommend modern UTF8-based archives
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Encoding Issues
|
||||
|
||||
### Filenames Show Question Marks (?)
|
||||
|
||||
```
|
||||
✗ Wrong encoding detected
|
||||
test文件.txt → test???.txt
|
||||
```
|
||||
|
||||
**Solution:** Specify correct encoding explicitly
|
||||
|
||||
```csharp
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
Default = Encoding.GetEncoding("gbk") // Try different encodings
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Filenames Show Replacement Character ()
|
||||
|
||||
```
|
||||
✗ Invalid bytes for selected encoding
|
||||
café.txt → caf.txt
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
1. Try a different encoding (see Common Encodings table)
|
||||
2. Use CustomDecoder with fallback encoding
|
||||
3. Archive might be corrupted
|
||||
|
||||
### Mixed Encodings in Single Archive
|
||||
|
||||
```csharp
|
||||
// Use CustomDecoder to handle mixed encodings
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
ArchiveEncoding = new ArchiveEncoding
|
||||
{
|
||||
CustomDecoder = (data, offset, length) =>
|
||||
{
|
||||
// Try multiple encodings in priority order
|
||||
var bytes = new byte[length];
|
||||
Array.Copy(data, offset, bytes, 0, length);
|
||||
|
||||
foreach (var encoding in new[]
|
||||
{
|
||||
Encoding.UTF8,
|
||||
Encoding.GetEncoding(932),
|
||||
Encoding.GetEncoding("iso-8859-1")
|
||||
})
|
||||
{
|
||||
try
|
||||
{
|
||||
var str = encoding.GetString(bytes);
|
||||
if (!str.Contains('\uFFFD'))
|
||||
return str;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Final fallback
|
||||
return Encoding.GetEncoding("iso-8859-1").GetString(bytes);
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Encoding Reference Table
|
||||
|
||||
| Encoding | Code | Use Case |
|
||||
|----------|------|----------|
|
||||
| UTF-8 | (default) | Modern archives, recommended |
|
||||
| cp932 | 932 | Japanese Windows |
|
||||
| gb2312 | "gb2312" | Simplified Chinese |
|
||||
| gbk | "gbk" | Extended Simplified Chinese |
|
||||
| big5 | "big5" | Traditional Chinese |
|
||||
| iso-8859-1 | "iso-8859-1" | Western European |
|
||||
| cp1252 | "cp1252" | Windows Western European |
|
||||
| cp1251 | "cp1251" | Russian/Cyrillic |
|
||||
| euc-jp | "euc-jp" | Japanese Unix |
|
||||
| euc-kr | "euc-kr" | Korean |
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use UTF-8 for new archives** - Most modern systems support it
|
||||
2. **Ask the archive creator** - When receiving archives with corrupted names
|
||||
3. **Provide encoding options** - If your app handles user archives
|
||||
4. **Document your assumption** - Tell users what encoding you're using
|
||||
5. **Test with international files** - Before releasing production code
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [USAGE.md](USAGE.md#extract-zip-which-has-non-utf8-encoded-filenamycp932) - Usage examples
|
||||
94
docs/FORMATS.md
Normal file
94
docs/FORMATS.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Formats
|
||||
|
||||
## Accessing Archives
|
||||
|
||||
* Archive classes allow random access to a seekable stream.
|
||||
* Reader classes allow forward-only reading on a stream.
|
||||
* Writer classes allow forward-only Writing on a stream.
|
||||
|
||||
## Supported Format Table
|
||||
|
||||
| Archive Format | Compression Format(s) | Compress/Decompress | Archive API | Reader API | Writer API |
|
||||
| ------------------ | ------------------------------------------------------------------- | ------------------- | --------------- | ---------- | --------------- |
|
||||
| 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, PPMd, ZStandard, XZ | Both | ZipArchive | ZipReader | ZipWriter |
|
||||
| Tar | None | Both | TarArchive | TarReader | TarWriter (3) |
|
||||
| Tar.GZip | DEFLATE | Both | TarArchive | TarReader | TarWriter (3) |
|
||||
| Tar.BZip2 | BZip2 | Both | TarArchive | TarReader | TarWriter (3) |
|
||||
| Tar.Zstandard | ZStandard | Decompress | TarArchive | TarReader | N/A |
|
||||
| Tar.LZip | LZMA | Both | TarArchive | TarReader | TarWriter (3) |
|
||||
| Tar.XZ | LZMA2 | Decompress | TarArchive | TarReader | N/A |
|
||||
| Tar.LZW | LZW | Decompress | TarArchive | TarReader | N/A |
|
||||
| GZip (single file) | DEFLATE | Both | GZipArchive | GZipReader | GZipWriter |
|
||||
| 7Zip (4) | LZMA, LZMA2, BZip2, PPMd, BCJ, BCJ2, Deflate | Both | SevenZipArchive | N/A | SevenZipWriter |
|
||||
|
||||
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, Shrink, Reduce, Implode, and XZ are only supported for reading. ZStandard is supported for reading and writing. 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 read support is only through the Archive API. Writing is supported through SevenZipWriter for non-solid archives with LZMA/LZMA2 and requires a seekable output stream. 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.
|
||||
|
||||
`ArchiveFactory.GetArchiveInformation(...).SupportsRandomAccess` is `true` when the detected format has an Archive API in this table. It is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW.
|
||||
|
||||
### 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.
|
||||
- ZIP supports ZStandard for reading and writing. Tar.Zstandard support is read/decompress only.
|
||||
|
||||
### 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.
|
||||
|
||||
### XZ Format Notes
|
||||
|
||||
- XZ is a container format around LZMA2-compressed blocks, not just raw LZMA/LZMA2 data.
|
||||
- XZ streams can include per-block integrity checks selected by the stream header: CRC32, CRC64/XZ, SHA-256, or none. SharpCompress validates these checks while reading XZ blocks.
|
||||
- Raw LZMA/LZMA2 decoding does not provide the same container-level CRC validation; it only validates what the decoder format itself can detect, such as malformed compressed data or invalid end markers.
|
||||
|
||||
## 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.
|
||||
|
||||
| Compressor | Compress/Decompress |
|
||||
| --------------- | ------------------- |
|
||||
| BZip2Stream | Both |
|
||||
| GZipStream | Both |
|
||||
| DeflateStream | Both |
|
||||
| Deflate64Stream | Decompress |
|
||||
| LZMAStream | Both |
|
||||
| PPMdStream | Both |
|
||||
| LzwStream | Decompress |
|
||||
| ADCStream | Decompress |
|
||||
| LZipStream | Both |
|
||||
| XZStream | Decompress |
|
||||
| ZStandard CompressionStream/DecompressionStream | Both |
|
||||
|
||||
## Archive Formats vs Compression
|
||||
|
||||
Sometimes the terminology gets mixed.
|
||||
|
||||
### Compression
|
||||
|
||||
DEFLATE, LZMA are pure compression algorithms
|
||||
|
||||
### Formats
|
||||
|
||||
Formats like Zip, 7Zip, Rar are archive formats only. They use other compression methods (e.g. DEFLATE, LZMA, etc.) or propriatory (e.g RAR)
|
||||
|
||||
### Overlap
|
||||
|
||||
GZip, BZip2, LZip, XZ, LZW, and ZStandard are single file or compression wrapper formats. The overlap in the API happens because Tar uses these formats as "compression" methods and the API tries to hide this a bit.
|
||||
|
||||
`ArchiveType` represents archive containers exposed by the high-level APIs (`Rar`, `Zip`, `Tar`, `SevenZip`, `GZip`, `Arc`, `Arj`, `Ace`, and `Lzw`). `XZ` and `ZStandard` are represented as `CompressionType` values rather than `ArchiveType` values.
|
||||
142
docs/OLD_CHANGELOG.md
Normal file
142
docs/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)
|
||||
465
docs/PERFORMANCE.md
Normal file
465
docs/PERFORMANCE.md
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
# SharpCompress Performance Guide
|
||||
|
||||
This guide helps you optimize SharpCompress for performance in various scenarios.
|
||||
|
||||
## API Selection Guide
|
||||
|
||||
### Archive API vs Reader API
|
||||
|
||||
Choose the right API based on your use case:
|
||||
|
||||
| Aspect | Archive API | Reader API |
|
||||
|--------|------------|-----------|
|
||||
| **Stream Type** | Seekable only | Non-seekable OK |
|
||||
| **Memory Usage** | All entries in memory | One entry at a time |
|
||||
| **Random Access** | ✓ Yes | ✗ No |
|
||||
| **Best For** | Small-to-medium archives | Large or streaming data |
|
||||
| **Performance** | Fast for random access | Better for large files |
|
||||
|
||||
### Archive API (Fast for Random Access)
|
||||
|
||||
```csharp
|
||||
// Use when:
|
||||
// - Archive fits in memory
|
||||
// - You need random access to entries
|
||||
// - Stream is seekable (file, MemoryStream)
|
||||
|
||||
using (var archive = ZipArchive.OpenArchive("archive.zip"))
|
||||
{
|
||||
// Random access - all entries available
|
||||
var specific = archive.Entries.FirstOrDefault(e => e.Key == "file.txt");
|
||||
if (specific != null)
|
||||
{
|
||||
specific.WriteToFile(@"C:\output\file.txt");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Characteristics:**
|
||||
- ✓ Instant entry lookup
|
||||
- ✓ Parallel extraction possible
|
||||
- ✗ Entire archive in memory
|
||||
- ✗ Can't process while downloading
|
||||
|
||||
### Reader API (Best for Large Files)
|
||||
|
||||
```csharp
|
||||
// Use when:
|
||||
// - Processing large archives (>100 MB)
|
||||
// - Streaming from network/pipe
|
||||
// - Memory is constrained
|
||||
// - Forward-only processing is acceptable
|
||||
|
||||
using (var stream = File.OpenRead("large.zip"))
|
||||
using (var reader = ReaderFactory.OpenReader(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
// Process one entry at a time
|
||||
reader.WriteEntryToDirectory(@"C:\output");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Characteristics:**
|
||||
- ✓ Minimal memory footprint
|
||||
- ✓ Works with non-seekable streams
|
||||
- ✓ Can process while downloading
|
||||
- ✗ Forward-only (no random access)
|
||||
- ✗ Entry lookup requires iteration
|
||||
|
||||
---
|
||||
|
||||
## Buffer Sizing
|
||||
|
||||
### Understanding Buffers
|
||||
|
||||
SharpCompress uses internal buffers for reading compressed data. Buffer size affects:
|
||||
- **Speed:** Larger buffers = fewer I/O operations = faster
|
||||
- **Memory:** Larger buffers = higher memory usage
|
||||
|
||||
### Recommended Buffer Sizes
|
||||
|
||||
| Scenario | Size | Notes |
|
||||
|----------|------|-------|
|
||||
| Embedded/IoT devices | 4-8 KB | Minimal memory usage |
|
||||
| Memory-constrained | 16-32 KB | Conservative default |
|
||||
| Standard use (default) | 64 KB | Recommended default |
|
||||
| Large file streaming | 256 KB | Better throughput |
|
||||
| High-speed SSD | 512 KB - 1 MB | Maximum throughput |
|
||||
|
||||
### How Buffer Size Affects Performance
|
||||
|
||||
```csharp
|
||||
// SharpCompress manages buffers internally
|
||||
// You can't directly set buffer size, but you can:
|
||||
|
||||
// 1. Use Stream.CopyTo with explicit buffer size
|
||||
using (var entryStream = reader.OpenEntryStream())
|
||||
using (var fileStream = File.Create(@"C:\output\file.txt"))
|
||||
{
|
||||
// 64 KB buffer (default)
|
||||
entryStream.CopyTo(fileStream);
|
||||
|
||||
// Or specify larger buffer for faster copy
|
||||
entryStream.CopyTo(fileStream, bufferSize: 262144); // 256 KB
|
||||
}
|
||||
|
||||
// 2. Use custom buffer for writing
|
||||
using (var entryStream = reader.OpenEntryStream())
|
||||
using (var fileStream = File.Create(@"C:\output\file.txt"))
|
||||
{
|
||||
byte[] buffer = new byte[262144]; // 256 KB
|
||||
int bytesRead;
|
||||
while ((bytesRead = entryStream.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
fileStream.Write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Streaming Large Files
|
||||
|
||||
### Non-Seekable Stream Patterns
|
||||
|
||||
For processing archives from downloads or pipes:
|
||||
|
||||
```csharp
|
||||
// Download stream (non-seekable)
|
||||
using (var httpStream = await httpClient.GetStreamAsync(url))
|
||||
using (var reader = ReaderFactory.OpenReader(httpStream))
|
||||
{
|
||||
// Process entries as they arrive
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
reader.WriteEntryToDirectory(@"C:\output");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Tips:**
|
||||
- Don't try to buffer the entire stream
|
||||
- Process entries immediately
|
||||
- Use async APIs for better responsiveness
|
||||
|
||||
### Download-Then-Extract vs Streaming
|
||||
|
||||
Choose based on your constraints:
|
||||
|
||||
| Approach | When to Use |
|
||||
|----------|------------|
|
||||
| **Download then extract** | Moderate size, need random access |
|
||||
| **Stream during download** | Large files, bandwidth limited, memory constrained |
|
||||
|
||||
```csharp
|
||||
// Download then extract (requires disk space)
|
||||
var archivePath = await DownloadFile(url, @"C:\temp\archive.zip");
|
||||
using (var archive = ZipArchive.OpenArchive(archivePath))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
|
||||
// Stream during download (on-the-fly extraction)
|
||||
using (var httpStream = await httpClient.GetStreamAsync(url))
|
||||
using (var reader = ReaderFactory.OpenReader(httpStream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
reader.WriteEntryToDirectory(@"C:\output");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Solid Archive Optimization
|
||||
|
||||
### Why Solid Archives Are Slow
|
||||
|
||||
Solid archives (Rar, 7Zip) group files together in a single compressed stream:
|
||||
|
||||
```
|
||||
Solid Archive Layout:
|
||||
[Header] [Compressed Stream] [Footer]
|
||||
├─ File1 compressed data
|
||||
├─ File2 compressed data
|
||||
├─ File3 compressed data
|
||||
└─ File4 compressed data
|
||||
```
|
||||
|
||||
Extracting File3 requires decompressing File1 and File2 first.
|
||||
|
||||
### Sequential vs Random Extraction
|
||||
|
||||
**Random Extraction (Slow):**
|
||||
```csharp
|
||||
using (var archive = RarArchive.OpenArchive("solid.rar"))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
entry.WriteToFile(@"C:\output\" + entry.Key); // ✗ Slow!
|
||||
// Each entry triggers full decompression from start
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Sequential Extraction (Fast):**
|
||||
```csharp
|
||||
using (var archive = RarArchive.OpenArchive("solid.rar"))
|
||||
{
|
||||
// Method 1: Use WriteToDirectory (recommended)
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
|
||||
// Method 2: Use ExtractAllEntries
|
||||
archive.ExtractAllEntries();
|
||||
|
||||
// Method 3: Use Reader API (also sequential)
|
||||
using (var reader = RarReader.Open(File.OpenRead("solid.rar")))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
reader.WriteEntryToDirectory(@"C:\output");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Impact:**
|
||||
- Random extraction: O(n²) - very slow for many files
|
||||
- Sequential extraction: O(n) - 10-100x faster
|
||||
|
||||
### Best Practices for Solid Archives
|
||||
|
||||
1. **Always extract sequentially** when possible
|
||||
2. **Use Reader API** for large solid archives
|
||||
3. **Process entries in order** from the archive
|
||||
4. **Consider using 7Zip command-line** for scripted extractions
|
||||
|
||||
---
|
||||
|
||||
## Compression Level Trade-offs
|
||||
|
||||
### Deflate/GZip Levels
|
||||
|
||||
```csharp
|
||||
// Level 1 = Fastest, largest size
|
||||
// Level 6 = Default (balanced)
|
||||
// Level 9 = Slowest, best compression
|
||||
|
||||
// Write with different compression levels
|
||||
using (var archive = ZipArchive.CreateArchive())
|
||||
{
|
||||
archive.AddAllFromDirectory(@"D:\data");
|
||||
|
||||
// Fast compression (level 1)
|
||||
archive.SaveTo("fast.zip", new WriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
CompressionLevel = 1
|
||||
});
|
||||
|
||||
// Default compression (level 6)
|
||||
archive.SaveTo("default.zip", CompressionType.Deflate);
|
||||
|
||||
// Best compression (level 9)
|
||||
archive.SaveTo("best.zip", new WriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
CompressionLevel = 9
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Speed vs Size:**
|
||||
| Level | Speed | Size | Use Case |
|
||||
|-------|-------|------|----------|
|
||||
| 1 | 10x | 90% | Network, streaming |
|
||||
| 6 | 1x | 75% | Default (good balance) |
|
||||
| 9 | 0.1x | 65% | Archival, static storage |
|
||||
|
||||
### BZip2 Block Size
|
||||
|
||||
```csharp
|
||||
// BZip2 block size affects memory and compression
|
||||
// 100K to 900K (default 900K)
|
||||
|
||||
// Smaller block size = lower memory, faster
|
||||
// Larger block size = better compression, slower
|
||||
|
||||
using (var archive = TarArchive.CreateArchive())
|
||||
{
|
||||
archive.AddAllFromDirectory(@"D:\data");
|
||||
|
||||
// These are preset in WriterOptions via CompressionLevel
|
||||
archive.SaveTo("archive.tar.bz2", CompressionType.BZip2);
|
||||
}
|
||||
```
|
||||
|
||||
### LZMA Settings
|
||||
|
||||
LZMA compression is very powerful but memory-intensive:
|
||||
|
||||
```csharp
|
||||
// LZMA (7Zip, .tar.lzma):
|
||||
// - Dictionary size: 16 KB to 1 GB (default 32 MB)
|
||||
// - Faster preset: smaller dictionary
|
||||
// - Better compression: larger dictionary
|
||||
|
||||
// Preset via CompressionType
|
||||
using (var archive = TarArchive.CreateArchive())
|
||||
{
|
||||
archive.AddAllFromDirectory(@"D:\data");
|
||||
archive.SaveTo("archive.tar.xz", CompressionType.LZMA); // Default settings
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Performance
|
||||
|
||||
### When Async Helps
|
||||
|
||||
Async is beneficial when:
|
||||
- **Long I/O operations** (network, slow disks)
|
||||
- **UI responsiveness** needed (Windows Forms, WPF, Blazor)
|
||||
- **Server applications** (ASP.NET, multiple concurrent operations)
|
||||
|
||||
```csharp
|
||||
// Async extraction (non-blocking)
|
||||
using (var archive = ZipArchive.OpenArchive("archive.zip"))
|
||||
{
|
||||
await archive.WriteToDirectoryAsync(
|
||||
@"C:\output",
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
}
|
||||
// Thread can handle other work while I/O happens
|
||||
```
|
||||
|
||||
### When Async Doesn't Help
|
||||
|
||||
Async doesn't improve performance for:
|
||||
- **CPU-bound operations** (already fast)
|
||||
- **Local SSD I/O** (I/O is fast enough)
|
||||
- **Single-threaded scenarios** (no parallelism benefit)
|
||||
|
||||
```csharp
|
||||
// Sync extraction (simpler, same performance on fast I/O)
|
||||
using (var archive = ZipArchive.OpenArchive("archive.zip"))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
// Simple and fast - no async needed
|
||||
```
|
||||
|
||||
### Cancellation Pattern
|
||||
|
||||
```csharp
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
// Cancel after 5 minutes
|
||||
cts.CancelAfter(TimeSpan.FromMinutes(5));
|
||||
|
||||
try
|
||||
{
|
||||
using (var archive = ZipArchive.OpenArchive("archive.zip"))
|
||||
{
|
||||
await archive.WriteToDirectoryAsync(
|
||||
@"C:\output",
|
||||
cancellationToken: cts.Token
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine("Extraction cancelled");
|
||||
// Clean up partial extraction if needed
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Practical Performance Tips
|
||||
|
||||
### 1. Choose the Right API
|
||||
|
||||
| Scenario | API | Why |
|
||||
|----------|-----|-----|
|
||||
| Small archives | Archive | Faster random access |
|
||||
| Large archives | Reader | Lower memory |
|
||||
| Streaming | Reader | Works on non-seekable streams |
|
||||
| Download streams | Reader | Async extraction while downloading |
|
||||
|
||||
### 2. Batch Operations
|
||||
|
||||
```csharp
|
||||
// ✗ Slow - opens each archive separately
|
||||
foreach (var file in files)
|
||||
{
|
||||
using (var archive = ZipArchive.OpenArchive("archive.zip"))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
}
|
||||
|
||||
// ✓ Better - process multiple entries at once
|
||||
using (var archive = ZipArchive.OpenArchive("archive.zip"))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Profile Your Code
|
||||
|
||||
```csharp
|
||||
var sw = Stopwatch.StartNew();
|
||||
using (var archive = ZipArchive.OpenArchive("large.zip"))
|
||||
{
|
||||
archive.WriteToDirectory(@"C:\output");
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
Console.WriteLine($"Extraction took {sw.ElapsedMilliseconds}ms");
|
||||
|
||||
// Measure memory before/after
|
||||
var beforeMem = GC.GetTotalMemory(true);
|
||||
// ... do work ...
|
||||
var afterMem = GC.GetTotalMemory(true);
|
||||
Console.WriteLine($"Memory used: {(afterMem - beforeMem) / 1024 / 1024}MB");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Performance
|
||||
|
||||
### Extraction is Slow
|
||||
|
||||
1. **Check if solid archive** → Use sequential extraction
|
||||
2. **Check API** → Reader API might be faster for large files
|
||||
3. **Check compression level** → Higher levels are slower to decompress
|
||||
4. **Check I/O** → Network drives are much slower than SSD
|
||||
5. **Check buffer size** → May need larger buffers for network
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
1. **Use Reader API** instead of Archive API
|
||||
2. **Process entries immediately** rather than buffering
|
||||
3. **Reduce compression level** if writing
|
||||
4. **Check for memory leaks** in your code
|
||||
|
||||
### CPU Usage at 100%
|
||||
|
||||
1. **Normal for compression** - especially with high compression levels
|
||||
2. **Consider lower level** for faster processing
|
||||
3. **Reduce parallelism** if processing multiple archives
|
||||
4. **Check if awaiting properly** in async code
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [PERFORMANCE.md](USAGE.md) - Usage examples with performance considerations
|
||||
- [FORMATS.md](FORMATS.md) - Format-specific performance notes
|
||||
291
docs/TAR_GAP_ANALYSIS.md
Normal file
291
docs/TAR_GAP_ANALYSIS.md
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
# Tar Gap Analysis
|
||||
|
||||
## Scope
|
||||
|
||||
This document compares the current Tar documentation, tests, and code paths in SharpCompress.
|
||||
|
||||
It is intentionally implementation-focused. The goal is to identify mismatches, omissions, and incomplete areas in the current SharpCompress Tar support.
|
||||
|
||||
Primary references:
|
||||
|
||||
- `docs/FORMATS.md`
|
||||
- `src/SharpCompress/Factories/TarFactory.cs`
|
||||
- `src/SharpCompress/Factories/TarWrapper.cs`
|
||||
- `src/SharpCompress/Archives/Tar/`
|
||||
- `src/SharpCompress/Readers/Tar/`
|
||||
- `src/SharpCompress/Writers/Tar/`
|
||||
- `src/SharpCompress/Common/Tar/`
|
||||
- `tests/SharpCompress.Test/Tar/`
|
||||
|
||||
## Implemented Since Baseline
|
||||
|
||||
- `Tar.XZ` is now documented as read-only (`Writer API = N/A`) in `docs/FORMATS.md`.
|
||||
- Local PAX extended headers (`x`) are now implemented on the read path for selected keys.
|
||||
- Global PAX extended headers (`g`) are now implemented on the read path for selected keys.
|
||||
- Tar tests now include local PAX coverage for reader/archive sync and async paths.
|
||||
- Tar tests now include global PAX coverage for reader/archive sync and async paths.
|
||||
- `TarWriterOptions.HeaderFormat` is now honored in sync and async file and directory write paths.
|
||||
- Tar tests now cover `USTAR` and `GNU_TAR_LONG_LINK`, including USTAR long-name failure scenarios.
|
||||
- Symlink coverage now includes `TarWithSymlink.tar.gz` for reader sync and async paths.
|
||||
- Tar tests now explicitly cover unsupported tar wrapper compression writes (`Xz`, `ZStandard`, `Lzw`) for sync and async writer paths.
|
||||
- `TarArchive.OpenAsyncArchive(Stream)` now enforces the same seekable-stream contract as `TarArchive.OpenArchive(Stream)`.
|
||||
- Sparse handling remains explicitly unsupported.
|
||||
- Non-modeled PAX keys remain explicitly unsupported.
|
||||
|
||||
## Claimed vs Actual Support
|
||||
|
||||
### `Tar.XZ` is read-only
|
||||
|
||||
`tar.xz` is supported for reading, but not for writing.
|
||||
|
||||
Actual implementation in `src/SharpCompress/Writers/Tar/TarWriter.cs` does not support `CompressionType.Xz`. The writer throws `InvalidFormatException` for any compression type outside:
|
||||
|
||||
- `None`
|
||||
- `GZip`
|
||||
- `BZip2`
|
||||
- `LZip`
|
||||
|
||||
Impact:
|
||||
|
||||
- Tar write support is narrower than Tar read support
|
||||
- `tar.xz` creation is not available through the built-in Tar writer
|
||||
|
||||
Recommended action:
|
||||
|
||||
- keep the format table marked `N/A` for Tar.XZ writer support
|
||||
|
||||
## Read-Path Gaps
|
||||
|
||||
### Local and global PAX headers are implemented for selected keys
|
||||
|
||||
Local (`x`) and global (`g`) POSIX PAX extended headers are now supported on the read path.
|
||||
|
||||
Supported keys in the current implementation:
|
||||
|
||||
- `path`
|
||||
- `linkpath`
|
||||
- `size`
|
||||
- `mtime`
|
||||
- `uid`
|
||||
- `gid`
|
||||
- `mode`
|
||||
|
||||
Remaining gap:
|
||||
|
||||
- non-modeled PAX keys are still ignored
|
||||
- PAX sparse extensions are still unsupported
|
||||
|
||||
Recommended action:
|
||||
|
||||
- keep supported-key boundaries documented and test-covered
|
||||
- keep unsupported-key behavior explicit in docs
|
||||
|
||||
### Sparse files are not semantically implemented
|
||||
|
||||
`EntryType` defines `SparseFile`, but the read path does not contain sparse map handling or sparse reconstruction logic.
|
||||
|
||||
PAX sparse extensions are also unsupported (for example `GNU.sparse.*` and similar sparse metadata keys).
|
||||
|
||||
Evidence:
|
||||
|
||||
- `src/SharpCompress/Common/Tar/Headers/EntryType.cs`
|
||||
- no sparse-specific code in `TarHeader`, `TarEntry`, `TarFilePart`, or `TarArchive`
|
||||
- no sparse tests
|
||||
|
||||
Impact:
|
||||
|
||||
- sparse entries may be treated as ordinary entries rather than sparse files with holes
|
||||
|
||||
Recommended action:
|
||||
|
||||
- keep sparse support explicitly documented as unsupported
|
||||
- add sparse fixtures and tests only when sparse reconstruction is implemented
|
||||
|
||||
### Non-modeled PAX keys are still unsupported
|
||||
|
||||
PAX parsing is intentionally limited to modeled keys (`path`, `linkpath`, `size`, `mtime`, `uid`, `gid`, `mode`).
|
||||
|
||||
Not currently modeled/supported:
|
||||
|
||||
- `uname`
|
||||
- `gname`
|
||||
- `atime`
|
||||
- `ctime`
|
||||
- device-specific values and vendor keys
|
||||
|
||||
Recommended action:
|
||||
|
||||
- keep unsupported-key behavior documented as ignored
|
||||
- add support only when there is a consumer-facing object model for it
|
||||
|
||||
### Device and FIFO semantics are not surfaced
|
||||
|
||||
The entry type enum includes `CharDevice`, `BlockDevice`, and `Fifo`, but the public tar model does not expose device metadata semantics.
|
||||
|
||||
Impact:
|
||||
|
||||
- such entries may not round-trip meaningfully through the API
|
||||
- behavior is undocumented and untested
|
||||
|
||||
Recommended action:
|
||||
|
||||
- either document them as raw/unmodeled entry types or add dedicated support
|
||||
|
||||
## Write-Path Gaps
|
||||
|
||||
### `HeaderFormat` consistency is resolved
|
||||
|
||||
`TarWriterOptions.HeaderFormat` is now applied across:
|
||||
|
||||
- sync file writes
|
||||
- sync directory writes
|
||||
- async file writes
|
||||
- async directory writes
|
||||
|
||||
Regression tests now cover both `USTAR` and `GNU_TAR_LONG_LINK` behavior.
|
||||
|
||||
### No public link-writing support
|
||||
|
||||
The read path supports symbolic and hard link targets through `TarEntry.LinkTarget`, but the write API exposes only regular file and directory creation.
|
||||
|
||||
Impact:
|
||||
|
||||
- symlink and hardlink tar archives cannot be created through the current public Tar writer API
|
||||
|
||||
Recommended action:
|
||||
|
||||
- either document this as a deliberate limitation or add link-writing APIs
|
||||
|
||||
### Metadata round-trip support is incomplete
|
||||
|
||||
The writer does not round-trip rich tar metadata beyond the basic fields needed for file and directory entries.
|
||||
|
||||
Current write behavior sets fixed defaults for some fields such as mode, owner id, and group id.
|
||||
|
||||
Impact:
|
||||
|
||||
- modified or newly created tar archives may lose metadata fidelity relative to the original archive
|
||||
|
||||
Recommended action:
|
||||
|
||||
- document current metadata write behavior clearly
|
||||
- expand metadata support only if needed by consumers
|
||||
|
||||
### No write support for some detected wrappers
|
||||
|
||||
The detection and read path supports wrappers that the write path does not support.
|
||||
|
||||
| Wrapper | Read support | Write support |
|
||||
| ------- | ------------ | ------------- |
|
||||
| `tar.xz` | Yes | No |
|
||||
| `tar.zst` | Yes | No |
|
||||
| `tar.Z` | Yes | No |
|
||||
|
||||
This is not inherently wrong, but it should be clearly documented everywhere support is summarized.
|
||||
|
||||
## Sync and Async API Inconsistencies
|
||||
|
||||
### Seekability contract alignment is resolved
|
||||
|
||||
`TarArchive.OpenArchive(Stream)` and `TarArchive.OpenAsyncArchive(Stream)` now both enforce the same seekable-stream contract and throw `ArgumentException` for non-seekable input.
|
||||
|
||||
Tar tests include an async regression case for non-seekable stream open.
|
||||
|
||||
### Header format alignment between sync and async is resolved
|
||||
|
||||
Sync and async Tar writer paths now both honor `TarWriterOptions.HeaderFormat`, and matching tests are present for both paths.
|
||||
|
||||
## Test Coverage Gaps
|
||||
|
||||
### Symlink coverage is now present for reader paths
|
||||
|
||||
Symlink behavior is now asserted for sync and async reader paths using:
|
||||
|
||||
- `tests/TestArchives/Archives/TarWithSymlink.tar.gz`
|
||||
|
||||
Archive-path symlink assertions currently rely on small tar fixtures rather than this large compressed sample.
|
||||
|
||||
### Header format coverage is now present
|
||||
|
||||
Tar tests now cover:
|
||||
|
||||
- `TarWriterOptions.HeaderFormat = USTAR`
|
||||
- `TarWriterOptions.HeaderFormat = GNU_TAR_LONG_LINK`
|
||||
- long-name failure in USTAR mode
|
||||
- long-name success in GNU mode through sync and async writer paths
|
||||
|
||||
### No tests for sparse tar semantics
|
||||
|
||||
Local and global PAX coverage now exists, but there is still no evidence of coverage for:
|
||||
|
||||
- sparse tar entries
|
||||
- sparse PAX extensions
|
||||
|
||||
Impact:
|
||||
|
||||
- unsupported or partial behavior is neither documented by tests nor protected from regression
|
||||
|
||||
Recommended action:
|
||||
|
||||
- either add fixtures and tests or document these as unsupported with no test coverage
|
||||
|
||||
### Unsupported-wrapper writer coverage is now present
|
||||
|
||||
Tar writer tests now explicitly verify `InvalidFormatException` for unsupported tar wrapper compression types:
|
||||
|
||||
- `CompressionType.Xz`
|
||||
- `CompressionType.ZStandard`
|
||||
- `CompressionType.Lzw`
|
||||
|
||||
Coverage exists in both sync and async writer test paths.
|
||||
|
||||
## Documentation Gaps
|
||||
|
||||
### Current format documentation is too coarse for Tar
|
||||
|
||||
`docs/FORMATS.md` summarizes support at the wrapper level, but Tar behavior depends on more than wrapper compression.
|
||||
|
||||
Missing implementation-specific details include:
|
||||
|
||||
- GNU long-name and long-link support
|
||||
- USTAR prefix handling
|
||||
- oldgnu numeric quirk handling
|
||||
- partial PAX support boundaries (selected local/global keys supported)
|
||||
- missing sparse support
|
||||
- reader vs archive behavior differences for compressed tar
|
||||
- file-size requirements for writing from non-seekable sources
|
||||
|
||||
Recommended action:
|
||||
|
||||
- keep `docs/FORMATS.md` high-level
|
||||
- add and maintain a dedicated Tar spec document for details
|
||||
|
||||
### The current docs do not call out partial support clearly
|
||||
|
||||
The codebase supports some tar dialect features and not others, but the docs do not separate:
|
||||
|
||||
- fully supported
|
||||
- partially supported
|
||||
- unsupported
|
||||
|
||||
Recommended action:
|
||||
|
||||
- use an explicit feature matrix in the Tar documentation
|
||||
|
||||
## Recommended Follow-Ups
|
||||
|
||||
### Priority 1
|
||||
|
||||
- Improve metadata round-trip behavior only if there is a consumer need
|
||||
- Evaluate whether non-modeled PAX keys should remain ignored or be surfaced in a future metadata API
|
||||
|
||||
## Summary
|
||||
|
||||
The SharpCompress Tar implementation is strong on common read scenarios and basic write scenarios, but the current gaps fall into four categories:
|
||||
|
||||
- documentation overstating or under-describing support
|
||||
- incomplete feature coverage for less common tar dialect features
|
||||
- intentionally deferred metadata and API-surface decisions
|
||||
- test coverage holes around advanced tar metadata features
|
||||
|
||||
`docs/TAR_SPEC.md` should be treated as the implementation baseline. This document identifies where that baseline is incomplete, inconsistent, or incorrectly reflected elsewhere in the repository.
|
||||
467
docs/TAR_SPEC.md
Normal file
467
docs/TAR_SPEC.md
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
# Tar Spec
|
||||
|
||||
## Scope
|
||||
|
||||
This document describes the Tar implementation that exists in SharpCompress today.
|
||||
|
||||
It is intentionally SharpCompress-specific. It documents actual behavior in the current codebase, including partial support and limitations. It is not a general tar format reference.
|
||||
|
||||
Primary implementation files:
|
||||
|
||||
- `src/SharpCompress/Factories/TarFactory.cs`
|
||||
- `src/SharpCompress/Factories/TarWrapper.cs`
|
||||
- `src/SharpCompress/Archives/Tar/TarArchive.cs`
|
||||
- `src/SharpCompress/Archives/Tar/TarArchive.Async.cs`
|
||||
- `src/SharpCompress/Archives/Tar/TarArchive.Factory.cs`
|
||||
- `src/SharpCompress/Readers/Tar/TarReader.cs`
|
||||
- `src/SharpCompress/Readers/Tar/TarReader.Async.cs`
|
||||
- `src/SharpCompress/Writers/Tar/TarWriter.cs`
|
||||
- `src/SharpCompress/Writers/Tar/TarWriter.Async.cs`
|
||||
- `src/SharpCompress/Writers/Tar/TarWriterOptions.cs`
|
||||
- `src/SharpCompress/Common/Tar/Headers/TarHeader.cs`
|
||||
- `src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs`
|
||||
- `src/SharpCompress/Common/Tar/TarHeaderFactory.cs`
|
||||
- `src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs`
|
||||
|
||||
## API Surface
|
||||
|
||||
SharpCompress exposes Tar support through four main entry points.
|
||||
|
||||
| Type | Role |
|
||||
| ---- | ---- |
|
||||
| `TarFactory` | Format detection and factory entry point for archive, reader, and writer APIs |
|
||||
| `TarArchive` | Archive API for enumerating and rewriting tar archives |
|
||||
| `TarReader` | Forward-only reader API for streaming tar extraction |
|
||||
| `TarWriter` | Forward-only writer API for creating tar archives |
|
||||
|
||||
`TarWriterOptions` controls output compression, stream ownership, archive finalization, encoding, and header write format.
|
||||
|
||||
## Supported Wrapper Formats
|
||||
|
||||
Tar wrapper detection is defined by `TarWrapper.Wrappers` in `src/SharpCompress/Factories/TarWrapper.cs`.
|
||||
|
||||
### Supported Extensions
|
||||
|
||||
| Wrapper | Extensions |
|
||||
| ------- | ---------- |
|
||||
| Plain tar | `tar` |
|
||||
| Tar + BZip2 | `tar.bz2`, `tb2`, `tbz`, `tbz2`, `tz2` |
|
||||
| Tar + GZip | `tar.gz`, `taz`, `tgz` |
|
||||
| Tar + ZStandard | `tar.zst`, `tar.zstd`, `tzst`, `tzstd` |
|
||||
| Tar + LZip | `tar.lz` |
|
||||
| Tar + XZ | `tar.xz`, `txz` |
|
||||
| Tar + LZW compress | `tar.Z`, `tZ`, `taZ` |
|
||||
|
||||
### API Support Matrix
|
||||
|
||||
| Wrapper | Detection | `TarArchive` read | `TarReader` read | `TarWriter` write |
|
||||
| ------- | --------- | ----------------- | ---------------- | ----------------- |
|
||||
| Plain tar | Yes | Yes | Yes | Yes |
|
||||
| Tar + GZip | Yes | Yes | Yes | Yes |
|
||||
| Tar + BZip2 | Yes | Yes | Yes | Yes |
|
||||
| Tar + LZip | Yes | Yes | Yes | Yes |
|
||||
| Tar + XZ | Yes | Yes | Yes | No |
|
||||
| Tar + ZStandard | Yes | Yes | Yes | No |
|
||||
| Tar + LZW compress | Yes | Yes | Yes | No |
|
||||
|
||||
Write support is implemented in `src/SharpCompress/Writers/Tar/TarWriter.cs` and currently accepts only `CompressionType.None`, `CompressionType.GZip`, `CompressionType.BZip2`, and `CompressionType.LZip`.
|
||||
|
||||
## Detection Behavior
|
||||
|
||||
Tar detection is implemented in `TarFactory.IsArchive`, `TarFactory.IsArchiveAsync`, `TarFactory.GetCompressionType`, and `TarFactory.GetCompressionTypeAsync`.
|
||||
|
||||
Detection behavior is:
|
||||
|
||||
1. Wrap the incoming stream in `SharpCompressStream`.
|
||||
2. Start recording with a rewind buffer sized from `TarWrapper.MaximumRewindBufferSize`.
|
||||
3. Probe each registered wrapper in order.
|
||||
4. If a wrapper matches, create a decompression stream for that wrapper.
|
||||
5. Call `TarArchive.IsTarFile` or `TarArchive.IsTarFileAsync` on the decompressed stream.
|
||||
6. If the tar probe succeeds, treat the stream as tar with that wrapper compression.
|
||||
|
||||
Implications:
|
||||
|
||||
- Tar detection is content-based, not extension-based.
|
||||
- Wrapper detection is not sufficient by itself. The decompressed payload must also parse as tar.
|
||||
- Non-seekable detection is supported through the recording and rewind mechanism.
|
||||
- The largest rewind requirement currently comes from BZip2, which declares a larger minimum probe buffer in `TarWrapper`.
|
||||
|
||||
`TarArchive.IsTarFile` and `TarArchive.IsTarFileAsync` attempt to read a single tar header and return `false` on any exception. They also treat an all-zero empty archive block as a valid empty tar archive when the entry type is defined.
|
||||
|
||||
## Reader Behavior
|
||||
|
||||
`TarReader` is the forward-only streaming API.
|
||||
|
||||
Implementation files:
|
||||
|
||||
- `src/SharpCompress/Readers/Tar/TarReader.cs`
|
||||
- `src/SharpCompress/Readers/Tar/TarReader.Async.cs`
|
||||
- `src/SharpCompress/Common/Tar/TarEntry.cs`
|
||||
- `src/SharpCompress/Common/Tar/TarEntry.Async.cs`
|
||||
- `src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs`
|
||||
|
||||
Reader behavior:
|
||||
|
||||
- The reader always enumerates entries in streaming mode.
|
||||
- It works with non-seekable input streams.
|
||||
- It applies decompression based on the detected wrapper compression type before parsing tar headers.
|
||||
- Entry streams are backed by `TarReadOnlySubStream`.
|
||||
|
||||
`TarReadOnlySubStream` has an important behavior: disposing an entry stream consumes any unread entry bytes and any required 512-byte padding so that the next header can be read correctly. This is what makes skipping entries work in streaming mode.
|
||||
|
||||
### Reader Compression Mapping
|
||||
|
||||
`TarReader.RequestInitialStream` and `RequestInitialStreamAsync` map the detected wrapper to the corresponding decompression stream:
|
||||
|
||||
- `None`
|
||||
- `BZip2`
|
||||
- `GZip`
|
||||
- `ZStandard`
|
||||
- `LZip`
|
||||
- `Xz`
|
||||
- `Lzw`
|
||||
|
||||
### Reader Entry Semantics
|
||||
|
||||
For each entry, SharpCompress exposes:
|
||||
|
||||
- `Key` from the parsed tar name
|
||||
- `LinkTarget` for symbolic and hard links
|
||||
- `Size`
|
||||
- `CompressedSize`
|
||||
- `LastModifiedTime`
|
||||
- `IsDirectory`
|
||||
- `Mode`
|
||||
- `UserID`
|
||||
- `GroupId`
|
||||
|
||||
Tar entries are always reported as unencrypted and CRC is always `0`.
|
||||
|
||||
## Archive Behavior
|
||||
|
||||
`TarArchive` is the archive API.
|
||||
|
||||
Implementation files:
|
||||
|
||||
- `src/SharpCompress/Archives/Tar/TarArchive.cs`
|
||||
- `src/SharpCompress/Archives/Tar/TarArchive.Async.cs`
|
||||
- `src/SharpCompress/Archives/Tar/TarArchive.Factory.cs`
|
||||
|
||||
### Open Behavior
|
||||
|
||||
`TarArchive.OpenArchive(Stream)` and `TarArchive.OpenAsyncArchive(Stream)` require a seekable stream and throw `ArgumentException` when `CanSeek` is `false`.
|
||||
|
||||
`TarArchive.OpenArchive(FileInfo)` and the list-based overloads use `SourceStream` and determine wrapper compression by calling `TarFactory.GetCompressionType`.
|
||||
|
||||
Asynchronous `OpenAsyncArchive` overloads use `TarFactory.GetCompressionTypeAsync` for wrapper detection.
|
||||
|
||||
### Entry Loading
|
||||
|
||||
`TarArchive.LoadEntries` and `LoadEntriesAsync` parse entries differently depending on wrapper compression:
|
||||
|
||||
- Uncompressed tar uses `StreamingMode.Seekable`.
|
||||
- Wrapped tar uses `StreamingMode.Streaming` because the decompressed stream is not treated as random-access.
|
||||
|
||||
When seekable mode is used, the header stores `DataStartPosition`, and entries reopen data through `TarFilePart` by seeking back to the data position.
|
||||
|
||||
When streaming mode is used, the header stores a `PackedStream`, and entry access follows streaming semantics over the decompressed stream.
|
||||
|
||||
### Archive Rewrite Behavior
|
||||
|
||||
`TarArchive` supports creating and modifying archives through `AbstractWritableArchive`:
|
||||
|
||||
- add file entries
|
||||
- add directory entries
|
||||
- remove entries
|
||||
- save to a new stream or path
|
||||
|
||||
Archive rewrite is implemented by enumerating the existing and new entries and writing them back out through `TarWriter`.
|
||||
|
||||
## Writer Behavior
|
||||
|
||||
`TarWriter` is the forward-only tar writer.
|
||||
|
||||
Implementation files:
|
||||
|
||||
- `src/SharpCompress/Writers/Tar/TarWriter.cs`
|
||||
- `src/SharpCompress/Writers/Tar/TarWriter.Async.cs`
|
||||
- `src/SharpCompress/Writers/Tar/TarWriterOptions.cs`
|
||||
|
||||
### Supported Output Compression
|
||||
|
||||
The writer supports these output compression types:
|
||||
|
||||
- `CompressionType.None`
|
||||
- `CompressionType.GZip`
|
||||
- `CompressionType.BZip2`
|
||||
- `CompressionType.LZip`
|
||||
|
||||
Any other compression type causes `InvalidFormatException`.
|
||||
|
||||
### Stream Ownership
|
||||
|
||||
If `LeaveStreamOpen` is `true`, `TarWriter` wraps the destination in a non-disposing stream.
|
||||
|
||||
### File Writing
|
||||
|
||||
`TarWriter.Write` and `WriteAsync` write a tar header followed by file contents, then pad the payload to the next 512-byte boundary.
|
||||
|
||||
If the source stream is non-seekable and the caller does not supply `size`, the writer throws `ArgumentException` because tar requires the file size in the header.
|
||||
|
||||
### Directory Writing
|
||||
|
||||
`WriteDirectory` and `WriteDirectoryAsync` normalize the directory name to use forward slashes and ensure the key ends with `/`.
|
||||
|
||||
Empty or root-equivalent directory names are skipped.
|
||||
|
||||
### Archive Finalization
|
||||
|
||||
If `FinalizeArchiveOnClose` is `true`, disposing the writer writes two 512-byte zero blocks to terminate the archive.
|
||||
|
||||
If the output stream implements `IFinishable`, dispose also calls `Finish()`.
|
||||
|
||||
## Header Write Formats
|
||||
|
||||
`TarHeaderWriteFormat` is defined in `src/SharpCompress/Common/Tar/Headers/TarHeaderWriteFormat.cs`.
|
||||
|
||||
Supported write formats:
|
||||
|
||||
- `GNU_TAR_LONG_LINK`
|
||||
- `USTAR`
|
||||
|
||||
`TarWriterOptions.HeaderFormat` defaults to `GNU_TAR_LONG_LINK`.
|
||||
|
||||
Current implementation behavior is narrower than the option surface suggests:
|
||||
|
||||
- sync file writes use the configured `HeaderFormat`
|
||||
- sync directory writes currently construct the default tar header format
|
||||
- async file writes currently construct the default tar header format
|
||||
- async directory writes currently construct the default tar header format
|
||||
|
||||
In practice, this means the configured `HeaderFormat` is currently honored only by the synchronous file write path.
|
||||
|
||||
### GNU Long Name Write Behavior
|
||||
|
||||
In GNU mode, when a file name exceeds the 100-byte field, `TarHeader.WriteGnuTarLongLink` writes a synthetic long-name header using `././@LongLink` and `EntryType.LongName`, then writes the long name payload, and finally writes the actual file entry.
|
||||
|
||||
GNU mode also writes large file sizes using binary size encoding when the size does not fit the standard octal field.
|
||||
|
||||
### USTAR Write Behavior
|
||||
|
||||
When the synchronous file write path is configured for `USTAR`, `TarHeader.WriteUstar` attempts to split a long path into:
|
||||
|
||||
- the main `name` field
|
||||
- the `prefix` field
|
||||
|
||||
If the name cannot be represented in USTAR field limits, the writer throws `InvalidFormatException` and instructs the caller to use GNU Tar format instead.
|
||||
|
||||
## Header Read Behavior
|
||||
|
||||
Tar header parsing is implemented in `TarHeader.Read` and `TarHeader.ReadAsync`.
|
||||
|
||||
### Implemented Read Features
|
||||
|
||||
| Feature | Read support |
|
||||
| ------- | ------------ |
|
||||
| Regular file entries | Yes |
|
||||
| Directory entries | Yes |
|
||||
| Symbolic link target reading | Yes |
|
||||
| Hard link target reading | Yes |
|
||||
| GNU long name (`L`) | Yes |
|
||||
| GNU long link (`K`) | Yes |
|
||||
| PAX local extended header (`x`) | Yes (selected keys) |
|
||||
| PAX global extended header (`g`) | Yes (selected keys) |
|
||||
| USTAR prefix reconstruction | Yes |
|
||||
| Binary size field parsing | Yes |
|
||||
| oldgnu uid/gid numeric quirk parsing | Yes |
|
||||
| POSIX and signed checksum validation | Yes |
|
||||
|
||||
### Entry Types Recognized by the Code
|
||||
|
||||
`EntryType` currently declares these values in `src/SharpCompress/Common/Tar/Headers/EntryType.cs`:
|
||||
|
||||
- `File`
|
||||
- `OldFile`
|
||||
- `HardLink`
|
||||
- `SymLink`
|
||||
- `CharDevice`
|
||||
- `BlockDevice`
|
||||
- `Directory`
|
||||
- `Fifo`
|
||||
- `LongLink`
|
||||
- `LongName`
|
||||
- `SparseFile`
|
||||
- `VolumeHeader`
|
||||
- `LocalExtendedHeader`
|
||||
- `GlobalExtendedHeader`
|
||||
|
||||
SharpCompress currently has explicit handling for only a subset of those values during read and write.
|
||||
|
||||
### Long Name and Long Link Reads
|
||||
|
||||
When `TarHeader.Read` encounters `EntryType.LongName` or `EntryType.LongLink`, it reads the payload and applies it to the next real header.
|
||||
|
||||
Long-name payload reads are capped at `32768` bytes to avoid memory exhaustion from malformed archives.
|
||||
|
||||
### PAX Local Header Reads
|
||||
|
||||
SharpCompress now consumes local PAX extended headers (`x`) and applies supported key overrides to the next real entry.
|
||||
|
||||
Currently supported keys:
|
||||
|
||||
- `path`
|
||||
- `linkpath`
|
||||
- `size`
|
||||
- `mtime`
|
||||
- `uid`
|
||||
- `gid`
|
||||
- `mode`
|
||||
|
||||
Unknown PAX keys are ignored.
|
||||
|
||||
### PAX Global Header Reads
|
||||
|
||||
SharpCompress consumes global PAX extended headers (`g`) and applies supported key overrides to subsequent entries.
|
||||
|
||||
Supported keys match local PAX support:
|
||||
|
||||
- `path`
|
||||
- `linkpath`
|
||||
- `size`
|
||||
- `mtime`
|
||||
- `uid`
|
||||
- `gid`
|
||||
- `mode`
|
||||
|
||||
Global metadata is overridden by local per-entry metadata when both are present.
|
||||
|
||||
### Name Reconstruction
|
||||
|
||||
For USTAR headers, if the magic field is `ustar` and the prefix field is populated, SharpCompress reconstructs the entry name as `prefix + "/" + name`.
|
||||
|
||||
## Name and Metadata Handling
|
||||
|
||||
### Path Normalization
|
||||
|
||||
Writer path normalization is implemented in `TarWriter.NormalizeFilename` and `NormalizeDirectoryName`.
|
||||
|
||||
Behavior:
|
||||
|
||||
- backslashes are converted to `/`
|
||||
- drive prefixes before `:` are removed
|
||||
- leading and trailing `/` are trimmed for file entries
|
||||
- directory entries are normalized to end with `/`
|
||||
|
||||
### Encoding
|
||||
|
||||
Tar name encoding and decoding is controlled by `IArchiveEncoding`.
|
||||
|
||||
- reader APIs decode names with `ReaderOptions.ArchiveEncoding`
|
||||
- writer APIs encode names with `TarWriterOptions.ArchiveEncoding`
|
||||
|
||||
The tests include UTF-8 and code page coverage for tar name handling.
|
||||
|
||||
### Metadata Surface
|
||||
|
||||
Tar metadata currently surfaced through `TarEntry` includes:
|
||||
|
||||
- name
|
||||
- link target
|
||||
- mode
|
||||
- uid
|
||||
- gid
|
||||
- size
|
||||
- last modified time
|
||||
|
||||
Writer metadata is narrower. The writer sets:
|
||||
|
||||
- `LastModifiedTime`
|
||||
- `Name`
|
||||
- `Size`
|
||||
- entry type for file or directory
|
||||
|
||||
The current writer writes fixed mode, owner id, and group id defaults rather than round-tripping full metadata.
|
||||
|
||||
## Async Behavior
|
||||
|
||||
Async tar support is provided by:
|
||||
|
||||
- `TarArchive.OpenAsyncArchive`
|
||||
- `TarReader.OpenAsyncReader`
|
||||
- `TarWriter.WriteAsync`
|
||||
- `TarWriter.WriteDirectoryAsync`
|
||||
- `TarHeader.ReadAsync`
|
||||
- `TarHeader.WriteAsync`
|
||||
|
||||
The async implementations generally mirror the sync implementations while using async header parsing, decompression, and stream copy paths.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
This section documents current implementation limits, not desired future behavior.
|
||||
|
||||
### Write limitations
|
||||
|
||||
- No write support for `tar.xz`
|
||||
- No write support for `tar.zst`
|
||||
- No write support for `tar.Z`
|
||||
- No public API for writing symbolic links or hard links
|
||||
- No PAX write support
|
||||
- No sparse file write support
|
||||
- No device or FIFO write support
|
||||
|
||||
### Read limitations or partial support
|
||||
|
||||
- PAX support is limited to selected keys (`path`, `linkpath`, `size`, `mtime`, `uid`, `gid`, `mode`)
|
||||
- No semantic sparse file handling beyond recognizing the entry type enum value
|
||||
- No special device or FIFO object model beyond the raw entry type information available internally
|
||||
|
||||
### Archive behavior limitations
|
||||
|
||||
- Stream-based archive open requires a seekable input stream
|
||||
- Compressed tar archive access is not full random-access in the same sense as uncompressed seekable tar
|
||||
|
||||
## Test Coverage Map
|
||||
|
||||
Tar tests live in `tests/SharpCompress.Test/Tar/`.
|
||||
|
||||
Representative coverage:
|
||||
|
||||
| Area | Tests |
|
||||
| ---- | ----- |
|
||||
| Wrapper detection and reading | `TarReaderTests.cs`, `TarReaderAsyncTests.cs` |
|
||||
| Archive open and rewrite | `TarArchiveTests.cs`, `TarArchiveAsyncTests.cs` |
|
||||
| Writer behavior | `TarWriterTests.cs`, `TarWriterAsyncTests.cs` |
|
||||
| Directory entry behavior | `TarWriterDirectoryTests.cs`, `TarArchiveDirectoryTests.cs` |
|
||||
| Long-name behavior | `TarArchiveTests.cs`, `TarReaderTests.cs` |
|
||||
| Corruption and broken stream handling | `TarReaderTests.cs`, `TarReaderAsyncTests.cs` |
|
||||
|
||||
Representative tar test archives in `tests/TestArchives/Archives/`:
|
||||
|
||||
- `Tar.tar`
|
||||
- `Tar.tar.gz`
|
||||
- `Tar.tar.bz2`
|
||||
- `Tar.tar.lz`
|
||||
- `Tar.tar.xz`
|
||||
- `Tar.tar.zst`
|
||||
- `Tar.tar.Z`
|
||||
- `Tar.oldgnu.tar.gz`
|
||||
- `very long filename.tar`
|
||||
- `ustar with long names.tar`
|
||||
- `Tar.LongPathsWithLongNameExtension.tar`
|
||||
- `Tar.PaxGlobalHeader.tar`
|
||||
- `Tar.PaxGlobalHeader.Link.tar`
|
||||
- `Tar.Empty.tar`
|
||||
- `TarCorrupted.tar`
|
||||
- `TarWithSymlink.tar.gz`
|
||||
|
||||
## Summary
|
||||
|
||||
SharpCompress Tar support is centered around:
|
||||
|
||||
- broad read support for common tar wrappers
|
||||
- forward-only reader behavior for streamed extraction
|
||||
- seekable archive support for uncompressed tar and archive rewrite workflows
|
||||
- narrower write support than read support
|
||||
- GNU long-name and USTAR write support
|
||||
- PAX local header (`x`) read support for selected metadata keys
|
||||
- partial coverage for less common tar dialect features
|
||||
474
docs/USAGE.md
Normal file
474
docs/USAGE.md
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
# 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, cancellationToken: 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
|
||||
|
||||
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.
|
||||
|
||||
However, the .NET Framework often has classes that will dispose streams by default to make things "easy" like the following:
|
||||
|
||||
```C#
|
||||
using (var reader = new StreamReader(File.Open("foo")))
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
In this example, reader should get disposed. However, stream rules should say the the `FileStream` created by `File.Open` should remain open. However, the .NET Framework closes it for you by default unless you override the constructor. In general, you should be writing Stream code like this:
|
||||
|
||||
```C#
|
||||
using (var fileStream = File.Open("foo"))
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
To deal with the "correct" rules as well as the expectations of users, I've decided to always close wrapped streams as of 0.21.
|
||||
|
||||
To be explicit though, consider always using the overloads that use `ReaderOptions` or `WriterOptions` and explicitly set `LeaveStreamOpen` the way you want.
|
||||
|
||||
Default behavior in factory APIs:
|
||||
- File path / `FileInfo` overloads set `LeaveStreamOpen = false`.
|
||||
- Caller-provided `Stream` overloads set `LeaveStreamOpen = true`.
|
||||
|
||||
If using Compression Stream classes directly and you don't want the wrapped stream to be closed. Use the `NonDisposingStream` as a wrapper to prevent the stream being disposed. The change in 0.21 simplified a lot even though the usage is a bit more convoluted.
|
||||
|
||||
## Samples
|
||||
|
||||
Also, look over the tests for more thorough [examples](https://github.com/adamhathcock/sharpcompress/tree/master/tests/SharpCompress.Test)
|
||||
|
||||
### Create Zip Archive from multiple files
|
||||
```C#
|
||||
using(var archive = ZipArchive.CreateArchive())
|
||||
{
|
||||
archive.AddEntry("file01.txt", "C:\\file01.txt");
|
||||
archive.AddEntry("file02.txt", "C:\\file02.txt");
|
||||
...
|
||||
|
||||
archive.SaveTo("C:\\temp.zip", CompressionType.Deflate);
|
||||
}
|
||||
```
|
||||
|
||||
### Create Zip Archive from all files in a directory to a file
|
||||
|
||||
```C#
|
||||
using (var archive = ZipArchive.CreateArchive())
|
||||
{
|
||||
archive.AddAllFromDirectory("D:\\temp");
|
||||
archive.SaveTo("C:\\temp.zip", CompressionType.Deflate);
|
||||
}
|
||||
```
|
||||
|
||||
### Create Zip Archive from all files in a directory and save in memory
|
||||
|
||||
```C#
|
||||
var memoryStream = new MemoryStream();
|
||||
using (var archive = ZipArchive.CreateArchive())
|
||||
{
|
||||
archive.AddAllFromDirectory("D:\\temp");
|
||||
archive.SaveTo(memoryStream, new WriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
LeaveStreamOpen = true
|
||||
});
|
||||
}
|
||||
//reset memoryStream to be usable now
|
||||
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.
|
||||
`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#
|
||||
// Use ReaderOptions for open-time behavior and ExtractionOptions for extract-time behavior
|
||||
using (var archive = RarArchive.OpenArchive("Test.rar", ReaderOptions.ForFilePath))
|
||||
{
|
||||
// Simple extraction with RarArchive; this WriteToDirectory pattern works for all archive types
|
||||
archive.WriteToDirectory(
|
||||
@"D:\temp",
|
||||
new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true,
|
||||
BufferSize = 131072,
|
||||
CheckCrc = true, // Default: validate payload checksums when available
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Iterate over all files from a Rar file using RarArchive
|
||||
|
||||
```C#
|
||||
using (var archive = RarArchive.OpenArchive("Test.rar"))
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
Console.WriteLine($"{entry.Key}: {entry.Size} bytes");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Extract solid Rar or 7Zip archives with progress reporting
|
||||
|
||||
For optimal performance with solid Rar and 7Zip archives, extract entries sequentially. `WriteToDirectory` handles that internally for simple extraction. Use `ExtractAllEntries` when you need to manually iterate in sequential order.
|
||||
|
||||
```C#
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
var progress = new Progress<ProgressReport>(report =>
|
||||
{
|
||||
Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%");
|
||||
});
|
||||
|
||||
using (var archive = RarArchive.OpenArchive("archive.rar",
|
||||
ReaderOptions.ForFilePath
|
||||
.WithProgress(progress)))
|
||||
{
|
||||
archive.WriteToDirectory(
|
||||
@"D:\output",
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true, CheckCrc = true }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Manual sequential extraction:
|
||||
|
||||
```C#
|
||||
using (var archive = RarArchive.OpenArchive("archive.rar",
|
||||
ReaderOptions.ForFilePath.WithProgress(progress)))
|
||||
using (var reader = archive.ExtractAllEntries())
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
reader.WriteEntryToDirectory(@"D:\output");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use ReaderFactory to autodetect archive type and Open the entry stream
|
||||
|
||||
```C#
|
||||
using (Stream stream = File.OpenRead("Tar.tar.bz2"))
|
||||
using (var reader = ReaderFactory.OpenReader(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Console.WriteLine(reader.Entry.Key);
|
||||
reader.WriteEntryToDirectory(@"C:\temp");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use ReaderFactory to autodetect archive type and Open the entry stream
|
||||
|
||||
```C#
|
||||
using (Stream stream = File.OpenRead("Tar.tar.bz2"))
|
||||
using (var reader = ReaderFactory.OpenReader(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
using (var entryStream = reader.OpenEntryStream())
|
||||
{
|
||||
entryStream.CopyTo(...);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use WriterFactory to write all files from a directory in a streaming manner.
|
||||
|
||||
```C#
|
||||
using (Stream stream = File.OpenWrite("C:\\temp.tgz"))
|
||||
using (var writer = WriterFactory.OpenWriter(
|
||||
stream,
|
||||
ArchiveType.Tar,
|
||||
WriterOptions.ForTar(CompressionType.GZip).WithLeaveStreamOpen(true)))
|
||||
{
|
||||
writer.WriteAll("D:\\temp", "*", SearchOption.AllDirectories);
|
||||
}
|
||||
```
|
||||
|
||||
### Use ArchiveFactory to autodetect and open archives
|
||||
|
||||
```C#
|
||||
if (ArchiveFactory.IsArchive("archive.zip", out var archiveType))
|
||||
{
|
||||
Console.WriteLine($"Detected {archiveType}");
|
||||
}
|
||||
|
||||
using (var archive = ArchiveFactory.OpenArchive("archive.zip"))
|
||||
{
|
||||
archive.WriteToDirectory(@"D:\output");
|
||||
}
|
||||
```
|
||||
|
||||
### Use ArchiveInformation to choose the right API
|
||||
|
||||
```C#
|
||||
var archivePath = "archive.arc";
|
||||
var info = ArchiveFactory.GetArchiveInformation(archivePath);
|
||||
if (info is null)
|
||||
{
|
||||
Console.WriteLine("Not a supported archive");
|
||||
}
|
||||
else if (info.SupportsRandomAccess)
|
||||
{
|
||||
using var archive = ArchiveFactory.OpenArchive(archivePath);
|
||||
archive.WriteToDirectory(@"D:\output");
|
||||
}
|
||||
else
|
||||
{
|
||||
using var reader = ReaderFactory.OpenReader(archivePath);
|
||||
reader.WriteAllToDirectory(@"D:\output");
|
||||
}
|
||||
```
|
||||
|
||||
`SupportsRandomAccess` is `false` for reader-only formats such as Ace, Arc, Arj, and standalone LZW. Use the Reader API for those formats.
|
||||
|
||||
### Open multi-volume archives
|
||||
|
||||
```C#
|
||||
var parts = ArchiveFactory.GetFileParts("archive.part1.rar")
|
||||
.Select(path => new FileInfo(path))
|
||||
.ToArray();
|
||||
|
||||
using (var archive = ArchiveFactory.OpenArchive(parts))
|
||||
{
|
||||
archive.WriteToDirectory(@"D:\output");
|
||||
}
|
||||
```
|
||||
|
||||
### Use ReaderOptions for self-extracting archives and detection hints
|
||||
|
||||
```C#
|
||||
var sfxOptions = ReaderOptions.ForSelfExtractingArchive("password");
|
||||
using (var archive = RarArchive.OpenArchive("setup.exe", sfxOptions))
|
||||
{
|
||||
archive.WriteToDirectory(@"D:\output");
|
||||
}
|
||||
|
||||
using (Stream stream = File.OpenRead("backup"))
|
||||
using (var reader = ReaderFactory.OpenReader(
|
||||
stream,
|
||||
ReaderOptions.ForExternalStream.WithExtensionHint("tar.gz")))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
reader.WriteEntryToDirectory(@"D:\output");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Write ZIP entries with per-entry options
|
||||
|
||||
```C#
|
||||
using Stream archiveStream = File.Create("output.zip");
|
||||
using var writer = new ZipWriter(
|
||||
archiveStream,
|
||||
new ZipWriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
ArchiveComment = "Archive comment",
|
||||
UseZip64 = true,
|
||||
});
|
||||
|
||||
using Stream source = File.OpenRead("input.txt");
|
||||
writer.Write("entry.txt", source, new ZipWriterEntryOptions
|
||||
{
|
||||
CompressionType = CompressionType.ZStandard,
|
||||
CompressionLevel = 3,
|
||||
EntryComment = "Entry comment",
|
||||
ModificationDateTime = DateTime.UtcNow,
|
||||
EnableZip64 = true,
|
||||
});
|
||||
```
|
||||
|
||||
### Write a 7z archive
|
||||
|
||||
```C#
|
||||
using Stream stream = File.Create("output.7z");
|
||||
using var writer = WriterFactory.OpenWriter(
|
||||
stream,
|
||||
ArchiveType.SevenZip,
|
||||
new SevenZipWriterOptions(CompressionType.LZMA2)
|
||||
{
|
||||
CompressHeader = true,
|
||||
});
|
||||
|
||||
using Stream source = File.OpenRead("input.txt");
|
||||
writer.Write("input.txt", source, DateTime.UtcNow);
|
||||
```
|
||||
|
||||
### Extract zip which has non-utf8 encoded filename(cp932)
|
||||
|
||||
```C#
|
||||
var encoding = Encoding.GetEncoding(932);
|
||||
var opts = new ReaderOptions()
|
||||
.WithArchiveEncoding(new ArchiveEncoding
|
||||
{
|
||||
CustomDecoder = (data, x, y) => encoding.GetString(data)
|
||||
});
|
||||
|
||||
using var archive = ZipArchive.OpenArchive("test.zip", opts);
|
||||
foreach(var entry in archive.Entries)
|
||||
{
|
||||
Console.WriteLine($"{entry.Key}");
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Compression Providers
|
||||
|
||||
By default `ReaderOptions` and `WriterOptions` already include `CompressionProviderRegistry.Default` via their `Providers` property, so you can read and write without touching the registry yet still get SharpCompress’s built-in implementations.
|
||||
|
||||
The configured registry is used consistently across Reader APIs, Writer APIs, Archive APIs, and async entry-stream extraction, including compressed TAR wrappers and ZIP async decompression.
|
||||
|
||||
To replace specific algorithms (for example to use `System.IO.Compression` for GZip or Deflate), create a modified registry and pass it through the same options:
|
||||
|
||||
```C#
|
||||
var customRegistry = CompressionProviderRegistry.Default
|
||||
.With(new SystemGZipCompressionProvider())
|
||||
.With(new SystemDeflateCompressionProvider());
|
||||
|
||||
var readerOptions = ReaderOptions.ForFilePath
|
||||
.WithProviders(customRegistry);
|
||||
using var reader = ReaderFactory.OpenReader(stream, readerOptions);
|
||||
|
||||
var writerOptions = new WriterOptions(CompressionType.GZip)
|
||||
.WithProviders(customRegistry);
|
||||
using var writer = WriterFactory.OpenWriter(outputStream, ArchiveType.GZip, writerOptions);
|
||||
```
|
||||
|
||||
The registry is immutable. `With(provider)` returns a new registry and replaces any existing provider for that provider's `CompressionType`. You can inspect or use providers directly with `GetProvider`, `CreateCompressStream`, `CreateDecompressStream`, and their async/context overloads, but most application code should flow the registry through `ReaderOptions` or `WriterOptions` so archive readers and writers can supply the right `CompressionContext`.
|
||||
|
||||
To implement a custom provider, implement `ICompressionProvider` directly or derive from `CompressionProviderBase` for default async methods. Derive from `DecompressionOnlyProviderBase` for read-only codecs. Providers can use `CompressionContext` for stream size, seekability, reader options, compression properties, and format-specific metadata.
|
||||
|
||||
The registry also exposes `GetCompressingProvider` (now returning `ICompressionProviderHooks`) when a compression format needs pre- or post-stream data (e.g., LZMA/PPMd). Implementations that need extra headers can supply those bytes through the `ICompressionProviderHooks` members while the rest of the API still works through the `Providers` property.
|
||||
|
||||
## Async Examples
|
||||
|
||||
### Async Reader Examples
|
||||
|
||||
**Extract single entry asynchronously:**
|
||||
```C#
|
||||
using Stream stream = File.OpenRead("archive.zip");
|
||||
await using var reader = await ReaderFactory.OpenAsyncReader(stream, cancellationToken: cancellationToken);
|
||||
while (await reader.MoveToNextEntryAsync(cancellationToken))
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
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");
|
||||
await using var reader = await ReaderFactory.OpenAsyncReader(stream, cancellationToken: cancellationToken);
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
@"D:\temp",
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
```
|
||||
|
||||
**Open and process entry stream asynchronously:**
|
||||
```C#
|
||||
await using var archive = await ZipArchive.OpenAsyncArchive("archive.zip", cancellationToken: cancellationToken);
|
||||
await foreach (var entry in archive.EntriesAsync)
|
||||
{
|
||||
if (!entry.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");
|
||||
await using var writer = await WriterFactory.OpenAsyncWriter(archiveStream, ArchiveType.Zip, new WriterOptions(CompressionType.Deflate), cancellationToken);
|
||||
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");
|
||||
await using var writer = await WriterFactory.OpenAsyncWriter(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip), cancellationToken);
|
||||
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");
|
||||
await using var writer = await WriterFactory.OpenAsyncWriter(stream, ArchiveType.Zip, new WriterOptions(CompressionType.Deflate), cts.Token);
|
||||
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#
|
||||
await using var archive = await ZipArchive.OpenAsyncArchive("archive.zip", cancellationToken: cancellationToken);
|
||||
// Simple async extraction - works for all archive types
|
||||
await archive.WriteToDirectoryAsync(
|
||||
@"C:\output",
|
||||
cancellationToken: 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
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "7.0.101",
|
||||
"rollForward": "latestFeature"
|
||||
"version": "10.0.300",
|
||||
"rollForward": "disable"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
opencode.json
Normal file
6
opencode.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"skills": {
|
||||
"paths": [".agents/skills"]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
#if !NETSTANDARD2_0 && !NETSTANDARD2_1 && !NETFRAMEWORK
|
||||
#if !LEGACY_DOTNET
|
||||
#define SUPPORTS_RUNTIME_INTRINSICS
|
||||
#define SUPPORTS_HOTPATH
|
||||
#endif
|
||||
|
|
@ -62,7 +62,7 @@ internal static class Adler32 // From https://github.com/SixLabors/ImageSharp/bl
|
|||
public static int ReduceSum(Vector256<int> accumulator)
|
||||
{
|
||||
// Add upper lane to lower lane.
|
||||
Vector128<int> vsum = Sse2.Add(accumulator.GetLower(), accumulator.GetUpper());
|
||||
var vsum = Sse2.Add(accumulator.GetLower(), accumulator.GetUpper());
|
||||
|
||||
// Add odd to even.
|
||||
vsum = Sse2.Add(vsum, Sse2.Shuffle(vsum, 0b_11_11_01_01));
|
||||
|
|
@ -81,7 +81,7 @@ internal static class Adler32 // From https://github.com/SixLabors/ImageSharp/bl
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int EvenReduceSum(Vector256<int> accumulator)
|
||||
{
|
||||
Vector128<int> vsum = Sse2.Add(accumulator.GetLower(), accumulator.GetUpper()); // add upper lane to lower lane
|
||||
var vsum = Sse2.Add(accumulator.GetLower(), accumulator.GetUpper()); // add upper lane to lower lane
|
||||
vsum = Sse2.Add(vsum, Sse2.Shuffle(vsum, 0b_11_10_11_10)); // add high to low
|
||||
|
||||
// Vector128<int>.ToScalar() isn't optimized pre-net5.0 https://github.com/dotnet/runtime/pull/37882
|
||||
|
|
@ -141,7 +141,7 @@ internal static class Adler32 // From https://github.com/SixLabors/ImageSharp/bl
|
|||
4,
|
||||
3,
|
||||
2,
|
||||
1 // tap2
|
||||
1, // tap2
|
||||
};
|
||||
#endif
|
||||
|
||||
|
|
@ -189,29 +189,29 @@ internal static class Adler32 // From https://github.com/SixLabors/ImageSharp/bl
|
|||
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)]
|
||||
private static unsafe uint CalculateSse(uint adler, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
uint s1 = adler & 0xFFFF;
|
||||
uint s2 = (adler >> 16) & 0xFFFF;
|
||||
var s1 = adler & 0xFFFF;
|
||||
var s2 = (adler >> 16) & 0xFFFF;
|
||||
|
||||
// Process the data in blocks.
|
||||
uint length = (uint)buffer.Length;
|
||||
uint blocks = length / BlockSize;
|
||||
var length = (uint)buffer.Length;
|
||||
var blocks = length / BlockSize;
|
||||
length -= blocks * BlockSize;
|
||||
|
||||
fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer))
|
||||
{
|
||||
fixed (byte* tapPtr = &MemoryMarshal.GetReference(Tap1Tap2))
|
||||
{
|
||||
byte* localBufferPtr = bufferPtr;
|
||||
var localBufferPtr = bufferPtr;
|
||||
|
||||
// _mm_setr_epi8 on x86
|
||||
Vector128<sbyte> tap1 = Sse2.LoadVector128((sbyte*)tapPtr);
|
||||
Vector128<sbyte> tap2 = Sse2.LoadVector128((sbyte*)(tapPtr + 0x10));
|
||||
Vector128<byte> zero = Vector128<byte>.Zero;
|
||||
var tap1 = Sse2.LoadVector128((sbyte*)tapPtr);
|
||||
var tap2 = Sse2.LoadVector128((sbyte*)(tapPtr + 0x10));
|
||||
var zero = Vector128<byte>.Zero;
|
||||
var ones = Vector128.Create((short)1);
|
||||
|
||||
while (blocks > 0)
|
||||
{
|
||||
uint n = NMAX / BlockSize; /* The NMAX constraint. */
|
||||
var n = NMAX / BlockSize; /* The NMAX constraint. */
|
||||
if (n > blocks)
|
||||
{
|
||||
n = blocks;
|
||||
|
|
@ -221,15 +221,15 @@ internal static class Adler32 // From https://github.com/SixLabors/ImageSharp/bl
|
|||
|
||||
// Process n blocks of data. At most NMAX data bytes can be
|
||||
// processed before s2 must be reduced modulo BASE.
|
||||
Vector128<uint> v_ps = Vector128.CreateScalar(s1 * n);
|
||||
Vector128<uint> v_s2 = Vector128.CreateScalar(s2);
|
||||
Vector128<uint> v_s1 = Vector128<uint>.Zero;
|
||||
var v_ps = Vector128.CreateScalar(s1 * n);
|
||||
var v_s2 = Vector128.CreateScalar(s2);
|
||||
var v_s1 = Vector128<uint>.Zero;
|
||||
|
||||
do
|
||||
{
|
||||
// Load 32 input bytes.
|
||||
Vector128<byte> bytes1 = Sse3.LoadDquVector128(localBufferPtr);
|
||||
Vector128<byte> bytes2 = Sse3.LoadDquVector128(localBufferPtr + 0x10);
|
||||
var bytes1 = Sse3.LoadDquVector128(localBufferPtr);
|
||||
var bytes2 = Sse3.LoadDquVector128(localBufferPtr + 0x10);
|
||||
|
||||
// Add previous block byte sum to v_ps.
|
||||
v_ps = Sse2.Add(v_ps, v_s1);
|
||||
|
|
@ -237,11 +237,11 @@ internal static class Adler32 // From https://github.com/SixLabors/ImageSharp/bl
|
|||
// Horizontally add the bytes for s1, multiply-adds the
|
||||
// bytes by [ 32, 31, 30, ... ] for s2.
|
||||
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsUInt32());
|
||||
Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
|
||||
var mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
|
||||
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones).AsUInt32());
|
||||
|
||||
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsUInt32());
|
||||
Vector128<short> mad2 = Ssse3.MultiplyAddAdjacent(bytes2, tap2);
|
||||
var mad2 = Ssse3.MultiplyAddAdjacent(bytes2, tap2);
|
||||
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad2, ones).AsUInt32());
|
||||
|
||||
localBufferPtr += BlockSize;
|
||||
|
|
@ -281,15 +281,15 @@ internal static class Adler32 // From https://github.com/SixLabors/ImageSharp/bl
|
|||
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)]
|
||||
public static unsafe uint CalculateAvx2(uint adler, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
uint s1 = adler & 0xFFFF;
|
||||
uint s2 = (adler >> 16) & 0xFFFF;
|
||||
uint length = (uint)buffer.Length;
|
||||
var s1 = adler & 0xFFFF;
|
||||
var s2 = (adler >> 16) & 0xFFFF;
|
||||
var length = (uint)buffer.Length;
|
||||
|
||||
fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer))
|
||||
{
|
||||
byte* localBufferPtr = bufferPtr;
|
||||
var localBufferPtr = bufferPtr;
|
||||
|
||||
Vector256<byte> zero = Vector256<byte>.Zero;
|
||||
var zero = Vector256<byte>.Zero;
|
||||
var dot3v = Vector256.Create((short)1);
|
||||
var dot2v = Vector256.Create(
|
||||
32,
|
||||
|
|
@ -333,29 +333,29 @@ internal static class Adler32 // From https://github.com/SixLabors/ImageSharp/bl
|
|||
|
||||
while (length >= 32)
|
||||
{
|
||||
int k = length < NMAX ? (int)length : (int)NMAX;
|
||||
var k = length < NMAX ? (int)length : (int)NMAX;
|
||||
k -= k % 32;
|
||||
length -= (uint)k;
|
||||
|
||||
Vector256<uint> vs10 = vs1;
|
||||
Vector256<uint> vs3 = Vector256<uint>.Zero;
|
||||
var vs10 = vs1;
|
||||
var vs3 = Vector256<uint>.Zero;
|
||||
|
||||
while (k >= 32)
|
||||
{
|
||||
// Load 32 input bytes.
|
||||
Vector256<byte> block = Avx.LoadVector256(localBufferPtr);
|
||||
var block = Avx.LoadVector256(localBufferPtr);
|
||||
|
||||
// Sum of abs diff, resulting in 2 x int32's
|
||||
Vector256<ushort> vs1sad = Avx2.SumAbsoluteDifferences(block, zero);
|
||||
var vs1sad = Avx2.SumAbsoluteDifferences(block, zero);
|
||||
|
||||
vs1 = Avx2.Add(vs1, vs1sad.AsUInt32());
|
||||
vs3 = Avx2.Add(vs3, vs10);
|
||||
|
||||
// sum 32 uint8s to 16 shorts.
|
||||
Vector256<short> vshortsum2 = Avx2.MultiplyAddAdjacent(block, dot2v);
|
||||
var vshortsum2 = Avx2.MultiplyAddAdjacent(block, dot2v);
|
||||
|
||||
// sum 16 shorts to 8 uint32s.
|
||||
Vector256<int> vsum2 = Avx2.MultiplyAddAdjacent(vshortsum2, dot3v);
|
||||
var vsum2 = Avx2.MultiplyAddAdjacent(vshortsum2, dot3v);
|
||||
|
||||
vs2 = Avx2.Add(vsum2.AsUInt32(), vs2);
|
||||
vs10 = vs1;
|
||||
|
|
@ -434,14 +434,14 @@ internal static class Adler32 // From https://github.com/SixLabors/ImageSharp/bl
|
|||
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)]
|
||||
private static unsafe uint CalculateScalar(uint adler, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
uint s1 = adler & 0xFFFF;
|
||||
uint s2 = (adler >> 16) & 0xFFFF;
|
||||
var s1 = adler & 0xFFFF;
|
||||
var s2 = (adler >> 16) & 0xFFFF;
|
||||
uint k;
|
||||
|
||||
fixed (byte* bufferPtr = buffer)
|
||||
{
|
||||
var localBufferPtr = bufferPtr;
|
||||
uint length = (uint)buffer.Length;
|
||||
var length = (uint)buffer.Length;
|
||||
|
||||
while (length > 0)
|
||||
{
|
||||
|
|
|
|||
105
src/SharpCompress/Archives/AbstractArchive.Async.cs
Normal file
105
src/SharpCompress/Archives/AbstractArchive.Async.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public abstract partial class AbstractArchive<TEntry, TVolume>
|
||||
where TEntry : IArchiveEntry
|
||||
where TVolume : IVolume
|
||||
{
|
||||
#region Async Support
|
||||
|
||||
// Async properties
|
||||
public virtual IAsyncEnumerable<TEntry> EntriesAsync => _lazyEntriesAsync;
|
||||
|
||||
public IAsyncEnumerable<TVolume> VolumesAsync => _lazyVolumesAsync;
|
||||
|
||||
protected virtual async IAsyncEnumerable<TEntry> LoadEntriesAsync(
|
||||
IAsyncEnumerable<TVolume> volumes
|
||||
)
|
||||
{
|
||||
foreach (var item in LoadEntries(await volumes.ToListAsync().ConfigureAwait(false)))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
await foreach (var v in _lazyVolumesAsync.ConfigureAwait(false))
|
||||
{
|
||||
v.Dispose();
|
||||
}
|
||||
foreach (var v in _lazyEntriesAsync.GetLoaded().Cast<Entry>())
|
||||
{
|
||||
v.Close();
|
||||
}
|
||||
_sourceStream?.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask EnsureEntriesLoadedAsync()
|
||||
{
|
||||
await _lazyEntriesAsync.EnsureFullyLoaded().ConfigureAwait(false);
|
||||
await _lazyVolumesAsync.EnsureFullyLoaded().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<IArchiveEntry> EntriesAsyncCast()
|
||||
{
|
||||
await foreach (var entry in EntriesAsync.ConfigureAwait(false))
|
||||
{
|
||||
yield return entry;
|
||||
}
|
||||
}
|
||||
|
||||
IAsyncEnumerable<IArchiveEntry> IAsyncArchive.EntriesAsync => EntriesAsyncCast();
|
||||
|
||||
IAsyncEnumerable<IVolume> IAsyncArchive.VolumesAsync => VolumesAsyncCast();
|
||||
|
||||
private async IAsyncEnumerable<IVolume> VolumesAsyncCast()
|
||||
{
|
||||
await foreach (var volume in _lazyVolumesAsync.ConfigureAwait(false))
|
||||
{
|
||||
yield return volume;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<IAsyncReader> ExtractAllEntriesAsync()
|
||||
{
|
||||
if (!await IsSolidAsync().ConfigureAwait(false) && Type != ArchiveType.SevenZip)
|
||||
{
|
||||
throw new SharpCompressException(
|
||||
"ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)."
|
||||
);
|
||||
}
|
||||
await EnsureEntriesLoadedAsync().ConfigureAwait(false);
|
||||
return await CreateReaderForSolidExtractionAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public virtual ValueTask<bool> IsSolidAsync() => new(false);
|
||||
|
||||
public async ValueTask<bool> IsCompleteAsync()
|
||||
{
|
||||
await EnsureEntriesLoadedAsync().ConfigureAwait(false);
|
||||
return await EntriesAsync.AllAsync(x => x.IsComplete).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask<long> TotalSizeAsync() =>
|
||||
await EntriesAsync
|
||||
.AggregateAsync(0L, (total, cf) => total + cf.CompressedSize)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
public async ValueTask<long> TotalUncompressedSizeAsync() =>
|
||||
await EntriesAsync.AggregateAsync(0L, (total, cf) => total + cf.Size).ConfigureAwait(false);
|
||||
|
||||
public ValueTask<bool> IsEncryptedAsync() => new(IsEncrypted);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -1,76 +1,67 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtractionListener
|
||||
public abstract partial class AbstractArchive<TEntry, TVolume> : IArchive, IAsyncArchive
|
||||
where TEntry : IArchiveEntry
|
||||
where TVolume : IVolume
|
||||
{
|
||||
private readonly LazyReadOnlyCollection<TVolume> lazyVolumes;
|
||||
private readonly LazyReadOnlyCollection<TEntry> lazyEntries;
|
||||
private readonly LazyReadOnlyCollection<TVolume> _lazyVolumes;
|
||||
private readonly LazyReadOnlyCollection<TEntry> _lazyEntries;
|
||||
private bool _disposed;
|
||||
private readonly SourceStream? _sourceStream;
|
||||
|
||||
public event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>>? EntryExtractionBegin;
|
||||
public event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>>? EntryExtractionEnd;
|
||||
// Async fields - kept in original file per refactoring rules
|
||||
private readonly LazyAsyncReadOnlyCollection<TVolume> _lazyVolumesAsync;
|
||||
private readonly LazyAsyncReadOnlyCollection<TEntry> _lazyEntriesAsync;
|
||||
|
||||
public event EventHandler<CompressedBytesReadEventArgs>? CompressedBytesRead;
|
||||
public event EventHandler<FilePartExtractionBeginEventArgs>? FilePartExtractionBegin;
|
||||
public ReaderOptions ReaderOptions { get; protected set; }
|
||||
|
||||
protected ReaderOptions ReaderOptions { get; }
|
||||
|
||||
private bool disposed;
|
||||
protected SourceStream SrcStream;
|
||||
|
||||
internal AbstractArchive(ArchiveType type, SourceStream srcStream)
|
||||
internal AbstractArchive(ArchiveType type, SourceStream sourceStream)
|
||||
{
|
||||
Type = type;
|
||||
ReaderOptions = srcStream.ReaderOptions;
|
||||
SrcStream = srcStream;
|
||||
lazyVolumes = new LazyReadOnlyCollection<TVolume>(LoadVolumes(SrcStream));
|
||||
lazyEntries = new LazyReadOnlyCollection<TEntry>(LoadEntries(Volumes));
|
||||
ReaderOptions = sourceStream.ReaderOptions;
|
||||
_sourceStream = sourceStream;
|
||||
_lazyVolumes = new LazyReadOnlyCollection<TVolume>(LoadVolumes(_sourceStream));
|
||||
_lazyEntries = new LazyReadOnlyCollection<TEntry>(LoadEntries(Volumes));
|
||||
_lazyVolumesAsync = new LazyAsyncReadOnlyCollection<TVolume>(
|
||||
LoadVolumesAsync(_sourceStream)
|
||||
);
|
||||
_lazyEntriesAsync = new LazyAsyncReadOnlyCollection<TEntry>(
|
||||
LoadEntriesAsync(_lazyVolumesAsync)
|
||||
);
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
internal AbstractArchive(ArchiveType type)
|
||||
{
|
||||
Type = type;
|
||||
lazyVolumes = new LazyReadOnlyCollection<TVolume>(Enumerable.Empty<TVolume>());
|
||||
lazyEntries = new LazyReadOnlyCollection<TEntry>(Enumerable.Empty<TEntry>());
|
||||
ReaderOptions = ReaderOptions.Default;
|
||||
_lazyVolumes = new LazyReadOnlyCollection<TVolume>(Enumerable.Empty<TVolume>());
|
||||
_lazyEntries = new LazyReadOnlyCollection<TEntry>(Enumerable.Empty<TEntry>());
|
||||
_lazyVolumesAsync = new LazyAsyncReadOnlyCollection<TVolume>(
|
||||
AsyncEnumerableEx.Empty<TVolume>()
|
||||
);
|
||||
_lazyEntriesAsync = new LazyAsyncReadOnlyCollection<TEntry>(
|
||||
AsyncEnumerableEx.Empty<TEntry>()
|
||||
);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
|
||||
public ArchiveType Type { get; }
|
||||
|
||||
void IArchiveExtractionListener.FireEntryExtractionBegin(IArchiveEntry entry) =>
|
||||
EntryExtractionBegin?.Invoke(this, new ArchiveExtractionEventArgs<IArchiveEntry>(entry));
|
||||
|
||||
void IArchiveExtractionListener.FireEntryExtractionEnd(IArchiveEntry entry) =>
|
||||
EntryExtractionEnd?.Invoke(this, new ArchiveExtractionEventArgs<IArchiveEntry>(entry));
|
||||
|
||||
private static Stream CheckStreams(Stream stream)
|
||||
{
|
||||
if (!stream.CanSeek || !stream.CanRead)
|
||||
{
|
||||
throw new ArgumentException("Archive streams must be Readable and Seekable");
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an ReadOnlyCollection of all the RarArchiveEntries across the one or many parts of the RarArchive.
|
||||
/// </summary>
|
||||
public virtual ICollection<TEntry> Entries => lazyEntries;
|
||||
public virtual ICollection<TEntry> Entries => _lazyEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Returns an ReadOnlyCollection of all the RarArchiveVolumes across the one or many parts of the RarArchive.
|
||||
/// </summary>
|
||||
public ICollection<TVolume> Volumes => lazyVolumes;
|
||||
public ICollection<TVolume> Volumes => _lazyVolumes;
|
||||
|
||||
/// <summary>
|
||||
/// The total size of the files compressed in the archive.
|
||||
|
|
@ -81,60 +72,37 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
|||
/// <summary>
|
||||
/// The total size of the files as uncompressed in the archive.
|
||||
/// </summary>
|
||||
public virtual long TotalUncompressSize =>
|
||||
public virtual long TotalUncompressedSize =>
|
||||
Entries.Aggregate(0L, (total, cf) => total + cf.Size);
|
||||
|
||||
protected abstract IEnumerable<TVolume> LoadVolumes(SourceStream srcStream);
|
||||
protected abstract IEnumerable<TVolume> LoadVolumes(SourceStream sourceStream);
|
||||
protected abstract IEnumerable<TEntry> LoadEntries(IEnumerable<TVolume> volumes);
|
||||
|
||||
protected virtual IAsyncEnumerable<TVolume> LoadVolumesAsync(SourceStream sourceStream) =>
|
||||
LoadVolumes(sourceStream).ToAsyncEnumerable();
|
||||
|
||||
IEnumerable<IArchiveEntry> IArchive.Entries => Entries.Cast<IArchiveEntry>();
|
||||
|
||||
IEnumerable<IVolume> IArchive.Volumes => lazyVolumes.Cast<IVolume>();
|
||||
IEnumerable<IVolume> IArchive.Volumes => _lazyVolumes.Cast<IVolume>();
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
if (!disposed)
|
||||
if (!_disposed)
|
||||
{
|
||||
lazyVolumes.ForEach(v => v.Dispose());
|
||||
lazyEntries.GetLoaded().Cast<Entry>().ForEach(x => x.Close());
|
||||
SrcStream?.Dispose();
|
||||
_lazyVolumes.ForEach(v => v.Dispose());
|
||||
_lazyEntries.GetLoaded().Cast<Entry>().ForEach(x => x.Close());
|
||||
_sourceStream?.Dispose();
|
||||
|
||||
disposed = true;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void IArchiveExtractionListener.EnsureEntriesLoaded()
|
||||
private void EnsureEntriesLoaded()
|
||||
{
|
||||
lazyEntries.EnsureFullyLoaded();
|
||||
lazyVolumes.EnsureFullyLoaded();
|
||||
_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
|
||||
|
|
@ -148,17 +116,29 @@ 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();
|
||||
}
|
||||
|
||||
protected abstract IReader CreateReaderForSolidExtraction();
|
||||
protected abstract ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files).
|
||||
/// </summary>
|
||||
public virtual bool IsSolid => false;
|
||||
|
||||
/// <summary>
|
||||
/// Archive is ENCRYPTED (this means the Archive has password-protected files).
|
||||
/// </summary>
|
||||
public virtual bool IsEncrypted => false;
|
||||
|
||||
/// <summary>
|
||||
/// The archive can find all the parts of the archive needed to fully extract the archive. This forces the parsing of the entire archive.
|
||||
/// </summary>
|
||||
|
|
@ -166,7 +146,7 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
|||
{
|
||||
get
|
||||
{
|
||||
((IArchiveExtractionListener)this).EnsureEntriesLoaded();
|
||||
EnsureEntriesLoaded();
|
||||
return Entries.All(x => x.IsComplete);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
140
src/SharpCompress/Archives/AbstractWritableArchive.Async.cs
Normal file
140
src/SharpCompress/Archives/AbstractWritableArchive.Async.cs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
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.Options;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public abstract partial class AbstractWritableArchive<TEntry, TVolume, TOptions>
|
||||
where TEntry : IArchiveEntry
|
||||
where TVolume : IVolume
|
||||
where TOptions : IWriterOptions
|
||||
{
|
||||
// Async property moved from main file
|
||||
private IAsyncEnumerable<TEntry> OldEntriesAsync =>
|
||||
base.EntriesAsync.Where(x => !removedEntries.Contains(x));
|
||||
|
||||
private async ValueTask RebuildModifiedCollectionAsync()
|
||||
{
|
||||
if (pauseRebuilding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
hasModifications = true;
|
||||
newEntries.RemoveAll(v => removedEntries.Contains(v));
|
||||
modifiedEntries.Clear();
|
||||
await foreach (var entry in OldEntriesAsync.ConfigureAwait(false))
|
||||
{
|
||||
modifiedEntries.Add(entry);
|
||||
}
|
||||
modifiedEntries.AddRange(newEntries);
|
||||
}
|
||||
|
||||
public async ValueTask RemoveEntryAsync(TEntry entry)
|
||||
{
|
||||
if (!removedEntries.Contains(entry))
|
||||
{
|
||||
removedEntries.Add(entry);
|
||||
await RebuildModifiedCollectionAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<bool> DoesKeyMatchExistingAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
await foreach (
|
||||
var entry in EntriesAsync.WithCancellation(cancellationToken).ConfigureAwait(false)
|
||||
)
|
||||
{
|
||||
var path = entry.Key;
|
||||
if (path is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var p = path.Replace('/', '\\');
|
||||
if (p.Length > 0 && p[0] == '\\')
|
||||
{
|
||||
p = p.Substring(1);
|
||||
}
|
||||
return string.Equals(p, key, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async ValueTask<TEntry> AddEntryAsync(
|
||||
string key,
|
||||
Stream source,
|
||||
bool closeStream,
|
||||
long size = 0,
|
||||
DateTime? modified = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (key.Length > 0 && key[0] is '/' or '\\')
|
||||
{
|
||||
key = key.Substring(1);
|
||||
}
|
||||
if (await DoesKeyMatchExistingAsync(key, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
throw new ArchiveException("Cannot add entry with duplicate key: " + key);
|
||||
}
|
||||
var entry = CreateEntry(key, source, size, modified, closeStream);
|
||||
newEntries.Add(entry);
|
||||
await RebuildModifiedCollectionAsync().ConfigureAwait(false);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public async ValueTask<TEntry> AddDirectoryEntryAsync(
|
||||
string key,
|
||||
DateTime? modified = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (key.Length > 0 && key[0] is '/' or '\\')
|
||||
{
|
||||
key = key.Substring(1);
|
||||
}
|
||||
if (await DoesKeyMatchExistingAsync(key, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
throw new ArchiveException("Cannot add entry with duplicate key: " + key);
|
||||
}
|
||||
var entry = CreateDirectoryEntry(key, modified);
|
||||
newEntries.Add(entry);
|
||||
await RebuildModifiedCollectionAsync().ConfigureAwait(false);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public async ValueTask SaveToAsync(
|
||||
Stream stream,
|
||||
TOptions options,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//reset streams of new entries
|
||||
newEntries.Cast<IWritableArchiveEntry>().ForEach(x => x.Stream.Seek(0, SeekOrigin.Begin));
|
||||
await SaveToAsync(stream, options, OldEntriesAsync, newEntries, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
newEntries.Cast<Entry>().ForEach(x => x.Close());
|
||||
removedEntries.Cast<Entry>().ForEach(x => x.Close());
|
||||
modifiedEntries.Cast<Entry>().ForEach(x => x.Close());
|
||||
}
|
||||
|
||||
protected abstract ValueTask SaveToAsync(
|
||||
Stream stream,
|
||||
TOptions options,
|
||||
IAsyncEnumerable<TEntry> oldEntries,
|
||||
IEnumerable<TEntry> newEntries,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
|
|
@ -2,23 +2,28 @@ 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.Options;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Writers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public abstract class AbstractWritableArchive<TEntry, TVolume>
|
||||
public abstract partial class AbstractWritableArchive<TEntry, TVolume, TOptions>
|
||||
: AbstractArchive<TEntry, TVolume>,
|
||||
IWritableArchive
|
||||
IWritableArchive<TOptions>,
|
||||
IWritableAsyncArchive<TOptions>
|
||||
where TEntry : IArchiveEntry
|
||||
where TVolume : IVolume
|
||||
where TOptions : IWriterOptions
|
||||
{
|
||||
private class RebuildPauseDisposable : IDisposable
|
||||
{
|
||||
private readonly AbstractWritableArchive<TEntry, TVolume> archive;
|
||||
private readonly AbstractWritableArchive<TEntry, TVolume, TOptions> archive;
|
||||
|
||||
public RebuildPauseDisposable(AbstractWritableArchive<TEntry, TVolume> archive)
|
||||
public RebuildPauseDisposable(AbstractWritableArchive<TEntry, TVolume, TOptions> archive)
|
||||
{
|
||||
this.archive = archive;
|
||||
archive.pauseRebuilding = true;
|
||||
|
|
@ -31,18 +36,18 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
|
|||
}
|
||||
}
|
||||
|
||||
private readonly List<TEntry> newEntries = new List<TEntry>();
|
||||
private readonly List<TEntry> removedEntries = new List<TEntry>();
|
||||
private readonly List<TEntry> newEntries = new();
|
||||
private readonly List<TEntry> removedEntries = new();
|
||||
|
||||
private readonly List<TEntry> modifiedEntries = new List<TEntry>();
|
||||
private readonly List<TEntry> modifiedEntries = new();
|
||||
private bool hasModifications;
|
||||
private bool pauseRebuilding;
|
||||
|
||||
internal AbstractWritableArchive(ArchiveType type)
|
||||
: base(type) { }
|
||||
|
||||
internal AbstractWritableArchive(ArchiveType type, SourceStream srcStream)
|
||||
: base(type, srcStream) { }
|
||||
internal AbstractWritableArchive(ArchiveType type, SourceStream sourceStream)
|
||||
: base(type, sourceStream) { }
|
||||
|
||||
public override ICollection<TEntry> Entries
|
||||
{
|
||||
|
|
@ -94,6 +99,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,
|
||||
|
|
@ -120,6 +128,10 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
|
|||
{
|
||||
foreach (var path in Entries.Select(x => x.Key))
|
||||
{
|
||||
if (path is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var p = path.Replace('/', '\\');
|
||||
if (p.Length > 0 && p[0] == '\\')
|
||||
{
|
||||
|
|
@ -130,7 +142,43 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
|
|||
return false;
|
||||
}
|
||||
|
||||
public void SaveTo(Stream stream, WriterOptions options)
|
||||
ValueTask IWritableAsyncArchive.RemoveEntryAsync(IArchiveEntry entry) =>
|
||||
RemoveEntryAsync((TEntry)entry);
|
||||
|
||||
async ValueTask<IArchiveEntry> IWritableAsyncArchive.AddEntryAsync(
|
||||
string key,
|
||||
Stream source,
|
||||
bool closeStream,
|
||||
long size,
|
||||
DateTime? modified,
|
||||
CancellationToken cancellationToken
|
||||
) =>
|
||||
await AddEntryAsync(key, source, closeStream, size, modified, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
async ValueTask<IArchiveEntry> IWritableAsyncArchive.AddDirectoryEntryAsync(
|
||||
string key,
|
||||
DateTime? modified,
|
||||
CancellationToken cancellationToken
|
||||
) => await AddDirectoryEntryAsync(key, modified, cancellationToken).ConfigureAwait(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, TOptions options)
|
||||
{
|
||||
//reset streams of new entries
|
||||
newEntries.Cast<IWritableArchiveEntry>().ForEach(x => x.Stream.Seek(0, SeekOrigin.Begin));
|
||||
|
|
@ -147,7 +195,7 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
|
|||
{
|
||||
if (!source.CanRead || !source.CanSeek)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
throw new ArchiveException(
|
||||
"Streams must be readable and seekable to use the Writing Archive API"
|
||||
);
|
||||
}
|
||||
|
|
@ -162,9 +210,11 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
|
|||
bool closeStream
|
||||
);
|
||||
|
||||
protected abstract TEntry CreateDirectoryEntry(string key, DateTime? modified);
|
||||
|
||||
protected abstract void SaveTo(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
TOptions options,
|
||||
IEnumerable<TEntry> oldEntries,
|
||||
IEnumerable<TEntry> newEntries
|
||||
);
|
||||
|
|
|
|||
110
src/SharpCompress/Archives/ArchiveFactory.Async.cs
Normal file
110
src/SharpCompress/Archives/ArchiveFactory.Async.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public static partial class ArchiveFactory
|
||||
{
|
||||
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
readerOptions ??= ReaderOptions.ForExternalStream;
|
||||
var factory = await FindFactoryAsync<IArchiveFactory>(stream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await factory
|
||||
.OpenAsyncArchive(stream, readerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
string filePath,
|
||||
ReaderOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return OpenAsyncArchive(
|
||||
new FileInfo(filePath),
|
||||
options ?? ReaderOptions.ForFilePath,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
FileInfo fileInfo,
|
||||
ReaderOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
options ??= ReaderOptions.ForFilePath;
|
||||
|
||||
var factory = await FindFactoryAsync<IArchiveFactory>(fileInfo, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await factory
|
||||
.OpenAsyncArchive(fileInfo, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var filesArray = fileInfos;
|
||||
if (filesArray.Count == 0)
|
||||
{
|
||||
throw new ArchiveOperationException("No files to open");
|
||||
}
|
||||
|
||||
var fileInfo = filesArray[0];
|
||||
if (filesArray.Count == 1)
|
||||
{
|
||||
return await OpenAsyncArchive(fileInfo, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
options ??= ReaderOptions.ForFilePath;
|
||||
|
||||
var factory = await FindFactoryAsync<IMultiArchiveFactory>(fileInfo, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await factory
|
||||
.OpenAsyncArchive(filesArray, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var streamsArray = streams.RequireReadable().RequireSeekable().ToList();
|
||||
var firstStream = streamsArray[0];
|
||||
if (streamsArray.Count == 1)
|
||||
{
|
||||
return await OpenAsyncArchive(firstStream, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
firstStream.NotNull(nameof(firstStream));
|
||||
options ??= ReaderOptions.ForExternalStream;
|
||||
|
||||
var factory = await FindFactoryAsync<IMultiArchiveFactory>(firstStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await factory
|
||||
.OpenAsyncArchive(streamsArray, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
264
src/SharpCompress/Archives/ArchiveFactory.Detection.cs
Normal file
264
src/SharpCompress/Archives/ArchiveFactory.Detection.cs
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public static partial class ArchiveFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns information about the archive at the given file path asynchronously,
|
||||
/// or <see langword="null"/> if the file is not a recognized archive.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the archive file.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await GetArchiveInformationAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Returns information about the archive at the given file path asynchronously,
|
||||
/// or <see langword="null"/> if the file is not a recognized archive.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the archive file.</param>
|
||||
/// <param name="readerOptions">Options controlling archive detection.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream stream = File.OpenRead(filePath);
|
||||
return await GetArchiveInformationAsync(
|
||||
stream,
|
||||
readerOptions ?? ReaderOptions.ForFilePath,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns information about the archive in the given stream asynchronously,
|
||||
/// or <see langword="null"/> if the stream is not a recognized archive.
|
||||
/// </summary>
|
||||
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await GetArchiveInformationAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Returns information about the archive in the given stream asynchronously,
|
||||
/// or <see langword="null"/> if the stream is not a recognized archive.
|
||||
/// </summary>
|
||||
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
|
||||
/// <param name="readerOptions">Options controlling archive detection.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
var factory = await TryFindFactoryAsync(
|
||||
stream,
|
||||
readerOptions ?? ReaderOptions.ForExternalStream,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
return factory is null
|
||||
? null
|
||||
: new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory);
|
||||
}
|
||||
|
||||
internal static ValueTask<T> FindFactoryAsync<T>(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
where T : IFactory
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return FindFactoryAsync<T>(new FileInfo(filePath), cancellationToken);
|
||||
}
|
||||
|
||||
internal static async ValueTask<T> FindFactoryAsync<T>(
|
||||
FileInfo fileInfo,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
where T : IFactory
|
||||
{
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
using Stream stream = fileInfo.OpenRead();
|
||||
return await FindFactoryAsync<T>(stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static async ValueTask<T> FindFactoryAsync<T>(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
where T : IFactory
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
// Use the shared async detection loop over all factories. If the matched factory
|
||||
// implements T we return it; otherwise (or if nothing matched) we fall through
|
||||
// to the same "unsupported format" exception that the original code produced,
|
||||
// listing the T-typed factories as the hint for the caller.
|
||||
var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
if (factory is T typedFactory)
|
||||
{
|
||||
return typedFactory;
|
||||
}
|
||||
|
||||
var extensions = string.Join(", ", Factory.Factories.OfType<T>().Select(item => item.Name));
|
||||
|
||||
throw new ArchiveOperationException(
|
||||
$"Cannot determine compressed stream type. Supported Archive Formats: {extensions}"
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async counterpart of <see cref="ArchiveFactory.TryFindFactory"/>.
|
||||
/// Iterates all registered factories and returns the first one whose
|
||||
/// <see cref="IFactory.IsArchiveAsync"/> recognises the stream, or <see langword="null"/>.
|
||||
/// Stream position is restored to its value at entry on both success and failure.
|
||||
/// </summary>
|
||||
private static async ValueTask<IFactory?> TryFindFactoryAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken
|
||||
) =>
|
||||
await TryFindFactoryAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
private static async ValueTask<IFactory?> TryFindFactoryAsync(
|
||||
Stream stream,
|
||||
ReaderOptions readerOptions,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var startPosition = stream.Position;
|
||||
|
||||
foreach (var factory in Factory.Factories)
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
var isArchive = await factory
|
||||
.IsArchiveAsync(stream, readerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (isArchive)
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns information about the archive at the given file path,
|
||||
/// or <see langword="null"/> if the file is not a recognized archive.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the archive file.</param>
|
||||
public static ArchiveInformation? GetArchiveInformation(string filePath) =>
|
||||
GetArchiveInformation(filePath, ReaderOptions.ForFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Returns information about the archive at the given file path,
|
||||
/// or <see langword="null"/> if the file is not a recognized archive.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the archive file.</param>
|
||||
/// <param name="readerOptions">Options controlling archive detection.</param>
|
||||
public static ArchiveInformation? GetArchiveInformation(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions
|
||||
)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream stream = File.OpenRead(filePath);
|
||||
return GetArchiveInformation(stream, readerOptions ?? ReaderOptions.ForFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns information about the archive in the given stream,
|
||||
/// or <see langword="null"/> if the stream is not a recognized archive.
|
||||
/// </summary>
|
||||
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
|
||||
public static ArchiveInformation? GetArchiveInformation(Stream stream) =>
|
||||
GetArchiveInformation(stream, ReaderOptions.ForExternalStream);
|
||||
|
||||
/// <summary>
|
||||
/// Returns information about the archive in the given stream,
|
||||
/// or <see langword="null"/> if the stream is not a recognized archive.
|
||||
/// </summary>
|
||||
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
|
||||
/// <param name="readerOptions">Options controlling archive detection.</param>
|
||||
public static ArchiveInformation? GetArchiveInformation(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions
|
||||
)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream);
|
||||
return factory is null
|
||||
? null
|
||||
: new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates all registered factories and returns the first one whose
|
||||
/// <see cref="IFactory.IsArchive"/> recognises the stream, or <see langword="null"/>.
|
||||
/// Stream position is restored to its value at entry on both success and failure.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the shared, seekable-stream detection core used by
|
||||
/// <see cref="FindFactory{T}(Stream)"/>, <see cref="IsArchive(Stream, out ArchiveType?)"/>,
|
||||
/// and <see cref="GetArchiveInformation(Stream)"/>.
|
||||
/// <para>
|
||||
/// <see cref="ReaderFactory.OpenReader(Stream, ReaderOptions)"/> uses a separate code path
|
||||
/// based on <see cref="IO.SharpCompressStream"/> rewindable buffering, which supports
|
||||
/// non-seekable streams and is therefore not unified with this helper.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private static IFactory? TryFindFactory(Stream stream) =>
|
||||
TryFindFactory(stream, ReaderOptions.ForExternalStream);
|
||||
|
||||
private static IFactory? TryFindFactory(Stream stream, ReaderOptions readerOptions)
|
||||
{
|
||||
var startPosition = stream.Position;
|
||||
|
||||
foreach (var factory in Factory.Factories)
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
var isArchive = factory.IsArchive(stream, readerOptions);
|
||||
|
||||
if (isArchive)
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,224 +2,232 @@ 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.Options;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public static class ArchiveFactory
|
||||
public static partial class ArchiveFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Opens an Archive for random access
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
/// <returns></returns>
|
||||
public static IArchive Open(Stream stream, ReaderOptions? readerOptions = null)
|
||||
public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
readerOptions ??= new ReaderOptions();
|
||||
|
||||
return FindFactory<IArchiveFactory>(stream).Open(stream, readerOptions);
|
||||
readerOptions ??= ReaderOptions.ForExternalStream;
|
||||
return FindFactory<IArchiveFactory>(stream).OpenArchive(stream, readerOptions);
|
||||
}
|
||||
|
||||
public static IWritableArchive Create(ArchiveType type)
|
||||
public static IWritableArchive<TOptions> CreateArchive<TOptions>()
|
||||
where TOptions : IWriterOptions
|
||||
{
|
||||
var factory = Factory.Factories
|
||||
.OfType<IWriteableArchiveFactory>()
|
||||
.FirstOrDefault(item => item.KnownArchiveType == type);
|
||||
var factory = Factory
|
||||
.Factories.OfType<IWritableArchiveFactory<TOptions>>()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (factory != null)
|
||||
{
|
||||
return factory.CreateWriteableArchive();
|
||||
return factory.CreateArchive();
|
||||
}
|
||||
|
||||
throw new NotSupportedException("Cannot create Archives of type: " + type);
|
||||
throw new NotSupportedException("Cannot create Archives of type: " + typeof(TOptions));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor expects a filepath to an existing file.
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="options"></param>
|
||||
public static IArchive Open(string filePath, ReaderOptions? options = null)
|
||||
public static IArchive OpenArchive(string filePath, ReaderOptions? options = null)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty(nameof(filePath));
|
||||
return Open(new FileInfo(filePath), options);
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return OpenArchive(new FileInfo(filePath), options ?? ReaderOptions.ForFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with a FileInfo object to an existing file.
|
||||
/// </summary>
|
||||
/// <param name="fileInfo"></param>
|
||||
/// <param name="options"></param>
|
||||
public static IArchive Open(FileInfo fileInfo, ReaderOptions? options = null)
|
||||
public static IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? options = null)
|
||||
{
|
||||
options ??= new ReaderOptions { LeaveStreamOpen = false };
|
||||
options ??= ReaderOptions.ForFilePath;
|
||||
|
||||
return FindFactory<IArchiveFactory>(fileInfo).Open(fileInfo, options);
|
||||
return FindFactory<IArchiveFactory>(fileInfo).OpenArchive(fileInfo, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with IEnumerable FileInfo objects, multi and split support.
|
||||
/// </summary>
|
||||
/// <param name="fileInfos"></param>
|
||||
/// <param name="options"></param>
|
||||
public static IArchive Open(IEnumerable<FileInfo> fileInfos, ReaderOptions? options = null)
|
||||
public static IArchive OpenArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? options = null
|
||||
)
|
||||
{
|
||||
fileInfos.CheckNotNull(nameof(fileInfos));
|
||||
var filesArray = fileInfos.ToArray();
|
||||
if (filesArray.Length == 0)
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var filesArray = fileInfos;
|
||||
if (filesArray.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No files to open");
|
||||
throw new ArchiveOperationException("No files to open");
|
||||
}
|
||||
|
||||
var fileInfo = filesArray[0];
|
||||
if (filesArray.Length == 1)
|
||||
if (filesArray.Count == 1)
|
||||
{
|
||||
return Open(fileInfo, options);
|
||||
return OpenArchive(fileInfo, options);
|
||||
}
|
||||
|
||||
fileInfo.CheckNotNull(nameof(fileInfo));
|
||||
options ??= new ReaderOptions { LeaveStreamOpen = false };
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
options ??= ReaderOptions.ForFilePath;
|
||||
|
||||
return FindFactory<IMultiArchiveFactory>(fileInfo).Open(filesArray, options);
|
||||
return FindFactory<IMultiArchiveFactory>(fileInfo).OpenArchive(filesArray, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with IEnumerable FileInfo objects, multi and split support.
|
||||
/// </summary>
|
||||
/// <param name="streams"></param>
|
||||
/// <param name="options"></param>
|
||||
public static IArchive Open(IEnumerable<Stream> streams, ReaderOptions? options = null)
|
||||
public static IArchive OpenArchive(IReadOnlyList<Stream> streams, ReaderOptions? options = null)
|
||||
{
|
||||
streams.CheckNotNull(nameof(streams));
|
||||
var streamsArray = streams.ToArray();
|
||||
if (streamsArray.Length == 0)
|
||||
var streamsArray = streams.RequireReadable().RequireSeekable().ToList();
|
||||
if (streamsArray.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No streams");
|
||||
throw new ArchiveOperationException("No streams");
|
||||
}
|
||||
|
||||
var firstStream = streamsArray[0];
|
||||
if (streamsArray.Length == 1)
|
||||
if (streamsArray.Count == 1)
|
||||
{
|
||||
return Open(firstStream, options);
|
||||
return OpenArchive(firstStream, options);
|
||||
}
|
||||
|
||||
firstStream.CheckNotNull(nameof(firstStream));
|
||||
options ??= new ReaderOptions();
|
||||
firstStream.NotNull(nameof(firstStream));
|
||||
options ??= ReaderOptions.ForExternalStream;
|
||||
|
||||
return FindFactory<IMultiArchiveFactory>(firstStream).Open(streamsArray, options);
|
||||
return FindFactory<IMultiArchiveFactory>(firstStream).OpenArchive(streamsArray, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory, retaining filename
|
||||
/// </summary>
|
||||
public static void WriteToDirectory(
|
||||
string sourceArchive,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
)
|
||||
{
|
||||
using var archive = Open(sourceArchive);
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
entry.WriteToDirectory(destinationDirectory, options);
|
||||
}
|
||||
using var archive = OpenArchive(sourceArchive);
|
||||
archive.WriteToDirectory(destinationDirectory, options);
|
||||
}
|
||||
|
||||
private static T FindFactory<T>(FileInfo finfo)
|
||||
public static T FindFactory<T>(string filePath)
|
||||
where T : IFactory
|
||||
{
|
||||
finfo.CheckNotNull(nameof(finfo));
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream stream = File.OpenRead(filePath);
|
||||
return FindFactory<T>(stream);
|
||||
}
|
||||
|
||||
public static T FindFactory<T>(FileInfo finfo)
|
||||
where T : IFactory
|
||||
{
|
||||
finfo.NotNull(nameof(finfo));
|
||||
using Stream stream = finfo.OpenRead();
|
||||
return FindFactory<T>(stream);
|
||||
}
|
||||
|
||||
private static T FindFactory<T>(Stream stream)
|
||||
public static T FindFactory<T>(Stream stream)
|
||||
where T : IFactory
|
||||
{
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
if (!stream.CanRead || !stream.CanSeek)
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
// Use the shared detection loop over all factories. If the matched factory
|
||||
// implements T we return it; otherwise (or if nothing matched) we fall through
|
||||
// to the same "unsupported format" exception that the original code produced,
|
||||
// listing the T-typed factories as the hint for the caller.
|
||||
var factory = TryFindFactory(stream);
|
||||
if (factory is T typedFactory)
|
||||
{
|
||||
throw new ArgumentException("Stream should be readable and seekable");
|
||||
return typedFactory;
|
||||
}
|
||||
|
||||
var factories = Factory.Factories.OfType<T>();
|
||||
var extensions = string.Join(", ", Factory.Factories.OfType<T>().Select(item => item.Name));
|
||||
|
||||
var startPosition = stream.Position;
|
||||
|
||||
foreach (var factory in factories)
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
|
||||
if (factory.IsArchive(stream))
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
|
||||
var extensions = string.Join(", ", factories.Select(item => item.Name));
|
||||
|
||||
throw new InvalidOperationException(
|
||||
throw new ArchiveOperationException(
|
||||
$"Cannot determine compressed stream type. Supported Archive Formats: {extensions}"
|
||||
);
|
||||
}
|
||||
|
||||
public static bool IsArchive(string filePath, out ArchiveType? type)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty(nameof(filePath));
|
||||
return IsArchive(filePath, ReaderOptions.ForFilePath, out type);
|
||||
}
|
||||
|
||||
public static bool IsArchive(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions,
|
||||
out ArchiveType? type
|
||||
)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream s = File.OpenRead(filePath);
|
||||
return IsArchive(s, out type);
|
||||
return IsArchive(s, readerOptions ?? ReaderOptions.ForFilePath, out type);
|
||||
}
|
||||
|
||||
public static bool IsArchive(Stream stream, out ArchiveType? type)
|
||||
{
|
||||
type = null;
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
|
||||
if (!stream.CanRead || !stream.CanSeek)
|
||||
{
|
||||
throw new ArgumentException("Stream should be readable and seekable");
|
||||
}
|
||||
|
||||
var startPosition = stream.Position;
|
||||
|
||||
foreach (var factory in Factory.Factories)
|
||||
{
|
||||
stream.Position = startPosition;
|
||||
|
||||
if (factory.IsArchive(stream, null))
|
||||
{
|
||||
type = factory.KnownArchiveType;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return IsArchive(stream, ReaderOptions.ForExternalStream, out type);
|
||||
}
|
||||
|
||||
public static bool IsArchive(Stream stream, ReaderOptions? readerOptions, out ArchiveType? type)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream);
|
||||
type = factory?.KnownArchiveType;
|
||||
return factory is not null;
|
||||
}
|
||||
|
||||
public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await IsArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream stream = File.OpenRead(filePath);
|
||||
return await IsArchiveAsync(
|
||||
stream,
|
||||
readerOptions ?? ReaderOptions.ForFilePath,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await IsArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
var factory = await TryFindFactoryAsync(
|
||||
stream,
|
||||
readerOptions ?? ReaderOptions.ForExternalStream,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
return (factory is not null, factory?.KnownArchiveType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// From a passed in archive (zip, rar, 7z, 001), return all parts.
|
||||
/// </summary>
|
||||
/// <param name="part1"></param>
|
||||
/// <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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// From a passed in archive (zip, rar, 7z, 001), return all parts.
|
||||
/// </summary>
|
||||
/// <param name="part1"></param>
|
||||
/// <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>())
|
||||
|
|
@ -230,7 +238,7 @@ public static class ArchiveFactory
|
|||
if (part != null)
|
||||
{
|
||||
yield return part;
|
||||
while ((part = factory.GetFilePart(i++, part1)) != null) //tests split too
|
||||
while ((part = factory.GetFilePart(i++, part1)) != null)
|
||||
{
|
||||
yield return part;
|
||||
}
|
||||
|
|
|
|||
38
src/SharpCompress/Archives/ArchiveInformation.cs
Normal file
38
src/SharpCompress/Archives/ArchiveInformation.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
/// <summary>
|
||||
/// Contains information about a detected archive, including its type and supported capabilities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use <see cref="ArchiveFactory.GetArchiveInformation(System.IO.Stream)"/> or
|
||||
/// <see cref="ArchiveFactory.GetArchiveInformationAsync(System.IO.Stream,System.Threading.CancellationToken)"/>
|
||||
/// to obtain an instance of this record.
|
||||
/// </remarks>
|
||||
public record ArchiveInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of archive detected, or <see langword="null"/> when the format is not a registered well-known type.
|
||||
/// </summary>
|
||||
public ArchiveType? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see langword="true"/> when this archive format supports random access via the <see cref="IArchive"/> API,
|
||||
/// meaning the full file listing can be retrieved without decompressing the entire archive.
|
||||
/// <see langword="false"/> when only the <see cref="SharpCompress.Readers.IReader"/> API is available,
|
||||
/// which reads entries sequentially and can only report per-entry progress.
|
||||
/// </summary>
|
||||
public bool SupportsRandomAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new archive information instance.
|
||||
/// </summary>
|
||||
/// <param name="type">The detected archive type.</param>
|
||||
/// <param name="supportsRandomAccess">Whether the detected format supports random access.</param>
|
||||
public ArchiveInformation(ArchiveType? type, bool supportsRandomAccess)
|
||||
{
|
||||
Type = type;
|
||||
SupportsRandomAccess = supportsRandomAccess;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
|
|
@ -11,20 +12,27 @@ internal abstract class ArchiveVolumeFactory
|
|||
FileInfo? item = null;
|
||||
|
||||
//split 001, 002 ...
|
||||
Match m = Regex.Match(part1.Name, @"^(.*\.)([0-9]+)$", RegexOptions.IgnoreCase);
|
||||
var m = Regex.Match(part1.Name, @"^(.*\.)([0-9]+)$", RegexOptions.IgnoreCase);
|
||||
if (m.Success)
|
||||
{
|
||||
item = new FileInfo(
|
||||
Path.Combine(
|
||||
part1.DirectoryName!,
|
||||
String.Concat(
|
||||
m.Groups[1].Value,
|
||||
(index + 1).ToString().PadLeft(m.Groups[2].Value.Length, '0')
|
||||
(index + 1)
|
||||
.ToString(Constants.DefaultCultureInfo)
|
||||
.PadLeft(m.Groups[2].Value.Length, '0')
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (item != null && item.Exists)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
93
src/SharpCompress/Archives/GZip/GZipArchive.Async.cs
Normal file
93
src/SharpCompress/Archives/GZip/GZipArchive.Async.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
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.Common.Options;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.GZip;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.GZip;
|
||||
|
||||
namespace SharpCompress.Archives.GZip;
|
||||
|
||||
public partial class GZipArchive
|
||||
{
|
||||
public ValueTask SaveToAsync(string filePath, CancellationToken cancellationToken = default) =>
|
||||
SaveToAsync(new FileInfo(filePath), cancellationToken);
|
||||
|
||||
public async ValueTask SaveToAsync(
|
||||
FileInfo fileInfo,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
|
||||
await SaveToAsync(stream, new GZipWriterOptions(CompressionType.GZip), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override async ValueTask SaveToAsync(
|
||||
Stream stream,
|
||||
GZipWriterOptions options,
|
||||
IAsyncEnumerable<GZipArchiveEntry> oldEntries,
|
||||
IEnumerable<GZipArchiveEntry> newEntries,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (Entries.Count > 1)
|
||||
{
|
||||
throw new InvalidFormatException("Only one entry is allowed in a GZip Archive");
|
||||
}
|
||||
await using var writer = new GZipWriter(stream, options);
|
||||
await foreach (
|
||||
var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false)
|
||||
)
|
||||
{
|
||||
if (!entry.IsDirectory)
|
||||
{
|
||||
using var entryStream = await entry
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await writer
|
||||
.WriteAsync(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
foreach (var entry in newEntries.Where(x => !x.IsDirectory))
|
||||
{
|
||||
using var entryStream = await entry
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await writer
|
||||
.WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync()
|
||||
{
|
||||
var stream = Volumes.Single().Stream;
|
||||
stream.Position = 0;
|
||||
return new((IAsyncReader)GZipReader.OpenReader(stream, ReaderOptions));
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<GZipArchiveEntry> LoadEntriesAsync(
|
||||
IAsyncEnumerable<GZipVolume> volumes
|
||||
)
|
||||
{
|
||||
var stream = (await volumes.SingleAsync().ConfigureAwait(false)).Stream;
|
||||
yield return new GZipArchiveEntry(
|
||||
this,
|
||||
await GZipFilePart
|
||||
.CreateAsync(stream, ReaderOptions.ArchiveEncoding, ReaderOptions.Providers)
|
||||
.ConfigureAwait(false),
|
||||
ReaderOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
199
src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs
Normal file
199
src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Writers.GZip;
|
||||
|
||||
namespace SharpCompress.Archives.GZip;
|
||||
|
||||
public partial class GZipArchive
|
||||
#if NET8_0_OR_GREATER
|
||||
: IWritableArchiveOpenable<GZipWriterOptions>,
|
||||
IMultiArchiveOpenable<
|
||||
IWritableArchive<GZipWriterOptions>,
|
||||
IWritableAsyncArchive<GZipWriterOptions>
|
||||
>
|
||||
#endif
|
||||
{
|
||||
public static ValueTask<IWritableAsyncArchive<GZipWriterOptions>> OpenAsyncArchive(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return OpenAsyncArchive(new FileInfo(filePath), readerOptions, cancellationToken);
|
||||
}
|
||||
|
||||
public static IWritableArchive<GZipWriterOptions> OpenArchive(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return OpenArchive(new FileInfo(filePath), readerOptions ?? ReaderOptions.ForFilePath);
|
||||
}
|
||||
|
||||
public static IWritableArchive<GZipWriterOptions> OpenArchive(
|
||||
FileInfo fileInfo,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
return new GZipArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
|
||||
readerOptions ?? ReaderOptions.ForFilePath
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static IWritableArchive<GZipWriterOptions> OpenArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var files = fileInfos;
|
||||
return new GZipArchive(
|
||||
new SourceStream(
|
||||
files[0],
|
||||
i => i < files.Count ? files[i] : null,
|
||||
readerOptions ?? ReaderOptions.ForFilePath
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static IWritableArchive<GZipWriterOptions> OpenArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
var strms = streams.RequireReadable().RequireSeekable().ToList();
|
||||
return new GZipArchive(
|
||||
new SourceStream(
|
||||
strms[0],
|
||||
i => i < strms.Count ? strms[i] : null,
|
||||
readerOptions ?? ReaderOptions.ForExternalStream
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static IWritableArchive<GZipWriterOptions> OpenArchive(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
return new GZipArchive(
|
||||
new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream)
|
||||
);
|
||||
}
|
||||
|
||||
public static ValueTask<IWritableAsyncArchive<GZipWriterOptions>> OpenAsyncArchive(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IWritableAsyncArchive<GZipWriterOptions>)OpenArchive(stream, readerOptions));
|
||||
}
|
||||
|
||||
public static ValueTask<IWritableAsyncArchive<GZipWriterOptions>> OpenAsyncArchive(
|
||||
FileInfo fileInfo,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IWritableAsyncArchive<GZipWriterOptions>)OpenArchive(fileInfo, readerOptions));
|
||||
}
|
||||
|
||||
public static ValueTask<IWritableAsyncArchive<GZipWriterOptions>> OpenAsyncArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IWritableAsyncArchive<GZipWriterOptions>)OpenArchive(streams, readerOptions));
|
||||
}
|
||||
|
||||
public static ValueTask<IWritableAsyncArchive<GZipWriterOptions>> OpenAsyncArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IWritableAsyncArchive<GZipWriterOptions>)OpenArchive(fileInfos, readerOptions));
|
||||
}
|
||||
|
||||
public static IWritableArchive<GZipWriterOptions> CreateArchive() => new GZipArchive();
|
||||
|
||||
public static ValueTask<IWritableAsyncArchive<GZipWriterOptions>> CreateAsyncArchive() =>
|
||||
new(new GZipArchive());
|
||||
|
||||
public static bool IsGZipFile(string filePath) => IsGZipFile(new FileInfo(filePath));
|
||||
|
||||
public static bool IsGZipFile(FileInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using Stream stream = fileInfo.OpenRead();
|
||||
return IsGZipFile(stream);
|
||||
}
|
||||
|
||||
public static bool IsGZipFile(Stream stream)
|
||||
{
|
||||
Span<byte> header = stackalloc byte[10];
|
||||
|
||||
if (!stream.ReadFully(header))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async ValueTask<bool> IsGZipFileAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var header = ArrayPool<byte>.Shared.Rent(10);
|
||||
try
|
||||
{
|
||||
await stream.ReadFullyAsync(header, 0, 10, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(header);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.GZip;
|
||||
using SharpCompress.Common.Options;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.GZip;
|
||||
|
|
@ -12,116 +13,19 @@ using SharpCompress.Writers.GZip;
|
|||
|
||||
namespace SharpCompress.Archives.GZip;
|
||||
|
||||
public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
public partial class GZipArchive
|
||||
: AbstractWritableArchive<GZipArchiveEntry, GZipVolume, GZipWriterOptions>
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor expects a filepath to an existing file.
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static GZipArchive Open(string filePath, ReaderOptions? readerOptions = null)
|
||||
private GZipArchive(SourceStream sourceStream)
|
||||
: base(ArchiveType.GZip, sourceStream) { }
|
||||
|
||||
internal GZipArchive()
|
||||
: base(ArchiveType.GZip) { }
|
||||
|
||||
protected override IEnumerable<GZipVolume> LoadVolumes(SourceStream sourceStream)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty(nameof(filePath));
|
||||
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with a FileInfo object to an existing file.
|
||||
/// </summary>
|
||||
/// <param name="fileInfo"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static GZipArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
fileInfo.CheckNotNull(nameof(fileInfo));
|
||||
return new GZipArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
|
||||
readerOptions ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with all file parts passed in
|
||||
/// </summary>
|
||||
/// <param name="fileInfos"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static GZipArchive Open(
|
||||
IEnumerable<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.CheckNotNull(nameof(fileInfos));
|
||||
var files = fileInfos.ToArray();
|
||||
return new GZipArchive(
|
||||
new SourceStream(
|
||||
files[0],
|
||||
i => i < files.Length ? files[i] : null,
|
||||
readerOptions ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with all stream parts passed in
|
||||
/// </summary>
|
||||
/// <param name="streams"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static GZipArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
streams.CheckNotNull(nameof(streams));
|
||||
var strms = streams.ToArray();
|
||||
return new GZipArchive(
|
||||
new SourceStream(
|
||||
strms[0],
|
||||
i => i < strms.Length ? strms[i] : null,
|
||||
readerOptions ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a seekable Stream as a source
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static GZipArchive Open(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
return new GZipArchive(
|
||||
new SourceStream(stream, i => null, readerOptions ?? new ReaderOptions())
|
||||
);
|
||||
}
|
||||
|
||||
public static GZipArchive Create() => new GZipArchive();
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with a SourceStream able to handle FileInfo and Streams.
|
||||
/// </summary>
|
||||
/// <param name="srcStream"></param>
|
||||
/// <param name="options"></param>
|
||||
internal GZipArchive(SourceStream srcStream)
|
||||
: base(ArchiveType.Tar, srcStream) { }
|
||||
|
||||
protected override IEnumerable<GZipVolume> LoadVolumes(SourceStream srcStream)
|
||||
{
|
||||
srcStream.LoadAllParts();
|
||||
var idx = 0;
|
||||
return srcStream.Streams.Select(a => new GZipVolume(a, ReaderOptions, idx++));
|
||||
}
|
||||
|
||||
public static bool IsGZipFile(string filePath) => IsGZipFile(new FileInfo(filePath));
|
||||
|
||||
public static bool IsGZipFile(FileInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using Stream stream = fileInfo.OpenRead();
|
||||
return IsGZipFile(stream);
|
||||
sourceStream.LoadAllParts();
|
||||
return sourceStream.Streams.Select(a => new GZipVolume(a, ReaderOptions, 0));
|
||||
}
|
||||
|
||||
public void SaveTo(string filePath) => SaveTo(new FileInfo(filePath));
|
||||
|
|
@ -129,33 +33,11 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
|||
public void SaveTo(FileInfo fileInfo)
|
||||
{
|
||||
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
|
||||
SaveTo(stream, new WriterOptions(CompressionType.GZip));
|
||||
SaveTo(stream, new GZipWriterOptions(CompressionType.GZip));
|
||||
}
|
||||
|
||||
public static bool IsGZipFile(Stream stream)
|
||||
{
|
||||
// read the header on the first read
|
||||
Span<byte> header = stackalloc byte[10];
|
||||
|
||||
// workitem 8501: handle edge case (decompress empty stream)
|
||||
if (!stream.ReadFully(header))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal GZipArchive()
|
||||
: base(ArchiveType.GZip) { }
|
||||
|
||||
protected override GZipArchiveEntry CreateEntryInternal(
|
||||
string filePath,
|
||||
string key,
|
||||
Stream source,
|
||||
long size,
|
||||
DateTime? modified,
|
||||
|
|
@ -164,27 +46,34 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
|||
{
|
||||
if (Entries.Any())
|
||||
{
|
||||
throw new InvalidOperationException("Only one entry is allowed in a GZip Archive");
|
||||
throw new InvalidFormatException("Only one entry is allowed in a GZip Archive");
|
||||
}
|
||||
return new GZipWritableArchiveEntry(this, source, filePath, size, modified, closeStream);
|
||||
return new GZipWritableArchiveEntry(this, source, key, size, modified, closeStream);
|
||||
}
|
||||
|
||||
protected override GZipArchiveEntry CreateDirectoryEntry(string key, DateTime? modified) =>
|
||||
throw new NotSupportedException("GZip archives do not support directory entries.");
|
||||
|
||||
protected override void SaveTo(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
GZipWriterOptions options,
|
||||
IEnumerable<GZipArchiveEntry> oldEntries,
|
||||
IEnumerable<GZipArchiveEntry> newEntries
|
||||
)
|
||||
{
|
||||
if (Entries.Count > 1)
|
||||
{
|
||||
throw new InvalidOperationException("Only one entry is allowed in a GZip Archive");
|
||||
throw new InvalidFormatException("Only one entry is allowed in a GZip Archive");
|
||||
}
|
||||
using var writer = new GZipWriter(stream, new GZipWriterOptions(options));
|
||||
using var writer = new GZipWriter(stream, options);
|
||||
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
writer.Write(entry.Key, entryStream, entry.LastModifiedTime);
|
||||
writer.Write(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
entry.LastModifiedTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,7 +82,8 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
|||
var stream = volumes.Single().Stream;
|
||||
yield return new GZipArchiveEntry(
|
||||
this,
|
||||
new GZipFilePart(stream, ReaderOptions.ArchiveEncoding)
|
||||
GZipFilePart.Create(stream, ReaderOptions.ArchiveEncoding, ReaderOptions.Providers),
|
||||
ReaderOptions
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -201,6 +91,6 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
|||
{
|
||||
var stream = Volumes.Single().Stream;
|
||||
stream.Position = 0;
|
||||
return GZipReader.Open(stream);
|
||||
return GZipReader.OpenReader(stream, ReaderOptions);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,43 @@
|
|||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.GZip;
|
||||
using SharpCompress.Common.Options;
|
||||
|
||||
namespace SharpCompress.Archives.GZip;
|
||||
|
||||
public class GZipArchiveEntry : GZipEntry, IArchiveEntry
|
||||
{
|
||||
internal GZipArchiveEntry(GZipArchive archive, GZipFilePart part)
|
||||
: base(part) => Archive = archive;
|
||||
internal GZipArchiveEntry(GZipArchive archive, GZipFilePart? part, IReaderOptions readerOptions)
|
||||
: base(part, readerOptions) => Archive = archive;
|
||||
|
||||
public virtual Stream OpenEntryStream()
|
||||
{
|
||||
//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();
|
||||
return Parts.Single().GetCompressedStream().NotNull();
|
||||
}
|
||||
|
||||
public virtual async ValueTask<Stream> OpenEntryStreamAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// Reset the stream position if seekable
|
||||
var part = (GZipFilePart)Parts.Single();
|
||||
var rawStream = part.GetRawStream();
|
||||
if (rawStream.CanSeek && rawStream.Position != part.EntryStartPosition)
|
||||
{
|
||||
rawStream.Position = part.EntryStartPosition;
|
||||
}
|
||||
return (
|
||||
await Parts.Single().GetCompressedStreamAsync(cancellationToken).ConfigureAwait(false)
|
||||
).NotNull();
|
||||
}
|
||||
|
||||
#region IArchiveEntry Members
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ internal sealed class GZipWritableArchiveEntry : GZipArchiveEntry, IWritableArch
|
|||
DateTime? lastModified,
|
||||
bool closeStream
|
||||
)
|
||||
: base(archive, null)
|
||||
: base(archive, null, archive.ReaderOptions)
|
||||
{
|
||||
this.stream = stream;
|
||||
Key = path;
|
||||
|
|
@ -32,7 +32,7 @@ internal sealed class GZipWritableArchiveEntry : GZipArchiveEntry, IWritableArch
|
|||
|
||||
public override long Crc => 0;
|
||||
|
||||
public override string Key { get; }
|
||||
public override string? Key { get; }
|
||||
|
||||
public override long CompressedSize => 0;
|
||||
|
||||
|
|
@ -60,7 +60,15 @@ internal sealed class GZipWritableArchiveEntry : GZipArchiveEntry, IWritableArch
|
|||
{
|
||||
//ensure new stream is at the start, this could be reset
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
return NonDisposingStream.Create(stream);
|
||||
return SharpCompressStream.CreateNonDisposing(stream);
|
||||
}
|
||||
|
||||
public override ValueTask<Stream> OpenEntryStreamAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new(OpenEntryStream());
|
||||
}
|
||||
|
||||
internal override void Close()
|
||||
|
|
|
|||
|
|
@ -7,17 +7,16 @@ 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; }
|
||||
|
||||
ArchiveType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The options used when opening this archive.
|
||||
/// </summary>
|
||||
ReaderOptions ReaderOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Use this method to extract all entries in an archive in order.
|
||||
/// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be
|
||||
|
|
@ -44,5 +43,10 @@ public interface IArchive : IDisposable
|
|||
/// <summary>
|
||||
/// The total size of the files as uncompressed in the archive.
|
||||
/// </summary>
|
||||
long TotalUncompressSize { get; }
|
||||
long TotalUncompressedSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the archive is encrypted.
|
||||
/// </summary>
|
||||
bool IsEncrypted { 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>
|
||||
ValueTask<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,65 +9,233 @@ namespace SharpCompress.Archives;
|
|||
|
||||
public static class IArchiveEntryExtensions
|
||||
{
|
||||
public static void WriteTo(this IArchiveEntry archiveEntry, Stream streamToWriteTo)
|
||||
/// <param name="archiveEntry">The archive entry to extract.</param>
|
||||
extension(IArchiveEntry archiveEntry)
|
||||
{
|
||||
if (archiveEntry.IsDirectory)
|
||||
/// <summary>
|
||||
/// Extract entry to the specified stream.
|
||||
/// </summary>
|
||||
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public void WriteTo(Stream streamToWriteTo, IProgress<ProgressReport>? progress = null) =>
|
||||
archiveEntry.WriteTo(streamToWriteTo, bufferSize: null, progress: progress);
|
||||
|
||||
/// <summary>
|
||||
/// Extract entry to the specified stream.
|
||||
/// </summary>
|
||||
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
|
||||
/// <param name="options">Options for configuring extraction behavior.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public void WriteTo(
|
||||
Stream streamToWriteTo,
|
||||
ExtractionOptions options,
|
||||
IProgress<ProgressReport>? progress = null
|
||||
) => archiveEntry.WriteTo(streamToWriteTo, options.BufferSize, options, progress);
|
||||
|
||||
private void WriteTo(
|
||||
Stream streamToWriteTo,
|
||||
int? bufferSize,
|
||||
ExtractionOptions? options = null,
|
||||
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 checkedStream = options is null
|
||||
? entryStream
|
||||
: IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options);
|
||||
var sourceStream = WrapWithProgress(checkedStream, archiveEntry, progress);
|
||||
sourceStream.CopyTo(streamToWriteTo, bufferSize ?? Constants.BufferSize);
|
||||
}
|
||||
|
||||
var streamListener = (IArchiveExtractionListener)archiveEntry.Archive;
|
||||
streamListener.EnsureEntriesLoaded();
|
||||
streamListener.FireEntryExtractionBegin(archiveEntry);
|
||||
streamListener.FireFilePartExtractionBegin(
|
||||
archiveEntry.Key,
|
||||
archiveEntry.Size,
|
||||
archiveEntry.CompressedSize
|
||||
);
|
||||
var entryStream = archiveEntry.OpenEntryStream();
|
||||
if (entryStream is null)
|
||||
/// <summary>
|
||||
/// Extract entry to the specified stream asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public async ValueTask WriteToAsync(
|
||||
Stream streamToWriteTo,
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await archiveEntry
|
||||
.WriteToAsync(
|
||||
streamToWriteTo,
|
||||
Constants.BufferSize,
|
||||
progress: progress,
|
||||
cancellationToken: cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Extract entry to the specified stream asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
|
||||
/// <param name="options">Options for configuring extraction behavior.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async ValueTask WriteToAsync(
|
||||
Stream streamToWriteTo,
|
||||
ExtractionOptions options,
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await archiveEntry
|
||||
.WriteToAsync(
|
||||
streamToWriteTo,
|
||||
options.BufferSize,
|
||||
options,
|
||||
progress,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
private async ValueTask WriteToAsync(
|
||||
Stream streamToWriteTo,
|
||||
int? bufferSize,
|
||||
ExtractionOptions? options = null,
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
return;
|
||||
if (archiveEntry.IsDirectory)
|
||||
{
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
}
|
||||
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await archiveEntry
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
await using var entryStream = await archiveEntry
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
var checkedStream = options is null
|
||||
? entryStream
|
||||
: IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options);
|
||||
var sourceStream = WrapWithProgress(checkedStream, archiveEntry, progress);
|
||||
await sourceStream
|
||||
.CopyToAsync(streamToWriteTo, bufferSize ?? Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
using (entryStream)
|
||||
{
|
||||
using Stream s = new ListeningStream(streamListener, entryStream);
|
||||
s.TransferTo(streamToWriteTo);
|
||||
}
|
||||
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
|
||||
) =>
|
||||
entry.WriteEntryToDirectory(
|
||||
destinationDirectory,
|
||||
options,
|
||||
(path) => entry.WriteToFile(path, options)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory asynchronously, retaining filename
|
||||
/// </summary>
|
||||
public async ValueTask WriteToDirectoryAsync(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await entry
|
||||
.WriteEntryToDirectoryAsync(
|
||||
destinationDirectory,
|
||||
options,
|
||||
async (path, ct) =>
|
||||
await entry.WriteToFileAsync(path, options, ct).ConfigureAwait(false),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file
|
||||
/// </summary>
|
||||
public void WriteToFile(string destinationFileName, ExtractionOptions? options = null)
|
||||
{
|
||||
options ??= new ExtractionOptions();
|
||||
entry.WriteEntryToFile(
|
||||
destinationFileName,
|
||||
options,
|
||||
(x, fm) =>
|
||||
{
|
||||
using var fs = File.Open(x, fm);
|
||||
entry.WriteTo(fs, options?.BufferSize ?? Constants.BufferSize, options, null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file asynchronously
|
||||
/// </summary>
|
||||
public async ValueTask WriteToFileAsync(
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
options ??= new ExtractionOptions();
|
||||
await entry
|
||||
.WriteEntryToFileAsync(
|
||||
destinationFileName,
|
||||
options,
|
||||
async (x, fm, ct) =>
|
||||
{
|
||||
using var fs = File.Open(x, fm);
|
||||
await entry
|
||||
.WriteToAsync(fs, options.BufferSize, options, null, ct)
|
||||
.ConfigureAwait(false);
|
||||
},
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,77 +1,69 @@
|
|||
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;
|
||||
|
||||
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
|
||||
)
|
||||
extension(IArchive archive)
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(x => !x.IsDirectory))
|
||||
/// <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
|
||||
)
|
||||
{
|
||||
entry.WriteToDirectory(destinationDirectory, options);
|
||||
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
|
||||
{
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
reader.WriteAllToDirectory(destinationDirectory, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
archive.WriteToDirectoryInternal(destinationDirectory, options, progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
)
|
||||
{
|
||||
// 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())
|
||||
private void WriteToDirectoryInternal(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
IProgress<ProgressReport>? progress
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
options ??= new ExtractionOptions();
|
||||
var fullDestinationDirectoryPath = DirectoryManagement.GetFullDestinationDirectoryPath(
|
||||
destinationDirectory
|
||||
);
|
||||
|
||||
var entry = entries.Entry;
|
||||
if (entry.IsDirectory)
|
||||
var totalBytes = archive.TotalUncompressedSize;
|
||||
var bytesRead = 0L;
|
||||
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
continue;
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
entry.WriteEntryToDirectoryCore(fullDestinationDirectoryPath, options, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.WriteEntryToDirectoryCore(
|
||||
fullDestinationDirectoryPath,
|
||||
options,
|
||||
path => entry.WriteToFile(path, options)
|
||||
);
|
||||
|
||||
bytesRead += entry.Size;
|
||||
progress?.Report(
|
||||
new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)
|
||||
);
|
||||
}
|
||||
|
||||
// Create each directory
|
||||
var path = Path.Combine(destination, entry.Key);
|
||||
if (Path.GetDirectoryName(path) is { } directory && seenDirectories.Add(path))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
// Write file
|
||||
using var fs = File.OpenWrite(path);
|
||||
entries.WriteEntryTo(fs);
|
||||
|
||||
// Update progress
|
||||
bytesRead += entry.Size;
|
||||
progressReport?.Invoke(bytesRead / (double)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,4 +1,6 @@
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
|
|
@ -24,12 +26,38 @@ public interface IArchiveFactory : IFactory
|
|||
/// </summary>
|
||||
/// <param name="stream">An open, readable and seekable stream.</param>
|
||||
/// <param name="readerOptions">reading options.</param>
|
||||
IArchive Open(Stream stream, ReaderOptions? readerOptions = null);
|
||||
IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null);
|
||||
|
||||
/// <summary>
|
||||
/// Opens an Archive for random access asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="stream">An open, readable and seekable stream.</param>
|
||||
/// <param name="readerOptions">reading options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> containing the opened async archive.</returns>
|
||||
ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with a FileInfo object to an existing file.
|
||||
/// </summary>
|
||||
/// <param name="fileInfo">the file to open.</param>
|
||||
/// <param name="readerOptions">reading options.</param>
|
||||
IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null);
|
||||
IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null);
|
||||
|
||||
/// <summary>
|
||||
/// Opens an Archive from a FileInfo object asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="fileInfo">the file to open.</param>
|
||||
/// <param name="readerOptions">reading options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> containing the opened async archive.</returns>
|
||||
ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
FileInfo fileInfo,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
|
|
|
|||
41
src/SharpCompress/Archives/IArchiveOpenable.cs
Normal file
41
src/SharpCompress/Archives/IArchiveOpenable.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#if NET8_0_OR_GREATER
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public interface IArchiveOpenable<TSync, TASync>
|
||||
where TSync : IArchive
|
||||
where TASync : IAsyncArchive
|
||||
{
|
||||
public static abstract TSync OpenArchive(string filePath, ReaderOptions? readerOptions = null);
|
||||
|
||||
public static abstract TSync OpenArchive(
|
||||
FileInfo fileInfo,
|
||||
ReaderOptions? readerOptions = null
|
||||
);
|
||||
|
||||
public static abstract TSync OpenArchive(Stream stream, ReaderOptions? readerOptions = null);
|
||||
|
||||
public static abstract ValueTask<TASync> OpenAsyncArchive(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
public static abstract ValueTask<TASync> OpenAsyncArchive(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
public static abstract ValueTask<TASync> OpenAsyncArchive(
|
||||
FileInfo fileInfo,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
|
||||
#endif
|
||||
48
src/SharpCompress/Archives/IAsyncArchive.cs
Normal file
48
src/SharpCompress/Archives/IAsyncArchive.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public interface IAsyncArchive : IAsyncDisposable
|
||||
{
|
||||
IAsyncEnumerable<IArchiveEntry> EntriesAsync { get; }
|
||||
IAsyncEnumerable<IVolume> VolumesAsync { get; }
|
||||
|
||||
ArchiveType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Use this method to extract all entries in an archive in order.
|
||||
/// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be
|
||||
/// extracted sequentially for the best performance.
|
||||
/// </summary>
|
||||
ValueTask<IAsyncReader> ExtractAllEntriesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files).
|
||||
/// Rar Archives can be SOLID while all 7Zip archives are considered SOLID.
|
||||
/// </summary>
|
||||
ValueTask<bool> IsSolidAsync();
|
||||
|
||||
/// <summary>
|
||||
/// This checks to see if all the known entries have IsComplete = true
|
||||
/// </summary>
|
||||
ValueTask<bool> IsCompleteAsync();
|
||||
|
||||
/// <summary>
|
||||
/// The total size of the files compressed in the archive.
|
||||
/// </summary>
|
||||
ValueTask<long> TotalSizeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// The total size of the files as uncompressed in the archive.
|
||||
/// </summary>
|
||||
ValueTask<long> TotalUncompressedSizeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the archive is encrypted.
|
||||
/// </summary>
|
||||
ValueTask<bool> IsEncryptedAsync();
|
||||
}
|
||||
126
src/SharpCompress/Archives/IAsyncArchiveExtensions.cs
Normal file
126
src/SharpCompress/Archives/IAsyncArchiveExtensions.cs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public static class IAsyncArchiveExtensions
|
||||
{
|
||||
extension(IAsyncArchive archive)
|
||||
{
|
||||
/// <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 ValueTask WriteToDirectoryAsync(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (
|
||||
await archive.IsSolidAsync().ConfigureAwait(false)
|
||||
|| archive.Type == ArchiveType.SevenZip
|
||||
)
|
||||
{
|
||||
var totalBytes = await archive.TotalUncompressedSizeAsync().ConfigureAwait(false);
|
||||
var bytesRead = 0L;
|
||||
await using var reader = await archive
|
||||
.ExtractAllEntriesAsync()
|
||||
.ConfigureAwait(false);
|
||||
while (await reader.MoveToNextEntryAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await reader
|
||||
.WriteEntryToDirectoryAsync(
|
||||
destinationDirectory,
|
||||
options,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (reader.Entry.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bytesRead += reader.Entry.Size;
|
||||
progress?.Report(
|
||||
new ProgressReport(reader.Entry.Key ?? string.Empty, bytesRead, totalBytes)
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await archive
|
||||
.WriteToDirectoryAsyncInternal(
|
||||
destinationDirectory,
|
||||
options,
|
||||
progress,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask WriteToDirectoryAsyncInternal(
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
IProgress<ProgressReport>? progress,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
options ??= new ExtractionOptions();
|
||||
var fullDestinationDirectoryPath = DirectoryManagement.GetFullDestinationDirectoryPath(
|
||||
destinationDirectory
|
||||
);
|
||||
|
||||
var totalBytes = await archive.TotalUncompressedSizeAsync().ConfigureAwait(false);
|
||||
var bytesRead = 0L;
|
||||
|
||||
await foreach (
|
||||
var entry in archive
|
||||
.EntriesAsync.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
await entry
|
||||
.WriteEntryToDirectoryAsyncCore(
|
||||
fullDestinationDirectoryPath,
|
||||
options,
|
||||
null,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
await entry
|
||||
.WriteEntryToDirectoryAsyncCore(
|
||||
fullDestinationDirectoryPath,
|
||||
options,
|
||||
async (path, ct) =>
|
||||
await entry.WriteToFileAsync(path, options, ct).ConfigureAwait(false),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
bytesRead += entry.Size;
|
||||
progress?.Report(
|
||||
new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
|
|
@ -25,12 +27,38 @@ public interface IMultiArchiveFactory : IFactory
|
|||
/// </summary>
|
||||
/// <param name="streams"></param>
|
||||
/// <param name="readerOptions">reading options.</param>
|
||||
IArchive Open(IReadOnlyList<Stream> streams, ReaderOptions? readerOptions = null);
|
||||
IArchive OpenArchive(IReadOnlyList<Stream> streams, ReaderOptions? readerOptions = null);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a multi-part archive from streams asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="streams"></param>
|
||||
/// <param name="readerOptions">reading options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> containing the opened async archive.</returns>
|
||||
ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with IEnumerable Stream objects, multi and split support.
|
||||
/// </summary>
|
||||
/// <param name="fileInfos"></param>
|
||||
/// <param name="readerOptions">reading options.</param>
|
||||
IArchive Open(IReadOnlyList<FileInfo> fileInfos, ReaderOptions? readerOptions = null);
|
||||
IArchive OpenArchive(IReadOnlyList<FileInfo> fileInfos, ReaderOptions? readerOptions = null);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a multi-part archive from files asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="fileInfos"></param>
|
||||
/// <param name="readerOptions">reading options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> containing the opened async archive.</returns>
|
||||
ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
|
|
|
|||
36
src/SharpCompress/Archives/IMultiArchiveOpenable.cs
Normal file
36
src/SharpCompress/Archives/IMultiArchiveOpenable.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#if NET8_0_OR_GREATER
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public interface IMultiArchiveOpenable<TSync, TASync>
|
||||
where TSync : IArchive
|
||||
where TASync : IAsyncArchive
|
||||
{
|
||||
public static abstract TSync OpenArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null
|
||||
);
|
||||
|
||||
public static abstract TSync OpenArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null
|
||||
);
|
||||
|
||||
public static abstract ValueTask<TASync> OpenAsyncArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
public static abstract ValueTask<TASync> OpenAsyncArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,13 +1,23 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Options;
|
||||
using SharpCompress.Writers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public interface IWritableArchive : IArchive
|
||||
public interface IWritableArchiveCommon
|
||||
{
|
||||
void RemoveEntry(IArchiveEntry entry);
|
||||
/// <summary>
|
||||
/// Use this to pause entry rebuilding when adding large collections of entries. Dispose when complete. A using statement is recommended.
|
||||
/// </summary>
|
||||
/// <returns>IDisposeable to resume entry rebuilding</returns>
|
||||
IDisposable PauseEntryRebuilding();
|
||||
}
|
||||
|
||||
public interface IWritableArchive : IArchive, IWritableArchiveCommon
|
||||
{
|
||||
IArchiveEntry AddEntry(
|
||||
string key,
|
||||
Stream source,
|
||||
|
|
@ -16,11 +26,61 @@ public interface IWritableArchive : IArchive
|
|||
DateTime? modified = null
|
||||
);
|
||||
|
||||
void SaveTo(Stream stream, WriterOptions options);
|
||||
IArchiveEntry AddDirectoryEntry(string key, DateTime? modified = null);
|
||||
|
||||
/// <summary>
|
||||
/// Use this to pause entry rebuilding when adding large collections of entries. Dispose when complete. A using statement is recommended.
|
||||
/// Removes the specified entry from the archive.
|
||||
/// </summary>
|
||||
/// <returns>IDisposeable to resume entry rebuilding</returns>
|
||||
IDisposable PauseEntryRebuilding();
|
||||
void RemoveEntry(IArchiveEntry entry);
|
||||
}
|
||||
|
||||
public interface IWritableArchive<TOptions> : IWritableArchive
|
||||
where TOptions : IWriterOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the archive to the specified stream using the given writer options.
|
||||
/// </summary>
|
||||
void SaveTo(Stream stream, TOptions options);
|
||||
}
|
||||
|
||||
public interface IWritableAsyncArchive : IAsyncArchive, IWritableArchiveCommon
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously adds an entry to the archive with the specified key, source stream, and options.
|
||||
/// </summary>
|
||||
ValueTask<IArchiveEntry> AddEntryAsync(
|
||||
string key,
|
||||
Stream source,
|
||||
bool closeStream,
|
||||
long size = 0,
|
||||
DateTime? modified = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously adds a directory entry to the archive with the specified key and modification time.
|
||||
/// </summary>
|
||||
ValueTask<IArchiveEntry> AddDirectoryEntryAsync(
|
||||
string key,
|
||||
DateTime? modified = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified entry from the archive.
|
||||
/// </summary>
|
||||
ValueTask RemoveEntryAsync(IArchiveEntry entry);
|
||||
}
|
||||
|
||||
public interface IWritableAsyncArchive<TOptions> : IWritableAsyncArchive
|
||||
where TOptions : IWriterOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously saves the archive to the specified stream using the given writer options.
|
||||
/// </summary>
|
||||
ValueTask SaveToAsync(
|
||||
Stream stream,
|
||||
TOptions options,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,86 +1,82 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Common.Options;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public static class IWritableArchiveExtensions
|
||||
{
|
||||
public static void AddEntry(
|
||||
this IWritableArchive writableArchive,
|
||||
string entryPath,
|
||||
string filePath
|
||||
)
|
||||
extension(IWritableArchive writableArchive)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
if (!fileInfo.Exists)
|
||||
public void AddAllFromDirectory(
|
||||
string directoryPath,
|
||||
string searchPattern = "*.*",
|
||||
SearchOption searchOption = SearchOption.AllDirectories
|
||||
)
|
||||
{
|
||||
throw new FileNotFoundException("Could not AddEntry: " + filePath);
|
||||
using (writableArchive.PauseEntryRebuilding())
|
||||
{
|
||||
foreach (
|
||||
var filePath in Directory.EnumerateFiles(
|
||||
directoryPath,
|
||||
searchPattern,
|
||||
searchOption
|
||||
)
|
||||
)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
writableArchive.AddEntry(
|
||||
filePath.Substring(directoryPath.Length),
|
||||
fileInfo.OpenRead(),
|
||||
true,
|
||||
fileInfo.Length,
|
||||
fileInfo.LastWriteTime
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IArchiveEntry AddEntry(string key, string file) =>
|
||||
writableArchive.AddEntry(key, new FileInfo(file));
|
||||
|
||||
public IArchiveEntry AddEntry(
|
||||
string key,
|
||||
Stream source,
|
||||
long size = 0,
|
||||
DateTime? modified = null
|
||||
) => writableArchive.AddEntry(key, source, false, size, modified);
|
||||
|
||||
public IArchiveEntry AddEntry(string key, FileInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
throw new ArgumentException("FileInfo does not exist.");
|
||||
}
|
||||
return writableArchive.AddEntry(
|
||||
key,
|
||||
fileInfo.OpenRead(),
|
||||
true,
|
||||
fileInfo.Length,
|
||||
fileInfo.LastWriteTime
|
||||
);
|
||||
}
|
||||
writableArchive.AddEntry(
|
||||
entryPath,
|
||||
new FileInfo(filePath).OpenRead(),
|
||||
true,
|
||||
fileInfo.Length,
|
||||
fileInfo.LastWriteTime
|
||||
);
|
||||
}
|
||||
|
||||
public static void SaveTo(
|
||||
this IWritableArchive writableArchive,
|
||||
public static void SaveTo<TOptions>(
|
||||
this IWritableArchive<TOptions> writableArchive,
|
||||
string filePath,
|
||||
WriterOptions options
|
||||
) => writableArchive.SaveTo(new FileInfo(filePath), options);
|
||||
|
||||
public static void SaveTo(
|
||||
this IWritableArchive writableArchive,
|
||||
FileInfo fileInfo,
|
||||
WriterOptions options
|
||||
TOptions options
|
||||
)
|
||||
where TOptions : IWriterOptions => writableArchive.SaveTo(new FileInfo(filePath), options);
|
||||
|
||||
public static void SaveTo<TOptions>(
|
||||
this IWritableArchive<TOptions> writableArchive,
|
||||
FileInfo fileInfo,
|
||||
TOptions options
|
||||
)
|
||||
where TOptions : IWriterOptions
|
||||
{
|
||||
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
|
||||
writableArchive.SaveTo(stream, options);
|
||||
}
|
||||
|
||||
public static void AddAllFromDirectory(
|
||||
this IWritableArchive writableArchive,
|
||||
string filePath,
|
||||
string searchPattern = "*.*",
|
||||
SearchOption searchOption = SearchOption.AllDirectories
|
||||
)
|
||||
{
|
||||
using (writableArchive.PauseEntryRebuilding())
|
||||
{
|
||||
foreach (var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption))
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
writableArchive.AddEntry(
|
||||
path.Substring(filePath.Length),
|
||||
fileInfo.OpenRead(),
|
||||
true,
|
||||
fileInfo.Length,
|
||||
fileInfo.LastWriteTime
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IArchiveEntry AddEntry(
|
||||
this IWritableArchive writableArchive,
|
||||
string key,
|
||||
FileInfo fileInfo
|
||||
)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
throw new ArgumentException("FileInfo does not exist.");
|
||||
}
|
||||
return writableArchive.AddEntry(
|
||||
key,
|
||||
fileInfo.OpenRead(),
|
||||
true,
|
||||
fileInfo.Length,
|
||||
fileInfo.LastWriteTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using SharpCompress.Common.Options;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
/// <summary>
|
||||
/// Decorator for <see cref="Factories.Factory"/> used to declare an archive format as able to create writeable archives
|
||||
/// Decorator for <see cref="Factories.Factory"/> used to declare an archive format as able to create writable archives.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implemented by:<br/>
|
||||
|
|
@ -10,11 +12,13 @@ namespace SharpCompress.Archives;
|
|||
/// <item><see cref="Factories.ZipFactory"/></item>
|
||||
/// <item><see cref="Factories.GZipFactory"/></item>
|
||||
/// </list>
|
||||
public interface IWriteableArchiveFactory : Factories.IFactory
|
||||
/// </remarks>
|
||||
public interface IWritableArchiveFactory<TOptions> : Factories.IFactory
|
||||
where TOptions : IWriterOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new, empty archive, ready to be written.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IWritableArchive CreateWriteableArchive();
|
||||
IWritableArchive<TOptions> CreateArchive();
|
||||
}
|
||||
14
src/SharpCompress/Archives/IWritableArchiveOpenable.cs
Normal file
14
src/SharpCompress/Archives/IWritableArchiveOpenable.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Options;
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public interface IWritableArchiveOpenable<TOptions>
|
||||
: IArchiveOpenable<IWritableArchive<TOptions>, IWritableAsyncArchive<TOptions>>
|
||||
where TOptions : IWriterOptions
|
||||
{
|
||||
public static abstract IWritableArchive<TOptions> CreateArchive();
|
||||
public static abstract ValueTask<IWritableAsyncArchive<TOptions>> CreateAsyncArchive();
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Options;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
|
||||
public static class IWritableAsyncArchiveExtensions
|
||||
{
|
||||
extension(IWritableAsyncArchive writableArchive)
|
||||
{
|
||||
public async ValueTask AddAllFromDirectoryAsync(
|
||||
string directoryPath,
|
||||
string searchPattern = "*.*",
|
||||
SearchOption searchOption = SearchOption.AllDirectories
|
||||
)
|
||||
{
|
||||
using (writableArchive.PauseEntryRebuilding())
|
||||
{
|
||||
foreach (
|
||||
var filePath in Directory.EnumerateFiles(
|
||||
directoryPath,
|
||||
searchPattern,
|
||||
searchOption
|
||||
)
|
||||
)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
await writableArchive
|
||||
.AddEntryAsync(
|
||||
filePath.Substring(directoryPath.Length),
|
||||
fileInfo.OpenRead(),
|
||||
true,
|
||||
fileInfo.Length,
|
||||
fileInfo.LastWriteTime
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<IArchiveEntry> AddEntryAsync(string key, string file) =>
|
||||
writableArchive.AddEntryAsync(key, new FileInfo(file));
|
||||
|
||||
public ValueTask<IArchiveEntry> AddEntryAsync(
|
||||
string key,
|
||||
Stream source,
|
||||
long size = 0,
|
||||
DateTime? modified = null
|
||||
) => writableArchive.AddEntryAsync(key, source, false, size, modified);
|
||||
|
||||
public ValueTask<IArchiveEntry> AddEntryAsync(string key, FileInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
throw new ArgumentException("FileInfo does not exist.");
|
||||
}
|
||||
return writableArchive.AddEntryAsync(
|
||||
key,
|
||||
fileInfo.OpenRead(),
|
||||
true,
|
||||
fileInfo.Length,
|
||||
fileInfo.LastWriteTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static ValueTask SaveToAsync<TOptions>(
|
||||
this IWritableAsyncArchive<TOptions> writableArchive,
|
||||
string filePath,
|
||||
TOptions options,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
where TOptions : IWriterOptions =>
|
||||
writableArchive.SaveToAsync(new FileInfo(filePath), options, cancellationToken);
|
||||
|
||||
public static async ValueTask SaveToAsync<TOptions>(
|
||||
this IWritableAsyncArchive<TOptions> writableArchive,
|
||||
FileInfo fileInfo,
|
||||
TOptions options,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
where TOptions : IWriterOptions
|
||||
{
|
||||
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
|
||||
await writableArchive.SaveToAsync(stream, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Common.Rar;
|
||||
|
|
@ -13,20 +14,18 @@ namespace SharpCompress.Archives.Rar;
|
|||
/// </summary>
|
||||
internal class FileInfoRarArchiveVolume : RarVolume
|
||||
{
|
||||
internal FileInfoRarArchiveVolume(FileInfo fileInfo, ReaderOptions options, int index = 0)
|
||||
: base(StreamingMode.Seekable, fileInfo.OpenRead(), FixOptions(options), index)
|
||||
internal FileInfoRarArchiveVolume(FileInfo fileInfo, ReaderOptions options, int index)
|
||||
: base(
|
||||
StreamingMode.Seekable,
|
||||
fileInfo.OpenRead(),
|
||||
options.WithLeaveStreamOpen(false),
|
||||
index
|
||||
)
|
||||
{
|
||||
FileInfo = fileInfo;
|
||||
FileParts = GetVolumeFileParts().ToArray().ToReadOnly();
|
||||
}
|
||||
|
||||
private static ReaderOptions FixOptions(ReaderOptions options)
|
||||
{
|
||||
//make sure we're closing streams with fileinfo
|
||||
options.LeaveStreamOpen = false;
|
||||
return options;
|
||||
}
|
||||
|
||||
internal ReadOnlyCollection<RarFilePart> FileParts { get; }
|
||||
|
||||
internal FileInfo FileInfo { get; }
|
||||
|
|
@ -35,4 +34,7 @@ internal class FileInfoRarArchiveVolume : RarVolume
|
|||
new FileInfoRarFilePart(this, ReaderOptions.Password, markHeader, fileHeader, FileInfo);
|
||||
|
||||
internal override IEnumerable<RarFilePart> ReadFileParts() => FileParts;
|
||||
|
||||
internal override IAsyncEnumerable<RarFilePart> ReadFilePartsAsync() =>
|
||||
FileParts.ToAsyncEnumerable();
|
||||
}
|
||||
|
|
|
|||
56
src/SharpCompress/Archives/Rar/RarArchive.Async.cs
Normal file
56
src/SharpCompress/Archives/Rar/RarArchive.Async.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives.Rar;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Rar;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Rar;
|
||||
|
||||
namespace SharpCompress.Archives.Rar;
|
||||
|
||||
public partial class RarArchive
|
||||
{
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (UnpackV1.IsValueCreated && UnpackV1.Value is IDisposable unpackV1)
|
||||
{
|
||||
unpackV1.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync()
|
||||
{
|
||||
if (await this.IsMultipartVolumeAsync().ConfigureAwait(false))
|
||||
{
|
||||
var streams = await VolumesAsync
|
||||
.Select(volume =>
|
||||
{
|
||||
volume.Stream.Position = 0;
|
||||
return volume.Stream;
|
||||
})
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
return (RarReader)RarReader.OpenReader(streams, ReaderOptions);
|
||||
}
|
||||
|
||||
var stream = (await VolumesAsync.FirstAsync().ConfigureAwait(false)).Stream;
|
||||
stream.Position = 0;
|
||||
return (RarReader)RarReader.OpenReader(stream, ReaderOptions);
|
||||
}
|
||||
|
||||
public override async ValueTask<bool> IsSolidAsync() =>
|
||||
await (await VolumesAsync.CastAsync<RarVolume>().FirstAsync().ConfigureAwait(false))
|
||||
.IsSolidArchiveAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
|
@ -1,18 +1,40 @@
|
|||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Rar;
|
||||
|
||||
namespace SharpCompress.Archives.Rar;
|
||||
|
||||
public static class RarArchiveExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// RarArchive is the first volume of a multi-part archive. If MultipartVolume is true and IsFirstVolume is false then the first volume file must be missing.
|
||||
/// </summary>
|
||||
public static bool IsFirstVolume(this RarArchive archive) =>
|
||||
archive.Volumes.First().IsFirstVolume;
|
||||
extension(IRarArchive archive)
|
||||
{
|
||||
/// <summary>
|
||||
/// RarArchive is the first volume of a multi-part archive. If MultipartVolume is true and IsFirstVolume is false then the first volume file must be missing.
|
||||
/// </summary>
|
||||
public bool IsFirstVolume() => archive.Volumes.Cast<RarVolume>().First().IsFirstVolume;
|
||||
|
||||
/// <summary>
|
||||
/// RarArchive is part of a multi-part archive.
|
||||
/// </summary>
|
||||
public static bool IsMultipartVolume(this RarArchive archive) =>
|
||||
archive.Volumes.First().IsMultiVolume;
|
||||
/// <summary>
|
||||
/// RarArchive is part of a multi-part archive.
|
||||
/// </summary>
|
||||
public bool IsMultipartVolume() => archive.Volumes.Cast<RarVolume>().First().IsMultiVolume;
|
||||
}
|
||||
|
||||
extension(IRarAsyncArchive archive)
|
||||
{
|
||||
/// <summary>
|
||||
/// RarArchive is the first volume of a multi-part archive. If MultipartVolume is true and IsFirstVolume is false then the first volume file must be missing.
|
||||
/// </summary>
|
||||
public async ValueTask<bool> IsFirstVolumeAsync() =>
|
||||
(
|
||||
await archive.VolumesAsync.CastAsync<RarVolume>().FirstAsync().ConfigureAwait(false)
|
||||
).IsFirstVolume;
|
||||
|
||||
/// <summary>
|
||||
/// RarArchive is part of a multi-part archive.
|
||||
/// </summary>
|
||||
public async ValueTask<bool> IsMultipartVolumeAsync() =>
|
||||
(
|
||||
await archive.VolumesAsync.CastAsync<RarVolume>().FirstAsync().ConfigureAwait(false)
|
||||
).IsMultiVolume;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
184
src/SharpCompress/Archives/Rar/RarArchive.Factory.cs
Normal file
184
src/SharpCompress/Archives/Rar/RarArchive.Factory.cs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
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;
|
||||
using SharpCompress.Compressors.Rar;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Rar;
|
||||
|
||||
namespace SharpCompress.Archives.Rar;
|
||||
|
||||
public partial class RarArchive
|
||||
#if NET8_0_OR_GREATER
|
||||
: IArchiveOpenable<IRarArchive, IRarAsyncArchive>,
|
||||
IMultiArchiveOpenable<IRarArchive, IRarAsyncArchive>
|
||||
#endif
|
||||
{
|
||||
public static ValueTask<IRarAsyncArchive> OpenAsyncArchive(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return new((IRarAsyncArchive)OpenArchive(new FileInfo(filePath), readerOptions));
|
||||
}
|
||||
|
||||
public static IRarArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo),
|
||||
readerOptions ?? ReaderOptions.ForFilePath
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static IRarArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo),
|
||||
readerOptions ?? ReaderOptions.ForFilePath
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static IRarArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
return new RarArchive(
|
||||
new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream)
|
||||
);
|
||||
}
|
||||
|
||||
public static IRarArchive OpenArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var files = fileInfos;
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
files[0],
|
||||
i => i < files.Count ? files[i] : null,
|
||||
readerOptions ?? ReaderOptions.ForFilePath
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static IRarArchive OpenArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
var strms = streams.RequireReadable().RequireSeekable().ToList();
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
strms[0],
|
||||
i => i < strms.Count ? strms[i] : null,
|
||||
readerOptions ?? ReaderOptions.ForExternalStream
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static ValueTask<IRarAsyncArchive> OpenAsyncArchive(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IRarAsyncArchive)OpenArchive(stream, readerOptions));
|
||||
}
|
||||
|
||||
public static ValueTask<IRarAsyncArchive> OpenAsyncArchive(
|
||||
FileInfo fileInfo,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IRarAsyncArchive)OpenArchive(fileInfo, readerOptions));
|
||||
}
|
||||
|
||||
public static ValueTask<IRarAsyncArchive> OpenAsyncArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IRarAsyncArchive)OpenArchive(streams, readerOptions));
|
||||
}
|
||||
|
||||
public static ValueTask<IRarAsyncArchive> OpenAsyncArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IRarAsyncArchive)OpenArchive(fileInfos, readerOptions));
|
||||
}
|
||||
|
||||
public static bool IsRarFile(string filePath) => IsRarFile(new FileInfo(filePath));
|
||||
|
||||
public static bool IsRarFile(FileInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using Stream stream = fileInfo.OpenRead();
|
||||
return IsRarFile(stream);
|
||||
}
|
||||
|
||||
public static bool IsRarFile(Stream stream, ReaderOptions? options = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
MarkHeader.Read(stream, true, options?.LookForHeader ?? false);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async ValueTask<bool> IsRarFileAsync(
|
||||
Stream stream,
|
||||
ReaderOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
await MarkHeader
|
||||
.ReadAsync(stream, true, options?.LookForHeader ?? false, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
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;
|
||||
|
|
@ -11,167 +14,98 @@ using SharpCompress.Readers.Rar;
|
|||
|
||||
namespace SharpCompress.Archives.Rar;
|
||||
|
||||
public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
public interface IRarArchiveCommon
|
||||
{
|
||||
internal Lazy<IRarUnpack> UnpackV2017 { get; } =
|
||||
new Lazy<IRarUnpack>(() => new Compressors.Rar.UnpackV2017.Unpack());
|
||||
internal Lazy<IRarUnpack> UnpackV1 { get; } =
|
||||
new Lazy<IRarUnpack>(() => new Compressors.Rar.UnpackV1.Unpack());
|
||||
int MinVersion { get; }
|
||||
int MaxVersion { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with a SourceStream able to handle FileInfo and Streams.
|
||||
/// </summary>
|
||||
/// <param name="srcStream"></param>
|
||||
/// <param name="options"></param>
|
||||
internal RarArchive(SourceStream srcStream)
|
||||
: base(ArchiveType.Rar, srcStream) { }
|
||||
public interface IRarArchive : IArchive, IRarArchiveCommon { }
|
||||
|
||||
public interface IRarAsyncArchive : IAsyncArchive, IRarArchiveCommon { }
|
||||
|
||||
public partial class RarArchive
|
||||
: AbstractArchive<RarArchiveEntry, RarVolume>,
|
||||
IRarArchive,
|
||||
IRarAsyncArchive
|
||||
{
|
||||
private bool _disposed;
|
||||
internal Lazy<IRarUnpack> UnpackV2017 { get; } =
|
||||
new(() => new Compressors.Rar.UnpackV2017.Unpack());
|
||||
internal Lazy<IRarUnpack> UnpackV1 { get; } = new(() => new Compressors.Rar.UnpackV1.Unpack());
|
||||
|
||||
private RarArchive(SourceStream sourceStream)
|
||||
: base(ArchiveType.Rar, sourceStream) { }
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (UnpackV1.IsValueCreated && UnpackV1.Value is IDisposable unpackV1)
|
||||
{
|
||||
unpackV1.Dispose();
|
||||
}
|
||||
if (UnpackV2017.IsValueCreated && UnpackV2017.Value is IDisposable unpackV2017)
|
||||
{
|
||||
unpackV2017.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<RarArchiveEntry> LoadEntries(IEnumerable<RarVolume> volumes) =>
|
||||
RarArchiveEntryFactory.GetEntries(this, volumes, ReaderOptions);
|
||||
|
||||
protected override IEnumerable<RarVolume> LoadVolumes(SourceStream srcStream)
|
||||
{
|
||||
SrcStream.LoadAllParts(); //request all streams
|
||||
var streams = SrcStream.Streams.ToArray();
|
||||
var idx = 0;
|
||||
if (streams.Length > 1 && IsRarFile(streams[1], ReaderOptions)) //test part 2 - true = multipart not split
|
||||
{
|
||||
SrcStream.IsVolumes = true;
|
||||
streams[1].Position = 0;
|
||||
SrcStream.Position = 0;
|
||||
// Simple async property - kept in original file
|
||||
protected override IAsyncEnumerable<RarArchiveEntry> LoadEntriesAsync(
|
||||
IAsyncEnumerable<RarVolume> volumes
|
||||
) => RarArchiveEntryFactory.GetEntriesAsync(this, volumes, ReaderOptions);
|
||||
|
||||
return srcStream.Streams.Select(
|
||||
a => new StreamRarArchiveVolume(a, ReaderOptions, idx++)
|
||||
);
|
||||
}
|
||||
else //split mode or single file
|
||||
protected override IEnumerable<RarVolume> LoadVolumes(SourceStream sourceStream)
|
||||
{
|
||||
sourceStream.LoadAllParts();
|
||||
var streams = sourceStream.Streams.ToArray();
|
||||
var i = 0;
|
||||
if (streams.Length > 1 && IsRarFile(streams[1], ReaderOptions))
|
||||
{
|
||||
return new StreamRarArchiveVolume(SrcStream, ReaderOptions, idx++).AsEnumerable();
|
||||
sourceStream.IsVolumes = true;
|
||||
streams[1].Position = 0;
|
||||
sourceStream.Position = 0;
|
||||
|
||||
return sourceStream.Streams.Select(a => new StreamRarArchiveVolume(
|
||||
a,
|
||||
ReaderOptions,
|
||||
i++
|
||||
));
|
||||
}
|
||||
|
||||
return new StreamRarArchiveVolume(sourceStream, ReaderOptions, i++).AsEnumerable();
|
||||
}
|
||||
|
||||
protected override IReader CreateReaderForSolidExtraction()
|
||||
{
|
||||
if (this.IsMultipartVolume())
|
||||
{
|
||||
var streams = Volumes.Select(volume =>
|
||||
{
|
||||
volume.Stream.Position = 0;
|
||||
return volume.Stream;
|
||||
});
|
||||
return (RarReader)RarReader.OpenReader(streams, ReaderOptions);
|
||||
}
|
||||
|
||||
var stream = Volumes.First().Stream;
|
||||
stream.Position = 0;
|
||||
return RarReader.Open(stream, ReaderOptions);
|
||||
return (RarReader)RarReader.OpenReader(stream, ReaderOptions);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
#region Creation
|
||||
/// <summary>
|
||||
/// Constructor with a FileInfo object to an existing file.
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="options"></param>
|
||||
public static RarArchive Open(string filePath, ReaderOptions? options = null)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty(nameof(filePath));
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo),
|
||||
options ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with a FileInfo object to an existing file.
|
||||
/// </summary>
|
||||
/// <param name="fileInfo"></param>
|
||||
/// <param name="options"></param>
|
||||
public static RarArchive Open(FileInfo fileInfo, ReaderOptions? options = null)
|
||||
{
|
||||
fileInfo.CheckNotNull(nameof(fileInfo));
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo),
|
||||
options ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a seekable Stream as a source
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="options"></param>
|
||||
public static RarArchive Open(Stream stream, ReaderOptions? options = null)
|
||||
{
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
return new RarArchive(new SourceStream(stream, i => null, options ?? new ReaderOptions()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with all file parts passed in
|
||||
/// </summary>
|
||||
/// <param name="fileInfos"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static RarArchive Open(
|
||||
IEnumerable<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.CheckNotNull(nameof(fileInfos));
|
||||
var files = fileInfos.ToArray();
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
files[0],
|
||||
i => i < files.Length ? files[i] : null,
|
||||
readerOptions ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with all stream parts passed in
|
||||
/// </summary>
|
||||
/// <param name="streams"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static RarArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
streams.CheckNotNull(nameof(streams));
|
||||
var strms = streams.ToArray();
|
||||
return new RarArchive(
|
||||
new SourceStream(
|
||||
strms[0],
|
||||
i => i < strms.Length ? strms[i] : null,
|
||||
readerOptions ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static bool IsRarFile(string filePath) => IsRarFile(new FileInfo(filePath));
|
||||
|
||||
public static bool IsRarFile(FileInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using Stream stream = fileInfo.OpenRead();
|
||||
return IsRarFile(stream);
|
||||
}
|
||||
|
||||
public static bool IsRarFile(Stream stream, ReaderOptions? options = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
MarkHeader.Read(stream, true, false);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
43
src/SharpCompress/Archives/Rar/RarArchiveEntry.Async.cs
Normal file
43
src/SharpCompress/Archives/Rar/RarArchiveEntry.Async.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Rar;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
using SharpCompress.Compressors.Rar;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives.Rar;
|
||||
|
||||
public partial class RarArchiveEntry
|
||||
{
|
||||
public async ValueTask<Stream> OpenEntryStreamAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
RarStream stream;
|
||||
if (IsRarV3)
|
||||
{
|
||||
stream = new RarStream(
|
||||
archive.UnpackV1.Value,
|
||||
FileHeader,
|
||||
await MultiVolumeReadOnlyAsyncStream
|
||||
.Create(Parts.ToAsyncEnumerable().CastAsync<RarFilePart>())
|
||||
.ConfigureAwait(false)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream = new RarStream(
|
||||
archive.UnpackV2017.Value,
|
||||
FileHeader,
|
||||
await MultiVolumeReadOnlyAsyncStream
|
||||
.Create(Parts.ToAsyncEnumerable().CastAsync<RarFilePart>())
|
||||
.ConfigureAwait(false)
|
||||
);
|
||||
}
|
||||
|
||||
await stream.InitializeAsync(cancellationToken).ConfigureAwait(false);
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
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;
|
||||
|
|
@ -9,7 +12,7 @@ using SharpCompress.Readers;
|
|||
|
||||
namespace SharpCompress.Archives.Rar;
|
||||
|
||||
public class RarArchiveEntry : RarEntry, IArchiveEntry
|
||||
public partial class RarArchiveEntry : RarEntry, IArchiveEntry
|
||||
{
|
||||
private readonly ICollection<RarFilePart> parts;
|
||||
private readonly RarArchive archive;
|
||||
|
|
@ -20,6 +23,7 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
|
|||
IEnumerable<RarFilePart> parts,
|
||||
ReaderOptions readerOptions
|
||||
)
|
||||
: base(readerOptions)
|
||||
{
|
||||
this.parts = parts.ToList();
|
||||
this.archive = archive;
|
||||
|
|
@ -40,7 +44,10 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
|
|||
get
|
||||
{
|
||||
CheckIncomplete();
|
||||
return parts.Select(fp => fp.FileHeader).Single(fh => !fh.IsSplitAfter).FileCrc;
|
||||
return BitConverter.ToUInt32(
|
||||
parts.Select(fp => fp.FileHeader).Single(fh => !fh.IsSplitAfter).FileCrc.NotNull(),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,20 +71,23 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
|
|||
|
||||
public Stream OpenEntryStream()
|
||||
{
|
||||
var readStream = new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>());
|
||||
RarStream stream;
|
||||
if (IsRarV3)
|
||||
{
|
||||
return new RarStream(
|
||||
archive.UnpackV1.Value,
|
||||
FileHeader,
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
|
||||
);
|
||||
stream = RarCrcStream.Create(archive.UnpackV1.Value, FileHeader, readStream);
|
||||
}
|
||||
else if (FileHeader.FileCrc?.Length > 5)
|
||||
{
|
||||
stream = RarBLAKE2spStream.Create(archive.UnpackV2017.Value, FileHeader, readStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream = RarCrcStream.Create(archive.UnpackV2017.Value, FileHeader, readStream);
|
||||
}
|
||||
|
||||
return new RarStream(
|
||||
archive.UnpackV2017.Value,
|
||||
FileHeader,
|
||||
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
|
||||
);
|
||||
stream.Initialize();
|
||||
return stream;
|
||||
}
|
||||
|
||||
public bool IsComplete
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Rar;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
|
|
@ -17,6 +18,19 @@ internal static class RarArchiveEntryFactory
|
|||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<RarFilePart> GetFilePartsAsync(
|
||||
IAsyncEnumerable<RarVolume> parts
|
||||
)
|
||||
{
|
||||
await foreach (var rarPart in parts.ConfigureAwait(false))
|
||||
{
|
||||
await foreach (var fp in rarPart.ReadFilePartsAsync().ConfigureAwait(false))
|
||||
{
|
||||
yield return fp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<IEnumerable<RarFilePart>> GetMatchedFileParts(
|
||||
IEnumerable<RarVolume> parts
|
||||
)
|
||||
|
|
@ -38,6 +52,27 @@ internal static class RarArchiveEntryFactory
|
|||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<IEnumerable<RarFilePart>> GetMatchedFilePartsAsync(
|
||||
IAsyncEnumerable<RarVolume> parts
|
||||
)
|
||||
{
|
||||
var groupedParts = new List<RarFilePart>();
|
||||
await foreach (var fp in GetFilePartsAsync(parts).ConfigureAwait(false))
|
||||
{
|
||||
groupedParts.Add(fp);
|
||||
|
||||
if (!fp.FileHeader.IsSplitAfter)
|
||||
{
|
||||
yield return groupedParts;
|
||||
groupedParts = new List<RarFilePart>();
|
||||
}
|
||||
}
|
||||
if (groupedParts.Count > 0)
|
||||
{
|
||||
yield return groupedParts;
|
||||
}
|
||||
}
|
||||
|
||||
internal static IEnumerable<RarArchiveEntry> GetEntries(
|
||||
RarArchive archive,
|
||||
IEnumerable<RarVolume> rarParts,
|
||||
|
|
@ -49,4 +84,16 @@ internal static class RarArchiveEntryFactory
|
|||
yield return new RarArchiveEntry(archive, groupedParts, readerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
internal static async IAsyncEnumerable<RarArchiveEntry> GetEntriesAsync(
|
||||
RarArchive archive,
|
||||
IAsyncEnumerable<RarVolume> rarParts,
|
||||
ReaderOptions readerOptions
|
||||
)
|
||||
{
|
||||
await foreach (var groupedParts in GetMatchedFilePartsAsync(rarParts).ConfigureAwait(false))
|
||||
{
|
||||
yield return new RarArchiveEntry(archive, groupedParts, readerOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Archives.Rar;
|
||||
|
||||
|
|
@ -11,23 +12,28 @@ internal static class RarArchiveVolumeFactory
|
|||
FileInfo? item = null;
|
||||
|
||||
//new style rar - ..part1 | /part01 | part001 ....
|
||||
Match m = Regex.Match(part1.Name, @"^(.*\.part)([0-9]+)(\.rar)$", RegexOptions.IgnoreCase);
|
||||
var m = Regex.Match(part1.Name, @"^(.*\.part)([0-9]+)(\.rar)$", RegexOptions.IgnoreCase);
|
||||
if (m.Success)
|
||||
{
|
||||
item = new FileInfo(
|
||||
Path.Combine(
|
||||
part1.DirectoryName!,
|
||||
String.Concat(
|
||||
m.Groups[1].Value,
|
||||
(index + 1).ToString().PadLeft(m.Groups[2].Value.Length, '0'),
|
||||
(index + 1)
|
||||
.ToString(Constants.DefaultCultureInfo)
|
||||
.PadLeft(m.Groups[2].Value.Length, '0'),
|
||||
m.Groups[3].Value
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
//old style - ...rar, .r00, .r01 ...
|
||||
m = Regex.Match(part1.Name, @"^(.*\.)([r-z{])(ar|[0-9]+)$", RegexOptions.IgnoreCase);
|
||||
if (m.Success)
|
||||
{
|
||||
item = new FileInfo(
|
||||
Path.Combine(
|
||||
part1.DirectoryName!,
|
||||
|
|
@ -36,16 +42,29 @@ internal static class RarArchiveVolumeFactory
|
|||
index == 0
|
||||
? m.Groups[2].Value + m.Groups[3].Value
|
||||
: (char)(m.Groups[2].Value[0] + ((index - 1) / 100))
|
||||
+ (index - 1).ToString("D4").Substring(2)
|
||||
+ (index - 1)
|
||||
.ToString(
|
||||
"D4",
|
||||
global::SharpCompress
|
||||
.Common
|
||||
.Constants
|
||||
.DefaultCultureInfo
|
||||
)
|
||||
.Substring(2)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
else //split .001, .002 ....
|
||||
{
|
||||
return ArchiveVolumeFactory.GetFilePart(index, part1);
|
||||
}
|
||||
}
|
||||
|
||||
if (item != null && item.Exists)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
|
||||
return null; //no more items
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ namespace SharpCompress.Archives.Rar;
|
|||
|
||||
internal class SeekableFilePart : RarFilePart
|
||||
{
|
||||
private readonly Stream stream;
|
||||
private readonly string? password;
|
||||
private readonly Stream _stream;
|
||||
private readonly string? _password;
|
||||
|
||||
internal SeekableFilePart(
|
||||
MarkHeader mh,
|
||||
|
|
@ -18,18 +18,27 @@ internal class SeekableFilePart : RarFilePart
|
|||
)
|
||||
: base(mh, fh, index)
|
||||
{
|
||||
this.stream = stream;
|
||||
this.password = password;
|
||||
_stream = stream;
|
||||
_password = password;
|
||||
}
|
||||
|
||||
internal override Stream GetCompressedStream()
|
||||
{
|
||||
stream.Position = FileHeader.DataStartPosition;
|
||||
_stream.Position = FileHeader.DataStartPosition;
|
||||
|
||||
if (FileHeader.R4Salt != null)
|
||||
{
|
||||
return new RarCryptoWrapper(stream, password!, FileHeader.R4Salt);
|
||||
var cryptKey = new CryptKey3(_password!);
|
||||
return new RarCryptoWrapper(_stream, FileHeader.R4Salt, cryptKey);
|
||||
}
|
||||
return stream;
|
||||
|
||||
if (FileHeader.Rar5CryptoInfo != null)
|
||||
{
|
||||
var cryptKey = new CryptKey5(_password!, FileHeader.Rar5CryptoInfo);
|
||||
return new RarCryptoWrapper(_stream, FileHeader.Rar5CryptoInfo.Salt, cryptKey);
|
||||
}
|
||||
|
||||
return _stream;
|
||||
}
|
||||
|
||||
internal override string FilePartName => "Unknown Stream - File Entry: " + FileHeader.FileName;
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@ namespace SharpCompress.Archives.Rar;
|
|||
|
||||
internal class StreamRarArchiveVolume : RarVolume
|
||||
{
|
||||
internal StreamRarArchiveVolume(Stream stream, ReaderOptions options, int index = 0)
|
||||
internal StreamRarArchiveVolume(Stream stream, ReaderOptions options, int index)
|
||||
: base(StreamingMode.Seekable, stream, options, index) { }
|
||||
|
||||
internal override IEnumerable<RarFilePart> ReadFileParts() => GetVolumeFileParts();
|
||||
|
||||
internal override IAsyncEnumerable<RarFilePart> ReadFilePartsAsync() =>
|
||||
GetVolumeFilePartsAsync();
|
||||
|
||||
internal override RarFilePart CreateFilePart(MarkHeader markHeader, FileHeader fileHeader) =>
|
||||
new SeekableFilePart(markHeader, fileHeader, Index, Stream, ReaderOptions.Password);
|
||||
}
|
||||
|
|
|
|||
80
src/SharpCompress/Archives/SevenZip/SevenZipArchive.Async.cs
Normal file
80
src/SharpCompress/Archives/SevenZip/SevenZipArchive.Async.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
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.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives.SevenZip;
|
||||
|
||||
public partial class SevenZipArchive
|
||||
{
|
||||
private async ValueTask LoadFactoryAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (_database is null)
|
||||
{
|
||||
stream.Position = 0;
|
||||
var reader = new ArchiveReader();
|
||||
await reader
|
||||
.OpenAsync(stream, lookForHeader: ReaderOptions.LookForHeader, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_database = await reader
|
||||
.ReadDatabaseAsync(new PasswordProvider(ReaderOptions.Password), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<SevenZipArchiveEntry> LoadEntriesAsync(
|
||||
IAsyncEnumerable<SevenZipVolume> volumes
|
||||
)
|
||||
{
|
||||
var stream = (await volumes.SingleAsync().ConfigureAwait(false)).Stream;
|
||||
await LoadFactoryAsync(stream).ConfigureAwait(false);
|
||||
if (_database is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
var entries = new SevenZipArchiveEntry[_database._files.Count];
|
||||
for (var i = 0; i < _database._files.Count; i++)
|
||||
{
|
||||
var file = _database._files[i];
|
||||
entries[i] = new SevenZipArchiveEntry(
|
||||
this,
|
||||
new SevenZipFilePart(stream, _database, i, file, ReaderOptions.ArchiveEncoding),
|
||||
ReaderOptions
|
||||
);
|
||||
}
|
||||
foreach (var group in entries.Where(x => !x.IsDirectory).GroupBy(x => x.FilePart.Folder))
|
||||
{
|
||||
var isSolid = false;
|
||||
foreach (var entry in group)
|
||||
{
|
||||
entry.IsSolid = isSolid;
|
||||
isSolid = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
yield return entry;
|
||||
}
|
||||
}
|
||||
|
||||
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync() =>
|
||||
new(new SevenZipReader(ReaderOptions, this));
|
||||
|
||||
public override async ValueTask<bool> IsSolidAsync()
|
||||
{
|
||||
var entries = await EntriesAsync
|
||||
.Where(x => !x.IsDirectory)
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
return entries.GroupBy(x => x.FilePart.Folder).Any(folder => folder.Skip(1).Any());
|
||||
}
|
||||
}
|
||||
263
src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs
Normal file
263
src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives.SevenZip;
|
||||
|
||||
public partial class SevenZipArchive
|
||||
#if NET8_0_OR_GREATER
|
||||
: IArchiveOpenable<IArchive, IAsyncArchive>,
|
||||
IMultiArchiveOpenable<IArchive, IAsyncArchive>
|
||||
#endif
|
||||
{
|
||||
public static ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return new(
|
||||
(IAsyncArchive)OpenArchive(
|
||||
new FileInfo(filePath),
|
||||
readerOptions ?? ReaderOptions.ForFilePath
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static IArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return OpenArchive(new FileInfo(filePath), readerOptions ?? ReaderOptions.ForFilePath);
|
||||
}
|
||||
|
||||
public static IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
return new SevenZipArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
|
||||
readerOptions ?? ReaderOptions.ForFilePath
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static IArchive OpenArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var files = fileInfos;
|
||||
return new SevenZipArchive(
|
||||
new SourceStream(
|
||||
files[0],
|
||||
i => i < files.Count ? files[i] : null,
|
||||
readerOptions ?? ReaderOptions.ForFilePath
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static IArchive OpenArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
var strms = streams.RequireReadable().RequireSeekable().ToList();
|
||||
return new SevenZipArchive(
|
||||
new SourceStream(
|
||||
strms[0],
|
||||
i => i < strms.Count ? strms[i] : null,
|
||||
readerOptions ?? ReaderOptions.ForExternalStream
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
return new SevenZipArchive(
|
||||
new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream)
|
||||
);
|
||||
}
|
||||
|
||||
public static ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IAsyncArchive)OpenArchive(stream, readerOptions));
|
||||
}
|
||||
|
||||
public static ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
FileInfo fileInfo,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IAsyncArchive)OpenArchive(fileInfo, readerOptions));
|
||||
}
|
||||
|
||||
public static ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IAsyncArchive)OpenArchive(streams, readerOptions));
|
||||
}
|
||||
|
||||
public static ValueTask<IAsyncArchive> OpenAsyncArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new((IAsyncArchive)OpenArchive(fileInfos, readerOptions));
|
||||
}
|
||||
|
||||
public static bool IsSevenZipFile(string filePath) => IsSevenZipFile(new FileInfo(filePath));
|
||||
|
||||
public static bool IsSevenZipFile(FileInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using Stream stream = fileInfo.OpenRead();
|
||||
return IsSevenZipFile(stream);
|
||||
}
|
||||
|
||||
public static bool IsSevenZipFile(Stream stream) =>
|
||||
IsSevenZipFile(stream, ReaderOptions.ForExternalStream);
|
||||
|
||||
public static bool IsSevenZipFile(Stream stream, ReaderOptions? readerOptions)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SignatureMatch(stream, readerOptions?.LookForHeader ?? false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async ValueTask<bool> IsSevenZipFileAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await IsSevenZipFileAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
public static async ValueTask<bool> IsSevenZipFileAsync(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
return await SignatureMatchAsync(
|
||||
stream,
|
||||
readerOptions?.LookForHeader ?? false,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<byte> Signature => [(byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C];
|
||||
|
||||
private static bool SignatureMatch(Stream stream, bool lookForHeader)
|
||||
{
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(6);
|
||||
try
|
||||
{
|
||||
var maxScanOffset = lookForHeader ? 0x80000 - 20 : 0;
|
||||
for (var offset = 0; offset <= maxScanOffset; offset++)
|
||||
{
|
||||
stream.ReadExact(buffer, 0, 6);
|
||||
if (buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!lookForHeader || !stream.CanSeek || stream.Length - stream.Position < 6)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stream.Position -= 5;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask<bool> SignatureMatchAsync(
|
||||
Stream stream,
|
||||
bool lookForHeader,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(6);
|
||||
try
|
||||
{
|
||||
var maxScanOffset = lookForHeader ? 0x80000 - 20 : 0;
|
||||
for (var offset = 0; offset <= maxScanOffset; offset++)
|
||||
{
|
||||
if (
|
||||
!await stream
|
||||
.ReadFullyAsync(buffer, 0, 6, cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!lookForHeader || !stream.CanSeek || stream.Length - stream.Position < 6)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stream.Position -= 5;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,129 +1,32 @@
|
|||
#nullable disable
|
||||
|
||||
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;
|
||||
using SharpCompress.Compressors.LZMA.Utilities;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Archives.SevenZip;
|
||||
|
||||
public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVolume>
|
||||
public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVolume>
|
||||
{
|
||||
private ArchiveDatabase database;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor expects a filepath to an existing file.
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static SevenZipArchive Open(string filePath, ReaderOptions readerOptions = null)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty("filePath");
|
||||
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with a FileInfo object to an existing file.
|
||||
/// </summary>
|
||||
/// <param name="fileInfo"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static SevenZipArchive Open(FileInfo fileInfo, ReaderOptions readerOptions = null)
|
||||
{
|
||||
fileInfo.CheckNotNull("fileInfo");
|
||||
return new SevenZipArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
|
||||
readerOptions ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with all file parts passed in
|
||||
/// </summary>
|
||||
/// <param name="fileInfos"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static SevenZipArchive Open(
|
||||
IEnumerable<FileInfo> fileInfos,
|
||||
ReaderOptions readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.CheckNotNull(nameof(fileInfos));
|
||||
var files = fileInfos.ToArray();
|
||||
return new SevenZipArchive(
|
||||
new SourceStream(
|
||||
files[0],
|
||||
i => i < files.Length ? files[i] : null,
|
||||
readerOptions ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with all stream parts passed in
|
||||
/// </summary>
|
||||
/// <param name="streams"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static SevenZipArchive Open(
|
||||
IEnumerable<Stream> streams,
|
||||
ReaderOptions readerOptions = null
|
||||
)
|
||||
{
|
||||
streams.CheckNotNull(nameof(streams));
|
||||
var strms = streams.ToArray();
|
||||
return new SevenZipArchive(
|
||||
new SourceStream(
|
||||
strms[0],
|
||||
i => i < strms.Length ? strms[i] : null,
|
||||
readerOptions ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a seekable Stream as a source
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static SevenZipArchive Open(Stream stream, ReaderOptions readerOptions = null)
|
||||
{
|
||||
stream.CheckNotNull("stream");
|
||||
return new SevenZipArchive(
|
||||
new SourceStream(stream, i => null, readerOptions ?? new ReaderOptions())
|
||||
);
|
||||
}
|
||||
private ArchiveDatabase? _database;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with a SourceStream able to handle FileInfo and Streams.
|
||||
/// </summary>
|
||||
/// <param name="srcStream"></param>
|
||||
/// <param name="options"></param>
|
||||
internal SevenZipArchive(SourceStream srcStream)
|
||||
: base(ArchiveType.SevenZip, srcStream) { }
|
||||
/// <param name="sourceStream"></param>
|
||||
private SevenZipArchive(SourceStream sourceStream)
|
||||
: base(ArchiveType.SevenZip, sourceStream) { }
|
||||
|
||||
protected override IEnumerable<SevenZipVolume> LoadVolumes(SourceStream srcStream)
|
||||
protected override IEnumerable<SevenZipVolume> LoadVolumes(SourceStream sourceStream)
|
||||
{
|
||||
SrcStream.LoadAllParts(); //request all streams
|
||||
var idx = 0;
|
||||
return new SevenZipVolume(srcStream, ReaderOptions, idx++).AsEnumerable(); //simple single volume or split, multivolume not supported
|
||||
}
|
||||
|
||||
public static bool IsSevenZipFile(string filePath) => IsSevenZipFile(new FileInfo(filePath));
|
||||
|
||||
public static bool IsSevenZipFile(FileInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using Stream stream = fileInfo.OpenRead();
|
||||
return IsSevenZipFile(stream);
|
||||
sourceStream.NotNull("SourceStream is null").LoadAllParts(); //request all streams
|
||||
return new SevenZipVolume(sourceStream, ReaderOptions, 0).AsEnumerable(); //simple single volume or split, multivolume not supported
|
||||
}
|
||||
|
||||
internal SevenZipArchive()
|
||||
|
|
@ -133,133 +36,281 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
|||
IEnumerable<SevenZipVolume> volumes
|
||||
)
|
||||
{
|
||||
var stream = volumes.Single().Stream;
|
||||
LoadFactory(stream);
|
||||
var entries = new SevenZipArchiveEntry[database._files.Count];
|
||||
for (var i = 0; i < database._files.Count; i++)
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
var file = database._files[i];
|
||||
entries[i] = new SevenZipArchiveEntry(
|
||||
this,
|
||||
new SevenZipFilePart(stream, database, i, file, ReaderOptions.ArchiveEncoding)
|
||||
);
|
||||
}
|
||||
foreach (var group in entries.Where(x => !x.IsDirectory).GroupBy(x => x.FilePart.Folder))
|
||||
{
|
||||
var isSolid = false;
|
||||
foreach (var entry in group)
|
||||
LoadFactory(volume.Stream);
|
||||
if (_database is null)
|
||||
{
|
||||
entry.IsSolid = isSolid;
|
||||
isSolid = true; //mark others in this group as solid - same as rar behaviour.
|
||||
yield break;
|
||||
}
|
||||
var entries = new SevenZipArchiveEntry[_database._files.Count];
|
||||
for (var i = 0; i < _database._files.Count; i++)
|
||||
{
|
||||
var file = _database._files[i];
|
||||
entries[i] = new SevenZipArchiveEntry(
|
||||
this,
|
||||
new SevenZipFilePart(
|
||||
volume.Stream,
|
||||
_database,
|
||||
i,
|
||||
file,
|
||||
ReaderOptions.ArchiveEncoding
|
||||
),
|
||||
ReaderOptions
|
||||
);
|
||||
}
|
||||
foreach (
|
||||
var group in entries.Where(x => !x.IsDirectory).GroupBy(x => x.FilePart.Folder)
|
||||
)
|
||||
{
|
||||
var isSolid = false;
|
||||
foreach (var entry in group)
|
||||
{
|
||||
entry.IsSolid = isSolid;
|
||||
isSolid = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
yield return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private void LoadFactory(Stream stream)
|
||||
{
|
||||
if (database is null)
|
||||
if (_database is null)
|
||||
{
|
||||
stream.Position = 0;
|
||||
var reader = new ArchiveReader();
|
||||
reader.Open(stream);
|
||||
database = reader.ReadDatabase(new PasswordProvider(ReaderOptions.Password));
|
||||
reader.Open(stream, lookForHeader: ReaderOptions.LookForHeader);
|
||||
_database = reader.ReadDatabase(new PasswordProvider(ReaderOptions.Password));
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsSevenZipFile(Stream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SignatureMatch(stream);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<byte> SIGNATURE =>
|
||||
new byte[] { (byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C };
|
||||
|
||||
private static bool SignatureMatch(Stream stream)
|
||||
{
|
||||
var reader = new BinaryReader(stream);
|
||||
ReadOnlySpan<byte> signatureBytes = reader.ReadBytes(6);
|
||||
return signatureBytes.SequenceEqual(SIGNATURE);
|
||||
}
|
||||
|
||||
protected override IReader CreateReaderForSolidExtraction() =>
|
||||
new SevenZipReader(ReaderOptions, this);
|
||||
|
||||
public override bool IsSolid =>
|
||||
Entries.Where(x => !x.IsDirectory).GroupBy(x => x.FilePart.Folder).Count() > 1;
|
||||
Entries
|
||||
.Where(x => !x.IsDirectory)
|
||||
.GroupBy(x => x.FilePart.Folder)
|
||||
.Any(folder => folder.Skip(1).Any());
|
||||
|
||||
public override long TotalSize
|
||||
{
|
||||
get
|
||||
{
|
||||
var i = Entries.Count;
|
||||
return database._packSizes.Aggregate(0L, (total, packSize) => total + packSize);
|
||||
}
|
||||
}
|
||||
public override bool IsEncrypted => Entries.First(x => !x.IsDirectory).IsEncrypted;
|
||||
|
||||
private sealed class SevenZipReader : AbstractReader<SevenZipEntry, SevenZipVolume>
|
||||
public override long TotalSize =>
|
||||
_database?._packSizes.Aggregate(0L, (total, packSize) => total + packSize) ?? 0;
|
||||
|
||||
internal sealed class SevenZipReader : AbstractReader<SevenZipEntry, SevenZipVolume>
|
||||
{
|
||||
private readonly SevenZipArchive archive;
|
||||
private CFolder currentFolder;
|
||||
private Stream currentStream;
|
||||
private CFileItem currentItem;
|
||||
private readonly SevenZipArchive _archive;
|
||||
private SevenZipEntry? _currentEntry;
|
||||
private Stream? _currentFolderStream;
|
||||
private CFolder? _currentFolder;
|
||||
|
||||
/// <summary>
|
||||
/// Enables internal diagnostics for tests.
|
||||
/// When disabled (default), diagnostics properties return null to avoid exposing internal state.
|
||||
/// </summary>
|
||||
internal bool DiagnosticsEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current folder instance used to decide whether the solid folder stream should be reused.
|
||||
/// Only available when <see cref="DiagnosticsEnabled"/> is true.
|
||||
/// </summary>
|
||||
internal object? DiagnosticsCurrentFolder => DiagnosticsEnabled ? _currentFolder : null;
|
||||
|
||||
/// <summary>
|
||||
/// Current shared folder stream instance.
|
||||
/// Only available when <see cref="DiagnosticsEnabled"/> is true.
|
||||
/// </summary>
|
||||
internal Stream? DiagnosticsCurrentFolderStream =>
|
||||
DiagnosticsEnabled ? _currentFolderStream : null;
|
||||
|
||||
internal SevenZipReader(ReaderOptions readerOptions, SevenZipArchive archive)
|
||||
: base(readerOptions, ArchiveType.SevenZip) => this.archive = archive;
|
||||
: base(readerOptions, ArchiveType.SevenZip, false) => this._archive = archive;
|
||||
|
||||
public override SevenZipVolume Volume => archive.Volumes.Single();
|
||||
public override SevenZipVolume Volume => _archive.Volumes.Single();
|
||||
|
||||
protected override IEnumerable<SevenZipEntry> GetEntries(Stream stream)
|
||||
{
|
||||
var entries = archive.Entries.ToList();
|
||||
var entries = _archive.Entries.ToList();
|
||||
stream.Position = 0;
|
||||
foreach (var dir in entries.Where(x => x.IsDirectory))
|
||||
{
|
||||
_currentEntry = dir;
|
||||
yield return dir;
|
||||
}
|
||||
foreach (
|
||||
var group in entries.Where(x => !x.IsDirectory).GroupBy(x => x.FilePart.Folder)
|
||||
)
|
||||
// For solid archives (entries in the same folder share a compressed stream),
|
||||
// we must iterate entries sequentially and maintain the folder stream state
|
||||
// across entries in the same folder to avoid recreating the decompression
|
||||
// stream for each file, which breaks contiguous streaming.
|
||||
foreach (var entry in entries.Where(x => !x.IsDirectory))
|
||||
{
|
||||
currentFolder = group.Key;
|
||||
if (group.Key is null)
|
||||
{
|
||||
currentStream = Stream.Null;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentStream = archive.database.GetFolderStream(
|
||||
stream,
|
||||
currentFolder,
|
||||
new PasswordProvider(Options.Password)
|
||||
);
|
||||
}
|
||||
foreach (var entry in group)
|
||||
{
|
||||
currentItem = entry.FilePart.Header;
|
||||
yield return entry;
|
||||
}
|
||||
_currentEntry = entry;
|
||||
yield return entry;
|
||||
}
|
||||
}
|
||||
|
||||
protected override EntryStream GetEntryStream() =>
|
||||
CreateEntryStream(new ReadOnlySubStream(currentStream, currentItem.Size));
|
||||
protected override EntryStream GetEntryStream()
|
||||
{
|
||||
var entry = _currentEntry.NotNull("currentEntry is not null");
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
return CreateEntryStream(Stream.Null);
|
||||
}
|
||||
|
||||
var folder = entry.FilePart.Folder;
|
||||
|
||||
// If folder is null (empty stream entry), return empty stream
|
||||
if (folder is null)
|
||||
{
|
||||
return CreateEntryStream(Stream.Null);
|
||||
}
|
||||
|
||||
// Check if we're starting a new folder - dispose old folder stream if needed
|
||||
if (folder != _currentFolder)
|
||||
{
|
||||
_currentFolderStream?.Dispose();
|
||||
_currentFolderStream = null;
|
||||
_currentFolder = folder;
|
||||
}
|
||||
|
||||
// Create the folder stream once per folder
|
||||
if (_currentFolderStream is null)
|
||||
{
|
||||
_currentFolderStream = _archive._database!.GetFolderStream(
|
||||
_archive.Volumes.Single().Stream,
|
||||
folder!,
|
||||
_archive._database.PasswordProvider
|
||||
);
|
||||
}
|
||||
|
||||
return CreateEntryStream(
|
||||
new ReadOnlySubStream(_currentFolderStream, entry.Size, leaveOpen: true)
|
||||
);
|
||||
}
|
||||
|
||||
protected override ValueTask<EntryStream> GetEntryStreamAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
) => new(GetEntryStream());
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_currentFolderStream?.Dispose();
|
||||
_currentFolderStream = null;
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WORKAROUND: Forces async operations to use synchronous equivalents.
|
||||
/// This is necessary because the LZMA decoder has bugs in its async implementation
|
||||
/// that cause state corruption (IndexOutOfRangeException, DataErrorException).
|
||||
///
|
||||
/// The proper fix would be to repair the LZMA decoder's async methods
|
||||
/// (LzmaStream.ReadAsync, Decoder.CodeAsync, OutWindow async operations),
|
||||
/// but that requires deep changes to the decoder state machine.
|
||||
/// </summary>
|
||||
private sealed class SyncOnlyStream : Stream
|
||||
{
|
||||
private readonly Stream _baseStream;
|
||||
|
||||
public SyncOnlyStream(Stream baseStream) => _baseStream = baseStream;
|
||||
|
||||
public override bool CanRead => _baseStream.CanRead;
|
||||
public override bool CanSeek => _baseStream.CanSeek;
|
||||
public override bool CanWrite => _baseStream.CanWrite;
|
||||
public override long Length => _baseStream.Length;
|
||||
public override long Position
|
||||
{
|
||||
get => _baseStream.Position;
|
||||
set => _baseStream.Position = value;
|
||||
}
|
||||
|
||||
public override void Flush() => _baseStream.Flush();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
_baseStream.Read(buffer, offset, count);
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) =>
|
||||
_baseStream.Seek(offset, origin);
|
||||
|
||||
public override void SetLength(long value) => _baseStream.SetLength(value);
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
_baseStream.Write(buffer, offset, count);
|
||||
|
||||
// Force async operations to use sync equivalents to avoid LZMA decoder bugs
|
||||
public override Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(_baseStream.Read(buffer, offset, count));
|
||||
}
|
||||
|
||||
public override Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_baseStream.Write(buffer, offset, count);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_baseStream.Flush();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#if !LEGACY_DOTNET
|
||||
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
|
||||
{
|
||||
private readonly string _password;
|
||||
private readonly string? _password;
|
||||
|
||||
public PasswordProvider(string password) => _password = password;
|
||||
public PasswordProvider(string? password) => _password = password;
|
||||
|
||||
public string CryptoGetTextPassword() => _password;
|
||||
public string? CryptoGetTextPassword() => _password;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,29 @@
|
|||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Options;
|
||||
using SharpCompress.Common.SevenZip;
|
||||
|
||||
namespace SharpCompress.Archives.SevenZip;
|
||||
|
||||
public class SevenZipArchiveEntry : SevenZipEntry, IArchiveEntry
|
||||
{
|
||||
internal SevenZipArchiveEntry(SevenZipArchive archive, SevenZipFilePart part)
|
||||
: base(part) => Archive = archive;
|
||||
internal SevenZipArchiveEntry(
|
||||
SevenZipArchive archive,
|
||||
SevenZipFilePart part,
|
||||
IReaderOptions readerOptions
|
||||
)
|
||||
: base(part, readerOptions) => Archive = archive;
|
||||
|
||||
public Stream OpenEntryStream() => FilePart.GetCompressedStream();
|
||||
|
||||
public async ValueTask<Stream> OpenEntryStreamAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
(
|
||||
await FilePart.GetCompressedStreamAsync(cancellationToken).ConfigureAwait(false)
|
||||
).NotNull();
|
||||
|
||||
public IArchive Archive { get; }
|
||||
|
||||
public bool IsComplete => true;
|
||||
|
|
|
|||
133
src/SharpCompress/Archives/Tar/TarArchive.Async.cs
Normal file
133
src/SharpCompress/Archives/Tar/TarArchive.Async.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Options;
|
||||
using SharpCompress.Common.Tar;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Tar;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.Tar;
|
||||
|
||||
namespace SharpCompress.Archives.Tar;
|
||||
|
||||
public partial class TarArchive
|
||||
{
|
||||
protected override async ValueTask SaveToAsync(
|
||||
Stream stream,
|
||||
TarWriterOptions options,
|
||||
IAsyncEnumerable<TarArchiveEntry> oldEntries,
|
||||
IEnumerable<TarArchiveEntry> newEntries,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var writer = new TarWriter(stream, options);
|
||||
await foreach (
|
||||
var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false)
|
||||
)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
foreach (var entry in 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync()
|
||||
{
|
||||
var stream = Volumes.Single().Stream;
|
||||
stream.Position = 0;
|
||||
return new((IAsyncReader)new TarReader(stream, ReaderOptions, _compressionType));
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<TarArchiveEntry> LoadEntriesAsync(
|
||||
IAsyncEnumerable<TarVolume> volumes
|
||||
)
|
||||
{
|
||||
var sourceStream = (await volumes.SingleAsync().ConfigureAwait(false)).Stream;
|
||||
var stream = await GetStreamAsync(sourceStream).ConfigureAwait(false);
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Position = 0;
|
||||
}
|
||||
|
||||
var streamingMode =
|
||||
_compressionType == CompressionType.None
|
||||
? StreamingMode.Seekable
|
||||
: StreamingMode.Streaming;
|
||||
|
||||
await foreach (
|
||||
var header in TarHeaderFactory.ReadHeaderAsync(
|
||||
streamingMode,
|
||||
stream,
|
||||
ReaderOptions.ArchiveEncoding
|
||||
)
|
||||
)
|
||||
{
|
||||
if (header != null)
|
||||
{
|
||||
yield return new TarArchiveEntry(
|
||||
this,
|
||||
new TarFilePart(
|
||||
header,
|
||||
_compressionType == CompressionType.None ? stream : null
|
||||
),
|
||||
CompressionType.None,
|
||||
ReaderOptions
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IncompleteArchiveException("Failed to read TAR header");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
257
src/SharpCompress/Archives/Tar/TarArchive.Factory.cs
Normal file
257
src/SharpCompress/Archives/Tar/TarArchive.Factory.cs
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Tar.Headers;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Writers.Tar;
|
||||
|
||||
namespace SharpCompress.Archives.Tar;
|
||||
|
||||
public partial class TarArchive
|
||||
#if NET8_0_OR_GREATER
|
||||
: IWritableArchiveOpenable<TarWriterOptions>,
|
||||
IMultiArchiveOpenable<
|
||||
IWritableArchive<TarWriterOptions>,
|
||||
IWritableAsyncArchive<TarWriterOptions>
|
||||
>
|
||||
#endif
|
||||
{
|
||||
public static IWritableArchive<TarWriterOptions> OpenArchive(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return OpenArchive(new FileInfo(filePath), readerOptions);
|
||||
}
|
||||
|
||||
public static IWritableArchive<TarWriterOptions> OpenArchive(
|
||||
FileInfo fileInfo,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
return OpenArchive([fileInfo], readerOptions ?? ReaderOptions.ForFilePath);
|
||||
}
|
||||
|
||||
public static IWritableArchive<TarWriterOptions> OpenArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var files = fileInfos;
|
||||
var sourceStream = new SourceStream(
|
||||
files[0],
|
||||
i => i < files.Count ? files[i] : null,
|
||||
readerOptions ?? ReaderOptions.ForFilePath
|
||||
);
|
||||
var compressionType = TarFactory.GetCompressionType(
|
||||
sourceStream,
|
||||
sourceStream.ReaderOptions
|
||||
);
|
||||
sourceStream.Seek(0, SeekOrigin.Begin);
|
||||
return new TarArchive(sourceStream, compressionType);
|
||||
}
|
||||
|
||||
public static IWritableArchive<TarWriterOptions> OpenArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
var strms = streams.RequireReadable().RequireSeekable().ToList();
|
||||
var sourceStream = new SourceStream(
|
||||
strms[0],
|
||||
i => i < strms.Count ? strms[i] : null,
|
||||
readerOptions ?? ReaderOptions.ForExternalStream
|
||||
);
|
||||
var compressionType = TarFactory.GetCompressionType(
|
||||
sourceStream,
|
||||
sourceStream.ReaderOptions
|
||||
);
|
||||
sourceStream.Seek(0, SeekOrigin.Begin);
|
||||
return new TarArchive(sourceStream, compressionType);
|
||||
}
|
||||
|
||||
public static IWritableArchive<TarWriterOptions> OpenArchive(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
|
||||
return OpenArchive([stream], readerOptions);
|
||||
}
|
||||
|
||||
public static async ValueTask<IWritableAsyncArchive<TarWriterOptions>> OpenAsyncArchive(
|
||||
Stream stream,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
stream.RequireReadable();
|
||||
stream.RequireSeekable();
|
||||
var sourceStream = new SourceStream(
|
||||
stream,
|
||||
i => null,
|
||||
readerOptions ?? ReaderOptions.ForExternalStream
|
||||
);
|
||||
var compressionType = await TarFactory
|
||||
.GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
sourceStream.Seek(0, SeekOrigin.Begin);
|
||||
return new TarArchive(sourceStream, compressionType);
|
||||
}
|
||||
|
||||
public static ValueTask<IWritableAsyncArchive<TarWriterOptions>> OpenAsyncArchive(
|
||||
string filePath,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
return OpenAsyncArchive(new FileInfo(filePath), readerOptions, cancellationToken);
|
||||
}
|
||||
|
||||
public static async ValueTask<IWritableAsyncArchive<TarWriterOptions>> OpenAsyncArchive(
|
||||
FileInfo fileInfo,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
fileInfo.NotNull(nameof(fileInfo));
|
||||
readerOptions ??= ReaderOptions.ForFilePath;
|
||||
var sourceStream = new SourceStream(fileInfo, i => null, readerOptions);
|
||||
var compressionType = await TarFactory
|
||||
.GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
sourceStream.Seek(0, SeekOrigin.Begin);
|
||||
return new TarArchive(sourceStream, compressionType);
|
||||
}
|
||||
|
||||
public static async ValueTask<IWritableAsyncArchive<TarWriterOptions>> OpenAsyncArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var strms = streams.RequireReadable().RequireSeekable().ToList();
|
||||
var sourceStream = new SourceStream(
|
||||
strms[0],
|
||||
i => i < strms.Count ? strms[i] : null,
|
||||
readerOptions ?? ReaderOptions.ForExternalStream
|
||||
);
|
||||
var compressionType = await TarFactory
|
||||
.GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
sourceStream.Seek(0, SeekOrigin.Begin);
|
||||
return new TarArchive(sourceStream, compressionType);
|
||||
}
|
||||
|
||||
public static async ValueTask<IWritableAsyncArchive<TarWriterOptions>> OpenAsyncArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
fileInfos.NotNull(nameof(fileInfos));
|
||||
var files = fileInfos;
|
||||
var sourceStream = new SourceStream(
|
||||
files[0],
|
||||
i => i < files.Count ? files[i] : null,
|
||||
readerOptions ?? ReaderOptions.ForFilePath
|
||||
);
|
||||
var compressionType = await TarFactory
|
||||
.GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
sourceStream.Seek(0, SeekOrigin.Begin);
|
||||
return new TarArchive(sourceStream, compressionType);
|
||||
}
|
||||
|
||||
public static bool IsTarFile(string filePath) => IsTarFile(new FileInfo(filePath));
|
||||
|
||||
public static bool IsTarFile(FileInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using Stream stream = fileInfo.OpenRead();
|
||||
return IsTarFile(stream);
|
||||
}
|
||||
|
||||
public static bool IsTarFile(Stream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tarHeader = new TarHeader(new ArchiveEncoding());
|
||||
var reader = new BinaryReader(stream, Encoding.UTF8, false);
|
||||
var readSucceeded = tarHeader.Read(reader);
|
||||
var isEmptyArchive =
|
||||
tarHeader.Name?.Length == 0
|
||||
&& tarHeader.Size == 0
|
||||
&& IsDefined(tarHeader.EntryType);
|
||||
return readSucceeded || isEmptyArchive;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Catch all exceptions during tar header reading to determine if this is a valid tar file
|
||||
// Invalid tar files or corrupted streams will throw various exceptions
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async ValueTask<bool> IsTarFileAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tarHeader = new TarHeader(new ArchiveEncoding());
|
||||
#if NET8_0_OR_GREATER
|
||||
await using var reader = new AsyncBinaryReader(stream, leaveOpen: true);
|
||||
#else
|
||||
using var reader = new AsyncBinaryReader(stream, leaveOpen: true);
|
||||
#endif
|
||||
var readSucceeded = await tarHeader.ReadAsync(reader).ConfigureAwait(false);
|
||||
var isEmptyArchive =
|
||||
tarHeader.Name?.Length == 0
|
||||
&& tarHeader.Size == 0
|
||||
&& IsDefined(tarHeader.EntryType);
|
||||
return readSucceeded || isEmptyArchive;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Catch all exceptions during tar header reading to determine if this is a valid tar file
|
||||
// Invalid tar files or corrupted streams will throw various exceptions
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static IWritableArchive<TarWriterOptions> CreateArchive() => new TarArchive();
|
||||
|
||||
public static ValueTask<IWritableAsyncArchive<TarWriterOptions>> CreateAsyncArchive() =>
|
||||
new(new TarArchive());
|
||||
|
||||
private static bool IsDefined(EntryType value)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
return Enum.IsDefined(typeof(EntryType), value);
|
||||
#else
|
||||
return Enum.IsDefined(value);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -2,152 +2,123 @@ 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;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Providers;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Tar;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.Tar;
|
||||
|
||||
namespace SharpCompress.Archives.Tar;
|
||||
|
||||
public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
public partial class TarArchive
|
||||
: AbstractWritableArchive<TarArchiveEntry, TarVolume, TarWriterOptions>
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor expects a filepath to an existing file.
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static TarArchive Open(string filePath, ReaderOptions? readerOptions = null)
|
||||
private readonly CompressionType _compressionType;
|
||||
|
||||
protected override IEnumerable<TarVolume> LoadVolumes(SourceStream sourceStream)
|
||||
{
|
||||
filePath.CheckNotNullOrEmpty(nameof(filePath));
|
||||
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
|
||||
sourceStream.NotNull("SourceStream is null").LoadAllParts();
|
||||
return new TarVolume(sourceStream, ReaderOptions, 1).AsEnumerable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with a FileInfo object to an existing file.
|
||||
/// </summary>
|
||||
/// <param name="fileInfo"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static TarArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
|
||||
internal TarArchive(SourceStream sourceStream, CompressionType compressionType)
|
||||
: base(ArchiveType.Tar, sourceStream)
|
||||
{
|
||||
fileInfo.CheckNotNull(nameof(fileInfo));
|
||||
return new TarArchive(
|
||||
new SourceStream(
|
||||
fileInfo,
|
||||
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
|
||||
readerOptions ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
_compressionType = compressionType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with all file parts passed in
|
||||
/// </summary>
|
||||
/// <param name="fileInfos"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static TarArchive Open(
|
||||
IEnumerable<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
fileInfos.CheckNotNull(nameof(fileInfos));
|
||||
var files = fileInfos.ToArray();
|
||||
return new TarArchive(
|
||||
new SourceStream(
|
||||
files[0],
|
||||
i => i < files.Length ? files[i] : null,
|
||||
readerOptions ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with all stream parts passed in
|
||||
/// </summary>
|
||||
/// <param name="streams"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static TarArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
streams.CheckNotNull(nameof(streams));
|
||||
var strms = streams.ToArray();
|
||||
return new TarArchive(
|
||||
new SourceStream(
|
||||
strms[0],
|
||||
i => i < strms.Length ? strms[i] : null,
|
||||
readerOptions ?? new ReaderOptions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a seekable Stream as a source
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="readerOptions"></param>
|
||||
public static TarArchive Open(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
return new TarArchive(
|
||||
new SourceStream(stream, i => null, readerOptions ?? new ReaderOptions())
|
||||
);
|
||||
}
|
||||
|
||||
public static bool IsTarFile(string filePath) => IsTarFile(new FileInfo(filePath));
|
||||
|
||||
public static bool IsTarFile(FileInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using Stream stream = fileInfo.OpenRead();
|
||||
return IsTarFile(stream);
|
||||
}
|
||||
|
||||
public static bool IsTarFile(Stream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tarHeader = new TarHeader(new ArchiveEncoding());
|
||||
var readSucceeded = tarHeader.Read(new BinaryReader(stream));
|
||||
var isEmptyArchive =
|
||||
tarHeader.Name.Length == 0
|
||||
&& tarHeader.Size == 0
|
||||
&& Enum.IsDefined(typeof(EntryType), tarHeader.EntryType);
|
||||
return readSucceeded || isEmptyArchive;
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override IEnumerable<TarVolume> LoadVolumes(SourceStream srcStream)
|
||||
{
|
||||
SrcStream.LoadAllParts(); //request all streams
|
||||
var idx = 0;
|
||||
return new TarVolume(srcStream, ReaderOptions, idx++).AsEnumerable(); //simple single volume or split, multivolume not supported
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with a SourceStream able to handle FileInfo and Streams.
|
||||
/// </summary>
|
||||
/// <param name="srcStream"></param>
|
||||
/// <param name="options"></param>
|
||||
internal TarArchive(SourceStream srcStream)
|
||||
: base(ArchiveType.Tar, srcStream) { }
|
||||
|
||||
internal TarArchive()
|
||||
private TarArchive()
|
||||
: base(ArchiveType.Tar) { }
|
||||
|
||||
private Stream GetStream(Stream stream) =>
|
||||
_compressionType switch
|
||||
{
|
||||
CompressionType.BZip2 => ReaderOptions.Providers.CreateDecompressStream(
|
||||
CompressionType.BZip2,
|
||||
stream
|
||||
),
|
||||
CompressionType.GZip => ReaderOptions.Providers.CreateDecompressStream(
|
||||
CompressionType.GZip,
|
||||
stream,
|
||||
CompressionContext.FromStream(stream).WithReaderOptions(ReaderOptions)
|
||||
),
|
||||
CompressionType.ZStandard => ReaderOptions.Providers.CreateDecompressStream(
|
||||
CompressionType.ZStandard,
|
||||
stream
|
||||
),
|
||||
CompressionType.LZip => ReaderOptions.Providers.CreateDecompressStream(
|
||||
CompressionType.LZip,
|
||||
stream
|
||||
),
|
||||
CompressionType.Xz => ReaderOptions.Providers.CreateDecompressStream(
|
||||
CompressionType.Xz,
|
||||
stream
|
||||
),
|
||||
CompressionType.Lzw => ReaderOptions.Providers.CreateDecompressStream(
|
||||
CompressionType.Lzw,
|
||||
stream
|
||||
),
|
||||
CompressionType.None => stream,
|
||||
_ => throw new NotSupportedException("Invalid compression type: " + _compressionType),
|
||||
};
|
||||
|
||||
private ValueTask<Stream> GetStreamAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
_compressionType switch
|
||||
{
|
||||
CompressionType.BZip2 => ReaderOptions.Providers.CreateDecompressStreamAsync(
|
||||
CompressionType.BZip2,
|
||||
stream,
|
||||
cancellationToken
|
||||
),
|
||||
CompressionType.GZip => ReaderOptions.Providers.CreateDecompressStreamAsync(
|
||||
CompressionType.GZip,
|
||||
stream,
|
||||
CompressionContext.FromStream(stream).WithReaderOptions(ReaderOptions),
|
||||
cancellationToken
|
||||
),
|
||||
CompressionType.ZStandard => ReaderOptions.Providers.CreateDecompressStreamAsync(
|
||||
CompressionType.ZStandard,
|
||||
stream,
|
||||
cancellationToken
|
||||
),
|
||||
CompressionType.LZip => ReaderOptions.Providers.CreateDecompressStreamAsync(
|
||||
CompressionType.LZip,
|
||||
stream,
|
||||
cancellationToken
|
||||
),
|
||||
CompressionType.Xz => ReaderOptions.Providers.CreateDecompressStreamAsync(
|
||||
CompressionType.Xz,
|
||||
stream,
|
||||
cancellationToken
|
||||
),
|
||||
CompressionType.Lzw => ReaderOptions.Providers.CreateDecompressStreamAsync(
|
||||
CompressionType.Lzw,
|
||||
stream,
|
||||
cancellationToken
|
||||
),
|
||||
CompressionType.None => new ValueTask<Stream>(stream),
|
||||
_ => throw new NotSupportedException("Invalid compression type: " + _compressionType),
|
||||
};
|
||||
|
||||
protected override IEnumerable<TarArchiveEntry> LoadEntries(IEnumerable<TarVolume> volumes)
|
||||
{
|
||||
var stream = volumes.Single().Stream;
|
||||
TarHeader? previousHeader = null;
|
||||
var stream = GetStream(volumes.Single().Stream);
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Position = 0;
|
||||
}
|
||||
foreach (
|
||||
var header in TarHeaderFactory.ReadHeader(
|
||||
StreamingMode.Seekable,
|
||||
_compressionType == CompressionType.None
|
||||
? StreamingMode.Seekable
|
||||
: StreamingMode.Streaming,
|
||||
stream,
|
||||
ReaderOptions.ArchiveEncoding
|
||||
)
|
||||
|
|
@ -155,50 +126,25 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
|||
{
|
||||
if (header != null)
|
||||
{
|
||||
if (header.EntryType == EntryType.LongName)
|
||||
{
|
||||
previousHeader = header;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (previousHeader != null)
|
||||
{
|
||||
var entry = new TarArchiveEntry(
|
||||
this,
|
||||
new TarFilePart(previousHeader, stream),
|
||||
CompressionType.None
|
||||
);
|
||||
|
||||
var oldStreamPos = stream.Position;
|
||||
|
||||
using (var entryStream = entry.OpenEntryStream())
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
entryStream.TransferTo(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
var bytes = memoryStream.ToArray();
|
||||
|
||||
header.Name = ReaderOptions.ArchiveEncoding.Decode(bytes).TrimNulls();
|
||||
}
|
||||
|
||||
stream.Position = oldStreamPos;
|
||||
|
||||
previousHeader = null;
|
||||
}
|
||||
yield return new TarArchiveEntry(
|
||||
this,
|
||||
new TarFilePart(header, stream),
|
||||
CompressionType.None
|
||||
);
|
||||
}
|
||||
yield return new TarArchiveEntry(
|
||||
this,
|
||||
new TarFilePart(
|
||||
header,
|
||||
_compressionType == CompressionType.None ? stream : null
|
||||
),
|
||||
CompressionType.None,
|
||||
ReaderOptions
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IncompleteArchiveException("Failed to read TAR header");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static TarArchive Create() => new TarArchive();
|
||||
|
||||
protected override TarArchiveEntry CreateEntryInternal(
|
||||
string filePath,
|
||||
string key,
|
||||
Stream source,
|
||||
long size,
|
||||
DateTime? modified,
|
||||
|
|
@ -208,24 +154,42 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
|||
this,
|
||||
source,
|
||||
CompressionType.Unknown,
|
||||
filePath,
|
||||
key,
|
||||
size,
|
||||
modified,
|
||||
closeStream
|
||||
);
|
||||
|
||||
protected override TarArchiveEntry CreateDirectoryEntry(string key, DateTime? modified) =>
|
||||
new TarWritableArchiveEntry(this, key, modified);
|
||||
|
||||
protected override void SaveTo(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
TarWriterOptions options,
|
||||
IEnumerable<TarArchiveEntry> oldEntries,
|
||||
IEnumerable<TarArchiveEntry> newEntries
|
||||
)
|
||||
{
|
||||
using var writer = new TarWriter(stream, new TarWriterOptions(options));
|
||||
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
|
||||
using var writer = new TarWriter(stream, options);
|
||||
foreach (var entry in oldEntries.Concat(newEntries))
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
writer.Write(entry.Key, 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,6 +197,6 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
|||
{
|
||||
var stream = Volumes.Single().Stream;
|
||||
stream.Position = 0;
|
||||
return TarReader.Open(stream);
|
||||
return new TarReader(stream, ReaderOptions, _compressionType);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,31 @@
|
|||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Options;
|
||||
using SharpCompress.Common.Tar;
|
||||
|
||||
namespace SharpCompress.Archives.Tar;
|
||||
|
||||
public class TarArchiveEntry : TarEntry, IArchiveEntry
|
||||
{
|
||||
internal TarArchiveEntry(TarArchive archive, TarFilePart part, CompressionType compressionType)
|
||||
: base(part, compressionType) => Archive = archive;
|
||||
internal TarArchiveEntry(
|
||||
TarArchive archive,
|
||||
TarFilePart? part,
|
||||
CompressionType compressionType,
|
||||
IReaderOptions readerOptions
|
||||
)
|
||||
: base(part, compressionType, readerOptions) => Archive = archive;
|
||||
|
||||
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream();
|
||||
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
|
||||
|
||||
public async ValueTask<Stream> OpenEntryStreamAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
(
|
||||
await Parts.Single().GetCompressedStreamAsync(cancellationToken).ConfigureAwait(false)
|
||||
).NotNull();
|
||||
|
||||
#region IArchiveEntry Members
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue