From 452c5920dd505e0040bae0bdac455dd95b0ea758 Mon Sep 17 00:00:00 2001 From: wapplay-home-linux Date: Mon, 13 Nov 2017 15:33:37 +0300 Subject: [PATCH 01/10] Add ZipModel for all changes. --- .codeclimate.yml | 9 - .gitignore | 3 +- .travis.yml | 10 +- README.RU.md | 814 +++++++++++++++++ README.md | 724 ++++++++++----- composer.json | 11 +- bootstrap.xml => phpunit.xml | 0 .../TraditionalPkwareEncryptionEngine.php | 37 +- src/PhpZip/Crypto/WinZipAesEngine.php | 23 +- ...yptoEngine.php => ZipEncryptionEngine.php} | 12 +- src/PhpZip/Exception/Crc32Exception.php | 14 +- .../Exception/InvalidArgumentException.php | 6 +- src/PhpZip/Exception/RuntimeException.php | 4 +- .../Exception/ZipAuthenticationException.php | 4 +- src/PhpZip/Exception/ZipCryptoException.php | 4 +- src/PhpZip/Exception/ZipException.php | 4 +- src/PhpZip/Exception/ZipNotFoundEntry.php | 4 +- src/PhpZip/Exception/ZipUnsupportMethod.php | 4 +- src/PhpZip/Extra/DefaultExtraField.php | 98 -- src/PhpZip/Extra/ExtraField.php | 115 +-- src/PhpZip/Extra/ExtraFieldHeader.php | 21 - src/PhpZip/Extra/ExtraFields.php | 200 ----- src/PhpZip/Extra/ExtraFieldsCollection.php | 240 +++++ src/PhpZip/Extra/ExtraFieldsFactory.php | 100 +++ src/PhpZip/Extra/Fields/DefaultExtraField.php | 71 ++ src/PhpZip/Extra/Fields/NtfsExtraField.php | 133 +++ .../{ => Fields}/WinZipAesEntryExtraField.php | 144 +-- src/PhpZip/Extra/Fields/Zip64ExtraField.php | 118 +++ src/PhpZip/Extra/NtfsExtraField.php | 176 ---- src/PhpZip/Mapper/OffsetPositionMapper.php | 5 +- src/PhpZip/Mapper/PositionMapper.php | 3 +- src/PhpZip/Model/CentralDirectory.php | 482 ---------- src/PhpZip/Model/EndOfCentralDirectory.php | 323 +------ src/PhpZip/Model/Entry/OutputOffsetEntry.php | 49 + src/PhpZip/Model/Entry/ZipAbstractEntry.php | 559 +++++------- src/PhpZip/Model/Entry/ZipChangesEntry.php | 63 ++ .../Model/Entry/ZipNewEmptyDirEntry.php | 26 - src/PhpZip/Model/Entry/ZipNewEntry.php | 289 ++---- src/PhpZip/Model/Entry/ZipNewStreamEntry.php | 55 -- src/PhpZip/Model/Entry/ZipNewStringEntry.php | 39 - src/PhpZip/Model/Entry/ZipReadEntry.php | 327 ------- src/PhpZip/Model/Entry/ZipSourceEntry.php | 95 ++ src/PhpZip/Model/ZipEntry.php | 109 +-- src/PhpZip/Model/ZipEntryMatcher.php | 166 ++++ src/PhpZip/Model/ZipInfo.php | 244 +++-- src/PhpZip/Model/ZipModel.php | 341 +++++++ src/PhpZip/Stream/ResponseStream.php | 298 +++++++ src/PhpZip/Stream/ZipInputStream.php | 532 +++++++++++ src/PhpZip/Stream/ZipInputStreamInterface.php | 50 ++ src/PhpZip/Stream/ZipOutputStream.php | 543 +++++++++++ .../Stream/ZipOutputStreamInterface.php | 29 + src/PhpZip/Util/CryptoUtil.php | 6 +- src/PhpZip/Util/DateTimeConverter.php | 9 +- src/PhpZip/Util/FilesUtil.php | 24 +- .../Iterator/IgnoreFilesFilterIterator.php | 5 +- .../IgnoreFilesRecursiveFilterIterator.php | 3 +- src/PhpZip/Util/PackUtil.php | 19 +- src/PhpZip/Util/StringUtil.php | 5 +- src/PhpZip/ZipFile.php | 840 +++++++++++------- src/PhpZip/ZipFileInterface.php | 630 +++++++++++++ tests/PhpZip/PhpZipExtResourceTest.php | 143 +++ tests/PhpZip/ZipFileAddDirTest.php | 38 +- tests/PhpZip/ZipFileTest.php | 599 +++++++++---- tests/PhpZip/ZipMatcherTest.php | 111 +++ tests/PhpZip/ZipPasswordTest.php | 330 +++++++ tests/PhpZip/ZipTestCase.php | 20 +- .../php-zip-ext-test-resources/binarynull.zip | Bin 0 -> 656 bytes .../php-zip-ext-test-resources/bug40228.zip | Bin 0 -> 274 bytes .../bug40228私はガラスを食べられます.zip | Bin 0 -> 274 bytes .../php-zip-ext-test-resources/bug49072.zip | Bin 0 -> 162657 bytes .../php-zip-ext-test-resources/bug70752.zip | Bin 0 -> 175 bytes .../php-zip-ext-test-resources/bug8009.zip | Bin 0 -> 112 bytes .../php-zip-ext-test-resources/pecl12414.zip | Bin 0 -> 17271 bytes 73 files changed, 7081 insertions(+), 3431 deletions(-) delete mode 100644 .codeclimate.yml create mode 100644 README.RU.md rename bootstrap.xml => phpunit.xml (100%) rename src/PhpZip/Crypto/{CryptoEngine.php => ZipEncryptionEngine.php} (66%) delete mode 100644 src/PhpZip/Extra/DefaultExtraField.php delete mode 100644 src/PhpZip/Extra/ExtraFieldHeader.php delete mode 100644 src/PhpZip/Extra/ExtraFields.php create mode 100644 src/PhpZip/Extra/ExtraFieldsCollection.php create mode 100644 src/PhpZip/Extra/ExtraFieldsFactory.php create mode 100644 src/PhpZip/Extra/Fields/DefaultExtraField.php create mode 100644 src/PhpZip/Extra/Fields/NtfsExtraField.php rename src/PhpZip/Extra/{ => Fields}/WinZipAesEntryExtraField.php (65%) create mode 100644 src/PhpZip/Extra/Fields/Zip64ExtraField.php delete mode 100644 src/PhpZip/Extra/NtfsExtraField.php delete mode 100644 src/PhpZip/Model/CentralDirectory.php create mode 100644 src/PhpZip/Model/Entry/OutputOffsetEntry.php create mode 100644 src/PhpZip/Model/Entry/ZipChangesEntry.php delete mode 100644 src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php delete mode 100644 src/PhpZip/Model/Entry/ZipNewStreamEntry.php delete mode 100644 src/PhpZip/Model/Entry/ZipNewStringEntry.php delete mode 100644 src/PhpZip/Model/Entry/ZipReadEntry.php create mode 100644 src/PhpZip/Model/Entry/ZipSourceEntry.php create mode 100644 src/PhpZip/Model/ZipEntryMatcher.php create mode 100644 src/PhpZip/Model/ZipModel.php create mode 100644 src/PhpZip/Stream/ResponseStream.php create mode 100644 src/PhpZip/Stream/ZipInputStream.php create mode 100644 src/PhpZip/Stream/ZipInputStreamInterface.php create mode 100644 src/PhpZip/Stream/ZipOutputStream.php create mode 100644 src/PhpZip/Stream/ZipOutputStreamInterface.php create mode 100644 src/PhpZip/ZipFileInterface.php create mode 100644 tests/PhpZip/PhpZipExtResourceTest.php create mode 100644 tests/PhpZip/ZipMatcherTest.php create mode 100644 tests/PhpZip/ZipPasswordTest.php create mode 100644 tests/PhpZip/php-zip-ext-test-resources/binarynull.zip create mode 100644 tests/PhpZip/php-zip-ext-test-resources/bug40228.zip create mode 100644 tests/PhpZip/php-zip-ext-test-resources/bug40228私はガラスを食べられます.zip create mode 100644 tests/PhpZip/php-zip-ext-test-resources/bug49072.zip create mode 100644 tests/PhpZip/php-zip-ext-test-resources/bug70752.zip create mode 100644 tests/PhpZip/php-zip-ext-test-resources/bug8009.zip create mode 100644 tests/PhpZip/php-zip-ext-test-resources/pecl12414.zip diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index eb2cfbb..0000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,9 +0,0 @@ -engines: - duplication: - enabled: true - config: - languages: - - php -exclude_paths: - - tests/ - - vendor/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index cec7fa3..7ec2d22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /vendor *.iml /.idea -/composer.lock \ No newline at end of file +/composer.lock +/.php_cs.cache \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6ee971a..281a057 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,10 +12,6 @@ cache: - vendor - $HOME/.composer/cache -addons: - code_climate: - repo_token: 486a09d58d663450146c53c81c6c64938bcf3bb0b7c8ddebdc125fe97c18213a - install: - travis_retry composer self-update && composer --version - travis_retry composer install --prefer-dist --no-interaction @@ -25,8 +21,4 @@ before_script: script: - composer validate --no-check-lock - - vendor/bin/phpunit -v -c bootstrap.xml --coverage-clover build/logs/clover.xml - -after_success: - - vendor/bin/test-reporter - + - vendor/bin/phpunit -v -c phpunit.xml diff --git a/README.RU.md b/README.RU.md new file mode 100644 index 0000000..5e53597 --- /dev/null +++ b/README.RU.md @@ -0,0 +1,814 @@ +`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) +- [Обновление версий](#Upgrade) + + [Обновление с версии 2 до версии 3.0](#Upgrade-v2-to-v3) + +### Функционал +- Открытие и разархивирование 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 + + Установка пароля для чтения архива глобально или для некоторых записей. + + Изменение пароля архива, в том числе и для отдельных записей. + + Удаление пароля архива глобально или для отдельных записей. + + Установка пароля и/или метода шифрования, как для всех, так и для отдельных записей в архиве. + + Установка разных паролей и методов шифрования для разных записей. + + Удаление пароля для всех или для некоторых записей. + + Поддержка методов шифрования `Traditional PKWARE Encryption (ZipCrypto)` и `WinZIP AES Encryption (128, 192 или 256 bit)`. + + Установка метода шифрования для всех или для отдельных записей в архиве. + +### Требования +- `PHP` >= 5.5 (предпочтительно 64-bit). +- Опционально php-расширение `bzip2` для поддержки BZIP2 компрессии. +- Опционально php-расширение `openssl` или `mcrypt` для `WinZip Aes Encryption` шифрования. + +### Установка +`composer require nelexa/zip` + +Последняя стабильная версия: [![Latest Stable Version](https://poser.pugx.org/nelexa/zip/v/stable)](https://packagist.org/packages/nelexa/zip) + +### Примеры +```php +// создание нового архива +$zipFile = new \PhpZip\ZipFile(); +$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'); // вывести в браузер без сохранения в файл +``` +Другие примеры можно посмотреть в папке `tests/`. + +### Глоссарий +**Запись в ZIP-архиве (Zip Entry)** - файл или папка в ZIP-архиве. У каждой записи в архиве есть определённые свойства, например: имя файла, метод сжатия, метод шифрования, размер файла до сжатия, размер файла после сжатия, CRC32 и другие. + +### Документация +#### Обзор методов класса `\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::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::removePassword](#Documentation-ZipFile-removePassword) - удаляет пароль у всех файлов в архиве. +- [ZipFile::removePasswordEntry](#Documentation-ZipFile-removePasswordEntry) - удаляет пароль у конкретного файла в архиве. +- [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::removePassword](#Documentation-ZipFile-removePassword). +- ~~ZipFile::withReadPassword~~ - устаревший метод (**deprecated**) используйте метод [ZipFile::setReadPassword](#Documentation-ZipFile-setReadPassword). + +#### Создание/Открытие ZIP-архива +**ZipFile::__construct** - Инициализацирует ZIP-архив. +```php +$zipFile = new \PhpZip\ZipFile(); +``` + **ZipFile::openFile** - открывает ZIP-архив из файла. +```php +$zipFile = new \PhpZip\ZipFile(); +$zipFile->openFile('file.zip'); +``` + **ZipFile::openFromString** - открывает ZIP-архив из строки. +```php +$zipFile = new \PhpZip\ZipFile(); +$zipFile->openFromString($stringContents); +``` + **ZipFile::openFromStream** - открывает ZIP-архив из потока. +```php +$stream = fopen('file.zip', 'rb'); + +$zipFile = new \PhpZip\ZipFile(); +$zipFile->openFromStream($stream); +``` +#### Чтение записей из архива + **ZipFile::count** - возвращает количество записей в архиве. +```php +$count = count($zipFile); +// или +$count = $zipFile->count(); +``` + **ZipFile::getListFiles** - возвращает список файлов архива. +```php +$listFiles = $zipFile->getListFiles(); + +// Пример содержимого массива: +// array ( +// 0 => 'info.txt', +// 1 => 'path/to/file.jpg', +// 2 => 'another path/', +// ) +``` + **ZipFile::getEntryContent** - возвращает содержимое записи. +```php +// $entryName = 'path/to/example-entry-name.txt'; + +$contents = $zipFile[$entryName]; +// или +$contents = $zipFile->getEntryContents($entryName); +``` + **ZipFile::hasEntry** - проверяет, присутствует ли запись в архиве. +```php +// $entryName = 'path/to/example-entry-name.txt'; + +$hasEntry = isset($zipFile[$entryName]); +// или +$hasEntry = $zipFile->hasEntry($entryName); +``` + **ZipFile::isDirectory** - проверяет, является ли запись в архиве директорией. +```php +// $entryName = 'path/to/'; + +$isDirectory = $zipFile->isDirectory($entryName); +``` + **ZipFile::extractTo** - извлекает содержимое архива в заданную директорию. +Директория должна существовать. +```php +$zipFile->extractTo($directory); +``` +Можно извлечь только некоторые записи в заданную директорию. +Директория должна существовать. +```php +$extractOnlyFiles = [ + "filename1", + "filename2", + "dir/dir/dir/" +]; +$zipFile->extractTo($directory, $extractOnlyFiles); +``` +#### Перебор записей/Итератор +`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(); +} +``` +#### Получение информации о записях + **ZipFile::getArchiveComment** - возвращает комментарий ZIP-архива. +```php +$commentArchive = $zipFile->getArchiveComment(); +``` + **ZipFile::getEntryComment** - возвращает комментарий к записи, используя её имя. +```php +$commentEntry = $zipFile->getEntryComment($entryName); +``` + **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} +``` + **ZipFile::getAllInfo** - возвращает подробную информацию обо всех записях в архиве. +```php +$zipAllInfo = $zipFile->getAllInfo(); + +print_r($zipAllInfo); +//Array +//( +// [file.txt] => PhpZip\Model\ZipInfo Object +// ( +// ... +// ) +// +// [file2.txt] => PhpZip\Model\ZipInfo Object +// ( +// ... +// ) +// +// ... +//) +``` +#### Добавление записей в архив + +Все методы добавления записей в ZIP-архив позволяют указать метод сжатия содержимого. + +Доступны следующие методы сжатия: +- `\PhpZip\ZipFile::METHOD_STORED` - без сжатия +- `\PhpZip\ZipFile::METHOD_DEFLATED` - Deflate сжатие +- `\PhpZip\ZipFile::METHOD_BZIP2` - Bzip2 сжатие при наличии расширения `ext-bz2` + + **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 сжатие +``` + **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 сжатие +``` + **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 сжатие +``` + **ZipFile::addEmptyDir** - добавляет в ZIP-архив новую (пустую) директорию. +```php +// $path = "path/to/"; + +$zipFile->addEmptyDir($path); +// или +$zipFile[$path] = null; +``` + **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); +``` + **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 сжатие +``` + **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 сжатие +``` + **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); +``` + **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 сжатие +``` + **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 сжатие +``` + **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 сжатие +``` + **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 сжатие +``` +#### Удаление записей из архива + **ZipFile::deleteFromName** - удаляет запись по имени. +```php +$zipFile->deleteFromName($entryName); +``` + **ZipFile::deleteFromGlob** - удаляет записи в соответствии с [glob шаблоном](https://en.wikipedia.org/wiki/Glob_(programming)). +```php +$globPattern = '**.{jpg,jpeg,png,gif}'; // пример glob шаблона -> удалить все .jpg, .jpeg, .png и .gif файлы + +$zipFile->deleteFromGlob($globPattern); +``` + **ZipFile::deleteFromRegex** - удаляет записи в соответствии с [регулярным выражением](https://en.wikipedia.org/wiki/Regular_expression). +```php +$regexPattern = '/\.(jpe?g|png|gif)$/si'; // пример регулярному выражения -> удалить все .jpg, .jpeg, .png и .gif файлы + +$zipFile->deleteFromRegex($regexPattern); +``` + **ZipFile::deleteAll** - удаляет все записи в ZIP-архиве. +```php +$zipFile->deleteAll(); +``` +#### Работа с записями и с архивом + **ZipFile::rename** - переименовывает запись по имени. +```php +$zipFile->rename($oldName, $newName); +``` + **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); +``` + **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); +``` + **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); +``` + **ZipFile::setArchiveComment** - устанавливает комментарий к ZIP-архиву. +```php +$zipFile->setArchiveComment($commentArchive); +``` + **ZipFile::setEntryComment** - устанавливает комментарий к записи, используя её имя. +```php +$zipFile->setEntryComment($entryName, $comment); +``` + - выборка записей в архиве для проведения операций над выбранными записями. +```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(); // отключает шифрование для выбранных записей +``` +#### Работа с паролями + +Реализована поддержка методов шифрования: +- `\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 + + **ZipFile::setReadPassword** - устанавливает пароль на чтение открытого запароленного архива для всех зашифрованных записей. + +> _Установка пароля не является обязательной для добавления новых записей или удаления существующих, но если вы захотите извлечь контент или изменить метод/уровень сжатия, метод шифрования или изменить пароль, то в этом случае пароль необходимо указать._ +```php +$zipFile->setReadPassword($password); +``` + **ZipFile::setReadPasswordEntry** - устанавливает пароль на чтение конкретной зашифрованной записи открытого запароленного архива. +```php +$zipFile->setReadPasswordEntry($entryName, $password); +``` + **ZipFile::setPassword** - устанавливает новый пароль для всех файлов, находящихся в архиве. + +> _Обратите внимание, что действие данного метода не распространяется на записи, добавленные после выполнения этого метода._ +```php +$zipFile->setPassword($password); +``` +Можно установить метод шифрования: +```php +$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; +$zipFile->setPassword($password, $encryptionMethod); +``` + **ZipFile::setPasswordEntry** - устанавливает новый пароль для конкретного файла. +```php +$zipFile->setPasswordEntry($entryName, $password); +``` +Можно установить метод шифрования: +```php +$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; +$zipFile->setPasswordEntry($entryName, $password, $encryptionMethod); +``` + **ZipFile::removePassword** - удаляет пароль у всех файлов в архиве. + +> _Обратите внимание, что действие данного метода не распространяется на записи, добавленные после выполнения этого метода._ +```php +$zipFile->removePassword(); +``` + **ZipFile::removePasswordEntry** - удаляет пароль у конкретного файла в архиве. +```php +$zipFile->removePasswordEntry($entryName); +``` +#### zipalign + **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); +``` +#### Отмена изменений + **ZipFile::unchangeAll** - отменяет все изменения, сделанные в архиве. +```php +$zipFile->unchangeAll(); +``` + **ZipFile::unchangeArchiveComment** - отменяет изменения в комментарии к архиву. +```php +$zipFile->unchangeArchiveComment(); +``` + **ZipFile::unchangeEntry** - отменяет изменения для конкретной записи архива. +```php +$zipFile->unchangeEntry($entryName); +``` +#### Сохранение файла или вывод в браузер + **ZipFile::saveAsFile** - сохраняет архив в файл. +```php +$zipFile->saveAsFile($filename); +``` + **ZipFile::saveAsStream** - записывает архив в поток. +```php +// $fp = fopen($filename, 'w+b'); + +$zipFile->saveAsStream($fp); +``` + **ZipFile::outputAsString** - выводит ZIP-архив в виде строки. +```php +$rawZipArchiveBytes = $zipFile->outputAsString(); +``` + **ZipFile::outputAsAttachment** - выводит ZIP-архив в браузер. + +При выводе устанавливаются необходимые заголовки, а после вывода завершается работа скрипта. +```php +$zipFile->outputAsAttachment($outputFilename); +``` +Можно установить MIME-тип: +```php +$mimeType = 'application/zip' +$zipFile->outputAsAttachment($outputFilename, $mimeType); +``` + **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(); +``` + **ZipFile::rewrite** - сохраняет изменения и заново открывает изменившийся архив. +```php +$zipFile->rewrite(); +``` +#### Закрытие архива + **ZipFile::close** - закрывает ZIP-архив. +```php +$zipFile->close(); +``` +### Запуск тестов +Установите зависимости для разработки. +```bash +composer install --dev +``` +Запустите тесты: +```bash +vendor/bin/phpunit -v -c phpunit.xml +``` +### Обновление версий +#### Обновление с версии 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::removePasswordAllEntries` в `ZipFile::withoutPassword` + + `ZipOutputFile::setComment` в `ZipFile::setArchiveComment` + + `ZipFile::getComment` в `ZipFile::getArchiveComment` +- Изменились сигнатуры для методов `addDir`, `addFilesFromGlob`, `addFilesFromRegex`. +- Удалены методы: + + `getLevel` + + `setCompressionMethod` + + `setEntryPassword` diff --git a/README.md b/README.md index 5f7269a..500d852 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,228 @@ `PhpZip` ======== -`PhpZip` - php library for manipulating zip archives. +`PhpZip` is a php-library for extended work with ZIP-archives. [![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%2064bit-%3E%3D%205.5-8892BF.svg)](https://php.net/) -[![Test Coverage](https://codeclimate.com/github/Ne-Lexa/php-zip/badges/coverage.svg)](https://codeclimate.com/github/Ne-Lexa/php-zip/coverage) +[![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) +[Russian Documentation](README.RU.md) + Table of contents ----------------- - [Features](#Features) - [Requirements](#Requirements) - [Installation](#Installation) - [Examples](#Examples) +- [Glossary](#Glossary) - [Documentation](#Documentation) - + [Open Zip Archive](#Documentation-Open-Zip-Archive) - + [Get Zip Entries](#Documentation-Open-Zip-Entries) - + [Add Zip Entries](#Documentation-Add-Zip-Entries) - + [ZipAlign Usage](#Documentation-ZipAlign-Usage) - + [Save Zip File or Output](#Documentation-Save-Or-Output-Entries) - + [Close Zip Archive](#Documentation-Close-Zip-Archive) -- [Running Tests](#Running-Tests) -- [Upgrade version 2 to version 3](#Upgrade) + + [Overview of methods of the class `\PhpZip\ZipFile`](#Documentation-Overview) + + [Creation/Opening of ZIP-archive](#Documentation-Open-Zip-Archive) + + [Reading entries from the archive](#Documentation-Open-Zip-Entries) + + [Iterating entries](#Documentation-Zip-Iterate) + + [Getting information about entries](#Documentation-Zip-Info) + + [Adding entries to the archive](#Documentation-Add-Zip-Entries) + + [Deleting entries from the archive](#Documentation-Remove-Zip-Entries) + + [Working with entries and archive](#Documentation-Entries) + + [Working with passwords](#Documentation-Password) + + [zipalign - alignment tool for Android (APK) files](#Documentation-ZipAlign-Usage) + + [Undo changes](#Documentation-Unchanged) + + [Saving a file or output to a browser](#Documentation-Save-Or-Output-Entries) + + [Closing the archive](#Documentation-Close-Zip-Archive) +- [Running the tests](#Running-Tests) +- [Upgrade](#Upgrade) + + [Upgrade version 2 to version 3.0](#Upgrade-v2-to-v3) ### Features - Opening and unzipping zip files. -- Create zip files. -- Update zip files. +- Creating ZIP-archives. +- Modifying ZIP archives. - Pure php (not require extension `php-zip` and class `\ZipArchive`). -- Output the modified archive as a string or output to the browser without saving the result to disk. -- Support archive comment and entries comments. -- Get info of zip entries. -- Support zip password for PHP 5.5, include update and remove password. -- Support encryption method `Traditional PKWARE Encryption (ZipCrypto)` and `WinZIP AES Encryption`. -- Support `ZIP64` (size > 4 GiB or files > 65535 in a .ZIP archive). -- Support archive alignment functional [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html). +- It supports saving the archive to a file, outputting the archive to the browser, or outputting it as a string without saving it to a file. +- Archival comments and comments of individual entry are supported. +- Get information about each entry in the archive. +- Only the following compression methods are supported: + + No compressed (Stored). + + Deflate compression. + + BZIP2 compression with the extension `php-bz2`. +- Support for `ZIP64` (file size is more than 4 GB or the number of entries in the archive is more than 65535). +- Built-in support for aligning the archive to optimize Android packages (APK) [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html). +- Working with passwords for PHP 5.5 + + Set the password to read the archive for all entries or only for some. + + Change the password for the archive, including for individual entries. + + Delete the archive password for all or individual entries. + + Set the password and/or the encryption method, both for all, and for individual entries in the archive. + + Set different passwords and encryption methods for different entries. + + Delete the password for all or some entries. + + Support `Traditional PKWARE Encryption (ZipCrypto)` and `WinZIP AES Encryption` encryption methods. + + Set the encryption method for all or individual entries in the archive. ### Requirements -- `PHP` >= 5.5 (64 bit) +- `PHP` >= 5.5 (preferably 64-bit). - Optional php-extension `bzip2` for BZIP2 compression. - Optional php-extension `openssl` or `mcrypt` for `WinZip Aes Encryption` support. ### Installation -`composer require nelexa/zip:^3.0` +`composer require nelexa/zip` + +Latest stable version: [![Latest Stable Version](https://poser.pugx.org/nelexa/zip/v/stable)](https://packagist.org/packages/nelexa/zip) ### Examples ```php // create new archive $zipFile = new \PhpZip\ZipFile(); $zipFile - ->addFromString("zip/entry/filename", "Is file content") - ->addFile("/path/to/file", "data/tofile") - ->addDir(__DIR__, "to/path/") - ->saveAsFile($outputFilename) - ->close(); + ->addFromString("zip/entry/filename", "Is file content") // add an entry from the string + ->addFile("/path/to/file", "data/tofile") // add an entry from the file + ->addDir(__DIR__, "to/path/") // add files from the directory + ->saveAsFile($outputFilename) // save the archive to a file + ->close(); // close archive // open archive, extract, add files, set password and output to browser. $zipFile - ->openFile($outputFilename) - ->extractTo($outputDirExtract) + ->openFile($outputFilename) // open archive from file + ->extractTo($outputDirExtract) // extract files to the specified directory ->deleteFromRegex('~^\.~') // delete all hidden (Unix) files - ->addFromString('dir/file.txt', 'Test file') - ->withNewPassword('password') - ->outputAsAttachment('library.jar'); + ->addFromString('dir/file.txt', 'Test file') // add a new entry from the string + ->setPassword('password') // set password for all entries + ->outputAsAttachment('library.jar'); // output to the browser without saving to a file ``` Other examples can be found in the `tests/` folder +### Glossary +**Zip Entry** - file or folder in a ZIP-archive. Each entry in the archive has certain properties, for example: file name, compression method, encryption method, file size before compression, file size after compression, CRC32 and others. + ### Documentation: -#### Open Zip Archive -Open zip archive from file. +#### Overview of methods of the class `\PhpZip\ZipFile` +- [ZipFile::__construct](#Documentation-ZipFile-__construct) - initializes the ZIP archive. +- [ZipFile::addAll](#Documentation-ZipFile-addAll) - adds all entries from an array. +- [ZipFile::addDir](#Documentation-ZipFile-addDir) - adds files to the archive from the directory on the specified path without subdirectories. +- [ZipFile::addDirRecursive](#Documentation-ZipFile-addDirRecursive) - adds files to the archive from the directory on the specified path with subdirectories. +- [ZipFile::addEmptyDir](#Documentation-ZipFile-addEmptyDir) - add a new directory. +- [ZipFile::addFile](#Documentation-ZipFile-addFile) - adds a file to a ZIP archive from the given path. +- [ZipFile::addFilesFromIterator](#Documentation-ZipFile-addFilesFromIterator) - adds files from the iterator of directories. +- [ZipFile::addFilesFromGlob](#Documentation-ZipFile-addFilesFromGlob) - adds files from a directory by glob pattern without subdirectories. +- [ZipFile::addFilesFromGlobRecursive](#Documentation-ZipFile-addFilesFromGlobRecursive) - adds files from a directory by glob pattern with subdirectories. +- [ZipFile::addFilesFromRegex](#Documentation-ZipFile-addFilesFromRegex) - adds files from a directory by PCRE pattern without subdirectories. +- [ZipFile::addFilesFromRegexRecursive](#Documentation-ZipFile-addFilesFromRegexRecursive) - adds files from a directory by PCRE pattern with subdirectories. +- [ZipFile::addFromStream](#Documentation-ZipFile-addFromStream) - adds a entry from the stream to the ZIP archive. +- [ZipFile::addFromString](#Documentation-ZipFile-addFromString) - adds a file to a ZIP archive using its contents. +- [ZipFile::close](#Documentation-ZipFile-close) - close the archive. +- [ZipFile::count](#Documentation-ZipFile-count) - returns the number of entries in the archive. +- [ZipFile::deleteFromName](#Documentation-ZipFile-deleteFromName) - deletes an entry in the archive using its name. +- [ZipFile::deleteFromGlob](#Documentation-ZipFile-deleteFromGlob) - deletes a entries in the archive using glob pattern. +- [ZipFile::deleteFromRegex](#Documentation-ZipFile-deleteFromRegex) - deletes a entries in the archive using PCRE pattern. +- [ZipFile::deleteAll](#Documentation-ZipFile-deleteAll) - deletes all entries in the ZIP archive. +- [ZipFile::extractTo](#Documentation-ZipFile-extractTo) - extract the archive contents. +- [ZipFile::getAllInfo](#Documentation-ZipFile-getAllInfo) - returns detailed information about all entries in the archive. +- [ZipFile::getArchiveComment](#Documentation-ZipFile-getArchiveComment) - returns the Zip archive comment. +- [ZipFile::getEntryComment](#Documentation-ZipFile-getEntryComment) - returns the comment of an entry using the entry name. +- [ZipFile::getEntryContent](#Documentation-ZipFile-getEntryContent) - returns the entry contents using its name. +- [ZipFile::getEntryInfo](#Documentation-ZipFile-getEntryInfo) - returns detailed information about the entry in the archive. +- [ZipFile::getListFiles](#Documentation-ZipFile-getListFiles) - returns list of archive files. +- [ZipFile::hasEntry](#Documentation-ZipFile-hasEntry) - checks if there is an entry in the archive. +- [ZipFile::isDirectory](#Documentation-ZipFile-isDirectory) - checks that the entry in the archive is a directory. +- [ZipFile::matcher](#Documentation-ZipFile-matcher) - selecting entries in the archive to perform operations on them. +- [ZipFile::openFile](#Documentation-ZipFile-openFile) - opens a zip-archive from a file. +- [ZipFile::openFromString](#Documentation-ZipFile-openFromString) - opens a zip-archive from a string. +- [ZipFile::openFromStream](#Documentation-ZipFile-openFromStream) - opens a zip-archive from the stream. +- [ZipFile::outputAsAttachment](#Documentation-ZipFile-outputAsAttachment) - outputs a ZIP-archive to the browser. +- [ZipFile::outputAsResponse](#Documentation-ZipFile-outputAsResponse) - outputs a ZIP-archive as PSR-7 Response. +- [ZipFile::outputAsString](#Documentation-ZipFile-outputAsString) - outputs a ZIP-archive as string. +- [ZipFile::removePassword](#Documentation-ZipFile-removePassword) - removes the password from all files in the archive. +- [ZipFile::removePasswordEntry](#Documentation-ZipFile-removePasswordEntry) - removes password from one entry in the archive. +- [ZipFile::rename](#Documentation-ZipFile-rename) - renames an entry defined by its name. +- [ZipFile::rewrite](#Documentation-ZipFile-rewrite) - save changes and re-open the changed archive. +- [ZipFile::saveAsFile](#Documentation-ZipFile-saveAsFile) - saves the archive to a file. +- [ZipFile::saveAsStream](#Documentation-ZipFile-saveAsStream) - writes the archive to the stream. +- [ZipFile::setArchiveComment](#Documentation-ZipFile-setArchiveComment) - set the comment of a ZIP archive. +- [ZipFile::setCompressionLevel](#Documentation-ZipFile-setCompressionLevel) - set the compression level for all files in the archive. +- [ZipFile::setCompressionLevelEntry](#Documentation-ZipFile-setCompressionLevelEntry) - sets the compression level for the entry by its name. +- [ZipFile::setCompressionMethodEntry](#Documentation-ZipFile-setCompressionMethodEntry) - sets the compression method for the entry by its name. +- [ZipFile::setEntryComment](#Documentation-ZipFile-setEntryComment) - set the comment of an entry defined by its name. +- [ZipFile::setReadPassword](#Documentation-ZipFile-setReadPassword) - set the password for the open archive. +- [ZipFile::setReadPasswordEntry](#Documentation-ZipFile-setReadPasswordEntry) - sets a password for reading of an entry defined by its name. +- ~~ZipFile::withNewPassword~~ - is an deprecated method, use the [ZipFile::setPassword](#Documentation-ZipFile-setPassword) method. +- [ZipFile::setPassword](#Documentation-ZipFile-setPassword) - sets a new password for all files in the archive. +- [ZipFile::setPasswordEntry](#Documentation-ZipFile-setPasswordEntry) - sets a new password of an entry defined by its name. +- [ZipFile::setZipAlign](#Documentation-ZipFile-setZipAlign) - sets the alignment of the archive to optimize APK files (Android packages). +- [ZipFile::unchangeAll](#Documentation-ZipFile-unchangeAll) - undo all changes done in the archive. +- [ZipFile::unchangeArchiveComment](#Documentation-ZipFile-unchangeArchiveComment) - undo changes to the archive comment. +- [ZipFile::unchangeEntry](#Documentation-ZipFile-unchangeEntry) - undo changes of an entry defined by its name. +- ~~ZipFile::withoutPassword~~ - is an deprecated method, use the [ZipFile::removePassword](#Documentation-ZipFile-removePassword) method. +- ~~ZipFile::withReadPassword~~ - is an deprecated method, use the [ZipFile::setReadPassword](#Documentation-ZipFile-setReadPassword) method. + +#### Creation/Opening of ZIP-archive +**ZipFile::__construct** - initializes the ZIP archive. ```php $zipFile = new \PhpZip\ZipFile(); -$zipFile->openFile($filename); ``` -Open zip archive from data string. + **ZipFile::openFile** - opens a zip-archive from a file. +```php +$zipFile = new \PhpZip\ZipFile(); +$zipFile->openFile('file.zip'); +``` + **ZipFile::openFromString** - opens a zip-archive from a string. ```php $zipFile = new \PhpZip\ZipFile(); $zipFile->openFromString($stringContents); ``` -Open zip archive from stream resource. + **ZipFile::openFromStream** - opens a zip-archive from the stream. ```php -$stream = fopen($filename, 'rb'); +$stream = fopen('file.zip', 'rb'); $zipFile = new \PhpZip\ZipFile(); $zipFile->openFromStream($stream); ``` -#### Get Zip Entries -Get num entries. +#### Reading entries from the archive + **ZipFile::count** - returns the number of entries in the archive. ```php $count = count($zipFile); // or $count = $zipFile->count(); ``` -Get list files. + **ZipFile::getListFiles** - returns list of archive files. ```php $listFiles = $zipFile->getListFiles(); -// Example result: -// -// $listFiles = [ -// 'info.txt', -// 'path/to/file.jpg', -// 'another path/' -// ]; +// example array contents: +// array ( +// 0 => 'info.txt', +// 1 => 'path/to/file.jpg', +// 2 => 'another path/', +// ) ``` -Get entry contents. + **ZipFile::getEntryContent** - returns the entry contents using its name. ```php // $entryName = 'path/to/example-entry-name.txt'; $contents = $zipFile[$entryName]; +// or +$contents = $zipFile->getEntryContents($entryName); ``` -Checks whether a entry exists. + **ZipFile::hasEntry** - checks if there is an entry in the archive. ```php // $entryName = 'path/to/example-entry-name.txt'; $hasEntry = isset($zipFile[$entryName]); +// or +$hasEntry = $zipFile->hasEntry($entryName); ``` -Check whether the directory entry. + **ZipFile::isDirectory** - checks that the entry in the archive is a directory. ```php // $entryName = 'path/to/'; $isDirectory = $zipFile->isDirectory($entryName); ``` -Extract all files to directory. + **ZipFile::extractTo** - extract the archive contents. +The directory must exist. ```php $zipFile->extractTo($directory); ``` -Extract some files to directory. +Extract some files to the directory. +The directory must exist. ```php $extractOnlyFiles = [ "filename1", @@ -137,77 +231,97 @@ $extractOnlyFiles = [ ]; $zipFile->extractTo($directory, $extractOnlyFiles); ``` -Iterate zip entries. +#### Iterating entries +`ZipFile` is an iterator. +Can iterate all the entries in the `foreach` loop. ```php -foreach($zipFile as $entryName => $dataContent){ - echo "Entry: $entryName" . PHP_EOL; - echo "Data: $dataContent" . PHP_EOL; +foreach($zipFile as $entryName => $contents){ + echo "Filename: $entryName" . PHP_EOL; + echo "Contents: $contents" . PHP_EOL; echo "-----------------------------" . PHP_EOL; } ``` -or +Can iterate through the `Iterator`. ```php $iterator = new \ArrayIterator($zipFile); while ($iterator->valid()) { $entryName = $iterator->key(); - $dataContent = $iterator->current(); + $contents = $iterator->current(); - echo "Entry: $entryName" . PHP_EOL; - echo "Data: $dataContent" . PHP_EOL; + echo "Filename: $entryName" . PHP_EOL; + echo "Contents: $contents" . PHP_EOL; echo "-----------------------------" . PHP_EOL; $iterator->next(); } ``` -Get comment archive. +#### Getting information about entries + **ZipFile::getArchiveComment** - returns the Zip archive comment. ```php $commentArchive = $zipFile->getArchiveComment(); ``` -Get comment zip entry. + **ZipFile::getEntryComment** - returns the comment of an entry using the entry name. ```php $commentEntry = $zipFile->getEntryComment($entryName); ``` -Set password for read encrypted entries. -```php -$zipFile->withReadPassword($password); -``` -Get entry info. + **ZipFile::getEntryInfo** - returns detailed information about the entry in the archive ```php $zipInfo = $zipFile->getEntryInfo('file.txt'); -echo $zipInfo . PHP_EOL; - -// Output: -// ZipInfo {Path="file.txt", Size=9.77KB, Compressed size=2.04KB, Modified time=2016-09-24T19:25:10+03:00, Crc=0x4b5ab5c7, Method="Deflate", Attributes="-rw-r--r--", Platform="UNIX", Version=20} +$arrayInfo = $zipInfo->toArray(); +// example array contents: +// 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); +// output: +//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; // Output: -// PhpZip\Model\ZipInfo Object -// ( -// [path:PhpZip\Model\ZipInfo:private] => file.txt -// [folder:PhpZip\Model\ZipInfo:private] => -// [size:PhpZip\Model\ZipInfo:private] => 10000 -// [compressedSize:PhpZip\Model\ZipInfo:private] => 2086 -// [mtime:PhpZip\Model\ZipInfo:private] => 1474734310 -// [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] => 1264235975 -// [method:PhpZip\Model\ZipInfo:private] => Deflate -// [platform:PhpZip\Model\ZipInfo:private] => UNIX -// [version:PhpZip\Model\ZipInfo:private] => 20 -// [attributes:PhpZip\Model\ZipInfo:private] => -rw-r--r-- -// ) +// 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} ``` -Get info for all entries. + **ZipFile::getAllInfo** - returns detailed information about all entries in the archive. ```php $zipAllInfo = $zipFile->getAllInfo(); print_r($zipAllInfo); - //Array //( // [file.txt] => PhpZip\Model\ZipInfo Object @@ -222,295 +336,447 @@ print_r($zipAllInfo); // // ... //) - ``` -#### Add Zip Entries -Adding a file to the zip-archive. +#### Adding entries to the archive + +All methods of adding entries to a ZIP archive allow you to specify a method for compressing content. + +The following methods of compression are available: +- `\PhpZip\ZipFile::METHOD_STORED` - no compression +- `\PhpZip\ZipFile::METHOD_DEFLATED` - Deflate compression +- `\PhpZip\ZipFile::METHOD_BZIP2` - Bzip2 compression with the extension `ext-bz2` + + **ZipFile::addFile** - adds a file to a ZIP archive from the given path. ```php -// entry name is file basename. -$zipFile->addFile($filename); -// or -$zipFile->addFile($filename, null); +// $file = '...../file.ext'; +$zipFile->addFile($file); -// with entry name -$zipFile->addFile($filename, $entryName); +// you can specify the name of the entry in the archive (if null, then the last component from the file name is used) +$zipFile->addFile($file, $entryName); // or -$zipFile[$entryName] = new \SplFileInfo($filename); +$zipFile[$entryName] = new \SplFileInfo($file); -// with compression method -$zipFile->addFile($filename, $entryName, ZipFile::METHOD_DEFLATED); // Deflate compression -$zipFile->addFile($filename, $entryName, ZipFile::METHOD_STORED); // No compression -$zipFile->addFile($filename, null, ZipFile::METHOD_BZIP2); // BZIP2 compression +// you can specify a compression method +$zipFile->addFile($file, $entryName, ZipFile::METHOD_STORED); // No compression +$zipFile->addFile($file, $entryName, ZipFile::METHOD_DEFLATED); // Deflate compression +$zipFile->addFile($file, $entryName, ZipFile::METHOD_BZIP2); // BZIP2 compression ``` -Add entry from string data. + **ZipFile::addFromString** - adds a file to a ZIP archive using its contents. ```php -$zipFile[$entryName] = $data; +$zipFile[$entryName] = $contents; // or -$zipFile->addFromString($entryName, $data); +$zipFile->addFromString($entryName, $contents); -// with compression method -$zipFile->addFromString($entryName, $data, ZipFile::METHOD_DEFLATED); // Deflate compression -$zipFile->addFromString($entryName, $data, ZipFile::METHOD_STORED); // No compression -$zipFile->addFromString($entryName, $data, ZipFile::METHOD_BZIP2); // BZIP2 compression +// you can specify a compression method +$zipFile->addFromString($entryName, $contents, ZipFile::METHOD_STORED); // No compression +$zipFile->addFromString($entryName, $contents, ZipFile::METHOD_DEFLATED); // Deflate compression +$zipFile->addFromString($entryName, $contents, ZipFile::METHOD_BZIP2); // BZIP2 compression ``` -Add entry from stream. + **ZipFile::addFromStream** - adds a entry from the stream to the ZIP archive. ```php -// $stream = fopen(...); +// $stream = fopen(..., 'rb'); $zipFile->addFromStream($stream, $entryName); +// or +$zipFile[$entryName] = $stream; -// with compression method -$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_DEFLATED); // Deflate compression +// you can specify a compression method $zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_STORED); // No compression +$zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_DEFLATED); // Deflate compression $zipFile->addFromStream($stream, $entryName, ZipFile::METHOD_BZIP2); // BZIP2 compression ``` -Add empty dir + **ZipFile::addEmptyDir** - add a new directory. ```php -// $dirName = "path/to/"; +// $path = "path/to/"; -$zipFile->addEmptyDir($dirName); +$zipFile->addEmptyDir($path); // or -$zipFile[$dirName] = null; +$zipFile[$path] = null; ``` -Add all entries form string contents. + **ZipFile::addAll** - adds all entries from an array. ```php -$mapData = [ - 'file.txt' => 'file contents', - 'path/to/file.txt' => 'another file contents', - 'empty dir/' => null, +$entries = [ + 'file.txt' => 'file contents', // add an entry from the string contents + 'empty dir/' => null, // add empty directory + 'path/to/file.jpg' => fopen('..../filename', 'r'), // add an entry from the stream + 'path/to/file.dat' => new \SplFileInfo('..../filename'), // add an entry from the file ]; -$zipFile->addAll($mapData); +$zipFile->addAll($entries); ``` -Add a directory **not recursively** to the archive. + **ZipFile::addDir** - adds files to the archive from the directory on the specified path without subdirectories. ```php $zipFile->addDir($dirName); -// with entry path +// you can specify the path in the archive to which you want to put entries $localPath = "to/path/"; $zipFile->addDir($dirName, $localPath); -// with compression method for all files -$zipFile->addDir($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression +// you can specify a compression method $zipFile->addDir($dirName, $localPath, ZipFile::METHOD_STORED); // No compression +$zipFile->addDir($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression $zipFile->addDir($dirName, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression ``` -Add a directory **recursively** to the archive. + **ZipFile::addDirRecursive** - adds files to the archive from the directory on the specified path with subdirectories. ```php $zipFile->addDirRecursive($dirName); -// with entry path +// you can specify the path in the archive to which you want to put entries $localPath = "to/path/"; $zipFile->addDirRecursive($dirName, $localPath); -// with compression method for all files -$zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression +// you can specify a compression method $zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_STORED); // No compression +$zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression $zipFile->addDirRecursive($dirName, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression ``` -Add a files from directory iterator. + **ZipFile::addFilesFromIterator** - adds files from the iterator of directories. ```php -// $directoryIterator = new \DirectoryIterator($dir); // not recursive -// $directoryIterator = new \RecursiveDirectoryIterator($dir); // recursive +// $directoryIterator = new \DirectoryIterator($dir); // without subdirectories +// $directoryIterator = new \RecursiveDirectoryIterator($dir); // with subdirectories $zipFile->addFilesFromIterator($directoryIterator); -// with entry path +// you can specify the path in the archive to which you want to put entries $localPath = "to/path/"; $zipFile->addFilesFromIterator($directoryIterator, $localPath); // or $zipFile[$localPath] = $directoryIterator; -// with compression method for all files -$zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression +// you can specify a compression method $zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_STORED); // No compression +$zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression $zipFile->addFilesFromIterator($directoryIterator, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression ``` -Example add a directory to the archive with ignoring files from directory iterator. +Example with some files ignoring: ```php $ignoreFiles = [ "file_ignore.txt", "dir_ignore/sub dir ignore/" ]; -// use \DirectoryIterator for not recursive -$directoryIterator = new \RecursiveDirectoryIterator($dir); +// $directoryIterator = new \DirectoryIterator($dir); // without subdirectories +// $directoryIterator = new \RecursiveDirectoryIterator($dir); // with subdirectories -// use IgnoreFilesFilterIterator for not recursive -$ignoreIterator = new IgnoreFilesRecursiveFilterIterator( +// use \PhpZip\Util\Iterator\IgnoreFilesFilterIterator for non-recursive search +$ignoreIterator = new \PhpZip\Util\Iterator\IgnoreFilesRecursiveFilterIterator( $directoryIterator, $ignoreFiles ); $zipFile->addFilesFromIterator($ignoreIterator); ``` -Add a files **recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive. -```php -$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files - -$zipFile->addFilesFromGlobRecursive($dir, $globPattern); - -// with entry path -$localPath = "to/path/"; -$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath); - -// with compression method for all files -$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath), ZipFile::METHOD_DEFLATED); // Deflate compression -$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath), ZipFile::METHOD_STORED); // No compression -$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath), ZipFile::METHOD_BZIP2); // BZIP2 compression -``` -Add a files **not recursively** from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) to the archive. + **ZipFile::addFilesFromGlob** - adds files from a directory by [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) without subdirectories. ```php $globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files $zipFile->addFilesFromGlob($dir, $globPattern); -// with entry path +// you can specify the path in the archive to which you want to put entries $localPath = "to/path/"; $zipFile->addFilesFromGlob($dir, $globPattern, $localPath); -// with compression method for all files -$zipFile->addFilesFromGlob($dir, $globPattern, $localPath), ZipFile::METHOD_DEFLATED); // Deflate compression -$zipFile->addFilesFromGlob($dir, $globPattern, $localPath), ZipFile::METHOD_STORED); // No compression -$zipFile->addFilesFromGlob($dir, $globPattern, $localPath), ZipFile::METHOD_BZIP2); // BZIP2 compression +// you can specify a compression method +$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, ZipFile::METHOD_STORED); // No compression +$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression +$zipFile->addFilesFromGlob($dir, $globPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression ``` -Add a files **recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive. + **ZipFile::addFilesFromGlobRecursive** - adds files from a directory by [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) with subdirectories. ```php -$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> add all .jpg, .jpeg, .png and .gif files +$globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> add all .jpg, .jpeg, .png and .gif files -$zipFile->addFilesFromRegexRecursive($dir, $regexPattern); +$zipFile->addFilesFromGlobRecursive($dir, $globPattern); -// with entry path +// you can specify the path in the archive to which you want to put entries $localPath = "to/path/"; -$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath); +$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath); -// with compression method for all files -$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression -$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_STORED); // No compression -$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression +// you can specify a compression method +$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, ZipFile::METHOD_STORED); // No compression +$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression +$zipFile->addFilesFromGlobRecursive($dir, $globPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression ``` -Add a files **not recursively** from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression) to the archive. + **ZipFile::addFilesFromRegex** - adds files from a directory by [PCRE pattern](https://en.wikipedia.org/wiki/Regular_expression) without subdirectories. ```php $regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> add all .jpg, .jpeg, .png and .gif files $zipFile->addFilesFromRegex($dir, $regexPattern); -// with entry path +// you can specify the path in the archive to which you want to put entries $localPath = "to/path/"; $zipFile->addFilesFromRegex($dir, $regexPattern, $localPath); -// with compression method for all files -$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression +// you can specify a compression method $zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_STORED); // No compression +$zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression $zipFile->addFilesFromRegex($dir, $regexPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression ``` -Rename entry name. + **ZipFile::addFilesFromRegexRecursive** - adds files from a directory by [PCRE pattern](https://en.wikipedia.org/wiki/Regular_expression) with subdirectories. ```php -$zipFile->rename($oldName, $newName); +$regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> add all .jpg, .jpeg, .png and .gif files + +$zipFile->addFilesFromRegexRecursive($dir, $regexPattern); + +// you can specify the path in the archive to which you want to put entries +$localPath = "to/path/"; +$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath); + +// you can specify a compression method +$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_STORED); // No compression +$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_DEFLATED); // Deflate compression +$zipFile->addFilesFromRegexRecursive($dir, $regexPattern, $localPath, ZipFile::METHOD_BZIP2); // BZIP2 compression ``` -Delete entry by name. +#### Deleting entries from the archive + **ZipFile::deleteFromName** - deletes an entry in the archive using its name. ```php $zipFile->deleteFromName($entryName); ``` -Delete entries from [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)). + **ZipFile::deleteFromGlob** - deletes a entries in the archive using [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)). ```php $globPattern = '**.{jpg,jpeg,png,gif}'; // example glob pattern -> delete all .jpg, .jpeg, .png and .gif files $zipFile->deleteFromGlob($globPattern); ``` -Delete entries from [RegEx (Regular Expression) pattern](https://en.wikipedia.org/wiki/Regular_expression). + **ZipFile::deleteFromRegex** - deletes a entries in the archive using [PCRE pattern](https://en.wikipedia.org/wiki/Regular_expression). ```php $regexPattern = '/\.(jpe?g|png|gif)$/si'; // example regex pattern -> delete all .jpg, .jpeg, .png and .gif files $zipFile->deleteFromRegex($regexPattern); ``` -Delete all entries. + **ZipFile::deleteAll** - deletes all entries in the ZIP archive. ```php $zipFile->deleteAll(); ``` -Sets the compression level for entries. +#### Working with entries and archive + **ZipFile::rename** - renames an entry defined by its name. ```php -// This property is only used if the effective compression method is DEFLATED or BZIP2. -// Legal values are ZipFile::LEVEL_DEFAULT_COMPRESSION or range from -// ZipFile::LEVEL_BEST_SPEED to ZipFile::LEVEL_BEST_COMPRESSION. - -$compressionMethod = ZipFile::LEVEL_BEST_COMPRESSION; - -$zipFile->setCompressionLevel($compressionLevel); +$zipFile->rename($oldName, $newName); ``` -Set comment archive. + **ZipFile::setCompressionLevel** - set the compression level for all files in the archive. + +> _Note that this method does not apply to entries that were added after this method was run._ + +By default, the compression level is -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) or the compression level specified in the archive for Deflate compression. + +The values -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) and the range from 1 (`\PhpZip\ZipFile::LEVEL_BEST_SPEED`) to 9 (`\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION`) are supported. The higher the number, the better and longer the compression. +```php +$zipFile->setCompressionLevel(\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION); +``` + **ZipFile::setCompressionLevelEntry** - sets the compression level for the entry by its name. + +The values -1 (`\PhpZip\ZipFile::LEVEL_DEFAULT_COMPRESSION`) and the range from 1 (`\PhpZip\ZipFile::LEVEL_BEST_SPEED`) to 9 (`\PhpZip\ZipFile::LEVEL_BEST_COMPRESSION`) are supported. The higher the number, the better and longer the compression. +```php +$zipFile->setCompressionLevelEntry($entryName, \PhpZip\ZipFile::LEVEL_BEST_COMPRESSION); +``` + **ZipFile::setCompressionMethodEntry** - sets the compression method for the entry by its name. + +The following compression methods are available: +- `\PhpZip\ZipFile::METHOD_STORED` - No compression +- `\PhpZip\ZipFile::METHOD_DEFLATED` - Deflate compression +- `\PhpZip\ZipFile::METHOD_BZIP2` - Bzip2 compression with the extension `ext-bz2` +```php +$zipFile->setCompressionMethodEntry($entryName, ZipFile::METHOD_DEFLATED); +``` + **ZipFile::setArchiveComment** - set the comment of a ZIP archive. ```php $zipFile->setArchiveComment($commentArchive); ``` -Set comment zip entry. + **ZipFile::setEntryComment** - set the comment of an entry defined by its name. ```php -$zipFile->setEntryComment($entryName, $entryComment); +$zipFile->setEntryComment($entryName, $comment); ``` -Set a new password. + - selecting entries in the archive to perform operations on them. ```php -$zipFile->withNewPassword($password); +$matcher = $zipFile->matcher(); ``` -Set a new password and encryption method. +Selecting files from the archive one at a time: ```php -$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES; // default value -$zipFile->withNewPassword($password, $encryptionMethod); +$matcher + ->add('entry name') + ->add('another entry'); +``` +Select multiple files in the archive: +```php +$matcher->add([ + 'entry name', + 'another entry name', + 'path/' +]); +``` +Selecting files by regular expression: +```php +$matcher->match('~\.jpe?g$~i'); +``` +Select all files in the archive: +```php +$matcher->all(); +``` +count() - gets the number of selected entries: +```php +$count = count($matcher); +// or +$count = $matcher->count(); +``` +getMatches() - returns a list of selected entries: +```php +$entries = $matcher->getMatches(); +// example array contents: ['entry name', 'another entry name']; +``` +invoke() - invoke a callable function on selected entries: +```php +// example +$matcher->invoke(function($entryName) use($zipFile) { + $newName = preg_replace('~\.(jpe?g)$~i', '.no_optimize.$1', $entryName); + $zipFile->rename($entryName, $newName); +}); +``` +Functions for working on the selected entries: +```php +$matcher->delete(); // remove selected entries from a ZIP archive +$matcher->setPassword($password); // sets a new password for the selected entries +$matcher->setPassword($password, $encryptionMethod); // sets a new password and encryption method to selected entries +$matcher->setEncryptionMethod($encryptionMethod); // sets the encryption method to the selected entries +$matcher->disableEncryption(); // disables encryption for selected entries +``` +#### Working with passwords -// Support encryption methods: -// ZipFile::ENCRYPTION_METHOD_TRADITIONAL - Traditional PKWARE Encryption -// ZipFile::ENCRYPTION_METHOD_WINZIP_AES - WinZip AES Encryption -``` -Remove password from all entries. +Implemented support for encryption methods: +- `\PhpZip\ZipFile::ENCRYPTION_METHOD_TRADITIONAL` - Traditional PKWARE encryption +- `\PhpZip\ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256` - WinZip AES encryption 256 bit (recommended) +- `\PhpZip\ZipFile::ENCRYPTION_METHOD_WINZIP_AES_192` - WinZip AES encryption 192 bit +- `\PhpZip\ZipFile::ENCRYPTION_METHOD_WINZIP_AES_128` - WinZip AES encryption 128 bit + + **ZipFile::setReadPassword** - set the password for the open archive. + +> _Setting a password is not required for adding new entries or deleting existing ones, but if you want to extract the content or change the method / compression level, the encryption method, or change the password, in this case the password must be specified._ ```php -$zipFile->withoutPassword(); +$zipFile->setReadPassword($password); ``` -#### ZipAlign Usage -Set archive alignment ([`zipalign`](https://developer.android.com/studio/command-line/zipalign.html)). + **ZipFile::setReadPasswordEntry** - gets a password for reading of an entry defined by its name. ```php -// before save or output -$zipFile->setAlign(4); // alternative command: zipalign -f -v 4 filename.zip +$zipFile->setReadPasswordEntry($entryName, $password); ``` -#### Save Zip File or Output -Save archive to a file. + **ZipFile::setPassword** - sets a new password for all files in the archive. + +> _Note that this method does not apply to entries that were added after this method was run._ +```php +$zipFile->setPassword($password); +``` +You can set the encryption method: +```php +$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; +$zipFile->setPassword($password, $encryptionMethod); +``` + **ZipFile::setPasswordEntry** - sets a new password of an entry defined by its name. +```php +$zipFile->setPasswordEntry($entryName, $password); +``` +You can set the encryption method: +```php +$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; +$zipFile->setPasswordEntry($entryName, $password, $encryptionMethod); +``` + **ZipFile::removePassword** - removes the password from all files in the archive. + +> _Note that this method does not apply to entries that were added after this method was run._ +```php +$zipFile->removePassword(); +``` + **ZipFile::removePasswordEntry** - removes password from one entry in the archive. +```php +$zipFile->removePasswordEntry($entryName); +``` +#### zipalign + **ZipFile::setZipAlign** - sets the alignment of the archive to optimize APK files (Android packages). + +This method adds padding to unencrypted and not compressed entries, to optimize memory consumption in the Android system. It is recommended to use for `APK` files. The file may grow slightly. + +This method is an alternative to executing the `zipalign -f -v 4 filename.zip`. + +More details can be found on the [link](https://developer.android.com/studio/command-line/zipalign.html). +```php +$zipFile->setZipAlign(4); +``` +#### Undo changes + **ZipFile::unchangeAll** - undo all changes done in the archive. +```php +$zipFile->unchangeAll(); +``` + **ZipFile::unchangeArchiveComment** - undo changes to the archive comment. +```php +$zipFile->unchangeArchiveComment(); +``` + **ZipFile::unchangeEntry** - undo changes of an entry defined by its name. +```php +$zipFile->unchangeEntry($entryName); +``` +#### Saving a file or output to a browser + **ZipFile::saveAsFile** - saves the archive to a file. ```php $zipFile->saveAsFile($filename); ``` -Save archive to a stream. + **ZipFile::saveAsStream** - writes the archive to the stream. ```php // $fp = fopen($filename, 'w+b'); $zipFile->saveAsStream($fp); ``` -Returns the zip archive as a string. + **ZipFile::outputAsString** - outputs a ZIP-archive as string. ```php $rawZipArchiveBytes = $zipFile->outputAsString(); ``` -Output .ZIP archive as attachment and terminate. + **ZipFile::outputAsAttachment** - outputs a ZIP-archive to the browser. ```php $zipFile->outputAsAttachment($outputFilename); -// or set mime type +``` +You can set the Mime-Type: +```php $mimeType = 'application/zip' $zipFile->outputAsAttachment($outputFilename, $mimeType); ``` -Rewrite and reopen zip archive. + **ZipFile::outputAsResponse** - outputs a ZIP-archive as [PSR-7 Response](http://www.php-fig.org/psr/psr-7/). + +The output method can be used in any PSR-7 compatible framework. +```php +// $response = ....; // instance Psr\Http\Message\ResponseInterface +$zipFile->outputAsResponse($response, $outputFilename); +``` +You can set the Mime-Type: +```php +$mimeType = 'application/zip' +$zipFile->outputAsResponse($response, $outputFilename, $mimeType); +``` +An example for the 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(); +``` + **ZipFile::rewrite** - save changes and re-open the changed archive. ```php $zipFile->rewrite(); ``` -#### Close Zip Archive -Close zip archive. +#### Closing the archive + **ZipFile::close** - close the archive. ```php $zipFile->close(); ``` -### Running Tests -Installing development dependencies. +### Running the tests +Install the dependencies for the development: ```bash composer install --dev ``` -Run tests +Run the tests: ```bash -vendor/bin/phpunit -v -c bootstrap.xml +vendor/bin/phpunit -v -c phpunit.xml ``` -### Upgrade version 2 to version 3 -Update to the New Major Version via Composer +### Upgrade +#### Upgrade version 2 to version 3.0 +Update the major version in the file `composer.json` to `^3.0`. ```json { "require": { @@ -518,11 +784,11 @@ Update to the New Major Version via Composer } } ``` -Next, use Composer to download new versions of the libraries: +Then install updates using `Composer`: ```bash composer update nelexa/zip ``` -Update your Code to Work with the New Version: +Update your code to work with the new version: - Class `ZipOutputFile` merged to `ZipFile` and removed. + `new \PhpZip\ZipOutputFile()` to `new \PhpZip\ZipFile()` - Static initialization methods are now not static. @@ -541,7 +807,7 @@ Update your Code to Work with the New Version: + `ZipOutputFile::setComment` to `ZipFile::setArchiveComment` + `ZipFile::getComment` to `ZipFile::getArchiveComment` - Changed signature for methods `addDir`, `addFilesFromGlob`, `addFilesFromRegex`. -- Remove methods +- Remove methods: + `getLevel` + `setCompressionMethod` + `setEntryPassword` diff --git a/composer.json b/composer.json index d5e6dc6..7a55c83 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "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.", + "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.", "type": "library", "keywords": [ "zip", @@ -8,11 +8,11 @@ "archive", "extract", "winzip", - "zipalign" + "zipalign", + "ziparchive" ], "require-dev": { - "phpunit/phpunit": "4.8", - "codeclimate/php-test-reporter": "^0.4.4" + "phpunit/phpunit": "4.8" }, "license": "MIT", "authors": [ @@ -24,7 +24,8 @@ ], "minimum-stability": "stable", "require": { - "php": "^5.5 || ^7.0" + "php": "^5.5 || ^7.0", + "psr/http-message": "^1.0" }, "autoload": { "psr-4": { diff --git a/bootstrap.xml b/phpunit.xml similarity index 100% rename from bootstrap.xml rename to phpunit.xml diff --git a/src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php b/src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php index e05a87c..961ac1f 100644 --- a/src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php +++ b/src/PhpZip/Crypto/TraditionalPkwareEncryptionEngine.php @@ -1,10 +1,12 @@ 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[1] = $this->keys[1] + ($this->keys[0] & 0xff); - $this->keys[1] = self::toInt($this->keys[1] * 134775813 + 1); - $this->keys[2] = self::toInt(self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff)); - } - - /** - * Cast to int - * - * @param $i - * @return int - */ - private static function toInt($i) - { - $i = (int)($i & 0xffffffff); - if ($i > 2147483647) { - return -(-$i & 0xffffffff); - } elseif ($i < -2147483648) { - return $i & -2147483648; - } - return $i; + $this->keys[1] = PackUtil::toSignedInt32($this->keys[1] * 134775813 + 1); + $this->keys[2] = PackUtil::toSignedInt32(self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff)); } /** @@ -147,7 +130,11 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine */ public function decrypt($content) { + $password = $this->entry->getPassword(); + $this->initKeys($password); + $headerBytes = array_values(unpack('C*', substr($content, 0, self::STD_DEC_HDR_SIZE))); + $byte = 0; foreach ($headerBytes as &$byte) { $byte = ($byte ^ $this->decryptByte()) & 0xff; $this->updateKeys($byte); @@ -198,7 +185,9 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine $headerBytes = CryptoUtil::randomBytes(self::STD_DEC_HDR_SIZE); // 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 - 2] = pack('c', ($crc >> 16) & 0xff); @@ -233,4 +222,4 @@ class TraditionalPkwareEncryptionEngine implements CryptoEngine $this->updateKeys($byte); return $tempVal; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Crypto/WinZipAesEngine.php b/src/PhpZip/Crypto/WinZipAesEngine.php index 876171a..04fd808 100644 --- a/src/PhpZip/Crypto/WinZipAesEngine.php +++ b/src/PhpZip/Crypto/WinZipAesEngine.php @@ -1,20 +1,22 @@ entry->getExtraFieldsCollection(); + + if (!isset($extraFieldsCollection[WinZipAesEntryExtraField::getHeaderId()])) { + throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)"); + } + /** * @var WinZipAesEntryExtraField $field */ - $field = $this->entry->getExtraField(WinZipAesEntryExtraField::getHeaderId()); - if (null === $field) { - throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)"); - } + $field = $extraFieldsCollection[WinZipAesEntryExtraField::getHeaderId()]; // Get key strength. $keyStrengthBits = $field->getKeyStrength(); @@ -218,8 +223,8 @@ class WinZipAesEngine implements CryptoEngine // @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/ $password = substr($password, 0, 99); - $keyStrengthBytes = 32; - $keyStrengthBits = $keyStrengthBytes * 8; + $keyStrengthBits = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod($this->entry->getEncryptionMethod()); + $keyStrengthBytes = $keyStrengthBits / 8; assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits); @@ -244,4 +249,4 @@ class WinZipAesEngine implements CryptoEngine substr($mac, 0, 10) ); } -} \ No newline at end of file +} diff --git a/src/PhpZip/Crypto/CryptoEngine.php b/src/PhpZip/Crypto/ZipEncryptionEngine.php similarity index 66% rename from src/PhpZip/Crypto/CryptoEngine.php rename to src/PhpZip/Crypto/ZipEncryptionEngine.php index 32d5b96..3187969 100644 --- a/src/PhpZip/Crypto/CryptoEngine.php +++ b/src/PhpZip/Crypto/ZipEncryptionEngine.php @@ -1,9 +1,17 @@ expectedCrc = $expected; $this->actualCrc = $actual; @@ -66,5 +63,4 @@ class Crc32Exception extends ZipException { return $this->actualCrc; } - -} \ No newline at end of file +} diff --git a/src/PhpZip/Exception/InvalidArgumentException.php b/src/PhpZip/Exception/InvalidArgumentException.php index 18654db..24ccc22 100644 --- a/src/PhpZip/Exception/InvalidArgumentException.php +++ b/src/PhpZip/Exception/InvalidArgumentException.php @@ -1,4 +1,5 @@ $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); - } - } -} \ No newline at end of file diff --git a/src/PhpZip/Extra/ExtraField.php b/src/PhpZip/Extra/ExtraField.php index f0e7ec4..cbf18bd 100644 --- a/src/PhpZip/Extra/ExtraField.php +++ b/src/PhpZip/Extra/ExtraField.php @@ -1,120 +1,35 @@ $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; - } + public static function getHeaderId(); /** - * Registered extra field classes. - * - * @return array|null + * Serializes a Data Block. + * @return string */ - private static function getRegistry() - { - if (null === self::$registry) { - self::$registry[WinZipAesEntryExtraField::getHeaderId()] = WinZipAesEntryExtraField::class; - self::$registry[NtfsExtraField::getHeaderId()] = NtfsExtraField::class; - } - return self::$registry; - } + public function serialize(); /** - * Returns a protective copy of the Data Block. - * - * @return resource - * @throws ZipException If size data block out of range. + * Initializes this Extra Field by deserializing a Data Block. + * @param string $data */ - public function getDataBlock() - { - $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); -} \ No newline at end of file + public function deserialize($data); +} diff --git a/src/PhpZip/Extra/ExtraFieldHeader.php b/src/PhpZip/Extra/ExtraFieldHeader.php deleted file mode 100644 index f586e5a..0000000 --- a/src/PhpZip/Extra/ExtraFieldHeader.php +++ /dev/null @@ -1,21 +0,0 @@ -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; - } - } - -} \ No newline at end of file diff --git a/src/PhpZip/Extra/ExtraFieldsCollection.php b/src/PhpZip/Extra/ExtraFieldsCollection.php new file mode 100644 index 0000000..3b42dd7 --- /dev/null +++ b/src/PhpZip/Extra/ExtraFieldsCollection.php @@ -0,0 +1,240 @@ +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

+ * An offset to check for. + *

+ * @return boolean true on success or false on failure. + *

+ *

+ * The return value will be casted to boolean if non-boolean was returned. + * @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

+ * The offset to retrieve. + *

+ * @return mixed Can return all value types. + * @since 5.0.0 + */ + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * Offset to set + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset

+ * The offset to assign the value to. + *

+ * @param mixed $value

+ * The value to set. + *

+ * @return void + * @throws InvalidArgumentException + * @since 5.0.0 + */ + public function offsetSet($offset, $value) + { + if ($value instanceof ExtraField) { + assert($offset == $value::getHeaderId()); + $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

+ * The offset to unset. + *

+ * @return void + * @since 5.0.0 + */ + 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; + } + } +} diff --git a/src/PhpZip/Extra/ExtraFieldsFactory.php b/src/PhpZip/Extra/ExtraFieldsFactory.php new file mode 100644 index 0000000..2e9fd82 --- /dev/null +++ b/src/PhpZip/Extra/ExtraFieldsFactory.php @@ -0,0 +1,100 @@ + $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. + * + * @return array + */ + protected static function getRegistry() + { + if (null === self::$registry) { + self::$registry[WinZipAesEntryExtraField::getHeaderId()] = WinZipAesEntryExtraField::class; + self::$registry[NtfsExtraField::getHeaderId()] = NtfsExtraField::class; + self::$registry[Zip64ExtraField::getHeaderId()] = Zip64ExtraField::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); + } +} diff --git a/src/PhpZip/Extra/Fields/DefaultExtraField.php b/src/PhpZip/Extra/Fields/DefaultExtraField.php new file mode 100644 index 0000000..77af380 --- /dev/null +++ b/src/PhpZip/Extra/Fields/DefaultExtraField.php @@ -0,0 +1,71 @@ + $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; + } +} diff --git a/src/PhpZip/Extra/Fields/NtfsExtraField.php b/src/PhpZip/Extra/Fields/NtfsExtraField.php new file mode 100644 index 0000000..45efb36 --- /dev/null +++ b/src/PhpZip/Extra/Fields/NtfsExtraField.php @@ -0,0 +1,133 @@ +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 (null !== $this->mtime && null !== $this->atime && null !== $this->ctime) { + $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; + } +} diff --git a/src/PhpZip/Extra/WinZipAesEntryExtraField.php b/src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php similarity index 65% rename from src/PhpZip/Extra/WinZipAesEntryExtraField.php rename to src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php index 6f32ce2..83b5e97 100644 --- a/src/PhpZip/Extra/WinZipAesEntryExtraField.php +++ b/src/PhpZip/Extra/Fields/WinZipAesEntryExtraField.php @@ -1,7 +1,10 @@ 0x01, self::KEY_STRENGTH_192BIT => 0x02, 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. * * @var int */ - private $vendorVersion = self::VV_AE_1; + protected $vendorVersion = self::VV_AE_1; /** * Encryption strength. * * @var int */ - private $encryptionStrength = self::KEY_STRENGTH_256BIT; + protected $encryptionStrength = self::KEY_STRENGTH_256BIT; /** * Zip compression method. * * @var int */ - private $method; + protected $method; /** * Returns the Header ID (type) of this Extra Field. @@ -71,21 +80,6 @@ class WinZipAesEntryExtraField extends ExtraField 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. * @@ -155,6 +149,32 @@ class WinZipAesEntryExtraField extends ExtraField return $this->method & 0xffff; } + /** + * Internal encryption method. + * + * @return int + */ + 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. * @@ -169,37 +189,6 @@ class WinZipAesEntryExtraField extends ExtraField $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. * @@ -218,19 +207,50 @@ class WinZipAesEntryExtraField extends ExtraField */ 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 - * resource $handle at the zero based offset $off. - * - * @param resource $handle - * @param int $off Offset bytes + * Serializes a Data Block. + * @return string */ - public function writeTo($handle, $off) + public function serialize() { - fseek($handle, $off, SEEK_SET); - fwrite($handle, pack('vvcv', $this->vendorVersion, self::VENDOR_ID, $this->encryptionStrength, $this->method)); + return pack( + 'vvcv', + $this->vendorVersion, + self::VENDOR_ID, + $this->encryptionStrength, + $this->method + ); } -} \ No newline at end of file + + /** + * 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']); + } +} diff --git a/src/PhpZip/Extra/Fields/Zip64ExtraField.php b/src/PhpZip/Extra/Fields/Zip64ExtraField.php new file mode 100644 index 0000000..52c3e38 --- /dev/null +++ b/src/PhpZip/Extra/Fields/Zip64ExtraField.php @@ -0,0 +1,118 @@ +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 + * @throws RuntimeException + */ + public function serialize() + { + if (null === $this->entry) { + 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 RuntimeException + */ + public function deserialize($data) + { + if (null === $this->entry) { + 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))); + } + } +} diff --git a/src/PhpZip/Extra/NtfsExtraField.php b/src/PhpZip/Extra/NtfsExtraField.php deleted file mode 100644 index da09225..0000000 --- a/src/PhpZip/Extra/NtfsExtraField.php +++ /dev/null @@ -1,176 +0,0 @@ -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; - } - -} \ No newline at end of file diff --git a/src/PhpZip/Mapper/OffsetPositionMapper.php b/src/PhpZip/Mapper/OffsetPositionMapper.php index 038cc47..7ea9116 100644 --- a/src/PhpZip/Mapper/OffsetPositionMapper.php +++ b/src/PhpZip/Mapper/OffsetPositionMapper.php @@ -1,4 +1,5 @@ offset = $offset; + $this->offset = (int)$offset; } /** @@ -39,4 +40,4 @@ class OffsetPositionMapper extends PositionMapper { return parent::unmap($position) - $this->offset; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Mapper/PositionMapper.php b/src/PhpZip/Mapper/PositionMapper.php index a7b02a4..e5d67c8 100644 --- a/src/PhpZip/Mapper/PositionMapper.php +++ b/src/PhpZip/Mapper/PositionMapper.php @@ -1,4 +1,5 @@ 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(); - } - -} \ No newline at end of file diff --git a/src/PhpZip/Model/EndOfCentralDirectory.php b/src/PhpZip/Model/EndOfCentralDirectory.php index 1730c0c..8016e5f 100644 --- a/src/PhpZip/Model/EndOfCentralDirectory.php +++ b/src/PhpZip/Model/EndOfCentralDirectory.php @@ -1,11 +1,6 @@ mapper = new PositionMapper(); - } - - /** - * 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; + $this->entryCount = $entryCount; + $this->comment = $comment; + $this->zip64 = $zip64; } /** @@ -256,9 +105,9 @@ class EndOfCentralDirectory /** * @return int */ - public function getCentralDirectoryEntriesSize() + public function getEntryCount() { - return $this->centralDirectoryEntriesSize; + return $this->entryCount; } /** @@ -268,152 +117,4 @@ class EndOfCentralDirectory { 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); - } - } - -} \ No newline at end of file +} diff --git a/src/PhpZip/Model/Entry/OutputOffsetEntry.php b/src/PhpZip/Model/Entry/OutputOffsetEntry.php new file mode 100644 index 0000000..94bd15b --- /dev/null +++ b/src/PhpZip/Model/Entry/OutputOffsetEntry.php @@ -0,0 +1,49 @@ +offset = $pos; + $this->entry = $entry; + } + + /** + * @return int + */ + public function getOffset() + { + return $this->offset; + } + + /** + * @return ZipEntry + */ + public function getEntry() + { + return $this->entry; + } +} diff --git a/src/PhpZip/Model/Entry/ZipAbstractEntry.php b/src/PhpZip/Model/Entry/ZipAbstractEntry.php index 17f8d24..bffe7bb 100644 --- a/src/PhpZip/Model/Entry/ZipAbstractEntry.php +++ b/src/PhpZip/Model/Entry/ZipAbstractEntry.php @@ -4,15 +4,14 @@ namespace PhpZip\Model\Entry; use PhpZip\Exception\InvalidArgumentException; use PhpZip\Exception\ZipException; -use PhpZip\Extra\DefaultExtraField; -use PhpZip\Extra\ExtraField; -use PhpZip\Extra\ExtraFields; -use PhpZip\Extra\WinZipAesEntryExtraField; -use PhpZip\Model\CentralDirectory; +use PhpZip\Extra\ExtraFieldsCollection; +use PhpZip\Extra\ExtraFieldsFactory; +use PhpZip\Extra\Fields\WinZipAesEntryExtraField; +use PhpZip\Extra\Fields\Zip64ExtraField; use PhpZip\Model\ZipEntry; use PhpZip\Util\DateTimeConverter; -use PhpZip\Util\PackUtil; -use PhpZip\ZipFile; +use PhpZip\Util\StringUtil; +use PhpZip\ZipFileInterface; /** * Abstract ZIP entry. @@ -23,16 +22,10 @@ use PhpZip\ZipFile; */ abstract class ZipAbstractEntry implements ZipEntry { - /** - * @var CentralDirectory - */ - private $centralDirectory; - /** * @var int Bit flags for init state. */ private $init; - /** * @var string Entry name (filename in archive) */ @@ -45,14 +38,14 @@ abstract class ZipAbstractEntry implements ZipEntry * @var int */ private $versionNeededToExtract = 20; - /** - * @var int - */ - private $general; /** * @var int Compression method */ private $method; + /** + * @var int + */ + private $general; /** * @var int Dos time */ @@ -78,13 +71,13 @@ abstract class ZipAbstractEntry implements ZipEntry */ private $offset = self::UNKNOWN; /** - * The map of Extra Fields. - * Maps from Header ID [Integer] to Extra Field [ExtraField]. + * Collections of Extra Fields. + * Keys from Header ID [int] and value Extra Field [ExtraField]. * 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. */ @@ -95,55 +88,48 @@ abstract class ZipAbstractEntry implements ZipEntry private $password; /** * Encryption method. - * @see ZipFile::ENCRYPTION_METHOD_TRADITIONAL - * @see ZipFile::ENCRYPTION_METHOD_WINZIP_AES + * @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 * @var int */ - private $encryptionMethod = ZipFile::ENCRYPTION_METHOD_TRADITIONAL; - + private $encryptionMethod = ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL; /** * @var int */ - private $compressionLevel = ZipFile::LEVEL_DEFAULT_COMPRESSION; + private $compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION; /** - * @param int $mask - * @return bool + * ZipAbstractEntry constructor. */ - private function isInit($mask) + public function __construct() { - return 0 !== ($this->init & $mask); + $this->extraFieldsCollection = new ExtraFieldsCollection(); } /** - * @param int $mask - * @param bool $init + * @param ZipEntry $entry */ - private function setInit($mask, $init) + public function setEntry(ZipEntry $entry) { - if ($init) { - $this->init |= $mask; - } else { - $this->init &= ~$mask; - } - } - - /** - * @return CentralDirectory - */ - public function getCentralDirectory() - { - return $this->centralDirectory; - } - - /** - * @param CentralDirectory $centralDirectory - * @return ZipEntry - */ - public function setCentralDirectory(CentralDirectory $centralDirectory) - { - $this->centralDirectory = $centralDirectory; - return $this; + $this->setName($entry->getName()); + $this->setPlatform($entry->getPlatform()); + $this->setVersionNeededToExtract($entry->getVersionNeededToExtract()); + $this->setMethod($entry->getMethod()); + $this->setGeneralPurposeBitFlags($entry->getGeneralPurposeBitFlags()); + $this->setDosTime($entry->getDosTime()); + $this->setCrc($entry->getCrc()); + $this->setCompressedSize($entry->getCompressedSize()); + $this->setSize($entry->getSize()); + $this->setExternalAttributes($entry->getExternalAttributes()); + $this->setOffset($entry->getOffset()); + $this->setExtra($entry->getExtra()); + $this->setComment($entry->getComment()); + $this->setPassword($entry->getPassword()); + $this->setEncryptionMethod($entry->getEncryptionMethod()); + $this->setCompressionLevel($entry->getCompressionLevel()); + $this->setEncrypted($entry->isEncrypted()); } /** @@ -174,6 +160,23 @@ abstract class ZipAbstractEntry implements ZipEntry 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 */ @@ -204,6 +207,28 @@ abstract class ZipAbstractEntry implements ZipEntry return $this; } + /** + * @param int $mask + * @return bool + */ + protected function isInit($mask) + { + return 0 !== ($this->init & $mask); + } + + /** + * @param int $mask + * @param bool $init + */ + protected function setInit($mask, $init) + { + if ($init) { + $this->init |= $mask; + } else { + $this->init &= ~$mask; + } + } + /** * Version needed to extract. * @@ -321,21 +346,9 @@ abstract class ZipAbstractEntry implements ZipEntry 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. - * - * @return bool + * @return int */ public function getGeneralPurposeBitFlags() { @@ -355,33 +368,19 @@ abstract class ZipAbstractEntry implements ZipEntry throw new ZipException('general out of range'); } $this->general = $general; - return $this; - } - - /** - * Returns the indexed General Purpose Bit Flag. - * - * @param int $mask - * @return bool - */ - public function getGeneralPurposeBitFlag($mask) - { - 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; + if ($this->method === ZipFileInterface::METHOD_DEFLATED) { + $bit1 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG1); + $bit2 = $this->getGeneralPurposeBitFlag(self::GPBF_COMPRESSION_FLAG2); + if ($bit1 && !$bit2) { + $this->compressionLevel = ZipFileInterface::LEVEL_BEST_COMPRESSION; + } elseif (!$bit1 && $bit2) { + $this->compressionLevel = ZipFileInterface::LEVEL_FAST; + } elseif ($bit1 && $bit2) { + $this->compressionLevel = ZipFileInterface::LEVEL_SUPER_FAST; + } else { + $this->compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION; + } + } return $this; } @@ -395,6 +394,17 @@ abstract class ZipAbstractEntry implements ZipEntry return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED); } + /** + * Returns the indexed General Purpose Bit Flag. + * + * @param int $mask + * @return bool + */ + public function getGeneralPurposeBitFlag($mask) + { + return 0 !== ($this->general & $mask); + } + /** * Sets the encryption property to false and removes any other * encryption artifacts. @@ -404,17 +414,16 @@ abstract class ZipAbstractEntry implements ZipEntry public function clearEncryption() { $this->setEncrypted(false); - if (null !== $this->fields) { - $field = $this->fields->get(WinZipAesEntryExtraField::getHeaderId()); - if (null !== $field) { - /** - * @var WinZipAesEntryExtraField $field - */ - $this->removeExtraField(WinZipAesEntryExtraField::getHeaderId()); - } + $headerId = WinZipAesEntryExtraField::getHeaderId(); + if (isset($this->extraFieldsCollection[$headerId])) { + /** + * @var WinZipAesEntryExtraField $field + */ + $field = $this->extraFieldsCollection[$headerId]; if (self::METHOD_WINZIP_AES === $this->getMethod()) { $this->setMethod(null === $field ? self::UNKNOWN : $field->getMethod()); } + unset($this->extraFieldsCollection[$headerId]); } $this->password = null; return $this; @@ -428,6 +437,7 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function setEncrypted($encrypted) { + $encrypted = (bool)$encrypted; $this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted); return $this; } @@ -451,6 +461,10 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function setMethod($method) { + if (self::UNKNOWN === $method) { + $this->method = $method; + return $this; + } if (0x0000 > $method || $method > 0xffff) { throw new ZipException('method out of range'); } @@ -458,21 +472,15 @@ abstract class ZipAbstractEntry implements ZipEntry case self::METHOD_WINZIP_AES: $this->method = $method; $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: + case ZipFileInterface::METHOD_STORED: + case ZipFileInterface::METHOD_DEFLATED: + case ZipFileInterface::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; - default: throw new ZipException($this->name . " (unsupported compression method $method)"); } @@ -492,24 +500,6 @@ abstract class ZipAbstractEntry implements ZipEntry 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 * @@ -535,6 +525,24 @@ abstract class ZipAbstractEntry implements ZipEntry $this->setInit(self::BIT_DATE_TIME, true); } + /** + * 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; + } + /** * Returns the external file attributes. * @@ -572,123 +580,43 @@ 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 */ - 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. - * - * @param int $headerId - * @return ExtraField|null + * @return ExtraFieldsCollection */ - 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. - * - * @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 * @throws ZipException */ - private function getExtraFields($zip64) + public function getExtra() { - if ($zip64) { - $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)); + $extraData = ''; + foreach ($this->getExtraFieldsCollection() as $extraField) { + $data = $extraField->serialize(); + $extraData .= pack('vv', $extraField::getHeaderId(), strlen($data)); + $extraData .= $data; } - 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)); + $size = strlen($extraData); + if (0x0000 > $size || $size > 0xffff) { + throw new ZipException('Size extra out of range: ' . $size . '. Extra data: ' . $extraData); } - // 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; + return $extraData; } /** @@ -701,99 +629,31 @@ abstract class ZipAbstractEntry implements ZipEntry * * @param string $data The byte array holding the serialized Extra Fields. * @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) { + $this->extraFieldsCollection = new ExtraFieldsCollection(); if (null !== $data) { - $length = strlen($data); - if (0x0000 > $length || $length > 0xffff) { - throw new ZipException("Extra Fields too large"); + $extraLength = strlen($data); + if (0x0000 > $extraLength || $extraLength > 0xffff) { + throw new ZipException("Extra Fields too large: " . $extraLength); } - } - 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; + $pos = 0; + $endPos = $extraLength; + while ($pos < $endPos) { + $unpack = unpack('vheaderId/vdataSize', substr($data, $pos, 4)); + $pos += 4; + $headerId = (int)$unpack['headerId']; + $dataSize = (int)$unpack['dataSize']; + $extraField = ExtraFieldsFactory::create($headerId); + if ($extraField instanceof Zip64ExtraField) { + $extraField->setEntry($this); } + $extraField->deserialize(substr($data, $pos, $dataSize)); + $pos += $dataSize; + $this->extraFieldsCollection[$headerId] = $extraField; } } - 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 +663,7 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function getComment() { - return null != $this->comment ? $this->comment : ""; + return null !== $this->comment ? $this->comment : ""; } /** @@ -883,7 +743,11 @@ abstract class ZipAbstractEntry implements ZipEntry if (null !== $encryptionMethod) { $this->setEncryptionMethod($encryptionMethod); } - $this->setEncrypted(!empty($this->password)); + if (!empty($this->password)) { + $this->setEncrypted(true); + } else { + $this->clearEncryption(); + } return $this; } @@ -895,6 +759,34 @@ abstract class ZipAbstractEntry implements ZipEntry 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 ( + 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 !== $encryptionMethod + ) { + throw new ZipException('Invalid encryption method'); + } + $this->encryptionMethod = $encryptionMethod; + } + return $this; + } + /** * @return int */ @@ -908,46 +800,23 @@ abstract class ZipAbstractEntry implements 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 || - $compressionLevel > ZipFile::LEVEL_BEST_COMPRESSION + if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION || + $compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION ) { 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; 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 */ - function __clone() + public function __clone() { - $this->fields = $this->fields !== null ? clone $this->fields : null; + $this->extraFieldsCollection = clone $this->extraFieldsCollection; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Model/Entry/ZipChangesEntry.php b/src/PhpZip/Model/Entry/ZipChangesEntry.php new file mode 100644 index 0000000..205a793 --- /dev/null +++ b/src/PhpZip/Model/Entry/ZipChangesEntry.php @@ -0,0 +1,63 @@ +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; + } +} diff --git a/src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php b/src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php deleted file mode 100644 index de5f480..0000000 --- a/src/PhpZip/Model/Entry/ZipNewEmptyDirEntry.php +++ /dev/null @@ -1,26 +0,0 @@ -content = $content; + } + + /** + * Returns an string content of the given entry. + * + * @return null|string + * @throws ZipException + */ + public function getEntryContent() + { + if (is_resource($this->content)) { + return stream_get_contents($this->content, -1, 0); + } + return $this->content; + } /** * Version needed to extract. @@ -32,237 +61,29 @@ abstract class ZipNewEntry extends ZipAbstractEntry { $method = $this->getMethod(); return self::METHOD_WINZIP_AES === $method ? 51 : - (ZipFile::METHOD_BZIP2 === $method ? 46 : - ($this->isZip64ExtensionsRequired() ? 45 : - (ZipFile::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10) + ( + ZipFileInterface::METHOD_BZIP2 === $method ? 46 : + ( + $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. - * - * @param resource $outputStream - * @throws ZipException + * Clone extra fields */ - public function writeEntry($outputStream) + public function __clone() { - $nameLength = strlen($this->getName()); - $size = $nameLength + strlen($this->getExtra()) + strlen($this->getComment()); - 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)"); - } + $this->clone = true; + parent::__clone(); } -} \ No newline at end of file + public function __destruct() + { + if (!$this->clone && null !== $this->content && is_resource($this->content)) { + fclose($this->content); + $this->content = null; + } + } +} diff --git a/src/PhpZip/Model/Entry/ZipNewStreamEntry.php b/src/PhpZip/Model/Entry/ZipNewStreamEntry.php deleted file mode 100644 index a8eb518..0000000 --- a/src/PhpZip/Model/Entry/ZipNewStreamEntry.php +++ /dev/null @@ -1,55 +0,0 @@ -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; - } - } -} \ No newline at end of file diff --git a/src/PhpZip/Model/Entry/ZipNewStringEntry.php b/src/PhpZip/Model/Entry/ZipNewStringEntry.php deleted file mode 100644 index d376957..0000000 --- a/src/PhpZip/Model/Entry/ZipNewStringEntry.php +++ /dev/null @@ -1,39 +0,0 @@ -entryContent = $entryContent; - } - - /** - * Returns an string content of the given entry. - * - * @return null|string - * @throws ZipException - */ - public function getEntryContent() - { - return $this->entryContent; - } -} \ No newline at end of file diff --git a/src/PhpZip/Model/Entry/ZipReadEntry.php b/src/PhpZip/Model/Entry/ZipReadEntry.php deleted file mode 100644 index 2363884..0000000 --- a/src/PhpZip/Model/Entry/ZipReadEntry.php +++ /dev/null @@ -1,327 +0,0 @@ -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); - } - } - -} \ No newline at end of file diff --git a/src/PhpZip/Model/Entry/ZipSourceEntry.php b/src/PhpZip/Model/Entry/ZipSourceEntry.php new file mode 100644 index 0000000..7c43f10 --- /dev/null +++ b/src/PhpZip/Model/Entry/ZipSourceEntry.php @@ -0,0 +1,95 @@ +inputStream = $inputStream; + } + + /** + * @return ZipInputStreamInterface + */ + public function getInputStream() + { + return $this->inputStream; + } + + /** + * Returns an string content of the given entry. + * + * @return string + * @throws ZipException + */ + public function getEntryContent() + { + if (null === $this->entryContent) { + $content = $this->inputStream->readEntryContent($this); + 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; + } + + /** + * Clone extra fields + */ + public function __clone() + { + $this->clone = true; + parent::__clone(); + } + + public function __destruct() + { + if (!$this->clone && null !== $this->entryContent && is_resource($this->entryContent)) { + fclose($this->entryContent); + } + } +} diff --git a/src/PhpZip/Model/ZipEntry.php b/src/PhpZip/Model/ZipEntry.php index b9caf4d..7f80995 100644 --- a/src/PhpZip/Model/ZipEntry.php +++ b/src/PhpZip/Model/ZipEntry.php @@ -1,9 +1,11 @@ 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->clearEncryption(); + } + }); + } + + /** + * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + *

+ *

+ * The return value is cast to an integer. + * @since 5.1.0 + */ + public function count() + { + return count($this->matches); + } +} diff --git a/src/PhpZip/Model/ZipInfo.php b/src/PhpZip/Model/ZipInfo.php index 1dfec17..193087c 100644 --- a/src/PhpZip/Model/ZipInfo.php +++ b/src/PhpZip/Model/ZipInfo.php @@ -1,10 +1,11 @@ 'no compression', + ZipEntry::UNKNOWN => 'unknown', + ZipFileInterface::METHOD_STORED => 'no compression', 1 => 'shrink', 2 => 'reduce level 1', 3 => 'reduce level 2', @@ -94,7 +96,7 @@ class ZipInfo 5 => 'reduce level 4', 6 => 'implode', 7 => 'reserved for Tokenizing compression algorithm', - ZipFile::METHOD_DEFLATED => 'deflate', + ZipFileInterface::METHOD_DEFLATED => 'deflate', 9 => 'deflate64', 10 => 'PKWARE Data Compression Library Imploding (old IBM TERSE)', 11 => 'reserved by PKWARE', @@ -114,72 +116,71 @@ class ZipInfo /** * @var string */ - private $path; - + private $name; /** * @var bool */ private $folder; - /** * @var int */ private $size; - /** * @var int */ private $compressedSize; - /** * @var int */ private $mtime; - /** * @var int|null */ private $ctime; - /** * @var int|null */ private $atime; - /** * @var bool */ private $encrypted; - /** * @var string|null */ private $comment; - /** * @var int */ private $crc; - /** * @var string */ - private $method; - + private $methodName; + /** + * @var int + */ + private $compressionMethod; /** * @var string */ private $platform; - /** * @var int */ private $version; - /** * @var string */ private $attributes; + /** + * @var int|null + */ + private $encryptionMethod; + /** + * @var int|null + */ + private $compressionLevel; /** * ZipInfo constructor. @@ -192,16 +193,17 @@ class ZipInfo $atime = null; $ctime = null; - $field = $entry->getExtraField(NtfsExtraField::getHeaderId()); + $field = $entry->getExtraFieldsCollection()->get(NtfsExtraField::getHeaderId()); if (null !== $field && $field instanceof NtfsExtraField) { /** * @var NtfsExtraField $field */ $atime = $field->getAtime(); $ctime = $field->getCtime(); + $mtime = $field->getMtime(); } - $this->path = $entry->getName(); + $this->name = $entry->getName(); $this->folder = $entry->isDirectory(); $this->size = $entry->getSize(); $this->compressedSize = $entry->getCompressedSize(); @@ -209,18 +211,22 @@ class ZipInfo $this->ctime = $ctime; $this->atime = $atime; $this->encrypted = $entry->isEncrypted(); + $this->encryptionMethod = $entry->getEncryptionMethod(); $this->comment = $entry->getComment(); $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->version = $entry->getVersionNeededToExtract(); + $this->compressionLevel = $entry->getCompressionLevel(); $attributes = str_repeat(" ", 12); $externalAttributes = $entry->getExternalAttributes(); $xattr = (($externalAttributes >> 16) & 0xFFFF); switch ($entry->getPlatform()) { case self::MADE_BY_MS_DOS: - /** @noinspection PhpMissingBreakStatementInspection */ + // no break + /** @noinspection PhpMissingBreakStatementInspection */ case self::MADE_BY_WINDOWS_NTFS: if ($entry->getPlatform() != self::MADE_BY_MS_DOS || ($xattr & 0700) != @@ -237,11 +243,12 @@ class ZipInfo if ($xattr & 0x10) { $attributes[0] = 'd'; $attributes[3] = 'x'; - } else + } else { $attributes[0] = '-'; - if ($xattr & 0x08) + } + if ($xattr & 0x08) { $attributes[0] = 'V'; - else { + } else { $ext = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION)); if (in_array($ext, ["com", "exe", "btm", "cmd", "bat"])) { $attributes[3] = 'x'; @@ -250,6 +257,7 @@ class ZipInfo break; } /* else: fall through! */ + // no break default: /* assume Unix-like */ switch ($xattr & self::UNX_IFMT) { case self::UNX_IFDIR: @@ -284,33 +292,59 @@ class ZipInfo $attributes[5] = ($xattr & self::UNX_IWGRP) ? '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'; - else - $attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; /* S==undefined */ - if ($xattr & self::UNX_IXGRP) - $attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; /* == UNX_ENFMT */ - else - $attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; /* SunOS 4.1.x */ - 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 */ + } else { + $attributes[3] = ($xattr & self::UNX_ISUID) ? 'S' : '-'; + } /* S==undefined */ + if ($xattr & self::UNX_IXGRP) { + $attributes[6] = ($xattr & self::UNX_ISGID) ? 's' : 'x'; + } /* == UNX_ENFMT */ + else { + $attributes[6] = ($xattr & self::UNX_ISGID) ? 'S' : '-'; + } /* SunOS 4.1.x */ + 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); } + /** + * @param ZipEntry $entry + * @return int + */ + 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 */ - public static function getMethodName(ZipEntry $entry) + private static function getEntryMethodName(ZipEntry $entry) { $return = ''; if ($entry->isEncrypted()) { if ($entry->getMethod() === ZipEntry::METHOD_WINZIP_AES) { - $field = $entry->getExtraField(WinZipAesEntryExtraField::getHeaderId()); $return = ucfirst(self::$valuesCompressionMethod[$entry->getMethod()]); + $field = $entry->getExtraFieldsCollection()->get(WinZipAesEntryExtraField::getHeaderId()); if (null !== $field) { /** * @var WinZipAesEntryExtraField $field @@ -348,34 +382,20 @@ class ZipInfo } /** - * @return array + * @return string */ - public function toArray() + public function getName() { - return [ - 'path' => $this->getPath(), - 'folder' => $this->isFolder(), - 'size' => $this->getSize(), - 'compressed_size' => $this->getCompressedSize(), - 'modified' => $this->getMtime(), - 'created' => $this->getCtime(), - 'accessed' => $this->getAtime(), - 'attributes' => $this->getAttributes(), - 'encrypted' => $this->isEncrypted(), - 'comment' => $this->getComment(), - 'crc' => $this->getCrc(), - 'method' => $this->getMethod(), - 'platform' => $this->getPlatform(), - 'version' => $this->getVersion() - ]; + return $this->name; } /** * @return string + * @deprecated use \PhpZip\Model\ZipInfo::getName() */ public function getPath() { - return $this->path; + return $this->getName(); } /** @@ -426,6 +446,14 @@ class ZipInfo return $this->atime; } + /** + * @return string + */ + public function getAttributes() + { + return $this->attributes; + } + /** * @return boolean */ @@ -452,10 +480,19 @@ class ZipInfo /** * @return string + * @deprecated use \PhpZip\Model\ZipInfo::getMethodName() */ public function getMethod() { - return $this->method; + return $this->getMethodName(); + } + + /** + * @return string + */ + public function getMethodName() + { + return $this->methodName; } /** @@ -475,35 +512,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 */ - function __toString() + public function __toString() { - return 'ZipInfo {' - . 'Path="' . $this->getPath() . '", ' - . ($this->isFolder() ? 'Folder, ' : '') - . 'Size=' . FilesUtil::humanSize($this->getSize()) - . ', Compressed size=' . FilesUtil::humanSize($this->getCompressedSize()) - . ', Modified time=' . date(DATE_W3C, $this->getMtime()) . ', ' - . ($this->getCtime() !== null ? 'Created time=' . date(DATE_W3C, $this->getCtime()) . ', ' : '') - . ($this->getAtime() !== null ? 'Accessed time=' . date(DATE_W3C, $this->getAtime()) . ', ' : '') - . ($this->isEncrypted() ? 'Encrypted, ' : '') - . (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '') - . (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '') - . 'Method="' . $this->getMethod() . '", ' - . 'Attributes="' . $this->getAttributes() . '", ' - . 'Platform="' . $this->getPlatform() . '", ' - . 'Version=' . $this->getVersion() - . '}'; + return __CLASS__ . ' {' + . 'Name="' . $this->getName() . '", ' + . ($this->isFolder() ? 'Folder, ' : '') + . 'Size="' . FilesUtil::humanSize($this->getSize()).'"' + . ', Compressed size="' . FilesUtil::humanSize($this->getCompressedSize()).'"' + . ', Modified time="' . date(DATE_W3C, $this->getMtime()) . '", ' + . ($this->getCtime() !== null ? 'Created time="' . date(DATE_W3C, $this->getCtime()) . '", ' : '') + . ($this->getAtime() !== null ? 'Accessed time="' . date(DATE_W3C, $this->getAtime()) . '", ' : '') + . ($this->isEncrypted() ? 'Encrypted, ' : '') + . (!empty($this->comment) ? 'Comment="' . $this->getComment() . '", ' : '') + . (!empty($this->crc) ? 'Crc=0x' . dechex($this->getCrc()) . ', ' : '') + . 'Method name="' . $this->getMethodName() . '", ' + . 'Attributes="' . $this->getAttributes() . '", ' + . 'Platform="' . $this->getPlatform() . '", ' + . 'Version=' . $this->getVersion() + . '}'; } - - -} \ No newline at end of file +} diff --git a/src/PhpZip/Model/ZipModel.php b/src/PhpZip/Model/ZipModel.php new file mode 100644 index 0000000..c5866b2 --- /dev/null +++ b/src/PhpZip/Model/ZipModel.php @@ -0,0 +1,341 @@ +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 + * @throws InvalidArgumentException + */ + public function setArchiveComment($comment) + { + 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'); + } + } + if ($comment !== $this->archiveComment) { + $this->archiveCommentChanges = $comment; + $this->archiveCommentChanged = true; + } else { + $this->archiveCommentChanged = false; + } + } + + /** + * Specify a password for extracting files. + * + * @param null|string $password + */ + public function setReadPassword($password) + { + foreach ($this->inputEntries as $entry) { + if ($entry->isEncrypted()) { + $entry->setPassword($password); + } + } + } + + /** + * @param string $entryName + * @param string $password + * @throws ZipNotFoundEntry + */ + public function setReadPasswordEntry($entryName, $password) + { + if (!isset($this->inputEntries[$entryName])) { + throw new ZipNotFoundEntry('Not found entry ' . $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); + } + + 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 InvalidArgumentException + * @throws ZipNotFoundEntry + */ + 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 + */ + 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 ZipNotFoundEntry + */ + public function getEntry($entryName) + { + $entryName = $entryName instanceof ZipEntry ? $entryName->getName() : (string)$entryName; + if (isset($this->outEntries[$entryName])) { + return $this->outEntries[$entryName]; + } + throw new ZipNotFoundEntry('Zip entry ' . $entryName . ' not found'); + } + + /** + * @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. + *

+ *

+ * 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 + * @throws ZipException + */ + public function setEncryptionMethod($encryptionMethod = ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256) + { + $this->matcher()->all()->setEncryptionMethod($encryptionMethod); + } + + /** + * @return ZipEntryMatcher + */ + public function matcher() + { + return new ZipEntryMatcher($this); + } +} diff --git a/src/PhpZip/Stream/ResponseStream.php b/src/PhpZip/Stream/ResponseStream.php new file mode 100644 index 0000000..172de1e --- /dev/null +++ b/src/PhpZip/Stream/ResponseStream.php @@ -0,0 +1,298 @@ + [ + '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; + } +} diff --git a/src/PhpZip/Stream/ZipInputStream.php b/src/PhpZip/Stream/ZipInputStream.php new file mode 100644 index 0000000..2510f43 --- /dev/null +++ b/src/PhpZip/Stream/ZipInputStream.php @@ -0,0 +1,532 @@ +in = $in; + $this->mapper = new PositionMapper(); + } + + /** + * @return ZipModel + */ + 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 (0 < $data['commentLength']) { + $comment = fread($this->in, $data['commentLength']); + } + $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 (0 !== $offset) { + $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()); + if ($lfhOff < $this->preamble) { + $this->preamble = $lfhOff; + } + $entries[$entry->getName()] = $entry; + } + + if (0 !== $numEntries % 0x10000) { + throw new ZipException("Expected " . abs($numEntries) . + ($numEntries > 0 ? " more" : " less") . + " entries in the Central Directory!"); + } + + if ($this->preamble + $this->postamble >= fstat($this->in)['size']) { + assert(0 === $numEntries); + $this->checkZipFileSignature(); + } + + return $entries; + } + + /** + * @return ZipEntry + * @throws InvalidArgumentException + */ + public function readEntry() + { + // central file header signature 4 bytes (0x02014b50) + $fileHeaderSig = unpack('V', fread($this->in, 4))[1]; + if (ZipOutputStreamInterface::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($this->in, 42) + ); + +// $utf8 = 0 !== ($data['gpbf'] & self::GPBF_UTF8); + + // See appendix D of PKWARE's ZIP File Format Specification. + $name = fread($this->in, $data['fileLength']); + + $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 (0 < $data['extraLength']) { + $entry->setExtra(fread($this->in, $data['extraLength'])); + } + if (0 < $data['commentLength']) { + $entry->setComment(fread($this->in, $data['commentLength'])); + } + 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 && null === $entry->getPassword()) { + throw new ZipException("Can not password from entry " . $entry->getName()); + } + + $pos = $entry->getOffset(); + assert(ZipEntry::UNKNOWN !== $pos); + $startPos = $pos = $this->mapper->map($pos); + fseek($this->in, $startPos); + + // local file header signature 4 bytes (0x04034b50) + if (ZipEntry::LOCAL_FILE_HEADER_SIG !== unpack('V', fread($this->in, 4))[1]) { + 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(); + if ($compressedSize > 0) { + $content = fread($this->in, $compressedSize); + } else { + $content = ''; + } + + $skipCheckCrc = false; + if ($isEncrypted) { + if (ZipEntry::METHOD_WINZIP_AES === $method) { + // 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 (ZipEntry::DATA_DESCRIPTOR_SIG === $localCrc) { + $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]); + } + if ($entry->getCrc() != $localCrc) { + throw new Crc32Exception($entry->getName(), $entry->getCrc(), $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'); + } + $content = bzdecompress($content); + break; + default: + throw new ZipUnsupportMethod($entry->getName() . + " (compression method " . $method . " is not supported)"); + } + if (!$skipCheckCrc) { + $localCrc = sprintf('%u', crc32($content)); + if ($entry->getCrc() != $localCrc) { + if ($isEncrypted) { + throw new ZipCryptoException("Wrong password"); + } + throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc); + } + } + return $content; + } + + /** + * @return resource + */ + public function getStream() + { + return $this->in; + } + + /** + * @param ZipEntry $entry + * @param ZipOutputStreamInterface $out + */ + public function copyEntry(ZipEntry $entry, ZipOutputStreamInterface $out) + { + $pos = $entry->getOffset(); + assert(ZipEntry::UNKNOWN !== $pos); + $pos = $this->mapper->map($pos); + + $extraLength = strlen($entry->getExtra()); + $nameLength = strlen($entry->getName()); + + $length = ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $extraLength + $nameLength; + + $padding = 0; + if ($this->zipModel->isZipAlign() && !$entry->isEncrypted() && $entry->getMethod() === ZipFileInterface::METHOD_STORED) { + $padding = + ( + $this->zipModel->getZipAlign() - + (ftell($out->getStream()) + $length) % $this->zipModel->getZipAlign() + ) % $this->zipModel->getZipAlign(); + } + + fseek($this->in, $pos, SEEK_SET); + if ($padding > 0) { + stream_copy_to_stream($this->in, $out->getStream(), ZipEntry::LOCAL_FILE_HEADER_MIN_LEN - 2); + fwrite($out->getStream(), pack('v', $extraLength + $padding)); + fseek($this->in, 2, SEEK_CUR); + stream_copy_to_stream($this->in, $out->getStream(), $nameLength + $extraLength); + fwrite($out->getStream(), str_repeat(chr(0), $padding)); + } else { + stream_copy_to_stream($this->in, $out->getStream(), $length); + } + $this->copyEntryData($entry, $out); + if ($entry->getGeneralPurposeBitFlag(ZipEntry::GPBF_DATA_DESCRIPTOR)) { + $length = 12; + if ($entry->isZip64ExtensionsRequired()) { + $length += 8; + } + stream_copy_to_stream($this->in, $out->getStream(), $length); + } + } + + /** + * @param ZipEntry $entry + * @param ZipOutputStreamInterface $out + */ + public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out) + { + $position = $entry->getOffset() + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + + strlen($entry->getName()) + strlen($entry->getExtra()); + $length = $entry->getCompressedSize(); + fseek($this->in, $position, SEEK_SET); + stream_copy_to_stream($this->in, $out->getStream(), $length); + } + + public function __destruct() + { + $this->close(); + } + + public function close() + { + if ($this->in != null) { + fclose($this->in); + $this->in = null; + } + } +} diff --git a/src/PhpZip/Stream/ZipInputStreamInterface.php b/src/PhpZip/Stream/ZipInputStreamInterface.php new file mode 100644 index 0000000..d5e98a1 --- /dev/null +++ b/src/PhpZip/Stream/ZipInputStreamInterface.php @@ -0,0 +1,50 @@ +out = $out; + $this->zipModel = $zipModel; + } + + 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); + $size = $nameLength + $extraLength; + if (0xffff < $size) { + 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)" + ); + } + + // zip align + $padding = 0; + if ($this->zipModel->isZipAlign() && !$entry->isEncrypted() && $entry->getMethod() === ZipFileInterface::METHOD_STORED) { + $padding = + ( + $this->zipModel->getZipAlign() - + ( + $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + $nameLength + $extraLength + ) % $this->zipModel->getZipAlign() + ) % $this->zipModel->getZipAlign(); + } + + $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 + $padding + ) + ); + fwrite($this->out, $entry->getName()); + if ($extraLength > 0) { + fwrite($this->out, $extra); + } + + if ($padding > 0) { + fwrite($this->out, str_repeat(chr(0), $padding)); + } + + if ($entry instanceof ZipChangesEntry && !$entry->isChangedContent()) { + $entry->getSourceEntry()->getInputStream()->copyEntryData($entry->getSourceEntry(), $this); + } elseif (null !== $entryContent) { + 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 ($entry->getCompressedSize() != $compressedSize) { + 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 (ZipEntry::UNKNOWN === $entry->getPlatform()) { + $entry->setPlatform(ZipEntry::PLATFORM_UNIX); + } + if (ZipEntry::UNKNOWN === $entry->getTime()) { + $entry->setTime(time()); + } + $method = $entry->getMethod(); + + $encrypted = $entry->isEncrypted(); + // See appendix D of PKWARE's ZIP File Format Specification. + $utf8 = true; + + if ($encrypted && null === $entry->getPassword()) { + 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); + + $skipCrc = false; + $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 && + ( + ZipEntry::METHOD_WINZIP_AES === $method || + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 || + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 || + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 + ) + ) { + $field = null; + $method = $entry->getMethod(); + $keyStrength = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod($entry->getEncryptionMethod()); // size bits + + $compressedSize = $entry->getCompressedSize(); + + if (ZipEntry::METHOD_WINZIP_AES === $method) { + /** + * @var WinZipAesEntryExtraField $field + */ + $field = $extraFieldsCollection->get(WinZipAesEntryExtraField::getHeaderId()); + if (null !== $field) { + $method = $field->getMethod(); + if (ZipEntry::UNKNOWN !== $compressedSize) { + $compressedSize -= $field->getKeyStrength() / 2 // salt value + + 2 // password verification value + + 10; // authentication code + } + $entry->setMethod($method); + } + } + if (null === $field) { + $field = ExtraFieldsFactory::createWinZipAesEntryExtra(); + } + $field->setKeyStrength($keyStrength); + $field->setMethod($method); + $size = $entry->getSize(); + if (20 <= $size && ZipFileInterface::METHOD_BZIP2 !== $method) { + $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_1); + } else { + $field->setVendorVersion(WinZipAesEntryExtraField::VV_AE_2); + $skipCrc = true; + } + $extraFieldsCollection->add($field); + if (ZipEntry::UNKNOWN !== $compressedSize) { + $compressedSize += $field->getKeyStrength() / 2 // salt value + + 2 // password verification value + + 10; // authentication code + $entry->setCompressedSize($compressedSize); + } + if ($skipCrc) { + $entry->setCrc(0); + } + } + + 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(); + $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 (ZipFileInterface::METHOD_DEFLATED === $method) { + $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 ( + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_128 || + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_192 || + $entry->getEncryptionMethod() === ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES_256 + ) { + if ($skipCrc) { + $entry->setCrc(0); + } + $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 + */ + protected function determineBestCompressionMethod(ZipEntry $entry, $content) + { + if (null !== $content) { + $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 + * @throws RuntimeException + * @internal param OutPosEntry $entry + */ + 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 (ZipEntry::UNKNOWN === ($compressedSize | $size)) { + 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 (0 < $extraSize) { + // extra field (variable size) + fwrite($this->out, $extra); + } + if (0 < $commentLength) { + // 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; + } +} diff --git a/src/PhpZip/Stream/ZipOutputStreamInterface.php b/src/PhpZip/Stream/ZipOutputStreamInterface.php new file mode 100644 index 0000000..57c397e --- /dev/null +++ b/src/PhpZip/Stream/ZipOutputStreamInterface.php @@ -0,0 +1,29 @@ +> 11) & 0x1f, // hour - ($dosTime >> 5) & 0x3f, // minute - 2 * ($dosTime & 0x1f), // second - ($dosTime >> 21) & 0x0f, // month + ($dosTime >> 5) & 0x3f, // minute + 2 * ($dosTime & 0x1f), // second + ($dosTime >> 21) & 0x0f, // month ($dosTime >> 16) & 0x1f, // day 1980 + (($dosTime >> 25) & 0x7f) // year ); @@ -74,4 +75,4 @@ class DateTimeConverter $date['mday'] << 16 | $date['hours'] << 11 | $date['minutes'] << 5 | $date['seconds'] >> 1); } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/FilesUtil.php b/src/PhpZip/Util/FilesUtil.php index 9192c6d..29e8688 100644 --- a/src/PhpZip/Util/FilesUtil.php +++ b/src/PhpZip/Util/FilesUtil.php @@ -1,4 +1,5 @@ 0 && !$escaping) { $regexPattern .= ')'; $inCurrent--; - } else if ($escaping) + } elseif ($escaping) { $regexPattern = "\\}"; - else + } else { $regexPattern = "}"; + } $escaping = false; break; case ',': if ($inCurrent > 0 && !$escaping) { $regexPattern .= '|'; - } else if ($escaping) + } elseif ($escaping) { $regexPattern .= "\\,"; - else + } else { $regexPattern = ","; + } break; default: $escaping = false; @@ -211,12 +214,15 @@ class FilesUtil */ 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"; - if (($unit === null && $size >= 1 << 20) || $unit === "MB") + } + if (($unit === null && $size >= 1 << 20) || $unit === "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) . " bytes"; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php b/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php index a3b9c9d..40e8fe0 100644 --- a/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php +++ b/src/PhpZip/Util/Iterator/IgnoreFilesFilterIterator.php @@ -1,4 +1,5 @@ ignoreFiles as $ignoreFile) { // handler dir and sub dir if ($fileInfo->isDir() - && $ignoreFile[strlen($ignoreFile) - 1] === '/' + && StringUtil::endsWith($ignoreFile, '/') && StringUtil::endsWith($pathname, substr($ignoreFile, 0, -1)) ) { return false; @@ -57,4 +58,4 @@ class IgnoreFilesFilterIterator extends \FilterIterator } return true; } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php b/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php index 131ee3f..7781576 100644 --- a/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php +++ b/src/PhpZip/Util/Iterator/IgnoreFilesRecursiveFilterIterator.php @@ -1,4 +1,5 @@ getInnerIterator()->getChildren(), $this->ignoreFiles); } -} \ No newline at end of file +} diff --git a/src/PhpZip/Util/PackUtil.php b/src/PhpZip/Util/PackUtil.php index 1ef10d0..b68a0c8 100644 --- a/src/PhpZip/Util/PackUtil.php +++ b/src/PhpZip/Util/PackUtil.php @@ -1,4 +1,5 @@ = 0) { - return current(unpack('P', $value)); + return unpack('P', $value)[1]; } $unpack = unpack('Va/Vb', $value); return $unpack['a'] + ($unpack['b'] << 32); } -} \ No newline at end of file + /** + * Cast to signed int 32-bit + * + * @param int $int + * @return int + */ + public static function toSignedInt32($int) + { + $int = $int & 0xffffffff; + if (PHP_INT_SIZE === 8 && ($int & 0x80000000)) { + return $int - 0x100000000; + } + return $int; + } +} diff --git a/src/PhpZip/Util/StringUtil.php b/src/PhpZip/Util/StringUtil.php index c596adf..0b75040 100644 --- a/src/PhpZip/Util/StringUtil.php +++ b/src/PhpZip/Util/StringUtil.php @@ -1,4 +1,5 @@ = 0 - && strpos($haystack, $needle, $temp) !== false); + && strpos($haystack, $needle, $temp) !== false); } -} \ No newline at end of file +} diff --git a/src/PhpZip/ZipFile.php b/src/PhpZip/ZipFile.php index 59e9b6b..fc6b9d9 100644 --- a/src/PhpZip/ZipFile.php +++ b/src/PhpZip/ZipFile.php @@ -1,17 +1,23 @@ 'application/epub+zip' ]; + /** + * Input seekable input stream. + * + * @var ZipInputStreamInterface + */ + protected $inputStream; + /** + * @var ZipModel + */ + protected $zipModel; + /** * ZipFile constructor. */ public function __construct() { - $this->centralDirectory = new CentralDirectory(); + $this->zipModel = new ZipModel(); } /** * Open zip archive from file * * @param string $filename - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException if file doesn't exists. * @throws ZipException if can't open file. */ @@ -129,7 +111,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Open zip archive from raw string data. * * @param string $data - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException if data not available. * @throws ZipException if can't open temp stream. */ @@ -151,7 +133,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Open zip archive from stream resource * * @param resource $handle - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException Invalid stream resource * or resource cannot seekable stream */ @@ -160,44 +142,36 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (!is_resource($handle)) { throw new InvalidArgumentException("Invalid stream resource."); } + $type = get_resource_type($handle); + if ('stream' !== $type) { + throw new InvalidArgumentException("Invalid resource type - $type."); + } $meta = stream_get_meta_data($handle); + if ('dir' === $meta['stream_type']) { + throw new InvalidArgumentException("Invalid stream type - {$meta['stream_type']}."); + } if (!$meta['seekable']) { throw new InvalidArgumentException("Resource cannot seekable stream."); } - $this->inputStream = $handle; - $this->centralDirectory = new CentralDirectory(); - $this->centralDirectory->mountCentralDirectory($this->inputStream); + $this->inputStream = new ZipInputStream($handle); + $this->zipModel = $this->inputStream->readZip(); return $this; } - /** - * @return int Returns the number of entries in this ZIP file. - */ - public function count() - { - return sizeof($this->centralDirectory->getEntries()); - } - /** * @return string[] Returns the list files. */ public function getListFiles() { - return array_keys($this->centralDirectory->getEntries()); + return array_keys($this->zipModel->getEntries()); } /** - * Check whether the directory entry. - * Returns true if and only if this ZIP entry represents a directory entry - * (i.e. end with '/'). - * - * @param string $entryName - * @return bool - * @throws ZipNotFoundEntry + * @return int Returns the number of entries in this ZIP file. */ - public function isDirectory($entryName) + public function count() { - return $this->centralDirectory->getEntry($entryName)->isDirectory(); + return $this->zipModel->count(); } /** @@ -207,34 +181,34 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getArchiveComment() { - return $this->centralDirectory->getArchiveComment(); - } - - /** - * Set password to all input encrypted entries. - * - * @param string $password Password - * @return ZipFile - */ - public function withReadPassword($password) - { - foreach ($this->centralDirectory->getEntries() as $entry) { - if ($entry->isEncrypted()) { - $entry->setPassword($password); - } - } - return $this; + return $this->zipModel->getArchiveComment(); } /** * Set archive comment. * * @param null|string $comment + * @return ZipFileInterface * @throws InvalidArgumentException Length comment out of range */ public function setArchiveComment($comment = null) { - $this->centralDirectory->getEndOfCentralDirectory()->setComment($comment); + $this->zipModel->setArchiveComment($comment); + return $this; + } + + /** + * 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 ZipNotFoundEntry + */ + public function isDirectory($entryName) + { + return $this->zipModel->getEntry($entryName)->isDirectory(); } /** @@ -246,7 +220,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getEntryComment($entryName) { - return $this->centralDirectory->getEntry($entryName)->getComment(); + return $this->zipModel->getEntry($entryName)->getComment(); } /** @@ -254,15 +228,37 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * * @param string $entryName * @param string|null $comment - * @return ZipFile + * @return ZipFileInterface * @throws ZipNotFoundEntry */ public function setEntryComment($entryName, $comment = null) { - $this->centralDirectory->setEntryComment($entryName, $comment); + $this->zipModel->getEntryForChanges($entryName)->setComment($comment); return $this; } + /** + * Returns the entry contents. + * + * @param string $entryName + * @return string + */ + public function getEntryContents($entryName) + { + return $this->zipModel->getEntry($entryName)->getEntryContent(); + } + + /** + * Checks if there is an entry in the archive. + * + * @param string $entryName + * @return bool + */ + public function hasEntry($entryName) + { + return $this->zipModel->hasEntry($entryName); + } + /** * Get info by entry. * @@ -272,10 +268,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getEntryInfo($entryName) { - if (!($entryName instanceof ZipEntry)) { - $entryName = $this->centralDirectory->getEntry($entryName); - } - return new ZipInfo($entryName); + return new ZipInfo($this->zipModel->getEntry($entryName)); } /** @@ -285,7 +278,15 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function getAllInfo() { - return array_map([$this, 'getEntryInfo'], $this->centralDirectory->getEntries()); + return array_map([$this, 'getEntryInfo'], $this->zipModel->getEntries()); + } + + /** + * @return ZipEntryMatcher + */ + public function matcher() + { + return $this->zipModel->matcher(); } /** @@ -296,7 +297,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @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 ZipFile + * @return ZipFileInterface * @throws ZipException */ public function extractTo($destination, $entries = null) @@ -311,9 +312,8 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator throw new ZipException("Destination is not writable directory"); } - /** - * @var ZipEntry[] $zipEntries - */ + $zipEntries = $this->zipModel->getEntries(); + if (!empty($entries)) { if (is_string($entries)) { $entries = (array)$entries; @@ -321,18 +321,10 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (is_array($entries)) { $entries = array_unique($entries); $flipEntries = array_flip($entries); - $zipEntries = array_filter( - $this->centralDirectory->getEntries(), - function ($zipEntry) use ($flipEntries) { - /** - * @var ZipEntry $zipEntry - */ - return isset($flipEntries[$zipEntry->getName()]); - } - ); + $zipEntries = array_filter($zipEntries, function (ZipEntry $zipEntry) use ($flipEntries) { + return isset($flipEntries[$zipEntry->getName()]); + }); } - } else { - $zipEntries = $this->centralDirectory->getEntries(); } foreach ($zipEntries as $entry) { @@ -355,7 +347,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator chmod($dir, 0755); touch($dir, $entry->getTime()); } - if (file_put_contents($file, $entry->getEntryContent()) === false) { + if (false === file_put_contents($file, $entry->getEntryContent())) { throw new ZipException('Can not extract file ' . $entry->getName()); } touch($file, $entry->getTime()); @@ -371,12 +363,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException If incorrect data or entry name. * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFromString($localName, $contents, $compressionMethod = null) { @@ -390,23 +382,23 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $contents = (string)$contents; $length = strlen($contents); if (null === $compressionMethod) { - if ($length >= 1024) { - $compressionMethod = self::METHOD_DEFLATED; + if ($length >= 512) { + $compressionMethod = ZipEntry::UNKNOWN; } else { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } } elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { - throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); + throw new ZipUnsupportMethod('Unsupported compression method ' . $compressionMethod); } $externalAttributes = 0100644 << 16; - $entry = new ZipNewStringEntry($contents); + $entry = new ZipNewEntry($contents); $entry->setName($localName); $entry->setMethod($compressionMethod); $entry->setTime(time()); $entry->setExternalAttributes($externalAttributes); - $this->centralDirectory->putInModified($localName, $entry); + $this->zipModel->addEntry($entry); return $this; } @@ -418,12 +410,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFile($filename, $localName = null, $compressionMethod = null) { @@ -439,16 +431,16 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $mimeType = @mime_content_type($filename); $type = strtok($mimeType, '/'); if ('image' === $type) { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } elseif ('text' === $type && filesize($filename) < 150) { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } else { - $compressionMethod = self::METHOD_DEFLATED; + $compressionMethod = ZipEntry::UNKNOWN; } - } elseif (@filesize($filename) >= 1024) { - $compressionMethod = self::METHOD_DEFLATED; + } elseif (@filesize($filename) >= 512) { + $compressionMethod = ZipEntry::UNKNOWN; } else { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } } elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); @@ -461,9 +453,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $localName = basename($filename); } $this->addFromStream($handle, $localName, $compressionMethod); - $this->centralDirectory - ->getModifiedEntry($localName) - ->setTime(filemtime($filename)); + $this->zipModel->getEntry($localName)->setTime(filemtime($filename)); return $this; } @@ -475,12 +465,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFromStream($stream, $localName, $compressionMethod = null) { @@ -494,10 +484,10 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $fstat = fstat($stream); $length = $fstat['size']; if (null === $compressionMethod) { - if ($length >= 1024) { - $compressionMethod = self::METHOD_DEFLATED; + if ($length >= 512) { + $compressionMethod = ZipEntry::UNKNOWN; } else { - $compressionMethod = self::METHOD_STORED; + $compressionMethod = ZipFileInterface::METHOD_STORED; } } elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); @@ -506,13 +496,13 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $mode = sprintf('%o', $fstat['mode']); $externalAttributes = (octdec($mode) & 0xffff) << 16; - $entry = new ZipNewStreamEntry($stream); + $entry = new ZipNewEntry($stream); $entry->setName($localName); $entry->setMethod($compressionMethod); $entry->setTime(time()); $entry->setExternalAttributes($externalAttributes); - $this->centralDirectory->putInModified($localName, $entry); + $this->zipModel->addEntry($entry); return $this; } @@ -520,7 +510,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Add an empty directory in the zip archive. * * @param string $dirName - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ public function addEmptyDir($dirName) @@ -532,33 +522,19 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $dirName = rtrim($dirName, '/') . '/'; $externalAttributes = 040755 << 16; - $entry = new ZipNewEmptyDirEntry(); + $entry = new ZipNewEntry(); $entry->setName($dirName); $entry->setTime(time()); - $entry->setMethod(self::METHOD_STORED); + $entry->setMethod(ZipFileInterface::METHOD_STORED); $entry->setSize(0); $entry->setCompressedSize(0); $entry->setCrc(0); $entry->setExternalAttributes($externalAttributes); - $this->centralDirectory->putInModified($dirName, $entry); + $this->zipModel->addEntry($entry); return $this; } - /** - * 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) - { - foreach ($mapData as $localName => $content) { - $this[$localName] = $content; - } - } - /** * Add directory not recursively to the zip archive. * @@ -567,7 +543,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ public function addDir($inputDir, $localPath = "/", $compressionMethod = null) @@ -593,12 +569,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addDirRecursive($inputDir, $localPath = "/", $compressionMethod = null) { @@ -623,19 +599,18 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipUnsupportMethod - * @see ZipFile::METHOD_STORED - * @see ZipFile::METHOD_DEFLATED - * @see ZipFile::METHOD_BZIP2 + * @see ZipFileInterface::METHOD_STORED + * @see ZipFileInterface::METHOD_DEFLATED + * @see ZipFileInterface::METHOD_BZIP2 */ public function addFilesFromIterator( \Iterator $iterator, $localPath = '/', $compressionMethod = null - ) - { + ) { $localPath = (string)$localPath; if (null !== $localPath && 0 !== strlen($localPath)) { $localPath = rtrim($localPath, '/'); @@ -690,7 +665,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ @@ -699,24 +674,6 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this->addGlob($inputDir, $globPattern, $localPath, false, $compressionMethod); } - /** - * 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 ZipFile - * @throws InvalidArgumentException - * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax - */ - public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null) - { - return $this->addGlob($inputDir, $globPattern, $localPath, true, $compressionMethod); - } - /** * Add files from glob pattern. * @@ -727,7 +684,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ @@ -737,8 +694,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $localPath = '/', $recursive = true, $compressionMethod = null - ) - { + ) { $inputDir = (string)$inputDir; if (null === $inputDir || 0 === strlen($inputDir)) { throw new InvalidArgumentException('Input dir empty'); @@ -779,6 +735,24 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this; } + /** + * 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 + * @throws InvalidArgumentException + * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax + */ + public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null) + { + return $this->addGlob($inputDir, $globPattern, $localPath, true, $compressionMethod); + } + /** * Add files from regex pattern. * @@ -788,7 +762,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @internal param bool $recursive Recursive search. */ public function addFilesFromRegex($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null) @@ -796,24 +770,6 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this->addRegex($inputDir, $regexPattern, $localPath, false, $compressionMethod); } - /** - * 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 ZipFile - * @internal param bool $recursive Recursive search. - */ - public function addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null) - { - return $this->addRegex($inputDir, $regexPattern, $localPath, true, $compressionMethod); - } - - /** * Add files from regex pattern. * @@ -824,7 +780,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * @param int|null $compressionMethod Compression method. * Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2. * If null, then auto choosing method. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ private function addRegex( @@ -833,8 +789,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $localPath = "/", $recursive = true, $compressionMethod = null - ) - { + ) { $regexPattern = (string)$regexPattern; if (empty($regexPattern)) { throw new InvalidArgumentException("regex pattern empty"); @@ -874,12 +829,43 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this; } + /** + * 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) + { + return $this->addRegex($inputDir, $regexPattern, $localPath, true, $compressionMethod); + } + + /** + * 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) + { + foreach ($mapData as $localName => $content) { + $this[$localName] = $content; + } + } + /** * Rename the entry. * * @param string $oldName Old entry name. * @param string $newName New entry name. - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipNotFoundEntry */ @@ -888,7 +874,9 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (null === $oldName || null === $newName) { throw new InvalidArgumentException("name is null"); } - $this->centralDirectory->rename($oldName, $newName); + if ($oldName !== $newName) { + $this->zipModel->renameEntry($oldName, $newName); + } return $this; } @@ -896,13 +884,15 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Delete entry by name. * * @param string $entryName Zip Entry name. - * @return ZipFile + * @return ZipFileInterface * @throws ZipNotFoundEntry If entry not found. */ public function deleteFromName($entryName) { $entryName = (string)$entryName; - $this->centralDirectory->deleteEntry($entryName); + if (!$this->zipModel->deleteEntry($entryName)) { + throw new ZipNotFoundEntry("Entry " . $entryName . ' not found!'); + } return $this; } @@ -910,7 +900,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Delete entries by glob pattern. * * @param string $globPattern Glob pattern - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ @@ -928,7 +918,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Delete entries by regex pattern. * * @param string $regexPattern Regex pattern - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ public function deleteFromRegex($regexPattern) @@ -936,17 +926,17 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (null === $regexPattern || !is_string($regexPattern) || empty($regexPattern)) { throw new InvalidArgumentException("Regex pattern is empty."); } - $this->centralDirectory->deleteEntriesFromRegex($regexPattern); + $this->matcher()->match($regexPattern)->delete(); return $this; } /** * Delete all entries - * @return ZipFile + * @return ZipFileInterface */ public function deleteAll() { - $this->centralDirectory->deleteAll(); + $this->zipModel->deleteAll(); return $this; } @@ -954,43 +944,239 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Set compression level for new entries. * * @param int $compressionLevel - * @see ZipFile::LEVEL_DEFAULT_COMPRESSION - * @see ZipFile::LEVEL_BEST_SPEED - * @see ZipFile::LEVEL_BEST_COMPRESSION + * @return ZipFileInterface + * @throws InvalidArgumentException + * @see ZipFileInterface::LEVEL_DEFAULT_COMPRESSION + * @see ZipFileInterface::LEVEL_SUPER_FAST + * @see ZipFileInterface::LEVEL_FAST + * @see ZipFileInterface::LEVEL_BEST_COMPRESSION */ - public function setCompressionLevel($compressionLevel = self::LEVEL_DEFAULT_COMPRESSION) + public function setCompressionLevel($compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION) { - $this->centralDirectory->setCompressionLevel($compressionLevel); + if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION || + $compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION + ) { + throw new InvalidArgumentException('Invalid compression level. Minimum level ' . + ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION); + } + $this->matcher()->all()->invoke(function ($entry) use ($compressionLevel) { + $this->setCompressionLevelEntry($entry, $compressionLevel); + }); + return $this; } /** + * @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) + { + if (null !== $compressionLevel) { + if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION || + $compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION + ) { + throw new InvalidArgumentException('Invalid compression level. Minimum level ' . + ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION); + } + $entry = $this->zipModel->getEntry($entryName); + if ($entry->getCompressionLevel() !== $compressionLevel) { + $entry = $this->zipModel->getEntryForChanges($entry); + $entry->setCompressionLevel($compressionLevel); + } + } + return $this; + } + + /** + * @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) + { + if (!in_array($compressionMethod, self::$allowCompressionMethods, true)) { + throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod); + } + $entry = $this->zipModel->getEntry($entryName); + if ($entry->getMethod() !== $compressionMethod) { + $this->zipModel + ->getEntryForChanges($entry) + ->setMethod($compressionMethod); + } + return $this; + } + + /** + * 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) { - $this->centralDirectory->setZipAlign($align); + $this->zipModel->setZipAlign($align); + return $this; + } + + /** + * Set password to all input encrypted entries. + * + * @param string $password Password + * @return ZipFileInterface + * @deprecated using ZipFileInterface::setReadPassword() + */ + public function withReadPassword($password) + { + return $this->setReadPassword($password); + } + + /** + * Set password to all input encrypted entries. + * + * @param string $password Password + * @return ZipFileInterface + */ + public function setReadPassword($password) + { + $this->zipModel->setReadPassword($password); + return $this; + } + + /** + * Set password to concrete input entry. + * + * @param string $entryName + * @param string $password Password + * @return ZipFileInterface + */ + public function setReadPasswordEntry($entryName, $password) + { + $this->zipModel->setReadPasswordEntry($entryName, $password); + return $this; } /** * Set password for all entries for update. * * @param string $password If password null then encryption clear - * @param int $encryptionMethod Encryption method - * @return ZipFile + * @param int|null $encryptionMethod Encryption method + * @return ZipFileInterface + * @deprecated using ZipFileInterface::setPassword() */ - public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES) + public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256) { - $this->centralDirectory->setNewPassword($password, $encryptionMethod); + return $this->setPassword($password, $encryptionMethod); + } + + /** + * Set password for zip archive + * + * @param string $password + * @param int|null $encryptionMethod Encryption method + * @return ZipFileInterface + * @throws ZipException + */ + public function setPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256) + { + $this->zipModel->setWritePassword($password); + if (null !== $encryptionMethod) { + if (!in_array($encryptionMethod, self::$allowEncryptionMethods)) { + throw new ZipException('Invalid encryption method'); + } + $this->zipModel->setEncryptionMethod($encryptionMethod); + } + return $this; + } + + /** + * @param string $entryName + * @param string|null $password + * @param int|null $encryptionMethod + * @return ZipFileInterface + * @throws ZipException + */ + public function setPasswordEntry($entryName, $password, $encryptionMethod = null) + { + if (null !== $encryptionMethod) { + if (!in_array($encryptionMethod, self::$allowEncryptionMethods)) { + throw new ZipException('Invalid encryption method'); + } + } + $this->matcher()->add($entryName)->setPassword($password, $encryptionMethod); return $this; } /** * Remove password for all entries for update. - * @return ZipFile + * @return ZipFileInterface + * @deprecated using ZipFileInterface::removePassword() */ public function withoutPassword() { - $this->centralDirectory->setNewPassword(null); + return $this->removePassword(); + } + + /** + * Remove password for all entries for update. + * @return ZipFileInterface + */ + public function removePassword() + { + $this->zipModel->removePassword(); + return $this; + } + + /** + * Remove password for concrete entry. + * @param string $entryName + * @return ZipFileInterface + */ + public function removePasswordEntry($entryName) + { + $this->zipModel->removePasswordEntry($entryName); + return $this; + } + + /** + * Undo all changes done in the archive + * @return ZipFileInterface + */ + public function unchangeAll() + { + $this->zipModel->unchangeAll(); + return $this; + } + + /** + * Undo change archive comment + * @return ZipFileInterface + */ + public function unchangeArchiveComment() + { + $this->zipModel->unchangeArchiveComment(); + return $this; + } + + /** + * 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) + { + $this->zipModel->unchangeEntry($entry); return $this; } @@ -998,6 +1184,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator * Save as file. * * @param string $filename Output filename + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipException */ @@ -1014,12 +1201,14 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (!@rename($tempFilename, $filename)) { throw new ZipException('Can not move ' . $tempFilename . ' to ' . $filename); } + return $this; } /** * Save as stream. * * @param resource $handle Output stream resource + * @return ZipFileInterface * @throws ZipException */ public function saveAsStream($handle) @@ -1028,8 +1217,9 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator throw new InvalidArgumentException('handle is not resource'); } ftruncate($handle, 0); - $this->centralDirectory->writeArchive($handle); + $this->writeZipToStream($handle); fclose($handle); + return $this; } /** @@ -1061,11 +1251,60 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $this->close(); header("Content-Type: " . $mimeType); - header("Content-Disposition: attachment; filename=" . rawurlencode($outputFilename)); + header('Content-Disposition: attachment; filename="' . $outputFilename . '"'); header("Content-Length: " . strlen($content)); exit($content); } + /** + * Output .ZIP archive as PSR-Message Response. + * + * @param ResponseInterface $response + * @param string $outputFilename + * @param string|null $mimeType + * @return ResponseInterface + * @throws InvalidArgumentException + */ + public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null) + { + $outputFilename = (string)$outputFilename; + if (strlen($outputFilename) === 0) { + throw new InvalidArgumentException("Output filename is empty."); + } + if (empty($mimeType) || !is_string($mimeType)) { + $ext = strtolower(pathinfo($outputFilename, PATHINFO_EXTENSION)); + + if (!empty($ext) && isset(self::$defaultMimeTypes[$ext])) { + $mimeType = self::$defaultMimeTypes[$ext]; + } else { + $mimeType = self::$defaultMimeTypes['zip']; + } + } + $outputFilename = basename($outputFilename); + + if (!($handle = fopen('php://memory', 'w+b'))) { + throw new InvalidArgumentException("Memory can not open from write."); + } + $this->writeZipToStream($handle); + rewind($handle); + + $stream = new ResponseStream($handle); + $response->withHeader('Content-Type', $mimeType); + $response->withHeader('Content-Disposition', 'attachment; filename="' . $outputFilename . '"'); + $response->withHeader('Content-Length', $stream->getSize()); + $response->withBody($stream); + return $response; + } + + /** + * @param $handle + */ + protected function writeZipToStream($handle) + { + $output = new ZipOutputStream($handle, $this->zipModel); + $output->writeZip(); + } + /** * Returns the zip archive as a string. * @return string @@ -1076,7 +1315,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (!($handle = fopen('php://memory', 'w+b'))) { throw new InvalidArgumentException("Memory can not open from write."); } - $this->centralDirectory->writeArchive($handle); + $this->writeZipToStream($handle); rewind($handle); $content = stream_get_contents($handle); fclose($handle); @@ -1084,8 +1323,20 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator } /** - * Rewrite and reopen zip archive. - * @return ZipFile + * Close zip archive and release input stream. + */ + public function close() + { + if (null !== $this->inputStream) { + $this->inputStream->close(); + $this->inputStream = null; + $this->zipModel = new ZipModel(); + } + } + + /** + * Save and reopen zip archive. + * @return ZipFileInterface * @throws ZipException */ public function rewrite() @@ -1093,7 +1344,7 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator if (null === $this->inputStream) { throw new ZipException('input stream is null'); } - $meta = stream_get_meta_data($this->inputStream); + $meta = stream_get_meta_data($this->inputStream->getStream()); $content = $this->outputAsString(); $this->close(); if ('plainfile' === $meta['wrapper_type']) { @@ -1108,53 +1359,14 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this->openFromString($content); } - /** - * Close zip archive and release input stream. - */ - public function close() - { - if (null !== $this->inputStream) { - fclose($this->inputStream); - $this->inputStream = null; - } - if (null !== $this->centralDirectory) { - $this->centralDirectory->release(); - $this->centralDirectory = null; - } - } - /** * Release all resources */ - function __destruct() + public function __destruct() { $this->close(); } - /** - * Whether a offset exists - * @link http://php.net/manual/en/arrayaccess.offsetexists.php - * @param string $entryName An offset to check for. - * @return boolean true on success or false on failure. - * The return value will be casted to boolean if non-boolean was returned. - */ - public function offsetExists($entryName) - { - return isset($this->centralDirectory->getEntries()[$entryName]); - } - - /** - * Offset to retrieve - * @link http://php.net/manual/en/arrayaccess.offsetget.php - * @param string $entryName The offset to retrieve. - * @return string|null - * @throws ZipNotFoundEntry - */ - public function offsetGet($entryName) - { - return $this->centralDirectory->getEntry($entryName)->getEntryContent(); - } - /** * Offset to set * @link http://php.net/manual/en/arrayaccess.offsetset.php @@ -1183,10 +1395,12 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator $this->addFile($contents->getPathname(), $entryName); return; } - $contents = (string)$contents; - if ('/' === $entryName[strlen($entryName) - 1]) { + if (StringUtil::endsWith($entryName, '/')) { $this->addEmptyDir($entryName); + } elseif (is_resource($contents)) { + $this->addFromStream($contents, $entryName); } else { + $contents = (string)$contents; $this->addFromString($entryName, $contents); } } @@ -1214,14 +1428,15 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator } /** - * 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 + * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param string $entryName The offset to retrieve. + * @return string|null + * @throws ZipNotFoundEntry */ - public function next() + public function offsetGet($entryName) { - next($this->centralDirectory->getEntries()); + return $this->getEntryContents($entryName); } /** @@ -1232,7 +1447,18 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function key() { - return key($this->centralDirectory->getEntries()); + return key($this->zipModel->getEntries()); + } + + /** + * 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->zipModel->getEntries()); } /** @@ -1247,6 +1473,18 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator return $this->offsetExists($this->key()); } + /** + * Whether a offset exists + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param string $entryName An offset to check for. + * @return boolean true on success or false on failure. + * The return value will be casted to boolean if non-boolean was returned. + */ + public function offsetExists($entryName) + { + return $this->hasEntry($entryName); + } + /** * Rewind the Iterator to the first element * @link http://php.net/manual/en/iterator.rewind.php @@ -1255,6 +1493,6 @@ class ZipFile implements \Countable, \ArrayAccess, \Iterator */ public function rewind() { - reset($this->centralDirectory->getEntries()); + reset($this->zipModel->getEntries()); } -} \ No newline at end of file +} diff --git a/src/PhpZip/ZipFileInterface.php b/src/PhpZip/ZipFileInterface.php new file mode 100644 index 0000000..09ddd23 --- /dev/null +++ b/src/PhpZip/ZipFileInterface.php @@ -0,0 +1,630 @@ +openFile($filename); + foreach ($zipFile as $name => $contents) { + $info = $zipFile->getEntryInfo($name); + self::assertEquals(strlen($contents), $info->getSize()); + } + $zipFile->close(); + + self::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 + */ + 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(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + self::assertCount(2, $zipFile); + self::assertTrue(isset($zipFile['1.txt'])); + self::assertTrue(isset($zipFile['2.txt'])); + self::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 + */ + public function testBug40228($filename) + { + self::assertTrue(mkdir($this->outputDirname, 0755, true)); + + $zipFile = new ZipFile(); + $zipFile->openFile($filename); + $zipFile->extractTo($this->outputDirname); + $zipFile->close(); + + self::assertTrue(is_dir($this->outputDirname . '/test/empty')); + } + + public function provideBug40228() + { + return [ + [__DIR__ . '/php-zip-ext-test-resources/bug40228.zip'], + [__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 (expected CRC32 value 0xc935c834, but is actually 0x76301511) + */ + 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 Bad password for entry bug70752.txt + */ + public function testBug70752() + { + $filename = __DIR__ . '/php-zip-ext-test-resources/bug70752.zip'; + + self::assertTrue(mkdir($this->outputDirname, 0755, true)); + + $zipFile = new ZipFile(); + try { + $zipFile->openFile($filename); + $zipFile->setReadPassword('bar'); + $zipFile->extractTo($this->outputDirname); + self::markTestIncomplete('failed test'); + } catch (ZipAuthenticationException $exception) { + self::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 + */ + 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); + self::assertTrue($info->getSize() > 0); + + $contents = $zipFile[$entryName]; + self::assertEquals(strlen($contents), $info->getSize()); + + $zipFile->close(); + } +} diff --git a/tests/PhpZip/ZipFileAddDirTest.php b/tests/PhpZip/ZipFileAddDirTest.php index f4a4753..039c1c3 100644 --- a/tests/PhpZip/ZipFileAddDirTest.php +++ b/tests/PhpZip/ZipFileAddDirTest.php @@ -1,4 +1,5 @@ 'Hidden file', 'text file.txt' => 'Text file', 'Текстовый документ.txt' => 'Текстовый документ', @@ -52,7 +53,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 = empty($localPath) ? "" : $localPath . '/'; @@ -134,6 +135,29 @@ class ZipFileAddDirTest extends ZipTestCase $zipFile->close(); } + public function testAddFilesFromIteratorEmptyLocalPath() + { + $localPath = ''; + + $directoryIterator = new \DirectoryIterator($this->outputDirname); + + $zipFile = new ZipFile(); + $zipFile->addFilesFromIterator($directoryIterator, $localPath); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + self::assertFilesResult($zipFile, [ + '.hidden', + 'text file.txt', + 'Текстовый документ.txt', + 'empty dir/', + ]); + $zipFile->close(); + } + public function testAddFilesFromRecursiveIterator() { $localPath = 'to/project'; @@ -182,7 +206,8 @@ class ZipFileAddDirTest extends ZipTestCase $zipFile->close(); } - public function testAddFilesFromIteratorWithIgnoreFiles(){ + public function testAddFilesFromIteratorWithIgnoreFiles() + { $localPath = 'to/project'; $ignoreFiles = [ 'Текстовый документ.txt', @@ -207,7 +232,8 @@ class ZipFileAddDirTest extends ZipTestCase $zipFile->close(); } - public function testAddFilesFromRecursiveIteratorWithIgnoreFiles(){ + public function testAddFilesFromRecursiveIteratorWithIgnoreFiles() + { $localPath = 'to/project'; $ignoreFiles = [ '.hidden', @@ -354,6 +380,4 @@ class ZipFileAddDirTest extends ZipTestCase self::assertFilesResult($zipFile, array_keys(self::$files), $localPath); $zipFile->close(); } - - -} \ No newline at end of file +} diff --git a/tests/PhpZip/ZipFileTest.php b/tests/PhpZip/ZipFileTest.php index a9810e9..aa3b5d7 100644 --- a/tests/PhpZip/ZipFileTest.php +++ b/tests/PhpZip/ZipFileTest.php @@ -1,10 +1,12 @@ openFromStream("stream resource"); } + /** + * @expectedException \PhpZip\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid resource type - gd. + */ + public function testOpenFromStreamInvalidResourceType2() + { + $zipFile = new ZipFile(); + if (!extension_loaded("gd")) { + $this->markTestSkipped('not extension gd'); + } + $zipFile->openFromStream(imagecreate(1, 1)); + } + + /** + * @expectedException \PhpZip\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid stream type - dir. + */ + public function testOpenFromStreamInvalidResourceType3() + { + $zipFile = new ZipFile(); + $zipFile->openFromStream(opendir(__DIR__)); + } + /** * @expectedException \PhpZip\Exception\InvalidArgumentException * @expectedExceptionMessage Resource cannot seekable stream. @@ -169,8 +195,8 @@ class ZipFileTest extends ZipTestCase $zipFile = new ZipFile(); $zipFile ->addFromString('file', 'content') - ->saveAsFile($this->outputFilename); - $zipFile->close(); + ->saveAsFile($this->outputFilename) + ->close(); $handle = fopen($this->outputFilename, 'rb'); $zipFile->openFromStream($handle); @@ -186,16 +212,18 @@ class ZipFileTest extends ZipTestCase public function testEmptyArchive() { $zipFile = new ZipFile(); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); self::assertCorrectEmptyZip($this->outputFilename); self::assertTrue(mkdir($this->outputDirname, 0755, true)); $zipFile->openFile($this->outputFilename); self::assertEquals($zipFile->count(), 0); - $zipFile->extractTo($this->outputDirname); - $zipFile->close(); + $zipFile + ->extractTo($this->outputDirname) + ->close(); self::assertTrue(FilesUtil::isEmptyDir($this->outputDirname)); } @@ -213,18 +241,23 @@ class ZipFileTest extends ZipTestCase $fileExpected = $this->outputDirname . DIRECTORY_SEPARATOR . 'file_expected.zip'; $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); - $zipFile->saveAsFile($fileActual); + $zipFile->addDirRecursive(__DIR__.'/../../src'); + $sourceCount = $zipFile->count(); + self::assertTrue($sourceCount > 0); + $zipFile + ->saveAsFile($fileActual) + ->close(); self::assertCorrectZipArchive($fileActual); - $zipFile->close(); - $zipFile->openFile($fileActual); - $zipFile->saveAsFile($fileExpected); + $zipFile + ->openFile($fileActual) + ->saveAsFile($fileExpected); self::assertCorrectZipArchive($fileExpected); $zipFileExpected = new ZipFile(); $zipFileExpected->openFile($fileExpected); + self::assertEquals($zipFile->count(), $sourceCount); self::assertEquals($zipFileExpected->count(), $zipFile->count()); self::assertEquals($zipFileExpected->getListFiles(), $zipFile->getListFiles()); @@ -242,13 +275,13 @@ class ZipFileTest extends ZipTestCase * @see ZipOutputFile::addFromString() * @see ZipOutputFile::addFromFile() * @see ZipOutputFile::addFromStream() - * @see ZipFile::getEntryContent() + * @see ZipFile::getEntryContents() */ public function testCreateArchiveAndAddFiles() { $outputFromString = file_get_contents(__FILE__); $outputFromString2 = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'README.md'); - $outputFromFile = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'bootstrap.xml'); + $outputFromFile = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'phpunit.xml'); $outputFromStream = file_get_contents(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'composer.json'); $filenameFromString = basename(__FILE__); @@ -266,15 +299,18 @@ class ZipFileTest extends ZipTestCase fwrite($tempStream, $outputFromStream); $zipFile = new ZipFile; - $zipFile->addFromString($filenameFromString, $outputFromString); - $zipFile->addFile($tempFile, $filenameFromFile); - $zipFile->addFromStream($tempStream, $filenameFromStream); - $zipFile->addEmptyDir($emptyDirName); + $zipFile + ->addFromString($filenameFromString, $outputFromString) + ->addFile($tempFile, $filenameFromFile) + ->addFromStream($tempStream, $filenameFromStream) + ->addEmptyDir($emptyDirName); $zipFile[$filenameFromString2] = $outputFromString2; $zipFile[$emptyDirName2] = null; $zipFile[$emptyDirName3] = 'this content ignoring'; - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); + self::assertEquals(count($zipFile), 7); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); unlink($tempFile); self::assertCorrectZipArchive($this->outputFilename); @@ -304,6 +340,18 @@ class ZipFileTest extends ZipTestCase $zipFile->close(); } + public function testEmptyContent() + { + $zipFile = new ZipFile(); + $zipFile['file'] = ''; + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + self::assertEquals($zipFile['file'], ''); + $zipFile->close(); + } + /** * Test compression method from image file. */ @@ -330,7 +378,7 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $info = $zipFile->getEntryInfo($basename); - self::assertEquals($info->getMethod(), 'No compression'); + self::assertEquals($info->getMethodName(), 'No compression'); $zipFile->close(); } @@ -343,7 +391,7 @@ class ZipFileTest extends ZipTestCase $newName = 'tests/' . $oldName; $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); + $zipFile->addDir(__DIR__); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -405,7 +453,6 @@ class ZipFileTest extends ZipTestCase /** * @expectedException \PhpZip\Exception\ZipNotFoundEntry - * @expectedExceptionMessage Not found entry */ public function testRenameEntryNotFound() { @@ -465,7 +512,6 @@ class ZipFileTest extends ZipTestCase /** * @expectedException \PhpZip\Exception\ZipNotFoundEntry - * @expectedExceptionMessage Not found entry entry */ public function testDeleteFromNameNotFoundEntry() { @@ -481,22 +527,32 @@ class ZipFileTest extends ZipTestCase $inputDir = dirname(dirname(__DIR__)); $zipFile = new ZipFile(); - $zipFile->addFilesFromGlobRecursive($inputDir, '**.{php,xml,json}', '/'); + $zipFile->addFilesFromGlobRecursive($inputDir, '**.{xml,json,md}', '/'); + self::assertTrue(isset($zipFile['composer.json'])); + self::assertTrue(isset($zipFile['phpunit.xml'])); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); + self::assertTrue(isset($zipFile['composer.json'])); + self::assertTrue(isset($zipFile['phpunit.xml'])); $zipFile->deleteFromGlob('**.{xml,json}'); + self::assertFalse(isset($zipFile['composer.json'])); + self::assertFalse(isset($zipFile['phpunit.xml'])); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); - self::assertFalse(isset($zipFile['composer.json'])); - self::assertFalse(isset($zipFile['bootstrap.xml'])); + self::assertTrue($zipFile->count() > 0); + + foreach ($zipFile->getListFiles() as $name) { + self::assertStringEndsWith('.md', $name); + } + $zipFile->close(); } @@ -528,7 +584,7 @@ class ZipFileTest extends ZipTestCase $inputDir = dirname(dirname(__DIR__)); $zipFile = new ZipFile(); - $zipFile->addFilesFromRegexRecursive($inputDir, '~\.(xml|php|json)$~i', 'Path'); + $zipFile->addFilesFromRegexRecursive($inputDir, '~\.(xml|json)$~i', 'Path'); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -546,7 +602,7 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); self::assertFalse(isset($zipFile['Path/composer.json'])); self::assertFalse(isset($zipFile['Path/test.txt'])); - self::assertTrue(isset($zipFile['Path/bootstrap.xml'])); + self::assertTrue(isset($zipFile['Path/phpunit.xml'])); $zipFile->close(); } @@ -576,7 +632,8 @@ class ZipFileTest extends ZipTestCase public function testDeleteAll() { $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); + $zipFile->addDirRecursive(dirname(dirname(__DIR__)) .DIRECTORY_SEPARATOR. 'src'); + self::assertTrue($zipFile->count() > 0); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -732,8 +789,7 @@ class ZipFileTest extends ZipTestCase } /** - * @expectedException \PhpZip\Exception\ZipException - * @expectedExceptionMessage Not found entry + * @expectedException \PhpZip\Exception\ZipNotFoundEntry */ public function testSetEntryCommentNotFoundEntry() { @@ -749,19 +805,19 @@ class ZipFileTest extends ZipTestCase $entries = [ '1' => [ 'data' => CryptoUtil::randomBytes(255), - 'method' => ZipFile::METHOD_STORED, + 'method' => ZipFileInterface::METHOD_STORED, 'expected' => 'No compression', ], '2' => [ 'data' => CryptoUtil::randomBytes(255), - 'method' => ZipFile::METHOD_DEFLATED, + 'method' => ZipFileInterface::METHOD_DEFLATED, 'expected' => 'Deflate', ], ]; if (extension_loaded("bz2")) { $entries['3'] = [ 'data' => CryptoUtil::randomBytes(255), - 'method' => ZipFile::METHOD_BZIP2, + 'method' => ZipFileInterface::METHOD_BZIP2, 'expected' => 'Bzip2', ]; } @@ -776,12 +832,12 @@ class ZipFileTest extends ZipTestCase self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); - $zipFile->setCompressionLevel(ZipFile::LEVEL_BEST_COMPRESSION); + $zipFile->setCompressionLevel(ZipFileInterface::LEVEL_BEST_COMPRESSION); $zipAllInfo = $zipFile->getAllInfo(); foreach ($zipAllInfo as $entryName => $info) { self::assertEquals($zipFile[$entryName], $entries[$entryName]['data']); - self::assertEquals($info->getMethod(), $entries[$entryName]['expected']); + self::assertEquals($info->getMethodName(), $entries[$entryName]['expected']); $entryInfo = $zipFile->getEntryInfo($entryName); self::assertEquals($entryInfo, $info); } @@ -959,105 +1015,6 @@ class ZipFileTest extends ZipTestCase $zipFile->extractTo($this->outputDirname); } - /** - * Test archive password. - */ - public function testSetPassword() - { - $password = base64_encode(CryptoUtil::randomBytes(100)); - $badPassword = "sdgt43r23wefe"; - - // create encryption password with ZipCrypto - $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); - $zipFile->withNewPassword($password, ZipFile::ENCRYPTION_METHOD_TRADITIONAL); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); - - self::assertCorrectZipArchive($this->outputFilename, $password); - - // check bad password for ZipCrypto - $zipFile->openFile($this->outputFilename); - $zipFile->withReadPassword($badPassword); - foreach ($zipFile->getListFiles() as $entryName) { - try { - $zipFile[$entryName]; - self::fail("Expected Exception has not been raised."); - } catch (ZipAuthenticationException $ae) { - self::assertNotNull($ae); - } - } - - // check correct password for ZipCrypto - $zipFile->withReadPassword($password); - foreach ($zipFile->getAllInfo() as $info) { - self::assertTrue($info->isEncrypted()); - self::assertContains('ZipCrypto', $info->getMethod()); - $decryptContent = $zipFile[$info->getPath()]; - self::assertNotEmpty($decryptContent); - self::assertContains('withNewPassword($password, ZipFile::ENCRYPTION_METHOD_WINZIP_AES); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); - - self::assertCorrectZipArchive($this->outputFilename, $password); - - // check from WinZip AES encryption - $zipFile->openFile($this->outputFilename); - // set bad password WinZip AES - $zipFile->withReadPassword($badPassword); - foreach ($zipFile->getListFiles() as $entryName) { - try { - $zipFile[$entryName]; - self::fail("Expected Exception has not been raised."); - } catch (ZipAuthenticationException $ae) { - self::assertNotNull($ae); - } - } - - // set correct password WinZip AES - $zipFile->withReadPassword($password); - foreach ($zipFile->getAllInfo() as $info) { - self::assertTrue($info->isEncrypted()); - self::assertContains('WinZip', $info->getMethod()); - $decryptContent = $zipFile[$info->getPath()]; - self::assertNotEmpty($decryptContent); - self::assertContains('addFromString('file1', ''); - $zipFile->withoutPassword(); - $zipFile->addFromString('file2', ''); - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); - - self::assertCorrectZipArchive($this->outputFilename); - - // check remove password - $zipFile->openFile($this->outputFilename); - foreach ($zipFile->getAllInfo() as $info) { - self::assertFalse($info->isEncrypted()); - } - $zipFile->close(); - } - - /** - * @expectedException \PhpZip\Exception\ZipException - * @expectedExceptionMessage Invalid encryption method - */ - public function testSetEncryptionMethodInvalid() - { - $zipFile = new ZipFile(); - $encryptionMethod = 9999; - $zipFile->withNewPassword('pass', $encryptionMethod); - $zipFile['entry'] = 'content'; - $zipFile->outputAsString(); - } - /** * @expectedException \PhpZip\Exception\InvalidArgumentException * @expectedExceptionMessage entryName is null @@ -1100,7 +1057,7 @@ class ZipFileTest extends ZipTestCase /** * @expectedException \PhpZip\Exception\ZipUnsupportMethod - * @expectedExceptionMessage Unsupported method + * @expectedExceptionMessage Unsupported compression method */ public function testAddFromStringUnsupportedMethod() { @@ -1141,8 +1098,8 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $infoStored = $zipFile->getEntryInfo(basename($fileStored)); $infoDeflated = $zipFile->getEntryInfo(basename($fileDeflated)); - self::assertEquals($infoStored->getMethod(), 'No compression'); - self::assertEquals($infoDeflated->getMethod(), 'Deflate'); + self::assertEquals($infoStored->getMethodName(), 'No compression'); + self::assertEquals($infoDeflated->getMethodName(), 'Deflate'); $zipFile->close(); } @@ -1153,6 +1110,7 @@ class ZipFileTest extends ZipTestCase public function testAddFromStreamInvalidResource() { $zipFile = new ZipFile(); + /** @noinspection PhpParamsInspection */ $zipFile->addFromStream("invalid resource", "name"); } @@ -1206,8 +1164,8 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); $infoStored = $zipFile->getEntryInfo(basename($fileStored)); $infoDeflated = $zipFile->getEntryInfo(basename($fileDeflated)); - self::assertEquals($infoStored->getMethod(), 'No compression'); - self::assertEquals($infoDeflated->getMethod(), 'Deflate'); + self::assertEquals($infoStored->getMethodName(), 'No compression'); + self::assertEquals($infoDeflated->getMethodName(), 'Deflate'); $zipFile->close(); } @@ -1521,6 +1479,7 @@ class ZipFileTest extends ZipTestCase public function testSaveAsStreamBadStream() { $zipFile = new ZipFile(); + /** @noinspection PhpParamsInspection */ $zipFile->saveAsStream("bad stream"); } @@ -1550,13 +1509,13 @@ class ZipFileTest extends ZipTestCase $files['file' . $i . '.txt'] = CryptoUtil::randomBytes(255); } - $methods = [ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED]; + $methods = [ZipFileInterface::METHOD_STORED, ZipFileInterface::METHOD_DEFLATED]; if (extension_loaded("bz2")) { - $methods[] = ZipFile::METHOD_BZIP2; + $methods[] = ZipFileInterface::METHOD_BZIP2; } $zipFile = new ZipFile(); - $zipFile->setCompressionLevel(ZipFile::LEVEL_BEST_SPEED); + $zipFile->setCompressionLevel(ZipFileInterface::LEVEL_BEST_SPEED); foreach ($files as $entryName => $content) { $zipFile->addFromString($entryName, $content, $methods[array_rand($methods)]); } @@ -1627,18 +1586,43 @@ class ZipFileTest extends ZipTestCase public function testArrayAccessAddFile() { $entryName = 'path/to/file.dat'; + $entryNameStream = 'path/to/' . basename(__FILE__); $zipFile = new ZipFile(); $zipFile[$entryName] = new \SplFileInfo(__FILE__); + $zipFile[$entryNameStream] = fopen(__FILE__, 'r'); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); self::assertCorrectZipArchive($this->outputFilename); $zipFile->openFile($this->outputFilename); - self::assertEquals(sizeof($zipFile), 1); + self::assertEquals(sizeof($zipFile), 2); self::assertTrue(isset($zipFile[$entryName])); + self::assertTrue(isset($zipFile[$entryNameStream])); self::assertEquals($zipFile[$entryName], file_get_contents(__FILE__)); + self::assertEquals($zipFile[$entryNameStream], file_get_contents(__FILE__)); + $zipFile->close(); + } + + public function testUnknownCompressionMethod() + { + $zipFile = new ZipFile(); + + $zipFile->addFromString('file', 'content', ZipEntry::UNKNOWN); + $zipFile->addFromString('file2', base64_encode(CryptoUtil::randomBytes(512)), ZipEntry::UNKNOWN); + + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'Unknown'); + self::assertEquals($zipFile->getEntryInfo('file2')->getMethodName(), 'Unknown'); + + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'No compression'); + self::assertEquals($zipFile->getEntryInfo('file2')->getMethodName(), 'Deflate'); + $zipFile->close(); } @@ -1700,8 +1684,10 @@ class ZipFileTest extends ZipTestCase $zipFile = new ZipFile(); $zipFile['file'] = 'content'; $zipFile['file2'] = 'content2'; - $zipFile->saveAsFile($this->outputFilename); - $zipFile->close(); + self::assertEquals(count($zipFile), 2); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); $md5file = md5_file($this->outputFilename); @@ -1710,7 +1696,7 @@ class ZipFileTest extends ZipTestCase self::assertTrue(isset($zipFile['file'])); self::assertTrue(isset($zipFile['file2'])); $zipFile['file3'] = 'content3'; - self::assertEquals(count($zipFile), 2); + self::assertEquals(count($zipFile), 3); $zipFile = $zipFile->rewrite(); self::assertEquals(count($zipFile), 3); self::assertTrue(isset($zipFile['file'])); @@ -1758,14 +1744,14 @@ class ZipFileTest extends ZipTestCase /** * Test zip alignment. */ - public function testZipAlign() + public function testZipAlignSourceZip() { $zipFile = new ZipFile(); for ($i = 0; $i < 100; $i++) { $zipFile->addFromString( 'entry' . $i . '.txt', CryptoUtil::randomBytes(mt_rand(100, 4096)), - ZipFile::METHOD_STORED + ZipFileInterface::METHOD_STORED ); } $zipFile->saveAsFile($this->outputFilename); @@ -1774,7 +1760,9 @@ class ZipFileTest extends ZipTestCase self::assertCorrectZipArchive($this->outputFilename); $result = self::doZipAlignVerify($this->outputFilename); - if ($result === null) return; // zip align not installed + if ($result === null) { + return; + } // zip align not installed // check not zip align self::assertFalse($result); @@ -1791,13 +1779,16 @@ class ZipFileTest extends ZipTestCase // check zip align self::assertTrue($result); + } + public function testZipAlignNewFiles() + { $zipFile = new ZipFile(); for ($i = 0; $i < 100; $i++) { $zipFile->addFromString( 'entry' . $i . '.txt', CryptoUtil::randomBytes(mt_rand(100, 4096)), - ZipFile::METHOD_STORED + ZipFileInterface::METHOD_STORED ); } $zipFile->setZipAlign(4); @@ -1807,10 +1798,306 @@ class ZipFileTest extends ZipTestCase self::assertCorrectZipArchive($this->outputFilename); $result = self::doZipAlignVerify($this->outputFilename); + if ($result === null) { + return; + } // zip align not installed // check not zip align self::assertTrue($result); } + 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(); + + self::assertCorrectZipArchive($this->outputFilename); + + $result = self::doZipAlignVerify($this->outputFilename); + if ($result === null) { + return; + } // zip align not installed + + // check not zip align + self::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(); + + self::assertCorrectZipArchive($this->outputFilename); + + $result = self::doZipAlignVerify($this->outputFilename, true); + self::assertNotNull($result); + + // check zip align + self::assertTrue($result); + } + + public function testFilename0() + { + $zipFile = new ZipFile(); + $zipFile[0] = 0; + self::assertTrue(isset($zipFile[0])); + self::assertTrue(isset($zipFile['0'])); + self::assertCount(1, $zipFile); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + self::assertTrue(isset($zipFile[0])); + self::assertTrue(isset($zipFile['0'])); + self::assertEquals($zipFile['0'], '0'); + self::assertCount(1, $zipFile); + $zipFile->close(); + + self::assertTrue(unlink($this->outputFilename)); + + $zipFile = new ZipFile(); + $zipFile->addFromString(0, 0); + self::assertTrue(isset($zipFile[0])); + self::assertTrue(isset($zipFile['0'])); + self::assertCount(1, $zipFile); + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); + + self::assertCorrectZipArchive($this->outputFilename); + } + + public function testPsrResponse() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 10; $i++) { + $zipFile[$i] = $i; + } + $filename = 'file.jar'; + $response = $this->getMock(ResponseInterface::class); + $response = $zipFile->outputAsResponse($response, $filename); + $this->assertInstanceOf(ResponseInterface::class, $response); + } + + /** + * @expectedException \PhpZip\Exception\InvalidArgumentException + * @expectedExceptionMessage Output filename is empty. + */ + public function testInvalidPsrResponse() + { + $zipFile = new ZipFile(); + $zipFile['file'] = 'content'; + $response = $this->getMock(ResponseInterface::class); + $zipFile->outputAsResponse($response, ''); + } + + public function testCompressionLevel() + { + $zipFile = new ZipFile(); + $zipFile + ->addFromString('file', 'content', ZipFileInterface::METHOD_DEFLATED) + ->setCompressionLevelEntry('file', ZipFileInterface::LEVEL_BEST_COMPRESSION) + ->addFromString('file2', 'content', ZipFileInterface::METHOD_DEFLATED) + ->setCompressionLevelEntry('file2', ZipFileInterface::LEVEL_FAST) + ->addFromString('file3', 'content', ZipFileInterface::METHOD_DEFLATED) + ->setCompressionLevelEntry('file3', ZipFileInterface::LEVEL_SUPER_FAST) + ->addFromString('file4', 'content', ZipFileInterface::METHOD_DEFLATED) + ->setCompressionLevelEntry('file4', ZipFileInterface::LEVEL_DEFAULT_COMPRESSION) + ->saveAsFile($this->outputFilename) + ->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + self::assertEquals($zipFile->getEntryInfo('file') + ->getCompressionLevel(), ZipFileInterface::LEVEL_BEST_COMPRESSION); + self::assertEquals($zipFile->getEntryInfo('file2') + ->getCompressionLevel(), ZipFileInterface::LEVEL_FAST); + self::assertEquals($zipFile->getEntryInfo('file3') + ->getCompressionLevel(), ZipFileInterface::LEVEL_SUPER_FAST); + self::assertEquals($zipFile->getEntryInfo('file4') + ->getCompressionLevel(), ZipFileInterface::LEVEL_DEFAULT_COMPRESSION); + $zipFile->close(); + } + + /** + * @expectedException \PhpZip\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid compression level + */ + public function testInvalidCompressionLevel() + { + $zipFile = new ZipFile(); + $zipFile->addFromString('file', 'content'); + $zipFile->setCompressionLevel(15); + } + + /** + * @expectedException \PhpZip\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid compression level + */ + public function testInvalidCompressionLevelEntry() + { + $zipFile = new ZipFile(); + $zipFile->addFromString('file', 'content'); + $zipFile->setCompressionLevelEntry('file', 15); + } + + public function testCompressionGlobal() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 10; $i++) { + $zipFile->addFromString('file' . $i, 'content', ZipFileInterface::METHOD_DEFLATED); + } + $zipFile + ->setCompressionLevel(ZipFileInterface::LEVEL_BEST_SPEED) + ->saveAsFile($this->outputFilename) + ->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + $zipFile->openFile($this->outputFilename); + $infoList = $zipFile->getAllInfo(); + array_walk($infoList, function (ZipInfo $zipInfo) { + self::assertEquals($zipInfo->getCompressionLevel(), ZipFileInterface::LEVEL_BEST_SPEED); + }); + $zipFile->close(); + } + + public function testCompressionMethodEntry() + { + $zipFile = new ZipFile(); + $zipFile->addFromString('file', 'content', ZipFileInterface::METHOD_STORED); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'No compression'); + $zipFile->setCompressionMethodEntry('file', ZipFileInterface::METHOD_DEFLATED); + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'Deflate'); + + $zipFile->rewrite(); + self::assertEquals($zipFile->getEntryInfo('file')->getMethodName(), 'Deflate'); + } + + /** + * @expectedException \PhpZip\Exception\ZipUnsupportMethod + * @expectedExceptionMessage Unsupported method + */ + public function testInvalidCompressionMethodEntry() + { + $zipFile = new ZipFile(); + $zipFile->addFromString('file', 'content', ZipFileInterface::METHOD_STORED); + $zipFile->setCompressionMethodEntry('file', 99); + } + + public function testUnchangeAll() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 10; $i++) { + $zipFile[$i] = $i; + } + $zipFile->setArchiveComment('comment'); + self::assertCount(10, $zipFile); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + $zipFile->saveAsFile($this->outputFilename); + + $zipFile->unchangeAll(); + self::assertCount(0, $zipFile); + self::assertEquals($zipFile->getArchiveComment(), null); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + self::assertCount(10, $zipFile); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + + for ($i = 10; $i < 100; $i++) { + $zipFile[$i] = $i; + } + $zipFile->setArchiveComment('comment 2'); + self::assertCount(100, $zipFile); + self::assertEquals($zipFile->getArchiveComment(), 'comment 2'); + + $zipFile->unchangeAll(); + self::assertCount(10, $zipFile); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + $zipFile->close(); + } + + public function testUnchangeArchiveComment() + { + $zipFile = new ZipFile(); + for ($i = 0; $i < 10; $i++) { + $zipFile[$i] = $i; + } + $zipFile->setArchiveComment('comment'); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + $zipFile->saveAsFile($this->outputFilename); + + $zipFile->unchangeArchiveComment(); + self::assertEquals($zipFile->getArchiveComment(), null); + $zipFile->close(); + + $zipFile->openFile($this->outputFilename); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + $zipFile->setArchiveComment('comment 2'); + self::assertEquals($zipFile->getArchiveComment(), 'comment 2'); + + $zipFile->unchangeArchiveComment(); + self::assertEquals($zipFile->getArchiveComment(), 'comment'); + $zipFile->close(); + } + + public function testUnchangeEntry() + { + $zipFile = new ZipFile(); + $zipFile['file 1'] = 'content 1'; + $zipFile['file 2'] = 'content 2'; + $zipFile + ->saveAsFile($this->outputFilename) + ->close(); + + $zipFile->openFile($this->outputFilename); + + $zipFile['file 1'] = 'modify content 1'; + $zipFile->setPasswordEntry('file 1', 'password'); + + self::assertEquals($zipFile['file 1'], 'modify content 1'); + self::assertTrue($zipFile->getEntryInfo('file 1')->isEncrypted()); + + self::assertEquals($zipFile['file 2'], 'content 2'); + self::assertFalse($zipFile->getEntryInfo('file 2')->isEncrypted()); + + $zipFile->unchangeEntry('file 1'); + + self::assertEquals($zipFile['file 1'], 'content 1'); + self::assertFalse($zipFile->getEntryInfo('file 1')->isEncrypted()); + + self::assertEquals($zipFile['file 2'], 'content 2'); + self::assertFalse($zipFile->getEntryInfo('file 2')->isEncrypted()); + $zipFile->close(); + } + /** * Test support ZIP64 ext (slow test - normal). * Create > 65535 files in archive and open and extract to /dev/null. @@ -1830,10 +2117,12 @@ class ZipFileTest extends ZipTestCase $zipFile->openFile($this->outputFilename); self::assertEquals($zipFile->count(), $countFiles); + $i = 0; foreach ($zipFile as $entry => $content) { - + self::assertEquals($entry, $i . '.txt'); + self::assertEquals($content, $i); + $i++; } $zipFile->close(); } - -} \ No newline at end of file +} diff --git a/tests/PhpZip/ZipMatcherTest.php b/tests/PhpZip/ZipMatcherTest.php new file mode 100644 index 0000000..c824765 --- /dev/null +++ b/tests/PhpZip/ZipMatcherTest.php @@ -0,0 +1,111 @@ +matcher(); + self::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) { + self::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) { + self::assertTrue(isset($zipFile[$name])); + } + + $matcher = $zipFile->matcher(); + $matcher->match('~^file_(1|5)\d+~'); + self::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) { + self::assertFalse(isset($zipFile[$name])); + + $pathInfo = pathinfo($name); + $newName = $pathInfo['filename'].'.no_optimize.'.$pathInfo['extension']; + self::assertTrue(isset($zipFile[$newName])); + } + + $zipFile->close(); + } +} diff --git a/tests/PhpZip/ZipPasswordTest.php b/tests/PhpZip/ZipPasswordTest.php new file mode 100644 index 0000000..014b6b6 --- /dev/null +++ b/tests/PhpZip/ZipPasswordTest.php @@ -0,0 +1,330 @@ +addDir(__DIR__); + $zipFile->setPassword($password, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename, $password); + + // check bad password for ZipCrypto + $zipFile->openFile($this->outputFilename); + $zipFile->setReadPassword($badPassword); + foreach ($zipFile->getListFiles() as $entryName) { + try { + $zipFile[$entryName]; + self::fail("Expected Exception has not been raised."); + } catch (ZipAuthenticationException $ae) { + self::assertNotNull($ae); + } + } + + // check correct password for ZipCrypto + $zipFile->setReadPassword($password); + foreach ($zipFile->getAllInfo() as $info) { + self::assertTrue($info->isEncrypted()); + self::assertContains('ZipCrypto', $info->getMethodName()); + $decryptContent = $zipFile[$info->getName()]; + self::assertNotEmpty($decryptContent); + self::assertContains('setPassword($password, ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::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]; + self::fail("Expected Exception has not been raised."); + } catch (ZipAuthenticationException $ae) { + self::assertNotNull($ae); + } + } + + // set correct password WinZip AES + $zipFile->setReadPassword($password); + foreach ($zipFile->getAllInfo() as $info) { + self::assertTrue($info->isEncrypted()); + self::assertContains('WinZip', $info->getMethodName()); + $decryptContent = $zipFile[$info->getName()]; + self::assertNotEmpty($decryptContent); + self::assertContains('addFromString('file1', ''); + $zipFile->removePassword(); + $zipFile->addFromString('file2', ''); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename); + + // check remove password + $zipFile->openFile($this->outputFilename); + foreach ($zipFile->getAllInfo() as $info) { + self::assertFalse($info->isEncrypted()); + } + $zipFile->close(); + } + + public function testTraditionalEncryption() + { + $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(); + + self::assertCorrectZipArchive($this->outputFilename, $password); + + $zip->openFile($this->outputFilename); + $zip->setReadPassword($password); + self::assertFilesResult($zip, array_keys(self::$files)); + foreach ($zip->getAllInfo() as $info) { + if (!$info->isFolder()) { + self::assertTrue($info->isEncrypted()); + self::assertContains('ZipCrypto', $info->getMethodName()); + } + } + $zip->close(); + } + + /** + * @dataProvider winZipKeyStrengthProvider + * @param int $encryptionMethod + * @param int $bitSize + */ + 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(); + + self::assertCorrectZipArchive($this->outputFilename, $password); + + $zip->openFile($this->outputFilename); + $zip->setReadPassword($password); + self::assertFilesResult($zip, array_keys(self::$files)); + foreach ($zip->getAllInfo() as $info) { + if (!$info->isFolder()) { + self::assertTrue($info->isEncrypted()); + self::assertEquals($info->getEncryptionMethod(), $encryptionMethod); + self::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], + ]; + } + + public function testEncryptionEntries() + { + $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); + self::assertFilesResult($zip, [ + '.hidden', + 'text file.txt', + 'Текстовый документ.txt', + 'empty dir/', + ]); + + $info = $zip->getEntryInfo('.hidden'); + self::assertTrue($info->isEncrypted()); + self::assertContains('ZipCrypto', $info->getMethodName()); + + $info = $zip->getEntryInfo('text file.txt'); + self::assertTrue($info->isEncrypted()); + self::assertContains('WinZip AES', $info->getMethodName()); + + self::assertFalse($zip->getEntryInfo('Текстовый документ.txt')->isEncrypted()); + self::assertFalse($zip->getEntryInfo('empty dir/')->isEncrypted()); + + $zip->close(); + } + + public function testEncryptionEntriesWithDefaultPassword() + { + $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); + self::assertFilesResult($zip, [ + '.hidden', + 'text file.txt', + 'Текстовый документ.txt', + 'empty dir/', + ]); + + $info = $zip->getEntryInfo('.hidden'); + self::assertTrue($info->isEncrypted()); + self::assertContains('ZipCrypto', $info->getMethodName()); + + $info = $zip->getEntryInfo('text file.txt'); + self::assertTrue($info->isEncrypted()); + self::assertContains('WinZip AES', $info->getMethodName()); + + $info = $zip->getEntryInfo('Текстовый документ.txt'); + self::assertTrue($info->isEncrypted()); + self::assertContains('WinZip AES', $info->getMethodName()); + + self::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(); + } + + public function testEntryPassword() + { + $zipFile = new ZipFile(); + $zipFile->setPassword('pass'); + $zipFile['file'] = 'content'; + self::assertFalse($zipFile->getEntryInfo('file')->isEncrypted()); + for ($i = 1; $i <= 10; $i++) { + $zipFile['file' . $i] = 'content'; + if ($i < 6) { + $zipFile->setPasswordEntry('file' . $i, 'pass'); + self::assertTrue($zipFile->getEntryInfo('file' . $i)->isEncrypted()); + } else { + self::assertFalse($zipFile->getEntryInfo('file' . $i)->isEncrypted()); + } + } + $zipFile->removePasswordEntry('file3'); + self::assertFalse($zipFile->getEntryInfo('file3')->isEncrypted()); + self::asserttrue($zipFile->getEntryInfo('file2')->isEncrypted()); + $zipFile->removePassword(); + $infoList = $zipFile->getAllInfo(); + array_walk($infoList, function (ZipInfo $zipInfo) { + self::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); + } + + public function testArchivePasswordUpdateWithoutSetReadPassword() + { + $zipFile = new ZipFile(); + $zipFile['file1'] = 'content'; + $zipFile['file2'] = 'content'; + $zipFile['file3'] = 'content'; + $zipFile->setPassword('password'); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + self::assertCorrectZipArchive($this->outputFilename, 'password'); + + $zipFile->openFile($this->outputFilename); + self::assertCount(3, $zipFile); + foreach ($zipFile->getAllInfo() as $info) { + self::assertTrue($info->isEncrypted()); + } + unset($zipFile['file3']); + $zipFile['file4'] = 'content'; + $zipFile->rewrite(); + + self::assertCorrectZipArchive($this->outputFilename, 'password'); + + self::assertCount(3, $zipFile); + self::assertFalse(isset($zipFile['file3'])); + self::assertTrue(isset($zipFile['file4'])); + self::assertTrue($zipFile->getEntryInfo('file1')->isEncrypted()); + self::assertTrue($zipFile->getEntryInfo('file2')->isEncrypted()); + self::assertFalse($zipFile->getEntryInfo('file4')->isEncrypted()); + self::assertEquals($zipFile['file4'], 'content'); + + $zipFile->extractTo($this->outputDirname, ['file4']); + + self::assertTrue(file_exists($this->outputDirname . DIRECTORY_SEPARATOR . 'file4')); + self::assertEquals(file_get_contents($this->outputDirname . DIRECTORY_SEPARATOR . 'file4'), $zipFile['file4']); + + $zipFile->close(); + } +} diff --git a/tests/PhpZip/ZipTestCase.php b/tests/PhpZip/ZipTestCase.php index 8fcb8d8..6de8537 100644 --- a/tests/PhpZip/ZipTestCase.php +++ b/tests/PhpZip/ZipTestCase.php @@ -1,4 +1,5 @@ outputFilename = sys_get_temp_dir() . '/' . $id . '.zip'; - $this->outputDirname = sys_get_temp_dir() . '/' . $id; + $tempDir = sys_get_temp_dir() . '/phpunit-phpzip'; + 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; } /** @@ -118,12 +125,13 @@ class ZipTestCase extends \PHPUnit_Framework_TestCase { if (DIRECTORY_SEPARATOR !== '\\' && `which zipalign`) { 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; } else { - fwrite(STDERR, 'Can not find program "zipalign" for test'); + fwrite(STDERR, 'Can not find program "zipalign" for test' . PHP_EOL); return null; } } - -} \ No newline at end of file +} diff --git a/tests/PhpZip/php-zip-ext-test-resources/binarynull.zip b/tests/PhpZip/php-zip-ext-test-resources/binarynull.zip new file mode 100644 index 0000000000000000000000000000000000000000..9da004efed082ebb572404a893dc32ee6f140ccd GIT binary patch literal 656 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 R`69aA?QYN8ezz|r_#Y`FUIYLD literal 0 HcmV?d00001 diff --git a/tests/PhpZip/php-zip-ext-test-resources/bug70752.zip b/tests/PhpZip/php-zip-ext-test-resources/bug70752.zip new file mode 100644 index 0000000000000000000000000000000000000000..9bec61bc18a874961ccf2c0bcad1239eed6c50e6 GIT binary patch literal 175 zcmWIWW@Zs#U}RumkYrZ#eBJqUsUVQe3dB4>oK%`_ZeVU|q*qc=qOHuIKCy7mPnlPp z^DhZ_&ruHWW@NHwz@=FQs2v0pz$B_sTu@O)1_=fciTr>g?3WmI-|lfjrUSfL*+5zu MfiMn8TZ7F30N(i{^Z)<= literal 0 HcmV?d00001 diff --git a/tests/PhpZip/php-zip-ext-test-resources/bug8009.zip b/tests/PhpZip/php-zip-ext-test-resources/bug8009.zip new file mode 100644 index 0000000000000000000000000000000000000000..45bedcbe8891cf667083f24d4ae4879d62765b88 GIT binary patch literal 112 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 gw#ErUU#}HvQzgKg71)RYwKKUGjDhV Date: Mon, 13 Nov 2017 15:40:05 +0300 Subject: [PATCH 02/10] update readme --- README.RU.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.RU.md b/README.RU.md index 5e53597..af686f8 100644 --- a/README.RU.md +++ b/README.RU.md @@ -583,7 +583,7 @@ $zipFile->setArchiveComment($commentArchive); ```php $zipFile->setEntryComment($entryName, $comment); ``` - - выборка записей в архиве для проведения операций над выбранными записями. + **ZipFile::matcher** - выборка записей в архиве для проведения операций над выбранными записями. ```php $matcher = $zipFile->matcher(); ``` diff --git a/README.md b/README.md index 500d852..d2430da 100644 --- a/README.md +++ b/README.md @@ -585,7 +585,7 @@ $zipFile->setArchiveComment($commentArchive); ```php $zipFile->setEntryComment($entryName, $comment); ``` - - selecting entries in the archive to perform operations on them. + **ZipFile::matcher** - selecting entries in the archive to perform operations on them. ```php $matcher = $zipFile->matcher(); ``` From 88802754b32efd5a538403e58672d929f3fca9ac Mon Sep 17 00:00:00 2001 From: wapplay-home-linux Date: Mon, 13 Nov 2017 15:47:24 +0300 Subject: [PATCH 03/10] travis try install zipalign --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 281a057..c543ae5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,8 @@ install: - travis_retry composer install --prefer-dist --no-interaction before_script: - - sudo apt-get install p7zip-full + - sudo apt-get install p7zip-full -y + - sudo apt-get install zipalign -y script: - composer validate --no-check-lock From ec919808d0980fed98f2c10eaa6ba9ed3042ac0c Mon Sep 17 00:00:00 2001 From: wapplay-home-linux Date: Mon, 13 Nov 2017 15:50:19 +0300 Subject: [PATCH 04/10] Revert "travis try install zipalign" This reverts commit 8880275 --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c543ae5..281a057 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,7 @@ install: - travis_retry composer install --prefer-dist --no-interaction before_script: - - sudo apt-get install p7zip-full -y - - sudo apt-get install zipalign -y + - sudo apt-get install p7zip-full script: - composer validate --no-check-lock From 129e69c29381b3aaa3198edb1a4941408e4be7d3 Mon Sep 17 00:00:00 2001 From: wapplay-home-linux Date: Tue, 14 Nov 2017 08:47:25 +0300 Subject: [PATCH 05/10] fixed some errors tests for php-32 bit platform --- src/PhpZip/Model/Entry/ZipAbstractEntry.php | 32 +++------------------ src/PhpZip/Model/ZipInfo.php | 15 +++++++--- src/PhpZip/Stream/ZipInputStream.php | 26 +++++++++++++---- src/PhpZip/Util/PackUtil.php | 4 +-- tests/PhpZip/PhpZipExtResourceTest.php | 2 +- 5 files changed, 38 insertions(+), 41 deletions(-) diff --git a/src/PhpZip/Model/Entry/ZipAbstractEntry.php b/src/PhpZip/Model/Entry/ZipAbstractEntry.php index bffe7bb..0825f19 100644 --- a/src/PhpZip/Model/Entry/ZipAbstractEntry.php +++ b/src/PhpZip/Model/Entry/ZipAbstractEntry.php @@ -260,7 +260,7 @@ abstract class ZipAbstractEntry implements ZipEntry // description of Data Descriptor in ZIP File Format Specification! return 0xffffffff <= $this->getCompressedSize() || 0xffffffff <= $this->getSize() - || 0xffffffff <= $this->getOffset(); + || 0xffffffff <= sprintf('%u', $this->getOffset()); } /** @@ -282,12 +282,6 @@ abstract class ZipAbstractEntry implements ZipEntry */ 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; return $this; } @@ -311,12 +305,6 @@ abstract class ZipAbstractEntry implements ZipEntry */ 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; return $this; } @@ -338,10 +326,6 @@ abstract class ZipAbstractEntry implements ZipEntry */ 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; return $this; } @@ -507,7 +491,7 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function getDosTime() { - return $this->dosTime & 0xffffffff; + return $this->dosTime; } /** @@ -553,7 +537,7 @@ abstract class ZipAbstractEntry implements ZipEntry if (!$this->isInit(self::BIT_EXTERNAL_ATTR)) { return $this->isDirectory() ? 0x10 : 0; } - return $this->externalAttributes & 0xffffffff; + return $this->externalAttributes; } /** @@ -567,10 +551,6 @@ abstract class ZipAbstractEntry implements ZipEntry { $known = self::UNKNOWN != $externalAttributes; 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; } else { $this->externalAttributes = 0; @@ -701,7 +681,7 @@ abstract class ZipAbstractEntry implements ZipEntry */ public function getCrc() { - return $this->crc & 0xffffffff; + return $this->crc; } /** @@ -713,10 +693,6 @@ abstract class ZipAbstractEntry implements ZipEntry */ 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->setInit(self::BIT_CRC, true); return $this; diff --git a/src/PhpZip/Model/ZipInfo.php b/src/PhpZip/Model/ZipInfo.php index 193087c..6434703 100644 --- a/src/PhpZip/Model/ZipInfo.php +++ b/src/PhpZip/Model/ZipInfo.php @@ -205,8 +205,12 @@ class ZipInfo $this->name = $entry->getName(); $this->folder = $entry->isDirectory(); - $this->size = $entry->getSize(); - $this->compressedSize = $entry->getCompressedSize(); + $this->size = PHP_INT_SIZE === 4 ? + sprintf('%u', $entry->getSize()) : + $entry->getSize(); + $this->compressedSize = PHP_INT_SIZE === 4 ? + sprintf('%u', $entry->getCompressedSize()) : + $entry->getCompressedSize(); $this->mtime = $mtime; $this->ctime = $ctime; $this->atime = $atime; @@ -222,6 +226,9 @@ class ZipInfo $attributes = str_repeat(" ", 12); $externalAttributes = $entry->getExternalAttributes(); + $externalAttributes = PHP_INT_SIZE === 4 ? + sprintf('%u', $externalAttributes) : + $externalAttributes; $xattr = (($externalAttributes >> 16) & 0xFFFF); switch ($entry->getPlatform()) { case self::MADE_BY_MS_DOS: @@ -570,8 +577,8 @@ class ZipInfo return __CLASS__ . ' {' . 'Name="' . $this->getName() . '", ' . ($this->isFolder() ? 'Folder, ' : '') - . 'Size="' . FilesUtil::humanSize($this->getSize()).'"' - . ', Compressed size="' . FilesUtil::humanSize($this->getCompressedSize()).'"' + . 'Size="' . FilesUtil::humanSize($this->getSize()) . '"' + . ', Compressed size="' . FilesUtil::humanSize($this->getCompressedSize()) . '"' . ', Modified time="' . date(DATE_W3C, $this->getMtime()) . '", ' . ($this->getCtime() !== null ? 'Created time="' . date(DATE_W3C, $this->getCtime()) . '", ' : '') . ($this->getAtime() !== null ? 'Accessed time="' . date(DATE_W3C, $this->getAtime()) . '", ' : '') diff --git a/src/PhpZip/Stream/ZipInputStream.php b/src/PhpZip/Stream/ZipInputStream.php index 2510f43..df5bd35 100644 --- a/src/PhpZip/Stream/ZipInputStream.php +++ b/src/PhpZip/Stream/ZipInputStream.php @@ -251,6 +251,7 @@ class ZipInputStream implements ZipInputStreamInterface // 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; } @@ -353,6 +354,8 @@ class ZipInputStream implements ZipInputStreamInterface $pos = $entry->getOffset(); assert(ZipEntry::UNKNOWN !== $pos); + $pos = PHP_INT_SIZE === 4 ? sprintf('%u', $pos) : $pos; + $startPos = $pos = $this->mapper->map($pos); fseek($this->in, $startPos); @@ -374,6 +377,7 @@ class ZipInputStream implements ZipInputStreamInterface // Get raw entry content $compressedSize = $entry->getCompressedSize(); + $compressedSize = PHP_INT_SIZE === 4 ? sprintf('%u', $compressedSize) : $compressedSize; if ($compressedSize > 0) { $content = fread($this->in, $compressedSize); } else { @@ -417,9 +421,13 @@ class ZipInputStream implements ZipInputStreamInterface 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; } - if ($entry->getCrc() != $localCrc) { - throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc); + + $crc = PHP_INT_SIZE === 4 ? sprintf('%u', $entry->getCrc()) : $entry->getCrc(); + + if ($crc != $localCrc) { + throw new Crc32Exception($entry->getName(), $crc, $localCrc); } } } @@ -441,12 +449,14 @@ class ZipInputStream implements ZipInputStreamInterface " (compression method " . $method . " is not supported)"); } if (!$skipCheckCrc) { - $localCrc = sprintf('%u', crc32($content)); - if ($entry->getCrc() != $localCrc) { + $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 ZipCryptoException("Wrong password"); } - throw new Crc32Exception($entry->getName(), $entry->getCrc(), $localCrc); + throw new Crc32Exception($entry->getName(), $crc, $localCrc); } } return $content; @@ -468,6 +478,7 @@ class ZipInputStream implements ZipInputStreamInterface { $pos = $entry->getOffset(); assert(ZipEntry::UNKNOWN !== $pos); + $pos = PHP_INT_SIZE === 4 ? sprintf('%u', $pos) : $pos; $pos = $this->mapper->map($pos); $extraLength = strlen($entry->getExtra()); @@ -510,7 +521,10 @@ class ZipInputStream implements ZipInputStreamInterface */ public function copyEntryData(ZipEntry $entry, ZipOutputStreamInterface $out) { - $position = $entry->getOffset() + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + + $offset = $entry->getOffset(); + $offset = PHP_INT_SIZE === 4 ? sprintf('%u', $offset) : $offset; + $offset = $this->mapper->map($offset); + $position = $offset + ZipEntry::LOCAL_FILE_HEADER_MIN_LEN + strlen($entry->getName()) + strlen($entry->getExtra()); $length = $entry->getCompressedSize(); fseek($this->in, $position, SEEK_SET); diff --git a/src/PhpZip/Util/PackUtil.php b/src/PhpZip/Util/PackUtil.php index b68a0c8..79ca6a3 100644 --- a/src/PhpZip/Util/PackUtil.php +++ b/src/PhpZip/Util/PackUtil.php @@ -19,7 +19,7 @@ class PackUtil */ 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); } @@ -39,7 +39,7 @@ class PackUtil */ public static function unpackLongLE($value) { - if (version_compare(PHP_VERSION, '5.6.3') >= 0) { + if (PHP_INT_SIZE === 8 && PHP_VERSION_ID >= 506030) { return unpack('P', $value)[1]; } $unpack = unpack('Va/Vb', $value); diff --git a/tests/PhpZip/PhpZipExtResourceTest.php b/tests/PhpZip/PhpZipExtResourceTest.php index c588843..2ec9166 100644 --- a/tests/PhpZip/PhpZipExtResourceTest.php +++ b/tests/PhpZip/PhpZipExtResourceTest.php @@ -83,7 +83,7 @@ class PhpZipExtResourceTest extends ZipTestCase * 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 (expected CRC32 value 0xc935c834, but is actually 0x76301511) + * @expectedExceptionMessage file1 */ public function testBug49072() { From 1b1495eee8aeb35fda72b7b0ec03130d8d4ba70f Mon Sep 17 00:00:00 2001 From: wapplay-home-linux Date: Tue, 14 Nov 2017 09:16:11 +0300 Subject: [PATCH 06/10] Skipped some tests for a users with root privileges --- tests/PhpZip/ZipFileTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/PhpZip/ZipFileTest.php b/tests/PhpZip/ZipFileTest.php index aa3b5d7..3685db0 100644 --- a/tests/PhpZip/ZipFileTest.php +++ b/tests/PhpZip/ZipFileTest.php @@ -30,6 +30,10 @@ class ZipFileTest extends ZipTestCase */ public function testOpenFileCantOpen() { + if (0 === posix_getuid()){ + $this->markTestSkipped('Skip the test for a user with root privileges'); + } + self::assertNotFalse(file_put_contents($this->outputFilename, 'content')); self::assertTrue(chmod($this->outputFilename, 0222)); @@ -1003,6 +1007,10 @@ class ZipFileTest extends ZipTestCase */ public function testExtractFail3() { + if (0 === posix_getuid()){ + $this->markTestSkipped('Skip the test for a user with root privileges'); + } + $zipFile = new ZipFile(); $zipFile['file'] = 'content'; $zipFile->saveAsFile($this->outputFilename); @@ -1205,6 +1213,10 @@ class ZipFileTest extends ZipTestCase */ public function testAddFileCantOpen() { + if (0 === posix_getuid()){ + $this->markTestSkipped('Skip the test for a user with root privileges'); + } + self::assertNotFalse(file_put_contents($this->outputFilename, '')); self::assertTrue(chmod($this->outputFilename, 0244)); @@ -1489,6 +1501,10 @@ class ZipFileTest extends ZipTestCase */ public function testSaveAsFileNotWritable() { + if (0 === posix_getuid()){ + $this->markTestSkipped('Skip the test for a user with root privileges'); + } + self::assertTrue(mkdir($this->outputDirname, 0444, true)); self::assertTrue(chmod($this->outputDirname, 0444)); From 0d4b10151075d005e6cef98363d28bdef7a25ea9 Mon Sep 17 00:00:00 2001 From: Ne-Lexa Date: Tue, 14 Nov 2017 11:28:02 +0300 Subject: [PATCH 07/10] Skipped some tests for php 32-bit platform. --- README.RU.md | 4 ++++ README.md | 4 ++++ src/PhpZip/Util/PackUtil.php | 8 +++++--- tests/PhpZip/ZipPasswordTest.php | 11 +++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/README.RU.md b/README.RU.md index af686f8..a0e8a4f 100644 --- a/README.RU.md +++ b/README.RU.md @@ -50,6 +50,10 @@ - Поддержка `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`, когда это возможно. + Установка пароля для чтения архива глобально или для некоторых записей. + Изменение пароля архива, в том числе и для отдельных записей. + Удаление пароля архива глобально или для отдельных записей. diff --git a/README.md b/README.md index d2430da..22ca737 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,10 @@ Table of contents - Support for `ZIP64` (file size is more than 4 GB or the number of entries in the archive is more than 65535). - Built-in support for aligning the archive to optimize Android packages (APK) [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html). - Working with passwords for PHP 5.5 +> **Attention!** +> +> For 32-bit systems, the `Traditional PKWARE Encryption (ZipCrypto)` encryption method is not currently supported. +> Use the encryption method `WinZIP AES Encryption`, whenever possible. + Set the password to read the archive for all entries or only for some. + Change the password for the archive, including for individual entries. + Delete the archive password for all or individual entries. diff --git a/src/PhpZip/Util/PackUtil.php b/src/PhpZip/Util/PackUtil.php index 79ca6a3..c622360 100644 --- a/src/PhpZip/Util/PackUtil.php +++ b/src/PhpZip/Util/PackUtil.php @@ -54,9 +54,11 @@ class PackUtil */ public static function toSignedInt32($int) { - $int = $int & 0xffffffff; - if (PHP_INT_SIZE === 8 && ($int & 0x80000000)) { - return $int - 0x100000000; + if (PHP_INT_SIZE === 8) { + $int = $int & 0xffffffff; + if ($int & 0x80000000) { + return $int - 0x100000000; + } } return $int; } diff --git a/tests/PhpZip/ZipPasswordTest.php b/tests/PhpZip/ZipPasswordTest.php index 014b6b6..379788e 100644 --- a/tests/PhpZip/ZipPasswordTest.php +++ b/tests/PhpZip/ZipPasswordTest.php @@ -6,6 +6,9 @@ use PhpZip\Exception\ZipAuthenticationException; use PhpZip\Model\ZipInfo; use PhpZip\Util\CryptoUtil; +/** + * Tests with zip password. + */ class ZipPasswordTest extends ZipFileAddDirTest { /** @@ -13,6 +16,10 @@ class ZipPasswordTest extends ZipFileAddDirTest */ 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 = "sdgt43r23wefe"; @@ -96,6 +103,10 @@ class ZipPasswordTest extends ZipFileAddDirTest 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(); From 02afaae56ca2452b3c0b2c666f64f741e016ca2d Mon Sep 17 00:00:00 2001 From: Ne-Lexa Date: Tue, 14 Nov 2017 11:39:16 +0300 Subject: [PATCH 08/10] Skipped some tests for php 32-bit platform. --- tests/PhpZip/ZipPasswordTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/PhpZip/ZipPasswordTest.php b/tests/PhpZip/ZipPasswordTest.php index 379788e..d4503f3 100644 --- a/tests/PhpZip/ZipPasswordTest.php +++ b/tests/PhpZip/ZipPasswordTest.php @@ -174,6 +174,10 @@ class ZipPasswordTest extends ZipFileAddDirTest 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'; @@ -210,6 +214,10 @@ class ZipPasswordTest extends ZipFileAddDirTest 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 '; From e62e51efb57cc67a1668138178a8b09ab157eec0 Mon Sep 17 00:00:00 2001 From: Ne-Lexa Date: Tue, 14 Nov 2017 14:03:44 +0300 Subject: [PATCH 09/10] issue #8 - Support inline Content-Disposition and empty output filename. --- phpunit.xml | 17 ++++++++- src/PhpZip/ZipFile.php | 68 +++++++++++++++++++-------------- src/PhpZip/ZipFileInterface.php | 19 ++++----- tests/PhpZip/ZipFileTest.php | 40 ++----------------- 4 files changed, 69 insertions(+), 75 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 25c557a..c69aee3 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,10 +1,23 @@ - + + + + + - ./tests + tests + + + src + + \ No newline at end of file diff --git a/src/PhpZip/ZipFile.php b/src/PhpZip/ZipFile.php index fc6b9d9..403bcd3 100644 --- a/src/PhpZip/ZipFile.php +++ b/src/PhpZip/ZipFile.php @@ -1226,61 +1226,64 @@ class ZipFile implements ZipFileInterface * Output .ZIP archive as attachment. * Die after output. * - * @param string $outputFilename - * @param string|null $mimeType + * @param string $outputFilename Output filename + * @param string|null $mimeType Mime-Type + * @param bool $attachment Http Header 'Content-Disposition' if true then attachment otherwise inline * @throws InvalidArgumentException */ - public function outputAsAttachment($outputFilename, $mimeType = null) + public function outputAsAttachment($outputFilename, $mimeType = null, $attachment = true) { $outputFilename = (string)$outputFilename; - if (strlen($outputFilename) === 0) { - throw new InvalidArgumentException("Output filename is empty."); - } - if (empty($mimeType) || !is_string($mimeType)) { + + if (empty($mimeType) || !is_string($mimeType) && !empty($outputFilename)) { $ext = strtolower(pathinfo($outputFilename, PATHINFO_EXTENSION)); if (!empty($ext) && isset(self::$defaultMimeTypes[$ext])) { $mimeType = self::$defaultMimeTypes[$ext]; - } else { - $mimeType = self::$defaultMimeTypes['zip']; } } - $outputFilename = basename($outputFilename); + if (empty($mimeType)) { + $mimeType = self::$defaultMimeTypes['zip']; + } $content = $this->outputAsString(); $this->close(); + $headerContentDisposition = 'Content-Disposition: ' . ($attachment ? 'attachment' : 'inline'); + if (!empty($outputFilename)) { + $headerContentDisposition .= '; filename="' . basename($outputFilename) . '"'; + } + + header($headerContentDisposition); header("Content-Type: " . $mimeType); - header('Content-Disposition: attachment; filename="' . $outputFilename . '"'); header("Content-Length: " . strlen($content)); exit($content); } /** - * Output .ZIP archive as PSR-Message Response. + * Output .ZIP archive as PSR-7 Response. * - * @param ResponseInterface $response - * @param string $outputFilename - * @param string|null $mimeType + * @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 * @throws InvalidArgumentException */ - public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null) + public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null, $attachment = true) { $outputFilename = (string)$outputFilename; - if (strlen($outputFilename) === 0) { - throw new InvalidArgumentException("Output filename is empty."); - } - if (empty($mimeType) || !is_string($mimeType)) { + + if (empty($mimeType) || !is_string($mimeType) && !empty($outputFilename)) { $ext = strtolower(pathinfo($outputFilename, PATHINFO_EXTENSION)); if (!empty($ext) && isset(self::$defaultMimeTypes[$ext])) { $mimeType = self::$defaultMimeTypes[$ext]; - } else { - $mimeType = self::$defaultMimeTypes['zip']; } } - $outputFilename = basename($outputFilename); + if (empty($mimeType)) { + $mimeType = self::$defaultMimeTypes['zip']; + } if (!($handle = fopen('php://memory', 'w+b'))) { throw new InvalidArgumentException("Memory can not open from write."); @@ -1288,9 +1291,14 @@ class ZipFile implements ZipFileInterface $this->writeZipToStream($handle); rewind($handle); + $contentDispositionValue = ($attachment ? 'attachment' : 'inline'); + if (!empty($outputFilename)) { + $contentDispositionValue .= '; filename="' . basename($outputFilename) . '"'; + } + $stream = new ResponseStream($handle); $response->withHeader('Content-Type', $mimeType); - $response->withHeader('Content-Disposition', 'attachment; filename="' . $outputFilename . '"'); + $response->withHeader('Content-Disposition', $contentDispositionValue); $response->withHeader('Content-Length', $stream->getSize()); $response->withBody($stream); return $response; @@ -1348,11 +1356,15 @@ class ZipFile implements ZipFileInterface $content = $this->outputAsString(); $this->close(); if ('plainfile' === $meta['wrapper_type']) { - if (file_put_contents($meta['uri'], $content) === false) { - throw new ZipException("Can not overwrite the zip file in the {$meta['uri']} file."); + /** + * @var resource $uri + */ + $uri = $meta['uri']; + if (file_put_contents($uri, $content) === false) { + throw new ZipException("Can not overwrite the zip file in the {$uri} file."); } - if (!($handle = @fopen($meta['uri'], 'rb'))) { - throw new ZipException("File {$meta['uri']} can't open."); + if (!($handle = @fopen($uri, 'rb'))) { + throw new ZipException("File {$uri} can't open."); } return $this->openFromStream($handle); } diff --git a/src/PhpZip/ZipFileInterface.php b/src/PhpZip/ZipFileInterface.php index 09ddd23..05d31e3 100644 --- a/src/PhpZip/ZipFileInterface.php +++ b/src/PhpZip/ZipFileInterface.php @@ -592,22 +592,23 @@ interface ZipFileInterface extends \Countable, \ArrayAccess, \Iterator * Output .ZIP archive as attachment. * Die after output. * - * @param string $outputFilename - * @param string|null $mimeType - * @throws InvalidArgumentException + * @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); + public function outputAsAttachment($outputFilename, $mimeType = null, $attachment = true); /** - * Output .ZIP archive as PSR-Message Response. + * Output .ZIP archive as PSR-7 Response. * - * @param ResponseInterface $response - * @param string $outputFilename - * @param string|null $mimeType + * @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 * @throws InvalidArgumentException */ - public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null); + public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null, $attachment = true); /** * Returns the zip archive as a string. diff --git a/tests/PhpZip/ZipFileTest.php b/tests/PhpZip/ZipFileTest.php index 3685db0..e23d82e 100644 --- a/tests/PhpZip/ZipFileTest.php +++ b/tests/PhpZip/ZipFileTest.php @@ -30,7 +30,7 @@ class ZipFileTest extends ZipTestCase */ public function testOpenFileCantOpen() { - if (0 === posix_getuid()){ + if (0 === posix_getuid()) { $this->markTestSkipped('Skip the test for a user with root privileges'); } @@ -1007,7 +1007,7 @@ class ZipFileTest extends ZipTestCase */ public function testExtractFail3() { - if (0 === posix_getuid()){ + if (0 === posix_getuid()) { $this->markTestSkipped('Skip the test for a user with root privileges'); } @@ -1213,7 +1213,7 @@ class ZipFileTest extends ZipTestCase */ public function testAddFileCantOpen() { - if (0 === posix_getuid()){ + if (0 === posix_getuid()) { $this->markTestSkipped('Skip the test for a user with root privileges'); } @@ -1501,7 +1501,7 @@ class ZipFileTest extends ZipTestCase */ public function testSaveAsFileNotWritable() { - if (0 === posix_getuid()){ + if (0 === posix_getuid()) { $this->markTestSkipped('Skip the test for a user with root privileges'); } @@ -1662,26 +1662,6 @@ class ZipFileTest extends ZipTestCase $zipFile->addEmptyDir(""); } - /** - * @expectedException \PhpZip\Exception\InvalidArgumentException - * @expectedExceptionMessage Output filename is empty. - */ - public function testOutputAsAttachmentNullName() - { - $zipFile = new ZipFile(); - $zipFile->outputAsAttachment(null); - } - - /** - * @expectedException \PhpZip\Exception\InvalidArgumentException - * @expectedExceptionMessage Output filename is empty. - */ - public function testOutputAsAttachmentEmptyName() - { - $zipFile = new ZipFile(); - $zipFile->outputAsAttachment(''); - } - /** * @expectedException \PhpZip\Exception\ZipNotFoundEntry * @expectedExceptionMessage Zip entry bad entry name not found @@ -1916,18 +1896,6 @@ class ZipFileTest extends ZipTestCase $this->assertInstanceOf(ResponseInterface::class, $response); } - /** - * @expectedException \PhpZip\Exception\InvalidArgumentException - * @expectedExceptionMessage Output filename is empty. - */ - public function testInvalidPsrResponse() - { - $zipFile = new ZipFile(); - $zipFile['file'] = 'content'; - $response = $this->getMock(ResponseInterface::class); - $zipFile->outputAsResponse($response, ''); - } - public function testCompressionLevel() { $zipFile = new ZipFile(); From d32b000855f8fd9ae58f5b2f31a98b2c59d6006f Mon Sep 17 00:00:00 2001 From: Ne-Lexa Date: Tue, 14 Nov 2017 14:47:46 +0300 Subject: [PATCH 10/10] Rename methods `removePassword*()` to `disableEncryption*()` --- README.RU.md | 16 ++++++++-------- README.md | 16 ++++++++-------- src/PhpZip/Model/Entry/ZipAbstractEntry.php | 4 ++-- src/PhpZip/Model/ZipEntry.php | 2 +- src/PhpZip/Model/ZipModel.php | 3 +++ src/PhpZip/ZipFile.php | 18 ++++++++++-------- src/PhpZip/ZipFileInterface.php | 16 +++++++++------- tests/PhpZip/ZipPasswordTest.php | 8 ++++---- 8 files changed, 45 insertions(+), 38 deletions(-) diff --git a/README.RU.md b/README.RU.md index a0e8a4f..379091c 100644 --- a/README.RU.md +++ b/README.RU.md @@ -119,6 +119,8 @@ $zipFile - [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-архива. @@ -135,8 +137,6 @@ $zipFile - [ZipFile::outputAsAttachment](#Documentation-ZipFile-outputAsAttachment) - выводит ZIP-архив в браузер. - [ZipFile::outputAsResponse](#Documentation-ZipFile-outputAsResponse) - выводит ZIP-архив, как Response PSR-7. - [ZipFile::outputAsString](#Documentation-ZipFile-outputAsString) - выводит ZIP-архив в виде строки. -- [ZipFile::removePassword](#Documentation-ZipFile-removePassword) - удаляет пароль у всех файлов в архиве. -- [ZipFile::removePasswordEntry](#Documentation-ZipFile-removePasswordEntry) - удаляет пароль у конкретного файла в архиве. - [ZipFile::rename](#Documentation-ZipFile-rename) - переименовывает запись по имени. - [ZipFile::rewrite](#Documentation-ZipFile-rewrite) - сохраняет изменения и заново открывает изменившийся архив. - [ZipFile::saveAsFile](#Documentation-ZipFile-saveAsFile) - сохраняет архив в файл. @@ -155,7 +155,7 @@ $zipFile - [ZipFile::unchangeAll](#Documentation-ZipFile-unchangeAll) - отменяет все изменения, сделанные в архиве. - [ZipFile::unchangeArchiveComment](#Documentation-ZipFile-unchangeArchiveComment) - отменяет изменения в комментарии к архиву. - [ZipFile::unchangeEntry](#Documentation-ZipFile-unchangeEntry) - отменяет изменения для конкретной записи архива. -- ~~ZipFile::withoutPassword~~ - устаревший метод (**deprecated**) используйте метод [ZipFile::removePassword](#Documentation-ZipFile-removePassword). +- ~~ZipFile::withoutPassword~~ - устаревший метод (**deprecated**) используйте метод [ZipFile::disableEncryption](#Documentation-ZipFile-disableEncryption). - ~~ZipFile::withReadPassword~~ - устаревший метод (**deprecated**) используйте метод [ZipFile::setReadPassword](#Documentation-ZipFile-setReadPassword). #### Создание/Открытие ZIP-архива @@ -678,15 +678,15 @@ $zipFile->setPasswordEntry($entryName, $password); $encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; $zipFile->setPasswordEntry($entryName, $password, $encryptionMethod); ``` - **ZipFile::removePassword** - удаляет пароль у всех файлов в архиве. + **ZipFile::disableEncryption** - отключает шифрования всех записей, находящихся в архиве. > _Обратите внимание, что действие данного метода не распространяется на записи, добавленные после выполнения этого метода._ ```php -$zipFile->removePassword(); +$zipFile->disableEncryption(); ``` - **ZipFile::removePasswordEntry** - удаляет пароль у конкретного файла в архиве. + **ZipFile::disableEncryptionEntry** - отключает шифрование записи по её имени. ```php -$zipFile->removePasswordEntry($entryName); +$zipFile->disableEncryptionEntry($entryName); ``` #### zipalign **ZipFile::setZipAlign** - устанавливает выравнивание архива для оптимизации APK файлов (Android packages). @@ -808,7 +808,7 @@ composer update nelexa/zip + `setLevel` в `setCompressionLevel` + `ZipFile::setPassword` в `ZipFile::withReadPassword` + `ZipOutputFile::setPassword` в `ZipFile::withNewPassword` - + `ZipOutputFile::removePasswordAllEntries` в `ZipFile::withoutPassword` + + `ZipOutputFile::disableEncryptionAllEntries` в `ZipFile::withoutPassword` + `ZipOutputFile::setComment` в `ZipFile::setArchiveComment` + `ZipFile::getComment` в `ZipFile::getArchiveComment` - Изменились сигнатуры для методов `addDir`, `addFilesFromGlob`, `addFilesFromRegex`. diff --git a/README.md b/README.md index 22ca737..eed4546 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,8 @@ Other examples can be found in the `tests/` folder - [ZipFile::deleteFromGlob](#Documentation-ZipFile-deleteFromGlob) - deletes a entries in the archive using glob pattern. - [ZipFile::deleteFromRegex](#Documentation-ZipFile-deleteFromRegex) - deletes a entries in the archive using PCRE pattern. - [ZipFile::deleteAll](#Documentation-ZipFile-deleteAll) - deletes all entries in the ZIP archive. +- [ZipFile::disableEncryption](#Documentation-ZipFile-disableEncryption) - disable encryption for all entries that are already in the archive. +- [ZipFile::disableEncryptionEntry](#Documentation-ZipFile-disableEncryptionEntry) - disable encryption of an entry defined by its name. - [ZipFile::extractTo](#Documentation-ZipFile-extractTo) - extract the archive contents. - [ZipFile::getAllInfo](#Documentation-ZipFile-getAllInfo) - returns detailed information about all entries in the archive. - [ZipFile::getArchiveComment](#Documentation-ZipFile-getArchiveComment) - returns the Zip archive comment. @@ -135,8 +137,6 @@ Other examples can be found in the `tests/` folder - [ZipFile::outputAsAttachment](#Documentation-ZipFile-outputAsAttachment) - outputs a ZIP-archive to the browser. - [ZipFile::outputAsResponse](#Documentation-ZipFile-outputAsResponse) - outputs a ZIP-archive as PSR-7 Response. - [ZipFile::outputAsString](#Documentation-ZipFile-outputAsString) - outputs a ZIP-archive as string. -- [ZipFile::removePassword](#Documentation-ZipFile-removePassword) - removes the password from all files in the archive. -- [ZipFile::removePasswordEntry](#Documentation-ZipFile-removePasswordEntry) - removes password from one entry in the archive. - [ZipFile::rename](#Documentation-ZipFile-rename) - renames an entry defined by its name. - [ZipFile::rewrite](#Documentation-ZipFile-rewrite) - save changes and re-open the changed archive. - [ZipFile::saveAsFile](#Documentation-ZipFile-saveAsFile) - saves the archive to a file. @@ -155,7 +155,7 @@ Other examples can be found in the `tests/` folder - [ZipFile::unchangeAll](#Documentation-ZipFile-unchangeAll) - undo all changes done in the archive. - [ZipFile::unchangeArchiveComment](#Documentation-ZipFile-unchangeArchiveComment) - undo changes to the archive comment. - [ZipFile::unchangeEntry](#Documentation-ZipFile-unchangeEntry) - undo changes of an entry defined by its name. -- ~~ZipFile::withoutPassword~~ - is an deprecated method, use the [ZipFile::removePassword](#Documentation-ZipFile-removePassword) method. +- ~~ZipFile::withoutPassword~~ - is an deprecated method, use the [ZipFile::disableEncryption](#Documentation-ZipFile-disableEncryption) method. - ~~ZipFile::withReadPassword~~ - is an deprecated method, use the [ZipFile::setReadPassword](#Documentation-ZipFile-setReadPassword) method. #### Creation/Opening of ZIP-archive @@ -680,15 +680,15 @@ You can set the encryption method: $encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; $zipFile->setPasswordEntry($entryName, $password, $encryptionMethod); ``` - **ZipFile::removePassword** - removes the password from all files in the archive. + **ZipFile::disableEncryption** - disable encryption for all entries that are already in the archive. > _Note that this method does not apply to entries that were added after this method was run._ ```php -$zipFile->removePassword(); +$zipFile->disableEncryption(); ``` - **ZipFile::removePasswordEntry** - removes password from one entry in the archive. + **ZipFile::disableEncryptionEntry** - disable encryption of an entry defined by its name. ```php -$zipFile->removePasswordEntry($entryName); +$zipFile->disableEncryptionEntry($entryName); ``` #### zipalign **ZipFile::setZipAlign** - sets the alignment of the archive to optimize APK files (Android packages). @@ -807,7 +807,7 @@ Update your code to work with the new version: + `setLevel` to `setCompressionLevel` + `ZipFile::setPassword` to `ZipFile::withReadPassword` + `ZipOutputFile::setPassword` to `ZipFile::withNewPassword` - + `ZipOutputFile::removePasswordAllEntries` to `ZipFile::withoutPassword` + + `ZipOutputFile::disableEncryptionAllEntries` to `ZipFile::withoutPassword` + `ZipOutputFile::setComment` to `ZipFile::setArchiveComment` + `ZipFile::getComment` to `ZipFile::getArchiveComment` - Changed signature for methods `addDir`, `addFilesFromGlob`, `addFilesFromRegex`. diff --git a/src/PhpZip/Model/Entry/ZipAbstractEntry.php b/src/PhpZip/Model/Entry/ZipAbstractEntry.php index 0825f19..396d234 100644 --- a/src/PhpZip/Model/Entry/ZipAbstractEntry.php +++ b/src/PhpZip/Model/Entry/ZipAbstractEntry.php @@ -395,7 +395,7 @@ abstract class ZipAbstractEntry implements ZipEntry * * @return ZipEntry */ - public function clearEncryption() + public function disableEncryption() { $this->setEncrypted(false); $headerId = WinZipAesEntryExtraField::getHeaderId(); @@ -722,7 +722,7 @@ abstract class ZipAbstractEntry implements ZipEntry if (!empty($this->password)) { $this->setEncrypted(true); } else { - $this->clearEncryption(); + $this->disableEncryption(); } return $this; } diff --git a/src/PhpZip/Model/ZipEntry.php b/src/PhpZip/Model/ZipEntry.php index 7f80995..37d54c3 100644 --- a/src/PhpZip/Model/ZipEntry.php +++ b/src/PhpZip/Model/ZipEntry.php @@ -252,7 +252,7 @@ interface ZipEntry * * @return ZipEntry */ - public function clearEncryption(); + public function disableEncryption(); /** * Returns the compression method for this entry. diff --git a/src/PhpZip/Model/ZipModel.php b/src/PhpZip/Model/ZipModel.php index c5866b2..9adcf4e 100644 --- a/src/PhpZip/Model/ZipModel.php +++ b/src/PhpZip/Model/ZipModel.php @@ -162,6 +162,9 @@ class ZipModel implements \Countable $this->matcher()->all()->setPassword(null); } + /** + * @param string|ZipEntry $entryName + */ public function removePasswordEntry($entryName) { $this->matcher()->add($entryName)->setPassword(null); diff --git a/src/PhpZip/ZipFile.php b/src/PhpZip/ZipFile.php index 403bcd3..483288a 100644 --- a/src/PhpZip/ZipFile.php +++ b/src/PhpZip/ZipFile.php @@ -1080,7 +1080,7 @@ class ZipFile implements ZipFileInterface } /** - * Set password for zip archive + * Sets a new password for all files in the archive. * * @param string $password * @param int|null $encryptionMethod Encryption method @@ -1100,8 +1100,10 @@ class ZipFile implements ZipFileInterface } /** + * Sets a new password of an entry defined by its name. + * * @param string $entryName - * @param string|null $password + * @param string $password * @param int|null $encryptionMethod * @return ZipFileInterface * @throws ZipException @@ -1120,29 +1122,29 @@ class ZipFile implements ZipFileInterface /** * Remove password for all entries for update. * @return ZipFileInterface - * @deprecated using ZipFileInterface::removePassword() + * @deprecated using ZipFileInterface::disableEncryption() */ public function withoutPassword() { - return $this->removePassword(); + return $this->disableEncryption(); } /** - * Remove password for all entries for update. + * Disable encryption for all entries that are already in the archive. * @return ZipFileInterface */ - public function removePassword() + public function disableEncryption() { $this->zipModel->removePassword(); return $this; } /** - * Remove password for concrete entry. + * Disable encryption of an entry defined by its name. * @param string $entryName * @return ZipFileInterface */ - public function removePasswordEntry($entryName) + public function disableEncryptionEntry($entryName) { $this->zipModel->removePasswordEntry($entryName); return $this; diff --git a/src/PhpZip/ZipFileInterface.php b/src/PhpZip/ZipFileInterface.php index 05d31e3..53ab761 100644 --- a/src/PhpZip/ZipFileInterface.php +++ b/src/PhpZip/ZipFileInterface.php @@ -513,7 +513,7 @@ interface ZipFileInterface extends \Countable, \ArrayAccess, \Iterator public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256); /** - * Set password for zip archive + * Sets a new password for all files in the archive. * * @param string $password * @param int|null $encryptionMethod Encryption method @@ -522,32 +522,34 @@ interface ZipFileInterface extends \Countable, \ArrayAccess, \Iterator 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 mixed + * @return ZipFileInterface */ public function setPasswordEntry($entryName, $password, $encryptionMethod = null); /** * Remove password for all entries for update. * @return ZipFileInterface - * @deprecated using ZipFileInterface::removePassword() + * @deprecated using ZipFileInterface::disableEncryption() */ public function withoutPassword(); /** - * Remove password for all entries for update. + * Disable encryption for all entries that are already in the archive. * @return ZipFileInterface */ - public function removePassword(); + public function disableEncryption(); /** - * Remove password for concrete entry. + * Disable encryption of an entry defined by its name. * @param string $entryName * @return ZipFileInterface */ - public function removePasswordEntry($entryName); + public function disableEncryptionEntry($entryName); /** * Undo all changes done in the archive diff --git a/tests/PhpZip/ZipPasswordTest.php b/tests/PhpZip/ZipPasswordTest.php index d4503f3..ac96f10 100644 --- a/tests/PhpZip/ZipPasswordTest.php +++ b/tests/PhpZip/ZipPasswordTest.php @@ -21,7 +21,7 @@ class ZipPasswordTest extends ZipFileAddDirTest } $password = base64_encode(CryptoUtil::randomBytes(100)); - $badPassword = "sdgt43r23wefe"; + $badPassword = "bad password"; // create encryption password with ZipCrypto $zipFile = new ZipFile(); @@ -86,7 +86,7 @@ class ZipPasswordTest extends ZipFileAddDirTest // clear password $zipFile->addFromString('file1', ''); - $zipFile->removePassword(); + $zipFile->disableEncryption(); $zipFile->addFromString('file2', ''); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -286,10 +286,10 @@ class ZipPasswordTest extends ZipFileAddDirTest self::assertFalse($zipFile->getEntryInfo('file' . $i)->isEncrypted()); } } - $zipFile->removePasswordEntry('file3'); + $zipFile->disableEncryptionEntry('file3'); self::assertFalse($zipFile->getEntryInfo('file3')->isEncrypted()); self::asserttrue($zipFile->getEntryInfo('file2')->isEncrypted()); - $zipFile->removePassword(); + $zipFile->disableEncryption(); $infoList = $zipFile->getAllInfo(); array_walk($infoList, function (ZipInfo $zipInfo) { self::assertFalse($zipInfo->isEncrypted());