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

65 Commits
3.0.2 ... 3.1.8

Author SHA1 Message Date
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
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
c49ec503c8 Merge tag '3.0.0' into develop
Tagging version 3.0.0 3.0.0
2017-03-15 15:35:18 +03:00
88 changed files with 8895 additions and 4183 deletions

View File

@@ -1,9 +0,0 @@
engines:
duplication:
enabled: true
config:
languages:
- php
exclude_paths:
- tests/
- vendor/

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 /vendor
*.iml *.iml
/.idea /.idea
/composer.lock /composer.lock
/.php_cs.cache

View File

@@ -4,6 +4,7 @@ php:
- '5.6' - '5.6'
- '7.0' - '7.0'
- '7.1' - '7.1'
- '7.2'
- nightly - nightly
# cache vendor dirs # cache vendor dirs
@@ -12,10 +13,6 @@ cache:
- vendor - vendor
- $HOME/.composer/cache - $HOME/.composer/cache
addons:
code_climate:
repo_token: 486a09d58d663450146c53c81c6c64938bcf3bb0b7c8ddebdc125fe97c18213a
install: install:
- travis_retry composer self-update && composer --version - travis_retry composer self-update && composer --version
- travis_retry composer install --prefer-dist --no-interaction - travis_retry composer install --prefer-dist --no-interaction
@@ -25,8 +22,4 @@ before_script:
script: script:
- composer validate --no-check-lock - composer validate --no-check-lock
- vendor/bin/phpunit -v -c bootstrap.xml --coverage-clover build/logs/clover.xml - vendor/bin/phpunit -v -c phpunit.xml
after_success:
- vendor/bin/test-reporter

View File

@@ -1,3 +1,36 @@
# Changelog
# 3.1.2 (2017-11-17)
- 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.
## 3.1.1 (2017-11-15)
- Fix resave zip aligned archive
## 3.1.0 (2017-11-14)
- Added class `ZipModel` for all changes.
- All manipulations with incoming and outgoing streams are in separate files: `ZipInputStream` and `ZipOutputStream`.
- Removed class `CentralDirectory`.
- Optimized extra fields classes.
- Fixed issue #4 (`count()` returns 0 when files are added in directories).
- Implemented issue #8 - support inline Content-Disposition and empty output filename.
- Optimized and tested on a php 32-bit platform (issue #5).
- Added output as PSR-7 Response.
- Added methods for canceling changes.
- Added [russian documentation](README.RU.md).
- Updated [documentation](README.md).
- Declared deprecated methods:
+ rename `ZipFile::withReadPassword` to `ZipFile::setReadPassword`
+ rename `ZipFile::withNewPassword` to `ZipFile::setPassword`
+ rename `ZipFile::withoutPassword` to `ZipFile::disableEncryption`
## 3.0.3 (2017-11-11)
Fix bug issue #8 - Error if the file is empty.
## 3.0.0 (2017-03-15) ## 3.0.0 (2017-03-15)
Merge `ZipOutputFile` with ZipFile and optimize the zip archive update. Merge `ZipOutputFile` with ZipFile and optimize the zip archive update.

829
README.RU.md Normal file
View File

@@ -0,0 +1,829 @@
`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)
[![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::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
$count = count($zipFile);
// или
$count = $zipFile->count();
```
<a name="Documentation-ZipFile-getListFiles"></a> **ZipFile::getListFiles** - возвращает список файлов архива.
```php
$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';
$contents = $zipFile[$entryName];
// или
$contents = $zipFile->getEntryContents($entryName);
```
<a name="Documentation-ZipFile-hasEntry"></a> **ZipFile::hasEntry** - проверяет, присутствует ли запись в архиве.
```php
// $entryName = 'path/to/example-entry-name.txt';
$hasEntry = isset($zipFile[$entryName]);
// или
$hasEntry = $zipFile->hasEntry($entryName);
```
<a name="Documentation-ZipFile-isDirectory"></a> **ZipFile::isDirectory** - проверяет, является ли запись в архиве директорией.
```php
// $entryName = 'path/to/';
$isDirectory = $zipFile->isDirectory($entryName);
```
<a name="Documentation-ZipFile-extractTo"></a> **ZipFile::extractTo** - извлекает содержимое архива в заданную директорию.
Директория должна существовать.
```php
$zipFile->extractTo($directory);
```
Можно извлечь только некоторые записи в заданную директорию.
Директория должна существовать.
```php
$extractOnlyFiles = [
"filename1",
"filename2",
"dir/dir/dir/"
];
$zipFile->extractTo($directory, $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
$zipInfo = $zipFile->getEntryInfo('file.txt');
$arrayInfo = $zipInfo->toArray();
// Пример содержимого массива:
// array (
// 'name' => 'file.gif',
// 'folder' => false,
// 'size' => '43',
// 'compressed_size' => '43',
// 'modified' => 1510489440,
// 'created' => null,
// 'accessed' => null,
// 'attributes' => '-rw-r--r--',
// 'encrypted' => false,
// 'encryption_method' => 0,
// 'comment' => '',
// 'crc' => 782934147,
// 'method_name' => 'No compression',
// 'compression_method' => 0,
// 'platform' => 'UNIX',
// 'version' => 10,
// )
print_r($zipInfo);
// Вывод:
//PhpZip\Model\ZipInfo Object
//(
// [name:PhpZip\Model\ZipInfo:private] => file.gif
// [folder:PhpZip\Model\ZipInfo:private] =>
// [size:PhpZip\Model\ZipInfo:private] => 43
// [compressedSize:PhpZip\Model\ZipInfo:private] => 43
// [mtime:PhpZip\Model\ZipInfo:private] => 1510489324
// [ctime:PhpZip\Model\ZipInfo:private] =>
// [atime:PhpZip\Model\ZipInfo:private] =>
// [encrypted:PhpZip\Model\ZipInfo:private] =>
// [comment:PhpZip\Model\ZipInfo:private] =>
// [crc:PhpZip\Model\ZipInfo:private] => 782934147
// [methodName:PhpZip\Model\ZipInfo:private] => No compression
// [compressionMethod:PhpZip\Model\ZipInfo:private] => 0
// [platform:PhpZip\Model\ZipInfo:private] => UNIX
// [version:PhpZip\Model\ZipInfo:private] => 10
// [attributes:PhpZip\Model\ZipInfo:private] => -rw-r--r--
// [encryptionMethod:PhpZip\Model\ZipInfo:private] => 0
// [compressionLevel:PhpZip\Model\ZipInfo:private] => -1
//)
echo $zipInfo;
// Вывод:
// PhpZip\Model\ZipInfo {Name="file.gif", Size="43 bytes", Compressed size="43 bytes", Modified time="2017-11-12T15:22:04+03:00", Crc=0x2eaaa083, Method name="No compression", Attributes="-rw-r--r--", Platform="UNIX", Version=10}
```
<a name="Documentation-ZipFile-getAllInfo"></a> **ZipFile::getAllInfo** - возвращает подробную информацию обо всех записях в архиве.
```php
$zipAllInfo = $zipFile->getAllInfo();
print_r($zipAllInfo);
//Array
//(
// [file.txt] => PhpZip\Model\ZipInfo Object
// (
// ...
// )
//
// [file2.txt] => PhpZip\Model\ZipInfo Object
// (
// ...
// )
//
// ...
//)
```
#### <a name="Documentation-Add-Zip-Entries"></a> Добавление записей в архив
Все методы добавления записей в ZIP-архив позволяют указать метод сжатия содержимого.
Доступны следующие методы сжатия:
- `\PhpZip\ZipFile::METHOD_STORED` - без сжатия
- `\PhpZip\ZipFile::METHOD_DEFLATED` - Deflate сжатие
- `\PhpZip\ZipFile::METHOD_BZIP2` - Bzip2 сжатие при наличии расширения `ext-bz2`
<a name="Documentation-ZipFile-addFile"></a> **ZipFile::addFile** - добавляет в ZIP-архив файл по указанному пути из файловой системы.
```php
// $file = '...../file.ext';
$zipFile->addFile($file);
// можно указать имя записи в архиве (если null, то используется последний компонент из имени файла)
$zipFile->addFile($file, $entryName);
// или
$zipFile[$entryName] = new \SplFileInfo($file);
// можно указать метод сжатия
$zipFile->addFile($file, $entryName, ZipFile::METHOD_STORED); // Без сжатия
$zipFile->addFile($file, $entryName, ZipFile::METHOD_DEFLATED); // Deflate сжатие
$zipFile->addFile($file, $entryName, ZipFile::METHOD_BZIP2); // BZIP2 сжатие
```
<a name="Documentation-ZipFile-addFromString"></a> **ZipFile::addFromString** - добавляет файл в ZIP-архив, используя его содержимое в виде строки.
```php
$zipFile[$entryName] = $contents;
// или
$zipFile->addFromString($entryName, $contents);
// можно указать метод сжатия
$zipFile->addFromString($entryName, $contents, ZipFile::METHOD_STORED); // Без сжатия
$zipFile->addFromString($entryName, $contents, ZipFile::METHOD_DEFLATED); // Deflate сжатие
$zipFile->addFromString($entryName, $contents, ZipFile::METHOD_BZIP2); // BZIP2 сжатие
```
<a name="Documentation-ZipFile-addFromStream"></a> **ZipFile::addFromStream** - добавляет в ZIP-архив запись из потока.
```php
// $stream = fopen(..., 'rb');
$zipFile->addFromStream($stream, $entryName);
// можно указать метод сжатия
$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_STORED); // Без сжатия
$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_DEFLATED); // Deflate сжатие
$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_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, ZipFile::METHOD_STORED); // Без сжатия
$zipFile->addDir($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate сжатие
$zipFile->addDir($dirName, $localPath, ZipFile::METHOD_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, ZipFile::METHOD_STORED); // Без сжатия
$zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate сжатие
$zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_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, ZipFile::METHOD_STORED); // Без сжатия
$zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_DEFLATED); // Deflate сжатие
$zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_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, ZipFile::METHOD_STORED); // Без сжатия
$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate сжатие
$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, ZipFile::METHOD_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, ZipFile::METHOD_STORED); // Без сжатия
$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate сжатие
$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, ZipFile::METHOD_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, ZipFile::METHOD_STORED); // Без сжатия
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate сжатие
$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_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, ZipFile::METHOD_STORED); // Без сжатия
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate сжатие
$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_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** - устанавливает уровень сжатия для всех файлов, находящихся в архиве.
> _Обратите внимание, что действие данного метода не распространяется на записи, добавленные после выполнения этого метода._
По умолчанию используется уровень сжатия -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) или уровень сжатия, определённый в архиве для Deflate сжатия.
Поддерживаются значения -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) и диапазон от 1 (`\PhpZip\ZipFile::LEVEL_BEST_SPEED`) до 9 (`\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION`). Чем выше число, тем лучше и дольше сжатие.
```php
$zipFile->setCompressionLevel(\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION);
```
<a name="Documentation-ZipFile-setCompressionLevelEntry"></a> **ZipFile::setCompressionLevelEntry** - устанавливает уровень сжатия для определённой записи в архиве.
Поддерживаются значения -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) и диапазон от 1 (`\PhpZip\ZipFile::LEVEL_BEST_SPEED`) до 9 (`\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION`). Чем выше число, тем лучше и дольше сжатие.
```php
$zipFile->setCompressionLevelEntry($entryName, \PhpZip\ZipFile::LEVEL_BEST_COMPRESSION);
```
<a name="Documentation-ZipFile-setCompressionMethodEntry"></a> **ZipFile::setCompressionMethodEntry** - устанавливает метод сжатия для определённой записи в архиве.
Доступны следующие методы сжатия:
- `\PhpZip\ZipFile::METHOD_STORED` - без сжатия
- `\PhpZip\ZipFile::METHOD_DEFLATED` - Deflate сжатие
- `\PhpZip\ZipFile::METHOD_BZIP2` - Bzip2 сжатие при наличии расширения `ext-bz2`
```php
$zipFile->setCompressionMethodEntry($entryName, ZipFile::METHOD_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\ZipFile::ENCRYPTION_METHOD_TRADITIONAL` - Traditional PKWARE encryption
- `\PhpZip\ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256` - WinZip AES encryption 256 bit (рекомендуемое)
- `\PhpZip\ZipFile::ENCRYPTION_METHOD_WINZIP_AES_192` - WinZip AES encryption 192 bit
- `\PhpZip\ZipFile::ENCRYPTION_METHOD_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 = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256;
$zipFile->setPassword($password, $encryptionMethod);
```
<a name="Documentation-ZipFile-setPasswordEntry"></a> **ZipFile::setPasswordEntry** - устанавливает новый пароль для конкретного файла.
```php
$zipFile->setPasswordEntry($entryName, $password);
```
Можно установить метод шифрования:
```php
$encryptionMethod = ZipFile::ENCRYPTION_METHOD_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> История изменений
[Ссылка на Changelog](CHANGELOG.md)
### <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`

752
README.md

File diff suppressed because it is too large Load Diff

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,44 +1,49 @@
{ {
"name": "nelexa/zip", "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",
"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": [ "keywords": [
"zip", "zip",
"unzip", "unzip",
"archive", "archive",
"extract", "extract",
"winzip", "winzip",
"zipalign" "zipalign",
], "ziparchive"
"require-dev": { ],
"phpunit/phpunit": "4.8", "homepage": "https://github.com/Ne-Lexa/php-zip",
"codeclimate/php-test-reporter": "^0.4.4" "license": "MIT",
}, "authors": [
"license": "MIT", {
"authors": [ "name": "Ne-Lexa",
{ "email": "alexey@nelexa.ru",
"name": "Ne-Lexa", "role": "Developer"
"email": "alexey@nelexa.ru", }
"role": "Developer" ],
} "require": {
], "ext-zlib": "*",
"minimum-stability": "stable", "php": "^5.5 || ^7.0",
"require": { "psr/http-message": "^1.0"
"php": "^5.5 || ^7.0" },
}, "require-dev": {
"autoload": { "phpunit/phpunit": "~4.8|~5.7",
"psr-4": { "zendframework/zend-diactoros": "^1.4"
"PhpZip\\": "src/PhpZip" },
} "autoload": {
}, "psr-4": {
"autoload-dev": { "PhpZip\\": "src/PhpZip"
"psr-4": { }
"PhpZip\\": "tests/PhpZip" },
} "autoload-dev": {
}, "psr-4": {
"suggest": { "PhpZip\\": "tests/PhpZip"
"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" "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"
} }

23
phpunit.xml Normal file
View File

@@ -0,0 +1,23 @@
<?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="vendor/autoload.php">
<php>
<ini name="error_reporting" value="-1"/>
</php>
<testsuites>
<testsuite name="PhpZip test suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -1,10 +1,12 @@
<?php <?php
namespace PhpZip\Crypto; namespace PhpZip\Crypto;
use PhpZip\Exception\ZipAuthenticationException; use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Exception\ZipCryptoException; use PhpZip\Exception\ZipCryptoException;
use PhpZip\Model\ZipEntry; use PhpZip\Model\ZipEntry;
use PhpZip\Util\CryptoUtil; use PhpZip\Util\CryptoUtil;
use PhpZip\Util\PackUtil;
/** /**
* Traditional PKWARE Encryption Engine. * Traditional PKWARE Encryption Engine.
@@ -13,7 +15,7 @@ use PhpZip\Util\CryptoUtil;
* @author Ne-Lexa alexey@nelexa.ru * @author Ne-Lexa alexey@nelexa.ru
* @license MIT * @license MIT
*/ */
class TraditionalPkwareEncryptionEngine implements CryptoEngine class TraditionalPkwareEncryptionEngine implements ZipEncryptionEngine
{ {
/** /**
* Encryption header size * Encryption header size
@@ -66,7 +68,6 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
* @var array * @var array
*/ */
private $keys = []; private $keys = [];
/** /**
* @var ZipEntry * @var ZipEntry
*/ */
@@ -80,7 +81,6 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
public function __construct(ZipEntry $entry) public function __construct(ZipEntry $entry)
{ {
$this->entry = $entry; $this->entry = $entry;
$this->initKeys($entry->getPassword());
} }
/** /**
@@ -107,25 +107,8 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
{ {
$this->keys[0] = self::crc32($this->keys[0], $charAt); $this->keys[0] = self::crc32($this->keys[0], $charAt);
$this->keys[1] = $this->keys[1] + ($this->keys[0] & 0xff); $this->keys[1] = $this->keys[1] + ($this->keys[0] & 0xff);
$this->keys[1] = self::toInt($this->keys[1] * 134775813 + 1); $this->keys[1] = PackUtil::toSignedInt32($this->keys[1] * 134775813 + 1);
$this->keys[2] = self::toInt(self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff)); $this->keys[2] = PackUtil::toSignedInt32(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;
} }
/** /**
@@ -147,7 +130,11 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
*/ */
public function decrypt($content) public function decrypt($content)
{ {
$password = $this->entry->getPassword();
$this->initKeys($password);
$headerBytes = array_values(unpack('C*', substr($content, 0, self::STD_DEC_HDR_SIZE))); $headerBytes = array_values(unpack('C*', substr($content, 0, self::STD_DEC_HDR_SIZE)));
$byte = 0;
foreach ($headerBytes as &$byte) { foreach ($headerBytes as &$byte) {
$byte = ($byte ^ $this->decryptByte()) & 0xff; $byte = ($byte ^ $this->decryptByte()) & 0xff;
$this->updateKeys($byte); $this->updateKeys($byte);
@@ -161,7 +148,10 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
$checkByte = ($this->entry->getCrc() >> 24) & 0xff; $checkByte = ($this->entry->getCrc() >> 24) & 0xff;
} }
if ($byte !== $checkByte) { if ($byte !== $checkByte) {
throw new ZipAuthenticationException("Bad password for entry " . $this->entry->getName()); throw new ZipAuthenticationException(sprintf(
'Invalid password for zip entry "%s"',
$this->entry->getName()
));
} }
$outputContent = ""; $outputContent = "";
@@ -189,6 +179,7 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
* *
* @param string $data * @param string $data
* @return string * @return string
* @throws ZipCryptoException
*/ */
public function encrypt($data) public function encrypt($data)
{ {
@@ -198,7 +189,9 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
$headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE); $headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE);
// Initialize again since the generated bytes were encrypted. // Initialize again since the generated bytes were encrypted.
$this->initKeys($this->entry->getPassword()); $password = $this->entry->getPassword();
$this->initKeys($password);
$headerBytes[self::STD_DEC_HDR_SIZE - 1] = pack('c', ($crc >> 24) & 0xff); $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[self::STD_DEC_HDR_SIZE - 2] = pack('c', ($crc >> 16) & 0xff);
@@ -213,7 +206,7 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
*/ */
private function encryptData($content) private function encryptData($content)
{ {
if (null === $content) { if ($content === null) {
throw new ZipCryptoException('content is null'); throw new ZipCryptoException('content is null');
} }
$buff = ''; $buff = '';
@@ -233,4 +226,4 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine
$this->updateKeys($byte); $this->updateKeys($byte);
return $tempVal; return $tempVal;
} }
} }

View File

@@ -1,20 +1,23 @@
<?php <?php
namespace PhpZip\Crypto; namespace PhpZip\Crypto;
use PhpZip\Exception\RuntimeException; use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipAuthenticationException; use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Exception\ZipCryptoException; use PhpZip\Exception\ZipCryptoException;
use PhpZip\Extra\WinZipAesEntryExtraField; use PhpZip\Exception\ZipException;
use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Model\ZipEntry; use PhpZip\Model\ZipEntry;
use PhpZip\Util\CryptoUtil; use PhpZip\Util\CryptoUtil;
/** /**
* WinZip Aes Encryption Engine. * WinZip Aes Encryption Engine.
* *
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru * @author Ne-Lexa alexey@nelexa.ru
* @license MIT * @license MIT
*/ */
class WinZipAesEngine implements CryptoEngine class WinZipAesEngine implements ZipEncryptionEngine
{ {
/** /**
* The block size of the Advanced Encryption Specification (AES) Algorithm * The block size of the Advanced Encryption Specification (AES) Algorithm
@@ -47,16 +50,20 @@ class WinZipAesEngine implements CryptoEngine
* @return string * @return string
* @throws ZipAuthenticationException * @throws ZipAuthenticationException
* @throws ZipCryptoException * @throws ZipCryptoException
* @throws \PhpZip\Exception\ZipException
*/ */
public function decrypt($content) public function decrypt($content)
{ {
$extraFieldsCollection = $this->entry->getExtraFieldsCollection();
if (!isset($extraFieldsCollection[WinZipAesEntryExtraField::getHeaderId()])) {
throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)");
}
/** /**
* @var WinZipAesEntryExtraField $field * @var WinZipAesEntryExtraField $field
*/ */
$field = $this->entry->getExtraField(WinZipAesEntryExtraField::getHeaderId()); $field = $extraFieldsCollection[WinZipAesEntryExtraField::getHeaderId()];
if (null === $field) {
throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)");
}
// Get key strength. // Get key strength.
$keyStrengthBits = $field->getKeyStrength(); $keyStrengthBits = $field->getKeyStrength();
@@ -124,26 +131,26 @@ class WinZipAesEngine implements CryptoEngine
" (authenticated WinZip AES entry content has been tampered with)"); " (authenticated WinZip AES entry content has been tampered with)");
} }
return self::aesCtrSegmentIntegerCounter(false, $content, $key, $iv); return self::aesCtrSegmentIntegerCounter($content, $key, $iv, false);
} }
/** /**
* Decryption or encryption AES-CTR with Segment Integer Count (SIC). * Decryption or encryption AES-CTR with Segment Integer Count (SIC).
* *
* @param bool $encrypted If true encryption else decryption
* @param string $str Data * @param string $str Data
* @param string $key Key * @param string $key Key
* @param string $iv IV * @param string $iv IV
* @param bool $encrypted If true encryption else decryption
* @return string * @return string
*/ */
private static function aesCtrSegmentIntegerCounter($encrypted = true, $str, $key, $iv) private static function aesCtrSegmentIntegerCounter($str, $key, $iv, $encrypted = true)
{ {
$numOfBlocks = ceil(strlen($str) / 16); $numOfBlocks = ceil(strlen($str) / 16);
$ctrStr = ''; $ctrStr = '';
for ($i = 0; $i < $numOfBlocks; ++$i) { for ($i = 0; $i < $numOfBlocks; ++$i) {
for ($j = 0; $j < 16; ++$j) { for ($j = 0; $j < 16; ++$j) {
$n = ord($iv[$j]); $n = ord($iv[$j]);
if (0x100 === ++$n) { if (++$n === 0x100) {
// overflow, set this one to 0, increment next // overflow, set this one to 0, increment next
$iv[$j] = chr(0); $iv[$j] = chr(0);
} else { } else {
@@ -167,14 +174,16 @@ class WinZipAesEngine implements CryptoEngine
* @param string $key Aes key * @param string $key Aes key
* @param string $iv Aes IV * @param string $iv Aes IV
* @return string Encrypted data * @return string Encrypted data
* @throws RuntimeException
*/ */
private static function encryptCtr($data, $key, $iv) private static function encryptCtr($data, $key, $iv)
{ {
if (extension_loaded("openssl")) { if (extension_loaded("openssl")) {
$numBits = strlen($key) * 8; $numBits = strlen($key) * 8;
/** @noinspection PhpComposerExtensionStubsInspection */
return openssl_encrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv); return openssl_encrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
} elseif (extension_loaded("mcrypt")) { } elseif (extension_loaded("mcrypt")) {
/** @noinspection PhpDeprecationInspection */
/** @noinspection PhpComposerExtensionStubsInspection */
return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv); return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
} else { } else {
throw new RuntimeException('Extension openssl or mcrypt not loaded'); throw new RuntimeException('Extension openssl or mcrypt not loaded');
@@ -188,14 +197,16 @@ class WinZipAesEngine implements CryptoEngine
* @param string $key Aes key * @param string $key Aes key
* @param string $iv Aes IV * @param string $iv Aes IV
* @return string Raw data * @return string Raw data
* @throws RuntimeException
*/ */
private static function decryptCtr($data, $key, $iv) private static function decryptCtr($data, $key, $iv)
{ {
if (extension_loaded("openssl")) { if (extension_loaded("openssl")) {
$numBits = strlen($key) * 8; $numBits = strlen($key) * 8;
/** @noinspection PhpComposerExtensionStubsInspection */
return openssl_decrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv); return openssl_decrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
} elseif (extension_loaded("mcrypt")) { } elseif (extension_loaded("mcrypt")) {
/** @noinspection PhpDeprecationInspection */
/** @noinspection PhpComposerExtensionStubsInspection */
return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv); return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
} else { } else {
throw new RuntimeException('Extension openssl or mcrypt not loaded'); throw new RuntimeException('Extension openssl or mcrypt not loaded');
@@ -207,21 +218,22 @@ class WinZipAesEngine implements CryptoEngine
* *
* @param string $content * @param string $content
* @return string * @return string
* @throws \PhpZip\Exception\ZipException
*/ */
public function encrypt($content) public function encrypt($content)
{ {
// Init key strength. // Init key strength.
$password = $this->entry->getPassword(); $password = $this->entry->getPassword();
assert($password !== null); if ($password === null) {
throw new ZipException('No password was set for the entry "'.$this->entry->getName().'"');
}
// WinZip 99-character limit // WinZip 99-character limit
// @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/ // @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
$password = substr($password, 0, 99); $password = substr($password, 0, 99);
$keyStrengthBytes = 32; $keyStrengthBits = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod($this->entry->getEncryptionMethod());
$keyStrengthBits = $keyStrengthBytes * 8; $keyStrengthBytes = $keyStrengthBits / 8;
assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
$salt = CryptoUtil::randomBytes($keyStrengthBytes / 2); $salt = CryptoUtil::randomBytes($keyStrengthBytes / 2);
@@ -234,7 +246,7 @@ class WinZipAesEngine implements CryptoEngine
$key = substr($keyParam, 0, $keyStrengthBytes); $key = substr($keyParam, 0, $keyStrengthBytes);
$content = self::aesCtrSegmentIntegerCounter(true, $content, $key, $iv); $content = self::aesCtrSegmentIntegerCounter($content, $key, $iv, true);
$mac = hash_hmac('sha1', $content, $sha1HMacParam, true); $mac = hash_hmac('sha1', $content, $sha1HMacParam, true);
@@ -244,4 +256,4 @@ class WinZipAesEngine implements CryptoEngine
substr($mac, 0, 10) substr($mac, 0, 10)
); );
} }
} }

View File

@@ -1,9 +1,17 @@
<?php <?php
namespace PhpZip\Crypto; namespace PhpZip\Crypto;
use PhpZip\Exception\ZipAuthenticationException; use PhpZip\Exception\ZipAuthenticationException;
interface CryptoEngine /**
* Encryption Engine
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipEncryptionEngine
{ {
/** /**
* Decryption string. * Decryption string.
@@ -21,4 +29,4 @@ interface CryptoEngine
* @return string * @return string
*/ */
public function encrypt($content); public function encrypt($content);
} }

View File

@@ -1,4 +1,5 @@
<?php <?php
namespace PhpZip\Exception; namespace PhpZip\Exception;
/** /**
@@ -6,7 +7,7 @@ namespace PhpZip\Exception;
* Central File Header and the Data Descriptor or between the declared value * Central File Header and the Data Descriptor or between the declared value
* and the computed value from the decompressed data. * 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 * @author Ne-Lexa alexey@nelexa.ru
* @license MIT * @license MIT
@@ -36,12 +37,14 @@ class Crc32Exception extends ZipException
*/ */
public function __construct($name, $expected, $actual) public function __construct($name, $expected, $actual)
{ {
parent::__construct($name parent::__construct(
. " (expected CRC32 value 0x" sprintf(
. dechex($expected) "%s (expected CRC32 value 0x%x, but is actually 0x%x)",
. ", but is actually 0x" $name,
. dechex($actual) $expected,
. ")"); $actual
)
);
assert($expected != $actual); assert($expected != $actual);
$this->expectedCrc = $expected; $this->expectedCrc = $expected;
$this->actualCrc = $actual; $this->actualCrc = $actual;
@@ -66,5 +69,4 @@ class Crc32Exception extends ZipException
{ {
return $this->actualCrc; return $this->actualCrc;
} }
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,38 @@
<?php
namespace PhpZip\Exception;
/**
* Thrown if entry not found.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipEntryNotFoundException extends ZipException
{
/**
* @var string
*/
private $entryName;
/**
* ZipEntryNotFoundException constructor.
* @param string $entryName
*/
public function __construct($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 <?php
namespace PhpZip\Exception; namespace PhpZip\Exception;
/** /**
@@ -10,5 +11,4 @@ namespace PhpZip\Exception;
*/ */
class ZipException extends \Exception class ZipException extends \Exception
{ {
}
}

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
<?php
namespace PhpZip\Exception;
class ZipUnsupportMethodException extends RuntimeException
{
}

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 +1,35 @@
<?php <?php
namespace PhpZip\Extra; namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
/** /**
* Abstract base class for an Extra Field in a Local or Central Header of a * Extra Field in a Local or Central Header of a ZIP archive.
* ZIP archive.
* It defines the common properties of all Extra Fields and how to * It defines the common properties of all Extra Fields and how to
* serialize/deserialize them to/from byte arrays. * serialize/deserialize them to/from byte arrays.
* *
* @author Ne-Lexa alexey@nelexa.ru * @author Ne-Lexa alexey@nelexa.ru
* @license MIT * @license MIT
*/ */
abstract class ExtraField implements ExtraFieldHeader interface ExtraField
{ {
/** The Header ID for a ZIP64 Extended Information Extra Field. */
const ZIP64_HEADER_ID = 0x0001;
/** /**
* @var array|null * Returns the Header ID (type) of this Extra Field.
*/ * The Header ID is an unsigned short integer (two bytes)
private static $registry; * which must be constant during the life cycle of this object.
/**
* 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 * @return int
* 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) public static function getHeaderId();
{
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 ($extraField::getHeaderId() !== $headerId) {
throw new ZipException('Runtime error support headerId ' . $headerId);
}
} else {
$extraField = new DefaultExtraField($headerId);
}
return $extraField;
}
/** /**
* Registered extra field classes. * Serializes a Data Block.
* * @return string
* @return array|null
*/ */
private static function getRegistry() public function serialize();
{
if (null === self::$registry) {
self::$registry[WinZipAesEntryExtraField::getHeaderId()] = WinZipAesEntryExtraField::class;
self::$registry[NtfsExtraField::getHeaderId()] = NtfsExtraField::class;
}
return self::$registry;
}
/** /**
* Returns a protective copy of the Data Block. * Initializes this Extra Field by deserializing a Data Block.
* * @param string $data
* @return resource
* @throws ZipException If size data block out of range.
*/ */
public function getDataBlock() public function deserialize($data);
{ }
$size = $this->getDataSize();
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('size data block out of range.');
}
$fp = fopen('php://memory', '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,200 +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://memory', 'r+b');
$offset = 0;
/**
* @var ExtraField $ef
*/
foreach ($this->extra as $ef) {
fwrite($fp, pack('vv', $ef::getHeaderId(), $ef->getDataSize()));
$offset += 4;
fwrite($fp, $ef->writeTo($fp, $offset));
$offset += $ef->getDataSize();
}
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;
}
/**
* 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);
$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

@@ -0,0 +1,244 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\InvalidArgumentException;
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 ExtraFieldsCollection implements \Countable, \ArrayAccess, \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 ExtraField[]
*/
protected $collection = [];
/**
* Returns the number of Extra Fields in this collection.
*
* @return int
*/
public function count()
{
return sizeof($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 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->collection[$headerId])) {
return $this->collection[$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->collection[$headerId] = $extraField;
return $extraField;
}
/**
* 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 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->collection[$headerId])) {
$ef = $this->collection[$headerId];
unset($this->collection[$headerId]);
return $ef;
}
throw new ZipException('ExtraField not found');
}
/**
* Whether a offset exists
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
* @param mixed $offset <p>
* An offset to check for.
* </p>
* @return boolean true on success or false on failure.
* </p>
* <p>
* The return value will be casted to boolean if non-boolean was returned.
* @since 5.0.0
*/
public function offsetExists($offset)
{
return isset($this->collection[$offset]);
}
/**
* Offset to retrieve
* @link http://php.net/manual/en/arrayaccess.offsetget.php
* @param mixed $offset <p>
* The offset to retrieve.
* </p>
* @return mixed Can return all value types.
* @since 5.0.0
* @throws ZipException
*/
public function offsetGet($offset)
{
return $this->get($offset);
}
/**
* Offset to set
* @link http://php.net/manual/en/arrayaccess.offsetset.php
* @param mixed $offset <p>
* The offset to assign the value to.
* </p>
* @param mixed $value <p>
* The value to set.
* </p>
* @return void
* @throws ZipException
* @since 5.0.0
*/
public function offsetSet($offset, $value)
{
if ($value instanceof ExtraField) {
if ($offset !== $value::getHeaderId()) {
throw new InvalidArgumentException("Value header id !== array access key");
}
$this->add($value);
} else {
throw new InvalidArgumentException('value is not instanceof ' . ExtraField::class);
}
}
/**
* Offset to unset
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
* @param mixed $offset <p>
* The offset to unset.
* </p>
* @return void
* @since 5.0.0
* @throws ZipException
*/
public function offsetUnset($offset)
{
$this->remove($offset);
}
/**
* 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 current($this->collection);
}
/**
* 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->collection);
}
/**
* 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->collection);
}
/**
* 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->collection);
}
/**
* If clone extra fields.
*/
public function __clone()
{
foreach ($this->collection as $k => $v) {
$this->collection[$k] = clone $v;
}
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace PhpZip\Extra;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\Fields\ApkAlignmentExtraField;
use PhpZip\Extra\Fields\DefaultExtraField;
use PhpZip\Extra\Fields\JarMarkerExtraField;
use PhpZip\Extra\Fields\NtfsExtraField;
use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Extra\Fields\Zip64ExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\StringUtil;
/**
* Extra Fields Factory
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ExtraFieldsFactory
{
/**
* @var array|null
*/
protected static $registry;
private function __construct()
{
}
/**
* @param string $extra
* @param ZipEntry|null $entry
* @return ExtraFieldsCollection
* @throws ZipException
*/
public static function createExtraFieldCollections($extra, ZipEntry $entry = null)
{
$extraFieldsCollection = new ExtraFieldsCollection();
if ($extra !== null) {
$extraLength = strlen($extra);
if ($extraLength > 0xffff) {
throw new ZipException("Extra Fields too large: " . $extraLength);
}
$pos = 0;
$endPos = $extraLength;
while ($endPos - $pos >= 4) {
$unpack = unpack('vheaderId/vdataSize', substr($extra, $pos, 4));
$pos += 4;
$headerId = (int)$unpack['headerId'];
$dataSize = (int)$unpack['dataSize'];
$extraField = ExtraFieldsFactory::create($headerId);
if ($extraField instanceof Zip64ExtraField && $entry !== null) {
$extraField->setEntry($entry);
}
$extraField->deserialize(substr($extra, $pos, $dataSize));
$pos += $dataSize;
$extraFieldsCollection[$headerId] = $extraField;
}
}
return $extraFieldsCollection;
}
/**
* @param ExtraFieldsCollection $extraFieldsCollection
* @return string
* @throws ZipException
*/
public static function createSerializedData(ExtraFieldsCollection $extraFieldsCollection)
{
$extraData = '';
foreach ($extraFieldsCollection as $extraField) {
$data = $extraField->serialize();
$extraData .= pack('vv', $extraField::getHeaderId(), strlen($data));
$extraData .= $data;
}
$size = strlen($extraData);
if (0x0000 > $size || $size > 0xffff) {
throw new ZipException('Size extra out of range: ' . $size . '. Extra data: ' . $extraData);
}
return $extraData;
}
/**
* 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
*/
protected static function getRegistry()
{
if (self::$registry === null) {
self::$registry[WinZipAesEntryExtraField::getHeaderId()] = WinZipAesEntryExtraField::class;
self::$registry[NtfsExtraField::getHeaderId()] = NtfsExtraField::class;
self::$registry[Zip64ExtraField::getHeaderId()] = Zip64ExtraField::class;
self::$registry[ApkAlignmentExtraField::getHeaderId()] = ApkAlignmentExtraField::class;
self::$registry[JarMarkerExtraField::getHeaderId()] = JarMarkerExtraField::class;
}
return self::$registry;
}
/**
* @return WinZipAesEntryExtraField
*/
public static function createWinZipAesEntryExtra()
{
return new WinZipAesEntryExtraField();
}
/**
* @return NtfsExtraField
*/
public static function createNtfsExtra()
{
return new NtfsExtraField();
}
/**
* @param ZipEntry $entry
* @return Zip64ExtraField
*/
public static function createZip64Extra(ZipEntry $entry)
{
return new Zip64ExtraField($entry);
}
/**
* @param ZipEntry $entry
* @param int $padding
* @return ApkAlignmentExtraField
*/
public static function createApkAlignExtra(ZipEntry $entry, $padding)
{
$padding = (int)$padding;
$multiple = 4;
if (StringUtil::endsWith($entry->getName(), '.so')) {
$multiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
}
$extraField = new ApkAlignmentExtraField();
$extraField->setMultiple($multiple);
$extraField->setPadding($padding);
return $extraField;
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace PhpZip\Extra\Fields;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Extra\ExtraField;
/**
* Apk Alignment Extra Field
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ApkAlignmentExtraField implements ExtraField
{
/**
* Minimum size (in bytes) of the extensible data block/field used
* for alignment of uncompressed entries.
*/
const ALIGNMENT_ZIP_EXTRA_MIN_SIZE_BYTES = 6;
const ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
/**
* @var int
*/
private $multiple;
/**
* @var int
*/
private $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 static function getHeaderId()
{
return 0xD935;
}
/**
* Serializes a Data Block.
* @return string
*/
public function serialize()
{
if ($this->padding > 0) {
$args = array_merge(
['vc*', $this->multiple],
array_fill(2, $this->padding, 0)
);
return call_user_func_array('pack', $args);
}
return pack('v', $this->multiple);
}
/**
* Initializes this Extra Field by deserializing a Data Block.
* @param string $data
*/
public function deserialize($data)
{
$length = strlen($data);
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 InvalidArgumentException("Minimum 6 bytes of the extensible data block/field used for alignment of uncompressed entries.");
}
$this->multiple = unpack('v', $data)[1];
$this->padding = $length - 2;
}
/**
* @return mixed
*/
public function getMultiple()
{
return $this->multiple;
}
/**
* @return int
*/
public function getPadding()
{
return $this->padding;
}
/**
* @param int $multiple
*/
public function setMultiple($multiple)
{
$this->multiple = $multiple;
}
/**
* @param int $padding
*/
public function setPadding($padding)
{
$this->padding = $padding;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace PhpZip\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\ExtraField;
/**
* 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 implements ExtraField
{
/**
* @var int
*/
private static $headerId;
/**
* @var string
*/
protected $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;
}
/**
* Serializes a Data Block.
* @return string
*/
public function serialize()
{
return $this->data;
}
/**
* Initializes this Extra Field by deserializing a Data Block.
* @param string $data
*/
public function deserialize($data)
{
$this->data = $data;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace PhpZip\Extra\Fields;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\ExtraField;
/**
* 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.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class JarMarkerExtraField implements ExtraField
{
/**
* 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 0xCAFE;
}
/**
* Serializes a Data Block.
* @return string
*/
public function serialize()
{
return '';
}
/**
* Initializes this Extra Field by deserializing a Data Block.
* @param string $data
* @throws ZipException
*/
public function deserialize($data)
{
if (strlen($data) !== 0) {
throw new ZipException("JarMarker doesn't expect any data");
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace PhpZip\Extra\Fields;
use PhpZip\Extra\ExtraField;
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 implements 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;
/**
* 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;
}
/**
* Initializes this Extra Field by deserializing a Data Block.
* @param string $data
*/
public function deserialize($data)
{
$unpack = unpack('vtag/vsizeAttr', substr($data, 0, 4));
if ($unpack['sizeAttr'] === 24) {
$tagData = substr($data, 4, $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;
}
}
/**
* Serializes a Data Block.
* @return string
*/
public function serialize()
{
$serialize = '';
if ($this->mtime !== null && $this->atime !== null && $this->ctime !== null) {
$mtimeLong = ($this->mtime + 11644473600) * 10000000;
$atimeLong = ($this->atime + 11644473600) * 10000000;
$ctimeLong = ($this->ctime + 11644473600) * 10000000;
$serialize .= pack('Vvv', 0, 1, 8 * 3)
. PackUtil::packLongLE($mtimeLong)
. PackUtil::packLongLE($atimeLong)
. PackUtil::packLongLE($ctimeLong);
}
return $serialize;
}
/**
* @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,7 +1,10 @@
<?php <?php
namespace PhpZip\Extra;
namespace PhpZip\Extra\Fields;
use PhpZip\Exception\ZipException; use PhpZip\Exception\ZipException;
use PhpZip\Extra\ExtraField;
use PhpZip\ZipFileInterface;
/** /**
* WinZip AES Extra Field. * WinZip AES Extra Field.
@@ -11,7 +14,7 @@ use PhpZip\Exception\ZipException;
* @author Ne-Lexa alexey@nelexa.ru * @author Ne-Lexa alexey@nelexa.ru
* @license MIT * @license MIT
*/ */
class WinZipAesEntryExtraField extends ExtraField class WinZipAesEntryExtraField implements ExtraField
{ {
const DATA_SIZE = 7; const DATA_SIZE = 7;
const VENDOR_ID = 17729; // 'A' | ('E' << 8); const VENDOR_ID = 17729; // 'A' | ('E' << 8);
@@ -32,32 +35,38 @@ class WinZipAesEntryExtraField extends ExtraField
const KEY_STRENGTH_192BIT = 192; const KEY_STRENGTH_192BIT = 192;
const KEY_STRENGTH_256BIT = 256; const KEY_STRENGTH_256BIT = 256;
private static $keyStrengths = [ protected static $keyStrengths = [
self::KEY_STRENGTH_128BIT => 0x01, self::KEY_STRENGTH_128BIT => 0x01,
self::KEY_STRENGTH_192BIT => 0x02, self::KEY_STRENGTH_192BIT => 0x02,
self::KEY_STRENGTH_256BIT => 0x03 self::KEY_STRENGTH_256BIT => 0x03
]; ];
protected static $encryptionMethods = [
self::KEY_STRENGTH_128BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128,
self::KEY_STRENGTH_192BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192,
self::KEY_STRENGTH_256BIT => ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
];
/** /**
* Vendor version. * Vendor version.
* *
* @var int * @var int
*/ */
private $vendorVersion = self::VV_AE_1; protected $vendorVersion = self::VV_AE_1;
/** /**
* Encryption strength. * Encryption strength.
* *
* @var int * @var int
*/ */
private $encryptionStrength = self::KEY_STRENGTH_256BIT; protected $encryptionStrength = self::KEY_STRENGTH_256BIT;
/** /**
* Zip compression method. * Zip compression method.
* *
* @var int * @var int
*/ */
private $method; protected $method;
/** /**
* Returns the Header ID (type) of this Extra Field. * Returns the Header ID (type) of this Extra Field.
@@ -71,21 +80,6 @@ class WinZipAesEntryExtraField extends ExtraField
return 0x9901; 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. * Returns the vendor version.
* *
@@ -125,6 +119,7 @@ class WinZipAesEntryExtraField extends ExtraField
/** /**
* @return bool|int * @return bool|int
* @throws ZipException
*/ */
public function getKeyStrength() public function getKeyStrength()
{ {
@@ -155,6 +150,33 @@ class WinZipAesEntryExtraField extends ExtraField
return $this->method & 0xffff; return $this->method & 0xffff;
} }
/**
* Internal encryption method.
*
* @return int
* @throws ZipException
*/
public function getEncryptionMethod()
{
return isset(self::$encryptionMethods[$this->getKeyStrength()]) ?
self::$encryptionMethods[$this->getKeyStrength()] :
self::$encryptionMethods[self::KEY_STRENGTH_256BIT];
}
/**
* @param int $encryptionMethod
* @return int
* @throws ZipException
*/
public static function getKeyStrangeFromEncryptionMethod($encryptionMethod)
{
$flipKey = array_flip(self::$encryptionMethods);
if (!isset($flipKey[$encryptionMethod])) {
throw new ZipException("Unsupport encryption method " . $encryptionMethod);
}
return $flipKey[$encryptionMethod];
}
/** /**
* Sets compression method. * Sets compression method.
* *
@@ -169,37 +191,6 @@ class WinZipAesEntryExtraField extends ExtraField
$this->method = $compressionMethod; $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. * Set key strength.
* *
@@ -218,19 +209,50 @@ class WinZipAesEntryExtraField extends ExtraField
*/ */
public static function encryptionStrength($keyStrength) public static function encryptionStrength($keyStrength)
{ {
return isset(self::$keyStrengths[$keyStrength]) ? self::$keyStrengths[$keyStrength] : self::$keyStrengths[self::KEY_STRENGTH_128BIT]; return isset(self::$keyStrengths[$keyStrength]) ?
self::$keyStrengths[$keyStrength] :
self::$keyStrengths[self::KEY_STRENGTH_128BIT];
} }
/** /**
* Serializes a Data Block of ExtraField::getDataSize bytes to the * Serializes a Data Block.
* resource $handle at the zero based offset $off. * @return string
*
* @param resource $handle
* @param int $off Offset bytes
*/ */
public function writeTo($handle, $off) public function serialize()
{ {
fseek($handle, $off, SEEK_SET); return pack(
fwrite($handle, pack('vvcv', $this->vendorVersion, self::VENDOR_ID, $this->encryptionStrength, $this->method)); 'vvcv',
$this->vendorVersion,
self::VENDOR_ID,
$this->encryptionStrength,
$this->method
);
} }
}
/**
* Initializes this Extra Field by deserializing a Data Block.
* @param string $data
* @throws ZipException
*/
public function deserialize($data)
{
$size = strlen($data);
if (self::DATA_SIZE !== $size) {
throw new ZipException('WinZip AES Extra data invalid size: ' . $size . '. Must be ' . self::DATA_SIZE);
}
/**
* @var int $vendorVersion
* @var int $vendorId
* @var int $keyStrength
* @var int $method
*/
$unpack = unpack('vvendorVersion/vvendorId/ckeyStrength/vmethod', $data);
$this->setVendorVersion($unpack['vendorVersion']);
if (self::VENDOR_ID !== $unpack['vendorId']) {
throw new ZipException('Vendor id invalid: ' . $unpack['vendorId'] . '. Must be ' . self::VENDOR_ID);
}
$this->setKeyStrength(self::keyStrength($unpack['keyStrength'])); // checked
$this->setMethod($unpack['method']);
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace PhpZip\Extra\Fields;
use PhpZip\Exception\RuntimeException;
use PhpZip\Extra\ExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\PackUtil;
/**
* ZIP64 Extra Field
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class Zip64ExtraField implements ExtraField
{
/** The Header ID for a ZIP64 Extended Information Extra Field. */
const ZIP64_HEADER_ID = 0x0001;
/**
* @var ZipEntry
*/
protected $entry;
/**
* Zip64ExtraField constructor.
* @param ZipEntry $entry
*/
public function __construct(ZipEntry $entry = null)
{
if ($entry !== null) {
$this->setEntry($entry);
}
}
/**
* @param ZipEntry $entry
*/
public function setEntry(ZipEntry $entry)
{
$this->entry = $entry;
}
/**
* 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 0x0001;
}
/**
* Serializes a Data Block.
* @return string
*/
public function serialize()
{
if ($this->entry === null) {
throw new RuntimeException("entry is null");
}
$data = '';
// Write out Uncompressed Size.
$size = $this->entry->getSize();
if (0xffffffff <= $size) {
$data .= PackUtil::packLongLE($size);
}
// Write out Compressed Size.
$compressedSize = $this->entry->getCompressedSize();
if (0xffffffff <= $compressedSize) {
$data .= PackUtil::packLongLE($compressedSize);
}
// Write out Relative Header Offset.
$offset = $this->entry->getOffset();
if (0xffffffff <= $offset) {
$data .= PackUtil::packLongLE($offset);
}
return $data;
}
/**
* Initializes this Extra Field by deserializing a Data Block.
* @param string $data
* @throws \PhpZip\Exception\ZipException
*/
public function deserialize($data)
{
if ($this->entry === null) {
throw new RuntimeException("entry is null");
}
$off = 0;
// Read in Uncompressed Size.
$size = $this->entry->getSize();
if (0xffffffff <= $size) {
assert(0xffffffff === $size);
$this->entry->setSize(PackUtil::unpackLongLE(substr($data, $off, 8)));
$off += 8;
}
// Read in Compressed Size.
$compressedSize = $this->entry->getCompressedSize();
if (0xffffffff <= $compressedSize) {
assert(0xffffffff === $compressedSize);
$this->entry->setCompressedSize(PackUtil::unpackLongLE(substr($data, $off, 8)));
$off += 8;
}
// Read in Relative Header Offset.
$offset = $this->entry->getOffset();
if (0xffffffff <= $offset) {
assert(0xffffffff, $offset);
$this->entry->setOffset(PackUtil::unpackLongLE(substr($data, $off, 8)));
}
}
}

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 (24 === $unpack['sizeAttr']) {
$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 (null !== $this->mtime && null !== $this->atime && null !== $this->ctime) {
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,4 +1,5 @@
<?php <?php
namespace PhpZip\Mapper; namespace PhpZip\Mapper;
/** /**
@@ -19,7 +20,7 @@ class OffsetPositionMapper extends PositionMapper
*/ */
public function __construct($offset) public function __construct($offset)
{ {
$this->offset = $offset; $this->offset = (int)$offset;
} }
/** /**
@@ -39,4 +40,4 @@ class OffsetPositionMapper extends PositionMapper
{ {
return parent::unmap($position) - $this->offset; return parent::unmap($position) - $this->offset;
} }
} }

View File

@@ -1,4 +1,5 @@
<?php <?php
namespace PhpZip\Mapper; namespace PhpZip\Mapper;
/** /**
@@ -26,4 +27,4 @@ class PositionMapper
{ {
return $position; return $position;
} }
} }

View File

@@ -1,482 +0,0 @@
<?php
namespace PhpZip\Model;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipNotFoundEntry;
use PhpZip\Model\Entry\ZipNewStringEntry;
use PhpZip\Model\Entry\ZipReadEntry;
use PhpZip\ZipFile;
/**
* Read Central Directory
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class CentralDirectory
{
/** Central File Header signature. */
const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
/**
* @var EndOfCentralDirectory End of Central Directory
*/
private $endOfCentralDirectory;
/**
* @var ZipEntry[] Maps entry names to zip entries.
*/
private $entries = [];
/**
* @var ZipEntry[] New and modified entries
*/
private $modifiedEntries = [];
/**
* @var int Default compression level for the methods DEFLATED and BZIP2.
*/
private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION;
/**
* @var int|null ZipAlign setting
*/
private $zipAlign;
/**
* @var string New password
*/
private $password;
/**
* @var int
*/
private $encryptionMethod;
/**
* @var bool
*/
private $clearPassword;
public function __construct()
{
$this->endOfCentralDirectory = new EndOfCentralDirectory();
}
/**
* 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 $inputStream
* @throws ZipException
*/
public function mountCentralDirectory($inputStream)
{
$this->modifiedEntries = [];
$this->checkZipFileSignature($inputStream);
$this->endOfCentralDirectory->findCentralDirectory($inputStream);
$numEntries = $this->endOfCentralDirectory->getCentralDirectoryEntriesSize();
$entries = [];
for (; $numEntries > 0; $numEntries--) {
$entry = new ZipReadEntry($inputStream);
$entry->setCentralDirectory($this);
// 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->endOfCentralDirectory->getMapper()->map($entry->getOffset());
if ($lfhOff < $this->endOfCentralDirectory->getPreamble()) {
$this->endOfCentralDirectory->setPreamble($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;
if ($this->endOfCentralDirectory->getPreamble() + $this->endOfCentralDirectory->getPostamble() >= fstat($inputStream)['size']) {
assert(0 === $numEntries);
$this->checkZipFileSignature($inputStream);
}
}
/**
* Check zip file signature
*
* @param resource $inputStream
* @throws ZipException if this not .ZIP file.
*/
private function checkZipFileSignature($inputStream)
{
rewind($inputStream);
// Constraint: A ZIP file must start with a Local File Header
// or a (ZIP64) End Of Central Directory Record if it's empty.
$signatureBytes = fread($inputStream, 4);
if (strlen($signatureBytes) < 4) {
throw new ZipException("Invalid zip file.");
}
$signature = unpack('V', $signatureBytes)[1];
if (
ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature
&& EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
&& EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
) {
throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
}
}
/**
* Set compression method for new or rewrites entries.
* @param int $compressionLevel
* @throws InvalidArgumentException
* @see ZipFile::LEVEL_DEFAULT_COMPRESSION
* @see ZipFile::LEVEL_BEST_SPEED
* @see ZipFile::LEVEL_BEST_COMPRESSION
*/
public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION)
{
if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION ||
$compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION
) {
throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION);
}
$this->compressionLevel = $compressionLevel;
}
/**
* @return ZipEntry[]
*/
public function &getEntries()
{
return $this->entries;
}
/**
* @param string $entryName
* @return ZipEntry
* @throws ZipNotFoundEntry
*/
public function getEntry($entryName)
{
if (!isset($this->entries[$entryName])) {
throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found');
}
return $this->entries[$entryName];
}
/**
* @param string $entryName
* @return ZipEntry
* @throws ZipNotFoundEntry
*/
public function getModifiedEntry($entryName){
if (!isset($this->modifiedEntries[$entryName])) {
throw new ZipNotFoundEntry('Zip modified entry ' . $entryName . ' not found');
}
return $this->modifiedEntries[$entryName];
}
/**
* @return EndOfCentralDirectory
*/
public function getEndOfCentralDirectory()
{
return $this->endOfCentralDirectory;
}
public function getArchiveComment()
{
return null === $this->endOfCentralDirectory->getComment() ?
'' :
$this->endOfCentralDirectory->getComment();
}
/**
* Set entry comment
* @param string $entryName
* @param string|null $comment
* @throws ZipNotFoundEntry
*/
public function setEntryComment($entryName, $comment)
{
if (isset($this->modifiedEntries[$entryName])) {
$this->modifiedEntries[$entryName]->setComment($comment);
} elseif (isset($this->entries[$entryName])) {
$entry = clone $this->entries[$entryName];
$entry->setComment($comment);
$this->putInModified($entryName, $entry);
} else {
throw new ZipNotFoundEntry("Not found entry " . $entryName);
}
}
/**
* @param string|null $password
* @param int|null $encryptionMethod
*/
public function setNewPassword($password, $encryptionMethod = null)
{
$this->password = $password;
$this->encryptionMethod = $encryptionMethod;
$this->clearPassword = $password === null;
}
/**
* @return int|null
*/
public function getZipAlign()
{
return $this->zipAlign;
}
/**
* @param int|null $zipAlign
*/
public function setZipAlign($zipAlign = null)
{
if (null === $zipAlign) {
$this->zipAlign = null;
return;
}
$this->zipAlign = (int)$zipAlign;
}
/**
* Put modification or new entries.
*
* @param $entryName
* @param ZipEntry $entry
*/
public function putInModified($entryName, ZipEntry $entry)
{
$this->modifiedEntries[$entryName] = $entry;
}
/**
* @param string $entryName
* @throws ZipNotFoundEntry
*/
public function deleteEntry($entryName)
{
if (isset($this->entries[$entryName])) {
$this->modifiedEntries[$entryName] = null;
} elseif (isset($this->modifiedEntries[$entryName])) {
unset($this->modifiedEntries[$entryName]);
} else {
throw new ZipNotFoundEntry("Not found entry " . $entryName);
}
}
/**
* @param string $regexPattern
* @return bool
*/
public function deleteEntriesFromRegex($regexPattern)
{
$count = 0;
foreach ($this->modifiedEntries as $entryName => &$entry) {
if (preg_match($regexPattern, $entryName)) {
unset($entry);
$count++;
}
}
foreach ($this->entries as $entryName => $entry) {
if (preg_match($regexPattern, $entryName)) {
$this->modifiedEntries[$entryName] = null;
$count++;
}
}
return $count > 0;
}
/**
* @param string $oldName
* @param string $newName
* @throws InvalidArgumentException
* @throws ZipNotFoundEntry
*/
public function rename($oldName, $newName)
{
$oldName = (string)$oldName;
$newName = (string)$newName;
if (isset($this->entries[$newName]) || isset($this->modifiedEntries[$newName])) {
throw new InvalidArgumentException("New entry name " . $newName . ' is exists.');
}
if (isset($this->modifiedEntries[$oldName]) || isset($this->entries[$oldName])) {
$newEntry = clone (isset($this->modifiedEntries[$oldName]) ?
$this->modifiedEntries[$oldName] :
$this->entries[$oldName]);
$newEntry->setName($newName);
$this->modifiedEntries[$oldName] = null;
$this->modifiedEntries[$newName] = $newEntry;
return;
}
throw new ZipNotFoundEntry("Not found entry " . $oldName);
}
/**
* Delete all entries.
*/
public function deleteAll()
{
$this->modifiedEntries = [];
foreach ($this->entries as $entry) {
$this->modifiedEntries[$entry->getName()] = null;
}
}
/**
* @param resource $outputStream
*/
public function writeArchive($outputStream)
{
/**
* @var ZipEntry[] $memoryEntriesResult
*/
$memoryEntriesResult = [];
foreach ($this->entries as $entryName => $entry) {
if (isset($this->modifiedEntries[$entryName])) continue;
if (
(null !== $this->password || $this->clearPassword) &&
$entry->isEncrypted() &&
$entry->getPassword() !== null &&
(
$entry->getPassword() !== $this->password ||
$entry->getEncryptionMethod() !== $this->encryptionMethod
)
) {
$prototypeEntry = new ZipNewStringEntry($entry->getEntryContent());
$prototypeEntry->setName($entry->getName());
$prototypeEntry->setMethod($entry->getMethod());
$prototypeEntry->setTime($entry->getTime());
$prototypeEntry->setExternalAttributes($entry->getExternalAttributes());
$prototypeEntry->setExtra($entry->getExtra());
$prototypeEntry->setPassword($this->password, $this->encryptionMethod);
if ($this->clearPassword) {
$prototypeEntry->clearEncryption();
}
} else {
$prototypeEntry = clone $entry;
}
$memoryEntriesResult[$entryName] = $prototypeEntry;
}
foreach ($this->modifiedEntries as $entryName => $outputEntry) {
if (null === $outputEntry) { // remove marked entry
unset($memoryEntriesResult[$entryName]);
} else {
if (null !== $this->password) {
$outputEntry->setPassword($this->password, $this->encryptionMethod);
}
$memoryEntriesResult[$entryName] = $outputEntry;
}
}
foreach ($memoryEntriesResult as $key => $outputEntry) {
$outputEntry->setCentralDirectory($this);
$outputEntry->writeEntry($outputStream);
}
$centralDirectoryOffset = ftell($outputStream);
foreach ($memoryEntriesResult as $key => $outputEntry) {
if (!$this->writeCentralFileHeader($outputStream, $outputEntry)) {
unset($memoryEntriesResult[$key]);
}
}
$centralDirectoryEntries = sizeof($memoryEntriesResult);
$this->getEndOfCentralDirectory()->writeEndOfCentralDirectory(
$outputStream,
$centralDirectoryEntries,
$centralDirectoryOffset
);
}
/**
* Writes a Central File Header record.
*
* @param resource $outputStream
* @param ZipEntry $entry
* @return bool false if and only if the record has been skipped,
* i.e. not written for some other reason than an I/O error.
*/
private function writeCentralFileHeader($outputStream, ZipEntry $entry)
{
$compressedSize = $entry->getCompressedSize();
$size = $entry->getSize();
// This test MUST NOT include the CRC-32 because VV_AE_2 sets it to
// UNKNOWN!
if (ZipEntry::UNKNOWN === ($compressedSize | $size)) {
return false;
}
$extra = $entry->getExtra();
$extraSize = strlen($extra);
$commentLength = strlen($entry->getComment());
fwrite(
$outputStream,
pack(
'VvvvvVVVVvvvvvVV',
// central file header signature 4 bytes (0x02014b50)
self::CENTRAL_FILE_HEADER_SIG,
// version made by 2 bytes
($entry->getPlatform() << 8) | 63,
// version needed to extract 2 bytes
$entry->getVersionNeededToExtract(),
// general purpose bit flag 2 bytes
$entry->getGeneralPurposeBitFlags(),
// compression method 2 bytes
$entry->getMethod(),
// last mod file datetime 4 bytes
$entry->getDosTime(),
// crc-32 4 bytes
$entry->getCrc(),
// compressed size 4 bytes
$entry->getCompressedSize(),
// uncompressed size 4 bytes
$entry->getSize(),
// file name length 2 bytes
strlen($entry->getName()),
// extra field length 2 bytes
$extraSize,
// file comment length 2 bytes
$commentLength,
// disk number start 2 bytes
0,
// internal file attributes 2 bytes
0,
// external file attributes 4 bytes
$entry->getExternalAttributes(),
// relative offset of local header 4 bytes
$entry->getOffset()
)
);
// file name (variable size)
fwrite($outputStream, $entry->getName());
if (0 < $extraSize) {
// extra field (variable size)
fwrite($outputStream, $extra);
}
if (0 < $commentLength) {
// file comment (variable size)
fwrite($outputStream, $entry->getComment());
}
return true;
}
public function release()
{
unset($this->entries);
unset($this->modifiedEntries);
}
function __destruct()
{
$this->release();
}
}

View File

@@ -1,11 +1,6 @@
<?php <?php
namespace PhpZip\Model;
use PhpZip\Exception\InvalidArgumentException; namespace PhpZip\Model;
use PhpZip\Exception\ZipException;
use PhpZip\Mapper\OffsetPositionMapper;
use PhpZip\Mapper\PositionMapper;
use PhpZip\Util\PackUtil;
/** /**
* Read End of Central Directory * Read End of Central Directory
@@ -77,172 +72,26 @@ class EndOfCentralDirectory
* @var string|null The archive comment. * @var string|null The archive comment.
*/ */
private $comment; private $comment;
/**
* @var int The number of bytes in the preamble of this ZIP file.
*/
private $preamble;
/**
* @var int The number of bytes in the postamble of this ZIP file.
*/
private $postamble;
/**
* @var PositionMapper Maps offsets specified in the ZIP file to real offsets in the file.
*/
private $mapper;
/** /**
* @var int * @var int
*/ */
private $centralDirectoryEntriesSize; private $entryCount;
/** /**
* @var bool * @var bool
*/ */
private $zip64 = false; private $zip64 = false;
/**
* @var string|null
*/
private $newComment;
/**
* @var bool
*/
private $modified;
/** /**
* EndOfCentralDirectory constructor. * EndOfCentralDirectory constructor.
* @param int $entryCount
* @param null|string $comment
* @param bool $zip64
*/ */
public function __construct() public function __construct($entryCount, $comment, $zip64 = false)
{ {
$this->mapper = new PositionMapper(); $this->entryCount = $entryCount;
} $this->comment = $comment;
$this->zip64 = $zip64;
/**
* Positions the file pointer at the first Central File Header.
* Performs some means to check that this is really a ZIP file.
*
* @param resource $inputStream
* @throws ZipException If the file is not compatible to the ZIP File
* Format Specification.
*/
public function findCentralDirectory($inputStream)
{
// Search for End of central directory record.
$stats = fstat($inputStream);
$size = $stats['size'];
$max = $size - self::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
$min = $max >= 0xffff ? $max - 0xffff : 0;
for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
fseek($inputStream, $endOfCentralDirRecordPos, SEEK_SET);
// end of central dir signature 4 bytes (0x06054b50)
if (self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== unpack('V', fread($inputStream, 4))[1])
continue;
// 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
$data = unpack(
'vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLength',
fread($inputStream, 18)
);
if (0 !== $data['diskNo'] || 0 !== $data['cdDiskNo'] || $data['cdEntriesDisk'] !== $data['cdEntries']) {
throw new ZipException(
"ZIP file spanning/splitting is not supported!"
);
}
// .ZIP file comment (variable size)
if (0 < $data['commentLength']) {
$this->comment = fread($inputStream, $data['commentLength']);
}
$this->preamble = $endOfCentralDirRecordPos;
$this->postamble = $size - ftell($inputStream);
// Check for ZIP64 End Of Central Directory Locator.
$endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
fseek($inputStream, $endOfCentralDirLocatorPos, SEEK_SET);
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
if (
0 > $endOfCentralDirLocatorPos ||
ftell($inputStream) === $size ||
self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($inputStream, 4))[1]
) {
// Seek and check first CFH, probably requiring an offset mapper.
$offset = $endOfCentralDirRecordPos - $data['cdSize'];
fseek($inputStream, $offset, SEEK_SET);
$offset -= $data['cdPos'];
if (0 !== $offset) {
$this->mapper = new OffsetPositionMapper($offset);
}
$this->centralDirectoryEntriesSize = $data['cdEntries'];
return;
}
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
$zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($inputStream, 4))[1];
// relative offset of the zip64
// end of central directory record 8 bytes
$zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($inputStream, 8));
// total number of disks 4 bytes
$totalDisks = unpack('V', fread($inputStream, 4))[1];
if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
throw new ZipException("ZIP file spanning/splitting is not supported!");
}
fseek($inputStream, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
$zip64EndOfCentralDirSig = unpack('V', fread($inputStream, 4))[1];
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($inputStream, 12, SEEK_CUR);
// number of this disk 4 bytes
$diskNo = unpack('V', fread($inputStream, 4))[1];
// number of the disk with the
// start of the central directory 4 bytes
$cdDiskNo = unpack('V', fread($inputStream, 4))[1];
// total number of entries in the
// central directory on this disk 8 bytes
$cdEntriesDisk = PackUtil::unpackLongLE(fread($inputStream, 8));
// total number of entries in the
// central directory 8 bytes
$cdEntries = PackUtil::unpackLongLE(fread($inputStream, 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
fseek($inputStream, 8, SEEK_CUR);
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
$cdPos = PackUtil::unpackLongLE(fread($inputStream, 8));
// zip64 extensible data sector (variable size)
fseek($inputStream, $cdPos, SEEK_SET);
$this->preamble = $zip64EndOfCentralDirectoryRecordPos;
$this->centralDirectoryEntriesSize = $cdEntries;
$this->zip64 = true;
return;
}
// Start recovering file entries from min.
$this->preamble = $min;
$this->postamble = $size - $min;
$this->centralDirectoryEntriesSize = 0;
} }
/** /**
@@ -256,9 +105,9 @@ class EndOfCentralDirectory
/** /**
* @return int * @return int
*/ */
public function getCentralDirectoryEntriesSize() public function getEntryCount()
{ {
return $this->centralDirectoryEntriesSize; return $this->entryCount;
} }
/** /**
@@ -268,152 +117,4 @@ class EndOfCentralDirectory
{ {
return $this->zip64; return $this->zip64;
} }
}
/**
* @return int
*/
public function getPreamble()
{
return $this->preamble;
}
/**
* @return int
*/
public function getPostamble()
{
return $this->postamble;
}
/**
* @return PositionMapper
*/
public function getMapper()
{
return $this->mapper;
}
/**
* @param int $preamble
*/
public function setPreamble($preamble)
{
$this->preamble = $preamble;
}
/**
* Set archive comment
* @param string|null $comment
* @throws InvalidArgumentException
*/
public function setComment($comment = null)
{
if (null !== $comment && strlen($comment) !== 0) {
$comment = (string)$comment;
$length = strlen($comment);
if (0x0000 > $length || $length > 0xffff) {
throw new InvalidArgumentException('Length comment out of range');
}
}
$this->modified = $comment !== $this->comment;
$this->newComment = $comment;
}
/**
* Write end of central directory.
*
* @param resource $outputStream Output stream
* @param int $centralDirectoryEntries Size entries
* @param int $centralDirectoryOffset Offset central directory
*/
public function writeEndOfCentralDirectory($outputStream, $centralDirectoryEntries, $centralDirectoryOffset)
{
$position = ftell($outputStream);
$centralDirectorySize = $position - $centralDirectoryOffset;
$centralDirectoryEntriesZip64 = $centralDirectoryEntries > 0xffff;
$centralDirectorySizeZip64 = $centralDirectorySize > 0xffffffff;
$centralDirectoryOffsetZip64 = $centralDirectoryOffset > 0xffffffff;
$centralDirectoryEntries16 = $centralDirectoryEntriesZip64 ? 0xffff : (int)$centralDirectoryEntries;
$centralDirectorySize32 = $centralDirectorySizeZip64 ? 0xffffffff : $centralDirectorySize;
$centralDirectoryOffset32 = $centralDirectoryOffsetZip64 ? 0xffffffff : $centralDirectoryOffset;
$zip64 // ZIP64 extensions?
= $centralDirectoryEntriesZip64
|| $centralDirectorySizeZip64
|| $centralDirectoryOffsetZip64;
if ($zip64) {
// relative offset of the zip64 end of central directory record
$zip64EndOfCentralDirectoryOffset = $position;
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
fwrite($outputStream, pack('V', self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG));
// size of zip64 end of central
// directory record 8 bytes
fwrite($outputStream, PackUtil::packLongLE(self::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 12));
// version made by 2 bytes
// version needed to extract 2 bytes
// due to potential use of BZIP2 compression
// number of this disk 4 bytes
// number of the disk with the
// start of the central directory 4 bytes
fwrite($outputStream, pack('vvVV', 63, 46, 0, 0));
// total number of entries in the
// central directory on this disk 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries));
// total number of entries in the
// central directory 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryEntries));
// size of the central directory 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectorySize));
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
fwrite($outputStream, PackUtil::packLongLE($centralDirectoryOffset));
// zip64 extensible data sector (variable size)
//
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
fwrite($outputStream, pack('VV', self::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG, 0));
// relative offset of the zip64
// end of central directory record 8 bytes
fwrite($outputStream, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset));
// total number of disks 4 bytes
fwrite($outputStream, pack('V', 1));
}
$comment = $this->modified ? $this->newComment : $this->comment;
$commentLength = strlen($comment);
fwrite(
$outputStream,
pack('VvvvvVVv',
// end of central dir signature 4 bytes (0x06054b50)
self::END_OF_CENTRAL_DIRECTORY_RECORD_SIG,
// 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
$centralDirectoryEntries16,
// total number of entries in
// the central directory 2 bytes
$centralDirectoryEntries16,
// size of the central directory 4 bytes
$centralDirectorySize32,
// offset of start of central
// directory with respect to
// the starting disk number 4 bytes
$centralDirectoryOffset32,
// .ZIP file comment length 2 bytes
$commentLength
)
);
if ($commentLength > 0) {
// .ZIP file comment (variable size)
fwrite($outputStream, $comment);
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Model\ZipEntry;
/**
* Entry to write to the central directory.
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class OutputOffsetEntry
{
/**
* @var int
*/
private $offset;
/**
* @var ZipEntry
*/
private $entry;
/**
* @param int $pos
* @param ZipEntry $entry
*/
public function __construct($pos, ZipEntry $entry)
{
$this->offset = $pos;
$this->entry = $entry;
}
/**
* @return int
*/
public function getOffset()
{
return $this->offset;
}
/**
* @return ZipEntry
*/
public function getEntry()
{
return $this->entry;
}
}

View File

@@ -4,15 +4,13 @@ namespace PhpZip\Model\Entry;
use PhpZip\Exception\InvalidArgumentException; use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException; use PhpZip\Exception\ZipException;
use PhpZip\Extra\DefaultExtraField; use PhpZip\Extra\ExtraFieldsCollection;
use PhpZip\Extra\ExtraField; use PhpZip\Extra\ExtraFieldsFactory;
use PhpZip\Extra\ExtraFields; use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Model\CentralDirectory;
use PhpZip\Model\ZipEntry; use PhpZip\Model\ZipEntry;
use PhpZip\Util\DateTimeConverter; use PhpZip\Util\DateTimeConverter;
use PhpZip\Util\PackUtil; use PhpZip\Util\StringUtil;
use PhpZip\ZipFile; use PhpZip\ZipFileInterface;
/** /**
* Abstract ZIP entry. * Abstract ZIP entry.
@@ -23,16 +21,10 @@ use PhpZip\ZipFile;
*/ */
abstract class ZipAbstractEntry implements ZipEntry abstract class ZipAbstractEntry implements ZipEntry
{ {
/**
* @var CentralDirectory
*/
private $centralDirectory;
/** /**
* @var int Bit flags for init state. * @var int Bit flags for init state.
*/ */
private $init; private $init;
/** /**
* @var string Entry name (filename in archive) * @var string Entry name (filename in archive)
*/ */
@@ -45,14 +37,14 @@ abstract class ZipAbstractEntry implements ZipEntry
* @var int * @var int
*/ */
private $versionNeededToExtract = 20; private $versionNeededToExtract = 20;
/**
* @var int
*/
private $general;
/** /**
* @var int Compression method * @var int Compression method
*/ */
private $method; private $method;
/**
* @var int
*/
private $general;
/** /**
* @var int Dos time * @var int Dos time
*/ */
@@ -78,13 +70,13 @@ abstract class ZipAbstractEntry implements ZipEntry
*/ */
private $offset = self::UNKNOWN; private $offset = self::UNKNOWN;
/** /**
* The map of Extra Fields. * Collections of Extra Fields.
* Maps from Header ID [Integer] to Extra Field [ExtraField]. * Keys from Header ID [int] and value Extra Field [ExtraField].
* Should be null or may be empty if no Extra Fields are used. * Should be null or may be empty if no Extra Fields are used.
* *
* @var ExtraFields * @var ExtraFieldsCollection
*/ */
private $fields; private $extraFieldsCollection;
/** /**
* @var string Comment field. * @var string Comment field.
*/ */
@@ -95,55 +87,49 @@ abstract class ZipAbstractEntry implements ZipEntry
private $password; private $password;
/** /**
* Encryption method. * Encryption method.
* @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL * @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
* @var int * @var int
*/ */
private $encryptionMethod = ZipFile::ENCRYPTION_METHOD_TRADITIONAL; private $encryptionMethod = ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL;
/** /**
* @var int * @var int
*/ */
private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION; private $compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION;
/** /**
* @param int $mask * ZipAbstractEntry constructor.
* @return bool
*/ */
private function isInit($mask) public function __construct()
{ {
return 0 !== ($this->init & $mask); $this->extraFieldsCollection = new ExtraFieldsCollection();
} }
/** /**
* @param int $mask * @param ZipEntry $entry
* @param bool $init * @throws ZipException
*/ */
private function setInit($mask, $init) public function setEntry(ZipEntry $entry)
{ {
if ($init) { $this->setName($entry->getName());
$this->init |= $mask; $this->setPlatform($entry->getPlatform());
} else { $this->setVersionNeededToExtract($entry->getVersionNeededToExtract());
$this->init &= ~$mask; $this->setMethod($entry->getMethod());
} $this->setGeneralPurposeBitFlags($entry->getGeneralPurposeBitFlags());
} $this->setDosTime($entry->getDosTime());
$this->setCrc($entry->getCrc());
/** $this->setCompressedSize($entry->getCompressedSize());
* @return CentralDirectory $this->setSize($entry->getSize());
*/ $this->setExternalAttributes($entry->getExternalAttributes());
public function getCentralDirectory() $this->setOffset($entry->getOffset());
{ $this->setExtra($entry->getExtra());
return $this->centralDirectory; $this->setComment($entry->getComment());
} $this->setPassword($entry->getPassword());
$this->setEncryptionMethod($entry->getEncryptionMethod());
/** $this->setCompressionLevel($entry->getCompressionLevel());
* @param CentralDirectory $centralDirectory $this->setEncrypted($entry->isEncrypted());
* @return ZipEntry
*/
public function setCentralDirectory(CentralDirectory $centralDirectory)
{
$this->centralDirectory = $centralDirectory;
return $this;
} }
/** /**
@@ -174,6 +160,23 @@ abstract class ZipAbstractEntry implements ZipEntry
return $this; return $this;
} }
/**
* Sets the indexed General Purpose Bit Flag.
*
* @param int $mask
* @param bool $bit
* @return ZipEntry
*/
public function setGeneralPurposeBitFlag($mask, $bit)
{
if ($bit) {
$this->general |= $mask;
} else {
$this->general &= ~$mask;
}
return $this;
}
/** /**
* @return int Get platform * @return int Get platform
*/ */
@@ -204,6 +207,28 @@ abstract class ZipAbstractEntry implements ZipEntry
return $this; return $this;
} }
/**
* @param int $mask
* @return bool
*/
protected function isInit($mask)
{
return ($this->init & $mask) !== 0;
}
/**
* @param int $mask
* @param bool $init
*/
protected function setInit($mask, $init)
{
if ($init) {
$this->init |= $mask;
} else {
$this->init &= ~$mask;
}
}
/** /**
* Version needed to extract. * Version needed to extract.
* *
@@ -231,11 +256,8 @@ abstract class ZipAbstractEntry implements ZipEntry
*/ */
public function isZip64ExtensionsRequired() public function isZip64ExtensionsRequired()
{ {
// Offset MUST be considered in decision about ZIP64 format - see
// description of Data Descriptor in ZIP File Format Specification!
return 0xffffffff <= $this->getCompressedSize() return 0xffffffff <= $this->getCompressedSize()
|| 0xffffffff <= $this->getSize() || 0xffffffff <= $this->getSize();
|| 0xffffffff <= $this->getOffset();
} }
/** /**
@@ -253,16 +275,9 @@ abstract class ZipAbstractEntry implements ZipEntry
* *
* @param int $compressedSize The Compressed Size. * @param int $compressedSize The Compressed Size.
* @return ZipEntry * @return ZipEntry
* @throws ZipException
*/ */
public function setCompressedSize($compressedSize) public function setCompressedSize($compressedSize)
{ {
if (self::UNKNOWN != $compressedSize) {
$compressedSize = sprintf('%u', $compressedSize);
if (0 > $compressedSize || $compressedSize > 0x7fffffffffffffff) {
throw new ZipException("Compressed size out of range - " . $this->name);
}
}
$this->compressedSize = $compressedSize; $this->compressedSize = $compressedSize;
return $this; return $this;
} }
@@ -282,16 +297,9 @@ abstract class ZipAbstractEntry implements ZipEntry
* *
* @param int $size The (Uncompressed) Size. * @param int $size The (Uncompressed) Size.
* @return ZipEntry * @return ZipEntry
* @throws ZipException
*/ */
public function setSize($size) public function setSize($size)
{ {
if (self::UNKNOWN != $size) {
$size = sprintf('%u', $size);
if (0 > $size || $size > 0x7fffffffffffffff) {
throw new ZipException("Uncompressed Size out of range - " . $this->name);
}
}
$this->size = $size; $this->size = $size;
return $this; return $this;
} }
@@ -309,33 +317,16 @@ abstract class ZipAbstractEntry implements ZipEntry
/** /**
* @param int $offset * @param int $offset
* @return ZipEntry * @return ZipEntry
* @throws ZipException
*/ */
public function setOffset($offset) public function setOffset($offset)
{ {
$offset = sprintf('%u', $offset);
if (0 > $offset || $offset > 0x7fffffffffffffff) {
throw new ZipException("Offset out of range - " . $this->name);
}
$this->offset = $offset; $this->offset = $offset;
return $this; return $this;
} }
/**
* Returns true if and only if this ZIP entry represents a directory entry
* (i.e. end with '/').
*
* @return bool
*/
public function isDirectory()
{
return $this->name[strlen($this->name) - 1] === '/';
}
/** /**
* Returns the General Purpose Bit Flags. * Returns the General Purpose Bit Flags.
* * @return int
* @return bool
*/ */
public function getGeneralPurposeBitFlags() public function getGeneralPurposeBitFlags()
{ {
@@ -355,33 +346,19 @@ abstract class ZipAbstractEntry implements ZipEntry
throw new ZipException('general out of range'); throw new ZipException('general out of range');
} }
$this->general = $general; $this->general = $general;
return $this; if ($this->method === ZipFileInterface::METHOD_DEFLATED) {
} $bit1 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG1);
$bit2 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG2);
/** if ($bit1 && !$bit2) {
* Returns the indexed General Purpose Bit Flag. $this->compressionLevel = ZipFileInterface::LEVEL_BEST_COMPRESSION;
* } elseif (!$bit1 && $bit2) {
* @param int $mask $this->compressionLevel = ZipFileInterface::LEVEL_FAST;
* @return bool } elseif ($bit1 && $bit2) {
*/ $this->compressionLevel = ZipFileInterface::LEVEL_SUPER_FAST;
public function getGeneralPurposeBitFlag($mask) } else {
{ $this->compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION;
return 0 !== ($this->general & $mask); }
} }
/**
* Sets the indexed General Purpose Bit Flag.
*
* @param int $mask
* @param bool $bit
* @return ZipEntry
*/
public function setGeneralPurposeBitFlag($mask, $bit)
{
if ($bit)
$this->general |= $mask;
else
$this->general &= ~$mask;
return $this; return $this;
} }
@@ -395,26 +372,37 @@ abstract class ZipAbstractEntry implements ZipEntry
return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED); return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED);
} }
/**
* Returns the indexed General Purpose Bit Flag.
*
* @param int $mask
* @return bool
*/
public function getGeneralPurposeBitFlag($mask)
{
return ($this->general & $mask) !== 0;
}
/** /**
* Sets the encryption property to false and removes any other * Sets the encryption property to false and removes any other
* encryption artifacts. * encryption artifacts.
* *
* @return ZipEntry * @return ZipEntry
* @throws ZipException
*/ */
public function clearEncryption() public function disableEncryption()
{ {
$this->setEncrypted(false); $this->setEncrypted(false);
if (null !== $this->fields) { $headerId = WinZipAesEntryExtraField::getHeaderId();
$field = $this->fields->get(WinZipAesEntryExtraField::getHeaderId()); if (isset($this->extraFieldsCollection[$headerId])) {
if (null !== $field) { /**
/** * @var WinZipAesEntryExtraField $field
* @var WinZipAesEntryExtraField $field */
*/ $field = $this->extraFieldsCollection[$headerId];
$this->removeExtraField(WinZipAesEntryExtraField::getHeaderId());
}
if (self::METHOD_WINZIP_AES === $this->getMethod()) { if (self::METHOD_WINZIP_AES === $this->getMethod()) {
$this->setMethod(null === $field ? self::UNKNOWN : $field->getMethod()); $this->setMethod($field === null ? self::UNKNOWN : $field->getMethod());
} }
unset($this->extraFieldsCollection[$headerId]);
} }
$this->password = null; $this->password = null;
return $this; return $this;
@@ -428,6 +416,7 @@ abstract class ZipAbstractEntry implements ZipEntry
*/ */
public function setEncrypted($encrypted) public function setEncrypted($encrypted)
{ {
$encrypted = (bool)$encrypted;
$this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted); $this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted);
return $this; return $this;
} }
@@ -439,7 +428,10 @@ abstract class ZipAbstractEntry implements ZipEntry
*/ */
public function getMethod() public function getMethod()
{ {
return $this->isInit(self::BIT_METHOD) ? $this->method & 0xffff : self::UNKNOWN; $isInit = $this->isInit(self::BIT_METHOD);
return $isInit ?
$this->method & 0xffff :
self::UNKNOWN;
} }
/** /**
@@ -451,26 +443,21 @@ abstract class ZipAbstractEntry implements ZipEntry
*/ */
public function setMethod($method) public function setMethod($method)
{ {
if ($method === self::UNKNOWN) {
$this->method = $method;
$this->setInit(self::BIT_METHOD, false);
return $this;
}
if (0x0000 > $method || $method > 0xffff) { if (0x0000 > $method || $method > 0xffff) {
throw new ZipException('method out of range'); throw new ZipException('method out of range: ' . $method);
} }
switch ($method) { switch ($method) {
case self::METHOD_WINZIP_AES: case self::METHOD_WINZIP_AES:
case ZipFileInterface::METHOD_STORED:
case ZipFileInterface::METHOD_DEFLATED:
case ZipFileInterface::METHOD_BZIP2:
$this->method = $method; $this->method = $method;
$this->setInit(self::BIT_METHOD, true); $this->setInit(self::BIT_METHOD, true);
$this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_WINZIP_AES);
break;
case ZipFile::METHOD_STORED:
case ZipFile::METHOD_DEFLATED:
case ZipFile::METHOD_BZIP2:
$this->method = $method;
$this->setInit(self::BIT_METHOD, true);
break;
case self::UNKNOWN:
$this->method = ZipFile::METHOD_STORED;
$this->setInit(self::BIT_METHOD, false);
break; break;
default: default:
@@ -492,24 +479,6 @@ abstract class ZipAbstractEntry implements ZipEntry
return DateTimeConverter::toUnixTimestamp($this->getDosTime()); return DateTimeConverter::toUnixTimestamp($this->getDosTime());
} }
/**
* Set time from unix timestamp.
*
* @param int $unixTimestamp
* @return ZipEntry
*/
public function setTime($unixTimestamp)
{
$known = self::UNKNOWN != $unixTimestamp;
if ($known) {
$this->dosTime = DateTimeConverter::toDosTime($unixTimestamp);
} else {
$this->dosTime = 0;
}
$this->setInit(self::BIT_DATE_TIME, $known);
return $this;
}
/** /**
* Get Dos Time * Get Dos Time
* *
@@ -517,7 +486,7 @@ abstract class ZipAbstractEntry implements ZipEntry
*/ */
public function getDosTime() public function getDosTime()
{ {
return $this->dosTime & 0xffffffff; return $this->dosTime;
} }
/** /**
@@ -535,6 +504,25 @@ abstract class ZipAbstractEntry implements ZipEntry
$this->setInit(self::BIT_DATE_TIME, true); $this->setInit(self::BIT_DATE_TIME, true);
} }
/**
* Set time from unix timestamp.
*
* @param int $unixTimestamp
* @return ZipEntry
* @throws ZipException
*/
public function setTime($unixTimestamp)
{
$known = self::UNKNOWN != $unixTimestamp;
if ($known) {
$this->dosTime = DateTimeConverter::toDosTime($unixTimestamp);
} else {
$this->dosTime = 0;
}
$this->setInit(self::BIT_DATE_TIME, $known);
return $this;
}
/** /**
* Returns the external file attributes. * Returns the external file attributes.
* *
@@ -545,7 +533,7 @@ abstract class ZipAbstractEntry implements ZipEntry
if (!$this->isInit(self::BIT_EXTERNAL_ATTR)) { if (!$this->isInit(self::BIT_EXTERNAL_ATTR)) {
return $this->isDirectory() ? 0x10 : 0; return $this->isDirectory() ? 0x10 : 0;
} }
return $this->externalAttributes & 0xffffffff; return $this->externalAttributes;
} }
/** /**
@@ -553,16 +541,11 @@ abstract class ZipAbstractEntry implements ZipEntry
* *
* @param int $externalAttributes the external file attributes. * @param int $externalAttributes the external file attributes.
* @return ZipEntry * @return ZipEntry
* @throws ZipException
*/ */
public function setExternalAttributes($externalAttributes) public function setExternalAttributes($externalAttributes)
{ {
$known = self::UNKNOWN != $externalAttributes; $known = self::UNKNOWN != $externalAttributes;
if ($known) { if ($known) {
$externalAttributes = sprintf('%u', $externalAttributes);
if (0x00000000 > $externalAttributes || $externalAttributes > 0xffffffff) {
throw new ZipException("external file attributes out of range - " . $this->name);
}
$this->externalAttributes = $externalAttributes; $this->externalAttributes = $externalAttributes;
} else { } else {
$this->externalAttributes = 0; $this->externalAttributes = 0;
@@ -572,123 +555,32 @@ abstract class ZipAbstractEntry implements ZipEntry
} }
/** /**
* Return extra field from header id. * Returns true if and only if this ZIP entry represents a directory entry
* (i.e. end with '/').
* *
* @param int $headerId
* @return ExtraField|null
*/
public function getExtraField($headerId)
{
return $this->fields === null ? null : $this->fields->get($headerId);
}
/**
* Add extra field.
*
* @param ExtraField $field
* @return ExtraField
* @throws ZipException
*/
public function addExtraField($field)
{
if (null === $field) {
throw new ZipException("extra field null");
}
if (null === $this->fields) {
$this->fields = new ExtraFields();
}
return $this->fields->add($field);
}
/**
* Return exists extra field from header id.
*
* @param int $headerId
* @return bool * @return bool
*/ */
public function hasExtraField($headerId) public function isDirectory()
{ {
return $this->fields === null ? false : $this->fields->has($headerId); return StringUtil::endsWith($this->name, '/');
} }
/** /**
* Remove extra field from header id. * @return ExtraFieldsCollection
*
* @param int $headerId
* @return ExtraField|null
*/ */
public function removeExtraField($headerId) public function &getExtraFieldsCollection()
{ {
return null !== $this->fields ? $this->fields->remove($headerId) : null; return $this->extraFieldsCollection;
} }
/** /**
* Returns a protective copy of the serialized Extra Fields. * Returns a protective copy of the serialized Extra Fields.
*
* @return string A new byte array holding the serialized Extra Fields.
* null is never returned.
*/
public function getExtra()
{
return $this->getExtraFields(false);
}
/**
* @param bool $zip64
* @return string * @return string
* @throws ZipException * @throws ZipException
*/ */
private function getExtraFields($zip64) public function getExtra()
{ {
if ($zip64) { return ExtraFieldsFactory::createSerializedData($this->extraFieldsCollection);
$field = $this->composeZip64ExtraField();
if (null !== $field) {
if (null === $this->fields) {
$this->fields = new ExtraFields();
}
$this->fields->add($field);
}
} else {
assert(null === $this->fields || null === $this->fields->get(ExtraField::ZIP64_HEADER_ID));
}
return null === $this->fields ? null : $this->fields->getExtra();
}
/**
* Composes a ZIP64 Extended Information Extra Field from the properties
* of this entry.
* If no ZIP64 Extended Information Extra Field is required it is removed
* from the collection of Extra Fields.
*
* @return ExtraField|null
*/
private function composeZip64ExtraField()
{
$handle = fopen('php://memory', 'r+b');
// Write out Uncompressed Size.
$size = $this->getSize();
if (0xffffffff <= $size) {
fwrite($handle, PackUtil::packLongLE($size));
}
// Write out Compressed Size.
$compressedSize = $this->getCompressedSize();
if (0xffffffff <= $compressedSize) {
fwrite($handle, PackUtil::packLongLE($compressedSize));
}
// Write out Relative Header Offset.
$offset = $this->getOffset();
if (0xffffffff <= $offset) {
fwrite($handle, PackUtil::packLongLE($offset));
}
// Create ZIP64 Extended Information Extra Field from serialized data.
$field = null;
if (ftell($handle) > 0) {
$field = new DefaultExtraField(ExtraField::ZIP64_HEADER_ID);
$field->readFrom($handle, 0, ftell($handle));
} else {
$field = null;
}
return $field;
} }
/** /**
@@ -701,99 +593,10 @@ abstract class ZipAbstractEntry implements ZipEntry
* *
* @param string $data The byte array holding the serialized Extra Fields. * @param string $data The byte array holding the serialized Extra Fields.
* @throws ZipException if the serialized Extra Fields exceed 64 KB * @throws ZipException if the serialized Extra Fields exceed 64 KB
* @return ZipEntry
* or do not conform to the ZIP File Format Specification
*/ */
public function setExtra($data) public function setExtra($data)
{ {
if (null !== $data) { $this->extraFieldsCollection = ExtraFieldsFactory::createExtraFieldCollections($data, $this);
$length = strlen($data);
if (0x0000 > $length || $length > 0xffff) {
throw new ZipException("Extra Fields too large");
}
}
if (null === $data || strlen($data) <= 0) {
$this->fields = null;
} else {
$this->setExtraFields($data, false);
}
return $this;
}
/**
* @param string $data
* @param bool $zip64
*/
private function setExtraFields($data, $zip64)
{
if (null === $this->fields) {
$this->fields = new ExtraFields();
}
$handle = fopen('php://memory', 'r+b');
fwrite($handle, $data);
rewind($handle);
$this->fields->readFrom($handle, 0, strlen($data));
$result = false;
if ($zip64) {
$result = $this->parseZip64ExtraField();
}
if ($result) {
$this->fields->remove(ExtraField::ZIP64_HEADER_ID);
if ($this->fields->size() <= 0) {
if (0 !== $this->fields->size()) {
$this->fields = null;
}
}
}
fclose($handle);
}
/**
* Parses the properties of this entry from the ZIP64 Extended Information
* Extra Field, if present.
* The ZIP64 Extended Information Extra Field is not removed.
*
* @return bool
* @throws ZipException
*/
private function parseZip64ExtraField()
{
if (null === $this->fields) {
return false;
}
$ef = $this->fields->get(ExtraField::ZIP64_HEADER_ID);
if (null === $ef) {
return false;
}
$dataBlockHandle = $ef->getDataBlock();
$off = 0;
// Read in Uncompressed Size.
$size = $this->getSize();
if (0xffffffff <= $size) {
assert(0xffffffff === $size);
fseek($dataBlockHandle, $off);
$this->setSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
$off += 8;
}
// Read in Compressed Size.
$compressedSize = $this->getCompressedSize();
if (0xffffffff <= $compressedSize) {
assert(0xffffffff === $compressedSize);
fseek($dataBlockHandle, $off);
$this->setCompressedSize(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
$off += 8;
}
// Read in Relative Header Offset.
$offset = $this->getOffset();
if (0xffffffff <= $offset) {
assert(0xffffffff, $offset);
fseek($dataBlockHandle, $off);
$this->setOffset(PackUtil::unpackLongLE(fread($dataBlockHandle, 8)));
//$off += 8;
}
fclose($dataBlockHandle);
return true;
} }
/** /**
@@ -803,7 +606,7 @@ abstract class ZipAbstractEntry implements ZipEntry
*/ */
public function getComment() public function getComment()
{ {
return null != $this->comment ? $this->comment : ""; return null !== $this->comment ? $this->comment : "";
} }
/** /**
@@ -815,7 +618,7 @@ abstract class ZipAbstractEntry implements ZipEntry
*/ */
public function setComment($comment) public function setComment($comment)
{ {
if (null !== $comment) { if ($comment !== null) {
$commentLength = strlen($comment); $commentLength = strlen($comment);
if (0x0000 > $commentLength || $commentLength > 0xffff) { if (0x0000 > $commentLength || $commentLength > 0xffff) {
throw new ZipException("Comment too long"); throw new ZipException("Comment too long");
@@ -831,7 +634,7 @@ abstract class ZipAbstractEntry implements ZipEntry
*/ */
public function isDataDescriptorRequired() public function isDataDescriptorRequired()
{ {
return self::UNKNOWN == ($this->getCrc() | $this->getCompressedSize() | $this->getSize()); return ($this->getCrc() | $this->getCompressedSize() | $this->getSize()) == self::UNKNOWN;
} }
/** /**
@@ -841,7 +644,7 @@ abstract class ZipAbstractEntry implements ZipEntry
*/ */
public function getCrc() public function getCrc()
{ {
return $this->crc & 0xffffffff; return $this->crc;
} }
/** /**
@@ -849,14 +652,9 @@ abstract class ZipAbstractEntry implements ZipEntry
* *
* @param int $crc * @param int $crc
* @return ZipEntry * @return ZipEntry
* @throws ZipException
*/ */
public function setCrc($crc) public function setCrc($crc)
{ {
$crc = sprintf('%u', $crc);
if (0x00000000 > $crc || $crc > 0xffffffff) {
throw new ZipException("CRC-32 out of range - " . $this->name);
}
$this->crc = $crc; $this->crc = $crc;
$this->setInit(self::BIT_CRC, true); $this->setInit(self::BIT_CRC, true);
return $this; return $this;
@@ -876,14 +674,19 @@ abstract class ZipAbstractEntry implements ZipEntry
* @param string $password * @param string $password
* @param null|int $encryptionMethod * @param null|int $encryptionMethod
* @return ZipEntry * @return ZipEntry
* @throws ZipException
*/ */
public function setPassword($password, $encryptionMethod = null) public function setPassword($password, $encryptionMethod = null)
{ {
$this->password = $password; $this->password = $password;
if (null !== $encryptionMethod) { if ($encryptionMethod !== null) {
$this->setEncryptionMethod($encryptionMethod); $this->setEncryptionMethod($encryptionMethod);
} }
$this->setEncrypted(!empty($this->password)); if (!empty($this->password)) {
$this->setEncrypted(true);
} else {
$this->disableEncryption();
}
return $this; return $this;
} }
@@ -895,6 +698,34 @@ abstract class ZipAbstractEntry implements ZipEntry
return $this->encryptionMethod; return $this->encryptionMethod;
} }
/**
* Set encryption method
*
* @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
*
* @param int $encryptionMethod
* @return ZipEntry
* @throws ZipException
*/
public function setEncryptionMethod($encryptionMethod)
{
if (null !== $encryptionMethod) {
if (
$encryptionMethod !== ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL
&& $encryptionMethod !== ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128
&& $encryptionMethod !== ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192
&& $encryptionMethod !== ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
) {
throw new ZipException('Invalid encryption method');
}
$this->encryptionMethod = $encryptionMethod;
}
return $this;
}
/** /**
* @return int * @return int
*/ */
@@ -906,48 +737,24 @@ abstract class ZipAbstractEntry implements ZipEntry
/** /**
* @param int $compressionLevel * @param int $compressionLevel
* @return ZipEntry * @return ZipEntry
* @throws InvalidArgumentException
*/ */
public function setCompressionLevel($compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION) public function setCompressionLevel($compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION)
{ {
if ($compressionLevel < ZipFile::LEVEL_DEFAULT_COMPRESSION || if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION ||
$compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION $compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION
) { ) {
throw new InvalidArgumentException('Invalid compression level. Minimum level ' . throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
ZipFile::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFile::LEVEL_BEST_COMPRESSION); ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION);
} }
$this->compressionLevel = $compressionLevel; $this->compressionLevel = $compressionLevel;
return $this; return $this;
} }
/**
* Set encryption method
*
* @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES
*
* @param int $encryptionMethod
* @return ZipEntry
* @throws ZipException
*/
public function setEncryptionMethod($encryptionMethod)
{
if (
ZipFile::ENCRYPTION_METHOD_TRADITIONAL !== $encryptionMethod &&
ZipFile::ENCRYPTION_METHOD_WINZIP_AES !== $encryptionMethod
) {
throw new ZipException('Invalid encryption method');
}
$this->encryptionMethod = $encryptionMethod;
$this->setEncrypted(true);
return $this;
}
/** /**
* Clone extra fields * Clone extra fields
*/ */
function __clone() public function __clone()
{ {
$this->fields = $this->fields !== null ? clone $this->fields : null; $this->extraFieldsCollection = clone $this->extraFieldsCollection;
} }
} }

View File

@@ -0,0 +1,65 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\ZipException;
/**
* Source Entry Changes
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipChangesEntry extends ZipAbstractEntry
{
/**
* @var ZipSourceEntry
*/
protected $entry;
/**
* ZipChangesEntry constructor.
* @param ZipSourceEntry $entry
* @throws ZipException
* @throws \PhpZip\Exception\InvalidArgumentException
*/
public function __construct(ZipSourceEntry $entry)
{
parent::__construct();
$this->entry = $entry;
$this->setEntry($entry);
}
/**
* @return bool
*/
public function isChangedContent()
{
return !(
$this->getCompressionLevel() === $this->entry->getCompressionLevel() &&
$this->getMethod() === $this->entry->getMethod() &&
$this->isEncrypted() === $this->entry->isEncrypted() &&
$this->getEncryptionMethod() === $this->entry->getEncryptionMethod() &&
$this->getPassword() === $this->entry->getPassword()
);
}
/**
* Returns an string content of the given entry.
*
* @return null|string
* @throws ZipException
*/
public function getEntryContent()
{
return $this->entry->getEntryContent();
}
/**
* @return ZipSourceEntry
*/
public function getSourceEntry()
{
return $this->entry;
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\ZipException;
/**
* New zip entry from empty dir.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipNewEmptyDirEntry extends ZipNewEntry
{
/**
* Returns an string content of the given entry.
*
* @return null|string
* @throws ZipException
*/
public function getEntryContent()
{
return null;
}
}

View File

@@ -1,27 +1,51 @@
<?php <?php
namespace PhpZip\Model\Entry; namespace PhpZip\Model\Entry;
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine; use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Crypto\WinZipAesEngine; use PhpZip\ZipFileInterface;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Model\ZipEntry;
use PhpZip\Util\PackUtil;
use PhpZip\ZipFile;
/** /**
* Abstract class for new zip entry.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru * @author Ne-Lexa alexey@nelexa.ru
* @license MIT * @license MIT
*/ */
abstract class ZipNewEntry extends ZipAbstractEntry class ZipNewEntry extends ZipAbstractEntry
{ {
/** /**
* Default compression level for bzip2 * @var resource|string|null
*/ */
const LEVEL_DEFAULT_BZIP2_COMPRESSION = 4; protected $content;
/**
* @var bool
*/
private $clone = false;
/**
* ZipNewEntry constructor.
* @param string|resource|null $content
*/
public function __construct($content = null)
{
parent::__construct();
if ($content !== null && !is_string($content) && !is_resource($content)) {
throw new InvalidArgumentException('invalid content');
}
$this->content = $content;
}
/**
* Returns an string content of the given entry.
*
* @return null|string
*/
public function getEntryContent()
{
if (is_resource($this->content)) {
rewind($this->content);
return stream_get_contents($this->content);
}
return $this->content;
}
/** /**
* Version needed to extract. * Version needed to extract.
@@ -32,237 +56,29 @@ abstract class ZipNewEntry extends ZipAbstractEntry
{ {
$method = $this->getMethod(); $method = $this->getMethod();
return self::METHOD_WINZIP_AES === $method ? 51 : return self::METHOD_WINZIP_AES === $method ? 51 :
(ZipFile::METHOD_BZIP2 === $method ? 46 : (
($this->isZip64ExtensionsRequired() ? 45 : ZipFileInterface::METHOD_BZIP2 === $method ? 46 :
(ZipFile::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10) (
$this->isZip64ExtensionsRequired() ? 45 :
(ZipFileInterface::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10)
) )
); );
} }
/** /**
* Write local file header, encryption header, file data and data descriptor to output stream. * Clone extra fields
*
* @param resource $outputStream
* @throws ZipException
*/ */
public function writeEntry($outputStream) public function __clone()
{ {
$nameLength = strlen($this->getName()); $this->clone = true;
$size = $nameLength + strlen($this->getExtra()) + strlen($this->getComment()); parent::__clone();
if (0xffff < $size) {
throw new ZipException($this->getName()
. " (the total size of "
. $size
. " bytes for the name, extra fields and comment exceeds the maximum size of "
. 0xffff . " bytes)");
}
if (self::UNKNOWN === $this->getPlatform()) {
$this->setPlatform(self::PLATFORM_UNIX);
}
if (self::UNKNOWN === $this->getTime()) {
$this->setTime(time());
}
$method = $this->getMethod();
if (self::UNKNOWN === $method) {
$this->setMethod($method = ZipFile::METHOD_DEFLATED);
}
$skipCrc = false;
$encrypted = $this->isEncrypted();
$dd = $this->isDataDescriptorRequired();
// Compose General Purpose Bit Flag.
// See appendix D of PKWARE's ZIP File Format Specification.
$utf8 = true;
$general = ($encrypted ? self::GPBF_ENCRYPTED : 0)
| ($dd ? self::GPBF_DATA_DESCRIPTOR : 0)
| ($utf8 ? self::GPBF_UTF8 : 0);
$entryContent = $this->getEntryContent();
$this->setSize(strlen($entryContent));
$this->setCrc(crc32($entryContent));
if ($encrypted && null === $this->getPassword()) {
throw new ZipException("Can not password from entry " . $this->getName());
}
if (
$encrypted &&
(
self::METHOD_WINZIP_AES === $method ||
$this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES
)
) {
$field = null;
$method = $this->getMethod();
$keyStrength = 256; // bits
$compressedSize = $this->getCompressedSize();
if (self::METHOD_WINZIP_AES === $method) {
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
if (null !== $field) {
$method = $field->getMethod();
if (self::UNKNOWN !== $compressedSize) {
$compressedSize -= $field->getKeyStrength() / 2 // salt value
+ 2 // password verification value
+ 10; // authentication code
}
$this->setMethod($method);
}
}
if (null === $field) {
$field = new WinZipAesEntryExtraField();
}
$field->setKeyStrength($keyStrength);
$field->setMethod($method);
$size = $this->getSize();
if (20 <= $size && ZipFile::METHOD_BZIP2 !== $method) {
$field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1);
} else {
$field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2);
$skipCrc = true;
}
$this->addExtraField($field);
if (self::UNKNOWN !== $compressedSize) {
$compressedSize += $field->getKeyStrength() / 2 // salt value
+ 2 // password verification value
+ 10; // authentication code
$this->setCompressedSize($compressedSize);
}
if ($skipCrc) {
$this->setCrc(0);
}
}
switch ($method) {
case ZipFile::METHOD_STORED:
break;
case ZipFile::METHOD_DEFLATED:
$entryContent = gzdeflate($entryContent, $this->getCompressionLevel());
break;
case ZipFile::METHOD_BZIP2:
$compressionLevel = $this->getCompressionLevel() === ZipFile::LEVEL_DEFAULT_COMPRESSION ?
self::LEVEL_DEFAULT_BZIP2_COMPRESSION :
$this->getCompressionLevel();
$entryContent = bzcompress($entryContent, $compressionLevel);
if (is_int($entryContent)) {
throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent);
}
break;
default:
throw new ZipException($this->getName() . " (unsupported compression method " . $method . ")");
}
if ($encrypted) {
if ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_WINZIP_AES) {
if ($skipCrc) {
$this->setCrc(0);
}
$this->setMethod(self::METHOD_WINZIP_AES);
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
$winZipAesEngine = new WinZipAesEngine($this, $field);
$entryContent = $winZipAesEngine->encrypt($entryContent);
} elseif ($this->getEncryptionMethod() === ZipFile::ENCRYPTION_METHOD_TRADITIONAL) {
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this);
$entryContent = $zipCryptoEngine->encrypt($entryContent);
}
}
$compressedSize = strlen($entryContent);
$this->setCompressedSize($compressedSize);
$offset = ftell($outputStream);
// Commit changes.
$this->setGeneralPurposeBitFlags($general);
$this->setOffset($offset);
$extra = $this->getExtra();
// zip align
$padding = 0;
$zipAlign = $this->getCentralDirectory()->getZipAlign();
$extraLength = strlen($extra);
if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) {
$padding =
(
$zipAlign -
(
$offset +
ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
$nameLength + $extraLength
) % $zipAlign
) % $zipAlign;
}
fwrite(
$outputStream,
pack(
'VvvvVVVVvv',
// local file header signature 4 bytes (0x04034b50)
self::LOCAL_FILE_HEADER_SIG,
// version needed to extract 2 bytes
$this->getVersionNeededToExtract(),
// general purpose bit flag 2 bytes
$general,
// compression method 2 bytes
$this->getMethod(),
// last mod file time 2 bytes
// last mod file date 2 bytes
$this->getDosTime(),
// crc-32 4 bytes
$dd ? 0 : $this->getCrc(),
// compressed size 4 bytes
$dd ? 0 : $this->getCompressedSize(),
// uncompressed size 4 bytes
$dd ? 0 : $this->getSize(),
// file name length 2 bytes
$nameLength,
// extra field length 2 bytes
$extraLength + $padding
)
);
fwrite($outputStream, $this->getName());
if ($extraLength > 0) {
fwrite($outputStream, $extra);
}
if ($padding > 0) {
fwrite($outputStream, str_repeat(chr(0), $padding));
}
if (null !== $entryContent) {
fwrite($outputStream, $entryContent);
}
assert(self::UNKNOWN !== $this->getCrc());
assert(self::UNKNOWN !== $this->getSize());
if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) {
// data descriptor signature 4 bytes (0x08074b50)
// crc-32 4 bytes
fwrite($outputStream, pack('VV', self::DATA_DESCRIPTOR_SIG, $this->getCrc()));
// compressed size 4 or 8 bytes
// uncompressed size 4 or 8 bytes
if ($this->isZip64ExtensionsRequired()) {
fwrite($outputStream, PackUtil::packLongLE($compressedSize));
fwrite($outputStream, PackUtil::packLongLE($this->getSize()));
} else {
fwrite($outputStream, pack('VV', $this->getCompressedSize(), $this->getSize()));
}
} elseif ($this->getCompressedSize() != $compressedSize) {
throw new ZipException($this->getName()
. " (expected compressed entry size of "
. $this->getCompressedSize() . " bytes, but is actually " . $compressedSize . " bytes)");
}
} }
} public function __destruct()
{
if (!$this->clone && $this->content !== null && is_resource($this->content)) {
fclose($this->content);
$this->content = null;
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipException;
/**
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipNewFileEntry extends ZipAbstractEntry
{
/**
* @var string Filename
*/
protected $file;
/**
* ZipNewEntry constructor.
* @param string $file
* @throws ZipException
*/
public function __construct($file)
{
parent::__construct();
if ($file === null) {
throw new InvalidArgumentException("file is null");
}
$file = (string)$file;
if (!is_file($file)) {
throw new ZipException("File $file does not exist.");
}
if (!is_readable($file)) {
throw new ZipException("The '$file' file could not be read. Check permissions.");
}
$this->file = $file;
}
/**
* Returns an string content of the given entry.
*
* @return null|string
*/
public function getEntryContent()
{
if (!is_file($this->file)) {
throw new RuntimeException("File {$this->file} does not exist.");
}
return file_get_contents($this->file);
}
}

View File

@@ -1,55 +0,0 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipException;
/**
* New zip entry from stream.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipNewStreamEntry extends ZipNewEntry
{
/**
* @var resource
*/
private $stream;
/**
* ZipNewStreamEntry constructor.
* @param resource $stream
* @throws InvalidArgumentException
*/
public function __construct($stream)
{
if (!is_resource($stream)) {
throw new InvalidArgumentException('stream is not resource');
}
$this->stream = $stream;
}
/**
* Returns an string content of the given entry.
*
* @return null|string
* @throws ZipException
*/
public function getEntryContent()
{
return stream_get_contents($this->stream, -1, 0);
}
/**
* Release stream resource.
*/
function __destruct()
{
if (null !== $this->stream) {
fclose($this->stream);
$this->stream = null;
}
}
}

View File

@@ -1,39 +0,0 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\ZipException;
/**
* New zip entry from string.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipNewStringEntry extends ZipNewEntry
{
/**
* @var string
*/
private $entryContent;
/**
* ZipNewStringEntry constructor.
* @param string $entryContent
*/
public function __construct($entryContent)
{
$this->entryContent = $entryContent;
}
/**
* Returns an string content of the given entry.
*
* @return null|string
* @throws ZipException
*/
public function getEntryContent()
{
return $this->entryContent;
}
}

View File

@@ -1,327 +0,0 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
use PhpZip\Crypto\WinZipAesEngine;
use PhpZip\Exception\Crc32Exception;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipCryptoException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipUnsupportMethod;
use PhpZip\Extra\WinZipAesEntryExtraField;
use PhpZip\Model\CentralDirectory;
use PhpZip\Model\ZipEntry;
use PhpZip\ZipFile;
/**
* This class is used to represent a ZIP file entry.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipReadEntry extends ZipAbstractEntry
{
/**
* Max size cached content in memory.
*/
const MAX_SIZE_CACHED_CONTENT_IN_MEMORY = 3145728; // 3 mb
/**
* @var resource
*/
private $inputStream;
/**
* @var string
*/
private $charset;
/**
* @var string|resource Cached entry content.
*/
private $entryContent;
/**
* ZipFileEntry constructor.
* @param $inputStream
*/
public function __construct($inputStream)
{
$this->inputStream = $inputStream;
$this->readZipEntry($inputStream);
}
/**
* @param resource $inputStream
* @throws InvalidArgumentException
*/
private function readZipEntry($inputStream)
{
// central file header signature 4 bytes (0x02014b50)
$fileHeaderSig = unpack('V', fread($inputStream, 4))[1];
if (CentralDirectory::CENTRAL_FILE_HEADER_SIG !== $fileHeaderSig) {
throw new InvalidArgumentException("Corrupt zip file. Can not read zip entry.");
}
// 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
$data = unpack(
'vversionMadeBy/vversionNeededToExtract/vgpbf/vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/' .
'VrawSize/vfileLength/vextraLength/vcommentLength/VrawInternalAttributes/VrawExternalAttributes/VlfhOff',
fread($inputStream, 42)
);
$utf8 = 0 !== ($data['gpbf'] & self::GPBF_UTF8);
if ($utf8) {
$this->charset = "UTF-8";
}
// See appendix D of PKWARE's ZIP File Format Specification.
$name = fread($inputStream, $data['fileLength']);
$this->setName($name);
$this->setVersionNeededToExtract($data['versionNeededToExtract']);
$this->setPlatform($data['versionMadeBy'] >> 8);
$this->setGeneralPurposeBitFlags($data['gpbf']);
$this->setMethod($data['rawMethod']);
$this->setDosTime($data['rawTime']);
$this->setCrc($data['rawCrc']);
$this->setCompressedSize($data['rawCompressedSize']);
$this->setSize($data['rawSize']);
$this->setExternalAttributes($data['rawExternalAttributes']);
$this->setOffset($data['lfhOff']); // must be unmapped!
if (0 < $data['extraLength']) {
$this->setExtra(fread($inputStream, $data['extraLength']));
}
if (0 < $data['commentLength']) {
$this->setComment(fread($inputStream, $data['commentLength']));
}
}
/**
* Returns an string content of the given entry.
*
* @return string
* @throws ZipException
*/
public function getEntryContent()
{
if (null === $this->entryContent) {
if ($this->isDirectory()) {
$this->entryContent = null;
return $this->entryContent;
}
$isEncrypted = $this->isEncrypted();
$password = $this->getPassword();
if ($isEncrypted && empty($password)) {
throw new ZipException("Not set password");
}
$pos = $this->getOffset();
assert(self::UNKNOWN !== $pos);
$startPos = $pos = $this->getCentralDirectory()->getEndOfCentralDirectory()->getMapper()->map($pos);
fseek($this->inputStream, $startPos);
// local file header signature 4 bytes (0x04034b50)
if (self::LOCAL_FILE_HEADER_SIG !== unpack('V', fread($this->inputStream, 4))[1]) {
throw new ZipException($this->getName() . " (expected Local File Header)");
}
fseek($this->inputStream, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS);
// file name length 2 bytes
// extra field length 2 bytes
$data = unpack('vfileLength/vextraLength', fread($this->inputStream, 4));
$pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength'];
assert(self::UNKNOWN !== $this->getCrc());
$method = $this->getMethod();
fseek($this->inputStream, $pos);
// Get raw entry content
$content = fread($this->inputStream, $this->getCompressedSize());
// Strong Encryption Specification - WinZip AES
if ($this->isEncrypted()) {
if (self::METHOD_WINZIP_AES === $method) {
$winZipAesEngine = new WinZipAesEngine($this);
$content = $winZipAesEngine->decrypt($content);
// Disable redundant CRC-32 check.
$isEncrypted = false;
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $this->getExtraField(WinZipAesEntryExtraField::getHeaderId());
$method = $field->getMethod();
$this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_WINZIP_AES);
} else {
// Traditional PKWARE Decryption
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($this);
$content = $zipCryptoEngine->decrypt($content);
$this->setEncryptionMethod(ZipFile::ENCRYPTION_METHOD_TRADITIONAL);
}
}
if ($isEncrypted) {
// Check CRC32 in the Local File Header or Data Descriptor.
$localCrc = null;
if ($this->getGeneralPurposeBitFlag(self::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 + $this->getCompressedSize());
$localCrc = unpack('V', fread($this->inputStream, 4))[1];
if (self::DATA_DESCRIPTOR_SIG === $localCrc) {
$localCrc = unpack('V', fread($this->inputStream, 4))[1];
}
} else {
fseek($this->inputStream, $startPos + 14);
// The CRC32 in the Local File Header.
$localCrc = unpack('V', fread($this->inputStream, 4))[1];
}
if ($this->getCrc() !== $localCrc) {
throw new Crc32Exception($this->getName(), $this->getCrc(), $localCrc);
}
}
switch ($method) {
case ZipFile::METHOD_STORED:
break;
case ZipFile::METHOD_DEFLATED:
$content = gzinflate($content);
break;
case ZipFile::METHOD_BZIP2:
if (!extension_loaded('bz2')) {
throw new ZipException('Extension bzip2 not install');
}
$content = bzdecompress($content);
break;
default:
throw new ZipUnsupportMethod($this->getName()
. " (compression method "
. $method
. " is not supported)");
}
if ($isEncrypted) {
$localCrc = crc32($content);
if ($this->getCrc() !== $localCrc) {
if ($this->isEncrypted()) {
throw new ZipCryptoException("Wrong password");
}
throw new Crc32Exception($this->getName(), $this->getCrc(), $localCrc);
}
}
if ($this->getSize() < self::MAX_SIZE_CACHED_CONTENT_IN_MEMORY) {
$this->entryContent = $content;
} else {
$this->entryContent = fopen('php://temp', 'rb');
fwrite($this->entryContent, $content);
}
return $content;
}
if (is_resource($this->entryContent)) {
return stream_get_contents($this->entryContent, -1, 0);
}
return $this->entryContent;
}
/**
* Write local file header, encryption header, file data and data descriptor to output stream.
*
* @param resource $outputStream
*/
public function writeEntry($outputStream)
{
$pos = $this->getOffset();
assert(ZipEntry::UNKNOWN !== $pos);
$pos = $this->getCentralDirectory()->getEndOfCentralDirectory()->getMapper()->map($pos);
$pos += ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS;
$this->setOffset(ftell($outputStream));
// zip align
$padding = 0;
$zipAlign = $this->getCentralDirectory()->getZipAlign();
$extra = $this->getExtra();
$extraLength = strlen($extra);
$nameLength = strlen($this->getName());
if ($zipAlign !== null && !$this->isEncrypted() && $this->getMethod() === ZipFile::METHOD_STORED) {
$padding =
(
$zipAlign -
($this->getOffset() + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength)
% $zipAlign
) % $zipAlign;
}
$dd = $this->isDataDescriptorRequired();
fwrite(
$outputStream,
pack(
'VvvvVVVVvv',
// local file header signature 4 bytes (0x04034b50)
self::LOCAL_FILE_HEADER_SIG,
// version needed to extract 2 bytes
$this->getVersionNeededToExtract(),
// general purpose bit flag 2 bytes
$this->getGeneralPurposeBitFlags(),
// compression method 2 bytes
$this->getMethod(),
// last mod file time 2 bytes
// last mod file date 2 bytes
$this->getDosTime(),
// crc-32 4 bytes
$dd ? 0 : $this->getCrc(),
// compressed size 4 bytes
$dd ? 0 : $this->getCompressedSize(),
// uncompressed size 4 bytes
$dd ? 0 : $this->getSize(),
$nameLength,
// extra field length 2 bytes
$extraLength + $padding
)
);
fwrite($outputStream, $this->getName());
if ($extraLength > 0) {
fwrite($outputStream, $extra);
}
if ($padding > 0) {
fwrite($outputStream, str_repeat(chr(0), $padding));
}
fseek($this->inputStream, $pos);
$data = unpack('vfileLength/vextraLength', fread($this->inputStream, 4));
fseek($this->inputStream, $data['fileLength'] + $data['extraLength'], SEEK_CUR);
$length = $this->getCompressedSize();
if ($this->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
$length += 12;
if ($this->isZip64ExtensionsRequired()) {
$length += 8;
}
}
stream_copy_to_stream($this->inputStream, $outputStream, $length);
}
function __destruct()
{
if (null !== $this->entryContent && is_resource($this->entryContent)) {
fclose($this->entryContent);
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace PhpZip\Model\Entry;
use PhpZip\Exception\ZipException;
use PhpZip\Stream\ZipInputStreamInterface;
/**
* This class is used to represent a ZIP file entry.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipSourceEntry extends ZipAbstractEntry
{
/**
* Max size cached content in memory.
*/
const MAX_SIZE_CACHED_CONTENT_IN_MEMORY = 524288; // 512 kb
/**
* @var ZipInputStreamInterface
*/
protected $inputStream;
/**
* @var string|resource Cached entry content.
*/
protected $entryContent;
/**
* @var string
*/
protected $readPassword;
/**
* @var bool
*/
private $clone = false;
/**
* ZipSourceEntry constructor.
* @param ZipInputStreamInterface $inputStream
*/
public function __construct(ZipInputStreamInterface $inputStream)
{
parent::__construct();
$this->inputStream = $inputStream;
}
/**
* @return ZipInputStreamInterface
*/
public function getInputStream()
{
return $this->inputStream;
}
/**
* Returns an string content of the given entry.
*
* @return string
*/
public function getEntryContent()
{
if ($this->entryContent === null) {
// In order not to unpack again, we cache the content in memory or on disk
$content = $this->inputStream->readEntryContent($this);
if ($this->getSize() < self::MAX_SIZE_CACHED_CONTENT_IN_MEMORY) {
$this->entryContent = $content;
} else {
$this->entryContent = fopen('php://temp', 'r+b');
fwrite($this->entryContent, $content);
}
return $content;
}
if (is_resource($this->entryContent)) {
rewind($this->entryContent);
return stream_get_contents($this->entryContent);
}
return $this->entryContent;
}
/**
* Clone extra fields
*/
public function __clone()
{
$this->clone = true;
parent::__clone();
}
public function __destruct()
{
if (!$this->clone && $this->entryContent !== null && is_resource($this->entryContent)) {
fclose($this->entryContent);
}
}
}

View File

@@ -1,9 +1,10 @@
<?php <?php
namespace PhpZip\Model; namespace PhpZip\Model;
use PhpZip\Exception\ZipException; use PhpZip\Exception\ZipException;
use PhpZip\Extra\ExtraField; use PhpZip\Extra\ExtraFieldsCollection;
use PhpZip\ZipFile; use PhpZip\ZipFileInterface;
/** /**
* ZIP file entry. * ZIP file entry.
@@ -17,7 +18,7 @@ interface ZipEntry
// Bit masks for initialized fields. // Bit masks for initialized fields.
const BIT_PLATFORM = 1, const BIT_PLATFORM = 1,
BIT_METHOD = 2 /* 1 << 1 */, BIT_METHOD = 2 /* 1 << 1 */,
BIT_CRC = 2 /* 1 << 2 */, BIT_CRC = 4 /* 1 << 2 */,
BIT_DATE_TIME = 64 /* 1 << 6 */, BIT_DATE_TIME = 64 /* 1 << 6 */,
BIT_EXTERNAL_ATTR = 128 /* 1 << 7*/ BIT_EXTERNAL_ATTR = 128 /* 1 << 7*/
; ;
@@ -39,11 +40,21 @@ interface ZipEntry
const METHOD_WINZIP_AES = 99; const METHOD_WINZIP_AES = 99;
/** General Purpose Bit Flag mask for encrypted data. */ /** General Purpose Bit Flag mask for encrypted data. */
const GPBF_ENCRYPTED = 1; const GPBF_ENCRYPTED = 1; // 1 << 0
// (For Methods 8 and 9 - Deflating)
// Bit 2 Bit 1
// 0 0 Normal compression
// 0 1 Maximum compression
// 1 0 Fast compression
// 1 1 Super Fast compression
const GPBF_COMPRESSION_FLAG1 = 2; // 1 << 1
const GPBF_COMPRESSION_FLAG2 = 4; // 1 << 2
/** General Purpose Bit Flag mask for data descriptor. */ /** General Purpose Bit Flag mask for data descriptor. */
const GPBF_DATA_DESCRIPTOR = 8; // 1 << 3; const GPBF_DATA_DESCRIPTOR = 8; // 1 << 3
/** General Purpose Bit Flag mask for strong encryption. */
const GPBF_STRONG_ENCRYPTION = 64; // 1 << 6
/** General Purpose Bit Flag mask for UTF-8. */ /** General Purpose Bit Flag mask for UTF-8. */
const GPBF_UTF8 = 2048; const GPBF_UTF8 = 2048; // 1 << 11
/** Local File Header signature. */ /** Local File Header signature. */
const LOCAL_FILE_HEADER_SIG = 0x04034B50; const LOCAL_FILE_HEADER_SIG = 0x04034B50;
@@ -76,19 +87,11 @@ interface ZipEntry
* Compressed Size 4 * Compressed Size 4
* Uncompressed Size 4 * Uncompressed Size 4
*/ */
const LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS = 26; // 1 << 11; const LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS = 26;
/** /**
* @return CentralDirectory * Default compression level for bzip2
*/ */
public function getCentralDirectory(); const LEVEL_DEFAULT_BZIP2_COMPRESSION = 4;
/**
* @param CentralDirectory $centralDirectory
* @return ZipEntry
*/
public function setCentralDirectory(CentralDirectory $centralDirectory);
/** /**
* Returns the ZIP entry name. * Returns the ZIP entry name.
@@ -197,7 +200,7 @@ interface ZipEntry
/** /**
* Returns the General Purpose Bit Flags. * Returns the General Purpose Bit Flags.
* *
* @return bool * @return int
*/ */
public function getGeneralPurposeBitFlags(); public function getGeneralPurposeBitFlags();
@@ -234,14 +237,6 @@ interface ZipEntry
*/ */
public function isEncrypted(); public function isEncrypted();
/**
* Sets the encryption property to false and removes any other
* encryption artifacts.
*
* @return ZipEntry
*/
public function clearEncryption();
/** /**
* Sets the encryption flag for this ZIP entry. * Sets the encryption flag for this ZIP entry.
* *
@@ -250,6 +245,14 @@ interface ZipEntry
*/ */
public function setEncrypted($encrypted); public function setEncrypted($encrypted);
/**
* Sets the encryption property to false and removes any other
* encryption artifacts.
*
* @return ZipEntry
*/
public function disableEncryption();
/** /**
* Returns the compression method for this entry. * Returns the compression method for this entry.
* *
@@ -312,37 +315,9 @@ interface ZipEntry
public function setExternalAttributes($externalAttributes); public function setExternalAttributes($externalAttributes);
/** /**
* Return extra field from header id. * @return ExtraFieldsCollection
*
* @param int $headerId
* @return ExtraField|null
*/ */
public function getExtraField($headerId); public function getExtraFieldsCollection();
/**
* Add extra field.
*
* @param ExtraField $field
* @return ExtraField
* @throws ZipException
*/
public function addExtraField($field);
/**
* Return exists extra field from header id.
*
* @param int $headerId
* @return bool
*/
public function hasExtraField($headerId);
/**
* Remove extra field from header id.
*
* @param int $headerId
* @return ExtraField|null
*/
public function removeExtraField($headerId);
/** /**
* Returns a protective copy of the serialized Extra Fields. * Returns a protective copy of the serialized Extra Fields.
@@ -362,8 +337,6 @@ interface ZipEntry
* *
* @param string $data The byte array holding the serialized Extra Fields. * @param string $data The byte array holding the serialized Extra Fields.
* @throws ZipException if the serialized Extra Fields exceed 64 KB * @throws ZipException if the serialized Extra Fields exceed 64 KB
* @return ZipEntry
* or do not conform to the ZIP File Format Specification
*/ */
public function setExtra($data); public function setExtra($data);
@@ -425,8 +398,10 @@ interface ZipEntry
/** /**
* Set encryption method * Set encryption method
* *
* @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL * @see ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL
* @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES * @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192
* @see ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256
* *
* @param int $encryptionMethod * @param int $encryptionMethod
* @return ZipEntry * @return ZipEntry
@@ -443,10 +418,13 @@ interface ZipEntry
public function getEntryContent(); public function getEntryContent();
/** /**
* Write local file header, encryption header, file data and data descriptor to output stream. * @param int $compressionLevel
* * @return ZipEntry
* @param resource $outputStream
* @throws ZipException
*/ */
public function writeEntry($outputStream); public function setCompressionLevel($compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION);
}
/**
* @return int
*/
public function getCompressionLevel();
}

View File

@@ -0,0 +1,166 @@
<?php
namespace PhpZip\Model;
/**
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipEntryMatcher implements \Countable
{
/**
* @var ZipModel
*/
protected $zipModel;
/**
* @var array
*/
protected $matches = [];
/**
* ZipEntryMatcher constructor.
* @param ZipModel $zipModel
*/
public function __construct(ZipModel $zipModel)
{
$this->zipModel = $zipModel;
}
/**
* @param string|array $entries
* @return ZipEntryMatcher
*/
public function add($entries)
{
$entries = (array)$entries;
$entries = array_map(function ($entry) {
return $entry instanceof ZipEntry ? $entry->getName() : $entry;
}, $entries);
$this->matches = array_unique(
array_merge(
$this->matches,
array_keys(
array_intersect_key(
$this->zipModel->getEntries(),
array_flip($entries)
)
)
)
);
return $this;
}
/**
* @param string $regexp
* @return ZipEntryMatcher
*/
public function match($regexp)
{
array_walk($this->zipModel->getEntries(), function (
/** @noinspection PhpUnusedParameterInspection */
$entry,
$entryName
) use ($regexp) {
if (preg_match($regexp, $entryName)) {
$this->matches[] = $entryName;
}
});
$this->matches = array_unique($this->matches);
return $this;
}
/**
* @return ZipEntryMatcher
*/
public function all()
{
$this->matches = array_keys($this->zipModel->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, function ($entryName) use ($callable) {
call_user_func($callable, $entryName);
});
}
}
/**
* @return array
*/
public function getMatches()
{
return $this->matches;
}
public function delete()
{
array_walk($this->matches, function ($entry) {
$this->zipModel->deleteEntry($entry);
});
$this->matches = [];
}
/**
* @param string|null $password
* @param int|null $encryptionMethod
*/
public function setPassword($password, $encryptionMethod = null)
{
array_walk($this->matches, function ($entry) use ($password, $encryptionMethod) {
$entry = $this->zipModel->getEntry($entry);
if (!$entry->isDirectory()) {
$this->zipModel->getEntryForChanges($entry)->setPassword($password, $encryptionMethod);
}
});
}
/**
* @param int $encryptionMethod
*/
public function setEncryptionMethod($encryptionMethod)
{
array_walk($this->matches, function ($entry) use ($encryptionMethod) {
$entry = $this->zipModel->getEntry($entry);
if (!$entry->isDirectory()) {
$this->zipModel->getEntryForChanges($entry)->setEncryptionMethod($encryptionMethod);
}
});
}
public function disableEncryption()
{
array_walk($this->matches, function ($entry) {
$entry = $this->zipModel->getEntry($entry);
if (!$entry->isDirectory()) {
$entry = $this->zipModel->getEntryForChanges($entry);
$entry->disableEncryption();
}
});
}
/**
* Count elements of an object
* @link http://php.net/manual/en/countable.count.php
* @return int The custom count as an integer.
* </p>
* <p>
* The return value is cast to an integer.
* @since 5.1.0
*/
public function count()
{
return count($this->matches);
}
}

View File

@@ -1,10 +1,11 @@
<?php <?php /** @noinspection PhpMissingBreakStatementInspection */
namespace PhpZip\Model; namespace PhpZip\Model;
use PhpZip\Extra\NtfsExtraField; use PhpZip\Extra\Fields\NtfsExtraField;
use PhpZip\Extra\WinZipAesEntryExtraField; use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Util\FilesUtil; use PhpZip\Util\FilesUtil;
use PhpZip\ZipFile; use PhpZip\ZipFileInterface;
/** /**
* Zip info * Zip info
@@ -86,7 +87,8 @@ class ZipInfo
]; ];
private static $valuesCompressionMethod = [ private static $valuesCompressionMethod = [
ZipFile::METHOD_STORED => 'no compression', ZipEntry::UNKNOWN => 'unknown',
ZipFileInterface::METHOD_STORED => 'no compression',
1 => 'shrink', 1 => 'shrink',
2 => 'reduce level 1', 2 => 'reduce level 1',
3 => 'reduce level 2', 3 => 'reduce level 2',
@@ -94,7 +96,7 @@ class ZipInfo
5 => 'reduce level 4', 5 => 'reduce level 4',
6 => 'implode', 6 => 'implode',
7 => 'reserved for Tokenizing compression algorithm', 7 => 'reserved for Tokenizing compression algorithm',
ZipFile::METHOD_DEFLATED => 'deflate', ZipFileInterface::METHOD_DEFLATED => 'deflate',
9 => 'deflate64', 9 => 'deflate64',
10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)', 10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)',
11 => 'reserved by PKWARE', 11 => 'reserved by PKWARE',
@@ -114,77 +116,77 @@ class ZipInfo
/** /**
* @var string * @var string
*/ */
private $path; private $name;
/** /**
* @var bool * @var bool
*/ */
private $folder; private $folder;
/** /**
* @var int * @var int
*/ */
private $size; private $size;
/** /**
* @var int * @var int
*/ */
private $compressedSize; private $compressedSize;
/** /**
* @var int * @var int
*/ */
private $mtime; private $mtime;
/** /**
* @var int|null * @var int|null
*/ */
private $ctime; private $ctime;
/** /**
* @var int|null * @var int|null
*/ */
private $atime; private $atime;
/** /**
* @var bool * @var bool
*/ */
private $encrypted; private $encrypted;
/** /**
* @var string|null * @var string|null
*/ */
private $comment; private $comment;
/** /**
* @var int * @var int
*/ */
private $crc; private $crc;
/** /**
* @var string * @var string
*/ */
private $method; private $methodName;
/**
* @var int
*/
private $compressionMethod;
/** /**
* @var string * @var string
*/ */
private $platform; private $platform;
/** /**
* @var int * @var int
*/ */
private $version; private $version;
/** /**
* @var string * @var string
*/ */
private $attributes; private $attributes;
/**
* @var int|null
*/
private $encryptionMethod;
/**
* @var int|null
*/
private $compressionLevel;
/** /**
* ZipInfo constructor. * ZipInfo constructor.
* *
* @param ZipEntry $entry * @param ZipEntry $entry
* @throws \PhpZip\Exception\ZipException
*/ */
public function __construct(ZipEntry $entry) public function __construct(ZipEntry $entry)
{ {
@@ -192,35 +194,45 @@ class ZipInfo
$atime = null; $atime = null;
$ctime = null; $ctime = null;
$field = $entry->getExtraField(NtfsExtraField::getHeaderId()); $field = $entry->getExtraFieldsCollection()->get(NtfsExtraField::getHeaderId());
if (null !== $field && $field instanceof NtfsExtraField) { if (null !== $field && $field instanceof NtfsExtraField) {
/** /**
* @var NtfsExtraField $field * @var NtfsExtraField $field
*/ */
$atime = $field->getAtime(); $atime = $field->getAtime();
$ctime = $field->getCtime(); $ctime = $field->getCtime();
$mtime = $field->getMtime();
} }
$this->path = $entry->getName(); $this->name = $entry->getName();
$this->folder = $entry->isDirectory(); $this->folder = $entry->isDirectory();
$this->size = $entry->getSize(); $this->size = PHP_INT_SIZE === 4 ?
$this->compressedSize = $entry->getCompressedSize(); sprintf('%u', $entry->getSize()) :
$entry->getSize();
$this->compressedSize = PHP_INT_SIZE === 4 ?
sprintf('%u', $entry->getCompressedSize()) :
$entry->getCompressedSize();
$this->mtime = $mtime; $this->mtime = $mtime;
$this->ctime = $ctime; $this->ctime = $ctime;
$this->atime = $atime; $this->atime = $atime;
$this->encrypted = $entry->isEncrypted(); $this->encrypted = $entry->isEncrypted();
$this->encryptionMethod = $entry->getEncryptionMethod();
$this->comment = $entry->getComment(); $this->comment = $entry->getComment();
$this->crc = $entry->getCrc(); $this->crc = $entry->getCrc();
$this->method = self::getMethodName($entry); $this->compressionMethod = self::getMethodId($entry);
$this->methodName = self::getEntryMethodName($entry);
$this->platform = self::getPlatformName($entry); $this->platform = self::getPlatformName($entry);
$this->version = $entry->getVersionNeededToExtract(); $this->version = $entry->getVersionNeededToExtract();
$this->compressionLevel = $entry->getCompressionLevel();
$attributes = str_repeat(" ", 12); $attributes = str_repeat(" ", 12);
$externalAttributes = $entry->getExternalAttributes(); $externalAttributes = $entry->getExternalAttributes();
$externalAttributes = PHP_INT_SIZE === 4 ?
sprintf('%u', $externalAttributes) :
$externalAttributes;
$xattr = (($externalAttributes >> 16) & 0xFFFF); $xattr = (($externalAttributes >> 16) & 0xFFFF);
switch ($entry->getPlatform()) { switch ($entry->getPlatform()) {
case self::MADE_BY_MS_DOS: case self::MADE_BY_MS_DOS:
/** @noinspection PhpMissingBreakStatementInspection */
case self::MADE_BY_WINDOWS_NTFS: case self::MADE_BY_WINDOWS_NTFS:
if ($entry->getPlatform() != self::MADE_BY_MS_DOS || if ($entry->getPlatform() != self::MADE_BY_MS_DOS ||
($xattr & 0700) != ($xattr & 0700) !=
@@ -237,11 +249,12 @@ class ZipInfo
if ($xattr & 0x10) { if ($xattr & 0x10) {
$attributes[0] = 'd'; $attributes[0] = 'd';
$attributes[3] = 'x'; $attributes[3] = 'x';
} else } else {
$attributes[0] = '-'; $attributes[0] = '-';
if ($xattr & 0x08) }
if ($xattr & 0x08) {
$attributes[0] = 'V'; $attributes[0] = 'V';
else { } else {
$ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION)); $ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION));
if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) { if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) {
$attributes[3] = 'x'; $attributes[3] = 'x';
@@ -250,6 +263,7 @@ class ZipInfo
break; break;
} /* else: fall through! */ } /* else: fall through! */
// no break
default: /* assume Unix-like */ default: /* assume Unix-like */
switch ($xattr & self::UNX_IFMT) { switch ($xattr & self::UNX_IFMT) {
case self::UNX_IFDIR: case self::UNX_IFDIR:
@@ -284,33 +298,61 @@ class ZipInfo
$attributes[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-'; $attributes[5] = ($xattr & self::UNX_IWGRP) ? 'w' : '-';
$attributes[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-'; $attributes[8] = ($xattr & self::UNX_IWOTH) ? 'w' : '-';
if ($xattr & self::UNX_IXUSR) if ($xattr & self::UNX_IXUSR) {
$attributes[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x'; $attributes[3] = ($xattr & self::UNX_ISUID) ? 's' : 'x';
else } else {
$attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */ $attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-';
if ($xattr & self::UNX_IXGRP) } /* S==undefined */
$attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */ if ($xattr & self::UNX_IXGRP) {
else $attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x';
$attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */ } /* == UNX_ENFMT */
if ($xattr & self::UNX_IXOTH) else {
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x'; /* "sticky bit" */ $attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-';
else } /* SunOS 4.1.x */
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-'; /* T==undefined */ if ($xattr & self::UNX_IXOTH) {
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 't' : 'x';
} /* "sticky bit" */
else {
$attributes[9] = ($xattr & self::UNX_ISVTX) ? 'T' : '-';
} /* T==undefined */
} }
$this->attributes = trim($attributes); $this->attributes = trim($attributes);
} }
/** /**
* @param ZipEntry $entry * @param ZipEntry $entry
* @return string * @return int
* @throws \PhpZip\Exception\ZipException
*/ */
public static function getMethodName(ZipEntry $entry) private static function getMethodId(ZipEntry $entry)
{
$method = $entry->getMethod();
if ($entry->isEncrypted()) {
if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) {
$field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId());
if (null !== $field) {
/**
* @var WinZipAesEntryExtraField $field
*/
$method = $field->getMethod();
}
}
}
return $method;
}
/**
* @param ZipEntry $entry
* @return string
* @throws \PhpZip\Exception\ZipException
*/
private static function getEntryMethodName(ZipEntry $entry)
{ {
$return = ''; $return = '';
if ($entry->isEncrypted()) { if ($entry->isEncrypted()) {
if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) { if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) {
$field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId());
$return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]); $return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]);
$field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId());
if (null !== $field) { if (null !== $field) {
/** /**
* @var WinZipAesEntryExtraField $field * @var WinZipAesEntryExtraField $field
@@ -348,34 +390,20 @@ class ZipInfo
} }
/** /**
* @return array * @return string
*/ */
public function toArray() public function getName()
{ {
return [ return $this->name;
'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 * @return string
* @deprecated use \PhpZip\Model\ZipInfo::getName()
*/ */
public function getPath() public function getPath()
{ {
return $this->path; return $this->getName();
} }
/** /**
@@ -426,6 +454,14 @@ class ZipInfo
return $this->atime; return $this->atime;
} }
/**
* @return string
*/
public function getAttributes()
{
return $this->attributes;
}
/** /**
* @return boolean * @return boolean
*/ */
@@ -452,10 +488,19 @@ class ZipInfo
/** /**
* @return string * @return string
* @deprecated use \PhpZip\Model\ZipInfo::getMethodName()
*/ */
public function getMethod() public function getMethod()
{ {
return $this->method; return $this->getMethodName();
}
/**
* @return string
*/
public function getMethodName()
{
return $this->methodName;
} }
/** /**
@@ -475,35 +520,76 @@ class ZipInfo
} }
/** /**
* @return string * @return int|null
*/ */
public function getAttributes() public function getEncryptionMethod()
{ {
return $this->attributes; return $this->encryptionMethod;
}
/**
* @return int|null
*/
public function getCompressionLevel()
{
return $this->compressionLevel;
}
/**
* @return int
*/
public function getCompressionMethod()
{
return $this->compressionMethod;
}
/**
* @return array
*/
public function toArray()
{
return [
'name' => $this->getName(),
'path' => $this->getName(), // deprecated
'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(),
'comment' => $this->getComment(),
'crc' => $this->getCrc(),
'method' => $this->getMethodName(), // deprecated
'method_name' => $this->getMethodName(),
'compression_method' => $this->getCompressionMethod(),
'platform' => $this->getPlatform(),
'version' => $this->getVersion()
];
} }
/** /**
* @return string * @return string
*/ */
function __toString() public function __toString()
{ {
return 'ZipInfo {' return __CLASS__ . ' {'
. 'Path="' . $this->getPath() . '", ' . 'Name="' . $this->getName() . '", '
. ($this->isFolder() ? 'Folder, ' : '') . ($this->isFolder() ? 'Folder, ' : '')
. 'Size=' . FilesUtil::humanSize($this->getSize()) . 'Size="' . FilesUtil::humanSize($this->getSize()) . '"'
. ', Compressed size=' . FilesUtil::humanSize($this->getCompressedSize()) . ', Compressed size="' . FilesUtil::humanSize($this->getCompressedSize()) . '"'
. ', Modified time=' . date(DATE_W3C, $this->getMtime()) . ', ' . ', Modified time="' . date(DATE_W3C, $this->getMtime()) . '", '
. ($this->getCtime() !== null ? 'Created time=' . date(DATE_W3C, $this->getCtime()) . ', ' : '') . ($this->getCtime() !== null ? 'Created time="' . date(DATE_W3C, $this->getCtime()) . '", ' : '')
. ($this->getAtime() !== null ? 'Accessed time=' . date(DATE_W3C, $this->getAtime()) . ', ' : '') . ($this->getAtime() !== null ? 'Accessed time="' . date(DATE_W3C, $this->getAtime()) . '", ' : '')
. ($this->isEncrypted() ? 'Encrypted, ' : '') . ($this->isEncrypted() ? 'Encrypted, ' : '')
. (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '') . (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '')
. (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '') . (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '')
. 'Method="' . $this->getMethod() . '", ' . 'Method name="' . $this->getMethodName() . '", '
. 'Attributes="' . $this->getAttributes() . '", ' . 'Attributes="' . $this->getAttributes() . '", '
. 'Platform="' . $this->getPlatform() . '", ' . 'Platform="' . $this->getPlatform() . '", '
. 'Version=' . $this->getVersion() . 'Version=' . $this->getVersion()
. '}'; . '}';
} }
}
}

View File

@@ -0,0 +1,345 @@
<?php
namespace PhpZip\Model;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\ZipEntryNotFoundException;
use PhpZip\Exception\ZipException;
use PhpZip\Model\Entry\ZipChangesEntry;
use PhpZip\Model\Entry\ZipSourceEntry;
use PhpZip\ZipFileInterface;
/**
* Zip Model
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipModel implements \Countable
{
/**
* @var ZipSourceEntry[]
*/
protected $inputEntries = [];
/**
* @var ZipEntry[]
*/
protected $outEntries = [];
/**
* @var string|null
*/
protected $archiveComment;
/**
* @var string|null
*/
protected $archiveCommentChanges;
/**
* @var bool
*/
protected $archiveCommentChanged = false;
/**
* @var int|null
*/
protected $zipAlign;
/**
* @var bool
*/
private $zip64;
/**
* @param ZipSourceEntry[] $entries
* @param EndOfCentralDirectory $endOfCentralDirectory
* @return ZipModel
*/
public static function newSourceModel(array $entries, EndOfCentralDirectory $endOfCentralDirectory)
{
$model = new self;
$model->inputEntries = $entries;
$model->outEntries = $entries;
$model->archiveComment = $endOfCentralDirectory->getComment();
$model->zip64 = $endOfCentralDirectory->isZip64();
return $model;
}
/**
* @return null|string
*/
public function getArchiveComment()
{
if ($this->archiveCommentChanged) {
return $this->archiveCommentChanges;
}
return $this->archiveComment;
}
/**
* @param string $comment
*/
public function setArchiveComment($comment)
{
if ($comment !== null && strlen($comment) !== 0) {
$comment = (string)$comment;
$length = strlen($comment);
if (0x0000 > $length || $length > 0xffff) {
throw new InvalidArgumentException('Length comment out of range');
}
}
if ($comment !== $this->archiveComment) {
$this->archiveCommentChanges = $comment;
$this->archiveCommentChanged = true;
} else {
$this->archiveCommentChanged = false;
}
}
/**
* Specify a password for extracting files.
*
* @param null|string $password
* @throws ZipException
*/
public function setReadPassword($password)
{
foreach ($this->inputEntries 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->inputEntries[$entryName])) {
throw new ZipEntryNotFoundException($entryName);
}
if ($this->inputEntries[$entryName]->isEncrypted()) {
$this->inputEntries[$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 null|string $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);
}
/**
* @return bool
*/
public function isArchiveCommentChanged()
{
return $this->archiveCommentChanged;
}
/**
* @param string|ZipEntry $old
* @param string|ZipEntry $new
* @throws ZipException
*/
public function renameEntry($old, $new)
{
$old = $old instanceof ZipEntry ? $old->getName() : (string)$old;
$new = $new instanceof ZipEntry ? $new->getName() : (string)$new;
if (isset($this->outEntries[$new])) {
throw new InvalidArgumentException("New entry name " . $new . ' is exists.');
}
$entry = $this->getEntryForChanges($old);
$entry->setName($new);
$this->deleteEntry($old);
$this->addEntry($entry);
}
/**
* @param string|ZipEntry $entry
* @return ZipChangesEntry|ZipEntry
* @throws ZipException
* @throws ZipEntryNotFoundException
*/
public function getEntryForChanges($entry)
{
$entry = $this->getEntry($entry);
if ($entry instanceof ZipSourceEntry) {
$entry = new ZipChangesEntry($entry);
$this->addEntry($entry);
}
return $entry;
}
/**
* @param string|ZipEntry $entryName
* @return ZipEntry
* @throws ZipEntryNotFoundException
*/
public function getEntry($entryName)
{
$entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string)$entryName;
if (isset($this->outEntries[$entryName])) {
return $this->outEntries[$entryName];
}
throw new ZipEntryNotFoundException($entryName);
}
/**
* @param string|ZipEntry $entry
* @return bool
*/
public function deleteEntry($entry)
{
$entry = $entry instanceof ZipEntry ? $entry->getName() : (string)$entry;
if (isset($this->outEntries[$entry])) {
unset($this->outEntries[$entry]);
return true;
}
return false;
}
/**
* @param ZipEntry $entry
*/
public function addEntry(ZipEntry $entry)
{
$this->outEntries[$entry->getName()] = $entry;
}
/**
* Get all entries with changes.
*
* @return ZipEntry[]
*/
public function &getEntries()
{
return $this->outEntries;
}
/**
* @param string|ZipEntry $entryName
* @return bool
*/
public function hasEntry($entryName)
{
$entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string)$entryName;
return isset($this->outEntries[$entryName]);
}
/**
* Delete all entries.
*/
public function deleteAll()
{
$this->outEntries = [];
}
/**
* Count elements of an object
* @link http://php.net/manual/en/countable.count.php
* @return int The custom count as an integer.
* </p>
* <p>
* The return value is cast to an integer.
* @since 5.1.0
*/
public function count()
{
return sizeof($this->outEntries);
}
/**
* Undo all changes done in the archive
*/
public function unchangeAll()
{
$this->outEntries = $this->inputEntries;
$this->unchangeArchiveComment();
}
/**
* Undo change archive comment
*/
public function unchangeArchiveComment()
{
$this->archiveCommentChanges = null;
$this->archiveCommentChanged = false;
}
/**
* 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 (isset($this->outEntries[$entry]) && isset($this->inputEntries[$entry])) {
$this->outEntries[$entry] = $this->inputEntries[$entry];
return true;
}
return false;
}
/**
* @param int $encryptionMethod
*/
public function setEncryptionMethod($encryptionMethod = ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256)
{
$this->matcher()->all()->setEncryptionMethod($encryptionMethod);
}
/**
* @return ZipEntryMatcher
*/
public function matcher()
{
return new ZipEntryMatcher($this);
}
}

View File

@@ -0,0 +1,298 @@
<?php
namespace PhpZip\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
*/
private $size;
/**
* @var bool
*/
private $seekable;
/**
* @var bool
*/
private $readable;
/**
* @var bool
*/
private $writable;
/**
* @var array|mixed|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.
*
* @link 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).
*
* @see seek()
* @link http://www.php.net/manual/en/function.fseek.php
* @throws \RuntimeException on failure.
*/
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) {
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
*
* @return int Position of the file pointer
* @throws \RuntimeException on error.
*/
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.
*
* @link 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.
* @return int Returns the number of bytes written to the stream.
* @throws \RuntimeException on failure.
*/
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.
* @return string Returns the data read from the stream, or an empty string
* if no bytes are available.
* @throws \RuntimeException if an error occurs.
*/
public function read($length)
{
return $this->readable ? fread($this->stream, $length) : "";
}
/**
* Returns the remaining contents in a string
*
* @return string
* @throws \RuntimeException if unable to read or an error occurs while
* reading.
*/
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.
*
* @return void
*/
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 = $this->size = $this->uri = null;
$this->readable = $this->writable = $this->seekable = false;
return $result;
}
}

View File

@@ -0,0 +1,660 @@
<?php
namespace PhpZip\Stream;
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
use PhpZip\Crypto\WinZipAesEngine;
use PhpZip\Exception\Crc32Exception;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Exception\ZipException;
use PhpZip\Exception\ZipUnsupportMethodException;
use PhpZip\Extra\ExtraFieldsCollection;
use PhpZip\Extra\ExtraFieldsFactory;
use PhpZip\Extra\Fields\ApkAlignmentExtraField;
use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Mapper\OffsetPositionMapper;
use PhpZip\Mapper\PositionMapper;
use PhpZip\Model\EndOfCentralDirectory;
use PhpZip\Model\Entry\ZipSourceEntry;
use PhpZip\Model\ZipEntry;
use PhpZip\Model\ZipModel;
use PhpZip\Util\PackUtil;
use PhpZip\Util\StringUtil;
use PhpZip\ZipFileInterface;
/**
* Read zip file
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipInputStream implements ZipInputStreamInterface
{
/**
* @var resource
*/
protected $in;
/**
* @var PositionMapper
*/
protected $mapper;
/**
* @var int The number of bytes in the preamble of this ZIP file.
*/
protected $preamble = 0;
/**
* @var int The number of bytes in the postamble of this ZIP file.
*/
protected $postamble = 0;
/**
* @var ZipModel
*/
protected $zipModel;
/**
* ZipInputStream constructor.
* @param resource $in
*/
public function __construct($in)
{
if (!is_resource($in)) {
throw new RuntimeException('$in must be resource');
}
$this->in = $in;
$this->mapper = new PositionMapper();
}
/**
* @return ZipModel
* @throws ZipException
*/
public function readZip()
{
$this->checkZipFileSignature();
$endOfCentralDirectory = $this->readEndOfCentralDirectory();
$entries = $this->mountCentralDirectory($endOfCentralDirectory);
$this->zipModel = ZipModel::newSourceModel($entries, $endOfCentralDirectory);
return $this->zipModel;
}
/**
* Check zip file signature
*
* @throws ZipException if this not .ZIP file.
*/
protected function checkZipFileSignature()
{
rewind($this->in);
// Constraint: A ZIP file must start with a Local File Header
// or a (ZIP64) End Of Central Directory Record if it's empty.
$signatureBytes = fread($this->in, 4);
if (strlen($signatureBytes) < 4) {
throw new ZipException("Invalid zip file.");
}
$signature = unpack('V', $signatureBytes)[1];
if (
ZipEntry::LOCAL_FILE_HEADER_SIG !== $signature
&& EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
&& EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== $signature
) {
throw new ZipException("Expected Local File Header or (ZIP64) End Of Central Directory Record! Signature: " . $signature);
}
}
/**
* @return EndOfCentralDirectory
* @throws ZipException
*/
protected function readEndOfCentralDirectory()
{
$comment = null;
// Search for End of central directory record.
$stats = fstat($this->in);
$size = $stats['size'];
$max = $size - EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN;
$min = $max >= 0xffff ? $max - 0xffff : 0;
for ($endOfCentralDirRecordPos = $max; $endOfCentralDirRecordPos >= $min; $endOfCentralDirRecordPos--) {
fseek($this->in, $endOfCentralDirRecordPos, SEEK_SET);
// end of central dir signature 4 bytes (0x06054b50)
if (EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG !== unpack('V', fread($this->in, 4))[1]) {
continue;
}
// 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
$data = unpack(
'vdiskNo/vcdDiskNo/vcdEntriesDisk/vcdEntries/VcdSize/VcdPos/vcommentLength',
fread($this->in, 18)
);
if (0 !== $data['diskNo'] || 0 !== $data['cdDiskNo'] || $data['cdEntriesDisk'] !== $data['cdEntries']) {
throw new ZipException(
"ZIP file spanning/splitting is not supported!"
);
}
// .ZIP file comment (variable size)
if ($data['commentLength'] > 0) {
$comment = '';
$offset = 0;
while ($offset < $data['commentLength']) {
$read = min(8192 /* chunk size */, $data['commentLength'] - $offset);
$comment .= fread($this->in, $read);
$offset += $read;
}
}
$this->preamble = $endOfCentralDirRecordPos;
$this->postamble = $size - ftell($this->in);
// Check for ZIP64 End Of Central Directory Locator.
$endOfCentralDirLocatorPos = $endOfCentralDirRecordPos - EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LEN;
fseek($this->in, $endOfCentralDirLocatorPos, SEEK_SET);
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
if (
0 > $endOfCentralDirLocatorPos ||
ftell($this->in) === $size ||
EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG !== unpack('V', fread($this->in, 4))[1]
) {
// Seek and check first CFH, probably requiring an offset mapper.
$offset = $endOfCentralDirRecordPos - $data['cdSize'];
fseek($this->in, $offset, SEEK_SET);
$offset -= $data['cdPos'];
if ($offset !== 0) {
$this->mapper = new OffsetPositionMapper($offset);
}
$entryCount = $data['cdEntries'];
return new EndOfCentralDirectory($entryCount, $comment);
}
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
$zip64EndOfCentralDirectoryRecordDisk = unpack('V', fread($this->in, 4))[1];
// relative offset of the zip64
// end of central directory record 8 bytes
$zip64EndOfCentralDirectoryRecordPos = PackUtil::unpackLongLE(fread($this->in, 8));
// total number of disks 4 bytes
$totalDisks = unpack('V', fread($this->in, 4))[1];
if (0 !== $zip64EndOfCentralDirectoryRecordDisk || 1 !== $totalDisks) {
throw new ZipException("ZIP file spanning/splitting is not supported!");
}
fseek($this->in, $zip64EndOfCentralDirectoryRecordPos, SEEK_SET);
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
$zip64EndOfCentralDirSig = unpack('V', fread($this->in, 4))[1];
if (EndOfCentralDirectory::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($this->in, 12, SEEK_CUR);
// number of this disk 4 bytes
$diskNo = unpack('V', fread($this->in, 4))[1];
// number of the disk with the
// start of the central directory 4 bytes
$cdDiskNo = unpack('V', fread($this->in, 4))[1];
// total number of entries in the
// central directory on this disk 8 bytes
$cdEntriesDisk = PackUtil::unpackLongLE(fread($this->in, 8));
// total number of entries in the
// central directory 8 bytes
$cdEntries = PackUtil::unpackLongLE(fread($this->in, 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
fseek($this->in, 8, SEEK_CUR);
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
$cdPos = PackUtil::unpackLongLE(fread($this->in, 8));
// zip64 extensible data sector (variable size)
fseek($this->in, $cdPos, SEEK_SET);
$this->preamble = $zip64EndOfCentralDirectoryRecordPos;
$entryCount = $cdEntries;
$zip64 = true;
return new EndOfCentralDirectory($entryCount, $comment, $zip64);
}
// Start recovering file entries from min.
$this->preamble = $min;
$this->postamble = $size - $min;
return new EndOfCentralDirectory(0, $comment);
}
/**
* 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 $endOfCentralDirectory
* @return ZipEntry[]
* @throws ZipException
*/
protected function mountCentralDirectory(EndOfCentralDirectory $endOfCentralDirectory)
{
$numEntries = $endOfCentralDirectory->getEntryCount();
$entries = [];
for (; $numEntries > 0; $numEntries--) {
$entry = $this->readEntry();
// 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());
$lfhOff = PHP_INT_SIZE === 4 ? sprintf('%u', $lfhOff) : $lfhOff;
if ($lfhOff < $this->preamble) {
$this->preamble = $lfhOff;
}
$entries[$entry->getName()] = $entry;
}
if (($numEntries % 0x10000) !== 0) {
throw new ZipException("Expected " . abs($numEntries) .
($numEntries > 0 ? " more" : " less") .
" entries in the Central Directory!");
}
if ($this->preamble + $this->postamble >= fstat($this->in)['size']) {
$this->checkZipFileSignature();
}
return $entries;
}
/**
* @return ZipEntry
* @throws ZipException
*/
public function readEntry()
{
// central file header signature 4 bytes (0x02014b50)
$fileHeaderSig = unpack('V', fread($this->in, 4))[1];
if ($fileHeaderSig !== ZipOutputStreamInterface::CENTRAL_FILE_HEADER_SIG) {
throw new InvalidArgumentException("Corrupt zip file. Can not read zip entry.");
}
// 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
$data = unpack(
'vversionMadeBy/vversionNeededToExtract/vgpbf/' .
'vrawMethod/VrawTime/VrawCrc/VrawCompressedSize/' .
'VrawSize/vfileLength/vextraLength/vcommentLength/' .
'VrawInternalAttributes/VrawExternalAttributes/VlfhOff',
fread($this->in, 42)
);
// $utf8 = ($data['gpbf'] & ZipEntry::GPBF_UTF8) !== 0;
// See appendix D of PKWARE's ZIP File Format Specification.
$name = '';
$offset = 0;
while ($offset < $data['fileLength']) {
$read = min(8192 /* chunk size */, $data['fileLength'] - $offset);
$name .= fread($this->in, $read);
$offset += $read;
}
$entry = new ZipSourceEntry($this);
$entry->setName($name);
$entry->setVersionNeededToExtract($data['versionNeededToExtract']);
$entry->setPlatform($data['versionMadeBy'] >> 8);
$entry->setMethod($data['rawMethod']);
$entry->setGeneralPurposeBitFlags($data['gpbf']);
$entry->setDosTime($data['rawTime']);
$entry->setCrc($data['rawCrc']);
$entry->setCompressedSize($data['rawCompressedSize']);
$entry->setSize($data['rawSize']);
$entry->setExternalAttributes($data['rawExternalAttributes']);
$entry->setOffset($data['lfhOff']); // must be unmapped!
if ($data['extraLength'] > 0) {
$extra = '';
$offset = 0;
while ($offset < $data['extraLength']) {
$read = min(8192 /* chunk size */, $data['extraLength'] - $offset);
$extra .= fread($this->in, $read);
$offset += $read;
}
$entry->setExtra($extra);
}
if ($data['commentLength'] > 0) {
$comment = '';
$offset = 0;
while ($offset < $data['commentLength']) {
$read = min(8192 /* chunk size */, $data['commentLength'] - $offset);
$comment .= fread($this->in, $read);
$offset += $read;
}
$entry->setComment($comment);
}
return $entry;
}
/**
* @param ZipEntry $entry
* @return string
* @throws ZipException
*/
public function readEntryContent(ZipEntry $entry)
{
if ($entry->isDirectory()) {
return null;
}
if (!($entry instanceof ZipSourceEntry)) {
throw new InvalidArgumentException('entry must be ' . ZipSourceEntry::class);
}
$isEncrypted = $entry->isEncrypted();
if ($isEncrypted && $entry->getPassword() === null) {
throw new ZipException("Can not password from entry " . $entry->getName());
}
$pos = $entry->getOffset();
$pos = PHP_INT_SIZE === 4
? sprintf('%u', $pos) // PHP 32-Bit
: $pos; // PHP 64-Bit
$startPos = $pos = $this->mapper->map($pos);
fseek($this->in, $startPos);
// local file header signature 4 bytes (0x04034b50)
if (unpack('V', fread($this->in, 4))[1] !== ZipEntry::LOCAL_FILE_HEADER_SIG) {
throw new ZipException($entry->getName() . " (expected Local File Header)");
}
fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_FILE_NAME_LENGTH_POS);
// file name length 2 bytes
// extra field length 2 bytes
$data = unpack('vfileLength/vextraLength', fread($this->in, 4));
$pos += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $data['fileLength'] + $data['extraLength'];
assert(ZipEntry::UNKNOWN !== $entry->getCrc());
$method = $entry->getMethod();
fseek($this->in, $pos);
// Get raw entry content
$compressedSize = $entry->getCompressedSize();
$compressedSize = PHP_INT_SIZE === 4 ? sprintf('%u', $compressedSize) : $compressedSize;
$content = '';
if ($compressedSize > 0) {
$offset = 0;
while ($offset < $compressedSize) {
$read = min(8192 /* chunk size */, $compressedSize - $offset);
$content .= fread($this->in, $read);
$offset += $read;
}
}
$skipCheckCrc = false;
if ($isEncrypted) {
if ($method === ZipEntry::METHOD_WINZIP_AES) {
// Strong Encryption Specification - WinZip AES
$winZipAesEngine = new WinZipAesEngine($entry);
$content = $winZipAesEngine->decrypt($content);
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId());
$method = $field->getMethod();
$entry->setEncryptionMethod($field->getEncryptionMethod());
$skipCheckCrc = true;
} else {
// Traditional PKWARE Decryption
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
$content = $zipCryptoEngine->decrypt($content);
$entry->setEncryptionMethod(ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL);
}
if (!$skipCheckCrc) {
// 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->in, $pos + $compressedSize);
$localCrc = unpack('V', fread($this->in, 4))[1];
if ($localCrc === ZipEntry::DATA_DESCRIPTOR_SIG) {
$localCrc = unpack('V', fread($this->in, 4))[1];
}
} else {
fseek($this->in, $startPos + 14);
// The CRC32 in the Local File Header.
$localCrc = sprintf('%u', fread($this->in, 4)[1]);
$localCrc = PHP_INT_SIZE === 4 ? sprintf('%u', $localCrc) : $localCrc;
}
$crc = PHP_INT_SIZE === 4 ? sprintf('%u', $entry->getCrc()) : $entry->getCrc();
if ($crc != $localCrc) {
throw new Crc32Exception($entry->getName(), $crc, $localCrc);
}
}
}
switch ($method) {
case ZipFileInterface::METHOD_STORED:
break;
case ZipFileInterface::METHOD_DEFLATED:
$content = @gzinflate($content);
break;
case ZipFileInterface::METHOD_BZIP2:
if (!extension_loaded('bz2')) {
throw new ZipException('Extension bzip2 not install');
}
/** @noinspection PhpComposerExtensionStubsInspection */
$content = bzdecompress($content);
if (is_int($content)) { // decompress error
$content = false;
}
break;
default:
throw new ZipUnsupportMethodException($entry->getName() .
" (compression method " . $method . " is not supported)");
}
if ($content === false) {
if ($isEncrypted) {
throw new ZipAuthenticationException(sprintf(
'Invalid password for zip entry "%s"',
$entry->getName()
));
}
throw new ZipException(sprintf(
'Failed to get the contents of the zip entry "%s"',
$entry->getName()
));
}
if (!$skipCheckCrc) {
$localCrc = crc32($content);
$localCrc = PHP_INT_SIZE === 4 ? sprintf('%u', $localCrc) : $localCrc;
$crc = PHP_INT_SIZE === 4 ? sprintf('%u', $entry->getCrc()) : $entry->getCrc();
if ($crc != $localCrc) {
if ($isEncrypted) {
throw new ZipAuthenticationException(sprintf(
'Invalid password for zip entry "%s"',
$entry->getName()
));
}
throw new Crc32Exception($entry->getName(), $crc, $localCrc);
}
}
return $content;
}
/**
* @return resource
*/
public function getStream()
{
return $this->in;
}
/**
* Copy the input stream of the LOC entry zip and the data into
* the output stream and zip the alignment if necessary.
*
* @param ZipEntry $entry
* @param ZipOutputStreamInterface $out
* @throws ZipException
*/
public function copyEntry(ZipEntry $entry, ZipOutputStreamInterface $out)
{
$pos = $entry->getOffset();
assert(ZipEntry::UNKNOWN !== $pos);
$pos = PHP_INT_SIZE === 4 ? sprintf('%u', $pos) : $pos;
$pos = $this->mapper->map($pos);
$nameLength = strlen($entry->getName());
fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, SEEK_SET);
$sourceExtraLength = $destExtraLength = unpack('v', fread($this->in, 2))[1];
if ($sourceExtraLength > 0) {
// read Local File Header extra fields
fseek($this->in, $pos + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength, SEEK_SET);
$extra = '';
$offset = 0;
while ($offset < $sourceExtraLength) {
$read = min(8192 /* chunk size */, $sourceExtraLength - $offset);
$extra .= fread($this->in, $read);
$offset += $read;
}
$extraFieldsCollection = ExtraFieldsFactory::createExtraFieldCollections($extra, $entry);
if (isset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]) && $this->zipModel->isZipAlign()) {
unset($extraFieldsCollection[ApkAlignmentExtraField::getHeaderId()]);
$destExtraLength = strlen(ExtraFieldsFactory::createSerializedData($extraFieldsCollection));
}
} else {
$extraFieldsCollection = new ExtraFieldsCollection();
}
$dataAlignmentMultiple = $this->zipModel->getZipAlign();
$copyInToOutLength = $entry->getCompressedSize();
fseek($this->in, $pos, SEEK_SET);
if (
$this->zipModel->isZipAlign() &&
!$entry->isEncrypted() &&
$entry->getMethod() === ZipFileInterface::METHOD_STORED
) {
if (StringUtil::endsWith($entry->getName(), '.so')) {
$dataAlignmentMultiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
}
$dataMinStartOffset =
ftell($out->getStream()) +
ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
$destExtraLength +
$nameLength +
ApkAlignmentExtraField::ALIGNMENT_ZIP_EXTRA_MIN_SIZE_BYTES;
$padding =
($dataAlignmentMultiple - ($dataMinStartOffset % $dataAlignmentMultiple))
% $dataAlignmentMultiple;
$alignExtra = new ApkAlignmentExtraField();
$alignExtra->setMultiple($dataAlignmentMultiple);
$alignExtra->setPadding($padding);
$extraFieldsCollection->add($alignExtra);
$extra = ExtraFieldsFactory::createSerializedData($extraFieldsCollection);
// copy Local File Header without extra field length
// from input stream to output stream
stream_copy_to_stream($this->in, $out->getStream(), ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2);
// write new extra field length (2 bytes) to output stream
fwrite($out->getStream(), pack('v', strlen($extra)));
// skip 2 bytes to input stream
fseek($this->in, 2, SEEK_CUR);
// copy name from input stream to output stream
stream_copy_to_stream($this->in, $out->getStream(), $nameLength);
// write extra field to output stream
fwrite($out->getStream(), $extra);
// skip source extraLength from input stream
fseek($this->in, $sourceExtraLength, SEEK_CUR);
} else {
$copyInToOutLength += ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $sourceExtraLength + $nameLength;
}
if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
// crc-32 4 bytes
// compressed size 4 bytes
// uncompressed size 4 bytes
$copyInToOutLength += 12;
if ($entry->isZip64ExtensionsRequired()) {
// compressed size +4 bytes
// uncompressed size +4 bytes
$copyInToOutLength += 8;
}
}
// copy loc, data, data descriptor from input to output stream
stream_copy_to_stream($this->in, $out->getStream(), $copyInToOutLength);
}
/**
* @param ZipEntry $entry
* @param ZipOutputStreamInterface $out
*/
public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out)
{
$offset = $entry->getOffset();
$offset = PHP_INT_SIZE === 4 ? sprintf('%u', $offset) : $offset;
$offset = $this->mapper->map($offset);
$nameLength = strlen($entry->getName());
fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2, SEEK_SET);
$extraLength = unpack('v', fread($this->in, 2))[1];
fseek($this->in, $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength, SEEK_SET);
// copy raw data from input stream to output stream
stream_copy_to_stream($this->in, $out->getStream(), $entry->getCompressedSize());
}
public function __destruct()
{
$this->close();
}
public function close()
{
if ($this->in !== null) {
fclose($this->in);
$this->in = null;
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace PhpZip\Stream;
use PhpZip\Exception\ZipException;
use PhpZip\Model\ZipEntry;
use PhpZip\Model\ZipModel;
/**
* Read zip file
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipInputStreamInterface
{
/**
* @return ZipModel
*/
public function readZip();
/**
* @return ZipEntry
*/
public function readEntry();
/**
* @param ZipEntry $entry
* @return string
* @throws ZipException
*/
public function readEntryContent(ZipEntry $entry);
/**
* @return resource
*/
public function getStream();
/**
* Copy the input stream of the LOC entry zip and the data into
* the output stream and zip the alignment if necessary.
*
* @param ZipEntry $entry
* @param ZipOutputStreamInterface $out
*/
public function copyEntry(ZipEntry $entry, ZipOutputStreamInterface $out);
/**
* @param ZipEntry $entry
* @param ZipOutputStreamInterface $out
*/
public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out);
public function close();
}

View File

@@ -0,0 +1,528 @@
<?php
namespace PhpZip\Stream;
use PhpZip\Crypto\TraditionalPkwareEncryptionEngine;
use PhpZip\Crypto\WinZipAesEngine;
use PhpZip\Exception\InvalidArgumentException;
use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipException;
use PhpZip\Extra\ExtraFieldsFactory;
use PhpZip\Extra\Fields\ApkAlignmentExtraField;
use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
use PhpZip\Extra\Fields\Zip64ExtraField;
use PhpZip\Model\EndOfCentralDirectory;
use PhpZip\Model\Entry\OutputOffsetEntry;
use PhpZip\Model\Entry\ZipChangesEntry;
use PhpZip\Model\Entry\ZipSourceEntry;
use PhpZip\Model\ZipEntry;
use PhpZip\Model\ZipModel;
use PhpZip\Util\PackUtil;
use PhpZip\Util\StringUtil;
use PhpZip\ZipFileInterface;
/**
* Write zip file
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
class ZipOutputStream implements ZipOutputStreamInterface
{
/**
* @var resource
*/
protected $out;
/**
* @var ZipModel
*/
protected $zipModel;
/**
* ZipOutputStream constructor.
* @param resource $out
* @param ZipModel $zipModel
*/
public function __construct($out, ZipModel $zipModel)
{
if (!is_resource($out)) {
throw new InvalidArgumentException('$out must be resource');
}
$this->out = $out;
$this->zipModel = $zipModel;
}
/**
* @throws ZipException
*/
public function writeZip()
{
$entries = $this->zipModel->getEntries();
$outPosEntries = [];
foreach ($entries as $entry) {
$outPosEntries[] = new OutputOffsetEntry(ftell($this->out), $entry);
$this->writeEntry($entry);
}
$centralDirectoryOffset = ftell($this->out);
foreach ($outPosEntries as $outputEntry) {
$this->writeCentralDirectoryHeader($outputEntry);
}
$this->writeEndOfCentralDirectoryRecord($centralDirectoryOffset);
}
/**
* @param ZipEntry $entry
* @throws ZipException
*/
public function writeEntry(ZipEntry $entry)
{
if ($entry instanceof ZipSourceEntry) {
$entry->getInputStream()->copyEntry($entry, $this);
return;
}
$entryContent = $this->entryCommitChangesAndReturnContent($entry);
$offset = ftell($this->out);
$compressedSize = $entry->getCompressedSize();
$extra = $entry->getExtra();
$nameLength = strlen($entry->getName());
$extraLength = strlen($extra);
// zip align
if (
$this->zipModel->isZipAlign() &&
!$entry->isEncrypted() &&
$entry->getMethod() === ZipFileInterface::METHOD_STORED
) {
$dataAlignmentMultiple = $this->zipModel->getZipAlign();
if (StringUtil::endsWith($entry->getName(), '.so')) {
$dataAlignmentMultiple = ApkAlignmentExtraField::ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
}
$dataMinStartOffset =
$offset +
ZipEntry::LOCAL_FILE_HEADER_MIN_LEN +
$extraLength +
$nameLength +
ApkAlignmentExtraField::ALIGNMENT_ZIP_EXTRA_MIN_SIZE_BYTES;
$padding =
($dataAlignmentMultiple - ($dataMinStartOffset % $dataAlignmentMultiple))
% $dataAlignmentMultiple;
$alignExtra = new ApkAlignmentExtraField();
$alignExtra->setMultiple($dataAlignmentMultiple);
$alignExtra->setPadding($padding);
$extraFieldsCollection = clone $entry->getExtraFieldsCollection();
$extraFieldsCollection->add($alignExtra);
$extra = ExtraFieldsFactory::createSerializedData($extraFieldsCollection);
$extraLength = strlen($extra);
}
$size = $nameLength + $extraLength;
if ($size > 0xffff) {
throw new ZipException(
$entry->getName() . " (the total size of " . $size .
" bytes for the name, extra fields and comment " .
"exceeds the maximum size of " . 0xffff . " bytes)"
);
}
$dd = $entry->isDataDescriptorRequired();
fwrite(
$this->out,
pack(
'VvvvVVVVvv',
// local file header signature 4 bytes (0x04034b50)
ZipEntry::LOCAL_FILE_HEADER_SIG,
// version needed to extract 2 bytes
$entry->getVersionNeededToExtract(),
// general purpose bit flag 2 bytes
$entry->getGeneralPurposeBitFlags(),
// compression method 2 bytes
$entry->getMethod(),
// last mod file time 2 bytes
// last mod file date 2 bytes
$entry->getDosTime(),
// crc-32 4 bytes
$dd ? 0 : $entry->getCrc(),
// compressed size 4 bytes
$dd ? 0 : $entry->getCompressedSize(),
// uncompressed size 4 bytes
$dd ? 0 : $entry->getSize(),
// file name length 2 bytes
$nameLength,
// extra field length 2 bytes
$extraLength
)
);
if ($nameLength > 0) {
fwrite($this->out, $entry->getName());
}
if ($extraLength > 0) {
fwrite($this->out, $extra);
}
if ($entry instanceof ZipChangesEntry && !$entry->isChangedContent()) {
$entry->getSourceEntry()->getInputStream()->copyEntryData($entry->getSourceEntry(), $this);
} elseif ($entryContent !== null) {
fwrite($this->out, $entryContent);
}
assert(ZipEntry::UNKNOWN !== $entry->getCrc());
assert(ZipEntry::UNKNOWN !== $entry->getSize());
if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) {
// data descriptor signature 4 bytes (0x08074b50)
// crc-32 4 bytes
fwrite($this->out, pack('VV', ZipEntry::DATA_DESCRIPTOR_SIG, $entry->getCrc()));
// compressed size 4 or 8 bytes
// uncompressed size 4 or 8 bytes
if ($entry->isZip64ExtensionsRequired()) {
fwrite($this->out, PackUtil::packLongLE($compressedSize));
fwrite($this->out, PackUtil::packLongLE($entry->getSize()));
} else {
fwrite($this->out, pack('VV', $entry->getCompressedSize(), $entry->getSize()));
}
} elseif ($compressedSize != $entry->getCompressedSize()) {
throw new ZipException(
$entry->getName() . " (expected compressed entry size of "
. $entry->getCompressedSize() . " bytes, " .
"but is actually " . $compressedSize . " bytes)"
);
}
}
/**
* @param ZipEntry $entry
* @return null|string
* @throws ZipException
*/
protected function entryCommitChangesAndReturnContent(ZipEntry $entry)
{
if ($entry->getPlatform() === ZipEntry::UNKNOWN) {
$entry->setPlatform(ZipEntry::PLATFORM_UNIX);
}
if ($entry->getTime() === ZipEntry::UNKNOWN) {
$entry->setTime(time());
}
$method = $entry->getMethod();
$encrypted = $entry->isEncrypted();
// See appendix D of PKWARE's ZIP File Format Specification.
$utf8 = true;
if ($encrypted && $entry->getPassword() === null) {
throw new ZipException("Can not password from entry " . $entry->getName());
}
// Compose General Purpose Bit Flag.
$general = ($encrypted ? ZipEntry::GPBF_ENCRYPTED : 0)
| ($entry->isDataDescriptorRequired() ? ZipEntry::GPBF_DATA_DESCRIPTOR : 0)
| ($utf8 ? ZipEntry::GPBF_UTF8 : 0);
$entryContent = null;
$extraFieldsCollection = $entry->getExtraFieldsCollection();
if (!($entry instanceof ZipChangesEntry && !$entry->isChangedContent())) {
$entryContent = $entry->getEntryContent();
if ($entryContent !== null) {
$entry->setSize(strlen($entryContent));
$entry->setCrc(crc32($entryContent));
if ($encrypted && $method === ZipEntry::METHOD_WINZIP_AES) {
/**
* @var WinZipAesEntryExtraField $field
*/
$field = $extraFieldsCollection->get(WinZipAesEntryExtraField::getHeaderId());
if ($field !== null) {
$method = $field->getMethod();
}
}
switch ($method) {
case ZipFileInterface::METHOD_STORED:
break;
case ZipFileInterface::METHOD_DEFLATED:
$entryContent = gzdeflate($entryContent, $entry->getCompressionLevel());
break;
case ZipFileInterface::METHOD_BZIP2:
$compressionLevel = $entry->getCompressionLevel() === ZipFileInterface::LEVEL_DEFAULT_COMPRESSION ?
ZipEntry::LEVEL_DEFAULT_BZIP2_COMPRESSION :
$entry->getCompressionLevel();
/** @noinspection PhpComposerExtensionStubsInspection */
$entryContent = bzcompress($entryContent, $compressionLevel);
if (is_int($entryContent)) {
throw new ZipException('Error bzip2 compress. Error code: ' . $entryContent);
}
break;
case ZipEntry::UNKNOWN:
$entryContent = $this->determineBestCompressionMethod($entry, $entryContent);
$method = $entry->getMethod();
break;
default:
throw new ZipException($entry->getName() . " (unsupported compression method " . $method . ")");
}
if ($method === ZipFileInterface::METHOD_DEFLATED) {
$bit1 = false;
$bit2 = false;
switch ($entry->getCompressionLevel()) {
case ZipFileInterface::LEVEL_BEST_COMPRESSION:
$bit1 = true;
break;
case ZipFileInterface::LEVEL_FAST:
$bit2 = true;
break;
case ZipFileInterface::LEVEL_SUPER_FAST:
$bit1 = true;
$bit2 = true;
break;
}
$general |= ($bit1 ? ZipEntry::GPBF_COMPRESSION_FLAG1 : 0);
$general |= ($bit2 ? ZipEntry::GPBF_COMPRESSION_FLAG2 : 0);
}
if ($encrypted) {
if (in_array($entry->getEncryptionMethod(), [
ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128,
ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192,
ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256,
], true)) {
$keyStrength = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod($entry->getEncryptionMethod()); // size bits
$field = ExtraFieldsFactory::createWinZipAesEntryExtra();
$field->setKeyStrength($keyStrength);
$field->setMethod($method);
$size = $entry->getSize();
if ($size >= 20 && $method !== ZipFileInterface::METHOD_BZIP2) {
$field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1);
} else {
$field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2);
$entry->setCrc(0);
}
$extraFieldsCollection->add($field);
$entry->setMethod(ZipEntry::METHOD_WINZIP_AES);
$winZipAesEngine = new WinZipAesEngine($entry);
$entryContent = $winZipAesEngine->encrypt($entryContent);
} elseif ($entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL) {
$zipCryptoEngine = new TraditionalPkwareEncryptionEngine($entry);
$entryContent = $zipCryptoEngine->encrypt($entryContent);
}
}
$compressedSize = strlen($entryContent);
$entry->setCompressedSize($compressedSize);
}
}
// Commit changes.
$entry->setGeneralPurposeBitFlags($general);
if ($entry->isZip64ExtensionsRequired()) {
$extraFieldsCollection->add(ExtraFieldsFactory::createZip64Extra($entry));
} elseif ($extraFieldsCollection->has(Zip64ExtraField::getHeaderId())) {
$extraFieldsCollection->remove(Zip64ExtraField::getHeaderId());
}
return $entryContent;
}
/**
* @param ZipEntry $entry
* @param string $content
* @return string
* @throws ZipException
*/
protected function determineBestCompressionMethod(ZipEntry $entry, $content)
{
if ($content !== null) {
$entryContent = gzdeflate($content, $entry->getCompressionLevel());
if (strlen($entryContent) < strlen($content)) {
$entry->setMethod(ZipFileInterface::METHOD_DEFLATED);
return $entryContent;
}
$entry->setMethod(ZipFileInterface::METHOD_STORED);
}
return $content;
}
/**
* Writes a Central File Header record.
*
* @param OutputOffsetEntry $outEntry
*/
protected function writeCentralDirectoryHeader(OutputOffsetEntry $outEntry)
{
$entry = $outEntry->getEntry();
$compressedSize = $entry->getCompressedSize();
$size = $entry->getSize();
// This test MUST NOT include the CRC-32 because VV_AE_2 sets it to
// UNKNOWN!
if (($compressedSize | $size) === ZipEntry::UNKNOWN) {
throw new RuntimeException("invalid entry");
}
$extra = $entry->getExtra();
$extraSize = strlen($extra);
$commentLength = strlen($entry->getComment());
fwrite(
$this->out,
pack(
'VvvvvVVVVvvvvvVV',
// central file header signature 4 bytes (0x02014b50)
self::CENTRAL_FILE_HEADER_SIG,
// version made by 2 bytes
($entry->getPlatform() << 8) | 63,
// version needed to extract 2 bytes
$entry->getVersionNeededToExtract(),
// general purpose bit flag 2 bytes
$entry->getGeneralPurposeBitFlags(),
// compression method 2 bytes
$entry->getMethod(),
// last mod file datetime 4 bytes
$entry->getDosTime(),
// crc-32 4 bytes
$entry->getCrc(),
// compressed size 4 bytes
$entry->getCompressedSize(),
// uncompressed size 4 bytes
$entry->getSize(),
// file name length 2 bytes
strlen($entry->getName()),
// extra field length 2 bytes
$extraSize,
// file comment length 2 bytes
$commentLength,
// disk number start 2 bytes
0,
// internal file attributes 2 bytes
0,
// external file attributes 4 bytes
$entry->getExternalAttributes(),
// relative offset of local header 4 bytes
$outEntry->getOffset()
)
);
// file name (variable size)
fwrite($this->out, $entry->getName());
if ($extraSize > 0) {
// extra field (variable size)
fwrite($this->out, $extra);
}
if ($commentLength > 0) {
// file comment (variable size)
fwrite($this->out, $entry->getComment());
}
}
protected function writeEndOfCentralDirectoryRecord($centralDirectoryOffset)
{
$centralDirectoryEntriesCount = count($this->zipModel);
$position = ftell($this->out);
$centralDirectorySize = $position - $centralDirectoryOffset;
$centralDirectoryEntriesZip64 = $centralDirectoryEntriesCount > 0xffff;
$centralDirectorySizeZip64 = $centralDirectorySize > 0xffffffff;
$centralDirectoryOffsetZip64 = $centralDirectoryOffset > 0xffffffff;
$centralDirectoryEntries16 = $centralDirectoryEntriesZip64 ? 0xffff : (int)$centralDirectoryEntriesCount;
$centralDirectorySize32 = $centralDirectorySizeZip64 ? 0xffffffff : $centralDirectorySize;
$centralDirectoryOffset32 = $centralDirectoryOffsetZip64 ? 0xffffffff : $centralDirectoryOffset;
$zip64 // ZIP64 extensions?
= $centralDirectoryEntriesZip64
|| $centralDirectorySizeZip64
|| $centralDirectoryOffsetZip64;
if ($zip64) {
// [zip64 end of central directory record]
// relative offset of the zip64 end of central directory record
$zip64EndOfCentralDirectoryOffset = $position;
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
fwrite($this->out, pack('V', EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIG));
// size of zip64 end of central
// directory record 8 bytes
fwrite($this->out, PackUtil::packLongLE(EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_MIN_LEN - 12));
// version made by 2 bytes
// version needed to extract 2 bytes
// due to potential use of BZIP2 compression
// number of this disk 4 bytes
// number of the disk with the
// start of the central directory 4 bytes
fwrite($this->out, pack('vvVV', 63, 46, 0, 0));
// total number of entries in the
// central directory on this disk 8 bytes
fwrite($this->out, PackUtil::packLongLE($centralDirectoryEntriesCount));
// total number of entries in the
// central directory 8 bytes
fwrite($this->out, PackUtil::packLongLE($centralDirectoryEntriesCount));
// size of the central directory 8 bytes
fwrite($this->out, PackUtil::packLongLE($centralDirectorySize));
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
fwrite($this->out, PackUtil::packLongLE($centralDirectoryOffset));
// zip64 extensible data sector (variable size)
// [zip64 end of central directory locator]
// signature 4 bytes (0x07064b50)
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
fwrite($this->out, pack('VV', EndOfCentralDirectory::ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIG, 0));
// relative offset of the zip64
// end of central directory record 8 bytes
fwrite($this->out, PackUtil::packLongLE($zip64EndOfCentralDirectoryOffset));
// total number of disks 4 bytes
fwrite($this->out, pack('V', 1));
}
$comment = $this->zipModel->getArchiveComment();
$commentLength = strlen($comment);
fwrite(
$this->out,
pack(
'VvvvvVVv',
// end of central dir signature 4 bytes (0x06054b50)
EndOfCentralDirectory::END_OF_CENTRAL_DIRECTORY_RECORD_SIG,
// 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
$centralDirectoryEntries16,
// total number of entries in
// the central directory 2 bytes
$centralDirectoryEntries16,
// size of the central directory 4 bytes
$centralDirectorySize32,
// offset of start of central
// directory with respect to
// the starting disk number 4 bytes
$centralDirectoryOffset32,
// .ZIP file comment length 2 bytes
$commentLength
)
);
if ($commentLength > 0) {
// .ZIP file comment (variable size)
fwrite($this->out, $comment);
}
}
/**
* @return resource
*/
public function getStream()
{
return $this->out;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace PhpZip\Stream;
use PhpZip\Model\ZipEntry;
/**
* Write zip file
*
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipOutputStreamInterface
{
/** Central File Header signature. */
const CENTRAL_FILE_HEADER_SIG = 0x02014B50;
public function writeZip();
/**
* @param ZipEntry $entry
*/
public function writeEntry(ZipEntry $entry);
/**
* @return resource
*/
public function getStream();
}

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace PhpZip\Util; namespace PhpZip\Util;
use PhpZip\Exception\RuntimeException; use PhpZip\Exception\RuntimeException;
use PhpZip\Exception\ZipException;
/** /**
* Crypto Utils * Crypto Utils
@@ -15,19 +15,25 @@ class CryptoUtil
* *
* @param int $length * @param int $length
* @return string * @return string
* @throws RuntimeException
*/ */
public static final function randomBytes($length) final public static function randomBytes($length)
{ {
$length = (int)$length; $length = (int)$length;
if (function_exists('random_bytes')) { if (function_exists('random_bytes')) {
return random_bytes($length); try {
return random_bytes($length);
} catch (\Exception $e) {
throw new \RuntimeException("Could not generate a random string.");
}
} elseif (function_exists('openssl_random_pseudo_bytes')) { } elseif (function_exists('openssl_random_pseudo_bytes')) {
/** @noinspection PhpComposerExtensionStubsInspection */
return openssl_random_pseudo_bytes($length); return openssl_random_pseudo_bytes($length);
} elseif (function_exists('mcrypt_create_iv')) { } elseif (function_exists('mcrypt_create_iv')) {
/** @noinspection PhpDeprecationInspection */
/** @noinspection PhpComposerExtensionStubsInspection */
return mcrypt_create_iv($length); return mcrypt_create_iv($length);
} else { } else {
throw new RuntimeException('Extension openssl or mcrypt not loaded'); throw new RuntimeException('Extension openssl or mcrypt not loaded');
} }
} }
} }

View File

@@ -1,4 +1,5 @@
<?php <?php
namespace PhpZip\Util; namespace PhpZip\Util;
use PhpZip\Exception\ZipException; use PhpZip\Exception\ZipException;
@@ -39,9 +40,9 @@ class DateTimeConverter
return mktime( return mktime(
($dosTime >> 11) & 0x1f, // hour ($dosTime >> 11) & 0x1f, // hour
($dosTime >> 5) & 0x3f, // minute ($dosTime >> 5) & 0x3f, // minute
2 * ($dosTime & 0x1f), // second 2 * ($dosTime & 0x1f), // second
($dosTime >> 21) & 0x0f, // month ($dosTime >> 21) & 0x0f, // month
($dosTime >> 16) & 0x1f, // day ($dosTime >> 16) & 0x1f, // day
1980 + (($dosTime >> 25) & 0x7f) // year 1980 + (($dosTime >> 25) & 0x7f) // year
); );
@@ -74,4 +75,4 @@ class DateTimeConverter
$date['mday'] << 16 | $date['hours'] << 11 | $date['mday'] << 16 | $date['hours'] << 11 |
$date['minutes'] << 5 | $date['seconds'] >> 1); $date['minutes'] << 5 | $date['seconds'] >> 1);
} }
} }

View File

@@ -1,4 +1,5 @@
<?php <?php
namespace PhpZip\Util; namespace PhpZip\Util;
use PhpZip\Util\Iterator\IgnoreFilesFilterIterator; use PhpZip\Util\Iterator\IgnoreFilesFilterIterator;
@@ -60,7 +61,7 @@ class FilesUtil
$inCurrent = 0; $inCurrent = 0;
$chars = str_split($globPattern); $chars = str_split($globPattern);
$regexPattern = ''; $regexPattern = '';
foreach ($chars AS $currentChar) { foreach ($chars as $currentChar) {
switch ($currentChar) { switch ($currentChar) {
case '*': case '*':
$regexPattern .= ($escaping ? "\\*" : '.*'); $regexPattern .= ($escaping ? "\\*" : '.*');
@@ -103,19 +104,21 @@ class FilesUtil
if ($inCurrent > 0 && !$escaping) { if ($inCurrent > 0 && !$escaping) {
$regexPattern .= ')'; $regexPattern .= ')';
$inCurrent--; $inCurrent--;
} else if ($escaping) } elseif ($escaping) {
$regexPattern = "\\}"; $regexPattern = "\\}";
else } else {
$regexPattern = "}"; $regexPattern = "}";
}
$escaping = false; $escaping = false;
break; break;
case ',': case ',':
if ($inCurrent > 0 && !$escaping) { if ($inCurrent > 0 && !$escaping) {
$regexPattern .= '|'; $regexPattern .= '|';
} else if ($escaping) } elseif ($escaping) {
$regexPattern .= "\\,"; $regexPattern .= "\\,";
else } else {
$regexPattern = ","; $regexPattern = ",";
}
break; break;
default: default:
$escaping = false; $escaping = false;
@@ -211,12 +214,15 @@ class FilesUtil
*/ */
public static function humanSize($size, $unit = null) public static function humanSize($size, $unit = null)
{ {
if (($unit === null && $size >= 1 << 30) || $unit === "GB") if (($unit === null && $size >= 1 << 30) || $unit === "GB") {
return number_format($size / (1 << 30), 2) . "GB"; return number_format($size / (1 << 30), 2) . "GB";
if (($unit === null && $size >= 1 << 20) || $unit === "MB") }
if (($unit === null && $size >= 1 << 20) || $unit === "MB") {
return number_format($size / (1 << 20), 2) . "MB"; return number_format($size / (1 << 20), 2) . "MB";
if (($unit === null && $size >= 1 << 10) || $unit === "KB") }
if (($unit === null && $size >= 1 << 10) || $unit === "KB") {
return number_format($size / (1 << 10), 2) . "KB"; return number_format($size / (1 << 10), 2) . "KB";
}
return number_format($size) . " bytes"; return number_format($size) . " bytes";
} }
} }

View File

@@ -1,4 +1,5 @@
<?php <?php
namespace PhpZip\Util\Iterator; namespace PhpZip\Util\Iterator;
use PhpZip\Util\StringUtil; use PhpZip\Util\StringUtil;
@@ -44,7 +45,7 @@ class IgnoreFilesFilterIterator extends \FilterIterator
foreach ($this->ignoreFiles as $ignoreFile) { foreach ($this->ignoreFiles as $ignoreFile) {
// handler dir and sub dir // handler dir and sub dir
if ($fileInfo->isDir() if ($fileInfo->isDir()
&& $ignoreFile[strlen($ignoreFile) - 1] === '/' && StringUtil::endsWith($ignoreFile, '/')
&& StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1)) && StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1))
) { ) {
return false; return false;
@@ -57,4 +58,4 @@ class IgnoreFilesFilterIterator extends \FilterIterator
} }
return true; return true;
} }
} }

View File

@@ -1,4 +1,5 @@
<?php <?php
namespace PhpZip\Util\Iterator; namespace PhpZip\Util\Iterator;
use PhpZip\Util\StringUtil; use PhpZip\Util\StringUtil;
@@ -11,7 +12,6 @@ use PhpZip\Util\StringUtil;
*/ */
class IgnoreFilesRecursiveFilterIterator extends \RecursiveFilterIterator class IgnoreFilesRecursiveFilterIterator extends \RecursiveFilterIterator
{ {
/** /**
* Ignore list files * Ignore list files
* *
@@ -64,6 +64,7 @@ class IgnoreFilesRecursiveFilterIterator extends \RecursiveFilterIterator
*/ */
public function getChildren() public function getChildren()
{ {
/** @noinspection PhpUndefinedMethodInspection */
return new self($this->getInnerIterator()->getChildren(), $this->ignoreFiles); return new self($this->getInnerIterator()->getChildren(), $this->ignoreFiles);
} }
} }

View File

@@ -1,7 +1,6 @@
<?php <?php
namespace PhpZip\Util;
use PhpZip\Exception\ZipException; namespace PhpZip\Util;
/** /**
* Pack util * Pack util
@@ -18,7 +17,7 @@ class PackUtil
*/ */
public static function packLongLE($longValue) public static function packLongLE($longValue)
{ {
if (version_compare(PHP_VERSION, '5.6.3') >= 0) { if (PHP_INT_SIZE === 8 && PHP_VERSION_ID >= 506030) {
return pack("P", $longValue); return pack("P", $longValue);
} }
@@ -34,15 +33,30 @@ class PackUtil
/** /**
* @param string|int $value * @param string|int $value
* @return int * @return int
* @throws ZipException
*/ */
public static function unpackLongLE($value) public static function unpackLongLE($value)
{ {
if (version_compare(PHP_VERSION, '5.6.3') >= 0) { if (PHP_INT_SIZE === 8 && PHP_VERSION_ID >= 506030) {
return current(unpack('P', $value)); return unpack('P', $value)[1];
} }
$unpack = unpack('Va/Vb', $value); $unpack = unpack('Va/Vb', $value);
return $unpack['a'] + ($unpack['b'] << 32); return $unpack['a'] + ($unpack['b'] << 32);
} }
} /**
* Cast to signed int 32-bit
*
* @param int $int
* @return int
*/
public static function toSignedInt32($int)
{
if (PHP_INT_SIZE === 8) {
$int = $int & 0xffffffff;
if ($int & 0x80000000) {
return $int - 0x100000000;
}
}
return $int;
}
}

View File

@@ -1,4 +1,5 @@
<?php <?php
namespace PhpZip\Util; namespace PhpZip\Util;
/** /**
@@ -25,6 +26,31 @@ class StringUtil
public static function endsWith($haystack, $needle) public static function endsWith($haystack, $needle)
{ {
return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0
&& strpos($haystack, $needle, $temp) !== false); && strpos($haystack, $needle, $temp) !== false);
} }
}
/**
* @param string $str
* @return string
*/
public static function cp866toUtf8($str)
{
if (function_exists('iconv')) {
/** @noinspection PhpComposerExtensionStubsInspection */
return iconv('CP866', 'UTF-8//IGNORE', $str);
} elseif (function_exists('mb_convert_encoding')) {
/** @noinspection PhpComposerExtensionStubsInspection */
return mb_convert_encoding($str, 'UTF-8', 'CP866');
} elseif (class_exists('UConverter')) {
/** @noinspection PhpComposerExtensionStubsInspection */
$converter = new \UConverter('UTF-8', 'CP866');
return $converter->convert($str, false);
} else {
static $cp866Utf8Pairs;
if (empty($cp866Utf8Pairs)) {
$cp866Utf8Pairs = require __DIR__ . '/encodings/cp866-utf8.php';
}
return strtr($str, $cp866Utf8Pairs);
}
}
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,606 @@
<?php
namespace PhpZip;
use PhpZip\Exception\ZipEntryNotFoundException;
use PhpZip\Exception\ZipException;
use PhpZip\Model\ZipEntry;
use PhpZip\Model\ZipEntryMatcher;
use PhpZip\Model\ZipInfo;
use Psr\Http\Message\ResponseInterface;
/**
* Create, open .ZIP files, modify, get info and extract files.
*
* 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.
* Support ZipAlign functional.
*
* @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
* @author Ne-Lexa alexey@nelexa.ru
* @license MIT
*/
interface ZipFileInterface extends \Countable, \ArrayAccess, \Iterator
{
/**
* Method for Stored (uncompressed) entries.
* @see ZipEntry::setMethod()
*/
const METHOD_STORED = 0;
/**
* Method for Deflated compressed entries.
* @see ZipEntry::setMethod()
*/
const METHOD_DEFLATED = 8;
/**
* Method for BZIP2 compressed entries.
* Require php extension bz2.
* @see ZipEntry::setMethod()
*/
const METHOD_BZIP2 = 12;
/**
* Default compression level.
*/
const LEVEL_DEFAULT_COMPRESSION = -1;
/**
* Compression level for fastest compression.
*/
const LEVEL_FAST = 2;
/**
* Compression level for fastest compression.
*/
const LEVEL_BEST_SPEED = 1;
const LEVEL_SUPER_FAST = self::LEVEL_BEST_SPEED;
/**
* Compression level for best compression.
*/
const LEVEL_BEST_COMPRESSION = 9;
/**
* No specified method for set encryption method to Traditional PKWARE encryption.
*/
const ENCRYPTION_METHOD_TRADITIONAL = 0;
/**
* No specified method for set encryption method to WinZip AES encryption.
* Default value 256 bit
*/
const ENCRYPTION_METHOD_WINZIP_AES = self::ENCRYPTION_METHOD_WINZIP_AES_256;
/**
* No specified method for set encryption method to WinZip AES encryption 128 bit.
*/
const ENCRYPTION_METHOD_WINZIP_AES_128 = 2;
/**
* No specified method for set encryption method to WinZip AES encryption 194 bit.
*/
const ENCRYPTION_METHOD_WINZIP_AES_192 = 3;
/**
* No specified method for set encryption method to WinZip AES encryption 256 bit.
*/
const ENCRYPTION_METHOD_WINZIP_AES_256 = 1;
/**
* Open zip archive from file
*
* @param string $filename
* @return ZipFileInterface
* @throws ZipException if can't open file.
*/
public function openFile($filename);
/**
* Open zip archive from raw string data.
*
* @param string $data
* @return ZipFileInterface
* @throws ZipException if can't open temp stream.
*/
public function openFromString($data);
/**
* Open zip archive from stream resource
*
* @param resource $handle
* @return ZipFileInterface
*/
public function openFromStream($handle);
/**
* @return string[] Returns the list files.
*/
public function getListFiles();
/**
* Returns the file comment.
*
* @return string The file comment.
*/
public function getArchiveComment();
/**
* Set archive comment.
*
* @param null|string $comment
* @return ZipFileInterface
*/
public function setArchiveComment($comment = null);
/**
* Checks that the entry in the archive is a directory.
* Returns true if and only if this ZIP entry represents a directory entry
* (i.e. end with '/').
*
* @param string $entryName
* @return bool
* @throws ZipEntryNotFoundException
*/
public function isDirectory($entryName);
/**
* Returns entry comment.
*
* @param string $entryName
* @return string
* @throws ZipEntryNotFoundException
*/
public function getEntryComment($entryName);
/**
* Set entry comment.
*
* @param string $entryName
* @param string|null $comment
* @return ZipFileInterface
* @throws ZipEntryNotFoundException
*/
public function setEntryComment($entryName, $comment = null);
/**
* Returns the entry contents.
*
* @param string $entryName
* @return string
*/
public function getEntryContents($entryName);
/**
* Checks if there is an entry in the archive.
*
* @param string $entryName
* @return bool
*/
public function hasEntry($entryName);
/**
* Get info by entry.
*
* @param string|ZipEntry $entryName
* @return ZipInfo
* @throws ZipEntryNotFoundException
*/
public function getEntryInfo($entryName);
/**
* Get info by all entries.
*
* @return ZipInfo[]
*/
public function getAllInfo();
/**
* @return ZipEntryMatcher
*/
public function matcher();
/**
* 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|string|null $entries The entries to extract. It accepts either
* a single entry name or an array of names.
* @return ZipFileInterface
* @throws ZipException
*/
public function extractTo($destination, $entries = null);
/**
* Add entry from the string.
*
* @param string $localName Zip entry name.
* @param string $contents String contents.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function addFromString($localName, $contents, $compressionMethod = null);
/**
* Add entry from the file.
*
* @param string $filename Destination file.
* @param string|null $localName Zip Entry name.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function addFile($filename, $localName = null, $compressionMethod = null);
/**
* Add entry from the stream.
*
* @param resource $stream Stream resource.
* @param string $localName Zip Entry name.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function addFromStream($stream, $localName, $compressionMethod = null);
/**
* Add an empty directory in the zip archive.
*
* @param string $dirName
* @return ZipFileInterface
*/
public function addEmptyDir($dirName);
/**
* Add directory not recursively to the zip archive.
*
* @param string $inputDir Input directory
* @param string $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
*/
public function addDir($inputDir, $localPath = "/", $compressionMethod = null);
/**
* Add recursive directory to the zip archive.
*
* @param string $inputDir Input directory
* @param string $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function addDirRecursive($inputDir, $localPath = "/", $compressionMethod = null);
/**
* Add directories from directory iterator.
*
* @param \Iterator $iterator Directory iterator.
* @param string $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function addFilesFromIterator(\Iterator $iterator, $localPath = '/', $compressionMethod = null);
/**
* Add files from glob pattern.
*
* @param string $inputDir Input directory
* @param string $globPattern Glob pattern.
* @param string|null $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
*/
public function addFilesFromGlob($inputDir, $globPattern, $localPath = '/', $compressionMethod = null);
/**
* Add files recursively from glob pattern.
*
* @param string $inputDir Input directory
* @param string $globPattern Glob pattern.
* @param string|null $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
*/
public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null);
/**
* Add files from regex pattern.
*
* @param string $inputDir Search files in this directory.
* @param string $regexPattern Regex pattern.
* @param string|null $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @internal param bool $recursive Recursive search.
*/
public function addFilesFromRegex($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null);
/**
* Add files recursively from regex pattern.
*
* @param string $inputDir Search files in this directory.
* @param string $regexPattern Regex pattern.
* @param string|null $localPath Add files to this directory, or the root.
* @param int|null $compressionMethod Compression method.
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
* If null, then auto choosing method.
* @return ZipFileInterface
* @internal param bool $recursive Recursive search.
*/
public function addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null);
/**
* Add array data to archive.
* Keys is local names.
* Values is contents.
*
* @param array $mapData Associative array for added to zip.
*/
public function addAll(array $mapData);
/**
* Rename the entry.
*
* @param string $oldName Old entry name.
* @param string $newName New entry name.
* @return ZipFileInterface
* @throws ZipEntryNotFoundException
*/
public function rename($oldName, $newName);
/**
* Delete entry by name.
*
* @param string $entryName Zip Entry name.
* @return ZipFileInterface
* @throws ZipEntryNotFoundException If entry not found.
*/
public function deleteFromName($entryName);
/**
* Delete entries by glob pattern.
*
* @param string $globPattern Glob pattern
* @return ZipFileInterface
* @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
*/
public function deleteFromGlob($globPattern);
/**
* Delete entries by regex pattern.
*
* @param string $regexPattern Regex pattern
* @return ZipFileInterface
*/
public function deleteFromRegex($regexPattern);
/**
* Delete all entries
* @return ZipFileInterface
*/
public function deleteAll();
/**
* Set compression level for new entries.
*
* @param int $compressionLevel
* @see ZipFileInterface::LEVEL_DEFAULT_COMPRESSION
* @see ZipFileInterface::LEVEL_SUPER_FAST
* @see ZipFileInterface::LEVEL_FAST
* @see ZipFileInterface::LEVEL_BEST_COMPRESSION
* @return ZipFileInterface
*/
public function setCompressionLevel($compressionLevel = self::LEVEL_DEFAULT_COMPRESSION);
/**
* @param string $entryName
* @param int $compressionLevel
* @return ZipFileInterface
* @throws ZipException
* @see ZipFileInterface::LEVEL_DEFAULT_COMPRESSION
* @see ZipFileInterface::LEVEL_SUPER_FAST
* @see ZipFileInterface::LEVEL_FAST
* @see ZipFileInterface::LEVEL_BEST_COMPRESSION
*/
public function setCompressionLevelEntry($entryName, $compressionLevel);
/**
* @param string $entryName
* @param int $compressionMethod
* @return ZipFileInterface
* @throws ZipException
* @see ZipFileInterface::METHOD_STORED
* @see ZipFileInterface::METHOD_DEFLATED
* @see ZipFileInterface::METHOD_BZIP2
*/
public function setCompressionMethodEntry($entryName, $compressionMethod);
/**
* zipalign is optimization to Android application (APK) files.
*
* @param int|null $align
* @return ZipFileInterface
* @link https://developer.android.com/studio/command-line/zipalign.html
*/
public function setZipAlign($align = null);
/**
* Set password to all input encrypted entries.
*
* @param string $password Password
* @return ZipFileInterface
* @deprecated using ZipFileInterface::setReadPassword()
*/
public function withReadPassword($password);
/**
* Set password to all input encrypted entries.
*
* @param string $password Password
* @return ZipFileInterface
*/
public function setReadPassword($password);
/**
* Set password to concrete input entry.
*
* @param string $entryName
* @param string $password Password
* @return ZipFileInterface
*/
public function setReadPasswordEntry($entryName, $password);
/**
* Set password for all entries for update.
*
* @param string $password If password null then encryption clear
* @param int|null $encryptionMethod Encryption method
* @return ZipFileInterface
* @deprecated using ZipFileInterface::setPassword()
*/
public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256);
/**
* Sets a new password for all files in the archive.
*
* @param string $password
* @param int|null $encryptionMethod Encryption method
* @return ZipFileInterface
*/
public function setPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256);
/**
* Sets a new password of an entry defined by its name.
*
* @param string $entryName
* @param string $password
* @param int|null $encryptionMethod
* @return ZipFileInterface
*/
public function setPasswordEntry($entryName, $password, $encryptionMethod = null);
/**
* Remove password for all entries for update.
* @return ZipFileInterface
* @deprecated using ZipFileInterface::disableEncryption()
*/
public function withoutPassword();
/**
* Disable encryption for all entries that are already in the archive.
* @return ZipFileInterface
*/
public function disableEncryption();
/**
* Disable encryption of an entry defined by its name.
* @param string $entryName
* @return ZipFileInterface
*/
public function disableEncryptionEntry($entryName);
/**
* Undo all changes done in the archive
* @return ZipFileInterface
*/
public function unchangeAll();
/**
* Undo change archive comment
* @return ZipFileInterface
*/
public function unchangeArchiveComment();
/**
* Revert all changes done to an entry with the given name.
*
* @param string|ZipEntry $entry Entry name or ZipEntry
* @return ZipFileInterface
*/
public function unchangeEntry($entry);
/**
* Save as file.
*
* @param string $filename Output filename
* @return ZipFileInterface
* @throws ZipException
*/
public function saveAsFile($filename);
/**
* Save as stream.
*
* @param resource $handle Output stream resource
* @return ZipFileInterface
* @throws ZipException
*/
public function saveAsStream($handle);
/**
* Output .ZIP archive as attachment.
* Die after output.
*
* @param string $outputFilename Output filename
* @param string|null $mimeType Mime-Type
* @param bool $attachment Http Header 'Content-Disposition' if true then attachment otherwise inline
*/
public function outputAsAttachment($outputFilename, $mimeType = null, $attachment = true);
/**
* Output .ZIP archive as PSR-7 Response.
*
* @param ResponseInterface $response Instance PSR-7 Response
* @param string $outputFilename Output filename
* @param string|null $mimeType Mime-Type
* @param bool $attachment Http Header 'Content-Disposition' if true then attachment otherwise inline
* @return ResponseInterface
*/
public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null, $attachment = true);
/**
* Returns the zip archive as a string.
* @return string
*/
public function outputAsString();
/**
* Save and reopen zip archive.
* @return ZipFileInterface
* @throws ZipException
*/
public function rewrite();
/**
* Close zip archive and release input stream.
*/
public function close();
}

View File

@@ -0,0 +1,104 @@
<?php
namespace PhpZip;
use PhpZip\Exception\ZipException;
use PhpZip\Util\CryptoUtil;
class Issue24Test extends ZipTestCase
{
/**
* This method is called before the first test of this test class is run.
*/
public static function setUpBeforeClass()
{
stream_wrapper_register("dummyfs", DummyFileSystemStream::class);
}
/**
* @throws ZipException
*/
public function testDummyFS()
{
$fileContents = str_repeat(base64_encode(CryptoUtil::randomBytes(12000)), 100);
// create zip file
$zip = new ZipFile();
$zip->addFromString(
'file.txt',
$fileContents,
ZipFile::METHOD_DEFLATED
);
$zip->saveAsFile($this->outputFilename);
$zip->close();
$this->assertCorrectZipArchive($this->outputFilename);
$stream = fopen('dummyfs://localhost/' . $this->outputFilename, 'rb');
$this->assertNotFalse($stream);
$zip->openFromStream($stream);
$this->assertEquals($zip->getListFiles(), ['file.txt']);
$this->assertEquals($zip['file.txt'], $fileContents);
$zip->close();
}
}
/**
* Try to load using dummy stream
*/
class DummyFileSystemStream
{
/**
* @var resource
*/
private $fp;
public function stream_open($path, $mode, $options, &$opened_path)
{
// echo "DummyFileSystemStream->stream_open($path, $mode, $options)" . PHP_EOL;
$parsedUrl = parse_url($path);
$path = $parsedUrl['path'];
$this->fp = fopen($path, $mode);
return true;
}
public function stream_read($count)
{
// echo "DummyFileSystemStream->stream_read($count)" . PHP_EOL;
$position = ftell($this->fp);
// echo "Loading chunk " . $position . " to " . ($position + $count - 1) . PHP_EOL;
$ret = fread($this->fp, $count);
// echo "String length: " . strlen($ret) . PHP_EOL;
return $ret;
}
public function stream_tell()
{
// echo "DummyFileSystemStream->stream_tell()" . PHP_EOL;
return ftell($this->fp);
}
public function stream_eof()
{
// echo "DummyFileSystemStream->stream_eof()" . PHP_EOL;
$isfeof = feof($this->fp);
return $isfeof;
}
public function stream_seek($offset, $whence)
{
// echo "DummyFileSystemStream->stream_seek($offset, $whence)" . PHP_EOL;
fseek($this->fp, $offset, $whence);
}
public function stream_stat()
{
// echo "DummyFileSystemStream->stream_stat()" . PHP_EOL;
return fstat($this->fp);
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace PhpZip;
use PhpZip\Exception\ZipException;
/**
* Some tests from the official extension of php-zip.
*/
class PhpZipExtResourceTest extends ZipTestCase
{
/**
* Bug #7214 (zip_entry_read() binary safe)
* @see https://github.com/php/php-src/blob/master/ext/zip/tests/bug7214.phpt
* @throws ZipException
*/
public function testBinaryNull()
{
$filename = __DIR__ . '/php-zip-ext-test-resources/binarynull.zip';
$zipFile = new ZipFile();
$zipFile->openFile($filename);
foreach ($zipFile as $name => $contents) {
$info = $zipFile->getEntryInfo($name);
$this->assertEquals(strlen($contents), $info->getSize());
}
$zipFile->close();
$this->assertCorrectZipArchive($filename);
}
/**
* Bug #8009 (cannot add again same entry to an archive)
* @see https://github.com/php/php-src/blob/master/ext/zip/tests/bug8009.phpt
* @throws ZipException
*/
public function testBug8009()
{
$filename = __DIR__ . '/php-zip-ext-test-resources/bug8009.zip';
$zipFile = new ZipFile();
$zipFile->openFile($filename);
$zipFile->addFromString('2.txt', '=)');
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
$this->assertCount(2, $zipFile);
$this->assertTrue(isset($zipFile['1.txt']));
$this->assertTrue(isset($zipFile['2.txt']));
$this->assertEquals($zipFile['2.txt'], $zipFile['1.txt']);
$zipFile->close();
}
/**
* Bug #40228 (extractTo does not create recursive empty path)
* @see https://github.com/php/php-src/blob/master/ext/zip/tests/bug40228.phpt
* @see https://github.com/php/php-src/blob/master/ext/zip/tests/bug40228-mb.phpt
* @dataProvider provideBug40228
* @param string $filename
* @throws ZipException
*/
public function testBug40228($filename)
{
$this->assertTrue(mkdir($this->outputDirname, 0755, true));
$zipFile = new ZipFile();
$zipFile->openFile($filename);
$zipFile->extractTo($this->outputDirname);
$zipFile->close();
$this->assertTrue(is_dir($this->outputDirname . '/test/empty'));
}
public function provideBug40228()
{
return [
[__DIR__ . '/php-zip-ext-test-resources/bug40228.zip'],
];
}
/**
* Bug #49072 (feof never returns true for damaged file in zip)
* @see https://github.com/php/php-src/blob/master/ext/zip/tests/bug49072.phpt
* @expectedException \PhpZip\Exception\Crc32Exception
* @expectedExceptionMessage file1
* @throws ZipException
*/
public function testBug49072()
{
$filename = __DIR__ . '/php-zip-ext-test-resources/bug49072.zip';
$zipFile = new ZipFile();
$zipFile->openFile($filename);
$zipFile->getEntryContents('file1');
}
/**
* Bug #70752 (Depacking with wrong password leaves 0 length files)
* @see https://github.com/php/php-src/blob/master/ext/zip/tests/bug70752.phpt
* @expectedException \PhpZip\Exception\ZipAuthenticationException
* @expectedExceptionMessage nvalid password for zip entry "bug70752.txt"
* @throws ZipException
*/
public function testBug70752()
{
$filename = __DIR__ . '/php-zip-ext-test-resources/bug70752.zip';
$this->assertTrue(mkdir($this->outputDirname, 0755, true));
$zipFile = new ZipFile();
try {
$zipFile->openFile($filename);
$zipFile->setReadPassword('bar');
$zipFile->extractTo($this->outputDirname);
$this->markTestIncomplete('failed test');
} catch (ZipException $exception) {
$this->assertFalse(file_exists($this->outputDirname . '/bug70752.txt'));
$zipFile->close();
throw $exception;
}
}
/**
* Bug #12414 ( extracting files from damaged archives)
* @see https://github.com/php/php-src/blob/master/ext/zip/tests/pecl12414.phpt
* @throws ZipException
*/
public function testPecl12414()
{
$filename = __DIR__ . '/php-zip-ext-test-resources/pecl12414.zip';
$entryName = 'MYLOGOV2.GFX';
$zipFile = new ZipFile();
$zipFile->openFile($filename);
$info = $zipFile->getEntryInfo($entryName);
$this->assertTrue($info->getSize() > 0);
$contents = $zipFile[$entryName];
$this->assertEquals(strlen($contents), $info->getSize());
$zipFile->close();
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace PhpZip;
use PhpZip\Exception\ZipException;
use PhpZip\Util\CryptoUtil;
/**
* Test ZipAlign
*/
class ZipAlignTest extends ZipTestCase
{
/**
* @throws ZipException
*/
public function testApkAlignedAndSetZipAlignAndReSave()
{
$filename = __DIR__ . '/resources/test.apk';
$this->assertCorrectZipArchive($filename);
$result = $this->assertVerifyZipAlign($filename);
if (null !== $result) {
$this->assertTrue($result);
}
$zipFile = new ZipFile();
$zipFile->openFile($filename);
$zipFile->setZipAlign(4);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename);
$result = $this->assertVerifyZipAlign($this->outputFilename, true);
if (null !== $result) {
$this->assertTrue($result);
}
}
/**
* Test zip alignment.
* @throws ZipException
*/
public function testZipAlignSourceZip()
{
$zipFile = new ZipFile();
for ($i = 0; $i < 100; $i++) {
$zipFile->addFromString(
'entry' . $i . '.txt',
CryptoUtil::randomBytes(mt_rand(100, 4096)),
ZipFileInterface::METHOD_STORED
);
}
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename);
$result = $this->assertVerifyZipAlign($this->outputFilename);
if ($result === null) {
return;
} // zip align not installed
// check not zip align
$this->assertFalse($result);
$zipFile->openFile($this->outputFilename);
$zipFile->setZipAlign(4);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename);
$result = $this->assertVerifyZipAlign($this->outputFilename, true);
$this->assertNotNull($result);
// check zip align
$this->assertTrue($result);
}
/**
* @throws ZipException
*/
public function testZipAlignNewFiles()
{
$zipFile = new ZipFile();
for ($i = 0; $i < 100; $i++) {
$zipFile->addFromString(
'entry' . $i . '.txt',
CryptoUtil::randomBytes(mt_rand(100, 4096)),
ZipFileInterface::METHOD_STORED
);
}
$zipFile->setZipAlign(4);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename);
$result = $this->assertVerifyZipAlign($this->outputFilename);
if ($result === null) {
return;
} // zip align not installed
// check not zip align
$this->assertTrue($result);
}
/**
* @throws ZipException
*/
public function testZipAlignFromModifiedZipArchive()
{
$zipFile = new ZipFile();
for ($i = 0; $i < 100; $i++) {
$zipFile->addFromString(
'entry' . $i . '.txt',
CryptoUtil::randomBytes(mt_rand(100, 4096)),
ZipFileInterface::METHOD_STORED
);
}
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename);
$result = $this->assertVerifyZipAlign($this->outputFilename);
if ($result === null) {
return;
} // zip align not installed
// check not zip align
$this->assertFalse($result);
$zipFile->openFile($this->outputFilename);
$zipFile->deleteFromRegex("~entry2[\d]+\.txt$~s");
for ($i = 0; $i < 100; $i++) {
$isStored = (bool)mt_rand(0, 1);
$zipFile->addFromString(
'entry_new_' . ($isStored ? 'stored' : 'deflated') . '_' . $i . '.txt',
CryptoUtil::randomBytes(mt_rand(100, 4096)),
$isStored ?
ZipFileInterface::METHOD_STORED :
ZipFileInterface::METHOD_DEFLATED
);
}
$zipFile->setZipAlign(4);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename);
$result = $this->assertVerifyZipAlign($this->outputFilename, true);
$this->assertNotNull($result);
// check zip align
$this->assertTrue($result);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace PhpZip;
use PhpZip\Exception\ZipException;
class ZipFileExtended extends ZipFile
{
protected function onBeforeSave()
{
parent::onBeforeSave();
$this->setZipAlign(4);
$this->deleteFromRegex('~^META\-INF/~i');
}
}
class ZipEventTest extends ZipTestCase
{
/**
* @throws ZipException
*/
public function testBeforeSave()
{
$zipFile = new ZipFileExtended();
$zipFile->openFile(__DIR__ . '/resources/test.apk');
$this->assertTrue(isset($zipFile['META-INF/MANIFEST.MF']));
$this->assertTrue(isset($zipFile['META-INF/CERT.SF']));
$this->assertTrue(isset($zipFile['META-INF/CERT.RSA']));
$zipFile->saveAsFile($this->outputFilename);
$this->assertFalse(isset($zipFile['META-INF/MANIFEST.MF']));
$this->assertFalse(isset($zipFile['META-INF/CERT.SF']));
$this->assertFalse(isset($zipFile['META-INF/CERT.RSA']));
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename);
$result = $this->assertVerifyZipAlign($this->outputFilename);
if (null !== $result) {
$this->assertTrue($result);
}
$zipFile->openFile($this->outputFilename);
$this->assertFalse(isset($zipFile['META-INF/MANIFEST.MF']));
$this->assertFalse(isset($zipFile['META-INF/CERT.SF']));
$this->assertFalse(isset($zipFile['META-INF/CERT.RSA']));
$zipFile->close();
}
}

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace PhpZip; namespace PhpZip;
use PhpZip\Exception\ZipException;
use PhpZip\Util\Iterator\IgnoreFilesFilterIterator; use PhpZip\Util\Iterator\IgnoreFilesFilterIterator;
use PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator; use PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator;
@@ -9,7 +11,7 @@ use PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator;
*/ */
class ZipFileAddDirTest extends ZipTestCase class ZipFileAddDirTest extends ZipTestCase
{ {
private static $files = [ protected static $files = [
'.hidden' => 'Hidden file', '.hidden' => 'Hidden file',
'text file.txt' => 'Text file', 'text file.txt' => 'Text file',
'Текстовый документ.txt' => 'Текстовый документ', 'Текстовый документ.txt' => 'Текстовый документ',
@@ -52,7 +54,7 @@ class ZipFileAddDirTest extends ZipTestCase
} }
} }
protected static function assertFilesResult(ZipFile $zipFile, array $actualResultFiles = [], $localPath = '/') protected static function assertFilesResult(ZipFileInterface $zipFile, array $actualResultFiles = [], $localPath = '/')
{ {
$localPath = rtrim($localPath, '/'); $localPath = rtrim($localPath, '/');
$localPath = empty($localPath) ? "" : $localPath . '/'; $localPath = empty($localPath) ? "" : $localPath . '/';
@@ -71,6 +73,9 @@ class ZipFileAddDirTest extends ZipTestCase
self::assertEmpty($actualResultFiles); self::assertEmpty($actualResultFiles);
} }
/**
* @throws ZipException
*/
public function testAddDirWithLocalPath() public function testAddDirWithLocalPath()
{ {
$localPath = 'to/path'; $localPath = 'to/path';
@@ -80,10 +85,10 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [ $this->assertFilesResult($zipFile, [
'.hidden', '.hidden',
'text file.txt', 'text file.txt',
'Текстовый документ.txt', 'Текстовый документ.txt',
@@ -92,6 +97,9 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->close(); $zipFile->close();
} }
/**
* @throws ZipException
*/
public function testAddDirWithoutLocalPath() public function testAddDirWithoutLocalPath()
{ {
$zipFile = new ZipFile(); $zipFile = new ZipFile();
@@ -99,10 +107,10 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [ $this->assertFilesResult($zipFile, [
'.hidden', '.hidden',
'text file.txt', 'text file.txt',
'Текстовый документ.txt', 'Текстовый документ.txt',
@@ -111,6 +119,9 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->close(); $zipFile->close();
} }
/**
* @throws ZipException
*/
public function testAddFilesFromIterator() public function testAddFilesFromIterator()
{ {
$localPath = 'to/project'; $localPath = 'to/project';
@@ -122,10 +133,10 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [ $this->assertFilesResult($zipFile, [
'.hidden', '.hidden',
'text file.txt', 'text file.txt',
'Текстовый документ.txt', 'Текстовый документ.txt',
@@ -134,6 +145,35 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->close(); $zipFile->close();
} }
/**
* @throws ZipException
*/
public function testAddFilesFromIteratorEmptyLocalPath()
{
$localPath = '';
$directoryIterator = new \DirectoryIterator($this->outputDirname);
$zipFile = new ZipFile();
$zipFile->addFilesFromIterator($directoryIterator, $localPath);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename);
$this->assertFilesResult($zipFile, [
'.hidden',
'text file.txt',
'Текстовый документ.txt',
'empty dir/',
]);
$zipFile->close();
}
/**
* @throws ZipException
*/
public function testAddFilesFromRecursiveIterator() public function testAddFilesFromRecursiveIterator()
{ {
$localPath = 'to/project'; $localPath = 'to/project';
@@ -145,13 +185,16 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, array_keys(self::$files), $localPath); $this->assertFilesResult($zipFile, array_keys(self::$files), $localPath);
$zipFile->close(); $zipFile->close();
} }
/**
* @throws ZipException
*/
public function testAddRecursiveDirWithLocalPath() public function testAddRecursiveDirWithLocalPath()
{ {
$localPath = 'to/path'; $localPath = 'to/path';
@@ -161,13 +204,16 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, array_keys(self::$files), $localPath); $this->assertFilesResult($zipFile, array_keys(self::$files), $localPath);
$zipFile->close(); $zipFile->close();
} }
/**
* @throws ZipException
*/
public function testAddRecursiveDirWithoutLocalPath() public function testAddRecursiveDirWithoutLocalPath()
{ {
$zipFile = new ZipFile(); $zipFile = new ZipFile();
@@ -175,14 +221,18 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, array_keys(self::$files)); $this->assertFilesResult($zipFile, array_keys(self::$files));
$zipFile->close(); $zipFile->close();
} }
public function testAddFilesFromIteratorWithIgnoreFiles(){ /**
* @throws ZipException
*/
public function testAddFilesFromIteratorWithIgnoreFiles()
{
$localPath = 'to/project'; $localPath = 'to/project';
$ignoreFiles = [ $ignoreFiles = [
'Текстовый документ.txt', 'Текстовый документ.txt',
@@ -197,17 +247,21 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [ $this->assertFilesResult($zipFile, [
'.hidden', '.hidden',
'text file.txt', 'text file.txt',
], $localPath); ], $localPath);
$zipFile->close(); $zipFile->close();
} }
public function testAddFilesFromRecursiveIteratorWithIgnoreFiles(){ /**
* @throws ZipException
*/
public function testAddFilesFromRecursiveIteratorWithIgnoreFiles()
{
$localPath = 'to/project'; $localPath = 'to/project';
$ignoreFiles = [ $ignoreFiles = [
'.hidden', '.hidden',
@@ -224,10 +278,10 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [ $this->assertFilesResult($zipFile, [
'text file.txt', 'text file.txt',
'Текстовый документ.txt', 'Текстовый документ.txt',
'empty dir/', 'empty dir/',
@@ -242,6 +296,7 @@ class ZipFileAddDirTest extends ZipTestCase
/** /**
* Create archive and add files from glob pattern * Create archive and add files from glob pattern
* @throws ZipException
*/ */
public function testAddFilesFromGlob() public function testAddFilesFromGlob()
{ {
@@ -252,10 +307,10 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [ $this->assertFilesResult($zipFile, [
'text file.txt', 'text file.txt',
'Текстовый документ.txt', 'Текстовый документ.txt',
], $localPath); ], $localPath);
@@ -264,6 +319,7 @@ class ZipFileAddDirTest extends ZipTestCase
/** /**
* Create archive and add recursively files from glob pattern * Create archive and add recursively files from glob pattern
* @throws ZipException
*/ */
public function testAddFilesFromGlobRecursive() public function testAddFilesFromGlobRecursive()
{ {
@@ -274,10 +330,10 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [ $this->assertFilesResult($zipFile, [
'text file.txt', 'text file.txt',
'Текстовый документ.txt', 'Текстовый документ.txt',
'category/list.txt', 'category/list.txt',
@@ -291,6 +347,7 @@ class ZipFileAddDirTest extends ZipTestCase
/** /**
* Create archive and add files from regex pattern * Create archive and add files from regex pattern
* @throws ZipException
*/ */
public function testAddFilesFromRegex() public function testAddFilesFromRegex()
{ {
@@ -301,10 +358,10 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [ $this->assertFilesResult($zipFile, [
'text file.txt', 'text file.txt',
'Текстовый документ.txt', 'Текстовый документ.txt',
], $localPath); ], $localPath);
@@ -313,6 +370,7 @@ class ZipFileAddDirTest extends ZipTestCase
/** /**
* Create archive and add files recursively from regex pattern * Create archive and add files recursively from regex pattern
* @throws ZipException
*/ */
public function testAddFilesFromRegexRecursive() public function testAddFilesFromRegexRecursive()
{ {
@@ -323,10 +381,10 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, [ $this->assertFilesResult($zipFile, [
'text file.txt', 'text file.txt',
'Текстовый документ.txt', 'Текстовый документ.txt',
'category/list.txt', 'category/list.txt',
@@ -338,6 +396,9 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->close(); $zipFile->close();
} }
/**
* @throws ZipException
*/
public function testArrayAccessAddDir() public function testArrayAccessAddDir()
{ {
$localPath = 'path/to'; $localPath = 'path/to';
@@ -348,12 +409,10 @@ class ZipFileAddDirTest extends ZipTestCase
$zipFile->saveAsFile($this->outputFilename); $zipFile->saveAsFile($this->outputFilename);
$zipFile->close(); $zipFile->close();
self::assertCorrectZipArchive($this->outputFilename); $this->assertCorrectZipArchive($this->outputFilename);
$zipFile->openFile($this->outputFilename); $zipFile->openFile($this->outputFilename);
self::assertFilesResult($zipFile, array_keys(self::$files), $localPath); $this->assertFilesResult($zipFile, array_keys(self::$files), $localPath);
$zipFile->close(); $zipFile->close();
} }
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,111 @@
<?php
namespace PhpZip;
use PhpZip\Model\ZipEntryMatcher;
use PhpZip\Model\ZipInfo;
use PhpZip\Util\CryptoUtil;
class ZipMatcherTest extends \PHPUnit_Framework_TestCase
{
public function testMatcher()
{
$zipFile = new ZipFile();
for ($i = 0; $i < 100; $i++) {
$zipFile[$i] = $i;
}
$matcher = $zipFile->matcher();
$this->assertInstanceOf(ZipEntryMatcher::class, $matcher);
$this->assertTrue(is_array($matcher->getMatches()));
$this->assertCount(0, $matcher);
$matcher->add(1)->add(10)->add(20);
$this->assertCount(3, $matcher);
$this->assertEquals($matcher->getMatches(), ['1', '10', '20']);
$matcher->delete();
$this->assertCount(97, $zipFile);
$this->assertCount(0, $matcher);
$matcher->match('~^[2][1-5]|[3][6-9]|40$~s');
$this->assertCount(10, $matcher);
$actualMatches = [
'21', '22', '23', '24', '25',
'36', '37', '38', '39',
'40'
];
$this->assertEquals($matcher->getMatches(), $actualMatches);
$matcher->setPassword('qwerty');
$info = $zipFile->getAllInfo();
array_walk($info, function (ZipInfo $zipInfo) use ($actualMatches) {
$this->assertEquals($zipInfo->isEncrypted(), in_array($zipInfo->getName(), $actualMatches));
});
$matcher->all();
$this->assertCount(count($zipFile), $matcher);
$expectedNames = [];
$matcher->invoke(function ($entryName) use (&$expectedNames) {
$expectedNames[] = $entryName;
});
$this->assertEquals($expectedNames, $matcher->getMatches());
$zipFile->close();
}
public function testDocsExample()
{
$zipFile = new ZipFile();
for ($i = 0; $i < 100; $i++) {
$zipFile['file_'.$i.'.jpg'] = CryptoUtil::randomBytes(100);
}
$renameEntriesArray = [
'file_10.jpg',
'file_11.jpg',
'file_12.jpg',
'file_13.jpg',
'file_14.jpg',
'file_15.jpg',
'file_16.jpg',
'file_17.jpg',
'file_18.jpg',
'file_19.jpg',
'file_50.jpg',
'file_51.jpg',
'file_52.jpg',
'file_53.jpg',
'file_54.jpg',
'file_55.jpg',
'file_56.jpg',
'file_57.jpg',
'file_58.jpg',
'file_59.jpg',
];
foreach ($renameEntriesArray as $name) {
$this->assertTrue(isset($zipFile[$name]));
}
$matcher = $zipFile->matcher();
$matcher->match('~^file_(1|5)\d+~');
$this->assertEquals($matcher->getMatches(), $renameEntriesArray);
$matcher->invoke(function ($entryName) use ($zipFile) {
$newName = preg_replace('~\.(jpe?g)$~i', '.no_optimize.$1', $entryName);
$zipFile->rename($entryName, $newName);
});
foreach ($renameEntriesArray as $name) {
$this->assertFalse(isset($zipFile[$name]));
$pathInfo = pathinfo($name);
$newName = $pathInfo['filename'].'.no_optimize.'.$pathInfo['extension'];
$this->assertTrue(isset($zipFile[$newName]));
}
$zipFile->close();
}
}

View File

@@ -0,0 +1,427 @@
<?php
namespace PhpZip;
use PhpZip\Exception\ZipAuthenticationException;
use PhpZip\Exception\ZipEntryNotFoundException;
use PhpZip\Exception\ZipException;
use PhpZip\Model\ZipInfo;
use PhpZip\Util\CryptoUtil;
/**
* Tests with zip password.
*/
class ZipPasswordTest extends ZipFileAddDirTest
{
/**
* Test archive password.
* @throws ZipException
*/
public function testSetPassword()
{
if (PHP_INT_SIZE === 4) {
$this->markTestSkipped('Skip test for 32-bit system. Not support Traditional PKWARE Encryption.');
}
$password = base64_encode(CryptoUtil::randomBytes(100));
$badPassword = "bad password";
// create encryption password with ZipCrypto
$zipFile = new ZipFile();
$zipFile->addDir(__DIR__);
$zipFile->setPassword($password, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename, $password);
// check bad password for ZipCrypto
$zipFile->openFile($this->outputFilename);
$zipFile->setReadPassword($badPassword);
foreach ($zipFile->getListFiles() as $entryName) {
try {
$zipFile[$entryName];
$this->fail("Expected Exception has not been raised.");
} catch (ZipAuthenticationException $ae) {
$this->assertContains('Invalid password for zip entry', $ae->getMessage());
}
}
// check correct password for ZipCrypto
$zipFile->setReadPassword($password);
foreach ($zipFile->getAllInfo() as $info) {
$this->assertTrue($info->isEncrypted());
$this->assertContains('ZipCrypto', $info->getMethodName());
$decryptContent = $zipFile[$info->getName()];
$this->assertNotEmpty($decryptContent);
$this->assertContains('<?php', $decryptContent);
}
// change encryption method to WinZip Aes and update file
$zipFile->setPassword($password, ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES);
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename, $password);
// check from WinZip AES encryption
$zipFile->openFile($this->outputFilename);
// set bad password WinZip AES
$zipFile->setReadPassword($badPassword);
foreach ($zipFile->getListFiles() as $entryName) {
try {
$zipFile[$entryName];
$this->fail("Expected Exception has not been raised.");
} catch (ZipAuthenticationException $ae) {
$this->assertNotNull($ae);
}
}
// set correct password WinZip AES
$zipFile->setReadPassword($password);
foreach ($zipFile->getAllInfo() as $info) {
$this->assertTrue($info->isEncrypted());
$this->assertContains('WinZip', $info->getMethodName());
$decryptContent = $zipFile[$info->getName()];
$this->assertNotEmpty($decryptContent);
$this->assertContains('<?php', $decryptContent);
}
// clear password
$zipFile->addFromString('file1', '');
$zipFile->disableEncryption();
$zipFile->addFromString('file2', '');
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename);
// check remove password
$zipFile->openFile($this->outputFilename);
foreach ($zipFile->getAllInfo() as $info) {
$this->assertFalse($info->isEncrypted());
}
$zipFile->close();
}
/**
* @throws ZipException
*/
public function testTraditionalEncryption()
{
if (PHP_INT_SIZE === 4) {
$this->markTestSkipped('Skip test for 32-bit system. Not support Traditional PKWARE Encryption.');
}
$password = base64_encode(CryptoUtil::randomBytes(50));
$zip = new ZipFile();
$zip->addDirRecursive($this->outputDirname);
$zip->setPassword($password, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL);
$zip->saveAsFile($this->outputFilename);
$zip->close();
$this->assertCorrectZipArchive($this->outputFilename, $password);
$zip->openFile($this->outputFilename);
$zip->setReadPassword($password);
$this->assertFilesResult($zip, array_keys(self::$files));
foreach ($zip->getAllInfo() as $info) {
if (!$info->isFolder()) {
$this->assertTrue($info->isEncrypted());
$this->assertContains('ZipCrypto', $info->getMethodName());
}
}
$zip->close();
}
/**
* @dataProvider winZipKeyStrengthProvider
* @param int $encryptionMethod
* @param int $bitSize
* @throws ZipException
*/
public function testWinZipAesEncryption($encryptionMethod, $bitSize)
{
$password = base64_encode(CryptoUtil::randomBytes(50));
$zip = new ZipFile();
$zip->addDirRecursive($this->outputDirname);
$zip->setPassword($password, $encryptionMethod);
$zip->saveAsFile($this->outputFilename);
$zip->close();
$this->assertCorrectZipArchive($this->outputFilename, $password);
$zip->openFile($this->outputFilename);
$zip->setReadPassword($password);
$this->assertFilesResult($zip, array_keys(self::$files));
foreach ($zip->getAllInfo() as $info) {
if (!$info->isFolder()) {
$this->assertTrue($info->isEncrypted());
$this->assertEquals($info->getEncryptionMethod(), $encryptionMethod);
$this->assertContains('WinZip AES-' . $bitSize, $info->getMethodName());
}
}
$zip->close();
}
/**
* @return array
*/
public function winZipKeyStrengthProvider()
{
return [
[ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128, 128],
[ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192, 192],
[ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES, 256],
[ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256, 256],
];
}
/**
* @throws Exception\ZipEntryNotFoundException
* @throws ZipException
*/
public function testEncryptionEntries()
{
if (PHP_INT_SIZE === 4) {
$this->markTestSkipped('Skip test for 32-bit system. Not support Traditional PKWARE Encryption.');
}
$password1 = '353442434235424234';
$password2 = 'adgerhvrwjhqqehtqhkbqrgewg';
$zip = new ZipFile();
$zip->addDir($this->outputDirname);
$zip->setPasswordEntry('.hidden', $password1, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL);
$zip->setPasswordEntry('text file.txt', $password2, ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES);
$zip->saveAsFile($this->outputFilename);
$zip->close();
$zip->openFile($this->outputFilename);
$zip->setReadPasswordEntry('.hidden', $password1);
$zip->setReadPasswordEntry('text file.txt', $password2);
$this->assertFilesResult($zip, [
'.hidden',
'text file.txt',
'Текстовый документ.txt',
'empty dir/',
]);
$info = $zip->getEntryInfo('.hidden');
$this->assertTrue($info->isEncrypted());
$this->assertContains('ZipCrypto', $info->getMethodName());
$info = $zip->getEntryInfo('text file.txt');
$this->assertTrue($info->isEncrypted());
$this->assertContains('WinZip AES', $info->getMethodName());
$this->assertFalse($zip->getEntryInfo('Текстовый документ.txt')->isEncrypted());
$this->assertFalse($zip->getEntryInfo('empty dir/')->isEncrypted());
$zip->close();
}
/**
* @throws Exception\ZipEntryNotFoundException
* @throws ZipException
*/
public function testEncryptionEntriesWithDefaultPassword()
{
if (PHP_INT_SIZE === 4) {
$this->markTestSkipped('Skip test for 32-bit system. Not support Traditional PKWARE Encryption.');
}
$password1 = '353442434235424234';
$password2 = 'adgerhvrwjhqqehtqhkbqrgewg';
$defaultPassword = ' f f f f f ffff f5 ';
$zip = new ZipFile();
$zip->addDir($this->outputDirname);
$zip->setPassword($defaultPassword);
$zip->setPasswordEntry('.hidden', $password1, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL);
$zip->setPasswordEntry('text file.txt', $password2, ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES);
$zip->saveAsFile($this->outputFilename);
$zip->close();
$zip->openFile($this->outputFilename);
$zip->setReadPassword($defaultPassword);
$zip->setReadPasswordEntry('.hidden', $password1);
$zip->setReadPasswordEntry('text file.txt', $password2);
$this->assertFilesResult($zip, [
'.hidden',
'text file.txt',
'Текстовый документ.txt',
'empty dir/',
]);
$info = $zip->getEntryInfo('.hidden');
$this->assertTrue($info->isEncrypted());
$this->assertContains('ZipCrypto', $info->getMethodName());
$info = $zip->getEntryInfo('text file.txt');
$this->assertTrue($info->isEncrypted());
$this->assertContains('WinZip AES', $info->getMethodName());
$info = $zip->getEntryInfo('Текстовый документ.txt');
$this->assertTrue($info->isEncrypted());
$this->assertContains('WinZip AES', $info->getMethodName());
$this->assertFalse($zip->getEntryInfo('empty dir/')->isEncrypted());
$zip->close();
}
/**
* @expectedException \PhpZip\Exception\ZipException
* @expectedExceptionMessage Invalid encryption method
*/
public function testSetEncryptionMethodInvalid()
{
$zipFile = new ZipFile();
$encryptionMethod = 9999;
$zipFile->setPassword('pass', $encryptionMethod);
$zipFile['entry'] = 'content';
$zipFile->outputAsString();
}
/**
* @throws Exception\ZipEntryNotFoundException
* @throws ZipException
*/
public function testEntryPassword()
{
$zipFile = new ZipFile();
$zipFile->setPassword('pass');
$zipFile['file'] = 'content';
$this->assertFalse($zipFile->getEntryInfo('file')->isEncrypted());
for ($i = 1; $i <= 10; $i++) {
$zipFile['file' . $i] = 'content';
if ($i < 6) {
$zipFile->setPasswordEntry('file' . $i, 'pass');
$this->assertTrue($zipFile->getEntryInfo('file' . $i)->isEncrypted());
} else {
$this->assertFalse($zipFile->getEntryInfo('file' . $i)->isEncrypted());
}
}
$zipFile->disableEncryptionEntry('file3');
$this->assertFalse($zipFile->getEntryInfo('file3')->isEncrypted());
$this->asserttrue($zipFile->getEntryInfo('file2')->isEncrypted());
$zipFile->disableEncryption();
$infoList = $zipFile->getAllInfo();
array_walk($infoList, function (ZipInfo $zipInfo) {
$this->assertFalse($zipInfo->isEncrypted());
});
$zipFile->close();
}
/**
* @expectedException \PhpZip\Exception\ZipException
* @expectedExceptionMessage Invalid encryption method
*/
public function testInvalidEncryptionMethodEntry()
{
$zipFile = new ZipFile();
$zipFile->addFromString('file', 'content', ZipFileInterface::METHOD_STORED);
$zipFile->setPasswordEntry('file', 'pass', 99);
}
/**
* @throws ZipEntryNotFoundException
* @throws ZipException
*/
public function testArchivePasswordUpdateWithoutSetReadPassword()
{
$zipFile = new ZipFile();
$zipFile['file1'] = 'content';
$zipFile['file2'] = 'content';
$zipFile['file3'] = 'content';
$zipFile->setPassword('password');
$zipFile->saveAsFile($this->outputFilename);
$zipFile->close();
$this->assertCorrectZipArchive($this->outputFilename, 'password');
$zipFile->openFile($this->outputFilename);
$this->assertCount(3, $zipFile);
foreach ($zipFile->getAllInfo() as $info) {
$this->assertTrue($info->isEncrypted());
}
unset($zipFile['file3']);
$zipFile['file4'] = 'content';
$zipFile->rewrite();
$this->assertCorrectZipArchive($this->outputFilename, 'password');
$this->assertCount(3, $zipFile);
$this->assertFalse(isset($zipFile['file3']));
$this->assertTrue(isset($zipFile['file4']));
$this->assertTrue($zipFile->getEntryInfo('file1')->isEncrypted());
$this->assertTrue($zipFile->getEntryInfo('file2')->isEncrypted());
$this->assertFalse($zipFile->getEntryInfo('file4')->isEncrypted());
$this->assertEquals($zipFile['file4'], 'content');
$zipFile->extractTo($this->outputDirname, ['file4']);
$this->assertTrue(file_exists($this->outputDirname . DIRECTORY_SEPARATOR . 'file4'));
$this->assertEquals(file_get_contents($this->outputDirname . DIRECTORY_SEPARATOR . 'file4'), $zipFile['file4']);
$zipFile->close();
}
/**
* @see https://github.com/Ne-Lexa/php-zip/issues/9
* @throws ZipException
*/
public function testIssues9()
{
$contents = str_pad('', 1000, 'test;test2;test3' . PHP_EOL, STR_PAD_RIGHT);
$password = base64_encode(CryptoUtil::randomBytes(20));
$encryptMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256;
$zipFile = new ZipFile();
$zipFile
->addFromString('codes.csv', $contents)
->setPassword($password, $encryptMethod)
->saveAsFile($this->outputFilename)
->close();
$this->assertCorrectZipArchive($this->outputFilename, $password);
$zipFile->openFile($this->outputFilename);
$zipFile->setReadPassword($password);
$this->assertEquals($zipFile['codes.csv'], $contents);
$zipFile->close();
}
/**
* @throws ZipEntryNotFoundException
* @throws ZipException
*/
public function testReadAesEncryptedAndRewriteArchive()
{
$file = __DIR__ . '/resources/aes_password_archive.zip';
$password = '1234567890';
$zipFile = new ZipFile();
$zipFile->openFile($file);
$zipFile->setReadPassword($password);
$zipFile->setEntryComment('contents.txt', 'comment'); // change entry, but not changed contents
$zipFile->saveAsFile($this->outputFilename);
$zipFile2 = new ZipFile();
$zipFile2->openFile($this->outputFilename);
$zipFile2->setReadPassword($password);
$this->assertEquals($zipFile2->getListFiles(), $zipFile->getListFiles());
foreach ($zipFile as $name => $contents) {
$this->assertNotEmpty($name);
$this->assertNotEmpty($contents);
$this->assertContains('test contents', $contents);
$this->assertEquals($zipFile2[$name], $contents);
}
$zipFile2->close();
$zipFile->close();
}
}

View File

@@ -1,4 +1,5 @@
<?php <?php
namespace PhpZip; namespace PhpZip;
use PhpZip\Model\EndOfCentralDirectory; use PhpZip\Model\EndOfCentralDirectory;
@@ -27,8 +28,14 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
parent::setUp(); parent::setUp();
$id = uniqid('phpzip'); $id = uniqid('phpzip');
$this->outputFilename = sys_get_temp_dir() . '/' . $id . '.zip'; $tempDir = sys_get_temp_dir() . '/phpunit-phpzip';
$this->outputDirname = sys_get_temp_dir() . '/' . $id; if (!is_dir($tempDir)) {
if (!mkdir($tempDir, 0755, true)) {
throw new \RuntimeException("Dir " . $tempDir . " can't created");
}
}
$this->outputFilename = $tempDir . '/' . $id . '.zip';
$this->outputDirname = $tempDir . '/' . $id;
} }
/** /**
@@ -72,7 +79,9 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
$command = "7z t -p" . escapeshellarg($password) . " " . escapeshellarg($filename); $command = "7z t -p" . escapeshellarg($password) . " " . escapeshellarg($filename);
exec($command, $output, $returnCode); exec($command, $output, $returnCode);
/**
* @var array $output
*/
$output = implode(PHP_EOL, $output); $output = implode(PHP_EOL, $output);
self::assertEquals($returnCode, 0); self::assertEquals($returnCode, 0);
@@ -114,16 +123,17 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase
* @param bool $showErrors * @param bool $showErrors
* @return bool|null If null - can not install zipalign * @return bool|null If null - can not install zipalign
*/ */
public static function doZipAlignVerify($filename, $showErrors = false) public static function assertVerifyZipAlign($filename, $showErrors = false)
{ {
if (DIRECTORY_SEPARATOR !== '\\' && `which zipalign`) { if (DIRECTORY_SEPARATOR !== '\\' && `which zipalign`) {
exec("zipalign -c -v 4 " . escapeshellarg($filename), $output, $returnCode); exec("zipalign -c -v 4 " . escapeshellarg($filename), $output, $returnCode);
if ($showErrors && $returnCode !== 0) fwrite(STDERR, implode(PHP_EOL, $output)); if ($showErrors && $returnCode !== 0) {
fwrite(STDERR, implode(PHP_EOL, $output));
}
return $returnCode === 0; return $returnCode === 0;
} else { } else {
fwrite(STDERR, 'Can not find program "zipalign" for test'); fwrite(STDERR, 'Can not find program "zipalign" for test' . PHP_EOL);
return null; return null;
} }
} }
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.