1
0
mirror of https://github.com/Ne-Lexa/php-zip.git synced 2025-08-15 11:44:56 +02:00

157 Commits

Author SHA1 Message Date
Ne-Lexa
074443dbc4 Merge branch 'hotfix/3.2.2' 2020-02-04 11:29:58 +03:00
Ne-Lexa
cbb693213e fix replace contents by file 2020-02-04 11:18:32 +03:00
Ne-Lexa
d305ab68bc Merge branch 'hotfix/3.2.1' 2020-01-14 16:44:56 +03:00
Ne-Lexa
8487dac9df zip extra tests, php 32-bit compat 2020-01-14 16:31:22 +03:00
Ne-Lexa
d21fdb35bb fix#13 fix#16 fix#27 fix#31 fix#41
Tagging version 3.2.0 3.2.0
- Fix memory leak problem
- Add new methods: `getEntry()`, `getEntries()`, `addSplFile()`, `addFromFinder()`
- Fix large zip files problem
2020-01-09 17:41:47 +03:00
Ne-Lexa
25d5ddcbff Merge branch 'release/3.2.0' 2020-01-09 17:39:56 +03:00
Ne-Lexa
f426871549 Merge branch 'feature/nextversion' into develop 2020-01-09 17:33:10 +03:00
Ne-Lexa
fd1fbf3ad6 fix deprecation use 2020-01-09 17:33:02 +03:00
Ne-Lexa
971747f5e7 fix coverage 2020-01-09 13:11:13 +03:00
Ne-Lexa
77f2841724 removed config for php nightly (php 8.0-dev) 2020-01-09 13:00:23 +03:00
Ne-Lexa
c82e303002 configure phpunit flags 2020-01-09 12:57:28 +03:00
Ne-Lexa
61e9d96439 fix travis syntax 2020-01-09 12:53:47 +03:00
Ne-Lexa
800afffffa fix travis env 2020-01-09 12:50:03 +03:00
Ne-Lexa
1a04a5cdd9 matrix config 2020-01-09 12:48:47 +03:00
Ne-Lexa
6c49dd54c8 fix test 2020-01-09 12:05:02 +03:00
wapplay
a16b0e7c15 minor fixes, ZipEntry tests 2020-01-06 11:53:17 +03:00
wapplay
70bb598fa6 php5.5 compat 2020-01-04 00:46:07 +03:00
wapplay
27b8a3d32f replaced abandoned package 2020-01-04 00:20:43 +03:00
wapplay
75578fd181 fix php 5.5 compat 2020-01-04 00:13:54 +03:00
wapplay
8efbfd67f4 fix php 5.5 compat, fix append stream filter 2020-01-03 23:52:52 +03:00
Ne-Lexa
ca6c4fee6e fix travis 2019-12-30 19:05:11 +03:00
Ne-Lexa
9d42215088 exclude zip64 tests 2019-12-30 18:59:22 +03:00
Ne-Lexa
9ca14707d4 adds coverage 2019-12-30 18:56:58 +03:00
Ne-Lexa
28bddadac3 fix syntax error for php 5 2019-12-30 18:55:35 +03:00
Ne-Lexa
f377b889b5 solving problems with lack of memory, the new ZipReader and ZipWriter class, adds .phpstorm.meta.php
#13
#16
#27
#31
#41
2019-12-30 18:47:37 +03:00
Ne-Lexa
28e220718c Merge tag '3.1.15' into develop
fix #42 ZipFile::outputAsAttachment() gives empty zip
2019-12-11 15:27:49 +03:00
Ne-Lexa
3b769793fc Merge branch 'hotfix/3.1.15' 2019-12-11 15:26:23 +03:00
Ne-Lexa
7cbf992492 fix #42 2019-12-11 15:21:28 +03:00
Ne-Lexa
3ef867cde4 Merge tag '3.1.14' into develop
fix unknown zip software version
2019-12-09 12:18:20 +03:00
Ne-Lexa
c420f191dd Merge branch 'hotfix/3.1.14' 2019-12-09 12:17:55 +03:00
Ne-Lexa
50015f25f3 fix unknown software version 2019-12-09 12:13:56 +03:00
Ne-Lexa
36b1af0263 Merge tag '3.1.13' into develop
improved compatibility with php 7.4 and php-32 bit, minor bug fixes
2019-12-09 11:23:00 +03:00
Ne-Lexa
f2d2954bee Merge branch 'hotfix/3.1.13' 2019-12-09 11:20:56 +03:00
wapplay
725959d98f fix encryption 2019-12-07 20:16:40 +03:00
wapplay
f82a57e662 fix duplicates 2019-12-07 19:40:36 +03:00
wapplay
a20e9e054d use random_compat 2019-12-06 23:23:44 +03:00
Ne-Lexa
95e3312e60 refactoring zip64, add property softwareVersion, internal attrs, extracted os 2019-12-06 17:36:22 +03:00
Ne-Lexa
e2c058840c fixed for php 32-bit 2019-12-06 12:09:37 +03:00
wapplay
e03c963dc1 refactoring 2019-12-06 08:32:33 +03:00
wapplay
c0786f5947 set travis dist 2019-12-05 23:52:17 +03:00
Ne-Lexa
3bafe01ff0 php cs fix 2019-12-05 19:36:11 +03:00
wapplay
558bb04066 Merge tag '3.1.12' into develop
Tagging hotfix 3.1.12 3.1.12
2019-07-25 23:08:42 +03:00
wapplay
ca068fa78f Merge branch 'hotfix/3.1.12' 2019-07-25 23:08:42 +03:00
wapplay
a84d2f9eff fix issue #39 2019-07-25 23:03:28 +03:00
Ne-Lexa
97db60a52d Merge pull request #36 from chrisgraham/develop
Fixed slightly confusing language regarding setPassword
2019-04-04 11:37:51 +03:00
Chris Graham
4134ca8daa Fixed slightly confusing language regarding setPassword 2019-04-02 16:53:33 -05:00
Ne-Lexa
e7528b2974 Merge tag '3.1.11' into develop
Tagging hotfix 3.1.11 3.1.11
2019-03-13 15:41:56 +03:00
Ne-Lexa
19e17fb730 Merge branch 'hotfix/3.1.11' 2019-03-13 15:41:56 +03:00
Ne-Lexa
d8f913ac67 removed nightly PHP (8.0-dev) from travis 2019-03-13 15:37:01 +03:00
Ne-Lexa
1d1c8559cd update travis config 2019-03-13 15:29:40 +03:00
Ne-Lexa
e1108f9a24 Added .gitattributes to exclude tests from the release version #34 2019-03-13 15:28:38 +03:00
Ne-Lexa
96d269b4ca Merge tag '3.1.10' into develop
Tagging hotfix 3.1.10 3.1.10
2019-03-13 15:16:08 +03:00
Ne-Lexa
53da7053ba Merge branch 'hotfix/3.1.10' 2019-03-13 15:16:08 +03:00
Ne-Lexa
e8745e0379 fix #34 2019-03-13 15:15:52 +03:00
Ne-Lexa
b3277fcc5c Replaced the execution operator backticks with the function call exec in tests 2019-03-13 15:14:01 +03:00
Ne-Lexa
4aa9711e00 Merge tag '3.1.9' into develop
Tagging version 3.1.9 3.1.9
2018-11-02 15:15:08 +03:00
Ne-Lexa
c9871c9f80 Merge branch 'release-3.1.9' 2018-11-02 15:15:08 +03:00
Ne-Lexa
650fab4bad Merge branch 'master' into develop 2018-11-02 15:14:52 +03:00
Ne-Lexa
fd9750c4f3 Merge pull request #26 from belgattitude/fix/non-seekable-streams
Re-enable support for remote streams, fix #25
2018-11-02 15:11:54 +03:00
Sébastien Vanvelthem
e903642893 Re-enable support for remote streams, fix #25 2018-11-02 11:53:35 +01:00
wapplay
516d0c1e77 Merge tag '3.1.8' into develop
Tagging version 3.1.8 3.1.8
2018-10-21 19:31:21 +03:00
wapplay
9934a860c1 Merge branch 'release/3.1.8' 2018-10-21 19:31:21 +03:00
wapplay
c9f597308e cs fix 2018-10-21 19:30:45 +03:00
wapplay
837454ba7e Added additional check for correct decompression 2018-10-21 19:25:13 +03:00
wapplay
680a9d92c1 Merge branch 'hotfix/3.1.7' 2018-10-21 01:54:45 +03:00
wapplay
04a92e7904 Merge tag '3.1.7' into develop
Tagging hotfix 3.1.7 3.1.7
2018-10-21 01:54:45 +03:00
wapplay
e4e3a7504e Test streamWrapper extract (issue #24) 2018-10-21 01:54:30 +03:00
wapplay
d8bb1be43b Fix #24 Change fread for variable size reads 2018-10-21 01:32:01 +03:00
Ne-Lexa
c163f0583e update pull request template 2018-10-18 10:46:49 +03:00
Ne-Lexa
116a617744 added github templates 2018-10-18 10:42:57 +03:00
Ne-Lexa
e207086a75 Merge tag '3.1.6' into develop
Tagging version 3.1.6 3.1.6
2018-10-11 10:54:30 +03:00
Ne-Lexa
9bb20cc15e Merge branch 'release-3.1.6' 2018-10-11 10:54:30 +03:00
Ne-Lexa
7e84f97473 resolve merge conflict 2018-10-11 10:49:53 +03:00
Ne-Lexa
a13f4cc32f Merge branch 'master' into develop
# Conflicts:
#	src/PhpZip/ZipFile.php
#	tests/PhpZip/ZipFileTest.php
2018-10-11 10:39:05 +03:00
Ne-Lexa
c863c18869 Fix #22 Adding links to files, not open handles 2018-10-10 16:53:17 +03:00
wapplay
ad3bac6f96 cp866 to utf8 converter 2018-10-10 10:34:06 +03:00
wapplay
d2e94ac9cd update REAMDE 2018-10-10 07:43:29 +03:00
wapplay
3f0c6a7bd8 add php 7.2 in travis config 2018-10-10 07:41:43 +03:00
wapplay
062762ed09 update README 2018-10-10 07:41:19 +03:00
wapplay
e1866215a6 cs-fix 2018-10-09 10:17:54 +03:00
wapplay
f9e6a73587 PHP-doc updated (@throws added everywhere) and minor refactoring done. 2018-10-09 10:06:04 +03:00
wapplay
59773d62a8 Merge branch 'litlife-patch-1' into develop 2018-10-09 09:55:45 +03:00
wapplay
9417d7dc95 Merge branch 'patch-1' of https://github.com/litlife/php-zip into litlife-patch-1 2018-10-09 09:55:13 +03:00
Ne-Lexa
45905eacf0 Merge pull request #21 from MohannadNaj/rewording
Rewording some exceptions messages
2018-05-08 07:23:02 +03:00
MohannadNaj
aa8846b944 Rewording some exceptions messages 2018-05-08 03:40:39 +03:00
Ne-Lexa
6058c289a4 Merge tag '3.1.5' into develop
version 3.1.5
2018-04-25 12:44:40 +03:00
Ne-Lexa
251ce11bdc Merge branch 'hotfix/3.1.5' 2018-04-25 12:44:11 +03:00
Ne-Lexa
f969e59319 Updated ZipFile::outputAsResponse() to conform to PSR standard 2018-04-25 12:43:37 +03:00
Frederik Buus Sauer
6808e4ffdc Lowered requirement to support PHP 5.5 2018-04-12 11:25:12 +02:00
Frederik Buus Sauer
91f08b9f55 Updated composer.json regarding unit tests, and corrected PSR implementation 2018-04-12 11:10:45 +02:00
litlife
8de3a70571 Файлы при записывании в кеш не могут считаться
Привет! Спасибо за такой прекрасный модуль для работы с архивами!
Нашел такой глюк. При превышении размера 524288 файлы записывается в php://temp. При запросе данных из кеша stream_get_contents ничего не выводит. В fopen mode поменял на r+b, что включился режим записи. При запросе данных из stream нужно установить указатель в начало файла, поэтому перед ним вставил функцию rewind($this->entryContent);
2018-04-09 18:09:19 +03:00
Ne-Lexa
bdd5423f67 Merge tag '3.1.4' into develop
Tagging version 3.1.4 3.1.4
2018-02-01 09:52:40 +03:00
Ne-Lexa
d0cf7f7d1d Merge branch 'hotfix/3.1.4' 2018-02-01 09:52:40 +03:00
Ne-Lexa
9f0d151f5e Fixed installing the package on the MAC (illegal byte sequence unzip).
close issue #11
2018-02-01 09:52:35 +03:00
Ne-Lexa
e58cf0f337 Merge tag '3.1.3' into develop
Tagging version 3.1.3 3.1.3
2017-12-06 15:43:25 +03:00
Ne-Lexa
171d4a8e4c Merge branch 'hotfix/3.1.3' 2017-12-06 15:43:25 +03:00
Ne-Lexa
aa09b24d02 added an additional test of the encrypted archive 2017-12-06 15:28:17 +03:00
Ne-Lexa
c34f90ac18 fix bug issues #9 2017-12-06 15:09:50 +03:00
Ne-Lexa
fb1a9ced88 Merge tag '3.1.2' into develop
Tagging version 3.1.2 3.1.2
2017-11-17 11:13:15 +03:00
Ne-Lexa
7d73ac417f Merge branch 'hotfix/3.1.2' 2017-11-17 11:13:15 +03:00
Ne-Lexa
4979829fad Fixed the call array_fill() with the number of elements 0 for PHP 5.5 2017-11-17 11:08:20 +03:00
Ne-Lexa
a3083b821c Changed the algorithm for adding paddings to zipalign.
Now we will use the special field ExtraField c ID 0xD935,
which was implemented by Google in the apksigner library.
Now this field corresponds to the ZIP standard for storing
ExtraField records, and not just filling with zero bytes,
as in the zipalign console utility.
2017-11-17 10:54:22 +03:00
Ne-Lexa
a1da1f0069 Added an event that runs before the archive is saved or output. 2017-11-15 16:24:14 +03:00
Ne-Lexa
d67fc4db7d Merge branch 'hotfix/3.1.1' 2017-11-15 10:48:35 +03:00
Ne-Lexa
f29fed2753 Merge tag '3.1.1' into develop
Tagging version 3.1.1 3.1.1
2017-11-15 10:48:35 +03:00
Ne-Lexa
ba2e314ca2 Fix resave aligned archive 2017-11-15 10:48:29 +03:00
Ne-Lexa
0788892831 Merge branch 'release/3.1.0' 2017-11-14 15:27:09 +03:00
Ne-Lexa
03998d79a9 Merge tag '3.1.0' into develop
Tagging version 3.1.0 3.1.0
2017-11-14 15:27:09 +03:00
Ne-Lexa
ab41e70d5c Added changes to changelog. 2017-11-14 15:26:44 +03:00
Ne-Lexa
23addc5695 Merge branch 'feature/zipmodel' into develop
# Conflicts:
#	.travis.yml
#	README.md
#	src/PhpZip/Model/Entry/ZipReadEntry.php
#	tests/PhpZip/ZipFileTest.php
2017-11-14 14:55:33 +03:00
Ne-Lexa
d32b000855 Rename methods removePassword*() to disableEncryption*() 2017-11-14 14:47:46 +03:00
Ne-Lexa
e62e51efb5 issue #8 - Support inline Content-Disposition and empty output filename. 2017-11-14 14:03:44 +03:00
Ne-Lexa
02afaae56c Skipped some tests for php 32-bit platform. 2017-11-14 11:39:16 +03:00
Ne-Lexa
0d4b101510 Skipped some tests for php 32-bit platform. 2017-11-14 11:28:02 +03:00
wapplay-home-linux
1b1495eee8 Skipped some tests for a users with root privileges 2017-11-14 09:16:11 +03:00
wapplay-home-linux
129e69c293 fixed some errors tests for php-32 bit platform 2017-11-14 08:47:25 +03:00
wapplay-home-linux
ec919808d0 Revert "travis try install zipalign"
This reverts commit 8880275
2017-11-13 15:50:19 +03:00
wapplay-home-linux
88802754b3 travis try install zipalign 2017-11-13 15:47:24 +03:00
wapplay-home-linux
2f87d4f5ab update readme 2017-11-13 15:40:05 +03:00
wapplay-home-linux
452c5920dd Add ZipModel for all changes. 2017-11-13 15:33:37 +03:00
wapplay-home-linux
42c0fc59df Merge branch 'hotfix/3.0.3' 2017-11-11 17:21:12 +03:00
wapplay-home-linux
dcd6ab933b Merge tag '3.0.3' into develop
Tagging version 3.0.3 3.0.3
2017-11-11 17:21:12 +03:00
wapplay-home-linux
6688f474b5 fix bug issue #8 - Error if the file is empty 2017-11-11 17:21:01 +03:00
wapplay-home-linux
a72db0aa7d Merge tag '3.0.2' into develop
Tagging version 3.0.2 3.0.2
2017-10-30 23:32:11 +03:00
wapplay-home-linux
810b7ca741 Merge branch 'hotfix/3.0.2' 2017-10-30 23:32:11 +03:00
wapplay-home-linux
115dfd3b52 some bugs fixed for php 32-bit 2017-10-30 23:31:39 +03:00
Ne-Lexa
183274d6da remove build on hhvm 2017-06-19 00:44:00 +03:00
Ne-Lexa
6812423c89 Merge tag '3.0.1' into develop
Tagging version 3.0.1 3.0.1
2017-03-15 19:03:17 +03:00
Ne-Lexa
f99c0278fd Merge branch 'hotfix/3.0.1' 2017-03-15 19:03:17 +03:00
Ne-Lexa
1b065c4cca fix incorrect time in archive 2017-03-15 19:03:04 +03:00
Ne-Lexa
c49ec503c8 Merge tag '3.0.0' into develop
Tagging version 3.0.0 3.0.0
2017-03-15 15:35:18 +03:00
Ne-Lexa
67fc1ea7ad Merge branch 'release/3.0.0' 2017-03-15 15:35:17 +03:00
Ne-Lexa
9716976002 update changelog 2017-03-15 15:35:09 +03:00
Ne-Lexa
4ca1717979 Merge branch 'feature/3.0.0-dev' into develop 2017-03-15 15:27:19 +03:00
wapplay-home-linux
560649b1e8 Update README 2017-03-15 10:43:52 +03:00
wapplay-home-linux
3ab98532a0 Update README 2017-03-15 10:42:46 +03:00
Ne-Lexa
0dbdc0faeb README update 2017-03-13 19:59:30 +03:00
Ne-Lexa
1e4b14177a Fix bug add files from directory iterator. 2017-03-13 19:58:51 +03:00
wapplay-home-linux
eb183c9da0 update travis ci config 2017-03-13 12:12:25 +03:00
wapplay-home-linux
72ecdca941 update travis ci config 2017-03-13 12:08:03 +03:00
wapplay-home-linux
b958cb7e19 update travis ci config 2017-03-13 11:55:59 +03:00
wapplay-home-linux
e4650b7de2 add codeclimate 2017-03-13 11:39:05 +03:00
wapplay-home-linux
f6fc289102 update ci config 2017-03-13 10:49:36 +03:00
wapplay-home-linux
af4b66bb6e update ci config 2017-03-13 09:56:25 +03:00
wapplay-home-linux
bcd7949efe update ci config 2017-03-13 09:44:34 +03:00
wapplay-home-linux
3d8be5c339 update ci config 2017-03-13 09:37:16 +03:00
wapplay-home-linux
46654e3e8d update ci config 2017-03-13 09:13:19 +03:00
wapplay-home-linux
16c214b8a4 Add .travis.yml 2017-03-13 08:37:47 +03:00
wapplay-home-linux
db50dd8e46 Add tests 2017-03-13 08:33:45 +03:00
Ne-Lexa
08c890ba24 Add tests 2017-03-10 19:03:59 +03:00
wapplay-home-linux
6691858b95 remove dependencies 2017-03-10 08:30:14 +03:00
wapplay-home-linux
f802861d86 Merge ZipFile and ZipOutputFile, optimization update archive. 2017-03-10 08:23:57 +03:00
wapplay-home-linux
d8e40ee3f1 Merge branch 'release/2.2.0' 2017-03-02 00:17:06 +03:00
wapplay-home-linux
cc75f44949 Merge tag '2.2.0' into develop
Tagging version 2.2.0 2.2.0
2017-03-02 00:17:06 +03:00
wapplay-home-linux
58e9f4bf73 Easy to initialize the output zip file. 2017-03-02 00:16:09 +03:00
wapplay-home-linux
8d140cc1a1 Merge tag '2.1.1' into develop
Tagging version 2.1.1 2.1.1
2016-12-14 12:36:20 +03:00
Ne-Lexa
676eca7f87 Merge tag '2.1.0' into develop
Tagging version 2.1.0 2.1.0
2016-10-14 18:10:04 +03:00
135 changed files with 24518 additions and 7921 deletions

8
.gitattributes vendored Normal file
View File

@@ -0,0 +1,8 @@
.gitattributes export-ignore
.github export-ignore
.gitignore export-ignore
.php_cs export-ignore
.travis.yml export-ignore
bootstrap.php export-ignore
phpunit.xml export-ignore
tests export-ignore

23
.github/ISSUE_TEMPLATE/1_Bug_report.md vendored Normal file
View File

@@ -0,0 +1,23 @@
---
name: 🔴 Bug Report
about: Report errors and problems
---
| Q | A
| -----------------------------| ---
| Library version(s) affected: | x.y.z
| PHP version(s): | x.y.z
| OS (with bit depth): | <!-- ex. Ubuntu 18.04 64-bit -->
**Description**
<!-- A clear and concise description of the problem. -->
**How to reproduce**
<!-- Code to reproduce the problem. -->
**Possible Solution**
<!--- Optional: only if you have suggestions on a fix/reason for the bug -->
**Additional context**
<!-- Optional: any other context about the problem: error messages, stack trace, zip files, etc. -->

View File

@@ -0,0 +1,12 @@
---
name: 🚀 Feature Request
about: Ideas for new features and improvements
---
**Description**
<!-- A clear and concise description of the new feature. -->
**Example**
<!-- A simple example of the new feature in action (include PHP code)
If the new feature changes an existing feature, include a simple before/after comparison. -->

12
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,12 @@
| Q | A
| ------------- | ---
| Bug fix? | yes/no
| New feature? | yes/no <!-- don't forget to update CHANGELOG.md file -->
<!--
Write a short README entry for your feature/bugfix here (replace this comment block.)
Do NOT send pull request to `master` branch.
Please send to `develop` branch instead.
Any PR to `master` branch will NOT be merged.
-->

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/vendor
*.iml
/.idea
/composer.lock
/composer.lock
/*.cache

1501
.php_cs Normal file

File diff suppressed because it is too large Load Diff

128
.phpstorm.meta.php Normal file
View File

@@ -0,0 +1,128 @@
<?php
namespace PHPSTORM_META {
registerArgumentsSet(
"bool",
true,
false
);
registerArgumentsSet(
"compression_methods",
\PhpZip\Constants\ZipCompressionMethod::STORED,
\PhpZip\Constants\ZipCompressionMethod::DEFLATED,
\PhpZip\Constants\ZipCompressionMethod::BZIP2
);
expectedArguments(\PhpZip\ZipFile::addFile(), 2, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\ZipFile::addFromStream(), 2, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\ZipFile::addFromString(), 2, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\ZipFile::addDir(), 2, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\ZipFile::addDirRecursive(), 2, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\ZipFile::addFilesFromIterator(), 2, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\ZipFile::addFilesFromIterator(), 2, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\ZipFile::addFilesFromGlob(), 3, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\ZipFile::addFilesFromGlobRecursive(), 3, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\ZipFile::addFilesFromRegex(), 3, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\ZipFile::addFilesFromRegexRecursive(), 3, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\ZipFile::setCompressionMethodEntry(), 1, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\Model\ZipEntry::setCompressionMethod(), 0, argumentsSet("compression_methods"));
expectedArguments(\PhpZip\Model\ZipEntry::setMethod(), 0, argumentsSet("compression_methods"));
registerArgumentsSet(
'compression_levels',
\PhpZip\Constants\ZipCompressionLevel::MAXIMUM,
\PhpZip\Constants\ZipCompressionLevel::NORMAL,
\PhpZip\Constants\ZipCompressionLevel::FAST,
\PhpZip\Constants\ZipCompressionLevel::SUPER_FAST
);
expectedArguments(\PhpZip\ZipFile::setCompressionLevel(), 0, argumentsSet("compression_levels"));
expectedArguments(\PhpZip\ZipFile::setCompressionLevelEntry(), 1, argumentsSet("compression_levels"));
expectedArguments(\PhpZip\Model\ZipEntry::setCompressionLevel(), 0, argumentsSet("compression_levels"));
registerArgumentsSet(
'encryption_methods',
\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_256,
\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_192,
\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_128,
\PhpZip\Constants\ZipEncryptionMethod::PKWARE
);
expectedArguments(\PhpZip\ZipFile::setPassword(), 1, argumentsSet("encryption_methods"));
expectedArguments(\PhpZip\ZipFile::setPasswordEntry(), 2, argumentsSet("encryption_methods"));
expectedArguments(\PhpZip\Model\ZipEntry::setEncryptionMethod(), 0, argumentsSet("encryption_methods"));
expectedArguments(\PhpZip\Model\ZipEntry::setPassword(), 1, argumentsSet("encryption_methods"));
registerArgumentsSet(
'zip_mime_types',
null,
'application/zip',
'application/vnd.android.package-archive',
'application/java-archive'
);
expectedArguments(\PhpZip\ZipFile::outputAsAttachment(), 1, argumentsSet("zip_mime_types"));
expectedArguments(\PhpZip\ZipFile::outputAsAttachment(), 2, argumentsSet("bool"));
expectedArguments(\PhpZip\ZipFile::outputAsResponse(), 2, argumentsSet("zip_mime_types"));
expectedArguments(\PhpZip\ZipFile::outputAsResponse(), 3, argumentsSet("bool"));
registerArgumentsSet(
'dos_charset',
\PhpZip\Constants\DosCodePage::CP_LATIN_US,
\PhpZip\Constants\DosCodePage::CP_GREEK,
\PhpZip\Constants\DosCodePage::CP_BALT_RIM,
\PhpZip\Constants\DosCodePage::CP_LATIN1,
\PhpZip\Constants\DosCodePage::CP_LATIN2,
\PhpZip\Constants\DosCodePage::CP_CYRILLIC,
\PhpZip\Constants\DosCodePage::CP_TURKISH,
\PhpZip\Constants\DosCodePage::CP_PORTUGUESE,
\PhpZip\Constants\DosCodePage::CP_ICELANDIC,
\PhpZip\Constants\DosCodePage::CP_HEBREW,
\PhpZip\Constants\DosCodePage::CP_CANADA,
\PhpZip\Constants\DosCodePage::CP_ARABIC,
\PhpZip\Constants\DosCodePage::CP_NORDIC,
\PhpZip\Constants\DosCodePage::CP_CYRILLIC_RUSSIAN,
\PhpZip\Constants\DosCodePage::CP_GREEK2,
\PhpZip\Constants\DosCodePage::CP_THAI,
);
expectedArguments(\PhpZip\Model\ZipEntry::setCharset(), 0, argumentsSet('dos_charset'));
expectedArguments(\PhpZip\Constants\DosCodePage::toUTF8(), 1, argumentsSet('dos_charset'));
expectedArguments(\PhpZip\Constants\DosCodePage::fromUTF8(), 1, argumentsSet('dos_charset'));
registerArgumentsSet(
"zip_os",
\PhpZip\Constants\ZipPlatform::OS_UNIX,
\PhpZip\Constants\ZipPlatform::OS_DOS,
\PhpZip\Constants\ZipPlatform::OS_MAC_OSX,
);
expectedArguments(\PhpZip\Model\ZipEntry::setCreatedOS(), 0, argumentsSet('zip_os'));
expectedArguments(\PhpZip\Model\ZipEntry::setExtractedOS(), 0, argumentsSet('zip_os'));
expectedArguments(\PhpZip\Model\ZipEntry::setPlatform(), 0, argumentsSet('zip_os'));
registerArgumentsSet(
"zip_gpbf",
\PhpZip\Constants\GeneralPurposeBitFlag::ENCRYPTION |
\PhpZip\Constants\GeneralPurposeBitFlag::DATA_DESCRIPTOR |
\PhpZip\Constants\GeneralPurposeBitFlag::COMPRESSION_FLAG1 |
\PhpZip\Constants\GeneralPurposeBitFlag::COMPRESSION_FLAG2 |
\PhpZip\Constants\GeneralPurposeBitFlag::UTF8
);
expectedArguments(\PhpZip\Model\ZipEntry::setGeneralPurposeBitFlags(), 0, argumentsSet('zip_gpbf'));
registerArgumentsSet(
"winzip_aes_vendor_version",
\PhpZip\Model\Extra\Fields\WinZipAesExtraField::VERSION_AE1,
\PhpZip\Model\Extra\Fields\WinZipAesExtraField::VERSION_AE2
);
registerArgumentsSet(
"winzip_aes_key_strength",
\PhpZip\Model\Extra\Fields\WinZipAesExtraField::KEY_STRENGTH_256BIT,
\PhpZip\Model\Extra\Fields\WinZipAesExtraField::KEY_STRENGTH_128BIT,
\PhpZip\Model\Extra\Fields\WinZipAesExtraField::KEY_STRENGTH_192BIT
);
expectedArguments(\PhpZip\Model\Extra\Fields\WinZipAesExtraField::__construct(), 0, argumentsSet('winzip_aes_vendor_version'));
expectedArguments(\PhpZip\Model\Extra\Fields\WinZipAesExtraField::__construct(), 1, argumentsSet('winzip_aes_key_strength'));
expectedArguments(\PhpZip\Model\Extra\Fields\WinZipAesExtraField::__construct(), 2, argumentsSet('compression_methods'));
expectedArguments(\PhpZip\Model\Extra\Fields\WinZipAesExtraField::setVendorVersion(), 0, argumentsSet('winzip_aes_vendor_version'));
expectedArguments(\PhpZip\Model\Extra\Fields\WinZipAesExtraField::setKeyStrength(), 0, argumentsSet('winzip_aes_key_strength'));
expectedArguments(\PhpZip\Model\Extra\Fields\WinZipAesExtraField::setCompressionMethod(), 0, argumentsSet('compression_methods'));
}

67
.travis.yml Normal file
View File

@@ -0,0 +1,67 @@
language: php
env:
global:
- ZIPALIGN_INSTALL=false
- COVERAGE=false
- PHPUNIT_FLAGS="-v -c phpunit.xml --testsuite only_fast_tests"
matrix:
include:
- php: 5.5
os: linux
dist: trusty
- php: 5.6
os: linux
dist: xenial
env: ZIPALIGN_INSTALL=true
- php: 7.0
os: linux
dist: xenial
env: ZIPALIGN_INSTALL=true
- php: 7.1
os: linux
dist: xenial
env: ZIPALIGN_INSTALL=true
- php: 7.2
os: linux
dist: xenial
env: ZIPALIGN_INSTALL=true
- php: 7.3
os: linux
dist: xenial
env: ZIPALIGN_INSTALL=true
- php: 7.4
os: linux
dist: xenial
env: COVERAGE=true ZIPALIGN_INSTALL=true PHPUNIT_FLAGS="-v -c phpunit.xml --testsuite only_fast_tests --coverage-clover=coverage.clover"
before_install:
- if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi
install:
- travis_retry composer self-update && composer --version
- travis_retry composer install --no-interaction
addons:
apt:
packages:
- unzip
- p7zip-full
before_script:
- if [[ $ZIPALIGN_INSTALL = true ]]; then sudo apt-get install -y zipalign; fi
script:
- composer validate --no-check-lock
- vendor/bin/phpunit ${PHPUNIT_FLAGS}
after_success:
- if [[ $COVERAGE = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi
- if [[ $COVERAGE = true ]]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi

816
README.RU.md Normal file
View File

@@ -0,0 +1,816 @@
`PhpZip`
========
`PhpZip` - php библиотека для продвинутой работы с ZIP-архивами.
[![Build Status](https://travis-ci.org/Ne-Lexa/php-zip.svg?branch=master)](https://travis-ci.org/Ne-Lexa/php-zip)
[![Code Coverage](https://scrutinizer-ci.com/g/Ne-Lexa/php-zip/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Ne-Lexa/php-zip/?branch=master)
[![Latest Stable Version](https://poser.pugx.org/nelexa/zip/v/stable)](https://packagist.org/packages/nelexa/zip)
[![Total Downloads](https://poser.pugx.org/nelexa/zip/downloads)](https://packagist.org/packages/nelexa/zip)
[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%205.5-8892BF.svg)](https://php.net/)
[![License](https://poser.pugx.org/nelexa/zip/license)](https://packagist.org/packages/nelexa/zip)
[English Documentation](README.md)
Содержание
----------
- [Функционал](#Features)
- [Требования](#Requirements)
- [Установка](#Installation)
- [Примеры](#Examples)
- [Глоссарий](#Glossary)
- [Документация](#Documentation)
+ [Обзор методов класса `\PhpZip\ZipFile`](#Documentation-Overview)
+ [Создание/Открытие ZIP-архива](#Documentation-Open-Zip-Archive)
+ [Чтение записей из архива](#Documentation-Open-Zip-Entries)
+ [Перебор записей/Итератор](#Documentation-Zip-Iterate)
+ [Получение информации о записях](#Documentation-Zip-Info)
+ [Добавление записей в архив](#Documentation-Add-Zip-Entries)
+ [Удаление записей из архива](#Documentation-Remove-Zip-Entries)
+ [Работа с записями и с архивом](#Documentation-Entries)
+ [Работа с паролями](#Documentation-Password)
+ [zipalign - выравнивание архива для оптимизации Android пакетов (APK)](#Documentation-ZipAlign-Usage)
+ [Отмена изменений](#Documentation-Unchanged)
+ [Сохранение файла или вывод в браузер](#Documentation-Save-Or-Output-Entries)
+ [Закрытие архива](#Documentation-Close-Zip-Archive)
- [Запуск тестов](#Running-Tests)
- [История изменений](#Changelog)
- [Обновление версий](#Upgrade)
+ [Обновление с версии 2 до версии 3.0](#Upgrade-v2-to-v3)
### <a name="Features"></a> Функционал
- Открытие и разархивирование ZIP-архивов.
- Создание ZIP-архивов.
- Модификация ZIP-архивов.
- Чистый php (не требуется расширение `php-zip` и класс `\ZipArchive`).
- Поддерживается сохранение архива в файл, вывод архива в браузер или вывод в виде строки, без сохранения в файл.
- Поддерживаются комментарии архива и комментарии отдельных записей.
- Получение подробной информации о каждой записи в архиве.
- Поддерживаются только следующие методы сжатия:
+ Без сжатия (Stored).
+ Deflate сжатие.
+ BZIP2 сжатие при наличии расширения `php-bz2`.
- Поддержка `ZIP64` (размер файла более 4 GB или количество записей в архиве более 65535).
- Встроенная поддержка выравнивания архива для оптимизации Android пакетов (APK) [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html).
- Работа с паролями для PHP 5.5
> **Внимание!**
>
> Для 32-bit систем, в данный момент не поддерживается метод шифрование `Traditional PKWARE Encryption (ZipCrypto)`.
> Используйте метод шифрования `WinZIP AES Encryption`, когда это возможно.
+ Установка пароля для чтения архива глобально или для некоторых записей.
+ Изменение пароля архива, в том числе и для отдельных записей.
+ Удаление пароля архива глобально или для отдельных записей.
+ Установка пароля и/или метода шифрования, как для всех, так и для отдельных записей в архиве.
+ Установка разных паролей и методов шифрования для разных записей.
+ Удаление пароля для всех или для некоторых записей.
+ Поддержка методов шифрования `Traditional PKWARE Encryption (ZipCrypto)` и `WinZIP AES Encryption (128, 192 или 256 bit)`.
+ Установка метода шифрования для всех или для отдельных записей в архиве.
### <a name="Requirements"></a> Требования
- `PHP` >= 5.5 (предпочтительно 64-bit).
- Опционально php-расширение `bzip2` для поддержки BZIP2 компрессии.
- Опционально php-расширение `openssl` или `mcrypt` для `WinZip Aes Encryption` шифрования.
### <a name="Installation"></a> Установка
`composer require nelexa/zip`
Последняя стабильная версия: [![Latest Stable Version](https://poser.pugx.org/nelexa/zip/v/stable)](https://packagist.org/packages/nelexa/zip)
### <a name="Examples"></a> Примеры
```php
// создание нового архива
$zipFile = new \PhpZip\ZipFile();
try{
$zipFile
->addFromString('zip/entry/filename', "Is file content") // добавить запись из строки
->addFile('/path/to/file', 'data/tofile') // добавить запись из файла
->addDir(__DIR__, 'to/path/') // добавить файлы из директории
->saveAsFile($outputFilename) // сохранить архив в файл
->close(); // закрыть архив
// открытие архива, извлечение файлов, удаление файлов, добавление файлов, установка пароля и вывод архива в браузер.
$zipFile
->openFile($outputFilename) // открыть архив из файла
->extractTo($outputDirExtract) // извлечь файлы в заданную директорию
->deleteFromRegex('~^\.~') // удалить все скрытые (Unix) файлы
->addFromString('dir/file.txt', 'Test file') // добавить новую запись из строки
->setPassword('password') // установить пароль на все записи
->outputAsAttachment('library.jar'); // вывести в браузер без сохранения в файл
}
catch(\PhpZip\Exception\ZipException $e){
// обработка исключения
}
finally{
$zipFile->close();
}
```
Другие примеры можно посмотреть в папке `tests/`.
### <a name="Glossary"></a> Глоссарий
**Запись в ZIP-архиве (Zip Entry)** - файл или папка в ZIP-архиве. У каждой записи в архиве есть определённые свойства, например: имя файла, метод сжатия, метод шифрования, размер файла до сжатия, размер файла после сжатия, CRC32 и другие.
### <a name="Documentation"></a> Документация
#### <a name="Documentation-Overview"></a> Обзор методов класса `\PhpZip\ZipFile`
- [ZipFile::__construct](#Documentation-ZipFile-__construct) - инициализацирует ZIP-архив.
- [ZipFile::addAll](#Documentation-ZipFile-addAll) - добавляет все записи из массива.
- [ZipFile::addDir](#Documentation-ZipFile-addDir) - добавляет файлы из директории по указанному пути без вложенных директорий.
- [ZipFile::addDirRecursive](#Documentation-ZipFile-addDirRecursive) - добавляет файлы из директории по указанному пути c вложенными директориями.
- [ZipFile::addEmptyDir](#Documentation-ZipFile-addEmptyDir) - добавляет в ZIP-архив новую директорию.
- [ZipFile::addFile](#Documentation-ZipFile-addFile) - добавляет в ZIP-архив файл по указанному пути.
- [ZipFile::addSplFile](#Documentation-ZipFile-addSplFile) - добавляет объект `\SplFileInfo` в zip-архив.
- [ZipFile::addFromFinder](#Documentation-ZipFile-addFromFinder) - добавляет файлы из `Symfony\Component\Finder\Finder` в zip архив.
- [ZipFile::addFilesFromIterator](#Documentation-ZipFile-addFilesFromIterator) - добавляет файлы из итератора директорий.
- [ZipFile::addFilesFromGlob](#Documentation-ZipFile-addFilesFromGlob) - добавляет файлы из директории в соответствии с glob шаблоном без вложенных директорий.
- [ZipFile::addFilesFromGlobRecursive](#Documentation-ZipFile-addFilesFromGlobRecursive) - добавляет файлы из директории в соответствии с glob шаблоном c вложенными директориями.
- [ZipFile::addFilesFromRegex](#Documentation-ZipFile-addFilesFromRegex) - добавляет файлы из директории в соответствии с регулярным выражением без вложенных директорий.
- [ZipFile::addFilesFromRegexRecursive](#Documentation-ZipFile-addFilesFromRegexRecursive) - добавляет файлы из директории в соответствии с регулярным выражением c вложенными директориями.
- [ZipFile::addFromStream](#Documentation-ZipFile-addFromStream) - добавляет в ZIP-архив запись из потока.
- [ZipFile::addFromString](#Documentation-ZipFile-addFromString) - добавляет файл в ZIP-архив, используя его содержимое в виде строки.
- [ZipFile::close](#Documentation-ZipFile-close) - закрывает ZIP-архив.
- [ZipFile::count](#Documentation-ZipFile-count) - возвращает количество записей в архиве.
- [ZipFile::deleteFromName](#Documentation-ZipFile-deleteFromName) - удаляет запись по имени.
- [ZipFile::deleteFromGlob](#Documentation-ZipFile-deleteFromGlob) - удаляет записи в соответствии с glob шаблоном.
- [ZipFile::deleteFromRegex](#Documentation-ZipFile-deleteFromRegex) - удаляет записи в соответствии с регулярным выражением.
- [ZipFile::deleteAll](#Documentation-ZipFile-deleteAll) - удаляет все записи в ZIP-архиве.
- [ZipFile::disableEncryption](#Documentation-ZipFile-disableEncryption) - отключает шифрования всех записей, находящихся в архиве.
- [ZipFile::disableEncryptionEntry](#Documentation-ZipFile-disableEncryptionEntry) - отключает шифрование записи по её имени.
- [ZipFile::extractTo](#Documentation-ZipFile-extractTo) - извлекает содержимое архива в заданную директорию.
- [ZipFile::getAllInfo](#Documentation-ZipFile-getAllInfo) - возвращает подробную информацию обо всех записях в архиве.
- [ZipFile::getArchiveComment](#Documentation-ZipFile-getArchiveComment) - возвращает комментарий ZIP-архива.
- [ZipFile::getEntryComment](#Documentation-ZipFile-getEntryComment) - возвращает комментарий к записи, используя её имя.
- [ZipFile::getEntryContent](#Documentation-ZipFile-getEntryContent) - возвращает содержимое записи.
- [ZipFile::getEntryInfo](#Documentation-ZipFile-getEntryInfo) - возвращает подробную информацию о записи в архиве.
- [ZipFile::getListFiles](#Documentation-ZipFile-getListFiles) - возвращает список файлов архива.
- [ZipFile::hasEntry](#Documentation-ZipFile-hasEntry) - проверяет, присутствует ли запись в архиве.
- [ZipFile::isDirectory](#Documentation-ZipFile-isDirectory) - проверяет, является ли запись в архиве директорией.
- [ZipFile::matcher](#Documentation-ZipFile-matcher) - выборка записей в архиве для проведения операций над выбранными записями.
- [ZipFile::openFile](#Documentation-ZipFile-openFile) - открывает ZIP-архив из файла.
- [ZipFile::openFromString](#Documentation-ZipFile-openFromString) - открывает ZIP-архив из строки.
- [ZipFile::openFromStream](#Documentation-ZipFile-openFromStream) - открывает ZIP-архив из потока.
- [ZipFile::outputAsAttachment](#Documentation-ZipFile-outputAsAttachment) - выводит ZIP-архив в браузер.
- [ZipFile::outputAsResponse](#Documentation-ZipFile-outputAsResponse) - выводит ZIP-архив, как Response PSR-7.
- [ZipFile::outputAsString](#Documentation-ZipFile-outputAsString) - выводит ZIP-архив в виде строки.
- [ZipFile::rename](#Documentation-ZipFile-rename) - переименовывает запись по имени.
- [ZipFile::rewrite](#Documentation-ZipFile-rewrite) - сохраняет изменения и заново открывает изменившийся архив.
- [ZipFile::saveAsFile](#Documentation-ZipFile-saveAsFile) - сохраняет архив в файл.
- [ZipFile::saveAsStream](#Documentation-ZipFile-saveAsStream) - записывает архив в поток.
- [ZipFile::setArchiveComment](#Documentation-ZipFile-setArchiveComment) - устанавливает комментарий к ZIP-архиву.
- [ZipFile::setCompressionLevel](#Documentation-ZipFile-setCompressionLevel) - устанавливает уровень сжатия для всех файлов, находящихся в архиве.
- [ZipFile::setCompressionLevelEntry](#Documentation-ZipFile-setCompressionLevelEntry) - устанавливает уровень сжатия для определённой записи в архиве.
- [ZipFile::setCompressionMethodEntry](#Documentation-ZipFile-setCompressionMethodEntry) - устанавливает метод сжатия для определённой записи в архиве.
- [ZipFile::setEntryComment](#Documentation-ZipFile-setEntryComment) - устанавливает комментарий к записи, используя её имя.
- [ZipFile::setReadPassword](#Documentation-ZipFile-setReadPassword) - устанавливает пароль на чтение открытого запароленного архива для всех зашифрованных записей.
- [ZipFile::setReadPasswordEntry](#Documentation-ZipFile-setReadPasswordEntry) - устанавливает пароль на чтение конкретной зашифрованной записи открытого запароленного архива.
- ~~ZipFile::withNewPassword~~ - устаревший метод (**deprecated**) используйте метод [ZipFile::setPassword](#Documentation-ZipFile-setPassword).
- [ZipFile::setPassword](#Documentation-ZipFile-setPassword) - устанавливает новый пароль для всех файлов, находящихся в архиве.
- [ZipFile::setPasswordEntry](#Documentation-ZipFile-setPasswordEntry) - устанавливает новый пароль для конкретного файла.
- [ZipFile::setZipAlign](#Documentation-ZipFile-setZipAlign) - устанавливает выравнивание архива для оптимизации APK файлов (Android packages).
- [ZipFile::unchangeAll](#Documentation-ZipFile-unchangeAll) - отменяет все изменения, сделанные в архиве.
- [ZipFile::unchangeArchiveComment](#Documentation-ZipFile-unchangeArchiveComment) - отменяет изменения в комментарии к архиву.
- [ZipFile::unchangeEntry](#Documentation-ZipFile-unchangeEntry) - отменяет изменения для конкретной записи архива.
- ~~ZipFile::withoutPassword~~ - устаревший метод (**deprecated**) используйте метод [ZipFile::disableEncryption](#Documentation-ZipFile-disableEncryption).
- ~~ZipFile::withReadPassword~~ - устаревший метод (**deprecated**) используйте метод [ZipFile::setReadPassword](#Documentation-ZipFile-setReadPassword).
#### <a name="Documentation-Open-Zip-Archive"></a> Создание/Открытие ZIP-архива
<a name="Documentation-ZipFile-__construct"></a>**ZipFile::__construct** - Инициализацирует ZIP-архив.
```php
$zipFile = new \PhpZip\ZipFile();
```
<a name="Documentation-ZipFile-openFile"></a> **ZipFile::openFile** - открывает ZIP-архив из файла.
```php
$zipFile = new \PhpZip\ZipFile();
$zipFile->openFile('file.zip');
```
<a name="Documentation-ZipFile-openFromString"></a> **ZipFile::openFromString** - открывает ZIP-архив из строки.
```php
$zipFile = new \PhpZip\ZipFile();
$zipFile->openFromString($stringContents);
```
<a name="Documentation-ZipFile-openFromStream"></a> **ZipFile::openFromStream** - открывает ZIP-архив из потока.
```php
$stream = fopen('file.zip', 'rb');
$zipFile = new \PhpZip\ZipFile();
$zipFile->openFromStream($stream);
```
#### <a name="Documentation-Open-Zip-Entries"></a> Чтение записей из архива
<a name="Documentation-ZipFile-count"></a> **ZipFile::count** - возвращает количество записей в архиве.
```php
$zipFile = new \PhpZip\ZipFile();
$count = count($zipFile);
// или
$count = $zipFile->count();
```
<a name="Documentation-ZipFile-getListFiles"></a> **ZipFile::getListFiles** - возвращает список файлов архива.
```php
$zipFile = new \PhpZip\ZipFile();
$listFiles = $zipFile->getListFiles();
// Пример содержимого массива:
// array (
// 0 => 'info.txt',
// 1 => 'path/to/file.jpg',
// 2 => 'another path/',
// )
```
<a name="Documentation-ZipFile-getEntryContent"></a> **ZipFile::getEntryContent** - возвращает содержимое записи.
```php
// $entryName = 'path/to/example-entry-name.txt';
$zipFile = new \PhpZip\ZipFile();
$contents = $zipFile[$entryName];
// или
$contents = $zipFile->getEntryContents($entryName);
```
<a name="Documentation-ZipFile-hasEntry"></a> **ZipFile::hasEntry** - проверяет, присутствует ли запись в архиве.
```php
// $entryName = 'path/to/example-entry-name.txt';
$zipFile = new \PhpZip\ZipFile();
$hasEntry = isset($zipFile[$entryName]);
// или
$hasEntry = $zipFile->hasEntry($entryName);
```
<a name="Documentation-ZipFile-isDirectory"></a> **ZipFile::isDirectory** - проверяет, является ли запись в архиве директорией.
```php
// $entryName = 'path/to/';
$zipFile = new \PhpZip\ZipFile();
$isDirectory = $zipFile->isDirectory($entryName);
```
<a name="Documentation-ZipFile-extractTo"></a> **ZipFile::extractTo** - извлекает содержимое архива в заданную директорию.
Директория должна существовать.
```php
$zipFile = new \PhpZip\ZipFile();
$zipFile->extractTo($directory);
```
Можно извлечь только некоторые записи в заданную директорию.
Директория должна существовать.
```php
$extractOnlyFiles = [
'filename1',
'filename2',
'dir/dir/dir/'
];
$zipFile = new \PhpZip\ZipFile();
$zipFile->extractTo($toDirectory, $extractOnlyFiles);
```
#### <a name="Documentation-Zip-Iterate"></a> Перебор записей/Итератор
`ZipFile` является итератором.
Можно перебрать все записи, через цикл `foreach`.
```php
foreach($zipFile as $entryName => $contents){
echo "Файл: $entryName" . PHP_EOL;
echo "Содержимое: $contents" . PHP_EOL;
echo '-----------------------------' . PHP_EOL;
}
```
Можно использовать паттерн `Iterator`.
```php
$iterator = new \ArrayIterator($zipFile);
while ($iterator->valid())
{
$entryName = $iterator->key();
$contents = $iterator->current();
echo "Файл: $entryName" . PHP_EOL;
echo "Содержимое: $contents" . PHP_EOL;
echo '-----------------------------' . PHP_EOL;
$iterator->next();
}
```
#### <a name="Documentation-Zip-Info"></a> Получение информации о записях
<a name="Documentation-ZipFile-getArchiveComment"></a> **ZipFile::getArchiveComment** - возвращает комментарий ZIP-архива.
```php
$commentArchive = $zipFile->getArchiveComment();
```
<a name="Documentation-ZipFile-getEntryComment"></a> **ZipFile::getEntryComment** - возвращает комментарий к записи, используя её имя.
```php
$commentEntry = $zipFile->getEntryComment($entryName);
```
<a name="Documentation-ZipFile-getEntryInfo"></a> **ZipFile::getEntryInfo** - возвращает подробную информацию о записи в архиве.
```php
$zipFile = new \PhpZip\ZipFile();
$zipInfo = $zipFile->getEntryInfo('file.txt');
```
<a name="Documentation-ZipFile-getAllInfo"></a> **ZipFile::getAllInfo** - возвращает подробную информацию обо всех записях в архиве.
```php
$zipAllInfo = $zipFile->getAllInfo();
```
#### <a name="Documentation-Add-Zip-Entries"></a> Добавление записей в архив
Все методы добавления записей в ZIP-архив позволяют указать метод сжатия содержимого.
Доступны следующие методы сжатия:
- `\PhpZip\Constants\ZipCompressionMethod::STORED` - без сжатия
- `\PhpZip\Constants\ZipCompressionMethod::DEFLATED` - Deflate сжатие
- `\PhpZip\Constants\ZipCompressionMethod::BZIP2` - Bzip2 сжатие при наличии расширения `ext-bz2`
<a name="Documentation-ZipFile-addFile"></a> **ZipFile::addFile** - добавляет в ZIP-архив файл по указанному пути из файловой системы.
```php
$zipFile = new \PhpZip\ZipFile();
// $file = '...../file.ext';
$zipFile->addFile($file);
// можно указать имя записи в архиве (если null, то используется последний компонент из имени файла)
$zipFile->addFile($file, $entryName);
// можно указать метод сжатия
$zipFile->addFile($file, $entryName, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия
$zipFile->addFile($file, $entryName, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие
$zipFile->addFile($file, $entryName, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие
```
<a name="Documentation-ZipFile-addSplFile"></a>
**ZipFile::addSplFile"** - добавляет объект `\SplFileInfo` в zip-архив.
```php
// $file = '...../file.ext';
// $entryName = 'file2.ext'
$zipFile = new \PhpZip\ZipFile();
$splFile = new \SplFileInfo('README.md');
$zipFile->addSplFile($splFile);
$zipFile->addSplFile($splFile, $entryName);
// or
$zipFile[$entryName] = new \SplFileInfo($file);
// установить метод сжатия
$zipFile->addSplFile($splFile, $entryName, $options = [
\PhpZip\Constants\ZipOptions::COMPRESSION_METHOD => \PhpZip\Constants\ZipCompressionMethod::DEFLATED,
]);
```
<a name="Documentation-ZipFile-addFromFinder"></a>
**ZipFile::addFromFinder"** - добавляет файлы из `Symfony\Component\Finder\Finder` в zip архив.
https://symfony.com/doc/current/components/finder.html
```php
$finder = new \Symfony\Component\Finder\Finder();
$finder
->files()
->name('*.{jpg,jpeg,gif,png}')
->name('/^[0-9a-f]\./')
->contains('/lorem\s+ipsum$/i')
->in('path');
$zipFile = new \PhpZip\ZipFile();
$zipFile->addFromFinder($finder, $options = [
\PhpZip\Constants\ZipOptions::COMPRESSION_METHOD => \PhpZip\Constants\ZipCompressionMethod::DEFLATED,
\PhpZip\Constants\ZipOptions::MODIFIED_TIME => new \DateTimeImmutable('-1 day 5 min')
]);
```
<a name="Documentation-ZipFile-addFromString"></a> **ZipFile::addFromString** - добавляет файл в ZIP-архив, используя его содержимое в виде строки.
```php
$zipFile = new \PhpZip\ZipFile();
$zipFile[$entryName] = $contents;
// или
$zipFile->addFromString($entryName, $contents);
// можно указать метод сжатия
$zipFile->addFromString($entryName, $contents, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия
$zipFile->addFromString($entryName, $contents, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие
$zipFile->addFromString($entryName, $contents, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие
```
<a name="Documentation-ZipFile-addFromStream"></a> **ZipFile::addFromStream** - добавляет в ZIP-архив запись из потока.
```php
// $stream = fopen(..., 'rb');
$zipFile->addFromStream($stream, $entryName);
// можно указать метод сжатия
$zipFile->addFromStream($stream, $entryName, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия
$zipFile->addFromStream($stream, $entryName, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие
$zipFile->addFromStream($stream, $entryName, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие
```
<a name="Documentation-ZipFile-addEmptyDir"></a> **ZipFile::addEmptyDir** - добавляет в ZIP-архив новую (пустую) директорию.
```php
// $path = "path/to/";
$zipFile->addEmptyDir($path);
// или
$zipFile[$path] = null;
```
<a name="Documentation-ZipFile-addAll"></a> **ZipFile::addAll** - добавляет все записи из массива.
```php
$entries = [
'file.txt' => 'file contents', // запись из строки данных
'empty dir/' => null, // пустой каталог
'path/to/file.jpg' => fopen('..../filename', 'r'), // запись из потока
'path/to/file.dat' => new \SplFileInfo('..../filename'), // запись из файла
];
$zipFile->addAll($entries);
```
<a name="Documentation-ZipFile-addDir"></a> **ZipFile::addDir** - добавляет файлы из директории по указанному пути без вложенных директорий.
```php
$zipFile->addDir($dirName);
// можно указать путь в архиве в который необходимо поместить записи
$localPath = "to/path/";
$zipFile->addDir($dirName, $localPath);
// можно указать метод сжатия
$zipFile->addDir($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия
$zipFile->addDir($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие
$zipFile->addDir($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие
```
<a name="Documentation-ZipFile-addDirRecursive"></a> **ZipFile::addDirRecursive** - добавляет файлы из директории по указанному пути c вложенными директориями.
```php
$zipFile->addDirRecursive($dirName);
// можно указать путь в архиве в который необходимо поместить записи
$localPath = "to/path/";
$zipFile->addDirRecursive($dirName, $localPath);
// можно указать метод сжатия
$zipFile->addDirRecursive($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия
$zipFile->addDirRecursive($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие
$zipFile->addDirRecursive($dirName, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие
```
<a name="Documentation-ZipFile-addFilesFromIterator"></a> **ZipFile::addFilesFromIterator** - добавляет файлы из итератора директорий.
```php
// $directoryIterator = new \DirectoryIterator($dir); // без вложенных директорий
// $directoryIterator = new \RecursiveDirectoryIterator($dir); // с вложенными директориями
$zipFile->addFilesFromIterator($directoryIterator);
// можно указать путь в архиве в который необходимо поместить записи
$localPath = "to/path/";
$zipFile->addFilesFromIterator($directoryIterator, $localPath);
// или
$zipFile[$localPath] = $directoryIterator;
// можно указать метод сжатия
$zipFile->addFilesFromIterator($directoryIterator, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия
$zipFile->addFilesFromIterator($directoryIterator, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие
$zipFile->addFilesFromIterator($directoryIterator, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие
```
Пример добавления файлов из директории в архив с игнорированием некоторых файлов при помощи итератора директорий.
```php
$ignoreFiles = [
"file_ignore.txt",
"dir_ignore/sub dir ignore/"
];
// $directoryIterator = new \DirectoryIterator($dir); // без вложенных директорий
// $directoryIterator = new \RecursiveDirectoryIterator($dir); // с вложенными директориями
// используйте \PhpZip\Util\Iterator\IgnoreFilesFilterIterator для не рекурсивного поиска
$ignoreIterator = new \PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator(
$directoryIterator,
$ignoreFiles
);
$zipFile->addFilesFromIterator($ignoreIterator);
```
<a name="Documentation-ZipFile-addFilesFromGlob"></a> **ZipFile::addFilesFromGlob** - добавляет файлы из директории в соответствии с [glob шаблоном](https://en.wikipedia.org/wiki/Glob_(programming)) без вложенных директорий.
```php
$globPattern = '**.{jpg,jpeg,png,gif}'; // пример glob шаблона -> добавить все .jpg, .jpeg, .png и .gif файлы
$zipFile->addFilesFromGlob($dir, $globPattern);
// можно указать путь в архиве в который необходимо поместить записи
$localPath = "to/path/";
$zipFile->addFilesFromGlob($dir, $globPattern, $localPath);
// можно указать метод сжатия
$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия
$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие
$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие
```
<a name="Documentation-ZipFile-addFilesFromGlobRecursive"></a> **ZipFile::addFilesFromGlobRecursive** - добавляет файлы из директории в соответствии с [glob шаблоном](https://en.wikipedia.org/wiki/Glob_(programming)) c вложенными директориями.
```php
$globPattern = '**.{jpg,jpeg,png,gif}'; // пример glob шаблона -> добавить все .jpg, .jpeg, .png и .gif файлы
$zipFile->addFilesFromGlobRecursive($dir, $globPattern);
// можно указать путь в архиве в который необходимо поместить записи
$localPath = "to/path/";
$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath);
// можно указать метод сжатия
$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия
$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие
$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие
```
<a name="Documentation-ZipFile-addFilesFromRegex"></a> **ZipFile::addFilesFromRegex** - добавляет файлы из директории в соответствии с [регулярным выражением](https://en.wikipedia.org/wiki/Regular_expression) без вложенных директорий.
```php
$regexPattern = '/\.(jpe?g|png|gif)$/si'; // пример регулярного выражения -> добавить все .jpg, .jpeg, .png и .gif файлы
$zipFile->addFilesFromRegex($dir, $regexPattern);
// можно указать путь в архиве в который необходимо поместить записи
$localPath = "to/path/";
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath);
// можно указать метод сжатия
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие
```
<a name="Documentation-ZipFile-addFilesFromRegexRecursive"></a> **ZipFile::addFilesFromRegexRecursive** - добавляет файлы из директории в соответствии с [регулярным выражением](https://en.wikipedia.org/wiki/Regular_expression) с вложенными директориями.
```php
$regexPattern = '/\.(jpe?g|png|gif)$/si'; // пример регулярного выражения -> добавить все .jpg, .jpeg, .png и .gif файлы
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern);
// можно указать путь в архиве в который необходимо поместить записи
$localPath = "to/path/";
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath);
// можно указать метод сжатия
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::STORED); // Без сжатия
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::DEFLATED); // Deflate сжатие
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, \PhpZip\Constants\ZipCompressionMethod::BZIP2); // BZIP2 сжатие
```
#### <a name="Documentation-Remove-Zip-Entries"></a> Удаление записей из архива
<a name="Documentation-ZipFile-deleteFromName"></a> **ZipFile::deleteFromName** - удаляет запись по имени.
```php
$zipFile->deleteFromName($entryName);
```
<a name="Documentation-ZipFile-deleteFromGlob"></a> **ZipFile::deleteFromGlob** - удаляет записи в соответствии с [glob шаблоном](https://en.wikipedia.org/wiki/Glob_(programming)).
```php
$globPattern = '**.{jpg,jpeg,png,gif}'; // пример glob шаблона -> удалить все .jpg, .jpeg, .png и .gif файлы
$zipFile->deleteFromGlob($globPattern);
```
<a name="Documentation-ZipFile-deleteFromRegex"></a> **ZipFile::deleteFromRegex** - удаляет записи в соответствии с [регулярным выражением](https://en.wikipedia.org/wiki/Regular_expression).
```php
$regexPattern = '/\.(jpe?g|png|gif)$/si'; // пример регулярному выражения -> удалить все .jpg, .jpeg, .png и .gif файлы
$zipFile->deleteFromRegex($regexPattern);
```
<a name="Documentation-ZipFile-deleteAll"></a> **ZipFile::deleteAll** - удаляет все записи в ZIP-архиве.
```php
$zipFile->deleteAll();
```
#### <a name="Documentation-Entries"></a> Работа с записями и с архивом
<a name="Documentation-ZipFile-rename"></a> **ZipFile::rename** - переименовывает запись по имени.
```php
$zipFile->rename($oldName, $newName);
```
<a name="Documentation-ZipFile-setCompressionLevel"></a> **ZipFile::setCompressionLevel** - устанавливает уровень сжатия для всех файлов, находящихся в архиве.
> _Обратите внимание, что действие данного метода не распространяется на записи, добавленные после выполнения этого метода._
По умолчанию используется уровень сжатия 5 (`\PhpZip\Constants\ZipCompressionLevel::NORMAL`) или уровень сжатия, определённый в архиве для Deflate сжатия.
Поддерживаются диапазон значений от 1 (`\PhpZip\Constants\ZipCompressionLevel::SUPER_FAST`) до 9 (`\PhpZip\Constants\ZipCompressionLevel::MAXIMUM`). Чем выше число, тем лучше и дольше сжатие.
```php
$zipFile->setCompressionLevel(\PhpZip\Constants\ZipCompressionLevel::MAXIMUM);
```
<a name="Documentation-ZipFile-setCompressionLevelEntry"></a> **ZipFile::setCompressionLevelEntry** - устанавливает уровень сжатия для определённой записи в архиве.
Поддерживаются диапазон значений от 1 (`\PhpZip\Constants\ZipCompressionLevel::SUPER_FAST`) до 9 (`\PhpZip\Constants\ZipCompressionLevel::MAXIMUM`). Чем выше число, тем лучше и дольше сжатие.
```php
$zipFile->setCompressionLevelEntry($entryName, \PhpZip\Constants\ZipCompressionLevel::MAXIMUM);
```
<a name="Documentation-ZipFile-setCompressionMethodEntry"></a> **ZipFile::setCompressionMethodEntry** - устанавливает метод сжатия для определённой записи в архиве.
Доступны следующие методы сжатия:
- `\PhpZip\Constants\ZipCompressionMethod::STORED` - без сжатия
- `\PhpZip\Constants\ZipCompressionMethod::DEFLATED` - Deflate сжатие
- `\PhpZip\Constants\ZipCompressionMethod::BZIP2` - Bzip2 сжатие при наличии расширения `ext-bz2`
```php
$zipFile->setCompressionMethodEntry($entryName, \PhpZip\Constants\ZipCompressionMethod::DEFLATED);
```
<a name="Documentation-ZipFile-setArchiveComment"></a> **ZipFile::setArchiveComment** - устанавливает комментарий к ZIP-архиву.
```php
$zipFile->setArchiveComment($commentArchive);
```
<a name="Documentation-ZipFile-setEntryComment"></a> **ZipFile::setEntryComment** - устанавливает комментарий к записи, используя её имя.
```php
$zipFile->setEntryComment($entryName, $comment);
```
<a name="Documentation-ZipFile-matcher"></a> **ZipFile::matcher** - выборка записей в архиве для проведения операций над выбранными записями.
```php
$matcher = $zipFile->matcher();
```
Выбор файлов из архива по одному:
```php
$matcher
->add('entry name')
->add('another entry');
```
Выбор нескольких файлов в архиве:
```php
$matcher->add([
'entry name',
'another entry name',
'path/'
]);
```
Выбор файлов по регулярному выражению:
```php
$matcher->match('~\.jpe?g$~i');
```
Выбор всех файлов в архиве:
```php
$matcher->all();
```
count() - получает количество выбранных записей:
```php
$count = count($matcher);
// или
$count = $matcher->count();
```
getMatches() - получает список выбранных записей:
```php
$entries = $matcher->getMatches();
// пример содержимого: ['entry name', 'another entry name'];
```
invoke() - выполняет пользовательскую функцию над выбранными записями:
```php
// пример
$matcher->invoke(function($entryName) use($zipFile) {
$newName = preg_replace('~\.(jpe?g)$~i', '.no_optimize.$1', $entryName);
$zipFile->rename($entryName, $newName);
});
```
Функции для работы над выбранными записями:
```php
$matcher->delete(); // удалет выбранные записи из ZIP-архива
$matcher->setPassword($password); // устанавливает новый пароль на выбранные записи
$matcher->setPassword($password, $encryptionMethod); // устанавливает новый пароль и метод шифрования на выбранные записи
$matcher->setEncryptionMethod($encryptionMethod); // устанавливает метод шифрования на выбранные записи
$matcher->disableEncryption(); // отключает шифрование для выбранных записей
```
#### <a name="Documentation-Password"></a> Работа с паролями
Реализована поддержка методов шифрования:
- `\PhpZip\Constants\ZipEncryptionMethod::PKWARE` - Traditional PKWARE encryption
- `\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_256` - WinZip AES encryption 256 bit (рекомендуемое)
- `\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_192` - WinZip AES encryption 192 bit
- `\PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_128` - WinZip AES encryption 128 bit
<a name="Documentation-ZipFile-setReadPassword"></a> **ZipFile::setReadPassword** - устанавливает пароль на чтение открытого запароленного архива для всех зашифрованных записей.
> _Установка пароля не является обязательной для добавления новых записей или удаления существующих, но если вы захотите извлечь контент или изменить метод/уровень сжатия, метод шифрования или изменить пароль, то в этом случае пароль необходимо указать._
```php
$zipFile->setReadPassword($password);
```
<a name="Documentation-ZipFile-setReadPasswordEntry"></a> **ZipFile::setReadPasswordEntry** - устанавливает пароль на чтение конкретной зашифрованной записи открытого запароленного архива.
```php
$zipFile->setReadPasswordEntry($entryName, $password);
```
<a name="Documentation-ZipFile-setPassword"></a> **ZipFile::setPassword** - устанавливает новый пароль для всех файлов, находящихся в архиве.
> _Обратите внимание, что действие данного метода не распространяется на записи, добавленные после выполнения этого метода._
```php
$zipFile->setPassword($password);
```
Можно установить метод шифрования:
```php
$encryptionMethod = \PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_256;
$zipFile->setPassword($password, $encryptionMethod);
```
<a name="Documentation-ZipFile-setPasswordEntry"></a> **ZipFile::setPasswordEntry** - устанавливает новый пароль для конкретного файла.
```php
$zipFile->setPasswordEntry($entryName, $password);
```
Можно установить метод шифрования:
```php
$encryptionMethod = \PhpZip\Constants\ZipEncryptionMethod::WINZIP_AES_256;
$zipFile->setPasswordEntry($entryName, $password, $encryptionMethod);
```
<a name="Documentation-ZipFile-disableEncryption"></a> **ZipFile::disableEncryption** - отключает шифрования всех записей, находящихся в архиве.
> _Обратите внимание, что действие данного метода не распространяется на записи, добавленные после выполнения этого метода._
```php
$zipFile->disableEncryption();
```
<a name="Documentation-ZipFile-disableEncryptionEntry"></a> **ZipFile::disableEncryptionEntry** - отключает шифрование записи по её имени.
```php
$zipFile->disableEncryptionEntry($entryName);
```
#### <a name="Documentation-ZipAlign-Usage"></a> zipalign
<a name="Documentation-ZipFile-setZipAlign"></a> **ZipFile::setZipAlign** - устанавливает выравнивание архива для оптимизации APK файлов (Android packages).
Метод добавляет паддинги незашифрованным и не сжатым записям, для оптимизации расхода памяти в системе Android. Рекомендуется использовать для `APK` файлов. Файл может незначительно увеличиться.
Этот метод является альтернативой вызова команды `zipalign -f -v 4 filename.zip`.
Подробнее можно ознакомиться по [ссылке](https://developer.android.com/studio/command-line/zipalign.html).
```php
// вызовите до сохранения или вывода архива
$zipFile->setZipAlign(4);
```
#### <a name="Documentation-Unchanged"></a> Отмена изменений
<a name="Documentation-ZipFile-unchangeAll"></a> **ZipFile::unchangeAll** - отменяет все изменения, сделанные в архиве.
```php
$zipFile->unchangeAll();
```
<a name="Documentation-ZipFile-unchangeArchiveComment"></a> **ZipFile::unchangeArchiveComment** - отменяет изменения в комментарии к архиву.
```php
$zipFile->unchangeArchiveComment();
```
<a name="Documentation-ZipFile-unchangeEntry"></a> **ZipFile::unchangeEntry** - отменяет изменения для конкретной записи архива.
```php
$zipFile->unchangeEntry($entryName);
```
#### <a name="Documentation-Save-Or-Output-Entries"></a> Сохранение файла или вывод в браузер
<a name="Documentation-ZipFile-saveAsFile"></a> **ZipFile::saveAsFile** - сохраняет архив в файл.
```php
$zipFile->saveAsFile($filename);
```
<a name="Documentation-ZipFile-saveAsStream"></a> **ZipFile::saveAsStream** - записывает архив в поток.
```php
// $fp = fopen($filename, 'w+b');
$zipFile->saveAsStream($fp);
```
<a name="Documentation-ZipFile-outputAsString"></a> **ZipFile::outputAsString** - выводит ZIP-архив в виде строки.
```php
$rawZipArchiveBytes = $zipFile->outputAsString();
```
<a name="Documentation-ZipFile-outputAsAttachment"></a> **ZipFile::outputAsAttachment** - выводит ZIP-архив в браузер.
При выводе устанавливаются необходимые заголовки, а после вывода завершается работа скрипта.
```php
$zipFile->outputAsAttachment($outputFilename);
```
Можно установить MIME-тип:
```php
$mimeType = 'application/zip'
$zipFile->outputAsAttachment($outputFilename, $mimeType);
```
<a name="Documentation-ZipFile-outputAsResponse"></a> **ZipFile::outputAsResponse** - выводит ZIP-архив, как Response [PSR-7](http://www.php-fig.org/psr/psr-7/).
Метод вывода может использоваться в любом PSR-7 совместимом фреймворке.
```php
// $response = ....; // instance Psr\Http\Message\ResponseInterface
$zipFile->outputAsResponse($response, $outputFilename);
```
Можно установить MIME-тип:
```php
$mimeType = 'application/zip'
$zipFile->outputAsResponse($response, $outputFilename, $mimeType);
```
Пример для Slim Framework:
```php
$app = new \Slim\App;
$app->get('/download', function ($req, $res, $args) {
$zipFile = new \PhpZip\ZipFile();
$zipFile['file.txt'] = 'content';
return $zipFile->outputAsResponse($res, 'file.zip');
});
$app->run();
```
<a name="Documentation-ZipFile-rewrite"></a> **ZipFile::rewrite** - сохраняет изменения и заново открывает изменившийся архив.
```php
$zipFile->rewrite();
```
#### <a name="Documentation-Close-Zip-Archive"></a> Закрытие архива
<a name="Documentation-ZipFile-close"></a> **ZipFile::close** - закрывает ZIP-архив.
```php
$zipFile->close();
```
### <a name="Running-Tests"></a> Запуск тестов
Установите зависимости для разработки.
```bash
composer install --dev
```
Запустите тесты:
```bash
vendor/bin/phpunit -v -c phpunit.xml
```
### <a name="Changelog"></a> История изменений
История изменений на [странице релизов](https://github.com/Ne-Lexa/php-zip/releases).
### <a name="Upgrade"></a> Обновление версий
#### <a name="Upgrade-v2-to-v3"></a> Обновление с версии 2 до версии 3.0
Обновите мажорную версию в файле `composer.json` до `^3.0`.
```json
{
"require": {
"nelexa/zip": "^3.0"
}
}
```
Затем установите обновления с помощью `Composer`:
```bash
composer update nelexa/zip
```
Обновите ваш код для работы с новой версией:
- Класс `ZipOutputFile` объединён с `ZipFile` и удалён.
+ Замените `new \PhpZip\ZipOutputFile()` на `new \PhpZip\ZipFile()`
- Статичиская инициализация методов стала не статической.
+ Замените `\PhpZip\ZipFile::openFromFile($filename);` на `(new \PhpZip\ZipFile())->openFile($filename);`
+ Замените `\PhpZip\ZipOutputFile::openFromFile($filename);` на `(new \PhpZip\ZipFile())->openFile($filename);`
+ Замените `\PhpZip\ZipFile::openFromString($contents);` на `(new \PhpZip\ZipFile())->openFromString($contents);`
+ Замените `\PhpZip\ZipFile::openFromStream($stream);` на `(new \PhpZip\ZipFile())->openFromStream($stream);`
+ Замените `\PhpZip\ZipOutputFile::create()` на `new \PhpZip\ZipFile()`
+ Замените `\PhpZip\ZipOutputFile::openFromZipFile($zipFile)` на `(new \PhpZip\ZipFile())->openFile($filename);`
- Переименуйте методы:
+ `addFromFile` в `addFile`
+ `setLevel` в `setCompressionLevel`
+ `ZipFile::setPassword` в `ZipFile::withReadPassword`
+ `ZipOutputFile::setPassword` в `ZipFile::withNewPassword`
+ `ZipOutputFile::disableEncryptionAllEntries` в `ZipFile::withoutPassword`
+ `ZipOutputFile::setComment` в `ZipFile::setArchiveComment`
+ `ZipFile::getComment` в `ZipFile::getArchiveComment`
- Изменились сигнатуры для методов `addDir`, `addFilesFromGlob`, `addFilesFromRegex`.
- Удалены методы:
+ `getLevel`
+ `setCompressionMethod`
+ `setEntryPassword`

1197
README.md

File diff suppressed because it is too large Load Diff

6
bootstrap.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
// see https://stackoverflow.com/questions/33299149/phpstorm-8-and-phpunit-problems-with-runinseparateprocess/37174348#37174348
if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
define('PHPUNIT_COMPOSER_INSTALL', __DIR__ . '/vendor/autoload.php');
}

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<phpunit bootstrap="./vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="PhpZip test suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -1,38 +1,59 @@
{
"name": "nelexa/zip",
"description": "Zip files CRUD. Open, create, update, extract and get info tool. Supports appending to existing ZIP files, WinZip AES encryption, Traditional PKWARE Encryption, ZipAlign tool, BZIP2 compression, external file attributes and ZIP64 extensions. Alternative ZipArchive. It does not require php-zip extension.",
"type": "library",
"keywords": [
"zip",
"archive",
"extract",
"winzip",
"zipalign"
],
"require-dev": {
"phpunit/phpunit": "4.8"
},
"license": "MIT",
"authors": [
{
"name": "Ne-Lexa",
"email": "alexey@nelexa.ru",
"role": "Developer"
"name": "nelexa/zip",
"type": "library",
"description": "PhpZip is a php-library for extended work with ZIP-archives. Open, create, update, delete, extract and get info tool. Supports appending to existing ZIP files, WinZip AES encryption, Traditional PKWARE Encryption, ZipAlign tool, BZIP2 compression, external file attributes and ZIP64 extensions. Alternative ZipArchive. It does not require php-zip extension.",
"keywords": [
"zip",
"unzip",
"archive",
"extract",
"winzip",
"zipalign",
"ziparchive"
],
"homepage": "https://github.com/Ne-Lexa/php-zip",
"license": "MIT",
"authors": [
{
"name": "Ne-Lexa",
"email": "alexey@nelexa.ru",
"role": "Developer"
}
],
"require": {
"php": "^5.5.9 || ^7.0",
"ext-zlib": "*",
"psr/http-message": "^1.0",
"paragonie/random_compat": ">=1 <9.99",
"symfony/finder": "^3.0|^4.0|^5.0"
},
"require-dev": {
"ext-bz2": "*",
"ext-openssl": "*",
"ext-fileinfo": "*",
"guzzlehttp/psr7": "^1.6",
"phpunit/phpunit": "^4.8|^5.7",
"symfony/var-dumper": "^3.0|^4.0|^5.0"
},
"autoload": {
"psr-4": {
"PhpZip\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"PhpZip\\Tests\\": "tests/"
}
},
"suggest": {
"ext-openssl": "Needed to support encrypt zip entries or use ext-mcrypt",
"ext-mcrypt": "Needed to support encrypt zip entries or use ext-openssl",
"ext-bz2": "Needed to support BZIP2 compression",
"ext-fileinfo": "Needed to get mime-type file"
},
"minimum-stability": "stable",
"scripts": {
"php:fix": "php .php_cs --force",
"php:fix:debug": "php .php_cs"
}
],
"minimum-stability": "stable",
"require": {
"php-64bit": "^5.4 || ^7.0",
"ext-mbstring": "*"
},
"autoload": {
"psr-4": {
"": "src/"
}
},
"autoload-dev": {
"psr-4": {
"": "tests/"
}
}
}

30
phpunit.xml Normal file
View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8" ?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="bootstrap.php">
<php>
<ini name="error_reporting" value="-1"/>
</php>
<testsuites>
<testsuite name="all_tests">
<directory>tests</directory>
</testsuite>
<testsuite name="only_fast_tests">
<directory>tests</directory>
<exclude>tests/SlowTests</exclude>
</testsuite>
<testsuite name="only_slow_tests">
<directory>tests/SlowTests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,33 @@
<?php
namespace PhpZip\Constants;
/**
* Interface DosAttrs.
*/
interface DosAttrs
{
/** @var int DOS File Attribute Read Only */
const DOS_READ_ONLY = 0x01;
/** @var int DOS File Attribute Hidden */
const DOS_HIDDEN = 0x02;
/** @var int DOS File Attribute System */
const DOS_SYSTEM = 0x04;
/** @var int DOS File Attribute Label */
const DOS_LABEL = 0x08;
/** @var int DOS File Attribute Directory */
const DOS_DIRECTORY = 0x10;
/** @var int DOS File Attribute Archive */
const DOS_ARCHIVE = 0x20;
/** @var int DOS File Attribute Link */
const DOS_LINK = 0x40;
/** @var int DOS File Attribute Execute */
const DOS_EXE = 0x80;
}

View File

@@ -0,0 +1,105 @@
<?php
/** @noinspection PhpComposerExtensionStubsInspection */
namespace PhpZip\Constants;
/**
* Class DosCodePage.
*/
final class DosCodePage
{
const CP_LATIN_US = 'cp437';
const CP_GREEK = 'cp737';
const CP_BALT_RIM = 'cp775';
const CP_LATIN1 = 'cp850';
const CP_LATIN2 = 'cp852';
const CP_CYRILLIC = 'cp855';
const CP_TURKISH = 'cp857';
const CP_PORTUGUESE = 'cp860';
const CP_ICELANDIC = 'cp861';
const CP_HEBREW = 'cp862';
const CP_CANADA = 'cp863';
const CP_ARABIC = 'cp864';
const CP_NORDIC = 'cp865';
const CP_CYRILLIC_RUSSIAN = 'cp866';
const CP_GREEK2 = 'cp869';
const CP_THAI = 'cp874';
/** @var string[] */
private static $CP_CHARSETS = [
self::CP_LATIN_US,
self::CP_GREEK,
self::CP_BALT_RIM,
self::CP_LATIN1,
self::CP_LATIN2,
self::CP_CYRILLIC,
self::CP_TURKISH,
self::CP_PORTUGUESE,
self::CP_ICELANDIC,
self::CP_HEBREW,
self::CP_CANADA,
self::CP_ARABIC,
self::CP_NORDIC,
self::CP_CYRILLIC_RUSSIAN,
self::CP_GREEK2,
self::CP_THAI,
];
/**
* @param string $str
* @param string $sourceEncoding
*
* @return string
*/
public static function toUTF8($str, $sourceEncoding)
{
$s = iconv($sourceEncoding, 'UTF-8', $str);
if ($s === false) {
return $str;
}
return $s;
}
/**
* @param string $str
* @param string $destEncoding
*
* @return string
*/
public static function fromUTF8($str, $destEncoding)
{
$s = iconv('UTF-8', $destEncoding, $str);
if ($s === false) {
return $str;
}
return $s;
}
/**
* @return string[]
*/
public static function getCodePages()
{
return self::$CP_CHARSETS;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace PhpZip\Constants;
/**
* General purpose bit flag constants.
*/
interface GeneralPurposeBitFlag
{
/**
* General Purpose Bit Flag mask for encrypted data.
* Bit 0: If set, indicates that the file is encrypted.
*/
const ENCRYPTION = 1; // 1 << 0
/**
* Compression Flag Bit 1 for method Deflating.
*
* Bit 2 Bit 1
* 0 0 Normal compression
* 0 1 Maximum compression
* 1 0 Fast compression
* 1 1 Super Fast compression
*
* @see GeneralPurposeBitFlag::COMPRESSION_FLAG2
*/
const COMPRESSION_FLAG1 = 2; // 1 << 1
/**
* Compression Flag Bit 2 for method Deflating.
*
* Bit 2 Bit 1
* 0 0 Normal compression
* 0 1 Maximum compression
* 1 0 Fast compression
* 1 1 Super Fast compression
*
* @see GeneralPurposeBitFlag::COMPRESSION_FLAG1
*/
const COMPRESSION_FLAG2 = 4; // 1 << 2
/**
* General Purpose Bit Flag mask for data descriptor.
*
* Bit 3: If this bit is set, the fields crc-32, compressed
* size and uncompressed size are set to zero in the
* local header. The correct values are put in the data
* descriptor immediately following the compressed data.
*/
const DATA_DESCRIPTOR = 8; // 1 << 3
/**
* General Purpose Bit Flag mask for strong encryption.
*
* Bit 6: Strong encryption.
* If this bit is set, you MUST set the version needed to extract
* value to at least 50 and you MUST also set bit 0.
* If AES encryption is used, the version needed to extract value
* MUST be at least 51.
*/
const STRONG_ENCRYPTION = 64; // 1 << 6
/**
* General Purpose Bit Flag mask for UTF-8.
*
* Bit 11: Language encoding flag (EFS).
* If this bit is set, the filename and comment fields
* for this file MUST be encoded using UTF-8. (see APPENDIX D)
*/
const UTF8 = 2048; // 1 << 11
}

View File

@@ -0,0 +1,84 @@
<?php
namespace PhpZip\Constants;
/**
* Unix stat constants.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface UnixStat
{
/** @var int unix file type mask */
const UNX_IFMT = 0170000;
/** @var int unix regular file */
const UNX_IFREG = 0100000;
/** @var int unix socket (BSD, not SysV or Amiga) */
const UNX_IFSOCK = 0140000;
/** @var int unix symbolic link (not SysV, Amiga) */
const UNX_IFLNK = 0120000;
/** @var int unix block special (not Amiga) */
const UNX_IFBLK = 0060000;
/** @var int unix directory */
const UNX_IFDIR = 0040000;
/** @var int unix character special (not Amiga) */
const UNX_IFCHR = 0020000;
/** @var int unix fifo (BCC, not MSC or Amiga) */
const UNX_IFIFO = 0010000;
/** @var int unix set user id on execution */
const UNX_ISUID = 04000;
/** @var int unix set group id on execution */
const UNX_ISGID = 02000;
/** @var int unix directory permissions control */
const UNX_ISVTX = 01000;
/** @var int unix record locking enforcement flag */
const UNX_ENFMT = 02000;
/** @var int unix read, write, execute: owner */
const UNX_IRWXU = 00700;
/** @var int unix read permission: owner */
const UNX_IRUSR = 00400;
/** @var int unix write permission: owner */
const UNX_IWUSR = 00200;
/** @var int unix execute permission: owner */
const UNX_IXUSR = 00100;
/** @var int unix read, write, execute: group */
const UNX_IRWXG = 00070;
/** @var int unix read permission: group */
const UNX_IRGRP = 00040;
/** @var int unix write permission: group */
const UNX_IWGRP = 00020;
/** @var int unix execute permission: group */
const UNX_IXGRP = 00010;
/** @var int unix read, write, execute: other */
const UNX_IRWXO = 00007;
/** @var int unix read permission: other */
const UNX_IROTH = 00004;
/** @var int unix write permission: other */
const UNX_IWOTH = 00002;
/** @var int unix execute permission: other */
const UNX_IXOTH = 00001;
}

View File

@@ -0,0 +1,54 @@
<?php
namespace PhpZip\Constants;
/**
* Compression levels for Deflate and BZIP2.
*
* {@see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT} Section 4.4.4:
*
* For Methods 8 and 9 - Deflating
* -------------------------------
* Bit 2 Bit 1
* 0 0 Normal (-en) compression option was used.
* 0 1 Maximum (-exx/-ex) compression option was used.
* 1 0 Fast (-ef) compression option was used.
* 1 1 Super Fast (-es) compression option was used.
*
* Different programs encode compression level information in different ways:
*
* Deflate Compress Level pkzip zip 7z, WinRAR WinZip
* ---------------------- ---------------- ------- ---------- ------
* Super Fast compression 1 1
* Fast compression 2 1, 2
* Normal Compression 3 - 8 (5 default) 3 - 7 1 - 9
* Maximum compression 9 8, 9 9
*/
interface ZipCompressionLevel
{
/** @var int Compression level for super fast compression. */
const SUPER_FAST = 1;
/** @var int compression level for fast compression */
const FAST = 2;
/** @var int compression level for normal compression */
const NORMAL = 5;
/** @var int compression level for maximum compression */
const MAXIMUM = 9;
/**
* @var int int Minimum compression level
*
* @internal
*/
const LEVEL_MIN = self::SUPER_FAST;
/**
* @var int int Maximum compression level
*
* @internal
*/
const LEVEL_MAX = self::MAXIMUM;
}

View File

@@ -0,0 +1,102 @@
<?php
namespace PhpZip\Constants;
use PhpZip\Exception\ZipUnsupportMethodException;
/**
* Class ZipCompressionMethod.
*/
final class ZipCompressionMethod
{
/** @var int Compression method Store */
const STORED = 0;
/** @var int Compression method Deflate */
const DEFLATED = 8;
/** @var int Compression method Bzip2 */
const BZIP2 = 12;
/** @var int Compression method AES-Encryption */
const WINZIP_AES = 99;
/** @var array Compression Methods */
private static $ZIP_COMPRESSION_METHODS = [
self::STORED => 'Stored',
1 => 'Shrunk',
2 => 'Reduced compression factor 1',
3 => 'Reduced compression factor 2',
4 => 'Reduced compression factor 3',
5 => 'Reduced compression factor 4',
6 => 'Imploded',
7 => 'Reserved for Tokenizing compression algorithm',
self::DEFLATED => 'Deflated',
9 => 'Enhanced Deflating using Deflate64(tm)',
10 => 'PKWARE Data Compression Library Imploding',
11 => 'Reserved by PKWARE',
self::BZIP2 => 'BZIP2',
13 => 'Reserved by PKWARE',
14 => 'LZMA',
15 => 'Reserved by PKWARE',
16 => 'Reserved by PKWARE',
17 => 'Reserved by PKWARE',
18 => 'File is compressed using IBM TERSE (new)',
19 => 'IBM LZ77 z Architecture (PFS)',
96 => 'WinZip JPEG Compression',
97 => 'WavPack compressed data',
98 => 'PPMd version I, Rev 1',
self::WINZIP_AES => 'AES Encryption',
];
/**
* @param int $value
*
* @return string
*/
public static function getCompressionMethodName($value)
{
return isset(self::$ZIP_COMPRESSION_METHODS[$value]) ?
self::$ZIP_COMPRESSION_METHODS[$value] :
'Unknown Method';
}
/**
* @return int[]
*/
public static function getSupportMethods()
{
static $methods;
if ($methods === null) {
$methods = [
self::STORED,
self::DEFLATED,
];
if (\extension_loaded('bz2')) {
$methods[] = self::BZIP2;
}
}
return $methods;
}
/**
* @param int $compressionMethod
*
* @throws ZipUnsupportMethodException
*/
public static function checkSupport($compressionMethod)
{
$compressionMethod = (int) $compressionMethod;
if (!\in_array($compressionMethod, self::getSupportMethods(), true)) {
throw new ZipUnsupportMethodException(sprintf(
'Compression method %d (%s) is not supported.',
$compressionMethod,
self::getCompressionMethodName($compressionMethod)
));
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace PhpZip\Constants;
/**
* Zip Constants.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipConstants
{
/** @var int End Of Central Directory Record signature. */
const END_CD = 0x06054B50; // "PK\005\006"
/** @var int Zip64 End Of Central Directory Record. */
const ZIP64_END_CD = 0x06064B50; // "PK\006\006"
/** @var int Zip64 End Of Central Directory Locator. */
const ZIP64_END_CD_LOC = 0x07064B50; // "PK\006\007"
/** @var int Central File Header signature. */
const CENTRAL_FILE_HEADER = 0x02014B50; // "PK\001\002"
/** @var int Local File Header signature. */
const LOCAL_FILE_HEADER = 0x04034B50; // "PK\003\004"
/** @var int Data Descriptor signature. */
const DATA_DESCRIPTOR = 0x08074B50; // "PK\007\008"
/**
* @var int value stored in four-byte size and similar fields
* if ZIP64 extensions are used
*/
const ZIP64_MAGIC = 0xFFFFFFFF;
/**
* Local File Header signature 4
* Version Needed To Extract 2
* General Purpose Bit Flags 2
* Compression Method 2
* Last Mod File Time 2
* Last Mod File Date 2
* CRC-32 4
* Compressed Size 4
* Uncompressed Size 4.
*
* @var int Local File Header filename position
*/
const LFH_FILENAME_LENGTH_POS = 26;
/**
* The minimum length of the Local File Header record.
*
* local file header signature 4
* version needed to extract 2
* general purpose bit flag 2
* compression method 2
* last mod file time 2
* last mod file date 2
* crc-32 4
* compressed size 4
* uncompressed size 4
* file name length 2
* extra field length 2
*/
const LFH_FILENAME_POS = 30;
/** @var int the length of the Zip64 End Of Central Directory Locator */
const ZIP64_END_CD_LOC_LEN = 20;
/** @var int the minimum length of the End Of Central Directory Record */
const END_CD_MIN_LEN = 22;
/**
* The minimum length of the Zip64 End Of Central Directory Record.
*
* zip64 end of central dir
* signature 4
* size of zip64 end of central
* directory record 8
* version made by 2
* version needed to extract 2
* number of this disk 4
* number of the disk with the
* start of the central directory 4
* total number of entries in the
* central directory on this disk 8
* total number of entries in
* the central directory 8
* size of the central directory 8
* offset of start of central
* directory with respect to
* the starting disk number 8
*
* @var int ZIP64 End Of Central Directory length
*/
const ZIP64_END_OF_CD_LEN = 56;
}

View File

@@ -0,0 +1,93 @@
<?php
namespace PhpZip\Constants;
use PhpZip\Exception\InvalidArgumentException;
/**
* Class ZipEncryptionMethod.
*/
final class ZipEncryptionMethod
{
const NONE = -1;
/** @var int Traditional PKWARE encryption. */
const PKWARE = 0;
/** @var int WinZip AES-256 */
const WINZIP_AES_256 = 1;
/** @var int WinZip AES-128 */
const WINZIP_AES_128 = 2;
/** @var int WinZip AES-192 */
const WINZIP_AES_192 = 3;
/** @var array<int, string> */
private static $ENCRYPTION_METHODS = [
self::NONE => 'no encryption',
self::PKWARE => 'Traditional PKWARE encryption',
self::WINZIP_AES_128 => 'WinZip AES-128',
self::WINZIP_AES_192 => 'WinZip AES-192',
self::WINZIP_AES_256 => 'WinZip AES-256',
];
/**
* @param int $value
*
* @return string
*/
public static function getEncryptionMethodName($value)
{
$value = (int) $value;
return isset(self::$ENCRYPTION_METHODS[$value]) ?
self::$ENCRYPTION_METHODS[$value] :
'Unknown Encryption Method';
}
/**
* @param int $encryptionMethod
*
* @return bool
*/
public static function hasEncryptionMethod($encryptionMethod)
{
return isset(self::$ENCRYPTION_METHODS[$encryptionMethod]);
}
/**
* @param int $encryptionMethod
*
* @return bool
*/
public static function isWinZipAesMethod($encryptionMethod)
{
return \in_array(
(int) $encryptionMethod,
[
self::WINZIP_AES_256,
self::WINZIP_AES_192,
self::WINZIP_AES_128,
],
true
);
}
/**
* @param int $encryptionMethod
*
* @throws InvalidArgumentException
*/
public static function checkSupport($encryptionMethod)
{
$encryptionMethod = (int) $encryptionMethod;
if (!self::hasEncryptionMethod($encryptionMethod)) {
throw new InvalidArgumentException(sprintf(
'Encryption method %d is not supported.',
$encryptionMethod
));
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace PhpZip\Constants;
/**
* Interface ZipOptions.
*/
interface ZipOptions
{
/**
* Boolean option for store just file names (skip directory names).
*
* @var string
*/
const STORE_ONLY_FILES = 'only_files';
/** @var string */
const COMPRESSION_METHOD = 'compression_method';
/** @var string */
const MODIFIED_TIME = 'mtime';
/**
* @var string
*
* @see DosCodePage::getCodePages()
*/
const CHARSET = 'charset';
}

View File

@@ -0,0 +1,53 @@
<?php
namespace PhpZip\Constants;
/**
* Class ZipPlatform.
*/
final class ZipPlatform
{
/** @var int MS-DOS OS */
const OS_DOS = 0;
/** @var int Unix OS */
const OS_UNIX = 3;
/** MacOS platform */
const OS_MAC_OSX = 19;
/** @var array Zip Platforms */
private static $platforms = [
self::OS_DOS => 'MS-DOS',
1 => 'Amiga',
2 => 'OpenVMS',
self::OS_UNIX => 'Unix',
4 => 'VM/CMS',
5 => 'Atari ST',
6 => 'HPFS (OS/2, NT 3.x)',
7 => 'Macintosh',
8 => 'Z-System',
9 => 'CP/M',
10 => 'Windows NTFS or TOPS-20',
11 => 'MVS or NTFS',
12 => 'VSE or SMS/QDOS',
13 => 'Acorn RISC OS',
14 => 'VFAT',
15 => 'alternate MVS',
16 => 'BeOS',
17 => 'Tandem',
18 => 'OS/400',
self::OS_MAC_OSX => 'OS/X (Darwin)',
30 => 'AtheOS/Syllable',
];
/**
* @param int $platform
*
* @return string
*/
public static function getPlatformName($platform)
{
return isset(self::$platforms[$platform]) ? self::$platforms[$platform] : 'Unknown';
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace PhpZip\Constants;
/**
* Version needed to extract or software version.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT Section 4.4.3
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipVersion
{
/** @var int 1.0 - Default value */
const v10_DEFAULT_MIN = 10;
/** @var int 1.1 - File is a volume label */
const v11_FILE_VOLUME_LABEL = 11;
/**
* 2.0 - File is a folder (directory)
* 2.0 - File is compressed using Deflate compression
* 2.0 - File is encrypted using traditional PKWARE encryption.
*
* @var int
*/
const v20_DEFLATED_FOLDER_ZIPCRYPTO = 20;
/** @var int 2.1 - File is compressed using Deflate64(tm) */
const v21_DEFLATED64 = 21;
/** @var int 2.5 - File is compressed using PKWARE DCL Implode */
const v25_IMPLODED = 25;
/** @var int 2.7 - File is a patch data set */
const v27_PATCH_DATA = 27;
/** @var int 4.5 - File uses ZIP64 format extensions */
const v45_ZIP64_EXT = 45;
/** @var int 4.6 - File is compressed using BZIP2 compression */
const v46_BZIP2 = 46;
/**
* 5.0 - File is encrypted using DES
* 5.0 - File is encrypted using 3DES
* 5.0 - File is encrypted using original RC2 encryption
* 5.0 - File is encrypted using RC4 encryption.
*
* @var int
*/
const v50_ENCR_DES_3DES_RC2_ORIG_RC4 = 50;
/**
* 5.1 - File is encrypted using AES encryption
* 5.1 - File is encrypted using corrected RC2 encryption**.
*
* @var int
*/
const v51_ENCR_AES_RC2_CORRECT = 51;
/** @var int 5.2 - File is encrypted using corrected RC2-64 encryption** */
const v52_ENCR_RC2_64_CORRECT = 52;
/** @var int 6.1 - File is encrypted using non-OAEP key wrapping*** */
const v61_ENCR_NON_OAE_KEY_WRAP = 61;
/** @var int 6.2 - Central directory encryption */
const v62_ENCR_CENTRAL_DIR = 62;
/**
* 6.3 - File is compressed using LZMA
* 6.3 - File is compressed using PPMd+
* 6.3 - File is encrypted using Blowfish
* 6.3 - File is encrypted using Twofish.
*
* @var int
*/
const v63_LZMA_PPMD_BLOWFISH_TWOFISH = 63;
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -6,7 +7,7 @@ namespace PhpZip\Exception;
* Central File Header and the Data Descriptor or between the declared value
* and the computed value from the decompressed data.
*
* The exception's detail message is the name of the ZIP entry.
* The exception detail message is the name of the ZIP entry.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
@@ -31,18 +32,19 @@ class Crc32Exception extends ZipException
* Crc32Exception constructor.
*
* @param string $name
* @param int $expected
* @param int $actual
* @param int $expected
* @param int $actual
*/
public function __construct($name, $expected, $actual)
{
parent::__construct($name
. " (expected CRC32 value 0x"
. dechex($expected)
. ", but is actually 0x"
. dechex($actual)
. ")");
assert($expected != $actual);
parent::__construct(
sprintf(
'%s (expected CRC32 value 0x%x, but is actually 0x%x)',
$name,
$expected,
$actual
)
);
$this->expectedCrc = $expected;
$this->actualCrc = $actual;
}
@@ -66,5 +68,4 @@ class Crc32Exception extends ZipException
{
return $this->actualCrc;
}
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -8,7 +9,6 @@ namespace PhpZip\Exception;
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class IllegalArgumentException extends ZipException
class InvalidArgumentException extends RuntimeException
{
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace PhpZip\Exception;
/**
* Runtime exception.
* Exception thrown if an error which can only be found on runtime occurs.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class RuntimeException extends \RuntimeException
{
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -9,5 +10,4 @@ namespace PhpZip\Exception;
*/
class ZipAuthenticationException extends ZipCryptoException
{
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -10,5 +11,4 @@ namespace PhpZip\Exception;
*/
class ZipCryptoException extends ZipException
{
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace PhpZip\Exception;
use PhpZip\Model\ZipEntry;
/**
* Thrown if entry not found.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipEntryNotFoundException extends ZipException
{
/** @var string */
private $entryName;
/**
* ZipEntryNotFoundException constructor.
*
* @param ZipEntry|string $entryName
*/
public function __construct($entryName)
{
$entryName = $entryName instanceof ZipEntry ? $entryName->getName() : $entryName;
parent::__construct(sprintf(
'Zip Entry "%s" was not found in the archive.',
$entryName
));
$this->entryName = $entryName;
}
/**
* @return string
*/
public function getEntryName()
{
return $this->entryName;
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace PhpZip\Exception;
/**
@@ -6,9 +7,9 @@ namespace PhpZip\Exception;
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*
* @see \Exception
*/
class ZipException extends \Exception
{
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace PhpZip\Exception;
/**
* Class ZipUnsupportMethodException.
*/
class ZipUnsupportMethodException extends ZipException
{
}

View File

@@ -0,0 +1,419 @@
<?php
namespace PhpZip\IO\Filter\Cipher\Pkware;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Util\PackUtil;
/**
* Traditional PKWARE Encryption Engine.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class PKCryptContext
{
/** Encryption header size */
const STD_DEC_HDR_SIZE = 12;
/**
* Crc table.
*
* @var int[]|array
*/
private static $CRC_TABLE = [
0x00000000,
0x77073096,
0xee0e612c,
0x990951ba,
0x076dc419,
0x706af48f,
0xe963a535,
0x9e6495a3,
0x0edb8832,
0x79dcb8a4,
0xe0d5e91e,
0x97d2d988,
0x09b64c2b,
0x7eb17cbd,
0xe7b82d07,
0x90bf1d91,
0x1db71064,
0x6ab020f2,
0xf3b97148,
0x84be41de,
0x1adad47d,
0x6ddde4eb,
0xf4d4b551,
0x83d385c7,
0x136c9856,
0x646ba8c0,
0xfd62f97a,
0x8a65c9ec,
0x14015c4f,
0x63066cd9,
0xfa0f3d63,
0x8d080df5,
0x3b6e20c8,
0x4c69105e,
0xd56041e4,
0xa2677172,
0x3c03e4d1,
0x4b04d447,
0xd20d85fd,
0xa50ab56b,
0x35b5a8fa,
0x42b2986c,
0xdbbbc9d6,
0xacbcf940,
0x32d86ce3,
0x45df5c75,
0xdcd60dcf,
0xabd13d59,
0x26d930ac,
0x51de003a,
0xc8d75180,
0xbfd06116,
0x21b4f4b5,
0x56b3c423,
0xcfba9599,
0xb8bda50f,
0x2802b89e,
0x5f058808,
0xc60cd9b2,
0xb10be924,
0x2f6f7c87,
0x58684c11,
0xc1611dab,
0xb6662d3d,
0x76dc4190,
0x01db7106,
0x98d220bc,
0xefd5102a,
0x71b18589,
0x06b6b51f,
0x9fbfe4a5,
0xe8b8d433,
0x7807c9a2,
0x0f00f934,
0x9609a88e,
0xe10e9818,
0x7f6a0dbb,
0x086d3d2d,
0x91646c97,
0xe6635c01,
0x6b6b51f4,
0x1c6c6162,
0x856530d8,
0xf262004e,
0x6c0695ed,
0x1b01a57b,
0x8208f4c1,
0xf50fc457,
0x65b0d9c6,
0x12b7e950,
0x8bbeb8ea,
0xfcb9887c,
0x62dd1ddf,
0x15da2d49,
0x8cd37cf3,
0xfbd44c65,
0x4db26158,
0x3ab551ce,
0xa3bc0074,
0xd4bb30e2,
0x4adfa541,
0x3dd895d7,
0xa4d1c46d,
0xd3d6f4fb,
0x4369e96a,
0x346ed9fc,
0xad678846,
0xda60b8d0,
0x44042d73,
0x33031de5,
0xaa0a4c5f,
0xdd0d7cc9,
0x5005713c,
0x270241aa,
0xbe0b1010,
0xc90c2086,
0x5768b525,
0x206f85b3,
0xb966d409,
0xce61e49f,
0x5edef90e,
0x29d9c998,
0xb0d09822,
0xc7d7a8b4,
0x59b33d17,
0x2eb40d81,
0xb7bd5c3b,
0xc0ba6cad,
0xedb88320,
0x9abfb3b6,
0x03b6e20c,
0x74b1d29a,
0xead54739,
0x9dd277af,
0x04db2615,
0x73dc1683,
0xe3630b12,
0x94643b84,
0x0d6d6a3e,
0x7a6a5aa8,
0xe40ecf0b,
0x9309ff9d,
0x0a00ae27,
0x7d079eb1,
0xf00f9344,
0x8708a3d2,
0x1e01f268,
0x6906c2fe,
0xf762575d,
0x806567cb,
0x196c3671,
0x6e6b06e7,
0xfed41b76,
0x89d32be0,
0x10da7a5a,
0x67dd4acc,
0xf9b9df6f,
0x8ebeeff9,
0x17b7be43,
0x60b08ed5,
0xd6d6a3e8,
0xa1d1937e,
0x38d8c2c4,
0x4fdff252,
0xd1bb67f1,
0xa6bc5767,
0x3fb506dd,
0x48b2364b,
0xd80d2bda,
0xaf0a1b4c,
0x36034af6,
0x41047a60,
0xdf60efc3,
0xa867df55,
0x316e8eef,
0x4669be79,
0xcb61b38c,
0xbc66831a,
0x256fd2a0,
0x5268e236,
0xcc0c7795,
0xbb0b4703,
0x220216b9,
0x5505262f,
0xc5ba3bbe,
0xb2bd0b28,
0x2bb45a92,
0x5cb36a04,
0xc2d7ffa7,
0xb5d0cf31,
0x2cd99e8b,
0x5bdeae1d,
0x9b64c2b0,
0xec63f226,
0x756aa39c,
0x026d930a,
0x9c0906a9,
0xeb0e363f,
0x72076785,
0x05005713,
0x95bf4a82,
0xe2b87a14,
0x7bb12bae,
0x0cb61b38,
0x92d28e9b,
0xe5d5be0d,
0x7cdcefb7,
0x0bdbdf21,
0x86d3d2d4,
0xf1d4e242,
0x68ddb3f8,
0x1fda836e,
0x81be16cd,
0xf6b9265b,
0x6fb077e1,
0x18b74777,
0x88085ae6,
0xff0f6a70,
0x66063bca,
0x11010b5c,
0x8f659eff,
0xf862ae69,
0x616bffd3,
0x166ccf45,
0xa00ae278,
0xd70dd2ee,
0x4e048354,
0x3903b3c2,
0xa7672661,
0xd06016f7,
0x4969474d,
0x3e6e77db,
0xaed16a4a,
0xd9d65adc,
0x40df0b66,
0x37d83bf0,
0xa9bcae53,
0xdebb9ec5,
0x47b2cf7f,
0x30b5ffe9,
0xbdbdf21c,
0xcabac28a,
0x53b39330,
0x24b4a3a6,
0xbad03605,
0xcdd70693,
0x54de5729,
0x23d967bf,
0xb3667a2e,
0xc4614ab8,
0x5d681b02,
0x2a6f2b94,
0xb40bbe37,
0xc30c8ea1,
0x5a05df1b,
0x2d02ef8d,
];
/**
* Encryption keys.
*
* @var array
*/
private $keys;
/**
* PKCryptContext constructor.
*
* @param string $password
*/
public function __construct($password)
{
if (\PHP_INT_SIZE === 4) {
throw new RuntimeException('Traditional PKWARE Encryption is not supported in 32-bit PHP.');
}
$this->keys = [
305419896,
591751049,
878082192,
];
foreach (unpack('C*', $password) as $b) {
$this->updateKeys($b);
}
}
/**
* @param string $header
* @param int $checkByte
*
* @throws ZipAuthenticationException
*/
public function checkHeader($header, $checkByte)
{
$byte = 0;
foreach (unpack('C*', $header) as $byte) {
$byte = ($byte ^ $this->decryptByte()) & 0xff;
$this->updateKeys($byte);
}
if ($byte !== $checkByte) {
throw new ZipAuthenticationException(sprintf('Invalid password'));
}
}
/**
* @param string $content
*
* @return string
*/
public function decryptString($content)
{
$decryptContent = '';
foreach (unpack('C*', $content) as $byte) {
$byte = ($byte ^ $this->decryptByte()) & 0xff;
$this->updateKeys($byte);
$decryptContent .= \chr($byte);
}
return $decryptContent;
}
/**
* Decrypt byte.
*
* @return int
*/
private function decryptByte()
{
$temp = $this->keys[2] | 2;
return (($temp * ($temp ^ 1)) >> 8) & 0xffffff;
}
/**
* Update keys.
*
* @param int $charAt
*/
private function updateKeys($charAt)
{
$this->keys[0] = $this->crc32($this->keys[0], $charAt);
$this->keys[1] += ($this->keys[0] & 0xff);
$this->keys[1] = PackUtil::toSignedInt32($this->keys[1] * 134775813 + 1);
$this->keys[2] = PackUtil::toSignedInt32($this->crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff));
}
/**
* Update crc.
*
* @param int $oldCrc
* @param int $charAt
*
* @return int
*/
private function crc32($oldCrc, $charAt)
{
return (($oldCrc >> 8) & 0xffffff) ^ self::$CRC_TABLE[($oldCrc ^ $charAt) & 0xff];
}
/**
* @param string $content
*
* @return string
*/
public function encryptString($content)
{
$encryptContent = '';
foreach (unpack('C*', $content) as $val) {
$encryptContent .= pack('c', $this->encryptByte($val));
}
return $encryptContent;
}
/**
* @param int $byte
*
* @return int
*/
private function encryptByte($byte)
{
$tempVal = $byte ^ $this->decryptByte() & 0xff;
$this->updateKeys($byte);
return $tempVal;
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace PhpZip\IO\Filter\Cipher\Pkware;
use PhpZip\Exception\ZipException;
use PhpZip\Model\ZipEntry;
/**
* Decryption PKWARE Traditional Encryption.
*/
class PKDecryptionStreamFilter extends \php_user_filter
{
const FILTER_NAME = 'phpzip.decryption.pkware';
/** @var int */
private $checkByte = 0;
/** @var int */
private $readLength = 0;
/** @var int */
private $size = 0;
/** @var bool */
private $readHeader = false;
/** @var PKCryptContext */
private $context;
/**
* @return bool
*/
public static function register()
{
return stream_filter_register(self::FILTER_NAME, __CLASS__);
}
/**
* @see https://php.net/manual/en/php-user-filter.oncreate.php
*
* @return bool
*/
public function onCreate()
{
if (!isset($this->params['entry'])) {
return false;
}
if (!($this->params['entry'] instanceof ZipEntry)) {
throw new \RuntimeException('ZipEntry expected');
}
/** @var ZipEntry $entry */
$entry = $this->params['entry'];
$password = $entry->getPassword();
if ($password === null) {
return false;
}
$this->size = $entry->getCompressedSize();
// init context
$this->context = new PKCryptContext($password);
// init check byte
if ($entry->isDataDescriptorEnabled()) {
$this->checkByte = ($entry->getDosTime() >> 8) & 0xff;
} else {
$this->checkByte = ($entry->getCrc() >> 24) & 0xff;
}
$this->readLength = 0;
$this->readHeader = false;
return true;
}
/**
* Decryption filter.
*
* @param resource $in
* @param resource $out
* @param int $consumed
* @param bool $closing
*
* @throws ZipException
*
* @return int
*
* @todo USE FFI in php 7.4
*/
public function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$buffer = $bucket->data;
$this->readLength += $bucket->datalen;
if ($this->readLength > $this->size) {
$buffer = substr($buffer, 0, $this->size - $this->readLength);
}
if (!$this->readHeader) {
$header = substr($buffer, 0, PKCryptContext::STD_DEC_HDR_SIZE);
$this->context->checkHeader($header, $this->checkByte);
$buffer = substr($buffer, PKCryptContext::STD_DEC_HDR_SIZE);
$this->readHeader = true;
}
$bucket->data = $this->context->decryptString($buffer);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return \PSFS_PASS_ON;
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace PhpZip\IO\Filter\Cipher\Pkware;
use PhpZip\Exception\RuntimeException;
use PhpZip\Model\ZipEntry;
/**
* Encryption PKWARE Traditional Encryption.
*/
class PKEncryptionStreamFilter extends \php_user_filter
{
const FILTER_NAME = 'phpzip.encryption.pkware';
/** @var int */
private $size;
/** @var string */
private $headerBytes;
/** @var int */
private $writeLength;
/** @var bool */
private $writeHeader;
/** @var PKCryptContext */
private $context;
/**
* @return bool
*/
public static function register()
{
return stream_filter_register(self::FILTER_NAME, __CLASS__);
}
/**
* @see https://php.net/manual/en/php-user-filter.oncreate.php
*
* @return bool
*/
public function onCreate()
{
if (\PHP_INT_SIZE === 4) {
throw new RuntimeException('Traditional PKWARE Encryption is not supported in 32-bit PHP.');
}
if (!isset($this->params['entry'], $this->params['size'])) {
return false;
}
if (!($this->params['entry'] instanceof ZipEntry)) {
throw new \RuntimeException('ZipEntry expected');
}
/** @var ZipEntry $entry */
$entry = $this->params['entry'];
$password = $entry->getPassword();
if ($password === null) {
return false;
}
$this->size = (int) $this->params['size'];
// init keys
$this->context = new PKCryptContext($password);
$crc = $entry->isDataDescriptorRequired() || $entry->getCrc() === ZipEntry::UNKNOWN ?
($entry->getDosTime() & 0x0000ffff) << 16 :
$entry->getCrc();
try {
$headerBytes = random_bytes(PKCryptContext::STD_DEC_HDR_SIZE);
} catch (\Exception $e) {
throw new \RuntimeException('Oops, our server is bust and cannot generate any random data.', 1, $e);
}
$headerBytes[PKCryptContext::STD_DEC_HDR_SIZE - 1] = pack('c', ($crc >> 24) & 0xff);
$headerBytes[PKCryptContext::STD_DEC_HDR_SIZE - 2] = pack('c', ($crc >> 16) & 0xff);
$this->headerBytes = $headerBytes;
$this->writeLength = 0;
$this->writeHeader = false;
return true;
}
/**
* Encryption filter.
*
* @param resource $in
* @param resource $out
* @param int $consumed
* @param bool $closing
*
* @return int
*
* @todo USE FFI in php 7.4
*/
public function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$buffer = $bucket->data;
$this->writeLength += $bucket->datalen;
if ($this->writeLength > $this->size) {
$buffer = substr($buffer, 0, $this->size - $this->writeLength);
}
$data = '';
if (!$this->writeHeader) {
$data .= $this->context->encryptString($this->headerBytes);
$this->writeHeader = true;
}
$data .= $this->context->encryptString($buffer);
$bucket->data = $data;
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return \PSFS_PASS_ON;
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace PhpZip\IO\Filter\Cipher\WinZipAes;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Util\CryptoUtil;
/**
* WinZip Aes Encryption.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT APPENDIX E
* @see https://www.winzip.com/win/en/aes_info.html
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*
* @internal
*/
class WinZipAesContext
{
/** @var int AES Block size */
const BLOCK_SIZE = self::IV_SIZE;
/** @var int Footer size */
const FOOTER_SIZE = 10;
/** @var int The iteration count for the derived keys of the cipher, KLAC and MAC. */
const ITERATION_COUNT = 1000;
/** @var int Password verifier size */
const PASSWORD_VERIFIER_SIZE = 2;
/** @var int IV size */
const IV_SIZE = 16;
/** @var string */
private $iv;
/** @var string */
private $key;
/** @var \HashContext|resource */
private $hmacContext;
/** @var string */
private $passwordVerifier;
/**
* WinZipAesContext constructor.
*
* @param int $encryptionStrengthBits
* @param string $password
* @param string $salt
*/
public function __construct($encryptionStrengthBits, $password, $salt)
{
$encryptionStrengthBits = (int) $encryptionStrengthBits;
if ($password === '') {
throw new RuntimeException('$password is empty');
}
if (empty($salt)) {
throw new RuntimeException('$salt is empty');
}
// WinZip 99-character limit https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
$password = substr($password, 0, 99);
$this->iv = str_repeat("\0", self::IV_SIZE);
$keyStrengthBytes = (int) ($encryptionStrengthBits / 8);
$hashLength = $keyStrengthBytes * 2 + self::PASSWORD_VERIFIER_SIZE * 8;
$hash = hash_pbkdf2(
'sha1',
$password,
$salt,
self::ITERATION_COUNT,
$hashLength,
true
);
$this->key = substr($hash, 0, $keyStrengthBytes);
$sha1Mac = substr($hash, $keyStrengthBytes, $keyStrengthBytes);
$this->hmacContext = hash_init('sha1', \HASH_HMAC, $sha1Mac);
$this->passwordVerifier = substr($hash, 2 * $keyStrengthBytes, self::PASSWORD_VERIFIER_SIZE);
}
/**
* @return string
*/
public function getPasswordVerifier()
{
return $this->passwordVerifier;
}
public function updateIv()
{
for ($ivCharIndex = 0; $ivCharIndex < self::IV_SIZE; $ivCharIndex++) {
$ivByte = \ord($this->iv[$ivCharIndex]);
if (++$ivByte === 256) {
// overflow, set this one to 0, increment next
$this->iv[$ivCharIndex] = "\0";
} else {
// no overflow, just write incremented number back and abort
$this->iv[$ivCharIndex] = \chr($ivByte);
break;
}
}
}
/**
* @param string $data
*
* @return string
*/
public function decryption($data)
{
hash_update($this->hmacContext, $data);
return CryptoUtil::decryptAesCtr($data, $this->key, $this->iv);
}
/**
* @param string $data
*
* @return string
*/
public function encrypt($data)
{
$encryptionData = CryptoUtil::encryptAesCtr($data, $this->key, $this->iv);
hash_update($this->hmacContext, $encryptionData);
return $encryptionData;
}
/**
* @param string $authCode
*
* @throws ZipAuthenticationException
*/
public function checkAuthCode($authCode)
{
$hmac = $this->getHmac();
// check authenticationCode
if (strcmp($hmac, $authCode) !== 0) {
throw new ZipAuthenticationException('Authenticated WinZip AES entry content has been tampered with.');
}
}
/**
* @return string
*/
public function getHmac()
{
return substr(
hash_final($this->hmacContext, true),
0,
self::FOOTER_SIZE
);
}
}

View File

@@ -0,0 +1,187 @@
<?php
namespace PhpZip\IO\Filter\Cipher\WinZipAes;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
use PhpZip\Model\ZipEntry;
/**
* Decrypt WinZip AES stream.
*/
class WinZipAesDecryptionStreamFilter extends \php_user_filter
{
const FILTER_NAME = 'phpzip.decryption.winzipaes';
/** @var string */
private $buffer;
/** @var string */
private $authenticationCode;
/** @var int */
private $encBlockPosition = 0;
/** @var int */
private $encBlockLength = 0;
/** @var int */
private $readLength = 0;
/** @var ZipEntry */
private $entry;
/** @var WinZipAesContext|null */
private $context;
/**
* @return bool
*/
public static function register()
{
return stream_filter_register(self::FILTER_NAME, __CLASS__);
}
/**
* @return bool
*
* @noinspection DuplicatedCode
*/
public function onCreate()
{
if (!isset($this->params['entry'])) {
return false;
}
if (!($this->params['entry'] instanceof ZipEntry)) {
throw new \RuntimeException('ZipEntry expected');
}
$this->entry = $this->params['entry'];
if (
$this->entry->getPassword() === null ||
!$this->entry->isEncrypted() ||
!$this->entry->hasExtraField(WinZipAesExtraField::HEADER_ID)
) {
return false;
}
$this->buffer = '';
return true;
}
/**
* @param resource $in
* @param resource $out
* @param int $consumed
* @param bool $closing
*
* @throws ZipAuthenticationException
*
* @return int
*/
public function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$this->buffer .= $bucket->data;
$this->readLength += $bucket->datalen;
if ($this->readLength > $this->entry->getCompressedSize()) {
$this->buffer = substr($this->buffer, 0, $this->entry->getCompressedSize() - $this->readLength);
}
// read header
if ($this->context === null) {
/**
* @var WinZipAesExtraField|null $winZipExtra
*/
$winZipExtra = $this->entry->getExtraField(WinZipAesExtraField::HEADER_ID);
if ($winZipExtra === null) {
throw new RuntimeException('$winZipExtra is null');
}
$saltSize = $winZipExtra->getSaltSize();
$headerSize = $saltSize + WinZipAesContext::PASSWORD_VERIFIER_SIZE;
if (\strlen($this->buffer) < $headerSize) {
return \PSFS_FEED_ME;
}
$salt = substr($this->buffer, 0, $saltSize);
$passwordVerifier = substr($this->buffer, $saltSize, WinZipAesContext::PASSWORD_VERIFIER_SIZE);
$password = $this->entry->getPassword();
if ($password === null) {
throw new RuntimeException('$password is null');
}
$this->context = new WinZipAesContext($winZipExtra->getEncryptionStrength(), $password, $salt);
unset($password);
// Verify password.
if ($passwordVerifier !== $this->context->getPasswordVerifier()) {
throw new ZipAuthenticationException('Invalid password');
}
$this->encBlockPosition = 0;
$this->encBlockLength = $this->entry->getCompressedSize() - $headerSize - WinZipAesContext::FOOTER_SIZE;
$this->buffer = substr($this->buffer, $headerSize);
}
// encrypt data
$plainText = '';
$offset = 0;
$len = \strlen($this->buffer);
$remaining = $this->encBlockLength - $this->encBlockPosition;
if ($remaining >= WinZipAesContext::BLOCK_SIZE && $len < WinZipAesContext::BLOCK_SIZE) {
return \PSFS_FEED_ME;
}
$limit = min($len, $remaining);
if ($remaining > $limit && ($limit % WinZipAesContext::BLOCK_SIZE) !== 0) {
$limit -= ($limit % WinZipAesContext::BLOCK_SIZE);
}
while ($offset < $limit) {
$this->context->updateIv();
$length = min(WinZipAesContext::BLOCK_SIZE, $limit - $offset);
$data = substr($this->buffer, 0, $length);
$plainText .= $this->context->decryption($data);
$offset += $length;
$this->buffer = substr($this->buffer, $length);
}
$this->encBlockPosition += $offset;
if (
$this->encBlockPosition === $this->encBlockLength &&
\strlen($this->buffer) === WinZipAesContext::FOOTER_SIZE
) {
$this->authenticationCode = $this->buffer;
$this->buffer = '';
}
$bucket->data = $plainText;
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return \PSFS_PASS_ON;
}
/**
* @see http://php.net/manual/en/php-user-filter.onclose.php
*
* @throws ZipAuthenticationException
*/
public function onClose()
{
$this->buffer = '';
if ($this->context !== null) {
$this->context->checkAuthCode($this->authenticationCode);
}
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace PhpZip\IO\Filter\Cipher\WinZipAes;
use PhpZip\Exception\RuntimeException;
use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
use PhpZip\Model\ZipEntry;
/**
* Encrypt WinZip AES stream.
*/
class WinZipAesEncryptionStreamFilter extends \php_user_filter
{
const FILTER_NAME = 'phpzip.encryption.winzipaes';
/** @var string */
private $buffer;
/** @var int */
private $remaining = 0;
/** @var ZipEntry */
private $entry;
/** @var int */
private $size;
/** @var WinZipAesContext|null */
private $context;
/**
* @return bool
*/
public static function register()
{
return stream_filter_register(self::FILTER_NAME, __CLASS__);
}
/**
* @return bool
*
* @noinspection DuplicatedCode
*/
public function onCreate()
{
if (!isset($this->params['entry'])) {
return false;
}
if (!($this->params['entry'] instanceof ZipEntry)) {
throw new \RuntimeException('ZipEntry expected');
}
$this->entry = $this->params['entry'];
if (
$this->entry->getPassword() === null ||
!$this->entry->isEncrypted() ||
!$this->entry->hasExtraField(WinZipAesExtraField::HEADER_ID)
) {
return false;
}
$this->size = (int) $this->params['size'];
$this->context = null;
$this->buffer = '';
return true;
}
/**
* @param resource $in
* @param resource $out
* @param int $consumed
* @param bool $closing
*
* @return int
*/
public function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$this->buffer .= $bucket->data;
$this->remaining += $bucket->datalen;
if ($this->remaining > $this->size) {
$this->buffer = substr($this->buffer, 0, $this->size - $this->remaining);
$this->remaining = $this->size;
}
$encryptionText = '';
// write header
if ($this->context === null) {
/**
* @var WinZipAesExtraField|null $winZipExtra
*/
$winZipExtra = $this->entry->getExtraField(WinZipAesExtraField::HEADER_ID);
if ($winZipExtra === null) {
throw new RuntimeException('$winZipExtra is null');
}
$saltSize = $winZipExtra->getSaltSize();
try {
$salt = random_bytes($saltSize);
} catch (\Exception $e) {
throw new \RuntimeException('Oops, our server is bust and cannot generate any random data.', 1, $e);
}
$password = $this->entry->getPassword();
if ($password === null) {
throw new RuntimeException('$password is null');
}
$this->context = new WinZipAesContext(
$winZipExtra->getEncryptionStrength(),
$password,
$salt
);
$encryptionText .= $salt . $this->context->getPasswordVerifier();
}
// encrypt data
$offset = 0;
$len = \strlen($this->buffer);
$remaining = $this->remaining - $this->size;
if ($remaining >= WinZipAesContext::BLOCK_SIZE && $len < WinZipAesContext::BLOCK_SIZE) {
return \PSFS_FEED_ME;
}
$limit = max($len, $remaining);
if ($remaining > $limit && ($limit % WinZipAesContext::BLOCK_SIZE) !== 0) {
$limit -= ($limit % WinZipAesContext::BLOCK_SIZE);
}
while ($offset < $limit) {
$this->context->updateIv();
$length = min(WinZipAesContext::BLOCK_SIZE, $limit - $offset);
$encryptionText .= $this->context->encrypt(
substr($this->buffer, 0, $length)
);
$offset += $length;
$this->buffer = substr($this->buffer, $length);
}
if ($remaining === 0) {
$encryptionText .= $this->context->getHmac();
}
$bucket->data = $encryptionText;
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return \PSFS_PASS_ON;
}
}

View File

@@ -0,0 +1,338 @@
<?php
namespace PhpZip\IO\Stream;
use Psr\Http\Message\StreamInterface;
/**
* Implement PSR Message Stream.
*/
class ResponseStream implements StreamInterface
{
/** @var array */
private static $readWriteHash = [
'read' => [
'r' => true,
'w+' => true,
'r+' => true,
'x+' => true,
'c+' => true,
'rb' => true,
'w+b' => true,
'r+b' => true,
'x+b' => true,
'c+b' => true,
'rt' => true,
'w+t' => true,
'r+t' => true,
'x+t' => true,
'c+t' => true,
'a+' => true,
],
'write' => [
'w' => true,
'w+' => true,
'rw' => true,
'r+' => true,
'x+' => true,
'c+' => true,
'wb' => true,
'w+b' => true,
'r+b' => true,
'x+b' => true,
'c+b' => true,
'w+t' => true,
'r+t' => true,
'x+t' => true,
'c+t' => true,
'a' => true,
'a+' => true,
],
];
/** @var resource */
private $stream;
/** @var int|null */
private $size;
/** @var bool */
private $seekable;
/** @var bool */
private $readable;
/** @var bool */
private $writable;
/** @var string|null */
private $uri;
/**
* @param resource $stream stream resource to wrap
*
* @throws \InvalidArgumentException if the stream is not a stream resource
*/
public function __construct($stream)
{
if (!\is_resource($stream)) {
throw new \InvalidArgumentException('Stream must be a resource');
}
$this->stream = $stream;
$meta = stream_get_meta_data($this->stream);
$this->seekable = $meta['seekable'];
$this->readable = isset(self::$readWriteHash['read'][$meta['mode']]);
$this->writable = isset(self::$readWriteHash['write'][$meta['mode']]);
$this->uri = $this->getMetadata('uri');
}
/**
* Get stream metadata as an associative array or retrieve a specific key.
*
* The keys returned are identical to the keys returned from PHP's
* stream_get_meta_data() function.
*
* @see http://php.net/manual/en/function.stream-get-meta-data.php
*
* @param string $key specific metadata to retrieve
*
* @return array|mixed|null Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata($key = null)
{
if (!$this->stream) {
return $key ? null : [];
}
$meta = stream_get_meta_data($this->stream);
return isset($meta[$key]) ? $meta[$key] : null;
}
/**
* Reads all data from the stream into a string, from the beginning to end.
*
* This method MUST attempt to seek to the beginning of the stream before
* reading data and read the stream until the end is reached.
*
* Warning: This could attempt to load a large amount of data into memory.
*
* This method MUST NOT raise an exception in order to conform with PHP's
* string casting operations.
*
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
*
* @return string
*/
public function __toString()
{
if (!$this->stream) {
return '';
}
$this->rewind();
return (string) stream_get_contents($this->stream);
}
/**
* Seek to the beginning of the stream.
*
* If the stream is not seekable, this method will raise an exception;
* otherwise, it will perform a seek(0).
*
* @throws \RuntimeException on failure
*
* @see http://www.php.net/manual/en/function.fseek.php
* @see seek()
*/
public function rewind()
{
$this->seekable && rewind($this->stream);
}
/**
* Get the size of the stream if known.
*
* @return int|null returns the size in bytes if known, or null if unknown
*/
public function getSize()
{
if ($this->size !== null) {
return $this->size;
}
if (!$this->stream) {
return null;
}
// Clear the stat cache if the stream has a URI
if ($this->uri !== null) {
clearstatcache(true, $this->uri);
}
$stats = fstat($this->stream);
if (isset($stats['size'])) {
$this->size = $stats['size'];
return $this->size;
}
return null;
}
/**
* Returns the current position of the file read/write pointer.
*
* @throws \RuntimeException on error
*
* @return int Position of the file pointer
*/
public function tell()
{
return $this->stream ? ftell($this->stream) : false;
}
/**
* Returns true if the stream is at the end of the stream.
*
* @return bool
*/
public function eof()
{
return !$this->stream || feof($this->stream);
}
/**
* Returns whether or not the stream is seekable.
*
* @return bool
*/
public function isSeekable()
{
return $this->seekable;
}
/**
* Seek to a position in the stream.
*
* @see http://www.php.net/manual/en/function.fseek.php
*
* @param int $offset Stream offset
* @param int $whence Specifies how the cursor position will be calculated
* based on the seek offset. Valid values are identical to the built-in
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
* offset bytes SEEK_CUR: Set position to current location plus offset
* SEEK_END: Set position to end-of-stream plus offset.
*
* @throws \RuntimeException on failure
*/
public function seek($offset, $whence = \SEEK_SET)
{
$this->seekable && fseek($this->stream, $offset, $whence);
}
/**
* Returns whether or not the stream is writable.
*
* @return bool
*/
public function isWritable()
{
return $this->writable;
}
/**
* Write data to the stream.
*
* @param string $string the string that is to be written
*
* @throws \RuntimeException on failure
*
* @return int returns the number of bytes written to the stream
*/
public function write($string)
{
$this->size = null;
return $this->writable ? fwrite($this->stream, $string) : false;
}
/**
* Returns whether or not the stream is readable.
*
* @return bool
*/
public function isReadable()
{
return $this->readable;
}
/**
* Read data from the stream.
*
* @param int $length Read up to $length bytes from the object and return
* them. Fewer than $length bytes may be returned if underlying stream
* call returns fewer bytes.
*
* @throws \RuntimeException if an error occurs
*
* @return string returns the data read from the stream, or an empty string
* if no bytes are available
*/
public function read($length)
{
return $this->readable ? fread($this->stream, $length) : '';
}
/**
* Returns the remaining contents in a string.
*
* @throws \RuntimeException if unable to read or an error occurs while
* reading
*
* @return string
*/
public function getContents()
{
return $this->stream ? stream_get_contents($this->stream) : '';
}
/**
* Closes the stream when the destructed.
*/
public function __destruct()
{
$this->close();
}
/**
* Closes the stream and any underlying resources.
*/
public function close()
{
if (\is_resource($this->stream)) {
fclose($this->stream);
}
$this->detach();
}
/**
* Separates any underlying resources from the stream.
*
* After the stream has been detached, the stream is in an unusable state.
*
* @return resource|null Underlying PHP stream, if any
*/
public function detach()
{
$result = $this->stream;
$this->stream = null;
$this->size = null;
$this->uri = null;
$this->readable = false;
$this->writable = false;
$this->seekable = false;
return $result;
}
}

View File

@@ -0,0 +1,309 @@
<?php
namespace PhpZip\IO\Stream;
use PhpZip\Exception\ZipException;
use PhpZip\Model\ZipEntry;
/**
* The class provides stream reuse functionality.
*
* Stream will not be closed at {@see fclose}.
*
* @see https://www.php.net/streamwrapper
*/
final class ZipEntryStreamWrapper
{
/** @var string the registered protocol */
const PROTOCOL = 'zipentry';
/** @var resource */
public $context;
/** @var resource */
private $fp;
/**
* @return bool
*/
public static function register()
{
$protocol = self::PROTOCOL;
if (!\in_array($protocol, stream_get_wrappers(), true)) {
if (!stream_wrapper_register($protocol, self::class)) {
throw new \RuntimeException("Failed to register '{$protocol}://' protocol");
}
return true;
}
return false;
}
public static function unregister()
{
stream_wrapper_unregister(self::PROTOCOL);
}
/**
* @param ZipEntry $entry
*
* @return resource
*/
public static function wrap(ZipEntry $entry)
{
self::register();
$context = stream_context_create(
[
self::PROTOCOL => [
'entry' => $entry,
],
]
);
$uri = self::PROTOCOL . '://' . $entry->getName();
$fp = fopen($uri, 'r+b', false, $context);
if ($fp === false) {
throw new \RuntimeException('Error open ' . $uri);
}
return $fp;
}
/**
* Opens file or URL.
*
* This method is called immediately after the wrapper is
* initialized (f.e. by {@see fopen()} and {@see file_get_contents()}).
*
* @param string $path specifies the URL that was passed to
* the original function
* @param string $mode the mode used to open the file, as detailed
* for {@see fopen()}
* @param int $options Holds additional flags set by the streams
* API. It can hold one or more of the
* following values OR'd together.
* @param string $opened_path if the path is opened successfully, and
* STREAM_USE_PATH is set in options,
* opened_path should be set to the
* full path of the file/resource that
* was actually opened
*
* @throws ZipException
*
* @return bool
*
* @see https://www.php.net/streamwrapper.stream-open
*/
public function stream_open($path, $mode, $options, &$opened_path)
{
if ($this->context === null) {
throw new \RuntimeException('stream context is null');
}
$streamOptions = stream_context_get_options($this->context);
if (!isset($streamOptions[self::PROTOCOL]['entry'])) {
throw new \RuntimeException('no stream option ["' . self::PROTOCOL . '"]["entry"]');
}
$zipEntry = $streamOptions[self::PROTOCOL]['entry'];
if (!$zipEntry instanceof ZipEntry) {
throw new \RuntimeException('invalid stream context');
}
$zipData = $zipEntry->getData();
if ($zipData === null) {
throw new ZipException(sprintf('No data for zip entry "%s"', $zipEntry->getName()));
}
$this->fp = $zipData->getDataAsStream();
return $this->fp !== false;
}
/**
* Read from stream.
*
* This method is called in response to {@see fread()} and {@see fgets()}.
*
* Note: Remember to update the read/write position of the stream
* (by the number of bytes that were successfully read).
*
* @param int $count how many bytes of data from the current
* position should be returned
*
* @return false|string If there are less than count bytes available,
* return as many as are available. If no more data
* is available, return either FALSE or
* an empty string.
*
* @see https://www.php.net/streamwrapper.stream-read
*/
public function stream_read($count)
{
return fread($this->fp, $count);
}
/**
* Seeks to specific location in a stream.
*
* This method is called in response to {@see fseek()}.
* The read/write position of the stream should be updated according
* to the offset and whence.
*
* @param int $offset the stream offset to seek to
* @param int $whence Possible values:
* {@see \SEEK_SET} - Set position equal to offset bytes.
* {@see \SEEK_CUR} - Set position to current location plus offset.
* {@see \SEEK_END} - Set position to end-of-file plus offset.
*
* @return bool return TRUE if the position was updated, FALSE otherwise
*
* @see https://www.php.net/streamwrapper.stream-seek
*/
public function stream_seek($offset, $whence = \SEEK_SET)
{
return fseek($this->fp, $offset, $whence) === 0;
}
/**
* Retrieve the current position of a stream.
*
* This method is called in response to {@see fseek()} to determine
* the current position.
*
* @return int should return the current position of the stream
*
* @see https://www.php.net/streamwrapper.stream-tell
*/
public function stream_tell()
{
$pos = ftell($this->fp);
if ($pos === false) {
throw new \RuntimeException('Cannot get stream position.');
}
return $pos;
}
/**
* Tests for end-of-file on a file pointer.
*
* This method is called in response to {@see feof()}.
*
* @return bool should return TRUE if the read/write position is at
* the end of the stream and if no more data is available
* to be read, or FALSE otherwise
*
* @see https://www.php.net/streamwrapper.stream-eof
*/
public function stream_eof()
{
return feof($this->fp);
}
/**
* Retrieve information about a file resource.
*
* This method is called in response to {@see fstat()}.
*
* @return array
*
* @see https://www.php.net/streamwrapper.stream-stat
* @see https://www.php.net/stat
* @see https://www.php.net/fstat
*/
public function stream_stat()
{
return fstat($this->fp);
}
/**
* Flushes the output.
*
* This method is called in response to {@see fflush()} and when the
* stream is being closed while any unflushed data has been written to
* it before.
* If you have cached data in your stream but not yet stored it into
* the underlying storage, you should do so now.
*
* @return bool should return TRUE if the cached data was successfully
* stored (or if there was no data to store), or FALSE
* if the data could not be stored
*
* @see https://www.php.net/streamwrapper.stream-flush
*/
public function stream_flush()
{
return fflush($this->fp);
}
/**
* Truncate stream.
*
* Will respond to truncation, e.g., through {@see ftruncate()}.
*
* @param int $new_size the new size
*
* @return bool returns TRUE on success or FALSE on failure
*
* @see https://www.php.net/streamwrapper.stream-truncate
*/
public function stream_truncate($new_size)
{
return ftruncate($this->fp, (int) $new_size);
}
/**
* Write to stream.
*
* This method is called in response to {@see fwrite().}
*
* Note: Remember to update the current position of the stream by
* number of bytes that were successfully written.
*
* @param string $data should be stored into the underlying stream
*
* @return int should return the number of bytes that were successfully stored, or 0 if none could be stored
*
* @see https://www.php.net/streamwrapper.stream-write
*/
public function stream_write($data)
{
$bytes = fwrite($this->fp, $data);
return $bytes === false ? 0 : $bytes;
}
/**
* Retrieve the underlaying resource.
*
* This method is called in response to {@see stream_select()}.
*
* @param int $cast_as can be {@see STREAM_CAST_FOR_SELECT} when {@see stream_select()}
* is callingstream_cast() or {@see STREAM_CAST_AS_STREAM} when
* stream_cast() is called for other uses
*
* @return resource
*/
public function stream_cast($cast_as)
{
return $this->fp;
}
/**
* Close a resource.
*
* This method is called in response to {@see fclose()}.
* All resources that were locked, or allocated, by the wrapper should be released.
*
* @see https://www.php.net/streamwrapper.stream-close
*/
public function stream_close()
{
}
}

896
src/IO/ZipReader.php Normal file
View File

@@ -0,0 +1,896 @@
<?php
namespace PhpZip\IO;
use PhpZip\Constants\DosCodePage;
use PhpZip\Constants\GeneralPurposeBitFlag;
use PhpZip\Constants\ZipCompressionMethod;
use PhpZip\Constants\ZipConstants;
use PhpZip\Constants\ZipEncryptionMethod;
use PhpZip\Constants\ZipOptions;
use PhpZip\Exception\Crc32Exception;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\IO\Filter\Cipher\Pkware\PKDecryptionStreamFilter;
use PhpZip\IO\Filter\Cipher\WinZipAes\WinZipAesDecryptionStreamFilter;
use PhpZip\Model\Data\ZipSourceFileData;
use PhpZip\Model\EndOfCentralDirectory;
use PhpZip\Model\Extra\ExtraFieldsCollection;
use PhpZip\Model\Extra\Fields\UnicodePathExtraField;
use PhpZip\Model\Extra\Fields\UnrecognizedExtraField;
use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
use PhpZip\Model\Extra\Fields\Zip64ExtraField;
use PhpZip\Model\Extra\ZipExtraDriver;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ImmutableZipContainer;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\PackUtil;
/**
* Zip reader.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipReader
{
/** @var int file size */
protected $size;
/** @var resource */
protected $inStream;
/** @var array */
protected $options;
/**
* @param resource $inStream
* @param array $options
*/
public function __construct($inStream, array $options = [])
{
if (!\is_resource($inStream)) {
throw new InvalidArgumentException('Stream must be a resource');
}
$type = get_resource_type($inStream);
if ($type !== 'stream') {
throw new InvalidArgumentException("Invalid resource type {$type}.");
}
$meta = stream_get_meta_data($inStream);
$wrapperType = isset($meta['wrapper_type']) ? $meta['wrapper_type'] : 'Unknown';
$supportStreamWrapperTypes = ['plainfile', 'PHP', 'user-space'];
if (!\in_array($wrapperType, $supportStreamWrapperTypes, true)) {
throw new InvalidArgumentException(
'The stream wrapper type "' . $wrapperType . '" is not supported. Support: ' . implode(
', ',
$supportStreamWrapperTypes
)
);
}
if (
$wrapperType === 'plainfile' &&
(
$meta['stream_type'] === 'dir' ||
(isset($meta['uri']) && is_dir($meta['uri']))
)
) {
throw new InvalidArgumentException('Directory stream not supported');
}
$seekable = $meta['seekable'];
if (!$seekable) {
throw new InvalidArgumentException('Resource does not support seekable.');
}
$this->size = fstat($inStream)['size'];
$this->inStream = $inStream;
/** @noinspection AdditionOperationOnArraysInspection */
$options += $this->getDefaultOptions();
$this->options = $options;
}
/**
* @return array
*/
protected function getDefaultOptions()
{
return [
ZipOptions::CHARSET => null,
];
}
/**
* @throws ZipException
*
* @return ImmutableZipContainer
*/
public function read()
{
if ($this->size < ZipConstants::END_CD_MIN_LEN) {
throw new ZipException('Corrupt zip file');
}
$endOfCentralDirectory = $this->readEndOfCentralDirectory();
$entries = $this->readCentralDirectory($endOfCentralDirectory);
return new ImmutableZipContainer($entries, $endOfCentralDirectory->getComment());
}
/**
* @return array
*/
public function getStreamMetaData()
{
return stream_get_meta_data($this->inStream);
}
/**
* Read End of central directory record.
*
* end of central dir signature 4 bytes (0x06054b50)
* number of this disk 2 bytes
* number of the disk with the
* start of the central directory 2 bytes
* total number of entries in the
* central directory on this disk 2 bytes
* total number of entries in
* the central directory 2 bytes
* size of the central directory 4 bytes
* offset of start of central
* directory with respect to
* the starting disk number 4 bytes
* .ZIP file comment length 2 bytes
* .ZIP file comment (variable size)
*
* @throws ZipException
*
* @return EndOfCentralDirectory
*/
protected function readEndOfCentralDirectory()
{
if (!$this->findEndOfCentralDirectory()) {
throw new ZipException('Invalid zip file. The end of the central directory could not be found.');
}
$positionECD = ftell($this->inStream) - 4;
$sizeECD = $this->size - ftell($this->inStream);
$buffer = fread($this->inStream, $sizeECD);
$unpack = unpack(
'vdiskNo/vcdDiskNo/vcdEntriesDisk/' .
'vcdEntries/VcdSize/VcdPos/vcommentLength',
substr($buffer, 0, 18)
);
if (
$unpack['diskNo'] !== 0 ||
$unpack['cdDiskNo'] !== 0 ||
$unpack['cdEntriesDisk'] !== $unpack['cdEntries']
) {
throw new ZipException(
'ZIP file spanning/splitting is not supported!'
);
}
// .ZIP file comment (variable sizeECD)
$comment = null;
if ($unpack['commentLength'] > 0) {
$comment = substr($buffer, 18, $unpack['commentLength']);
}
// Check for ZIP64 End Of Central Directory Locator exists.
$zip64ECDLocatorPosition = $positionECD - ZipConstants::ZIP64_END_CD_LOC_LEN;
fseek($this->inStream, $zip64ECDLocatorPosition);
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
if ($zip64ECDLocatorPosition > 0 && unpack(
'V',
fread($this->inStream, 4)
)[1] === ZipConstants::ZIP64_END_CD_LOC) {
if (!$this->isZip64Support()) {
throw new ZipException('ZIP64 not supported this archive.');
}
$positionECD = $this->findZip64ECDPosition();
$endCentralDirectory = $this->readZip64EndOfCentralDirectory($positionECD);
$endCentralDirectory->setComment($comment);
} else {
$endCentralDirectory = new EndOfCentralDirectory(
$unpack['cdEntries'],
$unpack['cdPos'],
$unpack['cdSize'],
false,
$comment
);
}
return $endCentralDirectory;
}
/**
* @return bool
*/
protected function findEndOfCentralDirectory()
{
$max = $this->size - ZipConstants::END_CD_MIN_LEN;
$min = $max >= 0xffff ? $max - 0xffff : 0;
// Search for End of central directory record.
for ($position = $max; $position >= $min; $position--) {
fseek($this->inStream, $position);
// end of central dir signature 4 bytes (0x06054b50)
if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::END_CD) {
continue;
}
return true;
}
return false;
}
/**
* Read Zip64 end of central directory locator and returns
* Zip64 end of central directory position.
*
* number of the disk with the
* start of the zip64 end of
* central directory 4 bytes
* relative offset of the zip64
* end of central directory record 8 bytes
* total number of disks 4 bytes
*
* @throws ZipException
*
* @return int Zip64 End Of Central Directory position
*/
protected function findZip64ECDPosition()
{
$diskNo = unpack('V', fread($this->inStream, 4))[1];
$zip64ECDPos = PackUtil::unpackLongLE(fread($this->inStream, 8));
$totalDisks = unpack('V', fread($this->inStream, 4))[1];
if ($diskNo !== 0 || $totalDisks > 1) {
throw new ZipException('ZIP file spanning/splitting is not supported!');
}
return $zip64ECDPos;
}
/**
* Read zip64 end of central directory locator and zip64 end
* of central directory record.
*
* zip64 end of central dir
* signature 4 bytes (0x06064b50)
* size of zip64 end of central
* directory record 8 bytes
* version made by 2 bytes
* version needed to extract 2 bytes
* number of this disk 4 bytes
* number of the disk with the
* start of the central directory 4 bytes
* total number of entries in the
* central directory on this disk 8 bytes
* total number of entries in the
* central directory 8 bytes
* size of the central directory 8 bytes
* offset of start of central
* directory with respect to
* the starting disk number 8 bytes
* zip64 extensible data sector (variable size)
*
* @param int $zip64ECDPosition
*
* @throws ZipException
*
* @return EndOfCentralDirectory
*/
protected function readZip64EndOfCentralDirectory($zip64ECDPosition)
{
fseek($this->inStream, $zip64ECDPosition);
$buffer = fread($this->inStream, ZipConstants::ZIP64_END_OF_CD_LEN);
if (unpack('V', $buffer)[1] !== ZipConstants::ZIP64_END_CD) {
throw new ZipException('Expected ZIP64 End Of Central Directory Record!');
}
$data = unpack(
// 'Psize/vversionMadeBy/vextractVersion/' .
'VdiskNo/VcdDiskNo',
substr($buffer, 16, 8)
);
$cdEntriesDisk = PackUtil::unpackLongLE(substr($buffer, 24, 8));
$entryCount = PackUtil::unpackLongLE(substr($buffer, 32, 8));
$cdSize = PackUtil::unpackLongLE(substr($buffer, 40, 8));
$cdPos = PackUtil::unpackLongLE(substr($buffer, 48, 8));
// $platform = ZipPlatform::fromValue(($data['versionMadeBy'] & 0xFF00) >> 8);
// $softwareVersion = $data['versionMadeBy'] & 0x00FF;
if ($data['diskNo'] !== 0 || $data['cdDiskNo'] !== 0 || $entryCount !== $cdEntriesDisk) {
throw new ZipException('ZIP file spanning/splitting is not supported!');
}
if ($entryCount < 0 || $entryCount > 0x7fffffff) {
throw new ZipException('Total Number Of Entries In The Central Directory out of range!');
}
// skip zip64 extensible data sector (variable sizeEndCD)
return new EndOfCentralDirectory(
$entryCount,
$cdPos,
$cdSize,
true
);
}
/**
* Reads the central directory from the given seekable byte channel
* and populates the internal tables with ZipEntry instances.
*
* The ZipEntry's will know all data that can be obtained from the
* central directory alone, but not the data that requires the local
* file header or additional data to be read.
*
* @param EndOfCentralDirectory $endCD
*
* @throws ZipException
*
* @return ZipEntry[]
*/
protected function readCentralDirectory(EndOfCentralDirectory $endCD)
{
$entries = [];
$cdOffset = $endCD->getCdOffset();
fseek($this->inStream, $cdOffset);
if (!($cdStream = fopen('php://temp', 'w+b'))) {
throw new ZipException('Temp resource can not open from write');
}
stream_copy_to_stream($this->inStream, $cdStream, $endCD->getCdSize());
rewind($cdStream);
for ($numEntries = $endCD->getEntryCount(); $numEntries > 0; $numEntries--) {
$zipEntry = $this->readZipEntry($cdStream);
$entryName = $zipEntry->getName();
/** @var UnicodePathExtraField|null $unicodePathExtraField */
$unicodePathExtraField = $zipEntry->getExtraField(UnicodePathExtraField::HEADER_ID);
if ($unicodePathExtraField !== null && $unicodePathExtraField->getCrc32() === crc32($entryName)) {
$unicodePath = $unicodePathExtraField->getUnicodeValue();
if ($unicodePath !== null) {
$unicodePath = str_replace('\\', '/', $unicodePath);
if (
$unicodePath !== '' &&
substr_count($entryName, '/') === substr_count($unicodePath, '/')
) {
$entryName = $unicodePath;
}
}
}
$entries[$entryName] = $zipEntry;
}
return $entries;
}
/**
* Read central directory entry.
*
* central file header signature 4 bytes (0x02014b50)
* version made by 2 bytes
* version needed to extract 2 bytes
* general purpose bit flag 2 bytes
* compression method 2 bytes
* last mod file time 2 bytes
* last mod file date 2 bytes
* crc-32 4 bytes
* compressed size 4 bytes
* uncompressed size 4 bytes
* file name length 2 bytes
* extra field length 2 bytes
* file comment length 2 bytes
* disk number start 2 bytes
* internal file attributes 2 bytes
* external file attributes 4 bytes
* relative offset of local header 4 bytes
*
* file name (variable size)
* extra field (variable size)
* file comment (variable size)
*
* @param resource $stream
*
* @throws ZipException
*
* @return ZipEntry
*/
protected function readZipEntry($stream)
{
if (unpack('V', fread($stream, 4))[1] !== ZipConstants::CENTRAL_FILE_HEADER) {
throw new ZipException('Corrupt zip file. Cannot read zip entry.');
}
$unpack = unpack(
'vversionMadeBy/vversionNeededToExtract/' .
'vgeneralPurposeBitFlag/vcompressionMethod/' .
'VlastModFile/Vcrc/VcompressedSize/' .
'VuncompressedSize/vfileNameLength/vextraFieldLength/' .
'vfileCommentLength/vdiskNumberStart/vinternalFileAttributes/' .
'VexternalFileAttributes/VoffsetLocalHeader',
fread($stream, 42)
);
if ($unpack['diskNumberStart'] !== 0) {
throw new ZipException('ZIP file spanning/splitting is not supported!');
}
$generalPurposeBitFlags = $unpack['generalPurposeBitFlag'];
$isUtf8 = ($generalPurposeBitFlags & GeneralPurposeBitFlag::UTF8) !== 0;
$name = fread($stream, $unpack['fileNameLength']);
$createdOS = ($unpack['versionMadeBy'] & 0xFF00) >> 8;
$softwareVersion = $unpack['versionMadeBy'] & 0x00FF;
$extractedOS = ($unpack['versionNeededToExtract'] & 0xFF00) >> 8;
$extractVersion = $unpack['versionNeededToExtract'] & 0x00FF;
$dosTime = $unpack['lastModFile'];
$comment = null;
if ($unpack['fileCommentLength'] > 0) {
$comment = fread($stream, $unpack['fileCommentLength']);
}
// decode code page names
$fallbackCharset = null;
if (!$isUtf8 && isset($this->options[ZipOptions::CHARSET])) {
$charset = $this->options[ZipOptions::CHARSET];
$fallbackCharset = $charset;
$name = DosCodePage::toUTF8($name, $charset);
if ($comment !== null) {
$comment = DosCodePage::toUTF8($comment, $charset);
}
}
$zipEntry = ZipEntry::create(
$name,
$createdOS,
$extractedOS,
$softwareVersion,
$extractVersion,
$unpack['compressionMethod'],
$generalPurposeBitFlags,
$dosTime,
$unpack['crc'],
$unpack['compressedSize'],
$unpack['uncompressedSize'],
$unpack['internalFileAttributes'],
$unpack['externalFileAttributes'],
$unpack['offsetLocalHeader'],
$comment,
$fallbackCharset
);
if ($unpack['extraFieldLength'] > 0) {
$this->parseExtraFields(
fread($stream, $unpack['extraFieldLength']),
$zipEntry,
false
);
/** @var Zip64ExtraField|null $extraZip64 */
$extraZip64 = $zipEntry->getCdExtraField(Zip64ExtraField::HEADER_ID);
if ($extraZip64 !== null) {
$this->handleZip64Extra($extraZip64, $zipEntry);
}
}
$this->loadLocalExtraFields($zipEntry);
$this->handleExtraEncryptionFields($zipEntry);
$this->handleExtraFields($zipEntry);
return $zipEntry;
}
/**
* @param string $buffer
* @param ZipEntry $zipEntry
* @param bool $local
*
* @return ExtraFieldsCollection
*/
protected function parseExtraFields($buffer, ZipEntry $zipEntry, $local = false)
{
$collection = $local ?
$zipEntry->getLocalExtraFields() :
$zipEntry->getCdExtraFields();
if (!empty($buffer)) {
$pos = 0;
$endPos = \strlen($buffer);
while ($endPos - $pos >= 4) {
/** @var int[] $data */
$data = unpack('vheaderId/vdataSize', substr($buffer, $pos, 4));
$pos += 4;
if ($endPos - $pos - $data['dataSize'] < 0) {
break;
}
$bufferData = substr($buffer, $pos, $data['dataSize']);
$headerId = $data['headerId'];
/** @var string|ZipExtraField|null $className */
$className = ZipExtraDriver::getClassNameOrNull($headerId);
try {
if ($className !== null) {
try {
$extraField = $local ?
\call_user_func([$className, 'unpackLocalFileData'], $bufferData, $zipEntry) :
\call_user_func([$className, 'unpackCentralDirData'], $bufferData, $zipEntry);
} catch (\Throwable $e) {
// skip errors while parsing invalid data
continue;
}
} else {
$extraField = new UnrecognizedExtraField($headerId, $bufferData);
}
$collection->add($extraField);
} finally {
$pos += $data['dataSize'];
}
}
}
return $collection;
}
/**
* @param Zip64ExtraField $extraZip64
* @param ZipEntry $zipEntry
*/
protected function handleZip64Extra(Zip64ExtraField $extraZip64, ZipEntry $zipEntry)
{
$uncompressedSize = $extraZip64->getUncompressedSize();
$compressedSize = $extraZip64->getCompressedSize();
$localHeaderOffset = $extraZip64->getLocalHeaderOffset();
if ($uncompressedSize !== null) {
$zipEntry->setUncompressedSize($uncompressedSize);
}
if ($compressedSize !== null) {
$zipEntry->setCompressedSize($compressedSize);
}
if ($localHeaderOffset !== null) {
$zipEntry->setLocalHeaderOffset($localHeaderOffset);
}
}
/**
* Read Local File Header.
*
* local file header signature 4 bytes (0x04034b50)
* version needed to extract 2 bytes
* general purpose bit flag 2 bytes
* compression method 2 bytes
* last mod file time 2 bytes
* last mod file date 2 bytes
* crc-32 4 bytes
* compressed size 4 bytes
* uncompressed size 4 bytes
* file name length 2 bytes
* extra field length 2 bytes
* file name (variable size)
* extra field (variable size)
*
* @param ZipEntry $entry
*
* @throws ZipException
*/
protected function loadLocalExtraFields(ZipEntry $entry)
{
$offsetLocalHeader = $entry->getLocalHeaderOffset();
fseek($this->inStream, $offsetLocalHeader);
if (unpack('V', fread($this->inStream, 4))[1] !== ZipConstants::LOCAL_FILE_HEADER) {
throw new ZipException(sprintf('%s (expected Local File Header)', $entry->getName()));
}
fseek($this->inStream, $offsetLocalHeader + ZipConstants::LFH_FILENAME_LENGTH_POS);
$unpack = unpack('vfileNameLength/vextraFieldLength', fread($this->inStream, 4));
$offsetData = ftell($this->inStream)
+ $unpack['fileNameLength']
+ $unpack['extraFieldLength'];
fseek($this->inStream, $unpack['fileNameLength'], \SEEK_CUR);
if ($unpack['extraFieldLength'] > 0) {
$this->parseExtraFields(
fread($this->inStream, $unpack['extraFieldLength']),
$entry,
true
);
}
$zipData = new ZipSourceFileData($this, $entry, $offsetData);
$entry->setData($zipData);
}
/**
* @param ZipEntry $zipEntry
*
* @throws ZipException
*/
private function handleExtraEncryptionFields(ZipEntry $zipEntry)
{
if ($zipEntry->isEncrypted()) {
if ($zipEntry->getCompressionMethod() === ZipCompressionMethod::WINZIP_AES) {
/** @var WinZipAesExtraField|null $extraField */
$extraField = $zipEntry->getExtraField(WinZipAesExtraField::HEADER_ID);
if ($extraField === null) {
throw new ZipException(
sprintf(
'Extra field 0x%04x (WinZip-AES Encryption) expected for compression method %d',
WinZipAesExtraField::HEADER_ID,
$zipEntry->getCompressionMethod()
)
);
}
$zipEntry->setCompressionMethod($extraField->getCompressionMethod());
$zipEntry->setEncryptionMethod($extraField->getEncryptionMethod());
} else {
$zipEntry->setEncryptionMethod(ZipEncryptionMethod::PKWARE);
}
}
}
/**
* Handle extra data in zip records.
*
* This is a special method in which you can process ExtraField
* and make changes to ZipEntry.
*
* @param ZipEntry $zipEntry
*/
protected function handleExtraFields(ZipEntry $zipEntry)
{
}
/**
* @param ZipSourceFileData $zipFileData
*
* @throws ZipException
* @throws Crc32Exception
*
* @return resource
*/
public function getEntryStream(ZipSourceFileData $zipFileData)
{
$outStream = fopen('php://temp', 'w+b');
$this->copyUncompressedDataToStream($zipFileData, $outStream);
rewind($outStream);
return $outStream;
}
/**
* @param ZipSourceFileData $zipFileData
* @param resource $outStream
*
* @throws Crc32Exception
* @throws ZipException
*/
public function copyUncompressedDataToStream(ZipSourceFileData $zipFileData, $outStream)
{
if (!\is_resource($outStream)) {
throw new InvalidArgumentException('outStream is not resource');
}
$entry = $zipFileData->getSourceEntry();
// if ($entry->isDirectory()) {
// throw new InvalidArgumentException('Streams not supported for directories');
// }
if ($entry->isStrongEncryption()) {
throw new ZipException('Not support encryption zip.');
}
$compressionMethod = $entry->getCompressionMethod();
fseek($this->inStream, $zipFileData->getOffset());
$filters = [];
$skipCheckCrc = false;
$isEncrypted = $entry->isEncrypted();
if ($isEncrypted) {
if ($entry->getPassword() === null) {
throw new ZipException('Can not password from entry ' . $entry->getName());
}
if (ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) {
/** @var WinZipAesExtraField|null $winZipAesExtra */
$winZipAesExtra = $entry->getExtraField(WinZipAesExtraField::HEADER_ID);
if ($winZipAesExtra === null) {
throw new ZipException(
sprintf('WinZip AES must contain the extra field %s', WinZipAesExtraField::HEADER_ID)
);
}
$compressionMethod = $winZipAesExtra->getCompressionMethod();
WinZipAesDecryptionStreamFilter::register();
$cipherFilterName = WinZipAesDecryptionStreamFilter::FILTER_NAME;
if ($winZipAesExtra->isV2()) {
$skipCheckCrc = true;
}
} else {
PKDecryptionStreamFilter::register();
$cipherFilterName = PKDecryptionStreamFilter::FILTER_NAME;
}
$encContextFilter = stream_filter_append(
$this->inStream,
$cipherFilterName,
\STREAM_FILTER_READ,
[
'entry' => $entry,
]
);
if (!$encContextFilter) {
throw new \RuntimeException('Not apply filter ' . $cipherFilterName);
}
$filters[] = $encContextFilter;
}
// hack, see https://groups.google.com/forum/#!topic/alt.comp.lang.php/37_JZeW63uc
$pos = ftell($this->inStream);
rewind($this->inStream);
fseek($this->inStream, $pos);
$contextDecompress = null;
switch ($compressionMethod) {
case ZipCompressionMethod::STORED:
// file without compression, do nothing
break;
case ZipCompressionMethod::DEFLATED:
if (!($contextDecompress = stream_filter_append(
$this->inStream,
'zlib.inflate',
\STREAM_FILTER_READ
))) {
throw new \RuntimeException('Could not append filter "zlib.inflate" to stream');
}
$filters[] = $contextDecompress;
break;
case ZipCompressionMethod::BZIP2:
if (!($contextDecompress = stream_filter_append(
$this->inStream,
'bzip2.decompress',
\STREAM_FILTER_READ
))) {
throw new \RuntimeException('Could not append filter "bzip2.decompress" to stream');
}
$filters[] = $contextDecompress;
break;
default:
throw new ZipException(
sprintf(
'%s (compression method %d (%s) is not supported)',
$entry->getName(),
$compressionMethod,
ZipCompressionMethod::getCompressionMethodName($compressionMethod)
)
);
}
$limit = $zipFileData->getUncompressedSize();
$offset = 0;
$chunkSize = 8192;
try {
if ($skipCheckCrc) {
while ($offset < $limit) {
$length = min($chunkSize, $limit - $offset);
$buffer = fread($this->inStream, $length);
if ($buffer === false) {
throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName()));
}
fwrite($outStream, $buffer);
$offset += $length;
}
} else {
$contextHash = hash_init('crc32b');
while ($offset < $limit) {
$length = min($chunkSize, $limit - $offset);
$buffer = fread($this->inStream, $length);
if ($buffer === false) {
throw new ZipException(sprintf('Error reading the contents of entry "%s".', $entry->getName()));
}
fwrite($outStream, $buffer);
hash_update($contextHash, $buffer);
$offset += $length;
}
$expectedCrc = (int) hexdec(hash_final($contextHash));
if ($expectedCrc !== $entry->getCrc()) {
throw new Crc32Exception($entry->getName(), $expectedCrc, $entry->getCrc());
}
}
} finally {
for ($i = \count($filters); $i > 0; $i--) {
stream_filter_remove($filters[$i - 1]);
}
}
}
/**
* @param ZipSourceFileData $zipData
* @param resource $outStream
*/
public function copyCompressedDataToStream(ZipSourceFileData $zipData, $outStream)
{
if ($zipData->getCompressedSize() > 0) {
fseek($this->inStream, $zipData->getOffset());
stream_copy_to_stream($this->inStream, $outStream, $zipData->getCompressedSize());
}
}
/**
* @return bool
*/
protected function isZip64Support()
{
return \PHP_INT_SIZE === 8; // true for 64bit system
}
public function close()
{
if (\is_resource($this->inStream)) {
fclose($this->inStream);
}
}
public function __destruct()
{
$this->close();
}
}

884
src/IO/ZipWriter.php Normal file
View File

@@ -0,0 +1,884 @@
<?php
namespace PhpZip\IO;
use PhpZip\Constants\DosCodePage;
use PhpZip\Constants\ZipCompressionMethod;
use PhpZip\Constants\ZipConstants;
use PhpZip\Constants\ZipEncryptionMethod;
use PhpZip\Constants\ZipPlatform;
use PhpZip\Constants\ZipVersion;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipUnsupportMethodException;
use PhpZip\IO\Filter\Cipher\Pkware\PKEncryptionStreamFilter;
use PhpZip\IO\Filter\Cipher\WinZipAes\WinZipAesEncryptionStreamFilter;
use PhpZip\Model\Data\ZipSourceFileData;
use PhpZip\Model\Extra\Fields\ApkAlignmentExtraField;
use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
use PhpZip\Model\Extra\Fields\Zip64ExtraField;
use PhpZip\Model\ZipContainer;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\PackUtil;
use PhpZip\Util\StringUtil;
/**
* Class ZipWriter.
*/
class ZipWriter
{
/** @var int Chunk read size */
const CHUNK_SIZE = 8192;
/** @var ZipContainer */
protected $zipContainer;
/**
* ZipWriter constructor.
*
* @param ZipContainer $container
*/
public function __construct(ZipContainer $container)
{
$this->zipContainer = $container;
}
/**
* @param resource $outStream
*
* @throws ZipException
*/
public function write($outStream)
{
if (!\is_resource($outStream)) {
throw new \InvalidArgumentException('$outStream must be resource');
}
$this->beforeWrite();
$this->writeLocalBlock($outStream);
$cdOffset = ftell($outStream);
$this->writeCentralDirectoryBlock($outStream);
$cdSize = ftell($outStream) - $cdOffset;
$this->writeEndOfCentralDirectoryBlock($outStream, $cdOffset, $cdSize);
}
protected function beforeWrite()
{
}
/**
* @param resource $outStream
*
* @throws ZipException
*/
protected function writeLocalBlock($outStream)
{
$zipEntries = $this->zipContainer->getEntries();
foreach ($zipEntries as $zipEntry) {
$this->writeLocalHeader($outStream, $zipEntry);
$this->writeData($outStream, $zipEntry);
if ($zipEntry->isDataDescriptorEnabled()) {
$this->writeDataDescriptor($outStream, $zipEntry);
}
}
}
/**
* @param resource $outStream
* @param ZipEntry $entry
*
* @throws ZipException
*/
protected function writeLocalHeader($outStream, ZipEntry $entry)
{
// todo in 4.0 version move zipalign functional to ApkWriter class
if ($this->zipContainer->isZipAlign()) {
$this->zipAlign($outStream, $entry);
}
$relativeOffset = ftell($outStream);
$entry->setLocalHeaderOffset($relativeOffset);
if ($entry->isEncrypted() && $entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
$entry->enableDataDescriptor(true);
}
$dd = $entry->isDataDescriptorRequired() ||
$entry->isDataDescriptorEnabled();
$compressedSize = $entry->getCompressedSize();
$uncompressedSize = $entry->getUncompressedSize();
$entry->getLocalExtraFields()->remove(Zip64ExtraField::HEADER_ID);
if ($compressedSize > ZipConstants::ZIP64_MAGIC || $uncompressedSize > ZipConstants::ZIP64_MAGIC) {
$entry->getLocalExtraFields()->add(
new Zip64ExtraField($uncompressedSize, $compressedSize)
);
$compressedSize = ZipConstants::ZIP64_MAGIC;
$uncompressedSize = ZipConstants::ZIP64_MAGIC;
}
$compressionMethod = $entry->getCompressionMethod();
$crc = $entry->getCrc();
if ($entry->isEncrypted() && ZipEncryptionMethod::isWinZipAesMethod($entry->getEncryptionMethod())) {
/** @var WinZipAesExtraField|null $winZipAesExtra */
$winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
if ($winZipAesExtra === null) {
$winZipAesExtra = WinZipAesExtraField::create($entry);
}
if ($winZipAesExtra->isV2()) {
$crc = 0;
}
$compressionMethod = ZipCompressionMethod::WINZIP_AES;
}
$extra = $this->getExtraFieldsContents($entry, true);
$name = $entry->getName();
$dosCharset = $entry->getCharset();
if ($dosCharset !== null && !$entry->isUtf8Flag()) {
$name = DosCodePage::fromUTF8($name, $dosCharset);
}
$nameLength = \strlen($name);
$extraLength = \strlen($extra);
$size = $nameLength + $extraLength;
if ($size > 0xffff) {
throw new ZipException(
sprintf(
'%s (the total size of %s bytes for the name, extra fields and comment exceeds the maximum size of %d bytes)',
$entry->getName(),
$size,
0xffff
)
);
}
$extractedBy = ($entry->getExtractedOS() << 8) | $entry->getExtractVersion();
fwrite(
$outStream,
pack(
'VvvvVVVVvv',
// local file header signature 4 bytes (0x04034b50)
ZipConstants::LOCAL_FILE_HEADER,
// version needed to extract 2 bytes
$extractedBy,
// general purpose bit flag 2 bytes
$entry->getGeneralPurposeBitFlags(),
// compression method 2 bytes
$compressionMethod,
// last mod file time 2 bytes
// last mod file date 2 bytes
$entry->getDosTime(),
// crc-32 4 bytes
$dd ? 0 : $crc,
// compressed size 4 bytes
$dd ? 0 : $compressedSize,
// uncompressed size 4 bytes
$dd ? 0 : $uncompressedSize,
// file name length 2 bytes
$nameLength,
// extra field length 2 bytes
$extraLength
)
);
if ($nameLength > 0) {
fwrite($outStream, $name);
}
if ($extraLength > 0) {
fwrite($outStream, $extra);
}
}
/**
* @param resource $outStream
* @param ZipEntry $entry
*
* @throws ZipException
*/
private function zipAlign($outStream, ZipEntry $entry)
{
if (!$entry->isDirectory() && $entry->getCompressionMethod() === ZipCompressionMethod::STORED) {
$entry->removeExtraField(ApkAlignmentExtraField::HEADER_ID);
$extra = $this->getExtraFieldsContents($entry, true);
$extraLength = \strlen($extra);
$name = $entry->getName();
$dosCharset = $entry->getCharset();
if ($dosCharset !== null && !$entry->isUtf8Flag()) {
$name = DosCodePage::fromUTF8($name, $dosCharset);
}
$nameLength = \strlen($name);
$multiple = ApkAlignmentExtraField::ALIGNMENT_BYTES;
if (StringUtil::endsWith($name, '.so')) {
$multiple = ApkAlignmentExtraField::COMMON_PAGE_ALIGNMENT_BYTES;
}
$offset = ftell($outStream);
$dataMinStartOffset =
$offset +
ZipConstants::LFH_FILENAME_POS +
$extraLength +
$nameLength;
$padding =
($multiple - ($dataMinStartOffset % $multiple))
% $multiple;
if ($padding > 0) {
$dataMinStartOffset += ApkAlignmentExtraField::MIN_SIZE;
$padding =
($multiple - ($dataMinStartOffset % $multiple))
% $multiple;
$entry->getLocalExtraFields()->add(
new ApkAlignmentExtraField($multiple, $padding)
);
}
}
}
/**
* Merges the local file data fields of the given ZipExtraFields.
*
* @param ZipEntry $entry
* @param bool $local
*
* @throws ZipException
*
* @return string
*/
protected function getExtraFieldsContents(ZipEntry $entry, $local)
{
$local = (bool) $local;
$collection = $local ?
$entry->getLocalExtraFields() :
$entry->getCdExtraFields();
$extraData = '';
foreach ($collection as $extraField) {
if ($local) {
$data = $extraField->packLocalFileData();
} else {
$data = $extraField->packCentralDirData();
}
$extraData .= pack(
'vv',
$extraField->getHeaderId(),
\strlen($data)
);
$extraData .= $data;
}
$size = \strlen($extraData);
if ($size > 0xffff) {
throw new ZipException(
sprintf(
'Size extra out of range: %d. Extra data: %s',
$size,
$extraData
)
);
}
return $extraData;
}
/**
* @param resource $outStream
* @param ZipEntry $entry
*
* @throws ZipException
*/
protected function writeData($outStream, ZipEntry $entry)
{
$zipData = $entry->getData();
if ($zipData === null) {
if ($entry->isDirectory()) {
return;
}
throw new ZipException(sprintf('No zip data for entry "%s"', $entry->getName()));
}
// data write variants:
// --------------------
// * data of source zip file -> copy compressed data
// * store - simple write
// * store and encryption - apply encryption filter and simple write
// * deflate or bzip2 - apply compression filter and simple write
// * (deflate or bzip2) and encryption - create temp stream and apply
// compression filter to it, then apply encryption filter to root
// stream and write temp stream data.
// (PHP cannot apply the filter for encryption after the compression
// filter, so a temporary stream is created for the compressed data)
if ($zipData instanceof ZipSourceFileData && !$zipData->hasRecompressData($entry)) {
// data of source zip file -> copy compressed data
$zipData->copyCompressedDataToStream($outStream);
return;
}
$entryStream = $zipData->getDataAsStream();
if (stream_get_meta_data($entryStream)['seekable']) {
rewind($entryStream);
}
$uncompressedSize = $entry->getUncompressedSize();
$posBeforeWrite = ftell($outStream);
$compressionMethod = $entry->getCompressionMethod();
if ($entry->isEncrypted()) {
if ($compressionMethod === ZipCompressionMethod::STORED) {
$contextFilter = $this->appendEncryptionFilter($outStream, $entry, $uncompressedSize);
$checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
} else {
$compressStream = fopen('php://temp', 'w+b');
$contextFilter = $this->appendCompressionFilter($compressStream, $entry);
$checksum = $this->writeAndCountChecksum($entryStream, $compressStream, $uncompressedSize);
if ($contextFilter !== null) {
stream_filter_remove($contextFilter);
$contextFilter = null;
}
rewind($compressStream);
$compressedSize = fstat($compressStream)['size'];
$contextFilter = $this->appendEncryptionFilter($outStream, $entry, $compressedSize);
stream_copy_to_stream($compressStream, $outStream);
}
} else {
$contextFilter = $this->appendCompressionFilter($outStream, $entry);
$checksum = $this->writeAndCountChecksum($entryStream, $outStream, $uncompressedSize);
}
if ($contextFilter !== null) {
stream_filter_remove($contextFilter);
$contextFilter = null;
}
// my hack {@see https://bugs.php.net/bug.php?id=49874}
fseek($outStream, 0, \SEEK_END);
$compressedSize = ftell($outStream) - $posBeforeWrite;
$entry->setCompressedSize($compressedSize);
$entry->setCrc($checksum);
if (!$entry->isDataDescriptorEnabled()) {
if ($uncompressedSize > ZipConstants::ZIP64_MAGIC || $compressedSize > ZipConstants::ZIP64_MAGIC) {
/** @var Zip64ExtraField|null $zip64ExtraLocal */
$zip64ExtraLocal = $entry->getLocalExtraField(Zip64ExtraField::HEADER_ID);
// if there is a zip64 extra record, then update it;
// if not, write data to data descriptor
if ($zip64ExtraLocal !== null) {
$zip64ExtraLocal->setCompressedSize($compressedSize);
$zip64ExtraLocal->setUncompressedSize($uncompressedSize);
$posExtra = $entry->getLocalHeaderOffset() + ZipConstants::LFH_FILENAME_POS + \strlen($entry->getName());
fseek($outStream, $posExtra);
fwrite($outStream, $this->getExtraFieldsContents($entry, true));
} else {
$posGPBF = $entry->getLocalHeaderOffset() + 6;
$entry->enableDataDescriptor(true);
fseek($outStream, $posGPBF);
fwrite(
$outStream,
pack(
'v',
// general purpose bit flag 2 bytes
$entry->getGeneralPurposeBitFlags()
)
);
}
$compressedSize = ZipConstants::ZIP64_MAGIC;
$uncompressedSize = ZipConstants::ZIP64_MAGIC;
}
$posChecksum = $entry->getLocalHeaderOffset() + 14;
/** @var WinZipAesExtraField|null $winZipAesExtra */
$winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
$checksum = 0;
}
fseek($outStream, $posChecksum);
fwrite(
$outStream,
pack(
'VVV',
// crc-32 4 bytes
$checksum,
// compressed size 4 bytes
$compressedSize,
// uncompressed size 4 bytes
$uncompressedSize
)
);
fseek($outStream, 0, \SEEK_END);
}
}
/**
* @param resource $inStream
* @param resource $outStream
* @param int $size
*
* @return int
*/
private function writeAndCountChecksum($inStream, $outStream, $size)
{
$contextHash = hash_init('crc32b');
$offset = 0;
while ($offset < $size) {
$read = min(self::CHUNK_SIZE, $size - $offset);
$buffer = fread($inStream, $read);
fwrite($outStream, $buffer);
hash_update($contextHash, $buffer);
$offset += $read;
}
return (int) hexdec(hash_final($contextHash));
}
/**
* @param resource $outStream
* @param ZipEntry $entry
*
* @throws ZipUnsupportMethodException
*
* @return resource|null
*/
protected function appendCompressionFilter($outStream, ZipEntry $entry)
{
$contextCompress = null;
switch ($entry->getCompressionMethod()) {
case ZipCompressionMethod::DEFLATED:
if (!($contextCompress = stream_filter_append(
$outStream,
'zlib.deflate',
\STREAM_FILTER_WRITE,
['level' => $entry->getCompressionLevel()]
))) {
throw new \RuntimeException('Could not append filter "zlib.deflate" to out stream');
}
break;
case ZipCompressionMethod::BZIP2:
if (!($contextCompress = stream_filter_append(
$outStream,
'bzip2.compress',
\STREAM_FILTER_WRITE,
['blocks' => $entry->getCompressionLevel(), 'work' => 0]
))) {
throw new \RuntimeException('Could not append filter "bzip2.compress" to out stream');
}
break;
case ZipCompressionMethod::STORED:
// file without compression, do nothing
break;
default:
throw new ZipUnsupportMethodException(
sprintf(
'%s (compression method %d (%s) is not supported)',
$entry->getName(),
$entry->getCompressionMethod(),
ZipCompressionMethod::getCompressionMethodName($entry->getCompressionMethod())
)
);
}
return $contextCompress;
}
/**
* @param resource $outStream
* @param ZipEntry $entry
* @param int $size
*
* @return resource|null
*/
protected function appendEncryptionFilter($outStream, ZipEntry $entry, $size)
{
$encContextFilter = null;
if ($entry->isEncrypted()) {
if ($entry->getEncryptionMethod() === ZipEncryptionMethod::PKWARE) {
PKEncryptionStreamFilter::register();
$cipherFilterName = PKEncryptionStreamFilter::FILTER_NAME;
} else {
WinZipAesEncryptionStreamFilter::register();
$cipherFilterName = WinZipAesEncryptionStreamFilter::FILTER_NAME;
}
$encContextFilter = stream_filter_append(
$outStream,
$cipherFilterName,
\STREAM_FILTER_WRITE,
[
'entry' => $entry,
'size' => $size,
]
);
if (!$encContextFilter) {
throw new \RuntimeException('Not apply filter ' . $cipherFilterName);
}
}
return $encContextFilter;
}
/**
* @param resource $outStream
* @param ZipEntry $entry
*/
protected function writeDataDescriptor($outStream, ZipEntry $entry)
{
$crc = $entry->getCrc();
/** @var WinZipAesExtraField|null $winZipAesExtra */
$winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
if ($winZipAesExtra !== null && $winZipAesExtra->isV2()) {
$crc = 0;
}
fwrite(
$outStream,
pack(
'VV',
// data descriptor signature 4 bytes (0x08074b50)
ZipConstants::DATA_DESCRIPTOR,
// crc-32 4 bytes
$crc
)
);
if (
$entry->isZip64ExtensionsRequired() ||
$entry->getLocalExtraFields()->has(Zip64ExtraField::HEADER_ID)
) {
$dd =
// compressed size 8 bytes
PackUtil::packLongLE($entry->getCompressedSize()) .
// uncompressed size 8 bytes
PackUtil::packLongLE($entry->getUncompressedSize());
} else {
$dd = pack(
'VV',
// compressed size 4 bytes
$entry->getCompressedSize(),
// uncompressed size 4 bytes
$entry->getUncompressedSize()
);
}
fwrite($outStream, $dd);
}
/**
* @param resource $outStream
*
* @throws ZipException
*/
protected function writeCentralDirectoryBlock($outStream)
{
foreach ($this->zipContainer->getEntries() as $outputEntry) {
$this->writeCentralDirectoryHeader($outStream, $outputEntry);
}
}
/**
* Writes a Central File Header record.
*
* @param resource $outStream
* @param ZipEntry $entry
*
* @throws ZipException
*/
protected function writeCentralDirectoryHeader($outStream, ZipEntry $entry)
{
$compressedSize = $entry->getCompressedSize();
$uncompressedSize = $entry->getUncompressedSize();
$localHeaderOffset = $entry->getLocalHeaderOffset();
$entry->getCdExtraFields()->remove(Zip64ExtraField::HEADER_ID);
if (
$localHeaderOffset > ZipConstants::ZIP64_MAGIC ||
$compressedSize > ZipConstants::ZIP64_MAGIC ||
$uncompressedSize > ZipConstants::ZIP64_MAGIC
) {
$zip64ExtraField = new Zip64ExtraField();
if ($uncompressedSize >= ZipConstants::ZIP64_MAGIC) {
$zip64ExtraField->setUncompressedSize($uncompressedSize);
$uncompressedSize = ZipConstants::ZIP64_MAGIC;
}
if ($compressedSize >= ZipConstants::ZIP64_MAGIC) {
$zip64ExtraField->setCompressedSize($compressedSize);
$compressedSize = ZipConstants::ZIP64_MAGIC;
}
if ($localHeaderOffset >= ZipConstants::ZIP64_MAGIC) {
$zip64ExtraField->setLocalHeaderOffset($localHeaderOffset);
$localHeaderOffset = ZipConstants::ZIP64_MAGIC;
}
$entry->getCdExtraFields()->add($zip64ExtraField);
}
$extra = $this->getExtraFieldsContents($entry, false);
$extraLength = \strlen($extra);
$name = $entry->getName();
$comment = $entry->getComment();
$dosCharset = $entry->getCharset();
if ($dosCharset !== null && !$entry->isUtf8Flag()) {
$name = DosCodePage::fromUTF8($name, $dosCharset);
if ($comment) {
$comment = DosCodePage::fromUTF8($comment, $dosCharset);
}
}
$commentLength = \strlen($comment);
$compressionMethod = $entry->getCompressionMethod();
$crc = $entry->getCrc();
/** @var WinZipAesExtraField|null $winZipAesExtra */
$winZipAesExtra = $entry->getLocalExtraField(WinZipAesExtraField::HEADER_ID);
if ($winZipAesExtra !== null) {
if ($winZipAesExtra->isV2()) {
$crc = 0;
}
$compressionMethod = ZipCompressionMethod::WINZIP_AES;
}
fwrite(
$outStream,
pack(
'VvvvvVVVVvvvvvVV',
// central file header signature 4 bytes (0x02014b50)
ZipConstants::CENTRAL_FILE_HEADER,
// version made by 2 bytes
($entry->getCreatedOS() << 8) | $entry->getSoftwareVersion(),
// version needed to extract 2 bytes
($entry->getExtractedOS() << 8) | $entry->getExtractVersion(),
// general purpose bit flag 2 bytes
$entry->getGeneralPurposeBitFlags(),
// compression method 2 bytes
$compressionMethod,
// last mod file datetime 4 bytes
$entry->getDosTime(),
// crc-32 4 bytes
$crc,
// compressed size 4 bytes
$compressedSize,
// uncompressed size 4 bytes
$uncompressedSize,
// file name length 2 bytes
\strlen($name),
// extra field length 2 bytes
$extraLength,
// file comment length 2 bytes
$commentLength,
// disk number start 2 bytes
0,
// internal file attributes 2 bytes
$entry->getInternalAttributes(),
// external file attributes 4 bytes
$entry->getExternalAttributes(),
// relative offset of local header 4 bytes
$localHeaderOffset
)
);
// file name (variable size)
fwrite($outStream, $name);
if ($extraLength > 0) {
// extra field (variable size)
fwrite($outStream, $extra);
}
if ($commentLength > 0) {
// file comment (variable size)
fwrite($outStream, $comment);
}
}
/**
* @param resource $outStream
* @param int $centralDirectoryOffset
* @param int $centralDirectorySize
*/
protected function writeEndOfCentralDirectoryBlock(
$outStream,
$centralDirectoryOffset,
$centralDirectorySize
) {
$cdEntriesCount = \count($this->zipContainer);
$cdEntriesZip64 = $cdEntriesCount > 0xffff;
$cdSizeZip64 = $centralDirectorySize > ZipConstants::ZIP64_MAGIC;
$cdOffsetZip64 = $centralDirectoryOffset > ZipConstants::ZIP64_MAGIC;
$zip64Required = $cdEntriesZip64
|| $cdSizeZip64
|| $cdOffsetZip64;
if ($zip64Required) {
$zip64EndOfCentralDirectoryOffset = ftell($outStream);
// find max software version, version needed to extract and most common platform
list($softwareVersion, $versionNeededToExtract) = array_reduce(
$this->zipContainer->getEntries(),
static function (array $carry, ZipEntry $entry) {
$carry[0] = max($carry[0], $entry->getSoftwareVersion() & 0xFF);
$carry[1] = max($carry[1], $entry->getExtractVersion() & 0xFF);
return $carry;
},
[ZipVersion::v10_DEFAULT_MIN, ZipVersion::v45_ZIP64_EXT]
);
$createdOS = $extractedOS = ZipPlatform::OS_DOS;
$versionMadeBy = ($createdOS << 8) | max($softwareVersion, ZipVersion::v45_ZIP64_EXT);
$versionExtractedBy = ($extractedOS << 8) | max($versionNeededToExtract, ZipVersion::v45_ZIP64_EXT);
// write zip64 end of central directory signature
fwrite(
$outStream,
pack(
'V',
// signature 4 bytes (0x06064b50)
ZipConstants::ZIP64_END_CD
)
);
// size of zip64 end of central
// directory record 8 bytes
fwrite($outStream, PackUtil::packLongLE(ZipConstants::ZIP64_END_OF_CD_LEN - 12));
fwrite(
$outStream,
pack(
'vvVV',
// version made by 2 bytes
$versionMadeBy & 0xFFFF,
// version needed to extract 2 bytes
$versionExtractedBy & 0xFFFF,
// number of this disk 4 bytes
0,
// number of the disk with the
// start of the central directory 4 bytes
0
)
);
fwrite(
$outStream,
// total number of entries in the
// central directory on this disk 8 bytes
PackUtil::packLongLE($cdEntriesCount) .
// total number of entries in the
// central directory 8 bytes
PackUtil::packLongLE($cdEntriesCount) .
// size of the central directory 8 bytes
PackUtil::packLongLE($centralDirectorySize) .
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
PackUtil::packLongLE($centralDirectoryOffset)
);
// write zip64 end of central directory locator
fwrite(
$outStream,
pack(
'VV',
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
ZipConstants::ZIP64_END_CD_LOC,
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
0
) .
// relative offset of the zip64
// end of central directory record 8 bytes
PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset) .
// total number of disks 4 bytes
pack('V', 1)
);
}
$comment = $this->zipContainer->getArchiveComment();
$commentLength = $comment !== null ? \strlen($comment) : 0;
fwrite(
$outStream,
pack(
'VvvvvVVv',
// end of central dir signature 4 bytes (0x06054b50)
ZipConstants::END_CD,
// number of this disk 2 bytes
0,
// number of the disk with the
// start of the central directory 2 bytes
0,
// total number of entries in the
// central directory on this disk 2 bytes
$cdEntriesZip64 ? 0xffff : $cdEntriesCount,
// total number of entries in
// the central directory 2 bytes
$cdEntriesZip64 ? 0xffff : $cdEntriesCount,
// size of the central directory 4 bytes
$cdSizeZip64 ? ZipConstants::ZIP64_MAGIC : $centralDirectorySize,
// offset of start of central
// directory with respect to
// the starting disk number 4 bytes
$cdOffsetZip64 ? ZipConstants::ZIP64_MAGIC : $centralDirectoryOffset,
// .ZIP file comment length 2 bytes
$commentLength
)
);
if ($comment !== null && $commentLength > 0) {
// .ZIP file comment (variable size)
fwrite($outStream, $comment);
}
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace PhpZip\Model\Data;
use PhpZip\Exception\ZipException;
use PhpZip\Model\ZipData;
use PhpZip\Model\ZipEntry;
/**
* Class ZipFileData.
*/
class ZipFileData implements ZipData
{
/** @var \SplFileInfo */
private $file;
/**
* ZipStringData constructor.
*
* @param ZipEntry $zipEntry
* @param \SplFileInfo $fileInfo
*
* @throws ZipException
*/
public function __construct(ZipEntry $zipEntry, \SplFileInfo $fileInfo)
{
if (!$fileInfo->isFile()) {
throw new ZipException('$fileInfo is not a file.');
}
if (!$fileInfo->isReadable()) {
throw new ZipException('$fileInfo is not readable.');
}
$this->file = $fileInfo;
$zipEntry->setUncompressedSize($fileInfo->getSize());
}
/**
* @throws ZipException
*
* @return resource returns stream data
*/
public function getDataAsStream()
{
if (!$this->file->isReadable()) {
throw new ZipException(sprintf('The %s file is no longer readable.', $this->file->getPathname()));
}
return fopen($this->file->getPathname(), 'rb');
}
/**
* @throws ZipException
*
* @return string returns data as string
*/
public function getDataAsString()
{
if (!$this->file->isReadable()) {
throw new ZipException(sprintf('The %s file is no longer readable.', $this->file->getPathname()));
}
return file_get_contents($this->file->getPathname());
}
/**
* @param resource $outStream
*
* @throws ZipException
*/
public function copyDataToStream($outStream)
{
try {
$stream = $this->getDataAsStream();
stream_copy_to_stream($stream, $outStream);
} finally {
fclose($stream);
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace PhpZip\Model\Data;
use PhpZip\Model\ZipData;
use PhpZip\Model\ZipEntry;
/**
* Class ZipNewData.
*/
class ZipNewData implements ZipData
{
/** @var resource */
private $stream;
/** @var ZipEntry */
private $zipEntry;
/**
* ZipStringData constructor.
*
* @param ZipEntry $zipEntry
* @param string|resource $data
*/
public function __construct(ZipEntry $zipEntry, $data)
{
$this->zipEntry = $zipEntry;
if (\is_string($data)) {
$zipEntry->setUncompressedSize(\strlen($data));
if (!($handle = fopen('php://temp', 'w+b'))) {
throw new \RuntimeException('Temp resource can not open from write.');
}
fwrite($handle, $data);
rewind($handle);
$this->stream = $handle;
} elseif (\is_resource($data)) {
$this->stream = $data;
}
}
/**
* @return resource returns stream data
*/
public function getDataAsStream()
{
if (!\is_resource($this->stream)) {
throw new \LogicException(sprintf('Resource was closed (entry=%s).', $this->zipEntry->getName()));
}
return $this->stream;
}
/**
* @return string returns data as string
*/
public function getDataAsString()
{
$stream = $this->getDataAsStream();
$pos = ftell($stream);
try {
rewind($stream);
return stream_get_contents($stream);
} finally {
fseek($stream, $pos);
}
}
/**
* @param resource $outStream
*/
public function copyDataToStream($outStream)
{
$stream = $this->getDataAsStream();
rewind($stream);
stream_copy_to_stream($stream, $outStream);
}
public function __destruct()
{
if (\is_resource($this->stream)) {
fclose($this->stream);
}
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace PhpZip\Model\Data;
use PhpZip\Exception\Crc32Exception;
use PhpZip\Exception\ZipException;
use PhpZip\IO\ZipReader;
use PhpZip\Model\ZipData;
use PhpZip\Model\ZipEntry;
/**
* Class ZipFileData.
*/
class ZipSourceFileData implements ZipData
{
/** @var ZipReader */
private $zipReader;
/** @var resource|null */
private $stream;
/** @var ZipEntry */
private $sourceEntry;
/** @var int */
private $offset;
/** @var int */
private $uncompressedSize;
/** @var int */
private $compressedSize;
/**
* ZipFileData constructor.
*
* @param ZipReader $zipReader
* @param ZipEntry $zipEntry
* @param int $offsetData
*/
public function __construct(ZipReader $zipReader, ZipEntry $zipEntry, $offsetData)
{
$this->zipReader = $zipReader;
$this->offset = $offsetData;
$this->sourceEntry = $zipEntry;
$this->compressedSize = $zipEntry->getCompressedSize();
$this->uncompressedSize = $zipEntry->getUncompressedSize();
}
/**
* @param ZipEntry $entry
*
* @return bool
*/
public function hasRecompressData(ZipEntry $entry)
{
return $this->sourceEntry->getCompressionLevel() !== $entry->getCompressionLevel() ||
$this->sourceEntry->getCompressionMethod() !== $entry->getCompressionMethod() ||
$this->sourceEntry->isEncrypted() !== $entry->isEncrypted() ||
$this->sourceEntry->getEncryptionMethod() !== $entry->getEncryptionMethod() ||
$this->sourceEntry->getPassword() !== $entry->getPassword() ||
$this->sourceEntry->getCompressedSize() !== $entry->getCompressedSize() ||
$this->sourceEntry->getUncompressedSize() !== $entry->getUncompressedSize() ||
$this->sourceEntry->getCrc() !== $entry->getCrc();
}
/**
* @throws ZipException
*
* @return resource returns stream data
*/
public function getDataAsStream()
{
if (!\is_resource($this->stream)) {
$this->stream = $this->zipReader->getEntryStream($this);
}
return $this->stream;
}
/**
* @throws ZipException
*
* @return string returns data as string
*/
public function getDataAsString()
{
$autoClosable = $this->stream === null;
$stream = $this->getDataAsStream();
$pos = ftell($stream);
try {
rewind($stream);
return stream_get_contents($stream);
} finally {
if ($autoClosable) {
fclose($stream);
$this->stream = null;
} else {
fseek($stream, $pos);
}
}
}
/**
* @param resource $outputStream Output stream
*
* @throws ZipException
* @throws Crc32Exception
*/
public function copyDataToStream($outputStream)
{
if (\is_resource($this->stream)) {
rewind($this->stream);
stream_copy_to_stream($this->stream, $outputStream);
} else {
$this->zipReader->copyUncompressedDataToStream($this, $outputStream);
}
}
/**
* @param resource $outputStream Output stream
*/
public function copyCompressedDataToStream($outputStream)
{
$this->zipReader->copyCompressedDataToStream($this, $outputStream);
}
/**
* @return ZipEntry
*/
public function getSourceEntry()
{
return $this->sourceEntry;
}
/**
* @return int
*/
public function getCompressedSize()
{
return $this->compressedSize;
}
/**
* @return int
*/
public function getUncompressedSize()
{
return $this->uncompressedSize;
}
/**
* @return int
*/
public function getOffset()
{
return $this->offset;
}
/**
* {@inheritdoc}
*/
public function __destruct()
{
if (\is_resource($this->stream)) {
fclose($this->stream);
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace PhpZip\Model;
/**
* End of Central Directory.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class EndOfCentralDirectory
{
/** @var int Count files. */
private $entryCount;
/** @var int Central Directory Offset. */
private $cdOffset;
/** @var int */
private $cdSize;
/** @var string|null The archive comment. */
private $comment;
/** @var bool Zip64 extension */
private $zip64;
/**
* EndOfCentralDirectory constructor.
*
* @param int $entryCount
* @param int $cdOffset
* @param int $cdSize
* @param bool $zip64
* @param string|null $comment
*/
public function __construct($entryCount, $cdOffset, $cdSize, $zip64, $comment = null)
{
$this->entryCount = $entryCount;
$this->cdOffset = $cdOffset;
$this->cdSize = $cdSize;
$this->zip64 = $zip64;
$this->comment = $comment;
}
/**
* @param string|null $comment
*/
public function setComment($comment)
{
$this->comment = $comment;
}
/**
* @return int
*/
public function getEntryCount()
{
return $this->entryCount;
}
/**
* @return int
*/
public function getCdOffset()
{
return $this->cdOffset;
}
/**
* @return int
*/
public function getCdSize()
{
return $this->cdSize;
}
/**
* @return string|null
*/
public function getComment()
{
return $this->comment;
}
/**
* @return bool
*/
public function isZip64()
{
return $this->zip64;
}
}

View File

@@ -0,0 +1,276 @@
<?php
namespace PhpZip\Model\Extra;
/**
* Represents a collection of Extra Fields as they may
* be present at several locations in ZIP files.
*/
class ExtraFieldsCollection implements \ArrayAccess, \Countable, \Iterator
{
/**
* The map of Extra Fields.
* Maps from Header ID to Extra Field.
* Must not be null, but may be empty if no Extra Fields are used.
* The map is sorted by Header IDs in ascending order.
*
* @var ZipExtraField[]
*/
protected $collection = [];
/**
* Returns the number of Extra Fields in this collection.
*
* @return int
*/
public function count()
{
return \count($this->collection);
}
/**
* Returns the Extra Field with the given Header ID or null
* if no such Extra Field exists.
*
* @param int $headerId the requested Header ID
*
* @return ZipExtraField|null the Extra Field with the given Header ID or
* if no such Extra Field exists
*/
public function get($headerId)
{
$this->validateHeaderId($headerId);
return isset($this->collection[$headerId]) ? $this->collection[$headerId] : null;
}
/**
* @param int $headerId
*/
private function validateHeaderId($headerId)
{
if ($headerId < 0 || $headerId > 0xffff) {
throw new \InvalidArgumentException('$headerId out of range');
}
}
/**
* Stores the given Extra Field in this collection.
*
* @param ZipExtraField $extraField the Extra Field to store in this collection
*
* @return ZipExtraField the Extra Field previously associated with the Header ID of
* of the given Extra Field or null if no such Extra Field existed
*/
public function add(ZipExtraField $extraField)
{
$headerId = $extraField->getHeaderId();
$this->validateHeaderId($headerId);
$this->collection[$headerId] = $extraField;
return $extraField;
}
/**
* @param ZipExtraField[] $extraFields
*/
public function addAll(array $extraFields)
{
foreach ($extraFields as $extraField) {
$this->add($extraField);
}
}
/**
* @param ExtraFieldsCollection $collection
*/
public function addCollection(self $collection)
{
$this->addAll($collection->collection);
}
/**
* @return ZipExtraField[]
*/
public function getAll()
{
return $this->collection;
}
/**
* Returns Extra Field exists.
*
* @param int $headerId the requested Header ID
*
* @return bool
*/
public function has($headerId)
{
return isset($this->collection[$headerId]);
}
/**
* Removes the Extra Field with the given Header ID.
*
* @param int $headerId the requested Header ID
*
* @return ZipExtraField|null the Extra Field with the given Header ID or null
* if no such Extra Field exists
*/
public function remove($headerId)
{
$this->validateHeaderId($headerId);
if (isset($this->collection[$headerId])) {
$ef = $this->collection[$headerId];
unset($this->collection[$headerId]);
return $ef;
}
return null;
}
/**
* Whether a offset exists.
*
* @see http://php.net/manual/en/arrayaccess.offsetexists.php
*
* @param int $offset an offset to check for
*
* @return bool true on success or false on failure
*/
public function offsetExists($offset)
{
return isset($this->collection[(int) $offset]);
}
/**
* Offset to retrieve.
*
* @see http://php.net/manual/en/arrayaccess.offsetget.php
*
* @param int $offset the offset to retrieve
*
* @return ZipExtraField|null
*/
public function offsetGet($offset)
{
return isset($this->collection[$offset]) ? $this->collection[$offset] : null;
}
/**
* Offset to set.
*
* @see http://php.net/manual/en/arrayaccess.offsetset.php
*
* @param mixed $offset the offset to assign the value to
* @param ZipExtraField $value the value to set
*/
public function offsetSet($offset, $value)
{
if (!$value instanceof ZipExtraField) {
throw new \InvalidArgumentException('value is not instanceof ' . ZipExtraField::class);
}
$this->add($value);
}
/**
* Offset to unset.
*
* @see http://php.net/manual/en/arrayaccess.offsetunset.php
*
* @param mixed $offset the offset to unset
*/
public function offsetUnset($offset)
{
$this->remove($offset);
}
/**
* Return the current element.
*
* @see http://php.net/manual/en/iterator.current.php
*
* @return ZipExtraField
*/
public function current()
{
return current($this->collection);
}
/**
* Move forward to next element.
*
* @see http://php.net/manual/en/iterator.next.php
*/
public function next()
{
next($this->collection);
}
/**
* Return the key of the current element.
*
* @see http://php.net/manual/en/iterator.key.php
*
* @return int scalar on success, or null on failure
*/
public function key()
{
return key($this->collection);
}
/**
* Checks if current position is valid.
*
* @see http://php.net/manual/en/iterator.valid.php
*
* @return bool The return value will be casted to boolean and then evaluated.
* Returns true on success or false on failure.
*/
public function valid()
{
return key($this->collection) !== null;
}
/**
* Rewind the Iterator to the first element.
*
* @see http://php.net/manual/en/iterator.rewind.php
*/
public function rewind()
{
reset($this->collection);
}
public function clear()
{
$this->collection = [];
}
/**
* @return string
*/
public function __toString()
{
$formats = [];
foreach ($this->collection as $key => $value) {
$formats[] = (string) $value;
}
return implode("\n", $formats);
}
/**
* If clone extra fields.
*/
public function __clone()
{
foreach ($this->collection as $k => $v) {
$this->collection[$k] = clone $v;
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* A common base class for Unicode extra information extra fields.
*/
abstract class AbstractUnicodeExtraField implements ZipExtraField
{
const DEFAULT_VERSION = 0x01;
/** @var int */
private $crc32;
/** @var string */
private $unicodeValue;
/**
* @param int $crc32
* @param string $unicodeValue
*/
public function __construct($crc32, $unicodeValue)
{
$this->crc32 = (int) $crc32;
$this->unicodeValue = (string) $unicodeValue;
}
/**
* @return int the CRC32 checksum of the filename or comment as
* encoded in the central directory of the zip file
*/
public function getCrc32()
{
return $this->crc32;
}
/**
* @param int $crc32
*/
public function setCrc32($crc32)
{
$this->crc32 = (int) $crc32;
}
/**
* @return string
*/
public function getUnicodeValue()
{
return $this->unicodeValue;
}
/**
* @param string $unicodeValue the UTF-8 encoded name to set
*/
public function setUnicodeValue($unicodeValue)
{
$this->unicodeValue = $unicodeValue;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException on error
*
* @return static
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
{
if (\strlen($buffer) < 5) {
throw new ZipException('Unicode path extra data must have at least 5 bytes.');
}
$data = unpack('Cversion/Vcrc32', $buffer);
if ($data['version'] !== self::DEFAULT_VERSION) {
throw new ZipException(sprintf('Unsupported version [%d] for Unicode path extra data.', $data['version']));
}
$unicodeValue = substr($buffer, 5);
return new static($data['crc32'], $unicodeValue);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException on error
*
* @return static
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData()
{
return pack(
'CV',
self::DEFAULT_VERSION,
$this->crc32
) .
$this->unicodeValue;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData()
{
return $this->packLocalFileData();
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* Apk Alignment Extra Field.
*
* @see https://android.googlesource.com/platform/tools/apksig/+/master/src/main/java/com/android/apksig/ApkSigner.java
* @see https://developer.android.com/studio/command-line/zipalign
*/
class ApkAlignmentExtraField implements ZipExtraField
{
/**
* @var int Extensible data block/field header ID used for storing
* information about alignment of uncompressed entries as
* well as for aligning the entries's data. See ZIP
* appnote.txt section 4.5 Extensible data fields.
*/
const HEADER_ID = 0xd935;
/**
* @var int minimum size (in bytes) of the extensible data block/field used
* for alignment of uncompressed entries
*/
const MIN_SIZE = 6;
/** @var int */
const ALIGNMENT_BYTES = 4;
/** @var int */
const COMMON_PAGE_ALIGNMENT_BYTES = 4096;
/** @var int */
private $multiple;
/** @var int */
private $padding;
/**
* @param int $multiple
* @param int $padding
*/
public function __construct($multiple, $padding)
{
$this->multiple = $multiple;
$this->padding = $padding;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return self::HEADER_ID;
}
/**
* @return int
*/
public function getMultiple()
{
return $this->multiple;
}
/**
* @return int
*/
public function getPadding()
{
return $this->padding;
}
/**
* @param int $multiple
*/
public function setMultiple($multiple)
{
$this->multiple = (int) $multiple;
}
/**
* @param int $padding
*/
public function setPadding($padding)
{
$this->padding = (int) $padding;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException
*
* @return ApkAlignmentExtraField
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
{
$length = \strlen($buffer);
if ($length < 2) {
// This is APK alignment field.
// FORMAT:
// * uint16 alignment multiple (in bytes)
// * remaining bytes -- padding to achieve alignment of data which starts after
// the extra field
throw new ZipException(
'Minimum 6 bytes of the extensible data block/field used for alignment of uncompressed entries.'
);
}
$multiple = unpack('v', $buffer)[1];
$padding = $length - 2;
return new self($multiple, $padding);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException on error
*
* @return ApkAlignmentExtraField
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData()
{
return pack('vx' . $this->padding, $this->multiple);
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData()
{
return $this->packLocalFileData();
}
/**
* @return string
*/
public function __toString()
{
return sprintf(
'0x%04x APK Alignment: Multiple=%d Padding=%d',
self::HEADER_ID,
$this->multiple,
$this->padding
);
}
}

View File

@@ -0,0 +1,302 @@
<?php
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Constants\UnixStat;
use PhpZip\Exception\Crc32Exception;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* ASi Unix Extra Field:
* ====================.
*
* The following is the layout of the ASi extra block for Unix. The
* local-header and central-header versions are identical.
* (Last Revision 19960916)
*
* Value Size Description
* ----- ---- -----------
* (Unix3) 0x756e Short tag for this extra block type ("nu")
* TSize Short total data size for this block
* CRC Long CRC-32 of the remaining data
* Mode Short file permissions
* SizDev Long symlink'd size OR major/minor dev num
* UID Short user ID
* GID Short group ID
* (var.) variable symbolic link filename
*
* Mode is the standard Unix st_mode field from struct stat, containing
* user/group/other permissions, setuid/setgid and symlink info, etc.
*
* If Mode indicates that this file is a symbolic link, SizDev is the
* size of the file to which the link points. Otherwise, if the file
* is a device, SizDev contains the standard Unix st_rdev field from
* struct stat (includes the major and minor numbers of the device).
* SizDev is undefined in other cases.
*
* If Mode indicates that the file is a symbolic link, the final field
* will be the name of the file to which the link points. The file-
* name length can be inferred from TSize.
*
* [Note that TSize may incorrectly refer to the data size not counting
* the CRC; i.e., it may be four bytes too small.]
*
* @see ftp://ftp.info-zip.org/pub/infozip/doc/appnote-iz-latest.zip Info-ZIP version Specification
*/
class AsiExtraField implements ZipExtraField
{
/** @var int Header id */
const HEADER_ID = 0x756e;
const USER_GID_PID = 1000;
/** Bits used for permissions (and sticky bit). */
const PERM_MASK = 07777;
/** @var int Standard Unix stat(2) file mode. */
private $mode;
/** @var int User ID. */
private $uid;
/** @var int Group ID. */
private $gid;
/**
* @var string File this entry points to, if it is a symbolic link.
* Empty string - if entry is not a symbolic link.
*/
private $link;
/**
* AsiExtraField constructor.
*
* @param int $mode
* @param int $uid
* @param int $gid
* @param string $link
*/
public function __construct($mode, $uid = self::USER_GID_PID, $gid = self::USER_GID_PID, $link = '')
{
$this->mode = $mode;
$this->uid = $uid;
$this->gid = $gid;
$this->link = $link;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws Crc32Exception
*
* @return static
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
{
$givenChecksum = unpack('V', $buffer)[1];
$buffer = substr($buffer, 4);
$realChecksum = crc32($buffer);
if ($givenChecksum !== $realChecksum) {
throw new Crc32Exception('Asi Unix Extra Filed Data', $givenChecksum, $realChecksum);
}
$data = unpack('vmode/VlinkSize/vuid/vgid', $buffer);
$link = '';
if ($data['linkSize'] > 0) {
$link = substr($buffer, 10);
}
return new self($data['mode'], $data['uid'], $data['gid'], $link);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws Crc32Exception
*
* @return AsiExtraField
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData()
{
$data = pack(
'vVvv',
$this->mode,
\strlen($this->link),
$this->uid,
$this->gid
) . $this->link;
return pack('V', crc32($data)) . $data;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData()
{
return $this->packLocalFileData();
}
/**
* Name of linked file.
*
* @return string name of the file this entry links to if it is a
* symbolic link, the empty string otherwise
*/
public function getLink()
{
return $this->link;
}
/**
* Indicate that this entry is a symbolic link to the given filename.
*
* @param string $link name of the file this entry links to, empty
* string if it is not a symbolic link
*/
public function setLink($link)
{
$this->link = (string) $link;
$this->mode = $this->getPermissionsMode($this->mode);
}
/**
* Is this entry a symbolic link?
*
* @return bool true if this is a symbolic link
*/
public function isLink()
{
return !empty($this->link);
}
/**
* Get the file mode for given permissions with the correct file type.
*
* @param int $mode the mode
*
* @return int the type with the mode
*/
protected function getPermissionsMode($mode)
{
$type = 0;
if ($this->isLink()) {
$type = UnixStat::UNX_IFLNK;
} elseif (($mode & UnixStat::UNX_IFREG) !== 0) {
$type = UnixStat::UNX_IFREG;
} elseif (($mode & UnixStat::UNX_IFDIR) !== 0) {
$type = UnixStat::UNX_IFDIR;
}
return $type | ($mode & self::PERM_MASK);
}
/**
* Is this entry a directory?
*
* @return bool true if this entry is a directory
*/
public function isDirectory()
{
return ($this->mode & UnixStat::UNX_IFDIR) !== 0 && !$this->isLink();
}
/**
* @return int
*/
public function getMode()
{
return $this->mode;
}
/**
* @param int $mode
*/
public function setMode($mode)
{
$this->mode = $this->getPermissionsMode($mode);
}
/**
* @return int
*/
public function getUserId()
{
return $this->uid;
}
/**
* @param int $uid
*/
public function setUserId($uid)
{
$this->uid = (int) $uid;
}
/**
* @return int
*/
public function getGroupId()
{
return $this->gid;
}
/**
* @param int $gid
*/
public function setGroupId($gid)
{
$this->gid = (int) $gid;
}
/**
* @return string
*/
public function __toString()
{
return sprintf(
'0x%04x ASI: Mode=%o UID=%d GID=%d Link="%s',
self::HEADER_ID,
$this->mode,
$this->uid,
$this->gid,
$this->link
);
}
}

View File

@@ -0,0 +1,446 @@
<?php
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* Extended Timestamp Extra Field:
* ==============================.
*
* The following is the layout of the extended-timestamp extra block.
* (Last Revision 19970118)
*
* Local-header version:
*
* Value Size Description
* ----- ---- -----------
* (time) 0x5455 Short tag for this extra block type ("UT")
* TSize Short total data size for this block
* Flags Byte info bits
* (ModTime) Long time of last modification (UTC/GMT)
* (AcTime) Long time of last access (UTC/GMT)
* (CrTime) Long time of original creation (UTC/GMT)
*
* Central-header version:
*
* Value Size Description
* ----- ---- -----------
* (time) 0x5455 Short tag for this extra block type ("UT")
* TSize Short total data size for this block
* Flags Byte info bits (refers to local header!)
* (ModTime) Long time of last modification (UTC/GMT)
*
* The central-header extra field contains the modification time only,
* or no timestamp at all. TSize is used to flag its presence or
* absence. But note:
*
* If "Flags" indicates that Modtime is present in the local header
* field, it MUST be present in the central header field, too!
* This correspondence is required because the modification time
* value may be used to support trans-timezone freshening and
* updating operations with zip archives.
*
* The time values are in standard Unix signed-long format, indicating
* the number of seconds since 1 January 1970 00:00:00. The times
* are relative to Coordinated Universal Time (UTC), also sometimes
* referred to as Greenwich Mean Time (GMT). To convert to local time,
* the software must know the local timezone offset from UTC/GMT.
*
* The lower three bits of Flags in both headers indicate which time-
* stamps are present in the LOCAL extra field:
*
* bit 0 if set, modification time is present
* bit 1 if set, access time is present
* bit 2 if set, creation time is present
* bits 3-7 reserved for additional timestamps; not set
*
* Those times that are present will appear in the order indicated, but
* any combination of times may be omitted. (Creation time may be
* present without access time, for example.) TSize should equal
* (1 + 4*(number of set bits in Flags)), as the block is currently
* defined. Other timestamps may be added in the future.
*
* @see ftp://ftp.info-zip.org/pub/infozip/doc/appnote-iz-latest.zip Info-ZIP version Specification
*/
class ExtendedTimestampExtraField implements ZipExtraField
{
/** @var int Header id */
const HEADER_ID = 0x5455;
/**
* @var int the bit set inside the flags by when the last modification time
* is present in this extra field
*/
const MODIFY_TIME_BIT = 1;
/**
* @var int the bit set inside the flags by when the last access time is
* present in this extra field
*/
const ACCESS_TIME_BIT = 2;
/**
* @var int the bit set inside the flags by when the original creation time
* is present in this extra field
*/
const CREATE_TIME_BIT = 4;
/**
* @var int The 3 boolean fields (below) come from this flags byte. The remaining 5 bits
* are ignored according to the current version of the spec (December 2012).
*/
private $flags;
/** @var int|null Modify time */
private $modifyTime;
/** @var int|null Access time */
private $accessTime;
/** @var int|null Create time */
private $createTime;
/**
* @param int $flags
* @param int|null $modifyTime
* @param int|null $accessTime
* @param int|null $createTime
*/
public function __construct($flags, $modifyTime, $accessTime, $createTime)
{
$this->flags = (int) $flags;
$this->modifyTime = $modifyTime;
$this->accessTime = $accessTime;
$this->createTime = $createTime;
}
/**
* @param int|null $modifyTime
* @param int|null $accessTime
* @param int|null $createTime
*
* @return ExtendedTimestampExtraField
*/
public static function create($modifyTime, $accessTime, $createTime)
{
$flags = 0;
if ($modifyTime !== null) {
$modifyTime = (int) $modifyTime;
$flags |= self::MODIFY_TIME_BIT;
}
if ($accessTime !== null) {
$accessTime = (int) $accessTime;
$flags |= self::ACCESS_TIME_BIT;
}
if ($createTime !== null) {
$createTime = (int) $createTime;
$flags |= self::CREATE_TIME_BIT;
}
return new self($flags, $modifyTime, $accessTime, $createTime);
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @return ExtendedTimestampExtraField
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
{
$length = \strlen($buffer);
$flags = unpack('C', $buffer)[1];
$offset = 1;
$modifyTime = null;
$accessTime = null;
$createTime = null;
if (($flags & self::MODIFY_TIME_BIT) === self::MODIFY_TIME_BIT) {
$modifyTime = unpack('V', substr($buffer, $offset, 4))[1];
$offset += 4;
}
// Notice the extra length check in case we are parsing the shorter
// central data field (for both access and create timestamps).
if ((($flags & self::ACCESS_TIME_BIT) === self::ACCESS_TIME_BIT) && $offset + 4 <= $length) {
$accessTime = unpack('V', substr($buffer, $offset, 4))[1];
$offset += 4;
}
if ((($flags & self::CREATE_TIME_BIT) === self::CREATE_TIME_BIT) && $offset + 4 <= $length) {
$createTime = unpack('V', substr($buffer, $offset, 4))[1];
}
return new self($flags, $modifyTime, $accessTime, $createTime);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @return ExtendedTimestampExtraField
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData()
{
$data = '';
if (($this->flags & self::MODIFY_TIME_BIT) === self::MODIFY_TIME_BIT && $this->modifyTime !== null) {
$data .= pack('V', $this->modifyTime);
}
if (($this->flags & self::ACCESS_TIME_BIT) === self::ACCESS_TIME_BIT && $this->accessTime !== null) {
$data .= pack('V', $this->accessTime);
}
if (($this->flags & self::CREATE_TIME_BIT) === self::CREATE_TIME_BIT && $this->createTime !== null) {
$data .= pack('V', $this->createTime);
}
return pack('C', $this->flags) . $data;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* Note: even if bit1 and bit2 are set, the Central data will still
* not contain access/create fields: only local data ever holds those!
*
* @return string the data
*/
public function packCentralDirData()
{
$cdLength = 1 + ($this->modifyTime !== null ? 4 : 0);
return substr($this->packLocalFileData(), 0, $cdLength);
}
/**
* Gets flags byte.
*
* The flags byte tells us which of the three datestamp fields are
* present in the data:
* bit0 - modify time
* bit1 - access time
* bit2 - create time
*
* Only first 3 bits of flags are used according to the
* latest version of the spec (December 2012).
*
* @return int flags byte indicating which of the
* three datestamp fields are present
*/
public function getFlags()
{
return $this->flags;
}
/**
* Returns the modify time (seconds since epoch) of this zip entry,
* or null if no such timestamp exists in the zip entry.
*
* @return int|null modify time (seconds since epoch) or null
*/
public function getModifyTime()
{
return $this->modifyTime;
}
/**
* Returns the access time (seconds since epoch) of this zip entry,
* or null if no such timestamp exists in the zip entry.
*
* @return int|null access time (seconds since epoch) or null
*/
public function getAccessTime()
{
return $this->accessTime;
}
/**
* Returns the create time (seconds since epoch) of this zip entry,
* or null if no such timestamp exists in the zip entry.
*
* Note: modern linux file systems (e.g., ext2)
* do not appear to store a "create time" value, and so
* it's usually omitted altogether in the zip extra
* field. Perhaps other unix systems track this.
*
* @return int|null create time (seconds since epoch) or null
*/
public function getCreateTime()
{
return $this->createTime;
}
/**
* Returns the modify time as a \DateTimeInterface
* of this zip entry, or null if no such timestamp exists in the zip entry.
* The milliseconds are always zeroed out, since the underlying data
* offers only per-second precision.
*
* @return \DateTimeInterface|null modify time as \DateTimeInterface or null
*/
public function getModifyDateTime()
{
return self::timestampToDateTime($this->modifyTime);
}
/**
* Returns the access time as a \DateTimeInterface
* of this zip entry, or null if no such timestamp exists in the zip entry.
* The milliseconds are always zeroed out, since the underlying data
* offers only per-second precision.
*
* @return \DateTimeInterface|null access time as \DateTimeInterface or null
*/
public function getAccessDateTime()
{
return self::timestampToDateTime($this->accessTime);
}
/**
* Returns the create time as a a \DateTimeInterface
* of this zip entry, or null if no such timestamp exists in the zip entry.
* The milliseconds are always zeroed out, since the underlying data
* offers only per-second precision.
*
* Note: modern linux file systems (e.g., ext2)
* do not appear to store a "create time" value, and so
* it's usually omitted altogether in the zip extra
* field. Perhaps other unix systems track $this->.
*
* @return \DateTimeInterface|null create time as \DateTimeInterface or null
*/
public function getCreateDateTime()
{
return self::timestampToDateTime($this->createTime);
}
/**
* Sets the modify time (seconds since epoch) of this zip entry
* using a integer.
*
* @param int|null $unixTime unix time of the modify time (seconds per epoch) or null
*/
public function setModifyTime($unixTime)
{
$this->modifyTime = $unixTime;
$this->updateFlags();
}
private function updateFlags()
{
$flags = 0;
if ($this->modifyTime !== null) {
$flags |= self::MODIFY_TIME_BIT;
}
if ($this->accessTime !== null) {
$flags |= self::ACCESS_TIME_BIT;
}
if ($this->createTime !== null) {
$flags |= self::CREATE_TIME_BIT;
}
$this->flags = $flags;
}
/**
* Sets the access time (seconds since epoch) of this zip entry
* using a integer.
*
* @param int|null $unixTime Unix time of the access time (seconds per epoch) or null
*/
public function setAccessTime($unixTime)
{
$this->accessTime = $unixTime;
$this->updateFlags();
}
/**
* Sets the create time (seconds since epoch) of this zip entry
* using a integer.
*
* @param int|null $unixTime Unix time of the create time (seconds per epoch) or null
*/
public function setCreateTime($unixTime)
{
$this->createTime = $unixTime;
$this->updateFlags();
}
/**
* @param int|null $timestamp
*
* @return \DateTimeInterface|null
*/
private static function timestampToDateTime($timestamp)
{
try {
return $timestamp !== null ? new \DateTimeImmutable('@' . $timestamp) : null;
} catch (\Exception $e) {
return null;
}
}
/**
* @return string
*/
public function __toString()
{
$args = [self::HEADER_ID];
$format = '0x%04x ExtendedTimestamp:';
if ($this->modifyTime !== null) {
$format .= ' Modify:[%s]';
$args[] = date(\DATE_W3C, $this->modifyTime);
}
if ($this->accessTime !== null) {
$format .= ' Access:[%s]';
$args[] = date(\DATE_W3C, $this->accessTime);
}
if ($this->createTime !== null) {
$format .= ' Create:[%s]';
$args[] = date(\DATE_W3C, $this->createTime);
}
return vsprintf($format, $args);
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipContainer;
use PhpZip\Model\ZipEntry;
/**
* Jar Marker Extra Field.
* An executable Java program can be packaged in a JAR file with all the libraries it uses.
* Executable JAR files can easily be distinguished from the files packed in the JAR file
* by the extra field in the first file, which is hexadecimal in the 0xCAFE bytes series.
* If this extra field is added as the very first extra field of
* the archive, Solaris will consider it an executable jar file.
*
* @license MIT
*/
class JarMarkerExtraField implements ZipExtraField
{
/** @var int Header id. */
const HEADER_ID = 0xCAFE;
/**
* @param ZipContainer $container
*/
public static function setJarMarker(ZipContainer $container)
{
$zipEntries = $container->getEntries();
if (!empty($zipEntries)) {
foreach ($zipEntries as $zipEntry) {
$zipEntry->removeExtraField(self::HEADER_ID);
}
// set jar execute bit
reset($zipEntries);
$zipEntry = current($zipEntries);
$zipEntry->getCdExtraFields()[] = new self();
}
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return self::HEADER_ID;
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData()
{
return '';
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData()
{
return '';
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException on error
*
* @return JarMarkerExtraField
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
{
if (!empty($buffer)) {
throw new ZipException("JarMarker doesn't expect any data");
}
return new self();
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException on error
*
* @return JarMarkerExtraField
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* @return string
*/
public function __toString()
{
return sprintf('0x%04x Jar Marker', self::HEADER_ID);
}
}

View File

@@ -0,0 +1,237 @@
<?php
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* Info-ZIP New Unix Extra Field:
* ====================================.
*
* Currently stores Unix UIDs/GIDs up to 32 bits.
* (Last Revision 20080509)
*
* Value Size Description
* ----- ---- -----------
* (UnixN) 0x7875 Short tag for this extra block type ("ux")
* TSize Short total data size for this block
* Version 1 byte version of this extra field, currently 1
* UIDSize 1 byte Size of UID field
* UID Variable UID for this entry
* GIDSize 1 byte Size of GID field
* GID Variable GID for this entry
*
* Currently Version is set to the number 1. If there is a need
* to change this field, the version will be incremented. Changes
* may not be backward compatible so this extra field should not be
* used if the version is not recognized.
*
* UIDSize is the size of the UID field in bytes. This size should
* match the size of the UID field on the target OS.
*
* UID is the UID for this entry in standard little endian format.
*
* GIDSize is the size of the GID field in bytes. This size should
* match the size of the GID field on the target OS.
*
* GID is the GID for this entry in standard little endian format.
*
* If both the old 16-bit Unix extra field (tag 0x7855, Info-ZIP Unix)
* and this extra field are present, the values in this extra field
* supercede the values in that extra field.
*/
class NewUnixExtraField implements ZipExtraField
{
/** @var int header id */
const HEADER_ID = 0x7875;
/** ID of the first non-root user created on a unix system. */
const USER_GID_PID = 1000;
/** @var int version of this extra field, currently 1 */
private $version = 1;
/** @var int User id */
private $uid;
/** @var int Group id */
private $gid;
/**
* NewUnixExtraField constructor.
*
* @param int $version
* @param int $uid
* @param int $gid
*/
public function __construct($version = 1, $uid = self::USER_GID_PID, $gid = self::USER_GID_PID)
{
$this->version = (int) $version;
$this->uid = (int) $uid;
$this->gid = (int) $gid;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException
*
* @return NewUnixExtraField
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
{
$length = \strlen($buffer);
if ($length < 3) {
throw new ZipException(sprintf('X7875_NewUnix length is too short, only %s bytes', $length));
}
$offset = 0;
$data = unpack('Cversion/CuidSize', $buffer);
$offset += 2;
$uidSize = $data['uidSize'];
$gid = self::readSizeIntegerLE(substr($buffer, $offset, $uidSize), $uidSize);
$offset += $uidSize;
$gidSize = unpack('C', $buffer[$offset])[1];
$offset++;
$uid = self::readSizeIntegerLE(substr($buffer, $offset, $gidSize), $gidSize);
return new self($data['version'], $gid, $uid);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException
*
* @return NewUnixExtraField
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData()
{
return pack(
'CCVCV',
$this->version,
4, // UIDSize
$this->uid,
4, // GIDSize
$this->gid
);
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData()
{
return $this->packLocalFileData();
}
/**
* @param string $data
* @param int $size
*
* @throws ZipException
*
* @return int
*/
private static function readSizeIntegerLE($data, $size)
{
$format = [
1 => 'C', // unsigned byte
2 => 'v', // unsigned short LE
4 => 'V', // unsigned int LE
];
if (!isset($format[$size])) {
throw new ZipException(sprintf('Invalid size bytes: %d', $size));
}
return unpack($format[$size], $data)[1];
}
/**
* @return int
*/
public function getUid()
{
return $this->uid;
}
/**
* @param int $uid
*/
public function setUid($uid)
{
$this->uid = $uid & 0xffffffff;
}
/**
* @return int
*/
public function getGid()
{
return $this->gid;
}
/**
* @param int $gid
*/
public function setGid($gid)
{
$this->gid = $gid & 0xffffffff;
}
/**
* @return int
*/
public function getVersion()
{
return $this->version;
}
/**
* @return string
*/
public function __toString()
{
return sprintf(
'0x%04x NewUnix: UID=%d GID=%d',
self::HEADER_ID,
$this->uid,
$this->gid
);
}
}

View File

@@ -0,0 +1,339 @@
<?php
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\PackUtil;
/**
* NTFS Extra Field.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
*
* @license MIT
*/
class NtfsExtraField implements ZipExtraField
{
/** @var int Header id */
const HEADER_ID = 0x000a;
/** @var int Tag ID */
const TIME_ATTR_TAG = 0x0001;
/** @var int Attribute size */
const TIME_ATTR_SIZE = 24; // 3 * 8
/**
* @var int A file time is a 64-bit value that represents the number of
* 100-nanosecond intervals that have elapsed since 12:00
* A.M. January 1, 1601 Coordinated Universal Time (UTC).
* this is the offset of Windows time 0 to Unix epoch in 100-nanosecond intervals.
*/
const EPOCH_OFFSET = -116444736000000000;
/** @var int Modify ntfs time */
private $modifyNtfsTime;
/** @var int Access ntfs time */
private $accessNtfsTime;
/** @var int Create ntfs time */
private $createNtfsTime;
/**
* @param int $modifyNtfsTime
* @param int $accessNtfsTime
* @param int $createNtfsTime
*/
public function __construct($modifyNtfsTime, $accessNtfsTime, $createNtfsTime)
{
$this->modifyNtfsTime = (int) $modifyNtfsTime;
$this->accessNtfsTime = (int) $accessNtfsTime;
$this->createNtfsTime = (int) $createNtfsTime;
}
/**
* @param \DateTimeInterface $modifyDateTime
* @param \DateTimeInterface $accessDateTime
* @param \DateTimeInterface $createNtfsTime
*
* @return NtfsExtraField
*/
public static function create(
\DateTimeInterface $modifyDateTime,
\DateTimeInterface $accessDateTime,
\DateTimeInterface $createNtfsTime
) {
return new self(
self::dateTimeToNtfsTime($modifyDateTime),
self::dateTimeToNtfsTime($accessDateTime),
self::dateTimeToNtfsTime($createNtfsTime)
);
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException
*
* @return NtfsExtraField
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
{
if (\PHP_INT_SIZE === 4) {
throw new ZipException('not supported for php-32bit');
}
$buffer = substr($buffer, 4);
$modifyTime = 0;
$accessTime = 0;
$createTime = 0;
while ($buffer || $buffer !== '') {
$unpack = unpack('vtag/vsizeAttr', $buffer);
if ($unpack['tag'] === self::TIME_ATTR_TAG && $unpack['sizeAttr'] === self::TIME_ATTR_SIZE) {
// refactoring will be needed when php 5.5 support ends
$modifyTime = PackUtil::unpackLongLE(substr($buffer, 4, 8));
$accessTime = PackUtil::unpackLongLE(substr($buffer, 12, 8));
$createTime = PackUtil::unpackLongLE(substr($buffer, 20, 8));
break;
}
$buffer = substr($buffer, 4 + $unpack['sizeAttr']);
}
return new self($modifyTime, $accessTime, $createTime);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException
*
* @return NtfsExtraField
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData()
{
$data = pack('Vvv', 0, self::TIME_ATTR_TAG, self::TIME_ATTR_SIZE);
// refactoring will be needed when php 5.5 support ends
$data .= PackUtil::packLongLE($this->modifyNtfsTime);
$data .= PackUtil::packLongLE($this->accessNtfsTime);
$data .= PackUtil::packLongLE($this->createNtfsTime);
return $data;
}
/**
* @return int
*/
public function getModifyNtfsTime()
{
return $this->modifyNtfsTime;
}
/**
* @param int $modifyNtfsTime
*/
public function setModifyNtfsTime($modifyNtfsTime)
{
$this->modifyNtfsTime = (int) $modifyNtfsTime;
}
/**
* @return int
*/
public function getAccessNtfsTime()
{
return $this->accessNtfsTime;
}
/**
* @param int $accessNtfsTime
*/
public function setAccessNtfsTime($accessNtfsTime)
{
$this->accessNtfsTime = (int) $accessNtfsTime;
}
/**
* @return int
*/
public function getCreateNtfsTime()
{
return $this->createNtfsTime;
}
/**
* @param int $createNtfsTime
*/
public function setCreateNtfsTime($createNtfsTime)
{
$this->createNtfsTime = (int) $createNtfsTime;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData()
{
return $this->packLocalFileData();
}
/**
* @return \DateTimeInterface
*/
public function getModifyDateTime()
{
return self::ntfsTimeToDateTime($this->modifyNtfsTime);
}
/**
* @param \DateTimeInterface $modifyTime
*/
public function setModifyDateTime(\DateTimeInterface $modifyTime)
{
$this->modifyNtfsTime = self::dateTimeToNtfsTime($modifyTime);
}
/**
* @return \DateTimeInterface
*/
public function getAccessDateTime()
{
return self::ntfsTimeToDateTime($this->accessNtfsTime);
}
/**
* @param \DateTimeInterface $accessTime
*/
public function setAccessDateTime(\DateTimeInterface $accessTime)
{
$this->accessNtfsTime = self::dateTimeToNtfsTime($accessTime);
}
/**
* @return \DateTimeInterface
*/
public function getCreateDateTime()
{
return self::ntfsTimeToDateTime($this->createNtfsTime);
}
/**
* @param \DateTimeInterface $createTime
*/
public function setCreateDateTime(\DateTimeInterface $createTime)
{
$this->createNtfsTime = self::dateTimeToNtfsTime($createTime);
}
/**
* @param float $timestamp Float timestamp
*
* @return int
*/
public static function timestampToNtfsTime($timestamp)
{
return (int) (((float) $timestamp * 10000000) - self::EPOCH_OFFSET);
}
/**
* @param \DateTimeInterface $dateTime
*
* @return int
*/
public static function dateTimeToNtfsTime(\DateTimeInterface $dateTime)
{
return self::timestampToNtfsTime((float) $dateTime->format('U.u'));
}
/**
* @param int $ntfsTime
*
* @return float Float unix timestamp
*/
public static function ntfsTimeToTimestamp($ntfsTime)
{
return (float) (($ntfsTime + self::EPOCH_OFFSET) / 10000000);
}
/**
* @param int $ntfsTime
*
* @return \DateTimeInterface
*/
public static function ntfsTimeToDateTime($ntfsTime)
{
$timestamp = self::ntfsTimeToTimestamp($ntfsTime);
$dateTime = \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6f', $timestamp));
if ($dateTime === false) {
throw new InvalidArgumentException('Cannot create date/time object for timestamp ' . $timestamp);
}
return $dateTime;
}
/**
* @return string
*/
public function __toString()
{
$args = [self::HEADER_ID];
$format = '0x%04x NtfsExtra:';
if ($this->modifyNtfsTime !== 0) {
$format .= ' Modify:[%s]';
$args[] = $this->getModifyDateTime()->format(\DATE_ATOM);
}
if ($this->accessNtfsTime !== 0) {
$format .= ' Access:[%s]';
$args[] = $this->getAccessDateTime()->format(\DATE_ATOM);
}
if ($this->createNtfsTime !== 0) {
$format .= ' Create:[%s]';
$args[] = $this->getCreateDateTime()->format(\DATE_ATOM);
}
return vsprintf($format, $args);
}
}

View File

@@ -0,0 +1,327 @@
<?php
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* Info-ZIP Unix Extra Field (type 1):
* ==================================.
*
* The following is the layout of the old Info-ZIP extra block for
* Unix. It has been replaced by the extended-timestamp extra block
* (0x5455) and the Unix type 2 extra block (0x7855).
* (Last Revision 19970118)
*
* Local-header version:
*
* Value Size Description
* ----- ---- -----------
* (Unix1) 0x5855 Short tag for this extra block type ("UX")
* TSize Short total data size for this block
* AcTime Long time of last access (UTC/GMT)
* ModTime Long time of last modification (UTC/GMT)
* UID Short Unix user ID (optional)
* GID Short Unix group ID (optional)
*
* Central-header version:
*
* Value Size Description
* ----- ---- -----------
* (Unix1) 0x5855 Short tag for this extra block type ("UX")
* TSize Short total data size for this block
* AcTime Long time of last access (GMT/UTC)
* ModTime Long time of last modification (GMT/UTC)
*
* The file access and modification times are in standard Unix signed-
* long format, indicating the number of seconds since 1 January 1970
* 00:00:00. The times are relative to Coordinated Universal Time
* (UTC), also sometimes referred to as Greenwich Mean Time (GMT). To
* convert to local time, the software must know the local timezone
* offset from UTC/GMT. The modification time may be used by non-Unix
* systems to support inter-timezone freshening and updating of zip
* archives.
*
* The local-header extra block may optionally contain UID and GID
* info for the file. The local-header TSize value is the only
* indication of this. Note that Unix UIDs and GIDs are usually
* specific to a particular machine, and they generally require root
* access to restore.
*
* This extra field type is obsolete, but it has been in use since
* mid-1994. Therefore future archiving software should continue to
* support it.
*/
class OldUnixExtraField implements ZipExtraField
{
/** @var int Header id */
const HEADER_ID = 0x5855;
/** @var int|null Access timestamp */
private $accessTime;
/** @var int|null Modify timestamp */
private $modifyTime;
/** @var int|null User id */
private $uid;
/** @var int|null Group id */
private $gid;
/**
* @param int|null $accessTime
* @param int|null $modifyTime
* @param int|null $uid
* @param int|null $gid
*/
public function __construct($accessTime, $modifyTime, $uid, $gid)
{
$this->accessTime = $accessTime;
$this->modifyTime = $modifyTime;
$this->uid = $uid;
$this->gid = $gid;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @return OldUnixExtraField
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
{
$length = \strlen($buffer);
$accessTime = $modifyTime = $uid = $gid = null;
if ($length >= 4) {
$accessTime = unpack('V', $buffer)[1];
}
if ($length >= 8) {
$modifyTime = unpack('V', substr($buffer, 4, 4))[1];
}
if ($length >= 10) {
$uid = unpack('v', substr($buffer, 8, 2))[1];
}
if ($length >= 12) {
$gid = unpack('v', substr($buffer, 10, 2))[1];
}
return new self($accessTime, $modifyTime, $uid, $gid);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @return OldUnixExtraField
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
{
$length = \strlen($buffer);
$accessTime = $modifyTime = null;
if ($length >= 4) {
$accessTime = unpack('V', $buffer)[1];
}
if ($length >= 8) {
$modifyTime = unpack('V', substr($buffer, 4, 4))[1];
}
return new self($accessTime, $modifyTime, null, null);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData()
{
$data = '';
if ($this->accessTime !== null) {
$data .= pack('V', $this->accessTime);
if ($this->modifyTime !== null) {
$data .= pack('V', $this->modifyTime);
if ($this->uid !== null) {
$data .= pack('v', $this->uid);
if ($this->gid !== null) {
$data .= pack('v', $this->gid);
}
}
}
}
return $data;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData()
{
$data = '';
if ($this->accessTime !== null) {
$data .= pack('V', $this->accessTime);
if ($this->modifyTime !== null) {
$data .= pack('V', $this->modifyTime);
}
}
return $data;
}
/**
* @return int|null
*/
public function getAccessTime()
{
return $this->accessTime;
}
/**
* @param int|null $accessTime
*/
public function setAccessTime($accessTime)
{
$this->accessTime = $accessTime;
}
/**
* @return \DateTimeInterface|null
*/
public function getAccessDateTime()
{
try {
return $this->accessTime === null ? null :
new \DateTimeImmutable('@' . $this->accessTime);
} catch (\Exception $e) {
return null;
}
}
/**
* @return int|null
*/
public function getModifyTime()
{
return $this->modifyTime;
}
/**
* @param int|null $modifyTime
*/
public function setModifyTime($modifyTime)
{
$this->modifyTime = $modifyTime;
}
/**
* @return \DateTimeInterface|null
*/
public function getModifyDateTime()
{
try {
return $this->modifyTime === null ? null :
new \DateTimeImmutable('@' . $this->modifyTime);
} catch (\Exception $e) {
return null;
}
}
/**
* @return int|null
*/
public function getUid()
{
return $this->uid;
}
/**
* @param int|null $uid
*/
public function setUid($uid)
{
$this->uid = $uid;
}
/**
* @return int|null
*/
public function getGid()
{
return $this->gid;
}
/**
* @param int|null $gid
*/
public function setGid($gid)
{
$this->gid = $gid;
}
/**
* @return string
*/
public function __toString()
{
$args = [self::HEADER_ID];
$format = '0x%04x OldUnix:';
if (($modifyTime = $this->getModifyDateTime()) !== null) {
$format .= ' Modify:[%s]';
$args[] = $modifyTime->format(\DATE_ATOM);
}
if (($accessTime = $this->getAccessDateTime()) !== null) {
$format .= ' Access:[%s]';
$args[] = $accessTime->format(\DATE_ATOM);
}
if ($this->uid !== null) {
$format .= ' UID=%d';
$args[] = $this->uid;
}
if ($this->gid !== null) {
$format .= ' GID=%d';
$args[] = $this->gid;
}
return vsprintf($format, $args);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace PhpZip\Model\Extra\Fields;
/**
* Info-ZIP Unicode Comment Extra Field (0x6375):.
*
* Stores the UTF-8 version of the file comment as stored in the
* central directory header. (Last Revision 20070912)
*
* Value Size Description
* ----- ---- -----------
* (UCom) 0x6375 Short tag for this extra block type ("uc")
* TSize Short total data size for this block
* Version 1 byte version of this extra field, currently 1
* ComCRC32 4 bytes Comment Field CRC32 Checksum
* UnicodeCom Variable UTF-8 version of the entry comment
*
* Currently Version is set to the number 1. If there is a need
* to change this field, the version will be incremented. Changes
* may not be backward compatible so this extra field should not be
* used if the version is not recognized.
*
* The ComCRC32 is the standard zip CRC32 checksum of the File Comment
* field in the central directory header. This is used to verify that
* the comment field has not changed since the Unicode Comment extra field
* was created. This can happen if a utility changes the File Comment
* field but does not update the UTF-8 Comment extra field. If the CRC
* check fails, this Unicode Comment extra field should be ignored and
* the File Comment field in the header should be used instead.
*
* The UnicodeCom field is the UTF-8 version of the File Comment field
* in the header. As UnicodeCom is defined to be UTF-8, no UTF-8 byte
* order mark (BOM) is used. The length of this field is determined by
* subtracting the size of the previous fields from TSize. If both the
* File Name and Comment fields are UTF-8, the new General Purpose Bit
* Flag, bit 11 (Language encoding flag (EFS)), can be used to indicate
* both the header File Name and Comment fields are UTF-8 and, in this
* case, the Unicode Path and Unicode Comment extra fields are not
* needed and should not be created. Note that, for backward
* compatibility, bit 11 should only be used if the native character set
* of the paths and comments being zipped up are already in UTF-8. It is
* expected that the same file comment storage method, either general
* purpose bit 11 or extra fields, be used in both the Local and Central
* Directory Header for a file.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT section 4.6.8
*/
class UnicodeCommentExtraField extends AbstractUnicodeExtraField
{
const HEADER_ID = 0x6375;
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return self::HEADER_ID;
}
/**
* @return string
*/
public function __toString()
{
return sprintf(
'0x%04x UnicodeComment: "%s"',
self::HEADER_ID,
$this->getUnicodeValue()
);
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace PhpZip\Model\Extra\Fields;
/**
* Info-ZIP Unicode Path Extra Field (0x7075):
* ==========================================.
*
* Stores the UTF-8 version of the file name field as stored in the
* local header and central directory header. (Last Revision 20070912)
*
* Value Size Description
* ----- ---- -----------
* (UPath) 0x7075 Short tag for this extra block type ("up")
* TSize Short total data size for this block
* Version 1 byte version of this extra field, currently 1
* NameCRC32 4 bytes File Name Field CRC32 Checksum
* UnicodeName Variable UTF-8 version of the entry File Name
*
* Currently Version is set to the number 1. If there is a need
* to change this field, the version will be incremented. Changes
* may not be backward compatible so this extra field should not be
* used if the version is not recognized.
*
* The NameCRC32 is the standard zip CRC32 checksum of the File Name
* field in the header. This is used to verify that the header
* File Name field has not changed since the Unicode Path extra field
* was created. This can happen if a utility renames the File Name but
* does not update the UTF-8 path extra field. If the CRC check fails,
* this UTF-8 Path Extra Field should be ignored and the File Name field
* in the header should be used instead.
*
* The UnicodeName is the UTF-8 version of the contents of the File Name
* field in the header. As UnicodeName is defined to be UTF-8, no UTF-8
* byte order mark (BOM) is used. The length of this field is determined
* by subtracting the size of the previous fields from TSize. If both
* the File Name and Comment fields are UTF-8, the new General Purpose
* Bit Flag, bit 11 (Language encoding flag (EFS)), can be used to
* indicate that both the header File Name and Comment fields are UTF-8
* and, in this case, the Unicode Path and Unicode Comment extra fields
* are not needed and should not be created. Note that, for backward
* compatibility, bit 11 should only be used if the native character set
* of the paths and comments being zipped up are already in UTF-8. It is
* expected that the same file name storage method, either general
* purpose bit 11 or extra fields, be used in both the Local and Central
* Directory Header for a file.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT section 4.6.9
*/
class UnicodePathExtraField extends AbstractUnicodeExtraField
{
const HEADER_ID = 0x7075;
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return self::HEADER_ID;
}
/**
* @return string
*/
public function __toString()
{
return sprintf(
'0x%04x UnicodePath: "%s"',
self::HEADER_ID,
$this->getUnicodeValue()
);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Exception\RuntimeException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* Simple placeholder for all those extra fields we don't want to deal with.
*/
class UnrecognizedExtraField implements ZipExtraField
{
/** @var int */
private $headerId;
/** @var string extra field data without Header-ID or length specifier */
private $data;
/**
* UnrecognizedExtraField constructor.
*
* @param int $headerId
* @param string $data
*/
public function __construct($headerId, $data)
{
$this->headerId = (int) $headerId;
$this->data = (string) $data;
}
/**
* @param int $headerId
*/
public function setHeaderId($headerId)
{
$this->headerId = $headerId;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return $this->headerId;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
{
throw new RuntimeException('Unsupport parse');
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
{
throw new RuntimeException('Unsupport parse');
}
/**
* {@inheritdoc}
*/
public function packLocalFileData()
{
return $this->data;
}
/**
* {@inheritdoc}
*/
public function packCentralDirData()
{
return $this->data;
}
/**
* @return string
*/
public function getData()
{
return $this->data;
}
/**
* @param string $data
*/
public function setData($data)
{
$this->data = (string) $data;
}
/**
* @return string
*/
public function __toString()
{
$args = [$this->headerId, $this->data];
$format = '0x%04x Unrecognized Extra Field: "%s"';
return vsprintf($format, $args);
}
}

View File

@@ -0,0 +1,387 @@
<?php
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Constants\ZipCompressionMethod;
use PhpZip\Constants\ZipEncryptionMethod;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipUnsupportMethodException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
/**
* WinZip AES Extra Field.
*
* @see http://www.winzip.com/win/en/aes_tips.htm AES Coding Tips for Developers
*/
class WinZipAesExtraField implements ZipExtraField
{
/** @var int Header id */
const HEADER_ID = 0x9901;
/**
* @var int Data size (currently 7, but subject to possible increase
* in the future)
*/
const DATA_SIZE = 7;
/**
* @var int The vendor ID field should always be set to the two ASCII
* characters "AE"
*/
const VENDOR_ID = 0x4541; // 'A' | ('E' << 8)
/**
* @var int Entries of this type do include the standard ZIP CRC-32 value.
* For use with {@see WinZipAesExtraField::setVendorVersion()}.
*/
const VERSION_AE1 = 1;
/**
* @var int Entries of this type do not include the standard ZIP CRC-32 value.
* For use with {@see WinZipAesExtraField::setVendorVersion().
*/
const VERSION_AE2 = 2;
/** @var int integer mode value indicating AES encryption 128-bit strength */
const KEY_STRENGTH_128BIT = 0x01;
/** @var int integer mode value indicating AES encryption 192-bit strength */
const KEY_STRENGTH_192BIT = 0x02;
/** @var int integer mode value indicating AES encryption 256-bit strength */
const KEY_STRENGTH_256BIT = 0x03;
/** @var int[] */
private static $allowVendorVersions = [
self::VERSION_AE1,
self::VERSION_AE2,
];
/** @var array<int, int> */
private static $encryptionStrengths = [
self::KEY_STRENGTH_128BIT => 128,
self::KEY_STRENGTH_192BIT => 192,
self::KEY_STRENGTH_256BIT => 256,
];
/** @var array<int, int> */
private static $MAP_KEY_STRENGTH_METHODS = [
self::KEY_STRENGTH_128BIT => ZipEncryptionMethod::WINZIP_AES_128,
self::KEY_STRENGTH_192BIT => ZipEncryptionMethod::WINZIP_AES_192,
self::KEY_STRENGTH_256BIT => ZipEncryptionMethod::WINZIP_AES_256,
];
/** @var int Integer version number specific to the zip vendor */
private $vendorVersion = self::VERSION_AE1;
/** @var int Integer mode value indicating AES encryption strength */
private $keyStrength = self::KEY_STRENGTH_256BIT;
/** @var int The actual compression method used to compress the file */
private $compressionMethod;
/**
* @param int $vendorVersion Integer version number specific to the zip vendor
* @param int $keyStrength Integer mode value indicating AES encryption strength
* @param int $compressionMethod The actual compression method used to compress the file
*
* @throws ZipUnsupportMethodException
*/
public function __construct($vendorVersion, $keyStrength, $compressionMethod)
{
$this->setVendorVersion($vendorVersion);
$this->setKeyStrength($keyStrength);
$this->setCompressionMethod($compressionMethod);
}
/**
* @param ZipEntry $entry
*
* @throws ZipUnsupportMethodException
*
* @return WinZipAesExtraField
*/
public static function create(ZipEntry $entry)
{
$keyStrength = array_search($entry->getEncryptionMethod(), self::$MAP_KEY_STRENGTH_METHODS, true);
if ($keyStrength === false) {
throw new InvalidArgumentException('Not support encryption method ' . $entry->getEncryptionMethod());
}
// WinZip 11 will continue to use AE-2, with no CRC, for very small files
// of less than 20 bytes. It will also use AE-2 for files compressed in
// BZIP2 format, because this format has internal integrity checks
// equivalent to a CRC check built in.
//
// https://www.winzip.com/win/en/aes_info.html
$vendorVersion = (
$entry->getUncompressedSize() < 20 ||
$entry->getCompressionMethod() === ZipCompressionMethod::BZIP2
) ?
self::VERSION_AE2 :
self::VERSION_AE1;
$field = new self($vendorVersion, $keyStrength, $entry->getCompressionMethod());
$entry->getLocalExtraFields()->add($field);
$entry->getCdExtraFields()->add($field);
return $field;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException on error
*
* @return WinZipAesExtraField
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
{
$size = \strlen($buffer);
if ($size !== self::DATA_SIZE) {
throw new ZipException(
sprintf(
'WinZip AES Extra data invalid size: %d. Must be %d',
$size,
self::DATA_SIZE
)
);
}
$data = unpack('vvendorVersion/vvendorId/ckeyStrength/vcompressionMethod', $buffer);
if ($data['vendorId'] !== self::VENDOR_ID) {
throw new ZipException(
sprintf(
'Vendor id invalid: %d. Must be %d',
$data['vendorId'],
self::VENDOR_ID
)
);
}
return new self(
$data['vendorVersion'],
$data['keyStrength'],
$data['compressionMethod']
);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException
*
* @return WinZipAesExtraField
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
{
return self::unpackLocalFileData($buffer, $entry);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData()
{
return pack(
'vvcv',
$this->vendorVersion,
self::VENDOR_ID,
$this->keyStrength,
$this->compressionMethod
);
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData()
{
return $this->packLocalFileData();
}
/**
* Returns the vendor version.
*
* @return int
*
* @see WinZipAesExtraField::VERSION_AE2
* @see WinZipAesExtraField::VERSION_AE1
*/
public function getVendorVersion()
{
return $this->vendorVersion;
}
/**
* Sets the vendor version.
*
* @param int $vendorVersion the vendor version
*
* @see WinZipAesExtraField::VERSION_AE2
* @see WinZipAesExtraField::VERSION_AE1
*/
public function setVendorVersion($vendorVersion)
{
$vendorVersion = (int) $vendorVersion;
if (!\in_array($vendorVersion, self::$allowVendorVersions, true)) {
throw new InvalidArgumentException(
sprintf(
'Unsupport WinZip AES vendor version: %d',
$vendorVersion
)
);
}
$this->vendorVersion = $vendorVersion;
}
/**
* Returns vendor id.
*
* @return int
*/
public function getVendorId()
{
return self::VENDOR_ID;
}
/**
* @return int
*/
public function getKeyStrength()
{
return $this->keyStrength;
}
/**
* Set key strength.
*
* @param int $keyStrength
*/
public function setKeyStrength($keyStrength)
{
$keyStrength = (int) $keyStrength;
if (!isset(self::$encryptionStrengths[$keyStrength])) {
throw new InvalidArgumentException(
sprintf(
'Key strength %d not support value. Allow values: %s',
$keyStrength,
implode(', ', array_keys(self::$encryptionStrengths))
)
);
}
$this->keyStrength = $keyStrength;
}
/**
* @return int
*/
public function getCompressionMethod()
{
return $this->compressionMethod;
}
/**
* @param int $compressionMethod
*
* @throws ZipUnsupportMethodException
*/
public function setCompressionMethod($compressionMethod)
{
$compressionMethod = (int) $compressionMethod;
ZipCompressionMethod::checkSupport($compressionMethod);
$this->compressionMethod = $compressionMethod;
}
/**
* @return int
*/
public function getEncryptionStrength()
{
return self::$encryptionStrengths[$this->keyStrength];
}
/**
* @return int
*/
public function getEncryptionMethod()
{
$keyStrength = $this->getKeyStrength();
if (!isset(self::$MAP_KEY_STRENGTH_METHODS[$keyStrength])) {
throw new InvalidArgumentException('Invalid encryption method');
}
return self::$MAP_KEY_STRENGTH_METHODS[$keyStrength];
}
/**
* @return bool
*/
public function isV1()
{
return $this->vendorVersion === self::VERSION_AE1;
}
/**
* @return bool
*/
public function isV2()
{
return $this->vendorVersion === self::VERSION_AE2;
}
/**
* @return int
*/
public function getSaltSize()
{
return (int) ($this->getEncryptionStrength() / 8 / 2);
}
/**
* @return string
*/
public function __toString()
{
return sprintf(
'0x%04x WINZIP AES: VendorVersion=%d KeyStrength=0x%02x CompressionMethod=%s',
__CLASS__,
$this->vendorVersion,
$this->keyStrength,
$this->compressionMethod
);
}
}

View File

@@ -0,0 +1,311 @@
<?php
namespace PhpZip\Model\Extra\Fields;
use PhpZip\Constants\ZipConstants;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\PackUtil;
/**
* ZIP64 Extra Field.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
*/
class Zip64ExtraField implements ZipExtraField
{
/** @var int The Header ID for a ZIP64 Extended Information Extra Field. */
const HEADER_ID = 0x0001;
/** @var int|null */
private $uncompressedSize;
/** @var int|null */
private $compressedSize;
/** @var int|null */
private $localHeaderOffset;
/** @var int|null */
private $diskStart;
/**
* Zip64ExtraField constructor.
*
* @param int|null $uncompressedSize
* @param int|null $compressedSize
* @param int|null $localHeaderOffset
* @param int|null $diskStart
*/
public function __construct(
$uncompressedSize = null,
$compressedSize = null,
$localHeaderOffset = null,
$diskStart = null
) {
$this->uncompressedSize = $uncompressedSize;
$this->compressedSize = $compressedSize;
$this->localHeaderOffset = $localHeaderOffset;
$this->diskStart = $diskStart;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId()
{
return self::HEADER_ID;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException on error
*
* @return Zip64ExtraField
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
{
$length = \strlen($buffer);
if ($length === 0) {
// no local file data at all, may happen if an archive
// only holds a ZIP64 extended information extra field
// inside the central directory but not inside the local
// file header
return new self();
}
if ($length < 16) {
throw new ZipException(
'Zip64 extended information must contain both size values in the local file header.'
);
}
$uncompressedSize = PackUtil::unpackLongLE(substr($buffer, 0, 8));
$compressedSize = PackUtil::unpackLongLE(substr($buffer, 8, 8));
return new self($uncompressedSize, $compressedSize);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @throws ZipException
*
* @return Zip64ExtraField
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
{
if ($entry === null) {
throw new RuntimeException('zipEntry is null');
}
$length = \strlen($buffer);
$remaining = $length;
$uncompressedSize = null;
$compressedSize = null;
$localHeaderOffset = null;
$diskStart = null;
if ($entry->getUncompressedSize() === ZipConstants::ZIP64_MAGIC) {
if ($remaining < 8) {
throw new ZipException('ZIP64 extension corrupt (no uncompressed size).');
}
$uncompressedSize = PackUtil::unpackLongLE(substr($buffer, $length - $remaining, 8));
$remaining -= 8;
}
if ($entry->getCompressedSize() === ZipConstants::ZIP64_MAGIC) {
if ($remaining < 8) {
throw new ZipException('ZIP64 extension corrupt (no compressed size).');
}
$compressedSize = PackUtil::unpackLongLE(substr($buffer, $length - $remaining, 8));
$remaining -= 8;
}
if ($entry->getLocalHeaderOffset() === ZipConstants::ZIP64_MAGIC) {
if ($remaining < 8) {
throw new ZipException('ZIP64 extension corrupt (no relative local header offset).');
}
$localHeaderOffset = PackUtil::unpackLongLE(substr($buffer, $length - $remaining, 8));
$remaining -= 8;
}
if ($remaining === 4) {
$diskStart = unpack('V', substr($buffer, $length - $remaining, 4))[1];
}
return new self($uncompressedSize, $compressedSize, $localHeaderOffset, $diskStart);
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData()
{
if ($this->uncompressedSize !== null || $this->compressedSize !== null) {
if ($this->uncompressedSize === null || $this->compressedSize === null) {
throw new \InvalidArgumentException(
'Zip64 extended information must contain both size values in the local file header.'
);
}
return $this->packSizes();
}
return '';
}
/**
* @return string
*/
private function packSizes()
{
$data = '';
if ($this->uncompressedSize !== null) {
$data .= PackUtil::packLongLE($this->uncompressedSize);
}
if ($this->compressedSize !== null) {
$data .= PackUtil::packLongLE($this->compressedSize);
}
return $data;
}
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData()
{
$data = $this->packSizes();
if ($this->localHeaderOffset !== null) {
$data .= PackUtil::packLongLE($this->localHeaderOffset);
}
if ($this->diskStart !== null) {
$data .= pack('V', $this->diskStart);
}
return $data;
}
/**
* @return int|null
*/
public function getUncompressedSize()
{
return $this->uncompressedSize;
}
/**
* @param int|null $uncompressedSize
*/
public function setUncompressedSize($uncompressedSize)
{
$this->uncompressedSize = $uncompressedSize;
}
/**
* @return int|null
*/
public function getCompressedSize()
{
return $this->compressedSize;
}
/**
* @param int|null $compressedSize
*/
public function setCompressedSize($compressedSize)
{
$this->compressedSize = $compressedSize;
}
/**
* @return int|null
*/
public function getLocalHeaderOffset()
{
return $this->localHeaderOffset;
}
/**
* @param int|null $localHeaderOffset
*/
public function setLocalHeaderOffset($localHeaderOffset)
{
$this->localHeaderOffset = $localHeaderOffset;
}
/**
* @return int|null
*/
public function getDiskStart()
{
return $this->diskStart;
}
/**
* @param int|null $diskStart
*/
public function setDiskStart($diskStart)
{
$this->diskStart = $diskStart;
}
/**
* @return string
*/
public function __toString()
{
$args = [self::HEADER_ID];
$format = '0x%04x ZIP64: ';
$formats = [];
if ($this->uncompressedSize !== null) {
$formats[] = 'SIZE=%d';
$args[] = $this->uncompressedSize;
}
if ($this->compressedSize !== null) {
$formats[] = 'COMP_SIZE=%d';
$args[] = $this->compressedSize;
}
if ($this->localHeaderOffset !== null) {
$formats[] = 'OFFSET=%d';
$args[] = $this->localHeaderOffset;
}
if ($this->diskStart !== null) {
$formats[] = 'DISK_START=%d';
$args[] = $this->diskStart;
}
$format .= implode(' ', $formats);
return vsprintf($format, $args);
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace PhpZip\Model\Extra;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Model\Extra\Fields\ApkAlignmentExtraField;
use PhpZip\Model\Extra\Fields\AsiExtraField;
use PhpZip\Model\Extra\Fields\ExtendedTimestampExtraField;
use PhpZip\Model\Extra\Fields\JarMarkerExtraField;
use PhpZip\Model\Extra\Fields\NewUnixExtraField;
use PhpZip\Model\Extra\Fields\NtfsExtraField;
use PhpZip\Model\Extra\Fields\OldUnixExtraField;
use PhpZip\Model\Extra\Fields\UnicodeCommentExtraField;
use PhpZip\Model\Extra\Fields\UnicodePathExtraField;
use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
use PhpZip\Model\Extra\Fields\Zip64ExtraField;
/**
* Class ZipExtraManager.
*/
final class ZipExtraDriver
{
/**
* @var array<int, string>
* @psalm-var array<int, class-string<\PhpZip\Model\Extra\ZipExtraField>>
*/
private static $implementations = [
ApkAlignmentExtraField::HEADER_ID => ApkAlignmentExtraField::class,
AsiExtraField::HEADER_ID => AsiExtraField::class,
ExtendedTimestampExtraField::HEADER_ID => ExtendedTimestampExtraField::class,
JarMarkerExtraField::HEADER_ID => JarMarkerExtraField::class,
NewUnixExtraField::HEADER_ID => NewUnixExtraField::class,
NtfsExtraField::HEADER_ID => NtfsExtraField::class,
OldUnixExtraField::HEADER_ID => OldUnixExtraField::class,
UnicodeCommentExtraField::HEADER_ID => UnicodeCommentExtraField::class,
UnicodePathExtraField::HEADER_ID => UnicodePathExtraField::class,
WinZipAesExtraField::HEADER_ID => WinZipAesExtraField::class,
Zip64ExtraField::HEADER_ID => Zip64ExtraField::class,
];
private function __construct()
{
}
/**
* @param string|ZipExtraField $extraField ZipExtraField object or class name
*/
public static function register($extraField)
{
if (!is_a($extraField, ZipExtraField::class, true)) {
throw new InvalidArgumentException(
sprintf(
'$extraField "%s" is not implements interface %s',
(string) $extraField,
ZipExtraField::class
)
);
}
self::$implementations[\call_user_func([$extraField, 'getHeaderId'])] = $extraField;
}
/**
* @param int|string|ZipExtraField $extraType ZipExtraField object or class name or extra header id
*
* @return bool
*/
public static function unregister($extraType)
{
$headerId = null;
if (\is_int($extraType)) {
$headerId = $extraType;
} elseif (is_a($extraType, ZipExtraField::class, true)) {
$headerId = \call_user_func([$extraType, 'getHeaderId']);
} else {
return false;
}
if (isset(self::$implementations[$headerId])) {
unset(self::$implementations[$headerId]);
return true;
}
return false;
}
/**
* @param int $headerId
*
* @return string|null
*/
public static function getClassNameOrNull($headerId)
{
$headerId = (int) $headerId;
if ($headerId < 0 || $headerId > 0xffff) {
throw new \InvalidArgumentException('$headerId out of range: ' . $headerId);
}
if (isset(self::$implementations[$headerId])) {
return self::$implementations[$headerId];
}
return null;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace PhpZip\Model\Extra;
use PhpZip\Model\ZipEntry;
/**
* Extra Field in a Local or Central Header of a ZIP archive.
* It defines the common properties of all Extra Fields and how to
* serialize/unserialize them to/from byte arrays.
*/
interface ZipExtraField
{
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public function getHeaderId();
/**
* Populate data from this array as if it was in local file data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @return static
*/
public static function unpackLocalFileData($buffer, ZipEntry $entry = null);
/**
* Populate data from this array as if it was in central directory data.
*
* @param string $buffer the buffer to read data from
* @param ZipEntry|null $entry
*
* @return static
*/
public static function unpackCentralDirData($buffer, ZipEntry $entry = null);
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return string the data
*/
public function packLocalFileData();
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return string the data
*/
public function packCentralDirData();
/**
* @return string
*/
public function __toString();
}

View File

@@ -0,0 +1,56 @@
<?php
namespace PhpZip\Model;
/**
* Class ImmutableZipContainer.
*/
class ImmutableZipContainer implements \Countable
{
/** @var ZipEntry[] */
protected $entries;
/** @var string|null Archive comment */
protected $archiveComment;
/**
* ZipContainer constructor.
*
* @param ZipEntry[] $entries
* @param string|null $archiveComment
*/
public function __construct(array $entries, $archiveComment)
{
$this->entries = $entries;
$this->archiveComment = $archiveComment;
}
/**
* @return ZipEntry[]
*/
public function &getEntries()
{
return $this->entries;
}
/**
* @return string|null
*/
public function getArchiveComment()
{
return $this->archiveComment;
}
/**
* Count elements of an object.
*
* @see https://php.net/manual/en/countable.count.php
*
* @return int The custom count as an integer.
* The return value is cast to an integer.
*/
public function count()
{
return \count($this->entries);
}
}

381
src/Model/ZipContainer.php Normal file
View File

@@ -0,0 +1,381 @@
<?php
namespace PhpZip\Model;
use PhpZip\Constants\ZipEncryptionMethod;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipEntryNotFoundException;
use PhpZip\Exception\ZipException;
/**
* Class ZipContainer.
*/
class ZipContainer extends ImmutableZipContainer
{
/** @var ImmutableZipContainer|null */
private $sourceContainer;
/**
* @var int|null Apk zipalign value
*
* @todo remove and use in ApkFileWriter
*/
private $zipAlign;
/**
* MutableZipContainer constructor.
*
* @param ImmutableZipContainer|null $sourceContainer
*/
public function __construct(ImmutableZipContainer $sourceContainer = null)
{
$entries = [];
$archiveComment = null;
if ($sourceContainer !== null) {
foreach ($sourceContainer->getEntries() as $entryName => $entry) {
$entries[$entryName] = clone $entry;
}
$archiveComment = $sourceContainer->getArchiveComment();
}
parent::__construct($entries, $archiveComment);
$this->sourceContainer = $sourceContainer;
}
/**
* @return ImmutableZipContainer|null
*/
public function getSourceContainer()
{
return $this->sourceContainer;
}
/**
* @param ZipEntry $entry
*/
public function addEntry(ZipEntry $entry)
{
$this->entries[$entry->getName()] = $entry;
}
/**
* @param string|ZipEntry $entry
*
* @return bool
*/
public function deleteEntry($entry)
{
$entry = $entry instanceof ZipEntry ? $entry->getName() : (string) $entry;
if (isset($this->entries[$entry])) {
unset($this->entries[$entry]);
return true;
}
return false;
}
/**
* @param string|ZipEntry $old
* @param string|ZipEntry $new
*
* @throws ZipException
*
* @return ZipEntry New zip entry
*/
public function renameEntry($old, $new)
{
$old = $old instanceof ZipEntry ? $old->getName() : (string) $old;
$new = $new instanceof ZipEntry ? $new->getName() : (string) $new;
if (isset($this->entries[$new])) {
throw new InvalidArgumentException('New entry name ' . $new . ' is exists.');
}
$entry = $this->getEntry($old);
$newEntry = $entry->rename($new);
$this->deleteEntry($entry);
$this->addEntry($newEntry);
return $newEntry;
}
/**
* @param string|ZipEntry $entryName
*
* @throws ZipEntryNotFoundException
*
* @return ZipEntry
*/
public function getEntry($entryName)
{
$entry = $this->getEntryOrNull($entryName);
if ($entry !== null) {
return $entry;
}
throw new ZipEntryNotFoundException($entryName);
}
/**
* @param string|ZipEntry $entryName
*
* @return ZipEntry|null
*/
public function getEntryOrNull($entryName)
{
$entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string) $entryName;
return isset($this->entries[$entryName]) ? $this->entries[$entryName] : null;
}
/**
* @param string|ZipEntry $entryName
*
* @return bool
*/
public function hasEntry($entryName)
{
$entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string) $entryName;
return isset($this->entries[$entryName]);
}
/**
* Delete all entries.
*/
public function deleteAll()
{
$this->entries = [];
}
/**
* Delete entries by regex pattern.
*
* @param string $regexPattern Regex pattern
*
* @return ZipEntry[] Deleted entries
*/
public function deleteByRegex($regexPattern)
{
if (empty($regexPattern)) {
throw new InvalidArgumentException('The regex pattern is not specified');
}
/** @var ZipEntry[] $found */
$found = [];
foreach ($this->entries as $entryName => $entry) {
if (preg_match($regexPattern, $entryName)) {
$found[] = $entry;
}
}
foreach ($found as $entry) {
$this->deleteEntry($entry);
}
return $found;
}
/**
* Undo all changes done in the archive.
*/
public function unchangeAll()
{
$this->entries = [];
if ($this->sourceContainer !== null) {
foreach ($this->sourceContainer->getEntries() as $entry) {
$this->entries[$entry->getName()] = clone $entry;
}
}
$this->unchangeArchiveComment();
}
/**
* Undo change archive comment.
*/
public function unchangeArchiveComment()
{
$this->archiveComment = null;
if ($this->sourceContainer !== null) {
$this->archiveComment = $this->sourceContainer->archiveComment;
}
}
/**
* Revert all changes done to an entry with the given name.
*
* @param string|ZipEntry $entry Entry name or ZipEntry
*
* @return bool
*/
public function unchangeEntry($entry)
{
$entry = $entry instanceof ZipEntry ? $entry->getName() : (string) $entry;
if (
$this->sourceContainer !== null &&
isset($this->entries[$entry], $this->sourceContainer->entries[$entry])
) {
$this->entries[$entry] = clone $this->sourceContainer->entries[$entry];
return true;
}
return false;
}
/**
* Entries sort by name.
*
* Example:
* ```php
* $zipContainer->sortByName(static function (string $nameA, string $nameB): int {
* return strcmp($nameA, $nameB);
* });
* ```
*
* @param callable $cmp
*/
public function sortByName(callable $cmp)
{
uksort($this->entries, $cmp);
}
/**
* Entries sort by entry.
*
* Example:
* ```php
* $zipContainer->sortByEntry(static function (ZipEntry $a, ZipEntry $b): int {
* return strcmp($a->getName(), $b->getName());
* });
* ```
*
* @param callable $cmp
*/
public function sortByEntry(callable $cmp)
{
uasort($this->entries, $cmp);
}
/**
* @param string|null $archiveComment
*/
public function setArchiveComment($archiveComment)
{
if ($archiveComment !== null && $archiveComment !== '') {
$archiveComment = (string) $archiveComment;
$length = \strlen($archiveComment);
if ($length > 0xffff) {
throw new InvalidArgumentException('Length comment out of range');
}
}
$this->archiveComment = $archiveComment;
}
/**
* @return ZipEntryMatcher
*/
public function matcher()
{
return new ZipEntryMatcher($this);
}
/**
* Specify a password for extracting files.
*
* @param string|null $password
*/
public function setReadPassword($password)
{
if ($this->sourceContainer !== null) {
foreach ($this->sourceContainer->entries as $entry) {
if ($entry->isEncrypted()) {
$entry->setPassword($password);
}
}
}
}
/**
* @param string $entryName
* @param string $password
*
* @throws ZipEntryNotFoundException
* @throws ZipException
*/
public function setReadPasswordEntry($entryName, $password)
{
if (!isset($this->sourceContainer->entries[$entryName])) {
throw new ZipEntryNotFoundException($entryName);
}
if ($this->sourceContainer->entries[$entryName]->isEncrypted()) {
$this->sourceContainer->entries[$entryName]->setPassword($password);
}
}
/**
* @return int|null
*/
public function getZipAlign()
{
return $this->zipAlign;
}
/**
* @param int|null $zipAlign
*/
public function setZipAlign($zipAlign)
{
$this->zipAlign = $zipAlign === null ? null : (int) $zipAlign;
}
/**
* @return bool
*/
public function isZipAlign()
{
return $this->zipAlign !== null;
}
/**
* @param string|null $writePassword
*/
public function setWritePassword($writePassword)
{
$this->matcher()->all()->setPassword($writePassword);
}
/**
* Remove password.
*/
public function removePassword()
{
$this->matcher()->all()->setPassword(null);
}
/**
* @param string|ZipEntry $entryName
*/
public function removePasswordEntry($entryName)
{
$this->matcher()->add($entryName)->setPassword(null);
}
/**
* @param int $encryptionMethod
*/
public function setEncryptionMethod($encryptionMethod = ZipEncryptionMethod::WINZIP_AES_256)
{
$this->matcher()->all()->setEncryptionMethod($encryptionMethod);
}
}

28
src/Model/ZipData.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
namespace PhpZip\Model;
use PhpZip\Exception\ZipException;
/**
* Interface ZipData.
*/
interface ZipData
{
/**
* @return string returns data as string
*/
public function getDataAsString();
/**
* @return resource returns stream data
*/
public function getDataAsStream();
/**
* @param resource $outStream
*
* @throws ZipException
*/
public function copyDataToStream($outStream);
}

1573
src/Model/ZipEntry.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,206 @@
<?php
namespace PhpZip\Model;
/**
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipEntryMatcher implements \Countable
{
/** @var ZipContainer */
protected $zipContainer;
/** @var array */
protected $matches = [];
/**
* ZipEntryMatcher constructor.
*
* @param ZipContainer $zipContainer
*/
public function __construct(ZipContainer $zipContainer)
{
$this->zipContainer = $zipContainer;
}
/**
* @param string|ZipEntry|string[]|ZipEntry[] $entries
*
* @return ZipEntryMatcher
*/
public function add($entries)
{
$entries = (array) $entries;
$entries = array_map(
static function ($entry) {
return $entry instanceof ZipEntry ? $entry->getName() : (string) $entry;
},
$entries
);
$this->matches = array_values(
array_map(
'strval',
array_unique(
array_merge(
$this->matches,
array_keys(
array_intersect_key(
$this->zipContainer->getEntries(),
array_flip($entries)
)
)
)
)
)
);
return $this;
}
/**
* @param string $regexp
*
* @return ZipEntryMatcher
*
* @noinspection PhpUnusedParameterInspection
*/
public function match($regexp)
{
array_walk(
$this->zipContainer->getEntries(),
/**
* @param ZipEntry $entry
* @param string $entryName
*/
function (ZipEntry $entry, $entryName) use ($regexp) {
if (preg_match($regexp, $entryName)) {
$this->matches[] = (string) $entryName;
}
}
);
$this->matches = array_unique($this->matches);
return $this;
}
/**
* @return ZipEntryMatcher
*/
public function all()
{
$this->matches = array_map(
'strval',
array_keys($this->zipContainer->getEntries())
);
return $this;
}
/**
* Callable function for all select entries.
*
* Callable function signature:
* function(string $entryName){}
*
* @param callable $callable
*/
public function invoke(callable $callable)
{
if (!empty($this->matches)) {
array_walk(
$this->matches,
/** @param string $entryName */
static function ($entryName) use ($callable) {
$callable($entryName);
}
);
}
}
/**
* @return array
*/
public function getMatches()
{
return $this->matches;
}
public function delete()
{
array_walk(
$this->matches,
/** @param string $entryName */
function ($entryName) {
$this->zipContainer->deleteEntry($entryName);
}
);
$this->matches = [];
}
/**
* @param string|null $password
* @param int|null $encryptionMethod
*/
public function setPassword($password, $encryptionMethod = null)
{
array_walk(
$this->matches,
/** @param string $entryName */
function ($entryName) use ($password, $encryptionMethod) {
$entry = $this->zipContainer->getEntry($entryName);
if (!$entry->isDirectory()) {
$entry->setPassword($password, $encryptionMethod);
}
}
);
}
/**
* @param int $encryptionMethod
*/
public function setEncryptionMethod($encryptionMethod)
{
array_walk(
$this->matches,
/** @param string $entryName */
function ($entryName) use ($encryptionMethod) {
$entry = $this->zipContainer->getEntry($entryName);
if (!$entry->isDirectory()) {
$entry->setEncryptionMethod($encryptionMethod);
}
}
);
}
public function disableEncryption()
{
array_walk(
$this->matches,
/** @param string $entryName */
function ($entryName) {
$entry = $this->zipContainer->getEntry($entryName);
if (!$entry->isDirectory()) {
$entry->disableEncryption();
}
}
);
}
/**
* Count elements of an object.
*
* @see http://php.net/manual/en/countable.count.php
*
* @return int the custom count as an integer
*
* @since 5.1.0
*/
public function count()
{
return \count($this->matches);
}
}

266
src/Model/ZipInfo.php Normal file
View File

@@ -0,0 +1,266 @@
<?php
namespace PhpZip\Model;
use PhpZip\Constants\ZipCompressionMethod;
use PhpZip\Constants\ZipEncryptionMethod;
use PhpZip\Constants\ZipPlatform;
use PhpZip\Util\FileAttribUtil;
use PhpZip\Util\FilesUtil;
/**
* Zip info.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*
* @deprecated Use ZipEntry
*/
class ZipInfo
{
/** @var ZipEntry */
private $entry;
/**
* ZipInfo constructor.
*
* @param ZipEntry $entry
*/
public function __construct(ZipEntry $entry)
{
$this->entry = $entry;
}
/**
* @param ZipEntry $entry
*
* @return string
*
* @deprecated Use {@see ZipPlatform::getPlatformName()}
*/
public static function getPlatformName(ZipEntry $entry)
{
return ZipPlatform::getPlatformName($entry->getExtractedOS());
}
/**
* @return string
*/
public function getName()
{
return $this->entry->getName();
}
/**
* @return bool
*/
public function isFolder()
{
return $this->entry->isDirectory();
}
/**
* @return int
*/
public function getSize()
{
return $this->entry->getUncompressedSize();
}
/**
* @return int
*/
public function getCompressedSize()
{
return $this->entry->getCompressedSize();
}
/**
* @return int
*/
public function getMtime()
{
return $this->entry->getMTime()->getTimestamp();
}
/**
* @return int|null
*/
public function getCtime()
{
$ctime = $this->entry->getCTime();
return $ctime === null ? null : $ctime->getTimestamp();
}
/**
* @return int|null
*/
public function getAtime()
{
$atime = $this->entry->getATime();
return $atime === null ? null : $atime->getTimestamp();
}
/**
* @return string
*/
public function getAttributes()
{
$externalAttributes = $this->entry->getExternalAttributes();
if ($this->entry->getCreatedOS() === ZipPlatform::OS_UNIX) {
$permission = (($externalAttributes >> 16) & 0xFFFF);
return FileAttribUtil::getUnixMode($permission);
}
return FileAttribUtil::getDosMode($externalAttributes);
}
/**
* @return bool
*/
public function isEncrypted()
{
return $this->entry->isEncrypted();
}
/**
* @return string|null
*/
public function getComment()
{
return $this->entry->getComment();
}
/**
* @return int
*/
public function getCrc()
{
return $this->entry->getCrc();
}
/**
* @return string
*
* @deprecated use \PhpZip\Model\ZipInfo::getMethodName()
*/
public function getMethod()
{
return $this->getMethodName();
}
/**
* @return string
*/
public function getMethodName()
{
return ZipCompressionMethod::getCompressionMethodName($this->entry->getCompressionMethod());
}
/**
* @return string
*/
public function getEncryptionMethodName()
{
return ZipEncryptionMethod::getEncryptionMethodName($this->entry->getEncryptionMethod());
}
/**
* @return string
*/
public function getPlatform()
{
return ZipPlatform::getPlatformName($this->entry->getExtractedOS());
}
/**
* @return int
*/
public function getVersion()
{
return $this->entry->getExtractVersion();
}
/**
* @return int|null
*/
public function getEncryptionMethod()
{
$encryptionMethod = $this->entry->getEncryptionMethod();
return $encryptionMethod === ZipEncryptionMethod::NONE ? null : $encryptionMethod;
}
/**
* @return int|null
*/
public function getCompressionLevel()
{
return $this->entry->getCompressionLevel();
}
/**
* @return int
*/
public function getCompressionMethod()
{
return $this->entry->getCompressionMethod();
}
/**
* @return array
*/
public function toArray()
{
return [
'name' => $this->getName(),
'folder' => $this->isFolder(),
'size' => $this->getSize(),
'compressed_size' => $this->getCompressedSize(),
'modified' => $this->getMtime(),
'created' => $this->getCtime(),
'accessed' => $this->getAtime(),
'attributes' => $this->getAttributes(),
'encrypted' => $this->isEncrypted(),
'encryption_method' => $this->getEncryptionMethod(),
'encryption_method_name' => $this->getEncryptionMethodName(),
'comment' => $this->getComment(),
'crc' => $this->getCrc(),
'method_name' => $this->getMethodName(),
'compression_method' => $this->getCompressionMethod(),
'platform' => $this->getPlatform(),
'version' => $this->getVersion(),
];
}
/**
* @return string
*/
public function __toString()
{
$ctime = $this->entry->getCTime();
$atime = $this->entry->getATime();
$comment = $this->getComment();
return __CLASS__ . ' {'
. 'Name="' . $this->getName() . '", '
. ($this->isFolder() ? 'Folder, ' : '')
. 'Size="' . FilesUtil::humanSize($this->getSize()) . '"'
. ', Compressed size="' . FilesUtil::humanSize($this->getCompressedSize()) . '"'
. ', Modified time="' . $this->entry->getMTime()->format(\DATE_W3C) . '", '
. ($ctime !== null ? 'Created time="' . $ctime->format(\DATE_W3C) . '", ' : '')
. ($atime !== null ? 'Accessed time="' . $atime->format(\DATE_W3C) . '", ' : '')
. ($this->isEncrypted() ? 'Encrypted, ' : '')
. ($comment !== null ? 'Comment="' . $comment . '", ' : '')
. (!empty($this->crc) ? 'Crc=0x' . dechex($this->crc) . ', ' : '')
. 'Method name="' . $this->getMethodName() . '", '
. 'Attributes="' . $this->getAttributes() . '", '
. 'Platform="' . $this->getPlatform() . '", '
. 'Version=' . $this->getVersion()
. '}';
}
}

View File

@@ -1,232 +0,0 @@
<?php
namespace PhpZip\Crypto;
use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\CryptoUtil;
/**
* Traditional PKWARE Encryption Engine.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class TraditionalPkwareEncryptionEngine
{
/**
* Encryption header size
*/
const STD_DEC_HDR_SIZE = 12;
/**
* Crc table
*
* @var array
*/
private static $CRC_TABLE = [
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
];
/**
* Encryption keys
*
* @var array
*/
private $keys = [];
/**
* @var ZipEntry
*/
private $entry;
/**
* ZipCryptoEngine constructor.
*
* @param ZipEntry $entry
*/
public function __construct(ZipEntry $entry)
{
$this->entry = $entry;
$this->initKeys($entry->getPassword());
}
/**
* Initial keys
*
* @param string $password
*/
private function initKeys($password)
{
$this->keys[0] = 305419896;
$this->keys[1] = 591751049;
$this->keys[2] = 878082192;
foreach (unpack('C*', $password) as $b) {
$this->updateKeys($b);
}
}
/**
* Update keys.
*
* @param string $charAt
*/
private function updateKeys($charAt)
{
$this->keys[0] = self::crc32($this->keys[0], $charAt);
$this->keys[1] = $this->keys[1] + ($this->keys[0] & 0xff);
$this->keys[1] = self::toInt($this->keys[1] * 134775813 + 1);
$this->keys[2] = self::toInt(self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff));
}
/**
* Cast to int
*
* @param $i
* @return int
*/
private static function toInt($i)
{
$i = (int)($i & 0xffffffff);
if ($i > 2147483647) {
return -(-$i & 0xffffffff);
} elseif ($i < -2147483648) {
return $i & -2147483648;
}
return $i;
}
/**
* Update crc.
*
* @param int $oldCrc
* @param string $charAt
* @return int
*/
private function crc32($oldCrc, $charAt)
{
return (($oldCrc >> 8) & 0xffffff) ^ self::$CRC_TABLE[($oldCrc ^ $charAt) & 0xff];
}
/**
* @param string $content
* @return string
* @throws ZipAuthenticationException
*/
public function decrypt($content)
{
$headerBytes = array_values(unpack('C*', substr($content, 0, self::STD_DEC_HDR_SIZE)));
foreach ($headerBytes as &$byte) {
$byte = ($byte ^ $this->decryptByte()) & 0xff;
$this->updateKeys($byte);
}
if ($this->entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
// compare against the file type from extended local headers
$checkByte = ($this->entry->getRawTime() >> 8) & 0xff;
} else {
// compare against the CRC otherwise
$checkByte = ($this->entry->getCrc() >> 24) & 0xff;
}
if ($headerBytes[11] !== $checkByte) {
throw new ZipAuthenticationException("Bad password for entry " . $this->entry->getName());
}
$outputContent = "";
foreach (unpack('C*', substr($content, self::STD_DEC_HDR_SIZE)) as $val) {
$val = ($val ^ $this->decryptByte()) & 0xff;
$this->updateKeys($val);
$outputContent .= pack('c', $val);
}
return $outputContent;
}
/**
* Decrypt byte.
*
* @return int
*/
private function decryptByte()
{
$temp = $this->keys[2] | 2;
return (($temp * ($temp ^ 1)) >> 8) & 0xffffff;
}
/**
* Encryption data
*
* @param string $data
* @param int $crc
* @return string
*/
public function encrypt($data, $crc)
{
$headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE);
// Initialize again since the generated bytes were encrypted.
$this->initKeys($this->entry->getPassword());
$headerBytes[self::STD_DEC_HDR_SIZE - 1] = pack('c', ($crc >> 24) & 0xff);
$headerBytes[self::STD_DEC_HDR_SIZE - 2] = pack('c', ($crc >> 16) & 0xff);
$headerBytes = $this->encryptData($headerBytes);
return $headerBytes . $this->encryptData($data);
}
/**
* @param string $content
* @return string
*/
private function encryptData($content)
{
if ($content === null) {
throw new \RuntimeException();
}
$buff = '';
foreach (unpack('C*', $content) as $val) {
$buff .= pack('c', $this->encryptByte($val));
}
return $buff;
}
/**
* @param int $byte
* @return int
*/
protected function encryptByte($byte)
{
$tempVal = $byte ^ $this->decryptByte() & 0xff;
$this->updateKeys($byte);
return $tempVal;
}
}

View File

@@ -1,239 +0,0 @@
<?php
namespace PhpZip\Crypto;
use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Exception\ZipCryptoException;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\CryptoUtil;
/**
* WinZip Aes Encryption Engine.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class WinZipAesEngine
{
/**
* The block size of the Advanced Encryption Specification (AES) Algorithm
* in bits (AES_BLOCK_SIZE_BITS).
*/
const AES_BLOCK_SIZE_BITS = 128;
const PWD_VERIFIER_BITS = 16;
/**
* The iteration count for the derived keys of the cipher, KLAC and MAC.
*/
const ITERATION_COUNT = 1000;
/**
* @var ZipEntry
*/
private $entry;
/**
* WinZipAesEngine constructor.
* @param ZipEntry $entry
*/
public function __construct(ZipEntry $entry)
{
$this->entry = $entry;
}
/**
* Decrypt from stream resource.
*
* @param resource $stream Input stream resource
* @return string
* @throws ZipAuthenticationException
* @throws ZipCryptoException
*/
public function decrypt($stream)
{
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $this->entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
if (null === $field) {
throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)");
}
$pos = ftell($stream);
// Get key strength.
$keyStrengthBits = $field->getKeyStrength();
$keyStrengthBytes = $keyStrengthBits / 8;
$salt = fread($stream, $keyStrengthBytes / 2);
$passwordVerifier = fread($stream, self::PWD_VERIFIER_BITS / 8);
$sha1Size = 20;
// Init start, end and size of encrypted data.
$endPos = $pos + $this->entry->getCompressedSize();
$start = ftell($stream);
$footerSize = $sha1Size / 2;
$end = $endPos - $footerSize;
$size = $end - $start;
if (0 > $size) {
throw new ZipCryptoException($this->entry->getName() . " (false positive WinZip AES entry is too short)");
}
// Load authentication code.
fseek($stream, $end, SEEK_SET);
$authenticationCode = fread($stream, $footerSize);
if (ftell($stream) !== $endPos) {
// This should never happen unless someone is writing to the
// end of the file concurrently!
throw new ZipCryptoException("Expected end of file after WinZip AES authentication code!");
}
$password = $this->entry->getPassword();
assert($password !== null);
assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
// WinZip 99-character limit
// @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
$password = substr($password, 0, 99);
do {
// Here comes the strange part about WinZip AES encryption:
// Its unorthodox use of the Password-Based Key Derivation
// Function 2 (PBKDF2) of PKCS #5 V2.0 alias RFC 2898.
// Yes, the password verifier is only a 16 bit value.
// So we must use the MAC for password verification, too.
$keyParam = hash_pbkdf2("sha1", $password, $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
$iv = str_repeat(chr(0), $ctrIvSize);
$key = substr($keyParam, 0, $keyStrengthBytes);
$sha1MacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
// Verify password.
} while (!$passwordVerifier === substr($keyParam, 2 * $keyStrengthBytes));
$content = stream_get_contents($stream, $size, $start);
$mac = hash_hmac('sha1', $content, $sha1MacParam, true);
if ($authenticationCode !== substr($mac, 0, 10)) {
throw new ZipAuthenticationException($this->entry->getName() . " (authenticated WinZip AES entry content has been tampered with)");
}
return self::aesCtrSegmentIntegerCounter(false, $content, $key, $iv);
}
/**
* Decryption or encryption AES-CTR with Segment Integer Count (SIC).
*
* @param bool $encrypted If true encryption else decryption
* @param string $str Data
* @param string $key Key
* @param string $iv IV
* @return string
*/
private static function aesCtrSegmentIntegerCounter($encrypted = true, $str, $key, $iv)
{
$numOfBlocks = ceil(strlen($str) / 16);
$ctrStr = '';
for ($i = 0; $i < $numOfBlocks; ++$i) {
for ($j = 0; $j < 16; ++$j) {
$n = ord($iv[$j]);
if (++$n === 0x100) {
// overflow, set this one to 0, increment next
$iv[$j] = chr(0);
} else {
// no overflow, just write incremented number back and abort
$iv[$j] = chr($n);
break;
}
}
$data = substr($str, $i * 16, 16);
$ctrStr .= $encrypted ?
self::encryptCtr($data, $key, $iv) :
self::decryptCtr($data, $key, $iv);
}
return $ctrStr;
}
/**
* Encrypt AES-CTR.
*
* @param string $data Raw data
* @param string $key Aes key
* @param string $iv Aes IV
* @return string Encrypted data
*/
private static function encryptCtr($data, $key, $iv)
{
if (extension_loaded("openssl")) {
$numBits = strlen($key) * 8;
return openssl_encrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
} elseif (extension_loaded("mcrypt")) {
return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
} else {
throw new \RuntimeException('Extension openssl or mcrypt not loaded');
}
}
/**
* Decrypt AES-CTR.
*
* @param string $data Encrypted data
* @param string $key Aes key
* @param string $iv Aes IV
* @return string Raw data
*/
private static function decryptCtr($data, $key, $iv)
{
if (extension_loaded("openssl")) {
$numBits = strlen($key) * 8;
return openssl_decrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
} elseif (extension_loaded("mcrypt")) {
return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
} else {
throw new \RuntimeException('Extension openssl or mcrypt not loaded');
}
}
/**
* Encryption string.
*
* @param string $content
* @return string
*/
public function encrypt($content)
{
// Init key strength.
$password = $this->entry->getPassword();
assert($password !== null);
// WinZip 99-character limit
// @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
$password = substr($password, 0, 99);
$keyStrengthBytes = 32;
$keyStrengthBits = $keyStrengthBytes * 8;
assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
$salt = CryptoUtil::randomBytes($keyStrengthBytes / 2);
$keyParam = hash_pbkdf2("sha1", $password, $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
$sha1HMacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
// Can you believe they "forgot" the nonce in the CTR mode IV?! :-(
$ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
$iv = str_repeat(chr(0), $ctrIvSize);
$key = substr($keyParam, 0, $keyStrengthBytes);
$content = self::aesCtrSegmentIntegerCounter(true, $content, $key, $iv);
$mac = hash_hmac('sha1', $content, $sha1HMacParam, true);
return ($salt .
substr($keyParam, 2 * $keyStrengthBytes, self::PWD_VERIFIER_BITS / 8) .
$content .
substr($mac, 0, 10)
);
}
}

View File

@@ -1,14 +0,0 @@
<?php
namespace PhpZip\Exception;
/**
* Thrown if entry not found.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
* @see \Exception
*/
class ZipNotFoundEntry extends ZipException
{
}

View File

@@ -1,14 +0,0 @@
<?php
namespace PhpZip\Exception;
/**
* Thrown if entry unsupport compression method.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
* @see \Exception
*/
class ZipUnsupportMethod extends ZipException
{
}

View File

@@ -1,98 +0,0 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
/**
* Default implementation for an Extra Field in a Local or Central Header of a
* ZIP file.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class DefaultExtraField extends ExtraField
{
/**
* @var int
*/
private static $headerId;
/**
* @var string
*/
private $data;
/**
* Constructs a new Extra Field.
*
* @param int $headerId an unsigned short integer (two bytes) indicating the
* type of the Extra Field.
* @throws ZipException
*/
public function __construct($headerId)
{
if (0x0000 > $headerId || $headerId > 0xffff) {
throw new ZipException('headerId out of range');
}
self::$headerId = $headerId;
}
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public static function getHeaderId()
{
return self::$headerId & 0xffff;
}
/**
* Returns the Data Size of this Extra Field.
* The Data Size is an unsigned short integer (two bytes)
* which indicates the length of the Data Block in bytes and does not
* include its own size in this Extra Field.
* This property may be initialized by calling ExtraField::readFrom.
*
* @return int The size of the Data Block in bytes
* or 0 if unknown.
*/
public function getDataSize()
{
return null !== $this->data ? strlen($this->data) : 0;
}
/**
* Initializes this Extra Field by deserializing a Data Block of
* size bytes $size from the resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
* @param int $size Size
* @throws ZipException
*/
public function readFrom($handle, $off, $size)
{
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size out of range');
}
if ($size > 0) {
fseek($handle, $off, SEEK_SET);
$this->data = fread($handle, $size);
}
}
/**
* @param resource $handle
* @param int $off
*/
public function writeTo($handle, $off)
{
if (null !== $this->data) {
fseek($handle, $off, SEEK_SET);
fwrite($handle, $this->data);
}
}
}

View File

@@ -1,120 +0,0 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
/**
* Abstract base class for an Extra Field in a Local or Central Header of a
* ZIP archive.
* It defines the common properties of all Extra Fields and how to
* serialize/deserialize them to/from byte arrays.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
abstract class ExtraField implements ExtraFieldHeader
{
/** The Header ID for a ZIP64 Extended Information Extra Field. */
const ZIP64_HEADER_ID = 0x0001;
/**
* @var array|null
*/
private static $registry;
/**
* A static factory method which creates a new Extra Field based on the
* given Header ID.
* The returned Extra Field still requires proper initialization, for
* example by calling ExtraField::readFrom.
*
* @param int $headerId An unsigned short integer (two bytes) which indicates
* the type of the returned Extra Field.
* @return ExtraField A new Extra Field or null if not support header id.
* @throws ZipException If headerId is out of range.
*/
public static function create($headerId)
{
if (0x0000 > $headerId || $headerId > 0xffff) {
throw new ZipException('headerId out of range');
}
/**
* @var ExtraField $extraField
*/
if (isset(self::getRegistry()[$headerId])) {
$extraClassName = self::getRegistry()[$headerId];
$extraField = new $extraClassName;
if ($headerId !== $extraField::getHeaderId()) {
throw new ZipException('Runtime error support headerId ' . $headerId);
}
} else {
$extraField = new DefaultExtraField($headerId);
}
return $extraField;
}
/**
* Registered extra field classes.
*
* @return array|null
*/
private static function getRegistry()
{
if (self::$registry === null) {
self::$registry[WinZipAesEntryExtraField::getHeaderId()] = '\PhpZip\Extra\WinZipAesEntryExtraField';
self::$registry[NtfsExtraField::getHeaderId()] = '\PhpZip\Extra\NtfsExtraField';
}
return self::$registry;
}
/**
* Returns a protective copy of the Data Block.
*
* @return resource
* @throws ZipException If size data block out of range.
*/
public function getDataBlock()
{
$size = $this->getDataSize();
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size data block out of range.');
}
$fp = fopen('php://temp', 'r+b');
if (0 === $size) return $fp;
$this->writeTo($fp, 0);
rewind($fp);
return $fp;
}
/**
* Returns the Data Size of this Extra Field.
* The Data Size is an unsigned short integer (two bytes)
* which indicates the length of the Data Block in bytes and does not
* include its own size in this Extra Field.
* This property may be initialized by calling ExtraField::readFrom.
*
* @return int The size of the Data Block in bytes
* or 0 if unknown.
*/
abstract public function getDataSize();
/**
* Serializes a Data Block of ExtraField::getDataSize bytes to the
* resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
*/
abstract public function writeTo($handle, $off);
/**
* Initializes this Extra Field by deserializing a Data Block of
* size bytes $size from the resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
* @param int $size Size
*/
abstract public function readFrom($handle, $off, $size);
}

View File

@@ -1,21 +0,0 @@
<?php
namespace PhpZip\Extra;
/**
* Interface ExtraFieldHeader
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ExtraFieldHeader
{
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public static function getHeaderId();
}

View File

@@ -1,213 +0,0 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
/**
* Represents a collection of Extra Fields as they may
* be present at several locations in ZIP files.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ExtraFields
{
/**
* The map of Extra Fields.
* Maps from Header ID to Extra Field.
* Must not be null, but may be empty if no Extra Fields are used.
* The map is sorted by Header IDs in ascending order.
*
* @var ExtraField[]
*/
private $extra = [];
/**
* Returns the number of Extra Fields in this collection.
*
* @return int
*/
public function size()
{
return sizeof($this->extra);
}
/**
* Returns the Extra Field with the given Header ID or null
* if no such Extra Field exists.
*
* @param int $headerId The requested Header ID.
* @return ExtraField The Extra Field with the given Header ID or
* if no such Extra Field exists.
* @throws ZipException If headerId is out of range.
*/
public function get($headerId)
{
if (0x0000 > $headerId || $headerId > 0xffff) {
throw new ZipException('headerId out of range');
}
if (isset($this->extra[$headerId])) {
return $this->extra[$headerId];
}
return null;
}
/**
* Stores the given Extra Field in this collection.
*
* @param ExtraField $extraField The Extra Field to store in this collection.
* @return ExtraField The Extra Field previously associated with the Header ID of
* of the given Extra Field or null if no such Extra Field existed.
* @throws ZipException If headerId is out of range.
*/
public function add(ExtraField $extraField)
{
$headerId = $extraField::getHeaderId();
if (0x0000 > $headerId || $headerId > 0xffff) {
throw new ZipException('headerId out of range');
}
$this->extra[$headerId] = $extraField;
return $extraField;
}
/**
* Returns Extra Field exists
*
* @param int $headerId The requested Header ID.
* @return bool
*/
public function has($headerId)
{
return isset($this->extra[$headerId]);
}
/**
* Removes the Extra Field with the given Header ID.
*
* @param int $headerId The requested Header ID.
* @return ExtraField The Extra Field with the given Header ID or null
* if no such Extra Field exists.
* @throws ZipException If headerId is out of range or extra field not found.
*/
public function remove($headerId)
{
if (0x0000 > $headerId || $headerId > 0xffff) {
throw new ZipException('headerId out of range');
}
if (isset($this->extra[$headerId])) {
$ef = $this->extra[$headerId];
unset($this->extra[$headerId]);
return $ef;
}
throw new ZipException('ExtraField not found');
}
/**
* Returns a protective copy of the Extra Fields.
* null is never returned.
*
* @return string
* @throws ZipException If size out of range
*/
public function getExtra()
{
$size = $this->getExtraLength();
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size out of range');
}
if (0 === $size) return '';
$fp = fopen('php://temp', 'r+b');
$this->writeTo($fp, 0);
rewind($fp);
$content = stream_get_contents($fp);
fclose($fp);
return $content;
}
/**
* Returns the number of bytes required to hold the Extra Fields.
*
* @return int The length of the Extra Fields in bytes. May be 0.
* @see #getExtra
*/
public function getExtraLength()
{
if (empty($this->extra)) {
return 0;
}
$length = 0;
/**
* @var ExtraField $extraField
*/
foreach ($this->extra as $extraField) {
$length += 4 + $extraField->getDataSize();
}
return $length;
}
/**
* Serializes a list of Extra Fields of ExtraField::getExtraLength bytes to the
* stream resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset
*/
private function writeTo($handle, $off)
{
fseek($handle, $off, SEEK_SET);
/**
* @var ExtraField $ef
*/
foreach ($this->extra as $ef) {
fwrite($handle, pack('vv', $ef::getHeaderId(), $ef->getDataSize()));
$off += 4;
fwrite($handle, $ef->writeTo($handle, $off));
$off += $ef->getDataSize();
}
}
/**
* Initializes this Extra Field by deserializing a Data Block of
* size bytes $size from the resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset
* @param int $size Size
* @throws ZipException If size out of range
*/
public function readFrom($handle, $off, $size)
{
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size out of range');
}
$map = [];
if (null !== $handle && 0 < $size) {
$end = $off + $size;
while ($off < $end) {
fseek($handle, $off, SEEK_SET);
$unpack = unpack('vheaderId/vdataSize', fread($handle, 4));
$off += 4;
$extraField = ExtraField::create($unpack['headerId']);
$extraField->readFrom($handle, $off, $unpack['dataSize']);
$off += $unpack['dataSize'];
$map[$unpack['headerId']] = $extraField;
}
assert($off === $end);
}
$this->extra = $map;
}
/**
* If clone extra fields.
*/
function __clone()
{
foreach ($this->extra as $k => $v) {
$this->extra[$k] = clone $v;
}
}
}

View File

@@ -1,176 +0,0 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
use PhpZip\Util\PackUtil;
/**
* NTFS Extra Field
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class NtfsExtraField extends ExtraField
{
/**
* Modify time
*
* @var int Unix Timestamp
*/
private $mtime;
/**
* Access Time
*
* @var int Unix Timestamp
*/
private $atime;
/**
* Create Time
*
* @var int Unix Time
*/
private $ctime;
/**
* @var string
*/
private $rawData = "";
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public static function getHeaderId()
{
return 0x000a;
}
/**
* Returns the Data Size of this Extra Field.
* The Data Size is an unsigned short integer (two bytes)
* which indicates the length of the Data Block in bytes and does not
* include its own size in this Extra Field.
* This property may be initialized by calling ExtraField::readFrom.
*
* @return int The size of the Data Block in bytes
* or 0 if unknown.
*/
public function getDataSize()
{
return 8 * 4 + strlen($this->rawData);
}
/**
* Initializes this Extra Field by deserializing a Data Block of
* size bytes $size from the resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
* @param int $size Size
* @throws ZipException If size out of range
*/
public function readFrom($handle, $off, $size)
{
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size out of range');
}
if ($size > 0) {
$off += 4;
fseek($handle, $off, SEEK_SET);
$unpack = unpack('vtag/vsizeAttr', fread($handle, 4));
if ($unpack['sizeAttr'] === 24) {
$tagData = fread($handle, $unpack['sizeAttr']);
$this->mtime = PackUtil::unpackLongLE(substr($tagData, 0, 8)) / 10000000 - 11644473600;
$this->atime = PackUtil::unpackLongLE(substr($tagData, 8, 8)) / 10000000 - 11644473600;
$this->ctime = PackUtil::unpackLongLE(substr($tagData, 16, 8)) / 10000000 - 11644473600;
}
$off += $unpack['sizeAttr'];
if ($size > $off) {
$this->rawData .= fread($handle, $size - $off);
}
}
}
/**
* Serializes a Data Block of ExtraField::getDataSize bytes to the
* resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
*/
public function writeTo($handle, $off)
{
if ($this->mtime !== null && $this->atime !== null && $this->ctime !== null) {
fseek($handle, $off, SEEK_SET);
fwrite($handle, pack('Vvv', 0, 1, 8 * 3 + strlen($this->rawData)));
$mtimeLong = ($this->mtime + 11644473600) * 10000000;
fwrite($handle, PackUtil::packLongLE($mtimeLong));
$atimeLong = ($this->atime + 11644473600) * 10000000;
fwrite($handle, PackUtil::packLongLE($atimeLong));
$ctimeLong = ($this->ctime + 11644473600) * 10000000;
fwrite($handle, PackUtil::packLongLE($ctimeLong));
if (!empty($this->rawData)) {
fwrite($handle, $this->rawData);
}
}
}
/**
* @return int
*/
public function getMtime()
{
return $this->mtime;
}
/**
* @param int $mtime
*/
public function setMtime($mtime)
{
$this->mtime = (int)$mtime;
}
/**
* @return int
*/
public function getAtime()
{
return $this->atime;
}
/**
* @param int $atime
*/
public function setAtime($atime)
{
$this->atime = (int)$atime;
}
/**
* @return int
*/
public function getCtime()
{
return $this->ctime;
}
/**
* @param int $ctime
*/
public function setCtime($ctime)
{
$this->ctime = (int)$ctime;
}
}

View File

@@ -1,236 +0,0 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
/**
* WinZip AES Extra Field.
*
* @see http://www.winzip.com/win/en/aes_info.htm AES Encryption Information: Encryption Specification AE-1 and AE-2 (WinZip Computing, S.L.)
* @see http://www.winzip.com/win/en/aes_tips.htm AES Coding Tips for Developers (WinZip Computing, S.L.)
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class WinZipAesEntryExtraField extends ExtraField
{
const DATA_SIZE = 7;
const VENDOR_ID = 17729; // 'A' | ('E' << 8);
/**
* Entries of this type <em>do</em> include the standard ZIP CRC-32 value.
* For use with @see WinZipAesEntryExtraField::setVendorVersion()}/@see WinZipAesEntryExtraField::getVendorVersion().
*/
const VV_AE_1 = 1;
/**
* Entries of this type do <em>not</em> include the standard ZIP CRC-32 value.
* For use with @see WinZipAesEntryExtraField::setVendorVersion()}/@see WinZipAesEntryExtraField::getVendorVersion().
*/
const VV_AE_2 = 2;
const KEY_STRENGTH_128BIT = 128;
const KEY_STRENGTH_192BIT = 192;
const KEY_STRENGTH_256BIT = 256;
private static $keyStrengths = [
self::KEY_STRENGTH_128BIT => 0x01,
self::KEY_STRENGTH_192BIT => 0x02,
self::KEY_STRENGTH_256BIT => 0x03
];
/**
* Vendor version.
*
* @var int
*/
private $vendorVersion = self::VV_AE_1;
/**
* Encryption strength.
*
* @var int
*/
private $encryptionStrength = self::KEY_STRENGTH_256BIT;
/**
* Zip compression method.
*
* @var int
*/
private $method;
/**
* Returns the Header ID (type) of this Extra Field.
* The Header ID is an unsigned short integer (two bytes)
* which must be constant during the life cycle of this object.
*
* @return int
*/
public static function getHeaderId()
{
return 0x9901;
}
/**
* Returns the Data Size of this Extra Field.
* The Data Size is an unsigned short integer (two bytes)
* which indicates the length of the Data Block in bytes and does not
* include its own size in this Extra Field.
* This property may be initialized by calling ExtraField::readFrom.
*
* @return int The size of the Data Block in bytes
* or 0 if unknown.
*/
public function getDataSize()
{
return self::DATA_SIZE;
}
/**
* Returns the vendor version.
*
* @see WinZipAesEntryExtraField::VV_AE_1
* @see WinZipAesEntryExtraField::VV_AE_2
*/
public function getVendorVersion()
{
return $this->vendorVersion & 0xffff;
}
/**
* Sets the vendor version.
*
* @see WinZipAesEntryExtraField::VV_AE_1
* @see WinZipAesEntryExtraField::VV_AE_2
* @param int $vendorVersion the vendor version.
* @throws ZipException Unsupport vendor version.
*/
public function setVendorVersion($vendorVersion)
{
if ($vendorVersion < self::VV_AE_1 || self::VV_AE_2 < $vendorVersion) {
throw new ZipException($vendorVersion);
}
$this->vendorVersion = $vendorVersion;
}
/**
* Returns vendor id.
*
* @return int
*/
public function getVendorId()
{
return self::VENDOR_ID;
}
/**
* @return bool|int
*/
public function getKeyStrength()
{
return self::keyStrength($this->encryptionStrength);
}
/**
* @param int $encryptionStrength Encryption strength as bits.
* @return int
* @throws ZipException If unsupport encryption strength.
*/
public static function keyStrength($encryptionStrength)
{
$flipKeyStrength = array_flip(self::$keyStrengths);
if (!isset($flipKeyStrength[$encryptionStrength])) {
throw new ZipException("Unsupport encryption strength " . $encryptionStrength);
}
return $flipKeyStrength[$encryptionStrength];
}
/**
* Returns compression method.
*
* @return int
*/
public function getMethod()
{
return $this->method & 0xffff;
}
/**
* Sets compression method.
*
* @param int $compressionMethod Compression method
* @throws ZipException Compression method out of range.
*/
public function setMethod($compressionMethod)
{
if (0x0000 > $compressionMethod || $compressionMethod > 0xffff) {
throw new ZipException('Compression method out of range');
}
$this->method = $compressionMethod;
}
/**
* Initializes this Extra Field by deserializing a Data Block of
* size bytes $size from the resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
* @param int $size Size
* @throws ZipException
*/
public function readFrom($handle, $off, $size)
{
if (self::DATA_SIZE != $size)
throw new ZipException();
fseek($handle, $off, SEEK_SET);
/**
* @var int $vendorVersion
* @var int $vendorId
* @var int $keyStrength
* @var int $method
*/
$unpack = unpack('vvendorVersion/vvendorId/ckeyStrength/vmethod', fread($handle, 7));
extract($unpack);
$this->setVendorVersion($vendorVersion);
if (self::VENDOR_ID != $vendorId) {
throw new ZipException();
}
$this->setKeyStrength(self::keyStrength($keyStrength)); // checked
$this->setMethod($method);
}
/**
* Set key strength.
*
* @param int $keyStrength
*/
public function setKeyStrength($keyStrength)
{
$this->encryptionStrength = self::encryptionStrength($keyStrength);
}
/**
* Returns encryption strength.
*
* @param int $keyStrength Key strength in bits.
* @return int
*/
public static function encryptionStrength($keyStrength)
{
return isset(self::$keyStrengths[$keyStrength]) ? self::$keyStrengths[$keyStrength] : self::$keyStrengths[self::KEY_STRENGTH_128BIT];
}
/**
* Serializes a Data Block of ExtraField::getDataSize bytes to the
* resource $handle at the zero based offset $off.
*
* @param resource $handle
* @param int $off Offset bytes
*/
public function writeTo($handle, $off)
{
fseek($handle, $off, SEEK_SET);
fwrite($handle, pack('vvcv', $this->vendorVersion, self::VENDOR_ID, $this->encryptionStrength, $this->method));
}
}

View File

@@ -1,42 +0,0 @@
<?php
namespace PhpZip\Mapper;
/**
* Adds a offset value to the given position.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class OffsetPositionMapper extends PositionMapper
{
/**
* @var int
*/
private $offset;
/**
* @param int $offset
*/
public function __construct($offset)
{
$this->offset = $offset;
}
/**
* @param int $position
* @return int
*/
public function map($position)
{
return parent::map($position) + $this->offset;
}
/**
* @param int $position
* @return int
*/
public function unmap($position)
{
return parent::unmap($position) - $this->offset;
}
}

View File

@@ -1,29 +0,0 @@
<?php
namespace PhpZip\Mapper;
/**
* Maps a given position.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class PositionMapper
{
/**
* @param int $position
* @return int
*/
public function map($position)
{
return $position;
}
/**
* @param int $position
* @return int
*/
public function unmap($position)
{
return $position;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,506 +0,0 @@
<?php
namespace PhpZip\Model;
use PhpZip\Extra\NtfsExtraField;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Util\FilesUtil;
/**
* Zip info
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipInfo
{
// made by constants
const MADE_BY_MS_DOS = 0;
const MADE_BY_AMIGA = 1;
const MADE_BY_OPEN_VMS = 2;
const MADE_BY_UNIX = 3;
const MADE_BY_VM_CMS = 4;
const MADE_BY_ATARI = 5;
const MADE_BY_OS_2 = 6;
const MADE_BY_MACINTOSH = 7;
const MADE_BY_Z_SYSTEM = 8;
const MADE_BY_CP_M = 9;
const MADE_BY_WINDOWS_NTFS = 10;
const MADE_BY_MVS = 11;
const MADE_BY_VSE = 12;
const MADE_BY_ACORN_RISC = 13;
const MADE_BY_VFAT = 14;
const MADE_BY_ALTERNATE_MVS = 15;
const MADE_BY_BEOS = 16;
const MADE_BY_TANDEM = 17;
const MADE_BY_OS_400 = 18;
const MADE_BY_OS_X = 19;
const MADE_BY_UNKNOWN = 20;
const UNX_IFMT = 0170000; /* Unix file type mask */
const UNX_IFREG = 0100000; /* Unix regular file */
const UNX_IFSOCK = 0140000; /* Unix socket (BSD, not SysV or Amiga) */
const UNX_IFLNK = 0120000; /* Unix symbolic link (not SysV, Amiga) */
const UNX_IFBLK = 0060000; /* Unix block special (not Amiga) */
const UNX_IFDIR = 0040000; /* Unix directory */
const UNX_IFCHR = 0020000; /* Unix character special (not Amiga) */
const UNX_IFIFO = 0010000; /* Unix fifo (BCC, not MSC or Amiga) */
const UNX_ISUID = 04000; /* Unix set user id on execution */
const UNX_ISGID = 02000; /* Unix set group id on execution */
const UNX_ISVTX = 01000; /* Unix directory permissions control */
const UNX_ENFMT = self::UNX_ISGID; /* Unix record locking enforcement flag */
const UNX_IRWXU = 00700; /* Unix read, write, execute: owner */
const UNX_IRUSR = 00400; /* Unix read permission: owner */
const UNX_IWUSR = 00200; /* Unix write permission: owner */
const UNX_IXUSR = 00100; /* Unix execute permission: owner */
const UNX_IRWXG = 00070; /* Unix read, write, execute: group */
const UNX_IRGRP = 00040; /* Unix read permission: group */
const UNX_IWGRP = 00020; /* Unix write permission: group */
const UNX_IXGRP = 00010; /* Unix execute permission: group */
const UNX_IRWXO = 00007; /* Unix read, write, execute: other */
const UNX_IROTH = 00004; /* Unix read permission: other */
const UNX_IWOTH = 00002; /* Unix write permission: other */
const UNX_IXOTH = 00001; /* Unix execute permission: other */
private static $valuesMadeBy = [
self::MADE_BY_MS_DOS => 'FAT',
self::MADE_BY_AMIGA => 'Amiga',
self::MADE_BY_OPEN_VMS => 'OpenVMS',
self::MADE_BY_UNIX => 'UNIX',
self::MADE_BY_VM_CMS => 'VM/CMS',
self::MADE_BY_ATARI => 'Atari ST',
self::MADE_BY_OS_2 => 'OS/2 H.P.F.S.',
self::MADE_BY_MACINTOSH => 'Macintosh',
self::MADE_BY_Z_SYSTEM => 'Z-System',
self::MADE_BY_CP_M => 'CP/M',
self::MADE_BY_WINDOWS_NTFS => 'Windows NTFS',
self::MADE_BY_MVS => 'MVS (OS/390 - Z/OS)',
self::MADE_BY_VSE => 'VSE',
self::MADE_BY_ACORN_RISC => 'Acorn Risc',
self::MADE_BY_VFAT => 'VFAT',
self::MADE_BY_ALTERNATE_MVS => 'Alternate MVS',
self::MADE_BY_BEOS => 'BeOS',
self::MADE_BY_TANDEM => 'Tandem',
self::MADE_BY_OS_400 => 'OS/400',
self::MADE_BY_OS_X => 'Mac OS X',
];
private static $valuesCompressionMethod = [
ZipEntry::METHOD_STORED => 'no compression',
1 => 'shrink',
2 => 'reduce level 1',
3 => 'reduce level 2',
4 => 'reduce level 3',
5 => 'reduce level 4',
6 => 'implode',
7 => 'reserved for Tokenizing compression algorithm',
ZipEntry::METHOD_DEFLATED => 'deflate',
9 => 'deflate64',
10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
11 => 'reserved by PKWARE',
12 => 'bzip2',
13 => 'reserved by PKWARE',
14 => 'LZMA (EFS)',
15 => 'reserved by PKWARE',
16 => 'reserved by PKWARE',
17 => 'reserved by PKWARE',
18 => 'IBM TERSE',
19 => 'IBM LZ77 z Architecture (PFS)',
97 => 'WavPack',
98 => 'PPMd version I, Rev 1',
ZipEntry::WINZIP_AES => 'WinZip AES',
];
/**
* @var string
*/
private $path;
/**
* @var bool
*/
private $folder;
/**
* @var int
*/
private $size;
/**
* @var int
*/
private $compressedSize;
/**
* @var int
*/
private $mtime;
/**
* @var int|null
*/
private $ctime;
/**
* @var int|null
*/
private $atime;
/**
* @var bool
*/
private $encrypted;
/**
* @var string|null
*/
private $comment;
/**
* @var int
*/
private $crc;
/**
* @var string
*/
private $method;
/**
* @var string
*/
private $platform;
/**
* @var int
*/
private $version;
/**
* @var string
*/
private $attributes;
/**
* ZipInfo constructor.
*
* @param ZipEntry $entry
*/
public function __construct(ZipEntry $entry)
{
$mtime = $entry->getTime();
$atime = null;
$ctime = null;
$field = $entry->getExtraField(NtfsExtraField::getHeaderId());
if ($field !== null && $field instanceof NtfsExtraField) {
/**
* @var NtfsExtraField $field
*/
$atime = $field->getAtime();
$ctime = $field->getCtime();
}
$this->path = $entry->getName();
$this->folder = $entry->isDirectory();
$this->size = $entry->getSize();
$this->compressedSize = $entry->getCompressedSize();
$this->mtime = $mtime;
$this->ctime = $ctime;
$this->atime = $atime;
$this->encrypted = $entry->isEncrypted();
$this->comment = $entry->getComment();
$this->crc = $entry->getCrc();
$this->method = self::getMethodName($entry);
$this->platform = self::getPlatformName($entry);
$this->version = $entry->getVersionNeededToExtract();
$attribs = str_repeat(" ", 12);
$xattr = (($entry->getRawExternalAttributes() >> 16) & 0xFFFF);
switch ($entry->getPlatform()) {
case self::MADE_BY_MS_DOS:
case self::MADE_BY_WINDOWS_NTFS:
if ($entry->getPlatform() != self::MADE_BY_MS_DOS ||
($xattr & 0700) !=
(0400 |
(!($entry->getRawExternalAttributes() & 1) << 7) |
(($entry->getRawExternalAttributes() & 0x10) << 2))
) {
$xattr = $entry->getRawExternalAttributes() & 0xFF;
$attribs = ".r.-... ";
$attribs[2] = ($xattr & 0x01) ? '-' : 'w';
$attribs[5] = ($xattr & 0x02) ? 'h' : '-';
$attribs[6] = ($xattr & 0x04) ? 's' : '-';
$attribs[4] = ($xattr & 0x20) ? 'a' : '-';
if ($xattr & 0x10) {
$attribs[0] = 'd';
$attribs[3] = 'x';
} else
$attribs[0] = '-';
if ($xattr & 0x08)
$attribs[0] = 'V';
else {
$ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION));
if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) {
$attribs[3] = 'x';
}
}
break;
} /* else: fall through! */
default: /* assume Unix-like */
switch ($xattr & self::UNX_IFMT) {
case self::UNX_IFDIR:
$attribs[0] = 'd';
break;
case self::UNX_IFREG:
$attribs[0] = '-';
break;
case self::UNX_IFLNK:
$attribs[0] = 'l';
break;
case self::UNX_IFBLK:
$attribs[0] = 'b';
break;
case self::UNX_IFCHR:
$attribs[0] = 'c';
break;
case self::UNX_IFIFO:
$attribs[0] = 'p';
break;
case self::UNX_IFSOCK:
$attribs[0] = 's';
break;
default:
$attribs[0] = '?';
break;
}
$attribs[1] = ($xattr & self::UNX_IRUSR) ? 'r' : '-';
$attribs[4] = ($xattr & self::UNX_IRGRP) ? 'r' : '-';
$attribs[7] = ($xattr & self::UNX_IROTH) ? 'r' : '-';
$attribs[2] = ($xattr & self::UNX_IWUSR) ? 'w' : '-';
$attribs[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-';
$attribs[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-';
if ($xattr & self::UNX_IXUSR)
$attribs[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x';
else
$attribs[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */
if ($xattr & self::UNX_IXGRP)
$attribs[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */
else
$attribs[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */
if ($xattr & self::UNX_IXOTH)
$attribs[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; /* "sticky bit" */
else
$attribs[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; /* T==undefined */
}
$this->attributes = trim($attribs);
}
/**
* @param ZipEntry $entry
* @return string
*/
public static function getMethodName(ZipEntry $entry)
{
$return = '';
if ($entry->isEncrypted()) {
if ($entry->getMethod() === ZipEntry::WINZIP_AES) {
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
if ($field !== null) {
/**
* @var WinZipAesEntryExtraField $field
*/
$return .= '-' . $field->getKeyStrength();
if (isset(self::$valuesCompressionMethod[$field->getMethod()])) {
$return .= ' ' . ucfirst(self::$valuesCompressionMethod[$field->getMethod()]);
}
}
} else {
$return .= 'ZipCrypto';
if (isset(self::$valuesCompressionMethod[$entry->getMethod()])) {
$return .= ' ' . ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
}
}
} elseif (isset(self::$valuesCompressionMethod[$entry->getMethod()])) {
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
} else {
$return = 'unknown';
}
return $return;
}
/**
* @param ZipEntry $entry
* @return string
*/
public static function getPlatformName(ZipEntry $entry)
{
if (isset(self::$valuesMadeBy[$entry->getPlatform()])) {
return self::$valuesMadeBy[$entry->getPlatform()];
} else {
return 'unknown';
}
}
/**
* @return array
*/
public function toArray()
{
return [
'path' => $this->getPath(),
'folder' => $this->isFolder(),
'size' => $this->getSize(),
'compressed_size' => $this->getCompressedSize(),
'modified' => $this->getMtime(),
'created' => $this->getCtime(),
'accessed' => $this->getAtime(),
'attributes' => $this->getAttributes(),
'encrypted' => $this->isEncrypted(),
'comment' => $this->getComment(),
'crc' => $this->getCrc(),
'method' => $this->getMethod(),
'platform' => $this->getPlatform(),
'version' => $this->getVersion()
];
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* @return boolean
*/
public function isFolder()
{
return $this->folder;
}
/**
* @return int
*/
public function getSize()
{
return $this->size;
}
/**
* @return int
*/
public function getCompressedSize()
{
return $this->compressedSize;
}
/**
* @return int
*/
public function getMtime()
{
return $this->mtime;
}
/**
* @return int|null
*/
public function getCtime()
{
return $this->ctime;
}
/**
* @return int|null
*/
public function getAtime()
{
return $this->atime;
}
/**
* @return boolean
*/
public function isEncrypted()
{
return $this->encrypted;
}
/**
* @return null|string
*/
public function getComment()
{
return $this->comment;
}
/**
* @return int
*/
public function getCrc()
{
return $this->crc;
}
/**
* @return string
*/
public function getMethod()
{
return $this->method;
}
/**
* @return string
*/
public function getPlatform()
{
return $this->platform;
}
/**
* @return int
*/
public function getVersion()
{
return $this->version;
}
/**
* @return string
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* @return string
*/
function __toString()
{
return 'ZipInfo {'
. 'Path="' . $this->getPath() . '", '
. ($this->isFolder() ? 'Folder, ' : '')
. 'Size=' . FilesUtil::humanSize($this->getSize())
. ', Compressed size=' . FilesUtil::humanSize($this->getCompressedSize())
. ', Modified time=' . date(DATE_W3C, $this->getMtime()) . ', '
. ($this->getCtime() !== null ? 'Created time=' . date(DATE_W3C, $this->getCtime()) . ', ' : '')
. ($this->getAtime() !== null ? 'Accessed time=' . date(DATE_W3C, $this->getAtime()) . ', ' : '')
. ($this->isEncrypted() ? 'Encrypted, ' : '')
. (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '')
. (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '')
. 'Method="' . $this->getMethod() . '", '
. 'Attributes="' . $this->getAttributes() . '", '
. 'Platform="' . $this->getPlatform() . '", '
. 'Version=' . $this->getVersion()
. '}';
}
}

View File

@@ -1,22 +0,0 @@
<?php
namespace PhpZip\Output;
/**
* Zip output entry for empty dir.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipOutputEmptyDirEntry extends ZipOutputEntry
{
/**
* Returns entry data.
*
* @return string
*/
public function getEntryContent()
{
return '';
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace PhpZip\Output;
use PhpZip\Model\ZipEntry;
/**
* Zip output Entry
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
abstract class ZipOutputEntry
{
/**
* @var ZipEntry
*/
private $entry;
/**
* @param ZipEntry $entry
*/
public function __construct(ZipEntry $entry)
{
if ($entry === null) {
throw new \RuntimeException('entry is null');
}
$this->entry = $entry;
}
/**
* Returns zip entry
*
* @return ZipEntry
*/
public function getEntry()
{
return $this->entry;
}
/**
* Returns entry data.
*
* @return string
*/
abstract public function getEntryContent();
}

View File

@@ -1,54 +0,0 @@
<?php
namespace PhpZip\Output;
use PhpZip\Model\ZipEntry;
use RuntimeException;
/**
* Zip output entry for stream resource.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipOutputStreamEntry extends ZipOutputEntry
{
/**
* @var resource
*/
private $stream;
/**
* @param resource $stream
* @param ZipEntry $entry
*/
public function __construct($stream, ZipEntry $entry)
{
parent::__construct($entry);
if (!is_resource($stream)) {
throw new RuntimeException('stream is not resource');
}
$this->stream = $stream;
}
/**
* Returns entry data.
*
* @return string
*/
public function getEntryContent()
{
rewind($this->stream);
return stream_get_contents($this->stream);
}
/**
* Release stream resource.
*/
function __destruct()
{
if ($this->stream !== null) {
fclose($this->stream);
$this->stream = null;
}
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace PhpZip\Output;
use PhpZip\Exception\ZipException;
use PhpZip\Model\ZipEntry;
/**
* Zip output entry for string data.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipOutputStringEntry extends ZipOutputEntry
{
/**
* Data content.
*
* @var string
*/
private $data;
/**
* @param string $data
* @param ZipEntry $entry
* @throws ZipException If data empty.
*/
public function __construct($data, ZipEntry $entry)
{
parent::__construct($entry);
$data = (string)$data;
if ($data === null) {
throw new ZipException("data is null");
}
$this->data = $data;
}
/**
* Returns entry data.
*
* @return string
*/
public function getEntryContent()
{
return $this->data;
}
}

View File

@@ -1,56 +0,0 @@
<?php
namespace PhpZip\Output;
use PhpZip\Exception\ZipException;
use PhpZip\Model\ZipEntry;
use PhpZip\ZipFile;
/**
* Zip output entry for input zip file.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipOutputZipFileEntry extends ZipOutputEntry
{
/**
* Input zip file.
*
* @var ZipFile
*/
private $inputZipFile;
/**
* Input entry name.
*
* @var string
*/
private $inputEntryName;
/**
* ZipOutputZipFileEntry constructor.
* @param ZipFile $zipFile
* @param ZipEntry $zipEntry
* @throws ZipException If input zip file is null.
*/
public function __construct(ZipFile $zipFile, ZipEntry $zipEntry)
{
if ($zipFile === null) {
throw new ZipException('ZipFile is null');
}
parent::__construct(clone $zipEntry);
$this->inputZipFile = $zipFile;
$this->inputEntryName = $zipEntry->getName();
}
/**
* Returns entry data.
*
* @return string
*/
public function getEntryContent()
{
return $this->inputZipFile->getEntryContent($this->inputEntryName);
}
}

View File

@@ -1,32 +0,0 @@
<?php
namespace PhpZip\Util;
use PhpZip\Exception\ZipException;
/**
* Crypto Utils
*/
class CryptoUtil
{
/**
* Returns random bytes.
*
* @param int $length
* @return string
* @throws ZipException
*/
public static final function randomBytes($length)
{
$length = (int)$length;
if (function_exists('random_bytes')) {
return random_bytes($length);
} elseif (function_exists('openssl_random_pseudo_bytes')) {
return openssl_random_pseudo_bytes($length);
} elseif (function_exists('mcrypt_create_iv')) {
return mcrypt_create_iv($length);
} else {
throw new ZipException('Extension openssl or mcrypt not loaded');
}
}
}

View File

@@ -1,77 +0,0 @@
<?php
namespace PhpZip\Util;
use PhpZip\Exception\ZipException;
/**
* Convert unix timestamp values to DOS date/time values and vice versa.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class DateTimeConverter
{
/**
* Smallest supported DOS date/time value in a ZIP file,
* which is January 1st, 1980 AD 00:00:00 local time.
*/
const MIN_DOS_TIME = 0x210000; // (1 << 21) | (1 << 16)
/**
* Largest supported DOS date/time value in a ZIP file,
* which is December 31st, 2107 AD 23:59:58 local time.
*/
const MAX_DOS_TIME = 0xff9fbf7d; // ((2107 - 1980) << 25) | (12 << 21) | (31 << 16) | (23 << 11) | (59 << 5) | (58 >> 1);
/**
* Convert a 32 bit integer DOS date/time value to a UNIX timestamp value.
*
* @param int $dosTime Dos date/time
* @return int Unix timestamp
*/
public static function toUnixTimestamp($dosTime)
{
if (self::MIN_DOS_TIME > $dosTime) {
$dosTime = self::MIN_DOS_TIME;
} elseif (self::MAX_DOS_TIME < $dosTime) {
$dosTime = self::MAX_DOS_TIME;
}
return mktime(
($dosTime >> 11) & 0x1f, // hour
($dosTime >> 5) & 0x3f, // minute
2 * ($dosTime & 0x1f), // second
($dosTime >> 21) & 0x0f, // month
($dosTime >> 16) & 0x1f, // day
1980 + (($dosTime >> 25) & 0x7f) // year
);
}
/**
* Converts a UNIX timestamp value to a DOS date/time value.
*
* @param int $unixTimestamp The number of seconds since midnight, January 1st,
* 1970 AD UTC.
* @return int A DOS date/time value reflecting the local time zone and
* rounded down to even seconds
* and is in between DateTimeConverter::MIN_DOS_TIME and DateTimeConverter::MAX_DOS_TIME.
* @throws ZipException If unix timestamp is negative.
*/
public static function toDosTime($unixTimestamp)
{
if (0 > $unixTimestamp) {
throw new ZipException("Negative unix timestamp: " . $unixTimestamp);
}
$date = getdate($unixTimestamp);
if ($date['year'] < 1980) {
return self::MIN_DOS_TIME;
}
$date['year'] -= 1980;
return ($date['year'] << 25 | $date['mon'] << 21 |
$date['mday'] << 16 | $date['hours'] << 11 |
$date['minutes'] << 5 | $date['seconds'] >> 1);
}
}

View File

@@ -1,222 +0,0 @@
<?php
namespace PhpZip\Util;
use PhpZip\Util\Iterator\IgnoreFilesFilterIterator;
use PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator;
/**
* Files util.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class FilesUtil
{
/**
* Is empty directory
*
* @param string $dir Directory
* @return bool
*/
public static function isEmptyDir($dir)
{
if (!is_readable($dir)) {
return false;
}
return count(scandir($dir)) === 2;
}
/**
* Remove recursive directory.
*
* @param string $dir Directory path.
*/
public static function removeDir($dir)
{
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileInfo) {
$function = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
$function($fileInfo->getRealPath());
}
rmdir($dir);
}
/**
* Convert glob pattern to regex pattern.
*
* @param string $globPattern
* @return string
*/
public static function convertGlobToRegEx($globPattern)
{
// Remove beginning and ending * globs because they're useless
$globPattern = trim($globPattern, '*');
$escaping = false;
$inCurrent = 0;
$chars = str_split($globPattern);
$regexPattern = '';
foreach ($chars AS $currentChar) {
switch ($currentChar) {
case '*':
$regexPattern .= ($escaping ? "\\*" : '.*');
$escaping = false;
break;
case '?':
$regexPattern .= ($escaping ? "\\?" : '.');
$escaping = false;
break;
case '.':
case '(':
case ')':
case '+':
case '|':
case '^':
case '$':
case '@':
case '%':
$regexPattern .= '\\' . $currentChar;
$escaping = false;
break;
case '\\':
if ($escaping) {
$regexPattern .= "\\\\";
$escaping = false;
} else {
$escaping = true;
}
break;
case '{':
if ($escaping) {
$regexPattern .= "\\{";
} else {
$regexPattern = '(';
$inCurrent++;
}
$escaping = false;
break;
case '}':
if ($inCurrent > 0 && !$escaping) {
$regexPattern .= ')';
$inCurrent--;
} else if ($escaping)
$regexPattern = "\\}";
else
$regexPattern = "}";
$escaping = false;
break;
case ',':
if ($inCurrent > 0 && !$escaping) {
$regexPattern .= '|';
} else if ($escaping)
$regexPattern .= "\\,";
else
$regexPattern = ",";
break;
default:
$escaping = false;
$regexPattern .= $currentChar;
}
}
return $regexPattern;
}
/**
* Search files.
*
* @param string $inputDir
* @param bool $recursive
* @param array $ignoreFiles
* @return array Searched file list
*/
public static function fileSearchWithIgnore($inputDir, $recursive = true, array $ignoreFiles = [])
{
$directoryIterator = $recursive ?
new \RecursiveDirectoryIterator($inputDir) :
new \DirectoryIterator($inputDir);
if (!empty($ignoreFiles)) {
$directoryIterator = $recursive ?
new IgnoreFilesRecursiveFilterIterator($directoryIterator, $ignoreFiles) :
new IgnoreFilesFilterIterator($directoryIterator, $ignoreFiles);
}
$iterator = $recursive ?
new \RecursiveIteratorIterator($directoryIterator) :
new \IteratorIterator($directoryIterator);
$fileList = [];
foreach ($iterator as $file) {
if ($file instanceof \SplFileInfo) {
$fileList[] = $file->getPathname();
}
}
return $fileList;
}
/**
* Search files from glob pattern.
*
* @param string $globPattern
* @param int $flags
* @param bool $recursive
* @return array Searched file list
*/
public static function globFileSearch($globPattern, $flags = 0, $recursive = true)
{
$flags = (int)$flags;
$recursive = (bool)$recursive;
$files = glob($globPattern, $flags);
if (!$recursive) {
return $files;
}
foreach (glob(dirname($globPattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) {
$files = array_merge($files, self::globFileSearch($dir . '/' . basename($globPattern), $flags, $recursive));
}
return $files;
}
/**
* Search files from regex pattern.
*
* @param string $folder
* @param string $pattern
* @param bool $recursive
* @return array Searched file list
*/
public static function regexFileSearch($folder, $pattern, $recursive = true)
{
$directoryIterator = $recursive ? new \RecursiveDirectoryIterator($folder) : new \DirectoryIterator($folder);
$iterator = $recursive ? new \RecursiveIteratorIterator($directoryIterator) : new \IteratorIterator($directoryIterator);
$regexIterator = new \RegexIterator($iterator, $pattern, \RegexIterator::MATCH);
$fileList = [];
foreach ($regexIterator as $file) {
if ($file instanceof \SplFileInfo) {
$fileList[] = $file->getPathname();
}
}
return $fileList;
}
/**
* Convert bytes to human size.
*
* @param int $size Size bytes
* @param string|null $unit Unit support 'GB', 'MB', 'KB'
* @return string
*/
public static function humanSize($size, $unit = null)
{
if (($unit === null && $size >= 1 << 30) || $unit === "GB")
return number_format($size / (1 << 30), 2) . "GB";
if (($unit === null && $size >= 1 << 20) || $unit === "MB")
return number_format($size / (1 << 20), 2) . "MB";
if (($unit === null && $size >= 1 << 10) || $unit === "KB")
return number_format($size / (1 << 10), 2) . "KB";
return number_format($size) . " bytes";
}
}

View File

@@ -1,48 +0,0 @@
<?php
namespace PhpZip\Util;
use PhpZip\Exception\ZipException;
/**
* Pack util
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class PackUtil
{
/**
* @param int|string $longValue
* @return string
*/
public static function packLongLE($longValue)
{
if (version_compare(PHP_VERSION, '5.6.3') >= 0) {
return pack("P", $longValue);
}
$left = 0xffffffff00000000;
$right = 0x00000000ffffffff;
$r = ($longValue & $left) >> 32;
$l = $longValue & $right;
return pack('VV', $l, $r);
}
/**
* @param string|int $value
* @return int
* @throws ZipException
*/
public static function unpackLongLE($value)
{
if (version_compare(PHP_VERSION, '5.6.3') >= 0) {
return current(unpack('P', $value));
}
$unpack = unpack('Va/Vb', $value);
return $unpack['a'] + ($unpack['b'] << 32);
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace PhpZip\Util;
/**
* String Util
*/
class StringUtil
{
/**
* @param string $haystack
* @param string $needle
* @return bool
*/
public static function startsWith($haystack, $needle)
{
return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== false;
}
/**
* @param string $haystack
* @param string $needle
* @return bool
*/
public static function endsWith($haystack, $needle)
{
return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0
&& strpos($haystack, $needle, $temp) !== false);
}
}

View File

@@ -1,115 +0,0 @@
<?php
namespace PhpZip;
/**
* Constants for ZIP files.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipConstants
{
/** Local File Header signature. */
const LOCAL_FILE_HEADER_SIG = 0x04034B50;
/** Data Descriptor signature. */
const DATA_DESCRIPTOR_SIG = 0x08074B50;
/** Central File Header signature. */
const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
/** Zip64 End Of Central Directory Record. */
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06064B50;
/** Zip64 End Of Central Directory Locator. */
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG = 0x07064B50;
/** End Of Central Directory Record signature. */
const END_OF_CENTRAL_DIRECTORY_RECORD_SIG = 0x06054B50;
/**
* The minimum length of the Local File Header record.
*
* local file header signature 4
* version needed to extract 2
* general purpose bit flag 2
* compression method 2
* last mod file time 2
* last mod file date 2
* crc-32 4
* compressed size 4
* uncompressed size 4
* file name length 2
* extra field length 2
*/
const LOCAL_FILE_HEADER_MIN_LEN = 30;
/**
* The minimum length of the End Of Central Directory Record.
*
* end of central dir signature 4
* number of this disk 2
* number of the disk with the
* start of the central directory 2
* total number of entries in the
* central directory on this disk 2
* total number of entries in
* the central directory 2
* size of the central directory 4
* offset of start of central *
* directory with respect to *
* the starting disk number 4
* zipfile comment length 2
*/
const END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 22;
/**
* The length of the Zip64 End Of Central Directory Locator.
* zip64 end of central dir locator
* signature 4
* number of the disk with the
* start of the zip64 end of
* central directory 4
* relative offset of the zip64
* end of central directory record 8
* total number of disks 4
*/
const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN = 20;
/**
* The minimum length of the Zip64 End Of Central Directory Record.
*
* zip64 end of central dir
* signature 4
* size of zip64 end of central
* directory record 8
* version made by 2
* version needed to extract 2
* number of this disk 4
* number of the disk with the
* start of the central directory 4
* total number of entries in the
* central directory on this disk 8
* total number of entries in
* the central directory 8
* size of the central directory 8
* offset of start of central
* directory with respect to
* the starting disk number 8
*/
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN = 56;
/**
* Local File Header signature 4
* Version Needed To Extract 2
* General Purpose Bit Flags 2
* Compression Method 2
* Last Mod File Time 2
* Last Mod File Date 2
* CRC-32 4
* Compressed Size 4
* Uncompressed Size 4
*/
const LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS = 26;
}

View File

@@ -1,908 +0,0 @@
<?php
namespace PhpZip;
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
use PhpZip\Crypto\WinZipAesEngine;
use PhpZip\Exception\Crc32Exception;
use PhpZip\Exception\IllegalArgumentException;
use PhpZip\Exception\ZipCryptoException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipNotFoundEntry;
use PhpZip\Exception\ZipUnsupportMethod;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Mapper\OffsetPositionMapper;
use PhpZip\Mapper\PositionMapper;
use PhpZip\Model\ZipEntry;
use PhpZip\Model\ZipInfo;
use PhpZip\Util\PackUtil;
/**
* This class is able to open the .ZIP file in read mode and extract files from it.
*
* Implemented support traditional PKWARE encryption and WinZip AES encryption.
* Implemented support ZIP64.
* Implemented support skip a preamble like the one found in self extracting archives.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipFile implements \Countable, \ArrayAccess, \Iterator, ZipConstants
{
/**
* Input seekable stream resource.
*
* @var resource
*/
private $inputStream;
/**
* The total number of bytes in the ZIP archive.
*
* @var int
*/
private $length;
/**
* The charset to use for entry names and comments.
*
* @var string
*/
private $charset;
/**
* The number of bytes in the preamble of this ZIP file.
*
* @var int
*/
private $preamble;
/**
* The number of bytes in the postamble of this ZIP file.
*
* @var int
*/
private $postamble;
/**
* Maps entry names to zip entries.
*
* @var ZipEntry[]
*/
private $entries;
/**
* The file comment.
*
* @var string
*/
private $comment;
/**
* Maps offsets specified in the ZIP file to real offsets in the file.
*
* @var PositionMapper
*/
private $mapper;
/**
* Private ZipFile constructor.
*
* @see ZipFile::openFromFile()
* @see ZipFile::openFromString()
* @see ZipFile::openFromStream()
*/
private function __construct()
{
$this->mapper = new PositionMapper();
$this->charset = "UTF-8";
}
/**
* Open zip archive from file
*
* @param string $filename
* @return ZipFile
* @throws IllegalArgumentException if file doesn't exists.
* @throws ZipException if can't open file.
*/
public static function openFromFile($filename)
{
if (!file_exists($filename)) {
throw new IllegalArgumentException("File $filename can't exists.");
}
if (!($handle = fopen($filename, 'rb'))) {
throw new ZipException("File $filename can't open.");
}
$zipFile = self::openFromStream($handle);
$zipFile->length = filesize($filename);
return $zipFile;
}
/**
* Open zip archive from stream resource
*
* @param resource $handle
* @return ZipFile
* @throws IllegalArgumentException Invalid stream resource
* or resource cannot seekable stream
*/
public static function openFromStream($handle)
{
if (!is_resource($handle)) {
throw new IllegalArgumentException("Invalid stream resource.");
}
$meta = stream_get_meta_data($handle);
if (!$meta['seekable']) {
throw new IllegalArgumentException("Resource cannot seekable stream.");
}
$zipFile = new self();
$stats = fstat($handle);
if (isset($stats['size'])) {
$zipFile->length = $stats['size'];
}
$zipFile->checkZipFileSignature($handle);
$numEntries = $zipFile->findCentralDirectory($handle);
$zipFile->mountCentralDirectory($handle, $numEntries);
if ($zipFile->preamble + $zipFile->postamble >= $zipFile->length) {
assert(0 === $numEntries);
$zipFile->checkZipFileSignature($handle);
}
assert(null !== $handle);
assert(null !== $zipFile->charset);
assert(null !== $zipFile->entries);
assert(null !== $zipFile->mapper);
$zipFile->inputStream = $handle;
// Do NOT close stream!
return $zipFile;
}
/**
* Check zip file signature
*
* @param resource $handle
* @throws ZipException if this not .ZIP file.
*/
private function checkZipFileSignature($handle)
{
rewind($handle);
$signature = current(unpack('V', fread($handle, 4)));
// Constraint: A ZIP file must start with a Local File Header
// or a (ZIP64) End Of Central Directory Record if it's empty.
if (self::LOCAL_FILE_HEADER_SIG !== $signature && self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature && self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
) {
throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
}
}
/**
* Positions the file pointer at the first Central File Header.
* Performs some means to check that this is really a ZIP file.
*
* @param resource $handle
* @return int
* @throws ZipException If the file is not compatible to the ZIP File
* Format Specification.
*/
private function findCentralDirectory($handle)
{
// Search for End of central directory record.
$max = $this->length - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
$min = $max >= 0xffff ? $max - 0xffff : 0;
for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
fseek($handle, $endOfCentralDirRecordPos, SEEK_SET);
// end of central dir signature 4 bytes (0x06054b50)
if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== current(unpack('V', fread($handle, 4))))
continue;
// Process End Of Central Directory Record.
$data = fread($handle, self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 4);
/**
* @var int $diskNo number of this disk - 2 bytes
* @var int $cdDiskNo number of the disk with the start of the
* central directory - 2 bytes
* @var int $cdEntriesDisk total number of entries in the central
* directory on this disk - 2 bytes
* @var int $cdEntries total number of entries in the central
* directory - 2 bytes
* @var int $cdSize size of the central directory - 4 bytes
* @var int $cdPos offset of start of central directory with
* respect to the starting disk number - 4 bytes
* @var int $commentLen ZIP file comment length - 2 bytes
*/
$unpack = unpack('vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLen', $data);
extract($unpack);
if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
throw new ZipException(
"ZIP file spanning/splitting is not supported!"
);
}
// .ZIP file comment (variable size)
if (0 < $commentLen) {
$this->comment = fread($handle, $commentLen);
}
$this->preamble = $endOfCentralDirRecordPos;
$this->postamble = $this->length - ftell($handle);
// Check for ZIP64 End Of Central Directory Locator.
$endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
fseek($handle, $endOfCentralDirLocatorPos, SEEK_SET);
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
if (
0 > $endOfCentralDirLocatorPos ||
ftell($handle) === $this->length ||
self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== current(unpack('V', fread($handle, 4)))
) {
// Seek and check first CFH, probably requiring an offset mapper.
$offset = $endOfCentralDirRecordPos - $cdSize;
fseek($handle, $offset, SEEK_SET);
$offset -= $cdPos;
if (0 !== $offset) {
$this->mapper = new OffsetPositionMapper($offset);
}
return (int)$cdEntries;
}
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
$zip64EndOfCentralDirectoryRecordDisk = current(unpack('V', fread($handle, 4)));
// relative offset of the zip64
// end of central directory record 8 bytes
$zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($handle, 8));
// total number of disks 4 bytes
$totalDisks = current(unpack('V', fread($handle, 4)));
if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
throw new ZipException("ZIP file spanning/splitting is not supported!");
}
fseek($handle, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
$zip64EndOfCentralDirSig = current(unpack('V', fread($handle, 4)));
if (self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $zip64EndOfCentralDirSig) {
throw new ZipException("Expected ZIP64 End Of Central Directory Record!");
}
// size of zip64 end of central
// directory record 8 bytes
// version made by 2 bytes
// version needed to extract 2 bytes
fseek($handle, 12, SEEK_CUR);
// number of this disk 4 bytes
$diskNo = current(unpack('V', fread($handle, 4)));
// number of the disk with the
// start of the central directory 4 bytes
$cdDiskNo = current(unpack('V', fread($handle, 4)));
// total number of entries in the
// central directory on this disk 8 bytes
$cdEntriesDisk = PackUtil::unpackLongLE(fread($handle, 8));
// total number of entries in the
// central directory 8 bytes
$cdEntries = PackUtil::unpackLongLE(fread($handle, 8));
if (0 !== $diskNo || 0 !== $cdDiskNo || $cdEntriesDisk !== $cdEntries) {
throw new ZipException(
"ZIP file spanning/splitting is not supported!");
}
if ($cdEntries < 0 || 0x7fffffff < $cdEntries) {
throw new ZipException(
"Total Number Of Entries In The Central Directory out of range!");
}
// size of the central directory 8 bytes
//$cdSize = self::getLongLE($channel);
fseek($handle, 8, SEEK_CUR);
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
$cdPos = PackUtil::unpackLongLE(fread($handle, 8));
// zip64 extensible data sector (variable size)
fseek($handle, $cdPos, SEEK_SET);
$this->preamble = $zip64EndOfCentralDirectoryRecordPos;
return (int)$cdEntries;
}
// Start recovering file entries from min.
$this->preamble = $min;
$this->postamble = $this->length - $min;
return 0;
}
/**
* Reads the central directory from the given seekable byte channel
* and populates the internal tables with ZipEntry instances.
*
* The ZipEntry's will know all data that can be obtained from the
* central directory alone, but not the data that requires the local
* file header or additional data to be read.
*
* @param resource $handle Input channel.
* @param int $numEntries Size zip entries.
* @throws ZipException
*/
private function mountCentralDirectory($handle, $numEntries)
{
$numEntries = (int)$numEntries;
$entries = [];
for (; ; $numEntries--) {
// central file header signature 4 bytes (0x02014b50)
if (self::CENTRAL_FILE_HEADER_SIG !== current(unpack('V', fread($handle, 4)))) {
break;
}
// version made by 2 bytes
$versionMadeBy = current(unpack('v', fread($handle, 2)));
// version needed to extract 2 bytes
fseek($handle, 2, SEEK_CUR);
$unpack = unpack('vgpbf/vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/VrawSize/vfileLen/vextraLen/vcommentLen', fread($handle, 26));
// disk number start 2 bytes
// internal file attributes 2 bytes
fseek($handle, 4, SEEK_CUR);
// external file attributes 4 bytes
// relative offset of local header 4 bytes
$unpack2 = unpack('VrawExternalAttributes/VlfhOff', fread($handle, 8));
$utf8 = 0 !== ($unpack['gpbf'] & ZipEntry::GPBF_UTF8);
if ($utf8) {
$this->charset = "UTF-8";
}
// See appendix D of PKWARE's ZIP File Format Specification.
$name = fread($handle, $unpack['fileLen']);
$entry = new ZipEntry($name, $handle);
$entry->setRawPlatform($versionMadeBy >> 8);
$entry->setGeneralPurposeBitFlags($unpack['gpbf']);
$entry->setRawMethod($unpack['rawMethod']);
$entry->setRawTime($unpack['rawTime']);
$entry->setRawCrc($unpack['rawCrc']);
$entry->setRawCompressedSize($unpack['rawCompressedSize']);
$entry->setRawSize($unpack['rawSize']);
$entry->setRawExternalAttributes($unpack2['rawExternalAttributes']);
$entry->setRawOffset($unpack2['lfhOff']); // must be unmapped!
if (0 < $unpack['extraLen']) {
$entry->setRawExtraFields(fread($handle, $unpack['extraLen']));
}
if (0 < $unpack['commentLen']) {
$entry->setComment(fread($handle, $unpack['commentLen']));
}
unset($unpack, $unpack2);
// Re-load virtual offset after ZIP64 Extended Information
// Extra Field may have been parsed, map it to the real
// offset and conditionally update the preamble size from it.
$lfhOff = $this->mapper->map($entry->getOffset());
if ($lfhOff < $this->preamble) {
$this->preamble = $lfhOff;
}
$entries[$entry->getName()] = $entry;
}
if (0 !== $numEntries % 0x10000) {
throw new ZipException("Expected " . abs($numEntries) .
($numEntries > 0 ? " more" : " less") .
" entries in the Central Directory!");
}
$this->entries = $entries;
}
/**
* Open zip archive from raw string data.
*
* @param string $data
* @return ZipFile
* @throws IllegalArgumentException if data not available.
* @throws ZipException if can't open temp stream.
*/
public static function openFromString($data)
{
if (null === $data || strlen($data) === 0) {
throw new IllegalArgumentException("Data not available");
}
if (!($handle = fopen('php://temp', 'r+b'))) {
throw new ZipException("Can't open temp stream.");
}
fwrite($handle, $data);
rewind($handle);
$zipFile = self::openFromStream($handle);
$zipFile->length = strlen($data);
return $zipFile;
}
/**
* Returns the number of entries in this ZIP file.
*
* @return int
*/
public function count()
{
return sizeof($this->entries);
}
/**
* Returns the list files.
*
* @return string[]
*/
public function getListFiles()
{
return array_keys($this->entries);
}
/**
* @api
* @return ZipEntry[]
*/
public function getRawEntries()
{
return $this->entries;
}
/**
* Checks whether a entry exists
*
* @param string $entryName
* @return bool
*/
public function hasEntry($entryName)
{
return isset($this->entries[$entryName]);
}
/**
* Check whether the directory entry.
* Returns true if and only if this ZIP entry represents a directory entry
* (i.e. end with '/').
*
* @param string $entryName
* @return bool
* @throws ZipNotFoundEntry
*/
public function isDirectory($entryName)
{
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
}
return $this->entries[$entryName]->isDirectory();
}
/**
* Set password to all encrypted entries.
*
* @param string $password Password
*/
public function setPassword($password)
{
foreach ($this->entries as $entry) {
if ($entry->isEncrypted()) {
$entry->setPassword($password);
}
}
}
/**
* Set password to concrete zip entry.
*
* @param string $entryName Zip entry name
* @param string $password Password
* @throws ZipNotFoundEntry if don't exist zip entry.
*/
public function setEntryPassword($entryName, $password)
{
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
}
$entry = $this->entries[$entryName];
if ($entry->isEncrypted()) {
$entry->setPassword($password);
}
}
/**
* Returns the file comment.
*
* @return string The file comment.
*/
public function getComment()
{
return null === $this->comment ? '' : $this->decode($this->comment);
}
/**
* Decode charset entry name.
*
* @param string $text
* @return string
*/
private function decode($text)
{
$inCharset = mb_detect_encoding($text, mb_detect_order(), true);
if ($inCharset === $this->charset) return $text;
return iconv($inCharset, $this->charset, $text);
}
/**
* Returns entry comment.
*
* @param string $entryName
* @return string
* @throws ZipNotFoundEntry
*/
public function getEntryComment($entryName)
{
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry("Not found entry " . $entryName);
}
return $this->entries[$entryName]->getComment();
}
/**
* Returns the name of the character set which is effectively used for
* decoding entry names and the file comment.
*
* @return string
*/
public function getCharset()
{
return $this->charset;
}
/**
* Returns the file length of this ZIP file in bytes.
*
* @return int
*/
public function length()
{
return $this->length;
}
/**
* Get info by entry.
*
* @param string|ZipEntry $entryName
* @return ZipInfo
* @throws ZipNotFoundEntry
*/
public function getEntryInfo($entryName)
{
if ($entryName instanceof ZipEntry) {
$entryName = $entryName->getName();
}
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
}
$entry = $this->entries[$entryName];
return new ZipInfo($entry);
}
/**
* Get info by all entries.
*
* @return ZipInfo[]
*/
public function getAllInfo()
{
return array_map([$this, 'getEntryInfo'], $this->entries);
}
/**
* Extract the archive contents
*
* Extract the complete archive or the given files to the specified destination.
*
* @param string $destination Location where to extract the files.
* @param array $entries The entries to extract. It accepts
* either a single entry name or an array of names.
* @return bool
* @throws ZipException
*/
public function extractTo($destination, $entries = null)
{
if ($this->entries === null) {
throw new ZipException("Zip entries not initial");
}
if (!file_exists($destination)) {
throw new ZipException("Destination " . $destination . " not found");
}
if (!is_dir($destination)) {
throw new ZipException("Destination is not directory");
}
if (!is_writable($destination)) {
throw new ZipException("Destination is not writable directory");
}
/**
* @var ZipEntry[] $zipEntries
*/
if (!empty($entries)) {
if (is_string($entries)) {
$entries = (array)$entries;
}
if (is_array($entries)) {
$flipEntries = array_flip($entries);
$zipEntries = array_filter($this->entries, function ($zipEntry) use ($flipEntries) {
/**
* @var ZipEntry $zipEntry
*/
return isset($flipEntries[$zipEntry->getName()]);
});
}
} else {
$zipEntries = $this->entries;
}
$extract = 0;
foreach ($zipEntries AS $entry) {
$file = $destination . DIRECTORY_SEPARATOR . $entry->getName();
if ($entry->isDirectory()) {
if (!is_dir($file)) {
if (!mkdir($file, 0755, true)) {
throw new ZipException("Can not create dir " . $file);
}
chmod($file, 0755);
touch($file, $entry->getTime());
}
continue;
}
$dir = dirname($file);
if (!file_exists($dir)) {
if (!mkdir($dir, 0755, true)) {
throw new ZipException("Can not create dir " . $dir);
}
chmod($dir, 0755);
touch($file, $entry->getTime());
}
if (file_put_contents($file, $this->getEntryContent($entry->getName())) === null) {
return false;
}
touch($file, $entry->getTime());
$extract++;
}
return $extract > 0;
}
/**
* Returns an string content of the given entry.
*
* @param string $entryName
* @return string|null
* @throws ZipException
*/
public function getEntryContent($entryName)
{
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
}
$entry = $this->entries[$entryName];
$pos = $entry->getOffset();
assert(ZipEntry::UNKNOWN !== $pos);
$startPos = $pos = $this->mapper->map($pos);
fseek($this->inputStream, $pos, SEEK_SET);
$localFileHeaderSig = current(unpack('V', fread($this->inputStream, 4)));
if (self::LOCAL_FILE_HEADER_SIG !== $localFileHeaderSig) {
throw new ZipException($entry->getName() . " (expected Local File Header)");
}
fseek($this->inputStream, $pos + self::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS, SEEK_SET);
$unpack = unpack('vfileLen/vextraLen', fread($this->inputStream, 4));
$pos += self::LOCAL_FILE_HEADER_MIN_LEN + $unpack['fileLen'] + $unpack['extraLen'];
assert(ZipEntry::UNKNOWN !== $entry->getCrc());
$check = $entry->isEncrypted();
$method = $entry->getMethod();
$password = $entry->getPassword();
if ($entry->isEncrypted() && empty($password)) {
throw new ZipException("Not set password");
}
// Strong Encryption Specification - WinZip AES
if ($entry->isEncrypted() && ZipEntry::WINZIP_AES === $method) {
fseek($this->inputStream, $pos, SEEK_SET);
$winZipAesEngine = new WinZipAesEngine($entry);
$content = $winZipAesEngine->decrypt($this->inputStream);
// Disable redundant CRC-32 check.
$check = false;
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
$method = $field->getMethod();
$entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_WINZIP_AES);
} else {
// Get raw entry content
$content = stream_get_contents($this->inputStream, $entry->getCompressedSize(), $pos);
// Traditional PKWARE Decryption
if ($entry->isEncrypted()) {
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
$content = $zipCryptoEngine->decrypt($content);
$entry->setEncryptionMethod(ZipEntry::ENCRYPTION_METHOD_TRADITIONAL);
}
}
if ($check) {
// Check CRC32 in the Local File Header or Data Descriptor.
$localCrc = null;
if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
// The CRC32 is in the Data Descriptor after the compressed
// size.
// Note the Data Descriptor's Signature is optional:
// All newer apps should write it (and so does TrueVFS),
// but older apps might not.
fseek($this->inputStream, $pos + $entry->getCompressedSize(), SEEK_SET);
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
if (self::DATA_DESCRIPTOR_SIG === $localCrc) {
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
}
} else {
fseek($this->inputStream, $startPos + 14, SEEK_SET);
// The CRC32 in the Local File Header.
$localCrc = current(unpack('V', fread($this->inputStream, 4)));
}
if ($entry->getCrc() !== $localCrc) {
throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
}
}
switch ($method) {
case ZipEntry::METHOD_STORED:
break;
case ZipEntry::METHOD_DEFLATED:
$content = gzinflate($content);
break;
case ZipEntry::METHOD_BZIP2:
if (!extension_loaded('bz2')) {
throw new ZipException('Extension bzip2 not install');
}
$content = bzdecompress($content);
break;
default:
throw new ZipUnsupportMethod($entry->getName()
. " (compression method "
. $method
. " is not supported)");
}
if ($check) {
$localCrc = crc32($content);
if ($entry->getCrc() !== $localCrc) {
if ($entry->isEncrypted()) {
throw new ZipCryptoException("Wrong password");
}
throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc);
}
}
return $content;
}
/**
* Release all resources
*/
function __destruct()
{
$this->close();
}
/**
* Close zip archive and release input stream.
*/
public function close()
{
$this->length = null;
if ($this->inputStream !== null) {
fclose($this->inputStream);
$this->inputStream = null;
}
}
/**
* Whether a offset exists
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
* @param string $entryName An offset to check for.
* @return boolean true on success or false on failure.
* The return value will be casted to boolean if non-boolean was returned.
*/
public function offsetExists($entryName)
{
return isset($this->entries[$entryName]);
}
/**
* Offset to retrieve
* @link http://php.net/manual/en/arrayaccess.offsetget.php
* @param string $entryName The offset to retrieve.
* @return string|null
*/
public function offsetGet($entryName)
{
return $this->offsetExists($entryName) ? $this->getEntryContent($entryName) : null;
}
/**
* Offset to set
* @link http://php.net/manual/en/arrayaccess.offsetset.php
* @param string $entryName The offset to assign the value to.
* @param mixed $value The value to set.
* @throws ZipUnsupportMethod
*/
public function offsetSet($entryName, $value)
{
throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.');
}
/**
* Offset to unset
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
* @param string $entryName The offset to unset.
* @throws ZipUnsupportMethod
*/
public function offsetUnset($entryName)
{
throw new ZipUnsupportMethod('Zip-file is read-only. This operation is prohibited.');
}
/**
* Return the current element
* @link http://php.net/manual/en/iterator.current.php
* @return mixed Can return any type.
* @since 5.0.0
*/
public function current()
{
return $this->offsetGet($this->key());
}
/**
* Move forward to next element
* @link http://php.net/manual/en/iterator.next.php
* @return void Any returned value is ignored.
* @since 5.0.0
*/
public function next()
{
next($this->entries);
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
* @return mixed scalar on success, or null on failure.
* @since 5.0.0
*/
public function key()
{
return key($this->entries);
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
* @return boolean The return value will be casted to boolean and then evaluated.
* Returns true on success or false on failure.
* @since 5.0.0
*/
public function valid()
{
return $this->offsetExists($this->key());
}
/**
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
* @return void Any returned value is ignored.
* @since 5.0.0
*/
public function rewind()
{
reset($this->entries);
}
}

File diff suppressed because it is too large Load Diff

77
src/Util/CryptoUtil.php Normal file
View File

@@ -0,0 +1,77 @@
<?php
namespace PhpZip\Util;
use PhpZip\Exception\RuntimeException;
/**
* Crypto Utils.
*
* @internal
*/
class CryptoUtil
{
/**
* Returns random bytes.
*
* @param int $length
*
* @throws \Exception
*
* @return string
*
* @deprecated Use random_bytes()
*/
final public static function randomBytes($length)
{
return random_bytes($length);
}
/**
* Decrypt AES-CTR.
*
* @param string $data Encrypted data
* @param string $key Aes key
* @param string $iv Aes IV
*
* @return string Raw data
*/
public static function decryptAesCtr($data, $key, $iv)
{
if (\extension_loaded('openssl')) {
$numBits = \strlen($key) * 8;
/** @noinspection PhpComposerExtensionStubsInspection */
return openssl_decrypt($data, 'AES-' . $numBits . '-CTR', $key, \OPENSSL_RAW_DATA, $iv);
}
if (\extension_loaded('mcrypt')) {
return mcrypt_decrypt(\MCRYPT_RIJNDAEL_128, $key, $data, 'ctr', $iv);
}
throw new RuntimeException('Extension openssl or mcrypt not loaded');
}
/**
* Encrypt AES-CTR.
*
* @param string $data Raw data
* @param string $key Aes key
* @param string $iv Aes IV
*
* @return string Encrypted data
*/
public static function encryptAesCtr($data, $key, $iv)
{
if (\extension_loaded('openssl')) {
$numBits = \strlen($key) * 8;
/** @noinspection PhpComposerExtensionStubsInspection */
return openssl_encrypt($data, 'AES-' . $numBits . '-CTR', $key, \OPENSSL_RAW_DATA, $iv);
}
if (\extension_loaded('mcrypt')) {
return mcrypt_encrypt(\MCRYPT_RIJNDAEL_128, $key, $data, 'ctr', $iv);
}
throw new RuntimeException('Extension openssl or mcrypt not loaded');
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace PhpZip\Util;
/**
* Convert unix timestamp values to DOS date/time values and vice versa.
*
* The DOS date/time format is a bitmask:
*
* 24 16 8 0
* +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
* |Y|Y|Y|Y|Y|Y|Y|M| |M|M|M|D|D|D|D|D| |h|h|h|h|h|m|m|m| |m|m|m|s|s|s|s|s|
* +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
* \___________/\________/\_________/ \________/\____________/\_________/
* year month day hour minute second
*
* The year is stored as an offset from 1980.
* Seconds are stored in two-second increments.
* (So if the "second" value is 15, it actually represents 30 seconds.)
*
* @see https://docs.microsoft.com/ru-ru/windows/win32/api/winbase/nf-winbase-filetimetodosdatetime?redirectedfrom=MSDN
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*
* @internal
*/
class DateTimeConverter
{
/**
* Smallest supported DOS date/time value in a ZIP file,
* which is January 1st, 1980 AD 00:00:00 local time.
*/
const MIN_DOS_TIME = 0x210000; // (1 << 21) | (1 << 16)
/**
* Largest supported DOS date/time value in a ZIP file,
* which is December 31st, 2107 AD 23:59:58 local time.
*/
const MAX_DOS_TIME = 0xff9fbf7d; // ((2107 - 1980) << 25) | (12 << 21) | (31 << 16) | (23 << 11) | (59 << 5) | (58 >> 1);
/**
* Convert a 32 bit integer DOS date/time value to a UNIX timestamp value.
*
* @param int $dosTime Dos date/time
*
* @return int Unix timestamp
*/
public static function msDosToUnix($dosTime)
{
if ($dosTime <= self::MIN_DOS_TIME) {
$dosTime = 0;
} elseif ($dosTime > self::MAX_DOS_TIME) {
$dosTime = self::MAX_DOS_TIME;
}
// date_default_timezone_set('UTC');
return mktime(
(($dosTime >> 11) & 0x1f), // hours
(($dosTime >> 5) & 0x3f), // minutes
(($dosTime << 1) & 0x3e), // seconds
(($dosTime >> 21) & 0x0f), // month
(($dosTime >> 16) & 0x1f), // day
((($dosTime >> 25) & 0x7f) + 1980) // year
);
}
/**
* Converts a UNIX timestamp value to a DOS date/time value.
*
* @param int $unixTimestamp the number of seconds since midnight, January 1st,
* 1970 AD UTC
*
* @return int a DOS date/time value reflecting the local time zone and
* rounded down to even seconds
* and is in between DateTimeConverter::MIN_DOS_TIME and DateTimeConverter::MAX_DOS_TIME
*/
public static function unixToMsDos($unixTimestamp)
{
if ($unixTimestamp < 0) {
throw new \InvalidArgumentException('Negative unix timestamp: ' . $unixTimestamp);
}
$date = getdate($unixTimestamp);
$dosTime = (
(($date['year'] - 1980) << 25) |
($date['mon'] << 21) |
($date['mday'] << 16) |
($date['hours'] << 11) |
($date['minutes'] << 5) |
($date['seconds'] >> 1)
);
if ($dosTime <= self::MIN_DOS_TIME) {
$dosTime = 0;
}
return $dosTime;
}
}

108
src/Util/FileAttribUtil.php Normal file
View File

@@ -0,0 +1,108 @@
<?php
namespace PhpZip\Util;
use PhpZip\Constants\DosAttrs;
use PhpZip\Constants\UnixStat;
/**
* Class FileAttribUtil.
*
* @internal
*/
class FileAttribUtil implements DosAttrs, UnixStat
{
/**
* Get DOS mode,.
*
* @param int $xattr
*
* @return string
*/
public static function getDosMode($xattr)
{
$xattr = (int) $xattr;
$mode = (($xattr & self::DOS_DIRECTORY) === self::DOS_DIRECTORY) ? 'd' : '-';
$mode .= (($xattr & self::DOS_ARCHIVE) === self::DOS_ARCHIVE) ? 'a' : '-';
$mode .= (($xattr & self::DOS_READ_ONLY) === self::DOS_READ_ONLY) ? 'r' : '-';
$mode .= (($xattr & self::DOS_HIDDEN) === self::DOS_HIDDEN) ? 'h' : '-';
$mode .= (($xattr & self::DOS_SYSTEM) === self::DOS_SYSTEM) ? 's' : '-';
$mode .= (($xattr & self::DOS_LABEL) === self::DOS_LABEL) ? 'l' : '-';
return $mode;
}
/**
* @param int $permission
*
* @return string
*/
public static function getUnixMode($permission)
{
$mode = '';
$permission = (int) $permission;
switch ($permission & self::UNX_IFMT) {
case self::UNX_IFDIR:
$mode .= 'd';
break;
case self::UNX_IFREG:
$mode .= '-';
break;
case self::UNX_IFLNK:
$mode .= 'l';
break;
case self::UNX_IFBLK:
$mode .= 'b';
break;
case self::UNX_IFCHR:
$mode .= 'c';
break;
case self::UNX_IFIFO:
$mode .= 'p';
break;
case self::UNX_IFSOCK:
$mode .= 's';
break;
default:
$mode .= '?';
break;
}
$mode .= ($permission & self::UNX_IRUSR) ? 'r' : '-';
$mode .= ($permission & self::UNX_IWUSR) ? 'w' : '-';
if ($permission & self::UNX_IXUSR) {
$mode .= ($permission & self::UNX_ISUID) ? 's' : 'x';
} else {
$mode .= ($permission & self::UNX_ISUID) ? 'S' : '-'; // S==undefined
}
$mode .= ($permission & self::UNX_IRGRP) ? 'r' : '-';
$mode .= ($permission & self::UNX_IWGRP) ? 'w' : '-';
if ($permission & self::UNX_IXGRP) {
$mode .= ($permission & self::UNX_ISGID) ? 's' : 'x';
} // == self::UNX_ENFMT
else {
$mode .= ($permission & self::UNX_ISGID) ? 'S' : '-';
} // SunOS 4.1.x
$mode .= ($permission & self::UNX_IROTH) ? 'r' : '-';
$mode .= ($permission & self::UNX_IWOTH) ? 'w' : '-';
if ($permission & self::UNX_IXOTH) {
$mode .= ($permission & self::UNX_ISVTX) ? 't' : 'x';
} // "sticky bit"
else {
$mode .= ($permission & self::UNX_ISVTX) ? 'T' : '-';
} // T==undefined
return $mode;
}
}

466
src/Util/FilesUtil.php Normal file
View File

@@ -0,0 +1,466 @@
<?php
namespace PhpZip\Util;
use PhpZip\Util\Iterator\IgnoreFilesFilterIterator;
use PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator;
/**
* Files util.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*
* @internal
*/
final class FilesUtil
{
/**
* Is empty directory.
*
* @param string $dir Directory
*
* @return bool
*/
public static function isEmptyDir($dir)
{
if (!is_readable($dir)) {
return false;
}
return \count(scandir($dir)) === 2;
}
/**
* Remove recursive directory.
*
* @param string $dir directory path
*/
public static function removeDir($dir)
{
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileInfo) {
$function = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
$function($fileInfo->getRealPath());
}
rmdir($dir);
}
/**
* Convert glob pattern to regex pattern.
*
* @param string $globPattern
*
* @return string
*/
public static function convertGlobToRegEx($globPattern)
{
// Remove beginning and ending * globs because they're useless
$globPattern = trim($globPattern, '*');
$escaping = false;
$inCurrent = 0;
$chars = str_split($globPattern);
$regexPattern = '';
foreach ($chars as $currentChar) {
switch ($currentChar) {
case '*':
$regexPattern .= ($escaping ? '\\*' : '.*');
$escaping = false;
break;
case '?':
$regexPattern .= ($escaping ? '\\?' : '.');
$escaping = false;
break;
case '.':
case '(':
case ')':
case '+':
case '|':
case '^':
case '$':
case '@':
case '%':
$regexPattern .= '\\' . $currentChar;
$escaping = false;
break;
case '\\':
if ($escaping) {
$regexPattern .= '\\\\';
$escaping = false;
} else {
$escaping = true;
}
break;
case '{':
if ($escaping) {
$regexPattern .= '\\{';
} else {
$regexPattern = '(';
$inCurrent++;
}
$escaping = false;
break;
case '}':
if ($inCurrent > 0 && !$escaping) {
$regexPattern .= ')';
$inCurrent--;
} elseif ($escaping) {
$regexPattern = '\\}';
} else {
$regexPattern = '}';
}
$escaping = false;
break;
case ',':
if ($inCurrent > 0 && !$escaping) {
$regexPattern .= '|';
} elseif ($escaping) {
$regexPattern .= '\\,';
} else {
$regexPattern = ',';
}
break;
default:
$escaping = false;
$regexPattern .= $currentChar;
}
}
return $regexPattern;
}
/**
* Search files.
*
* @param string $inputDir
* @param bool $recursive
* @param array $ignoreFiles
*
* @return array Searched file list
*/
public static function fileSearchWithIgnore($inputDir, $recursive = true, array $ignoreFiles = [])
{
if ($recursive) {
$directoryIterator = new \RecursiveDirectoryIterator($inputDir);
if (!empty($ignoreFiles)) {
$directoryIterator = new IgnoreFilesRecursiveFilterIterator($directoryIterator, $ignoreFiles);
}
$iterator = new \RecursiveIteratorIterator($directoryIterator);
} else {
$directoryIterator = new \DirectoryIterator($inputDir);
if (!empty($ignoreFiles)) {
$directoryIterator = new IgnoreFilesFilterIterator($directoryIterator, $ignoreFiles);
}
$iterator = new \IteratorIterator($directoryIterator);
}
$fileList = [];
foreach ($iterator as $file) {
if ($file instanceof \SplFileInfo) {
$fileList[] = $file->getPathname();
}
}
return $fileList;
}
/**
* Search files from glob pattern.
*
* @param string $globPattern
* @param int $flags
* @param bool $recursive
*
* @return array Searched file list
*/
public static function globFileSearch($globPattern, $flags = 0, $recursive = true)
{
$flags = (int) $flags;
$recursive = (bool) $recursive;
$files = glob($globPattern, $flags);
if (!$recursive) {
return $files;
}
foreach (glob(\dirname($globPattern) . '/*', \GLOB_ONLYDIR | \GLOB_NOSORT) as $dir) {
// Unpacking the argument via ... is supported starting from php 5.6 only
/** @noinspection SlowArrayOperationsInLoopInspection */
$files = array_merge($files, self::globFileSearch($dir . '/' . basename($globPattern), $flags, $recursive));
}
return $files;
}
/**
* Search files from regex pattern.
*
* @param string $folder
* @param string $pattern
* @param bool $recursive
*
* @return array Searched file list
*/
public static function regexFileSearch($folder, $pattern, $recursive = true)
{
if ($recursive) {
$directoryIterator = new \RecursiveDirectoryIterator($folder);
$iterator = new \RecursiveIteratorIterator($directoryIterator);
} else {
$directoryIterator = new \DirectoryIterator($folder);
$iterator = new \IteratorIterator($directoryIterator);
}
$regexIterator = new \RegexIterator($iterator, $pattern, \RegexIterator::MATCH);
$fileList = [];
foreach ($regexIterator as $file) {
if ($file instanceof \SplFileInfo) {
$fileList[] = $file->getPathname();
}
}
return $fileList;
}
/**
* Convert bytes to human size.
*
* @param int $size Size bytes
* @param string|null $unit Unit support 'GB', 'MB', 'KB'
*
* @return string
*/
public static function humanSize($size, $unit = null)
{
if (($unit === null && $size >= 1 << 30) || $unit === 'GB') {
return number_format($size / (1 << 30), 2) . 'GB';
}
if (($unit === null && $size >= 1 << 20) || $unit === 'MB') {
return number_format($size / (1 << 20), 2) . 'MB';
}
if (($unit === null && $size >= 1 << 10) || $unit === 'KB') {
return number_format($size / (1 << 10), 2) . 'KB';
}
return number_format($size) . ' bytes';
}
/**
* Normalizes zip path.
*
* @param string $path Zip path
*
* @return string
*/
public static function normalizeZipPath($path)
{
return implode(
'/',
array_filter(
explode('/', (string) $path),
static function ($part) {
return $part !== '.' && $part !== '..';
}
)
);
}
/**
* Returns whether the file path is an absolute path.
*
* @param string $file A file path
*
* @return bool
*
* @see source symfony filesystem component
*/
public static function isAbsolutePath($file)
{
return strspn($file, '/\\', 0, 1)
|| (
\strlen($file) > 3 && ctype_alpha($file[0])
&& $file[1] === ':'
&& strspn($file, '/\\', 2, 1)
)
|| parse_url($file, \PHP_URL_SCHEME) !== null;
}
/**
* @param string $linkPath
* @param string $target
*
* @return bool
*/
public static function symlink($target, $linkPath)
{
if (\DIRECTORY_SEPARATOR === '\\') {
$linkPath = str_replace('/', '\\', $linkPath);
$target = str_replace('/', '\\', $target);
$abs = null;
if (!self::isAbsolutePath($target)) {
$abs = realpath(\dirname($linkPath) . \DIRECTORY_SEPARATOR . $target);
if (\is_string($abs)) {
$target = $abs;
}
}
}
if (!symlink($target, $linkPath)) {
if (\DIRECTORY_SEPARATOR === '\\' && is_file($target)) {
return copy($target, $linkPath);
}
return false;
}
return true;
}
/**
* @param string $file
*
* @return bool
*/
public static function isBadCompressionFile($file)
{
$badCompressFileExt = [
'dic',
'dng',
'f4v',
'flipchart',
'h264',
'lrf',
'mobi',
'mts',
'nef',
'pspimage',
];
$ext = strtolower(pathinfo($file, \PATHINFO_EXTENSION));
if (\in_array($ext, $badCompressFileExt, true)) {
return true;
}
$mimeType = self::getMimeTypeFromFile($file);
return self::isBadCompressionMimeType($mimeType);
}
/**
* @param string $mimeType
*
* @return bool
*/
public static function isBadCompressionMimeType($mimeType)
{
static $badDeflateCompMimeTypes = [
'application/epub+zip',
'application/gzip',
'application/vnd.debian.binary-package',
'application/vnd.oasis.opendocument.graphics',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.text-master',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.rn-realmedia',
'application/x-7z-compressed',
'application/x-arj',
'application/x-bzip2',
'application/x-hwp',
'application/x-lzip',
'application/x-lzma',
'application/x-ms-reader',
'application/x-rar',
'application/x-rpm',
'application/x-stuffit',
'application/x-tar',
'application/x-xz',
'application/zip',
'application/zlib',
'audio/flac',
'audio/mpeg',
'audio/ogg',
'audio/vnd.dolby.dd-raw',
'audio/webm',
'audio/x-ape',
'audio/x-hx-aac-adts',
'audio/x-m4a',
'audio/x-m4a',
'audio/x-wav',
'image/gif',
'image/heic',
'image/jp2',
'image/jpeg',
'image/png',
'image/vnd.djvu',
'image/webp',
'image/x-canon-cr2',
'video/ogg',
'video/webm',
'video/x-matroska',
'video/x-ms-asf',
'x-epoc/x-sisx-app',
];
if (\in_array($mimeType, $badDeflateCompMimeTypes, true)) {
return true;
}
return false;
}
/**
* @param string $file
*
* @return string
*
* @noinspection PhpComposerExtensionStubsInspection
*/
public static function getMimeTypeFromFile($file)
{
if (\function_exists('mime_content_type')) {
return mime_content_type($file);
}
return 'application/octet-stream';
}
/**
* @param string $contents
*
* @return string
* @noinspection PhpComposerExtensionStubsInspection
*/
public static function getMimeTypeFromString($contents)
{
$contents = (string) $contents;
$finfo = new \finfo(\FILEINFO_MIME);
$mimeType = $finfo->buffer($contents);
if ($mimeType === false) {
$mimeType = 'application/octet-stream';
}
return explode(';', $mimeType)[0];
}
}

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