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 ecce87e..281a057 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,4 +21,4 @@ before_script: script: - composer validate --no-check-lock - - vendor/bin/phpunit -v -c bootstrap.xml + - vendor/bin/phpunit -v -c phpunit.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b14722..d1d97f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# Changelog + +## 3.1.0 (2017-11-14) +- Added class `ZipModel` for all changes. +- All manipulations with incoming and outgoing streams are in separate files: `ZipInputStream` and `ZipOutputStream`. +- Removed class `CentralDirectory`. +- Optimized extra fields classes. +- Fixed issue #4 (`count()` returns 0 when files are added in directories). +- Implemented issue #8 - support inline Content-Disposition and empty output filename. +- Optimized and tested on a php 32-bit platform (issue #5). +- Added output as PSR-7 Response. +- Added methods for canceling changes. +- Added [russian documentation](README.RU.md). +- Updated [documentation](README.md). +- Declared deprecated methods: + + rename `ZipFile::withReadPassword` to `ZipFile::setReadPassword` + + rename `ZipFile::withNewPassword` to `ZipFile::setPassword` + + rename `ZipFile::withoutPassword` to `ZipFile::disableEncryption` + ## 3.0.3 (2017-11-11) Fix bug issue #8 - Error if the file is empty. diff --git a/README.RU.md b/README.RU.md new file mode 100644 index 0000000..692350f --- /dev/null +++ b/README.RU.md @@ -0,0 +1,821 @@ +`PhpZip` +======== +`PhpZip` - php библиотека для продвинутой работы с ZIP-архивами. + +[![Build Status](https://travis-ci.org/Ne-Lexa/php-zip.svg?branch=master)](https://travis-ci.org/Ne-Lexa/php-zip) +[![Latest Stable Version](https://poser.pugx.org/nelexa/zip/v/stable)](https://packagist.org/packages/nelexa/zip) +[![Total Downloads](https://poser.pugx.org/nelexa/zip/downloads)](https://packagist.org/packages/nelexa/zip) +[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%205.5-8892BF.svg)](https://php.net/) +[![License](https://poser.pugx.org/nelexa/zip/license)](https://packagist.org/packages/nelexa/zip) + +[English Documentation](README.md) + +Содержание +---------- +- [Функционал](#Features) +- [Требования](#Requirements) +- [Установка](#Installation) +- [Примеры](#Examples) +- [Глоссарий](#Glossary) +- [Документация](#Documentation) + + [Обзор методов класса `\PhpZip\ZipFile`](#Documentation-Overview) + + [Создание/Открытие ZIP-архива](#Documentation-Open-Zip-Archive) + + [Чтение записей из архива](#Documentation-Open-Zip-Entries) + + [Перебор записей/Итератор](#Documentation-Zip-Iterate) + + [Получение информации о записях](#Documentation-Zip-Info) + + [Добавление записей в архив](#Documentation-Add-Zip-Entries) + + [Удаление записей из архива](#Documentation-Remove-Zip-Entries) + + [Работа с записями и с архивом](#Documentation-Entries) + + [Работа с паролями](#Documentation-Password) + + [zipalign - выравнивание архива для оптимизации Android пакетов (APK)](#Documentation-ZipAlign-Usage) + + [Отмена изменений](#Documentation-Unchanged) + + [Сохранение файла или вывод в браузер](#Documentation-Save-Or-Output-Entries) + + [Закрытие архива](#Documentation-Close-Zip-Archive) +- [Запуск тестов](#Running-Tests) +- [История изменений](#Changelog) +- [Обновление версий](#Upgrade) + + [Обновление с версии 2 до версии 3.0](#Upgrade-v2-to-v3) + +### Функционал +- Открытие и разархивирование ZIP-архивов. +- Создание ZIP-архивов. +- Модификация ZIP-архивов. +- Чистый php (не требуется расширение `php-zip` и класс `\ZipArchive`). +- Поддерживается сохранение архива в файл, вывод архива в браузер или вывод в виде строки, без сохранения в файл. +- Поддерживаются комментарии архива и комментарии отдельных записей. +- Получение подробной информации о каждой записи в архиве. +- Поддерживаются только следующие методы сжатия: + + Без сжатия (Stored). + + Deflate сжатие. + + BZIP2 сжатие при наличии расширения `php-bz2`. +- Поддержка `ZIP64` (размер файла более 4 GB или количество записей в архиве более 65535). +- Встроенная поддержка выравнивания архива для оптимизации Android пакетов (APK) [`zipalign`](https://developer.android.com/studio/command-line/zipalign.html). +- Работа с паролями для PHP 5.5 +> **Внимание!** +> +> Для 32-bit систем, в данный момент не поддерживается метод шифрование `Traditional PKWARE Encryption (ZipCrypto)`. +> Используйте метод шифрования `WinZIP AES Encryption`, когда это возможно. + + Установка пароля для чтения архива глобально или для некоторых записей. + + Изменение пароля архива, в том числе и для отдельных записей. + + Удаление пароля архива глобально или для отдельных записей. + + Установка пароля и/или метода шифрования, как для всех, так и для отдельных записей в архиве. + + Установка разных паролей и методов шифрования для разных записей. + + Удаление пароля для всех или для некоторых записей. + + Поддержка методов шифрования `Traditional PKWARE Encryption (ZipCrypto)` и `WinZIP AES Encryption (128, 192 или 256 bit)`. + + Установка метода шифрования для всех или для отдельных записей в архиве. + +### Требования +- `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::disableEncryption](#Documentation-ZipFile-disableEncryption) - отключает шифрования всех записей, находящихся в архиве. +- [ZipFile::disableEncryptionEntry](#Documentation-ZipFile-disableEncryptionEntry) - отключает шифрование записи по её имени. +- [ZipFile::extractTo](#Documentation-ZipFile-extractTo) - извлекает содержимое архива в заданную директорию. +- [ZipFile::getAllInfo](#Documentation-ZipFile-getAllInfo) - возвращает подробную информацию обо всех записях в архиве. +- [ZipFile::getArchiveComment](#Documentation-ZipFile-getArchiveComment) - возвращает комментарий ZIP-архива. +- [ZipFile::getEntryComment](#Documentation-ZipFile-getEntryComment) - возвращает комментарий к записи, используя её имя. +- [ZipFile::getEntryContent](#Documentation-ZipFile-getEntryContent) - возвращает содержимое записи. +- [ZipFile::getEntryInfo](#Documentation-ZipFile-getEntryInfo) - возвращает подробную информацию о записи в архиве. +- [ZipFile::getListFiles](#Documentation-ZipFile-getListFiles) - возвращает список файлов архива. +- [ZipFile::hasEntry](#Documentation-ZipFile-hasEntry) - проверяет, присутствует ли запись в архиве. +- [ZipFile::isDirectory](#Documentation-ZipFile-isDirectory) - проверяет, является ли запись в архиве директорией. +- [ZipFile::matcher](#Documentation-ZipFile-matcher) - выборка записей в архиве для проведения операций над выбранными записями. +- [ZipFile::openFile](#Documentation-ZipFile-openFile) - открывает ZIP-архив из файла. +- [ZipFile::openFromString](#Documentation-ZipFile-openFromString) - открывает ZIP-архив из строки. +- [ZipFile::openFromStream](#Documentation-ZipFile-openFromStream) - открывает ZIP-архив из потока. +- [ZipFile::outputAsAttachment](#Documentation-ZipFile-outputAsAttachment) - выводит ZIP-архив в браузер. +- [ZipFile::outputAsResponse](#Documentation-ZipFile-outputAsResponse) - выводит ZIP-архив, как Response PSR-7. +- [ZipFile::outputAsString](#Documentation-ZipFile-outputAsString) - выводит ZIP-архив в виде строки. +- [ZipFile::rename](#Documentation-ZipFile-rename) - переименовывает запись по имени. +- [ZipFile::rewrite](#Documentation-ZipFile-rewrite) - сохраняет изменения и заново открывает изменившийся архив. +- [ZipFile::saveAsFile](#Documentation-ZipFile-saveAsFile) - сохраняет архив в файл. +- [ZipFile::saveAsStream](#Documentation-ZipFile-saveAsStream) - записывает архив в поток. +- [ZipFile::setArchiveComment](#Documentation-ZipFile-setArchiveComment) - устанавливает комментарий к ZIP-архиву. +- [ZipFile::setCompressionLevel](#Documentation-ZipFile-setCompressionLevel) - устанавливает уровень сжатия для всех файлов, находящихся в архиве. +- [ZipFile::setCompressionLevelEntry](#Documentation-ZipFile-setCompressionLevelEntry) - устанавливает уровень сжатия для определённой записи в архиве. +- [ZipFile::setCompressionMethodEntry](#Documentation-ZipFile-setCompressionMethodEntry) - устанавливает метод сжатия для определённой записи в архиве. +- [ZipFile::setEntryComment](#Documentation-ZipFile-setEntryComment) - устанавливает комментарий к записи, используя её имя. +- [ZipFile::setReadPassword](#Documentation-ZipFile-setReadPassword) - устанавливает пароль на чтение открытого запароленного архива для всех зашифрованных записей. +- [ZipFile::setReadPasswordEntry](#Documentation-ZipFile-setReadPasswordEntry) - устанавливает пароль на чтение конкретной зашифрованной записи открытого запароленного архива. +- ~~ZipFile::withNewPassword~~ - устаревший метод (**deprecated**) используйте метод [ZipFile::setPassword](#Documentation-ZipFile-setPassword). +- [ZipFile::setPassword](#Documentation-ZipFile-setPassword) - устанавливает новый пароль для всех файлов, находящихся в архиве. +- [ZipFile::setPasswordEntry](#Documentation-ZipFile-setPasswordEntry) - устанавливает новый пароль для конкретного файла. +- [ZipFile::setZipAlign](#Documentation-ZipFile-setZipAlign) - устанавливает выравнивание архива для оптимизации APK файлов (Android packages). +- [ZipFile::unchangeAll](#Documentation-ZipFile-unchangeAll) - отменяет все изменения, сделанные в архиве. +- [ZipFile::unchangeArchiveComment](#Documentation-ZipFile-unchangeArchiveComment) - отменяет изменения в комментарии к архиву. +- [ZipFile::unchangeEntry](#Documentation-ZipFile-unchangeEntry) - отменяет изменения для конкретной записи архива. +- ~~ZipFile::withoutPassword~~ - устаревший метод (**deprecated**) используйте метод [ZipFile::disableEncryption](#Documentation-ZipFile-disableEncryption). +- ~~ZipFile::withReadPassword~~ - устаревший метод (**deprecated**) используйте метод [ZipFile::setReadPassword](#Documentation-ZipFile-setReadPassword). + +#### Создание/Открытие 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); +``` + **ZipFile::matcher** - выборка записей в архиве для проведения операций над выбранными записями. +```php +$matcher = $zipFile->matcher(); +``` +Выбор файлов из архива по одному: +```php +$matcher + ->add('entry name') + ->add('another entry'); +``` +Выбор нескольких файлов в архиве: +```php +$matcher->add([ + 'entry name', + 'another entry name', + 'path/' +]); +``` +Выбор файлов по регулярному выражению: +```php +$matcher->match('~\.jpe?g$~i'); +``` +Выбор всех файлов в архиве: +```php +$matcher->all(); +``` +count() - получает количество выбранных записей: +```php +$count = count($matcher); +// или +$count = $matcher->count(); +``` +getMatches() - получает список выбранных записей: +```php +$entries = $matcher->getMatches(); +// пример содержимого: ['entry name', 'another entry name']; +``` +invoke() - выполняет пользовательскую функцию над выбранными записями: +```php +// пример +$matcher->invoke(function($entryName) use($zipFile) { + $newName = preg_replace('~\.(jpe?g)$~i', '.no_optimize.$1', $entryName); + $zipFile->rename($entryName, $newName); +}); +``` +Функции для работы над выбранными записями: +```php +$matcher->delete(); // удалет выбранные записи из ZIP-архива +$matcher->setPassword($password); // устанавливает новый пароль на выбранные записи +$matcher->setPassword($password, $encryptionMethod); // устанавливает новый пароль и метод шифрования на выбранные записи +$matcher->setEncryptionMethod($encryptionMethod); // устанавливает метод шифрования на выбранные записи +$matcher->disableEncryption(); // отключает шифрование для выбранных записей +``` +#### Работа с паролями + +Реализована поддержка методов шифрования: +- `\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::disableEncryption** - отключает шифрования всех записей, находящихся в архиве. + +> _Обратите внимание, что действие данного метода не распространяется на записи, добавленные после выполнения этого метода._ +```php +$zipFile->disableEncryption(); +``` + **ZipFile::disableEncryptionEntry** - отключает шифрование записи по её имени. +```php +$zipFile->disableEncryptionEntry($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 +``` +### История изменений +[Ссылка на Changelog](CHANGELOG.md) +### Обновление версий +#### Обновление с версии 2 до версии 3.0 +Обновите мажорную версию в файле `composer.json` до `^3.0`. +```json +{ + "require": { + "nelexa/zip": "^3.0" + } +} +``` +Затем установите обновления с помощью `Composer`: +```bash +composer update nelexa/zip +``` +Обновите ваш код для работы с новой версией: +- Класс `ZipOutputFile` объединён с `ZipFile` и удалён. + + Замените `new \PhpZip\ZipOutputFile()` на `new \PhpZip\ZipFile()` +- Статичиская инициализация методов стала не статической. + + Замените `\PhpZip\ZipFile::openFromFile($filename);` на `(new \PhpZip\ZipFile())->openFile($filename);` + + Замените `\PhpZip\ZipOutputFile::openFromFile($filename);` на `(new \PhpZip\ZipFile())->openFile($filename);` + + Замените `\PhpZip\ZipFile::openFromString($contents);` на `(new \PhpZip\ZipFile())->openFromString($contents);` + + Замените `\PhpZip\ZipFile::openFromStream($stream);` на `(new \PhpZip\ZipFile())->openFromStream($stream);` + + Замените `\PhpZip\ZipOutputFile::create()` на `new \PhpZip\ZipFile()` + + Замените `\PhpZip\ZipOutputFile::openFromZipFile($zipFile)` на `(new \PhpZip\ZipFile())->openFile($filename);` +- Переименуйте методы: + + `addFromFile` в `addFile` + + `setLevel` в `setCompressionLevel` + + `ZipFile::setPassword` в `ZipFile::withReadPassword` + + `ZipOutputFile::setPassword` в `ZipFile::withNewPassword` + + `ZipOutputFile::disableEncryptionAllEntries` в `ZipFile::withoutPassword` + + `ZipOutputFile::setComment` в `ZipFile::setArchiveComment` + + `ZipFile::getComment` в `ZipFile::getArchiveComment` +- Изменились сигнатуры для методов `addDir`, `addFilesFromGlob`, `addFilesFromRegex`. +- Удалены методы: + + `getLevel` + + `setCompressionMethod` + + `setEntryPassword` diff --git a/README.md b/README.md index 2657496..3cde2b5 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,233 @@ `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/) +[![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) +- [Changelog](#Changelog) +- [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 +> **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. + + 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::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. +- [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::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::disableEncryption](#Documentation-ZipFile-disableEncryption) 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** - opens a zip-archive from a file. ```php $zipFile = new \PhpZip\ZipFile(); -$zipFile->openFile($filename); +$zipFile->openFile('file.zip'); ``` -Open zip archive from data string. + **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", @@ -136,77 +236,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 @@ -221,295 +341,450 @@ 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. +$zipFile->rename($oldName, $newName); +``` + **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. -$compressionMethod = ZipFile::LEVEL_BEST_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. -$zipFile->setCompressionLevel($compressionLevel); +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); ``` -Set comment archive. + **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, $comment); +``` + **ZipFile::matcher** - selecting entries in the archive to perform operations on them. +```php +$matcher = $zipFile->matcher(); +``` +Selecting files from the archive one at a time: +```php +$matcher + ->add('entry name') + ->add('another entry'); +``` +Select multiple files in the archive: ```php -$zipFile->setEntryComment($entryName, $entryComment); +$matcher->add([ + 'entry name', + 'another entry name', + 'path/' +]); ``` -Set a new password. +Selecting files by regular expression: ```php -$zipFile->withNewPassword($password); +$matcher->match('~\.jpe?g$~i'); ``` -Set a new password and encryption method. +Select all files in the archive: ```php -$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES; // default value -$zipFile->withNewPassword($password, $encryptionMethod); +$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 + +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. -// Support encryption methods: -// ZipFile::ENCRYPTION_METHOD_TRADITIONAL - Traditional PKWARE Encryption -// ZipFile::ENCRYPTION_METHOD_WINZIP_AES - WinZip AES Encryption +> _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->setReadPassword($password); +``` + **ZipFile::setReadPasswordEntry** - gets a password for reading of an entry defined by its name. +```php +$zipFile->setReadPasswordEntry($entryName, $password); ``` -Remove password from all entries. + **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->withoutPassword(); +$zipFile->setPassword($password); ``` -#### ZipAlign Usage -Set archive alignment ([`zipalign`](https://developer.android.com/studio/command-line/zipalign.html)). +You can set the encryption method: ```php -// before save or output -$zipFile->setAlign(4); // alternative command: zipalign -f -v 4 filename.zip +$encryptionMethod = ZipFile::ENCRYPTION_METHOD_WINZIP_AES_256; +$zipFile->setPassword($password, $encryptionMethod); ``` -#### Save Zip File or Output -Save archive to a file. + **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::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->disableEncryption(); +``` + **ZipFile::disableEncryptionEntry** - disable encryption of an entry defined by its name. +```php +$zipFile->disableEncryptionEntry($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 +### Changelog +[Link to Changelog](CHANGELOG.md) + +### Upgrade +#### Upgrade version 2 to version 3.0 +Update the major version in the file `composer.json` to `^3.0`. ```json { "require": { @@ -517,11 +792,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. @@ -536,11 +811,11 @@ 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`. -- Remove methods +- Remove methods: + `getLevel` + `setCompressionMethod` + `setEntryPassword` diff --git a/bootstrap.xml b/bootstrap.xml deleted file mode 100644 index 25c557a..0000000 --- a/bootstrap.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - ./tests - - - - \ No newline at end of file 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/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..c69aee3 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,23 @@ + + + + + + + + + + tests + + + + + + src + + + \ No newline at end of file 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 @@ private function updateKeys($charAt) { $this->keys[0] = self::crc32($this->keys[0], $charAt); $this->keys[1] = $this->keys[1] + ($this->keys[0] & 0xff); - $this->keys[1] = self::toInt($this->keys[1] * 134775813 + 1); - $this->keys[2] = self::toInt(self::crc32($this->keys[2], ($this->keys[1] >> 24) & 0xff)); - } - - /** - * Cast to int - * - * @param $i - * @return int - */ - private static function toInt($i) - { - $i = (int)($i & 0xffffffff); - if ($i > 2147483647) { - return -(-$i & 0xffffffff); - } elseif ($i < -2147483648) { - return $i & -2147483648; - } - return $i; + $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 @@ private function crc32($oldCrc, $charAt) */ 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 @@ public function encrypt($data) $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 @@ private function encryptByte($byte) $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 @@ public function encrypt($content) // @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 @@ public function encrypt($content) 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 @@ public function getActualCrc() { 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. - */ - 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 + * Initializes this Extra Field by deserializing a Data Block. + * @param string $data */ - 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 @@ public static function getHeaderId() return 0x9901; } - /** - * Returns the Data Size of this Extra Field. - * The Data Size is an unsigned short integer (two bytes) - * which indicates the length of the Data Block in bytes and does not - * include its own size in this Extra Field. - * This property may be initialized by calling ExtraField::readFrom. - * - * @return int The size of the Data Block in bytes - * or 0 if unknown. - */ - public function getDataSize() - { - return self::DATA_SIZE; - } - /** * Returns the vendor version. * @@ -156,48 +150,43 @@ public function getMethod() } /** - * Sets compression method. + * Internal encryption method. * - * @param int $compressionMethod Compression method - * @throws ZipException Compression method out of range. + * @return int */ - public function setMethod($compressionMethod) + public function getEncryptionMethod() { - if (0x0000 > $compressionMethod || $compressionMethod > 0xffff) { - throw new ZipException('Compression method out of range'); - } - $this->method = $compressionMethod; + return isset(self::$encryptionMethods[$this->getKeyStrength()]) ? + self::$encryptionMethods[$this->getKeyStrength()] : + self::$encryptionMethods[self::KEY_STRENGTH_256BIT]; } /** - * 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 + * @param int $encryptionMethod + * @return int * @throws ZipException */ - public function readFrom($handle, $off, $size) + public static function getKeyStrangeFromEncryptionMethod($encryptionMethod) { - if (self::DATA_SIZE != $size) - throw new ZipException(); + $flipKey = array_flip(self::$encryptionMethods); + if (!isset($flipKey[$encryptionMethod])) { + throw new ZipException("Unsupport encryption method " . $encryptionMethod); + } + return $flipKey[$encryptionMethod]; + } - 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(); + /** + * Sets compression method. + * + * @param int $compressionMethod Compression method + * @throws ZipException Compression method out of range. + */ + public function setMethod($compressionMethod) + { + if (0x0000 > $compressionMethod || $compressionMethod > 0xffff) { + throw new ZipException('Compression method out of range'); } - $this->setKeyStrength(self::keyStrength($keyStrength)); // checked - $this->setMethod($method); + $this->method = $compressionMethod; } /** @@ -218,19 +207,50 @@ public function setKeyStrength($keyStrength) */ public static function encryptionStrength($keyStrength) { - return isset(self::$keyStrengths[$keyStrength]) ? self::$keyStrengths[$keyStrength] : self::$keyStrengths[self::KEY_STRENGTH_128BIT]; + return isset(self::$keyStrengths[$keyStrength]) ? + self::$keyStrengths[$keyStrength] : + self::$keyStrengths[self::KEY_STRENGTH_128BIT]; } /** - * Serializes a Data Block of ExtraField::getDataSize bytes to the - * 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 + ); + } + + /** + * 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']); } -} \ No newline at end of file +} 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 @@ public function unmap($position) { 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 @@ public function getComment() /** * @return int */ - public function getCentralDirectoryEntriesSize() + public function getEntryCount() { - return $this->centralDirectoryEntriesSize; + return $this->entryCount; } /** @@ -268,152 +117,4 @@ public function isZip64() { 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..396d234 100644 --- a/src/PhpZip/Model/Entry/ZipAbstractEntry.php +++ b/src/PhpZip/Model/Entry/ZipAbstractEntry.php @@ -4,15 +4,14 @@ 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 @@ */ 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; - - /** - * @param int $mask - * @return bool - */ - private function isInit($mask) - { - return 0 !== ($this->init & $mask); - } - - /** - * @param int $mask - * @param bool $init - */ - private function setInit($mask, $init) - { - if ($init) { - $this->init |= $mask; - } else { - $this->init &= ~$mask; - } - } + private $compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION; /** - * @return CentralDirectory + * ZipAbstractEntry constructor. */ - public function getCentralDirectory() + public function __construct() { - return $this->centralDirectory; + $this->extraFieldsCollection = new ExtraFieldsCollection(); } /** - * @param CentralDirectory $centralDirectory - * @return ZipEntry + * @param ZipEntry $entry */ - public function setCentralDirectory(CentralDirectory $centralDirectory) + public function setEntry(ZipEntry $entry) { - $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 @@ public function setName($name) 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 @@ public function setPlatform($platform) 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. * @@ -235,7 +260,7 @@ public function isZip64ExtensionsRequired() // description of Data Descriptor in ZIP File Format Specification! return 0xffffffff <= $this->getCompressedSize() || 0xffffffff <= $this->getSize() - || 0xffffffff <= $this->getOffset(); + || 0xffffffff <= sprintf('%u', $this->getOffset()); } /** @@ -257,12 +282,6 @@ public function getCompressedSize() */ 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; } @@ -286,12 +305,6 @@ public function getSize() */ 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; } @@ -313,29 +326,13 @@ public function getOffset() */ 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; } - /** - * 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,44 +352,41 @@ public function setGeneralPurposeBitFlags($general) throw new ZipException('general out of range'); } $this->general = $general; + 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; } /** - * Returns the indexed General Purpose Bit Flag. + * Returns true if and only if this ZIP entry is encrypted. * - * @param int $mask * @return bool */ - public function getGeneralPurposeBitFlag($mask) + public function isEncrypted() { - return 0 !== ($this->general & $mask); + return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED); } /** - * Sets the indexed General Purpose Bit Flag. + * Returns 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; - } - - /** - * Returns true if and only if this ZIP entry is encrypted. - * * @return bool */ - public function isEncrypted() + public function getGeneralPurposeBitFlag($mask) { - return $this->getGeneralPurposeBitFlag(self::GPBF_ENCRYPTED); + return 0 !== ($this->general & $mask); } /** @@ -401,20 +395,19 @@ public function isEncrypted() * * @return ZipEntry */ - public function clearEncryption() + public function disableEncryption() { $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 +421,7 @@ public function clearEncryption() */ public function setEncrypted($encrypted) { + $encrypted = (bool)$encrypted; $this->setGeneralPurposeBitFlag(self::GPBF_ENCRYPTED, $encrypted); return $this; } @@ -451,6 +445,10 @@ public function getMethod() */ 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 +456,15 @@ public function setMethod($method) 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 +484,6 @@ public function getTime() 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 * @@ -517,7 +491,7 @@ public function setTime($unixTimestamp) */ public function getDosTime() { - return $this->dosTime & 0xffffffff; + return $this->dosTime; } /** @@ -535,6 +509,24 @@ public function setDosTime($dosTime) $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. * @@ -545,7 +537,7 @@ public function getExternalAttributes() if (!$this->isInit(self::BIT_EXTERNAL_ATTR)) { return $this->isDirectory() ? 0x10 : 0; } - return $this->externalAttributes & 0xffffffff; + return $this->externalAttributes; } /** @@ -559,10 +551,6 @@ public function setExternalAttributes($externalAttributes) { $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; @@ -572,123 +560,43 @@ public function setExternalAttributes($externalAttributes) } /** - * Return extra field from header id. - * - * @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. + * Returns true if and only if this ZIP entry represents a directory entry + * (i.e. end with '/'). * - * @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)); - } - // 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; + $size = strlen($extraData); + if (0x0000 > $size || $size > 0xffff) { + throw new ZipException('Size extra out of range: ' . $size . '. Extra data: ' . $extraData); } - return $field; + return $extraData; } /** @@ -701,99 +609,31 @@ private function composeZip64ExtraField() * * @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 +643,7 @@ private function parseZip64ExtraField() */ public function getComment() { - return null != $this->comment ? $this->comment : ""; + return null !== $this->comment ? $this->comment : ""; } /** @@ -841,7 +681,7 @@ public function isDataDescriptorRequired() */ public function getCrc() { - return $this->crc & 0xffffffff; + return $this->crc; } /** @@ -853,10 +693,6 @@ public function getCrc() */ 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; @@ -883,7 +719,11 @@ public function setPassword($password, $encryptionMethod = null) if (null !== $encryptionMethod) { $this->setEncryptionMethod($encryptionMethod); } - $this->setEncrypted(!empty($this->password)); + if (!empty($this->password)) { + $this->setEncrypted(true); + } else { + $this->disableEncryption(); + } return $this; } @@ -895,6 +735,34 @@ public function getEncryptionMethod() 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 +776,23 @@ public function getCompressionLevel() * @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 @@ -getMethod(); - return self::METHOD_WINZIP_AES === $method ? 51 : - (ZipFile::METHOD_BZIP2 === $method ? 46 : - ($this->isZip64ExtensionsRequired() ? 45 : - (ZipFile::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10) - ) - ); + parent::__construct(); + if ($content !== null && !is_string($content) && !is_resource($content)) { + throw new InvalidArgumentException('invalid content'); + } + $this->content = $content; } /** - * Write local file header, encryption header, file data and data descriptor to output stream. + * Returns an string content of the given entry. * - * @param resource $outputStream + * @return null|string * @throws ZipException */ - public function writeEntry($outputStream) + public function getEntryContent() { - $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 (is_resource($this->content)) { + return stream_get_contents($this->content, -1, 0); } + return $this->content; + } - if (self::UNKNOWN === $this->getPlatform()) { - $this->setPlatform(self::PLATFORM_UNIX); - } - if (self::UNKNOWN === $this->getTime()) { - $this->setTime(time()); - } + /** + * Version needed to extract. + * + * @return int + */ + public function getVersionNeededToExtract() + { $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 && + return self::METHOD_WINZIP_AES === $method ? 51 : ( - 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 = + ZipFileInterface::METHOD_BZIP2 === $method ? 46 : ( - $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)); - } + $this->isZip64ExtensionsRequired() ? 45 : + (ZipFileInterface::METHOD_DEFLATED === $method || $this->isDirectory() ? 20 : 10) + ) + ); + } - if (null !== $entryContent) { - fwrite($outputStream, $entryContent); - } + /** + * Clone extra fields + */ + public function __clone() + { + $this->clone = true; + parent::__clone(); + } - assert(self::UNKNOWN !== $this->getCrc()); - assert(self::UNKNOWN !== $this->getSize()); - if ($this->getGeneralPurposeBitFlag(self::GPBF_DATA_DESCRIPTOR)) { - // data descriptor signature 4 bytes (0x08074b50) - // crc-32 4 bytes - fwrite($outputStream, pack('VV', self::DATA_DESCRIPTOR_SIG, $this->getCrc())); - // compressed size 4 or 8 bytes - // uncompressed size 4 or 8 bytes - if ($this->isZip64ExtensionsRequired()) { - fwrite($outputStream, PackUtil::packLongLE($compressedSize)); - fwrite($outputStream, PackUtil::packLongLE($this->getSize())); - } else { - fwrite($outputStream, pack('VV', $this->getCompressedSize(), $this->getSize())); - } - } elseif ($this->getCompressedSize() != $compressedSize) { - throw new ZipException($this->getName() - . " (expected compressed entry size of " - . $this->getCompressedSize() . " bytes, but is actually " . $compressedSize . " bytes)"); + public function __destruct() + { + if (!$this->clone && null !== $this->content && is_resource($this->content)) { + fclose($this->content); + $this->content = null; } } - -} \ No newline at end of file +} 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 aea781c..0000000 --- a/src/PhpZip/Model/Entry/ZipReadEntry.php +++ /dev/null @@ -1,330 +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 = ''; - if ($this->getCompressedSize() > 0) { - $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..37d54c3 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..6434703 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,35 +193,47 @@ public function __construct(ZipEntry $entry) $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(); + $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; $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(); + $externalAttributes = PHP_INT_SIZE === 4 ? + sprintf('%u', $externalAttributes) : + $externalAttributes; $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 +250,12 @@ public function __construct(ZipEntry $entry) 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 +264,7 @@ public function __construct(ZipEntry $entry) break; } /* else: fall through! */ + // no break default: /* assume Unix-like */ switch ($xattr & self::UNX_IFMT) { case self::UNX_IFDIR: @@ -284,33 +299,59 @@ public function __construct(ZipEntry $entry) $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 +389,20 @@ public static function getPlatformName(ZipEntry $entry) } /** - * @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 +453,14 @@ public function getAtime() return $this->atime; } + /** + * @return string + */ + public function getAttributes() + { + return $this->attributes; + } + /** * @return boolean */ @@ -452,10 +487,19 @@ public function getCrc() /** * @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 +519,76 @@ public function getVersion() } /** - * @return string + * @return int|null */ - public function getAttributes() + public function getEncryptionMethod() { - return $this->attributes; + return $this->encryptionMethod; } /** - * @return string + * @return int|null + */ + public function getCompressionLevel() + { + return $this->compressionLevel; + } + + /** + * @return int */ - function __toString() + public function getCompressionMethod() { - 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 $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() + ]; + } -} \ No newline at end of file + /** + * @return string + */ + public function __toString() + { + 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() + . '}'; + } +} diff --git a/src/PhpZip/Model/ZipModel.php b/src/PhpZip/Model/ZipModel.php new file mode 100644 index 0000000..9adcf4e --- /dev/null +++ b/src/PhpZip/Model/ZipModel.php @@ -0,0 +1,344 @@ +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); + } + + /** + * @param string|ZipEntry $entryName + */ + public function removePasswordEntry($entryName) + { + $this->matcher()->add($entryName)->setPassword(null); + } + + /** + * @return bool + */ + public function isArchiveCommentChanged() + { + return $this->archiveCommentChanged; + } + + /** + * @param string|ZipEntry $old + * @param string|ZipEntry $new + * @throws 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..df5bd35 --- /dev/null +++ b/src/PhpZip/Stream/ZipInputStream.php @@ -0,0 +1,546 @@ +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()); + $lfhOff = PHP_INT_SIZE === 4 ? sprintf('%u', $lfhOff) : $lfhOff; + 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); + $pos = PHP_INT_SIZE === 4 ? sprintf('%u', $pos) : $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(); + $compressedSize = PHP_INT_SIZE === 4 ? sprintf('%u', $compressedSize) : $compressedSize; + 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]); + $localCrc = PHP_INT_SIZE === 4 ? sprintf('%u', $localCrc) : $localCrc; + } + + $crc = PHP_INT_SIZE === 4 ? sprintf('%u', $entry->getCrc()) : $entry->getCrc(); + + if ($crc != $localCrc) { + throw new Crc32Exception($entry->getName(), $crc, $localCrc); + } + } + } + + switch ($method) { + case ZipFileInterface::METHOD_STORED: + break; + case ZipFileInterface::METHOD_DEFLATED: + $content = gzinflate($content); + break; + case ZipFileInterface::METHOD_BZIP2: + if (!extension_loaded('bz2')) { + throw new ZipException('Extension bzip2 not install'); + } + $content = bzdecompress($content); + break; + default: + throw new ZipUnsupportMethod($entry->getName() . + " (compression method " . $method . " is not supported)"); + } + if (!$skipCheckCrc) { + $localCrc = crc32($content); + $localCrc = PHP_INT_SIZE === 4 ? sprintf('%u', $localCrc) : $localCrc; + $crc = PHP_INT_SIZE === 4 ? sprintf('%u', $entry->getCrc()) : $entry->getCrc(); + if ($crc != $localCrc) { + if ($isEncrypted) { + throw new ZipCryptoException("Wrong password"); + } + throw new Crc32Exception($entry->getName(), $crc, $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 = PHP_INT_SIZE === 4 ? sprintf('%u', $pos) : $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) + { + $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); + 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 @@ public static function toDosTime($unixTimestamp) $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 @@ public static function regexFileSearch($folder, $pattern, $recursive = true) */ 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 @@ public function accept() } 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..c622360 100644 --- a/src/PhpZip/Util/PackUtil.php +++ b/src/PhpZip/Util/PackUtil.php @@ -1,4 +1,5 @@ = 0) { + if (PHP_INT_SIZE === 8 && PHP_VERSION_ID >= 506030) { return pack("P", $longValue); } @@ -38,11 +39,27 @@ public static function packLongLE($longValue) */ public static function unpackLongLE($value) { - if (version_compare(PHP_VERSION, '5.6.3') >= 0) { - return current(unpack('P', $value)); + if (PHP_INT_SIZE === 8 && PHP_VERSION_ID >= 506030) { + 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) + { + if (PHP_INT_SIZE === 8) { + $int = $int & 0xffffffff; + if ($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..483288a 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 @@ public function openFile($filename) * 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 @@ public function openFromString($data) * 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 @@ public function openFromStream($handle) 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 @@ public function isDirectory($entryName) */ public function getArchiveComment() { - return $this->centralDirectory->getArchiveComment(); + return $this->zipModel->getArchiveComment(); } /** - * Set password to all input encrypted entries. + * Set archive comment. * - * @param string $password Password - * @return ZipFile + * @param null|string $comment + * @return ZipFileInterface + * @throws InvalidArgumentException Length comment out of range */ - public function withReadPassword($password) + public function setArchiveComment($comment = null) { - foreach ($this->centralDirectory->getEntries() as $entry) { - if ($entry->isEncrypted()) { - $entry->setPassword($password); - } - } + $this->zipModel->setArchiveComment($comment); return $this; } /** - * Set archive comment. + * 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 null|string $comment - * @throws InvalidArgumentException Length comment out of range + * @param string $entryName + * @return bool + * @throws ZipNotFoundEntry */ - public function setArchiveComment($comment = null) + public function isDirectory($entryName) { - $this->centralDirectory->getEndOfCentralDirectory()->setComment($comment); + return $this->zipModel->getEntry($entryName)->isDirectory(); } /** @@ -246,7 +220,7 @@ public function setArchiveComment($comment = null) */ public function getEntryComment($entryName) { - return $this->centralDirectory->getEntry($entryName)->getComment(); + return $this->zipModel->getEntry($entryName)->getComment(); } /** @@ -254,15 +228,37 @@ public function getEntryComment($entryName) * * @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 @@ public function setEntryComment($entryName, $comment = null) */ 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 @@ public function getEntryInfo($entryName) */ 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 @@ public function getAllInfo() * @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 @@ public function extractTo($destination, $entries = null) 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 @@ public function extractTo($destination, $entries = null) 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 @@ function ($zipEntry) use ($flipEntries) { 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 @@ function ($zipEntry) use ($flipEntries) { * @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 @@ public function addFromString($localName, $contents, $compressionMethod = null) $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 @@ public function addFromString($localName, $contents, $compressionMethod = null) * @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 @@ public function addFile($filename, $localName = null, $compressionMethod = null) $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 @@ public function addFile($filename, $localName = null, $compressionMethod = null) $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 @@ public function addFile($filename, $localName = null, $compressionMethod = null) * @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 @@ public function addFromStream($stream, $localName, $compressionMethod = null) $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 @@ public function addFromStream($stream, $localName, $compressionMethod = null) $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 @@ public function addFromStream($stream, $localName, $compressionMethod = null) * 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 @@ public function addEmptyDir($dirName) $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 @@ public function addAll(array $mapData) * @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 @@ public function addDir($inputDir, $localPath = "/", $compressionMethod = null) * @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 @@ public function addDirRecursive($inputDir, $localPath = "/", $compressionMethod * @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 @@ public function addFilesFromIterator( * @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 @@ public function addFilesFromGlob($inputDir, $globPattern, $localPath = '/', $com 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 @@ public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = * @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 @@ private function addGlob( $localPath = '/', $recursive = true, $compressionMethod = null - ) - { + ) { $inputDir = (string)$inputDir; if (null === $inputDir || 0 === strlen($inputDir)) { throw new InvalidArgumentException('Input dir empty'); @@ -780,24 +736,25 @@ private function addGlob( } /** - * Add files from regex pattern. + * Add files recursively from glob pattern. * - * @param string $inputDir Search files in this directory. - * @param string $regexPattern Regex 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 - * @internal param bool $recursive Recursive search. + * @return ZipFileInterface + * @throws InvalidArgumentException + * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ - public function addFilesFromRegex($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null) + public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null) { - return $this->addRegex($inputDir, $regexPattern, $localPath, false, $compressionMethod); + return $this->addGlob($inputDir, $globPattern, $localPath, true, $compressionMethod); } /** - * Add files recursively from regex pattern. + * Add files from regex pattern. * * @param string $inputDir Search files in this directory. * @param string $regexPattern Regex pattern. @@ -805,15 +762,14 @@ public function addFilesFromRegex($inputDir, $regexPattern, $localPath = "/", $c * @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 addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null) + public function addFilesFromRegex($inputDir, $regexPattern, $localPath = "/", $compressionMethod = null) { - return $this->addRegex($inputDir, $regexPattern, $localPath, true, $compressionMethod); + return $this->addRegex($inputDir, $regexPattern, $localPath, false, $compressionMethod); } - /** * Add files from regex pattern. * @@ -824,7 +780,7 @@ public function addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath * @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 @@ private function addRegex( $localPath = "/", $recursive = true, $compressionMethod = null - ) - { + ) { $regexPattern = (string)$regexPattern; if (empty($regexPattern)) { throw new InvalidArgumentException("regex pattern empty"); @@ -874,12 +829,43 @@ private function addRegex( 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 @@ public function rename($oldName, $newName) 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 @@ public function rename($oldName, $newName) * 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 @@ public function deleteFromName($entryName) * 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 @@ public function deleteFromGlob($globPattern) * Delete entries by regex pattern. * * @param string $regexPattern Regex pattern - * @return ZipFile + * @return ZipFileInterface * @throws InvalidArgumentException */ public function deleteFromRegex($regexPattern) @@ -936,17 +926,17 @@ public function deleteFromRegex($regexPattern) 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,241 @@ public function deleteAll() * 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 = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION) + { + 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 setCompressionLevel($compressionLevel = self::LEVEL_DEFAULT_COMPRESSION) + public function setCompressionMethodEntry($entryName, $compressionMethod) { - $this->centralDirectory->setCompressionLevel($compressionLevel); + 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); + } + + /** + * Sets a new password for all files in the 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; + } + + /** + * Sets a new password of an entry defined by its name. + * + * @param string $entryName + * @param string $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::disableEncryption() */ public function withoutPassword() { - $this->centralDirectory->setNewPassword(null); + return $this->disableEncryption(); + } + + /** + * Disable encryption for all entries that are already in the archive. + * @return ZipFileInterface + */ + public function disableEncryption() + { + $this->zipModel->removePassword(); + return $this; + } + + /** + * Disable encryption of an entry defined by its name. + * @param string $entryName + * @return ZipFileInterface + */ + public function disableEncryptionEntry($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 +1186,7 @@ public function withoutPassword() * Save as file. * * @param string $filename Output filename + * @return ZipFileInterface * @throws InvalidArgumentException * @throws ZipException */ @@ -1014,12 +1203,14 @@ public function saveAsFile($filename) 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,44 +1219,102 @@ public function saveAsStream($handle) throw new InvalidArgumentException('handle is not resource'); } ftruncate($handle, 0); - $this->centralDirectory->writeArchive($handle); + $this->writeZipToStream($handle); fclose($handle); + return $this; } /** * 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=" . rawurlencode($outputFilename)); header("Content-Length: " . strlen($content)); exit($content); } + /** + * Output .ZIP archive as PSR-7 Response. + * + * @param ResponseInterface $response Instance PSR-7 Response + * @param string $outputFilename Output filename + * @param string|null $mimeType Mime-Type + * @param bool $attachment Http Header 'Content-Disposition' if true then attachment otherwise inline + * @return ResponseInterface + * @throws InvalidArgumentException + */ + public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null, $attachment = true) + { + $outputFilename = (string)$outputFilename; + + 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]; + } + } + if (empty($mimeType)) { + $mimeType = self::$defaultMimeTypes['zip']; + } + + if (!($handle = fopen('php://memory', 'w+b'))) { + throw new InvalidArgumentException("Memory can not open from write."); + } + $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', $contentDispositionValue); + $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 +1325,7 @@ public function outputAsString() 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 +1333,20 @@ public function outputAsString() } /** - * 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,68 +1354,33 @@ public function rewrite() 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']) { - 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); } 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 +1409,12 @@ public function offsetSet($entryName, $contents) $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 +1442,15 @@ public function current() } /** - * 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 +1461,18 @@ public function next() */ 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 +1487,18 @@ public function valid() 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 +1507,6 @@ public function valid() */ 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..53ab761 --- /dev/null +++ b/src/PhpZip/ZipFileInterface.php @@ -0,0 +1,633 @@ +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 + */ + 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 @@ protected function fillDirectory() } } - 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 @@ public function testAddFilesFromIterator() $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 @@ public function testAddRecursiveDirWithoutLocalPath() $zipFile->close(); } - public function testAddFilesFromIteratorWithIgnoreFiles(){ + public function testAddFilesFromIteratorWithIgnoreFiles() + { $localPath = 'to/project'; $ignoreFiles = [ 'Текстовый документ.txt', @@ -207,7 +232,8 @@ public function testAddFilesFromIteratorWithIgnoreFiles(){ $zipFile->close(); } - public function testAddFilesFromRecursiveIteratorWithIgnoreFiles(){ + public function testAddFilesFromRecursiveIteratorWithIgnoreFiles() + { $localPath = 'to/project'; $ignoreFiles = [ '.hidden', @@ -354,6 +380,4 @@ public function testArrayAccessAddDir() 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 549e650..e23d82e 100644 --- a/tests/PhpZip/ZipFileTest.php +++ b/tests/PhpZip/ZipFileTest.php @@ -2,10 +2,11 @@ namespace PhpZip; -use PhpZip\Exception\ZipAuthenticationException; use PhpZip\Model\ZipEntry; +use PhpZip\Model\ZipInfo; use PhpZip\Util\CryptoUtil; use PhpZip\Util\FilesUtil; +use Psr\Http\Message\ResponseInterface; /** * ZipFile test @@ -29,6 +30,10 @@ public function testOpenFileCantExists() */ 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)); @@ -122,9 +127,33 @@ public function testOpenFromStreamNullStream() public function testOpenFromStreamInvalidResourceType() { $zipFile = new ZipFile(); + /** @noinspection PhpParamsInspection */ $zipFile->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. @@ -170,8 +199,8 @@ public function testOpenFromStream() $zipFile = new ZipFile(); $zipFile ->addFromString('file', 'content') - ->saveAsFile($this->outputFilename); - $zipFile->close(); + ->saveAsFile($this->outputFilename) + ->close(); $handle = fopen($this->outputFilename, 'rb'); $zipFile->openFromStream($handle); @@ -187,16 +216,18 @@ public function testOpenFromStream() 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)); } @@ -214,18 +245,23 @@ public function testNoModifiedArchive() $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()); @@ -243,13 +279,13 @@ public function testNoModifiedArchive() * @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__); @@ -267,15 +303,18 @@ public function testCreateArchiveAndAddFiles() 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); @@ -305,6 +344,18 @@ public function testCreateArchiveAndAddFiles() $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. */ @@ -331,7 +382,7 @@ public function testCompressionMethodFromImageMimeType() $zipFile->openFile($this->outputFilename); $info = $zipFile->getEntryInfo($basename); - self::assertEquals($info->getMethod(), 'No compression'); + self::assertEquals($info->getMethodName(), 'No compression'); $zipFile->close(); } @@ -344,7 +395,7 @@ public function testRename() $newName = 'tests/' . $oldName; $zipFile = new ZipFile(); - $zipFile->addDirRecursive(__DIR__); + $zipFile->addDir(__DIR__); $zipFile->saveAsFile($this->outputFilename); $zipFile->close(); @@ -406,7 +457,6 @@ public function testRenameEntryNewEntyExists() /** * @expectedException \PhpZip\Exception\ZipNotFoundEntry - * @expectedExceptionMessage Not found entry */ public function testRenameEntryNotFound() { @@ -466,7 +516,6 @@ public function testDeleteNewEntry() /** * @expectedException \PhpZip\Exception\ZipNotFoundEntry - * @expectedExceptionMessage Not found entry entry */ public function testDeleteFromNameNotFoundEntry() { @@ -482,22 +531,32 @@ public function testDeleteFromGlob() $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(); } @@ -529,7 +588,7 @@ public function testDeleteFromRegex() $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(); @@ -547,7 +606,7 @@ public function testDeleteFromRegex() $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(); } @@ -577,7 +636,8 @@ public function testDeleteFromRegexFailEmpty() 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(); @@ -733,8 +793,7 @@ public function testVeryLongEntryComment() } /** - * @expectedException \PhpZip\Exception\ZipException - * @expectedExceptionMessage Not found entry + * @expectedException \PhpZip\Exception\ZipNotFoundEntry */ public function testSetEntryCommentNotFoundEntry() { @@ -750,19 +809,19 @@ public function testCompressionMethod() $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', ]; } @@ -777,12 +836,12 @@ public function testCompressionMethod() 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); } @@ -948,6 +1007,10 @@ public function testExtractFail2() */ 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); @@ -960,105 +1023,6 @@ public function testExtractFail3() $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 @@ -1101,7 +1065,7 @@ public function testAddFromStringNullEntryName() /** * @expectedException \PhpZip\Exception\ZipUnsupportMethod - * @expectedExceptionMessage Unsupported method + * @expectedExceptionMessage Unsupported compression method */ public function testAddFromStringUnsupportedMethod() { @@ -1142,8 +1106,8 @@ public function testAddFromStringCompressionMethod() $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(); } @@ -1154,6 +1118,7 @@ public function testAddFromStringCompressionMethod() public function testAddFromStreamInvalidResource() { $zipFile = new ZipFile(); + /** @noinspection PhpParamsInspection */ $zipFile->addFromStream("invalid resource", "name"); } @@ -1207,8 +1172,8 @@ public function testAddFromStreamCompressionMethod() $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(); } @@ -1248,6 +1213,10 @@ public function testAddFileUnsupportedMethod() */ 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)); @@ -1522,6 +1491,7 @@ public function testAddFilesFromRegexRecursiveEmptyPattern() public function testSaveAsStreamBadStream() { $zipFile = new ZipFile(); + /** @noinspection PhpParamsInspection */ $zipFile->saveAsStream("bad stream"); } @@ -1531,6 +1501,10 @@ public function testSaveAsStreamBadStream() */ 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)); @@ -1551,13 +1525,13 @@ public function testZipFileArrayAccessAndCountableAndIterator() $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)]); } @@ -1628,18 +1602,43 @@ public function testZipFileArrayAccessAndCountableAndIterator() 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(); } @@ -1663,26 +1662,6 @@ public function testAddEmptyDirEmptyName() $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 @@ -1701,8 +1680,10 @@ public function testRewriteFile() $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); @@ -1711,7 +1692,7 @@ public function testRewriteFile() 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'])); @@ -1759,14 +1740,14 @@ public function testRewriteNullStream() /** * 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); @@ -1775,7 +1756,9 @@ public function testZipAlign() 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); @@ -1792,13 +1775,16 @@ public function testZipAlign() // 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); @@ -1808,20 +1794,291 @@ public function testZipAlign() 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 testEmptyContents() + public function testZipAlignFromModifiedZipArchive() { $zipFile = new ZipFile(); - $contents = ''; - $zipFile->addFromString('file', $contents); + 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); + } + + 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['file'], $contents); + 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(); } @@ -1844,10 +2101,12 @@ public function testCreateAndOpenZip64Ext() $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..ac96f10 --- /dev/null +++ b/tests/PhpZip/ZipPasswordTest.php @@ -0,0 +1,349 @@ +markTestSkipped('Skip test for 32-bit system. Not support Traditional PKWARE Encryption.'); + } + + $password = base64_encode(CryptoUtil::randomBytes(100)); + $badPassword = "bad password"; + + // create encryption password with ZipCrypto + $zipFile = new ZipFile(); + $zipFile->addDir(__DIR__); + $zipFile->setPassword($password, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL); + $zipFile->saveAsFile($this->outputFilename); + $zipFile->close(); + + 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->disableEncryption(); + $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() + { + if (PHP_INT_SIZE === 4) { + $this->markTestSkipped('Skip test for 32-bit system. Not support Traditional PKWARE Encryption.'); + } + + $password = base64_encode(CryptoUtil::randomBytes(50)); + + $zip = new ZipFile(); + $zip->addDirRecursive($this->outputDirname); + $zip->setPassword($password, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL); + $zip->saveAsFile($this->outputFilename); + $zip->close(); + + 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() + { + if (PHP_INT_SIZE === 4) { + $this->markTestSkipped('Skip test for 32-bit system. Not support Traditional PKWARE Encryption.'); + } + + $password1 = '353442434235424234'; + $password2 = 'adgerhvrwjhqqehtqhkbqrgewg'; + + $zip = new ZipFile(); + $zip->addDir($this->outputDirname); + $zip->setPasswordEntry('.hidden', $password1, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL); + $zip->setPasswordEntry('text file.txt', $password2, ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES); + $zip->saveAsFile($this->outputFilename); + $zip->close(); + + $zip->openFile($this->outputFilename); + $zip->setReadPasswordEntry('.hidden', $password1); + $zip->setReadPasswordEntry('text file.txt', $password2); + 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() + { + if (PHP_INT_SIZE === 4) { + $this->markTestSkipped('Skip test for 32-bit system. Not support Traditional PKWARE Encryption.'); + } + + $password1 = '353442434235424234'; + $password2 = 'adgerhvrwjhqqehtqhkbqrgewg'; + $defaultPassword = ' f f f f f ffff f5 '; + + $zip = new ZipFile(); + $zip->addDir($this->outputDirname); + $zip->setPassword($defaultPassword); + $zip->setPasswordEntry('.hidden', $password1, ZipFileInterface::ENCRYPTION_METHOD_TRADITIONAL); + $zip->setPasswordEntry('text file.txt', $password2, ZipFileInterface::ENCRYPTION_METHOD_WINZIP_AES); + $zip->saveAsFile($this->outputFilename); + $zip->close(); + + $zip->openFile($this->outputFilename); + $zip->setReadPassword($defaultPassword); + $zip->setReadPasswordEntry('.hidden', $password1); + $zip->setReadPasswordEntry('text file.txt', $password2); + 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->disableEncryptionEntry('file3'); + self::assertFalse($zipFile->getEntryInfo('file3')->isEncrypted()); + self::asserttrue($zipFile->getEntryInfo('file2')->isEncrypted()); + $zipFile->disableEncryption(); + $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 @@ public static function doZipAlignVerify($filename, $showErrors = false) { 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 0000000..9da004e Binary files /dev/null and b/tests/PhpZip/php-zip-ext-test-resources/binarynull.zip differ diff --git a/tests/PhpZip/php-zip-ext-test-resources/bug40228.zip b/tests/PhpZip/php-zip-ext-test-resources/bug40228.zip new file mode 100644 index 0000000..bbcd951 Binary files /dev/null and b/tests/PhpZip/php-zip-ext-test-resources/bug40228.zip differ diff --git "a/tests/PhpZip/php-zip-ext-test-resources/bug40228\347\247\201\343\201\257\343\202\254\343\203\251\343\202\271\343\202\222\351\243\237\343\201\271\343\202\211\343\202\214\343\201\276\343\201\231.zip" "b/tests/PhpZip/php-zip-ext-test-resources/bug40228\347\247\201\343\201\257\343\202\254\343\203\251\343\202\271\343\202\222\351\243\237\343\201\271\343\202\211\343\202\214\343\201\276\343\201\231.zip" new file mode 100644 index 0000000..bbcd951 Binary files /dev/null and "b/tests/PhpZip/php-zip-ext-test-resources/bug40228\347\247\201\343\201\257\343\202\254\343\203\251\343\202\271\343\202\222\351\243\237\343\201\271\343\202\211\343\202\214\343\201\276\343\201\231.zip" differ diff --git a/tests/PhpZip/php-zip-ext-test-resources/bug49072.zip b/tests/PhpZip/php-zip-ext-test-resources/bug49072.zip new file mode 100644 index 0000000..16bbcd0 Binary files /dev/null and b/tests/PhpZip/php-zip-ext-test-resources/bug49072.zip differ 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 0000000..9bec61b Binary files /dev/null and b/tests/PhpZip/php-zip-ext-test-resources/bug70752.zip differ 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 0000000..45bedcb Binary files /dev/null and b/tests/PhpZip/php-zip-ext-test-resources/bug8009.zip differ diff --git a/tests/PhpZip/php-zip-ext-test-resources/pecl12414.zip b/tests/PhpZip/php-zip-ext-test-resources/pecl12414.zip new file mode 100644 index 0000000..6cbc60f Binary files /dev/null and b/tests/PhpZip/php-zip-ext-test-resources/pecl12414.zip differ